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
......@@ -63,6 +63,8 @@ Grafana generates a CSV file in your default browser download location. You can
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.
......
......@@ -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', () => {
......
......@@ -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;
......
......@@ -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<SelectableValue<DataTransformerID>>;
transformedData: DataFrame[];
downloadForExcel: boolean;
}
export class InspectDataTab extends PureComponent<Props, State> {
......@@ -51,6 +53,7 @@ export class InspectDataTab extends PureComponent<Props, State> {
transformId: DataTransformerID.noop,
transformationOptions: buildTransformationOptions(),
transformedData: props.data ?? [],
downloadForExcel: false,
};
}
......@@ -82,11 +85,11 @@ export class InspectDataTab extends PureComponent<Props, State> {
}
}
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<Props, State> {
}
}
if (this.state.downloadForExcel) {
parts.push('Excel header');
}
return parts.join(', ');
}
......@@ -233,6 +240,12 @@ export class InspectDataTab extends PureComponent<Props, State> {
/>
</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>
</VerticalGroup>
</div>
......@@ -269,7 +282,7 @@ export class InspectDataTab extends PureComponent<Props, State> {
<div className={styles.dataDisplayOptions}>{this.renderDataOptions(dataFrames)}</div>
<Button
variant="primary"
onClick={() => this.exportCsv(dataFrames[dataFrameIndex])}
onClick={() => this.exportCsv(dataFrames[dataFrameIndex], { useExcelHeader: this.state.downloadForExcel })}
className={css`
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