mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -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_enhanced',
|
||||||
'dashboard',
|
'dashboard',
|
||||||
'data',
|
'data',
|
||||||
'embeddable',
|
|
||||||
'esql_editor',
|
'esql_editor',
|
||||||
'expression_error',
|
'expression_error',
|
||||||
'expression_image',
|
'expression_image',
|
||||||
|
|
|
@ -29,7 +29,6 @@ export const storybookAliases = {
|
||||||
dashboard: 'src/plugins/dashboard/.storybook',
|
dashboard: 'src/plugins/dashboard/.storybook',
|
||||||
data: 'src/plugins/data/.storybook',
|
data: 'src/plugins/data/.storybook',
|
||||||
discover: 'src/plugins/discover/.storybook',
|
discover: 'src/plugins/discover/.storybook',
|
||||||
embeddable: 'src/plugins/embeddable/.storybook',
|
|
||||||
esql_ast_inspector: 'examples/esql_ast_inspector/.storybook',
|
esql_ast_inspector: 'examples/esql_ast_inspector/.storybook',
|
||||||
es_ui_shared: 'src/plugins/es_ui_shared/.storybook',
|
es_ui_shared: 'src/plugins/es_ui_shared/.storybook',
|
||||||
expandable_flyout: 'packages/kbn-expandable-flyout/.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,
|
defaultEmbeddableFactoryProvider,
|
||||||
Embeddable,
|
Embeddable,
|
||||||
EmbeddableFactoryNotFoundError,
|
EmbeddableFactoryNotFoundError,
|
||||||
EmbeddableRenderer,
|
|
||||||
EmbeddableRoot,
|
|
||||||
EmbeddableStateTransfer,
|
EmbeddableStateTransfer,
|
||||||
ErrorEmbeddable,
|
ErrorEmbeddable,
|
||||||
genericEmbeddableInputIsEqual,
|
genericEmbeddableInputIsEqual,
|
||||||
|
@ -52,7 +50,6 @@ export {
|
||||||
SELECT_RANGE_TRIGGER,
|
SELECT_RANGE_TRIGGER,
|
||||||
shouldFetch$,
|
shouldFetch$,
|
||||||
shouldRefreshFilterCompareOptions,
|
shouldRefreshFilterCompareOptions,
|
||||||
useEmbeddableFactory,
|
|
||||||
VALUE_CLICK_TRIGGER,
|
VALUE_CLICK_TRIGGER,
|
||||||
ViewMode,
|
ViewMode,
|
||||||
withEmbeddableSubscription,
|
withEmbeddableSubscription,
|
||||||
|
@ -72,7 +69,6 @@ export type {
|
||||||
EmbeddableInstanceConfiguration,
|
EmbeddableInstanceConfiguration,
|
||||||
EmbeddableOutput,
|
EmbeddableOutput,
|
||||||
EmbeddablePackageState,
|
EmbeddablePackageState,
|
||||||
EmbeddableRendererProps,
|
|
||||||
FilterableEmbeddable,
|
FilterableEmbeddable,
|
||||||
IContainer,
|
IContainer,
|
||||||
IEmbeddable,
|
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 { EmbeddableErrorHandler } from './embeddable_error_handler';
|
||||||
export * from './embeddable_factory';
|
export * from './embeddable_factory';
|
||||||
export * from './embeddable_factory_definition';
|
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 { ErrorEmbeddable } from './error_embeddable';
|
||||||
export { isErrorEmbeddable } from './is_error_embeddable';
|
export { isErrorEmbeddable } from './is_error_embeddable';
|
||||||
export { isEmbeddable } from './is_embeddable';
|
export { isEmbeddable } from './is_embeddable';
|
||||||
|
|
|
@ -3,19 +3,16 @@
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "target/types"
|
"outDir": "target/types"
|
||||||
},
|
},
|
||||||
"include": ["*.ts", ".storybook/**/*", "common/**/*", "public/**/*", "server/**/*"],
|
"include": ["*.ts", "common/**/*", "public/**/*", "server/**/*"],
|
||||||
"kbn_references": [
|
"kbn_references": [
|
||||||
"@kbn/core",
|
"@kbn/core",
|
||||||
"@kbn/inspector-plugin",
|
"@kbn/inspector-plugin",
|
||||||
"@kbn/saved-objects-plugin",
|
"@kbn/saved-objects-plugin",
|
||||||
"@kbn/kibana-utils-plugin",
|
"@kbn/kibana-utils-plugin",
|
||||||
"@kbn/kibana-react-plugin",
|
|
||||||
"@kbn/ui-actions-plugin",
|
"@kbn/ui-actions-plugin",
|
||||||
"@kbn/i18n-react",
|
"@kbn/i18n-react",
|
||||||
"@kbn/storybook",
|
|
||||||
"@kbn/utility-types",
|
"@kbn/utility-types",
|
||||||
"@kbn/es-query",
|
"@kbn/es-query",
|
||||||
"@kbn/core-theme-browser",
|
|
||||||
"@kbn/i18n",
|
"@kbn/i18n",
|
||||||
"@kbn/std",
|
"@kbn/std",
|
||||||
"@kbn/expressions-plugin",
|
"@kbn/expressions-plugin",
|
||||||
|
|
|
@ -21,7 +21,8 @@
|
||||||
"dashboard",
|
"dashboard",
|
||||||
"dashboardEnhanced",
|
"dashboardEnhanced",
|
||||||
"developerExamples",
|
"developerExamples",
|
||||||
"unifiedSearch"
|
"unifiedSearch",
|
||||||
|
"embeddable",
|
||||||
],
|
],
|
||||||
"requiredBundles": [
|
"requiredBundles": [
|
||||||
"dashboardEnhanced",
|
"dashboardEnhanced",
|
||||||
|
|
|
@ -7,15 +7,19 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { EuiPage } from '@elastic/eui';
|
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 { Page } from '../../components/page';
|
||||||
import { DrilldownsManager } from '../drilldowns_manager';
|
import { DrilldownsManager } from '../drilldowns_manager';
|
||||||
|
|
||||||
export const App: React.FC = () => {
|
export const App = ({ core }: { core: CoreStart }) => {
|
||||||
return (
|
return (
|
||||||
<EuiPage>
|
<KibanaRenderContextProvider i18n={core.i18n} theme={core.theme}>
|
||||||
<Page title={'UI Actions Enhanced'}>
|
<EuiPage>
|
||||||
<DrilldownsManager />
|
<Page title={'UI Actions Enhanced'}>
|
||||||
</Page>
|
<DrilldownsManager />
|
||||||
</EuiPage>
|
</Page>
|
||||||
|
</EuiPage>
|
||||||
|
</KibanaRenderContextProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -18,29 +18,12 @@ import {
|
||||||
EuiFlexItem,
|
EuiFlexItem,
|
||||||
EuiFlexGroup,
|
EuiFlexGroup,
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
import { EmbeddableRoot, VALUE_CLICK_TRIGGER } from '@kbn/embeddable-plugin/public';
|
import { ReactEmbeddableRenderer, VALUE_CLICK_TRIGGER } from '@kbn/embeddable-plugin/public';
|
||||||
import { SampleMlJob, SampleApp1ClickContext } from '../../triggers';
|
|
||||||
import { ButtonEmbeddable } from '../../embeddables/button_embeddable';
|
|
||||||
import { useUiActions } from '../../context';
|
import { useUiActions } from '../../context';
|
||||||
|
import { BUTTON_EMBEDDABLE } from '../../embeddables/register_button_embeddable';
|
||||||
export const job: SampleMlJob = {
|
|
||||||
job_id: '123',
|
|
||||||
job_type: 'anomaly_detector',
|
|
||||||
description: 'This is some ML job.',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const context: SampleApp1ClickContext = { job };
|
|
||||||
|
|
||||||
export const DrilldownsWithEmbeddableExample: React.FC = () => {
|
export const DrilldownsWithEmbeddableExample: React.FC = () => {
|
||||||
const { plugins, managerWithEmbeddable } = useUiActions();
|
const { plugins, managerWithEmbeddable } = useUiActions();
|
||||||
const embeddable = React.useMemo(
|
|
||||||
() =>
|
|
||||||
new ButtonEmbeddable(
|
|
||||||
{ id: 'DrilldownsWithEmbeddableExample' },
|
|
||||||
{ uiActions: plugins.uiActionsEnhanced }
|
|
||||||
),
|
|
||||||
[plugins.uiActionsEnhanced]
|
|
||||||
);
|
|
||||||
const [showManager, setShowManager] = React.useState(false);
|
const [showManager, setShowManager] = React.useState(false);
|
||||||
const [openPopup, setOpenPopup] = React.useState(false);
|
const [openPopup, setOpenPopup] = React.useState(false);
|
||||||
const viewRef = React.useRef<'/create' | '/manage'>('/create');
|
const viewRef = React.useRef<'/create' | '/manage'>('/create');
|
||||||
|
@ -112,7 +95,13 @@ export const DrilldownsWithEmbeddableExample: React.FC = () => {
|
||||||
<EuiFlexItem grow={false}>{openManagerButton}</EuiFlexItem>
|
<EuiFlexItem grow={false}>{openManagerButton}</EuiFlexItem>
|
||||||
<EuiFlexItem grow={false}>
|
<EuiFlexItem grow={false}>
|
||||||
<div style={{ maxWidth: 200 }}>
|
<div style={{ maxWidth: 200 }}>
|
||||||
<EmbeddableRoot embeddable={embeddable} />
|
<ReactEmbeddableRenderer<{}, {}>
|
||||||
|
type={BUTTON_EMBEDDABLE}
|
||||||
|
getParentApi={() => ({
|
||||||
|
getSerializedStateForChild: () => undefined,
|
||||||
|
})}
|
||||||
|
hidePanelChrome={true}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
</EuiFlexGroup>
|
</EuiFlexGroup>
|
||||||
|
@ -124,7 +113,6 @@ export const DrilldownsWithEmbeddableExample: React.FC = () => {
|
||||||
initialRoute={viewRef.current}
|
initialRoute={viewRef.current}
|
||||||
dynamicActionManager={managerWithEmbeddable}
|
dynamicActionManager={managerWithEmbeddable}
|
||||||
triggers={[VALUE_CLICK_TRIGGER]}
|
triggers={[VALUE_CLICK_TRIGGER]}
|
||||||
placeContext={{ embeddable }}
|
|
||||||
onClose={() => setShowManager(false)}
|
onClose={() => setShowManager(false)}
|
||||||
/>
|
/>
|
||||||
</EuiFlyout>
|
</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 = (
|
const reactElement = (
|
||||||
<context.Provider value={deps}>
|
<context.Provider value={deps}>
|
||||||
<App />
|
<App core={core} />
|
||||||
</context.Provider>
|
</context.Provider>
|
||||||
);
|
);
|
||||||
render(reactElement, element);
|
render(reactElement, element);
|
||||||
|
|
|
@ -21,6 +21,7 @@ import {
|
||||||
UiActionsEnhancedMemoryActionStorage,
|
UiActionsEnhancedMemoryActionStorage,
|
||||||
UiActionsEnhancedDynamicActionManager,
|
UiActionsEnhancedDynamicActionManager,
|
||||||
} from '@kbn/ui-actions-enhanced-plugin/public';
|
} from '@kbn/ui-actions-enhanced-plugin/public';
|
||||||
|
import { EmbeddableSetup } from '@kbn/embeddable-plugin/public';
|
||||||
import { DashboardHelloWorldDrilldown } from './drilldowns/dashboard_hello_world_drilldown';
|
import { DashboardHelloWorldDrilldown } from './drilldowns/dashboard_hello_world_drilldown';
|
||||||
import { DashboardToDiscoverDrilldown } from './drilldowns/dashboard_to_discover_drilldown';
|
import { DashboardToDiscoverDrilldown } from './drilldowns/dashboard_to_discover_drilldown';
|
||||||
import { App1ToDashboardDrilldown } from './drilldowns/app1_to_dashboard_drilldown';
|
import { App1ToDashboardDrilldown } from './drilldowns/app1_to_dashboard_drilldown';
|
||||||
|
@ -35,12 +36,14 @@ import {
|
||||||
} from './triggers';
|
} from './triggers';
|
||||||
import { mount } from './mount';
|
import { mount } from './mount';
|
||||||
import { App2ToDashboardDrilldown } from './drilldowns/app2_to_dashboard_drilldown';
|
import { App2ToDashboardDrilldown } from './drilldowns/app2_to_dashboard_drilldown';
|
||||||
|
import { registerButtonEmbeddable } from './embeddables/register_button_embeddable';
|
||||||
|
|
||||||
export interface SetupDependencies {
|
export interface SetupDependencies {
|
||||||
dashboard: DashboardSetup;
|
dashboard: DashboardSetup;
|
||||||
data: DataPublicPluginSetup;
|
data: DataPublicPluginSetup;
|
||||||
developerExamples: DeveloperExamplesSetup;
|
developerExamples: DeveloperExamplesSetup;
|
||||||
discover: DiscoverSetup;
|
discover: DiscoverSetup;
|
||||||
|
embeddable: EmbeddableSetup;
|
||||||
uiActionsEnhanced: AdvancedUiActionsSetup;
|
uiActionsEnhanced: AdvancedUiActionsSetup;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,7 +65,7 @@ export class UiActionsEnhancedExamplesPlugin
|
||||||
{
|
{
|
||||||
public setup(
|
public setup(
|
||||||
core: CoreSetup<StartDependencies, UiActionsEnhancedExamplesStart>,
|
core: CoreSetup<StartDependencies, UiActionsEnhancedExamplesStart>,
|
||||||
{ uiActionsEnhanced: uiActions, developerExamples }: SetupDependencies
|
{ embeddable, uiActionsEnhanced: uiActions, developerExamples }: SetupDependencies
|
||||||
) {
|
) {
|
||||||
const start = createStartServicesGetter(core.getStartServices);
|
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 {
|
public start(_core: CoreStart, plugins: StartDependencies): UiActionsEnhancedExamplesStart {
|
||||||
|
|
|
@ -30,5 +30,6 @@
|
||||||
"@kbn/utility-types",
|
"@kbn/utility-types",
|
||||||
"@kbn/presentation-publishing",
|
"@kbn/presentation-publishing",
|
||||||
"@kbn/react-kibana-mount",
|
"@kbn/react-kibana-mount",
|
||||||
|
"@kbn/react-kibana-context-render",
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue