Drilldown plugin (#58097)

* feat: 🎸 add <PanelOptionsMenu> component

* feat: 🎸 use presentational <PanelOptionsMenu> component

* feat: 🎸 create stubs for Drilldown components

* feat: 🎸 open new drilldown flyout from panel's context menu

* feat: 🎸 setup Drilldowns plugin in X-Pack

* feat: 🎸 add Storybook to drilldowns plugin

* refactor: 💡 move drilldown components to x-pack

* feat: 🎸 add stub action to open drilldown flyout

* feat: 🎸 add drilldowns plugin to translation index

* fix: 🐛 correct TypeScript type check

* fix: 🐛 use correct i18n namespace

* ci: 🎡 add drilldowns plugin to CODEOWNERS file

* fix: 🐛 revert back <PanelOptionsMenu> change

* fix: 🐛 type must not be empty
This commit is contained in:
Vadim Dalecky 2020-02-20 21:59:29 +01:00 committed by GitHub
parent 970bb75b84
commit 4cd809aa7d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 483 additions and 34 deletions

1
.github/CODEOWNERS vendored
View file

@ -54,6 +54,7 @@
/src/plugins/ui_actions/ @elastic/kibana-app-arch
/src/plugins/visualizations/ @elastic/kibana-app-arch
/x-pack/plugins/advanced_ui_actions/ @elastic/kibana-app-arch
/x-pack/plugins/drilldowns/ @elastic/kibana-app-arch
# APM
/x-pack/legacy/plugins/apm/ @elastic/apm-ui

View file

@ -20,6 +20,7 @@
export const storybookAliases = {
apm: 'x-pack/legacy/plugins/apm/scripts/storybook.js',
canvas: 'x-pack/legacy/plugins/canvas/scripts/storybook_new.js',
drilldowns: 'x-pack/plugins/drilldowns/scripts/storybook.js',
embeddable: 'src/plugins/embeddable/scripts/storybook.js',
infra: 'x-pack/legacy/plugins/infra/scripts/storybook.js',
siem: 'x-pack/legacy/plugins/siem/scripts/storybook.js',

View file

@ -1,29 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { EuiPanel } from '@elastic/eui';
import * as React from 'react';
export const EmbeddablePanel = () => {
return (
<EuiPanel data-test-subj="embeddablePanel" paddingSize="none" role="figure">
Hello world
</EuiPanel>
);
};

View file

@ -19,6 +19,35 @@
import * as React from 'react';
import { storiesOf } from '@storybook/react';
import { EmbeddablePanel } from '..';
import { action } from '@storybook/addon-actions';
import { withKnobs, boolean } from '@storybook/addon-knobs';
import { PanelOptionsMenu } from '..';
storiesOf('components/EmbeddablePanel', module).add('default', () => <EmbeddablePanel />);
const euiContextDescriptors = {
id: 'mainMenu',
title: 'Options',
items: [
{
name: 'Inspect',
icon: 'inspect',
onClick: action('onClick(inspect)'),
},
{
name: 'Full screen',
icon: 'expand',
onClick: action('onClick(expand)'),
},
],
};
storiesOf('components/PanelOptionsMenu', module)
.addDecorator(withKnobs)
.add('default', () => {
const isViewMode = boolean('isViewMode', false);
return (
<div style={{ height: 150 }}>
<PanelOptionsMenu panelDescriptor={euiContextDescriptors} isViewMode={isViewMode} />
</div>
);
});

View file

@ -0,0 +1,93 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { i18n } from '@kbn/i18n';
import React, { useState, useEffect } from 'react';
import {
EuiButtonIcon,
EuiContextMenu,
EuiContextMenuPanelDescriptor,
EuiPopover,
} from '@elastic/eui';
export interface PanelOptionsMenuProps {
panelDescriptor?: EuiContextMenuPanelDescriptor;
close?: boolean;
isViewMode?: boolean;
title?: string;
}
export const PanelOptionsMenu: React.FC<PanelOptionsMenuProps> = ({
panelDescriptor,
close,
isViewMode,
title,
}) => {
const [open, setOpen] = useState(false);
useEffect(() => {
if (!close) setOpen(false);
}, [close]);
const handleContextMenuClick = () => {
setOpen(isOpen => !isOpen);
};
const handlePopoverClose = () => {
setOpen(false);
};
const enhancedAriaLabel = i18n.translate(
'embeddableApi.panel.optionsMenu.panelOptionsButtonEnhancedAriaLabel',
{
defaultMessage: 'Panel options for {title}',
values: { title },
}
);
const ariaLabelWithoutTitle = i18n.translate(
'embeddableApi.panel.optionsMenu.panelOptionsButtonAriaLabel',
{
defaultMessage: 'Panel options',
}
);
const button = (
<EuiButtonIcon
iconType={isViewMode ? 'boxesHorizontal' : 'gear'}
color="text"
className="embPanel__optionsMenuButton"
aria-label={title ? enhancedAriaLabel : ariaLabelWithoutTitle}
data-test-subj="embeddablePanelToggleMenuIcon"
onClick={handleContextMenuClick}
/>
);
return (
<EuiPopover
button={button}
isOpen={open}
closePopover={handlePopoverClose}
panelPaddingSize="none"
anchorPosition="downRight"
data-test-subj={open ? 'embeddablePanelContextMenuOpen' : 'embeddablePanelContextMenuClosed'}
withTitle
>
<EuiContextMenu initialPanelId="mainMenu" panels={panelDescriptor ? [panelDescriptor] : []} />
</EuiPopover>
);
};

View file

@ -100,7 +100,6 @@
}
}
.embPanel__optionsMenuPopover[class*='-isOpen'],
.embPanel:hover {
.embPanel__optionsMenuButton {
opacity: 1;

View file

@ -4,12 +4,13 @@
"xpack.actions": "plugins/actions",
"xpack.advancedUiActions": "plugins/advanced_ui_actions",
"xpack.alerting": "plugins/alerting",
"xpack.triggersActionsUI": "plugins/triggers_actions_ui",
"xpack.apm": ["legacy/plugins/apm", "plugins/apm"],
"xpack.beatsManagement": "legacy/plugins/beats_management",
"xpack.canvas": "legacy/plugins/canvas",
"xpack.crossClusterReplication": "legacy/plugins/cross_cluster_replication",
"xpack.dashboardMode": "legacy/plugins/dashboard_mode",
"xpack.data": "plugins/data_enhanced",
"xpack.drilldowns": "plugins/drilldowns",
"xpack.endpoint": "plugins/endpoint",
"xpack.features": "plugins/features",
"xpack.fileUpload": "legacy/plugins/file_upload",
@ -19,7 +20,6 @@
"xpack.indexLifecycleMgmt": "legacy/plugins/index_lifecycle_management",
"xpack.infra": "plugins/infra",
"xpack.ingestManager": "plugins/ingest_manager",
"xpack.data": "plugins/data_enhanced",
"xpack.lens": "legacy/plugins/lens",
"xpack.licenseMgmt": "legacy/plugins/license_management",
"xpack.licensing": "plugins/licensing",
@ -39,6 +39,7 @@
"xpack.spaces": ["legacy/plugins/spaces", "plugins/spaces"],
"xpack.taskManager": "legacy/plugins/task_manager",
"xpack.transform": "legacy/plugins/transform",
"xpack.triggersActionsUI": "plugins/triggers_actions_ui",
"xpack.upgradeAssistant": "legacy/plugins/upgrade_assistant",
"xpack.uptime": "legacy/plugins/uptime",
"xpack.watcher": "plugins/watcher"

View file

@ -0,0 +1,3 @@
# Drilldowns
Provides functionality to navigate between Kibana apps with context information.

View file

@ -0,0 +1,10 @@
{
"id": "drilldowns",
"version": "kibana",
"server": false,
"ui": true,
"requiredPlugins": [
"uiActions",
"embeddable"
]
}

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 './open_flyout_add_drilldown';

View file

@ -0,0 +1,50 @@
/*
* 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 { i18n } from '@kbn/i18n';
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';
export const OPEN_FLYOUT_ADD_DRILLDOWN = 'OPEN_FLYOUT_ADD_DRILLDOWN';
interface ActionContext {
embeddable: IEmbeddable;
}
export interface OpenFlyoutAddDrilldownParams {
overlays: () => Promise<CoreStart['overlays']>;
}
export class OpenFlyoutAddDrilldown implements Action<ActionContext> {
public readonly type = OPEN_FLYOUT_ADD_DRILLDOWN;
public readonly id = OPEN_FLYOUT_ADD_DRILLDOWN;
public order = 100;
constructor(protected readonly params: OpenFlyoutAddDrilldownParams) {}
public getDisplayName() {
return i18n.translate('xpack.drilldowns.panel.openFlyoutAddDrilldown.displayName', {
defaultMessage: 'Add drilldown',
});
}
public getIconType() {
return 'empty';
}
public async isCompatible({ embeddable }: ActionContext) {
return true;
}
public async execute({ embeddable }: ActionContext) {
const overlays = await this.params.overlays();
overlays.openFlyout(toMountPoint(<FormCreateDrilldown />));
}
}

View file

@ -0,0 +1,13 @@
/*
* 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 * as React from 'react';
import { storiesOf } from '@storybook/react';
import { DrilldownHelloBar } from '..';
storiesOf('components/DrilldownHelloBar', module).add('default', () => {
return <DrilldownHelloBar />;
});

View file

@ -0,0 +1,27 @@
/*
* 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;
}
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>
);
};

View file

@ -0,0 +1,13 @@
/*
* 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 * as React from 'react';
import { storiesOf } from '@storybook/react';
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

@ -0,0 +1,13 @@
/*
* 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 * as React from 'react';
import { storiesOf } from '@storybook/react';
import { FormCreateDrilldown } from '..';
storiesOf('components/FormCreateDrilldown', module).add('default', () => {
return <FormCreateDrilldown />;
});

View file

@ -0,0 +1,28 @@
/*
* 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 txtNameOfDrilldown = i18n.translate(
'xpack.drilldowns.components.form_create_drilldown.nameOfDrilldown',
{
defaultMessage: 'Name of drilldown',
}
);
export const txtUntitledDrilldown = i18n.translate(
'xpack.drilldowns.components.form_create_drilldown.untitledDrilldown',
{
defaultMessage: 'Untitled drilldown',
}
);
export const txtDrilldownAction = i18n.translate(
'xpack.drilldowns.components.form_create_drilldown.drilldownAction',
{
defaultMessage: 'Drilldown action',
}
);

View file

@ -0,0 +1,30 @@
/*
* 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';
// 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>
);
};

View file

@ -0,0 +1,18 @@
/*
* 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 { DrilldownsPlugin } from './plugin';
export {
DrilldownsSetupContract,
DrilldownsSetupDependencies,
DrilldownsStartContract,
DrilldownsStartDependencies,
} from './plugin';
export function plugin() {
return new DrilldownsPlugin();
}

View file

@ -0,0 +1,28 @@
/*
* 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 { DrilldownsSetupContract, DrilldownsStartContract } from '.';
export type Setup = jest.Mocked<DrilldownsSetupContract>;
export type Start = jest.Mocked<DrilldownsStartContract>;
const createSetupContract = (): Setup => {
const setupContract: Setup = {
registerDrilldown: jest.fn(),
};
return setupContract;
};
const createStartContract = (): Start => {
const startContract: Start = {};
return startContract;
};
export const bfetchPluginMock = {
createSetupContract,
createStartContract,
};

View file

@ -0,0 +1,45 @@
/*
* 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 { CoreStart, CoreSetup, Plugin } from 'src/core/public';
import { UiActionsSetup, UiActionsStart } from '../../../../src/plugins/ui_actions/public';
import { DrilldownService } from './service';
export interface DrilldownsSetupDependencies {
uiActions: UiActionsSetup;
}
export interface DrilldownsStartDependencies {
uiActions: UiActionsStart;
}
export type DrilldownsSetupContract = Pick<DrilldownService, 'registerDrilldown'>;
// eslint-disable-next-line
export interface DrilldownsStartContract {}
export class DrilldownsPlugin
implements
Plugin<
DrilldownsSetupContract,
DrilldownsStartContract,
DrilldownsSetupDependencies,
DrilldownsStartDependencies
> {
private readonly service = new DrilldownService();
public setup(core: CoreSetup, plugins: DrilldownsSetupDependencies): DrilldownsSetupContract {
this.service.bootstrap(core, plugins);
return this.service;
}
public start(core: CoreStart, plugins: DrilldownsStartDependencies): DrilldownsStartContract {
return {};
}
public stop() {}
}

View file

@ -0,0 +1,28 @@
/*
* 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 { CoreSetup } from 'src/core/public';
import { OpenFlyoutAddDrilldown } from '../actions/open_flyout_add_drilldown';
import { DrilldownsSetupDependencies } from '../plugin';
export class DrilldownService {
bootstrap(core: CoreSetup, { uiActions }: DrilldownsSetupDependencies) {
const actionOpenFlyoutAddDrilldown = new OpenFlyoutAddDrilldown({
overlays: async () => (await core.getStartServices())[0].overlays,
});
uiActions.registerAction(actionOpenFlyoutAddDrilldown);
uiActions.attachAction('CONTEXT_MENU_TRIGGER', actionOpenFlyoutAddDrilldown.id);
}
/**
* Convenience method to register a drilldown. (It should set-up all the
* necessary triggers and actions.)
*/
registerDrilldown = (): void => {
throw new Error('not implemented');
};
}

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 './drilldown_service';

View file

@ -0,0 +1,13 @@
/*
* 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 { join } from 'path';
// eslint-disable-next-line
require('@kbn/storybook').runStorybookCli({
name: 'drilldowns',
storyGlobs: [join(__dirname, '..', 'public', 'components', '**', '*.examples.tsx')],
});