mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
NodeGraph: Add node graph visualization (#29706)
* Add GraphView component * Add service map panel * Add more metadata visuals * Add context menu on click * Add context menu for services * Fix service map in dashboard * Add field proxy in explore linkSupplier * Refactor the link creation * Remove test file * Fix scale change when view is panned * Fix node centering * Don't show context menu if no links * Fix service map containers * Add collapsible around the service map * Fix stats computation * Remove debug log * Fix time stats * Allow string timestamp * Make panning bounded * Add zooming by mouse wheel * Clean up the colors * Fix stats for single trace graph * Don't show debug config * Add more complex layout * Update layout with better fixing of the root nodes * Code cleanup * Change how we pass in link creation function and some more cleanup * Refactor the panel section into separate render methods * Make the edge hover more readable * Move stats computation to data source * Put edge labels to front * Simplify layout for better multi graph layout * Update for dark theme * Move function to utils * Visual improvements * Improve context menu detail * Allow custom details * Rename to NodeGraph * Remove unused dependencies * Use named color palette and add some fallbacks for missing data * Add test data scenario * Rename plugin * Switch scroll zoom direction to align with google maps * Do some perf optimisations and rise the node limit * Update alert styling * Rename function * Add tests * Add more tests * Change data frame column mapping to use column names * Fix test * Fix type errors * Don't show context menu without links * Add beta status to panel * Fix tests * Changed function to standard methods * Fix typing * Clean up yarn.lock * Add some UI improvements - better styling of the zoom buttons - disable buttons when max reached * Fix panel references after rename * Add panel icon
This commit is contained in:
155
public/app/plugins/datasource/testdata/nodeGraphUtils.ts
vendored
Normal file
155
public/app/plugins/datasource/testdata/nodeGraphUtils.ts
vendored
Normal file
@@ -0,0 +1,155 @@
|
||||
import { ArrayVector, FieldType, MutableDataFrame } from '@grafana/data';
|
||||
import { nodes, edges } from './testData/serviceMapResponse';
|
||||
import { NodeGraphDataFrameFieldNames } from '@grafana/ui';
|
||||
|
||||
export function generateRandomNodes(count = 10) {
|
||||
const nodes = [];
|
||||
|
||||
const root = {
|
||||
id: '0',
|
||||
title: 'root',
|
||||
subTitle: 'client',
|
||||
success: 1,
|
||||
error: 0,
|
||||
stat1: Math.random(),
|
||||
stat2: Math.random(),
|
||||
edges: [] as any[],
|
||||
};
|
||||
nodes.push(root);
|
||||
const nodesWithoutMaxEdges = [root];
|
||||
|
||||
const maxEdges = 3;
|
||||
|
||||
for (let i = 1; i < count; i++) {
|
||||
const node = makeRandomNode(i);
|
||||
nodes.push(node);
|
||||
const sourceIndex = Math.floor(Math.random() * Math.floor(nodesWithoutMaxEdges.length - 1));
|
||||
const source = nodesWithoutMaxEdges[sourceIndex];
|
||||
source.edges.push(node.id);
|
||||
if (source.edges.length >= maxEdges) {
|
||||
nodesWithoutMaxEdges.splice(sourceIndex, 1);
|
||||
}
|
||||
nodesWithoutMaxEdges.push(node);
|
||||
}
|
||||
|
||||
// Add some random edges to create possible cycle
|
||||
const additionalEdges = Math.floor(count / 2);
|
||||
for (let i = 0; i <= additionalEdges; i++) {
|
||||
const sourceIndex = Math.floor(Math.random() * Math.floor(nodes.length - 1));
|
||||
const targetIndex = Math.floor(Math.random() * Math.floor(nodes.length - 1));
|
||||
if (sourceIndex === targetIndex || nodes[sourceIndex].id === '0' || nodes[sourceIndex].id === '0') {
|
||||
continue;
|
||||
}
|
||||
|
||||
nodes[sourceIndex].edges.push(nodes[sourceIndex].id);
|
||||
}
|
||||
|
||||
const nodeFields: any = {
|
||||
[NodeGraphDataFrameFieldNames.id]: {
|
||||
values: new ArrayVector(),
|
||||
type: FieldType.string,
|
||||
},
|
||||
[NodeGraphDataFrameFieldNames.title]: {
|
||||
values: new ArrayVector(),
|
||||
type: FieldType.string,
|
||||
},
|
||||
[NodeGraphDataFrameFieldNames.subTitle]: {
|
||||
values: new ArrayVector(),
|
||||
type: FieldType.string,
|
||||
},
|
||||
[NodeGraphDataFrameFieldNames.mainStat]: {
|
||||
values: new ArrayVector(),
|
||||
type: FieldType.number,
|
||||
},
|
||||
[NodeGraphDataFrameFieldNames.secondaryStat]: {
|
||||
values: new ArrayVector(),
|
||||
type: FieldType.number,
|
||||
},
|
||||
[NodeGraphDataFrameFieldNames.arc + 'success']: {
|
||||
values: new ArrayVector(),
|
||||
type: FieldType.number,
|
||||
config: { color: { fixedColor: 'green' } },
|
||||
},
|
||||
[NodeGraphDataFrameFieldNames.arc + 'errors']: {
|
||||
values: new ArrayVector(),
|
||||
type: FieldType.number,
|
||||
config: { color: { fixedColor: 'red' } },
|
||||
},
|
||||
};
|
||||
|
||||
const nodeFrame = new MutableDataFrame({
|
||||
name: 'nodes',
|
||||
fields: Object.keys(nodeFields).map(key => ({
|
||||
...nodeFields[key],
|
||||
name: key,
|
||||
})),
|
||||
meta: { preferredVisualisationType: 'nodeGraph' },
|
||||
});
|
||||
|
||||
const edgeFields: any = {
|
||||
[NodeGraphDataFrameFieldNames.id]: {
|
||||
values: new ArrayVector(),
|
||||
type: FieldType.string,
|
||||
},
|
||||
[NodeGraphDataFrameFieldNames.source]: {
|
||||
values: new ArrayVector(),
|
||||
type: FieldType.string,
|
||||
},
|
||||
[NodeGraphDataFrameFieldNames.target]: {
|
||||
values: new ArrayVector(),
|
||||
type: FieldType.string,
|
||||
},
|
||||
};
|
||||
|
||||
const edgesFrame = new MutableDataFrame({
|
||||
name: 'edges',
|
||||
fields: Object.keys(edgeFields).map(key => ({
|
||||
...edgeFields[key],
|
||||
name: key,
|
||||
})),
|
||||
meta: { preferredVisualisationType: 'nodeGraph' },
|
||||
});
|
||||
|
||||
const edgesSet = new Set();
|
||||
for (const node of nodes) {
|
||||
nodeFields.id.values.add(node.id);
|
||||
nodeFields.title.values.add(node.title);
|
||||
nodeFields.subTitle.values.add(node.subTitle);
|
||||
nodeFields.mainStat.values.add(node.stat1);
|
||||
nodeFields.secondaryStat.values.add(node.stat2);
|
||||
nodeFields.arc__success.values.add(node.success);
|
||||
nodeFields.arc__errors.values.add(node.error);
|
||||
for (const edge of node.edges) {
|
||||
const id = `${node.id}--${edge}`;
|
||||
// We can have duplicate edges when we added some more by random
|
||||
if (edgesSet.has(id)) {
|
||||
continue;
|
||||
}
|
||||
edgesSet.add(id);
|
||||
edgeFields.id.values.add(`${node.id}--${edge}`);
|
||||
edgeFields.source.values.add(node.id);
|
||||
edgeFields.target.values.add(edge);
|
||||
}
|
||||
}
|
||||
|
||||
return [nodeFrame, edgesFrame];
|
||||
}
|
||||
|
||||
function makeRandomNode(index: number) {
|
||||
const success = Math.random();
|
||||
const error = 1 - success;
|
||||
return {
|
||||
id: index.toString(),
|
||||
title: `service:${index}`,
|
||||
subTitle: 'service',
|
||||
success,
|
||||
error,
|
||||
stat1: Math.random(),
|
||||
stat2: Math.random(),
|
||||
edges: [],
|
||||
};
|
||||
}
|
||||
|
||||
export function savedNodesResponse(): any {
|
||||
return [new MutableDataFrame(nodes), new MutableDataFrame(edges)];
|
||||
}
|
||||
Reference in New Issue
Block a user