From 939a7781113b0d2a0e5bd7b078891a9fd6695b85 Mon Sep 17 00:00:00 2001 From: Connor Lindsey Date: Wed, 13 Apr 2022 08:18:40 -0700 Subject: [PATCH] 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 --- docs/sources/visualizations/node-graph.md | 26 ++--- packages/grafana-data/src/utils/nodeGraph.ts | 6 +- .../plugins/datasource/jaeger/testResponse.ts | 6 +- .../datasource/tempo/datasource.test.ts | 6 +- .../datasource/tempo/graphTransform.test.ts | 30 ++--- .../datasource/zipkin/utils/testResponse.ts | 6 +- .../app/plugins/panel/nodeGraph/utils.test.ts | 104 ++++++++++++++++-- public/app/plugins/panel/nodeGraph/utils.ts | 48 +++++--- 8 files changed, 167 insertions(+), 65 deletions(-) diff --git a/docs/sources/visualizations/node-graph.md b/docs/sources/visualizations/node-graph.md index 2545d108325..e0fcc69fed5 100644 --- a/docs/sources/visualizations/node-graph.md +++ b/docs/sources/visualizations/node-graph.md @@ -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. | diff --git a/packages/grafana-data/src/utils/nodeGraph.ts b/packages/grafana-data/src/utils/nodeGraph.ts index 03a1c48eda9..caccc59e5ef 100644 --- a/packages/grafana-data/src/utils/nodeGraph.ts +++ b/packages/grafana-data/src/utils/nodeGraph.ts @@ -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__', diff --git a/public/app/plugins/datasource/jaeger/testResponse.ts b/public/app/plugins/datasource/jaeger/testResponse.ts index 39c5b4121c3..e9627249eae 100644 --- a/public/app/plugins/datasource/jaeger/testResponse.ts +++ b/public/app/plugins/datasource/jaeger/testResponse.ts @@ -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: { diff --git a/public/app/plugins/datasource/tempo/datasource.test.ts b/public/app/plugins/datasource/tempo/datasource.test.ts index e5884138fe9..e3c14e675b3 100644 --- a/public/app/plugins/datasource/tempo/datasource.test.ts +++ b/public/app/plugins/datasource/tempo/datasource.test.ts @@ -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] }, ]); diff --git a/public/app/plugins/datasource/tempo/graphTransform.test.ts b/public/app/plugins/datasource/tempo/graphTransform.test.ts index a6dd5b417cf..666ce5a8945 100644 --- a/public/app/plugins/datasource/tempo/graphTransform.test.ts +++ b/public/app/plugins/datasource/tempo/graphTransform.test.ts @@ -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]) }, ]); diff --git a/public/app/plugins/datasource/zipkin/utils/testResponse.ts b/public/app/plugins/datasource/zipkin/utils/testResponse.ts index bdf76fdb35d..7a6ba459102 100644 --- a/public/app/plugins/datasource/zipkin/utils/testResponse.ts +++ b/public/app/plugins/datasource/zipkin/utils/testResponse.ts @@ -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: { diff --git a/public/app/plugins/panel/nodeGraph/utils.test.ts b/public/app/plugins/panel/nodeGraph/utils.test.ts index b2e25b0b98e..d95e4e72fdd 100644 --- a/public/app/plugins/panel/nodeGraph/utils.test.ts +++ b/public/app/plugins/panel/nodeGraph/utils.test.ts @@ -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(); + }); }); diff --git a/public/app/plugins/panel/nodeGraph/utils.ts b/public/app/plugins/panel/nodeGraph/utils.ts index c79274427f1..0aac55cedf8 100644 --- a/public/app/plugins/panel/nodeGraph/utils.ts +++ b/public/app/plugins/panel/nodeGraph/utils.ts @@ -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; }