mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[embeddable] remove EmbeddableRenderer and embeddable stories (#203007)
PR starts cleaning up legacy embeddable components by removing EmbeddableRenderer, EmbedddableRoot, and embeddable story books. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> Co-authored-by: Anton Dosov <dosantappdev@gmail.com>
This commit is contained in:
parent
4d9a70f48e
commit
80f915f9e3
29 changed files with 116 additions and 1065 deletions
|
@ -26,7 +26,6 @@ const STORYBOOKS = [
|
|||
'dashboard_enhanced',
|
||||
'dashboard',
|
||||
'data',
|
||||
'embeddable',
|
||||
'esql_editor',
|
||||
'expression_error',
|
||||
'expression_image',
|
||||
|
|
|
@ -29,7 +29,6 @@ export const storybookAliases = {
|
|||
dashboard: 'src/plugins/dashboard/.storybook',
|
||||
data: 'src/plugins/data/.storybook',
|
||||
discover: 'src/plugins/discover/.storybook',
|
||||
embeddable: 'src/plugins/embeddable/.storybook',
|
||||
esql_ast_inspector: 'examples/esql_ast_inspector/.storybook',
|
||||
es_ui_shared: 'src/plugins/es_ui_shared/.storybook',
|
||||
expandable_flyout: 'packages/kbn-expandable-flyout/.storybook',
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { DecoratorFn } from '@storybook/react';
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common';
|
||||
|
||||
export const servicesContextDecorator: DecoratorFn = (story, { globals }) => {
|
||||
const darkMode = ['v8.dark', 'v7.dark'].includes(globals.euiTheme);
|
||||
|
||||
return (
|
||||
<I18nProvider>
|
||||
<EuiThemeProvider darkMode={darkMode}>{story()}</EuiThemeProvider>
|
||||
</I18nProvider>
|
||||
);
|
||||
};
|
|
@ -1,11 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { defaultConfig as default } from '@kbn/storybook';
|
|
@ -1,22 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { addons } from '@storybook/addons';
|
||||
import { create } from '@storybook/theming';
|
||||
import { PANEL_ID } from '@storybook/addon-actions';
|
||||
|
||||
addons.setConfig({
|
||||
theme: create({
|
||||
base: 'light',
|
||||
brandTitle: 'Kibana Embeddable Storybook',
|
||||
brandUrl: 'https://github.com/elastic/kibana/tree/main/src/plugins/embeddable',
|
||||
}),
|
||||
showPanel: true.valueOf,
|
||||
selectedPanel: PANEL_ID,
|
||||
});
|
|
@ -1,30 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { addDecorator } from '@storybook/react';
|
||||
import { Title, Subtitle, Description, Primary, Stories } from '@storybook/addon-docs/blocks';
|
||||
|
||||
import { servicesContextDecorator } from './decorator';
|
||||
|
||||
addDecorator(servicesContextDecorator);
|
||||
|
||||
export const parameters = {
|
||||
docs: {
|
||||
page: () => (
|
||||
<>
|
||||
<Title />
|
||||
<Subtitle />
|
||||
<Description />
|
||||
<Primary />
|
||||
<Stories />
|
||||
</>
|
||||
),
|
||||
},
|
||||
};
|
|
@ -1,279 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React, {
|
||||
forwardRef,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { ReplaySubject } from 'rxjs';
|
||||
import { ThemeContext } from '@emotion/react';
|
||||
import { DecoratorFn, Meta } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { CoreTheme } from '@kbn/core-theme-browser';
|
||||
import type { Action } from '@kbn/ui-actions-plugin/public';
|
||||
|
||||
import { CONTEXT_MENU_TRIGGER, EmbeddablePanel, PANEL_BADGE_TRIGGER, ViewMode } from '..';
|
||||
import { actions } from '../store';
|
||||
import { HelloWorldEmbeddable } from './hello_world_embeddable';
|
||||
|
||||
const layout: DecoratorFn = (story) => {
|
||||
return (
|
||||
<EuiFlexGroup direction="row" justifyContent="center">
|
||||
<EuiFlexItem grow={false} style={{ height: 300, width: 500 }}>
|
||||
{story()}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export default {
|
||||
title: 'components/EmbeddablePanel',
|
||||
argTypes: {
|
||||
hideHeader: {
|
||||
name: 'Hide Header',
|
||||
control: { type: 'boolean' },
|
||||
},
|
||||
loading: {
|
||||
name: 'Loading',
|
||||
control: { type: 'boolean' },
|
||||
},
|
||||
showShadow: {
|
||||
name: 'Show Shadow',
|
||||
control: { type: 'boolean' },
|
||||
},
|
||||
title: {
|
||||
name: 'Title',
|
||||
control: { type: 'text' },
|
||||
},
|
||||
viewMode: {
|
||||
name: 'View Mode',
|
||||
control: { type: 'boolean' },
|
||||
},
|
||||
},
|
||||
decorators: [layout],
|
||||
} as Meta;
|
||||
|
||||
interface HelloWorldEmbeddablePanelProps {
|
||||
getActions?(type: string): Promise<Action[]>;
|
||||
hideHeader: boolean;
|
||||
loading: boolean;
|
||||
showShadow: boolean;
|
||||
showBorder: boolean;
|
||||
title: string;
|
||||
viewMode: boolean;
|
||||
}
|
||||
|
||||
const HelloWorldEmbeddablePanel = forwardRef<
|
||||
{ embeddable: HelloWorldEmbeddable },
|
||||
HelloWorldEmbeddablePanelProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
getActions,
|
||||
hideHeader,
|
||||
loading,
|
||||
showShadow,
|
||||
showBorder,
|
||||
title,
|
||||
viewMode,
|
||||
}: HelloWorldEmbeddablePanelProps,
|
||||
ref
|
||||
) => {
|
||||
const embeddable = useMemo(() => new HelloWorldEmbeddable({ id: `${Math.random()}` }, {}), []);
|
||||
const theme$ = useMemo(() => new ReplaySubject<CoreTheme>(1), []);
|
||||
const theme = useContext(ThemeContext) as CoreTheme;
|
||||
|
||||
useEffect(() => theme$.next(theme), [theme$, theme]);
|
||||
useEffect(() => {
|
||||
embeddable.store.dispatch(actions.input.setTitle(title));
|
||||
}, [embeddable.store, title]);
|
||||
useEffect(() => {
|
||||
embeddable.store.dispatch(
|
||||
actions.input.setViewMode(viewMode ? ViewMode.VIEW : ViewMode.EDIT)
|
||||
);
|
||||
}, [embeddable.store, viewMode]);
|
||||
useEffect(
|
||||
() => void embeddable.store.dispatch(actions.output.setLoading(loading)),
|
||||
[embeddable, loading]
|
||||
);
|
||||
useImperativeHandle(ref, () => ({ embeddable }));
|
||||
|
||||
return (
|
||||
<EmbeddablePanel
|
||||
embeddable={embeddable}
|
||||
getActions={getActions}
|
||||
hideHeader={hideHeader}
|
||||
showShadow={showShadow}
|
||||
showBorder={showBorder}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const Default = HelloWorldEmbeddablePanel as Meta<HelloWorldEmbeddablePanelProps>;
|
||||
|
||||
Default.args = {
|
||||
hideHeader: false,
|
||||
loading: false,
|
||||
showShadow: false,
|
||||
showBorder: false,
|
||||
title: 'Hello World',
|
||||
viewMode: true,
|
||||
};
|
||||
|
||||
interface DefaultWithBadgesProps extends HelloWorldEmbeddablePanelProps {
|
||||
badges: string[];
|
||||
}
|
||||
|
||||
export function DefaultWithBadges({ badges, ...props }: DefaultWithBadgesProps) {
|
||||
const getActions = useCallback(
|
||||
async (type: string) => {
|
||||
switch (type) {
|
||||
case PANEL_BADGE_TRIGGER:
|
||||
return (
|
||||
badges?.map<Action>((badge, id) => ({
|
||||
execute: async (...args) => action(`onClick(${badge})`)(...args),
|
||||
getDisplayName: () => badge,
|
||||
getIconType: () => ['help', 'search', undefined][id % 3],
|
||||
id: `${id}`,
|
||||
isCompatible: async () => true,
|
||||
type: '',
|
||||
})) ?? []
|
||||
);
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
},
|
||||
[badges]
|
||||
);
|
||||
const ref = useRef<React.ComponentRef<typeof HelloWorldEmbeddablePanel>>(null);
|
||||
|
||||
useEffect(
|
||||
() =>
|
||||
void ref.current?.embeddable.store.dispatch(
|
||||
actions.input.setLastReloadRequestTime(new Date().getMilliseconds())
|
||||
),
|
||||
[getActions]
|
||||
);
|
||||
|
||||
return <HelloWorldEmbeddablePanel ref={ref} {...props} getActions={getActions} />;
|
||||
}
|
||||
|
||||
DefaultWithBadges.args = {
|
||||
...Default.args,
|
||||
badges: ['Help', 'Search', 'Something'],
|
||||
};
|
||||
|
||||
DefaultWithBadges.argTypes = {
|
||||
badges: { name: 'Badges' },
|
||||
};
|
||||
|
||||
interface DefaultWithContextMenuProps extends HelloWorldEmbeddablePanelProps {
|
||||
items: string[];
|
||||
}
|
||||
|
||||
export function DefaultWithContextMenu({ items, ...props }: DefaultWithContextMenuProps) {
|
||||
const getActions = useCallback(
|
||||
async (type: string) => {
|
||||
switch (type) {
|
||||
case CONTEXT_MENU_TRIGGER:
|
||||
return (
|
||||
items?.map<Action>((item, id) => ({
|
||||
execute: async (...args) => action(`onClick(${item})`)(...args),
|
||||
getDisplayName: () => item,
|
||||
getIconType: () => ['help', 'search', undefined][id % 3],
|
||||
id: `${id}`,
|
||||
isCompatible: async () => true,
|
||||
type: '',
|
||||
})) ?? []
|
||||
);
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
},
|
||||
[items]
|
||||
);
|
||||
const ref = useRef<React.ComponentRef<typeof HelloWorldEmbeddablePanel>>(null);
|
||||
|
||||
useEffect(
|
||||
() =>
|
||||
void ref.current?.embeddable.store.dispatch(
|
||||
actions.input.setLastReloadRequestTime(new Date().getMilliseconds())
|
||||
),
|
||||
[getActions]
|
||||
);
|
||||
|
||||
return <HelloWorldEmbeddablePanel ref={ref} {...props} getActions={getActions} />;
|
||||
}
|
||||
|
||||
DefaultWithContextMenu.args = {
|
||||
...Default.args,
|
||||
items: ['Help', 'Search', 'Something'],
|
||||
};
|
||||
|
||||
DefaultWithContextMenu.argTypes = {
|
||||
items: { name: 'Context Menu Items' },
|
||||
};
|
||||
|
||||
interface DefaultWithErrorProps extends HelloWorldEmbeddablePanelProps {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export function DefaultWithError({ message, ...props }: DefaultWithErrorProps) {
|
||||
const ref = useRef<React.ComponentRef<typeof HelloWorldEmbeddablePanel>>(null);
|
||||
|
||||
useEffect(
|
||||
() => void ref.current?.embeddable.store.dispatch(actions.output.setError(new Error(message))),
|
||||
[message]
|
||||
);
|
||||
|
||||
return <HelloWorldEmbeddablePanel ref={ref} {...props} />;
|
||||
}
|
||||
|
||||
DefaultWithError.args = {
|
||||
...Default.args,
|
||||
message: 'Something went wrong',
|
||||
};
|
||||
|
||||
DefaultWithError.argTypes = {
|
||||
message: { name: 'Message', control: { type: 'text' } },
|
||||
};
|
||||
|
||||
export function DefaultWithCustomError({ message, ...props }: DefaultWithErrorProps) {
|
||||
const ref = useRef<React.ComponentRef<typeof HelloWorldEmbeddablePanel>>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
ref.current.embeddable.catchError = (error) => {
|
||||
return <EuiEmptyPrompt iconColor="warning" iconType="bug" body={error.message} />;
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
useEffect(
|
||||
() => void ref.current?.embeddable.store.dispatch(actions.output.setError(new Error(message))),
|
||||
[message]
|
||||
);
|
||||
|
||||
return <HelloWorldEmbeddablePanel ref={ref} {...props} />;
|
||||
}
|
||||
|
||||
DefaultWithCustomError.args = {
|
||||
...Default.args,
|
||||
message: 'Something went wrong',
|
||||
};
|
||||
|
||||
DefaultWithCustomError.argTypes = {
|
||||
message: { name: 'Message', control: { type: 'text' } },
|
||||
};
|
|
@ -1,78 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import { DecoratorFn, Meta } from '@storybook/react';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
|
||||
import { EmbeddableInput, EmbeddableRoot } from '..';
|
||||
import { HelloWorldEmbeddable } from './hello_world_embeddable';
|
||||
|
||||
const layout: DecoratorFn = (story) => {
|
||||
return (
|
||||
<EuiFlexGroup direction="row" justifyContent="center">
|
||||
<EuiFlexItem grow={false} style={{ height: 300, width: 500 }}>
|
||||
{story()}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export default {
|
||||
title: 'components/EmbeddableRoot',
|
||||
argTypes: {
|
||||
loading: {
|
||||
name: 'Loading',
|
||||
control: { type: 'boolean' },
|
||||
},
|
||||
title: {
|
||||
name: 'Title',
|
||||
control: { type: 'text' },
|
||||
},
|
||||
},
|
||||
decorators: [layout],
|
||||
} as Meta;
|
||||
|
||||
interface DefaultProps {
|
||||
error?: string;
|
||||
loading?: boolean;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export function Default({ title, ...props }: DefaultProps) {
|
||||
const id = useMemo(() => `${Math.random()}`, []);
|
||||
const input = useMemo<EmbeddableInput>(
|
||||
() => ({
|
||||
id,
|
||||
title,
|
||||
lastReloadRequestTime: new Date().getMilliseconds(),
|
||||
}),
|
||||
[id, title]
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const embeddable = useMemo(() => new HelloWorldEmbeddable(input, {}), []);
|
||||
|
||||
return <EmbeddableRoot {...props} embeddable={embeddable} input={input} />;
|
||||
}
|
||||
|
||||
Default.args = {
|
||||
title: 'Hello World',
|
||||
loading: false,
|
||||
};
|
||||
|
||||
export const DefaultWithError = Default.bind({}) as Meta<DefaultProps>;
|
||||
|
||||
DefaultWithError.args = {
|
||||
...Default.args,
|
||||
error: 'Something went wrong',
|
||||
};
|
||||
|
||||
DefaultWithError.argTypes = {
|
||||
error: { name: 'Error', control: { type: 'text' } },
|
||||
};
|
|
@ -1,43 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { Meta } from '@storybook/react';
|
||||
|
||||
import { ErrorEmbeddable } from '..';
|
||||
|
||||
export default {
|
||||
title: 'components/ErrorEmbeddable',
|
||||
argTypes: {
|
||||
message: {
|
||||
name: 'Message',
|
||||
control: { type: 'text' },
|
||||
},
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
interface ErrorEmbeddableWrapperProps {
|
||||
message: string;
|
||||
}
|
||||
|
||||
function ErrorEmbeddableWrapper({ message }: ErrorEmbeddableWrapperProps) {
|
||||
const embeddable = useMemo(
|
||||
() => new ErrorEmbeddable(message, { id: `${Math.random()}` }, undefined),
|
||||
[message]
|
||||
);
|
||||
useEffect(() => () => embeddable.destroy(), [embeddable]);
|
||||
|
||||
return embeddable.render();
|
||||
}
|
||||
|
||||
export const Default = ErrorEmbeddableWrapper as Meta<ErrorEmbeddableWrapperProps>;
|
||||
|
||||
Default.args = {
|
||||
message: 'Something went wrong',
|
||||
};
|
|
@ -1,33 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { connect, Provider } from 'react-redux';
|
||||
import { EuiEmptyPrompt } from '@elastic/eui';
|
||||
import { Embeddable } from '..';
|
||||
import { createStore, State } from '../store';
|
||||
|
||||
export class HelloWorldEmbeddable extends Embeddable {
|
||||
// eslint-disable-next-line @kbn/eslint/no_this_in_property_initializers
|
||||
readonly store = createStore(this);
|
||||
|
||||
readonly type = 'hello-world';
|
||||
|
||||
reload() {}
|
||||
|
||||
render() {
|
||||
const HelloWorld = connect((state: State) => ({ body: state.input.title }))(EuiEmptyPrompt);
|
||||
|
||||
return (
|
||||
<Provider store={this.store}>
|
||||
<HelloWorld />
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -22,8 +22,6 @@ export {
|
|||
defaultEmbeddableFactoryProvider,
|
||||
Embeddable,
|
||||
EmbeddableFactoryNotFoundError,
|
||||
EmbeddableRenderer,
|
||||
EmbeddableRoot,
|
||||
EmbeddableStateTransfer,
|
||||
ErrorEmbeddable,
|
||||
genericEmbeddableInputIsEqual,
|
||||
|
@ -52,7 +50,6 @@ export {
|
|||
SELECT_RANGE_TRIGGER,
|
||||
shouldFetch$,
|
||||
shouldRefreshFilterCompareOptions,
|
||||
useEmbeddableFactory,
|
||||
VALUE_CLICK_TRIGGER,
|
||||
ViewMode,
|
||||
withEmbeddableSubscription,
|
||||
|
@ -72,7 +69,6 @@ export type {
|
|||
EmbeddableInstanceConfiguration,
|
||||
EmbeddableOutput,
|
||||
EmbeddablePackageState,
|
||||
EmbeddableRendererProps,
|
||||
FilterableEmbeddable,
|
||||
IContainer,
|
||||
IEmbeddable,
|
||||
|
|
|
@ -1,67 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { render, renderHook } from '@testing-library/react';
|
||||
import {
|
||||
HelloWorldEmbeddable,
|
||||
HelloWorldEmbeddableFactoryDefinition,
|
||||
HELLO_WORLD_EMBEDDABLE,
|
||||
} from '../../tests/fixtures';
|
||||
import { EmbeddableRenderer, useEmbeddableFactory } from './embeddable_renderer';
|
||||
import { embeddablePluginMock } from '../../mocks';
|
||||
|
||||
describe('useEmbeddableFactory', () => {
|
||||
it('should update upstream value changes', async () => {
|
||||
const { setup, doStart } = embeddablePluginMock.createInstance();
|
||||
const getFactory = setup.registerEmbeddableFactory(
|
||||
HELLO_WORLD_EMBEDDABLE,
|
||||
new HelloWorldEmbeddableFactoryDefinition()
|
||||
);
|
||||
doStart();
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useEmbeddableFactory({ factory: getFactory(), input: { id: 'hello' } })
|
||||
);
|
||||
|
||||
const [, loading] = result.current;
|
||||
|
||||
expect(loading).toBe(true);
|
||||
|
||||
await waitFor(() => {
|
||||
const [embeddable] = result.current;
|
||||
expect(embeddable).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('<EmbeddableRenderer/>', () => {
|
||||
test('Render embeddable', () => {
|
||||
const embeddable = new HelloWorldEmbeddable({ id: 'hello' });
|
||||
const { getByTestId } = render(<EmbeddableRenderer embeddable={embeddable} />);
|
||||
expect(getByTestId('helloWorldEmbeddable')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Render factory', async () => {
|
||||
const { setup, doStart } = embeddablePluginMock.createInstance();
|
||||
const getFactory = setup.registerEmbeddableFactory(
|
||||
HELLO_WORLD_EMBEDDABLE,
|
||||
new HelloWorldEmbeddableFactoryDefinition()
|
||||
);
|
||||
doStart();
|
||||
|
||||
const { getByTestId, queryByTestId } = render(
|
||||
<EmbeddableRenderer factory={getFactory()} input={{ id: 'hello' }} />
|
||||
);
|
||||
expect(getByTestId('embedSpinner')).toBeInTheDocument();
|
||||
await waitFor(() => !queryByTestId('embedSpinner')); // wait until spinner disappears
|
||||
expect(getByTestId('helloWorldEmbeddable')).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -1,165 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { EmbeddableInput, IEmbeddable } from './i_embeddable';
|
||||
import { EmbeddableRoot } from './embeddable_root';
|
||||
import { EmbeddableFactory } from './embeddable_factory';
|
||||
import { ErrorEmbeddable } from './error_embeddable';
|
||||
import { isErrorEmbeddable } from './is_error_embeddable';
|
||||
|
||||
/**
|
||||
* This type is a publicly exposed props of {@link EmbeddableRenderer}
|
||||
* Union is used to validate that or factory or embeddable is passed in, but it can't be both simultaneously
|
||||
* In case when embeddable is passed in, input is optional, because there is already an input inside of embeddable object
|
||||
* In case when factory is used, then input is required, because it will be used as initial input to create an embeddable object
|
||||
*/
|
||||
export type EmbeddableRendererProps<I extends EmbeddableInput> =
|
||||
| EmbeddableRendererPropsWithEmbeddable<I>
|
||||
| EmbeddableRendererWithFactory<I>;
|
||||
|
||||
interface EmbeddableRendererPropsWithEmbeddable<I extends EmbeddableInput> {
|
||||
input?: I;
|
||||
onInputUpdated?: (newInput: I) => void;
|
||||
embeddable: IEmbeddable<I>;
|
||||
}
|
||||
|
||||
interface EmbeddableRendererWithFactory<I extends EmbeddableInput> {
|
||||
input: I;
|
||||
onInputUpdated?: (newInput: I) => void;
|
||||
factory: EmbeddableFactory<I>;
|
||||
}
|
||||
|
||||
function isWithFactory<I extends EmbeddableInput>(
|
||||
props: EmbeddableRendererProps<I>
|
||||
): props is EmbeddableRendererWithFactory<I> {
|
||||
return 'factory' in props;
|
||||
}
|
||||
|
||||
export function useEmbeddableFactory<I extends EmbeddableInput>({
|
||||
input,
|
||||
factory,
|
||||
onInputUpdated,
|
||||
}: EmbeddableRendererWithFactory<I>) {
|
||||
const [embeddable, setEmbeddable] = useState<IEmbeddable<I> | ErrorEmbeddable | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string | undefined>();
|
||||
const latestInput = React.useRef(input);
|
||||
useEffect(() => {
|
||||
latestInput.current = input;
|
||||
}, [input]);
|
||||
|
||||
useEffect(() => {
|
||||
let canceled = false;
|
||||
|
||||
// keeping track of embeddables created by this component to be able to destroy them
|
||||
let createdEmbeddableRef: IEmbeddable | ErrorEmbeddable | undefined;
|
||||
setEmbeddable(undefined);
|
||||
setLoading(true);
|
||||
factory
|
||||
.create(latestInput.current!)
|
||||
.then((createdEmbeddable) => {
|
||||
if (canceled) {
|
||||
if (createdEmbeddable) {
|
||||
createdEmbeddable.destroy();
|
||||
}
|
||||
} else {
|
||||
createdEmbeddableRef = createdEmbeddable;
|
||||
setEmbeddable(createdEmbeddable);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (canceled) return;
|
||||
setError(err?.message);
|
||||
})
|
||||
.finally(() => {
|
||||
if (canceled) return;
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
return () => {
|
||||
canceled = true;
|
||||
if (createdEmbeddableRef) {
|
||||
createdEmbeddableRef.destroy();
|
||||
}
|
||||
};
|
||||
}, [factory]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!embeddable) return;
|
||||
if (isErrorEmbeddable(embeddable)) return;
|
||||
if (!onInputUpdated) return;
|
||||
const sub = embeddable.getInput$().subscribe((newInput) => {
|
||||
onInputUpdated(newInput);
|
||||
});
|
||||
return () => {
|
||||
sub.unsubscribe();
|
||||
};
|
||||
}, [embeddable, onInputUpdated]);
|
||||
|
||||
return [embeddable, loading, error] as const;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper react component to render an embeddable
|
||||
* Can be used if you have an embeddable object or an embeddable factory
|
||||
* Supports updating input by passing `input` prop
|
||||
*
|
||||
* @remarks
|
||||
* This component shouldn't be used inside an embeddable container to render embeddable children
|
||||
* because children may lose inherited input, here is why:
|
||||
*
|
||||
* When passing `input` inside a prop, internally there is a call:
|
||||
*
|
||||
* ```ts
|
||||
* embeddable.updateInput(input);
|
||||
* ```
|
||||
* If you are simply rendering an embeddable, it's no problem.
|
||||
*
|
||||
* However when you are dealing with containers,
|
||||
* you want to be sure to only pass into updateInput the actual state that changed.
|
||||
* This is because calling child.updateInput({ foo }) will make foo explicit state.
|
||||
* It cannot be inherited from it's parent.
|
||||
*
|
||||
* For example, on a dashboard, the time range is inherited by all children,
|
||||
* unless they had their time range set explicitly.
|
||||
* This is how "per panel time range" works.
|
||||
* That action calls embeddable.updateInput({ timeRange }),
|
||||
* and the time range will no longer be inherited from the container.
|
||||
*
|
||||
* see: https://github.com/elastic/kibana/pull/67783#discussion_r435447657 for more details.
|
||||
* refer to: examples/embeddable_explorer for examples with correct usage of this component.
|
||||
*
|
||||
* @public
|
||||
* @param props - {@link EmbeddableRendererProps}
|
||||
*/
|
||||
export const EmbeddableRenderer = <I extends EmbeddableInput>(
|
||||
props: EmbeddableRendererProps<I>
|
||||
) => {
|
||||
if (isWithFactory(props)) {
|
||||
return <EmbeddableByFactory {...props} />;
|
||||
}
|
||||
return <EmbeddableRoot embeddable={props.embeddable} input={props.input} />;
|
||||
};
|
||||
|
||||
//
|
||||
const EmbeddableByFactory = <I extends EmbeddableInput>({
|
||||
factory,
|
||||
input,
|
||||
onInputUpdated,
|
||||
}: EmbeddableRendererWithFactory<I>) => {
|
||||
const [embeddable, loading, error] = useEmbeddableFactory({
|
||||
factory,
|
||||
input,
|
||||
onInputUpdated,
|
||||
});
|
||||
return <EmbeddableRoot embeddable={embeddable} loading={loading} error={error} input={input} />;
|
||||
};
|
|
@ -1,61 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { HelloWorldEmbeddable, HelloWorldEmbeddableReact } from '../../tests/fixtures';
|
||||
import { EmbeddableRoot } from './embeddable_root';
|
||||
import { mount } from 'enzyme';
|
||||
import { findTestSubject } from '@elastic/eui/lib/test';
|
||||
|
||||
test('EmbeddableRoot renders an embeddable', async () => {
|
||||
const embeddable = new HelloWorldEmbeddable({ id: 'hello' });
|
||||
const component = mount(<EmbeddableRoot embeddable={embeddable} />);
|
||||
// Due to the way embeddables mount themselves on the dom node, they are not forced to be
|
||||
// react components, and hence, we can't use the usual
|
||||
// findTestSubject.
|
||||
expect(
|
||||
component.getDOMNode().querySelectorAll('[data-test-subj="helloWorldEmbeddable"]').length
|
||||
).toBe(1);
|
||||
expect(findTestSubject(component, 'embedSpinner').length).toBe(0);
|
||||
expect(findTestSubject(component, 'embedError').length).toBe(0);
|
||||
});
|
||||
|
||||
test('EmbeddableRoot renders a React-based embeddable', async () => {
|
||||
const embeddable = new HelloWorldEmbeddableReact({ id: 'hello' });
|
||||
const component = mount(<EmbeddableRoot embeddable={embeddable} />);
|
||||
|
||||
expect(component.find('[data-test-subj="helloWorldEmbeddable"]')).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('EmbeddableRoot updates input', async () => {
|
||||
const embeddable = new HelloWorldEmbeddable({ id: 'hello' });
|
||||
const component = mount(<EmbeddableRoot embeddable={embeddable} />);
|
||||
const spy = jest.spyOn(embeddable, 'updateInput');
|
||||
const newInput = { id: 'hello', something: 'new' };
|
||||
component.setProps({ embeddable, input: newInput });
|
||||
expect(spy).toHaveBeenCalledWith(newInput);
|
||||
});
|
||||
|
||||
test('EmbeddableRoot renders a spinner if loading an no embeddable given', async () => {
|
||||
const component = mount(<EmbeddableRoot loading={true} />);
|
||||
// Due to the way embeddables mount themselves on the dom node, they are not forced to be
|
||||
// react components, and hence, we can't use the usual
|
||||
// findTestSubject.
|
||||
expect(findTestSubject(component, 'embedSpinner').length).toBe(1);
|
||||
expect(findTestSubject(component, 'embedError').length).toBe(0);
|
||||
});
|
||||
|
||||
test('EmbeddableRoot renders an error if given with no embeddable', async () => {
|
||||
const component = mount(<EmbeddableRoot error="bad" />);
|
||||
// Due to the way embeddables mount themselves on the dom node, they are not forced to be
|
||||
// react components, and hence, we can't use the usual
|
||||
// findTestSubject.
|
||||
expect(findTestSubject(component, 'embedError').length).toBe(1);
|
||||
expect(findTestSubject(component, 'embedSpinner').length).toBe(0);
|
||||
});
|
|
@ -1,66 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React, { ReactNode, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { EuiLoadingSpinner } from '@elastic/eui';
|
||||
import { EuiText } from '@elastic/eui';
|
||||
import { isPromise } from '@kbn/std';
|
||||
import { MaybePromise } from '@kbn/utility-types';
|
||||
import { EmbeddableInput, EmbeddableOutput, IEmbeddable } from './i_embeddable';
|
||||
import { EmbeddableErrorHandler } from './embeddable_error_handler';
|
||||
|
||||
interface Props {
|
||||
embeddable?: IEmbeddable<EmbeddableInput, EmbeddableOutput, MaybePromise<ReactNode>>;
|
||||
loading?: boolean;
|
||||
error?: string;
|
||||
input?: EmbeddableInput;
|
||||
}
|
||||
|
||||
export const EmbeddableRoot: React.FC<Props> = ({ embeddable, loading, error, input }) => {
|
||||
const [node, setNode] = useState<ReactNode | undefined>();
|
||||
const [embeddableHasMounted, setEmbeddableHasMounted] = useState(false);
|
||||
const rootRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const updateNode = useCallback((newNode: MaybePromise<ReactNode>) => {
|
||||
if (isPromise(newNode)) {
|
||||
newNode.then(updateNode);
|
||||
return;
|
||||
}
|
||||
|
||||
setNode(newNode);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!rootRef.current || !embeddable) {
|
||||
return;
|
||||
}
|
||||
|
||||
setEmbeddableHasMounted(true);
|
||||
updateNode(embeddable.render(rootRef.current) ?? undefined);
|
||||
embeddable.render(rootRef.current);
|
||||
}, [updateNode, embeddable]);
|
||||
|
||||
useEffect(() => {
|
||||
if (input && embeddable && embeddableHasMounted) {
|
||||
embeddable.updateInput(input);
|
||||
}
|
||||
}, [input, embeddable, embeddableHasMounted]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div ref={rootRef}>{node}</div>
|
||||
{loading && <EuiLoadingSpinner data-test-subj="embedSpinner" />}
|
||||
{error && (
|
||||
<EmbeddableErrorHandler embeddable={embeddable} error={error}>
|
||||
{({ message }) => <EuiText data-test-subj="embedError">{message}</EuiText>}
|
||||
</EmbeddableErrorHandler>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,54 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { setStubKibanaServices as setPresentationPanelMocks } from '@kbn/presentation-panel-plugin/public/mocks';
|
||||
import { waitFor, render } from '@testing-library/react';
|
||||
import { ErrorEmbeddable } from './error_embeddable';
|
||||
import { EmbeddableRoot } from './embeddable_root';
|
||||
|
||||
test('ErrorEmbeddable renders an embeddable', async () => {
|
||||
setPresentationPanelMocks();
|
||||
const embeddable = new ErrorEmbeddable('some error occurred', { id: '123', title: 'Error' });
|
||||
const { getByTestId, getByText } = render(<EmbeddableRoot embeddable={embeddable} />);
|
||||
|
||||
expect(getByTestId('embeddableStackError')).toBeVisible();
|
||||
await waitFor(() => getByTestId('errorMessageMarkdown')); // wait for lazy markdown component
|
||||
expect(getByText(/some error occurred/i)).toBeVisible();
|
||||
});
|
||||
|
||||
test('ErrorEmbeddable renders an embeddable with markdown message', async () => {
|
||||
setPresentationPanelMocks();
|
||||
const error = '[some link](http://localhost:5601/takeMeThere)';
|
||||
const embeddable = new ErrorEmbeddable(error, { id: '123', title: 'Error' });
|
||||
const { getByTestId, getByText } = render(<EmbeddableRoot embeddable={embeddable} />);
|
||||
|
||||
expect(getByTestId('embeddableStackError')).toBeVisible();
|
||||
await waitFor(() => getByTestId('errorMessageMarkdown')); // wait for lazy markdown component
|
||||
expect(getByText(/some link/i)).toMatchInlineSnapshot(`
|
||||
<a
|
||||
class="euiLink emotion-euiLink-primary"
|
||||
href="http://localhost:5601/takeMeThere"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
some link
|
||||
<span
|
||||
class="emotion-EuiExternalLinkIcon"
|
||||
data-euiicon-type="popout"
|
||||
role="presentation"
|
||||
/>
|
||||
<span
|
||||
class="emotion-euiScreenReaderOnly"
|
||||
>
|
||||
(external, opens in a new tab or window)
|
||||
</span>
|
||||
</a>
|
||||
`);
|
||||
});
|
|
@ -14,9 +14,6 @@ export { Embeddable } from './embeddable';
|
|||
export { EmbeddableErrorHandler } from './embeddable_error_handler';
|
||||
export * from './embeddable_factory';
|
||||
export * from './embeddable_factory_definition';
|
||||
export { EmbeddableRenderer, useEmbeddableFactory } from './embeddable_renderer';
|
||||
export type { EmbeddableRendererProps } from './embeddable_renderer';
|
||||
export { EmbeddableRoot } from './embeddable_root';
|
||||
export { ErrorEmbeddable } from './error_embeddable';
|
||||
export { isErrorEmbeddable } from './is_error_embeddable';
|
||||
export { isEmbeddable } from './is_embeddable';
|
||||
|
|
|
@ -3,19 +3,16 @@
|
|||
"compilerOptions": {
|
||||
"outDir": "target/types"
|
||||
},
|
||||
"include": ["*.ts", ".storybook/**/*", "common/**/*", "public/**/*", "server/**/*"],
|
||||
"include": ["*.ts", "common/**/*", "public/**/*", "server/**/*"],
|
||||
"kbn_references": [
|
||||
"@kbn/core",
|
||||
"@kbn/inspector-plugin",
|
||||
"@kbn/saved-objects-plugin",
|
||||
"@kbn/kibana-utils-plugin",
|
||||
"@kbn/kibana-react-plugin",
|
||||
"@kbn/ui-actions-plugin",
|
||||
"@kbn/i18n-react",
|
||||
"@kbn/storybook",
|
||||
"@kbn/utility-types",
|
||||
"@kbn/es-query",
|
||||
"@kbn/core-theme-browser",
|
||||
"@kbn/i18n",
|
||||
"@kbn/std",
|
||||
"@kbn/expressions-plugin",
|
||||
|
|
|
@ -21,7 +21,8 @@
|
|||
"dashboard",
|
||||
"dashboardEnhanced",
|
||||
"developerExamples",
|
||||
"unifiedSearch"
|
||||
"unifiedSearch",
|
||||
"embeddable",
|
||||
],
|
||||
"requiredBundles": [
|
||||
"dashboardEnhanced",
|
||||
|
|
|
@ -7,15 +7,19 @@
|
|||
|
||||
import React from 'react';
|
||||
import { EuiPage } from '@elastic/eui';
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
|
||||
import { Page } from '../../components/page';
|
||||
import { DrilldownsManager } from '../drilldowns_manager';
|
||||
|
||||
export const App: React.FC = () => {
|
||||
export const App = ({ core }: { core: CoreStart }) => {
|
||||
return (
|
||||
<EuiPage>
|
||||
<Page title={'UI Actions Enhanced'}>
|
||||
<DrilldownsManager />
|
||||
</Page>
|
||||
</EuiPage>
|
||||
<KibanaRenderContextProvider i18n={core.i18n} theme={core.theme}>
|
||||
<EuiPage>
|
||||
<Page title={'UI Actions Enhanced'}>
|
||||
<DrilldownsManager />
|
||||
</Page>
|
||||
</EuiPage>
|
||||
</KibanaRenderContextProvider>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -18,29 +18,12 @@ import {
|
|||
EuiFlexItem,
|
||||
EuiFlexGroup,
|
||||
} from '@elastic/eui';
|
||||
import { EmbeddableRoot, VALUE_CLICK_TRIGGER } from '@kbn/embeddable-plugin/public';
|
||||
import { SampleMlJob, SampleApp1ClickContext } from '../../triggers';
|
||||
import { ButtonEmbeddable } from '../../embeddables/button_embeddable';
|
||||
import { ReactEmbeddableRenderer, VALUE_CLICK_TRIGGER } from '@kbn/embeddable-plugin/public';
|
||||
import { useUiActions } from '../../context';
|
||||
|
||||
export const job: SampleMlJob = {
|
||||
job_id: '123',
|
||||
job_type: 'anomaly_detector',
|
||||
description: 'This is some ML job.',
|
||||
};
|
||||
|
||||
export const context: SampleApp1ClickContext = { job };
|
||||
import { BUTTON_EMBEDDABLE } from '../../embeddables/register_button_embeddable';
|
||||
|
||||
export const DrilldownsWithEmbeddableExample: React.FC = () => {
|
||||
const { plugins, managerWithEmbeddable } = useUiActions();
|
||||
const embeddable = React.useMemo(
|
||||
() =>
|
||||
new ButtonEmbeddable(
|
||||
{ id: 'DrilldownsWithEmbeddableExample' },
|
||||
{ uiActions: plugins.uiActionsEnhanced }
|
||||
),
|
||||
[plugins.uiActionsEnhanced]
|
||||
);
|
||||
const [showManager, setShowManager] = React.useState(false);
|
||||
const [openPopup, setOpenPopup] = React.useState(false);
|
||||
const viewRef = React.useRef<'/create' | '/manage'>('/create');
|
||||
|
@ -112,7 +95,13 @@ export const DrilldownsWithEmbeddableExample: React.FC = () => {
|
|||
<EuiFlexItem grow={false}>{openManagerButton}</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<div style={{ maxWidth: 200 }}>
|
||||
<EmbeddableRoot embeddable={embeddable} />
|
||||
<ReactEmbeddableRenderer<{}, {}>
|
||||
type={BUTTON_EMBEDDABLE}
|
||||
getParentApi={() => ({
|
||||
getSerializedStateForChild: () => undefined,
|
||||
})}
|
||||
hidePanelChrome={true}
|
||||
/>
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
@ -124,7 +113,6 @@ export const DrilldownsWithEmbeddableExample: React.FC = () => {
|
|||
initialRoute={viewRef.current}
|
||||
dynamicActionManager={managerWithEmbeddable}
|
||||
triggers={[VALUE_CLICK_TRIGGER]}
|
||||
placeContext={{ embeddable }}
|
||||
onClose={() => setShowManager(false)}
|
||||
/>
|
||||
</EuiFlyout>
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import {
|
||||
DefaultEmbeddableApi,
|
||||
ReactEmbeddableFactory,
|
||||
VALUE_CLICK_TRIGGER,
|
||||
} from '@kbn/embeddable-plugin/public';
|
||||
import { EuiCard, EuiFlexItem, EuiIcon } from '@elastic/eui';
|
||||
import { AdvancedUiActionsStart } from '@kbn/ui-actions-enhanced-plugin/public';
|
||||
import { BUTTON_EMBEDDABLE } from './register_button_embeddable';
|
||||
|
||||
export const getButtonEmbeddableFactory = (uiActionsEnhanced: AdvancedUiActionsStart) => {
|
||||
const factory: ReactEmbeddableFactory<{}, {}, DefaultEmbeddableApi<{}>> = {
|
||||
type: BUTTON_EMBEDDABLE,
|
||||
deserializeState: (state) => state.rawState,
|
||||
buildEmbeddable: async (state, buildApi, uuid, parentApi) => {
|
||||
const api = buildApi(
|
||||
{
|
||||
serializeState: () => {
|
||||
return {
|
||||
rawState: {},
|
||||
references: [],
|
||||
};
|
||||
},
|
||||
},
|
||||
{}
|
||||
);
|
||||
return {
|
||||
api,
|
||||
Component: () => {
|
||||
const onClick = useCallback(() => {
|
||||
uiActionsEnhanced.getTrigger(VALUE_CLICK_TRIGGER).exec({
|
||||
embeddable: api,
|
||||
data: {
|
||||
data: [],
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
return (
|
||||
<EuiFlexItem>
|
||||
<EuiCard
|
||||
icon={<EuiIcon size="xxl" type={`logoKibana`} />}
|
||||
title={`Click me!`}
|
||||
description={'This embeddable fires "VALUE_CLICK" trigger on click'}
|
||||
onClick={onClick}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
return factory;
|
||||
};
|
|
@ -1,53 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { createElement } from 'react';
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
import { AdvancedUiActionsStart } from '@kbn/ui-actions-enhanced-plugin/public';
|
||||
import { Embeddable, EmbeddableInput, VALUE_CLICK_TRIGGER } from '@kbn/embeddable-plugin/public';
|
||||
import { ButtonEmbeddableComponent } from './button_embeddable_component';
|
||||
|
||||
export const BUTTON_EMBEDDABLE = 'BUTTON_EMBEDDABLE';
|
||||
|
||||
export interface ButtonEmbeddableParams {
|
||||
uiActions: AdvancedUiActionsStart;
|
||||
}
|
||||
|
||||
export class ButtonEmbeddable extends Embeddable {
|
||||
type = BUTTON_EMBEDDABLE;
|
||||
|
||||
constructor(input: EmbeddableInput, private readonly params: ButtonEmbeddableParams) {
|
||||
super(input, {});
|
||||
}
|
||||
|
||||
reload() {}
|
||||
|
||||
private el?: HTMLElement;
|
||||
|
||||
public render(el: HTMLElement): void {
|
||||
super.render(el);
|
||||
this.el = el;
|
||||
render(
|
||||
createElement(ButtonEmbeddableComponent, {
|
||||
onClick: () => {
|
||||
this.params.uiActions.getTrigger(VALUE_CLICK_TRIGGER).exec({
|
||||
embeddable: this,
|
||||
data: {
|
||||
data: [],
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
el
|
||||
);
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
super.destroy();
|
||||
if (this.el) unmountComponentAtNode(this.el);
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import { EuiCard, EuiFlexItem, EuiIcon } from '@elastic/eui';
|
||||
|
||||
export interface ButtonEmbeddableComponentProps {
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export const ButtonEmbeddableComponent: React.FC<ButtonEmbeddableComponentProps> = ({
|
||||
onClick,
|
||||
}) => {
|
||||
return (
|
||||
<EuiFlexItem>
|
||||
<EuiCard
|
||||
icon={<EuiIcon size="xxl" type={`logoKibana`} />}
|
||||
title={`Click me!`}
|
||||
description={'This embeddable fires "VALUE_CLICK" trigger on click'}
|
||||
onClick={onClick}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
};
|
|
@ -1,8 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export * from './button_embeddable';
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EmbeddableSetup } from '@kbn/embeddable-plugin/public';
|
||||
import { StartDependencies } from '../plugin';
|
||||
|
||||
export const BUTTON_EMBEDDABLE = 'BUTTON_EMBEDDABLE';
|
||||
|
||||
export function registerButtonEmbeddable(
|
||||
embeddable: EmbeddableSetup,
|
||||
services: Promise<StartDependencies>
|
||||
) {
|
||||
embeddable.registerReactEmbeddableFactory(BUTTON_EMBEDDABLE, async () => {
|
||||
const { getButtonEmbeddableFactory } = await import('./button_embeddable');
|
||||
const { uiActionsEnhanced } = await services;
|
||||
return getButtonEmbeddableFactory(uiActionsEnhanced);
|
||||
});
|
||||
}
|
|
@ -31,7 +31,7 @@ export const mount =
|
|||
};
|
||||
const reactElement = (
|
||||
<context.Provider value={deps}>
|
||||
<App />
|
||||
<App core={core} />
|
||||
</context.Provider>
|
||||
);
|
||||
render(reactElement, element);
|
||||
|
|
|
@ -21,6 +21,7 @@ import {
|
|||
UiActionsEnhancedMemoryActionStorage,
|
||||
UiActionsEnhancedDynamicActionManager,
|
||||
} from '@kbn/ui-actions-enhanced-plugin/public';
|
||||
import { EmbeddableSetup } from '@kbn/embeddable-plugin/public';
|
||||
import { DashboardHelloWorldDrilldown } from './drilldowns/dashboard_hello_world_drilldown';
|
||||
import { DashboardToDiscoverDrilldown } from './drilldowns/dashboard_to_discover_drilldown';
|
||||
import { App1ToDashboardDrilldown } from './drilldowns/app1_to_dashboard_drilldown';
|
||||
|
@ -35,12 +36,14 @@ import {
|
|||
} from './triggers';
|
||||
import { mount } from './mount';
|
||||
import { App2ToDashboardDrilldown } from './drilldowns/app2_to_dashboard_drilldown';
|
||||
import { registerButtonEmbeddable } from './embeddables/register_button_embeddable';
|
||||
|
||||
export interface SetupDependencies {
|
||||
dashboard: DashboardSetup;
|
||||
data: DataPublicPluginSetup;
|
||||
developerExamples: DeveloperExamplesSetup;
|
||||
discover: DiscoverSetup;
|
||||
embeddable: EmbeddableSetup;
|
||||
uiActionsEnhanced: AdvancedUiActionsSetup;
|
||||
}
|
||||
|
||||
|
@ -62,7 +65,7 @@ export class UiActionsEnhancedExamplesPlugin
|
|||
{
|
||||
public setup(
|
||||
core: CoreSetup<StartDependencies, UiActionsEnhancedExamplesStart>,
|
||||
{ uiActionsEnhanced: uiActions, developerExamples }: SetupDependencies
|
||||
{ embeddable, uiActionsEnhanced: uiActions, developerExamples }: SetupDependencies
|
||||
) {
|
||||
const start = createStartServicesGetter(core.getStartServices);
|
||||
|
||||
|
@ -150,6 +153,12 @@ export class UiActionsEnhancedExamplesPlugin
|
|||
},
|
||||
],
|
||||
});
|
||||
|
||||
const startServicesPromise = core.getStartServices();
|
||||
registerButtonEmbeddable(
|
||||
embeddable,
|
||||
startServicesPromise.then(([_, startDeps]) => startDeps)
|
||||
);
|
||||
}
|
||||
|
||||
public start(_core: CoreStart, plugins: StartDependencies): UiActionsEnhancedExamplesStart {
|
||||
|
|
|
@ -30,5 +30,6 @@
|
|||
"@kbn/utility-types",
|
||||
"@kbn/presentation-publishing",
|
||||
"@kbn/react-kibana-mount",
|
||||
"@kbn/react-kibana-context-render",
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue