Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Lens] Filter columns not visible in the visualization for CSV download #121802

Closed
wants to merge 9 commits into from
34 changes: 34 additions & 0 deletions x-pack/plugins/lens/public/app_plugin/app.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
57 changes: 48 additions & 9 deletions x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<string, Datatable['columns']> = {};
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({
Expand All @@ -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', {
Expand All @@ -275,15 +306,22 @@ export const LensTopNavMenu = ({
if (!activeData) {
return;
}
const datatables = Object.values(activeData);
const content = datatables.reduce<Record<string, { content: string; type: string }>>(
(memo, datatable, i) => {
const layerIds = Object.keys(activeData);

const content = layerIds.reduce<Record<string, { content: string; type: string }>>(
(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,
Expand Down Expand Up @@ -341,16 +379,17 @@ export const LensTopNavMenu = ({
initialInput,
isLinkedToOriginatingApp,
isSaveable,
lensInspector,
title,
onAppLeave,
redirectToOrigin,
runSave,
savingToDashboardPermitted,
savingToLibraryPermitted,
setIsSaveModalVisible,
tableVisibleColumnsPerLayer,
uiSettings,
unsavedTitle,
lensInspector,
]
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -430,18 +430,19 @@ export function getIndexPatternDatasource({

getPublicAPI({ state, layerId }: PublicAPIProps<IndexPatternPrivateState>) {
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]);
Expand All @@ -450,8 +451,7 @@ export function getIndexPatternDatasource({
return null;
},
getVisualDefaults: () => {
const layer = state.layers[layerId];
return getVisualDefaultsForLayer(layer);
return layer && getVisualDefaultsForLayer(layer);
},
};
},
Expand Down
2 changes: 1 addition & 1 deletion x-pack/plugins/lens/public/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ export interface DatasourcePublicAPI {
/**
* Collect all default visual values given the current state
*/
getVisualDefaults: () => Record<string, Record<string, unknown>>;
getVisualDefaults: () => Record<string, Record<string, unknown>> | undefined;
}

export interface DatasourceDataPanelProps<T = unknown> {
Expand Down