Skip to content

Commit e3feb27

Browse files
committed
Show subchart of the currently selected component in the modal; separate graph generation and layout construction into separate web workers
1 parent 53e4bb5 commit e3feb27

File tree

7 files changed

+215
-91
lines changed

7 files changed

+215
-91
lines changed

src/lib/components/Bom.svelte

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,22 @@
22
import type { Bom, Component } from '$lib/cyclonedx/models';
33
import ComponentsTable from '$lib/components/ComponentsTable.svelte';
44
import { Button, Tab, TabContent, Tabs, Tile } from 'carbon-components-svelte';
5-
import ComponentsTreeChart from '$lib/components/ComponentsTreeChart.svelte';
65
import ComponentModal from '$lib/components/ComponentModal.svelte';
76
import { Document } from 'carbon-icons-svelte';
7+
import Graph from 'graphology';
8+
import type { AbstractGraph } from 'graphology-types';
9+
import { onDestroy } from 'svelte';
10+
import { TreeGenerationWorkerWrapper } from '$lib/worker/GraphWorkerWrapper';
11+
import GlobalComponentsTreeChart from '$lib/components/GlobalComponentsTreeChart.svelte';
812
913
let { bom = null }: { bom: Bom | null } = $props();
1014
15+
let graph: AbstractGraph | null = $state(null);
16+
17+
const treeGenerationWorkerWrapper = new TreeGenerationWorkerWrapper((g: Graph) => {
18+
graph = g;
19+
});
20+
1121
let selectedComponentForModal: Component | undefined = $state();
1222
let selectedComponentRefInTreeGraph: string | undefined = $state();
1323
let searchValueInTable: string = $state('');
@@ -30,7 +40,17 @@
3040
3141
let showGraph: boolean = $state(false);
3242
33-
$effect.pre(() => {
43+
onDestroy(() => {
44+
treeGenerationWorkerWrapper?.terminate();
45+
});
46+
47+
$effect(() => {
48+
if (bom) {
49+
treeGenerationWorkerWrapper.sendMessage(bom);
50+
}
51+
});
52+
53+
$effect(() => {
3454
if (selectedTabIndex === 1) {
3555
showGraph = true;
3656
}
@@ -75,8 +95,8 @@
7595
<TabContent>
7696
<div class="tab__tile">
7797
{#if showGraph}
78-
<ComponentsTreeChart
79-
{bom}
98+
<GlobalComponentsTreeChart
99+
{graph}
80100
selectedComponentRef={selectedComponentRefInTreeGraph}
81101
searchForComponent={searchForComponentInTable}
82102
/>
@@ -89,7 +109,7 @@
89109
{/if}
90110
{/if}
91111

92-
<ComponentModal component={selectedComponentForModal} />
112+
<ComponentModal component={selectedComponentForModal} {graph} />
93113

94114
<style lang="scss">
95115
@use '@carbon/layout';

src/lib/components/ComponentModal.svelte

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,31 @@
33
import ComponentInfo from '$lib/components/ComponentInfo.svelte';
44
import { CodeSnippet, Modal } from 'carbon-components-svelte';
55
import { ComponentModalContent } from '$lib/components/ComponentModalContent';
6+
import type { AbstractGraph } from 'graphology-types';
7+
import { allSimplePaths } from 'graphology-simple-path';
8+
import { subgraph as generateSubgraph } from 'graphology-operators';
9+
import ComponentsTreeChart from '$lib/components/ComponentsTreeChart.svelte';
10+
import { getGraphRootNode } from '$lib/graphs/utils';
611
7-
let { component }: { component?: Component } = $props();
12+
let { component, graph }: { component?: Component; graph?: AbstractGraph | null } = $props();
813
914
let componentModalContent = $derived(new ComponentModalContent(component));
1015
let componentModelIsOpen: boolean = $state(false);
1116
17+
let subgraph: AbstractGraph | null | undefined = $derived.by(() => {
18+
if (graph && component) {
19+
const root = getGraphRootNode(graph);
20+
const paths = allSimplePaths(graph, root, component['bom-ref']);
21+
if (paths && paths.length > 0) {
22+
const interestingNodes = paths.reduce((p, c) => p.union(new Set(c)), new Set<string>());
23+
return generateSubgraph(graph, interestingNodes);
24+
}
25+
console.warn(`No useful subgraph found: ${paths.join(',')}`);
26+
27+
return null;
28+
}
29+
});
30+
1231
$effect(() => {
1332
if (component) {
1433
componentModelIsOpen = true;
@@ -19,6 +38,11 @@
1938
<Modal bind:open={componentModelIsOpen} passiveModal modalHeading={componentModalContent.heading}>
2039
{#if componentModalContent.component}
2140
<ComponentInfo component={componentModalContent.component} />
41+
{#if !!subgraph}
42+
<h4>Dependency Graph</h4>
43+
<ComponentsTreeChart graph={subgraph} />
44+
{/if}
45+
<h4>Component JSON</h4>
2246
<CodeSnippet type="multi" code={componentModalContent.code} expanded />
2347
{/if}
2448
</Modal>
Lines changed: 57 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,48 @@
11
<script lang="ts">
22
import { getContext, onDestroy } from 'svelte';
33
import Sigma from 'sigma';
4-
import type { Bom } from '$lib/cyclonedx/models';
54
import type { Coordinates, EdgeDisplayData, NodeDisplayData } from 'sigma/types';
6-
import type { AbstractGraph, SerializedGraph } from 'graphology-types';
5+
import type { AbstractGraph } from 'graphology-types';
76
import { borderSubtle01, borderStrong01, tagTokens, textDisabled } from '@carbon/themes';
87
import type { TreeChartState } from '$lib/models/treechart';
9-
import type { PostMessage } from '$lib/models/worker';
10-
import Graph from 'graphology';
118
import { SkeletonPlaceholder } from 'carbon-components-svelte';
129
import {
1310
getHighlightedGraphState,
1411
isInterestingEdge,
1512
isInterestingNode
1613
} from '$lib/graphs/highlighting';
14+
import { LayoutGraphWorkerWrapper } from '$lib/worker/GraphWorkerWrapper';
15+
import Graph from 'graphology';
1716
1817
let {
19-
bom,
18+
graph,
2019
selectedComponentRef,
2120
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();
2426
const theme: () => string = getContext('theme');
2527
28+
let internalGraph: Graph | undefined = $state();
29+
let lastUnprocessedGraph: Graph | undefined = $state();
30+
2631
let container: HTMLDivElement;
2732
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);
3040
3141
const treeChartState: TreeChartState = {
3242
hovered: {},
3343
selected: {}
3444
};
3545
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-
5246
function refreshGraph() {
5347
// Refresh rendering
5448
renderer?.refresh({
@@ -57,35 +51,44 @@
5751
});
5852
}
5953
60-
function setupGraph(localGraph: Graph) {
61-
graph = localGraph;
54+
function setupGraph() {
55+
if (!internalGraph) {
56+
return;
57+
}
6258
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, {
6567
defaultEdgeType: 'arrow',
6668
defaultNodeColor: tagTokens.tagTokens.tagColorBlue[theme()],
6769
defaultEdgeColor: borderSubtle01
6870
});
69-
isLoading = false;
7071
71-
// Bind graph interactions:
72+
// Bind internalGraph interactions:
7273
renderer?.on('enterNode', ({ node }) => {
73-
treeChartState.hovered = getHighlightedGraphState(graph, node);
74+
treeChartState.hovered = getHighlightedGraphState(internalGraph, node);
7475
refreshGraph();
7576
});
7677
renderer?.on('clickNode', ({ node }) => {
77-
treeChartState.selected = getHighlightedGraphState(graph, node);
78+
treeChartState.selected = getHighlightedGraphState(internalGraph, node);
7879
refreshGraph();
7980
});
8081
renderer?.on('doubleClickNode', ({ node }) => {
81-
searchForComponent(node);
82+
if (searchForComponent) {
83+
searchForComponent(node);
84+
}
8285
});
8386
renderer?.on('leaveNode', () => {
84-
treeChartState.hovered = getHighlightedGraphState(graph);
87+
treeChartState.hovered = getHighlightedGraphState(internalGraph);
8588
refreshGraph();
8689
});
8790
renderer?.on('clickStage', () => {
88-
treeChartState.selected = getHighlightedGraphState(graph);
91+
treeChartState.selected = getHighlightedGraphState(internalGraph);
8992
refreshGraph();
9093
});
9194
@@ -122,61 +125,34 @@
122125
}
123126
124127
onDestroy(() => {
125-
treeGenerationWorker?.terminate();
126128
renderer?.kill();
129+
treeGenerationWorkerWrapper?.terminate();
127130
});
128131
129132
$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());
139136
}
140137
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+
}
152151
}
153152
});
154153
</script>
155154

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-
162155
{#if isLoading}
163156
<SkeletonPlaceholder style="min-height: 50rem; width: 100%;" />
164157
{/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>
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<script lang="ts">
2+
import type { AbstractGraph } from 'graphology-types';
3+
import ComponentsTreeChart from '$lib/components/ComponentsTreeChart.svelte';
4+
5+
let {
6+
graph,
7+
selectedComponentRef,
8+
searchForComponent
9+
}: {
10+
graph: AbstractGraph | null;
11+
selectedComponentRef?: string;
12+
searchForComponent: (ref: string) => void;
13+
} = $props();
14+
</script>
15+
16+
<h2>Dependency Graph</h2>
17+
18+
<p>
19+
<em>Double click a node to locate it in the dependency table.</em>
20+
</p>
21+
22+
<ComponentsTreeChart {graph} {selectedComponentRef} {searchForComponent} />
23+
24+
<style lang="scss">
25+
@use '@carbon/layout';
26+
27+
h2 {
28+
margin-bottom: layout.$spacing-05;
29+
}
30+
31+
p {
32+
margin-bottom: layout.$spacing-05;
33+
}
34+
</style>

src/lib/transformations/graph.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,17 +45,14 @@ export function createGraphFromBom(bom: Bom): Graph {
4545
addNodes(graph, bom.components);
4646
addEdges(graph, bom.dependencies);
4747

48-
// Use dagre to compute the layout
49-
computeLayoutWithDagre(graph);
50-
5148
return graph;
5249
}
5350

5451
/**
5552
* Function to compute the layout using dagre and assign positions to graph nodes
5653
* @param graph - The graphology graph
5754
*/
58-
function computeLayoutWithDagre(graph: Graph) {
55+
export function computeLayoutWithDagre(graph: Graph): Graph {
5956
// Create a new directed graph for dagre
6057
const g = new dagre.graphlib.Graph();
6158

@@ -94,4 +91,5 @@ function computeLayoutWithDagre(graph: Graph) {
9491
y: nodeData.y
9592
});
9693
});
94+
return graph;
9795
}

0 commit comments

Comments
 (0)