Skip to content

Commit d1d1307

Browse files
committed
Use the same data structure for the tree view and for the tree chart
1 parent 9a1a9a5 commit d1d1307

File tree

7 files changed

+159
-140
lines changed

7 files changed

+159
-140
lines changed

src/lib/components/Bom.svelte

Lines changed: 61 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
import ComponentsTable from '$lib/components/ComponentsTable.svelte';
44
import ComponentsTreeView from '$lib/components/ComponentsTreeView.svelte';
55
import { Button, Tab, TabContent, Tabs, Tile } from 'carbon-components-svelte';
6-
import ComponentsTree from '$lib/components/ComponentsTree.svelte';
6+
import ComponentsTreeChart from '$lib/components/ComponentsTreeChart.svelte';
77
import ComponentModal from '$lib/components/ComponentModal.svelte';
88
import { Document } from 'carbon-icons-svelte';
9+
import { createTreeDataFromBom, type TreeItem } from '$lib/tree';
910
1011
let { bom = null }: { bom: Bom | null } = $props();
1112
@@ -16,43 +17,64 @@
1617
function searchComponentInTreeView(id: string) {
1718
selectedComponentRefInTreeView = id;
1819
}
20+
21+
let treeData: TreeItem[] | undefined = $derived.by(() => {
22+
if (bom) {
23+
return createTreeDataFromBom(bom);
24+
}
25+
});
1926
</script>
2027

2128
{#if bom}
2229
{#if bom.metadata?.component}
23-
<Tile>
24-
<div class="tile__content">
25-
<p>
26-
SBOM for <strong>{bom.metadata.component.name}</strong>
27-
</p>
28-
<Button
29-
size="small"
30-
icon={Document}
31-
iconDescription="Show details"
32-
kind="primary"
33-
on:click={() => (subjectComponentModalOpened = true)}
34-
/>
35-
</div>
36-
</Tile>
30+
<section class="tile">
31+
<Tile>
32+
<div class="tile__content">
33+
<p>
34+
SBOM for <strong>{bom.metadata.component.name}</strong>
35+
</p>
36+
<Button
37+
size="small"
38+
icon={Document}
39+
iconDescription="Show details"
40+
kind="primary"
41+
on:click={() => (subjectComponentModalOpened = true)}
42+
/>
43+
</div>
44+
</Tile>
45+
</section>
3746
<ComponentModal component={subjectComponentModalOpened ? bom.metadata.component : undefined} />
3847
{/if}
3948
{#if bom.components}
40-
<Tabs>
41-
<Tab label="Table" />
42-
<Tab label="Diagram" />
43-
<svelte:fragment slot="content">
44-
<TabContent>
45-
<ComponentsTreeView {bom} selectedComponentRef={selectedComponentRefInTreeView} />
46-
<ComponentsTable
47-
components={bom.components}
48-
searchComponent={searchComponentInTreeView}
49-
/>
50-
</TabContent>
51-
<TabContent>
52-
<ComponentsTree {bom} />
53-
</TabContent>
54-
</svelte:fragment>
55-
</Tabs>
49+
<section class="tabs">
50+
<Tabs>
51+
<Tab label="Table" />
52+
<Tab label="Chart" />
53+
<svelte:fragment slot="content">
54+
<TabContent>
55+
{#if treeData}
56+
<div class="treeview">
57+
<ComponentsTreeView
58+
nodes={treeData}
59+
selectedComponentRef={selectedComponentRefInTreeView}
60+
/>
61+
</div>
62+
{/if}
63+
<div class="table">
64+
<ComponentsTable
65+
components={bom.components}
66+
searchComponent={searchComponentInTreeView}
67+
/>
68+
</div>
69+
</TabContent>
70+
<TabContent>
71+
{#if treeData}
72+
<ComponentsTreeChart {bom} nodes={treeData} />
73+
{/if}
74+
</TabContent>
75+
</svelte:fragment>
76+
</Tabs>
77+
</section>
5678
{/if}
5779
{/if}
5880

@@ -64,4 +86,12 @@
6486
align-items: center;
6587
gap: layout.$spacing-03;
6688
}
89+
90+
.tile {
91+
margin-bottom: layout.$spacing-07;
92+
}
93+
94+
.treeview {
95+
margin-bottom: layout.$spacing-07;
96+
}
6797
</style>

src/lib/components/ComponentsTree.svelte

Lines changed: 0 additions & 26 deletions
This file was deleted.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<script lang="ts">
2+
import { TreeChart, type TreeChartOptions } from '@carbon/charts-svelte';
3+
import '@carbon/charts-svelte/styles.css';
4+
import type { TreeChartItem } from '$lib/tree';
5+
import type { Bom } from '$lib/cyclonedx/models';
6+
7+
let { bom, nodes = [] }: { bom: Bom; nodes: TreeChartItem[] } = $props();
8+
9+
let treeOptions: TreeChartOptions = $state({});
10+
11+
$effect.pre(() => {
12+
treeOptions = {
13+
title: 'Dependency Tree',
14+
height: `${(bom.components?.length ?? 10) * 15}px`,
15+
tree: {
16+
rootTitle: bom.metadata?.component?.name
17+
}
18+
};
19+
});
20+
</script>
21+
22+
<TreeChart data={nodes} options={treeOptions} />

src/lib/components/ComponentsTreeView.svelte

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,11 @@
11
<script lang="ts">
2-
import type { Bom } from '$lib/cyclonedx/models';
32
import '@carbon/charts-svelte/styles.css';
43
import { Button, ButtonSet, TreeView } from 'carbon-components-svelte';
5-
import { createTreeNodesFromBom } from '$lib/treeview';
64
import type { TreeNode } from 'carbon-components-svelte/src/TreeView/TreeView.svelte';
75
import { CollapseAll, ExpandAll } from 'carbon-icons-svelte';
86
9-
let { bom, selectedComponentRef }: { bom: Bom; selectedComponentRef?: string } = $props();
10-
11-
let nodes: TreeNode[] = $derived.by(() => {
12-
return createTreeNodesFromBom(bom);
13-
});
7+
let { nodes, selectedComponentRef }: { nodes: TreeNode[]; selectedComponentRef?: string } =
8+
$props();
149
1510
let treeview: TreeView | null = null;
1611

src/lib/cyclonedx/parse.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import type { Bom } from './models';
22

3-
export function parseJson(dataString: string): Bom {
4-
return JSON.parse(dataString) as Bom;
3+
export function parseJson(dataString: string): Bom | null {
4+
try {
5+
return JSON.parse(dataString) as Bom;
6+
} catch (e) {
7+
console.error(`BOM could not be parsed: ${e}`);
8+
}
9+
return null;
510
}

src/lib/tree.ts

Lines changed: 67 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,69 @@
1-
import type { ChartTabularData } from '@carbon/charts-svelte';
2-
import type { Bom, Component } from '$lib/cyclonedx/models';
1+
import type { Bom } from '$lib/cyclonedx/models';
2+
import type { TreeNode } from 'carbon-components-svelte/src/TreeView/TreeView.svelte';
33

4-
interface TreeItem {
4+
// Used for the tree chart
5+
export interface TreeChartItem {
56
name: string;
67
value?: string;
7-
children?: TreeItem[];
8+
children?: TreeChartItem[];
89
}
910

10-
export function createTreeFromBom(bom: Bom): ChartTabularData {
11-
const componentMap = new Map<string, Component>();
11+
// The combined type that can be used for the TreeView and the TreeChart
12+
export type TreeItem = TreeChartItem & TreeNode;
13+
14+
class TreeItemImpl implements TreeItem {
15+
private readonly _name: string;
16+
private readonly _ref: string;
17+
private readonly _children: TreeItem[] | undefined;
18+
19+
constructor(name: string, ref: string, children?: TreeItem[]) {
20+
this._name = name;
21+
this._ref = ref;
22+
this._children = children && children.length > 0 ? children : undefined;
23+
}
24+
25+
get name() {
26+
return this._name;
27+
}
28+
29+
get text() {
30+
return this._name;
31+
}
32+
33+
get id() {
34+
return this._ref;
35+
}
36+
37+
get value() {
38+
return this._ref;
39+
}
40+
41+
get children() {
42+
return this._children;
43+
}
44+
45+
get nodes() {
46+
return this._children;
47+
}
48+
49+
get icon() {
50+
return undefined;
51+
}
52+
53+
get disabled() {
54+
return undefined;
55+
}
56+
}
57+
58+
export function createTreeDataFromBom(bom: Bom): TreeItem[] {
59+
const componentRefToName = new Map<string, string>();
1260
const dependencyMap = new Map<string, string[]>();
1361

14-
const data: ChartTabularData = [];
62+
const data: TreeItem[] = [];
1563

1664
for (const component of bom.components ?? []) {
1765
if (component['bom-ref']) {
18-
componentMap.set(component['bom-ref'], component);
66+
componentRefToName.set(component['bom-ref'], component.name);
1967
}
2068
}
2169

@@ -25,29 +73,27 @@ export function createTreeFromBom(bom: Bom): ChartTabularData {
2573

2674
const subject = bom.metadata?.component;
2775

28-
function getChildren(component: Component) {
29-
return (component['bom-ref'] ? (dependencyMap.get(component['bom-ref']) ?? []) : [])
76+
function getChildTreeItems(componentRef: string) {
77+
return (dependencyMap.get(componentRef) ?? [])
3078
.map((child) => {
31-
const childComponent = componentMap.get(child);
32-
if (childComponent) {
33-
return getTreeItem(childComponent);
79+
if (componentRefToName.has(child)) {
80+
return getTreeItem(child, componentRefToName.get(child)!);
3481
} else {
3582
return null;
3683
}
3784
})
3885
.filter((child) => child !== null);
3986
}
4087

41-
const getTreeItem: (component: Component) => TreeItem = (component: Component) => {
42-
return {
43-
name: component.name,
44-
value: component['bom-ref'] ?? null,
45-
children: getChildren(component)
46-
} as TreeItem;
88+
const getTreeItem: (componentRef: string, componentName: string) => TreeItem = (
89+
componentRef,
90+
componentName
91+
) => {
92+
return new TreeItemImpl(componentName, componentRef, getChildTreeItems(componentRef));
4793
};
4894

49-
if (subject) {
50-
data.push(...getChildren(subject));
95+
if (subject && subject['bom-ref']) {
96+
data.push(...getChildTreeItems(subject['bom-ref']));
5197
} else {
5298
console.error(`No subject found in ${bom.metadata}`);
5399
}

src/lib/treeview.ts

Lines changed: 0 additions & 53 deletions
This file was deleted.

0 commit comments

Comments
 (0)