From 20e70838e35ea86604e156d9b8b6b41e1a77042a Mon Sep 17 00:00:00 2001 From: Andrej Ocenas Date: Wed, 27 Mar 2024 14:23:04 +0100 Subject: [PATCH] NodeGraph: Fix possible metadata mismatch between nodes in graph (#85254) --- .../plugins/panel/nodeGraph/layout.test.ts | 303 +++++++++++++++++- public/app/plugins/panel/nodeGraph/layout.ts | 16 +- 2 files changed, 312 insertions(+), 7 deletions(-) diff --git a/public/app/plugins/panel/nodeGraph/layout.test.ts b/public/app/plugins/panel/nodeGraph/layout.test.ts index fb662d14450..63965301e11 100644 --- a/public/app/plugins/panel/nodeGraph/layout.test.ts +++ b/public/app/plugins/panel/nodeGraph/layout.test.ts @@ -1,4 +1,5 @@ import { renderHook } from '@testing-library/react'; +import { act } from 'react-test-renderer'; import { useLayout } from './layout'; import { EdgeDatum, NodeDatum } from './types'; @@ -6,6 +7,11 @@ import { EdgeDatum, NodeDatum } from './types'; let onmessage: jest.Mock; let postMessage: jest.Mock; let terminate: jest.Mock; +let worker: { + onmessage: (e: MessageEvent) => void; + postMessage: (d: unknown) => void; + terminate: () => void; +}; jest.mock('./createLayoutWorker', () => { return { @@ -14,11 +20,23 @@ jest.mock('./createLayoutWorker', () => { onmessage = jest.fn(); postMessage = jest.fn(); terminate = jest.fn(); - return { + worker = { onmessage: onmessage, postMessage: postMessage, terminate: terminate, }; + return worker; + }, + createMsaglWorker: () => { + onmessage = jest.fn(); + postMessage = jest.fn(); + terminate = jest.fn(); + worker = { + onmessage: onmessage, + postMessage: postMessage, + terminate: terminate, + }; + return worker; }, }; }); @@ -64,6 +82,32 @@ describe('layout', () => { expect(result.current.edges).toEqual([]); expect(localTerminate).toBeCalledTimes(1); }); + + it('correctly maps the layout data', async () => { + // This specifically tests whether the mapping of data from the layout code back to the original nodes is correct. + + const { result, rerender } = renderHook( + ({ nodes, edges }) => { + return useLayout(nodes, edges, undefined, 100, 1000); + }, + { initialProps: { nodes: rawNodes, edges: rawEdges } } + ); + + // Simulate response from the layout worker + act(() => { + worker.onmessage(new MessageEvent('message', { data: { nodes: testNodes, edges: testEdges } })); + }); + + rerender({ nodes: rawNodes, edges: rawEdges }); + + expect(result.current.nodes).toMatchObject([ + { id: 'otel-demo-alt/frontend', title: 'frontend' }, + { id: 'otel-demo-alt/cartservice', title: 'cartservice' }, + { id: 'otel-demo-alt/checkoutservice', title: 'checkoutservice' }, + { id: 'otel-demo-alt/frontend-proxy', title: 'frontend-proxy' }, + { id: 'otel-demo-alt/recommendationservice', title: 'recommendationservice' }, + ]); + }); }); function makeNode(index: number, incoming: number): NodeDatum { @@ -92,3 +136,260 @@ function makeEdge(source: number, target: number): EdgeDatum { thickness: 1, }; } + +const rawNodes: NodeDatum[] = [ + { + dataFrameRowIndex: 0, + id: 'otel-demo-alt/cartservice', + incoming: 1, + subTitle: 'otel-demo-alt', + title: 'cartservice', + highlighted: false, + arcSections: [], + }, + { + dataFrameRowIndex: 1, + id: 'otel-demo-alt/frontend', + incoming: 1, + subTitle: 'otel-demo-alt', + title: 'frontend', + highlighted: false, + arcSections: [], + }, + { + dataFrameRowIndex: 2, + id: 'otel-demo-alt/checkoutservice', + incoming: 1, + subTitle: 'otel-demo-alt', + title: 'checkoutservice', + highlighted: false, + arcSections: [], + }, + { + dataFrameRowIndex: 3, + id: 'otel-demo-alt/frontend-proxy', + incoming: 1, + subTitle: 'otel-demo-alt', + title: 'frontend-proxy', + highlighted: false, + arcSections: [], + }, + { + dataFrameRowIndex: 4, + id: 'otel-demo-alt/recommendationservice', + incoming: 1, + subTitle: 'otel-demo-alt', + title: 'recommendationservice', + highlighted: false, + arcSections: [], + }, +]; + +const rawEdges = [ + { + id: 'otel-demo-alt/frontend_otel-demo-alt/cartservice', + dataFrameRowIndex: 0, + source: 'otel-demo-alt/frontend', + target: 'otel-demo-alt/cartservice', + sourceNodeRadius: 40, + targetNodeRadius: 40, + mainStat: '1.08 ms/r', + secondaryStat: '0.93 r/sec', + highlighted: false, + thickness: 1, + }, + { + id: 'otel-demo-alt/frontend_otel-demo-alt/checkoutservice', + dataFrameRowIndex: 1, + source: 'otel-demo-alt/frontend', + target: 'otel-demo-alt/checkoutservice', + sourceNodeRadius: 40, + targetNodeRadius: 40, + mainStat: '12.66 ms/r', + secondaryStat: '0.12 r/sec', + highlighted: false, + thickness: 1, + }, + { + id: 'otel-demo-alt/frontend_otel-demo-alt/frontend-proxy', + dataFrameRowIndex: 2, + source: 'otel-demo-alt/frontend', + target: 'otel-demo-alt/frontend-proxy', + sourceNodeRadius: 40, + targetNodeRadius: 40, + mainStat: '7.45 ms/r', + secondaryStat: '0.99 r/sec', + highlighted: false, + thickness: 1, + }, + { + id: 'otel-demo-alt/frontend_otel-demo-alt/recommendationservice', + dataFrameRowIndex: 3, + source: 'otel-demo-alt/frontend', + target: 'otel-demo-alt/recommendationservice', + sourceNodeRadius: 40, + targetNodeRadius: 40, + mainStat: '13.01 ms/r', + secondaryStat: '0.18 r/sec', + highlighted: false, + thickness: 1, + }, + { + id: 'otel-demo-alt/frontend-proxy_otel-demo-alt/frontend', + dataFrameRowIndex: 4, + source: 'otel-demo-alt/frontend-proxy', + target: 'otel-demo-alt/frontend', + sourceNodeRadius: 40, + targetNodeRadius: 40, + mainStat: '7.50 ms/r', + secondaryStat: '6.06 r/sec', + highlighted: false, + thickness: 1, + }, +]; + +const testNodes = [ + { + id: 'otel-demo-alt/frontend', + incoming: 1, + x: -80.84375, + y: -70.49999999999997, + }, + { + id: 'otel-demo-alt/cartservice', + incoming: 1, + x: 80.84375, + y: 211.5, + }, + { + id: 'otel-demo-alt/checkoutservice', + incoming: 1, + x: 80.84375, + y: 70.5, + }, + { + id: 'otel-demo-alt/frontend-proxy', + incoming: 1, + x: 80.84375, + y: -70.5, + }, + { + id: 'otel-demo-alt/recommendationservice', + incoming: 1, + x: 80.84375, + y: -211.5, + }, +]; + +const testEdges = [ + { + id: 'otel-demo-alt/frontend_otel-demo-alt/cartservice', + dataFrameRowIndex: 0, + source: { + id: 'otel-demo-alt/frontend', + incoming: 1, + x: -80.84375, + y: -70.49999999999997, + }, + target: { + id: 'otel-demo-alt/cartservice', + incoming: 1, + x: 80.84375, + y: 211.5, + }, + sourceNodeRadius: 40, + targetNodeRadius: 40, + mainStat: '1.07 ms/r', + secondaryStat: '1.08 r/sec', + highlighted: false, + thickness: 1, + }, + { + id: 'otel-demo-alt/frontend_otel-demo-alt/checkoutservice', + dataFrameRowIndex: 1, + source: { + id: 'otel-demo-alt/frontend', + incoming: 1, + x: -80.84375, + y: -70.49999999999997, + }, + target: { + id: 'otel-demo-alt/checkoutservice', + incoming: 1, + x: 80.84375, + y: 70.5, + }, + sourceNodeRadius: 40, + targetNodeRadius: 40, + mainStat: '8.18 ms/r', + secondaryStat: '0.14 r/sec', + highlighted: false, + thickness: 1, + }, + { + id: 'otel-demo-alt/frontend_otel-demo-alt/frontend-proxy', + dataFrameRowIndex: 2, + source: { + id: 'otel-demo-alt/frontend', + incoming: 1, + x: -80.84375, + y: -70.49999999999997, + }, + target: { + id: 'otel-demo-alt/frontend-proxy', + incoming: 1, + x: 80.84375, + y: -70.5, + }, + sourceNodeRadius: 40, + targetNodeRadius: 40, + mainStat: '7.57 ms/r', + secondaryStat: '1.14 r/sec', + highlighted: false, + thickness: 1, + }, + { + id: 'otel-demo-alt/frontend_otel-demo-alt/recommendationservice', + dataFrameRowIndex: 3, + source: { + id: 'otel-demo-alt/frontend', + incoming: 1, + x: -80.84375, + y: -70.49999999999997, + }, + target: { + id: 'otel-demo-alt/recommendationservice', + incoming: 1, + x: 80.84375, + y: -211.5, + }, + sourceNodeRadius: 40, + targetNodeRadius: 40, + mainStat: '8.60 ms/r', + secondaryStat: '0.22 r/sec', + highlighted: false, + thickness: 1, + }, + { + id: 'otel-demo-alt/frontend-proxy_otel-demo-alt/frontend', + dataFrameRowIndex: 4, + source: { + id: 'otel-demo-alt/frontend-proxy', + incoming: 1, + x: 80.84375, + y: -70.5, + }, + target: { + id: 'otel-demo-alt/frontend', + incoming: 1, + x: -80.84375, + y: -70.49999999999997, + }, + sourceNodeRadius: 40, + targetNodeRadius: 40, + mainStat: '7.53 ms/r', + secondaryStat: '6.82 r/sec', + highlighted: false, + thickness: 1, + }, +]; diff --git a/public/app/plugins/panel/nodeGraph/layout.ts b/public/app/plugins/panel/nodeGraph/layout.ts index 75776374efe..b3fd26d4c93 100644 --- a/public/app/plugins/panel/nodeGraph/layout.ts +++ b/public/app/plugins/panel/nodeGraph/layout.ts @@ -1,3 +1,4 @@ +import { fromPairs } from 'lodash'; import { useEffect, useMemo, useRef, useState } from 'react'; import { useUnmount } from 'react-use'; import useMountedState from 'react-use/lib/useMountedState'; @@ -159,13 +160,16 @@ function layout( const worker = engine === 'default' ? createWorker() : createMsaglWorker(); worker.onmessage = (event: MessageEvent<{ nodes: NodeDatum[]; edges: EdgeDatumLayout[] }>) => { - for (let i = 0; i < nodes.length; i++) { - // These stats needs to be Field class but the data is stringified over the worker boundary - event.data.nodes[i] = { - ...nodes[i], - ...event.data.nodes[i], + const nodesMap = fromPairs(nodes.map((node) => [node.id, node])); + + // Add the x,y coordinates from the layout algorithm to the original nodes. + event.data.nodes = event.data.nodes.map((node) => { + return { + ...nodesMap[node.id], + ...node, }; - } + }); + done(event.data); };