mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
5ce25509a1
commit
0a56527ed9
148
public/app/features/inspector/InspectDataOptions.tsx
Normal file
148
public/app/features/inspector/InspectDataOptions.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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>
|
||||
);
|
||||
}}
|
||||
|
Loading…
Reference in New Issue
Block a user