From dc662025cd10b908b04c25f2b931a8d1bd51888f Mon Sep 17 00:00:00 2001 From: Tom Daly Date: Fri, 9 Oct 2020 09:42:57 +0100 Subject: [PATCH] Panel Inspect: Allow CSV download for Excel (#27284) * fix: Use locale to find delimiter for CSV export * Add sep= Excel header to CSV exporter * Add modal for Excel export * Move Excel download to 'Data options' as toggle * Add 'Download for Excel' documentation --- docs/sources/panels/inspect-panel.md | 4 +++- packages/grafana-data/src/utils/csv.test.ts | 19 +++++++++++++++++++ packages/grafana-data/src/utils/csv.ts | 14 ++++++++++++-- .../components/Inspector/InspectDataTab.tsx | 19 ++++++++++++++++--- 4 files changed, 50 insertions(+), 6 deletions(-) diff --git a/docs/sources/panels/inspect-panel.md b/docs/sources/panels/inspect-panel.md index 5440ff66bc1..e95a1a99510 100644 --- a/docs/sources/panels/inspect-panel.md +++ b/docs/sources/panels/inspect-panel.md @@ -62,7 +62,9 @@ Grafana generates a CSV file in your default browser download location. You can 1. Open the panel inspector. 1. Inspect the raw query results as described above. Adjust settings until you see the raw data that you want to export. 1. Click **Download CSV**. - + +To download a CSV file specifically formatted for Excel, expand the **Data options** panel and enable the **Download for Excel** toggle before clicking **Download CSV**. + ### Inspect query performance The Stats tab displays statistics that tell you how long your query takes, how many queries you send, and the number of rows returned. This information can help you troubleshoot your queries, especially if any of the numbers are unexpectedly high or low. diff --git a/packages/grafana-data/src/utils/csv.test.ts b/packages/grafana-data/src/utils/csv.test.ts index eb29df0a671..fffc82425ca 100644 --- a/packages/grafana-data/src/utils/csv.test.ts +++ b/packages/grafana-data/src/utils/csv.test.ts @@ -88,6 +88,25 @@ describe('write csv', () => { expect(getDataFrameRow(f[0], 0)).toEqual(firstRow); expect(fields.map(f => f.name).join(',')).toEqual('a,b,c'); // the names }); + + it('should add Excel header given config', () => { + const dataFrame = new MutableDataFrame({ + fields: [ + { name: 'Time', values: [1598784913123, 1598784914123] }, + { name: 'Value', values: ['1234', '5678'] }, + ], + }); + + const csv = toCSV([dataFrame], { useExcelHeader: true }); + expect(csv).toMatchInlineSnapshot(` + "sep=, + \\"Time\\",\\"Value\\" + 1598784913123,1234 + 1598784914123,5678 + + " + `); + }); }); describe('DataFrame to CSV', () => { diff --git a/packages/grafana-data/src/utils/csv.ts b/packages/grafana-data/src/utils/csv.ts index fa291b2a84e..3fe22dd8589 100644 --- a/packages/grafana-data/src/utils/csv.ts +++ b/packages/grafana-data/src/utils/csv.ts @@ -21,6 +21,7 @@ export interface CSVConfig { newline?: string; // default: "\r\n" quoteChar?: string; // default: '"' encoding?: string; // default: "", + useExcelHeader?: boolean; // default: false headerStyle?: CSVHeaderStyle; } @@ -246,19 +247,28 @@ function getHeaderLine(key: string, fields: Field[], config: CSVConfig): string return ''; } +function getLocaleDelimiter(): string { + const arr = ['x', 'y']; + if (arr.toLocaleString) { + return arr.toLocaleString().charAt(1); + } + return ','; +} + export function toCSV(data: DataFrame[], config?: CSVConfig): string { if (!data) { return ''; } - let csv = ''; config = defaults(config, { - delimiter: ',', + delimiter: getLocaleDelimiter(), newline: '\r\n', quoteChar: '"', encoding: '', headerStyle: CSVHeaderStyle.name, + useExcelHeader: false, }); + let csv = config.useExcelHeader ? `sep=${config.delimiter}${config.newline}` : ''; for (const series of data) { const { fields } = series; diff --git a/public/app/features/dashboard/components/Inspector/InspectDataTab.tsx b/public/app/features/dashboard/components/Inspector/InspectDataTab.tsx index 68f5e6b534a..00278f62362 100644 --- a/public/app/features/dashboard/components/Inspector/InspectDataTab.tsx +++ b/public/app/features/dashboard/components/Inspector/InspectDataTab.tsx @@ -12,6 +12,7 @@ import { transformDataFrame, } from '@grafana/data'; import { Button, Container, Field, HorizontalGroup, Icon, Select, Switch, Table, VerticalGroup } from '@grafana/ui'; +import { CSVConfig } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { getPanelInspectorStyles } from './styles'; @@ -39,6 +40,7 @@ interface State { dataFrameIndex: number; transformationOptions: Array>; transformedData: DataFrame[]; + downloadForExcel: boolean; } export class InspectDataTab extends PureComponent { @@ -51,6 +53,7 @@ export class InspectDataTab extends PureComponent { transformId: DataTransformerID.noop, transformationOptions: buildTransformationOptions(), transformedData: props.data ?? [], + downloadForExcel: false, }; } @@ -82,11 +85,11 @@ export class InspectDataTab extends PureComponent { } } - exportCsv = (dataFrame: DataFrame) => { + exportCsv = (dataFrame: DataFrame, csvConfig: CSVConfig = {}) => { const { panel } = this.props; const { transformId } = this.state; - const dataFrameCsv = toCSV([dataFrame]); + const dataFrameCsv = toCSV([dataFrame], csvConfig); const blob = new Blob([String.fromCharCode(0xfeff), dataFrameCsv], { type: 'text/csv;charset=utf-8', @@ -156,6 +159,10 @@ export class InspectDataTab extends PureComponent { } } + if (this.state.downloadForExcel) { + parts.push('Excel header'); + } + return parts.join(', '); } @@ -233,6 +240,12 @@ export class InspectDataTab extends PureComponent { /> )} + + this.setState({ downloadForExcel: !this.state.downloadForExcel })} + /> + @@ -269,7 +282,7 @@ export class InspectDataTab extends PureComponent {
{this.renderDataOptions(dataFrames)}