From 0503b820dee2fed2f59c6091a7000748e3dfb2d7 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Wed, 20 Dec 2023 17:19:48 +0200 Subject: [PATCH] [ES|QL] Creates charts from the dashboard (#171973) ## Summary Closes https://github.com/elastic/kibana/issues/165928 Enables the creation of ES|QL charts from the dashboard. ![esql](https://github.com/elastic/kibana/assets/17003240/86dd5594-d130-4fb7-b495-29ddbaee5e5b) The implementation is using UIActions which I think is the correct way to register a new panel action to a dashboard. Lens is responsible to register the ESQL panel action and owns the code. ### How it works - A new ES|QL panel has been added to the dashboard toolbar registered by a ui action - A new panel is been created with a default esql query `from | limit 10` - This results to a datatable and opens the flyout - If a user clicks cancel then the embeddable is being removed ### Checklist - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../add_panel_action_menu_items.test.ts | 40 ++++++ .../top_nav/add_panel_action_menu_items.ts | 52 +++++++ .../top_nav/dashboard_editing_toolbar.tsx | 48 +++++-- .../dashboard_app/top_nav/editor_menu.tsx | 48 ++++++- src/plugins/dashboard/public/plugin.tsx | 15 ++- .../public/services/plugin_services.stub.ts | 2 + .../public/services/plugin_services.ts | 2 + .../dashboard/public/services/types.ts | 2 + .../public/services/ui_actions/types.ts | 13 ++ .../ui_actions/ui_actions_service.stub.ts | 17 +++ .../services/ui_actions/ui_actions_service.ts | 25 ++++ .../dashboard/public/triggers/index.ts | 20 +++ .../dashboard/group6/dashboard_esql_chart.ts | 73 ++++++++++ .../functional/apps/dashboard/group6/index.ts | 1 + .../services/dashboard/add_panel.ts | 4 + .../shared/edit_on_the_fly/flyout_wrapper.tsx | 18 ++- .../get_edit_lens_configuration.tsx | 4 + .../lens_configuration_flyout.test.tsx | 10 ++ .../lens_configuration_flyout.tsx | 15 ++- .../shared/edit_on_the_fly/types.ts | 5 + x-pack/plugins/lens/public/async_services.ts | 3 +- .../lens/public/embeddable/embeddable.tsx | 8 +- x-pack/plugins/lens/public/plugin.ts | 10 +- .../open_lens_config/create_action.test.tsx | 41 ++++++ .../open_lens_config/create_action.tsx | 67 +++++++++ .../open_lens_config/create_action_helpers.ts | 127 ++++++++++++++++++ .../{action.test.tsx => edit_action.test.tsx} | 2 +- .../{action.tsx => edit_action.tsx} | 8 +- .../{helpers.ts => edit_action_helpers.ts} | 17 ++- 29 files changed, 663 insertions(+), 34 deletions(-) create mode 100644 src/plugins/dashboard/public/dashboard_app/top_nav/add_panel_action_menu_items.test.ts create mode 100644 src/plugins/dashboard/public/dashboard_app/top_nav/add_panel_action_menu_items.ts create mode 100644 src/plugins/dashboard/public/services/ui_actions/types.ts create mode 100644 src/plugins/dashboard/public/services/ui_actions/ui_actions_service.stub.ts create mode 100644 src/plugins/dashboard/public/services/ui_actions/ui_actions_service.ts create mode 100644 src/plugins/dashboard/public/triggers/index.ts create mode 100644 test/functional/apps/dashboard/group6/dashboard_esql_chart.ts create mode 100644 x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action.test.tsx create mode 100644 x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action.tsx create mode 100644 x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action_helpers.ts rename x-pack/plugins/lens/public/trigger_actions/open_lens_config/{action.test.tsx => edit_action.test.tsx} (98%) rename x-pack/plugins/lens/public/trigger_actions/open_lens_config/{action.tsx => edit_action.tsx} (88%) rename x-pack/plugins/lens/public/trigger_actions/open_lens_config/{helpers.ts => edit_action_helpers.ts} (85%) diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/add_panel_action_menu_items.test.ts b/src/plugins/dashboard/public/dashboard_app/top_nav/add_panel_action_menu_items.test.ts new file mode 100644 index 0000000000000..024a7518aba0c --- /dev/null +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/add_panel_action_menu_items.test.ts @@ -0,0 +1,40 @@ +/* + * 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 { getAddPanelActionMenuItems } from './add_panel_action_menu_items'; + +describe('getAddPanelActionMenuItems', () => { + it('returns the items correctly', async () => { + const registeredActions = [ + { + id: 'ACTION_CREATE_ESQL_CHART', + type: 'ACTION_CREATE_ESQL_CHART', + getDisplayName: () => 'Action name', + getIconType: () => 'pencil', + getDisplayNameTooltip: () => 'Action tooltip', + isCompatible: () => Promise.resolve(true), + execute: jest.fn(), + }, + ]; + const items = getAddPanelActionMenuItems(registeredActions, jest.fn(), jest.fn(), jest.fn()); + expect(items).toStrictEqual([ + { + 'data-test-subj': 'create-action-Action name', + icon: 'pencil', + name: 'Action name', + onClick: expect.any(Function), + toolTipContent: 'Action tooltip', + }, + ]); + }); + + it('returns empty array if no actions have been registered', async () => { + const items = getAddPanelActionMenuItems([], jest.fn(), jest.fn(), jest.fn()); + expect(items).toStrictEqual([]); + }); +}); diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/add_panel_action_menu_items.ts b/src/plugins/dashboard/public/dashboard_app/top_nav/add_panel_action_menu_items.ts new file mode 100644 index 0000000000000..3df8343887982 --- /dev/null +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/add_panel_action_menu_items.ts @@ -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 + * 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 type { ActionExecutionContext, Action } from '@kbn/ui-actions-plugin/public'; +import type { EmbeddableFactory } from '@kbn/embeddable-plugin/public'; +import { addPanelMenuTrigger } from '../../triggers'; + +const onAddPanelActionClick = + (action: Action, context: ActionExecutionContext, closePopover: () => void) => + (event: React.MouseEvent) => { + closePopover(); + if (event.currentTarget instanceof HTMLAnchorElement) { + if ( + !event.defaultPrevented && // onClick prevented default + event.button === 0 && + (!event.currentTarget.target || event.currentTarget.target === '_self') && + !(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) + ) { + event.preventDefault(); + action.execute(context); + } + } else action.execute(context); + }; + +export const getAddPanelActionMenuItems = ( + actions: Array> | undefined, + createNewEmbeddable: (embeddableFactory: EmbeddableFactory) => void, + deleteEmbeddable: (embeddableId: string) => void, + closePopover: () => void +) => { + return ( + actions?.map((item) => { + const context = { + createNewEmbeddable, + deleteEmbeddable, + trigger: addPanelMenuTrigger, + }; + const actionName = item.getDisplayName(context); + return { + name: actionName, + icon: item.getIconType(context), + onClick: onAddPanelActionClick(item, context, closePopover), + 'data-test-subj': `create-action-${actionName}`, + toolTipContent: item?.getDisplayNameTooltip?.(context), + }; + }) ?? [] + ); +}; diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_editing_toolbar.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_editing_toolbar.tsx index 6afdf1429663b..e39e227da0643 100644 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_editing_toolbar.tsx +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_editing_toolbar.tsx @@ -80,8 +80,17 @@ export function DashboardEditingToolbar({ isDisabled }: { isDisabled?: boolean } [stateTransferService, dashboard, search.session, trackUiMetric] ); + /** + * embeddableFactory: Required, you can get the factory from embeddableStart.getEmbeddableFactory() + * initialInput: Optional, use it in case you want to pass your own input to the factory + * dismissNotification: Optional, if not passed a toast will appear in the dashboard + */ const createNewEmbeddable = useCallback( - async (embeddableFactory: EmbeddableFactory) => { + async ( + embeddableFactory: EmbeddableFactory, + initialInput?: Partial, + dismissNotification?: boolean + ) => { if (trackUiMetric) { trackUiMetric(METRIC_TYPE.CLICK, embeddableFactory.type); } @@ -89,12 +98,19 @@ export function DashboardEditingToolbar({ isDisabled }: { isDisabled?: boolean } let explicitInput: Partial; let attributes: unknown; try { - const explicitInputReturn = await embeddableFactory.getExplicitInput(undefined, dashboard); - if (isExplicitInputWithAttributes(explicitInputReturn)) { - explicitInput = explicitInputReturn.newInput; - attributes = explicitInputReturn.attributes; + if (initialInput) { + explicitInput = initialInput; } else { - explicitInput = explicitInputReturn; + const explicitInputReturn = await embeddableFactory.getExplicitInput( + undefined, + dashboard + ); + if (isExplicitInputWithAttributes(explicitInputReturn)) { + explicitInput = explicitInputReturn.newInput; + attributes = explicitInputReturn.attributes; + } else { + explicitInput = explicitInputReturn; + } } } catch (e) { // error likely means user canceled embeddable creation @@ -110,19 +126,31 @@ export function DashboardEditingToolbar({ isDisabled }: { isDisabled?: boolean } if (newEmbeddable) { dashboard.setScrollToPanelId(newEmbeddable.id); dashboard.setHighlightPanelId(newEmbeddable.id); - toasts.addSuccess({ - title: dashboardReplacePanelActionStrings.getSuccessMessage(newEmbeddable.getTitle()), - 'data-test-subj': 'addEmbeddableToDashboardSuccess', - }); + + if (!dismissNotification) { + toasts.addSuccess({ + title: dashboardReplacePanelActionStrings.getSuccessMessage(newEmbeddable.getTitle()), + 'data-test-subj': 'addEmbeddableToDashboardSuccess', + }); + } } + return newEmbeddable; }, [trackUiMetric, dashboard, toasts] ); + const deleteEmbeddable = useCallback( + (embeddableId: string) => { + dashboard.removeEmbeddable(embeddableId); + }, + [dashboard] + ); + const extraButtons = [ , () => void; /** Handler for creating a new embeddable of a specified type */ createNewEmbeddable: (embeddableFactory: EmbeddableFactory) => void; + /** Handler for deleting an embeddable */ + deleteEmbeddable: (embeddableId: string) => void; } interface FactoryGroup { @@ -44,7 +49,13 @@ interface UnwrappedEmbeddableFactory { isEditable: boolean; } -export const EditorMenu = ({ createNewVisType, createNewEmbeddable, isDisabled }: Props) => { +export const EditorMenu = ({ + createNewVisType, + createNewEmbeddable, + deleteEmbeddable, + isDisabled, +}: Props) => { + const isMounted = useRef(false); const { embeddable, visualizations: { @@ -52,6 +63,7 @@ export const EditorMenu = ({ createNewVisType, createNewEmbeddable, isDisabled } getByGroup: getVisTypesByGroup, showNewVisModal, }, + uiActions, } = pluginServices.getServices(); const { euiTheme } = useEuiTheme(); @@ -64,6 +76,10 @@ export const EditorMenu = ({ createNewVisType, createNewEmbeddable, isDisabled } UnwrappedEmbeddableFactory[] >([]); + const [addPanelActions, setAddPanelActions] = useState> | undefined>( + undefined + ); + useEffect(() => { Promise.all( embeddableFactories.map>(async (factory) => ({ @@ -121,6 +137,28 @@ export const EditorMenu = ({ createNewVisType, createNewEmbeddable, isDisabled } let panelCount = 1 + aggBasedPanelID; + useEffect(() => { + isMounted.current = true; + + return () => { + isMounted.current = false; + }; + }, []); + + // Retrieve ADD_PANEL_TRIGGER actions + useEffect(() => { + async function loadPanelActions() { + const registeredActions = await uiActions?.getTriggerCompatibleActions?.( + ADD_PANEL_TRIGGER, + {} + ); + if (isMounted.current) { + setAddPanelActions(registeredActions); + } + } + loadPanelActions(); + }, [uiActions]); + factories.forEach(({ factory }) => { const { grouping } = factory; @@ -236,6 +274,12 @@ export const EditorMenu = ({ createNewVisType, createNewEmbeddable, isDisabled } })), ...promotedVisTypes.map(getVisTypeMenuItem), + ...getAddPanelActionMenuItems( + addPanelActions, + createNewEmbeddable, + deleteEmbeddable, + closePopover + ), ]; if (aggsBasedVisTypes.length > 0) { initialPanelItems.push({ diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 98af5088967f2..4c75362485b6a 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -70,6 +70,7 @@ import { import { DashboardMountContextProps } from './dashboard_app/types'; import type { FindDashboardsService } from './services/dashboard_content_management/types'; import { CONTENT_ID, LATEST_VERSION } from '../common/content_management'; +import { addPanelMenuTrigger } from './triggers'; export interface DashboardFeatureFlagConfig { allowByValueEmbeddables: boolean; @@ -149,11 +150,23 @@ export class DashboardPlugin public setup( core: CoreSetup, - { share, embeddable, home, urlForwarding, data, contentManagement }: DashboardSetupDependencies + { + share, + embeddable, + home, + urlForwarding, + data, + contentManagement, + uiActions, + }: DashboardSetupDependencies ): DashboardSetup { this.dashboardFeatureFlagConfig = this.initializerContext.config.get(); + // this trigger enables external consumers to register actions for + // adding items to the add panel menu + uiActions.registerTrigger(addPanelMenuTrigger); + if (share) { this.locator = share.url.locators.create( new DashboardAppLocatorDefinition({ diff --git a/src/plugins/dashboard/public/services/plugin_services.stub.ts b/src/plugins/dashboard/public/services/plugin_services.stub.ts index b77888f1293f5..e61aab184b09c 100644 --- a/src/plugins/dashboard/public/services/plugin_services.stub.ts +++ b/src/plugins/dashboard/public/services/plugin_services.stub.ts @@ -43,6 +43,7 @@ import { savedObjectsManagementServiceFactory } from './saved_objects_management import { contentManagementServiceFactory } from './content_management/content_management_service.stub'; import { serverlessServiceFactory } from './serverless/serverless_service.stub'; import { noDataPageServiceFactory } from './no_data_page/no_data_page_service.stub'; +import { uiActionsServiceFactory } from './ui_actions/ui_actions_service.stub'; export const providers: PluginServiceProviders = { dashboardContentManagement: new PluginServiceProvider(dashboardContentManagementServiceFactory), @@ -74,6 +75,7 @@ export const providers: PluginServiceProviders = { contentManagement: new PluginServiceProvider(contentManagementServiceFactory), serverless: new PluginServiceProvider(serverlessServiceFactory), noDataPage: new PluginServiceProvider(noDataPageServiceFactory), + uiActions: new PluginServiceProvider(uiActionsServiceFactory), }; export const registry = new PluginServiceRegistry(providers); diff --git a/src/plugins/dashboard/public/services/plugin_services.ts b/src/plugins/dashboard/public/services/plugin_services.ts index 1d159014a4e72..2c9c1d95828e8 100644 --- a/src/plugins/dashboard/public/services/plugin_services.ts +++ b/src/plugins/dashboard/public/services/plugin_services.ts @@ -44,6 +44,7 @@ import { dashboardContentManagementServiceFactory } from './dashboard_content_ma import { contentManagementServiceFactory } from './content_management/content_management_service'; import { serverlessServiceFactory } from './serverless/serverless_service'; import { noDataPageServiceFactory } from './no_data_page/no_data_page_service'; +import { uiActionsServiceFactory } from './ui_actions/ui_actions_service'; const providers: PluginServiceProviders = { dashboardContentManagement: new PluginServiceProvider(dashboardContentManagementServiceFactory, [ @@ -88,6 +89,7 @@ const providers: PluginServiceProviders(); diff --git a/src/plugins/dashboard/public/services/types.ts b/src/plugins/dashboard/public/services/types.ts index c1c7c1aa39e71..420ba257b6a6a 100644 --- a/src/plugins/dashboard/public/services/types.ts +++ b/src/plugins/dashboard/public/services/types.ts @@ -39,6 +39,7 @@ import { DashboardUsageCollectionService } from './usage_collection/types'; import { DashboardVisualizationsService } from './visualizations/types'; import { DashboardServerlessService } from './serverless/types'; import { NoDataPageService } from './no_data_page/types'; +import { DashboardUiActionsService } from './ui_actions/types'; export type DashboardPluginServiceParams = KibanaPluginServiceParams & { initContext: PluginInitializerContext; // need a custom type so that initContext is a required parameter for initializerContext @@ -74,4 +75,5 @@ export interface DashboardServices { contentManagement: ContentManagementPublicStart; serverless: DashboardServerlessService; // TODO: make this optional in follow up noDataPage: NoDataPageService; + uiActions: DashboardUiActionsService; } diff --git a/src/plugins/dashboard/public/services/ui_actions/types.ts b/src/plugins/dashboard/public/services/ui_actions/types.ts new file mode 100644 index 0000000000000..102cb70d8a11d --- /dev/null +++ b/src/plugins/dashboard/public/services/ui_actions/types.ts @@ -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 + * 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 type { UiActionsStart } from '@kbn/ui-actions-plugin/public'; + +export interface DashboardUiActionsService { + getTriggerCompatibleActions?: UiActionsStart['getTriggerCompatibleActions']; +} diff --git a/src/plugins/dashboard/public/services/ui_actions/ui_actions_service.stub.ts b/src/plugins/dashboard/public/services/ui_actions/ui_actions_service.stub.ts new file mode 100644 index 0000000000000..033889959b89a --- /dev/null +++ b/src/plugins/dashboard/public/services/ui_actions/ui_actions_service.stub.ts @@ -0,0 +1,17 @@ +/* + * 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 { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks'; +import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; +import { DashboardUiActionsService } from './types'; + +export type UIActionsServiceFactory = PluginServiceFactory; + +export const uiActionsServiceFactory: UIActionsServiceFactory = () => { + const pluginMock = uiActionsPluginMock.createStartContract(); + return { getTriggerCompatibleActions: pluginMock.getTriggerCompatibleActions }; +}; diff --git a/src/plugins/dashboard/public/services/ui_actions/ui_actions_service.ts b/src/plugins/dashboard/public/services/ui_actions/ui_actions_service.ts new file mode 100644 index 0000000000000..221380c6d0479 --- /dev/null +++ b/src/plugins/dashboard/public/services/ui_actions/ui_actions_service.ts @@ -0,0 +1,25 @@ +/* + * 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 type { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public'; +import type { DashboardStartDependencies } from '../../plugin'; +import type { DashboardUiActionsService } from './types'; + +export type UsageCollectionServiceFactory = KibanaPluginServiceFactory< + DashboardUiActionsService, + DashboardStartDependencies +>; +export const uiActionsServiceFactory: UsageCollectionServiceFactory = ({ startPlugins }) => { + const { uiActions } = startPlugins; + if (!uiActions) return {}; + + const { getTriggerCompatibleActions } = uiActions; + return { + getTriggerCompatibleActions, + }; +}; diff --git a/src/plugins/dashboard/public/triggers/index.ts b/src/plugins/dashboard/public/triggers/index.ts new file mode 100644 index 0000000000000..96dfa814b949a --- /dev/null +++ b/src/plugins/dashboard/public/triggers/index.ts @@ -0,0 +1,20 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import type { Trigger } from '@kbn/ui-actions-plugin/public'; + +export const ADD_PANEL_TRIGGER = 'ADD_PANEL_TRIGGER'; +export const addPanelMenuTrigger: Trigger = { + id: ADD_PANEL_TRIGGER, + title: i18n.translate('dashboard.addPanelMenuTrigger.title', { + defaultMessage: 'Add panel menu', + }), + description: i18n.translate('dashboard.addPanelMenuTrigger.description', { + defaultMessage: "A new action will appear to the dashboard's add panel menu", + }), +}; diff --git a/test/functional/apps/dashboard/group6/dashboard_esql_chart.ts b/test/functional/apps/dashboard/group6/dashboard_esql_chart.ts new file mode 100644 index 0000000000000..f69477b926b71 --- /dev/null +++ b/test/functional/apps/dashboard/group6/dashboard_esql_chart.ts @@ -0,0 +1,73 @@ +/* + * 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 expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const retry = getService('retry'); + const kibanaServer = getService('kibanaServer'); + const PageObjects = getPageObjects(['common', 'dashboard', 'timePicker', 'header']); + const testSubjects = getService('testSubjects'); + const monacoEditor = getService('monacoEditor'); + const dashboardAddPanel = getService('dashboardAddPanel'); + + describe('dashboard add ES|QL chart', function () { + before(async () => { + await kibanaServer.savedObjects.cleanStandardList(); + await kibanaServer.importExport.load( + 'test/functional/fixtures/kbn_archiver/dashboard/current/kibana' + ); + await kibanaServer.uiSettings.replace({ + defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c', + }); + }); + + it('should add an ES|QL datatable chart when the ES|QL panel action is clicked', async () => { + await PageObjects.dashboard.navigateToApp(); + await PageObjects.dashboard.clickNewDashboard(); + await PageObjects.timePicker.setDefaultDataRange(); + await PageObjects.dashboard.switchToEditMode(); + await dashboardAddPanel.clickEditorMenuButton(); + await dashboardAddPanel.clickAddNewPanelFromUIActionLink('ES|QL'); + await dashboardAddPanel.expectEditorMenuClosed(); + await PageObjects.dashboard.waitForRenderComplete(); + + await retry.try(async () => { + const panelCount = await PageObjects.dashboard.getPanelCount(); + expect(panelCount).to.eql(1); + }); + + expect(await testSubjects.exists('lnsDataTable')).to.be(true); + }); + + it('should remove the panel if cancel button is clicked', async () => { + await testSubjects.click('cancelFlyoutButton'); + await PageObjects.dashboard.waitForRenderComplete(); + await retry.try(async () => { + const panelCount = await PageObjects.dashboard.getPanelCount(); + expect(panelCount).to.eql(0); + }); + }); + + it('should be able to edit the query and render another chart', async () => { + await dashboardAddPanel.clickEditorMenuButton(); + await dashboardAddPanel.clickAddNewPanelFromUIActionLink('ES|QL'); + await dashboardAddPanel.expectEditorMenuClosed(); + await PageObjects.dashboard.waitForRenderComplete(); + + await monacoEditor.setCodeEditorValue('from logstash-* | stats maxB = max(bytes)'); + await testSubjects.click('TextBasedLangEditor-run-query-button'); + await PageObjects.header.waitUntilLoadingHasFinished(); + + await testSubjects.click('applyFlyoutButton'); + expect(await testSubjects.exists('mtrVis')).to.be(true); + }); + }); +} diff --git a/test/functional/apps/dashboard/group6/index.ts b/test/functional/apps/dashboard/group6/index.ts index 95d18049053ae..75decec10fb4c 100644 --- a/test/functional/apps/dashboard/group6/index.ts +++ b/test/functional/apps/dashboard/group6/index.ts @@ -35,5 +35,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { // If we don't use the timestamp in the URL, the colors in the charts will be different. loadTestFile(require.resolve('./dashboard_snapshots')); loadTestFile(require.resolve('./embeddable_library')); + loadTestFile(require.resolve('./dashboard_esql_chart')); }); } diff --git a/test/functional/services/dashboard/add_panel.ts b/test/functional/services/dashboard/add_panel.ts index 00a91dff87b85..65adf6dee5359 100644 --- a/test/functional/services/dashboard/add_panel.ts +++ b/test/functional/services/dashboard/add_panel.ts @@ -77,6 +77,10 @@ export class DashboardAddPanelService extends FtrService { await this.testSubjects.click(`createNew-${type}`); } + async clickAddNewPanelFromUIActionLink(type: string) { + await this.testSubjects.click(`create-action-${type}`); + } + async addEveryEmbeddableOnCurrentPage() { this.log.debug('addEveryEmbeddableOnCurrentPage'); const itemList = await this.testSubjects.find('savedObjectsFinderTable'); diff --git a/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/flyout_wrapper.tsx b/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/flyout_wrapper.tsx index bab45a9ffe856..f9a6ac0ce397b 100644 --- a/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/flyout_wrapper.tsx +++ b/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/flyout_wrapper.tsx @@ -32,6 +32,7 @@ export const FlyoutWrapper = ({ displayFlyoutHeader, language, attributesChanged, + isNewPanel, onCancel, navigateToLensEditor, onApply, @@ -49,12 +50,17 @@ export const FlyoutWrapper = ({ > - +

- {i18n.translate('xpack.lens.config.editVisualizationLabel', { - defaultMessage: 'Edit {lang} visualization', - values: { lang: language }, - })} + {isNewPanel + ? i18n.translate('xpack.lens.config.createVisualizationLabel', { + defaultMessage: 'Create {lang} visualization', + values: { lang: language }, + }) + : i18n.translate('xpack.lens.config.editVisualizationLabel', { + defaultMessage: 'Edit {lang} visualization', + values: { lang: language }, + })} diff --git a/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/get_edit_lens_configuration.tsx b/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/get_edit_lens_configuration.tsx index 2421f303d7b3a..acef8feb6da50 100644 --- a/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/get_edit_lens_configuration.tsx +++ b/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/get_edit_lens_configuration.tsx @@ -114,6 +114,8 @@ export async function getEditLensConfiguration( navigateToLensEditor, displayFlyoutHeader, canEditTextBasedQuery, + isNewPanel, + deletePanel, hidesSuggestions, }: EditLensConfigurationProps) => { if (!lensServices || !datasourceMap || !visualizationMap) { @@ -208,6 +210,8 @@ export async function getEditLensConfiguration( canEditTextBasedQuery, hidesSuggestions, setCurrentAttributes, + isNewPanel, + deletePanel, }; return getWrapper( diff --git a/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.test.tsx b/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.test.tsx index adc0b83861b9c..6aad89b50ef0c 100644 --- a/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.test.tsx @@ -94,6 +94,16 @@ describe('LensEditConfigurationFlyout', () => { expect(navigateToLensEditorSpy).toHaveBeenCalled(); }); + it('should display the header title correctly for a newly created panel', async () => { + renderConfigFlyout({ + displayFlyoutHeader: true, + isNewPanel: true, + }); + expect(screen.getByTestId('inlineEditingFlyoutLabel').textContent).toBe( + 'Create ES|QL visualization' + ); + }); + it('should call the closeFlyout callback if cancel button is clicked', async () => { const closeFlyoutSpy = jest.fn(); diff --git a/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.tsx b/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.tsx index 13fedcf5cd7e0..39801f41c391c 100644 --- a/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.tsx +++ b/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.tsx @@ -61,6 +61,8 @@ export function LensEditConfigurationFlyout({ navigateToLensEditor, displayFlyoutHeader, canEditTextBasedQuery, + isNewPanel, + deletePanel, hidesSuggestions, }: EditConfigPanelProps) { const euiTheme = useEuiTheme(); @@ -165,18 +167,23 @@ export function LensEditConfigurationFlyout({ updateByRefInput?.(savedObjectId); } } + // for a newly created chart, I want cancelling to also remove the panel + if (isNewPanel && deletePanel) { + deletePanel(); + } closeFlyout?.(); }, [ - previousAttributes, attributesChanged, + isNewPanel, + deletePanel, closeFlyout, + visualization.activeId, + savedObjectId, datasourceMap, datasourceId, updatePanelState, updateSuggestion, - savedObjectId, updateByRefInput, - visualization, ]); const onApply = useCallback(() => { @@ -279,6 +286,7 @@ export function LensEditConfigurationFlyout({ onApply={onApply} isScrollable={true} attributesChanged={attributesChanged} + isNewPanel={isNewPanel} > void; onApply?: () => void; navigateToLensEditor?: () => void; @@ -75,6 +76,10 @@ export interface EditConfigPanelProps { displayFlyoutHeader?: boolean; /** If set to true the layout changes to accordion and the text based query (i.e. ES|QL) can be edited */ canEditTextBasedQuery?: boolean; + /** The flyout is used for adding a new panel by scratch */ + isNewPanel?: boolean; + /** Handler for deleting the embeddable, used in case a user cancels a newly created chart */ + deletePanel?: () => void; /** If set to true the layout changes to accordion and the text based query (i.e. ES|QL) can be edited */ hidesSuggestions?: boolean; } diff --git a/x-pack/plugins/lens/public/async_services.ts b/x-pack/plugins/lens/public/async_services.ts index 7bbfaf415db03..85724c871cda6 100644 --- a/x-pack/plugins/lens/public/async_services.ts +++ b/x-pack/plugins/lens/public/async_services.ts @@ -50,4 +50,5 @@ export * from './app_plugin/save_modal_container'; export * from './chart_info_api'; export * from './trigger_actions/open_in_discover_helpers'; -export * from './trigger_actions/open_lens_config/helpers'; +export * from './trigger_actions/open_lens_config/edit_action_helpers'; +export * from './trigger_actions/open_lens_config/create_action_helpers'; diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.tsx index c1dd7901f3822..529f80b803c6f 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.tsx @@ -845,7 +845,11 @@ export class Embeddable this.updateInput({ attributes: attrs, savedObjectId }); } - async openConfingPanel(startDependencies: LensPluginStartDependencies) { + async openConfingPanel( + startDependencies: LensPluginStartDependencies, + isNewPanel?: boolean, + deletePanel?: () => void + ) { const { getEditLensConfiguration } = await import('../async_services'); const Component = await getEditLensConfiguration( this.deps.coreStart, @@ -875,6 +879,8 @@ export class Embeddable } displayFlyoutHeader={true} canEditTextBasedQuery={this.isTextBasedLanguage()} + isNewPanel={isNewPanel} + deletePanel={deletePanel} /> ); } diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index 460864d1c86f2..872e1a566ab20 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -105,7 +105,8 @@ import type { } from './types'; import { getLensAliasConfig } from './vis_type_alias'; import { createOpenInDiscoverAction } from './trigger_actions/open_in_discover_action'; -import { ConfigureInLensPanelAction } from './trigger_actions/open_lens_config/action'; +import { ConfigureInLensPanelAction } from './trigger_actions/open_lens_config/edit_action'; +import { CreateESQLPanelAction } from './trigger_actions/open_lens_config/create_action'; import { visualizeFieldAction } from './trigger_actions/visualize_field_actions'; import { visualizeTSVBAction } from './trigger_actions/visualize_tsvb_actions'; import { visualizeAggBasedVisAction } from './trigger_actions/visualize_agg_based_vis_actions'; @@ -327,6 +328,9 @@ export class LensPlugin { this.editorFrameService!.loadVisualizations(), this.editorFrameService!.loadDatasources(), ]); + const { setVisualizationMap, setDatasourceMap } = await import('./async_services'); + setDatasourceMap(datasourceMap); + setVisualizationMap(visualizationMap); const eventAnnotationService = await plugins.eventAnnotation.getService(); if (plugins.usageCollection) { @@ -598,6 +602,10 @@ export class LensPlugin { ); startDependencies.uiActions.addTriggerAction('CONTEXT_MENU_TRIGGER', editInLensAction); + // Displays the add ESQL panel in the dashboard add Panel menu + const createESQLPanelAction = new CreateESQLPanelAction(startDependencies, core); + startDependencies.uiActions.addTriggerAction('ADD_PANEL_TRIGGER', createESQLPanelAction); + const discoverLocator = startDependencies.share?.url.locators.get('DISCOVER_APP_LOCATOR'); if (discoverLocator) { startDependencies.uiActions.addTriggerAction( diff --git a/x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action.test.tsx b/x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action.test.tsx new file mode 100644 index 0000000000000..9a06fea94cd98 --- /dev/null +++ b/x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action.test.tsx @@ -0,0 +1,41 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { CoreStart } from '@kbn/core/public'; +import { coreMock } from '@kbn/core/public/mocks'; +import type { LensPluginStartDependencies } from '../../plugin'; +import { createMockStartDependencies } from '../../editor_frame_service/mocks'; +import { CreateESQLPanelAction } from './create_action'; + +describe('create Lens panel action', () => { + const core = coreMock.createStart(); + const mockStartDependencies = + createMockStartDependencies() as unknown as LensPluginStartDependencies; + describe('compatibility check', () => { + it('is incompatible if ui setting for ES|QL is off', async () => { + const configurablePanelAction = new CreateESQLPanelAction(mockStartDependencies, core); + const isCompatible = await configurablePanelAction.isCompatible(); + + expect(isCompatible).toBeFalsy(); + }); + + it('is compatible if ui setting for ES|QL is on', async () => { + const updatedCore = { + ...core, + uiSettings: { + ...core.uiSettings, + get: (setting: string) => { + return setting === 'discover:enableESQL'; + }, + }, + } as CoreStart; + const createESQLAction = new CreateESQLPanelAction(mockStartDependencies, updatedCore); + const isCompatible = await createESQLAction.isCompatible(); + + expect(isCompatible).toBeTruthy(); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action.tsx b/x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action.tsx new file mode 100644 index 0000000000000..aa33a629c3969 --- /dev/null +++ b/x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action.tsx @@ -0,0 +1,67 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { i18n } from '@kbn/i18n'; +import type { CoreStart } from '@kbn/core/public'; +import { Action } from '@kbn/ui-actions-plugin/public'; +import type { + EmbeddableFactory, + EmbeddableInput, + IEmbeddable, +} from '@kbn/embeddable-plugin/public'; +import type { LensPluginStartDependencies } from '../../plugin'; + +const ACTION_CREATE_ESQL_CHART = 'ACTION_CREATE_ESQL_CHART'; + +interface Context { + createNewEmbeddable: ( + embeddableFactory: EmbeddableFactory, + initialInput?: Partial, + dismissNotification?: boolean + ) => Promise; + deleteEmbeddable: (embeddableId: string) => void; + initialInput?: Partial; +} + +export const getAsyncHelpers = async () => await import('../../async_services'); + +export class CreateESQLPanelAction implements Action { + public type = ACTION_CREATE_ESQL_CHART; + public id = ACTION_CREATE_ESQL_CHART; + public order = 50; + + constructor( + protected readonly startDependencies: LensPluginStartDependencies, + protected readonly core: CoreStart + ) {} + + public getDisplayName(): string { + return i18n.translate('xpack.lens.app.createVisualizationLabel', { + defaultMessage: 'ES|QL', + }); + } + + public getIconType() { + // need to create a new one + return 'esqlVis'; + } + + public async isCompatible() { + // compatible only when ES|QL advanced setting is enabled + const { isCreateActionCompatible } = await getAsyncHelpers(); + return isCreateActionCompatible(this.core); + } + + public async execute({ createNewEmbeddable, deleteEmbeddable }: Context) { + const { executeCreateAction } = await getAsyncHelpers(); + executeCreateAction({ + deps: this.startDependencies, + core: this.core, + createNewEmbeddable, + deleteEmbeddable, + }); + } +} diff --git a/x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action_helpers.ts b/x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action_helpers.ts new file mode 100644 index 0000000000000..1eddc499f4170 --- /dev/null +++ b/x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action_helpers.ts @@ -0,0 +1,127 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { createGetterSetter } from '@kbn/kibana-utils-plugin/common'; +import type { CoreStart } from '@kbn/core/public'; +import type { + EmbeddableFactory, + EmbeddableInput, + IEmbeddable, +} from '@kbn/embeddable-plugin/public'; +import { IncompatibleActionError } from '@kbn/ui-actions-plugin/public'; +import type { Datasource, Visualization } from '../../types'; +import type { LensPluginStartDependencies } from '../../plugin'; +import { fetchDataFromAggregateQuery } from '../../datasources/text_based/fetch_data_from_aggregate_query'; +import { suggestionsApi } from '../../lens_suggestions_api'; +import { getLensAttributes } from '../../app_plugin/shared/edit_on_the_fly/helpers'; +import { generateId } from '../../id_generator'; +import { executeEditAction } from './edit_action_helpers'; + +// datasourceMap and visualizationMap setters/getters +export const [getVisualizationMap, setVisualizationMap] = createGetterSetter< + Record> +>('VisualizationMap', false); + +export const [getDatasourceMap, setDatasourceMap] = createGetterSetter< + Record> +>('DatasourceMap', false); + +export function isCreateActionCompatible(core: CoreStart) { + return core.uiSettings.get('discover:enableESQL'); +} + +export async function executeCreateAction({ + deps, + core, + createNewEmbeddable, + deleteEmbeddable, +}: { + deps: LensPluginStartDependencies; + core: CoreStart; + createNewEmbeddable: ( + embeddableFactory: EmbeddableFactory, + initialInput?: Partial, + dismissNotification?: boolean + ) => Promise; + deleteEmbeddable: (embeddableId: string) => void; +}) { + const isCompatibleAction = isCreateActionCompatible(core); + const defaultDataView = await deps.dataViews.getDefaultDataView({ + displayErrors: false, + }); + if (!isCompatibleAction || !defaultDataView) { + throw new IncompatibleActionError(); + } + const visualizationMap = getVisualizationMap(); + const datasourceMap = getDatasourceMap(); + + const defaultIndex = defaultDataView.getIndexPattern(); + const defaultEsqlQuery = { + esql: `from ${defaultIndex} | limit 10`, + }; + + // For the suggestions api we need only the columns + // so we are requesting them with limit 0 + // this is much more performant than requesting + // all the table + const performantQuery = { + esql: `from ${defaultIndex} | limit 0`, + }; + + const table = await fetchDataFromAggregateQuery( + performantQuery, + defaultDataView, + deps.data, + deps.expressions + ); + + const context = { + dataViewSpec: defaultDataView.toSpec(), + fieldName: '', + textBasedColumns: table?.columns, + query: defaultEsqlQuery, + }; + + // get the initial attributes from the suggestions api + const allSuggestions = + suggestionsApi({ context, dataView: defaultDataView, datasourceMap, visualizationMap }) ?? []; + + // Lens might not return suggestions for some cases, i.e. in case of errors + if (!allSuggestions.length) return undefined; + const [firstSuggestion] = allSuggestions; + const attrs = getLensAttributes({ + filters: [], + query: defaultEsqlQuery, + suggestion: firstSuggestion, + dataView: defaultDataView, + }); + + const input = { + attributes: attrs, + id: generateId(), + }; + const embeddableStart = deps.embeddable; + const factory = embeddableStart.getEmbeddableFactory('lens'); + if (!factory) { + return undefined; + } + const embeddable = await createNewEmbeddable(factory, input, true); + // open the flyout if embeddable has been created successfully + if (embeddable) { + const deletePanel = () => { + deleteEmbeddable(embeddable.id); + }; + + executeEditAction({ + embeddable, + startDependencies: deps, + overlays: core.overlays, + theme: core.theme, + isNewPanel: true, + deletePanel, + }); + } +} diff --git a/x-pack/plugins/lens/public/trigger_actions/open_lens_config/action.test.tsx b/x-pack/plugins/lens/public/trigger_actions/open_lens_config/edit_action.test.tsx similarity index 98% rename from x-pack/plugins/lens/public/trigger_actions/open_lens_config/action.test.tsx rename to x-pack/plugins/lens/public/trigger_actions/open_lens_config/edit_action.test.tsx index d8dbc219c6c17..e1ab0f715cac5 100644 --- a/x-pack/plugins/lens/public/trigger_actions/open_lens_config/action.test.tsx +++ b/x-pack/plugins/lens/public/trigger_actions/open_lens_config/edit_action.test.tsx @@ -12,7 +12,7 @@ import { themeServiceMock } from '@kbn/core-theme-browser-mocks'; import type { LensPluginStartDependencies } from '../../plugin'; import { createMockStartDependencies } from '../../editor_frame_service/mocks'; import { DOC_TYPE } from '../../../common/constants'; -import { ConfigureInLensPanelAction } from './action'; +import { ConfigureInLensPanelAction } from './edit_action'; describe('open config panel action', () => { const overlays = overlayServiceMock.createStartContract(); diff --git a/x-pack/plugins/lens/public/trigger_actions/open_lens_config/action.tsx b/x-pack/plugins/lens/public/trigger_actions/open_lens_config/edit_action.tsx similarity index 88% rename from x-pack/plugins/lens/public/trigger_actions/open_lens_config/action.tsx rename to x-pack/plugins/lens/public/trigger_actions/open_lens_config/edit_action.tsx index 376cae58d1339..c4960532bdd66 100644 --- a/x-pack/plugins/lens/public/trigger_actions/open_lens_config/action.tsx +++ b/x-pack/plugins/lens/public/trigger_actions/open_lens_config/edit_action.tsx @@ -43,13 +43,13 @@ export class ConfigureInLensPanelAction implements Action { } public async isCompatible({ embeddable }: Context) { - const { isActionCompatible } = await getConfigureLensHelpersAsync(); - return isActionCompatible(embeddable); + const { isEditActionCompatible } = await getConfigureLensHelpersAsync(); + return isEditActionCompatible(embeddable); } public async execute({ embeddable }: Context) { - const { executeAction } = await getConfigureLensHelpersAsync(); - return executeAction({ + const { executeEditAction } = await getConfigureLensHelpersAsync(); + return executeEditAction({ embeddable, startDependencies: this.startDependencies, overlays: this.overlays, diff --git a/x-pack/plugins/lens/public/trigger_actions/open_lens_config/helpers.ts b/x-pack/plugins/lens/public/trigger_actions/open_lens_config/edit_action_helpers.ts similarity index 85% rename from x-pack/plugins/lens/public/trigger_actions/open_lens_config/helpers.ts rename to x-pack/plugins/lens/public/trigger_actions/open_lens_config/edit_action_helpers.ts index 8fd011fddfb2e..38f1aadc2c576 100644 --- a/x-pack/plugins/lens/public/trigger_actions/open_lens_config/helpers.ts +++ b/x-pack/plugins/lens/public/trigger_actions/open_lens_config/edit_action_helpers.ts @@ -18,22 +18,31 @@ interface Context { startDependencies: LensPluginStartDependencies; overlays: OverlayStart; theme: ThemeServiceStart; + isNewPanel?: boolean; + deletePanel?: () => void; } -export async function isActionCompatible(embeddable: IEmbeddable) { +export async function isEditActionCompatible(embeddable: IEmbeddable) { // display the action only if dashboard is on editable mode const inDashboardEditMode = embeddable.getInput().viewMode === 'edit'; return Boolean(isLensEmbeddable(embeddable) && embeddable.getIsEditable() && inDashboardEditMode); } -export async function executeAction({ embeddable, startDependencies, overlays, theme }: Context) { - const isCompatibleAction = await isActionCompatible(embeddable); +export async function executeEditAction({ + embeddable, + startDependencies, + overlays, + theme, + isNewPanel, + deletePanel, +}: Context) { + const isCompatibleAction = await isEditActionCompatible(embeddable); if (!isCompatibleAction || !isLensEmbeddable(embeddable)) { throw new IncompatibleActionError(); } const rootEmbeddable = embeddable.getRoot(); const overlayTracker = tracksOverlays(rootEmbeddable) ? rootEmbeddable : undefined; - const ConfigPanel = await embeddable.openConfingPanel(startDependencies); + const ConfigPanel = await embeddable.openConfingPanel(startDependencies, isNewPanel, deletePanel); if (ConfigPanel) { const handle = overlays.openFlyout( toMountPoint(