diff --git a/packages/grafana-data/src/utils/csv.test.ts b/packages/grafana-data/src/utils/csv.test.ts index 4d9eca2fd75..bb869848cf2 100644 --- a/packages/grafana-data/src/utils/csv.test.ts +++ b/packages/grafana-data/src/utils/csv.test.ts @@ -4,6 +4,7 @@ import { getDataFrameRow } from '../dataframe/processDataFrame'; // Test with local CSV files import fs from 'fs'; import { toDataFrameDTO } from '../dataframe/processDataFrame'; +import { MutableDataFrame } from '../dataframe'; describe('read csv', () => { it('should get X and y', () => { @@ -88,3 +89,30 @@ describe('write csv', () => { expect(fields.map(f => f.name).join(',')).toEqual('a,b,c'); // the names }); }); + +describe('DataFrame to CSV', () => { + it('should escape double quotes in the field names', () => { + const dataFrame = new MutableDataFrame({ + fields: [ + { name: 'Time', values: [1589455688623] }, + // As we have traceId in message already this will shadow it. + { + name: 'Value', + values: ['1234'], + labels: { + label1: 'value1', + label2: 'value1', + }, + }, + ], + }); + + const csv = toCSV([dataFrame]); + expect(csv).toMatchInlineSnapshot(` + "\\"Time\\",\\"{label1=\\"\\"value1\\"\\", label2=\\"\\"value1\\"\\"}\\" + 1589455688623,1234 + + " + `); + }); +}); diff --git a/packages/grafana-data/src/utils/csv.ts b/packages/grafana-data/src/utils/csv.ts index 4ee0a74779d..aa6eac251d7 100644 --- a/packages/grafana-data/src/utils/csv.ts +++ b/packages/grafana-data/src/utils/csv.ts @@ -7,6 +7,7 @@ import isNumber from 'lodash/isNumber'; import { DataFrame, Field, FieldType, FieldConfig } from '../types'; import { guessFieldTypeFromValue } from '../dataframe/processDataFrame'; import { MutableDataFrame } from '../dataframe/MutableDataFrame'; +import { getFieldDisplayName } from '../field'; export enum CSVHeaderStyle { full, @@ -289,7 +290,7 @@ export function toCSV(data: DataFrame[], config?: CSVConfig): string { if (i > 0) { csv += config.delimiter; } - csv += fields[i].name; + csv += `"${getFieldDisplayName(fields[i], series).replace(/"/g, '""')}"`; } csv += config.newline; } diff --git a/public/app/features/dashboard/components/Inspector/InspectDataTab.tsx b/public/app/features/dashboard/components/Inspector/InspectDataTab.tsx index 73a62cc7c0d..9c7614e31e5 100644 --- a/public/app/features/dashboard/components/Inspector/InspectDataTab.tsx +++ b/public/app/features/dashboard/components/Inspector/InspectDataTab.tsx @@ -7,6 +7,7 @@ import { toCSV, transformDataFrame, getFrameDisplayName, + dateTimeFormat, } from '@grafana/data'; import { Button, Field, Icon, LegacyForms, Select, Table } from '@grafana/ui'; import { selectors } from '@grafana/e2e-selectors'; @@ -49,13 +50,16 @@ export class InspectDataTab extends PureComponent { } exportCsv = (dataFrame: DataFrame) => { + const { panel } = this.props; + const { transformId } = this.state; const dataFrameCsv = toCSV([dataFrame]); const blob = new Blob([dataFrameCsv], { type: 'application/csv;charset=utf-8', }); - - saveAs(blob, dataFrame.name + '-' + new Date().getUTCDate() + '.csv'); + const transformation = transformId !== DataTransformerID.noop ? '-as-' + transformId.toLocaleLowerCase() : ''; + const fileName = `${panel.title}-data${transformation}-${dateTimeFormat(new Date())}.csv`; + saveAs(blob, fileName); }; onSelectedFrameChanged = (item: SelectableValue) => {