@eqtylab/explorer 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/.env.api +6 -0
  2. package/.env.iframe +2 -0
  3. package/.env.manifest +2 -0
  4. package/.envrc +1 -0
  5. package/.eslintrc.cjs +17 -0
  6. package/.github/workflows/ci.yml +46 -0
  7. package/.github/workflows/publish.yml +44 -0
  8. package/.husky/pre-push +2 -0
  9. package/.prettierignore +2 -0
  10. package/.prettierrc.json +13 -0
  11. package/LICENSE +201 -0
  12. package/README.md +84 -0
  13. package/flake.lock +61 -0
  14. package/flake.nix +23 -0
  15. package/index.html +13 -0
  16. package/package.json +93 -0
  17. package/postcss.config.js +5 -0
  18. package/public/logos/eci.svg +23 -0
  19. package/public/logos/eqty.svg +3 -0
  20. package/public/manifests/B-200-merged.json +773 -0
  21. package/public/manifests/amd-sev.json +426 -0
  22. package/public/manifests/anchors.json +609 -0
  23. package/public/manifests/association-no-metadata.json +1393 -0
  24. package/public/manifests/association.json +1402 -0
  25. package/public/manifests/eqty-docker.json +562 -0
  26. package/public/manifests/governance.json +1324 -0
  27. package/public/manifests/intel-tdx-with-nvidia.json +441 -0
  28. package/public/manifests/intel-tdx.json +404 -0
  29. package/public/manifests/iroh-collections.json +1285 -0
  30. package/public/manifests/model-signing.json +1536 -0
  31. package/public/manifests/multiple-registrations.json +129 -0
  32. package/public/manifests/no-vcs.json +114 -0
  33. package/public/manifests/storage.json +294 -0
  34. package/public/manifests/sub-graphs.json +1 -0
  35. package/src/animations/variants.ts +52 -0
  36. package/src/app/apiClient/integrityService/fetchAttributes.ts +29 -0
  37. package/src/app/apiClient/integrityService/fetchManifest.ts +28 -0
  38. package/src/app/apiClient/integrityService/types.ts +12 -0
  39. package/src/app/components/integrityService/IntegrityService.tsx +105 -0
  40. package/src/app/components/integrityService/components/AuthButton.tsx +26 -0
  41. package/src/app/components/integrityService/components/FiltersModal.tsx +214 -0
  42. package/src/app/components/integrityService/components/FiltersToggle.tsx +28 -0
  43. package/src/app/components/manifestViewer/ManifestViewer.tsx +90 -0
  44. package/src/app/components/manifestViewer/components/ManifestButtons.tsx +215 -0
  45. package/src/app/config.json +23 -0
  46. package/src/app/config.ts +28 -0
  47. package/src/app/pages/index.tsx +34 -0
  48. package/src/app/stores/manifestStore.ts +19 -0
  49. package/src/app/utils/appMode.ts +37 -0
  50. package/src/assets/fonts/TWKLausanne-300.woff +0 -0
  51. package/src/assets/fonts/TWKLausanne-300.woff2 +0 -0
  52. package/src/assets/fonts/TWKLausanne-400.woff +0 -0
  53. package/src/assets/fonts/TWKLausanne-400.woff2 +0 -0
  54. package/src/assets/fonts/TWKLausanne-500.woff +0 -0
  55. package/src/assets/fonts/TWKLausanne-500.woff2 +0 -0
  56. package/src/assets/fonts/TWKLausanne-600.woff +0 -0
  57. package/src/assets/fonts/TWKLausanne-600.woff2 +0 -0
  58. package/src/assets/vectors/eqtylab.svg +6 -0
  59. package/src/components/app-header.tsx +61 -0
  60. package/src/components/dark-mode-switch.tsx +16 -0
  61. package/src/components/ui/accordion.tsx +47 -0
  62. package/src/components/ui/certificate-chain.tsx +60 -0
  63. package/src/components/ui/certificate-code.tsx +39 -0
  64. package/src/components/ui/details.tsx +20 -0
  65. package/src/components/ui/display-code.tsx +47 -0
  66. package/src/components/ui/panel-label.tsx +12 -0
  67. package/src/components/ui/verify-state-badge.tsx +32 -0
  68. package/src/explorer-component/LineageExplorer.tsx +89 -0
  69. package/src/explorer-component/components/CustomEdge.tsx +28 -0
  70. package/src/explorer-component/components/LineageFlow.tsx +220 -0
  71. package/src/explorer-component/components/NodeIconLoader.tsx +26 -0
  72. package/src/explorer-component/components/nodes/ComputationNode.tsx +56 -0
  73. package/src/explorer-component/components/nodes/CustomNode.tsx +112 -0
  74. package/src/explorer-component/components/nodes/index.ts +4 -0
  75. package/src/explorer-component/components/nodes/nodes.tsx +144 -0
  76. package/src/explorer-component/components/nodes/types.ts +167 -0
  77. package/src/explorer-component/components/sidebar/SectionList.tsx +54 -0
  78. package/src/explorer-component/components/sidebar/SectionTitle.tsx +10 -0
  79. package/src/explorer-component/components/sidebar/Sidebar.tsx +70 -0
  80. package/src/explorer-component/components/sidebar/SidebarWrapper.tsx +76 -0
  81. package/src/explorer-component/components/sidebar/sections/CustomSection.tsx +238 -0
  82. package/src/explorer-component/components/sidebar/sections/FileViewer.tsx +229 -0
  83. package/src/explorer-component/components/sidebar/sections/GovernanceSection.tsx +52 -0
  84. package/src/explorer-component/components/sidebar/sections/RegistrationSection.tsx +254 -0
  85. package/src/explorer-component/components/sidebar/sections/SigstoreSection.tsx +37 -0
  86. package/src/explorer-component/components/sidebar/sections/StorageSection.tsx +73 -0
  87. package/src/explorer-component/components/sidebar/sections/custom/IrohCollectionsView.tsx +139 -0
  88. package/src/explorer-component/components/sidebar/sections/registration/VerificationIcon.tsx +108 -0
  89. package/src/explorer-component/components/sidebar/sections/registration/actor/ActorBadge.tsx +62 -0
  90. package/src/explorer-component/components/sidebar/sections/registration/actor/svg/AMDLogo.tsx +40 -0
  91. package/src/explorer-component/components/sidebar/sections/registration/actor/svg/AzureLogo.tsx +65 -0
  92. package/src/explorer-component/components/sidebar/sections/registration/actor/svg/IntelLogo.tsx +24 -0
  93. package/src/explorer-component/components/sidebar/sections/registration/actor/svg/NvidiaLogo.tsx +27 -0
  94. package/src/explorer-component/components/sidebar/sections/registration/verifierClient.ts +13 -0
  95. package/src/explorer-component/icons/AttributionIcon.svg +6 -0
  96. package/src/explorer-component/icons/BenchmarkIcon.svg +6 -0
  97. package/src/explorer-component/icons/BenchmarkResultIcon.svg +6 -0
  98. package/src/explorer-component/icons/CertificateIcon.svg +6 -0
  99. package/src/explorer-component/icons/CodeIcon.svg +6 -0
  100. package/src/explorer-component/icons/ComputationIcon.svg +6 -0
  101. package/src/explorer-component/icons/DataIcon.svg +6 -0
  102. package/src/explorer-component/icons/DatabaseIcon.svg +6 -0
  103. package/src/explorer-component/icons/DatasetIcon.svg +6 -0
  104. package/src/explorer-component/icons/DeltaLakeIcon.svg +7 -0
  105. package/src/explorer-component/icons/DocumentIcon.svg +6 -0
  106. package/src/explorer-component/icons/MediaIcon.svg +6 -0
  107. package/src/explorer-component/icons/ModelIcon.svg +6 -0
  108. package/src/explorer-component/icons/TeamIcon.svg +3 -0
  109. package/src/explorer-component/icons/TokenIcon.svg +6 -0
  110. package/src/explorer-component/icons/UnknownIcon.svg +6 -0
  111. package/src/explorer-component/icons/index.ts +35 -0
  112. package/src/explorer-component/index.ts +5 -0
  113. package/src/explorer-component/integrityTypes/AnchorTypes.ts +31 -0
  114. package/src/explorer-component/integrityTypes/StatementTypes.ts +333 -0
  115. package/src/explorer-component/integrityTypes/VCTypes.ts +85 -0
  116. package/src/explorer-component/integrityTypes/types.ts +3 -0
  117. package/src/explorer-component/utils/config.ts +163 -0
  118. package/src/explorer-component/utils/formatTimestamp.ts +5 -0
  119. package/src/explorer-component/utils/generateGraph.ts +271 -0
  120. package/src/explorer-component/utils/layoutGraph.ts +148 -0
  121. package/src/explorer-component/utils/resolveIrohBlobs.ts +117 -0
  122. package/src/explorer-component/utils/statementHelpers.ts +252 -0
  123. package/src/explorer-component/utils/stripPrefix.ts +4 -0
  124. package/src/hooks/use-mobile.tsx +19 -0
  125. package/src/index.ts +3 -0
  126. package/src/lib/resource-types.ts +31 -0
  127. package/src/lib/use-escape-key.tsx +16 -0
  128. package/src/lib/utils.ts +19 -0
  129. package/src/main.css +82 -0
  130. package/src/main.tsx +39 -0
  131. package/src/v-comp-viewer/components/container-did-details.tsx +53 -0
  132. package/src/v-comp-viewer/components/system-did-details.tsx +356 -0
  133. package/src/v-comp-viewer/useVComp.ts +196 -0
  134. package/src/v-comp-viewer/utils.tsx +824 -0
  135. package/src/vite-env.d.ts +1 -0
  136. package/tsconfig.json +33 -0
  137. package/tsconfig.node.json +10 -0
  138. package/vercel.json +13 -0
  139. package/vite.app.config.ts +48 -0
  140. package/vite.lib.config.ts +61 -0
@@ -0,0 +1,47 @@
1
+ import { DisplayField, DisplayFieldProps } from "@eqtylab/equality";
2
+
3
+ import { Validated } from "@/v-comp-viewer/utils";
4
+
5
+ type DisplayCodeProps = DisplayFieldProps & {
6
+ value: string;
7
+ validated?: Validated;
8
+ };
9
+
10
+ const DisplayCode = ({ value, validated, ...props }: DisplayCodeProps) => {
11
+ const splitValue = value.split(":");
12
+ let prefix = "";
13
+ let id = value;
14
+ if (splitValue.length === 3) {
15
+ prefix = splitValue[1];
16
+ id = splitValue[2];
17
+ } else if (splitValue.length === 2) {
18
+ prefix = splitValue[0];
19
+ id = splitValue[1];
20
+ } else if (!prefix && value.startsWith("0x")) {
21
+ prefix = "0x";
22
+ }
23
+
24
+ const isHexadecimal = prefix.startsWith("0x");
25
+ const truncatedString = isHexadecimal ? id.slice(2) : id;
26
+
27
+ const variants: Partial<Record<Validated, DisplayFieldProps["variant"]>> = {
28
+ valid: "success",
29
+ partial: "neutral",
30
+ invalid: "failure",
31
+ };
32
+
33
+ return (
34
+ <DisplayField
35
+ className="break-normal"
36
+ variant={props.variant || (validated ? variants[validated] : undefined)}
37
+ prefix={props.prefix || (isHexadecimal ? prefix : prefix.toUpperCase())}
38
+ truncate={props.truncate || "middle"}
39
+ elevation="raised"
40
+ {...props}
41
+ >
42
+ {truncatedString}
43
+ </DisplayField>
44
+ );
45
+ };
46
+
47
+ export default DisplayCode;
@@ -0,0 +1,12 @@
1
+ import { cn } from "@/lib/utils";
2
+
3
+ interface PanelLabelProps {
4
+ className?: string;
5
+ label: string;
6
+ }
7
+
8
+ const PanelLabel = ({ className, label }: PanelLabelProps) => {
9
+ return <div className={cn("text-xxs font-semibold uppercase tracking-[0.5px]", className)}>{label}</div>;
10
+ };
11
+
12
+ export default PanelLabel;
@@ -0,0 +1,32 @@
1
+ import { Badge, BadgeProps } from "@eqtylab/equality";
2
+
3
+ export type VerificationStateType = "verifying" | "verified" | "unverified" | "partial";
4
+
5
+ interface VerifyStateBadgeProps {
6
+ state: VerificationStateType;
7
+ displayText?: boolean;
8
+ }
9
+
10
+ const getConfig = (state: VerificationStateType): { icon?: string; variant: BadgeProps["variant"]; label: string } => {
11
+ switch (state) {
12
+ case "verifying":
13
+ return { icon: "Ellipsis", variant: "neutral", label: "Verifying" };
14
+ case "verified":
15
+ return { variant: "success", label: "Verified" };
16
+ case "partial":
17
+ return { variant: "warning", label: "Partially Verified" };
18
+ case "unverified":
19
+ return { variant: "danger", label: "Unverified" };
20
+ }
21
+ };
22
+
23
+ const VerifyStateBadge = ({ state, displayText }: VerifyStateBadgeProps) => {
24
+ const { icon, variant, label } = getConfig(state);
25
+ return (
26
+ <Badge variant={variant} icon={icon} display={displayText ? "both" : "icon-only"}>
27
+ {displayText && <span>{label}</span>}
28
+ </Badge>
29
+ );
30
+ };
31
+
32
+ export default VerifyStateBadge;
@@ -0,0 +1,89 @@
1
+ import React, { useEffect, useState } from "react";
2
+ import { ReactFlowProvider } from "reactflow";
3
+ import "@xyflow/react/dist/style.css";
4
+ import LineageFlow from "./components/LineageFlow.tsx";
5
+ import { generateGraphFromStatements, generateGraphFromGraphs } from "./utils/generateGraph.ts";
6
+ import { LineageNode, LineageEdge } from "./components/nodes/types.ts";
7
+ import Sidebar from "./components/sidebar/Sidebar.tsx";
8
+ import { ExplorerConfig } from "./utils/config";
9
+ import { ParsedManifest, UploadVerification } from "@/v-comp-viewer/utils";
10
+
11
+ interface LineageExplorerProps {
12
+ manifestJson: any;
13
+ config: ExplorerConfig;
14
+ customIcons?: Record<string, string>;
15
+ vCompData: ParsedManifest | null;
16
+ vCompVerifyData: UploadVerification | null;
17
+ }
18
+
19
+ const LineageExplorer: React.FC<LineageExplorerProps> = ({
20
+ manifestJson,
21
+ config,
22
+ customIcons,
23
+ vCompData,
24
+ vCompVerifyData,
25
+ }: LineageExplorerProps) => {
26
+ const [nodes, setNodes] = useState<LineageNode[]>([]);
27
+ const [edges, setEdges] = useState<LineageEdge[]>([]);
28
+ const [selectedNode, setSelectedNode] = useState<LineageNode | null>(null);
29
+
30
+ const isSidebarOpen = selectedNode !== null;
31
+
32
+ useEffect(() => {
33
+ if (!manifestJson || Object.keys(manifestJson).length === 0) {
34
+ setNodes([]);
35
+ setEdges([]);
36
+ setSelectedNode(null);
37
+ return;
38
+ }
39
+ setSelectedNode(null);
40
+
41
+ let nodes: LineageNode[];
42
+ let edges: LineageEdge[];
43
+
44
+ // Check if this is the new graph-based manifest format or the legacy format
45
+ if (manifestJson.version == "4") {
46
+ console.log("Processing V4 manifest");
47
+ // New format: manifest has graphs array with nested statements
48
+ [nodes, edges] = generateGraphFromGraphs(config, manifestJson, customIcons);
49
+ } else {
50
+ // Legacy format: manifest has flat statements object
51
+ [nodes, edges] = generateGraphFromStatements(
52
+ config,
53
+ manifestJson.statements,
54
+ manifestJson.blobs,
55
+ manifestJson.anchors,
56
+ customIcons,
57
+ );
58
+ }
59
+
60
+ setNodes(nodes);
61
+ setEdges(edges);
62
+ }, [manifestJson]);
63
+
64
+ return (
65
+ <div
66
+ id="lineage-explorer"
67
+ className="relative size-full bg-background font-sans text-text-primary transition-colors"
68
+ >
69
+ <ReactFlowProvider>
70
+ <LineageFlow
71
+ manifestJson={manifestJson}
72
+ nodes={nodes}
73
+ edges={edges}
74
+ onNodeSelect={setSelectedNode}
75
+ sidebarOpen={isSidebarOpen}
76
+ />
77
+ </ReactFlowProvider>
78
+ <Sidebar
79
+ node={selectedNode}
80
+ onCloseClick={() => setSelectedNode(null)}
81
+ vCompData={vCompData}
82
+ vCompVerifyData={vCompVerifyData}
83
+ />
84
+ <div id="explorer-portal-root" />
85
+ </div>
86
+ );
87
+ };
88
+
89
+ export default LineageExplorer;
@@ -0,0 +1,28 @@
1
+ import { BaseEdge, EdgeProps, getBezierPath } from "reactflow";
2
+ import { motion } from "framer-motion";
3
+ import { LineageEdge } from "./nodes/types.ts";
4
+
5
+ const CustomEdge: React.FC<EdgeProps<LineageEdge["data"]>> = ({
6
+ sourceX,
7
+ sourceY,
8
+ sourcePosition,
9
+ targetX,
10
+ targetY,
11
+ targetPosition,
12
+ data,
13
+ }) => {
14
+ const [edgePath] = getBezierPath({ sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition });
15
+
16
+ return (
17
+ <motion.g
18
+ animate={{ opacity: data?.isInSubGraph ? 1 : 0.1 }}
19
+ initial={{ opacity: 0 }}
20
+ transition={{ duration: 1.5 }}
21
+ className="*:stroke-text-secondary! *:stroke-2!"
22
+ >
23
+ <BaseEdge path={edgePath} />
24
+ </motion.g>
25
+ );
26
+ };
27
+
28
+ export default CustomEdge;
@@ -0,0 +1,220 @@
1
+ import React, { useEffect, useCallback, useState, useRef } from "react";
2
+ import ReactFlow, { useReactFlow, Background, NodeChange } from "reactflow";
3
+ import { CustomNode, ComputationNode } from "./nodes";
4
+ import CustomEdge from "./CustomEdge.tsx";
5
+ import { LineageNode, LineageEdge } from "./nodes/types.ts";
6
+
7
+ const nodeTypes = {
8
+ CustomNode,
9
+ ComputationNode,
10
+ };
11
+ const edgeTypes = { "custom-edge": CustomEdge };
12
+
13
+ interface Props {
14
+ manifestJson: any;
15
+ nodes: LineageNode[];
16
+ edges: LineageEdge[];
17
+ onNodeSelect: (node: LineageNode | null) => void;
18
+ sidebarOpen: boolean;
19
+ }
20
+
21
+ const LineageFlow: React.FC<Props> = ({ nodes, edges, onNodeSelect, sidebarOpen }) => {
22
+ const [graphNodes, setNodes] = useState<LineageNode[]>(nodes);
23
+ const [graphEdges, setEdges] = useState<LineageEdge[]>(edges);
24
+ const reactFlowInstance = useReactFlow();
25
+
26
+ useEffect(() => {
27
+ if (!sidebarOpen) {
28
+ // Handling closed sidebar
29
+ setActiveSubGraph(null);
30
+ }
31
+ }, [sidebarOpen]);
32
+
33
+ useEffect(() => {
34
+ setNodes(nodes);
35
+ }, [nodes]);
36
+
37
+ useEffect(() => {
38
+ setEdges(edges);
39
+ }, [edges]);
40
+
41
+ // Finds the node that was selected and passes it to the onNodeSelect callback
42
+ const handleNodesChange = useCallback(
43
+ (changes: NodeChange[]) => {
44
+ for (const change of changes) {
45
+ if (change.type === "select" && change.selected) {
46
+ const node = nodes.find((n) => n.id === change.id);
47
+ if (node) {
48
+ setActiveSubGraph(node);
49
+ onNodeSelect(node);
50
+ // Delay subgraph update to avoid UI jump
51
+ setTimeout(() => zoomToNode(node), 300);
52
+ }
53
+ }
54
+ }
55
+ },
56
+ [graphNodes],
57
+ );
58
+
59
+ // Returns the IDs of all ancestor and descendant nodes and edges in the lineage chain
60
+ const findNeighbors = (nodeId: string | null): string[] => {
61
+ if (!nodeId) return [];
62
+
63
+ const visitedNodes = new Set<string>();
64
+ const visitedEdges = new Set<string>();
65
+ const allConnectedIds: string[] = [];
66
+
67
+ const traverseAncestors = (currentNodeId: string) => {
68
+ if (visitedNodes.has(currentNodeId)) return;
69
+
70
+ visitedNodes.add(currentNodeId);
71
+
72
+ // Find edges where the current node is the TARGET (meaning we go backwards in lineage)
73
+ const incomingEdges = edges.filter((edge) => edge.target === currentNodeId);
74
+
75
+ for (const edge of incomingEdges) {
76
+ if (!visitedEdges.has(edge.id)) {
77
+ visitedEdges.add(edge.id);
78
+ allConnectedIds.push(edge.id);
79
+
80
+ // Get the source node (the ancestor)
81
+ const ancestorNodeId = edge.source;
82
+
83
+ // Recursively traverse the ancestor node
84
+ traverseAncestors(ancestorNodeId);
85
+ }
86
+ }
87
+ };
88
+
89
+ const traverseDescendants = (currentNodeId: string) => {
90
+ if (visitedNodes.has(currentNodeId)) return;
91
+
92
+ visitedNodes.add(currentNodeId);
93
+
94
+ // Find edges where the current node is the SOURCE (meaning we go forwards in lineage)
95
+ const outgoingEdges = edges.filter((edge) => edge.source === currentNodeId);
96
+
97
+ for (const edge of outgoingEdges) {
98
+ if (!visitedEdges.has(edge.id)) {
99
+ visitedEdges.add(edge.id);
100
+ allConnectedIds.push(edge.id);
101
+
102
+ // Get the target node (the descendant)
103
+ const descendantNodeId = edge.target;
104
+
105
+ // Recursively traverse the descendant node
106
+ traverseDescendants(descendantNodeId);
107
+ }
108
+ }
109
+ };
110
+
111
+ // First traverse ancestors (this will mark the original node as visited)
112
+ traverseAncestors(nodeId);
113
+
114
+ // Then traverse descendants (starting fresh from the original node)
115
+ // We need to reset the visited state for the original node to allow descendant traversal
116
+ visitedNodes.delete(nodeId);
117
+ traverseDescendants(nodeId);
118
+
119
+ // Add all visited node IDs (excluding the original node)
120
+ const connectedNodeIds = Array.from(visitedNodes).filter((id) => id !== nodeId);
121
+
122
+ return [...allConnectedIds, ...connectedNodeIds];
123
+ };
124
+
125
+ // Updates Node and Edge Property `isInSubGraph`
126
+ const setActiveSubGraph = (node: LineageNode | null) => {
127
+ let updatedNodes: LineageNode[] = [];
128
+ let updatedEdges: LineageEdge[] = [];
129
+ if (!node) {
130
+ // If no node is selected, all elements are 'active' in the graph
131
+ updatedNodes = nodes.map((n) => ({
132
+ ...n,
133
+ data: {
134
+ ...n.data,
135
+ isInSubGraph: true,
136
+ },
137
+ }));
138
+ updatedEdges = edges.map((edge) => ({
139
+ ...edge,
140
+ data: {
141
+ ...edge.data,
142
+ isInSubGraph: true,
143
+ },
144
+ }));
145
+ } else {
146
+ // If the node or edge is a neighbor to the selected node, set isInSubGraph = true, else set it to false
147
+ const neighbors = findNeighbors(node.id);
148
+
149
+ updatedNodes = nodes.map((n) => ({
150
+ ...n,
151
+ data: {
152
+ ...n.data,
153
+ isInSubGraph: neighbors.includes(n.id) || n.id === node.id,
154
+ isActive: n.id === node.id,
155
+ },
156
+ }));
157
+ updatedEdges = edges.map((edge) => ({
158
+ ...edge,
159
+ data: {
160
+ ...edge.data,
161
+ isInSubGraph: neighbors.includes(edge.id),
162
+ },
163
+ }));
164
+ }
165
+
166
+ setNodes(updatedNodes);
167
+ setEdges(updatedEdges);
168
+ };
169
+
170
+ // moves the graph to the center of the screen on load
171
+ const hasFitViewRun = useRef(false);
172
+ // Reset whenever the nodes prop changes (meaning, new data)
173
+ useEffect(() => {
174
+ hasFitViewRun.current = false;
175
+ }, [nodes]);
176
+ useEffect(() => {
177
+ if (!reactFlowInstance || hasFitViewRun.current || nodes.length === 0) return;
178
+
179
+ hasFitViewRun.current = true;
180
+
181
+ const timeout = setTimeout(() => {
182
+ reactFlowInstance.fitView({
183
+ duration: 1200,
184
+ padding: 0.3,
185
+ });
186
+ }, 100);
187
+
188
+ return () => clearTimeout(timeout);
189
+ }, [reactFlowInstance, nodes]);
190
+
191
+ const zoomToNode = (node: LineageNode) => {
192
+ if (!reactFlowInstance) return;
193
+
194
+ const sidebarOffset = 250;
195
+ reactFlowInstance.setCenter(node.position.x + sidebarOffset, node.position.y, { zoom: 1.05, duration: 1000 });
196
+ };
197
+
198
+ return (
199
+ <div className="size-full">
200
+ <ReactFlow
201
+ nodes={graphNodes}
202
+ edges={graphEdges}
203
+ onNodesChange={handleNodesChange}
204
+ snapToGrid={false}
205
+ defaultViewport={{ x: -500, y: -500, zoom: 0.5 }}
206
+ minZoom={0.05}
207
+ proOptions={{ hideAttribution: true }}
208
+ nodeTypes={nodeTypes}
209
+ edgeTypes={edgeTypes}
210
+ nodesDraggable={false}
211
+ nodesConnectable={false}
212
+ edgesFocusable={false}
213
+ >
214
+ <Background gap={12} size={1} />
215
+ </ReactFlow>
216
+ </div>
217
+ );
218
+ };
219
+
220
+ export default LineageFlow;
@@ -0,0 +1,26 @@
1
+ import React from "react";
2
+ import { BuiltInIcons } from "@/explorer-component/icons";
3
+
4
+ interface NodeIconLoaderProps {
5
+ path: string;
6
+ className?: string;
7
+ iconMap?: Record<string, string | React.FC<React.SVGProps<SVGSVGElement>>>;
8
+ }
9
+
10
+ const NodeIconLoader: React.FC<NodeIconLoaderProps> = ({ path, className, iconMap }) => {
11
+ const iconEntry = iconMap?.[path] ?? BuiltInIcons[path];
12
+
13
+ if (!iconEntry) {
14
+ console.error(`Icon "${path}" not found.`);
15
+ return null;
16
+ }
17
+
18
+ if (typeof iconEntry === "string") {
19
+ return <img src={iconEntry} className={className} alt={path} />;
20
+ }
21
+
22
+ const SvgComponent = iconEntry;
23
+ return <SvgComponent className={className} />;
24
+ };
25
+
26
+ export default NodeIconLoader;
@@ -0,0 +1,56 @@
1
+ import { motion } from "framer-motion";
2
+ import { Handle, Position, NodeProps } from "reactflow";
3
+
4
+ import { cn } from "@/lib/utils.ts";
5
+
6
+ import { NODES_STYLES } from "./nodes.tsx";
7
+
8
+ import { LineageNode } from "./types.ts";
9
+
10
+ export const ComputationNode: React.FC<NodeProps<LineageNode["data"]>> = ({ data }) => {
11
+ const { hasPrecedingNode, hasSuccessorNode, isInSubGraph, isActive } = data;
12
+
13
+ return (
14
+ <motion.div
15
+ animate={{ opacity: isInSubGraph ? 1 : 0.1 }}
16
+ initial={{ opacity: 0 }}
17
+ transition={{ duration: 1.5 }}
18
+ >
19
+ <div className="relative z-20">
20
+ <div
21
+ className={cn(
22
+ "flex size-14 items-center justify-center rounded-full bg-resource-compute border hover:border-resource-compute transition-all",
23
+ isActive ? "border-resource-compute" : "border-background",
24
+ )}
25
+ >
26
+ {NODES_STYLES["computation"].icon}
27
+ </div>
28
+
29
+ {hasPrecedingNode && (
30
+ <Handle
31
+ type="target"
32
+ position={Position.Left}
33
+ id="left"
34
+ className="-translate-x-full! -translate-y-1/2! border! border-text-primary! bg-card-background!"
35
+ />
36
+ )}
37
+ {hasSuccessorNode && (
38
+ <Handle
39
+ type="source"
40
+ position={Position.Right}
41
+ id="source"
42
+ className="-translate-y-1/2! translate-x-full! border! border-text-primary! bg-card-background!"
43
+ />
44
+ )}
45
+ </div>
46
+ <motion.div
47
+ className="absolute inset-0 size-full opacity-50"
48
+ animate={{ scale: isActive ? 1 : 0 }}
49
+ initial={{ scale: 0 }}
50
+ transition={{ duration: 1 }}
51
+ >
52
+ <div className="rounded-full size-full scale-[1.25] animate-pulse bg-resource-compute" />
53
+ </motion.div>
54
+ </motion.div>
55
+ );
56
+ };
@@ -0,0 +1,112 @@
1
+ import { useState, useEffect } from "react";
2
+ import { motion } from "framer-motion";
3
+ import { Handle, Position, useReactFlow, NodeProps } from "reactflow";
4
+
5
+ import { LineageNode } from "./types.ts";
6
+ import { cn } from "@/lib/utils.ts";
7
+ import { NODES_STYLES } from "./nodes.tsx";
8
+
9
+ import PanelLabel from "@/components/ui/panel-label.tsx";
10
+
11
+ export const CustomNode: React.FC<NodeProps<LineageNode["data"]>> = ({ data }) => {
12
+ const { getZoom } = useReactFlow();
13
+ const [showLabel, setShowLabel] = useState(true);
14
+
15
+ const { label, displayType, hasSuccessorNode, hasPrecedingNode, isInSubGraph, isActive } = data;
16
+
17
+ const maxLength = 19;
18
+ const nodeLabel = label.length > maxLength ? label.substring(0, maxLength - 3) + "..." : label;
19
+
20
+ useEffect(() => {
21
+ const handleZoom = () => {
22
+ const zoom = getZoom();
23
+ setShowLabel(zoom > 0.5);
24
+ };
25
+ // Polling as zoom change listener isn't exposed directly
26
+ const interval = setInterval(handleZoom, 300);
27
+ return () => clearInterval(interval);
28
+ }, [getZoom]);
29
+
30
+ const nodeType = (() => {
31
+ if (!displayType) return "default";
32
+ switch (displayType) {
33
+ case "Unknown":
34
+ return "default";
35
+ default:
36
+ return displayType.toLowerCase();
37
+ }
38
+ })() as keyof typeof NODES_STYLES;
39
+
40
+ const nodeStyle = NODES_STYLES[nodeType] || NODES_STYLES["default"];
41
+
42
+ return (
43
+ <motion.div
44
+ animate={{ opacity: isInSubGraph ? 1 : 0.1 }}
45
+ initial={{ opacity: 0 }}
46
+ transition={{ duration: 1.5 }}
47
+ >
48
+ <div
49
+ className={cn(
50
+ "relative border border-card-border bg-card-background p-2 transition-all rounded-full z-20",
51
+ nodeStyle.hoverBorderColor,
52
+ isActive ? nodeStyle.activeStyle : "",
53
+ )}
54
+ >
55
+ <div className="grid w-50 min-w-50 grid-cols-[auto_1fr] items-center gap-3">
56
+ <div
57
+ className={cn(
58
+ "flex size-10 items-center justify-center rounded-full",
59
+ nodeStyle.backgroundColor,
60
+ )}
61
+ >
62
+ {nodeStyle.icon}
63
+ </div>
64
+ <div className="relative">
65
+ <div className={cn("transition-all", showLabel ? "opacity-100" : "opacity-0")}>
66
+ <PanelLabel label={displayType} className="opacity-70" />
67
+ <p className="mt-0.5 line-clamp-2 text-sm leading-[1.1] text-foreground">{nodeLabel}</p>
68
+ </div>
69
+ <div
70
+ className={cn(
71
+ "transition-all absolute top-1/2 left-0 -translate-y-1/2",
72
+ !showLabel ? "opacity-100" : "opacity-0",
73
+ )}
74
+ >
75
+ <p>{displayType}</p>
76
+ </div>
77
+ </div>
78
+ </div>
79
+
80
+ {hasPrecedingNode && (
81
+ <Handle
82
+ type="target"
83
+ position={Position.Left}
84
+ id="left"
85
+ className="-translate-x-full! -translate-y-1/2! border! border-text-secondary! bg-card-background!"
86
+ />
87
+ )}
88
+ {hasSuccessorNode && (
89
+ <Handle
90
+ type="source"
91
+ position={Position.Right}
92
+ id="source"
93
+ className="-translate-y-1/2! translate-x-full! border! border-text-secondary! bg-card-background!"
94
+ />
95
+ )}
96
+ </div>
97
+ <motion.div
98
+ className={cn("absolute inset-0 size-full opacity-50")}
99
+ animate={{ scale: isActive ? 1 : 0 }}
100
+ initial={{ scale: 0 }}
101
+ transition={{ duration: 1 }}
102
+ >
103
+ <div
104
+ className={cn(
105
+ "rounded-full size-full scale-x-[1.06] scale-y-[1.20] animate-pulse",
106
+ nodeStyle.backgroundColor,
107
+ )}
108
+ />
109
+ </motion.div>
110
+ </motion.div>
111
+ );
112
+ };
@@ -0,0 +1,4 @@
1
+ import { ComputationNode } from "./ComputationNode";
2
+ import { CustomNode } from "./CustomNode";
3
+
4
+ export { ComputationNode, CustomNode };