diff --git a/scripts/functional_tests.js b/scripts/functional_tests.js
index 75e4eab07ac06..4d4f255aa02ad 100644
--- a/scripts/functional_tests.js
+++ b/scripts/functional_tests.js
@@ -21,4 +21,5 @@ require('../src/setup_node_env');
require('../packages/kbn-test').runTestsCli([
require.resolve('../test/functional/config.js'),
require.resolve('../test/api_integration/config.js'),
+ require.resolve('../test/panel_actions/config.js'),
]);
diff --git a/src/ui/public/flyout/flyout_session.tsx b/src/ui/public/flyout/flyout_session.tsx
new file mode 100644
index 0000000000000..185c7378fa86a
--- /dev/null
+++ b/src/ui/public/flyout/flyout_session.tsx
@@ -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(
+
+ {flyoutChildren}
+ ,
+ container
+ );
+
+ return session;
+}
+
+export { FlyoutSession };
diff --git a/src/ui/public/flyout/index.ts b/src/ui/public/flyout/index.ts
new file mode 100644
index 0000000000000..2c0f11bcc72ba
--- /dev/null
+++ b/src/ui/public/flyout/index.ts
@@ -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';
diff --git a/src/ui/public/inspector/inspector.tsx b/src/ui/public/inspector/inspector.tsx
index d463cba47468c..7d19674945a6a 100644
--- a/src/ui/public/inspector/inspector.tsx
+++ b/src/ui/public/inspector/inspector.tsx
@@ -16,67 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
-
-import { EventEmitter } from 'events';
import React from 'react';
-import ReactDOM from 'react-dom';
+import { FlyoutSession, openFlyout } from 'ui/flyout';
import { Adapters } from './types';
import { InspectorPanel } from './ui/inspector_panel';
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.
*
@@ -98,6 +44,8 @@ interface InspectorOptions {
title?: string;
}
+export type InspectorSession = FlyoutSession;
+
/**
* 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
@@ -110,11 +58,6 @@ interface InspectorOptions {
* @return {InspectorSession} The session instance for the opened inspector.
*/
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);
// 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.`);
}
- const container = getOrCreateContainerElement();
- const session = (activeSession = new InspectorSession());
-
- ReactDOM.render(
- session.close()}
- title={options.title}
- />,
- container
- );
-
- return session;
+ return openFlyout(, {
+ 'data-test-subj': 'inspectorPanel',
+ });
}
const Inspector = {
diff --git a/src/ui/public/inspector/ui/__snapshots__/inspector_panel.test.js.snap b/src/ui/public/inspector/ui/__snapshots__/inspector_panel.test.js.snap
index 3ec781c8e7b1d..8fff7e5caf3b8 100644
--- a/src/ui/public/inspector/ui/__snapshots__/inspector_panel.test.js.snap
+++ b/src/ui/public/inspector/ui/__snapshots__/inspector_panel.test.js.snap
@@ -34,313 +34,213 @@ exports[`InspectorPanel should render as expected 1`] = `
]
}
>
-
-
-
+
-
+
`;
diff --git a/src/ui/public/inspector/ui/inspector_panel.d.ts b/src/ui/public/inspector/ui/inspector_panel.d.ts
index 14aea5d7229d1..154b1a58f5732 100644
--- a/src/ui/public/inspector/ui/inspector_panel.d.ts
+++ b/src/ui/public/inspector/ui/inspector_panel.d.ts
@@ -22,7 +22,6 @@ import { Adapters, InspectorViewDescription } from '../types';
interface InspectorPanelProps {
adapters: Adapters;
- onClose: () => void;
title?: string;
views: InspectorViewDescription[];
}
diff --git a/src/ui/public/inspector/ui/inspector_panel.js b/src/ui/public/inspector/ui/inspector_panel.js
index 2c0c3dd9ae29e..7ec34d732387e 100644
--- a/src/ui/public/inspector/ui/inspector_panel.js
+++ b/src/ui/public/inspector/ui/inspector_panel.js
@@ -22,7 +22,6 @@ import PropTypes from 'prop-types';
import {
EuiFlexGroup,
EuiFlexItem,
- EuiFlyout,
EuiFlyoutHeader,
EuiTitle,
} from '@elastic/eui';
@@ -79,14 +78,11 @@ class InspectorPanel extends Component {
}
render() {
- const { views, onClose, title } = this.props;
+ const { views, title } = this.props;
const { selectedView } = this.state;
return (
-
+
{ this.renderSelectedPanel() }
-
+
);
}
}
@@ -125,7 +121,6 @@ InspectorPanel.propTypes = {
);
}
},
- onClose: PropTypes.func.isRequired,
title: PropTypes.string,
};
diff --git a/tasks/config/run.js b/tasks/config/run.js
index b6952c907f7a6..9bc7cab7b61d7 100644
--- a/tasks/config/run.js
+++ b/tasks/config/run.js
@@ -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: {
cmd: process.execPath,
args: [
diff --git a/tasks/test.js b/tasks/test.js
index 77d73237c9c51..87d4fc26a4715 100644
--- a/tasks/test.js
+++ b/tasks/test.js
@@ -31,16 +31,9 @@ module.exports = function (grunt) {
}
);
- grunt.registerTask('test:server', [
- 'checkPlugins',
- 'run:mocha',
- ]);
+ grunt.registerTask('test:server', ['checkPlugins', 'run:mocha']);
- grunt.registerTask('test:browser', [
- 'checkPlugins',
- 'run:browserTestServer',
- 'karma:unit',
- ]);
+ grunt.registerTask('test:browser', ['checkPlugins', 'run:browserTestServer', 'karma:unit']);
grunt.registerTask('test:browser-ci', () => {
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.task.run([
- 'run:browserTestServer',
- ...ciShardTasks
- ]);
+ grunt.task.run(['run:browserTestServer', ...ciShardTasks]);
});
- grunt.registerTask('test:coverage', [ 'run:testCoverageServer', 'karma:coverage' ]);
+ grunt.registerTask('test:coverage', ['run:testCoverageServer', 'karma:coverage']);
grunt.registerTask('test:quick', [
'checkPlugins',
@@ -65,26 +55,24 @@ module.exports = function (grunt) {
'test:jest_integration',
'test:projects',
'test:browser',
- 'run:apiIntegrationTests'
+ 'run:apiIntegrationTests',
]);
- grunt.registerTask('test:dev', [
- 'checkPlugins',
- 'run:devBrowserTestServer',
- 'karma:dev'
- ]);
+ grunt.registerTask('test:dev', ['checkPlugins', 'run:devBrowserTestServer', 'karma:dev']);
grunt.registerTask('test', subTask => {
if (subTask) grunt.fail.fatal(`invalid task "test:${subTask}"`);
- grunt.task.run(_.compact([
- !grunt.option('quick') && 'run:eslint',
- !grunt.option('quick') && 'run:tslint',
- 'run:checkFileCasing',
- 'licenses',
- 'test:quick',
- 'verifyTranslations',
- ]));
+ grunt.task.run(
+ _.compact([
+ !grunt.option('quick') && 'run:eslint',
+ !grunt.option('quick') && 'run:tslint',
+ 'run:checkFileCasing',
+ 'licenses',
+ 'test:quick',
+ 'verifyTranslations',
+ ])
+ );
});
grunt.registerTask('quick-test', ['test:quick']); // historical alias
@@ -98,7 +86,7 @@ module.exports = function (grunt) {
const serverCmd = {
cmd: 'yarn',
args: ['kbn', 'run', 'test', '--exclude', 'kibana', '--oss', '--skip-kibana-extra'],
- opts: { stdio: 'inherit' }
+ opts: { stdio: 'inherit' },
};
return new Promise((resolve, reject) => {
diff --git a/test/panel_actions/config.js b/test/panel_actions/config.js
new file mode 100644
index 0000000000000..8493c56ae8687
--- /dev/null
+++ b/test/panel_actions/config.js
@@ -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')}`,
+ ],
+ },
+ };
+}
diff --git a/test/panel_actions/index.js b/test/panel_actions/index.js
new file mode 100644
index 0000000000000..48f743371f326
--- /dev/null
+++ b/test/panel_actions/index.js
@@ -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'));
+ });
+}
diff --git a/test/panel_actions/panel_actions.js b/test/panel_actions/panel_actions.js
new file mode 100644
index 0000000000000..29605638b025a
--- /dev/null
+++ b/test/panel_actions/panel_actions.js
@@ -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);
+ });
+ });
+}
diff --git a/test/panel_actions/sample_panel_action/index.js b/test/panel_actions/sample_panel_action/index.js
new file mode 100644
index 0000000000000..3702b17846fe6
--- /dev/null
+++ b/test/panel_actions/sample_panel_action/index.js
@@ -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),
+ ];
+};
+
diff --git a/test/panel_actions/sample_panel_action/package.json b/test/panel_actions/sample_panel_action/package.json
new file mode 100644
index 0000000000000..f1603503a439b
--- /dev/null
+++ b/test/panel_actions/sample_panel_action/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "sample_panel_action",
+ "version": "7.0.0-alpha1",
+ "dependencies": {
+ "@elastic/eui": "0.0.55",
+ "react": "^16.4.1"
+ }
+}
diff --git a/test/panel_actions/sample_panel_action/public/sample_panel_action.js b/test/panel_actions/sample_panel_action/public/sample_panel_action.js
new file mode 100644
index 0000000000000..3f5cbf8f18f43
--- /dev/null
+++ b/test/panel_actions/sample_panel_action/public/sample_panel_action.js
@@ -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(
+
+
+
+