File size: 2,597 Bytes
fc01079
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
import { useEffect, useMemo } from "react";
import ReactFlow, {
  Background,
  BackgroundVariant,
  Controls,
  MiniMap,
  useReactFlow,
  ReactFlowProvider,
  type Node,
  type NodeMouseHandler,
} from "reactflow";
import "reactflow/dist/style.css";
import "./graph-overrides.css";
import { layoutGraph } from "../lib/layout";
import type { IRGraph, IRNode } from "../types";
import { nodeTypes } from "./CustomNodes";

interface Props {
  ir: IRGraph;
  onSelectNode: (node: IRNode | null) => void;
}

function GraphViewInner({ ir, onSelectNode }: Props) {
  const { nodes, edges } = useMemo(() => layoutGraph(ir), [ir]);
  const { fitView } = useReactFlow();

  // Re-fit the camera every time the underlying graph topology changes
  // (e.g. a new model loaded, or the granularity slider collapsed clusters).
  // Without this, the viewport stays where the previous graph was and the
  // user can end up staring at an empty panel.
  useEffect(() => {
    const id = window.setTimeout(() => {
      fitView({ duration: 200, padding: 0.15 });
    }, 50);
    return () => window.clearTimeout(id);
  }, [nodes.length, edges.length, fitView]);

  const handleNodeClick: NodeMouseHandler = (_e, node) => {
    const irNode = (node.data as { irNode?: IRNode }).irNode ?? null;
    onSelectNode(irNode);
  };

  const handlePaneClick = () => onSelectNode(null);

  const nodeColor = (n: Node) =>
    (n.data as { color?: string }).color ?? "#6477a8";

  return (
    <ReactFlow
      nodes={nodes}
      edges={edges}
      nodeTypes={nodeTypes}
      onNodeClick={handleNodeClick}
      onPaneClick={handlePaneClick}
      fitView
      fitViewOptions={{ padding: 0.15 }}
      minZoom={0.05}
      maxZoom={3}
      proOptions={{ hideAttribution: true }}
      nodesDraggable={false}
      nodesConnectable={false}
      elementsSelectable
    >
      <Background variant={BackgroundVariant.Dots} gap={20} size={1} color="#1f2a48" />
      <Controls
        showInteractive={false}
        style={{
          background: "rgba(17,26,48,0.85)",
          border: "1px solid rgba(155,180,255,0.12)",
          borderRadius: 8,
        }}
      />
      <MiniMap
        pannable
        zoomable
        nodeColor={nodeColor}
        maskColor="rgba(10,15,28,0.7)"
        style={{
          background: "rgba(17,26,48,0.85)",
          border: "1px solid rgba(155,180,255,0.12)",
          borderRadius: 8,
        }}
      />
    </ReactFlow>
  );
}

export function GraphView(props: Props) {
  return (
    <ReactFlowProvider>
      <GraphViewInner {...props} />
    </ReactFlowProvider>
  );
}