Commit dc662025 by Tom Daly Committed by GitHub

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
parent d1817829
...@@ -62,7 +62,9 @@ Grafana generates a CSV file in your default browser download location. You can ...@@ -62,7 +62,9 @@ Grafana generates a CSV file in your default browser download location. You can
1. Open the panel inspector. 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. 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**. 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 ### 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. 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.
......
...@@ -88,6 +88,25 @@ describe('write csv', () => { ...@@ -88,6 +88,25 @@ describe('write csv', () => {
expect(getDataFrameRow(f[0], 0)).toEqual(firstRow); expect(getDataFrameRow(f[0], 0)).toEqual(firstRow);
expect(fields.map(f => f.name).join(',')).toEqual('a,b,c'); // the names 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', () => { describe('DataFrame to CSV', () => {
......
...@@ -21,6 +21,7 @@ export interface CSVConfig { ...@@ -21,6 +21,7 @@ export interface CSVConfig {
newline?: string; // default: "\r\n" newline?: string; // default: "\r\n"
quoteChar?: string; // default: '"' quoteChar?: string; // default: '"'
encoding?: string; // default: "", encoding?: string; // default: "",
useExcelHeader?: boolean; // default: false
headerStyle?: CSVHeaderStyle; headerStyle?: CSVHeaderStyle;
} }
...@@ -246,19 +247,28 @@ function getHeaderLine(key: string, fields: Field[], config: CSVConfig): string ...@@ -246,19 +247,28 @@ function getHeaderLine(key: string, fields: Field[], config: CSVConfig): string
return ''; 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 { export function toCSV(data: DataFrame[], config?: CSVConfig): string {
if (!data) { if (!data) {
return ''; return '';
} }
let csv = '';
config = defaults(config, { config = defaults(config, {
delimiter: ',', delimiter: getLocaleDelimiter(),
newline: '\r\n', newline: '\r\n',
quoteChar: '"', quoteChar: '"',
encoding: '', encoding: '',
headerStyle: CSVHeaderStyle.name, headerStyle: CSVHeaderStyle.name,
useExcelHeader: false,
}); });
let csv = config.useExcelHeader ? `sep=${config.delimiter}${config.newline}` : '';
for (const series of data) { for (const series of data) {
const { fields } = series; const { fields } = series;
......
...@@ -12,6 +12,7 @@ import { ...@@ -12,6 +12,7 @@ import {
transformDataFrame, transformDataFrame,
} from '@grafana/data'; } from '@grafana/data';
import { Button, Container, Field, HorizontalGroup, Icon, Select, Switch, Table, VerticalGroup } from '@grafana/ui'; 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 { selectors } from '@grafana/e2e-selectors';
import { getPanelInspectorStyles } from './styles'; import { getPanelInspectorStyles } from './styles';
...@@ -39,6 +40,7 @@ interface State { ...@@ -39,6 +40,7 @@ interface State {
dataFrameIndex: number; dataFrameIndex: number;
transformationOptions: Array<SelectableValue<DataTransformerID>>; transformationOptions: Array<SelectableValue<DataTransformerID>>;
transformedData: DataFrame[]; transformedData: DataFrame[];
downloadForExcel: boolean;
} }
export class InspectDataTab extends PureComponent<Props, State> { export class InspectDataTab extends PureComponent<Props, State> {
...@@ -51,6 +53,7 @@ export class InspectDataTab extends PureComponent<Props, State> { ...@@ -51,6 +53,7 @@ export class InspectDataTab extends PureComponent<Props, State> {
transformId: DataTransformerID.noop, transformId: DataTransformerID.noop,
transformationOptions: buildTransformationOptions(), transformationOptions: buildTransformationOptions(),
transformedData: props.data ?? [], transformedData: props.data ?? [],
downloadForExcel: false,
}; };
} }
...@@ -82,11 +85,11 @@ export class InspectDataTab extends PureComponent<Props, State> { ...@@ -82,11 +85,11 @@ export class InspectDataTab extends PureComponent<Props, State> {
} }
} }
exportCsv = (dataFrame: DataFrame) => { exportCsv = (dataFrame: DataFrame, csvConfig: CSVConfig = {}) => {
const { panel } = this.props; const { panel } = this.props;
const { transformId } = this.state; const { transformId } = this.state;
const dataFrameCsv = toCSV([dataFrame]); const dataFrameCsv = toCSV([dataFrame], csvConfig);
const blob = new Blob([String.fromCharCode(0xfeff), dataFrameCsv], { const blob = new Blob([String.fromCharCode(0xfeff), dataFrameCsv], {
type: 'text/csv;charset=utf-8', type: 'text/csv;charset=utf-8',
...@@ -156,6 +159,10 @@ export class InspectDataTab extends PureComponent<Props, State> { ...@@ -156,6 +159,10 @@ export class InspectDataTab extends PureComponent<Props, State> {
} }
} }
if (this.state.downloadForExcel) {
parts.push('Excel header');
}
return parts.join(', '); return parts.join(', ');
} }
...@@ -233,6 +240,12 @@ export class InspectDataTab extends PureComponent<Props, State> { ...@@ -233,6 +240,12 @@ export class InspectDataTab extends PureComponent<Props, State> {
/> />
</Field> </Field>
)} )}
<Field label="Download for Excel" description="Adds header to CSV for use with Excel">
<Switch
value={this.state.downloadForExcel}
onChange={() => this.setState({ downloadForExcel: !this.state.downloadForExcel })}
/>
</Field>
</HorizontalGroup> </HorizontalGroup>
</VerticalGroup> </VerticalGroup>
</div> </div>
...@@ -269,7 +282,7 @@ export class InspectDataTab extends PureComponent<Props, State> { ...@@ -269,7 +282,7 @@ export class InspectDataTab extends PureComponent<Props, State> {
<div className={styles.dataDisplayOptions}>{this.renderDataOptions(dataFrames)}</div> <div className={styles.dataDisplayOptions}>{this.renderDataOptions(dataFrames)}</div>
<Button <Button
variant="primary" variant="primary"
onClick={() => this.exportCsv(dataFrames[dataFrameIndex])} onClick={() => this.exportCsv(dataFrames[dataFrameIndex], { useExcelHeader: this.state.downloadForExcel })}
className={css` className={css`
margin-bottom: 10px; margin-bottom: 10px;
`} `}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment