From 078e554dcc204609ec4c61253a566f5f07963adc Mon Sep 17 00:00:00 2001 From: dej611 Date: Mon, 16 Nov 2020 11:15:43 +0100 Subject: [PATCH 01/22] :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/22] :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/22] :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/22] :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/22] :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/22] :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 08d75d00c8644e3a1a67d3572dc94add401c2a7e Mon Sep 17 00:00:00 2001 From: dej611 Date: Tue, 17 Nov 2020 13:23:51 +0100 Subject: [PATCH 07/22] :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 88eefc64f1d2397c1ea4da8028f9ece52bc6a6bc Mon Sep 17 00:00:00 2001 From: dej611 Date: Wed, 18 Nov 2020 13:11:10 +0100 Subject: [PATCH 08/22] :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 09/22] :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 10/22] :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 11/22] :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 4de7ae84b13ce11a0aa8c9149c267cb06913f715 Mon Sep 17 00:00:00 2001 From: dej611 Date: Thu, 19 Nov 2020 18:07:14 +0100 Subject: [PATCH 12/22] :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 f851e690e091cce00ac561ff3d2e3bd06c863c3d Mon Sep 17 00:00:00 2001 From: dej611 Date: Fri, 20 Nov 2020 11:12:47 +0100 Subject: [PATCH 13/22] :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 3dad98598ba4f4ce4b1e48f641d9a7f38241bfb3 Mon Sep 17 00:00:00 2001 From: dej611 Date: Fri, 20 Nov 2020 15:16:44 +0100 Subject: [PATCH 14/22] :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 5749f32fad2b890d92f036bac6f129d93933b4e4 Mon Sep 17 00:00:00 2001 From: dej611 Date: Fri, 20 Nov 2020 18:46:26 +0100 Subject: [PATCH 15/22] :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 16/22] :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 4cada295fee08c52a000ea06760d07bddc2fb751 Mon Sep 17 00:00:00 2001 From: dej611 Date: Mon, 23 Nov 2020 16:26:05 +0100 Subject: [PATCH 17/22] :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 18/22] :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 ca868b315aa6bc81c7dc88d5c69fee23877e97c2 Mon Sep 17 00:00:00 2001 From: dej611 Date: Mon, 23 Nov 2020 19:24:52 +0100 Subject: [PATCH 19/22] :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 20/22] :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 21/22] :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 22/22] :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); + }); }); }