|
1 | 1 | <script lang="ts"> |
2 | 2 | import { getContext, onDestroy } from 'svelte'; |
3 | 3 | import Sigma from 'sigma'; |
4 | | - import type { Bom } from '$lib/cyclonedx/models'; |
5 | 4 | import type { Coordinates, EdgeDisplayData, NodeDisplayData } from 'sigma/types'; |
6 | | - import type { AbstractGraph, SerializedGraph } from 'graphology-types'; |
| 5 | + import type { AbstractGraph } from 'graphology-types'; |
7 | 6 | import { borderSubtle01, borderStrong01, tagTokens, textDisabled } from '@carbon/themes'; |
8 | 7 | import type { TreeChartState } from '$lib/models/treechart'; |
9 | | - import type { PostMessage } from '$lib/models/worker'; |
10 | | - import Graph from 'graphology'; |
11 | 8 | import { SkeletonPlaceholder } from 'carbon-components-svelte'; |
12 | 9 | import { |
13 | 10 | getHighlightedGraphState, |
14 | 11 | isInterestingEdge, |
15 | 12 | isInterestingNode |
16 | 13 | } from '$lib/graphs/highlighting'; |
| 14 | + import { LayoutGraphWorkerWrapper } from '$lib/worker/GraphWorkerWrapper'; |
| 15 | + import Graph from 'graphology'; |
17 | 16 |
|
18 | 17 | let { |
19 | | - bom, |
| 18 | + graph, |
20 | 19 | selectedComponentRef, |
21 | 20 | searchForComponent |
22 | | - }: { bom: Bom; selectedComponentRef?: string; searchForComponent: (ref?: string) => void } = |
23 | | - $props(); |
| 21 | + }: { |
| 22 | + graph: AbstractGraph | null; |
| 23 | + selectedComponentRef?: string; |
| 24 | + searchForComponent?: (ref: string) => void; |
| 25 | + } = $props(); |
24 | 26 | const theme: () => string = getContext('theme'); |
25 | 27 |
|
| 28 | + let internalGraph: Graph | undefined = $state(); |
| 29 | + let lastUnprocessedGraph: Graph | undefined = $state(); |
| 30 | +
|
26 | 31 | let container: HTMLDivElement; |
27 | 32 | let renderer: Sigma | undefined; |
28 | | - let graph: AbstractGraph | undefined; |
29 | | - let isLoading: boolean = $state(true); |
| 33 | +
|
| 34 | + const treeGenerationWorkerWrapper = new LayoutGraphWorkerWrapper((g: Graph) => { |
| 35 | + internalGraph = g; |
| 36 | + setupGraph(); |
| 37 | + }); |
| 38 | +
|
| 39 | + let isLoading: boolean = $derived(graph === null); |
30 | 40 |
|
31 | 41 | const treeChartState: TreeChartState = { |
32 | 42 | hovered: {}, |
33 | 43 | selected: {} |
34 | 44 | }; |
35 | 45 |
|
36 | | - let treeGenerationWorker: Worker | undefined = undefined; |
37 | | -
|
38 | | - function handleWorkerMessage({ data: { payload } }: MessageEvent<PostMessage<SerializedGraph>>) { |
39 | | - if (payload) { |
40 | | - setupGraph(new Graph().import(payload)); |
41 | | - } |
42 | | - } |
43 | | -
|
44 | | - function setupWorker() { |
45 | | - treeGenerationWorker = new Worker(new URL('$lib/worker/tree-generation.ts', import.meta.url), { |
46 | | - type: 'module' |
47 | | - }); |
48 | | -
|
49 | | - treeGenerationWorker.onmessage = handleWorkerMessage; |
50 | | - } |
51 | | -
|
52 | 46 | function refreshGraph() { |
53 | 47 | // Refresh rendering |
54 | 48 | renderer?.refresh({ |
|
57 | 51 | }); |
58 | 52 | } |
59 | 53 |
|
60 | | - function setupGraph(localGraph: Graph) { |
61 | | - graph = localGraph; |
| 54 | + function setupGraph() { |
| 55 | + if (!internalGraph) { |
| 56 | + return; |
| 57 | + } |
62 | 58 |
|
63 | | - // Initialize sigma to render the graph |
64 | | - renderer = new Sigma(graph, container, { |
| 59 | + // Stop old renderer and reset chart state |
| 60 | + if (renderer) { |
| 61 | + renderer.kill(); |
| 62 | + treeChartState.hovered = {}; |
| 63 | + treeChartState.selected = {}; |
| 64 | + } |
| 65 | + // Initialize sigma to render the internalGraph |
| 66 | + renderer = new Sigma(internalGraph, container, { |
65 | 67 | defaultEdgeType: 'arrow', |
66 | 68 | defaultNodeColor: tagTokens.tagTokens.tagColorBlue[theme()], |
67 | 69 | defaultEdgeColor: borderSubtle01 |
68 | 70 | }); |
69 | | - isLoading = false; |
70 | 71 |
|
71 | | - // Bind graph interactions: |
| 72 | + // Bind internalGraph interactions: |
72 | 73 | renderer?.on('enterNode', ({ node }) => { |
73 | | - treeChartState.hovered = getHighlightedGraphState(graph, node); |
| 74 | + treeChartState.hovered = getHighlightedGraphState(internalGraph, node); |
74 | 75 | refreshGraph(); |
75 | 76 | }); |
76 | 77 | renderer?.on('clickNode', ({ node }) => { |
77 | | - treeChartState.selected = getHighlightedGraphState(graph, node); |
| 78 | + treeChartState.selected = getHighlightedGraphState(internalGraph, node); |
78 | 79 | refreshGraph(); |
79 | 80 | }); |
80 | 81 | renderer?.on('doubleClickNode', ({ node }) => { |
81 | | - searchForComponent(node); |
| 82 | + if (searchForComponent) { |
| 83 | + searchForComponent(node); |
| 84 | + } |
82 | 85 | }); |
83 | 86 | renderer?.on('leaveNode', () => { |
84 | | - treeChartState.hovered = getHighlightedGraphState(graph); |
| 87 | + treeChartState.hovered = getHighlightedGraphState(internalGraph); |
85 | 88 | refreshGraph(); |
86 | 89 | }); |
87 | 90 | renderer?.on('clickStage', () => { |
88 | | - treeChartState.selected = getHighlightedGraphState(graph); |
| 91 | + treeChartState.selected = getHighlightedGraphState(internalGraph); |
89 | 92 | refreshGraph(); |
90 | 93 | }); |
91 | 94 |
|
|
122 | 125 | } |
123 | 126 |
|
124 | 127 | onDestroy(() => { |
125 | | - treeGenerationWorker?.terminate(); |
126 | 128 | renderer?.kill(); |
| 129 | + treeGenerationWorkerWrapper?.terminate(); |
127 | 130 | }); |
128 | 131 |
|
129 | 132 | $effect(() => { |
130 | | - if (!treeGenerationWorker) { |
131 | | - setupWorker(); |
132 | | - } |
133 | | -
|
134 | | - if (bom && !graph) { |
135 | | - isLoading = true; |
136 | | - treeGenerationWorker?.postMessage({ |
137 | | - payload: bom |
138 | | - }); |
| 133 | + if (graph !== null && graph !== lastUnprocessedGraph) { |
| 134 | + lastUnprocessedGraph = graph; |
| 135 | + treeGenerationWorkerWrapper.sendMessage(graph.export()); |
139 | 136 | } |
140 | 137 |
|
141 | | - if (selectedComponentRef) { |
142 | | - treeChartState.selected = getHighlightedGraphState(graph, selectedComponentRef); |
143 | | - refreshGraph(); |
144 | | -
|
145 | | - const nodePosition = renderer?.getNodeDisplayData(selectedComponentRef) as Coordinates; |
146 | | - renderer?.getCamera().animate( |
147 | | - { ...nodePosition, ratio: 0.2 }, |
148 | | - { |
149 | | - duration: 500 |
150 | | - } |
151 | | - ); |
| 138 | + if (internalGraph) { |
| 139 | + if (selectedComponentRef) { |
| 140 | + treeChartState.selected = getHighlightedGraphState(internalGraph, selectedComponentRef); |
| 141 | + refreshGraph(); |
| 142 | +
|
| 143 | + const nodePosition = renderer?.getNodeDisplayData(selectedComponentRef) as Coordinates; |
| 144 | + renderer?.getCamera().animate( |
| 145 | + { ...nodePosition, ratio: 0.2 }, |
| 146 | + { |
| 147 | + duration: 500 |
| 148 | + } |
| 149 | + ); |
| 150 | + } |
152 | 151 | } |
153 | 152 | }); |
154 | 153 | </script> |
155 | 154 |
|
156 | | -<h2>Dependency Graph</h2> |
157 | | - |
158 | | -<p> |
159 | | - <em>Double click a node to locate it in the dependency table.</em> |
160 | | -</p> |
161 | | - |
162 | 155 | {#if isLoading} |
163 | 156 | <SkeletonPlaceholder style="min-height: 50rem; width: 100%;" /> |
164 | 157 | {/if} |
165 | | -<div class="chart-container" bind:this={container}></div> |
166 | | - |
167 | | -<style lang="scss"> |
168 | | - @use '@carbon/layout'; |
169 | | -
|
170 | | - h2 { |
171 | | - margin-bottom: layout.$spacing-05; |
172 | | - } |
173 | | -
|
174 | | - p { |
175 | | - margin-bottom: layout.$spacing-05; |
176 | | - } |
177 | | -
|
178 | | - .chart-container { |
179 | | - width: 100%; |
180 | | - min-height: 50rem; |
181 | | - } |
182 | | -</style> |
| 158 | +<div style="min-height: 50rem; width: 100%;" bind:this={container}></div> |
0 commit comments