Files
timo 3294411027 NodeGraph: Fix configuring arc colors with mixed case field names (#84609)
* NodeGraphPanel: Fix case comparison for arc field colors

When a field has mixed case in the data frame, the options editor
offers it with mixed case as well, so the options will have the field
with mixed case as well, making the comparison in utils.ts
applyOptiosToFrames fail, leaving the arcs uncolored.

This version of the commit allows mismatched cases between field in
dataframe and options panel in case users were depending on this
behavior in their dashboards.

* Update comment

---------

Co-authored-by: Andrej Ocenas <mr.ocenas@gmail.com>
2024-04-05 13:39:07 +02:00

357 lines
10 KiB
TypeScript

import { DataFrame, FieldType, createDataFrame } from '@grafana/data';
import { NodeDatum, NodeGraphOptions } from './types';
import {
findConnectedNodesForEdge,
findConnectedNodesForNode,
getEdgeFields,
getNodeFields,
getNodeGraphDataFrames,
makeEdgesDataFrame,
makeNodesDataFrame,
processNodes,
} from './utils';
describe('processNodes', () => {
it('handles empty args', async () => {
expect(processNodes(undefined, undefined)).toEqual({ nodes: [], edges: [] });
});
it('returns proper nodes and edges', async () => {
const { nodes, edges, legend } = processNodes(
makeNodesDataFrame(3),
makeEdgesDataFrame([
{ source: '0', target: '1' },
{ source: '0', target: '2' },
{ source: '1', target: '2' },
])
);
expect(nodes).toEqual([
makeNodeDatum(),
makeNodeDatum({ dataFrameRowIndex: 1, id: '1', incoming: 1, title: 'service:1' }),
makeNodeDatum({ dataFrameRowIndex: 2, id: '2', incoming: 2, title: 'service:2' }),
]);
expect(edges).toEqual([makeEdgeDatum('0--1', 0), makeEdgeDatum('0--2', 1), makeEdgeDatum('1--2', 2)]);
expect(legend).toEqual([
{
color: 'green',
name: 'arc__success',
},
{
color: 'red',
name: 'arc__errors',
},
]);
});
it('returns nodes just from edges dataframe', () => {
const { nodes, edges } = processNodes(
undefined,
makeEdgesDataFrame([
{ source: '0', target: '1', mainstat: 1, secondarystat: 1 },
{ source: '0', target: '2', mainstat: 1, secondarystat: 1 },
{ source: '1', target: '2', mainstat: 1, secondarystat: 1 },
])
);
expect(nodes).toEqual([
expect.objectContaining(makeNodeFromEdgeDatum({ dataFrameRowIndex: 0, title: '0' })),
expect.objectContaining(makeNodeFromEdgeDatum({ dataFrameRowIndex: 1, id: '1', incoming: 1, title: '1' })),
expect.objectContaining(makeNodeFromEdgeDatum({ dataFrameRowIndex: 2, id: '2', incoming: 2, title: '2' })),
]);
expect(nodes[0].mainStat?.values).toEqual([undefined, 1, 2]);
expect(nodes[0].secondaryStat?.values).toEqual([undefined, 1, 2]);
expect(nodes[0].mainStat).toEqual(nodes[1].mainStat);
expect(nodes[0].mainStat).toEqual(nodes[2].mainStat);
expect(nodes[0].secondaryStat).toEqual(nodes[1].secondaryStat);
expect(nodes[0].secondaryStat).toEqual(nodes[2].secondaryStat);
expect(edges).toEqual([
makeEdgeDatum('0--1', 0, '1.00', '1.00'),
makeEdgeDatum('0--2', 1, '1.00', '1.00'),
makeEdgeDatum('1--2', 2, '1.00', '1.00'),
]);
});
it('detects dataframes correctly', () => {
const validFrames = [
createDataFrame({
refId: 'hasPreferredVisualisationType',
fields: [],
meta: {
preferredVisualisationType: 'nodeGraph',
},
}),
createDataFrame({
refId: 'hasName',
fields: [],
name: 'nodes',
}),
createDataFrame({
refId: 'nodes', // hasRefId
fields: [],
}),
createDataFrame({
refId: 'hasValidNodesShape',
fields: [{ name: 'id', type: FieldType.string }],
}),
createDataFrame({
refId: 'hasValidEdgesShape',
fields: [
{ name: 'id', type: FieldType.string },
{ name: 'source', type: FieldType.string },
{ name: 'target', type: FieldType.string },
],
}),
];
const invalidFrames = [
createDataFrame({
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 = createDataFrame({
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'] },
{ name: 'nodeRadius', type: FieldType.number, values: [20] },
],
});
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 = createDataFrame({
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();
});
it('interpolates panel options correctly', () => {
const frames = [
createDataFrame({
refId: 'nodes',
fields: [
{ name: 'id', type: FieldType.string },
{ name: 'mainStat', type: FieldType.string },
{ name: 'secondaryStat', type: FieldType.string },
{ name: 'arc__primary', type: FieldType.string },
{ name: 'arc__Secondary', type: FieldType.string },
{ name: 'arc__tertiary', type: FieldType.string },
],
}),
createDataFrame({
refId: 'edges',
fields: [
{ name: 'id', type: FieldType.string },
{ name: 'source', type: FieldType.string },
{ name: 'target', type: FieldType.string },
{ name: 'mainStat', type: FieldType.string },
{ name: 'secondaryStat', type: FieldType.string },
],
}),
];
const panelOptions: NodeGraphOptions = {
nodes: {
mainStatUnit: 'r/min',
secondaryStatUnit: 'ms/r',
arcs: [
{ field: 'arc__primary', color: 'red' },
{ field: 'arc__Secondary', color: 'yellow' },
{ field: 'arc__tertiary', color: '#dd40ec' },
],
},
edges: {
mainStatUnit: 'r/sec',
secondaryStatUnit: 'ft^2',
},
};
const nodeGraphFrames = getNodeGraphDataFrames(frames, panelOptions);
expect(nodeGraphFrames).toHaveLength(2);
const nodesFrame = nodeGraphFrames.find((f) => f.refId === 'nodes');
expect(nodesFrame).toBeDefined();
expect(nodesFrame?.fields.find((f) => f.name === 'mainStat')?.config).toEqual({ unit: 'r/min' });
expect(nodesFrame?.fields.find((f) => f.name === 'secondaryStat')?.config).toEqual({ unit: 'ms/r' });
expect(nodesFrame?.fields.find((f) => f.name === 'arc__primary')?.config).toEqual({
color: { mode: 'fixed', fixedColor: 'red' },
});
expect(nodesFrame?.fields.find((f) => f.name === 'arc__Secondary')?.config).toEqual({
color: { mode: 'fixed', fixedColor: 'yellow' },
});
expect(nodesFrame?.fields.find((f) => f.name === 'arc__tertiary')?.config).toEqual({
color: { mode: 'fixed', fixedColor: '#dd40ec' },
});
const edgesFrame = nodeGraphFrames.find((f) => f.refId === 'edges');
expect(edgesFrame).toBeDefined();
expect(edgesFrame?.fields.find((f) => f.name === 'mainStat')?.config).toEqual({ unit: 'r/sec' });
expect(edgesFrame?.fields.find((f) => f.name === 'secondaryStat')?.config).toEqual({ unit: 'ft^2' });
});
});
describe('finds connections', () => {
it('finds connected nodes given an edge id', () => {
const { nodes, edges } = processNodes(
makeNodesDataFrame(3),
makeEdgesDataFrame([
{ source: '0', target: '1' },
{ source: '0', target: '2' },
{ source: '1', target: '2' },
])
);
const linked = findConnectedNodesForEdge(nodes, edges, edges[0].id);
expect(linked).toEqual(['0', '1']);
});
it('finds connected nodes given a node id', () => {
const { nodes, edges } = processNodes(
makeNodesDataFrame(4),
makeEdgesDataFrame([
{ source: '0', target: '1' },
{ source: '0', target: '2' },
{ source: '1', target: '2' },
])
);
const linked = findConnectedNodesForNode(nodes, edges, nodes[0].id);
expect(linked).toEqual(['0', '1', '2']);
});
});
function makeNodeDatum(options: Partial<NodeDatum> = {}) {
const colorField = {
config: {
color: {
mode: 'continuous-GrYlRd',
},
},
index: 7,
name: 'color',
type: 'number',
values: [0.5, 0.5, 0.5],
};
return {
arcSections: [
{
config: {
color: {
fixedColor: 'green',
mode: 'fixed',
},
},
name: 'arc__success',
type: 'number',
values: [0.5, 0.5, 0.5],
},
{
config: {
color: {
fixedColor: 'red',
mode: 'fixed',
},
},
name: 'arc__errors',
type: 'number',
values: [0.5, 0.5, 0.5],
},
],
color: colorField,
dataFrameRowIndex: 0,
highlighted: false,
id: '0',
incoming: 0,
mainStat: {
config: {},
index: 3,
name: 'mainstat',
type: 'number',
values: [0.1, 0.1, 0.1],
},
secondaryStat: {
config: {},
index: 4,
name: 'secondarystat',
type: 'number',
values: [2, 2, 2],
},
subTitle: 'service',
title: 'service:0',
icon: 'database',
nodeRadius: {
config: {},
index: 9,
name: 'noderadius',
type: 'number',
values: [40, 40, 40],
},
...options,
};
}
function makeEdgeDatum(id: string, index: number, mainStat = '', secondaryStat = '') {
return {
dataFrameRowIndex: index,
id,
mainStat,
secondaryStat,
source: id.split('--')[0],
target: id.split('--')[1],
sourceNodeRadius: 40,
targetNodeRadius: 40,
highlighted: false,
thickness: 1,
};
}
function makeNodeFromEdgeDatum(options: Partial<NodeDatum> = {}): NodeDatum {
return {
arcSections: [],
dataFrameRowIndex: 0,
id: '0',
incoming: 0,
subTitle: '',
title: 'service:0',
...options,
highlighted: false,
};
}