import dagre from "dagre";
import {
Background,
BackgroundVariant,
Controls,
Handle,
MarkerType,
MiniMap,
Panel,
Position,
ReactFlow,
type Edge,
type Node,
type NodeProps,
} from "@xyflow/react";
import {
Box,
Braces,
Database,
ExternalLink,
FunctionSquare,
} from "lucide-react";
import { useMemo } from "react";
import { field, type ApiItem } from "../lib/api";
type GraphNodeData = {
label: string;
kind: "package" | "subprogram" | "table" | "external";
eyebrow: string;
};
type GraphNode = Node;
function inferKind(label: string, relationship = ""): GraphNodeData["kind"] {
const normalized = ${label} ${relationship}.toUpperCase();
if (
normalized.includes("TABLE") ||
normalized.includes("VIEW") ||
normalized.includes("SELECT") ||
normalized.includes("INSERT") ||
normalized.includes("UPDATE") ||
normalized.includes("DELETE") ||
normalized.includes("MERGE")
) {
return "table";
}
if (
normalized.includes("HTTP") ||
normalized.includes("FILE") ||
normalized.includes("MAIL") ||
normalized.includes("EXTERNAL") ||
normalized.includes("DBLINK")
) {
return "external";
}
if (
normalized.includes("PROCEDURE") ||
normalized.includes("FUNCTION") ||
normalized.includes("CALL")
) {
return "subprogram";
}
return "package";
}
function kindLabel(kind: GraphNodeData["kind"]) {
return {
package: "Package / object",
subprogram: "Procedure / function",
table: "Table / view",
external: "External system",
}[kind];
}
function NodeIcon({ kind }: { kind: GraphNodeData["kind"] }) {
if (kind === "table") return ;
if (kind === "subprogram") return ;
if (kind === "external") return ;
return ;
}
function InsightNode({ data, selected }: NodeProps) {
return (
{data.eyebrow}
{data.label}
);
}
const nodeTypes = { insight: InsightNode };
function layoutGraph(nodes: GraphNode[], edges: Edge[]) {
const graph = new dagre.graphlib.Graph();
graph.setDefaultEdgeLabel(() => ({}));
graph.setGraph({
rankdir: "LR",
nodesep: 46,
ranksep: 112,
marginx: 34,
marginy: 34,
});
nodes.forEach((node) => graph.setNode(node.id, { width: 230, height: 72 }));
edges.forEach((edge) => graph.setEdge(edge.source, edge.target));
dagre.layout(graph);
return nodes.map((node) => {
const point = graph.node(node.id);
return {
...node,
position: { x: point.x - 115, y: point.y - 36 },
};
});
}
export function DependencyGraph({ edges: rawEdges }: { edges: ApiItem[] }) {
const { nodes, edges } = useMemo(() => {
const nodeMap = new Map();
const graphEdges: Edge[] = [];
rawEdges.forEach((item, index) => {
const source = String(
field(item, "SOURCE_NODE", field(item, "source", "Source")),
);
const target = String(
field(item, "TARGET_NODE", field(item, "target", "Target")),
);
const relationship = String(
field(
item,
"RELATIONSHIP_TYPE",
field(item, "relationship", "DEPENDS_ON"),
),
);
const sourceKind = inferKind(source);
const targetKind = inferKind(target, relationship);
if (!nodeMap.has(source)) {
nodeMap.set(source, {
label: source,
kind: sourceKind,
eyebrow: kindLabel(sourceKind),
});
}
if (!nodeMap.has(target)) {
nodeMap.set(target, {
label: target,
kind: targetKind,
eyebrow: kindLabel(targetKind),
});
}
graphEdges.push({
id: `edge-${index}-${source}-${target}`,
source,
target,
type: "smoothstep",
label: relationship.replaceAll("_", " "),
markerEnd: { type: MarkerType.ArrowClosed, color: "#7796ff" },
style: { stroke: "#7796ff", strokeWidth: 1.7 },
labelStyle: {
fill: "#aebbd0",
fontSize: 10,
fontWeight: 700,
},
labelBgStyle: {
fill: "#111a28",
fillOpacity: 0.94,
},
labelBgPadding: [7, 4],
labelBgBorderRadius: 6,
});
});
const graphNodes: GraphNode[] = Array.from(nodeMap.entries()).map(
([id, data]) => ({
id,
type: "insight",
data,
position: { x: 0, y: 0 },
}),
);
return {
nodes: layoutGraph(graphNodes, graphEdges),
edges: graphEdges,
};
}, [rawEdges]);
if (!rawEdges.length) {
return (
No dependency edges yet
Analyze an object first. Calls, tables, package containment, and
external systems will appear here.
);
}
return (
nodes={nodes}
edges={edges}
nodeTypes={nodeTypes}
fitView
fitViewOptions={{ padding: 0.24 }}
minZoom={0.28}
maxZoom={1.8}
nodesConnectable={false}
deleteKeyCode={null}
proOptions={{ hideAttribution: true }}
>
variant={BackgroundVariant.Dots}
gap={22}
size={1.2}
color="#263248"
/>
position="bottom-right"
pannable
zoomable
nodeStrokeWidth={3}
nodeColor={(node) => {
const kind = (node.data as GraphNodeData).kind;
if (kind === "table") return "#4de2c5";
if (kind === "external") return "#ffbc70";
if (kind === "subprogram") return "#a99eff";
return "#7796ff";
}}
maskColor="rgba(5, 9, 16, .72)"
/>
{(["package", "subprogram", "table", "external"] as const).map(
(kind) => (
{kindLabel(kind)}
),
)}
);
}
Top comments (0)