NodeGraph: Detect dataframes more accurately based on fields (#47213)

* NodeGraph: Detect dataframes more accurately based on fields

* Make get fields case insensitive

* Update node graph docs
This commit is contained in:
Connor Lindsey 2022-04-13 08:18:40 -07:00 committed by GitHub
parent 6db470c11e
commit 939a778111
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 167 additions and 65 deletions

View File

@ -88,14 +88,14 @@ Required fields:
Optional fields:
| Field name | Type | Description |
| ------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| title | string | Name of the node visible in just under the node. |
| subTitle | string | Additional, name, type or other identifier that will be shown right under the title. |
| mainStat | string/number | First stat shown inside the node itself. Can be either string in which case the value will be shown as it is or it can be a number in which case any unit associated with that field will be also shown. |
| secondaryStat | string/number | Same as mainStat but shown right under it inside the node. |
| arc\_\_\* | number | Any field prefixed with `arc__` will be used to create the color circle around the node. All values in these fields should add up to 1. You can specify color using `config.color.fixedColor`. |
| detail\_\_\* | string/number | Any field prefixed with `detail__` will be shown in the header of context menu when clicked on the node. Use `config.displayName` for more human readable label. |
| Field name | Type | Description |
| ------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| title | string | Name of the node visible in just under the node. |
| subtitle | string | Additional, name, type or other identifier shown under the title. |
| mainstat | string/number | First stat shown inside the node itself. It can either be a string showing the value as is or a number. If it is a number, any unit associated with that field is also shown. |
| secondarystat | string/number | Same as mainStat, but shown under it inside the node. |
| arc\_\_\* | number | Any field prefixed with `arc__` will be used to create the color circle around the node. All values in these fields should add up to 1. You can specify color using `config.color.fixedColor`. |
| detail\_\_\* | string/number | Any field prefixed with `detail__` will be shown in the header of context menu when clicked on the node. Use `config.displayName` for more human readable label. |
### Edge parameters
@ -109,8 +109,8 @@ Required fields:
Optional fields:
| Field name | Type | Description |
| ------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| mainStat | string/number | First stat shown in the overlay when hovering over the edge. Can be either string in which case the value will be shown as it is or it can be a number in which case any unit associated with that field will be also shown |
| secondaryStat | string/number | Same as mainStat but shown right under it. |
| detail\_\_\* | string/number | Any field prefixed with `detail__` will be shown in the header of context menu when clicked on the edge. Use `config.displayName` for more human readable label. |
| Field name | Type | Description |
| ------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| mainstat | string/number | First stat shown in the overlay when hovering over the edge. It can be a string showing the value as is or it can be a number. If it is a number, any unit associated with that field is also shown |
| secondarystat | string/number | Same as mainStat, but shown right under it. |
| detail\_\_\* | string/number | Any field prefixed with `detail__` will be shown in the header of context menu when clicked on the edge. Use `config.displayName` for more human readable label. |

View File

@ -1,9 +1,9 @@
export enum NodeGraphDataFrameFieldNames {
id = 'id',
title = 'title',
subTitle = 'subTitle',
mainStat = 'mainStat',
secondaryStat = 'secondaryStat',
subTitle = 'subtitle',
mainStat = 'mainstat',
secondaryStat = 'secondarystat',
source = 'source',
target = 'target',
detail = 'detail__',

View File

@ -111,9 +111,9 @@ export function toNodesFrame(values: any[]) {
return toVectors([
{ name: 'id', values: values[0] },
{ name: 'title', values: values[1] },
{ name: 'subTitle', values: values[2] },
{ name: 'mainStat', values: values[3] },
{ name: 'secondaryStat', values: values[4] },
{ name: 'subtitle', values: values[2] },
{ name: 'mainstat', values: values[3] },
{ name: 'secondarystat', values: values[4] },
{
name: 'color',
config: {

View File

@ -77,9 +77,9 @@ describe('Tempo data source', () => {
).toMatchObject([
{ name: 'id', values: ['4322526419282105830'] },
{ name: 'title', values: ['service'] },
{ name: 'subTitle', values: ['store.validateQueryTimeRange'] },
{ name: 'mainStat', values: ['14.98ms (100%)'] },
{ name: 'secondaryStat', values: ['14.98ms (100%)'] },
{ name: 'subtitle', values: ['store.validateQueryTimeRange'] },
{ name: 'mainstat', values: ['14.98ms (100%)'] },
{ name: 'secondarystat', values: ['14.98ms (100%)'] },
{ name: 'color', values: [1.000007560204647] },
]);

View File

@ -13,18 +13,18 @@ describe('createGraphFrames', () => {
expect(view.get(0)).toMatchObject({
id: '4322526419282105830',
title: 'loki-all',
subTitle: 'store.validateQueryTimeRange',
mainStat: '0ms (0.02%)',
secondaryStat: '0ms (100%)',
subtitle: 'store.validateQueryTimeRange',
mainstat: '0ms (0.02%)',
secondarystat: '0ms (100%)',
color: 0.00021968356127648162,
});
expect(view.get(29)).toMatchObject({
id: '4450900759028499335',
title: 'loki-all',
subTitle: 'HTTP GET - loki_api_v1_query_range',
mainStat: '18.21ms (100%)',
secondaryStat: '3.22ms (17.71%)',
subtitle: 'HTTP GET - loki_api_v1_query_range',
mainstat: '18.21ms (100%)',
secondarystat: '3.22ms (17.71%)',
color: 0.17707117189595056,
});
@ -43,9 +43,9 @@ describe('createGraphFrames', () => {
expect(view.get(0)).toMatchObject({
id: '4322526419282105830',
title: 'loki-all',
subTitle: 'store.validateQueryTimeRange',
mainStat: '14.98ms (100%)',
secondaryStat: '14.98ms (100%)',
subtitle: 'store.validateQueryTimeRange',
mainstat: '14.98ms (100%)',
secondarystat: '14.98ms (100%)',
color: 1.000007560204647,
});
});
@ -75,8 +75,8 @@ describe('mapPromMetricsToServiceMap', () => {
expect(nodes.fields).toMatchObject([
{ name: 'id', values: new ArrayVector(['db', 'app', 'lb']) },
{ name: 'title', values: new ArrayVector(['db', 'app', 'lb']) },
{ name: 'mainStat', values: new ArrayVector([1000, 2000, NaN]) },
{ name: 'secondaryStat', values: new ArrayVector([0.17, 0.33, NaN]) },
{ name: 'mainstat', values: new ArrayVector([1000, 2000, NaN]) },
{ name: 'secondarystat', values: new ArrayVector([0.17, 0.33, NaN]) },
{ name: 'arc__success', values: new ArrayVector([0.8, 0.25, 1]) },
{ name: 'arc__failed', values: new ArrayVector([0.2, 0.75, 0]) },
]);
@ -84,8 +84,8 @@ describe('mapPromMetricsToServiceMap', () => {
{ name: 'id', values: new ArrayVector(['app_db', 'lb_app']) },
{ name: 'source', values: new ArrayVector(['app', 'lb']) },
{ name: 'target', values: new ArrayVector(['db', 'app']) },
{ name: 'mainStat', values: new ArrayVector([10, 20]) },
{ name: 'secondaryStat', values: new ArrayVector([1000, 2000]) },
{ name: 'mainstat', values: new ArrayVector([10, 20]) },
{ name: 'secondarystat', values: new ArrayVector([1000, 2000]) },
]);
});
@ -107,8 +107,8 @@ describe('mapPromMetricsToServiceMap', () => {
expect(nodes.fields).toMatchObject([
{ name: 'id', values: new ArrayVector(['db', 'app', 'lb']) },
{ name: 'title', values: new ArrayVector(['db', 'app', 'lb']) },
{ name: 'mainStat', values: new ArrayVector([1000, 2000, NaN]) },
{ name: 'secondaryStat', values: new ArrayVector([0.17, 0.33, NaN]) },
{ name: 'mainstat', values: new ArrayVector([1000, 2000, NaN]) },
{ name: 'secondarystat', values: new ArrayVector([0.17, 0.33, NaN]) },
{ name: 'arc__success', values: new ArrayVector([0, 0, 1]) },
{ name: 'arc__failed', values: new ArrayVector([1, 1, 0]) },
]);

View File

@ -95,9 +95,9 @@ export function toNodesFrame(values: any[]) {
return toVectors([
{ name: 'id', values: values[0] },
{ name: 'title', values: values[1] },
{ name: 'subTitle', values: values[2] },
{ name: 'mainStat', values: values[3] },
{ name: 'secondaryStat', values: values[4] },
{ name: 'subtitle', values: values[2] },
{ name: 'mainstat', values: values[3] },
{ name: 'secondarystat', values: values[4] },
{
name: 'color',
config: {

View File

@ -1,5 +1,12 @@
import { ArrayVector, createTheme } from '@grafana/data';
import { makeEdgesDataFrame, makeNodesDataFrame, processNodes } from './utils';
import { ArrayVector, createTheme, DataFrame, FieldType, MutableDataFrame } from '@grafana/data';
import {
getEdgeFields,
getNodeFields,
getNodeGraphDataFrames,
makeEdgesDataFrame,
makeNodesDataFrame,
processNodes,
} from './utils';
describe('processNodes', () => {
const theme = createTheme();
@ -62,14 +69,14 @@ describe('processNodes', () => {
mainStat: {
config: {},
index: 3,
name: 'mainStat',
name: 'mainstat',
type: 'number',
values: new ArrayVector([0.1, 0.1, 0.1]),
},
secondaryStat: {
config: {},
index: 4,
name: 'secondaryStat',
name: 'secondarystat',
type: 'number',
values: new ArrayVector([2, 2, 2]),
},
@ -106,14 +113,14 @@ describe('processNodes', () => {
mainStat: {
config: {},
index: 3,
name: 'mainStat',
name: 'mainstat',
type: 'number',
values: new ArrayVector([0.1, 0.1, 0.1]),
},
secondaryStat: {
config: {},
index: 4,
name: 'secondaryStat',
name: 'secondarystat',
type: 'number',
values: new ArrayVector([2, 2, 2]),
},
@ -150,14 +157,14 @@ describe('processNodes', () => {
mainStat: {
config: {},
index: 3,
name: 'mainStat',
name: 'mainstat',
type: 'number',
values: new ArrayVector([0.1, 0.1, 0.1]),
},
secondaryStat: {
config: {},
index: 4,
name: 'secondaryStat',
name: 'secondarystat',
type: 'number',
values: new ArrayVector([2, 2, 2]),
},
@ -204,4 +211,85 @@ describe('processNodes', () => {
},
]);
});
it('detects dataframes correctly', () => {
const validFrames = [
new MutableDataFrame({
refId: 'hasPreferredVisualisationType',
fields: [],
meta: {
preferredVisualisationType: 'nodeGraph',
},
}),
new MutableDataFrame({
refId: 'hasName',
fields: [],
name: 'nodes',
}),
new MutableDataFrame({
refId: 'nodes', // hasRefId
fields: [],
}),
new MutableDataFrame({
refId: 'hasValidNodesShape',
fields: [{ name: 'id', type: FieldType.string }],
}),
new MutableDataFrame({
refId: 'hasValidEdgesShape',
fields: [
{ name: 'id', type: FieldType.string },
{ name: 'source', type: FieldType.string },
{ name: 'target', type: FieldType.string },
],
}),
];
const invalidFrames = [
new MutableDataFrame({
refId: 'invalidData',
fields: [],
}),
];
const frames = [...validFrames, ...invalidFrames];
const nodeGraphFrames = getNodeGraphDataFrames(frames as DataFrame[]);
expect(nodeGraphFrames.length).toBe(5);
expect(nodeGraphFrames).toEqual(validFrames);
});
it('getting fields is case insensitive', () => {
const nodeFrame = new MutableDataFrame({
refId: 'nodes',
fields: [
{ name: 'id', type: FieldType.string, values: ['id'] },
{ name: 'title', type: FieldType.string, values: ['title'] },
{ name: 'SUBTITLE', type: FieldType.string, values: ['subTitle'] },
{ name: 'mainstat', type: FieldType.string, values: ['mainStat'] },
{ name: 'seconDarysTat', type: FieldType.string, values: ['secondaryStat'] },
],
});
const nodeFields = getNodeFields(nodeFrame);
expect(nodeFields.id).toBeDefined();
expect(nodeFields.title).toBeDefined();
expect(nodeFields.subTitle).toBeDefined();
expect(nodeFields.mainStat).toBeDefined();
expect(nodeFields.secondaryStat).toBeDefined();
const edgeFrame = new MutableDataFrame({
refId: 'nodes',
fields: [
{ name: 'id', type: FieldType.string, values: ['id'] },
{ name: 'source', type: FieldType.string, values: ['title'] },
{ name: 'TARGET', type: FieldType.string, values: ['subTitle'] },
{ name: 'mainstat', type: FieldType.string, values: ['mainStat'] },
{ name: 'secondarystat', type: FieldType.string, values: ['secondaryStat'] },
],
});
const edgeFields = getEdgeFields(edgeFrame);
expect(edgeFields.id).toBeDefined();
expect(edgeFields.source).toBeDefined();
expect(edgeFields.target).toBeDefined();
expect(edgeFields.mainStat).toBeDefined();
expect(edgeFields.secondaryStat).toBeDefined();
});
});

View File

@ -35,13 +35,17 @@ export function shortenLine(line: Line, length: number): Line {
}
export function getNodeFields(nodes: DataFrame) {
const fieldsCache = new FieldCache(nodes);
const normalizedFrames = {
...nodes,
fields: nodes.fields.map((field) => ({ ...field, name: field.name.toLowerCase() })),
};
const fieldsCache = new FieldCache(normalizedFrames);
return {
id: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.id),
title: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.title),
subTitle: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.subTitle),
mainStat: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.mainStat),
secondaryStat: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.secondaryStat),
id: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.id.toLowerCase()),
title: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.title.toLowerCase()),
subTitle: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.subTitle.toLowerCase()),
mainStat: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.mainStat.toLowerCase()),
secondaryStat: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.secondaryStat.toLowerCase()),
arc: findFieldsByPrefix(nodes, NodeGraphDataFrameFieldNames.arc),
details: findFieldsByPrefix(nodes, NodeGraphDataFrameFieldNames.detail),
color: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.color),
@ -49,14 +53,18 @@ export function getNodeFields(nodes: DataFrame) {
}
export function getEdgeFields(edges: DataFrame) {
const fieldsCache = new FieldCache(edges);
const normalizedFrames = {
...edges,
fields: edges.fields.map((field) => ({ ...field, name: field.name.toLowerCase() })),
};
const fieldsCache = new FieldCache(normalizedFrames);
return {
id: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.id),
source: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.source),
target: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.target),
mainStat: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.mainStat),
secondaryStat: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.secondaryStat),
details: findFieldsByPrefix(edges, NodeGraphDataFrameFieldNames.detail),
id: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.id.toLowerCase()),
source: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.source.toLowerCase()),
target: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.target.toLowerCase()),
mainStat: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.mainStat.toLowerCase()),
secondaryStat: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.secondaryStat.toLowerCase()),
details: findFieldsByPrefix(edges, NodeGraphDataFrameFieldNames.detail.toLowerCase()),
};
}
@ -172,11 +180,11 @@ function makeNode(index: number) {
return {
id: index.toString(),
title: `service:${index}`,
subTitle: 'service',
subtitle: 'service',
arc__success: 0.5,
arc__errors: 0.5,
mainStat: 0.1,
secondaryStat: 2,
mainstat: 0.1,
secondarystat: 2,
color: 0.5,
};
}
@ -327,7 +335,13 @@ export function getNodeGraphDataFrames(frames: DataFrame[]) {
if (frame.meta?.preferredVisualisationType === 'nodeGraph') {
return true;
}
if (frame.name === 'nodes' || frame.name === 'edges') {
if (frame.name === 'nodes' || frame.name === 'edges' || frame.refId === 'nodes' || frame.refId === 'edges') {
return true;
}
const fieldsCache = new FieldCache(frame);
if (fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.id)) {
return true;
}