Inspector: Download logs for manual processing (#32764)

* Add download logs functionality

* Refactor InspectDataTab to be smaller

* Add test

* Remove console log

* Add metedata info

* Add metedata info
This commit is contained in:
Ivana Huckova 2021-04-12 18:21:05 +02:00 committed by GitHub
parent 5ce25509a1
commit 0a56527ed9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 235 additions and 131 deletions

View File

@ -0,0 +1,148 @@
import React, { FC } from 'react';
import { DataFrame, DataTransformerID, getFrameDisplayName, SelectableValue } from '@grafana/data';
import { Field, HorizontalGroup, Select, Switch, VerticalGroup } from '@grafana/ui';
import { getPanelInspectorStyles } from './styles';
import { GetDataOptions } from 'app/features/query/state/PanelQueryRunner';
import { QueryOperationRow } from 'app/core/components/QueryOperationRow/QueryOperationRow';
import { PanelModel } from 'app/features/dashboard/state';
import { DetailText } from 'app/features/inspector/DetailText';
interface Props {
options: GetDataOptions;
dataFrames: DataFrame[];
transformId: DataTransformerID;
transformationOptions: Array<SelectableValue<DataTransformerID>>;
selectedDataFrame: number | DataTransformerID;
downloadForExcel: boolean;
onDataFrameChange: (item: SelectableValue<DataTransformerID | number>) => void;
toggleDownloadForExcel: () => void;
data?: DataFrame[];
panel?: PanelModel;
onOptionsChange?: (options: GetDataOptions) => void;
}
export const InspectDataOptions: FC<Props> = ({
options,
onOptionsChange,
panel,
data,
dataFrames,
transformId,
transformationOptions,
selectedDataFrame,
onDataFrameChange,
downloadForExcel,
toggleDownloadForExcel,
}) => {
const styles = getPanelInspectorStyles();
const panelTransformations = panel?.getTransformations();
const showPanelTransformationsOption =
Boolean(panelTransformations?.length) && (transformId as any) !== 'join by time';
const showFieldConfigsOption = panel && !panel.plugin?.fieldConfigRegistry.isEmpty();
let dataSelect = dataFrames;
if (selectedDataFrame === DataTransformerID.seriesToColumns) {
dataSelect = data!;
}
const choices = dataSelect.map((frame, index) => {
return {
value: index,
label: `${getFrameDisplayName(frame)} (${index})`,
} as SelectableValue<number>;
});
const selectableOptions = [...transformationOptions, ...choices];
function getActiveString() {
let activeString = '';
if (!data) {
return activeString;
}
const parts: string[] = [];
if (selectedDataFrame === DataTransformerID.seriesToColumns) {
parts.push('Series joined by time');
} else if (data.length > 1) {
parts.push(getFrameDisplayName(data[selectedDataFrame as number]));
}
if (options.withTransforms || options.withFieldConfig) {
if (options.withTransforms) {
parts.push('Panel transforms');
}
if (options.withTransforms && options.withFieldConfig) {
}
if (options.withFieldConfig) {
parts.push('Formatted data');
}
}
if (downloadForExcel) {
parts.push('Excel header');
}
return parts.join(', ');
}
return (
<div className={styles.dataDisplayOptions}>
<QueryOperationRow
id="Data options"
index={0}
title="Data options"
headerElement={<DetailText>{getActiveString()}</DetailText>}
isOpen={false}
>
<div className={styles.options} data-testid="dataOptions">
<VerticalGroup spacing="none">
{data!.length > 1 && (
<Field label="Show data frame">
<Select
options={selectableOptions}
value={selectedDataFrame}
onChange={onDataFrameChange}
width={30}
aria-label="Select dataframe"
/>
</Field>
)}
<HorizontalGroup>
{showPanelTransformationsOption && onOptionsChange && (
<Field
label="Apply panel transformations"
description="Table data is displayed with transformations defined in the panel Transform tab."
>
<Switch
value={!!options.withTransforms}
onChange={() => onOptionsChange({ ...options, withTransforms: !options.withTransforms })}
/>
</Field>
)}
{showFieldConfigsOption && onOptionsChange && (
<Field
label="Formatted data"
description="Table data is formatted with options defined in the Field and Override tabs."
>
<Switch
value={!!options.withFieldConfig}
onChange={() => onOptionsChange({ ...options, withFieldConfig: !options.withFieldConfig })}
/>
</Field>
)}
<Field label="Download for Excel" description="Adds header to CSV for use with Excel">
<Switch value={downloadForExcel} onChange={toggleDownloadForExcel} />
</Field>
</HorizontalGroup>
</VerticalGroup>
</div>
</QueryOperationRow>
</div>
);
};

View File

@ -1,5 +1,5 @@
import React, { ComponentProps } from 'react';
import { FieldType } from '@grafana/data';
import { FieldType, DataFrame } from '@grafana/data';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { InspectDataTab } from './InspectDataTab';
@ -61,5 +61,27 @@ describe('InspectDataTab', () => {
userEvent.click(dataFrameInput);
expect(screen.getByText(/Second data frame/i)).toBeInTheDocument();
});
it('should show download logs button if logs data', () => {
const dataWithLogs = ([
{
name: 'Data frame with logs',
fields: [
{ name: 'time', type: FieldType.time, values: [100, 200, 300] },
{ name: 'name', type: FieldType.string, values: ['uniqueA', 'b', 'c'] },
{ name: 'value', type: FieldType.number, values: [1, 2, 3] },
],
length: 3,
meta: {
preferredVisualisationType: 'logs',
},
},
] as unknown) as DataFrame[];
render(<InspectDataTab {...createProps({ data: dataWithLogs })} />);
expect(screen.getByText(/Download logs/i)).toBeInTheDocument();
});
it('should not show download logs button if no logs data', () => {
render(<InspectDataTab {...createProps()} />);
expect(screen.queryByText(/Download logs/i)).not.toBeInTheDocument();
});
});
});

View File

@ -7,22 +7,21 @@ import {
DataFrame,
DataTransformerID,
dateTimeFormat,
getFrameDisplayName,
dateTimeFormatISO,
SelectableValue,
toCSV,
transformDataFrame,
} from '@grafana/data';
import { Button, Container, Field, HorizontalGroup, Select, Spinner, Switch, Table, VerticalGroup } from '@grafana/ui';
import { Button, Container, Spinner, Table } from '@grafana/ui';
import { selectors } from '@grafana/e2e-selectors';
import { InspectDataOptions } from './InspectDataOptions';
import { getPanelInspectorStyles } from './styles';
import { config } from 'app/core/config';
import { saveAs } from 'file-saver';
import { css } from '@emotion/css';
import { GetDataOptions } from 'app/features/query/state/PanelQueryRunner';
import { QueryOperationRow } from 'app/core/components/QueryOperationRow/QueryOperationRow';
import { PanelModel } from 'app/features/dashboard/state';
import { DetailText } from 'app/features/inspector/DetailText';
import { dataFrameToLogsModel } from 'app/core/logs_model';
interface Props {
isLoading: boolean;
@ -99,6 +98,30 @@ export class InspectDataTab extends PureComponent<Props, State> {
saveAs(blob, fileName);
};
exportLogsAsTxt = () => {
const { data, panel } = this.props;
const logsModel = dataFrameToLogsModel(data || [], undefined, 'utc');
let textToDownload = '';
logsModel.meta?.forEach((metaItem) => {
const string = `${metaItem.label}: ${JSON.stringify(metaItem.value)}\n`;
textToDownload = textToDownload + string;
});
textToDownload = textToDownload + '\n\n';
logsModel.rows.forEach((row) => {
const newRow = dateTimeFormatISO(row.timeEpochMs) + '\t' + row.entry + '\n';
textToDownload = textToDownload + newRow;
});
const blob = new Blob([textToDownload], {
type: 'text/plain;charset=utf-8',
});
const displayTitle = panel ? panel.getDisplayTitle() : 'Explore';
const fileName = `${displayTitle}-logs-${dateTimeFormat(new Date())}.txt`;
saveAs(blob, fileName);
};
onDataFrameChange = (item: SelectableValue<DataTransformerID | number>) => {
this.setState({
transformId:
@ -108,6 +131,12 @@ export class InspectDataTab extends PureComponent<Props, State> {
});
};
toggleDownloadForExcel() {
this.setState((prevState) => ({
downloadForExcel: !prevState.downloadForExcel,
}));
}
getProcessedData(): DataFrame[] {
const { options, panel } = this.props;
const data = this.state.transformedData;
@ -128,129 +157,9 @@ export class InspectDataTab extends PureComponent<Props, State> {
});
}
getActiveString() {
const { selectedDataFrame } = this.state;
const { options, data } = this.props;
let activeString = '';
if (!data) {
return activeString;
}
const parts: string[] = [];
if (selectedDataFrame === DataTransformerID.seriesToColumns) {
parts.push('Series joined by time');
} else if (data.length > 1) {
parts.push(getFrameDisplayName(data[selectedDataFrame as number]));
}
if (options.withTransforms || options.withFieldConfig) {
if (options.withTransforms) {
parts.push('Panel transforms');
}
if (options.withTransforms && options.withFieldConfig) {
}
if (options.withFieldConfig) {
parts.push('Formatted data');
}
}
if (this.state.downloadForExcel) {
parts.push('Excel header');
}
return parts.join(', ');
}
renderDataOptions(dataFrames: DataFrame[]) {
const { options, onOptionsChange, panel, data } = this.props;
const { transformId, transformationOptions, selectedDataFrame } = this.state;
const styles = getPanelInspectorStyles();
const panelTransformations = panel?.getTransformations();
const showPanelTransformationsOption =
Boolean(panelTransformations?.length) && (transformId as any) !== 'join by time';
const showFieldConfigsOption = panel && !panel.plugin?.fieldConfigRegistry.isEmpty();
let dataSelect = dataFrames;
if (selectedDataFrame === DataTransformerID.seriesToColumns) {
dataSelect = data!;
}
const choices = dataSelect.map((frame, index) => {
return {
value: index,
label: `${getFrameDisplayName(frame)} (${index})`,
} as SelectableValue<number>;
});
const selectableOptions = [...transformationOptions, ...choices];
return (
<QueryOperationRow
id="Data options"
index={0}
title="Data options"
headerElement={<DetailText>{this.getActiveString()}</DetailText>}
isOpen={false}
>
<div className={styles.options} data-testid="dataOptions">
<VerticalGroup spacing="none">
{data!.length > 1 && (
<Field label="Show data frame">
<Select
options={selectableOptions}
value={selectedDataFrame}
onChange={this.onDataFrameChange}
width={30}
aria-label="Select dataframe"
/>
</Field>
)}
<HorizontalGroup>
{showPanelTransformationsOption && onOptionsChange && (
<Field
label="Apply panel transformations"
description="Table data is displayed with transformations defined in the panel Transform tab."
>
<Switch
value={!!options.withTransforms}
onChange={() => onOptionsChange({ ...options, withTransforms: !options.withTransforms })}
/>
</Field>
)}
{showFieldConfigsOption && onOptionsChange && (
<Field
label="Formatted data"
description="Table data is formatted with options defined in the Field and Override tabs."
>
<Switch
value={!!options.withFieldConfig}
onChange={() => onOptionsChange({ ...options, withFieldConfig: !options.withFieldConfig })}
/>
</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>
</QueryOperationRow>
);
}
render() {
const { isLoading } = this.props;
const { dataFrameIndex } = this.state;
const { isLoading, options, data, panel, onOptionsChange } = this.props;
const { dataFrameIndex, transformId, transformationOptions, selectedDataFrame, downloadForExcel } = this.state;
const styles = getPanelInspectorStyles();
if (isLoading) {
@ -269,12 +178,25 @@ export class InspectDataTab extends PureComponent<Props, State> {
// let's make sure we don't try to render a frame that doesn't exists
const index = !dataFrames[dataFrameIndex] ? 0 : dataFrameIndex;
const data = dataFrames[index];
const dataFrame = dataFrames[index];
const hasLogs = dataFrames.some((df) => df?.meta?.preferredVisualisationType === 'logs');
return (
<div className={styles.dataTabContent} aria-label={selectors.components.PanelInspector.Data.content}>
<div className={styles.actionsWrapper}>
<div className={styles.dataDisplayOptions}>{this.renderDataOptions(dataFrames)}</div>
<InspectDataOptions
data={data}
panel={panel}
options={options}
dataFrames={dataFrames}
transformId={transformId}
transformationOptions={transformationOptions}
selectedDataFrame={selectedDataFrame}
downloadForExcel={downloadForExcel}
onOptionsChange={onOptionsChange}
onDataFrameChange={this.onDataFrameChange}
toggleDownloadForExcel={this.toggleDownloadForExcel}
/>
<Button
variant="primary"
onClick={() => this.exportCsv(dataFrames[dataFrameIndex], { useExcelHeader: this.state.downloadForExcel })}
@ -284,6 +206,18 @@ export class InspectDataTab extends PureComponent<Props, State> {
>
Download CSV
</Button>
{hasLogs && (
<Button
variant="primary"
onClick={this.exportLogsAsTxt}
className={css`
margin-bottom: 10px;
margin-left: 10px;
`}
>
Download logs
</Button>
)}
</div>
<Container grow={1}>
<AutoSizer>
@ -294,7 +228,7 @@ export class InspectDataTab extends PureComponent<Props, State> {
return (
<div style={{ width, height }}>
<Table width={width} height={height} data={data} />
<Table width={width} height={height} data={dataFrame} />
</div>
);
}}