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);
+ });
});
}