Files
compliance-scanner-agent/compliance-dashboard/assets/graph-viz.js
Sharang Parnerkar a22cf1595f
Some checks failed
CI / Format (push) Failing after 3s
CI / Clippy (push) Failing after 1m19s
CI / Security Audit (push) Has been skipped
CI / Tests (push) Has been skipped
CI / Format (pull_request) Failing after 2s
CI / Clippy (pull_request) Failing after 1m18s
CI / Security Audit (pull_request) Has been skipped
CI / Tests (pull_request) Has been skipped
Add SBOM enhancements, delete repo feature, and embedding build spinner
- Fix SBOM display bug by removing incorrect BSON serde helpers on DateTime fields
- Add filtered/searchable SBOM list with repo, package manager, search, vuln, and license filters
- Add SBOM export (CycloneDX 1.5 / SPDX 2.3), license compliance tab, and cross-repo diff
- Add vulnerability drill-down with inline CVE details and advisory links
- Add DELETE /api/v1/repositories/{id} with cascade delete of all related data
- Add delete repository button with confirmation modal warning in dashboard
- Add spinner and progress bar for embedding builds with auto-polling status
- Install syft in agent Dockerfile for SBOM generation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 00:17:14 +01:00

353 lines
9.9 KiB
JavaScript

// ═══════════════════════════════════════════════════════════════
// Graph Visualization — vis-network wrapper for Code Knowledge Graph
// Obsidian Control theme
// ═══════════════════════════════════════════════════════════════
(function () {
"use strict";
// Color palette matching Obsidian Control
const NODE_COLORS = {
function: { bg: "#00c8ff", border: "#00a0cc", font: "#060a13" },
method: { bg: "#00c8ff", border: "#00a0cc", font: "#060a13" },
struct: { bg: "#e040fb", border: "#b030d0", font: "#060a13" },
class: { bg: "#e040fb", border: "#b030d0", font: "#060a13" },
enum: { bg: "#ffb020", border: "#cc8c18", font: "#060a13" },
interface: { bg: "#7c4dff", border: "#5c38c0", font: "#ffffff" },
trait: { bg: "#7c4dff", border: "#5c38c0", font: "#ffffff" },
module: { bg: "#ff8a3d", border: "#cc6e30", font: "#060a13" },
file: { bg: "#00e676", border: "#00b85e", font: "#060a13" },
};
const EDGE_COLORS = {
calls: "#00c8ff",
imports: "#ffb020",
inherits: "#e040fb",
implements: "#7c4dff",
contains: "rgba(94, 114, 145, 0.6)",
type_ref: "rgba(94, 114, 145, 0.5)",
};
const DEFAULT_NODE_COLOR = { bg: "#5e7291", border: "#3d506b", font: "#e4eaf4" };
const DEFAULT_EDGE_COLOR = "rgba(94, 114, 145, 0.3)";
let network = null;
let nodesDataset = null;
let edgesDataset = null;
let rawNodes = [];
let rawEdges = [];
function getNodeColor(kind) {
return NODE_COLORS[kind] || DEFAULT_NODE_COLOR;
}
function getEdgeColor(kind) {
return EDGE_COLORS[kind] || DEFAULT_EDGE_COLOR;
}
// Build a vis-network node from raw graph data
function toVisNode(node, edgeCounts) {
const color = getNodeColor(node.kind);
const connections = edgeCounts[node.qualified_name] || 0;
const size = Math.max(8, Math.min(35, 8 + connections * 2));
return {
id: node.qualified_name,
label: node.name,
title: `${node.kind}: ${node.qualified_name}\nFile: ${node.file_path}:${node.start_line}`,
size: size,
color: {
background: color.bg,
border: color.border,
highlight: { background: color.bg, border: "#ffffff" },
hover: { background: color.bg, border: "#ffffff" },
},
font: {
color: color.font,
size: 10,
face: "'JetBrains Mono', monospace",
strokeWidth: 2,
strokeColor: "#060a13",
},
borderWidth: 1,
borderWidthSelected: 3,
shape: node.kind === "file" ? "diamond" : node.kind === "module" ? "square" : "dot",
// Store raw data for click handler
_raw: node,
};
}
function toVisEdge(edge) {
const color = getEdgeColor(edge.kind);
const isDim = edge.kind === "contains" || edge.kind === "type_ref";
return {
from: edge.source,
to: edge.target,
color: {
color: color,
highlight: "#ffffff",
hover: color,
},
width: isDim ? 1 : 2,
arrows: {
to: {
enabled: edge.kind !== "contains",
scaleFactor: 0.5,
},
},
smooth: {
enabled: true,
type: "continuous",
roundness: 0.3,
},
title: `${edge.kind}: ${edge.source}${edge.target}`,
_raw: edge,
};
}
/**
* Initialize or reload the graph visualization.
* Called from Rust via eval().
*/
window.__loadGraph = function (nodes, edges) {
var container = document.getElementById("graph-canvas");
if (!container) {
console.error("[graph-viz] #graph-canvas not found");
return;
}
// Count edges per node for sizing
const edgeCounts = {};
edges.forEach(function (e) {
edgeCounts[e.source] = (edgeCounts[e.source] || 0) + 1;
edgeCounts[e.target] = (edgeCounts[e.target] || 0) + 1;
});
// Deduplicate nodes by qualified_name (keep first occurrence)
var seenNodes = {};
var uniqueNodes = [];
nodes.forEach(function (n) {
if (!seenNodes[n.qualified_name]) {
seenNodes[n.qualified_name] = true;
uniqueNodes.push(n);
}
});
// Deduplicate edges by source+target+kind
var seenEdges = {};
var uniqueEdges = [];
edges.forEach(function (e) {
var key = e.source + "|" + e.target + "|" + e.kind;
if (!seenEdges[key]) {
seenEdges[key] = true;
uniqueEdges.push(e);
}
});
rawNodes = uniqueNodes;
rawEdges = uniqueEdges;
var visNodes = uniqueNodes.map(function (n) {
return toVisNode(n, edgeCounts);
});
var visEdges = uniqueEdges.map(toVisEdge);
nodesDataset = new vis.DataSet(visNodes);
edgesDataset = new vis.DataSet(visEdges);
const options = {
nodes: {
font: { color: "#e4eaf4", size: 10 },
scaling: { min: 8, max: 35 },
},
edges: {
font: { color: "#5e7291", size: 9, strokeWidth: 0 },
selectionWidth: 3,
},
physics: {
enabled: true,
solver: "forceAtlas2Based",
forceAtlas2Based: {
gravitationalConstant: -80,
centralGravity: 0.005,
springLength: 120,
springConstant: 0.04,
damping: 0.5,
avoidOverlap: 0.6,
},
stabilization: {
enabled: true,
iterations: 1500,
updateInterval: 25,
},
maxVelocity: 50,
minVelocity: 0.75,
},
interaction: {
hover: true,
tooltipDelay: 200,
hideEdgesOnDrag: false,
hideEdgesOnZoom: false,
multiselect: false,
navigationButtons: false,
keyboard: { enabled: true },
},
layout: {
improvedLayout: uniqueNodes.length < 300,
},
};
// Destroy previous instance
if (network) {
network.destroy();
}
network = new vis.Network(
container,
{ nodes: nodesDataset, edges: edgesDataset },
options
);
// Click handler — sends data to Rust
network.on("click", function (params) {
if (params.nodes.length > 0) {
const nodeId = params.nodes[0];
const visNode = nodesDataset.get(nodeId);
if (visNode && visNode._raw && window.__onNodeClick) {
window.__onNodeClick(JSON.stringify(visNode._raw));
}
}
});
// Stabilization overlay
var overlay = document.getElementById("graph-stabilization-overlay");
var progressFill = document.getElementById("graph-stab-progress-fill");
var pctLabel = document.getElementById("graph-stab-pct");
// Show the overlay
if (overlay) {
overlay.classList.remove("graph-stab-fade-out");
overlay.style.display = "flex";
}
network.on("stabilizationProgress", function (params) {
var pct = Math.round((params.iterations / params.total) * 100);
if (progressFill) {
progressFill.style.width = pct + "%";
}
if (pctLabel) {
pctLabel.textContent = pct + "% — computing layout";
}
});
network.on("stabilizationIterationsDone", function () {
if (progressFill) progressFill.style.width = "100%";
if (pctLabel) pctLabel.textContent = "Complete";
// Fade out overlay
if (overlay) {
overlay.classList.add("graph-stab-fade-out");
setTimeout(function () {
overlay.style.display = "none";
}, 900);
}
// Keep physics running so nodes float and respond to dragging,
// but reduce forces for a calm, settled feel
network.setOptions({
physics: {
enabled: true,
solver: "forceAtlas2Based",
forceAtlas2Based: {
gravitationalConstant: -40,
centralGravity: 0.003,
springLength: 120,
springConstant: 0.03,
damping: 0.7,
avoidOverlap: 0.6,
},
maxVelocity: 20,
minVelocity: 0.75,
},
});
});
console.log(
"[graph-viz] Loaded " + nodes.length + " nodes, " + edges.length + " edges"
);
};
/**
* Highlight a node by qualified name. Focuses + selects it.
*/
window.__highlightNode = function (qualifiedName) {
if (!network || !nodesDataset) return;
const node = nodesDataset.get(qualifiedName);
if (!node) return;
network.selectNodes([qualifiedName]);
network.focus(qualifiedName, {
scale: 1.5,
animation: { duration: 500, easingFunction: "easeInOutQuad" },
});
// Trigger click callback too
if (node._raw && window.__onNodeClick) {
window.__onNodeClick(JSON.stringify(node._raw));
}
};
/**
* Highlight multiple nodes (e.g., search results or file nodes).
*/
window.__highlightNodes = function (qualifiedNames) {
if (!network || !nodesDataset) return;
network.selectNodes(qualifiedNames);
if (qualifiedNames.length > 0) {
network.fit({
nodes: qualifiedNames,
animation: { duration: 500, easingFunction: "easeInOutQuad" },
});
}
};
/**
* Highlight all nodes belonging to a specific file path.
*/
window.__highlightFileNodes = function (filePath) {
if (!network || !nodesDataset) return;
var matching = [];
rawNodes.forEach(function (n) {
if (n.file_path === filePath) {
matching.push(n.qualified_name);
}
});
if (matching.length > 0) {
network.selectNodes(matching);
network.fit({
nodes: matching,
animation: { duration: 500, easingFunction: "easeInOutQuad" },
});
}
};
/**
* Clear selection.
*/
window.__clearGraphSelection = function () {
if (!network) return;
network.unselectAll();
};
/**
* Fit entire graph in view.
*/
window.__fitGraph = function () {
if (!network) return;
network.fit({
animation: { duration: 400, easingFunction: "easeInOutQuad" },
});
};
})();