@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.
- package/.env.api +6 -0
- package/.env.iframe +2 -0
- package/.env.manifest +2 -0
- package/.envrc +1 -0
- package/.eslintrc.cjs +17 -0
- package/.github/workflows/ci.yml +46 -0
- package/.github/workflows/publish.yml +44 -0
- package/.husky/pre-push +2 -0
- package/.prettierignore +2 -0
- package/.prettierrc.json +13 -0
- package/LICENSE +201 -0
- package/README.md +84 -0
- package/flake.lock +61 -0
- package/flake.nix +23 -0
- package/index.html +13 -0
- package/package.json +93 -0
- package/postcss.config.js +5 -0
- package/public/logos/eci.svg +23 -0
- package/public/logos/eqty.svg +3 -0
- package/public/manifests/B-200-merged.json +773 -0
- package/public/manifests/amd-sev.json +426 -0
- package/public/manifests/anchors.json +609 -0
- package/public/manifests/association-no-metadata.json +1393 -0
- package/public/manifests/association.json +1402 -0
- package/public/manifests/eqty-docker.json +562 -0
- package/public/manifests/governance.json +1324 -0
- package/public/manifests/intel-tdx-with-nvidia.json +441 -0
- package/public/manifests/intel-tdx.json +404 -0
- package/public/manifests/iroh-collections.json +1285 -0
- package/public/manifests/model-signing.json +1536 -0
- package/public/manifests/multiple-registrations.json +129 -0
- package/public/manifests/no-vcs.json +114 -0
- package/public/manifests/storage.json +294 -0
- package/public/manifests/sub-graphs.json +1 -0
- package/src/animations/variants.ts +52 -0
- package/src/app/apiClient/integrityService/fetchAttributes.ts +29 -0
- package/src/app/apiClient/integrityService/fetchManifest.ts +28 -0
- package/src/app/apiClient/integrityService/types.ts +12 -0
- package/src/app/components/integrityService/IntegrityService.tsx +105 -0
- package/src/app/components/integrityService/components/AuthButton.tsx +26 -0
- package/src/app/components/integrityService/components/FiltersModal.tsx +214 -0
- package/src/app/components/integrityService/components/FiltersToggle.tsx +28 -0
- package/src/app/components/manifestViewer/ManifestViewer.tsx +90 -0
- package/src/app/components/manifestViewer/components/ManifestButtons.tsx +215 -0
- package/src/app/config.json +23 -0
- package/src/app/config.ts +28 -0
- package/src/app/pages/index.tsx +34 -0
- package/src/app/stores/manifestStore.ts +19 -0
- package/src/app/utils/appMode.ts +37 -0
- package/src/assets/fonts/TWKLausanne-300.woff +0 -0
- package/src/assets/fonts/TWKLausanne-300.woff2 +0 -0
- package/src/assets/fonts/TWKLausanne-400.woff +0 -0
- package/src/assets/fonts/TWKLausanne-400.woff2 +0 -0
- package/src/assets/fonts/TWKLausanne-500.woff +0 -0
- package/src/assets/fonts/TWKLausanne-500.woff2 +0 -0
- package/src/assets/fonts/TWKLausanne-600.woff +0 -0
- package/src/assets/fonts/TWKLausanne-600.woff2 +0 -0
- package/src/assets/vectors/eqtylab.svg +6 -0
- package/src/components/app-header.tsx +61 -0
- package/src/components/dark-mode-switch.tsx +16 -0
- package/src/components/ui/accordion.tsx +47 -0
- package/src/components/ui/certificate-chain.tsx +60 -0
- package/src/components/ui/certificate-code.tsx +39 -0
- package/src/components/ui/details.tsx +20 -0
- package/src/components/ui/display-code.tsx +47 -0
- package/src/components/ui/panel-label.tsx +12 -0
- package/src/components/ui/verify-state-badge.tsx +32 -0
- package/src/explorer-component/LineageExplorer.tsx +89 -0
- package/src/explorer-component/components/CustomEdge.tsx +28 -0
- package/src/explorer-component/components/LineageFlow.tsx +220 -0
- package/src/explorer-component/components/NodeIconLoader.tsx +26 -0
- package/src/explorer-component/components/nodes/ComputationNode.tsx +56 -0
- package/src/explorer-component/components/nodes/CustomNode.tsx +112 -0
- package/src/explorer-component/components/nodes/index.ts +4 -0
- package/src/explorer-component/components/nodes/nodes.tsx +144 -0
- package/src/explorer-component/components/nodes/types.ts +167 -0
- package/src/explorer-component/components/sidebar/SectionList.tsx +54 -0
- package/src/explorer-component/components/sidebar/SectionTitle.tsx +10 -0
- package/src/explorer-component/components/sidebar/Sidebar.tsx +70 -0
- package/src/explorer-component/components/sidebar/SidebarWrapper.tsx +76 -0
- package/src/explorer-component/components/sidebar/sections/CustomSection.tsx +238 -0
- package/src/explorer-component/components/sidebar/sections/FileViewer.tsx +229 -0
- package/src/explorer-component/components/sidebar/sections/GovernanceSection.tsx +52 -0
- package/src/explorer-component/components/sidebar/sections/RegistrationSection.tsx +254 -0
- package/src/explorer-component/components/sidebar/sections/SigstoreSection.tsx +37 -0
- package/src/explorer-component/components/sidebar/sections/StorageSection.tsx +73 -0
- package/src/explorer-component/components/sidebar/sections/custom/IrohCollectionsView.tsx +139 -0
- package/src/explorer-component/components/sidebar/sections/registration/VerificationIcon.tsx +108 -0
- package/src/explorer-component/components/sidebar/sections/registration/actor/ActorBadge.tsx +62 -0
- package/src/explorer-component/components/sidebar/sections/registration/actor/svg/AMDLogo.tsx +40 -0
- package/src/explorer-component/components/sidebar/sections/registration/actor/svg/AzureLogo.tsx +65 -0
- package/src/explorer-component/components/sidebar/sections/registration/actor/svg/IntelLogo.tsx +24 -0
- package/src/explorer-component/components/sidebar/sections/registration/actor/svg/NvidiaLogo.tsx +27 -0
- package/src/explorer-component/components/sidebar/sections/registration/verifierClient.ts +13 -0
- package/src/explorer-component/icons/AttributionIcon.svg +6 -0
- package/src/explorer-component/icons/BenchmarkIcon.svg +6 -0
- package/src/explorer-component/icons/BenchmarkResultIcon.svg +6 -0
- package/src/explorer-component/icons/CertificateIcon.svg +6 -0
- package/src/explorer-component/icons/CodeIcon.svg +6 -0
- package/src/explorer-component/icons/ComputationIcon.svg +6 -0
- package/src/explorer-component/icons/DataIcon.svg +6 -0
- package/src/explorer-component/icons/DatabaseIcon.svg +6 -0
- package/src/explorer-component/icons/DatasetIcon.svg +6 -0
- package/src/explorer-component/icons/DeltaLakeIcon.svg +7 -0
- package/src/explorer-component/icons/DocumentIcon.svg +6 -0
- package/src/explorer-component/icons/MediaIcon.svg +6 -0
- package/src/explorer-component/icons/ModelIcon.svg +6 -0
- package/src/explorer-component/icons/TeamIcon.svg +3 -0
- package/src/explorer-component/icons/TokenIcon.svg +6 -0
- package/src/explorer-component/icons/UnknownIcon.svg +6 -0
- package/src/explorer-component/icons/index.ts +35 -0
- package/src/explorer-component/index.ts +5 -0
- package/src/explorer-component/integrityTypes/AnchorTypes.ts +31 -0
- package/src/explorer-component/integrityTypes/StatementTypes.ts +333 -0
- package/src/explorer-component/integrityTypes/VCTypes.ts +85 -0
- package/src/explorer-component/integrityTypes/types.ts +3 -0
- package/src/explorer-component/utils/config.ts +163 -0
- package/src/explorer-component/utils/formatTimestamp.ts +5 -0
- package/src/explorer-component/utils/generateGraph.ts +271 -0
- package/src/explorer-component/utils/layoutGraph.ts +148 -0
- package/src/explorer-component/utils/resolveIrohBlobs.ts +117 -0
- package/src/explorer-component/utils/statementHelpers.ts +252 -0
- package/src/explorer-component/utils/stripPrefix.ts +4 -0
- package/src/hooks/use-mobile.tsx +19 -0
- package/src/index.ts +3 -0
- package/src/lib/resource-types.ts +31 -0
- package/src/lib/use-escape-key.tsx +16 -0
- package/src/lib/utils.ts +19 -0
- package/src/main.css +82 -0
- package/src/main.tsx +39 -0
- package/src/v-comp-viewer/components/container-did-details.tsx +53 -0
- package/src/v-comp-viewer/components/system-did-details.tsx +356 -0
- package/src/v-comp-viewer/useVComp.ts +196 -0
- package/src/v-comp-viewer/utils.tsx +824 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.json +33 -0
- package/tsconfig.node.json +10 -0
- package/vercel.json +13 -0
- package/vite.app.config.ts +48 -0
- 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
|
+
};
|