Skip to content

Commit

Permalink
cleanup csv sorting logic, now we directly pass all tables to be expo…
Browse files Browse the repository at this point in the history
…rted to the csv export utility

- move column & row sorting into vis definition
- use `@kbn/sort-predicates` to sort rows
- remove sorted columns logic
- remove column sorting logic
  • Loading branch information
nickofthyme committed Oct 14, 2024
1 parent fd5974d commit 44baf77
Show file tree
Hide file tree
Showing 7 changed files with 73 additions and 133 deletions.
69 changes: 12 additions & 57 deletions src/plugins/data/common/exports/export_csv.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,8 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/

// Inspired by the inspector CSV exporter

import { Datatable } from '@kbn/expressions-plugin/common';
import { FormatFactory } from '@kbn/field-formats-plugin/common';
import { EuiDataGridColumnSortingConfig } from '@elastic/eui';
import { createEscapeValue } from './escape_value';

export const LINE_FEED_CHARACTER = '\r\n';
Expand All @@ -23,83 +20,41 @@ interface CSVOptions {
escapeFormulaValues: boolean;
formatFactory: FormatFactory;
raw?: boolean;
/** Order of exported columns. Should use transposed column ids if available.
*
* Defaults to order of columns in state
*/
sortedColumns?: string[];
columnSorting?: EuiDataGridColumnSortingConfig[];
}

export function datatableToCSV(
{ columns, rows }: Datatable,
{
csvSeparator,
quoteValues,
formatFactory,
raw,
escapeFormulaValues,
sortedColumns,
columnSorting,
}: CSVOptions
{ csvSeparator, quoteValues, formatFactory, raw, escapeFormulaValues }: CSVOptions
) {
const escapeValues = createEscapeValue({
separator: csvSeparator,
quoteValues,
escapeFormulaValues,
});

const sortedIds = sortedColumns || columns.map((col) => col.id);
const columnIndexLookup = new Map(sortedIds.map((id, i) => [id, i]));

const header: string[] = [];
const sortedColumnIds: string[] = [];
const formatters: Record<string, ReturnType<FormatFactory>> = {};

for (const column of columns) {
const columnIndex = columnIndexLookup.get(column.id) ?? -1;
if (columnIndex < 0) continue; // hidden or not found

header[columnIndex] = escapeValues(column.name);
sortedColumnIds[columnIndex] = column.id;
columns.forEach((column, i) => {
header[i] = escapeValues(column.name);
sortedColumnIds[i] = column.id;
formatters[column.id] = formatFactory(column.meta?.params);
}
});

if (header.length === 0) {
return '';
}

// Convert the array of row objects to an array of row arrays
const csvRows = rows
.map((row) => {
return sortedColumnIds.map((id) =>
escapeValues(raw ? row[id] : formatters[id].convert(row[id]))
);
})
.sort(rowSortPredicate(sortedColumnIds, columnSorting));
const csvRows = rows.map((row) => {
return sortedColumnIds.map((id) =>
escapeValues(raw ? row[id] : formatters[id].convert(row[id]))
);
});

return (
[header, ...csvRows].map((row) => row.join(csvSeparator)).join(LINE_FEED_CHARACTER) +
LINE_FEED_CHARACTER
); // Add \r\n after last line
}

function rowSortPredicate(
sortedColumnIds: string[],
columnSorting?: EuiDataGridColumnSortingConfig[]
) {
if (!columnSorting) return () => 0;

const columnIdMap = new Map(columnSorting.map(({ id }) => [id, sortedColumnIds.indexOf(id)]));
return (rowA: string[], rowB: string[]) => {
return columnSorting.reduce((acc, { id, direction }) => {
const i = columnIdMap.get(id) ?? -1;
if (i < 0) return acc;

const a = rowA[i];
const b = rowB[i];
const emptyValueSort = a === '' ? 1 : b === '' ? -1 : 0; // always put empty values at bottom
return acc || emptyValueSort || a.localeCompare(b) * (direction === 'asc' ? 1 : -1);
}, 0);
};
LINE_FEED_CHARACTER // Add \r\n after last line
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,13 @@ import { downloadMultipleAs, ShareContext, ShareMenuProvider } from '@kbn/share-
import { exporters } from '@kbn/data-plugin/public';
import { IUiSettingsClient } from '@kbn/core-ui-settings-browser';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiDataGridColumnSortingConfig } from '@elastic/eui';
import type { Datatable } from '@kbn/expressions-plugin/common';
import { FormatFactory } from '../../../common/types';
import { TableInspectorAdapter } from '../../editor_frame_service/types';

export interface CSVSharingData {
title: string;
activeData: TableInspectorAdapter;
datatables: Datatable[];
csvEnabled: boolean;
tablesToShare?: string[];
sortedColumns?: string[];
columnSorting?: EuiDataGridColumnSortingConfig[];
}

declare global {
Expand All @@ -36,32 +32,20 @@ declare global {
}

async function downloadCSVs({
activeData,
title,
datatables,
formatFactory,
uiSettings,
tablesToShare = [],
sortedColumns,
columnSorting,
}: {
title: string;
activeData: TableInspectorAdapter;
formatFactory: FormatFactory;
uiSettings: IUiSettingsClient;
tablesToShare?: string[];
sortedColumns?: string[];
columnSorting?: EuiDataGridColumnSortingConfig[];
}) {
if (!activeData) {
} & Pick<CSVSharingData, 'title' | 'datatables'>) {
if (datatables.length === 0) {
if (window.ELASTIC_LENS_CSV_DOWNLOAD_DEBUG) {
window.ELASTIC_LENS_CSV_CONTENT = undefined;
}
return;
}
const seletedDatatables = Object.entries(activeData)
.filter(([id]) => tablesToShare.includes(id))
.map(([, table]) => table);
const datatables = seletedDatatables.length > 0 ? seletedDatatables : Object.values(activeData);

const content = datatables.reduce<Record<string, { content: string; type: string }>>(
(memo, datatable, i) => {
Expand All @@ -75,8 +59,6 @@ async function downloadCSVs({
quoteValues: uiSettings.get('csv:quoteValues', true),
formatFactory,
escapeFormulaValues: false,
columnSorting,
sortedColumns,
}),
type: exporters.CSV_MIME_TYPE,
};
Expand All @@ -85,18 +67,19 @@ async function downloadCSVs({
},
{}
);

if (window.ELASTIC_LENS_CSV_DOWNLOAD_DEBUG) {
window.ELASTIC_LENS_CSV_CONTENT = content;
}

if (content) {
downloadMultipleAs(content);
}
}

function getWarnings(activeData: TableInspectorAdapter) {
function getWarnings(datatables: Datatable[]) {
const messages: string[] = [];
if (activeData) {
const datatables = Object.values(activeData);
if (datatables.length > 0) {
const formulaDetected = datatables.some((datatable) => {
return tableHasFormulas(datatable.columns, datatable.rows);
});
Expand Down Expand Up @@ -129,8 +112,7 @@ export const downloadCsvShareProvider = ({
}

// TODO fix sharingData types
const { title, activeData, csvEnabled, sortedColumns, columnSorting, tablesToShare } =
sharingData as unknown as CSVSharingData;
const { title, datatables, csvEnabled } = sharingData as unknown as CSVSharingData;

const panelTitle = i18n.translate(
'xpack.lens.reporting.shareContextMenu.csvReportsButtonLabel',
Expand All @@ -152,11 +134,8 @@ export const downloadCsvShareProvider = ({
downloadCSVs({
title,
formatFactory: formatFactoryFn(),
activeData,
datatables,
uiSettings,
tablesToShare,
sortedColumns,
columnSorting,
});

return [
Expand All @@ -182,7 +161,7 @@ export const downloadCsvShareProvider = ({
}
: {
isDisabled: !csvEnabled,
warnings: getWarnings(activeData),
warnings: getWarnings(datatables),
helpText: (
<FormattedMessage
id="xpack.lens.application.csvPanelContent.generationDescription"
Expand Down
16 changes: 7 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 @@ -606,18 +606,16 @@ export const LensTopNavMenu = ({
dataViews.indexPatterns
);

const sharingData = {
activeData,
sortedColumns: activeVisualization.getSortedColumns?.(
const exportDatatables =
activeVisualization.getExportDatatables?.(
visualization.state,
datasourceLayers,
activeData
),
columnSorting: activeVisualization.getColumnSorting?.(
visualization.state,
datasourceLayers
),
tablesToShare: activeVisualization.getTablesToShare?.(),
) ?? [];
const datatables =
exportDatatables.length > 0 ? exportDatatables : Object.values(activeData ?? {});
const sharingData = {
datatables,
csvEnabled,
reportingDisabled: !csvEnabled,
title: title || defaultLensTitle,
Expand Down
19 changes: 4 additions & 15 deletions x-pack/plugins/lens/public/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import type { FieldSpec, DataViewSpec, DataView } from '@kbn/data-views-plugin/c
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import type { FieldFormatParams } from '@kbn/field-formats-plugin/common';
import type { SearchResponseWarning } from '@kbn/search-response-warnings';
import type { EuiButtonIconProps, EuiDataGridColumnSortingConfig } from '@elastic/eui';
import type { EuiButtonIconProps } from '@elastic/eui';
import { estypes } from '@elastic/elasticsearch';
import React from 'react';
import { CellValueContext } from '@kbn/embeddable-plugin/public';
Expand Down Expand Up @@ -1352,24 +1352,13 @@ export interface Visualization<T = unknown, P = T, ExtraAppendLayerArg = unknown
*/
getReportingLayout?: (state: T) => { height: number; width: number };
/**
* Set the order of columns when exporting to csv
* Get all datatables to be exported as csv
*/
getSortedColumns?: (
getExportDatatables?: (
state: T,
datasourceLayers?: DatasourceLayers,
activeData?: TableInspectorAdapter
) => string[];
/**
* Set the row ordering of columns when exporting to csv
*/
getColumnSorting?: (
state: T,
datasourceLayers?: DatasourceLayers
) => EuiDataGridColumnSortingConfig[];
/**
* Table ids to export via csv, corresponding to the tables in inspector adapter
*/
getTablesToShare?: () => string[];
) => Datatable[];
/**
* returns array of telemetry events for the visualization on save
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,10 @@ function isRange(meta: { params?: { id?: string } } | undefined) {
return meta?.params?.id === 'range';
}

export function getSimpleColumnType(meta?: DatatableColumnMeta) {
return isRange(meta) ? 'range' : meta?.type;
}

function getColumnType({
columnConfig,
columnId,
Expand All @@ -185,7 +189,7 @@ function getColumnType({
>;
}) {
const sortingHint = columnConfig.columns.find((col) => col.columnId === columnId)?.sortingHint;
return sortingHint ?? (isRange(lookup[columnId]?.meta) ? 'range' : lookup[columnId]?.meta?.type);
return sortingHint ?? getSimpleColumnType(lookup[columnId]?.meta);
}

export const buildSchemaDetectors = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export class DatatableVisualization {
return getDatatableVisualization({
paletteService: palettes,
kibanaTheme: core.theme,
formatFactory,
});
});
}
Expand Down
Loading

0 comments on commit 44baf77

Please sign in to comment.