diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md index 5a1ab83551d34..fd6ade88479af 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md @@ -11,5 +11,5 @@ Signature: ```typescript -readonly addTriggerAction: (triggerId: T, action: ActionDefinition | Action) => void; +readonly addTriggerAction: (triggerId: T, action: ActionDefinition | Action) => void; ``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.getaction.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.getaction.md index 5b0b3eea01cb1..d540de7637441 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.getaction.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.getaction.md @@ -7,5 +7,5 @@ Signature: ```typescript -readonly getAction: >(id: string) => Action, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION">; +readonly getAction: >(id: string) => Action, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">; ``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggeractions.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggeractions.md index 2dda422046318..0a9b674a45de2 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggeractions.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggeractions.md @@ -7,5 +7,5 @@ Signature: ```typescript -readonly getTriggerActions: (triggerId: T) => Action[]; +readonly getTriggerActions: (triggerId: T) => Action[]; ``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggercompatibleactions.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggercompatibleactions.md index e087753726a8a..faed81236342d 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggercompatibleactions.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggercompatibleactions.md @@ -7,5 +7,5 @@ Signature: ```typescript -readonly getTriggerCompatibleActions: (triggerId: T, context: TriggerContextMapping[T]) => Promise[]>; +readonly getTriggerCompatibleActions: (triggerId: T, context: TriggerContextMapping[T]) => Promise[]>; ``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.md index f9eb693b492f7..e3c5dbb92ae90 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.md @@ -21,19 +21,19 @@ export declare class UiActionsService | Property | Modifiers | Type | Description | | --- | --- | --- | --- | | [actions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.actions.md) | | ActionRegistry | | -| [addTriggerAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md) | | <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, action: ActionDefinition<TriggerContextMapping[T]> | Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION">) => void | addTriggerAction is similar to attachAction as it attaches action to a trigger, but it also registers the action, if it has not been registered, yet.addTriggerAction also infers better typing of the action argument. | +| [addTriggerAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md) | | <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, action: ActionDefinition<TriggerContextMapping[T]> | Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">) => void | addTriggerAction is similar to attachAction as it attaches action to a trigger, but it also registers the action, if it has not been registered, yet.addTriggerAction also infers better typing of the action argument. | | [attachAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.attachaction.md) | | <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, actionId: string) => void | | | [clear](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.clear.md) | | () => void | Removes all registered triggers and actions. | | [detachAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.detachaction.md) | | (triggerId: TriggerId, actionId: string) => void | | | [executeTriggerActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.executetriggeractions.md) | | <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, context: TriggerContext<T>) => Promise<void> | | | [executionService](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.executionservice.md) | | UiActionsExecutionService | | | [fork](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.fork.md) | | () => UiActionsService | "Fork" a separate instance of UiActionsService that inherits all existing triggers and actions, but going forward all new triggers and actions added to this instance of UiActionsService are only available within this instance. | -| [getAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.getaction.md) | | <T extends ActionDefinition<{}>>(id: string) => Action<ActionContext<T>, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION"> | | +| [getAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.getaction.md) | | <T extends ActionDefinition<{}>>(id: string) => Action<ActionContext<T>, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV"> | | | [getTrigger](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettrigger.md) | | <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T) => TriggerContract<T> | | -| [getTriggerActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggeractions.md) | | <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T) => Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION">[] | | -| [getTriggerCompatibleActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggercompatibleactions.md) | | <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, context: TriggerContextMapping[T]) => Promise<Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION">[]> | | +| [getTriggerActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggeractions.md) | | <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T) => Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">[] | | +| [getTriggerCompatibleActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggercompatibleactions.md) | | <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, context: TriggerContextMapping[T]) => Promise<Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">[]> | | | [hasAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.hasaction.md) | | (actionId: string) => boolean | | -| [registerAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.registeraction.md) | | <A extends ActionDefinition<{}>>(definition: A) => Action<ActionContext<A>, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION"> | | +| [registerAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.registeraction.md) | | <A extends ActionDefinition<{}>>(definition: A) => Action<ActionContext<A>, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV"> | | | [registerTrigger](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.registertrigger.md) | | (trigger: Trigger) => void | | | [triggers](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.triggers.md) | | TriggerRegistry | | | [triggerToActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.triggertoactions.md) | | TriggerToActionsRegistry | | diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.registeraction.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.registeraction.md index bd340eb76fbac..6f03777e14552 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.registeraction.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.registeraction.md @@ -7,5 +7,5 @@ Signature: ```typescript -readonly registerAction: >(definition: A) => Action, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION">; +readonly registerAction: >(definition: A) => Action, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">; ``` diff --git a/docs/user/dashboard/download-underlying-data.asciidoc b/docs/user/dashboard/download-underlying-data.asciidoc new file mode 100644 index 0000000000000..78403ba797d78 --- /dev/null +++ b/docs/user/dashboard/download-underlying-data.asciidoc @@ -0,0 +1,15 @@ +[float] +[role="xpack"] +[[download_csv]] +=== Download CSV + +To download the underlying data of the Lens panels on your dashboard, you can use the *Download as CSV* option. + +TIP: The *Download as CSV* option supports multiple CSV file downloads from the same Lens visualization out of the box, if configured: for instance with multiple layers on a bar chart. + +To use the *Download as CSV* option: + +* Click the from the panel menu, then click *Download as CSV*. ++ +[role="screenshot"] +image::images/download_csv_context_menu.png[Download as CSV from panel context menu] \ No newline at end of file diff --git a/docs/user/dashboard/explore-dashboard-data.asciidoc b/docs/user/dashboard/explore-dashboard-data.asciidoc index 238dfb79e900b..66f91dc2bc18c 100644 --- a/docs/user/dashboard/explore-dashboard-data.asciidoc +++ b/docs/user/dashboard/explore-dashboard-data.asciidoc @@ -16,3 +16,4 @@ The data that displays depends on the element that you inspect. image:images/Dashboard_inspect.png[Inspect in dashboard] include::explore-underlying-data.asciidoc[] +include::download-underlying-data.asciidoc[] diff --git a/docs/user/dashboard/images/download_csv_context_menu.png b/docs/user/dashboard/images/download_csv_context_menu.png new file mode 100644 index 0000000000000..09f82b7812495 Binary files /dev/null and b/docs/user/dashboard/images/download_csv_context_menu.png differ diff --git a/src/plugins/dashboard/kibana.json b/src/plugins/dashboard/kibana.json index bd19a9f0d9cd3..b5451203e2365 100644 --- a/src/plugins/dashboard/kibana.json +++ b/src/plugins/dashboard/kibana.json @@ -9,9 +9,10 @@ "urlForwarding", "navigation", "uiActions", - "savedObjects" + "savedObjects", + "share" ], - "optionalPlugins": ["home", "share", "usageCollection", "savedObjectsTaggingOss"], + "optionalPlugins": ["home", "usageCollection", "savedObjectsTaggingOss"], "server": true, "ui": true, "requiredBundles": ["kibanaUtils", "kibanaReact", "home"] diff --git a/src/plugins/dashboard/public/application/actions/export_csv_action.test.tsx b/src/plugins/dashboard/public/application/actions/export_csv_action.test.tsx new file mode 100644 index 0000000000000..770e01d6190cb --- /dev/null +++ b/src/plugins/dashboard/public/application/actions/export_csv_action.test.tsx @@ -0,0 +1,134 @@ +/* + * 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 { CoreStart } from 'kibana/public'; + +import { isErrorEmbeddable, IContainer, ErrorEmbeddable } from '../../embeddable_plugin'; +import { DashboardContainer } from '../../application/embeddable'; +import { getSampleDashboardInput, getSampleDashboardPanel } from '../../application/test_helpers'; +import { + ContactCardEmbeddable, + ContactCardEmbeddableInput, + ContactCardEmbeddableOutput, + ContactCardExportableEmbeddableFactory, + CONTACT_CARD_EXPORTABLE_EMBEDDABLE, +} from '../../embeddable_plugin_test_samples'; +import { coreMock } from '../../../../../core/public/mocks'; +import { ExportCSVAction } from './export_csv_action'; +import { embeddablePluginMock } from '../../../../embeddable/public/mocks'; +import { DataPublicPluginStart } from '../../../../data/public/types'; +import { dataPluginMock } from '../../../../data/public/mocks'; +import { LINE_FEED_CHARACTER } from 'src/plugins/data/common/exports/export_csv'; + +describe('Export CSV action', () => { + const { setup, doStart } = embeddablePluginMock.createInstance(); + setup.registerEmbeddableFactory( + CONTACT_CARD_EXPORTABLE_EMBEDDABLE, + new ContactCardExportableEmbeddableFactory((() => null) as any, {} as any) + ); + const start = doStart(); + + let container: DashboardContainer; + let embeddable: ContactCardEmbeddable; + let coreStart: CoreStart; + let dataMock: jest.Mocked; + + beforeEach(async () => { + coreStart = coreMock.createStart(); + coreStart.savedObjects.client = { + ...coreStart.savedObjects.client, + get: jest.fn().mockImplementation(() => ({ attributes: { title: 'Holy moly' } })), + find: jest.fn().mockImplementation(() => ({ total: 15 })), + create: jest.fn().mockImplementation(() => ({ id: 'brandNewSavedObject' })), + }; + + const options = { + ExitFullScreenButton: () => null, + SavedObjectFinder: () => null, + application: {} as any, + embeddable: start, + inspector: {} as any, + notifications: {} as any, + overlays: coreStart.overlays, + savedObjectMetaData: {} as any, + uiActions: {} as any, + }; + const input = getSampleDashboardInput({ + panels: { + '123': getSampleDashboardPanel({ + explicitInput: { firstName: 'Kibanana', id: '123' }, + type: CONTACT_CARD_EXPORTABLE_EMBEDDABLE, + }), + }, + }); + container = new DashboardContainer(input, options); + dataMock = dataPluginMock.createStartContract(); + + const contactCardEmbeddable = await container.addNewEmbeddable< + ContactCardEmbeddableInput, + ContactCardEmbeddableOutput, + ContactCardEmbeddable + >(CONTACT_CARD_EXPORTABLE_EMBEDDABLE, { + firstName: 'Kibana', + }); + + if (isErrorEmbeddable(contactCardEmbeddable)) { + throw new Error('Failed to create embeddable'); + } else { + embeddable = contactCardEmbeddable; + } + }); + + test('Download is incompatible with embeddables without getInspectorAdapters implementation', async () => { + const action = new ExportCSVAction({ core: coreStart, data: dataMock }); + const errorEmbeddable = new ErrorEmbeddable( + 'Wow what an awful error', + { id: ' 404' }, + embeddable.getRoot() as IContainer + ); + expect(await action.isCompatible({ embeddable: errorEmbeddable })).toBe(false); + }); + + test('Should download a compatible Embeddable', async () => { + const action = new ExportCSVAction({ core: coreStart, data: dataMock }); + const result = ((await action.execute({ embeddable, asString: true })) as unknown) as + | undefined + | Record; + expect(result).toEqual({ + 'Hello Kibana.csv': { + content: `First Name,Last Name${LINE_FEED_CHARACTER}Kibana,undefined${LINE_FEED_CHARACTER}`, + type: 'text/plain;charset=utf-8', + }, + }); + }); + + test('Should not download incompatible Embeddable', async () => { + const action = new ExportCSVAction({ core: coreStart, data: dataMock }); + const errorEmbeddable = new ErrorEmbeddable( + 'Wow what an awful error', + { id: ' 404' }, + embeddable.getRoot() as IContainer + ); + const result = ((await action.execute({ + embeddable: errorEmbeddable, + asString: true, + })) as unknown) as undefined | Record; + expect(result).toBeUndefined(); + }); +}); diff --git a/src/plugins/dashboard/public/application/actions/export_csv_action.tsx b/src/plugins/dashboard/public/application/actions/export_csv_action.tsx new file mode 100644 index 0000000000000..48a7877f9383e --- /dev/null +++ b/src/plugins/dashboard/public/application/actions/export_csv_action.tsx @@ -0,0 +1,138 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { Datatable } from 'src/plugins/expressions/public'; +import { FormatFactory } from '../../../../data/common/field_formats/utils'; +import { DataPublicPluginStart, exporters } from '../../../../data/public'; +import { downloadMultipleAs } from '../../../../share/public'; +import { Adapters, IEmbeddable } from '../../../../embeddable/public'; +import { ActionByType } from '../../../../ui_actions/public'; +import { CoreStart } from '../../../../../core/public'; + +export const ACTION_EXPORT_CSV = 'ACTION_EXPORT_CSV'; + +export interface Params { + core: CoreStart; + data: DataPublicPluginStart; +} + +export interface ExportContext { + embeddable?: IEmbeddable; + // used for testing + asString?: boolean; +} + +/** + * This is "Export CSV" action which appears in the context + * menu of a dashboard panel. + */ +export class ExportCSVAction implements ActionByType { + public readonly id = ACTION_EXPORT_CSV; + + public readonly type = ACTION_EXPORT_CSV; + + public readonly order = 5; + + constructor(protected readonly params: Params) {} + + public getIconType() { + return 'exportAction'; + } + + public readonly getDisplayName = (context: ExportContext): string => + i18n.translate('dashboard.actions.DownloadCreateDrilldownAction.displayName', { + defaultMessage: 'Download as CSV', + }); + + public async isCompatible(context: ExportContext): Promise { + return !!this.hasDatatableContent(context.embeddable?.getInspectorAdapters?.()); + } + + private hasDatatableContent = (adapters: Adapters | undefined) => { + return Object.keys(adapters?.tables || {}).length > 0; + }; + + private getFormatter = (): FormatFactory | undefined => { + if (this.params.data) { + return this.params.data.fieldFormats.deserialize; + } + }; + + private getDataTableContent = (adapters: Adapters | undefined) => { + if (this.hasDatatableContent(adapters)) { + return adapters?.tables; + } + return; + }; + + private exportCSV = async (context: ExportContext) => { + const formatFactory = this.getFormatter(); + // early exit if not formatter is available + if (!formatFactory) { + return; + } + const tableAdapters = this.getDataTableContent( + context?.embeddable?.getInspectorAdapters() + ) as Record; + + if (tableAdapters) { + const datatables = Object.values(tableAdapters); + const content = datatables.reduce>( + (memo, datatable, i) => { + // skip empty datatables + if (datatable) { + const postFix = datatables.length > 1 ? `-${i + 1}` : ''; + const untitledFilename = i18n.translate( + 'dashboard.actions.downloadOptionsUnsavedFilename', + { + defaultMessage: 'unsaved', + } + ); + + memo[`${context!.embeddable!.getTitle() || untitledFilename}${postFix}.csv`] = { + content: exporters.datatableToCSV(datatable, { + csvSeparator: this.params.core.uiSettings.get('csv:separator', ','), + quoteValues: this.params.core.uiSettings.get('csv:quoteValues', true), + formatFactory, + }), + type: exporters.CSV_MIME_TYPE, + }; + } + return memo; + }, + {} + ); + + // useful for testing + if (context.asString) { + return (content as unknown) as Promise; + } + + if (content) { + return downloadMultipleAs(content); + } + } + }; + + public async execute(context: ExportContext): Promise { + // make it testable: type here will be forced + return await this.exportCSV(context); + } +} diff --git a/src/plugins/dashboard/public/application/actions/index.ts b/src/plugins/dashboard/public/application/actions/index.ts index cd32c2025456f..3d7ebe76cb66a 100644 --- a/src/plugins/dashboard/public/application/actions/index.ts +++ b/src/plugins/dashboard/public/application/actions/index.ts @@ -47,3 +47,4 @@ export { LibraryNotificationAction, ACTION_LIBRARY_NOTIFICATION, } from './library_notification_action'; +export { ExportContext, ExportCSVAction, ACTION_EXPORT_CSV } from './export_csv_action'; diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index c47a4c2d21b11..76b1ccc037e89 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -101,6 +101,11 @@ import { DashboardConstants } from './dashboard_constants'; import { addEmbeddableToDashboardUrl } from './url_utils/url_helper'; import { PlaceholderEmbeddableFactory } from './application/embeddable/placeholder'; import { UrlGeneratorState } from '../../share/public'; +import { + ACTION_EXPORT_CSV, + ExportContext, + ExportCSVAction, +} from './application/actions/export_csv_action'; declare module '../../share/public' { export interface UrlGeneratorStateMapping { @@ -160,6 +165,7 @@ declare module '../../../plugins/ui_actions/public' { [ACTION_ADD_TO_LIBRARY]: AddToLibraryActionContext; [ACTION_UNLINK_FROM_LIBRARY]: UnlinkFromLibraryActionContext; [ACTION_LIBRARY_NOTIFICATION]: LibraryNotificationActionContext; + [ACTION_EXPORT_CSV]: ExportContext; } } @@ -414,7 +420,7 @@ export class DashboardPlugin public start(core: CoreStart, plugins: StartDependencies): DashboardStart { const { notifications } = core; - const { uiActions } = plugins; + const { uiActions, data, share } = plugins; const SavedObjectFinder = getSavedObjectFinder(core.savedObjects, core.uiSettings); @@ -431,6 +437,11 @@ export class DashboardPlugin uiActions.registerAction(clonePanelAction); uiActions.attachAction(CONTEXT_MENU_TRIGGER, clonePanelAction.id); + if (share) { + const ExportCSVPlugin = new ExportCSVAction({ core, data }); + uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, ExportCSVPlugin); + } + if (this.dashboardFeatureFlagConfig?.allowByValueEmbeddables) { const addToLibraryAction = new AddToLibraryAction({ toasts: notifications.toasts }); uiActions.registerAction(addToLibraryAction); diff --git a/src/plugins/data/common/exports/export_csv.tsx b/src/plugins/data/common/exports/export_csv.tsx index 1e1420c245eb4..116586c5b66e8 100644 --- a/src/plugins/data/common/exports/export_csv.tsx +++ b/src/plugins/data/common/exports/export_csv.tsx @@ -22,7 +22,7 @@ import { FormatFactory } from 'src/plugins/data/common/field_formats/utils'; import { Datatable } from 'src/plugins/expressions'; -const LINE_FEED_CHARACTER = '\r\n'; +export const LINE_FEED_CHARACTER = '\r\n'; const nonAlphaNumRE = /[^a-zA-Z0-9]/; const allDoubleQuoteRE = /"/g; export const CSV_MIME_TYPE = 'text/plain;charset=utf-8'; diff --git a/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_exportable_embeddable.tsx b/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_exportable_embeddable.tsx new file mode 100644 index 0000000000000..338eb4877a50a --- /dev/null +++ b/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_exportable_embeddable.tsx @@ -0,0 +1,44 @@ +/* + * 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 { ContactCardEmbeddable } from './contact_card_embeddable'; + +export class ContactCardExportableEmbeddable extends ContactCardEmbeddable { + public getInspectorAdapters = () => { + return { + tables: { + layer1: { + type: 'datatable', + columns: [ + { id: 'firstName', name: 'First Name' }, + { id: 'originalLastName', name: 'Last Name' }, + ], + rows: [ + { + firstName: this.getInput().firstName, + orignialLastName: this.getInput().lastName, + }, + ], + }, + }, + }; + }; +} + +export const CONTACT_EXPORTABLE_USER_TRIGGER = 'CONTACT_EXPORTABLE_USER_TRIGGER'; diff --git a/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_exportable_embeddable_factory.tsx b/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_exportable_embeddable_factory.tsx new file mode 100644 index 0000000000000..5b8827ac6fc2a --- /dev/null +++ b/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_exportable_embeddable_factory.tsx @@ -0,0 +1,85 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { UiActionsStart } from 'src/plugins/ui_actions/public'; + +import { CoreStart } from 'src/core/public'; +import { toMountPoint } from '../../../../../../kibana_react/public'; +import { EmbeddableFactoryDefinition } from '../../../embeddables'; +import { Container } from '../../../containers'; +import { ContactCardEmbeddableInput } from './contact_card_embeddable'; +import { ContactCardExportableEmbeddable } from './contact_card_exportable_embeddable'; +import { ContactCardInitializer } from './contact_card_initializer'; + +export const CONTACT_CARD_EXPORTABLE_EMBEDDABLE = 'CONTACT_CARD_EXPORTABLE_EMBEDDABLE'; + +export class ContactCardExportableEmbeddableFactory + implements EmbeddableFactoryDefinition { + public readonly type = CONTACT_CARD_EXPORTABLE_EMBEDDABLE; + + constructor( + private readonly execTrigger: UiActionsStart['executeTriggerActions'], + private readonly overlays: CoreStart['overlays'] + ) {} + + public async isEditable() { + return true; + } + + public getDisplayName() { + return i18n.translate('embeddableApi.samples.contactCard.displayName', { + defaultMessage: 'contact card', + }); + } + + public getExplicitInput = (): Promise> => { + return new Promise((resolve) => { + const modalSession = this.overlays.openModal( + toMountPoint( + { + modalSession.close(); + // @ts-expect-error + resolve(undefined); + }} + onCreate={(input: { firstName: string; lastName?: string }) => { + modalSession.close(); + resolve(input); + }} + /> + ), + { + 'data-test-subj': 'createContactCardEmbeddable', + } + ); + }); + }; + + public create = async (initialInput: ContactCardEmbeddableInput, parent?: Container) => { + return new ContactCardExportableEmbeddable( + initialInput, + { + execAction: this.execTrigger, + }, + parent + ); + }; +} diff --git a/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/index.ts b/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/index.ts index c79a4f517916e..a9006cdc7b477 100644 --- a/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/index.ts +++ b/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/index.ts @@ -20,5 +20,7 @@ export * from './contact_card'; export * from './contact_card_embeddable'; export * from './contact_card_embeddable_factory'; +export * from './contact_card_exportable_embeddable'; +export * from './contact_card_exportable_embeddable_factory'; export * from './contact_card_initializer'; export * from './slow_contact_card_embeddable_factory'; diff --git a/src/plugins/ui_actions/public/public.api.md b/src/plugins/ui_actions/public/public.api.md index 3a14f49169e09..ca27e19b247c2 100644 --- a/src/plugins/ui_actions/public/public.api.md +++ b/src/plugins/ui_actions/public/public.api.md @@ -234,7 +234,7 @@ export class UiActionsService { // // (undocumented) protected readonly actions: ActionRegistry; - readonly addTriggerAction: (triggerId: T, action: UiActionsActionDefinition | Action) => void; + readonly addTriggerAction: (triggerId: T, action: UiActionsActionDefinition | Action) => void; // (undocumented) readonly attachAction: (triggerId: T, actionId: string) => void; readonly clear: () => void; @@ -248,21 +248,21 @@ export class UiActionsService { readonly executionService: UiActionsExecutionService; readonly fork: () => UiActionsService; // (undocumented) - readonly getAction: >(id: string) => Action, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION">; + readonly getAction: >(id: string) => Action, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">; // Warning: (ae-forgotten-export) The symbol "TriggerContract" needs to be exported by the entry point index.d.ts // // (undocumented) readonly getTrigger: (triggerId: T) => TriggerContract; // (undocumented) - readonly getTriggerActions: (triggerId: T) => Action[]; + readonly getTriggerActions: (triggerId: T) => Action[]; // (undocumented) - readonly getTriggerCompatibleActions: (triggerId: T, context: TriggerContextMapping[T]) => Promise[]>; + readonly getTriggerCompatibleActions: (triggerId: T, context: TriggerContextMapping[T]) => Promise[]>; // (undocumented) readonly hasAction: (actionId: string) => boolean; // Warning: (ae-forgotten-export) The symbol "ActionContext" needs to be exported by the entry point index.d.ts // // (undocumented) - readonly registerAction: >(definition: A) => Action, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION">; + readonly registerAction: >(definition: A) => Action, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">; // (undocumented) readonly registerTrigger: (trigger: Trigger) => void; // Warning: (ae-forgotten-export) The symbol "TriggerRegistry" needs to be exported by the entry point index.d.ts diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx index 76276f8b4c828..56d471be63d3e 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx @@ -44,6 +44,7 @@ import { IndexPatternsContract } from '../../../../../../src/plugins/data/public import { getEditPath, DOC_TYPE } from '../../../common'; import { IBasePath } from '../../../../../../src/core/public'; import { LensAttributeService } from '../../lens_attribute_service'; +import { LensInspectorAdapters } from '../types'; export type LensSavedObjectAttributes = Omit; @@ -84,6 +85,7 @@ export class Embeddable private subscription: Subscription; private autoRefreshFetchSubscription: Subscription; private isInitialized = false; + private activeData: LensInspectorAdapters | undefined; private externalSearchContext: { timeRange?: TimeRange; @@ -131,6 +133,10 @@ export class Embeddable } } + public getInspectorAdapters() { + return this.activeData; + } + async initializeSavedVis(input: LensEmbeddableInput) { const attributes: | LensSavedObjectAttributes @@ -175,6 +181,13 @@ export class Embeddable } } + private updateActiveData = ( + data: unknown, + inspectorAdapters?: LensInspectorAdapters | undefined + ) => { + this.activeData = inspectorAdapters; + }; + /** * * @param {HTMLElement} domNode @@ -194,6 +207,7 @@ export class Embeddable variables={input.palette ? { theme: { palette: input.palette } } : {}} searchSessionId={this.input.searchSessionId} handleEvent={this.handleEvent} + onData$={this.updateActiveData} renderMode={input.renderMode} />, domNode diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx index d18372246b0e6..4645420898314 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx @@ -15,6 +15,7 @@ import { import { ExecutionContextSearch } from 'src/plugins/data/public'; import { RenderMode } from 'src/plugins/expressions'; import { getOriginalRequestErrorMessage } from '../error_helper'; +import { LensInspectorAdapters } from '../types'; export interface ExpressionWrapperProps { ExpressionRenderer: ReactExpressionRendererType; @@ -23,6 +24,7 @@ export interface ExpressionWrapperProps { searchContext: ExecutionContextSearch; searchSessionId?: string; handleEvent: (event: ExpressionRendererEvent) => void; + onData$: (data: unknown, inspectorAdapters?: LensInspectorAdapters | undefined) => void; renderMode?: RenderMode; } @@ -33,6 +35,7 @@ export function ExpressionWrapper({ variables, handleEvent, searchSessionId, + onData$, renderMode, }: ExpressionWrapperProps) { return ( @@ -60,6 +63,7 @@ export function ExpressionWrapper({ expression={expression} searchContext={searchContext} searchSessionId={searchSessionId} + onData$={onData$} renderMode={renderMode} renderError={(errorMessage, error) => (
diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index 2f9310ee24ae9..24075facb68eb 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -5,13 +5,16 @@ */ import { AppMountParameters, CoreSetup, CoreStart } from 'kibana/public'; -import { DataPublicPluginSetup, DataPublicPluginStart } from 'src/plugins/data/public'; -import { EmbeddableSetup, EmbeddableStart } from 'src/plugins/embeddable/public'; -import { DashboardStart } from 'src/plugins/dashboard/public'; -import { ExpressionsSetup, ExpressionsStart } from 'src/plugins/expressions/public'; -import { VisualizationsSetup, VisualizationsStart } from 'src/plugins/visualizations/public'; -import { NavigationPublicPluginStart } from 'src/plugins/navigation/public'; -import { UrlForwardingSetup } from 'src/plugins/url_forwarding/public'; +import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../../src/plugins/data/public'; +import { EmbeddableSetup, EmbeddableStart } from '../../../../src/plugins/embeddable/public'; +import { DashboardStart } from '../../../../src/plugins/dashboard/public'; +import { ExpressionsSetup, ExpressionsStart } from '../../../../src/plugins/expressions/public'; +import { + VisualizationsSetup, + VisualizationsStart, +} from '../../../../src/plugins/visualizations/public'; +import { NavigationPublicPluginStart } from '../../../../src/plugins/navigation/public'; +import { UrlForwardingSetup } from '../../../../src/plugins/url_forwarding/public'; import { GlobalSearchPluginSetup } from '../../global_search/public'; import { ChartsPluginSetup, ChartsPluginStart } from '../../../../src/plugins/charts/public'; import { EditorFrameService } from './editor_frame_service'; @@ -64,6 +67,7 @@ export interface LensPluginStartDependencies { charts: ChartsPluginStart; savedObjectsTagging?: SavedObjectTaggingPluginStart; } + export class LensPlugin { private datatableVisualization: DatatableVisualization; private editorFrameService: EditorFrameService; diff --git a/x-pack/test/functional/apps/lens/dashboard.ts b/x-pack/test/functional/apps/lens/dashboard.ts index 17b70b8510f04..c332d05039255 100644 --- a/x-pack/test/functional/apps/lens/dashboard.ts +++ b/x-pack/test/functional/apps/lens/dashboard.ts @@ -140,5 +140,20 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const hasGeoSrcFilter = await filterBar.hasFilter('geo.src', 'US', true, true); expect(hasGeoSrcFilter).to.be(true); }); + + it('CSV export action exists in panel context menu', async () => { + const ACTION_ID = 'ACTION_EXPORT_CSV'; + const ACTION_TEST_SUBJ = `embeddablePanelAction-${ACTION_ID}`; + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.clickNewDashboard(); + await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.filterEmbeddableNames('lnsPieVis'); + await find.clickByButtonText('lnsPieVis'); + await dashboardAddPanel.closeAddPanel(); + + await panelActions.openContextMenu(); + await panelActions.clickContextMenuMoreItem(); + await testSubjects.existOrFail(ACTION_TEST_SUBJ); + }); }); }