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 bafcd8bdffff9..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 @@ -108,6 +108,7 @@ | [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/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/common/exports/export_csv.tsx b/src/plugins/data/common/exports/export_csv.tsx new file mode 100644 index 0000000000000..1e1420c245eb4 --- /dev/null +++ b/src/plugins/data/common/exports/export_csv.tsx @@ -0,0 +1,82 @@ +/* + * 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 + +import { FormatFactory } from 'src/plugins/data/common/field_formats/utils'; +import { Datatable } from 'src/plugins/expressions'; + +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) { + 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; + raw?: boolean; +} + +export function datatableToCSV( + { columns, rows }: Datatable, + { csvSeparator, quoteValues, formatFactory, raw }: 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(raw ? row[column.id] : formatters[column.id].convert(row[column.id]), quoteValues) + ); + }); + + 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 +} diff --git a/src/plugins/data/common/exports/index.ts b/src/plugins/data/common/exports/index.ts new file mode 100644 index 0000000000000..72faac654b421 --- /dev/null +++ b/src/plugins/data/common/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 { 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/index.ts b/src/plugins/data/public/index.ts index 129addf3de70e..e0b0c5a0ea980 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -212,6 +212,16 @@ export { FieldFormat, } from '../common'; +/** + * Exporters (CSV) + */ + +import { datatableToCSV, CSV_MIME_TYPE } from '../common'; +export const exporters = { + datatableToCSV, + CSV_MIME_TYPE, +}; + /* * Index patterns: */ diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 5a707393b39f4..e1af3cc1d1b4d 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -17,7 +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_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'; @@ -35,6 +36,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'; @@ -672,6 +674,14 @@ export type ExistsFilter = Filter & { exists?: FilterExistsProperty; }; +// 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) @@ -2392,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: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: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/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/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 diff --git a/src/plugins/share/public/index.ts b/src/plugins/share/public/index.ts index 950ecebeaadc7..9f98d9c21d233 100644 --- a/src/plugins/share/public/index.ts +++ b/src/plugins/share/public/index.ts @@ -41,5 +41,7 @@ export { import { SharePlugin } from './plugin'; export { KibanaURL } from './kibana_url'; +export { downloadMultipleAs, downloadFileAs } from './lib/download_as'; +export type { DownloadableContent } 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.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({}); diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index cdd701271be2c..addc263acca29 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 { downloadMultipleAs } from '../../../../../src/plugins/share/public'; import { createKbnUrlStateStorage, withNotifyOnErrors, @@ -25,6 +26,7 @@ import { NativeRenderer } from '../native_renderer'; import { trackUiEvent } from '../lens_ui_telemetry'; import { esFilters, + exporters, IndexPattern as IndexPatternInstance, IndexPatternsContract, syncQueryStateWithUrl, @@ -474,16 +476,50 @@ 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 && // Temporarily required until the 'by value' paradigm is default. (dashboardFeatureFlag.allowByValueEmbeddables || Boolean(initialInput)) ), + enableExportToCSV: Boolean( + state.isSaveable && state.activeData && Object.keys(state.activeData).length + ), isByValueMode: getIsByValueMode(), showCancel: Boolean(state.isLinkedToOriginatingApp), savingPermitted, actions: { + exportToCSV: () => { + 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); + } + }, saveAndReturn: () => { if (savingPermitted && lastKnownDoc) { // disabling the validation on app leave because the document has been saved. @@ -605,13 +641,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/app_plugin/lens_top_nav.tsx b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx index 9162af52052ee..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 @@ -10,12 +10,13 @@ import { LensTopNavActions } from './types'; export function getLensTopNavConfig(options: { showSaveAndReturn: boolean; + enableExportToCSV: boolean; showCancel: boolean; isByValueMode: boolean; actions: LensTopNavActions; savingPermitted: boolean; }): TopNavMenuData[] { - const { showSaveAndReturn, showCancel, actions, savingPermitted } = options; + const { showSaveAndReturn, showCancel, actions, savingPermitted, enableExportToCSV } = options; const topNavMenu: TopNavMenuData[] = []; const saveButtonLabel = options.isByValueMode @@ -30,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', { 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/save.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.ts index 4cb523f128a8c..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[] = []; @@ -74,5 +76,6 @@ export function getSavedObjectFormat({ }, filterableIndexPatterns: uniqueFilterableIndexPatternIds, isSaveable: expression !== null, + activeData: state.activeData, }; } 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/types.ts b/x-pack/plugins/lens/public/types.ts index 225fedb987c76..2f40f21455310 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; }) => void; showNoDataPopover: () => void; } 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); + }); }); }