Skip to content

Commit

Permalink
[Embeddable] Add storybook stories for the embeddable components (#13…
Browse files Browse the repository at this point in the history
…5904)

* Update embeddable panel not to require actions getter
* Add embeddable panel stories
* Add embeddable root stories
* Add error embeddable stories
  • Loading branch information
dokmic authored Jul 19, 2022
1 parent 8fa849d commit b16577e
Show file tree
Hide file tree
Showing 10 changed files with 538 additions and 11 deletions.
23 changes: 23 additions & 0 deletions src/plugins/embeddable/.storybook/decorator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* 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 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 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>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@
* Side Public License, v 1.
*/

module.exports = require('@kbn/storybook').defaultConfig;
// eslint-disable-next-line import/no-default-export
export { defaultConfig as default } from '@kbn/storybook';
21 changes: 21 additions & 0 deletions src/plugins/embeddable/.storybook/manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* 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 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 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,
});
29 changes: 29 additions & 0 deletions src/plugins/embeddable/.storybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* 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 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 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 />
</>
),
},
};
271 changes: 271 additions & 0 deletions src/plugins/embeddable/public/__stories__/embeddable_panel.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/

import React, {
forwardRef,
useCallback,
useContext,
useEffect,
useImperativeHandle,
useMemo,
useRef,
} from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
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 { 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;
title: string;
viewMode: boolean;
}

const HelloWorldEmbeddablePanel = forwardRef<
{ embeddable: HelloWorldEmbeddable },
HelloWorldEmbeddablePanelProps
>(
(
{
getActions,
hideHeader,
loading,
showShadow,
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.updateInput({
title,
viewMode: viewMode ? ViewMode.VIEW : ViewMode.EDIT,
lastReloadRequestTime: new Date().getMilliseconds(),
}),
[embeddable, title, viewMode]
);
useEffect(
() =>
embeddable.updateOutput({
loading,
}),
[embeddable, loading]
);
useImperativeHandle(ref, () => ({ embeddable }));

return (
<EmbeddablePanel
embeddable={embeddable}
getActions={getActions}
hideHeader={hideHeader}
showShadow={showShadow}
theme={{ theme$ }}
/>
);
}
);

export const Default = HelloWorldEmbeddablePanel as Meta<HelloWorldEmbeddablePanelProps>;

Default.args = {
hideHeader: false,
loading: false,
showShadow: true,
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(
() =>
ref.current?.embeddable.updateInput({ lastReloadRequestTime: 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(
() =>
ref.current?.embeddable.updateInput({ lastReloadRequestTime: 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(() => ref.current?.embeddable.updateOutput({ error: 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(
() =>
ref.current?.embeddable.setErrorRenderer((node, error) => {
render(<EuiEmptyPrompt iconColor="warning" iconType="bug" body={error.message} />, node);

return () => unmountComponentAtNode(node);
}),
[]
);
useEffect(() => ref.current?.embeddable.updateOutput({ error: 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' } },
};
Loading

0 comments on commit b16577e

Please sign in to comment.