From 078e554dcc204609ec4c61253a566f5f07963adc Mon Sep 17 00:00:00 2001 From: dej611 Date: Mon, 16 Nov 2020 11:15:43 +0100 Subject: [PATCH 01/44] :sparkles: First draft of csv export in Lens --- x-pack/plugins/lens/public/app_plugin/app.tsx | 9 ++ .../lens/public/app_plugin/lens_top_nav.tsx | 16 ++- .../plugins/lens/public/app_plugin/types.ts | 3 + .../editor_frame/editor_frame.tsx | 1 + .../editor_frame/export_csv.tsx | 108 ++++++++++++++++++ .../editor_frame_service/editor_frame/save.ts | 1 + .../editor_frame/state_management.ts | 2 +- .../public/persistence/saved_object_store.ts | 2 + 8 files changed, 140 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/lens/public/editor_frame_service/editor_frame/export_csv.tsx diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index cdd701271be2c..3a09f1d0252a2 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -37,6 +37,7 @@ import { LensByReferenceInput, LensEmbeddableInput, } from '../editor_frame_service/embeddable/embeddable'; +import { exportAsCSVs } from '../editor_frame_service/editor_frame/export_csv'; export function App({ history, @@ -480,10 +481,18 @@ export function App({ // Temporarily required until the 'by value' paradigm is default. (dashboardFeatureFlag.allowByValueEmbeddables || Boolean(initialInput)) ), + showExportToCSV: Boolean(lastKnownDoc?.state.activeData), isByValueMode: getIsByValueMode(), showCancel: Boolean(state.isLinkedToOriginatingApp), savingPermitted, actions: { + exportToCSV: () => { + exportAsCSVs(lastKnownDoc?.title || 'unsaved', lastKnownDoc?.state.activeData, { + csvSeparator: uiSettings.get('csv:separator', ','), + quoteValues: uiSettings.get('csv:quoteValues', true), + formatFactory: data.fieldFormats.deserialize, + }); + }, saveAndReturn: () => { if (savingPermitted && lastKnownDoc) { // disabling the validation on app leave because the document has been saved. diff --git a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx index 9162af52052ee..f1b72c39fa9d4 100644 --- a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx +++ b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx @@ -10,12 +10,13 @@ import { LensTopNavActions } from './types'; export function getLensTopNavConfig(options: { showSaveAndReturn: boolean; + showExportToCSV: boolean; showCancel: boolean; isByValueMode: boolean; actions: LensTopNavActions; savingPermitted: boolean; }): TopNavMenuData[] { - const { showSaveAndReturn, showCancel, actions, savingPermitted } = options; + const { showSaveAndReturn, showCancel, actions, savingPermitted, showExportToCSV } = options; const topNavMenu: TopNavMenuData[] = []; const saveButtonLabel = options.isByValueMode @@ -43,6 +44,19 @@ export function getLensTopNavConfig(options: { }); } + if (showExportToCSV) { + topNavMenu.push({ + label: i18n.translate('xpack.lens.app.downloadCSV', { + defaultMessage: 'Download as CSV', + }), + run: actions.exportToCSV, + testId: 'lnsApp_downloadCSVButton', + description: i18n.translate('xpack.lens.app.cancelButtonAriaLabel', { + defaultMessage: 'Download the data as CSV file', + }), + }); + } + topNavMenu.push({ label: saveButtonLabel, iconType: !showSaveAndReturn ? 'save' : undefined, diff --git a/x-pack/plugins/lens/public/app_plugin/types.ts b/x-pack/plugins/lens/public/app_plugin/types.ts index 6c222bed7a83f..07dc69078e337 100644 --- a/x-pack/plugins/lens/public/app_plugin/types.ts +++ b/x-pack/plugins/lens/public/app_plugin/types.ts @@ -34,6 +34,7 @@ import { ACTION_VISUALIZE_LENS_FIELD, } from '../../../../../src/plugins/ui_actions/public'; import { EmbeddableEditorState } from '../../../../../src/plugins/embeddable/public'; +import { TableInspectorAdapter } from '../editor_frame_service/types'; import { EditorFrameInstance } from '..'; export interface LensAppState { @@ -60,6 +61,7 @@ export interface LensAppState { filters: Filter[]; savedQuery?: SavedQuery; isSaveable: boolean; + activeData?: TableInspectorAdapter; } export interface RedirectToOriginProps { @@ -111,4 +113,5 @@ export interface LensTopNavActions { saveAndReturn: () => void; showSaveModal: () => void; cancel: () => void; + exportToCSV: () => void; } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx index 935d65bfb6c08..fea9723aa700d 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx @@ -244,6 +244,7 @@ export function EditorFrame(props: EditorFrameProps) { activeVisualization, state.datasourceStates, state.visualization, + state.activeData, props.query, props.dateRange, props.filters, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/export_csv.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/export_csv.tsx new file mode 100644 index 0000000000000..9a3e15db1957c --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/export_csv.tsx @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// Inspired by the inspector CSV exporter + +// @ts-ignore +import { saveAs } from '@elastic/filesaver'; +import pMap from 'p-map'; + +import { FormatFactory } from 'src/plugins/data/common/field_formats/utils'; +import { Datatable } from 'src/plugins/expressions'; +import { TableInspectorAdapter } from '../types'; + +const LINE_FEED_CHARACTER = '\r\n'; +const nonAlphaNumRE = /[^a-zA-Z0-9]/; +const allDoubleQuoteRE = /"/g; + +// TODO: enhance this later on +function escape(val: object | string, quoteValues: boolean) { + if (val != null && typeof val === 'object') { + val = val.valueOf(); + } + + val = String(val); + + if (quoteValues && nonAlphaNumRE.test(val)) { + val = `"${val.replace(allDoubleQuoteRE, '""')}"`; + } + + return val; +} + +interface CSVOptions { + csvSeparator: string; + quoteValues: boolean; + formatFactory: FormatFactory; +} + +function buildCSV( + { columns, rows }: Datatable, + { csvSeparator, quoteValues, formatFactory }: CSVOptions +) { + // Build the header row by its names + const header = columns.map((col) => escape(col.name, quoteValues)); + + const formatters = columns.reduce>>( + (memo, { id, meta }) => { + memo[id] = formatFactory(meta?.params); + return memo; + }, + {} + ); + + // Convert the array of row objects to an array of row arrays + const csvRows = rows.map((row) => { + return columns.map((column) => + escape(formatters[column.id].convert(row[column.id]), quoteValues) + ); + }); + + return ( + [header, ...csvRows].map((row) => row.join(csvSeparator)).join(LINE_FEED_CHARACTER) + + LINE_FEED_CHARACTER + ); // Add \r\n after last line +} + +export function exportAsCSVs( + filename: string, + datatables: TableInspectorAdapter = {}, + options: CSVOptions +) { + // build a csv for datatable layer + const csvs = Object.keys(datatables) + .filter((layerId) => { + return ( + datatables[layerId].columns.length && + datatables[layerId].rows.length && + datatables[layerId].rows.every((row) => Object.keys(row).length) + ); + }) + .reduce>((memo, layerId) => { + memo[layerId] = buildCSV(datatables[layerId], options); + return memo; + }, {}); + + const layerIds = Object.keys(csvs); + + const downloadQueue = layerIds.map((layerId, i) => { + const blob = new Blob([csvs[layerId]]); + const postFix = layerIds.length > 1 ? `-${i + 1}` : ''; + // TODO: remove this workaround for multiple files when fixed (in filesaver?) + return () => Promise.resolve().then(() => saveAs(blob, `${filename}${postFix}.csv`)); + }); + + // There's a bug in some browser with multiple files downloaded at once + // * sometimes only the first/last content is downloaded multiple times + // * sometimes only the first/last filename is used multiple times + pMap(downloadQueue, (downloadFn) => Promise.all([downloadFn(), wait(50)]), { + concurrency: 1, + }); +} +// Probably there's already another one around? +function wait(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.ts index 4cb523f128a8c..8cb4e5bf56110 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.ts @@ -69,6 +69,7 @@ export function getSavedObjectFormat({ visualization: state.visualization.state, query: framePublicAPI.query, filters: persistableFilters, + activeData: state.activeData, }, references, }, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.ts index e0101493b27aa..55a4cb567fda1 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.ts @@ -148,7 +148,7 @@ export const reducer = (state: EditorFrameState, action: Action): EditorFrameSta case 'UPDATE_ACTIVE_DATA': return { ...state, - activeData: action.tables, + activeData: { ...action.tables }, }; case 'UPDATE_LAYER': return { diff --git a/x-pack/plugins/lens/public/persistence/saved_object_store.ts b/x-pack/plugins/lens/public/persistence/saved_object_store.ts index 2d293d4e0a5a0..ef3067e769173 100644 --- a/x-pack/plugins/lens/public/persistence/saved_object_store.ts +++ b/x-pack/plugins/lens/public/persistence/saved_object_store.ts @@ -11,6 +11,7 @@ import { } from 'kibana/public'; import { Query } from '../../../../../src/plugins/data/public'; import { DOC_TYPE, PersistableFilter } from '../../common'; +import { TableInspectorAdapter } from '../editor_frame_service/types'; export interface Document { savedObjectId?: string; @@ -27,6 +28,7 @@ export interface Document { state?: unknown; }; filters: PersistableFilter[]; + activeData?: TableInspectorAdapter; }; references: SavedObjectReference[]; } From 5d789d644d62386431a22c988ffb3d8803022b31 Mon Sep 17 00:00:00 2001 From: dej611 Date: Mon, 16 Nov 2020 14:33:42 +0100 Subject: [PATCH 02/44] :alembic: Create action stub --- .../embeddable/export_csv_action.tsx | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 x-pack/plugins/lens/public/editor_frame_service/embeddable/export_csv_action.tsx diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/export_csv_action.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/export_csv_action.tsx new file mode 100644 index 0000000000000..4bf14ef02a491 --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/export_csv_action.tsx @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { IEmbeddable } from 'src/plugins/embeddable/public'; +// import { StartServicesGetter } from 'src/plugins/kibana_utils/public'; +import { Action } from 'src/plugins/ui_actions/public'; + +export const ACTION_EXPORT_CSV = 'ACTION_EXPORT_CSV'; + +export interface Params { + // start: StartServicesGetter; +} + +interface ExportContext { + embeddable?: IEmbeddable; +} + +/** + * This is "Export CSV" action which appears in the context + * menu of a dashboard panel. + */ +export class ExportCSVAction implements Action { + public readonly id = ACTION_EXPORT_CSV; + + public readonly type = ACTION_EXPORT_CSV; + + public readonly order = 200; + + constructor(protected readonly params: Params) {} + + public getIconType() { + return 'exportAction'; + } + + public readonly getDisplayName = (context: ExportContext): string => + i18n.translate('xpack.lens.DownloadCreateDrilldownAction.displayName', { + defaultMessage: 'Download as CSV', + }); + + public async isCompatible(context: ExportContext): Promise { + return context.embeddable?.type === 'lens'; + } + + protected readonly exportCSV = async (context: ExportContext): Promise => { + // Call the Export CSV method on Lens here + console.log('Export CSV'); + }; + + public async execute(context: ExportContext): Promise { + // const { core } = this.params.start(); + + await this.exportCSV(context); + } +} From 965bb697f47690edf146426e35d96eba33650d1a Mon Sep 17 00:00:00 2001 From: dej611 Date: Tue, 17 Nov 2020 10:23:00 +0100 Subject: [PATCH 03/44] :truck: + :white_check_mark: Move export csv to data plugins + basic testing --- .../data/public/exports/export_csv.test.ts | 116 ++++++++++++++++++ .../data/public/exports}/export_csv.tsx | 43 +++++-- src/plugins/data/public/exports/index.ts | 20 +++ src/plugins/data/public/index.ts | 6 + x-pack/plugins/lens/public/app_plugin/app.tsx | 4 +- 5 files changed, 179 insertions(+), 10 deletions(-) create mode 100644 src/plugins/data/public/exports/export_csv.test.ts rename {x-pack/plugins/lens/public/editor_frame_service/editor_frame => src/plugins/data/public/exports}/export_csv.tsx (65%) create mode 100644 src/plugins/data/public/exports/index.ts diff --git a/src/plugins/data/public/exports/export_csv.test.ts b/src/plugins/data/public/exports/export_csv.test.ts new file mode 100644 index 0000000000000..f6d8889fed7bd --- /dev/null +++ b/src/plugins/data/public/exports/export_csv.test.ts @@ -0,0 +1,116 @@ +/* + * 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 { Datatable } from 'src/plugins/expressions'; +import { FieldFormat } from '../../common/field_formats'; +import { exportAsCSVs } from './export_csv'; + +function getDefaultOptions() { + const formatFactory = jest.fn(); + formatFactory.mockReturnValue({ convert: (v: unknown) => `Formatted_${v}` } as FieldFormat); + return { + csvSeparator: ',', + quoteValues: true, + formatFactory, + // this is for testing + asString: true, + }; +} + +function getDataTable({ + multipleLayers, + multipleColumns, +}: { multipleLayers?: boolean; multipleColumns?: boolean } = {}): Record { + const datatables: Record = { + layer1: { + type: 'datatable', + columns: [{ id: 'col1', name: 'columnOne', meta: { type: 'string' } }], + rows: [{ col1: 'value' }], + }, + }; + if (multipleColumns) { + datatables.layer1.columns.push({ id: 'col2', name: 'columnTwo', meta: { type: 'number' } }); + datatables.layer1.rows[0].col2 = 5; + } + if (multipleLayers) { + datatables.layer2 = { + type: 'datatable', + columns: [{ id: 'col1', name: 'columnOne', meta: { type: 'string' } }], + rows: [{ col1: 'value' }], + }; + } + return datatables; +} + +describe('CSV exporter', () => { + test('should do nothing with no data', () => { + expect(exportAsCSVs('noData', undefined, getDefaultOptions())).toStrictEqual(undefined); + }); + + test('should not break with empty data', () => { + expect(exportAsCSVs('emptyFile', {}, getDefaultOptions())).toStrictEqual({}); + }); + + test('should export formatted values by default', () => { + expect(exportAsCSVs('oneCSV', getDataTable(), getDefaultOptions())).toStrictEqual({ + 'oneCSV.csv': 'columnOne\r\n"Formatted_value"\r\n', + }); + }); + + test('should not quote values when requested', () => { + return expect( + exportAsCSVs('oneCSV', getDataTable(), { ...getDefaultOptions(), quoteValues: false }) + ).toStrictEqual({ + 'oneCSV.csv': 'columnOne\r\nFormatted_value\r\n', + }); + }); + + test('should use raw values when requested', () => { + expect( + exportAsCSVs('oneCSV', getDataTable(), { ...getDefaultOptions(), raw: true }) + ).toStrictEqual({ + 'oneCSV.csv': 'columnOne\r\nvalue\r\n', + }); + }); + + test('should use separator for multiple columns', () => { + expect( + exportAsCSVs('oneCSV', getDataTable({ multipleColumns: true }), getDefaultOptions()) + ).toStrictEqual({ + 'oneCSV.csv': 'columnOne,columnTwo\r\n"Formatted_value","Formatted_5"\r\n', + }); + }); + + test('should support multiple layers', () => { + expect( + exportAsCSVs('twoCSVs', getDataTable({ multipleLayers: true }), getDefaultOptions()) + ).toStrictEqual({ + 'twoCSVs-1.csv': 'columnOne\r\n"Formatted_value"\r\n', + 'twoCSVs-2.csv': 'columnOne\r\n"Formatted_value"\r\n', + }); + }); + + test('should escape values', () => { + const datatables = getDataTable(); + datatables.layer1.rows[0].col1 = '"value"'; + expect(exportAsCSVs('oneCSV', datatables, getDefaultOptions())).toStrictEqual({ + 'oneCSV.csv': 'columnOne\r\n"Formatted_""value"""\r\n', + }); + }); +}); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/export_csv.tsx b/src/plugins/data/public/exports/export_csv.tsx similarity index 65% rename from x-pack/plugins/lens/public/editor_frame_service/editor_frame/export_csv.tsx rename to src/plugins/data/public/exports/export_csv.tsx index 9a3e15db1957c..634c0c91ac1d4 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/export_csv.tsx +++ b/src/plugins/data/public/exports/export_csv.tsx @@ -1,7 +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; - * you may not use this file except in compliance with the Elastic License. + * 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. */ // Inspired by the inspector CSV exporter @@ -12,7 +25,6 @@ import pMap from 'p-map'; import { FormatFactory } from 'src/plugins/data/common/field_formats/utils'; import { Datatable } from 'src/plugins/expressions'; -import { TableInspectorAdapter } from '../types'; const LINE_FEED_CHARACTER = '\r\n'; const nonAlphaNumRE = /[^a-zA-Z0-9]/; @@ -37,11 +49,13 @@ interface CSVOptions { csvSeparator: string; quoteValues: boolean; formatFactory: FormatFactory; + raw?: boolean; + asString?: boolean; // use it for testing } function buildCSV( { columns, rows }: Datatable, - { csvSeparator, quoteValues, formatFactory }: CSVOptions + { csvSeparator, quoteValues, formatFactory, raw }: Omit ) { // Build the header row by its names const header = columns.map((col) => escape(col.name, quoteValues)); @@ -57,7 +71,7 @@ function buildCSV( // Convert the array of row objects to an array of row arrays const csvRows = rows.map((row) => { return columns.map((column) => - escape(formatters[column.id].convert(row[column.id]), quoteValues) + escape(raw ? row[column.id] : formatters[column.id].convert(row[column.id]), quoteValues) ); }); @@ -69,9 +83,12 @@ function buildCSV( export function exportAsCSVs( filename: string, - datatables: TableInspectorAdapter = {}, - options: CSVOptions + datatables: Record | undefined, + { asString, ...options }: CSVOptions ) { + if (datatables == null) { + return; + } // build a csv for datatable layer const csvs = Object.keys(datatables) .filter((layerId) => { @@ -88,6 +105,16 @@ export function exportAsCSVs( const layerIds = Object.keys(csvs); + // useful for testing + if (asString) { + return layerIds.reduce>((memo, layerId, i) => { + const content = csvs[layerId]; + const postFix = layerIds.length > 1 ? `-${i + 1}` : ''; + memo[`${filename}${postFix}.csv`] = content; + return memo; + }, {}); + } + const downloadQueue = layerIds.map((layerId, i) => { const blob = new Blob([csvs[layerId]]); const postFix = layerIds.length > 1 ? `-${i + 1}` : ''; diff --git a/src/plugins/data/public/exports/index.ts b/src/plugins/data/public/exports/index.ts new file mode 100644 index 0000000000000..6870acfe8547e --- /dev/null +++ b/src/plugins/data/public/exports/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './export_csv'; diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 129addf3de70e..9a536230ccb9e 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -212,6 +212,12 @@ export { FieldFormat, } from '../common'; +/** + * Exporters (CSV) + */ + +export * from './exports'; + /* * Index patterns: */ diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 3a09f1d0252a2..4585012c5423e 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -11,6 +11,7 @@ import React, { useState, useEffect, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { NotificationsStart } from 'kibana/public'; import { EuiBreadcrumb } from '@elastic/eui'; +import { exportAsCSVs } from '../../../../../src/plugins/data/public'; import { createKbnUrlStateStorage, withNotifyOnErrors, @@ -37,7 +38,6 @@ import { LensByReferenceInput, LensEmbeddableInput, } from '../editor_frame_service/embeddable/embeddable'; -import { exportAsCSVs } from '../editor_frame_service/editor_frame/export_csv'; export function App({ history, @@ -487,7 +487,7 @@ export function App({ savingPermitted, actions: { exportToCSV: () => { - exportAsCSVs(lastKnownDoc?.title || 'unsaved', lastKnownDoc?.state.activeData, { + exportAsCSVs(lastKnownDoc?.title || 'unsaved', lastKnownDoc?.state?.activeData, { csvSeparator: uiSettings.get('csv:separator', ','), quoteValues: uiSettings.get('csv:quoteValues', true), formatFactory: data.fieldFormats.deserialize, From 456d14aa43b88737cfc29b51773bd46aa86072b1 Mon Sep 17 00:00:00 2001 From: dej611 Date: Tue, 17 Nov 2020 10:27:33 +0100 Subject: [PATCH 04/44] :lipstick: Enable/disable rather than show/hide button --- x-pack/plugins/lens/public/app_plugin/app.tsx | 4 ++- .../lens/public/app_plugin/lens_top_nav.tsx | 27 +++++++++---------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 4585012c5423e..6bc09162f6056 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -481,7 +481,9 @@ export function App({ // Temporarily required until the 'by value' paradigm is default. (dashboardFeatureFlag.allowByValueEmbeddables || Boolean(initialInput)) ), - showExportToCSV: Boolean(lastKnownDoc?.state.activeData), + enableExportToCSV: Boolean( + lastKnownDoc?.state.activeData && Object.keys(lastKnownDoc?.state.activeData).length + ), isByValueMode: getIsByValueMode(), showCancel: Boolean(state.isLinkedToOriginatingApp), savingPermitted, diff --git a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx index f1b72c39fa9d4..52aac2608f088 100644 --- a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx +++ b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx @@ -10,13 +10,13 @@ import { LensTopNavActions } from './types'; export function getLensTopNavConfig(options: { showSaveAndReturn: boolean; - showExportToCSV: boolean; + enableExportToCSV: boolean; showCancel: boolean; isByValueMode: boolean; actions: LensTopNavActions; savingPermitted: boolean; }): TopNavMenuData[] { - const { showSaveAndReturn, showCancel, actions, savingPermitted, showExportToCSV } = options; + const { showSaveAndReturn, showCancel, actions, savingPermitted, enableExportToCSV } = options; const topNavMenu: TopNavMenuData[] = []; const saveButtonLabel = options.isByValueMode @@ -44,18 +44,17 @@ export function getLensTopNavConfig(options: { }); } - if (showExportToCSV) { - topNavMenu.push({ - label: i18n.translate('xpack.lens.app.downloadCSV', { - defaultMessage: 'Download as CSV', - }), - run: actions.exportToCSV, - testId: 'lnsApp_downloadCSVButton', - description: i18n.translate('xpack.lens.app.cancelButtonAriaLabel', { - defaultMessage: 'Download the data as CSV file', - }), - }); - } + topNavMenu.push({ + label: i18n.translate('xpack.lens.app.downloadCSV', { + defaultMessage: 'Download as CSV', + }), + run: actions.exportToCSV, + testId: 'lnsApp_downloadCSVButton', + description: i18n.translate('xpack.lens.app.cancelButtonAriaLabel', { + defaultMessage: 'Download the data as CSV file', + }), + disableButton: !enableExportToCSV, + }); topNavMenu.push({ label: saveButtonLabel, From 8f917b101fe85dcf8e723009b6965ec7d9cf9a2f Mon Sep 17 00:00:00 2001 From: dej611 Date: Tue, 17 Nov 2020 11:00:56 +0100 Subject: [PATCH 05/44] :bug: Fix tests --- x-pack/plugins/lens/public/app_plugin/app.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 6bc09162f6056..5780f1f9d78e4 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -482,7 +482,7 @@ export function App({ (dashboardFeatureFlag.allowByValueEmbeddables || Boolean(initialInput)) ), enableExportToCSV: Boolean( - lastKnownDoc?.state.activeData && Object.keys(lastKnownDoc?.state.activeData).length + lastKnownDoc?.state?.activeData && Object.keys(lastKnownDoc.state.activeData).length ), isByValueMode: getIsByValueMode(), showCancel: Boolean(state.isLinkedToOriginatingApp), From 851b98f32f4abe18449963fd10be485026879c52 Mon Sep 17 00:00:00 2001 From: dej611 Date: Tue, 17 Nov 2020 12:19:13 +0100 Subject: [PATCH 06/44] :speech_balloon: Fix i18n checks + :packages: Update API docs --- ...plugin-plugins-data-public.exportascsvs.md | 26 ++++++++++ .../kibana-plugin-plugins-data-public.md | 1 + .../data/public/exports/export_csv.tsx | 8 +++ src/plugins/data/public/public.api.md | 50 +++++++++++-------- .../lens/public/app_plugin/lens_top_nav.tsx | 2 +- 5 files changed, 65 insertions(+), 22 deletions(-) create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.exportascsvs.md diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.exportascsvs.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.exportascsvs.md new file mode 100644 index 0000000000000..c8bc128ea93a3 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.exportascsvs.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [exportAsCSVs](./kibana-plugin-plugins-data-public.exportascsvs.md) + +## exportAsCSVs() function + +Signature: + +```typescript +export declare function exportAsCSVs(filename: string, datatables: Record | undefined, { asString, ...options }: CSVOptions): Record | undefined; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| filename | string | filename to use (either as is, or as prefix for multiple CSVs) for the files to download | +| datatables | Record<string, Datatable> | undefined | data (as a dictionary of Datatable) to be translated into CSVs. It can contain multiple tables. | +| { asString, ...options } | CSVOptions | | + +Returns: + +`Record | undefined` + +undefined (download) - Record<string, string> (only for testing) + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index bafcd8bdffff9..dc7139f722bfe 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -41,6 +41,7 @@ | Function | Description | | --- | --- | +| [exportAsCSVs(filename, datatables, { asString, ...options })](./kibana-plugin-plugins-data-public.exportascsvs.md) | | | [getDefaultQuery(language)](./kibana-plugin-plugins-data-public.getdefaultquery.md) | | | [getEsPreference(uiSettings, sessionId)](./kibana-plugin-plugins-data-public.getespreference.md) | | | [getSearchParamsFromRequest(searchRequest, dependencies)](./kibana-plugin-plugins-data-public.getsearchparamsfromrequest.md) | | diff --git a/src/plugins/data/public/exports/export_csv.tsx b/src/plugins/data/public/exports/export_csv.tsx index 634c0c91ac1d4..a04f3c2c648bf 100644 --- a/src/plugins/data/public/exports/export_csv.tsx +++ b/src/plugins/data/public/exports/export_csv.tsx @@ -81,6 +81,14 @@ function buildCSV( ); // Add \r\n after last line } +/** + * + * @param filename - filename to use (either as is, or as prefix for multiple CSVs) for the files to download + * @param datatables - data (as a dictionary of Datatable) to be translated into CSVs. It can contain multiple tables. + * @param options - set of options for the exporter + * + * @returns undefined (download) - Record\ (only for testing) + */ export function exportAsCSVs( filename: string, datatables: Record | undefined, diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 78b974758f8c0..a09f64d737862 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -18,6 +18,7 @@ import { CoreSetup as CoreSetup_2 } from 'kibana/public'; import { CoreStart } from 'kibana/public'; import { CoreStart as CoreStart_2 } from 'src/core/public'; import { Datatable as Datatable_2 } from 'src/plugins/expressions/common'; +import { Datatable as Datatable_3 } from 'src/plugins/expressions'; import { DatatableColumn as DatatableColumn_2 } from 'src/plugins/expressions'; import { Ensure } from '@kbn/utility-types'; import { EnvironmentMode } from '@kbn/config'; @@ -34,6 +35,7 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { ExpressionFunctionDefinition as ExpressionFunctionDefinition_2 } from 'src/plugins/expressions/public'; import { ExpressionsSetup } from 'src/plugins/expressions/public'; import { ExpressionValueBoxed } from 'src/plugins/expressions/common'; +import { FormatFactory as FormatFactory_2 } from 'src/plugins/data/common/field_formats/utils'; import { History } from 'history'; import { Href } from 'history'; import { IconType } from '@elastic/eui'; @@ -669,6 +671,12 @@ export type ExistsFilter = Filter & { exists?: FilterExistsProperty; }; +// Warning: (ae-forgotten-export) The symbol "CSVOptions" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "exportAsCSVs" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export function exportAsCSVs(filename: string, datatables: Record | undefined, { asString, ...options }: CSVOptions): Record | undefined; + // Warning: (ae-missing-release-tag) "ExpressionFunctionKibana" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -2353,27 +2361,27 @@ export const UI_SETTINGS: { // src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "UrlFormat" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "StringFormat" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "getFromSavedObject" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:393:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:393:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:393:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:393:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:395:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:396:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:405:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:406:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:407:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:408:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:412:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:413:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:416:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:417:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:420:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:242:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:242:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:242:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:242:27 - (ae-forgotten-export) The symbol "getFromSavedObject" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:242:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:242:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:399:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:399:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:399:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:399:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:401:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:402:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:411:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:412:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:413:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:414:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:418:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:419:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:422:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:423:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:426:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:45:5 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx index 52aac2608f088..2c2fed8eaca79 100644 --- a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx +++ b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx @@ -50,7 +50,7 @@ export function getLensTopNavConfig(options: { }), run: actions.exportToCSV, testId: 'lnsApp_downloadCSVButton', - description: i18n.translate('xpack.lens.app.cancelButtonAriaLabel', { + description: i18n.translate('xpack.lens.app.downloadButtonAriaLabel', { defaultMessage: 'Download the data as CSV file', }), disableButton: !enableExportToCSV, From b67f3f535ff583811c750df96219d698dcd0b40c Mon Sep 17 00:00:00 2001 From: dej611 Date: Tue, 17 Nov 2020 13:21:21 +0100 Subject: [PATCH 07/44] :sparkles: Add getInspectorAdapters API to Lens embeddable --- .../public/editor_frame_service/embeddable/embeddable.tsx | 4 ++++ 1 file changed, 4 insertions(+) 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 8139631daa971..3e1cd34bd46d0 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 @@ -129,6 +129,10 @@ export class Embeddable } } + public getInspectorAdapters() { + return this.savedVis?.state?.activeData; + } + async initializeSavedVis(input: LensEmbeddableInput) { const attributes: | LensSavedObjectAttributes From d7e9595e1b3bbbf81175d5cadd8fb8dc4dc3fde9 Mon Sep 17 00:00:00 2001 From: dej611 Date: Tue, 17 Nov 2020 13:22:39 +0100 Subject: [PATCH 08/44] :sparkles: Make action more generic --- .../embeddable/export_csv_action.tsx | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/export_csv_action.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/export_csv_action.tsx index 4bf14ef02a491..616364254690e 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/export_csv_action.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/export_csv_action.tsx @@ -5,15 +5,16 @@ */ import { i18n } from '@kbn/i18n'; +import { exportAsCSVs, FieldFormat } from 'src/plugins/data/public'; import { IEmbeddable } from 'src/plugins/embeddable/public'; // import { StartServicesGetter } from 'src/plugins/kibana_utils/public'; import { Action } from 'src/plugins/ui_actions/public'; export const ACTION_EXPORT_CSV = 'ACTION_EXPORT_CSV'; -export interface Params { - // start: StartServicesGetter; -} +// export interface Params { +// start: StartServicesGetter; +// } interface ExportContext { embeddable?: IEmbeddable; @@ -30,7 +31,7 @@ export class ExportCSVAction implements Action { - return context.embeddable?.type === 'lens'; + return Boolean(context.embeddable && 'getInspectorAdapters' in context.embeddable); } protected readonly exportCSV = async (context: ExportContext): Promise => { - // Call the Export CSV method on Lens here - console.log('Export CSV'); + if (context.embeddable) { + exportAsCSVs(context.embeddable.getTitle()!, context.embeddable.getInspectorAdapters(), { + csvSeparator: ',', + quoteValues: true, + formatFactory: () => ({ convert: (v) => `${v}` } as FieldFormat), + }); + } }; public async execute(context: ExportContext): Promise { From 08d75d00c8644e3a1a67d3572dc94add401c2a7e Mon Sep 17 00:00:00 2001 From: dej611 Date: Tue, 17 Nov 2020 13:23:51 +0100 Subject: [PATCH 09/44] :fire: removed embeddable action for now --- .../embeddable/export_csv_action.tsx | 58 ------------------- 1 file changed, 58 deletions(-) delete mode 100644 x-pack/plugins/lens/public/editor_frame_service/embeddable/export_csv_action.tsx diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/export_csv_action.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/export_csv_action.tsx deleted file mode 100644 index 4bf14ef02a491..0000000000000 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/export_csv_action.tsx +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; -import { IEmbeddable } from 'src/plugins/embeddable/public'; -// import { StartServicesGetter } from 'src/plugins/kibana_utils/public'; -import { Action } from 'src/plugins/ui_actions/public'; - -export const ACTION_EXPORT_CSV = 'ACTION_EXPORT_CSV'; - -export interface Params { - // start: StartServicesGetter; -} - -interface ExportContext { - embeddable?: IEmbeddable; -} - -/** - * This is "Export CSV" action which appears in the context - * menu of a dashboard panel. - */ -export class ExportCSVAction implements Action { - public readonly id = ACTION_EXPORT_CSV; - - public readonly type = ACTION_EXPORT_CSV; - - public readonly order = 200; - - constructor(protected readonly params: Params) {} - - public getIconType() { - return 'exportAction'; - } - - public readonly getDisplayName = (context: ExportContext): string => - i18n.translate('xpack.lens.DownloadCreateDrilldownAction.displayName', { - defaultMessage: 'Download as CSV', - }); - - public async isCompatible(context: ExportContext): Promise { - return context.embeddable?.type === 'lens'; - } - - protected readonly exportCSV = async (context: ExportContext): Promise => { - // Call the Export CSV method on Lens here - console.log('Export CSV'); - }; - - public async execute(context: ExportContext): Promise { - // const { core } = this.params.start(); - - await this.exportCSV(context); - } -} From 0ac58e0e6f31bac29786b83872174d039e92bf32 Mon Sep 17 00:00:00 2001 From: dej611 Date: Tue, 17 Nov 2020 18:51:02 +0100 Subject: [PATCH 10/44] :sparkles: First action implementation --- .../embeddable/export_csv_action.tsx | 23 +++++++++---------- x-pack/plugins/lens/public/plugin.ts | 21 ++++++++++++++++- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/export_csv_action.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/export_csv_action.tsx index 616364254690e..70aca551a864e 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/export_csv_action.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/export_csv_action.tsx @@ -5,18 +5,19 @@ */ import { i18n } from '@kbn/i18n'; -import { exportAsCSVs, FieldFormat } from 'src/plugins/data/public'; +import { DataPublicPluginStart, exportAsCSVs, FieldFormat } from 'src/plugins/data/public'; import { IEmbeddable } from 'src/plugins/embeddable/public'; -// import { StartServicesGetter } from 'src/plugins/kibana_utils/public'; import { Action } from 'src/plugins/ui_actions/public'; +import { CoreStart } from 'src/core/public'; export const ACTION_EXPORT_CSV = 'ACTION_EXPORT_CSV'; -// export interface Params { -// start: StartServicesGetter; -// } +export interface Params { + core: CoreStart; + data: DataPublicPluginStart; +} -interface ExportContext { +export interface ExportContext { embeddable?: IEmbeddable; } @@ -31,7 +32,7 @@ export class ExportCSVAction implements Action => { if (context.embeddable) { exportAsCSVs(context.embeddable.getTitle()!, context.embeddable.getInspectorAdapters(), { - csvSeparator: ',', - quoteValues: true, - formatFactory: () => ({ convert: (v) => `${v}` } as FieldFormat), + csvSeparator: this.params.core.uiSettings.get('csv:separator', ','), + quoteValues: this.params.core.uiSettings.get('csv:quoteValues', true), + formatFactory: this.params.data.fieldFormats.deserialize, }); } }; public async execute(context: ExportContext): Promise { - // const { core } = this.params.start(); - await this.exportCSV(context); } } diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index 2f9310ee24ae9..8e626b0e2a9da 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -6,7 +6,11 @@ import { AppMountParameters, CoreSetup, CoreStart } from 'kibana/public'; import { DataPublicPluginSetup, DataPublicPluginStart } from 'src/plugins/data/public'; -import { EmbeddableSetup, EmbeddableStart } from 'src/plugins/embeddable/public'; +import { + CONTEXT_MENU_TRIGGER, + 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'; @@ -42,6 +46,11 @@ import { visualizeFieldAction } from './trigger_actions/visualize_field_actions' import { getSearchProvider } from './search_provider'; import { LensAttributeService } from './lens_attribute_service'; +import { + ACTION_EXPORT_CSV, + ExportContext, + ExportCSVAction, +} from './editor_frame_service/embeddable/export_csv_action'; export interface LensPluginSetupDependencies { urlForwarding: UrlForwardingSetup; @@ -64,6 +73,13 @@ export interface LensPluginStartDependencies { charts: ChartsPluginStart; savedObjectsTagging?: SavedObjectTaggingPluginStart; } + +declare module 'src/plugins/ui_actions/public' { + export interface ActionContextMapping { + [ACTION_EXPORT_CSV]: ExportContext; + } +} + export class LensPlugin { private datatableVisualization: DatatableVisualization; private editorFrameService: EditorFrameService; @@ -182,6 +198,9 @@ export class LensPlugin { VISUALIZE_FIELD_TRIGGER, visualizeFieldAction(core.application) ); + + const ExportCSVPlugin = new ExportCSVAction({ core, data: startDependencies.data }); + startDependencies.uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, ExportCSVPlugin); } stop() { From 5f5052260c14bab020e225a8253c477700ceff2f Mon Sep 17 00:00:00 2001 From: dej611 Date: Wed, 18 Nov 2020 13:08:54 +0100 Subject: [PATCH 11/44] :sparkles: Make it work for Lens and Visualize embeddables --- .../embeddable/export_csv_action.tsx | 62 ++++++++++++++++--- x-pack/plugins/lens/public/plugin.ts | 19 +++--- 2 files changed, 64 insertions(+), 17 deletions(-) diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/export_csv_action.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/export_csv_action.tsx index 61c2310fe56bd..81b0ab294ad91 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/export_csv_action.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/export_csv_action.tsx @@ -5,10 +5,11 @@ */ import { i18n } from '@kbn/i18n'; -import { DataPublicPluginStart, exportAsCSVs } from 'src/plugins/data/public'; -import { IEmbeddable } from 'src/plugins/embeddable/public'; -import { Action } from 'src/plugins/ui_actions/public'; -import { CoreStart } from 'src/core/public'; +import { FormatFactory } from 'src/plugins/data/common/field_formats/utils'; +import { DataPublicPluginStart, exportAsCSVs } from '../../../../../../src/plugins/data/public'; +import { Adapters, IEmbeddable } from '../../../../../../src/plugins/embeddable/public'; +import { Action } from '../../../../../../src/plugins/ui_actions/public'; +import { CoreStart } from '../../../../../../src/core/public'; export const ACTION_EXPORT_CSV = 'ACTION_EXPORT_CSV'; @@ -44,15 +45,58 @@ export class ExportCSVAction implements Action { - return Boolean(context.embeddable && 'getInspectorAdapters' in context.embeddable); + return Boolean( + context.embeddable && + 'getInspectorAdapters' in context.embeddable && + this.hasDatatableContent(context.embeddable.getInspectorAdapters()) + ); } - protected readonly exportCSV = async (context: ExportContext): Promise => { - if (context.embeddable) { - exportAsCSVs(context.embeddable.getTitle()!, context.embeddable.getInspectorAdapters(), { + private hasDatatableContent = (adapters: Adapters | undefined) => { + return adapters && (adapters.data || adapters[Object.keys(adapters)[0]]?.columns); + }; + + private getFormatter = (type: string | undefined): FormatFactory | undefined => { + if (type === 'visualize') { + return (() => ({ + convert: (item: { raw: string; formatted: string }) => item.formatted, + })) as FormatFactory; + } + if (type === 'lens') { + return this.params.data.fieldFormats.deserialize; + } + }; + + private getDataTableContent = async (adapters: Adapters | undefined) => { + if (!adapters) { + return; + } + // Visualize + if (adapters.data) { + const datatable = await adapters.data.tabular(); + datatable.columns = datatable.columns.map(({ field, ...rest }: { field: string }) => ({ + id: field, + field, + ...rest, + })); + return { type: 'visualize', datatables: { layer1: datatable } }; + } + // Lens + if (adapters[Object.keys(adapters)[0]]?.columns) { + return { type: 'lens', datatables: adapters }; + } + return; + }; + + private exportCSV = async (context: ExportContext): Promise => { + const content = await this.getDataTableContent(context?.embeddable?.getInspectorAdapters()); + const formatFactory = this.getFormatter(content?.type); + + if (content && formatFactory) { + exportAsCSVs(context?.embeddable?.getTitle()!, content.datatables, { csvSeparator: this.params.core.uiSettings.get('csv:separator', ','), quoteValues: this.params.core.uiSettings.get('csv:quoteValues', true), - formatFactory: this.params.data.fieldFormats.deserialize, + formatFactory, }); } }; diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index 8e626b0e2a9da..ea68c9c46bd1c 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -5,17 +5,20 @@ */ import { AppMountParameters, CoreSetup, CoreStart } from 'kibana/public'; -import { DataPublicPluginSetup, DataPublicPluginStart } from 'src/plugins/data/public'; +import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../../src/plugins/data/public'; import { CONTEXT_MENU_TRIGGER, 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'; +} 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'; @@ -74,7 +77,7 @@ export interface LensPluginStartDependencies { savedObjectsTagging?: SavedObjectTaggingPluginStart; } -declare module 'src/plugins/ui_actions/public' { +declare module '../../../../src/plugins/ui_actions/public' { export interface ActionContextMapping { [ACTION_EXPORT_CSV]: ExportContext; } From 88eefc64f1d2397c1ea4da8028f9ece52bc6a6bc Mon Sep 17 00:00:00 2001 From: dej611 Date: Wed, 18 Nov 2020 13:11:10 +0100 Subject: [PATCH 12/44] :bug: Forgotten type --- src/plugins/data/public/exports/export_csv.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/plugins/data/public/exports/export_csv.tsx b/src/plugins/data/public/exports/export_csv.tsx index a04f3c2c648bf..3a9d42221ec4d 100644 --- a/src/plugins/data/public/exports/export_csv.tsx +++ b/src/plugins/data/public/exports/export_csv.tsx @@ -123,8 +123,10 @@ export function exportAsCSVs( }, {}); } + const type = 'text/plain;charset=utf-8'; + const downloadQueue = layerIds.map((layerId, i) => { - const blob = new Blob([csvs[layerId]]); + const blob = new Blob([csvs[layerId]], { type }); const postFix = layerIds.length > 1 ? `-${i + 1}` : ''; // TODO: remove this workaround for multiple files when fixed (in filesaver?) return () => Promise.resolve().then(() => saveAs(blob, `${filename}${postFix}.csv`)); From 88c2a8d25bc4fefeb11cb5013b27a9f81261296c Mon Sep 17 00:00:00 2001 From: dej611 Date: Wed, 18 Nov 2020 14:45:09 +0100 Subject: [PATCH 13/44] :lipstick: Move the cancel button on the right --- .../lens/public/app_plugin/lens_top_nav.tsx | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx index 2c2fed8eaca79..2c23dc291405c 100644 --- a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx +++ b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx @@ -31,6 +31,18 @@ export function getLensTopNavConfig(options: { defaultMessage: 'Save', }); + topNavMenu.push({ + label: i18n.translate('xpack.lens.app.downloadCSV', { + defaultMessage: 'Download as CSV', + }), + run: actions.exportToCSV, + testId: 'lnsApp_downloadCSVButton', + description: i18n.translate('xpack.lens.app.downloadButtonAriaLabel', { + defaultMessage: 'Download the data as CSV file', + }), + disableButton: !enableExportToCSV, + }); + if (showCancel) { topNavMenu.push({ label: i18n.translate('xpack.lens.app.cancel', { @@ -44,18 +56,6 @@ export function getLensTopNavConfig(options: { }); } - topNavMenu.push({ - label: i18n.translate('xpack.lens.app.downloadCSV', { - defaultMessage: 'Download as CSV', - }), - run: actions.exportToCSV, - testId: 'lnsApp_downloadCSVButton', - description: i18n.translate('xpack.lens.app.downloadButtonAriaLabel', { - defaultMessage: 'Download the data as CSV file', - }), - disableButton: !enableExportToCSV, - }); - topNavMenu.push({ label: saveButtonLabel, iconType: !showSaveAndReturn ? 'save' : undefined, From 27678f414debffbf47d36e42b367537acfb43ca3 Mon Sep 17 00:00:00 2001 From: dej611 Date: Wed, 18 Nov 2020 15:43:03 +0100 Subject: [PATCH 14/44] :bug: Avoid to save the activeData content --- x-pack/plugins/lens/public/app_plugin/app.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 5780f1f9d78e4..4bfb782f9dbc6 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -119,11 +119,13 @@ export function App({ injectFilterReferences(lastKnownDoc.state?.filters || [], lastKnownDoc.references), esFilters.isFilterPinned ); + // do not save the activeData content + const { activeData, ...stateWithoutActiveData } = lastKnownDoc.state; return pinnedFilters?.length ? { ...lastKnownDoc, state: { - ...lastKnownDoc.state, + ...stateWithoutActiveData, filters: appFilters, }, } From 52f606fad08843a84d70a81591d0f222d251b033 Mon Sep 17 00:00:00 2001 From: dej611 Date: Wed, 18 Nov 2020 16:44:11 +0100 Subject: [PATCH 15/44] :bug: Fix tests --- x-pack/plugins/lens/public/app_plugin/app.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 4bfb782f9dbc6..bcc611ecbeaca 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -120,7 +120,7 @@ export function App({ esFilters.isFilterPinned ); // do not save the activeData content - const { activeData, ...stateWithoutActiveData } = lastKnownDoc.state; + const { activeData, ...stateWithoutActiveData } = lastKnownDoc.state ?? {}; return pinnedFilters?.length ? { ...lastKnownDoc, From c14da827b72ec7406d93e059d18f872b1b7e3b4b Mon Sep 17 00:00:00 2001 From: dej611 Date: Thu, 19 Nov 2020 14:31:22 +0100 Subject: [PATCH 16/44] :white_check_mark: Improved logic + add basic unit testing --- .../data/public/exports/export_csv.tsx | 2 +- .../contact_card/contact_card_embeddable.tsx | 18 +++ .../embeddable/export_csv_action.test.tsx | 130 ++++++++++++++++++ .../embeddable/export_csv_action.tsx | 62 +++++++-- 4 files changed, 196 insertions(+), 16 deletions(-) create mode 100644 x-pack/plugins/lens/public/editor_frame_service/embeddable/export_csv_action.test.tsx diff --git a/src/plugins/data/public/exports/export_csv.tsx b/src/plugins/data/public/exports/export_csv.tsx index 3a9d42221ec4d..0a8e10b28e069 100644 --- a/src/plugins/data/public/exports/export_csv.tsx +++ b/src/plugins/data/public/exports/export_csv.tsx @@ -26,7 +26,7 @@ import pMap from 'p-map'; 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; diff --git a/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_embeddable.tsx b/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_embeddable.tsx index b82cd9ca7cc31..46b8b958f86e9 100644 --- a/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_embeddable.tsx +++ b/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_embeddable.tsx @@ -95,6 +95,24 @@ export class ContactCardEmbeddable extends Embeddable< } public reload() {} + + public getInspectorAdapters = () => { + return { + 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_USER_TRIGGER = 'CONTACT_USER_TRIGGER'; diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/export_csv_action.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/export_csv_action.test.tsx new file mode 100644 index 0000000000000..2dc27992c6dfe --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/export_csv_action.test.tsx @@ -0,0 +1,130 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + isErrorEmbeddable, + IContainer, + ErrorEmbeddable, +} from 'src/plugins/dashboard/public/embeddable_plugin'; +import { DashboardContainer } from 'src/plugins/dashboard/public/application/embeddable'; +import { + getSampleDashboardInput, + getSampleDashboardPanel, +} from 'src/plugins/dashboard/public/application/test_helpers'; +import { + CONTACT_CARD_EMBEDDABLE, + ContactCardEmbeddableFactory, + ContactCardEmbeddable, + ContactCardEmbeddableInput, + ContactCardEmbeddableOutput, +} from 'src/plugins/dashboard/public/embeddable_plugin_test_samples'; +import { coreMock } from 'src/core/public/mocks'; +import { CoreStart } from 'kibana/public'; +import { ExportCSVAction } from './export_csv_action'; +import { embeddablePluginMock } from 'src/plugins/embeddable/public/mocks'; +import { DataPublicPluginStart } from 'src/plugins/data/public/types'; +import { dataPluginMock } from 'src/plugins/data/public/mocks'; +import { LINE_FEED_CHARACTER } from 'src/plugins/data/public/exports/export_csv'; + +describe('Export CSV action', () => { + const { setup, doStart } = embeddablePluginMock.createInstance(); + setup.registerEmbeddableFactory( + CONTACT_CARD_EMBEDDABLE, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + new ContactCardEmbeddableFactory((() => 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, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + application: {} as any, + embeddable: start, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + inspector: {} as any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + notifications: {} as any, + overlays: coreStart.overlays, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + savedObjectMetaData: {} as any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + uiActions: {} as any, + }; + const input = getSampleDashboardInput({ + panels: { + '123': getSampleDashboardPanel({ + explicitInput: { firstName: 'Kibanana', id: '123' }, + type: CONTACT_CARD_EMBEDDABLE, + }), + }, + }); + container = new DashboardContainer(input, options); + dataMock = dataPluginMock.createStartContract(); + + const contactCardEmbeddable = await container.addNewEmbeddable< + ContactCardEmbeddableInput, + ContactCardEmbeddableOutput, + ContactCardEmbeddable + >(CONTACT_CARD_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': `First Name,Last Name${LINE_FEED_CHARACTER}Kibana,undefined${LINE_FEED_CHARACTER}`, + }); + }); + + 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/x-pack/plugins/lens/public/editor_frame_service/embeddable/export_csv_action.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/export_csv_action.tsx index 81b0ab294ad91..8ba5750b74ae8 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/export_csv_action.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/export_csv_action.tsx @@ -20,6 +20,8 @@ export interface Params { export interface ExportContext { embeddable?: IEmbeddable; + // used for testing + asString?: boolean; } /** @@ -56,52 +58,82 @@ export class ExportCSVAction implements Action { + private getFormatter = ( + type: string | undefined, + adapters: Adapters | undefined + ): FormatFactory | undefined => { + if (type === 'lens') { + return this.params.data.fieldFormats.deserialize; + } + if (type === 'visualize') { return (() => ({ convert: (item: { raw: string; formatted: string }) => item.formatted, })) as FormatFactory; } - if (type === 'lens') { - return this.params.data.fieldFormats.deserialize; + + if (this.hasDatatableContent(adapters)) { + // if of unknown type, return an identity + return (() => ({ + convert: (item) => item, + })) as FormatFactory; } }; - private getDataTableContent = async (adapters: Adapters | undefined) => { - if (!adapters) { + private getDataTableContent = async ( + type: string | undefined, + adapters: Adapters | undefined + ) => { + if (!adapters || !type) { return; } // Visualize - if (adapters.data) { + if (type === 'visualize') { const datatable = await adapters.data.tabular(); datatable.columns = datatable.columns.map(({ field, ...rest }: { field: string }) => ({ id: field, field, ...rest, })); - return { type: 'visualize', datatables: { layer1: datatable } }; + return { layer1: datatable }; } // Lens - if (adapters[Object.keys(adapters)[0]]?.columns) { - return { type: 'lens', datatables: adapters }; + if (type === 'lens') { + return adapters; + } + // Make a last attempt to duck type the adapter (useful for testing) + if (this.hasDatatableContent(adapters)) { + return adapters; } return; }; - private exportCSV = async (context: ExportContext): Promise => { - const content = await this.getDataTableContent(context?.embeddable?.getInspectorAdapters()); - const formatFactory = this.getFormatter(content?.type); + private exportCSV = async (context: ExportContext) => { + const formatFactory = this.getFormatter( + context?.embeddable?.type, + context?.embeddable?.getInspectorAdapters() + ); + // early exit if not formatter is available + if (!formatFactory) { + return; + } + const datatables = await this.getDataTableContent( + context?.embeddable?.type, + context?.embeddable?.getInspectorAdapters() + ); - if (content && formatFactory) { - exportAsCSVs(context?.embeddable?.getTitle()!, content.datatables, { + if (datatables) { + return exportAsCSVs(context?.embeddable?.getTitle()!, datatables, { csvSeparator: this.params.core.uiSettings.get('csv:separator', ','), quoteValues: this.params.core.uiSettings.get('csv:quoteValues', true), formatFactory, + asString: context.asString, }); } }; public async execute(context: ExportContext): Promise { - await this.exportCSV(context); + // make it testable: type here will be forced + return ((await this.exportCSV(context)) as unknown) as Promise; } } From 4de7ae84b13ce11a0aa8c9149c267cb06913f715 Mon Sep 17 00:00:00 2001 From: dej611 Date: Thu, 19 Nov 2020 18:07:14 +0100 Subject: [PATCH 17/44] :sparkles: split the current plugin in two distinct areas --- .../data/public/exports/export_csv.test.ts | 21 +++--- .../data/public/exports/export_csv.tsx | 44 +++--------- src/plugins/share/public/index.ts | 1 + src/plugins/share/public/lib/download_as.ts | 67 +++++++++++++++++++ x-pack/plugins/lens/kibana.json | 3 +- x-pack/plugins/lens/public/app_plugin/app.tsx | 20 ++++-- 6 files changed, 104 insertions(+), 52 deletions(-) create mode 100644 src/plugins/share/public/lib/download_as.ts diff --git a/src/plugins/data/public/exports/export_csv.test.ts b/src/plugins/data/public/exports/export_csv.test.ts index f6d8889fed7bd..c7bdcbd973ae3 100644 --- a/src/plugins/data/public/exports/export_csv.test.ts +++ b/src/plugins/data/public/exports/export_csv.test.ts @@ -19,7 +19,7 @@ import { Datatable } from 'src/plugins/expressions'; import { FieldFormat } from '../../common/field_formats'; -import { exportAsCSVs } from './export_csv'; +import { CSV_MIME_TYPE, exportAsCSVs } from './export_csv'; function getDefaultOptions() { const formatFactory = jest.fn(); @@ -28,8 +28,6 @@ function getDefaultOptions() { csvSeparator: ',', quoteValues: true, formatFactory, - // this is for testing - asString: true, }; } @@ -69,7 +67,7 @@ describe('CSV exporter', () => { test('should export formatted values by default', () => { expect(exportAsCSVs('oneCSV', getDataTable(), getDefaultOptions())).toStrictEqual({ - 'oneCSV.csv': 'columnOne\r\n"Formatted_value"\r\n', + 'oneCSV.csv': { content: 'columnOne\r\n"Formatted_value"\r\n', type: CSV_MIME_TYPE }, }); }); @@ -77,7 +75,7 @@ describe('CSV exporter', () => { return expect( exportAsCSVs('oneCSV', getDataTable(), { ...getDefaultOptions(), quoteValues: false }) ).toStrictEqual({ - 'oneCSV.csv': 'columnOne\r\nFormatted_value\r\n', + 'oneCSV.csv': { content: 'columnOne\r\nFormatted_value\r\n', type: CSV_MIME_TYPE }, }); }); @@ -85,7 +83,7 @@ describe('CSV exporter', () => { expect( exportAsCSVs('oneCSV', getDataTable(), { ...getDefaultOptions(), raw: true }) ).toStrictEqual({ - 'oneCSV.csv': 'columnOne\r\nvalue\r\n', + 'oneCSV.csv': { content: 'columnOne\r\nvalue\r\n', type: CSV_MIME_TYPE }, }); }); @@ -93,7 +91,10 @@ describe('CSV exporter', () => { expect( exportAsCSVs('oneCSV', getDataTable({ multipleColumns: true }), getDefaultOptions()) ).toStrictEqual({ - 'oneCSV.csv': 'columnOne,columnTwo\r\n"Formatted_value","Formatted_5"\r\n', + 'oneCSV.csv': { + content: 'columnOne,columnTwo\r\n"Formatted_value","Formatted_5"\r\n', + type: CSV_MIME_TYPE, + }, }); }); @@ -101,8 +102,8 @@ describe('CSV exporter', () => { expect( exportAsCSVs('twoCSVs', getDataTable({ multipleLayers: true }), getDefaultOptions()) ).toStrictEqual({ - 'twoCSVs-1.csv': 'columnOne\r\n"Formatted_value"\r\n', - 'twoCSVs-2.csv': 'columnOne\r\n"Formatted_value"\r\n', + 'twoCSVs-1.csv': { content: 'columnOne\r\n"Formatted_value"\r\n', type: CSV_MIME_TYPE }, + 'twoCSVs-2.csv': { content: 'columnOne\r\n"Formatted_value"\r\n', type: CSV_MIME_TYPE }, }); }); @@ -110,7 +111,7 @@ describe('CSV exporter', () => { const datatables = getDataTable(); datatables.layer1.rows[0].col1 = '"value"'; expect(exportAsCSVs('oneCSV', datatables, getDefaultOptions())).toStrictEqual({ - 'oneCSV.csv': 'columnOne\r\n"Formatted_""value"""\r\n', + 'oneCSV.csv': { content: 'columnOne\r\n"Formatted_""value"""\r\n', type: CSV_MIME_TYPE }, }); }); }); diff --git a/src/plugins/data/public/exports/export_csv.tsx b/src/plugins/data/public/exports/export_csv.tsx index 3a9d42221ec4d..d780b336931af 100644 --- a/src/plugins/data/public/exports/export_csv.tsx +++ b/src/plugins/data/public/exports/export_csv.tsx @@ -19,16 +19,14 @@ // Inspired by the inspector CSV exporter -// @ts-ignore -import { saveAs } from '@elastic/filesaver'; -import pMap from 'p-map'; - import { FormatFactory } from 'src/plugins/data/common/field_formats/utils'; import { Datatable } from 'src/plugins/expressions'; +import { DownloadableContent } from 'src/plugins/share/public/'; 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'; // TODO: enhance this later on function escape(val: object | string, quoteValues: boolean) { @@ -50,7 +48,6 @@ interface CSVOptions { quoteValues: boolean; formatFactory: FormatFactory; raw?: boolean; - asString?: boolean; // use it for testing } function buildCSV( @@ -87,12 +84,12 @@ function buildCSV( * @param datatables - data (as a dictionary of Datatable) to be translated into CSVs. It can contain multiple tables. * @param options - set of options for the exporter * - * @returns undefined (download) - Record\ (only for testing) + * @returns A dictionary of files to download: the key is the filename (w/o extension) and the */ export function exportAsCSVs( filename: string, datatables: Record | undefined, - { asString, ...options }: CSVOptions + options: CSVOptions ) { if (datatables == null) { return; @@ -113,33 +110,10 @@ export function exportAsCSVs( const layerIds = Object.keys(csvs); - // useful for testing - if (asString) { - return layerIds.reduce>((memo, layerId, i) => { - const content = csvs[layerId]; - const postFix = layerIds.length > 1 ? `-${i + 1}` : ''; - memo[`${filename}${postFix}.csv`] = content; - return memo; - }, {}); - } - - const type = 'text/plain;charset=utf-8'; - - const downloadQueue = layerIds.map((layerId, i) => { - const blob = new Blob([csvs[layerId]], { type }); + return layerIds.reduce>>((memo, layerId, i) => { + const content = csvs[layerId]; const postFix = layerIds.length > 1 ? `-${i + 1}` : ''; - // TODO: remove this workaround for multiple files when fixed (in filesaver?) - return () => Promise.resolve().then(() => saveAs(blob, `${filename}${postFix}.csv`)); - }); - - // There's a bug in some browser with multiple files downloaded at once - // * sometimes only the first/last content is downloaded multiple times - // * sometimes only the first/last filename is used multiple times - pMap(downloadQueue, (downloadFn) => Promise.all([downloadFn(), wait(50)]), { - concurrency: 1, - }); -} -// Probably there's already another one around? -function wait(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)); + memo[`${filename}${postFix}.csv`] = { content, type: CSV_MIME_TYPE }; + return memo; + }, {}); } diff --git a/src/plugins/share/public/index.ts b/src/plugins/share/public/index.ts index 950ecebeaadc7..2153a98648d0b 100644 --- a/src/plugins/share/public/index.ts +++ b/src/plugins/share/public/index.ts @@ -41,5 +41,6 @@ export { import { SharePlugin } from './plugin'; export { KibanaURL } from './kibana_url'; +export * from './lib/download_as'; export const plugin = () => new SharePlugin(); diff --git a/src/plugins/share/public/lib/download_as.ts b/src/plugins/share/public/lib/download_as.ts new file mode 100644 index 0000000000000..6f40b894f85bc --- /dev/null +++ b/src/plugins/share/public/lib/download_as.ts @@ -0,0 +1,67 @@ +/* + * 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. + */ + +// @ts-ignore +import { saveAs } from '@elastic/filesaver'; +import pMap from 'p-map'; + +export type DownloadableContent = { content: string; type: string } | Blob; + +/** + * Convenient method to use for a single file download + * **Note**: for multiple files use the downloadMultipleAs method, do not iterate with this method here + * @param filename full name of the file + * @param payload either a Blob content, or a Record with a stringified content and type + * + * @returns a Promise that resolves when the download has been correctly started + */ +export function downloadFileAs(filename: string, payload: DownloadableContent) { + return downloadMultipleAs({ [filename]: payload }); +} + +/** + * Multiple files download method + * @param files a Record containing one entry per file: the key entry should be the filename + * and the value either a Blob content, or a Record with a stringified content and type + * + * @returns a Promise that resolves when all the downloads have been correctly started + */ +export async function downloadMultipleAs(files: Record) { + const filenames = Object.keys(files); + const downloadQueue = filenames.map((filename, i) => { + const payload = files[filename]; + const blob = + // probably this is enough? It does not support Node or custom implementations + payload instanceof Blob ? payload : new Blob([payload.content], { type: payload.type }); + + // TODO: remove this workaround for multiple files when fixed (in filesaver?) + return () => Promise.resolve().then(() => saveAs(blob, filename)); + }); + + // There's a bug in some browser with multiple files downloaded at once + // * sometimes only the first/last content is downloaded multiple times + // * sometimes only the first/last filename is used multiple times + await pMap(downloadQueue, (downloadFn) => Promise.all([downloadFn(), wait(50)]), { + concurrency: 1, + }); +} +// Probably there's already another one around? +function wait(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/x-pack/plugins/lens/kibana.json b/x-pack/plugins/lens/kibana.json index ce78757676bcc..5476be50fee88 100644 --- a/x-pack/plugins/lens/kibana.json +++ b/x-pack/plugins/lens/kibana.json @@ -14,7 +14,8 @@ "dashboard", "charts", "uiActions", - "embeddable" + "embeddable", + "share" ], "optionalPlugins": ["usageCollection", "taskManager", "globalSearch", "savedObjectsTagging"], "configPath": ["xpack", "lens"], diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index bcc611ecbeaca..c60d7cc9995d7 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -11,7 +11,7 @@ import React, { useState, useEffect, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { NotificationsStart } from 'kibana/public'; import { EuiBreadcrumb } from '@elastic/eui'; -import { exportAsCSVs } from '../../../../../src/plugins/data/public'; +import { downloadMultipleAs } from '../../../../../src/plugins/share/public'; import { createKbnUrlStateStorage, withNotifyOnErrors, @@ -26,6 +26,7 @@ import { NativeRenderer } from '../native_renderer'; import { trackUiEvent } from '../lens_ui_telemetry'; import { esFilters, + exportAsCSVs, IndexPattern as IndexPatternInstance, IndexPatternsContract, syncQueryStateWithUrl, @@ -491,11 +492,18 @@ export function App({ savingPermitted, actions: { exportToCSV: () => { - exportAsCSVs(lastKnownDoc?.title || 'unsaved', lastKnownDoc?.state?.activeData, { - csvSeparator: uiSettings.get('csv:separator', ','), - quoteValues: uiSettings.get('csv:quoteValues', true), - formatFactory: data.fieldFormats.deserialize, - }); + const content = exportAsCSVs( + lastKnownDoc?.title || 'unsaved', + lastKnownDoc?.state?.activeData, + { + csvSeparator: uiSettings.get('csv:separator', ','), + quoteValues: uiSettings.get('csv:quoteValues', true), + formatFactory: data.fieldFormats.deserialize, + } + ); + if (content) { + downloadMultipleAs(content); + } }, saveAndReturn: () => { if (savingPermitted && lastKnownDoc) { From 5157f312644a5d516a07f8fb3389916181aef47c Mon Sep 17 00:00:00 2001 From: dej611 Date: Fri, 20 Nov 2020 10:55:55 +0100 Subject: [PATCH 18/44] :truck: Update with the new download plugin --- .../editor_frame_service/embeddable/export_csv_action.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/export_csv_action.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/export_csv_action.tsx index 8ba5750b74ae8..dfe3d7305fbeb 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/export_csv_action.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/export_csv_action.tsx @@ -7,6 +7,7 @@ import { i18n } from '@kbn/i18n'; import { FormatFactory } from 'src/plugins/data/common/field_formats/utils'; import { DataPublicPluginStart, exportAsCSVs } from '../../../../../../src/plugins/data/public'; +import { downloadMultipleAs } from '../../../../../../src/plugins/share/public'; import { Adapters, IEmbeddable } from '../../../../../../src/plugins/embeddable/public'; import { Action } from '../../../../../../src/plugins/ui_actions/public'; import { CoreStart } from '../../../../../../src/core/public'; @@ -123,12 +124,14 @@ export class ExportCSVAction implements Action Date: Fri, 20 Nov 2020 11:12:47 +0100 Subject: [PATCH 19/44] :memo: Update API documentation --- ...ana-plugin-plugins-data-public.csv_mime_type.md | 11 +++++++++++ ...bana-plugin-plugins-data-public.exportascsvs.md | 14 ++++++++++---- .../public/kibana-plugin-plugins-data-public.md | 3 ++- src/plugins/data/public/exports/export_csv.tsx | 2 +- src/plugins/data/public/public.api.md | 10 +++++++++- 5 files changed, 33 insertions(+), 7 deletions(-) create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.csv_mime_type.md diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.csv_mime_type.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.csv_mime_type.md new file mode 100644 index 0000000000000..5b81d09ddf60a --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.csv_mime_type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [CSV\_MIME\_TYPE](./kibana-plugin-plugins-data-public.csv_mime_type.md) + +## CSV\_MIME\_TYPE variable + +Signature: + +```typescript +CSV_MIME_TYPE = "text/plain;charset=utf-8" +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.exportascsvs.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.exportascsvs.md index c8bc128ea93a3..fe6f2f1f84e9c 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.exportascsvs.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.exportascsvs.md @@ -7,7 +7,10 @@ Signature: ```typescript -export declare function exportAsCSVs(filename: string, datatables: Record | undefined, { asString, ...options }: CSVOptions): Record | undefined; +export declare function exportAsCSVs(filename: string, datatables: Record | undefined, options: CSVOptions): Record | undefined; ``` ## Parameters @@ -16,11 +19,14 @@ export declare function exportAsCSVs(filename: string, datatables: Recordstring | filename to use (either as is, or as prefix for multiple CSVs) for the files to download | | datatables | Record<string, Datatable> | undefined | data (as a dictionary of Datatable) to be translated into CSVs. It can contain multiple tables. | -| { asString, ...options } | CSVOptions | | +| options | CSVOptions | set of options for the exporter | Returns: -`Record | undefined` +`Record | undefined` -undefined (download) - Record<string, string> (only for testing) +A dictionary of files to download: the key is the filename and the value the CSV string diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index dc7139f722bfe..1232a8bbe3ddd 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -41,7 +41,7 @@ | Function | Description | | --- | --- | -| [exportAsCSVs(filename, datatables, { asString, ...options })](./kibana-plugin-plugins-data-public.exportascsvs.md) | | +| [exportAsCSVs(filename, datatables, options)](./kibana-plugin-plugins-data-public.exportascsvs.md) | | | [getDefaultQuery(language)](./kibana-plugin-plugins-data-public.getdefaultquery.md) | | | [getEsPreference(uiSettings, sessionId)](./kibana-plugin-plugins-data-public.getespreference.md) | | | [getSearchParamsFromRequest(searchRequest, dependencies)](./kibana-plugin-plugins-data-public.getsearchparamsfromrequest.md) | | @@ -105,6 +105,7 @@ | [castEsToKbnFieldTypeName](./kibana-plugin-plugins-data-public.castestokbnfieldtypename.md) | Get the KbnFieldType name for an esType string | | [connectToQueryState](./kibana-plugin-plugins-data-public.connecttoquerystate.md) | Helper to setup two-way syncing of global data and a state container | | [createSavedQueryService](./kibana-plugin-plugins-data-public.createsavedqueryservice.md) | | +| [CSV\_MIME\_TYPE](./kibana-plugin-plugins-data-public.csv_mime_type.md) | | | [ES\_SEARCH\_STRATEGY](./kibana-plugin-plugins-data-public.es_search_strategy.md) | | | [esFilters](./kibana-plugin-plugins-data-public.esfilters.md) | | | [esKuery](./kibana-plugin-plugins-data-public.eskuery.md) | | diff --git a/src/plugins/data/public/exports/export_csv.tsx b/src/plugins/data/public/exports/export_csv.tsx index d780b336931af..a58c449b8dbab 100644 --- a/src/plugins/data/public/exports/export_csv.tsx +++ b/src/plugins/data/public/exports/export_csv.tsx @@ -84,7 +84,7 @@ function buildCSV( * @param datatables - data (as a dictionary of Datatable) to be translated into CSVs. It can contain multiple tables. * @param options - set of options for the exporter * - * @returns A dictionary of files to download: the key is the filename (w/o extension) and the + * @returns A dictionary of files to download: the key is the filename and the value the CSV string */ export function exportAsCSVs( filename: string, diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 677e85e1087aa..be3ae00002a27 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -398,6 +398,11 @@ export const connectToQueryState: ({ timefilter: { timefil // @public (undocumented) export const createSavedQueryService: (savedObjectsClient: SavedObjectsClientContract) => SavedQueryService; +// Warning: (ae-missing-release-tag) "CSV_MIME_TYPE" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const CSV_MIME_TYPE = "text/plain;charset=utf-8"; + // Warning: (ae-missing-release-tag) "CustomFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -678,7 +683,10 @@ export type ExistsFilter = Filter & { // Warning: (ae-missing-release-tag) "exportAsCSVs" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function exportAsCSVs(filename: string, datatables: Record | undefined, { asString, ...options }: CSVOptions): Record | undefined; +export function exportAsCSVs(filename: string, datatables: Record | undefined, options: CSVOptions): Record | undefined; // Warning: (ae-missing-release-tag) "ExpressionFunctionKibana" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // From 074c9897528da112617e6385337a5f1295aa1137 Mon Sep 17 00:00:00 2001 From: dej611 Date: Fri, 20 Nov 2020 11:59:28 +0100 Subject: [PATCH 20/44] :bug: Fix for Visualize Embeddable type checking --- .../embeddable/export_csv_action.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/export_csv_action.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/export_csv_action.tsx index dfe3d7305fbeb..493d783bf1f6a 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/export_csv_action.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/export_csv_action.tsx @@ -9,7 +9,7 @@ import { FormatFactory } from 'src/plugins/data/common/field_formats/utils'; import { DataPublicPluginStart, exportAsCSVs } from '../../../../../../src/plugins/data/public'; import { downloadMultipleAs } from '../../../../../../src/plugins/share/public'; import { Adapters, IEmbeddable } from '../../../../../../src/plugins/embeddable/public'; -import { Action } from '../../../../../../src/plugins/ui_actions/public'; +import { ActionByType } from '../../../../../../src/plugins/ui_actions/public'; import { CoreStart } from '../../../../../../src/core/public'; export const ACTION_EXPORT_CSV = 'ACTION_EXPORT_CSV'; @@ -29,7 +29,7 @@ export interface ExportContext { * This is "Export CSV" action which appears in the context * menu of a dashboard panel. */ -export class ExportCSVAction implements Action { +export class ExportCSVAction implements ActionByType { public readonly id = ACTION_EXPORT_CSV; public readonly type = ACTION_EXPORT_CSV; @@ -67,7 +67,7 @@ export class ExportCSVAction implements Action ({ convert: (item: { raw: string; formatted: string }) => item.formatted, })) as FormatFactory; @@ -89,7 +89,7 @@ export class ExportCSVAction implements Action ({ id: field, @@ -102,6 +102,7 @@ export class ExportCSVAction implements Action Date: Fri, 20 Nov 2020 15:16:44 +0100 Subject: [PATCH 21/44] :ok_hand: Integrated feedback from review --- x-pack/plugins/lens/public/app_plugin/app.tsx | 28 +++++++++---------- .../editor_frame_service/editor_frame/save.ts | 4 ++- x-pack/plugins/lens/public/types.ts | 1 + 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index c60d7cc9995d7..4b7581908a3ef 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -120,13 +120,11 @@ export function App({ injectFilterReferences(lastKnownDoc.state?.filters || [], lastKnownDoc.references), esFilters.isFilterPinned ); - // do not save the activeData content - const { activeData, ...stateWithoutActiveData } = lastKnownDoc.state ?? {}; return pinnedFilters?.length ? { ...lastKnownDoc, state: { - ...stateWithoutActiveData, + ...lastKnownDoc.state, filters: appFilters, }, } @@ -478,6 +476,9 @@ export function App({ const { TopNavMenu } = navigation.ui; const savingPermitted = Boolean(state.isSaveable && application.capabilities.visualize.save); + const unsavedTitle = i18n.translate('xpack.lens.app.unsavedFilename', { + defaultMessage: 'unsaved', + }); const topNavConfig = getLensTopNavConfig({ showSaveAndReturn: Boolean( state.isLinkedToOriginatingApp && @@ -485,22 +486,18 @@ export function App({ (dashboardFeatureFlag.allowByValueEmbeddables || Boolean(initialInput)) ), enableExportToCSV: Boolean( - lastKnownDoc?.state?.activeData && Object.keys(lastKnownDoc.state.activeData).length + state.isSaveable && state.activeData && Object.keys(state.activeData).length ), isByValueMode: getIsByValueMode(), showCancel: Boolean(state.isLinkedToOriginatingApp), savingPermitted, actions: { exportToCSV: () => { - const content = exportAsCSVs( - lastKnownDoc?.title || 'unsaved', - lastKnownDoc?.state?.activeData, - { - csvSeparator: uiSettings.get('csv:separator', ','), - quoteValues: uiSettings.get('csv:quoteValues', true), - formatFactory: data.fieldFormats.deserialize, - } - ); + const content = exportAsCSVs(lastKnownDoc?.title || unsavedTitle, state.activeData, { + csvSeparator: uiSettings.get('csv:separator', ','), + quoteValues: uiSettings.get('csv:quoteValues', true), + formatFactory: data.fieldFormats.deserialize, + }); if (content) { downloadMultipleAs(content); } @@ -626,13 +623,16 @@ export function App({ onError, showNoDataPopover, initialContext, - onChange: ({ filterableIndexPatterns, doc, isSaveable }) => { + onChange: ({ filterableIndexPatterns, doc, isSaveable, activeData }) => { if (isSaveable !== state.isSaveable) { setState((s) => ({ ...s, isSaveable })); } if (!_.isEqual(state.persistedDoc, doc)) { setState((s) => ({ ...s, lastKnownDoc: doc })); } + if (!_.isEqual(state.activeData, activeData)) { + setState((s) => ({ ...s, activeData })); + } // Update the cached index patterns if the user made a change to any of them if ( diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.ts index 8cb4e5bf56110..eec3f68ced5fc 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.ts @@ -6,6 +6,7 @@ import _ from 'lodash'; import { SavedObjectReference } from 'kibana/public'; +import { Datatable } from 'src/plugins/expressions'; import { EditorFrameState } from './state_management'; import { Document } from '../../persistence/saved_object_store'; import { Datasource, Visualization, FramePublicAPI } from '../../types'; @@ -28,6 +29,7 @@ export function getSavedObjectFormat({ doc: Document; filterableIndexPatterns: string[]; isSaveable: boolean; + activeData: Record | undefined; } { const datasourceStates: Record = {}; const references: SavedObjectReference[] = []; @@ -69,11 +71,11 @@ export function getSavedObjectFormat({ visualization: state.visualization.state, query: framePublicAPI.query, filters: persistableFilters, - activeData: state.activeData, }, references, }, filterableIndexPatterns: uniqueFilterableIndexPatternIds, isSaveable: expression !== null, + activeData: state.activeData, }; } diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 225fedb987c76..d5644c157b916 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -50,6 +50,7 @@ export interface EditorFrameProps { filterableIndexPatterns: string[]; doc: Document; isSaveable: boolean; + activeData: Record | undefined; }) => void; showNoDataPopover: () => void; } From 64df643e4a9e310de1ae294f77e3428b86f8d644 Mon Sep 17 00:00:00 2001 From: dej611 Date: Fri, 20 Nov 2020 17:05:39 +0100 Subject: [PATCH 22/44] :white_check_mark: Add basic functional test --- .../dashboard/drilldowns/export_csv_action.ts | 25 +++++++++++++++++++ .../apps/dashboard/drilldowns/index.ts | 1 + 2 files changed, 26 insertions(+) create mode 100644 x-pack/test/functional/apps/dashboard/drilldowns/export_csv_action.ts diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/export_csv_action.ts b/x-pack/test/functional/apps/dashboard/drilldowns/export_csv_action.ts new file mode 100644 index 0000000000000..a8b064d550cff --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/drilldowns/export_csv_action.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +const ACTION_ID = 'ACTION_EXPORT_CSV'; +const ACTION_TEST_SUBJ = `embeddablePanelAction-${ACTION_ID}`; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const drilldowns = getService('dashboardDrilldownsManage'); + const { dashboard } = getPageObjects(['dashboard']); + const panelActions = getService('dashboardPanelActions'); + const testSubjects = getService('testSubjects'); + + describe('Export to CSV action', () => { + it('action exists in panel context menu', async () => { + await dashboard.loadSavedDashboard(drilldowns.DASHBOARD_WITH_PIE_CHART_NAME); + await panelActions.openContextMenu(); + await testSubjects.existOrFail(ACTION_TEST_SUBJ); + }); + }); +} diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/index.ts b/x-pack/test/functional/apps/dashboard/drilldowns/index.ts index 57454f50266da..7300dfe8a1bfa 100644 --- a/x-pack/test/functional/apps/dashboard/drilldowns/index.ts +++ b/x-pack/test/functional/apps/dashboard/drilldowns/index.ts @@ -25,6 +25,7 @@ export default function ({ loadTestFile, getService }: FtrProviderContext) { loadTestFile(require.resolve('./dashboard_to_dashboard_drilldown')); loadTestFile(require.resolve('./dashboard_to_url_drilldown')); loadTestFile(require.resolve('./explore_data_panel_action')); + loadTestFile(require.resolve('./export_csv_action')); // Disabled for now as it requires xpack.discoverEnhanced.actions.exploreDataInChart.enabled // setting set in kibana.yml to work. Once that is enabled by default, we can re-enable this test suite. From 5749f32fad2b890d92f036bac6f129d93933b4e4 Mon Sep 17 00:00:00 2001 From: dej611 Date: Fri, 20 Nov 2020 18:46:26 +0100 Subject: [PATCH 23/44] :label: fix type issue --- x-pack/plugins/lens/public/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index d5644c157b916..2f40f21455310 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -50,7 +50,7 @@ export interface EditorFrameProps { filterableIndexPatterns: string[]; doc: Document; isSaveable: boolean; - activeData: Record | undefined; + activeData?: Record; }) => void; showNoDataPopover: () => void; } From 4674a83a1974136d920ce9c164f56a44259bff0d Mon Sep 17 00:00:00 2001 From: dej611 Date: Fri, 20 Nov 2020 18:47:01 +0100 Subject: [PATCH 24/44] :white_check_mark: Add download unit tests --- .../lens/public/app_plugin/app.test.tsx | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx index a211416472f48..7cd33bd258552 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -895,6 +895,71 @@ describe('Lens App', () => { }); }); + describe('download button', () => { + function getButton(inst: ReactWrapper): TopNavMenuData { + return (inst + .find('[data-test-subj="lnsApp_topNav"]') + .prop('config') as TopNavMenuData[]).find( + (button) => button.testId === 'lnsApp_downloadCSVButton' + )!; + } + + it('should be disabled when no data is available', async () => { + const { component, frame } = mountWith({}); + const onChange = frame.mount.mock.calls[0][1].onChange; + await act(async () => + onChange({ + filterableIndexPatterns: [], + doc: ({} as unknown) as Document, + isSaveable: true, + }) + ); + component.update(); + expect(getButton(component).disableButton).toEqual(true); + }); + + it('should disable download when not saveable', async () => { + const { component, frame } = mountWith({}); + const onChange = frame.mount.mock.calls[0][1].onChange; + + await act(async () => + onChange({ + filterableIndexPatterns: [], + doc: ({} as unknown) as Document, + isSaveable: false, + activeData: { layer1: { type: 'datatable', columns: [], rows: [] } }, + }) + ); + + component.update(); + expect(getButton(component).disableButton).toEqual(true); + }); + + it('should still be enabled even if the user is missing save permissions', async () => { + const services = makeDefaultServices(); + services.application = { + ...services.application, + capabilities: { + ...services.application.capabilities, + visualize: { save: false, saveQuery: false, show: true }, + }, + }; + + const { component, frame } = mountWith({ services }); + const onChange = frame.mount.mock.calls[0][1].onChange; + await act(async () => + onChange({ + filterableIndexPatterns: [], + doc: ({} as unknown) as Document, + isSaveable: true, + activeData: { layer1: { type: 'datatable', columns: [], rows: [] } }, + }) + ); + component.update(); + expect(getButton(component).disableButton).toEqual(false); + }); + }); + describe('query bar state management', () => { it('uses the default time and query language settings', () => { const { frame } = mountWith({}); From 817aba0dae01b0d77c265879602d4a74d920917d Mon Sep 17 00:00:00 2001 From: dej611 Date: Mon, 23 Nov 2020 11:45:13 +0100 Subject: [PATCH 25/44] :truck: Move the action to the dashboard actions folder --- .../actions}/export_csv_action.test.tsx | 53 ++++++++++--------- .../actions}/export_csv_action.tsx | 31 +++++++---- src/plugins/dashboard/public/plugin.tsx | 13 ++++- x-pack/plugins/lens/public/plugin.ts | 20 +------ 4 files changed, 62 insertions(+), 55 deletions(-) rename {x-pack/plugins/lens/public/editor_frame_service/embeddable => src/plugins/dashboard/public/application/actions}/export_csv_action.test.tsx (71%) rename {x-pack/plugins/lens/public/editor_frame_service/embeddable => src/plugins/dashboard/public/application/actions}/export_csv_action.tsx (76%) diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/export_csv_action.test.tsx b/src/plugins/dashboard/public/application/actions/export_csv_action.test.tsx similarity index 71% rename from x-pack/plugins/lens/public/editor_frame_service/embeddable/export_csv_action.test.tsx rename to src/plugins/dashboard/public/application/actions/export_csv_action.test.tsx index 2dc27992c6dfe..2bc9c31d99a01 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/export_csv_action.test.tsx +++ b/src/plugins/dashboard/public/application/actions/export_csv_action.test.tsx @@ -1,39 +1,45 @@ /* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * 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 { - isErrorEmbeddable, - IContainer, - ErrorEmbeddable, -} from 'src/plugins/dashboard/public/embeddable_plugin'; -import { DashboardContainer } from 'src/plugins/dashboard/public/application/embeddable'; -import { - getSampleDashboardInput, - getSampleDashboardPanel, -} from 'src/plugins/dashboard/public/application/test_helpers'; +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 { CONTACT_CARD_EMBEDDABLE, ContactCardEmbeddableFactory, ContactCardEmbeddable, ContactCardEmbeddableInput, ContactCardEmbeddableOutput, -} from 'src/plugins/dashboard/public/embeddable_plugin_test_samples'; -import { coreMock } from 'src/core/public/mocks'; -import { CoreStart } from 'kibana/public'; +} from '../../embeddable_plugin_test_samples'; +import { coreMock } from '../../../../../core/public/mocks'; import { ExportCSVAction } from './export_csv_action'; -import { embeddablePluginMock } from 'src/plugins/embeddable/public/mocks'; -import { DataPublicPluginStart } from 'src/plugins/data/public/types'; -import { dataPluginMock } from 'src/plugins/data/public/mocks'; -import { LINE_FEED_CHARACTER } from 'src/plugins/data/public/exports/export_csv'; +import { embeddablePluginMock } from '../../../../embeddable/public/mocks'; +import { DataPublicPluginStart } from '../../../../data/public/types'; +import { dataPluginMock } from '../../../../data/public/mocks'; +import { LINE_FEED_CHARACTER } from '../../../../data/public/exports/export_csv'; describe('Export CSV action', () => { const { setup, doStart } = embeddablePluginMock.createInstance(); setup.registerEmbeddableFactory( CONTACT_CARD_EMBEDDABLE, - // eslint-disable-next-line @typescript-eslint/no-explicit-any new ContactCardEmbeddableFactory((() => null) as any, {} as any) ); const start = doStart(); @@ -55,17 +61,12 @@ describe('Export CSV action', () => { const options = { ExitFullScreenButton: () => null, SavedObjectFinder: () => null, - // eslint-disable-next-line @typescript-eslint/no-explicit-any application: {} as any, embeddable: start, - // eslint-disable-next-line @typescript-eslint/no-explicit-any inspector: {} as any, - // eslint-disable-next-line @typescript-eslint/no-explicit-any notifications: {} as any, overlays: coreStart.overlays, - // eslint-disable-next-line @typescript-eslint/no-explicit-any savedObjectMetaData: {} as any, - // eslint-disable-next-line @typescript-eslint/no-explicit-any uiActions: {} as any, }; const input = getSampleDashboardInput({ diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/export_csv_action.tsx b/src/plugins/dashboard/public/application/actions/export_csv_action.tsx similarity index 76% rename from x-pack/plugins/lens/public/editor_frame_service/embeddable/export_csv_action.tsx rename to src/plugins/dashboard/public/application/actions/export_csv_action.tsx index 493d783bf1f6a..47d70d2ed38e7 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/export_csv_action.tsx +++ b/src/plugins/dashboard/public/application/actions/export_csv_action.tsx @@ -1,16 +1,29 @@ /* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * 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 { FormatFactory } from 'src/plugins/data/common/field_formats/utils'; -import { DataPublicPluginStart, exportAsCSVs } from '../../../../../../src/plugins/data/public'; -import { downloadMultipleAs } from '../../../../../../src/plugins/share/public'; -import { Adapters, IEmbeddable } from '../../../../../../src/plugins/embeddable/public'; -import { ActionByType } from '../../../../../../src/plugins/ui_actions/public'; -import { CoreStart } from '../../../../../../src/core/public'; +import { FormatFactory } from '../../../../data/common/field_formats/utils'; +import { DataPublicPluginStart, exportAsCSVs } 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'; diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 24bf736cfa274..f75a954802d0a 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; } } @@ -416,7 +422,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); @@ -433,6 +439,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/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index ea68c9c46bd1c..24075facb68eb 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -6,11 +6,7 @@ import { AppMountParameters, CoreSetup, CoreStart } from 'kibana/public'; import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../../src/plugins/data/public'; -import { - CONTEXT_MENU_TRIGGER, - EmbeddableSetup, - EmbeddableStart, -} from '../../../../src/plugins/embeddable/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 { @@ -49,11 +45,6 @@ import { visualizeFieldAction } from './trigger_actions/visualize_field_actions' import { getSearchProvider } from './search_provider'; import { LensAttributeService } from './lens_attribute_service'; -import { - ACTION_EXPORT_CSV, - ExportContext, - ExportCSVAction, -} from './editor_frame_service/embeddable/export_csv_action'; export interface LensPluginSetupDependencies { urlForwarding: UrlForwardingSetup; @@ -77,12 +68,6 @@ export interface LensPluginStartDependencies { savedObjectsTagging?: SavedObjectTaggingPluginStart; } -declare module '../../../../src/plugins/ui_actions/public' { - export interface ActionContextMapping { - [ACTION_EXPORT_CSV]: ExportContext; - } -} - export class LensPlugin { private datatableVisualization: DatatableVisualization; private editorFrameService: EditorFrameService; @@ -201,9 +186,6 @@ export class LensPlugin { VISUALIZE_FIELD_TRIGGER, visualizeFieldAction(core.application) ); - - const ExportCSVPlugin = new ExportCSVAction({ core, data: startDependencies.data }); - startDependencies.uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, ExportCSVPlugin); } stop() { From 2d1a67f39b37697d1951193bd9abc540272394d8 Mon Sep 17 00:00:00 2001 From: dej611 Date: Mon, 23 Nov 2020 12:50:01 +0100 Subject: [PATCH 26/44] :bug: Fix Datatable fetching mechanism after app refactoring --- .../editor_frame_service/embeddable/embeddable.tsx | 13 ++++++++++++- .../embeddable/expression_wrapper.tsx | 4 ++++ 2 files changed, 16 insertions(+), 1 deletion(-) 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 3e1cd34bd46d0..1912108275d56 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 @@ -33,6 +33,7 @@ import { IContainer, SavedObjectEmbeddableInput, ReferenceOrValueEmbeddable, + Adapters, } from '../../../../../../src/plugins/embeddable/public'; import { Document, injectFilterReferences } from '../../persistence'; import { ExpressionWrapper } from './expression_wrapper'; @@ -43,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 { TableInspectorAdapter } from '../types'; export type LensSavedObjectAttributes = Omit; @@ -53,6 +55,7 @@ export type LensByValueInput = { export type LensByReferenceInput = SavedObjectEmbeddableInput & EmbeddableInput; export type LensEmbeddableInput = (LensByValueInput | LensByReferenceInput) & { palette?: PaletteOutput; + activeData?: TableInspectorAdapter; }; export interface LensEmbeddableOutput extends EmbeddableOutput { @@ -82,6 +85,7 @@ export class Embeddable private subscription: Subscription; private autoRefreshFetchSubscription: Subscription; private isInitialized = false; + private activeData: TableInspectorAdapter | undefined; private externalSearchContext: { timeRange?: TimeRange; @@ -130,7 +134,7 @@ export class Embeddable } public getInspectorAdapters() { - return this.savedVis?.state?.activeData; + return this.activeData; } async initializeSavedVis(input: LensEmbeddableInput) { @@ -177,6 +181,12 @@ export class Embeddable } } + private updateActiveData = (data: unknown, inspectorAdapters?: Adapters | undefined) => { + if (inspectorAdapters?.tables) { + this.activeData = inspectorAdapters.tables; + } + }; + /** * * @param {HTMLElement} domNode @@ -196,6 +206,7 @@ export class Embeddable variables={input.palette ? { theme: { palette: input.palette } } : {}} searchSessionId={this.input.searchSessionId} handleEvent={this.handleEvent} + onData$={this.updateActiveData} />, 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 4a3ba971381fb..15a150efcedbe 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 @@ -14,6 +14,7 @@ import { } from 'src/plugins/expressions/public'; import { ExecutionContextSearch } from 'src/plugins/data/public'; import { getOriginalRequestErrorMessage } from '../error_helper'; +import { LensInspectorAdapters } from '../types'; export interface ExpressionWrapperProps { ExpressionRenderer: ReactExpressionRendererType; @@ -22,6 +23,7 @@ export interface ExpressionWrapperProps { searchContext: ExecutionContextSearch; searchSessionId?: string; handleEvent: (event: ExpressionRendererEvent) => void; + onData$: (data: unknown, inspectorAdapters?: LensInspectorAdapters | undefined) => void; } export function ExpressionWrapper({ @@ -31,6 +33,7 @@ export function ExpressionWrapper({ variables, handleEvent, searchSessionId, + onData$, }: ExpressionWrapperProps) { return ( @@ -57,6 +60,7 @@ export function ExpressionWrapper({ expression={expression} searchContext={searchContext} searchSessionId={searchSessionId} + onData$={onData$} renderError={(errorMessage, error) => (
From c56bbfbf683be7dba7d732091a27b0c8ee64c54b Mon Sep 17 00:00:00 2001 From: dej611 Date: Mon, 23 Nov 2020 12:50:30 +0100 Subject: [PATCH 27/44] :sparkles: Make share plugin required --- src/plugins/dashboard/kibana.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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"] From 4cada295fee08c52a000ea06760d07bddc2fb751 Mon Sep 17 00:00:00 2001 From: dej611 Date: Mon, 23 Nov 2020 16:26:05 +0100 Subject: [PATCH 28/44] :ok_hand: Integrate feedback --- src/plugins/data/public/exports/export_csv.tsx | 4 ++-- x-pack/plugins/lens/public/persistence/saved_object_store.ts | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/plugins/data/public/exports/export_csv.tsx b/src/plugins/data/public/exports/export_csv.tsx index a58c449b8dbab..ba34ed65be3b5 100644 --- a/src/plugins/data/public/exports/export_csv.tsx +++ b/src/plugins/data/public/exports/export_csv.tsx @@ -50,7 +50,7 @@ interface CSVOptions { raw?: boolean; } -function buildCSV( +export function datatableToCSV( { columns, rows }: Datatable, { csvSeparator, quoteValues, formatFactory, raw }: Omit ) { @@ -104,7 +104,7 @@ export function exportAsCSVs( ); }) .reduce>((memo, layerId) => { - memo[layerId] = buildCSV(datatables[layerId], options); + memo[layerId] = datatableToCSV(datatables[layerId], options); return memo; }, {}); diff --git a/x-pack/plugins/lens/public/persistence/saved_object_store.ts b/x-pack/plugins/lens/public/persistence/saved_object_store.ts index ef3067e769173..2d293d4e0a5a0 100644 --- a/x-pack/plugins/lens/public/persistence/saved_object_store.ts +++ b/x-pack/plugins/lens/public/persistence/saved_object_store.ts @@ -11,7 +11,6 @@ import { } from 'kibana/public'; import { Query } from '../../../../../src/plugins/data/public'; import { DOC_TYPE, PersistableFilter } from '../../common'; -import { TableInspectorAdapter } from '../editor_frame_service/types'; export interface Document { savedObjectId?: string; @@ -28,7 +27,6 @@ export interface Document { state?: unknown; }; filters: PersistableFilter[]; - activeData?: TableInspectorAdapter; }; references: SavedObjectReference[]; } From 084a22f7337378540d8f3f5fabeec4fa496b8e33 Mon Sep 17 00:00:00 2001 From: dej611 Date: Mon, 23 Nov 2020 16:52:08 +0100 Subject: [PATCH 29/44] :memo: Update API doc --- ...ugin-plugins-data-public.datatabletocsv.md | 23 +++++++++++++++++++ .../kibana-plugin-plugins-data-public.md | 1 + src/plugins/data/public/public.api.md | 7 +++++- 3 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datatabletocsv.md diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datatabletocsv.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datatabletocsv.md new file mode 100644 index 0000000000000..0c3a536ee5c3b --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datatabletocsv.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [datatableToCSV](./kibana-plugin-plugins-data-public.datatabletocsv.md) + +## datatableToCSV() function + +Signature: + +```typescript +export declare function datatableToCSV({ columns, rows }: Datatable, { csvSeparator, quoteValues, formatFactory, raw }: Omit): string; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| { columns, rows } | Datatable | | +| { csvSeparator, quoteValues, formatFactory, raw } | Omit<CSVOptions, 'asString'> | | + +Returns: + +`string` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index 1232a8bbe3ddd..ef6ffd9d732e7 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -41,6 +41,7 @@ | Function | Description | | --- | --- | +| [datatableToCSV({ columns, rows }, { csvSeparator, quoteValues, formatFactory, raw })](./kibana-plugin-plugins-data-public.datatabletocsv.md) | | | [exportAsCSVs(filename, datatables, options)](./kibana-plugin-plugins-data-public.exportascsvs.md) | | | [getDefaultQuery(language)](./kibana-plugin-plugins-data-public.getdefaultquery.md) | | | [getEsPreference(uiSettings, sessionId)](./kibana-plugin-plugins-data-public.getespreference.md) | | diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 5a46dd7a1dee3..9bfca907a15f7 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -469,6 +469,12 @@ export interface DataPublicPluginStartUi { SearchBar: React.ComponentType; } +// Warning: (ae-forgotten-export) The symbol "CSVOptions" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "datatableToCSV" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export function datatableToCSV({ columns, rows }: Datatable_3, { csvSeparator, quoteValues, formatFactory, raw }: Omit): string; + // Warning: (ae-missing-release-tag) "DuplicateIndexPatternError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -679,7 +685,6 @@ export type ExistsFilter = Filter & { exists?: FilterExistsProperty; }; -// Warning: (ae-forgotten-export) The symbol "CSVOptions" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "exportAsCSVs" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) From 6079644924ce280cecc65ad7bc19ec3003171696 Mon Sep 17 00:00:00 2001 From: dej611 Date: Mon, 23 Nov 2020 17:54:00 +0100 Subject: [PATCH 30/44] :white_check_mark: Fix tests for exportable embeddable --- .../actions/export_csv_action.test.tsx | 19 +++-- .../application/actions/export_csv_action.tsx | 5 ++ .../contact_card/contact_card_embeddable.tsx | 18 ---- .../contact_card_exportable_embeddable.tsx | 42 ++++++++++ ...act_card_exportable_embeddable_factory.tsx | 84 +++++++++++++++++++ .../embeddables/contact_card/index.ts | 2 + 6 files changed, 144 insertions(+), 26 deletions(-) create mode 100644 src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_exportable_embeddable.tsx create mode 100644 src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_exportable_embeddable_factory.tsx 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 index 2bc9c31d99a01..4fbb48b4848e4 100644 --- a/src/plugins/dashboard/public/application/actions/export_csv_action.test.tsx +++ b/src/plugins/dashboard/public/application/actions/export_csv_action.test.tsx @@ -23,8 +23,8 @@ import { isErrorEmbeddable, IContainer, ErrorEmbeddable } from '../../embeddable import { DashboardContainer } from '../../application/embeddable'; import { getSampleDashboardInput, getSampleDashboardPanel } from '../../application/test_helpers'; import { - CONTACT_CARD_EMBEDDABLE, - ContactCardEmbeddableFactory, + CONTACT_CARD_EXPORTABLE_EMBEDDABLE, + ContactCardExportableEmbeddableFactory, ContactCardEmbeddable, ContactCardEmbeddableInput, ContactCardEmbeddableOutput, @@ -39,8 +39,8 @@ import { LINE_FEED_CHARACTER } from '../../../../data/public/exports/export_csv' describe('Export CSV action', () => { const { setup, doStart } = embeddablePluginMock.createInstance(); setup.registerEmbeddableFactory( - CONTACT_CARD_EMBEDDABLE, - new ContactCardEmbeddableFactory((() => null) as any, {} as any) + CONTACT_CARD_EXPORTABLE_EMBEDDABLE, + new ContactCardExportableEmbeddableFactory((() => null) as any, {} as any) ); const start = doStart(); @@ -73,7 +73,7 @@ describe('Export CSV action', () => { panels: { '123': getSampleDashboardPanel({ explicitInput: { firstName: 'Kibanana', id: '123' }, - type: CONTACT_CARD_EMBEDDABLE, + type: CONTACT_CARD_EXPORTABLE_EMBEDDABLE, }), }, }); @@ -84,7 +84,7 @@ describe('Export CSV action', () => { ContactCardEmbeddableInput, ContactCardEmbeddableOutput, ContactCardEmbeddable - >(CONTACT_CARD_EMBEDDABLE, { + >(CONTACT_CARD_EXPORTABLE_EMBEDDABLE, { firstName: 'Kibana', }); @@ -109,9 +109,12 @@ describe('Export CSV action', () => { const action = new ExportCSVAction({ core: coreStart, data: dataMock }); const result = ((await action.execute({ embeddable, asString: true })) as unknown) as | undefined - | Record; + | Record; expect(result).toEqual({ - 'Hello Kibana.csv': `First Name,Last Name${LINE_FEED_CHARACTER}Kibana,undefined${LINE_FEED_CHARACTER}`, + 'Hello Kibana.csv': { + content: `First Name,Last Name${LINE_FEED_CHARACTER}Kibana,undefined${LINE_FEED_CHARACTER}`, + type: 'text/plain;charset=utf-8', + }, }); }); diff --git a/src/plugins/dashboard/public/application/actions/export_csv_action.tsx b/src/plugins/dashboard/public/application/actions/export_csv_action.tsx index 47d70d2ed38e7..7ae17c60bf101 100644 --- a/src/plugins/dashboard/public/application/actions/export_csv_action.tsx +++ b/src/plugins/dashboard/public/application/actions/export_csv_action.tsx @@ -143,6 +143,11 @@ export class ExportCSVAction implements ActionByType { quoteValues: this.params.core.uiSettings.get('csv:quoteValues', true), formatFactory, }); + + if (context.asString) { + return content; + } + if (content) { return downloadMultipleAs(content); } diff --git a/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_embeddable.tsx b/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_embeddable.tsx index 46b8b958f86e9..b82cd9ca7cc31 100644 --- a/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_embeddable.tsx +++ b/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_embeddable.tsx @@ -95,24 +95,6 @@ export class ContactCardEmbeddable extends Embeddable< } public reload() {} - - public getInspectorAdapters = () => { - return { - 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_USER_TRIGGER = 'CONTACT_USER_TRIGGER'; 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..c605e5603bafd --- /dev/null +++ b/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_exportable_embeddable.tsx @@ -0,0 +1,42 @@ +/* + * 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 { + 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..50e5dc24a2101 --- /dev/null +++ b/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_exportable_embeddable_factory.tsx @@ -0,0 +1,84 @@ +/* + * 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(); + 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'; From ca868b315aa6bc81c7dc88d5c69fee23877e97c2 Mon Sep 17 00:00:00 2001 From: dej611 Date: Mon, 23 Nov 2020 19:24:52 +0100 Subject: [PATCH 31/44] :recycle: Refactor export plugin as per feedback received --- .../data/common/exports/export_csv.test.ts | 85 +++++++++++++ .../{public => common}/exports/export_csv.tsx | 47 +------ .../data/{public => common}/exports/index.ts | 2 +- src/plugins/data/common/index.ts | 1 + .../data/public/exports/export_csv.test.ts | 117 ------------------ src/plugins/data/public/index.ts | 6 +- src/plugins/data/server/index.ts | 10 ++ x-pack/plugins/lens/public/app_plugin/app.tsx | 30 ++++- 8 files changed, 131 insertions(+), 167 deletions(-) create mode 100644 src/plugins/data/common/exports/export_csv.test.ts rename src/plugins/data/{public => common}/exports/export_csv.tsx (61%) rename src/plugins/data/{public => common}/exports/index.ts (92%) delete mode 100644 src/plugins/data/public/exports/export_csv.test.ts diff --git a/src/plugins/data/common/exports/export_csv.test.ts b/src/plugins/data/common/exports/export_csv.test.ts new file mode 100644 index 0000000000000..73878111b1479 --- /dev/null +++ b/src/plugins/data/common/exports/export_csv.test.ts @@ -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 { Datatable } from 'src/plugins/expressions'; +import { FieldFormat } from '../../common/field_formats'; +import { datatableToCSV } from './export_csv'; + +function getDefaultOptions() { + const formatFactory = jest.fn(); + formatFactory.mockReturnValue({ convert: (v: unknown) => `Formatted_${v}` } as FieldFormat); + return { + csvSeparator: ',', + quoteValues: true, + formatFactory, + }; +} + +function getDataTable({ multipleColumns }: { multipleColumns?: boolean } = {}): Datatable { + const layer1: Datatable = { + type: 'datatable', + columns: [{ id: 'col1', name: 'columnOne', meta: { type: 'string' } }], + rows: [{ col1: 'value' }], + }; + if (multipleColumns) { + layer1.columns.push({ id: 'col2', name: 'columnTwo', meta: { type: 'number' } }); + layer1.rows[0].col2 = 5; + } + return layer1; +} + +describe('CSV exporter', () => { + test('should not break with empty data', () => { + expect( + datatableToCSV({ type: 'datatable', columns: [], rows: [] }, getDefaultOptions()) + ).toMatch(''); + }); + + test('should export formatted values by default', () => { + expect(datatableToCSV(getDataTable(), getDefaultOptions())).toMatch( + 'columnOne\r\n"Formatted_value"\r\n' + ); + }); + + test('should not quote values when requested', () => { + return expect( + datatableToCSV(getDataTable(), { ...getDefaultOptions(), quoteValues: false }) + ).toMatch('columnOne\r\nFormatted_value\r\n'); + }); + + test('should use raw values when requested', () => { + expect(datatableToCSV(getDataTable(), { ...getDefaultOptions(), raw: true })).toMatch( + 'columnOne\r\nvalue\r\n' + ); + }); + + test('should use separator for multiple columns', () => { + expect(datatableToCSV(getDataTable({ multipleColumns: true }), getDefaultOptions())).toMatch( + 'columnOne,columnTwo\r\n"Formatted_value","Formatted_5"\r\n' + ); + }); + + test('should escape values', () => { + const datatable = getDataTable(); + datatable.rows[0].col1 = '"value"'; + expect(datatableToCSV(datatable, getDefaultOptions())).toMatch( + 'columnOne\r\n"Formatted_""value"""\r\n' + ); + }); +}); diff --git a/src/plugins/data/public/exports/export_csv.tsx b/src/plugins/data/common/exports/export_csv.tsx similarity index 61% rename from src/plugins/data/public/exports/export_csv.tsx rename to src/plugins/data/common/exports/export_csv.tsx index ba34ed65be3b5..1e1420c245eb4 100644 --- a/src/plugins/data/public/exports/export_csv.tsx +++ b/src/plugins/data/common/exports/export_csv.tsx @@ -21,7 +21,6 @@ import { FormatFactory } from 'src/plugins/data/common/field_formats/utils'; import { Datatable } from 'src/plugins/expressions'; -import { DownloadableContent } from 'src/plugins/share/public/'; const LINE_FEED_CHARACTER = '\r\n'; const nonAlphaNumRE = /[^a-zA-Z0-9]/; @@ -52,7 +51,7 @@ interface CSVOptions { export function datatableToCSV( { columns, rows }: Datatable, - { csvSeparator, quoteValues, formatFactory, raw }: Omit + { csvSeparator, quoteValues, formatFactory, raw }: CSVOptions ) { // Build the header row by its names const header = columns.map((col) => escape(col.name, quoteValues)); @@ -72,48 +71,12 @@ export function datatableToCSV( ); }); + if (header.length === 0) { + return ''; + } + return ( [header, ...csvRows].map((row) => row.join(csvSeparator)).join(LINE_FEED_CHARACTER) + LINE_FEED_CHARACTER ); // Add \r\n after last line } - -/** - * - * @param filename - filename to use (either as is, or as prefix for multiple CSVs) for the files to download - * @param datatables - data (as a dictionary of Datatable) to be translated into CSVs. It can contain multiple tables. - * @param options - set of options for the exporter - * - * @returns A dictionary of files to download: the key is the filename and the value the CSV string - */ -export function exportAsCSVs( - filename: string, - datatables: Record | undefined, - options: CSVOptions -) { - if (datatables == null) { - return; - } - // build a csv for datatable layer - const csvs = Object.keys(datatables) - .filter((layerId) => { - return ( - datatables[layerId].columns.length && - datatables[layerId].rows.length && - datatables[layerId].rows.every((row) => Object.keys(row).length) - ); - }) - .reduce>((memo, layerId) => { - memo[layerId] = datatableToCSV(datatables[layerId], options); - return memo; - }, {}); - - const layerIds = Object.keys(csvs); - - return layerIds.reduce>>((memo, layerId, i) => { - const content = csvs[layerId]; - const postFix = layerIds.length > 1 ? `-${i + 1}` : ''; - memo[`${filename}${postFix}.csv`] = { content, type: CSV_MIME_TYPE }; - return memo; - }, {}); -} diff --git a/src/plugins/data/public/exports/index.ts b/src/plugins/data/common/exports/index.ts similarity index 92% rename from src/plugins/data/public/exports/index.ts rename to src/plugins/data/common/exports/index.ts index 6870acfe8547e..72faac654b421 100644 --- a/src/plugins/data/public/exports/index.ts +++ b/src/plugins/data/common/exports/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export * from './export_csv'; +export { datatableToCSV, CSV_MIME_TYPE } from './export_csv'; diff --git a/src/plugins/data/common/index.ts b/src/plugins/data/common/index.ts index 2d6637daf4324..36129a4d3f8cd 100644 --- a/src/plugins/data/common/index.ts +++ b/src/plugins/data/common/index.ts @@ -26,6 +26,7 @@ export * from './query'; export * from './search'; export * from './types'; export * from './utils'; +export * from './exports'; /** * Use data plugin interface instead diff --git a/src/plugins/data/public/exports/export_csv.test.ts b/src/plugins/data/public/exports/export_csv.test.ts deleted file mode 100644 index c7bdcbd973ae3..0000000000000 --- a/src/plugins/data/public/exports/export_csv.test.ts +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { Datatable } from 'src/plugins/expressions'; -import { FieldFormat } from '../../common/field_formats'; -import { CSV_MIME_TYPE, exportAsCSVs } from './export_csv'; - -function getDefaultOptions() { - const formatFactory = jest.fn(); - formatFactory.mockReturnValue({ convert: (v: unknown) => `Formatted_${v}` } as FieldFormat); - return { - csvSeparator: ',', - quoteValues: true, - formatFactory, - }; -} - -function getDataTable({ - multipleLayers, - multipleColumns, -}: { multipleLayers?: boolean; multipleColumns?: boolean } = {}): Record { - const datatables: Record = { - layer1: { - type: 'datatable', - columns: [{ id: 'col1', name: 'columnOne', meta: { type: 'string' } }], - rows: [{ col1: 'value' }], - }, - }; - if (multipleColumns) { - datatables.layer1.columns.push({ id: 'col2', name: 'columnTwo', meta: { type: 'number' } }); - datatables.layer1.rows[0].col2 = 5; - } - if (multipleLayers) { - datatables.layer2 = { - type: 'datatable', - columns: [{ id: 'col1', name: 'columnOne', meta: { type: 'string' } }], - rows: [{ col1: 'value' }], - }; - } - return datatables; -} - -describe('CSV exporter', () => { - test('should do nothing with no data', () => { - expect(exportAsCSVs('noData', undefined, getDefaultOptions())).toStrictEqual(undefined); - }); - - test('should not break with empty data', () => { - expect(exportAsCSVs('emptyFile', {}, getDefaultOptions())).toStrictEqual({}); - }); - - test('should export formatted values by default', () => { - expect(exportAsCSVs('oneCSV', getDataTable(), getDefaultOptions())).toStrictEqual({ - 'oneCSV.csv': { content: 'columnOne\r\n"Formatted_value"\r\n', type: CSV_MIME_TYPE }, - }); - }); - - test('should not quote values when requested', () => { - return expect( - exportAsCSVs('oneCSV', getDataTable(), { ...getDefaultOptions(), quoteValues: false }) - ).toStrictEqual({ - 'oneCSV.csv': { content: 'columnOne\r\nFormatted_value\r\n', type: CSV_MIME_TYPE }, - }); - }); - - test('should use raw values when requested', () => { - expect( - exportAsCSVs('oneCSV', getDataTable(), { ...getDefaultOptions(), raw: true }) - ).toStrictEqual({ - 'oneCSV.csv': { content: 'columnOne\r\nvalue\r\n', type: CSV_MIME_TYPE }, - }); - }); - - test('should use separator for multiple columns', () => { - expect( - exportAsCSVs('oneCSV', getDataTable({ multipleColumns: true }), getDefaultOptions()) - ).toStrictEqual({ - 'oneCSV.csv': { - content: 'columnOne,columnTwo\r\n"Formatted_value","Formatted_5"\r\n', - type: CSV_MIME_TYPE, - }, - }); - }); - - test('should support multiple layers', () => { - expect( - exportAsCSVs('twoCSVs', getDataTable({ multipleLayers: true }), getDefaultOptions()) - ).toStrictEqual({ - 'twoCSVs-1.csv': { content: 'columnOne\r\n"Formatted_value"\r\n', type: CSV_MIME_TYPE }, - 'twoCSVs-2.csv': { content: 'columnOne\r\n"Formatted_value"\r\n', type: CSV_MIME_TYPE }, - }); - }); - - test('should escape values', () => { - const datatables = getDataTable(); - datatables.layer1.rows[0].col1 = '"value"'; - expect(exportAsCSVs('oneCSV', datatables, getDefaultOptions())).toStrictEqual({ - 'oneCSV.csv': { content: 'columnOne\r\n"Formatted_""value"""\r\n', type: CSV_MIME_TYPE }, - }); - }); -}); diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 9a536230ccb9e..e0b0c5a0ea980 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -216,7 +216,11 @@ export { * Exporters (CSV) */ -export * from './exports'; +import { datatableToCSV, CSV_MIME_TYPE } from '../common'; +export const exporters = { + datatableToCSV, + CSV_MIME_TYPE, +}; /* * Index patterns: diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index e24869f5237ea..9d85caa624e7a 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -49,6 +49,16 @@ export const esFilters = { isFilterDisabled, }; +/** + * Exporters (CSV) + */ + +import { datatableToCSV, CSV_MIME_TYPE } from '../common'; +export const exporters = { + datatableToCSV, + CSV_MIME_TYPE, +}; + /* * esQuery and esKuery: */ diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 4b7581908a3ef..addc263acca29 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -26,7 +26,7 @@ import { NativeRenderer } from '../native_renderer'; import { trackUiEvent } from '../lens_ui_telemetry'; import { esFilters, - exportAsCSVs, + exporters, IndexPattern as IndexPatternInstance, IndexPatternsContract, syncQueryStateWithUrl, @@ -493,11 +493,29 @@ export function App({ savingPermitted, actions: { exportToCSV: () => { - const content = exportAsCSVs(lastKnownDoc?.title || unsavedTitle, state.activeData, { - csvSeparator: uiSettings.get('csv:separator', ','), - quoteValues: uiSettings.get('csv:quoteValues', true), - formatFactory: data.fieldFormats.deserialize, - }); + if (!state.activeData) { + return; + } + const datatables = Object.values(state.activeData); + const content = datatables.reduce>( + (memo, datatable, i) => { + // skip empty datatables + if (datatable) { + const postFix = datatables.length > 1 ? `-${i + 1}` : ''; + + memo[`${lastKnownDoc?.title || unsavedTitle}${postFix}.csv`] = { + content: exporters.datatableToCSV(datatable, { + csvSeparator: uiSettings.get('csv:separator', ','), + quoteValues: uiSettings.get('csv:quoteValues', true), + formatFactory: data.fieldFormats.deserialize, + }), + type: exporters.CSV_MIME_TYPE, + }; + } + return memo; + }, + {} + ); if (content) { downloadMultipleAs(content); } From 0e7770c94d93f945037c12213069779a7c92892b Mon Sep 17 00:00:00 2001 From: dej611 Date: Mon, 23 Nov 2020 19:32:06 +0100 Subject: [PATCH 32/44] :memo: Update API doc --- ...lugin-plugins-data-public.csv_mime_type.md | 11 --- ...ugin-plugins-data-public.datatabletocsv.md | 23 ------ ...plugin-plugins-data-public.exportascsvs.md | 32 -------- ...na-plugin-plugins-data-public.exporters.md | 14 ++++ .../kibana-plugin-plugins-data-public.md | 4 +- ...na-plugin-plugins-data-server.exporters.md | 14 ++++ .../kibana-plugin-plugins-data-server.md | 1 + src/plugins/data/public/public.api.md | 68 +++++++--------- src/plugins/data/server/server.api.md | 81 +++++++++++-------- 9 files changed, 105 insertions(+), 143 deletions(-) delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.csv_mime_type.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datatabletocsv.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.exportascsvs.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.exporters.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.exporters.md diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.csv_mime_type.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.csv_mime_type.md deleted file mode 100644 index 5b81d09ddf60a..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.csv_mime_type.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [CSV\_MIME\_TYPE](./kibana-plugin-plugins-data-public.csv_mime_type.md) - -## CSV\_MIME\_TYPE variable - -Signature: - -```typescript -CSV_MIME_TYPE = "text/plain;charset=utf-8" -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datatabletocsv.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datatabletocsv.md deleted file mode 100644 index 0c3a536ee5c3b..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datatabletocsv.md +++ /dev/null @@ -1,23 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [datatableToCSV](./kibana-plugin-plugins-data-public.datatabletocsv.md) - -## datatableToCSV() function - -Signature: - -```typescript -export declare function datatableToCSV({ columns, rows }: Datatable, { csvSeparator, quoteValues, formatFactory, raw }: Omit): string; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| { columns, rows } | Datatable | | -| { csvSeparator, quoteValues, formatFactory, raw } | Omit<CSVOptions, 'asString'> | | - -Returns: - -`string` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.exportascsvs.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.exportascsvs.md deleted file mode 100644 index fe6f2f1f84e9c..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.exportascsvs.md +++ /dev/null @@ -1,32 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [exportAsCSVs](./kibana-plugin-plugins-data-public.exportascsvs.md) - -## exportAsCSVs() function - -Signature: - -```typescript -export declare function exportAsCSVs(filename: string, datatables: Record | undefined, options: CSVOptions): Record | undefined; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| filename | string | filename to use (either as is, or as prefix for multiple CSVs) for the files to download | -| datatables | Record<string, Datatable> | undefined | data (as a dictionary of Datatable) to be translated into CSVs. It can contain multiple tables. | -| options | CSVOptions | set of options for the exporter | - -Returns: - -`Record | undefined` - -A dictionary of files to download: the key is the filename and the value the CSV string - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.exporters.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.exporters.md new file mode 100644 index 0000000000000..883dbcfe289cb --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.exporters.md @@ -0,0 +1,14 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [exporters](./kibana-plugin-plugins-data-public.exporters.md) + +## exporters variable + +Signature: + +```typescript +exporters: { + datatableToCSV: typeof datatableToCSV; + CSV_MIME_TYPE: string; +} +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index ef6ffd9d732e7..b8e45cde3c18b 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -41,8 +41,6 @@ | Function | Description | | --- | --- | -| [datatableToCSV({ columns, rows }, { csvSeparator, quoteValues, formatFactory, raw })](./kibana-plugin-plugins-data-public.datatabletocsv.md) | | -| [exportAsCSVs(filename, datatables, options)](./kibana-plugin-plugins-data-public.exportascsvs.md) | | | [getDefaultQuery(language)](./kibana-plugin-plugins-data-public.getdefaultquery.md) | | | [getEsPreference(uiSettings, sessionId)](./kibana-plugin-plugins-data-public.getespreference.md) | | | [getSearchParamsFromRequest(searchRequest, dependencies)](./kibana-plugin-plugins-data-public.getsearchparamsfromrequest.md) | | @@ -106,11 +104,11 @@ | [castEsToKbnFieldTypeName](./kibana-plugin-plugins-data-public.castestokbnfieldtypename.md) | Get the KbnFieldType name for an esType string | | [connectToQueryState](./kibana-plugin-plugins-data-public.connecttoquerystate.md) | Helper to setup two-way syncing of global data and a state container | | [createSavedQueryService](./kibana-plugin-plugins-data-public.createsavedqueryservice.md) | | -| [CSV\_MIME\_TYPE](./kibana-plugin-plugins-data-public.csv_mime_type.md) | | | [ES\_SEARCH\_STRATEGY](./kibana-plugin-plugins-data-public.es_search_strategy.md) | | | [esFilters](./kibana-plugin-plugins-data-public.esfilters.md) | | | [esKuery](./kibana-plugin-plugins-data-public.eskuery.md) | | | [esQuery](./kibana-plugin-plugins-data-public.esquery.md) | | +| [exporters](./kibana-plugin-plugins-data-public.exporters.md) | | | [extractSearchSourceReferences](./kibana-plugin-plugins-data-public.extractsearchsourcereferences.md) | | | [fieldFormats](./kibana-plugin-plugins-data-public.fieldformats.md) | | | [fieldList](./kibana-plugin-plugins-data-public.fieldlist.md) | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.exporters.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.exporters.md new file mode 100644 index 0000000000000..6fda400d09fd0 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.exporters.md @@ -0,0 +1,14 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [exporters](./kibana-plugin-plugins-data-server.exporters.md) + +## exporters variable + +Signature: + +```typescript +exporters: { + datatableToCSV: typeof datatableToCSV; + CSV_MIME_TYPE: string; +} +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md index 8957f6d0f06b4..d9f14950be0e8 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md @@ -76,6 +76,7 @@ | [esFilters](./kibana-plugin-plugins-data-server.esfilters.md) | | | [esKuery](./kibana-plugin-plugins-data-server.eskuery.md) | | | [esQuery](./kibana-plugin-plugins-data-server.esquery.md) | | +| [exporters](./kibana-plugin-plugins-data-server.exporters.md) | | | [fieldFormats](./kibana-plugin-plugins-data-server.fieldformats.md) | | | [indexPatterns](./kibana-plugin-plugins-data-server.indexpatterns.md) | | | [mergeCapabilitiesWithFields](./kibana-plugin-plugins-data-server.mergecapabilitieswithfields.md) | | diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index ae5d29c41d194..e1af3cc1d1b4d 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -17,8 +17,8 @@ import { CoreSetup } from 'src/core/public'; import { CoreSetup as CoreSetup_2 } from 'kibana/public'; import { CoreStart } from 'kibana/public'; import { CoreStart as CoreStart_2 } from 'src/core/public'; -import { Datatable as Datatable_2 } from 'src/plugins/expressions/common'; -import { Datatable as Datatable_3 } from 'src/plugins/expressions'; +import { Datatable as Datatable_2 } from 'src/plugins/expressions'; +import { Datatable as Datatable_3 } from 'src/plugins/expressions/common'; import { DatatableColumn as DatatableColumn_2 } from 'src/plugins/expressions'; import { Ensure } from '@kbn/utility-types'; import { EnvironmentMode } from '@kbn/config'; @@ -398,11 +398,6 @@ export const connectToQueryState: ({ timefilter: { timefil // @public (undocumented) export const createSavedQueryService: (savedObjectsClient: SavedObjectsClientContract) => SavedQueryService; -// Warning: (ae-missing-release-tag) "CSV_MIME_TYPE" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const CSV_MIME_TYPE = "text/plain;charset=utf-8"; - // Warning: (ae-missing-release-tag) "CustomFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -469,12 +464,6 @@ export interface DataPublicPluginStartUi { SearchBar: React.ComponentType; } -// Warning: (ae-forgotten-export) The symbol "CSVOptions" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "datatableToCSV" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export function datatableToCSV({ columns, rows }: Datatable_3, { csvSeparator, quoteValues, formatFactory, raw }: Omit): string; - // Warning: (ae-missing-release-tag) "DuplicateIndexPatternError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -685,13 +674,13 @@ export type ExistsFilter = Filter & { exists?: FilterExistsProperty; }; -// Warning: (ae-missing-release-tag) "exportAsCSVs" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// Warning: (ae-missing-release-tag) "exporters" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function exportAsCSVs(filename: string, datatables: Record | undefined, options: CSVOptions): Record | undefined; +export const exporters: { + datatableToCSV: typeof datatableToCSV; + CSV_MIME_TYPE: string; +}; // Warning: (ae-missing-release-tag) "ExpressionFunctionKibana" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -2413,27 +2402,28 @@ export const UI_SETTINGS: { // src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "UrlFormat" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "StringFormat" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:242:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:242:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:242:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:242:27 - (ae-forgotten-export) The symbol "getFromSavedObject" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:242:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:242:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:399:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:399:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:399:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:399:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:401:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:402:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:411:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:412:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:413:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:414:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:418:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:419:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:422:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:423:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:426:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:220:23 - (ae-forgotten-export) The symbol "datatableToCSV" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:246:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:246:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:246:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:246:27 - (ae-forgotten-export) The symbol "getFromSavedObject" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:246:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:246:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:403:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:403:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:403:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:403:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:405:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:406:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:415:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:416:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:417:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:418:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:422:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:423:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:426:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:427:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:430:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:45:5 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 94114288eb1f3..6583651e074c3 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -14,7 +14,8 @@ import { CoreSetup } from 'src/core/server'; import { CoreSetup as CoreSetup_2 } from 'kibana/server'; import { CoreStart } from 'src/core/server'; import { CoreStart as CoreStart_2 } from 'kibana/server'; -import { Datatable } from 'src/plugins/expressions/common'; +import { Datatable } from 'src/plugins/expressions'; +import { Datatable as Datatable_2 } from 'src/plugins/expressions/common'; import { DatatableColumn } from 'src/plugins/expressions'; import { Duration } from 'moment'; import { ElasticsearchClient } from 'src/core/server'; @@ -27,6 +28,7 @@ import { ExpressionAstFunction } from 'src/plugins/expressions/common'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; import { ExpressionValueBoxed } from 'src/plugins/expressions/common'; +import { FormatFactory } from 'src/plugins/data/common/field_formats/utils'; import { ISavedObjectsRepository } from 'src/core/server'; import { IScopedClusterClient } from 'src/core/server'; import { ISearchOptions as ISearchOptions_2 } from 'src/plugins/data/public'; @@ -299,6 +301,14 @@ export type ExecutionContextSearch = { timeRange?: TimeRange; }; +// Warning: (ae-missing-release-tag) "exporters" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const exporters: { + datatableToCSV: typeof datatableToCSV; + CSV_MIME_TYPE: string; +}; + // Warning: (ae-missing-release-tag) "ExpressionFunctionKibana" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1216,40 +1226,41 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:135:7 - (ae-forgotten-export) The symbol "FieldAttrSet" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:40:23 - (ae-forgotten-export) The symbol "buildCustomFilter" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:40:23 - (ae-forgotten-export) The symbol "buildFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:71:21 - (ae-forgotten-export) The symbol "getEsQueryConfig" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:71:21 - (ae-forgotten-export) The symbol "buildEsQuery" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "FieldFormatsRegistry" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "FieldFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "BoolFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "BytesFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "ColorFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "DurationFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "IpFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "NumberFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "PercentFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "RelativeDateFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "SourceFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "StaticLookupFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "UrlFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "StringFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:127:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:127:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:243:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:243:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:243:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:243:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:258:5 - (ae-forgotten-export) The symbol "getTotalLoaded" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:259:5 - (ae-forgotten-export) The symbol "toSnakeCase" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:263:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:264:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:273:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:274:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:275:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:279:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:280:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:284:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:287:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:57:23 - (ae-forgotten-export) The symbol "datatableToCSV" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:81:21 - (ae-forgotten-export) The symbol "getEsQueryConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:81:21 - (ae-forgotten-export) The symbol "buildEsQuery" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:111:26 - (ae-forgotten-export) The symbol "FieldFormatsRegistry" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:111:26 - (ae-forgotten-export) The symbol "FieldFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:111:26 - (ae-forgotten-export) The symbol "BoolFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:111:26 - (ae-forgotten-export) The symbol "BytesFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:111:26 - (ae-forgotten-export) The symbol "ColorFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:111:26 - (ae-forgotten-export) The symbol "DurationFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:111:26 - (ae-forgotten-export) The symbol "IpFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:111:26 - (ae-forgotten-export) The symbol "NumberFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:111:26 - (ae-forgotten-export) The symbol "PercentFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:111:26 - (ae-forgotten-export) The symbol "RelativeDateFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:111:26 - (ae-forgotten-export) The symbol "SourceFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:111:26 - (ae-forgotten-export) The symbol "StaticLookupFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:111:26 - (ae-forgotten-export) The symbol "UrlFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:111:26 - (ae-forgotten-export) The symbol "StringFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:111:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:137:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:137:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:253:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:253:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:253:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:253:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:268:5 - (ae-forgotten-export) The symbol "getTotalLoaded" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:269:5 - (ae-forgotten-export) The symbol "toSnakeCase" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:273:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:274:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:283:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:284:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:285:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:289:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:290:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:294:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:297:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index_patterns/index_patterns_service.ts:58:14 - (ae-forgotten-export) The symbol "IndexPatternsService" needs to be exported by the entry point index.d.ts // src/plugins/data/server/plugin.ts:88:66 - (ae-forgotten-export) The symbol "DataEnhancements" needs to be exported by the entry point index.d.ts // src/plugins/data/server/search/types.ts:104:5 - (ae-forgotten-export) The symbol "ISearchStartSearchSource" needs to be exported by the entry point index.d.ts From 80eac9faba36c53ecb19e49a99a2850547941277 Mon Sep 17 00:00:00 2001 From: dej611 Date: Mon, 23 Nov 2020 19:39:00 +0100 Subject: [PATCH 33/44] :ok_hand: use named exports --- src/plugins/share/public/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/plugins/share/public/index.ts b/src/plugins/share/public/index.ts index 2153a98648d0b..9f98d9c21d233 100644 --- a/src/plugins/share/public/index.ts +++ b/src/plugins/share/public/index.ts @@ -41,6 +41,7 @@ export { import { SharePlugin } from './plugin'; export { KibanaURL } from './kibana_url'; -export * from './lib/download_as'; +export { downloadMultipleAs, downloadFileAs } from './lib/download_as'; +export type { DownloadableContent } from './lib/download_as'; export const plugin = () => new SharePlugin(); From c3941a17b3a6e8c8e786bf889ea7932bb260c980 Mon Sep 17 00:00:00 2001 From: dej611 Date: Tue, 24 Nov 2020 10:17:32 +0100 Subject: [PATCH 34/44] :white_check_mark: Add basic functional test --- .../test/functional/apps/lens/smokescreen.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/x-pack/test/functional/apps/lens/smokescreen.ts b/x-pack/test/functional/apps/lens/smokescreen.ts index 29b42230673c9..b91399a4a6756 100644 --- a/x-pack/test/functional/apps/lens/smokescreen.ts +++ b/x-pack/test/functional/apps/lens/smokescreen.ts @@ -330,5 +330,26 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.switchFirstLayerIndexPattern('log*'); expect(await PageObjects.lens.getFirstLayerIndexPattern()).to.equal('log*'); }); + + it('should show a download button only when the configuration is valid', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + await PageObjects.lens.switchToVisualization('pie'); + await PageObjects.lens.configureDimension({ + dimension: 'lnsPie_sliceByDimensionPanel > lns-empty-dimension', + operation: 'date_histogram', + field: '@timestamp', + }); + // incomplete configuration should not be downloadable + expect(await testSubjects.isEnabled('lnsApp_downloadCSVButton')).to.eql(false); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsPie_sizeByDimensionPanel > lns-empty-dimension', + operation: 'avg', + field: 'bytes', + }); + expect(await testSubjects.isEnabled('lnsApp_downloadCSVButton')).to.eql(true); + }); }); } From a81ca6241b8e0393f29613bc6a9169fd0c98ef72 Mon Sep 17 00:00:00 2001 From: dej611 Date: Tue, 24 Nov 2020 11:33:27 +0100 Subject: [PATCH 35/44] :memo: + :label: + :recycle: Align its parent PR + some fixes --- ...ublic.uiactionsservice.addtriggeraction.md | 2 +- ...tions-public.uiactionsservice.getaction.md | 2 +- ...blic.uiactionsservice.gettriggeractions.md | 2 +- ...ionsservice.gettriggercompatibleactions.md | 2 +- ...gins-ui_actions-public.uiactionsservice.md | 10 ++-- ...-public.uiactionsservice.registeraction.md | 2 +- .../application/actions/export_csv_action.tsx | 53 +++++++++++++------ .../public/application/actions/index.ts | 1 + src/plugins/ui_actions/public/public.api.md | 10 ++-- 9 files changed, 52 insertions(+), 32 deletions(-) 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/src/plugins/dashboard/public/application/actions/export_csv_action.tsx b/src/plugins/dashboard/public/application/actions/export_csv_action.tsx index 7ae17c60bf101..4dc9384253155 100644 --- a/src/plugins/dashboard/public/application/actions/export_csv_action.tsx +++ b/src/plugins/dashboard/public/application/actions/export_csv_action.tsx @@ -18,8 +18,9 @@ */ import { i18n } from '@kbn/i18n'; +import { Datatable } from 'src/plugins/expressions/public'; import { FormatFactory } from '../../../../data/common/field_formats/utils'; -import { DataPublicPluginStart, exportAsCSVs } from '../../../../data/public'; +import { DataPublicPluginStart, exporters } from '../../../../data/public'; import { downloadMultipleAs } from '../../../../share/public'; import { Adapters, IEmbeddable } from '../../../../embeddable/public'; import { ActionByType } from '../../../../ui_actions/public'; @@ -56,7 +57,7 @@ export class ExportCSVAction implements ActionByType { } public readonly getDisplayName = (context: ExportContext): string => - i18n.translate('xpack.lens.DownloadCreateDrilldownAction.displayName', { + i18n.translate('dashboard.actions.DownloadCreateDrilldownAction.displayName', { defaultMessage: 'Download as CSV', }); @@ -103,12 +104,15 @@ export class ExportCSVAction implements ActionByType { } // Visualize if (type === 'visualization') { - const datatable = await adapters.data.tabular(); - datatable.columns = datatable.columns.map(({ field, ...rest }: { field: string }) => ({ - id: field, - field, - ...rest, - })); + const tabularData = (await adapters.data?.getTabular()!).data!; + const datatable = { + columns: tabularData.columns.map(({ field, ...rest }: { field: string }) => ({ + id: field, + field, + ...rest, + })), + rows: tabularData.rows, + }; return { layer1: datatable }; } // Lens @@ -132,17 +136,32 @@ export class ExportCSVAction implements ActionByType { if (!formatFactory) { return; } - const datatables = await this.getDataTableContent( + const adapters = (await this.getDataTableContent( context?.embeddable?.type, context?.embeddable?.getInspectorAdapters() - ); - - if (datatables) { - const content = exportAsCSVs(context?.embeddable?.getTitle()!, datatables, { - csvSeparator: this.params.core.uiSettings.get('csv:separator', ','), - quoteValues: this.params.core.uiSettings.get('csv:quoteValues', true), - formatFactory, - }); + )) as Record; + + if (adapters) { + const datatables = Object.values(adapters); + const content = datatables.reduce>( + (memo, datatable, i) => { + // skip empty datatables + if (datatable) { + const postFix = datatables.length > 1 ? `-${i + 1}` : ''; + + memo[`${context!.embeddable!.getTitle()!}${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; + }, + {} + ); if (context.asString) { return content; 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/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 From acd5049b9868f2d9735a18144153fecc211482d3 Mon Sep 17 00:00:00 2001 From: dej611 Date: Thu, 26 Nov 2020 19:26:34 +0100 Subject: [PATCH 36/44] :ok_hand: Remove visualize and lens specific logic from here --- .../application/actions/export_csv_action.tsx | 70 +++---------------- 1 file changed, 11 insertions(+), 59 deletions(-) diff --git a/src/plugins/dashboard/public/application/actions/export_csv_action.tsx b/src/plugins/dashboard/public/application/actions/export_csv_action.tsx index 4dc9384253155..1ceb96b9b4030 100644 --- a/src/plugins/dashboard/public/application/actions/export_csv_action.tsx +++ b/src/plugins/dashboard/public/application/actions/export_csv_action.tsx @@ -62,65 +62,20 @@ export class ExportCSVAction implements ActionByType { }); public async isCompatible(context: ExportContext): Promise { - return Boolean( - context.embeddable && - 'getInspectorAdapters' in context.embeddable && - this.hasDatatableContent(context.embeddable.getInspectorAdapters()) - ); + return this.hasDatatableContent(context.embeddable?.getInspectorAdapters?.()); } private hasDatatableContent = (adapters: Adapters | undefined) => { - return adapters && (adapters.data || adapters[Object.keys(adapters)[0]]?.columns); + return adapters && adapters[Object.keys(adapters)[0]]?.columns; }; - private getFormatter = ( - type: string | undefined, - adapters: Adapters | undefined - ): FormatFactory | undefined => { - if (type === 'lens') { + private getFormatter = (): FormatFactory | undefined => { + if (this.params.data) { return this.params.data.fieldFormats.deserialize; } - - if (type === 'visualization') { - return (() => ({ - convert: (item: { raw: string; formatted: string }) => item.formatted, - })) as FormatFactory; - } - - if (this.hasDatatableContent(adapters)) { - // if of unknown type, return an identity - return (() => ({ - convert: (item) => item, - })) as FormatFactory; - } }; - private getDataTableContent = async ( - type: string | undefined, - adapters: Adapters | undefined - ) => { - if (!adapters || !type) { - return; - } - // Visualize - if (type === 'visualization') { - const tabularData = (await adapters.data?.getTabular()!).data!; - const datatable = { - columns: tabularData.columns.map(({ field, ...rest }: { field: string }) => ({ - id: field, - field, - ...rest, - })), - rows: tabularData.rows, - }; - return { layer1: datatable }; - } - // Lens - if (type === 'lens') { - return adapters; - } - - // Make a last attempt to duck type the adapter (useful for testing) + private getDataTableContent = (adapters: Adapters | undefined) => { if (this.hasDatatableContent(adapters)) { return adapters; } @@ -128,18 +83,14 @@ export class ExportCSVAction implements ActionByType { }; private exportCSV = async (context: ExportContext) => { - const formatFactory = this.getFormatter( - context?.embeddable?.type, - context?.embeddable?.getInspectorAdapters() - ); + const formatFactory = this.getFormatter(); // early exit if not formatter is available if (!formatFactory) { return; } - const adapters = (await this.getDataTableContent( - context?.embeddable?.type, + const adapters = this.getDataTableContent( context?.embeddable?.getInspectorAdapters() - )) as Record; + ) as Record; if (adapters) { const datatables = Object.values(adapters); @@ -163,8 +114,9 @@ export class ExportCSVAction implements ActionByType { {} ); + // useful for testing if (context.asString) { - return content; + return (content as unknown) as Promise; } if (content) { @@ -175,6 +127,6 @@ export class ExportCSVAction implements ActionByType { public async execute(context: ExportContext): Promise { // make it testable: type here will be forced - return ((await this.exportCSV(context)) as unknown) as Promise; + return await this.exportCSV(context); } } From 5d30c036b2999ec9fdaeaeb7ee76ee9364689bb3 Mon Sep 17 00:00:00 2001 From: dej611 Date: Thu, 26 Nov 2020 20:29:05 +0100 Subject: [PATCH 37/44] :label: + :white_check_mark: Fix type and testing issues --- .../public/application/actions/export_csv_action.test.tsx | 6 +++--- .../public/application/actions/export_csv_action.tsx | 2 +- .../contact_card_exportable_embeddable_factory.tsx | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) 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 index 4fbb48b4848e4..770e01d6190cb 100644 --- a/src/plugins/dashboard/public/application/actions/export_csv_action.test.tsx +++ b/src/plugins/dashboard/public/application/actions/export_csv_action.test.tsx @@ -23,18 +23,18 @@ import { isErrorEmbeddable, IContainer, ErrorEmbeddable } from '../../embeddable import { DashboardContainer } from '../../application/embeddable'; import { getSampleDashboardInput, getSampleDashboardPanel } from '../../application/test_helpers'; import { - CONTACT_CARD_EXPORTABLE_EMBEDDABLE, - ContactCardExportableEmbeddableFactory, 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 '../../../../data/public/exports/export_csv'; +import { LINE_FEED_CHARACTER } from 'src/plugins/data/common/exports/export_csv'; describe('Export CSV action', () => { const { setup, doStart } = embeddablePluginMock.createInstance(); diff --git a/src/plugins/dashboard/public/application/actions/export_csv_action.tsx b/src/plugins/dashboard/public/application/actions/export_csv_action.tsx index 1ceb96b9b4030..a3d51841d018e 100644 --- a/src/plugins/dashboard/public/application/actions/export_csv_action.tsx +++ b/src/plugins/dashboard/public/application/actions/export_csv_action.tsx @@ -62,7 +62,7 @@ export class ExportCSVAction implements ActionByType { }); public async isCompatible(context: ExportContext): Promise { - return this.hasDatatableContent(context.embeddable?.getInspectorAdapters?.()); + return !!this.hasDatatableContent(context.embeddable?.getInspectorAdapters?.()); } private hasDatatableContent = (adapters: Adapters | undefined) => { 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 index 50e5dc24a2101..5b8827ac6fc2a 100644 --- 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 @@ -57,6 +57,7 @@ export class ContactCardExportableEmbeddableFactory { modalSession.close(); + // @ts-expect-error resolve(undefined); }} onCreate={(input: { firstName: string; lastName?: string }) => { From 6d8da50d438251aff53953e492349fb005c86d67 Mon Sep 17 00:00:00 2001 From: dej611 Date: Mon, 30 Nov 2020 10:35:53 +0100 Subject: [PATCH 38/44] :white_check_marck: Migrate functional test to lens specific suite for now --- .../dashboard/drilldowns/export_csv_action.ts | 25 ------------------- .../apps/dashboard/drilldowns/index.ts | 1 - x-pack/test/functional/apps/lens/dashboard.ts | 14 +++++++++++ 3 files changed, 14 insertions(+), 26 deletions(-) delete mode 100644 x-pack/test/functional/apps/dashboard/drilldowns/export_csv_action.ts diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/export_csv_action.ts b/x-pack/test/functional/apps/dashboard/drilldowns/export_csv_action.ts deleted file mode 100644 index a8b064d550cff..0000000000000 --- a/x-pack/test/functional/apps/dashboard/drilldowns/export_csv_action.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { FtrProviderContext } from '../../../ftr_provider_context'; - -const ACTION_ID = 'ACTION_EXPORT_CSV'; -const ACTION_TEST_SUBJ = `embeddablePanelAction-${ACTION_ID}`; - -export default function ({ getService, getPageObjects }: FtrProviderContext) { - const drilldowns = getService('dashboardDrilldownsManage'); - const { dashboard } = getPageObjects(['dashboard']); - const panelActions = getService('dashboardPanelActions'); - const testSubjects = getService('testSubjects'); - - describe('Export to CSV action', () => { - it('action exists in panel context menu', async () => { - await dashboard.loadSavedDashboard(drilldowns.DASHBOARD_WITH_PIE_CHART_NAME); - await panelActions.openContextMenu(); - await testSubjects.existOrFail(ACTION_TEST_SUBJ); - }); - }); -} diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/index.ts b/x-pack/test/functional/apps/dashboard/drilldowns/index.ts index 7300dfe8a1bfa..57454f50266da 100644 --- a/x-pack/test/functional/apps/dashboard/drilldowns/index.ts +++ b/x-pack/test/functional/apps/dashboard/drilldowns/index.ts @@ -25,7 +25,6 @@ export default function ({ loadTestFile, getService }: FtrProviderContext) { loadTestFile(require.resolve('./dashboard_to_dashboard_drilldown')); loadTestFile(require.resolve('./dashboard_to_url_drilldown')); loadTestFile(require.resolve('./explore_data_panel_action')); - loadTestFile(require.resolve('./export_csv_action')); // Disabled for now as it requires xpack.discoverEnhanced.actions.exploreDataInChart.enabled // setting set in kibana.yml to work. Once that is enabled by default, we can re-enable this test suite. diff --git a/x-pack/test/functional/apps/lens/dashboard.ts b/x-pack/test/functional/apps/lens/dashboard.ts index 17b70b8510f04..893b2ec5b62f0 100644 --- a/x-pack/test/functional/apps/lens/dashboard.ts +++ b/x-pack/test/functional/apps/lens/dashboard.ts @@ -140,5 +140,19 @@ 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 testSubjects.existOrFail(ACTION_TEST_SUBJ); + }); }); } From 6084433027f1f1502a5225ce844055593c4ba086 Mon Sep 17 00:00:00 2001 From: dej611 Date: Mon, 30 Nov 2020 16:23:26 +0100 Subject: [PATCH 39/44] :ok_hand: Integrate feedback --- .../application/actions/export_csv_action.tsx | 12 ++++----- .../contact_card_exportable_embeddable.tsx | 26 ++++++++++--------- .../embeddable/embeddable.tsx | 4 +-- x-pack/test/functional/apps/lens/dashboard.ts | 1 + 4 files changed, 22 insertions(+), 21 deletions(-) diff --git a/src/plugins/dashboard/public/application/actions/export_csv_action.tsx b/src/plugins/dashboard/public/application/actions/export_csv_action.tsx index a3d51841d018e..085d5b799542a 100644 --- a/src/plugins/dashboard/public/application/actions/export_csv_action.tsx +++ b/src/plugins/dashboard/public/application/actions/export_csv_action.tsx @@ -48,7 +48,7 @@ export class ExportCSVAction implements ActionByType { public readonly type = ACTION_EXPORT_CSV; - public readonly order = 200; + public readonly order = 20; constructor(protected readonly params: Params) {} @@ -66,7 +66,7 @@ export class ExportCSVAction implements ActionByType { } private hasDatatableContent = (adapters: Adapters | undefined) => { - return adapters && adapters[Object.keys(adapters)[0]]?.columns; + return Object.keys(adapters?.tables || {}).length > 0; }; private getFormatter = (): FormatFactory | undefined => { @@ -77,7 +77,7 @@ export class ExportCSVAction implements ActionByType { private getDataTableContent = (adapters: Adapters | undefined) => { if (this.hasDatatableContent(adapters)) { - return adapters; + return adapters?.tables; } return; }; @@ -88,12 +88,12 @@ export class ExportCSVAction implements ActionByType { if (!formatFactory) { return; } - const adapters = this.getDataTableContent( + const tableAdapters = this.getDataTableContent( context?.embeddable?.getInspectorAdapters() ) as Record; - if (adapters) { - const datatables = Object.values(adapters); + if (tableAdapters) { + const datatables = Object.values(tableAdapters); const content = datatables.reduce>( (memo, datatable, i) => { // skip empty datatables 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 index c605e5603bafd..338eb4877a50a 100644 --- 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 @@ -22,18 +22,20 @@ import { ContactCardEmbeddable } from './contact_card_embeddable'; export class ContactCardExportableEmbeddable extends ContactCardEmbeddable { public getInspectorAdapters = () => { return { - layer1: { - type: 'datatable', - columns: [ - { id: 'firstName', name: 'First Name' }, - { id: 'originalLastName', name: 'Last Name' }, - ], - rows: [ - { - firstName: this.getInput().firstName, - orignialLastName: this.getInput().lastName, - }, - ], + tables: { + layer1: { + type: 'datatable', + columns: [ + { id: 'firstName', name: 'First Name' }, + { id: 'originalLastName', name: 'Last Name' }, + ], + rows: [ + { + firstName: this.getInput().firstName, + orignialLastName: this.getInput().lastName, + }, + ], + }, }, }; }; 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 a40de9d883aee..bc0cea5326efc 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 @@ -184,9 +184,7 @@ export class Embeddable } private updateActiveData = (data: unknown, inspectorAdapters?: Adapters | undefined) => { - if (inspectorAdapters?.tables) { - this.activeData = inspectorAdapters.tables; - } + this.activeData = inspectorAdapters; }; /** diff --git a/x-pack/test/functional/apps/lens/dashboard.ts b/x-pack/test/functional/apps/lens/dashboard.ts index 893b2ec5b62f0..c332d05039255 100644 --- a/x-pack/test/functional/apps/lens/dashboard.ts +++ b/x-pack/test/functional/apps/lens/dashboard.ts @@ -152,6 +152,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dashboardAddPanel.closeAddPanel(); await panelActions.openContextMenu(); + await panelActions.clickContextMenuMoreItem(); await testSubjects.existOrFail(ACTION_TEST_SUBJ); }); }); From 09358d6736718e6e69d334637b06f7e3d6650653 Mon Sep 17 00:00:00 2001 From: dej611 Date: Tue, 1 Dec 2020 17:45:49 +0100 Subject: [PATCH 40/44] :ok_hand: Integrated feedback --- .../editor_frame_service/embeddable/embeddable.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) 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 bc0cea5326efc..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 @@ -34,7 +34,6 @@ import { IContainer, SavedObjectEmbeddableInput, ReferenceOrValueEmbeddable, - Adapters, } from '../../../../../../src/plugins/embeddable/public'; import { Document, injectFilterReferences } from '../../persistence'; import { ExpressionWrapper } from './expression_wrapper'; @@ -45,7 +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 { TableInspectorAdapter } from '../types'; +import { LensInspectorAdapters } from '../types'; export type LensSavedObjectAttributes = Omit; @@ -56,7 +55,6 @@ export type LensByValueInput = { export type LensByReferenceInput = SavedObjectEmbeddableInput & EmbeddableInput; export type LensEmbeddableInput = (LensByValueInput | LensByReferenceInput) & { palette?: PaletteOutput; - activeData?: TableInspectorAdapter; renderMode?: RenderMode; }; @@ -87,7 +85,7 @@ export class Embeddable private subscription: Subscription; private autoRefreshFetchSubscription: Subscription; private isInitialized = false; - private activeData: TableInspectorAdapter | undefined; + private activeData: LensInspectorAdapters | undefined; private externalSearchContext: { timeRange?: TimeRange; @@ -183,7 +181,10 @@ export class Embeddable } } - private updateActiveData = (data: unknown, inspectorAdapters?: Adapters | undefined) => { + private updateActiveData = ( + data: unknown, + inspectorAdapters?: LensInspectorAdapters | undefined + ) => { this.activeData = inspectorAdapters; }; From 3357e26f2441dec36c76292819b78d918cf79bef Mon Sep 17 00:00:00 2001 From: dej611 Date: Tue, 1 Dec 2020 18:20:29 +0100 Subject: [PATCH 41/44] :ok_hand: Reduced priority --- .../dashboard/public/application/actions/export_csv_action.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/dashboard/public/application/actions/export_csv_action.tsx b/src/plugins/dashboard/public/application/actions/export_csv_action.tsx index 085d5b799542a..60b90b48faf41 100644 --- a/src/plugins/dashboard/public/application/actions/export_csv_action.tsx +++ b/src/plugins/dashboard/public/application/actions/export_csv_action.tsx @@ -48,7 +48,7 @@ export class ExportCSVAction implements ActionByType { public readonly type = ACTION_EXPORT_CSV; - public readonly order = 20; + public readonly order = 5; constructor(protected readonly params: Params) {} From 2b37204731dce7f17e56ffd289ff5e751236bafa Mon Sep 17 00:00:00 2001 From: dej611 Date: Wed, 2 Dec 2020 14:20:09 +0100 Subject: [PATCH 42/44] :bug: Fix untitled Lens downloads --- .../public/application/actions/export_csv_action.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/plugins/dashboard/public/application/actions/export_csv_action.tsx b/src/plugins/dashboard/public/application/actions/export_csv_action.tsx index 60b90b48faf41..48a7877f9383e 100644 --- a/src/plugins/dashboard/public/application/actions/export_csv_action.tsx +++ b/src/plugins/dashboard/public/application/actions/export_csv_action.tsx @@ -99,8 +99,14 @@ export class ExportCSVAction implements ActionByType { // skip empty datatables if (datatable) { const postFix = datatables.length > 1 ? `-${i + 1}` : ''; - - memo[`${context!.embeddable!.getTitle()!}${postFix}.csv`] = { + 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), From 57f01c442b1fe9ffcf17648cec9a76bc4f202d15 Mon Sep 17 00:00:00 2001 From: dej611 Date: Wed, 2 Dec 2020 17:52:19 +0100 Subject: [PATCH 43/44] :memo: Add section for download csv action in the dashboard doc --- .../download-underlying-data.asciidoc | 15 +++++++++++++++ .../dashboard/explore-dashboard-data.asciidoc | 1 + .../images/download_csv_context_menu.png | Bin 0 -> 153535 bytes 3 files changed, 16 insertions(+) create mode 100644 docs/user/dashboard/download-underlying-data.asciidoc create mode 100644 docs/user/dashboard/images/download_csv_context_menu.png diff --git a/docs/user/dashboard/download-underlying-data.asciidoc b/docs/user/dashboard/download-underlying-data.asciidoc new file mode 100644 index 0000000000000..8ad5df9291f18 --- /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* drill down 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 0000000000000000000000000000000000000000..09f82b781249579c7f5ee92a111059112ad3b4d1 GIT binary patch literal 153535 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 Date: Wed, 2 Dec 2020 18:11:39 +0100 Subject: [PATCH 44/44] :pencil2: Remove word --- docs/user/dashboard/download-underlying-data.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user/dashboard/download-underlying-data.asciidoc b/docs/user/dashboard/download-underlying-data.asciidoc index 8ad5df9291f18..78403ba797d78 100644 --- a/docs/user/dashboard/download-underlying-data.asciidoc +++ b/docs/user/dashboard/download-underlying-data.asciidoc @@ -3,7 +3,7 @@ [[download_csv]] === Download CSV -To download the underlying data of the Lens panels on your dashboard, you can use the *Download as CSV* drill down option. +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.