Add <FormCreateDrilldown> component (#58335)

* refactor: 💡 use .story for Storybook and standartize dir struct

* feat: 🎸 add <FlyoutFrame> component

* feat: 🎸 add <FlyoutCreateDrilldown> component

* refactor: 💡 improve FlyoutCreateDrilldownAction

* test: 💍 add <FlyoutFrame> tests

* docs: ✏️ add @todo for <DrilldownHelloBar>

* feat: 🎸 make name editable in <FormCreateDrilldown>

* test: 💍 add <FormCreateDrilldown> name field tests

* chore: 🤖 change drilldown translation keys
This commit is contained in:
Vadim Dalecky 2020-02-25 18:02:25 +01:00 committed by GitHub
parent bd40f557a5
commit 120f03dc06
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 528 additions and 86 deletions

View file

@ -10,11 +10,11 @@ import { CoreStart } from 'src/core/public';
import { Action } from '../../../../../../src/plugins/ui_actions/public';
import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public';
import { IEmbeddable } from '../../../../../../src/plugins/embeddable/public';
import { FormCreateDrilldown } from '../../components/form_create_drilldown';
import { FlyoutCreateDrilldown } from '../../components/flyout_create_drilldown';
export const OPEN_FLYOUT_ADD_DRILLDOWN = 'OPEN_FLYOUT_ADD_DRILLDOWN';
interface ActionContext {
export interface FlyoutCreateDrilldownActionContext {
embeddable: IEmbeddable;
}
@ -22,29 +22,31 @@ export interface OpenFlyoutAddDrilldownParams {
overlays: () => Promise<CoreStart['overlays']>;
}
export class OpenFlyoutAddDrilldown implements Action<ActionContext> {
export class FlyoutCreateDrilldownAction implements Action<FlyoutCreateDrilldownActionContext> {
public readonly type = OPEN_FLYOUT_ADD_DRILLDOWN;
public readonly id = OPEN_FLYOUT_ADD_DRILLDOWN;
public order = 100;
public order = 5;
constructor(protected readonly params: OpenFlyoutAddDrilldownParams) {}
public getDisplayName() {
return i18n.translate('xpack.drilldowns.panel.openFlyoutAddDrilldown.displayName', {
defaultMessage: 'Add drilldown',
return i18n.translate('xpack.drilldowns.FlyoutCreateDrilldownAction.displayName', {
defaultMessage: 'Create Drilldown',
});
}
public getIconType() {
return 'empty';
return 'plusInCircle';
}
public async isCompatible({ embeddable }: ActionContext) {
public async isCompatible({ embeddable }: FlyoutCreateDrilldownActionContext) {
return true;
}
public async execute({ embeddable }: ActionContext) {
public async execute(context: FlyoutCreateDrilldownActionContext) {
const overlays = await this.params.overlays();
overlays.openFlyout(toMountPoint(<FormCreateDrilldown />));
const handle = overlays.openFlyout(
toMountPoint(<FlyoutCreateDrilldown context={context} onClose={() => handle.close()} />)
);
}
}

View file

@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
export * from './open_flyout_add_drilldown';
export * from './flyout_create_drilldown';

View file

@ -6,7 +6,7 @@
import * as React from 'react';
import { storiesOf } from '@storybook/react';
import { DrilldownHelloBar } from '..';
import { DrilldownHelloBar } from '.';
storiesOf('components/DrilldownHelloBar', module).add('default', () => {
return <DrilldownHelloBar />;

View file

@ -0,0 +1,26 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
export interface DrilldownHelloBarProps {
docsLink?: string;
}
/**
* @todo https://github.com/elastic/kibana/issues/55311
*/
export const DrilldownHelloBar: React.FC<DrilldownHelloBarProps> = ({ docsLink }) => {
return (
<div>
<p>
Drilldowns provide the ability to define a new behavior when interacting with a panel. You
can add multiple options or simply override the default filtering behavior.
</p>
<a href={docsLink}>View docs</a>
</div>
);
};

View file

@ -4,24 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
export interface DrilldownHelloBarProps {
docsLink?: string;
}
export const DrilldownHelloBar: React.FC<DrilldownHelloBarProps> = ({ docsLink }) => {
return (
<div>
<p>
Drilldowns provide the ability to define a new behavior when interacting with a panel. You
can add multiple options or simply override the default filtering behavior.
</p>
<a href={docsLink}>View docs</a>
<img
src="https://user-images.githubusercontent.com/9773803/72729009-e5803180-3b8e-11ea-8330-b86089bf5f0a.png"
alt=""
/>
</div>
);
};
export * from './drilldown_hello_bar';

View file

@ -6,7 +6,7 @@
import * as React from 'react';
import { storiesOf } from '@storybook/react';
import { DrilldownPicker } from '..';
import { DrilldownPicker } from '.';
storiesOf('components/DrilldownPicker', module).add('default', () => {
return <DrilldownPicker />;

View file

@ -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;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
// eslint-disable-next-line
export interface DrilldownPickerProps {}
export const DrilldownPicker: React.FC<DrilldownPickerProps> = () => {
return (
<img
src={
'https://user-images.githubusercontent.com/9773803/72725665-9e8e3e00-3b86-11ea-9314-8724c521b41f.png'
}
alt=""
/>
);
};

View file

@ -4,18 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
// eslint-disable-next-line
export interface DrilldownPickerProps {}
export const DrilldownPicker: React.FC<DrilldownPickerProps> = () => {
return (
<img
src={
'https://user-images.githubusercontent.com/9773803/72725665-9e8e3e00-3b86-11ea-9314-8724c521b41f.png'
}
alt=""
/>
);
};
export * from './drilldown_picker';

View file

@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
/* eslint-disable no-console */
import * as React from 'react';
import { EuiFlyout } from '@elastic/eui';
import { storiesOf } from '@storybook/react';
import { FlyoutCreateDrilldown } from '.';
storiesOf('components/FlyoutCreateDrilldown', module)
.add('default', () => {
return <FlyoutCreateDrilldown context={{} as any} />;
})
.add('open in flyout', () => {
return (
<EuiFlyout>
<FlyoutCreateDrilldown context={{} as any} />
</EuiFlyout>
);
});

View file

@ -0,0 +1,34 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { EuiButton } from '@elastic/eui';
import { FormCreateDrilldown } from '../form_create_drilldown';
import { FlyoutFrame } from '../flyout_frame';
import { txtCreateDrilldown } from './i18n';
import { FlyoutCreateDrilldownActionContext } from '../../actions';
export interface FlyoutCreateDrilldownProps {
context: FlyoutCreateDrilldownActionContext;
onClose?: () => void;
}
export const FlyoutCreateDrilldown: React.FC<FlyoutCreateDrilldownProps> = ({
context,
onClose,
}) => {
const footer = (
<EuiButton onClick={() => {}} fill>
{txtCreateDrilldown}
</EuiButton>
);
return (
<FlyoutFrame title={txtCreateDrilldown} footer={footer} onClose={onClose}>
<FormCreateDrilldown />
</FlyoutFrame>
);
};

View file

@ -0,0 +1,14 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
export const txtCreateDrilldown = i18n.translate(
'xpack.drilldowns.components.FlyoutCreateDrilldown.CreateDrilldown',
{
defaultMessage: 'Create drilldown',
}
);

View file

@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export * from './flyout_create_drilldown';

View file

@ -0,0 +1,39 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
/* eslint-disable no-console */
import * as React from 'react';
import { EuiFlyout, EuiButton } from '@elastic/eui';
import { storiesOf } from '@storybook/react';
import { FlyoutFrame } from '.';
storiesOf('components/FlyoutFrame', module)
.add('default', () => {
return <FlyoutFrame>test</FlyoutFrame>;
})
.add('with title', () => {
return <FlyoutFrame title="Hello world">test</FlyoutFrame>;
})
.add('with onClose', () => {
return <FlyoutFrame onClose={() => console.log('onClose')}>test</FlyoutFrame>;
})
.add('custom footer', () => {
return <FlyoutFrame footer={<button>click me!</button>}>test</FlyoutFrame>;
})
.add('open in flyout', () => {
return (
<EuiFlyout>
<FlyoutFrame
title="Create drilldown"
footer={<EuiButton>Save</EuiButton>}
onClose={() => console.log('onClose')}
>
test
</FlyoutFrame>
</EuiFlyout>
);
});

View file

@ -0,0 +1,102 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { render } from 'react-dom';
import { render as renderTestingLibrary, fireEvent } from '@testing-library/react';
import { FlyoutFrame } from '.';
describe('<FlyoutFrame>', () => {
test('renders without crashing', () => {
const div = document.createElement('div');
render(<FlyoutFrame />, div);
});
describe('[title=]', () => {
test('renders title in <h1> tag', () => {
const div = document.createElement('div');
render(<FlyoutFrame title={'foobar'} />, div);
const title = div.querySelector('h1');
expect(title?.textContent).toBe('foobar');
});
test('title can be any react node', () => {
const div = document.createElement('div');
render(
<FlyoutFrame
title={
<>
foo <strong>bar</strong>
</>
}
/>,
div
);
const title = div.querySelector('h1');
expect(title?.innerHTML).toBe('foo <strong>bar</strong>');
});
});
describe('[footer=]', () => {
test('if [footer=] prop not provided, does not render footer', () => {
const div = document.createElement('div');
render(<FlyoutFrame />, div);
const footer = div.querySelector('[data-test-subj="flyoutFooter"]');
expect(footer).toBe(null);
});
test('can render anything in footer', () => {
const div = document.createElement('div');
render(
<FlyoutFrame
footer={
<>
a <em>b</em>
</>
}
/>,
div
);
const footer = div.querySelector('[data-test-subj="flyoutFooter"]');
expect(footer?.innerHTML).toBe('a <em>b</em>');
});
});
describe('[onClose=]', () => {
test('does not render close button if "onClose" prop is missing', () => {
const div = document.createElement('div');
render(<FlyoutFrame />, div);
const closeButton = div.querySelector('[data-test-subj="flyoutCloseButton"]');
expect(closeButton).toBe(null);
});
test('renders close button if "onClose" prop is provided', () => {
const div = document.createElement('div');
render(<FlyoutFrame onClose={() => {}} />, div);
const closeButton = div.querySelector('[data-test-subj="flyoutCloseButton"]');
expect(closeButton).not.toBe(null);
});
test('calls onClose prop when close button clicked', async () => {
const onClose = jest.fn();
const el = renderTestingLibrary(<FlyoutFrame onClose={onClose} />);
const closeButton = el.queryByText('Close');
expect(onClose).toHaveBeenCalledTimes(0);
fireEvent.click(closeButton!);
expect(onClose).toHaveBeenCalledTimes(1);
});
});
});

View file

@ -0,0 +1,71 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import {
EuiFlyoutHeader,
EuiTitle,
EuiFlyoutBody,
EuiFlyoutFooter,
EuiFlexGroup,
EuiFlexItem,
EuiButtonEmpty,
} from '@elastic/eui';
import { txtClose } from './i18n';
export interface FlyoutFrameProps {
title?: React.ReactNode;
footer?: React.ReactNode;
onClose?: () => void;
}
/**
* @todo This component can be moved to `kibana_react`.
*/
export const FlyoutFrame: React.FC<FlyoutFrameProps> = ({
title = '',
footer,
onClose,
children,
}) => {
const headerFragment = title && (
<EuiFlyoutHeader hasBorder>
<EuiTitle size="s">
<h1>{title}</h1>
</EuiTitle>
</EuiFlyoutHeader>
);
const footerFragment = (onClose || footer) && (
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
{onClose && (
<EuiButtonEmpty
iconType="cross"
onClick={onClose}
flush="left"
data-test-subj="flyoutCloseButton"
>
{txtClose}
</EuiButtonEmpty>
)}
</EuiFlexItem>
<EuiFlexItem grow={false} data-test-subj="flyoutFooter">
{footer}
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
);
return (
<>
{headerFragment}
<EuiFlyoutBody>{children}</EuiFlyoutBody>
{footerFragment}
</>
);
};

View file

@ -4,10 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import * as React from 'react';
import { storiesOf } from '@storybook/react';
import { FormCreateDrilldown } from '..';
import { i18n } from '@kbn/i18n';
storiesOf('components/FormCreateDrilldown', module).add('default', () => {
return <FormCreateDrilldown />;
export const txtClose = i18n.translate('xpack.drilldowns.components.FlyoutFrame.Close', {
defaultMessage: 'Close',
});

View file

@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export * from './flyout_frame';

View file

@ -0,0 +1,34 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
/* eslint-disable no-console */
import * as React from 'react';
import { EuiFlyout } from '@elastic/eui';
import { storiesOf } from '@storybook/react';
import { FormCreateDrilldown } from '.';
const DemoEditName: React.FC = () => {
const [name, setName] = React.useState('');
return <FormCreateDrilldown name={name} onNameChange={setName} />;
};
storiesOf('components/FormCreateDrilldown', module)
.add('default', () => {
return <FormCreateDrilldown />;
})
.add('[name=foobar]', () => {
return <FormCreateDrilldown name={'foobar'} />;
})
.add('can edit name', () => <DemoEditName />)
.add('open in flyout', () => {
return (
<EuiFlyout>
<FormCreateDrilldown />
</EuiFlyout>
);
});

View file

@ -0,0 +1,68 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { render } from 'react-dom';
import { FormCreateDrilldown } from '.';
import { render as renderTestingLibrary, fireEvent } from '@testing-library/react';
import { txtNameOfDrilldown } from './i18n';
describe('<FormCreateDrilldown>', () => {
test('renders without crashing', () => {
const div = document.createElement('div');
render(<FormCreateDrilldown name={''} onNameChange={() => {}} />, div);
});
describe('[name=]', () => {
test('if name not provided, uses to empty string', () => {
const div = document.createElement('div');
render(<FormCreateDrilldown />, div);
const input = div.querySelector(
'[data-test-subj="dynamicActionNameInput"]'
) as HTMLInputElement;
expect(input?.value).toBe('');
});
test('can set name input field value', () => {
const div = document.createElement('div');
render(<FormCreateDrilldown name={'foo'} />, div);
const input = div.querySelector(
'[data-test-subj="dynamicActionNameInput"]'
) as HTMLInputElement;
expect(input?.value).toBe('foo');
render(<FormCreateDrilldown name={'bar'} />, div);
expect(input?.value).toBe('bar');
});
test('fires onNameChange callback on name change', () => {
const onNameChange = jest.fn();
const utils = renderTestingLibrary(
<FormCreateDrilldown name={''} onNameChange={onNameChange} />
);
const input = utils.getByLabelText(txtNameOfDrilldown);
expect(onNameChange).toHaveBeenCalledTimes(0);
fireEvent.change(input, { target: { value: 'qux' } });
expect(onNameChange).toHaveBeenCalledTimes(1);
expect(onNameChange).toHaveBeenCalledWith('qux');
fireEvent.change(input, { target: { value: 'quxx' } });
expect(onNameChange).toHaveBeenCalledTimes(2);
expect(onNameChange).toHaveBeenCalledWith('quxx');
});
});
});

View file

@ -0,0 +1,52 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { EuiForm, EuiFormRow, EuiFieldText } from '@elastic/eui';
import { DrilldownHelloBar } from '../drilldown_hello_bar';
import { txtNameOfDrilldown, txtUntitledDrilldown, txtDrilldownAction } from './i18n';
import { DrilldownPicker } from '../drilldown_picker';
const noop = () => {};
export interface FormCreateDrilldownProps {
name?: string;
onNameChange?: (name: string) => void;
}
export const FormCreateDrilldown: React.FC<FormCreateDrilldownProps> = ({
name = '',
onNameChange = noop,
}) => {
const nameFragment = (
<EuiFormRow label={txtNameOfDrilldown}>
<EuiFieldText
name="drilldown_name"
placeholder={txtUntitledDrilldown}
value={name}
disabled={onNameChange === noop}
onChange={event => onNameChange(event.target.value)}
data-test-subj="dynamicActionNameInput"
/>
</EuiFormRow>
);
const triggerPicker = <div>Trigger Picker will be here</div>;
const actionPicker = (
<EuiFormRow label={txtDrilldownAction}>
<DrilldownPicker />
</EuiFormRow>
);
return (
<>
<DrilldownHelloBar />
<EuiForm>{nameFragment}</EuiForm>
{triggerPicker}
{actionPicker}
</>
);
};

View file

@ -7,21 +7,21 @@
import { i18n } from '@kbn/i18n';
export const txtNameOfDrilldown = i18n.translate(
'xpack.drilldowns.components.form_create_drilldown.nameOfDrilldown',
'xpack.drilldowns.components.FormCreateDrilldown.nameOfDrilldown',
{
defaultMessage: 'Name of drilldown',
}
);
export const txtUntitledDrilldown = i18n.translate(
'xpack.drilldowns.components.form_create_drilldown.untitledDrilldown',
'xpack.drilldowns.components.FormCreateDrilldown.untitledDrilldown',
{
defaultMessage: 'Untitled drilldown',
}
);
export const txtDrilldownAction = i18n.translate(
'xpack.drilldowns.components.form_create_drilldown.drilldownAction',
'xpack.drilldowns.components.FormCreateDrilldown.drilldownAction',
{
defaultMessage: 'Drilldown action',
}

View file

@ -4,27 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { EuiForm, EuiFormRow, EuiFieldText } from '@elastic/eui';
import { DrilldownHelloBar } from '../drilldown_hello_bar';
import { txtNameOfDrilldown, txtUntitledDrilldown, txtDrilldownAction } from './i18n';
import { DrilldownPicker } from '../drilldown_picker';
// eslint-disable-next-line
export interface FormCreateDrilldownProps {}
export const FormCreateDrilldown: React.FC<FormCreateDrilldownProps> = () => {
return (
<div>
<DrilldownHelloBar />
<EuiForm>
<EuiFormRow label={txtNameOfDrilldown}>
<EuiFieldText name="drilldown_name" placeholder={txtUntitledDrilldown} />
</EuiFormRow>
<EuiFormRow label={txtDrilldownAction}>
<DrilldownPicker />
</EuiFormRow>
</EuiForm>
</div>
);
};
export * from './form_create_drilldown';

View file

@ -5,17 +5,17 @@
*/
import { CoreSetup } from 'src/core/public';
import { OpenFlyoutAddDrilldown } from '../actions/open_flyout_add_drilldown';
import { FlyoutCreateDrilldownAction } from '../actions';
import { DrilldownsSetupDependencies } from '../plugin';
export class DrilldownService {
bootstrap(core: CoreSetup, { uiActions }: DrilldownsSetupDependencies) {
const actionOpenFlyoutAddDrilldown = new OpenFlyoutAddDrilldown({
const actionFlyoutCreateDrilldown = new FlyoutCreateDrilldownAction({
overlays: async () => (await core.getStartServices())[0].overlays,
});
uiActions.registerAction(actionOpenFlyoutAddDrilldown);
uiActions.attachAction('CONTEXT_MENU_TRIGGER', actionOpenFlyoutAddDrilldown.id);
uiActions.registerAction(actionFlyoutCreateDrilldown);
uiActions.attachAction('CONTEXT_MENU_TRIGGER', actionFlyoutCreateDrilldown.id);
}
/**

View file

@ -9,5 +9,5 @@ import { join } from 'path';
// eslint-disable-next-line
require('@kbn/storybook').runStorybookCli({
name: 'drilldowns',
storyGlobs: [join(__dirname, '..', 'public', 'components', '**', '*.examples.tsx')],
storyGlobs: [join(__dirname, '..', 'public', 'components', '**', '*.story.tsx')],
});