Add graph explorer components, API handlers, and dependency updates
Adds code inspector, file tree components, graph visualization JS, graph API handlers, sidebar navigation updates, and misc improvements. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
335
compliance-dashboard/assets/graph-viz.js
Normal file
335
compliance-dashboard/assets/graph-viz.js
Normal file
@@ -0,0 +1,335 @@
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// 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: -60,
|
||||
centralGravity: 0.012,
|
||||
springLength: 80,
|
||||
springConstant: 0.06,
|
||||
damping: 0.4,
|
||||
avoidOverlap: 0.5,
|
||||
},
|
||||
stabilization: {
|
||||
enabled: true,
|
||||
iterations: 1000,
|
||||
updateInterval: 25,
|
||||
},
|
||||
maxVelocity: 40,
|
||||
minVelocity: 0.1,
|
||||
},
|
||||
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);
|
||||
}
|
||||
network.setOptions({ physics: { enabled: false } });
|
||||
});
|
||||
|
||||
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" },
|
||||
});
|
||||
};
|
||||
})();
|
||||
34
compliance-dashboard/assets/vis-network.min.js
vendored
Normal file
34
compliance-dashboard/assets/vis-network.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user