From ebe06f812fe8519a282e239d508168586249a976 Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Tue, 24 Oct 2023 17:49:22 +0200 Subject: [PATCH 01/18] Early graph stuff --- package-lock.json | 75 +++++++++++++++++++ package.json | 6 +- src/lib/components/DiscoveryGraph.svelte | 32 ++++++++ src/lib/components/GraphEdge.svelte | 14 ++++ src/lib/components/GraphNode.svelte | 14 ++++ .../(app)/cloud/[id]/resources/+page.svelte | 60 +++++++++++---- 6 files changed, 187 insertions(+), 14 deletions(-) create mode 100644 src/lib/components/DiscoveryGraph.svelte create mode 100644 src/lib/components/GraphEdge.svelte create mode 100644 src/lib/components/GraphNode.svelte diff --git a/package-lock.json b/package-lock.json index 7c9db3c..d9ac7d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,10 +14,14 @@ "@sveltejs/adapter-node": "^1.3.1", "@sveltejs/kit": "^1.27.0", "@tailwindcss/forms": "^0.5.3", + "@types/cytoscape": "^3.19.13", + "@types/cytoscape-dagre": "^2.3.2", "@typescript-eslint/eslint-plugin": "^5.45.0", "@typescript-eslint/parser": "^5.45.0", "autoprefixer": "^10.4.14", "chart.js": "^4.4.0", + "cytoscape": "^3.26.0", + "cytoscape-dagre": "^2.5.0", "eslint": "^8.28.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-svelte": "^2.34.0", @@ -878,6 +882,21 @@ "integrity": "sha512-COUnqfB2+ckwXXSFInsFdOAWQzCCx+a5hq2ruyj+Vjund94RJQd4LG2u9hnvJrTgunKAaax7ancBYlDrNYxA0g==", "dev": true }, + "node_modules/@types/cytoscape": { + "version": "3.19.13", + "resolved": "https://registry.npmjs.org/@types/cytoscape/-/cytoscape-3.19.13.tgz", + "integrity": "sha512-FZY6Zyh8kGqEwZ6jizCJa5rbKriOWqJaEoGR8AG2LNQyo5sy1q7hFNo8ojbRpHNBluBmCJa+/w//OWaehxolgA==", + "dev": true + }, + "node_modules/@types/cytoscape-dagre": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@types/cytoscape-dagre/-/cytoscape-dagre-2.3.2.tgz", + "integrity": "sha512-EvEfDqoUVoGpra4MuA5REJx9sbjfQlPndMZeR0uswmpdRw5s8tXGzBZrhMejgqL69NGshgFYZkt8E29yW5zgdg==", + "dev": true, + "dependencies": { + "@types/cytoscape": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.3.tgz", @@ -1505,6 +1524,41 @@ "node": ">=4" } }, + "node_modules/cytoscape": { + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.26.0.tgz", + "integrity": "sha512-IV+crL+KBcrCnVVUCZW+zRRRFUZQcrtdOPXki+o4CFUWLdAEYvuZLcBSJC9EBK++suamERKzeY7roq2hdovV3w==", + "dev": true, + "dependencies": { + "heap": "^0.2.6", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cytoscape-dagre": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/cytoscape-dagre/-/cytoscape-dagre-2.5.0.tgz", + "integrity": "sha512-VG2Knemmshop4kh5fpLO27rYcyUaaDkRw+6PiX4bstpB+QFt0p2oauMrsjVbUamGWQ6YNavh7x2em2uZlzV44g==", + "dev": true, + "dependencies": { + "dagre": "^0.8.5" + }, + "peerDependencies": { + "cytoscape": "^3.2.22" + } + }, + "node_modules/dagre": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz", + "integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==", + "dev": true, + "dependencies": { + "graphlib": "^2.1.8", + "lodash": "^4.17.15" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -2149,6 +2203,15 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "node_modules/graphlib": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz", + "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==", + "dev": true, + "dependencies": { + "lodash": "^4.17.15" + } + }, "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -2170,6 +2233,12 @@ "node": ">=8" } }, + "node_modules/heap": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/heap/-/heap-0.2.7.tgz", + "integrity": "sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==", + "dev": true + }, "node_modules/ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -2422,6 +2491,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", diff --git a/package.json b/package.json index 3f26d70..ee9702a 100644 --- a/package.json +++ b/package.json @@ -18,10 +18,14 @@ "@sveltejs/adapter-node": "^1.3.1", "@sveltejs/kit": "^1.27.0", "@tailwindcss/forms": "^0.5.3", + "@types/cytoscape": "^3.19.13", + "@types/cytoscape-dagre": "^2.3.2", "@typescript-eslint/eslint-plugin": "^5.45.0", "@typescript-eslint/parser": "^5.45.0", "autoprefixer": "^10.4.14", "chart.js": "^4.4.0", + "cytoscape": "^3.26.0", + "cytoscape-dagre": "^2.5.0", "eslint": "^8.28.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-svelte": "^2.34.0", @@ -39,4 +43,4 @@ "vite": "^4.5.0" }, "type": "module" -} \ No newline at end of file +} diff --git a/src/lib/components/DiscoveryGraph.svelte b/src/lib/components/DiscoveryGraph.svelte new file mode 100644 index 0000000..1913462 --- /dev/null +++ b/src/lib/components/DiscoveryGraph.svelte @@ -0,0 +1,32 @@ + + +
+ {#if cy} + + {/if} +
diff --git a/src/lib/components/GraphEdge.svelte b/src/lib/components/GraphEdge.svelte new file mode 100644 index 0000000..c4b6e9c --- /dev/null +++ b/src/lib/components/GraphEdge.svelte @@ -0,0 +1,14 @@ + diff --git a/src/lib/components/GraphNode.svelte b/src/lib/components/GraphNode.svelte new file mode 100644 index 0000000..922601d --- /dev/null +++ b/src/lib/components/GraphNode.svelte @@ -0,0 +1,14 @@ + diff --git a/src/routes/(app)/cloud/[id]/resources/+page.svelte b/src/routes/(app)/cloud/[id]/resources/+page.svelte index db58c67..ca8252e 100644 --- a/src/routes/(app)/cloud/[id]/resources/+page.svelte +++ b/src/routes/(app)/cloud/[id]/resources/+page.svelte @@ -1,24 +1,24 @@ + + {#each nodes as node} + + {/each} + + {#each edges as edge} + + {/each} + + {#if data.resources.length == 0} From 23c621dab50fb428694ad1c7b7f994115d2e2311 Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Wed, 25 Oct 2023 09:58:09 +0200 Subject: [PATCH 02/18] Very early graph prototype --- src/lib/api/discovery.ts | 29 +++++++++++++++++-- src/lib/components/GraphEdge.svelte | 14 +++++---- .../(app)/cloud/[id]/resources/+page.svelte | 28 ++---------------- .../(app)/cloud/[id]/resources/+page.ts | 3 ++ 4 files changed, 41 insertions(+), 33 deletions(-) diff --git a/src/lib/api/discovery.ts b/src/lib/api/discovery.ts index bdbe0d1..2df80d6 100644 --- a/src/lib/api/discovery.ts +++ b/src/lib/api/discovery.ts @@ -1,5 +1,12 @@ import { throwError } from './errors'; import { clouditorize } from './util'; + +export interface GraphEdge { + id: string + source: string + target: string +} + export interface StartDiscoveryResponse { successful: boolean } @@ -8,6 +15,10 @@ export interface QueryResponse { results: Resource[] } +export interface ListGraphEdgesResponse { + edges: GraphEdge[] +} + export interface ListResourcesRequest { filter?: Filter orderBy?: string @@ -70,8 +81,6 @@ export async function listResources( apiUrl += `&filter.type=${type}` } - const emptyResource: Resource[] = []; - return fetch(apiUrl, { method: 'GET', headers: { @@ -82,4 +91,20 @@ export async function listResources( .then((response: QueryResponse) => { return response.results; }); +} + + +export async function listGraphEdges(fetch = window.fetch): Promise { + const apiUrl = clouditorize(`/v1/discovery/graph/edges`); + + return fetch(apiUrl, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${localStorage.token}`, + } + }).then(throwError) + .then((res) => res.json()) + .then((response: ListGraphEdgesResponse) => { + return response.edges; + }); } \ No newline at end of file diff --git a/src/lib/components/GraphEdge.svelte b/src/lib/components/GraphEdge.svelte index c4b6e9c..73004c5 100644 --- a/src/lib/components/GraphEdge.svelte +++ b/src/lib/components/GraphEdge.svelte @@ -6,9 +6,13 @@ const { getCyInstance } = getContext('graphSharedState'); const cy = getCyInstance(); - cy.add({ - group: 'edges', - id: edge.id, - data: { ...edge } - }); + try { + cy.add({ + group: 'edges', + id: edge.id, + data: { ...edge } + }); + } catch (err) { + console.log(err); + } diff --git a/src/routes/(app)/cloud/[id]/resources/+page.svelte b/src/routes/(app)/cloud/[id]/resources/+page.svelte index ca8252e..693fa83 100644 --- a/src/routes/(app)/cloud/[id]/resources/+page.svelte +++ b/src/routes/(app)/cloud/[id]/resources/+page.svelte @@ -140,38 +140,14 @@ const results = data.results.filter((result) => result.resourceId === resourceId); return results.length; } - - const nodes = [ - { id: 'N1', label: 'Start' }, - { id: 'N2', label: '4' }, - { id: 'N4', label: '8' }, - { id: 'N5', label: '15' }, - { id: 'N3', label: '16' }, - { id: 'N6', label: '23' }, - { id: 'N7', label: '42' }, - { id: 'N8', label: 'End' } - ]; - - const edges = [ - /*{ id: 'E1', source: 'N1', target: 'N2' }, - { id: 'E2', source: 'N2', target: 'N3' }, - { id: 'E3', source: 'N3', target: 'N6' }, - { id: 'E4', source: 'N2', target: 'N4' }, - { id: 'E5', source: 'N4', target: 'N5' }, - { id: 'E6', source: 'N5', target: 'N4', label: '2' }, - { id: 'E7', source: 'N5', target: 'N6' }, - { id: 'E8', source: 'N6', target: 'N7' }, - { id: 'E9', source: 'N7', target: 'N7', label: '3' }, - { id: 'E10', source: 'N7', target: 'N8' }*/ - ]; - {#each nodes as node} + {#each data.resources as node} {/each} - {#each edges as edge} + {#each data.edges as edge} {/each} diff --git a/src/routes/(app)/cloud/[id]/resources/+page.ts b/src/routes/(app)/cloud/[id]/resources/+page.ts index 9a6d414..bcafe19 100644 --- a/src/routes/(app)/cloud/[id]/resources/+page.ts +++ b/src/routes/(app)/cloud/[id]/resources/+page.ts @@ -3,6 +3,7 @@ import { listCloudServiceAssessmentResults } from '$lib/api/orchestrator'; import { error } from '@sveltejs/kit'; import type { PageLoad } from './$types'; +import { listGraphEdges } from '$lib/api/discovery'; export const load = (async ({ fetch, params, url }) => { if (params.id == undefined) { @@ -10,10 +11,12 @@ export const load = (async ({ fetch, params, url }) => { } const results = await listCloudServiceAssessmentResults(params.id, fetch); + const edges = await listGraphEdges() const page = Number(url.searchParams.get('page')); return { results, + edges, page }; }) satisfies PageLoad; From 23891edaa4f8752a482cce9032c737909fc4fd0b Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Wed, 25 Oct 2023 11:59:13 +0200 Subject: [PATCH 03/18] Very basic graph --- src/lib/api/discovery.ts | 2 +- src/lib/components/DiscoveryGraph.svelte | 21 ++++++++++++++++++- .../(app)/cloud/[id]/assessments/+page.svelte | 10 +++++---- .../(app)/cloud/[id]/resources/+page.svelte | 9 +++++++- 4 files changed, 35 insertions(+), 7 deletions(-) diff --git a/src/lib/api/discovery.ts b/src/lib/api/discovery.ts index 2df80d6..87c1100 100644 --- a/src/lib/api/discovery.ts +++ b/src/lib/api/discovery.ts @@ -95,7 +95,7 @@ export async function listResources( export async function listGraphEdges(fetch = window.fetch): Promise { - const apiUrl = clouditorize(`/v1/discovery/graph/edges`); + const apiUrl = clouditorize(`/v1/discovery/graph/edges?&pageSize=1500`); return fetch(apiUrl, { method: 'GET', diff --git a/src/lib/components/DiscoveryGraph.svelte b/src/lib/components/DiscoveryGraph.svelte index 1913462..a54658c 100644 --- a/src/lib/components/DiscoveryGraph.svelte +++ b/src/lib/components/DiscoveryGraph.svelte @@ -14,7 +14,26 @@ cytoscape.use(dagre); cy = cytoscape({ - container: graph + container: graph, + style: [ + { + selector: 'node', + style: { + width: '50', + height: '50', + 'font-size': '18', + 'font-weight': 'bold', + content: `data(label)`, + 'text-valign': 'center', + 'text-wrap': 'wrap', + 'text-max-width': '140', + 'background-color': 'gold', + 'border-color': 'orange', + 'border-width': '3', + color: 'darkred' + } + } + ] }); cy.on('add', () => { diff --git a/src/routes/(app)/cloud/[id]/assessments/+page.svelte b/src/routes/(app)/cloud/[id]/assessments/+page.svelte index 5c44060..601292f 100644 --- a/src/routes/(app)/cloud/[id]/assessments/+page.svelte +++ b/src/routes/(app)/cloud/[id]/assessments/+page.svelte @@ -20,10 +20,12 @@ ? data.results : data.results.filter((result) => { return ( - (data.filterIds === undefined || data.filterIds?.includes(result.id)) && - (data.filterResourceId === undefined || - result.resourceId.split('/')[result.resourceId.split('/').length - 1] === - data.filterResourceId) + data.filterIds === undefined || + (data.filterIds?.includes(result.id) && + (data.filterResourceId === null || + data.filterResourceId === undefined || + result.resourceId.split('/')[result.resourceId.split('/').length - 1] === + data.filterResourceId)) ); }); diff --git a/src/routes/(app)/cloud/[id]/resources/+page.svelte b/src/routes/(app)/cloud/[id]/resources/+page.svelte index 693fa83..ac97450 100644 --- a/src/routes/(app)/cloud/[id]/resources/+page.svelte +++ b/src/routes/(app)/cloud/[id]/resources/+page.svelte @@ -140,10 +140,17 @@ const results = data.results.filter((result) => result.resourceId === resourceId); return results.length; } + + $: nodes = data.resources.map((n) => { + return { + id: n.id, + label: n.properties.name + }; + }); - {#each data.resources as node} + {#each nodes as node} {/each} From ac90e456f1b1a91bad9f6ca1a92c888b3c254f1e Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Thu, 26 Oct 2023 09:56:02 +0200 Subject: [PATCH 04/18] ++ --- package-lock.json | 138 ++++++++++++++++++ package.json | 5 + src/lib/components/DiscoveryGraph.svelte | 73 +++++---- .../(app)/cloud/[id]/resources/+page.svelte | 26 ++-- 4 files changed, 204 insertions(+), 38 deletions(-) diff --git a/package-lock.json b/package-lock.json index d9ac7d9..3fbd00b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,12 +16,17 @@ "@tailwindcss/forms": "^0.5.3", "@types/cytoscape": "^3.19.13", "@types/cytoscape-dagre": "^2.3.2", + "@types/cytoscape-euler": "^1.2.2", "@typescript-eslint/eslint-plugin": "^5.45.0", "@typescript-eslint/parser": "^5.45.0", "autoprefixer": "^10.4.14", "chart.js": "^4.4.0", "cytoscape": "^3.26.0", + "cytoscape-cola": "^2.5.1", + "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-dagre": "^2.5.0", + "cytoscape-euler": "^1.2.2", + "cytoscape-spread": "^3.0.0", "eslint": "^8.28.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-svelte": "^2.34.0", @@ -897,6 +902,15 @@ "@types/cytoscape": "*" } }, + "node_modules/@types/cytoscape-euler": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@types/cytoscape-euler/-/cytoscape-euler-1.2.2.tgz", + "integrity": "sha512-V/Ze9TADokvTwJLDS8jqkNfRwKDHIDQB7e/2VJw30Fk5Pr6POsnm6Low1BKGdblAeziqIZnZgJy7MEyvCsx5KQ==", + "dev": true, + "dependencies": { + "@types/cytoscape": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.3.tgz", @@ -1492,6 +1506,15 @@ "node": ">= 0.6" } }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "dev": true, + "dependencies": { + "layout-base": "^1.0.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -1537,6 +1560,30 @@ "node": ">=0.10" } }, + "node_modules/cytoscape-cola": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/cytoscape-cola/-/cytoscape-cola-2.5.1.tgz", + "integrity": "sha512-4/2S9bW1LvdsEPmxXN1OEAPFPbk7DvCx2c9d+TblkQAAvptGaSgtPWCByTEGgT8UxCxcVqes2aFPO5pzwo7R2w==", + "dev": true, + "dependencies": { + "webcola": "^3.4.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "dev": true, + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, "node_modules/cytoscape-dagre": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/cytoscape-dagre/-/cytoscape-dagre-2.5.0.tgz", @@ -1549,6 +1596,70 @@ "cytoscape": "^3.2.22" } }, + "node_modules/cytoscape-euler": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cytoscape-euler/-/cytoscape-euler-1.2.2.tgz", + "integrity": "sha512-A24ZGrFpqCOutTIlGoXA5kmjFj68iy7HvqXuhcZUL1a7Z8bL59Bl2bB7hkSvFcCZBVCNcArxKr+YlB8bJo9Ftw==", + "dev": true, + "peerDependencies": { + "cytoscape": "^3.0.0" + } + }, + "node_modules/cytoscape-spread": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cytoscape-spread/-/cytoscape-spread-3.0.0.tgz", + "integrity": "sha512-ekuo4ByFRTZ4TOJylE2bPOMcVVyi8rD+qjvEjMWS2BHcyan40pmhlA4ramz/nTxZR+EtlxEa1asnmfiN8R5HyQ==", + "dev": true, + "dependencies": { + "weaverjs": "^1.2.0" + }, + "peerDependencies": { + "cytoscape": "^3.0.0" + } + }, + "node_modules/d3-dispatch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.6.tgz", + "integrity": "sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==", + "dev": true + }, + "node_modules/d3-drag": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-1.2.5.tgz", + "integrity": "sha512-rD1ohlkKQwMZYkQlYVCrSFxsWPzI97+W+PaEIBNTMxRuxz9RF0Hi5nJWHGVJ3Om9d2fRTe1yOBINJyy/ahV95w==", + "dev": true, + "dependencies": { + "d3-dispatch": "1", + "d3-selection": "1" + } + }, + "node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "dev": true + }, + "node_modules/d3-selection": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-1.4.2.tgz", + "integrity": "sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg==", + "dev": true + }, + "node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "dev": true, + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-timer": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.10.tgz", + "integrity": "sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==", + "dev": true + }, "node_modules/dagre": { "version": "0.8.5", "resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz", @@ -2448,6 +2559,12 @@ "integrity": "sha512-9pSL5XB4J+ifHP0e0jmmC98OGC1nL8/JjS+fi6mnTlIf//yt/MfVLtKg7S6nCtj/8KTcWX7nRlY0XywoYY1ISQ==", "dev": true }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", + "dev": true + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -3948,6 +4065,27 @@ } } }, + "node_modules/weaverjs": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/weaverjs/-/weaverjs-1.2.0.tgz", + "integrity": "sha512-X+nDGl5mrc8ysArmafu6dD3GNFP2r+NdV6L/PiWac8TpH4BVODO/HMaPLhrXmOZhdI3XM0LVxW5ZrAbwKqkkmw==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/webcola": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/webcola/-/webcola-3.4.0.tgz", + "integrity": "sha512-4BiLXjXw3SJHo3Xd+rF+7fyClT6n7I+AR6TkBqyQ4kTsePSAMDLRCXY1f3B/kXJeP9tYn4G1TblxTO+jAt0gaw==", + "dev": true, + "dependencies": { + "d3-dispatch": "^1.0.3", + "d3-drag": "^1.0.4", + "d3-shape": "^1.3.5", + "d3-timer": "^1.0.5" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index ee9702a..28366ff 100644 --- a/package.json +++ b/package.json @@ -20,12 +20,17 @@ "@tailwindcss/forms": "^0.5.3", "@types/cytoscape": "^3.19.13", "@types/cytoscape-dagre": "^2.3.2", + "@types/cytoscape-euler": "^1.2.2", "@typescript-eslint/eslint-plugin": "^5.45.0", "@typescript-eslint/parser": "^5.45.0", "autoprefixer": "^10.4.14", "chart.js": "^4.4.0", "cytoscape": "^3.26.0", + "cytoscape-cola": "^2.5.1", + "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-dagre": "^2.5.0", + "cytoscape-euler": "^1.2.2", + "cytoscape-spread": "^3.0.0", "eslint": "^8.28.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-svelte": "^2.34.0", diff --git a/src/lib/components/DiscoveryGraph.svelte b/src/lib/components/DiscoveryGraph.svelte index a54658c..f93ed6d 100644 --- a/src/lib/components/DiscoveryGraph.svelte +++ b/src/lib/components/DiscoveryGraph.svelte @@ -1,8 +1,12 @@ -
- {#if cy} - - {/if} -
+
diff --git a/src/routes/(app)/cloud/[id]/resources/+page.svelte b/src/routes/(app)/cloud/[id]/resources/+page.svelte index ac97450..5e39042 100644 --- a/src/routes/(app)/cloud/[id]/resources/+page.svelte +++ b/src/routes/(app)/cloud/[id]/resources/+page.svelte @@ -19,6 +19,7 @@ import DiscoveryGraph from '$lib/components/DiscoveryGraph.svelte'; import GraphEdge from '$lib/components/GraphEdge.svelte'; import GraphNode from '$lib/components/GraphNode.svelte'; + import type { EdgeDefinition, NodeDefinition } from 'cytoscape'; export let data: PageData; @@ -143,21 +144,22 @@ $: nodes = data.resources.map((n) => { return { - id: n.id, - label: n.properties.name - }; + data: { + id: n.id, + label: n.properties.name, + type: n.resourceType.split(',').reduce((a, v) => ({ ...a, [v]: true }), {}) + } + } satisfies NodeDefinition; }); - - - {#each nodes as node} - - {/each} + $: edges = data.edges.map((e) => { + return { + data: e + } satisfies EdgeDefinition; + }); + - {#each data.edges as edge} - - {/each} - + {#if data.resources.length == 0} From dd2557593b4a39065e03d3058535efe257c6fa78 Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Thu, 26 Oct 2023 20:38:08 +0200 Subject: [PATCH 05/18] Graph in extra tab --- src/routes/(app)/cloud/[id]/+layout.svelte | 7 +++++- .../(app)/cloud/[id]/resources/+page.svelte | 24 ++----------------- 2 files changed, 8 insertions(+), 23 deletions(-) diff --git a/src/routes/(app)/cloud/[id]/+layout.svelte b/src/routes/(app)/cloud/[id]/+layout.svelte index d2a269c..119ab6a 100644 --- a/src/routes/(app)/cloud/[id]/+layout.svelte +++ b/src/routes/(app)/cloud/[id]/+layout.svelte @@ -3,7 +3,7 @@ import { removeCloudService } from '$lib/api/orchestrator'; import Header from '$lib/components/Header.svelte'; import Tabs from '$lib/components/Tabs.svelte'; - import { CheckBadge, Cog6Tooth, QueueList, Squares2x2, User } from '@steeze-ui/heroicons'; + import { CheckBadge, Cog6Tooth, QueueList, Squares2x2, Sun, User } from '@steeze-ui/heroicons'; import type { LayoutData } from './$types'; export let data: LayoutData; @@ -20,6 +20,11 @@ href: '/cloud/' + data.service.id + '/resources', icon: Squares2x2 }, + { + name: 'Resource Graph', + href: '/cloud/' + data.service.id + '/graph', + icon: Sun + }, { name: 'Assessment Results', href: '/cloud/' + data.service.id + '/assessments', diff --git a/src/routes/(app)/cloud/[id]/resources/+page.svelte b/src/routes/(app)/cloud/[id]/resources/+page.svelte index 5e39042..774bf18 100644 --- a/src/routes/(app)/cloud/[id]/resources/+page.svelte +++ b/src/routes/(app)/cloud/[id]/resources/+page.svelte @@ -2,6 +2,7 @@ import { goto } from '$app/navigation'; import { page } from '$app/stores'; import type { Resource } from '$lib/api/discovery'; + import DiscoveryGraph from '$lib/components/DiscoveryGraph.svelte'; import StarterHint from '$lib/components/StarterHint.svelte'; import { Check, @@ -15,11 +16,8 @@ XCircle } from '@steeze-ui/heroicons'; import { Icon } from '@steeze-ui/svelte-icon'; - import type { PageData } from './$types'; - import DiscoveryGraph from '$lib/components/DiscoveryGraph.svelte'; - import GraphEdge from '$lib/components/GraphEdge.svelte'; - import GraphNode from '$lib/components/GraphNode.svelte'; import type { EdgeDefinition, NodeDefinition } from 'cytoscape'; + import type { PageData } from './$types'; export let data: PageData; @@ -141,26 +139,8 @@ const results = data.results.filter((result) => result.resourceId === resourceId); return results.length; } - - $: nodes = data.resources.map((n) => { - return { - data: { - id: n.id, - label: n.properties.name, - type: n.resourceType.split(',').reduce((a, v) => ({ ...a, [v]: true }), {}) - } - } satisfies NodeDefinition; - }); - - $: edges = data.edges.map((e) => { - return { - data: e - } satisfies EdgeDefinition; - }); - - {#if data.resources.length == 0} From a45f183580950ca2f33066ac8d3c5c9f2f558121 Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Thu, 26 Oct 2023 20:44:47 +0200 Subject: [PATCH 06/18] Marketing pages --- .../(app)/cloud/[id]/graph/+page.svelte | 25 +++++++++++++++++++ src/routes/(app)/cloud/[id]/graph/+page.ts | 15 +++++++++++ .../about/{+page.svelte => +layout.svelte} | 17 ++++++------- src/routes/(marketing)/about/+page.ts | 6 +++++ .../(marketing)/about/page1/+page.svelte | 1 + .../(marketing)/about/page2/+page.svelte | 1 + .../(marketing)/about/page3/+page.svelte | 1 + .../(marketing)/about/start/+page.svelte | 7 ++++++ 8 files changed, 64 insertions(+), 9 deletions(-) create mode 100644 src/routes/(app)/cloud/[id]/graph/+page.svelte create mode 100644 src/routes/(app)/cloud/[id]/graph/+page.ts rename src/routes/(marketing)/about/{+page.svelte => +layout.svelte} (88%) create mode 100644 src/routes/(marketing)/about/+page.ts create mode 100644 src/routes/(marketing)/about/page1/+page.svelte create mode 100644 src/routes/(marketing)/about/page2/+page.svelte create mode 100644 src/routes/(marketing)/about/page3/+page.svelte create mode 100644 src/routes/(marketing)/about/start/+page.svelte diff --git a/src/routes/(app)/cloud/[id]/graph/+page.svelte b/src/routes/(app)/cloud/[id]/graph/+page.svelte new file mode 100644 index 0000000..45b3e99 --- /dev/null +++ b/src/routes/(app)/cloud/[id]/graph/+page.svelte @@ -0,0 +1,25 @@ + + + diff --git a/src/routes/(app)/cloud/[id]/graph/+page.ts b/src/routes/(app)/cloud/[id]/graph/+page.ts new file mode 100644 index 0000000..b8bcd2d --- /dev/null +++ b/src/routes/(app)/cloud/[id]/graph/+page.ts @@ -0,0 +1,15 @@ +import { error } from '@sveltejs/kit'; +import type { PageLoad } from './$types'; +import { listGraphEdges } from '$lib/api/discovery'; + +export const load = (async ({ fetch, params }) => { + if (params.id == undefined) { + throw error(405, 'Required parameter missing'); + } + + const edges = await listGraphEdges(fetch) + + return { + edges + }; +}) satisfies PageLoad; diff --git a/src/routes/(marketing)/about/+page.svelte b/src/routes/(marketing)/about/+layout.svelte similarity index 88% rename from src/routes/(marketing)/about/+page.svelte rename to src/routes/(marketing)/about/+layout.svelte index b5ac2c5..5efd421 100644 --- a/src/routes/(marketing)/about/+page.svelte +++ b/src/routes/(marketing)/about/+layout.svelte @@ -5,18 +5,21 @@ const features = [ { name: 'Future proof.', + path: '/about/page1', description: "Today's security landscape is changing. The EU Cyber Security/Resilience Act will change how european companies will do business in the cloud. Clouditor is already equipped with rules for the latest EU certifications.", icon: CloudArrowUp }, { name: 'Avoid vendor lock-in.', + path: '/about/page2', description: 'Using our ontology approach, we can easily target multi-cloud environments and our rule engine is equipped with mappings from our metrics to different security standards, such as EUCS or BSI C5.', icon: LockOpen }, { name: 'Hollistic.', + path: '/about/page3', description: 'Using integrations with other tools, such as Codyze, we can provide a hollistic view of the cloud service, from infrastructure to code.', icon: PuzzlePiece @@ -32,7 +35,9 @@

Be audit ready, any time

-

Clouditor

+

+ Clouditor +

Clouditor, developed by Fraunhofer AISEC, is an open-source cloud compliance tool that helps security professionals to get a continuous evaluation of their cloud service. @@ -46,7 +51,7 @@ class="absolute left-1 top-1 h-5 w-5 text-clouditor" aria-hidden="true" /> - {feature.name} + {feature.name} {' '}

{feature.description}
@@ -55,13 +60,7 @@
- - Product screenshot - +
diff --git a/src/routes/(marketing)/about/+page.ts b/src/routes/(marketing)/about/+page.ts new file mode 100644 index 0000000..28fadc0 --- /dev/null +++ b/src/routes/(marketing)/about/+page.ts @@ -0,0 +1,6 @@ +import { redirect } from "@sveltejs/kit" +import type { PageLoad } from "./$types" + +export const load = (async () => { + throw redirect(301, "/about/start") +}) satisfies PageLoad \ No newline at end of file diff --git a/src/routes/(marketing)/about/page1/+page.svelte b/src/routes/(marketing)/about/page1/+page.svelte new file mode 100644 index 0000000..898e191 --- /dev/null +++ b/src/routes/(marketing)/about/page1/+page.svelte @@ -0,0 +1 @@ +Page 1 diff --git a/src/routes/(marketing)/about/page2/+page.svelte b/src/routes/(marketing)/about/page2/+page.svelte new file mode 100644 index 0000000..5df0999 --- /dev/null +++ b/src/routes/(marketing)/about/page2/+page.svelte @@ -0,0 +1 @@ +Page 2 diff --git a/src/routes/(marketing)/about/page3/+page.svelte b/src/routes/(marketing)/about/page3/+page.svelte new file mode 100644 index 0000000..de62506 --- /dev/null +++ b/src/routes/(marketing)/about/page3/+page.svelte @@ -0,0 +1 @@ +Page 3 diff --git a/src/routes/(marketing)/about/start/+page.svelte b/src/routes/(marketing)/about/start/+page.svelte new file mode 100644 index 0000000..2fcb34f --- /dev/null +++ b/src/routes/(marketing)/about/start/+page.svelte @@ -0,0 +1,7 @@ + + Product screenshot + From b725c150a87c2902906c8be19253f0986043fb05 Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Fri, 27 Oct 2023 15:14:34 +0200 Subject: [PATCH 07/18] Display assessment results in resource graph --- src/lib/api/orchestrator.ts | 4 +- src/lib/components/DiscoveryGraph.svelte | 36 ++++++++++++++-- .../(app)/cloud/[id]/assessments/+page.svelte | 2 +- .../(app)/cloud/[id]/graph/+page.svelte | 43 ++++++++++++++++++- src/routes/(app)/cloud/[id]/graph/+page.ts | 3 ++ 5 files changed, 79 insertions(+), 9 deletions(-) diff --git a/src/lib/api/orchestrator.ts b/src/lib/api/orchestrator.ts index 84e3216..16baa30 100644 --- a/src/lib/api/orchestrator.ts +++ b/src/lib/api/orchestrator.ts @@ -171,8 +171,8 @@ export async function getRuntimeInfo(fetch = window.fetch): Promise { * * @returns an array of {@link AssessmentResult}s. */ -export async function listAssessmentResults(fetch = window.fetch): Promise { - const apiUrl = clouditorize(`/v1/orchestrator/assessment_results?pageSize=1500&orderBy=timestamp&asc=false`); +export async function listAssessmentResults(fetch = window.fetch, latestByResourceId = false): Promise { + const apiUrl = clouditorize(`/v1/orchestrator/assessment_results?pageSize=1500&latestByResourceId=${latestByResourceId}&orderBy=timestamp&asc=false`); return fetch(apiUrl, { method: 'GET', diff --git a/src/lib/components/DiscoveryGraph.svelte b/src/lib/components/DiscoveryGraph.svelte index f93ed6d..56ae682 100644 --- a/src/lib/components/DiscoveryGraph.svelte +++ b/src/lib/components/DiscoveryGraph.svelte @@ -1,15 +1,28 @@ diff --git a/src/routes/(app)/cloud/[id]/assessments/+page.svelte b/src/routes/(app)/cloud/[id]/assessments/+page.svelte index 601292f..f06546f 100644 --- a/src/routes/(app)/cloud/[id]/assessments/+page.svelte +++ b/src/routes/(app)/cloud/[id]/assessments/+page.svelte @@ -156,7 +156,7 @@ - {data.metrics.get(assessment.metricId)?.name ?? assessment.metricId} + {assessment.metricId} diff --git a/src/routes/(app)/cloud/[id]/graph/+page.svelte b/src/routes/(app)/cloud/[id]/graph/+page.svelte index 45b3e99..173ae7f 100644 --- a/src/routes/(app)/cloud/[id]/graph/+page.svelte +++ b/src/routes/(app)/cloud/[id]/graph/+page.svelte @@ -1,7 +1,10 @@ - +
+
+ +
+
+ {#if node} + ID: {node.data.id}
+ Type: {Object.keys(node.data.type)} + + Assessment Results: +
    + {#each results as result} +
  • + {#if result.compliant} + + {:else} + + {/if} + + {result.metricId} + +
  • + {/each} +
+ {:else} + Nothing selected + {/if} +
+
diff --git a/src/routes/(app)/cloud/[id]/graph/+page.ts b/src/routes/(app)/cloud/[id]/graph/+page.ts index b8bcd2d..e807968 100644 --- a/src/routes/(app)/cloud/[id]/graph/+page.ts +++ b/src/routes/(app)/cloud/[id]/graph/+page.ts @@ -1,15 +1,18 @@ import { error } from '@sveltejs/kit'; import type { PageLoad } from './$types'; import { listGraphEdges } from '$lib/api/discovery'; +import { listAssessmentResults } from '$lib/api/orchestrator'; export const load = (async ({ fetch, params }) => { if (params.id == undefined) { throw error(405, 'Required parameter missing'); } + const results = listAssessmentResults(fetch, true) const edges = await listGraphEdges(fetch) return { + results, edges }; }) satisfies PageLoad; From 972e21476763da89ca34f887bb39d250555e8b78 Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Sat, 28 Oct 2023 17:15:46 +0200 Subject: [PATCH 08/18] Better detail view --- src/lib/components/AssessmentIcon.svelte | 13 +++++ src/lib/components/NodeDetail.svelte | 54 ++++++++++++++++++ .../(app)/cloud/[id]/assessments/+page.svelte | 8 +-- .../(app)/cloud/[id]/graph/+page.svelte | 56 +++++++++---------- src/routes/(app)/cloud/[id]/graph/+page.ts | 10 ++-- 5 files changed, 101 insertions(+), 40 deletions(-) create mode 100644 src/lib/components/AssessmentIcon.svelte create mode 100644 src/lib/components/NodeDetail.svelte diff --git a/src/lib/components/AssessmentIcon.svelte b/src/lib/components/AssessmentIcon.svelte new file mode 100644 index 0000000..72aadde --- /dev/null +++ b/src/lib/components/AssessmentIcon.svelte @@ -0,0 +1,13 @@ + + +{#if result.compliant} + +{:else} + +{/if} diff --git a/src/lib/components/NodeDetail.svelte b/src/lib/components/NodeDetail.svelte new file mode 100644 index 0000000..4862728 --- /dev/null +++ b/src/lib/components/NodeDetail.svelte @@ -0,0 +1,54 @@ + + +
+
+
+
+ {name(selected.id)} +
+
+
+

+ This resource is of type + + {selected.resourceType.split(',')[0]} + + and has {results.length} assessment results associated to it. +

+
+
+
+
Assessment Results
+
+ {#each results as result} +
+
+ +
+
+ +

+ {metrics.get(result.metricId)?.description} +

+
+
+ {/each} +
+
+
diff --git a/src/routes/(app)/cloud/[id]/assessments/+page.svelte b/src/routes/(app)/cloud/[id]/assessments/+page.svelte index f06546f..dfed4c1 100644 --- a/src/routes/(app)/cloud/[id]/assessments/+page.svelte +++ b/src/routes/(app)/cloud/[id]/assessments/+page.svelte @@ -7,6 +7,8 @@ import Button from '$lib/components/Button.svelte'; import { page } from '$app/stores'; import { goto } from '$app/navigation'; + import AssessmentIcon from '$lib/components/AssessmentIcon.svelte'; + import { Result } from 'postcss'; export let data: PageData; @@ -142,11 +144,7 @@ {:else} - {#if assessment.compliant} - - {:else} - - {/if} + { + return r.id == data.id; + }); + function select(e: CustomEvent) { - console.log(e.detail); - node = e.detail; + data.id = e.detail.data.id ?? null; + replaceHistory(); + } + + // a crude attempt to implement shallow routing until + // https://github.com/sveltejs/kit/pull/9847 is merged + function replaceHistory() { + const url = new URL($page.url); + if (data.id != null) { + url.searchParams.set('id', data.id); + } + + history.replaceState({}, '', url); } - $: results = data.results.filter((r) => r.resourceId == node?.data.id); + $: results = data.results.filter((r) => r.resourceId == data.id); -
-
+
+
-
- {#if node} - ID: {node.data.id}
- Type: {Object.keys(node.data.type)} - - Assessment Results: -
    - {#each results as result} -
  • - {#if result.compliant} - - {:else} - - {/if} - - {result.metricId} - -
  • - {/each} -
- {:else} - Nothing selected +
+ {#if selected} + {/if}
diff --git a/src/routes/(app)/cloud/[id]/graph/+page.ts b/src/routes/(app)/cloud/[id]/graph/+page.ts index e807968..b9ea696 100644 --- a/src/routes/(app)/cloud/[id]/graph/+page.ts +++ b/src/routes/(app)/cloud/[id]/graph/+page.ts @@ -3,16 +3,18 @@ import type { PageLoad } from './$types'; import { listGraphEdges } from '$lib/api/discovery'; import { listAssessmentResults } from '$lib/api/orchestrator'; -export const load = (async ({ fetch, params }) => { +export const load = (async ({ fetch, params, url }) => { if (params.id == undefined) { throw error(405, 'Required parameter missing'); } - const results = listAssessmentResults(fetch, true) - const edges = await listGraphEdges(fetch) + const results = listAssessmentResults(fetch, true); + const edges = await listGraphEdges(fetch); + const id = url.searchParams.get('id'); return { results, - edges + edges, + id }; }) satisfies PageLoad; From 638ed9f8693f01822f74eef3a5a613a50380a8a0 Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Sat, 28 Oct 2023 23:00:20 +0200 Subject: [PATCH 09/18] Node graph detail view --- package-lock.json | 24 +++++ package.json | 1 + src/lib/components/DiscoveryGraph.svelte | 16 +--- src/lib/components/NodeDetail.svelte | 88 +++++++++++++++---- .../(app)/cloud/[id]/compliance/+page.svelte | 6 +- 5 files changed, 101 insertions(+), 34 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3fbd00b..83e61b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "ui-svelte", "version": "0.0.1", "devDependencies": { + "@fortawesome/free-solid-svg-icons": "^6.4.2", "@rgossiaux/svelte-headlessui": "^2.0.0", "@steeze-ui/heroicons": "^2.2.3", "@steeze-ui/svelte-icon": "^1.4.1", @@ -482,6 +483,29 @@ "node": ">=14" } }, + "node_modules/@fortawesome/fontawesome-common-types": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.2.tgz", + "integrity": "sha512-1DgP7f+XQIJbLFCTX1V2QnxVmpLdKdzzo2k8EmvDOePfchaIGQ9eCHj2up3/jNEbZuBqel5OxiaOJf37TWauRA==", + "dev": true, + "hasInstallScript": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-solid-svg-icons": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.4.2.tgz", + "integrity": "sha512-sYwXurXUEQS32fZz9hVCUUv/xu49PEJEyUOsA51l6PU/qVgfbTb2glsTEaJngVVT8VqBATRIdh7XVgV1JF1LkA==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.4.2" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", diff --git a/package.json b/package.json index 28366ff..e3d3c06 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "format": "prettier --plugin-search-dir . --write ." }, "devDependencies": { + "@fortawesome/free-solid-svg-icons": "^6.4.2", "@rgossiaux/svelte-headlessui": "^2.0.0", "@steeze-ui/heroicons": "^2.2.3", "@steeze-ui/svelte-icon": "^1.4.1", diff --git a/src/lib/components/DiscoveryGraph.svelte b/src/lib/components/DiscoveryGraph.svelte index 56ae682..d7ba399 100644 --- a/src/lib/components/DiscoveryGraph.svelte +++ b/src/lib/components/DiscoveryGraph.svelte @@ -1,14 +1,9 @@
@@ -31,24 +66,45 @@

-
-
Assessment Results
-
- {#each results as result} -
-
- -
-
- -

- {metrics.get(result.metricId)?.description} -

+
+
+
+

Resource Types

+
+
+ {#each selected.resourceType.split(',').filter((r) => r != 'Resource') as type (type)} +
+ +
+ {/each}
- {/each} +
+
+ {#if results.length > 0} +
Assessment Results
+
+ {#each results as result} +
+
+ +
+
+ +

+ {metrics.get(result.metricId)?.description} +

+
+
+ {/each} +
+ {/if} +
diff --git a/src/routes/(app)/cloud/[id]/compliance/+page.svelte b/src/routes/(app)/cloud/[id]/compliance/+page.svelte index 0a77188..40ebbe4 100644 --- a/src/routes/(app)/cloud/[id]/compliance/+page.svelte +++ b/src/routes/(app)/cloud/[id]/compliance/+page.svelte @@ -5,11 +5,7 @@ type ComplianceStatus, type EvaluationResult } from '$lib/api/evaluation'; - import { - createTargetOfEvaluation, - removeTargetOfEvaluation, - type TargetOfEvaluation - } from '$lib/api/orchestrator'; + import { removeTargetOfEvaluation, type TargetOfEvaluation } from '$lib/api/orchestrator'; import CatalogComplianceItem from '$lib/components/CatalogComplianceItem.svelte'; import EnableCatalogButton from '$lib/components/EnableCatalogButton.svelte'; import type { PageData } from './$types'; From c2a139274e1846ae99427a5af3e169c747186806 Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Mon, 30 Oct 2023 09:53:05 +0100 Subject: [PATCH 10/18] Added labels --- src/lib/api/discovery.ts | 3 +- src/lib/components/NodeDetail.svelte | 48 ++++++++++++++++------------ vite.config.ts | 1 + 3 files changed, 30 insertions(+), 22 deletions(-) diff --git a/src/lib/api/discovery.ts b/src/lib/api/discovery.ts index 87c1100..cfee335 100644 --- a/src/lib/api/discovery.ts +++ b/src/lib/api/discovery.ts @@ -46,6 +46,7 @@ export interface ResourceProperties { type: string[] serviceId: string id: string + labels: object } export interface Resource { @@ -95,7 +96,7 @@ export async function listResources( export async function listGraphEdges(fetch = window.fetch): Promise { - const apiUrl = clouditorize(`/v1/discovery/graph/edges?&pageSize=1500`); + const apiUrl = clouditorize(`/v1experimental/discovery/graph/edges?&pageSize=1500`); return fetch(apiUrl, { method: 'GET', diff --git a/src/lib/components/NodeDetail.svelte b/src/lib/components/NodeDetail.svelte index 0d95617..e59dd6f 100644 --- a/src/lib/components/NodeDetail.svelte +++ b/src/lib/components/NodeDetail.svelte @@ -2,9 +2,6 @@ import type { AssessmentResult, Metric } from '$lib/api/assessment'; import type { Resource } from '$lib/api/discovery'; import type { IconDefinition } from '@fortawesome/fontawesome-common-types'; - import { CircleStack, CodeBracket, CpuChip, Trash } from '@steeze-ui/heroicons'; - import Fa from 'svelte-fa'; - import AssessmentIcon from './AssessmentIcon.svelte'; import { faDatabase, faGenderless, @@ -14,6 +11,7 @@ faServer, faWarehouse } from '@fortawesome/free-solid-svg-icons'; + import AssessmentIcon from './AssessmentIcon.svelte'; export let selected: Resource; export let results: AssessmentResult[]; @@ -68,23 +66,8 @@
-
-

Resource Types

-
-
- {#each selected.resourceType.split(',').filter((r) => r != 'Resource') as type (type)} -
- -
- {/each} -
-
-
-
- {#if results.length > 0} + {#if results.length > 0} +
Assessment Results
{#each results as result} @@ -103,7 +86,30 @@
{/each}
- {/if} +
+ {/if} +
+

Labels

+
+
+
+
+ {#each Object.entries(selected.properties.labels ?? {}) as entry} +
+
+ {entry[0]} +
+
+

+ {entry[1]} +

+
+
+ {/each} +
+
+
+
diff --git a/vite.config.ts b/vite.config.ts index a38229b..eff43a8 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -8,6 +8,7 @@ const config: UserConfig = { proxy: { '/v1/auth/': "http://localhost:8080", '/v1/discovery/': "http://localhost:8080", + '/v1experimental/discovery/': "http://localhost:8080", '/v1/evidence_store/': "http://localhost:8080", '/v1/assessment/': "http://localhost:8080", '/v1/evaluation/': "http://localhost:8080", From 363de7163f22b6677b51e7ea2cc9ea9777f30052 Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Mon, 30 Oct 2023 10:17:40 +0100 Subject: [PATCH 11/18] node selection --- src/lib/components/DiscoveryGraph.svelte | 6 ++++++ src/lib/components/NodeDetail.svelte | 6 +++++- src/routes/(app)/cloud/[id]/assessments/+page.svelte | 12 +++++++----- src/routes/(app)/cloud/[id]/graph/+page.svelte | 2 +- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/lib/components/DiscoveryGraph.svelte b/src/lib/components/DiscoveryGraph.svelte index d7ba399..f537198 100644 --- a/src/lib/components/DiscoveryGraph.svelte +++ b/src/lib/components/DiscoveryGraph.svelte @@ -7,6 +7,8 @@ export let edges: EdgeDefinition[]; export let nodes: NodeDefinition[]; + export let initialSelect: string | null; + let graph: HTMLElement; let cy: cytoscape.Core; @@ -81,6 +83,10 @@ } }); + if (initialSelect) { + cy.nodes(`node[id="${initialSelect}"]`).select(); + } + cy.nodes().on('click', function (e) { dispatch('select', { id: e.target.id(), data: e.target.data() }); }); diff --git a/src/lib/components/NodeDetail.svelte b/src/lib/components/NodeDetail.svelte index e59dd6f..fc2b3c8 100644 --- a/src/lib/components/NodeDetail.svelte +++ b/src/lib/components/NodeDetail.svelte @@ -77,7 +77,11 @@

{metrics.get(result.metricId)?.description} diff --git a/src/routes/(app)/cloud/[id]/assessments/+page.svelte b/src/routes/(app)/cloud/[id]/assessments/+page.svelte index dfed4c1..8d6274b 100644 --- a/src/routes/(app)/cloud/[id]/assessments/+page.svelte +++ b/src/routes/(app)/cloud/[id]/assessments/+page.svelte @@ -159,11 +159,13 @@ - {assessment.resourceId.split('/')[ - assessment.resourceId.split('/').length - 1 - ]} + + + {assessment.resourceId.split('/')[ + assessment.resourceId.split('/').length - 1 + ]} + + {assessment.resourceTypes[0]} diff --git a/src/routes/(app)/cloud/[id]/graph/+page.svelte b/src/routes/(app)/cloud/[id]/graph/+page.svelte index f018e39..5a6bf41 100644 --- a/src/routes/(app)/cloud/[id]/graph/+page.svelte +++ b/src/routes/(app)/cloud/[id]/graph/+page.svelte @@ -48,7 +48,7 @@

- +
{#if selected} From 64f82bcdc8593f1eacd147608a8f53022fcdd83d Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Mon, 30 Oct 2023 10:36:22 +0100 Subject: [PATCH 12/18] Nice color --- src/lib/components/DiscoveryGraph.svelte | 130 ++++++++++++++++-- .../(app)/cloud/[id]/graph/+page.svelte | 10 +- 2 files changed, 124 insertions(+), 16 deletions(-) diff --git a/src/lib/components/DiscoveryGraph.svelte b/src/lib/components/DiscoveryGraph.svelte index f537198..e4925c2 100644 --- a/src/lib/components/DiscoveryGraph.svelte +++ b/src/lib/components/DiscoveryGraph.svelte @@ -13,27 +13,35 @@ let cy: cytoscape.Core; const dispatch = createEventDispatcher<{ - select: NodeDefinition; + select: NodeDefinition | null; }>(); interface $$Events { - select: CustomEvent; + select: CustomEvent; } setContext('graphSharedState', { getCyInstance: () => cy }); - const compute = - 'data:image/svg+xml;utf8,' + - encodeURIComponent(` + const compute = ` -`); - const storage = - 'data:image/svg+xml;utf8,' + - encodeURIComponent(` +`; + const storage = ` -`); +`; + const network = ` + +`; + const group = ` + +`; + const server = ` + +`; + const account = ` + +`; onMount(() => { cytoscape.use(euler); @@ -58,23 +66,107 @@ 'text-background-opacity': 1 } }, + { + selector: 'node:selected', + style: { + 'background-color': '#007FC3' + } + }, { selector: 'node[type\\.Compute]', style: { shape: 'rectangle', - 'background-image': compute, + 'background-image': svg(compute, 'black'), 'background-fit': 'cover', 'background-color': 'white' } }, + { + selector: 'node[type\\.Compute]:selected', + style: { + shape: 'rectangle', + 'background-image': svg(compute, '#007FC3') + } + }, { selector: 'node[type\\.Storage]', style: { shape: 'rectangle', - 'background-image': storage, + 'background-image': svg(storage, 'black'), + 'background-fit': 'cover', + 'background-color': 'white' + } + }, + { + selector: 'node[type\\.Storage]:selected', + style: { + shape: 'rectangle', + 'background-image': svg(storage, '#007FC3') + } + }, + { + selector: 'node[type\\.Networking]', + style: { + shape: 'rectangle', + 'background-image': svg(network, 'black'), + 'background-fit': 'cover', + 'background-color': 'white' + } + }, + { + selector: 'node[type\\.Networking]:selected', + style: { + shape: 'rectangle', + 'background-image': svg(network, '#007FC3') + } + }, + { + selector: 'node[type\\.NetworkService]', + style: { + shape: 'rectangle', + 'background-image': svg(server, 'black'), 'background-fit': 'cover', 'background-color': 'white' } + }, + { + selector: 'node[type\\.NetworkService]:selected', + style: { + shape: 'rectangle', + 'background-image': svg(server, '#007FC3') + } + }, + { + selector: 'node[type\\.ResourceGroup]', + style: { + shape: 'rectangle', + 'background-image': svg(group, 'black'), + 'background-fit': 'cover', + 'background-color': 'white' + } + }, + { + selector: 'node[type\\.ResourceGroup]:selected', + style: { + shape: 'rectangle', + 'background-image': svg(group, '#007FC3') + } + }, + { + selector: 'node[type\\.Account]', + style: { + shape: 'rectangle', + 'background-image': svg(account, 'black'), + 'background-fit': 'cover', + 'background-color': 'white' + } + }, + { + selector: 'node[type\\.Account]:selected', + style: { + shape: 'rectangle', + 'background-image': svg(account, '#007FC3') + } } ], elements: { @@ -87,10 +179,20 @@ cy.nodes(`node[id="${initialSelect}"]`).select(); } - cy.nodes().on('click', function (e) { - dispatch('select', { id: e.target.id(), data: e.target.data() }); + cy.on('tap', function (e) { + var target = e.target; + + if (target === cy) { + dispatch('select', null); + } else { + dispatch('select', { id: target.id(), data: target.data() }); + } }); }); + + function svg(raw: string, color: string): string { + return 'data:image/svg+xml;utf8,' + encodeURIComponent(raw.replace('currentColor', color)); + }
diff --git a/src/routes/(app)/cloud/[id]/graph/+page.svelte b/src/routes/(app)/cloud/[id]/graph/+page.svelte index 5a6bf41..83f9b6d 100644 --- a/src/routes/(app)/cloud/[id]/graph/+page.svelte +++ b/src/routes/(app)/cloud/[id]/graph/+page.svelte @@ -27,8 +27,12 @@ return r.id == data.id; }); - function select(e: CustomEvent) { - data.id = e.detail.data.id ?? null; + function select(e: CustomEvent) { + if (e.detail == null) { + data.id = null; + } else { + data.id = e.detail.data.id ?? null; + } replaceHistory(); } @@ -38,6 +42,8 @@ const url = new URL($page.url); if (data.id != null) { url.searchParams.set('id', data.id); + } else { + url.searchParams.set('id', ''); } history.replaceState({}, '', url); From 5fcec03e0628958053ec3d24cb7f90a957a2e98c Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Mon, 30 Oct 2023 10:57:27 +0100 Subject: [PATCH 13/18] ++ --- src/lib/components/DiscoveryGraph.svelte | 40 ++++++++++++++++++- .../(app)/cloud/[id]/graph/+page.svelte | 14 +++---- 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/src/lib/components/DiscoveryGraph.svelte b/src/lib/components/DiscoveryGraph.svelte index e4925c2..e27ca3f 100644 --- a/src/lib/components/DiscoveryGraph.svelte +++ b/src/lib/components/DiscoveryGraph.svelte @@ -42,6 +42,12 @@ const account = ` `; + const vm = ` + +`; + const func = ` + +`; onMount(() => { cytoscape.use(euler); @@ -60,7 +66,7 @@ style: { content: `data(label)`, 'font-family': `"Inter var", sans-serif`, - 'font-size': '8px', + 'font-size': '0.8em', 'text-background-color': 'white', 'text-background-shape': 'rectangle', 'text-background-opacity': 1 @@ -88,6 +94,38 @@ 'background-image': svg(compute, '#007FC3') } }, + { + selector: 'node[type\\.VirtualMachine]', + style: { + shape: 'rectangle', + 'background-image': svg(vm, 'black'), + 'background-fit': 'cover', + 'background-color': 'white' + } + }, + { + selector: 'node[type\\.VirtualMachine]:selected', + style: { + shape: 'rectangle', + 'background-image': svg(vm, '#007FC3') + } + }, + { + selector: 'node[type\\.Function]', + style: { + shape: 'rectangle', + 'background-image': svg(func, 'black'), + 'background-fit': 'cover', + 'background-color': 'white' + } + }, + { + selector: 'node[type\\.Function]:selected', + style: { + shape: 'rectangle', + 'background-image': svg(func, '#007FC3') + } + }, { selector: 'node[type\\.Storage]', style: { diff --git a/src/routes/(app)/cloud/[id]/graph/+page.svelte b/src/routes/(app)/cloud/[id]/graph/+page.svelte index 83f9b6d..5f9a5a6 100644 --- a/src/routes/(app)/cloud/[id]/graph/+page.svelte +++ b/src/routes/(app)/cloud/[id]/graph/+page.svelte @@ -52,13 +52,9 @@ $: results = data.results.filter((r) => r.resourceId == data.id); -
-
- -
-
- {#if selected} - - {/if} -
+ +
+ {#if selected} + + {/if}
From 0e66182067c62126f8d42345f0d1d5a73c7a1164 Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Mon, 30 Oct 2023 12:11:13 +0100 Subject: [PATCH 14/18] dynamic node style --- src/lib/components/DiscoveryGraph.svelte | 247 +++++++----------- .../(app)/cloud/[id]/graph/+page.svelte | 50 +++- 2 files changed, 146 insertions(+), 151 deletions(-) diff --git a/src/lib/components/DiscoveryGraph.svelte b/src/lib/components/DiscoveryGraph.svelte index e27ca3f..97f2204 100644 --- a/src/lib/components/DiscoveryGraph.svelte +++ b/src/lib/components/DiscoveryGraph.svelte @@ -1,13 +1,15 @@ -
+
diff --git a/src/routes/(app)/cloud/[id]/graph/+page.svelte b/src/routes/(app)/cloud/[id]/graph/+page.svelte index 5f9a5a6..86738a1 100644 --- a/src/routes/(app)/cloud/[id]/graph/+page.svelte +++ b/src/routes/(app)/cloud/[id]/graph/+page.svelte @@ -7,10 +7,32 @@ export let data: PageData; + enum Status { + WAITING, + GOOD, + BAD + } + $: nodes = data.resources.map((n) => { + let status = Status.WAITING; + + // fetch assessment results + let results = data.results.filter((r) => { + return r.resourceId == n.id; + }); + + if (results.length >= 1) { + if (results.filter((r) => r.compliant == false).length > 0) { + status = Status.BAD; + } else { + status = Status.GOOD; + } + } + return { data: { id: n.id, + status: status, label: n.properties.name, type: n.resourceType.split(',').reduce((a, v) => ({ ...a, [v]: true }), {}) } @@ -33,6 +55,7 @@ } else { data.id = e.detail.data.id ?? null; } + replaceHistory(); } @@ -50,10 +73,33 @@ } $: results = data.results.filter((r) => r.resourceId == data.id); + + let overlay = false; - -
+
+
+ +
+
+ + + Show overlay + of assessment results. + +
+
+ + + +
{#if selected} {/if} From 7b49920438d0d92980048716b46184169ee08f03 Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Mon, 30 Oct 2023 13:14:34 +0100 Subject: [PATCH 15/18] Using heroicons --- src/lib/components/DiscoveryGraph.svelte | 96 +++++++++++++----------- 1 file changed, 54 insertions(+), 42 deletions(-) diff --git a/src/lib/components/DiscoveryGraph.svelte b/src/lib/components/DiscoveryGraph.svelte index 97f2204..5ca203c 100644 --- a/src/lib/components/DiscoveryGraph.svelte +++ b/src/lib/components/DiscoveryGraph.svelte @@ -1,10 +1,18 @@
From 1cb8b41826ef82dbdec212613b5e14ac069e399c Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Wed, 1 Nov 2023 10:16:46 +0100 Subject: [PATCH 17/18] Showing properties tab --- package-lock.json | 13 ++ package.json | 3 +- src/lib/components/DiscoveryGraph.svelte | 7 +- src/lib/components/NodeDetail.svelte | 168 +++++++++++++----- .../(app)/cloud/[id]/graph/+page.svelte | 2 +- src/routes/(app)/cloud/[id]/graph/+page.ts | 4 +- 6 files changed, 148 insertions(+), 49 deletions(-) diff --git a/package-lock.json b/package-lock.json index 000b1bb..774b147 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "eslint": "^8.28.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-svelte": "^2.34.0", + "flat": "^6.0.1", "inter-ui": "^3.19.3", "oidc-client-ts": "^2.4.0", "postcss": "^8.4.31", @@ -2091,6 +2092,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/flat": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/flat/-/flat-6.0.1.tgz", + "integrity": "sha512-/3FfIa8mbrg3xE7+wAhWeV+bd7L2Mof+xtZb5dRDKZ+wDvYJK4WDYeIOuOhre5Yv5aQObZrlbRmk3RTSiuQBtw==", + "dev": true, + "bin": { + "flat": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/flat-cache": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", diff --git a/package.json b/package.json index f67e40f..9527762 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "eslint": "^8.28.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-svelte": "^2.34.0", + "flat": "^6.0.1", "inter-ui": "^3.19.3", "oidc-client-ts": "^2.4.0", "postcss": "^8.4.31", @@ -44,4 +45,4 @@ "vite": "^4.5.0" }, "type": "module" -} \ No newline at end of file +} diff --git a/src/lib/components/DiscoveryGraph.svelte b/src/lib/components/DiscoveryGraph.svelte index 61d33c1..6419e3b 100644 --- a/src/lib/components/DiscoveryGraph.svelte +++ b/src/lib/components/DiscoveryGraph.svelte @@ -1,7 +1,10 @@
@@ -32,56 +63,103 @@
-
- {#if results.length > 0} -
-
Assessment Results
-
- {#each results as result} -
-
- -
-
- -

- {metrics.get(result.metricId)?.description} -

-
-
+ +
+
+ + + +
+ - {/if} -
-

Labels

-
-
-
-
- {#each Object.entries(selected.properties.labels ?? {}) as entry} -
-
- {entry[0]} -
-
-

- {entry[1]} -

-
+
+
+ +
+ {#if tab == 'results'} + {#if results.length > 0} +
+
+ {#each results as result} +
+
+
- {/each} - +
+ +

+ {metrics.get(result.metricId)?.description} +

+
+
+ {/each} +
+
+ {:else} +
No assessment results found for this resource.
+ {/if} + {/if} + {#if tab == 'properties'} +
+
+
+
+
+ {#each humanProperties(selected) as [k, v] (k)} +
+
+ {k} +
+
+

+ {v} +

+
+
+ {/each} +
+
-
+ {/if}
diff --git a/src/routes/(app)/cloud/[id]/graph/+page.svelte b/src/routes/(app)/cloud/[id]/graph/+page.svelte index 86738a1..ec3e2b1 100644 --- a/src/routes/(app)/cloud/[id]/graph/+page.svelte +++ b/src/routes/(app)/cloud/[id]/graph/+page.svelte @@ -101,6 +101,6 @@
{#if selected} - + {/if}
diff --git a/src/routes/(app)/cloud/[id]/graph/+page.ts b/src/routes/(app)/cloud/[id]/graph/+page.ts index b9ea696..2ac4d2e 100644 --- a/src/routes/(app)/cloud/[id]/graph/+page.ts +++ b/src/routes/(app)/cloud/[id]/graph/+page.ts @@ -11,10 +11,12 @@ export const load = (async ({ fetch, params, url }) => { const results = listAssessmentResults(fetch, true); const edges = await listGraphEdges(fetch); const id = url.searchParams.get('id'); + const tab = url.searchParams.get('tab'); return { results, edges, - id + id, + tab }; }) satisfies PageLoad; From 025f2f218900be75f3f2c34f92edaa6fa232ced7 Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Wed, 1 Nov 2023 10:24:13 +0100 Subject: [PATCH 18/18] Property view --- src/lib/components/NodeDetail.svelte | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/lib/components/NodeDetail.svelte b/src/lib/components/NodeDetail.svelte index 87a06a9..c875529 100644 --- a/src/lib/components/NodeDetail.svelte +++ b/src/lib/components/NodeDetail.svelte @@ -149,7 +149,11 @@

- {v} + {#if k == 'url'} + {v} + {:else} + {v} + {/if}