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 08f046925cb46..60b8e1c000434 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -779,6 +779,40 @@ describe('Lens App', () => { }); expect(getButton(instance).disableButton).toEqual(false); }); + + it('should show a warning tooltip if the datatable contains any char likely to be interpreted as a spreadsheet formula', async () => { + const services = makeDefaultServicesForApp(); + services.application = { + ...services.application, + capabilities: { + ...services.application.capabilities, + visualize: { save: false, saveQuery: false, show: true }, + }, + }; + + const { instance } = await mountWith({ + services, + preloadedState: { + isSaveable: true, + activeData: { + layer1: { + type: 'datatable', + // mind the indexpattern mock will return no columns to be visible + // we use this test to indirectly test the visible columns feature: + // the @myField name should trigger the warning tooltip check, but because + // of the mock no warning is shown + columns: [{ id: 'a', name: '@myField', meta: { type: 'number' } }], + rows: [], + }, + }, + }, + }); + const tooltipFn = getButton(instance).tooltip; + expect(tooltipFn).toEqual(expect.any(Function)); + if (typeof tooltipFn === 'function') { + expect(tooltipFn()).toEqual(undefined); + } + }); }); describe('inspector', () => { 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 9045080f83b26..49b5c3342ce8e 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 @@ -8,6 +8,7 @@ import { isEqual } from 'lodash'; import { i18n } from '@kbn/i18n'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { Datatable } from 'src/plugins/expressions'; import { TopNavMenuData } from '../../../../../src/plugins/navigation/public'; import { LensAppServices, @@ -238,6 +239,35 @@ export const LensTopNavMenu = ({ const unsavedTitle = i18n.translate('xpack.lens.app.unsavedFilename', { defaultMessage: 'unsaved', }); + + // Compute the list of visible columns, per layer/datatable, once + const tableVisibleColumnsPerLayer = useMemo(() => { + if ( + !activeData || + activeDatasourceId == null || + datasourceStates[activeDatasourceId]?.state == null + ) { + return {}; + } + const datatableColumns: Record = {}; + const layerIds = Object.keys(activeData); + for (const layerId of layerIds) { + const visibleColumns = new Set( + datasourceMap[activeDatasourceId] + .getPublicAPI({ + state: datasourceStates[activeDatasourceId].state, + layerId, + }) + .getTableSpec() + .map(({ columnId }) => columnId) + ); + + datatableColumns[layerId] = activeData[layerId].columns.filter(({ id }) => + visibleColumns.has(id) + ); + } + return datatableColumns; + }, [activeData, activeDatasourceId, datasourceMap, datasourceStates]); const topNavConfig = useMemo( () => getLensTopNavConfig({ @@ -255,9 +285,10 @@ export const LensTopNavMenu = ({ tooltips: { showExportWarning: () => { if (activeData) { - const datatables = Object.values(activeData); - const formulaDetected = datatables.some((datatable) => { - return tableHasFormulas(datatable.columns, datatable.rows); + const layerIds = Object.keys(activeData); + const formulaDetected = layerIds.some((layerId) => { + const datatable = activeData[layerId]; + return tableHasFormulas(tableVisibleColumnsPerLayer[layerId], datatable.rows); }); if (formulaDetected) { return i18n.translate('xpack.lens.app.downloadButtonFormulasWarning', { @@ -275,15 +306,22 @@ export const LensTopNavMenu = ({ if (!activeData) { return; } - const datatables = Object.values(activeData); - const content = datatables.reduce>( - (memo, datatable, i) => { + const layerIds = Object.keys(activeData); + + const content = layerIds.reduce>( + (memo, layerId, i) => { + const datatable = activeData[layerId]; // skip empty datatables if (datatable) { - const postFix = datatables.length > 1 ? `-${i + 1}` : ''; + const postFix = layerIds.length > 1 ? `-${i + 1}` : ''; + + const filteredDatatable = { + ...datatable, + columns: tableVisibleColumnsPerLayer[layerId], + }; memo[`${title || unsavedTitle}${postFix}.csv`] = { - content: exporters.datatableToCSV(datatable, { + content: exporters.datatableToCSV(filteredDatatable, { csvSeparator: uiSettings.get('csv:separator', ','), quoteValues: uiSettings.get('csv:quoteValues', true), formatFactory: fieldFormats.deserialize, @@ -341,6 +379,7 @@ export const LensTopNavMenu = ({ initialInput, isLinkedToOriginatingApp, isSaveable, + lensInspector, title, onAppLeave, redirectToOrigin, @@ -348,9 +387,9 @@ export const LensTopNavMenu = ({ savingToDashboardPermitted, savingToLibraryPermitted, setIsSaveModalVisible, + tableVisibleColumnsPerLayer, uiSettings, unsavedTitle, - lensInspector, ] ); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index 49a85f3f3af79..1fc931399c0e9 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -430,18 +430,19 @@ export function getIndexPatternDatasource({ getPublicAPI({ state, layerId }: PublicAPIProps) { const columnLabelMap = indexPatternDatasource.uniqueLabels(state); + const layer = state ? state.layers[layerId] : undefined; return { datasourceId: 'indexpattern', getTableSpec: () => { - return state.layers[layerId].columnOrder - .filter((colId) => !isReferenced(state.layers[layerId], colId)) - .map((colId) => ({ columnId: colId })); + return layer + ? layer.columnOrder + .filter((colId) => !isReferenced(layer, colId)) + .map((colId) => ({ columnId: colId })) + : []; }, getOperationForColumnId: (columnId: string) => { - const layer = state.layers[layerId]; - if (layer && layer.columns[columnId]) { if (!isReferenced(layer, columnId)) { return columnToOperation(layer.columns[columnId], columnLabelMap[columnId]); @@ -450,8 +451,7 @@ export function getIndexPatternDatasource({ return null; }, getVisualDefaults: () => { - const layer = state.layers[layerId]; - return getVisualDefaultsForLayer(layer); + return layer && getVisualDefaultsForLayer(layer); }, }; }, diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index eb82bb67c0829..6842d3465e032 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -306,7 +306,7 @@ export interface DatasourcePublicAPI { /** * Collect all default visual values given the current state */ - getVisualDefaults: () => Record>; + getVisualDefaults: () => Record> | undefined; } export interface DatasourceDataPanelProps {