The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/public/app/features/inspector/InspectDataTab.tsx

318 lines
10 KiB

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;
formattedDataDescription?: string;
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) {
return applyRawFieldOverrides(data);
}
let fieldConfigCleaned = fieldConfig ?? { defaults: {}, overrides: [] };
// Because we visualize this data in a table we have to remove any custom table display settings
if (panelPluginId === 'table' && fieldConfig) {
fieldConfigCleaned = this.cleanTableConfigFromFieldConfig(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(fieldConfig: FieldConfigSource): FieldConfigSource {
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, formattedDataDescription, 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}
formattedDataDescription={formattedDataDescription}
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;
}