Flamegraph: Add nice empty state for dashboard panel (#72583)

This commit is contained in:
Andrej Ocenas 2023-08-01 10:49:52 +02:00 committed by GitHub
parent b4c4b512d7
commit 09c7190bfe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 89 additions and 18 deletions

View File

@ -28,7 +28,7 @@ e2e.scenario({
// Loop through every panel type and ensure no crash
Object.entries(win.grafanaBootData.settings.panels).forEach(([_, panel]) => {
// TODO: Remove Flame Graph check as part of addressing #66803
if (!panel.hideFromList && panel.state !== 'deprecated' && panel.name !== 'Flame Graph') {
if (!panel.hideFromList && panel.state !== 'deprecated') {
e2e.components.PanelEditor.toggleVizPicker().click();
e2e.components.PluginVisualization.item(panel.name).scrollIntoView().should('be.visible').click();

View File

@ -1,9 +1,17 @@
import React from 'react';
import { CoreApp, PanelProps } from '@grafana/data';
import { PanelDataErrorView } from '@grafana/runtime';
import { checkFields, getMessageCheckFieldsResult } from './components/FlameGraph/dataTransform';
import FlameGraphContainer from './components/FlameGraphContainer';
export const FlameGraphPanel = (props: PanelProps) => {
const wrongFields = checkFields(props.data.series[0]);
if (wrongFields) {
return (
<PanelDataErrorView panelId={props.id} data={props.data} message={getMessageCheckFieldsResult(wrongFields)} />
);
}
return <FlameGraphContainer data={props.data.series[0]} app={CoreApp.Unknown} />;
};

View File

@ -1,4 +1,4 @@
import { createDataFrame } from '@grafana/data';
import { createDataFrame, FieldType } from '@grafana/data';
import { FlameGraphDataContainer, LevelItem, nestedSetToLevels } from './dataTransform';
@ -13,7 +13,7 @@ describe('nestedSetToLevels', () => {
fields: [
{ name: 'level', values: [0, 1, 2, 3, 2, 1, 2, 3, 4] },
{ name: 'value', values: [10, 5, 3, 1, 1, 4, 3, 2, 1] },
{ name: 'label', values: ['1', '2', '3', '4', '5', '6', '7', '8', '9'] },
{ name: 'label', values: ['1', '2', '3', '4', '5', '6', '7', '8', '9'], type: FieldType.string },
{ name: 'self', values: [0, 0, 0, 0, 0, 0, 0, 0, 0] },
],
});
@ -50,7 +50,7 @@ describe('nestedSetToLevels', () => {
fields: [
{ name: 'level', values: [0, 1, 1, 1] },
{ name: 'value', values: [10, 5, 3, 1] },
{ name: 'label', values: ['1', '2', '3', '4'] },
{ name: 'label', values: ['1', '2', '3', '4'], type: FieldType.string },
{ name: 'self', values: [10, 5, 3, 1] },
],
});

View File

@ -1,4 +1,12 @@
import { createTheme, DataFrame, DisplayProcessor, Field, getDisplayProcessor, GrafanaTheme2 } from '@grafana/data';
import {
createTheme,
DataFrame,
DisplayProcessor,
Field,
FieldType,
getDisplayProcessor,
GrafanaTheme2,
} from '@grafana/data';
import { SampleUnit } from '../types';
@ -69,6 +77,57 @@ export function nestedSetToLevels(container: FlameGraphDataContainer): [LevelIte
return [levels, uniqueLabels];
}
export function getMessageCheckFieldsResult(wrongFields: CheckFieldsResult) {
if (wrongFields.missingFields.length) {
return `Data is missing fields: ${wrongFields.missingFields.join(', ')}`;
}
if (wrongFields.wrongTypeFields.length) {
return `Data has fields of wrong type: ${wrongFields.wrongTypeFields
.map((f) => `${f.name} has type ${f.type} but should be ${f.expectedTypes.join(' or ')}`)
.join(', ')}`;
}
return '';
}
export type CheckFieldsResult = {
wrongTypeFields: Array<{ name: string; expectedTypes: FieldType[]; type: FieldType }>;
missingFields: string[];
};
export function checkFields(data: DataFrame): CheckFieldsResult | undefined {
const fields: Array<[string, FieldType[]]> = [
['label', [FieldType.string, FieldType.enum]],
['level', [FieldType.number]],
['value', [FieldType.number]],
['self', [FieldType.number]],
];
const missingFields = [];
const wrongTypeFields = [];
for (const field of fields) {
const [name, types] = field;
const frameField = data.fields.find((f) => f.name === name);
if (!frameField) {
missingFields.push(name);
continue;
}
if (!types.includes(frameField.type)) {
wrongTypeFields.push({ name, expectedTypes: types, type: frameField.type });
}
}
if (missingFields.length > 0 || wrongTypeFields.length > 0) {
return {
wrongTypeFields,
missingFields,
};
}
return undefined;
}
export class FlameGraphDataContainer {
data: DataFrame;
labelField: Field;
@ -85,15 +144,17 @@ export class FlameGraphDataContainer {
constructor(data: DataFrame, theme: GrafanaTheme2 = createTheme()) {
this.data = data;
const wrongFields = checkFields(data);
if (wrongFields) {
throw new Error(getMessageCheckFieldsResult(wrongFields));
}
this.labelField = data.fields.find((f) => f.name === 'label')!;
this.levelField = data.fields.find((f) => f.name === 'level')!;
this.valueField = data.fields.find((f) => f.name === 'value')!;
this.selfField = data.fields.find((f) => f.name === 'self')!;
if (!(this.labelField && this.levelField && this.valueField && this.selfField)) {
throw new Error('Malformed dataFrame: value, level and label and self fields are required.');
}
const enumConfig = this.labelField?.config?.type?.enum;
// Label can actually be an enum field so depending on that we have to access it through display processor. This is
// both a backward compatibility but also to allow using a simple dataFrame without enum config. This would allow

View File

@ -1,4 +1,4 @@
import { createDataFrame } from '@grafana/data';
import { createDataFrame, FieldType } from '@grafana/data';
import { FlameGraphDataContainer, LevelItem } from './dataTransform';
import { getRectDimensionsForLevel } from './rendering';
@ -8,6 +8,7 @@ function makeDataFrame(fields: Record<string, Array<number | string>>) {
fields: Object.keys(fields).map((key) => ({
name: key,
values: fields[key],
type: typeof fields[key][0] === 'string' ? FieldType.string : FieldType.number,
})),
});
}

View File

@ -1,4 +1,4 @@
import { arrayToDataFrame } from '@grafana/data';
import { arrayToDataFrame, FieldType } from '@grafana/data';
import { FlameGraphDataContainer, LevelItem } from './dataTransform';
@ -77,6 +77,8 @@ export function textToDataContainer(text: string) {
}
const df = arrayToDataFrame(dfSorted);
const labelField = df.fields.find((f) => f.name === 'label')!;
labelField.type = FieldType.string;
return new FlameGraphDataContainer(df);
}

View File

@ -1,7 +1,7 @@
import { VisualizationSuggestionsBuilder } from '@grafana/data';
import { SuggestionName } from 'app/types/suggestions';
import { FlameGraphDataContainer as FlameGraphDataContainer } from './components/FlameGraph/dataTransform';
import { checkFields } from './components/FlameGraph/dataTransform';
export class FlameGraphSuggestionsSupplier {
getListWithDefaults(builder: VisualizationSuggestionsBuilder) {
@ -16,13 +16,12 @@ export class FlameGraphSuggestionsSupplier {
return;
}
// Try to instantiate FlameGraphDataContainer (depending on the version), since the instantiation can fail due
// to the format of the data - meaning that a Flame Graph cannot be used to visualize those data.
// Without this check, a suggestion containing an error is shown to the user.
const dataFrame = builder.data.series[0];
try {
new FlameGraphDataContainer(dataFrame);
} catch (err) {
if (!dataFrame) {
return;
}
const wrongFields = checkFields(dataFrame);
if (wrongFields) {
return;
}