grafana/public/app/features/inspector/InspectDataTab.tsx

317 lines
10 KiB
TypeScript

import { cloneDeep } from 'lodash';
import React, { PureComponent } from 'react';
import AutoSizer from 'react-virtualized-auto-sizer';
import {
applyFieldOverrides,
applyRawFieldOverrides,
CoreApp,
DataFrame,
DataTransformerID,
FieldConfigSource,
SelectableValue,
TimeZone,
transformDataFrame,
} from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { getTemplateSrv, reportInteraction } from '@grafana/runtime';
import { Button, Spinner, Table } from '@grafana/ui';
import { config } from 'app/core/config';
import { t, Trans } from 'app/core/internationalization';
import { GetDataOptions } from 'app/features/query/state/PanelQueryRunner';
import { dataFrameToLogsModel } from '../logs/logsModel';
import { InspectDataOptions } from './InspectDataOptions';
import { getPanelInspectorStyles } from './styles';
import { downloadAsJson, downloadDataFrameAsCsv, downloadLogsModelAsTxt, downloadTraceAsJson } from './utils/download';
interface Props {
isLoading: boolean;
options: GetDataOptions;
timeZone: TimeZone;
app?: CoreApp;
data?: DataFrame[];
/** The title of the panel or other context name */
dataName: string;
panelPluginId?: string;
fieldConfig?: FieldConfigSource;
hasTransformations?: boolean;
onOptionsChange?: (options: GetDataOptions) => void;
}
interface State {
/** The string is joinByField transformation. Otherwise it is a dataframe index */
selectedDataFrame: number | DataTransformerID;
transformId: DataTransformerID;
dataFrameIndex: number;
transformationOptions: Array<SelectableValue<DataTransformerID>>;
transformedData: DataFrame[];
downloadForExcel: boolean;
}
export class InspectDataTab extends PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
selectedDataFrame: 0,
dataFrameIndex: 0,
transformId: DataTransformerID.noop,
transformationOptions: buildTransformationOptions(),
transformedData: props.data ?? [],
downloadForExcel: false,
};
}
componentDidUpdate(prevProps: Props, prevState: State) {
if (!this.props.data) {
this.setState({ transformedData: [] });
return;
}
if (this.props.options.withTransforms) {
this.setState({ transformedData: this.props.data });
return;
}
if (prevProps.data !== this.props.data || prevState.transformId !== this.state.transformId) {
const currentTransform = this.state.transformationOptions.find((item) => item.value === this.state.transformId);
if (currentTransform && currentTransform.transformer.id !== DataTransformerID.noop) {
const selectedDataFrame = this.state.selectedDataFrame;
const dataFrameIndex = this.state.dataFrameIndex;
const subscription = transformDataFrame([currentTransform.transformer], this.props.data).subscribe((data) => {
this.setState({ transformedData: data, selectedDataFrame, dataFrameIndex }, () => subscription.unsubscribe());
});
return;
}
this.setState({ transformedData: this.props.data });
return;
}
}
exportCsv(dataFrames: DataFrame[], hasLogs: boolean) {
const { dataName } = this.props;
const { transformId } = this.state;
const dataFrame = dataFrames[this.state.dataFrameIndex];
if (hasLogs) {
reportInteraction('grafana_logs_download_clicked', { app: this.props.app, format: 'csv' });
}
downloadDataFrameAsCsv(dataFrame, dataName, { useExcelHeader: this.state.downloadForExcel }, transformId);
}
onExportLogsAsTxt = () => {
const { data, dataName, app } = this.props;
reportInteraction('grafana_logs_download_logs_clicked', {
app,
format: 'logs',
area: 'inspector',
});
const logsModel = dataFrameToLogsModel(data || []);
downloadLogsModelAsTxt(logsModel, dataName);
};
onExportTracesAsJson = () => {
const { data, dataName, app } = this.props;
if (!data) {
return;
}
for (const df of data) {
// Only export traces
if (df.meta?.preferredVisualisationType !== 'trace') {
continue;
}
const traceFormat = downloadTraceAsJson(df, dataName + '-traces');
reportInteraction('grafana_traces_download_traces_clicked', {
app,
grafana_version: config.buildInfo.version,
trace_format: traceFormat,
location: 'inspector',
});
}
};
onExportServiceGraph = () => {
const { data, dataName, app } = this.props;
reportInteraction('grafana_traces_download_service_graph_clicked', {
app,
grafana_version: config.buildInfo.version,
location: 'inspector',
});
if (!data) {
return;
}
downloadAsJson(data, dataName);
};
onDataFrameChange = (item: SelectableValue<DataTransformerID | number>) => {
this.setState({
transformId:
item.value === DataTransformerID.joinByField ? DataTransformerID.joinByField : DataTransformerID.noop,
dataFrameIndex: typeof item.value === 'number' ? item.value : 0,
selectedDataFrame: item.value!,
});
};
onToggleDownloadForExcel = () => {
this.setState((prevState) => ({
downloadForExcel: !prevState.downloadForExcel,
}));
};
getProcessedData(): DataFrame[] {
const { options, panelPluginId, fieldConfig, timeZone } = this.props;
const data = this.state.transformedData;
if (!options.withFieldConfig || !panelPluginId || !fieldConfig) {
return applyRawFieldOverrides(data);
}
const fieldConfigCleaned = this.cleanTableConfigFromFieldConfig(panelPluginId, fieldConfig);
// We need to apply field config as it's not done by PanelQueryRunner (even when withFieldConfig is true).
// It's because transformers create new fields and data frames, and we need to clean field config of any table settings.
return applyFieldOverrides({
data,
theme: config.theme2,
fieldConfig: fieldConfigCleaned,
timeZone,
replaceVariables: (value, scopedVars, format) => getTemplateSrv().replace(value, scopedVars, format),
});
}
// Because we visualize this data in a table we have to remove any custom table display settings
cleanTableConfigFromFieldConfig(panelPluginId: string, fieldConfig: FieldConfigSource): FieldConfigSource {
if (panelPluginId !== 'table') {
return fieldConfig;
}
fieldConfig = cloneDeep(fieldConfig);
// clear all table specific options
fieldConfig.defaults.custom = {};
// clear all table override properties
for (const override of fieldConfig.overrides) {
for (const prop of override.properties) {
if (prop.id.startsWith('custom.')) {
const index = override.properties.indexOf(prop);
override.properties.slice(index, 1);
}
}
}
return fieldConfig;
}
renderActions(dataFrames: DataFrame[], hasLogs: boolean, hasTraces: boolean, hasServiceGraph: boolean) {
return (
<>
<Button variant="primary" onClick={() => this.exportCsv(dataFrames, hasLogs)} size="sm">
<Trans i18nKey="dashboard.inspect-data.download-csv">Download CSV</Trans>
</Button>
{hasLogs && (
<Button variant="primary" onClick={this.onExportLogsAsTxt} size="sm">
<Trans i18nKey="dashboard.inspect-data.download-logs">Download logs</Trans>
</Button>
)}
{hasTraces && (
<Button variant="primary" onClick={this.onExportTracesAsJson} size="sm">
<Trans i18nKey="dashboard.inspect-data.download-traces">Download traces</Trans>
</Button>
)}
{hasServiceGraph && (
<Button variant="primary" onClick={this.onExportServiceGraph} size="sm">
<Trans i18nKey="dashboard.inspect-data.download-service">Download service graph</Trans>
</Button>
)}
</>
);
}
render() {
const { isLoading, options, data, onOptionsChange, hasTransformations } = this.props;
const { dataFrameIndex, transformationOptions, selectedDataFrame, downloadForExcel } = this.state;
const styles = getPanelInspectorStyles();
if (isLoading) {
return (
<div>
<Spinner inline={true} /> Loading
</div>
);
}
const dataFrames = this.getProcessedData();
if (!dataFrames || !dataFrames.length) {
return <div>No Data</div>;
}
// let's make sure we don't try to render a frame that doesn't exists
const index = !dataFrames[dataFrameIndex] ? 0 : dataFrameIndex;
const dataFrame = dataFrames[index];
const hasLogs = dataFrames.some((df) => df?.meta?.preferredVisualisationType === 'logs');
const hasTraces = dataFrames.some((df) => df?.meta?.preferredVisualisationType === 'trace');
const hasServiceGraph = dataFrames.some((df) => df?.meta?.preferredVisualisationType === 'nodeGraph');
return (
<div className={styles.wrap} aria-label={selectors.components.PanelInspector.Data.content}>
<div className={styles.toolbar}>
<InspectDataOptions
data={data}
hasTransformations={hasTransformations}
options={options}
dataFrames={dataFrames}
transformationOptions={transformationOptions}
selectedDataFrame={selectedDataFrame}
downloadForExcel={downloadForExcel}
onOptionsChange={onOptionsChange}
onDataFrameChange={this.onDataFrameChange}
toggleDownloadForExcel={this.onToggleDownloadForExcel}
actions={this.renderActions(dataFrames, hasLogs, hasTraces, hasServiceGraph)}
/>
</div>
<div className={styles.content}>
<AutoSizer>
{({ width, height }) => {
if (width === 0) {
return null;
}
return <Table width={width} height={height} data={dataFrame} showTypeIcons={true} />;
}}
</AutoSizer>
</div>
</div>
);
}
}
function buildTransformationOptions() {
const transformations: Array<SelectableValue<DataTransformerID>> = [
{
value: DataTransformerID.joinByField,
label: t('dashboard.inspect-data.transformation', 'Series joined by time'),
transformer: {
id: DataTransformerID.joinByField,
options: { byField: undefined }, // defaults to time field
},
},
];
return transformations;
}