Add pluggable panel action tests (#20163)

* Add pluggable panel action tests

* address code review comments

* update inspector snapshot

* remove temp declared ts module now that eui has EuiFlyout typings

* address code comments
This commit is contained in:
Stacey Gammon 2018-07-28 10:41:57 -04:00 committed by GitHub
parent 0078e66f16
commit bed72f224d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 611 additions and 400 deletions

View file

@ -21,4 +21,5 @@ require('../src/setup_node_env');
require('../packages/kbn-test').runTestsCli([ require('../packages/kbn-test').runTestsCli([
require.resolve('../test/functional/config.js'), require.resolve('../test/functional/config.js'),
require.resolve('../test/api_integration/config.js'), require.resolve('../test/api_integration/config.js'),
require.resolve('../test/panel_actions/config.js'),
]); ]);

View file

@ -0,0 +1,115 @@
/*
* 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 React from 'react';
import { EuiFlyout } from '@elastic/eui';
import { EventEmitter } from 'events';
import ReactDOM from 'react-dom';
let activeSession: FlyoutSession | null = null;
const CONTAINER_ID = 'flyout-container';
function getOrCreateContainerElement() {
let container = document.getElementById(CONTAINER_ID);
if (!container) {
container = document.createElement('div');
container.id = CONTAINER_ID;
document.body.appendChild(container);
}
return container;
}
/**
* A FlyoutSession describes the session of one opened flyout panel. It offers
* methods to close the flyout panel again. If you open a flyout panel you should make
* sure you call {@link FlyoutSession#close} when it should be closed.
* Since a flyout could also be closed without calling this method (e.g. because
* the user closes it), you must listen to the "closed" event on this instance.
* It will be emitted whenever the flyout will be closed and you should throw
* away your reference to this instance whenever you receive that event.
* @extends EventEmitter
*/
class FlyoutSession extends EventEmitter {
/**
* Binds the current flyout session to an Angular scope, meaning this flyout
* session will be closed as soon as the Angular scope gets destroyed.
* @param {object} scope - An angular scope object to bind to.
*/
public bindToAngularScope(scope: ng.IScope): void {
const removeWatch = scope.$on('$destroy', () => this.close());
this.on('closed', () => removeWatch());
}
/**
* Closes the opened flyout as long as it's still the open one.
* If this is not the active session anymore, this method won't do anything.
* If this session was still active and a flyout was closed, the 'closed'
* event will be emitted on this FlyoutSession instance.
*/
public close(): void {
if (activeSession === this) {
const container = document.getElementById(CONTAINER_ID);
if (container) {
ReactDOM.unmountComponentAtNode(container);
this.emit('closed');
}
}
}
}
/**
* Opens a flyout panel with the given component inside. You can use
* {@link FlyoutSession#close} on the return value to close the flyout.
*
* @param flyoutChildren - Mounts the children inside a fly out panel
* @return {FlyoutSession} The session instance for the opened flyout panel.
*/
export function openFlyout(
flyoutChildren: React.ReactNode,
flyoutProps: {
onClose?: () => void;
'data-test-subj'?: string;
} = {}
): FlyoutSession {
// If there is an active inspector session close it before opening a new one.
if (activeSession) {
activeSession.close();
}
const container = getOrCreateContainerElement();
const session = (activeSession = new FlyoutSession());
const onClose = () => {
if (flyoutProps.onClose) {
flyoutProps.onClose();
}
session.close();
};
ReactDOM.render(
<EuiFlyout {...flyoutProps} onClose={onClose}>
{flyoutChildren}
</EuiFlyout>,
container
);
return session;
}
export { FlyoutSession };

View file

@ -0,0 +1,20 @@
/*
* 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.
*/
export * from './flyout_session';

View file

@ -16,67 +16,13 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import { EventEmitter } from 'events';
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom';
import { FlyoutSession, openFlyout } from 'ui/flyout';
import { Adapters } from './types'; import { Adapters } from './types';
import { InspectorPanel } from './ui/inspector_panel'; import { InspectorPanel } from './ui/inspector_panel';
import { viewRegistry } from './view_registry'; import { viewRegistry } from './view_registry';
let activeSession: InspectorSession | null = null;
const CONTAINER_ID = 'inspector-container';
function getOrCreateContainerElement() {
let container = document.getElementById(CONTAINER_ID);
if (!container) {
container = document.createElement('div');
container.id = CONTAINER_ID;
document.body.appendChild(container);
}
return container;
}
/**
* An InspectorSession describes the session of one opened inspector. It offers
* methods to close the inspector again. If you open an inspector you should make
* sure you call {@link InspectorSession#close} when it should be closed.
* Since an inspector could also be closed without calling this method (e.g. because
* the user closes it), you must listen to the "closed" event on this instance.
* It will be emitted whenever the inspector will be closed and you should throw
* away your reference to this instance whenever you receive that event.
* @extends EventEmitter
*/
class InspectorSession extends EventEmitter {
/**
* Binds the current inspector session to an Angular scope, meaning this inspector
* session will be closed as soon as the Angular scope gets destroyed.
* @param {object} scope - And angular scope object to bind to.
*/
public bindToAngularScope(scope: ng.IScope): void {
const removeWatch = scope.$on('$destroy', () => this.close());
this.on('closed', () => removeWatch());
}
/**
* Closes the opened inspector as long as it's stil the open one.
* If this is not the active session anymore, this method won't do anything.
* If this session was still active and an inspector was closed, the 'closed'
* event will be emitted on this InspectorSession instance.
*/
public close(): void {
if (activeSession === this) {
const container = document.getElementById(CONTAINER_ID);
if (container) {
ReactDOM.unmountComponentAtNode(container);
this.emit('closed');
}
}
}
}
/** /**
* Checks if a inspector panel could be shown based on the passed adapters. * Checks if a inspector panel could be shown based on the passed adapters.
* *
@ -98,6 +44,8 @@ interface InspectorOptions {
title?: string; title?: string;
} }
export type InspectorSession = FlyoutSession;
/** /**
* Opens the inspector panel for the given adapters and close any previously opened * Opens the inspector panel for the given adapters and close any previously opened
* inspector panel. The previously panel will be closed also if no new panel will be * inspector panel. The previously panel will be closed also if no new panel will be
@ -110,11 +58,6 @@ interface InspectorOptions {
* @return {InspectorSession} The session instance for the opened inspector. * @return {InspectorSession} The session instance for the opened inspector.
*/ */
function open(adapters: Adapters, options: InspectorOptions = {}): InspectorSession { function open(adapters: Adapters, options: InspectorOptions = {}): InspectorSession {
// If there is an active inspector session close it before opening a new one.
if (activeSession) {
activeSession.close();
}
const views = viewRegistry.getVisible(adapters); const views = viewRegistry.getVisible(adapters);
// Don't open inspector if there are no views available for the passed adapters // Don't open inspector if there are no views available for the passed adapters
@ -124,20 +67,9 @@ function open(adapters: Adapters, options: InspectorOptions = {}): InspectorSess
if an inspector can be shown.`); if an inspector can be shown.`);
} }
const container = getOrCreateContainerElement(); return openFlyout(<InspectorPanel views={views} adapters={adapters} title={options.title} />, {
const session = (activeSession = new InspectorSession()); 'data-test-subj': 'inspectorPanel',
});
ReactDOM.render(
<InspectorPanel
views={views}
adapters={adapters}
onClose={() => session.close()}
title={options.title}
/>,
container
);
return session;
} }
const Inspector = { const Inspector = {

View file

@ -34,101 +34,6 @@ exports[`InspectorPanel should render as expected 1`] = `
] ]
} }
> >
<EuiFlyout
data-test-subj="inspectorPanel"
hideCloseButton={false}
onClose={[Function]}
ownFocus={false}
size="m"
>
<span>
<FocusTrap
_createFocusTrap={[Function]}
active={true}
focusTrapOptions={
Object {
"clickOutsideDeactivates": true,
"fallbackFocus": [Function],
}
}
paused={false}
tag="div"
>
<div>
<div
className="euiFlyout euiFlyout--medium"
data-test-subj="inspectorPanel"
onKeyDown={[Function]}
role="dialog"
tabIndex={0}
>
<EuiButtonIcon
aria-label="Closes this dialog"
className="euiFlyout__closeButton"
color="text"
data-test-subj="euiFlyoutCloseButton"
iconType="cross"
onClick={[Function]}
type="button"
>
<button
aria-label="Closes this dialog"
className="euiButtonIcon euiButtonIcon--text euiFlyout__closeButton"
data-test-subj="euiFlyoutCloseButton"
onClick={[Function]}
type="button"
>
<EuiIcon
aria-hidden="true"
className="euiButtonIcon__icon"
size="m"
type="cross"
>
<cross
aria-hidden="true"
className="euiIcon euiIcon--medium euiButtonIcon__icon"
focusable="false"
height="16"
style={
Object {
"fill": undefined,
}
}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
>
<svg
aria-hidden="true"
className="euiIcon euiIcon--medium euiButtonIcon__icon"
focusable="false"
height="16"
style={
Object {
"fill": undefined,
}
}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
>
<defs>
<path
d="M7.293 8l-4.147 4.146a.5.5 0 0 0 .708.708L8 8.707l4.146 4.147a.5.5 0 0 0 .708-.708L8.707 8l4.147-4.146a.5.5 0 0 0-.708-.708L8 7.293 3.854 3.146a.5.5 0 1 0-.708.708L7.293 8z"
id="cross-a"
/>
</defs>
<use
fillRule="nonzero"
xlinkHref="#cross-a"
/>
</svg>
</cross>
</EuiIcon>
</button>
</EuiButtonIcon>
<EuiFlyoutHeader <EuiFlyoutHeader
hasBorder={true} hasBorder={true}
> >
@ -337,10 +242,5 @@ exports[`InspectorPanel should render as expected 1`] = `
View 1 View 1
</h1> </h1>
</component> </component>
</div>
</div>
</FocusTrap>
</span>
</EuiFlyout>
</InspectorPanel> </InspectorPanel>
`; `;

View file

@ -22,7 +22,6 @@ import { Adapters, InspectorViewDescription } from '../types';
interface InspectorPanelProps { interface InspectorPanelProps {
adapters: Adapters; adapters: Adapters;
onClose: () => void;
title?: string; title?: string;
views: InspectorViewDescription[]; views: InspectorViewDescription[];
} }

View file

@ -22,7 +22,6 @@ import PropTypes from 'prop-types';
import { import {
EuiFlexGroup, EuiFlexGroup,
EuiFlexItem, EuiFlexItem,
EuiFlyout,
EuiFlyoutHeader, EuiFlyoutHeader,
EuiTitle, EuiTitle,
} from '@elastic/eui'; } from '@elastic/eui';
@ -79,14 +78,11 @@ class InspectorPanel extends Component {
} }
render() { render() {
const { views, onClose, title } = this.props; const { views, title } = this.props;
const { selectedView } = this.state; const { selectedView } = this.state;
return ( return (
<EuiFlyout <React.Fragment>
onClose={onClose}
data-test-subj="inspectorPanel"
>
<EuiFlyoutHeader hasBorder> <EuiFlyoutHeader hasBorder>
<EuiFlexGroup <EuiFlexGroup
justifyContent="spaceBetween" justifyContent="spaceBetween"
@ -107,7 +103,7 @@ class InspectorPanel extends Component {
</EuiFlexGroup> </EuiFlexGroup>
</EuiFlyoutHeader> </EuiFlyoutHeader>
{ this.renderSelectedPanel() } { this.renderSelectedPanel() }
</EuiFlyout> </React.Fragment>
); );
} }
} }
@ -125,7 +121,6 @@ InspectorPanel.propTypes = {
); );
} }
}, },
onClose: PropTypes.func.isRequired,
title: PropTypes.string, title: PropTypes.string,
}; };

View file

@ -153,6 +153,17 @@ module.exports = function (grunt) {
], ],
}, },
panelActionTests: {
cmd: process.execPath,
args: [
'scripts/functional_tests',
'--config', 'test/panel_actions/config.js',
'--esFrom', 'source',
'--bail',
'--debug',
],
},
functionalTests: { functionalTests: {
cmd: process.execPath, cmd: process.execPath,
args: [ args: [

View file

@ -31,16 +31,9 @@ module.exports = function (grunt) {
} }
); );
grunt.registerTask('test:server', [ grunt.registerTask('test:server', ['checkPlugins', 'run:mocha']);
'checkPlugins',
'run:mocha',
]);
grunt.registerTask('test:browser', [ grunt.registerTask('test:browser', ['checkPlugins', 'run:browserTestServer', 'karma:unit']);
'checkPlugins',
'run:browserTestServer',
'karma:unit',
]);
grunt.registerTask('test:browser-ci', () => { grunt.registerTask('test:browser-ci', () => {
const ciShardTasks = keys(grunt.config.get('karma')) const ciShardTasks = keys(grunt.config.get('karma'))
@ -49,13 +42,10 @@ module.exports = function (grunt) {
grunt.log.ok(`Running UI tests in ${ciShardTasks.length} shards`); grunt.log.ok(`Running UI tests in ${ciShardTasks.length} shards`);
grunt.task.run([ grunt.task.run(['run:browserTestServer', ...ciShardTasks]);
'run:browserTestServer',
...ciShardTasks
]);
}); });
grunt.registerTask('test:coverage', [ 'run:testCoverageServer', 'karma:coverage' ]); grunt.registerTask('test:coverage', ['run:testCoverageServer', 'karma:coverage']);
grunt.registerTask('test:quick', [ grunt.registerTask('test:quick', [
'checkPlugins', 'checkPlugins',
@ -65,26 +55,24 @@ module.exports = function (grunt) {
'test:jest_integration', 'test:jest_integration',
'test:projects', 'test:projects',
'test:browser', 'test:browser',
'run:apiIntegrationTests' 'run:apiIntegrationTests',
]); ]);
grunt.registerTask('test:dev', [ grunt.registerTask('test:dev', ['checkPlugins', 'run:devBrowserTestServer', 'karma:dev']);
'checkPlugins',
'run:devBrowserTestServer',
'karma:dev'
]);
grunt.registerTask('test', subTask => { grunt.registerTask('test', subTask => {
if (subTask) grunt.fail.fatal(`invalid task "test:${subTask}"`); if (subTask) grunt.fail.fatal(`invalid task "test:${subTask}"`);
grunt.task.run(_.compact([ grunt.task.run(
_.compact([
!grunt.option('quick') && 'run:eslint', !grunt.option('quick') && 'run:eslint',
!grunt.option('quick') && 'run:tslint', !grunt.option('quick') && 'run:tslint',
'run:checkFileCasing', 'run:checkFileCasing',
'licenses', 'licenses',
'test:quick', 'test:quick',
'verifyTranslations', 'verifyTranslations',
])); ])
);
}); });
grunt.registerTask('quick-test', ['test:quick']); // historical alias grunt.registerTask('quick-test', ['test:quick']); // historical alias
@ -98,7 +86,7 @@ module.exports = function (grunt) {
const serverCmd = { const serverCmd = {
cmd: 'yarn', cmd: 'yarn',
args: ['kbn', 'run', 'test', '--exclude', 'kibana', '--oss', '--skip-kibana-extra'], args: ['kbn', 'run', 'test', '--exclude', 'kibana', '--oss', '--skip-kibana-extra'],
opts: { stdio: 'inherit' } opts: { stdio: 'inherit' },
}; };
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {

View file

@ -0,0 +1,50 @@
/*
* 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 path from 'path';
export default async function ({ readConfigFile }) {
const functionalConfig = await readConfigFile(require.resolve('../functional/config'));
return {
testFiles: [
require.resolve('./index'),
],
services: functionalConfig.get('services'),
pageObjects: functionalConfig.get('pageObjects'),
servers: functionalConfig.get('servers'),
env: functionalConfig.get('env'),
esTestCluster: functionalConfig.get('esTestCluster'),
apps: functionalConfig.get('apps'),
esArchiver: {
directory: path.resolve(__dirname, '../es_archives')
},
screenshots: functionalConfig.get('screenshots'),
junit: {
reportName: 'Panel Actions Functional Tests',
},
kbnTestServer: {
...functionalConfig.get('kbnTestServer'),
serverArgs: [
...functionalConfig.get('kbnTestServer.serverArgs'),
`--plugin-path=${path.resolve(__dirname, './sample_panel_action')}`,
],
},
};
}

View file

@ -0,0 +1,50 @@
/*
* 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 path from 'path';
export const KIBANA_ARCHIVE_PATH = path.resolve(__dirname, '../functional/fixtures/es_archiver/dashboard/current/kibana');
export const DATA_ARCHIVE_PATH = path.resolve(__dirname, '../functional/fixtures/es_archiver/dashboard/current/data');
export default function ({ getService, getPageObjects, loadTestFile }) {
const remote = getService('remote');
const esArchiver = getService('esArchiver');
const PageObjects = getPageObjects(['dashboard']);
describe('pluggable panel actions', function () {
before(async () => {
await remote.setWindowSize(1300, 900);
await PageObjects.dashboard.initTests({
kibanaIndex: KIBANA_ARCHIVE_PATH,
dataIndex: DATA_ARCHIVE_PATH,
defaultIndex: 'logstash-*',
});
await PageObjects.dashboard.preserveCrossAppState();
});
after(async function () {
await PageObjects.dashboard.clearSavedObjectsFromAppLinks();
await esArchiver.unload(KIBANA_ARCHIVE_PATH);
await esArchiver.unload(DATA_ARCHIVE_PATH);
});
loadTestFile(require.resolve('./panel_actions'));
});
}

View file

@ -0,0 +1,54 @@
/*
* 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 expect from 'expect.js';
export default function ({ getService, getPageObjects }) {
const dashboardPanelActions = getService('dashboardPanelActions');
const testSubjects = getService('testSubjects');
const PageObjects = getPageObjects(['dashboard']);
describe('Panel Actions', () => {
before(async () => {
await PageObjects.dashboard.loadSavedDashboard('few panels');
});
it('Sample action appears in context menu in view mode', async () => {
await dashboardPanelActions.openContextMenu();
const newPanelActionExists = await testSubjects.exists(
'dashboardPanelAction-samplePanelAction'
);
expect(newPanelActionExists).to.be(true);
});
it('Clicking sample action shows a flyout', async () => {
await dashboardPanelActions.openContextMenu();
await testSubjects.click('dashboardPanelAction-samplePanelAction');
const flyoutExists = await testSubjects.exists('samplePanelActionFlyout');
expect(flyoutExists).to.be(true);
});
it('flyout shows the correct contents', async () => {
const titleExists = await testSubjects.exists('samplePanelActionTitle');
expect(titleExists).to.be(true);
const bodyExists = await testSubjects.exists('samplePanelActionBody');
expect(bodyExists).to.be(true);
});
});
}

View file

@ -0,0 +1,33 @@
/*
* 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.
*/
function samplePanelAction(kibana) {
return new kibana.Plugin({
uiExports: {
dashboardPanelActions: ['plugins/sample_panel_action/sample_panel_action'],
},
});
}
module.exports = function (kibana) {
return [
samplePanelAction(kibana),
];
};

View file

@ -0,0 +1,8 @@
{
"name": "sample_panel_action",
"version": "7.0.0-alpha1",
"dependencies": {
"@elastic/eui": "0.0.55",
"react": "^16.4.1"
}
}

View file

@ -0,0 +1,55 @@
/*
* 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 { EuiFlyoutBody, EuiFlyoutHeader, EuiTitle } from '@elastic/eui';
import React from 'react';
import { openFlyout } from '../../../../src/ui/public/flyout';
import {
DashboardPanelAction,
DashboardPanelActionsRegistryProvider,
} from '../../../../src/ui/public/dashboard_panel_actions';
class SamplePanelAction extends DashboardPanelAction {
constructor() {
super({
displayName: 'Sample Panel Action',
id: 'samplePanelAction',
parentPanelId: 'mainMenu',
});
}
onClick({ embeddable }) {
openFlyout(
<React.Fragment>
<EuiFlyoutHeader>
<EuiTitle size="s" data-test-subj="samplePanelActionTitle">
<h1>{embeddable.metadata.title}</h1>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<h1 data-test-subj="samplePanelActionBody">This is a sample action</h1>
</EuiFlyoutBody>
</React.Fragment>,
{
'data-test-subj': 'samplePanelActionFlyout',
},
);
}
}
DashboardPanelActionsRegistryProvider.register(() => new SamplePanelAction());