@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,271 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LineageEdge,
|
|
3
|
+
LineageNode,
|
|
4
|
+
ComputeNodeSidebarData,
|
|
5
|
+
DataNodeSidebarData,
|
|
6
|
+
createEdge,
|
|
7
|
+
createDefaultNode,
|
|
8
|
+
IrohData,
|
|
9
|
+
DataNodeRegistration,
|
|
10
|
+
MetadataLineage,
|
|
11
|
+
StorageLineage,
|
|
12
|
+
Subgraph,
|
|
13
|
+
SubgraphInfo,
|
|
14
|
+
GraphManifest,
|
|
15
|
+
} from "@/explorer-component/components/nodes/types";
|
|
16
|
+
import { resolveIrohCollections } from "./resolveIrohBlobs";
|
|
17
|
+
import { stripCidPrefix } from "./stripPrefix.ts";
|
|
18
|
+
|
|
19
|
+
import { layoutNodes as layoutNodes } from "./layoutGraph.ts";
|
|
20
|
+
import { sortStatements, indexStatements } from "./statementHelpers.ts";
|
|
21
|
+
import {
|
|
22
|
+
Anchor,
|
|
23
|
+
ComputationStatement,
|
|
24
|
+
ComputationStatementFromJSON,
|
|
25
|
+
DataStatement,
|
|
26
|
+
DataStatementFromJSON,
|
|
27
|
+
VcStatement,
|
|
28
|
+
RegistrationTypes,
|
|
29
|
+
GovernanceStatement,
|
|
30
|
+
SigstoreBundle,
|
|
31
|
+
} from "@/explorer-component/integrityTypes/types.ts";
|
|
32
|
+
import { ExplorerConfig } from "./config.ts";
|
|
33
|
+
|
|
34
|
+
/// Iterate over graphs and their statements to create the ReactFlow nodes and edges
|
|
35
|
+
/// Handles the new manifest format with graphs containing statements
|
|
36
|
+
export function generateGraphFromGraphs(
|
|
37
|
+
config: ExplorerConfig,
|
|
38
|
+
manifest: GraphManifest,
|
|
39
|
+
customIcons?: Record<string, string>,
|
|
40
|
+
): [LineageNode[], LineageEdge[]] {
|
|
41
|
+
const allStatements: Record<string, object> = {};
|
|
42
|
+
|
|
43
|
+
for (const graph of manifest.graphs) {
|
|
44
|
+
for (const statement of graph.statements) {
|
|
45
|
+
const statementId = (statement as any)["@id"];
|
|
46
|
+
if (statementId) {
|
|
47
|
+
const graphInfo: SubgraphInfo = {
|
|
48
|
+
name: graph.name,
|
|
49
|
+
id: graph.id,
|
|
50
|
+
parent: graph.parent,
|
|
51
|
+
};
|
|
52
|
+
(statement as any)["graph"] = graphInfo;
|
|
53
|
+
allStatements[statementId] = statement;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Use the existing function with the flattened statements
|
|
59
|
+
return generateGraphFromStatements(config, allStatements, manifest.blobs, manifest.anchors, customIcons);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// Iterate over all the statements to create the ReactFlow nodes and edges
|
|
63
|
+
/// related info (VCs, metadata, etc) is attached to the ReactFlow node
|
|
64
|
+
export function generateGraphFromStatements(
|
|
65
|
+
config: ExplorerConfig,
|
|
66
|
+
statements: Record<string, object>,
|
|
67
|
+
blobs: Record<string, string>,
|
|
68
|
+
anchors?: Anchor[],
|
|
69
|
+
customIcons?: Record<string, string>,
|
|
70
|
+
): [LineageNode[], LineageEdge[]] {
|
|
71
|
+
// The Nodes to diplay in the graph
|
|
72
|
+
const nodesMap: Map<string, LineageNode> = new Map();
|
|
73
|
+
// The edges in the graph
|
|
74
|
+
let edgesMap: LineageEdge[] = [];
|
|
75
|
+
|
|
76
|
+
const irohCollections = resolveIrohCollections(blobs);
|
|
77
|
+
|
|
78
|
+
const sortedStatements = sortStatements(statements);
|
|
79
|
+
|
|
80
|
+
// Map of metadata that has already been resolved. Key is metadataSubjectID.
|
|
81
|
+
const { metadataLineage, vcs, actors, storage, governance, sigstoreBundles } = indexStatements(
|
|
82
|
+
sortedStatements,
|
|
83
|
+
blobs,
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
for (const [cid, statement] of sortedStatements) {
|
|
87
|
+
try {
|
|
88
|
+
// @ts-ignore
|
|
89
|
+
switch (statement["@type"]) {
|
|
90
|
+
case RegistrationTypes.Computation: {
|
|
91
|
+
const computeStatement: ComputationStatement = ComputationStatementFromJSON(statement);
|
|
92
|
+
|
|
93
|
+
const [computeNodes, computeEdges] = createComputeNode(
|
|
94
|
+
config,
|
|
95
|
+
computeStatement,
|
|
96
|
+
metadataLineage,
|
|
97
|
+
vcs,
|
|
98
|
+
actors,
|
|
99
|
+
irohCollections,
|
|
100
|
+
anchors,
|
|
101
|
+
customIcons,
|
|
102
|
+
governance,
|
|
103
|
+
(statement as any)["graph"],
|
|
104
|
+
);
|
|
105
|
+
edgesMap = [...edgesMap, ...computeEdges];
|
|
106
|
+
for (const node of computeNodes) {
|
|
107
|
+
if (!nodesMap.has(node.id)) {
|
|
108
|
+
// fail safe to create the data node for inputs or outputs that don't have a data registration statement
|
|
109
|
+
nodesMap.set(node.id, node);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
case RegistrationTypes.Data: {
|
|
115
|
+
const dataStatement: DataStatement = DataStatementFromJSON(statement);
|
|
116
|
+
|
|
117
|
+
if (nodesMap.has(dataStatement.data)) {
|
|
118
|
+
const registeredByActor = actors.get(dataStatement.registeredBy);
|
|
119
|
+
const vcStatement = vcs.get(dataStatement.id);
|
|
120
|
+
const anchor = anchors?.find((anchor) => anchor.statements.includes(dataStatement.id));
|
|
121
|
+
|
|
122
|
+
const registration: DataNodeRegistration = {
|
|
123
|
+
statement: dataStatement,
|
|
124
|
+
registeredByActor,
|
|
125
|
+
anchors: anchor,
|
|
126
|
+
vcs: vcStatement,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const existingNode = nodesMap.get(dataStatement.data);
|
|
130
|
+
const sidebarData = existingNode?.data.sidebarData as DataNodeSidebarData;
|
|
131
|
+
if (sidebarData && "registrations" in sidebarData) {
|
|
132
|
+
// Node already has a DataRegistration statement. Append this one to that list.
|
|
133
|
+
const existingRegistrations = sidebarData.registrations ?? [];
|
|
134
|
+
sidebarData.registrations = [...existingRegistrations, registration];
|
|
135
|
+
}
|
|
136
|
+
} else {
|
|
137
|
+
const irohCollection = irohCollections.get(stripCidPrefix(dataStatement.data)) || null;
|
|
138
|
+
const sourceData = irohCollection ? irohCollection : blobs[stripCidPrefix(dataStatement.data)];
|
|
139
|
+
|
|
140
|
+
const dataNode = createDataNode(
|
|
141
|
+
config,
|
|
142
|
+
dataStatement,
|
|
143
|
+
metadataLineage,
|
|
144
|
+
vcs,
|
|
145
|
+
actors,
|
|
146
|
+
storage,
|
|
147
|
+
sourceData,
|
|
148
|
+
anchors,
|
|
149
|
+
customIcons,
|
|
150
|
+
governance,
|
|
151
|
+
sigstoreBundles,
|
|
152
|
+
(statement as any)["graph"],
|
|
153
|
+
);
|
|
154
|
+
nodesMap.set(dataNode.id, dataNode);
|
|
155
|
+
}
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
default:
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
} catch (error) {
|
|
162
|
+
console.error(`Failed to parse statement for ID ${cid}:`, error);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const nodes: LineageNode[] = Array.from(nodesMap.values());
|
|
167
|
+
return layoutNodes(nodes, edgesMap);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/// Creates a single data node. If the nodeID already exists, it is updated with this DataStatement data.
|
|
171
|
+
function createDataNode(
|
|
172
|
+
config: ExplorerConfig,
|
|
173
|
+
statement: DataStatement,
|
|
174
|
+
// metadataJsonMap: Map<string, any[]>,
|
|
175
|
+
metadataLineage: Map<string, MetadataLineage[]>,
|
|
176
|
+
vcs: Map<string, VcStatement[]>,
|
|
177
|
+
actors: Map<string, any>,
|
|
178
|
+
storage: Map<string, StorageLineage>,
|
|
179
|
+
sourceData?: IrohData[] | string,
|
|
180
|
+
anchors?: Anchor[],
|
|
181
|
+
customIcons?: Record<string, string>,
|
|
182
|
+
governance?: Map<string, GovernanceStatement>,
|
|
183
|
+
sigstoreBundles?: Map<string, SigstoreBundle[]>,
|
|
184
|
+
graph?: Subgraph,
|
|
185
|
+
): LineageNode {
|
|
186
|
+
// Use the first metadata info for the node display
|
|
187
|
+
const metadata = metadataLineage.get(statement.data)?.[0].metadata as { assetType: string; name: string };
|
|
188
|
+
const nodeType = metadata["assetType"];
|
|
189
|
+
const nodeLabel = metadata["name"] ? metadata["name"] : statement.data.slice(-6);
|
|
190
|
+
|
|
191
|
+
const nodeConfig = config.nodes.get(nodeType) || config.fallbackConfig;
|
|
192
|
+
if (nodeConfig == config.fallbackConfig && nodeType) {
|
|
193
|
+
nodeConfig.displayType = nodeType;
|
|
194
|
+
}
|
|
195
|
+
const node = createDefaultNode(statement.data, nodeConfig, nodeLabel, customIcons, graph);
|
|
196
|
+
|
|
197
|
+
const registeredByActor = actors.get(statement.registeredBy);
|
|
198
|
+
const vcStatement = vcs.get(statement.id);
|
|
199
|
+
const anchor = anchors?.find((anchor) => anchor.statements.includes(statement.id));
|
|
200
|
+
|
|
201
|
+
const registration: DataNodeRegistration = { statement, registeredByActor, anchors: anchor, vcs: vcStatement };
|
|
202
|
+
const sidebarData: DataNodeSidebarData = {
|
|
203
|
+
registrations: [registration],
|
|
204
|
+
metadataLineage: metadataLineage.get(statement.data),
|
|
205
|
+
sourceData: sourceData,
|
|
206
|
+
storage: storage.get(statement.data),
|
|
207
|
+
governance: governance?.get(statement.data),
|
|
208
|
+
sigstoreBundles: sigstoreBundles?.get(statement.data),
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
node.data.sidebarData = sidebarData;
|
|
212
|
+
return node;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/// Creates a Compute node (and all edges) and generic Data nodes for all the inputs and outputs.
|
|
216
|
+
/// Returns the created Nodes and Edges
|
|
217
|
+
function createComputeNode(
|
|
218
|
+
config: ExplorerConfig,
|
|
219
|
+
statement: ComputationStatement,
|
|
220
|
+
metadataLineage: Map<string, MetadataLineage[]>,
|
|
221
|
+
vcs: Map<string, VcStatement[]>,
|
|
222
|
+
actors: Map<string, any>,
|
|
223
|
+
irohCollections: Map<string, IrohData[]>,
|
|
224
|
+
anchors?: Anchor[],
|
|
225
|
+
customIcons?: Record<string, string>,
|
|
226
|
+
governance?: Map<string, GovernanceStatement>,
|
|
227
|
+
graph?: Subgraph,
|
|
228
|
+
): [LineageNode[], LineageEdge[]] {
|
|
229
|
+
const nodeConfig = config.nodes.get("Computation") || config.fallbackConfig;
|
|
230
|
+
const nodes: LineageNode[] = [];
|
|
231
|
+
const computeNode = createDefaultNode(statement.id, nodeConfig, "compute", customIcons, graph);
|
|
232
|
+
const sourceData = statement.computation
|
|
233
|
+
? irohCollections.get(stripCidPrefix(statement.computation)) || undefined
|
|
234
|
+
: undefined;
|
|
235
|
+
|
|
236
|
+
const sideBarData: ComputeNodeSidebarData = {
|
|
237
|
+
statement: statement,
|
|
238
|
+
metadataLineage: metadataLineage.get(statement.id), // can only be one metadata for a compute
|
|
239
|
+
vcStatement: vcs.get(statement.id),
|
|
240
|
+
registeredByActor: actors.get(statement.registeredBy),
|
|
241
|
+
executedOnActor: statement.executedOn && actors.get(statement.executedOn),
|
|
242
|
+
operatedByActor: statement.operatedBy && actors.get(statement.operatedBy),
|
|
243
|
+
sourceData: sourceData,
|
|
244
|
+
anchor: anchors?.find((anchor) => anchor.statements.includes(statement.id)),
|
|
245
|
+
governance: governance?.get(statement.id),
|
|
246
|
+
};
|
|
247
|
+
computeNode.data.sidebarData = sideBarData;
|
|
248
|
+
|
|
249
|
+
nodes.push(computeNode);
|
|
250
|
+
|
|
251
|
+
const inputs = Array.isArray(statement.input) ? statement.input : [statement.input];
|
|
252
|
+
const outputs = Array.isArray(statement.output) ? statement.output : [statement.output];
|
|
253
|
+
|
|
254
|
+
const defaultDataNodeConfig = config.nodes.get("Data") || config.fallbackConfig;
|
|
255
|
+
const inputEdges: LineageEdge[] = inputs.map((input: string) => {
|
|
256
|
+
// Create default input nodes in case they don't have a DataRegistration
|
|
257
|
+
const inputNode = createDefaultNode(input, defaultDataNodeConfig, "data");
|
|
258
|
+
nodes.push(inputNode);
|
|
259
|
+
return createEdge(input, statement.id);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const outputEdges = outputs.map((output: string) => {
|
|
263
|
+
const outputNode = createDefaultNode(output, defaultDataNodeConfig, "data");
|
|
264
|
+
nodes.push(outputNode);
|
|
265
|
+
return createEdge(statement.id, output);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const edges = [...inputEdges, ...outputEdges];
|
|
269
|
+
|
|
270
|
+
return [nodes, edges];
|
|
271
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { LineageEdge, LineageNode } from "../components/nodes/types";
|
|
2
|
+
import dagre from "@dagrejs/dagre";
|
|
3
|
+
|
|
4
|
+
/// Positions all the nodes and edges
|
|
5
|
+
export const layoutNodes = (nodes: LineageNode[], edges: LineageEdge[]): [LineageNode[], LineageEdge[]] => {
|
|
6
|
+
const verticalGapBetweenNodes = 30;
|
|
7
|
+
const horizontalGapBetweenColumns = 180;
|
|
8
|
+
|
|
9
|
+
const dagreGraph = initializeGraph(nodes, edges);
|
|
10
|
+
|
|
11
|
+
// Map of column number and Nodes in that column
|
|
12
|
+
const columnMap = getNodesByColumn(dagreGraph, nodes);
|
|
13
|
+
|
|
14
|
+
const { columnHeights, columnWidths, maxHeight } = sizeGraphColumns(dagreGraph, columnMap, verticalGapBetweenNodes);
|
|
15
|
+
|
|
16
|
+
const positionedNodes = setNodePositions(
|
|
17
|
+
dagreGraph,
|
|
18
|
+
columnMap,
|
|
19
|
+
columnHeights,
|
|
20
|
+
columnWidths,
|
|
21
|
+
verticalGapBetweenNodes,
|
|
22
|
+
horizontalGapBetweenColumns,
|
|
23
|
+
maxHeight,
|
|
24
|
+
);
|
|
25
|
+
return [positionedNodes, edges];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Initializes the dagre Graph with all the nodes and edges
|
|
29
|
+
function initializeGraph(nodes: LineageNode[], edges: LineageEdge[]): dagre.graphlib.Graph {
|
|
30
|
+
const dagreGraph = new dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
|
|
31
|
+
|
|
32
|
+
const direction = "LR";
|
|
33
|
+
dagreGraph.setGraph({
|
|
34
|
+
rankdir: direction,
|
|
35
|
+
nodesep: 30,
|
|
36
|
+
ranksep: 300,
|
|
37
|
+
align: "UL",
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
dagreGraph.setGraph({ rankdir: direction });
|
|
41
|
+
|
|
42
|
+
for (const node of nodes) {
|
|
43
|
+
dagreGraph.setNode(node.id, {
|
|
44
|
+
...node.data.size,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
edges.forEach((edge) => {
|
|
49
|
+
dagreGraph.setEdge(edge.source, edge.target);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
dagre.layout(dagreGraph);
|
|
53
|
+
|
|
54
|
+
return dagreGraph;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Returns a MAP of each "column" (aka Rank) and the nodes in the column
|
|
58
|
+
function getNodesByColumn(graph: dagre.graphlib.Graph, nodes: LineageNode[]): Map<number, LineageNode[]> {
|
|
59
|
+
const columnMap = new Map<number, LineageNode[]>();
|
|
60
|
+
for (const node of nodes) {
|
|
61
|
+
const dNode = graph.node(node.id) as { rank: number } | undefined;
|
|
62
|
+
|
|
63
|
+
if (dNode && typeof dNode.rank === "number") {
|
|
64
|
+
const rankNodes = columnMap.get(dNode.rank) ?? [];
|
|
65
|
+
rankNodes.push(node);
|
|
66
|
+
columnMap.set(dNode.rank, rankNodes);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return columnMap;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Calculates the column heights and width for each column in the graph based off the number and size of the nodes in each column
|
|
74
|
+
// Also returns the height of the tallest column
|
|
75
|
+
function sizeGraphColumns(
|
|
76
|
+
graph: dagre.graphlib.Graph,
|
|
77
|
+
columnMap: Map<number, LineageNode[]>,
|
|
78
|
+
verticalGapBetweenNodes: number,
|
|
79
|
+
): { columnHeights: Map<number, number>; columnWidths: Map<number, number>; maxHeight: number } {
|
|
80
|
+
const columnHeights = new Map<number, number>();
|
|
81
|
+
const columnWidths = new Map<number, number>();
|
|
82
|
+
let maxHeight = 0;
|
|
83
|
+
for (const [rank, nodes] of columnMap) {
|
|
84
|
+
nodes.sort((a, b) => {
|
|
85
|
+
const da = graph.node(a.id);
|
|
86
|
+
const db = graph.node(b.id);
|
|
87
|
+
return da.y - db.y;
|
|
88
|
+
});
|
|
89
|
+
let rankHeight = 0;
|
|
90
|
+
let maxWidth = 0;
|
|
91
|
+
for (const node of nodes) {
|
|
92
|
+
const graphNode = graph.node(node.id);
|
|
93
|
+
rankHeight += graphNode.height;
|
|
94
|
+
maxWidth = Math.max(maxWidth, graphNode.width);
|
|
95
|
+
}
|
|
96
|
+
rankHeight += (nodes.length - 1) * verticalGapBetweenNodes;
|
|
97
|
+
|
|
98
|
+
maxHeight = Math.max(maxHeight, rankHeight);
|
|
99
|
+
columnHeights.set(rank, rankHeight);
|
|
100
|
+
columnWidths.set(rank, maxWidth);
|
|
101
|
+
}
|
|
102
|
+
return { columnHeights: columnHeights, columnWidths: columnWidths, maxHeight };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Sets the position of each node in the graph, and returns the nodes with updated positions
|
|
106
|
+
function setNodePositions(
|
|
107
|
+
graph: dagre.graphlib.Graph,
|
|
108
|
+
columnMap: Map<number, LineageNode[]>,
|
|
109
|
+
columnHeights: Map<number, number>,
|
|
110
|
+
columnWidths: Map<number, number>,
|
|
111
|
+
verticalGapBetweenNodes: number,
|
|
112
|
+
horizontalGapBetweenNodes: number,
|
|
113
|
+
maxColumnHeight: number,
|
|
114
|
+
): LineageNode[] {
|
|
115
|
+
const positionedNodes: LineageNode[] = [];
|
|
116
|
+
let xPosition = 0;
|
|
117
|
+
const rankEntries = Array.from(columnMap.entries());
|
|
118
|
+
rankEntries.sort((a, b) => {
|
|
119
|
+
return a[0] - b[0];
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
for (const [rank, nodes] of rankEntries) {
|
|
123
|
+
const rankHeight = columnHeights.get(rank) || 0;
|
|
124
|
+
const rankWidth = columnWidths.get(rank) || 0;
|
|
125
|
+
const rankOffset = (maxColumnHeight - rankHeight) / 2;
|
|
126
|
+
let yPosition = 0;
|
|
127
|
+
|
|
128
|
+
for (const [, node] of nodes.entries()) {
|
|
129
|
+
const graphNode = graph.node(node.id);
|
|
130
|
+
node.position = {
|
|
131
|
+
x: xPosition + rankWidth / 2 - graphNode.width / 2,
|
|
132
|
+
y: yPosition + rankOffset,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const predecessors = graph.predecessors(node.id);
|
|
136
|
+
node.data.hasPrecedingNode = (predecessors && predecessors.length > 0) || false;
|
|
137
|
+
|
|
138
|
+
const successors = graph.successors(node.id);
|
|
139
|
+
node.data.hasSuccessorNode = (successors && successors.length > 0) || false;
|
|
140
|
+
|
|
141
|
+
positionedNodes.push(node);
|
|
142
|
+
yPosition += graphNode.height + verticalGapBetweenNodes;
|
|
143
|
+
}
|
|
144
|
+
xPosition += rankWidth + horizontalGapBetweenNodes;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return positionedNodes;
|
|
148
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { CID } from "multiformats/cid";
|
|
2
|
+
import { create as createMultihash } from "multiformats/hashes/digest";
|
|
3
|
+
import { IrohData } from "../components/nodes/types";
|
|
4
|
+
|
|
5
|
+
const RAW_BINARY = 0x55;
|
|
6
|
+
const BLAKE3_HASHSEQ = 0x80;
|
|
7
|
+
const BLAKE3_256 = 0x1e;
|
|
8
|
+
|
|
9
|
+
export function getIrohMetaCid(codec: number, collectionBlob: Uint8Array): string {
|
|
10
|
+
const first32 = collectionBlob.slice(0, 32);
|
|
11
|
+
const multihash = createMultihash(BLAKE3_256, first32); // 0x1e == BLAKE3-256 multihash code
|
|
12
|
+
const cid = CID.createV1(codec, multihash);
|
|
13
|
+
return cid.toString();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function resolveIrohCollections(blobs: Record<string, string>): Map<string, IrohData[]> {
|
|
17
|
+
// Map<cid, bytes> of possible iroh collections from the Blob
|
|
18
|
+
const irohCandidateBlobs: Record<string, Uint8Array> = {};
|
|
19
|
+
for (const [cid, blob] of Object.entries(blobs)) {
|
|
20
|
+
try {
|
|
21
|
+
const decoded = Uint8Array.from(atob(blob), (c) => c.charCodeAt(0));
|
|
22
|
+
|
|
23
|
+
if (decoded.length >= 64) {
|
|
24
|
+
irohCandidateBlobs[cid] = decoded;
|
|
25
|
+
} else {
|
|
26
|
+
//console.log(`Skipping blob '${cid}' — too short (${decoded.length} bytes)`);
|
|
27
|
+
}
|
|
28
|
+
} catch (e) {
|
|
29
|
+
console.error(`Failed to decode blob for CID '${cid}':`, e);
|
|
30
|
+
throw e;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const irohCollections = new Map();
|
|
35
|
+
|
|
36
|
+
for (const [cid, blob] of Object.entries(irohCandidateBlobs)) {
|
|
37
|
+
const metaCidLegacy = getIrohMetaCid(RAW_BINARY, blob);
|
|
38
|
+
const meta_cid = getIrohMetaCid(BLAKE3_HASHSEQ, blob);
|
|
39
|
+
if (metaCidLegacy in blobs) {
|
|
40
|
+
const metaBlob = blobs[metaCidLegacy];
|
|
41
|
+
const collection_blob = blobs[cid];
|
|
42
|
+
const nameCidMap = extractCollectionNames(blobs, metaBlob, collection_blob);
|
|
43
|
+
irohCollections.set(cid, nameCidMap);
|
|
44
|
+
} else if (meta_cid in blobs) {
|
|
45
|
+
const metaBlob = blobs[metaCidLegacy];
|
|
46
|
+
const collection_blob = blobs[cid];
|
|
47
|
+
const nameCidMap = extractCollectionNames(blobs, metaBlob, collection_blob);
|
|
48
|
+
irohCollections.set(cid, nameCidMap);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return irohCollections;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Decodes a base64 collection blob and extracts file names after CollectionV0. or CollectionV1.
|
|
57
|
+
* Handles both versions and multiple names.
|
|
58
|
+
* @param metaBlob - The base64-encoded meta blob that has the names of the items in the collection.
|
|
59
|
+
* @param collectionBlob - The base64-encoded collection blob of the iroh collection.
|
|
60
|
+
* @returns Map of FileName -> CID in the iroh collection.
|
|
61
|
+
*/
|
|
62
|
+
function extractCollectionNames(blobs: Record<string, string>, metaBlob: string, collectionBlob: string): IrohData[] {
|
|
63
|
+
// collection blob contains the hash sequence.
|
|
64
|
+
const collectionBlobStr = atob(collectionBlob);
|
|
65
|
+
const hashSeq = Uint8Array.from(collectionBlobStr, (c) => c.charCodeAt(0));
|
|
66
|
+
|
|
67
|
+
const hashes = parseHashSeq(hashSeq).slice(1); // drop meta hash
|
|
68
|
+
|
|
69
|
+
// meta blob is encoded with `postcard` encoding, which is a length-prefixed format.
|
|
70
|
+
const names = [];
|
|
71
|
+
|
|
72
|
+
const decodedMetaBlob = atob(metaBlob);
|
|
73
|
+
const metaBytes = Uint8Array.from(decodedMetaBlob, (c) => c.charCodeAt(0));
|
|
74
|
+
|
|
75
|
+
const headerLen = 13; // fixed length u8 array, no length prefix
|
|
76
|
+
const namesBytes = metaBytes.slice(headerLen); // drop header
|
|
77
|
+
|
|
78
|
+
let unprocessedNamesBytes = namesBytes.slice(1); // drop array length byte
|
|
79
|
+
while (unprocessedNamesBytes.length > 0) {
|
|
80
|
+
const nameLen = unprocessedNamesBytes[0];
|
|
81
|
+
unprocessedNamesBytes = unprocessedNamesBytes.slice(1); // drop length byte
|
|
82
|
+
|
|
83
|
+
const nameBytes = unprocessedNamesBytes.slice(0, nameLen);
|
|
84
|
+
unprocessedNamesBytes = unprocessedNamesBytes.slice(nameLen); // drop name bytes
|
|
85
|
+
|
|
86
|
+
const name = new TextDecoder().decode(nameBytes);
|
|
87
|
+
names.push(name);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (names.length !== hashes.length) {
|
|
91
|
+
console.error(`Names and hashes length mismatch: ${names.length} != ${hashes.length}. Names: '${names}'`);
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const irohDataList: IrohData[] = [];
|
|
96
|
+
for (let i = 0; i < names.length; i++) {
|
|
97
|
+
const blob = blobs[hashes[i]];
|
|
98
|
+
const data = blob ? atob(blob) : null;
|
|
99
|
+
irohDataList.push({
|
|
100
|
+
path: names[i],
|
|
101
|
+
cid: hashes[i],
|
|
102
|
+
data: data,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return irohDataList;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function parseHashSeq(bytes: Uint8Array): string[] {
|
|
109
|
+
if (bytes.length % 32 !== 0) throw new Error("Invalid hash sequence length");
|
|
110
|
+
const hashes: string[] = [];
|
|
111
|
+
for (let i = 0; i < bytes.length; i += 32) {
|
|
112
|
+
const chunk = bytes.slice(i, i + 32);
|
|
113
|
+
const hashCid = getIrohMetaCid(RAW_BINARY, chunk);
|
|
114
|
+
hashes.push(hashCid);
|
|
115
|
+
}
|
|
116
|
+
return hashes;
|
|
117
|
+
}
|