@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,238 @@
|
|
|
1
|
+
import { Download } from "lucide-react";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import {
|
|
4
|
+
Button,
|
|
5
|
+
ListOrGridViewToggle,
|
|
6
|
+
ResourceBadge,
|
|
7
|
+
Select,
|
|
8
|
+
SelectContent,
|
|
9
|
+
SelectItem,
|
|
10
|
+
SelectTrigger,
|
|
11
|
+
SelectValue,
|
|
12
|
+
} from "@eqtylab/equality";
|
|
13
|
+
|
|
14
|
+
import { LineageNode, MetadataLineage } from "../../nodes/types.ts";
|
|
15
|
+
import IrohCollectionView from "../sections/custom/IrohCollectionsView.tsx";
|
|
16
|
+
import { StatementVerficationIcon, VCVerficationIcon } from "./registration/VerificationIcon";
|
|
17
|
+
import { FileViewer } from "./FileViewer.tsx";
|
|
18
|
+
import SectionTitle from "../SectionTitle.tsx";
|
|
19
|
+
import { SectionList } from "../SectionList.tsx";
|
|
20
|
+
import { formatTimestamp } from "@/explorer-component/utils/formatTimestamp.ts";
|
|
21
|
+
import { getExtensionFromLanguage, getLanguageFromContent } from "./FileViewer.tsx";
|
|
22
|
+
import { getResourceTypeFromDisplayType } from "@/lib/resource-types.ts";
|
|
23
|
+
|
|
24
|
+
type Props = {
|
|
25
|
+
node: LineageNode;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Convert camelCase, snake_case, and kebab-case to seperate words with the first letter capitalized
|
|
29
|
+
const formatKeyLabel = (rawKey: string): string => {
|
|
30
|
+
return rawKey
|
|
31
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2") // camelCase → camel Case
|
|
32
|
+
.replace(/[_-]/g, " ") // snake_case, kebab-case → snake case
|
|
33
|
+
.toLowerCase()
|
|
34
|
+
.replace(/\b\w/g, (char) => char.toUpperCase()); // capitalize each word
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const MetadataSection = ({
|
|
38
|
+
metadataStatements,
|
|
39
|
+
displayType,
|
|
40
|
+
}: {
|
|
41
|
+
metadataStatements: MetadataLineage[];
|
|
42
|
+
displayType: string;
|
|
43
|
+
}) => {
|
|
44
|
+
const [metadataIndex, setMetadataIndex] = useState(0);
|
|
45
|
+
|
|
46
|
+
const metadataObject = metadataStatements[metadataIndex] || metadataStatements[0];
|
|
47
|
+
|
|
48
|
+
if (!metadataObject) return null;
|
|
49
|
+
|
|
50
|
+
const data = [
|
|
51
|
+
{
|
|
52
|
+
label: "Instance",
|
|
53
|
+
value:
|
|
54
|
+
metadataStatements.length > 1 ? (
|
|
55
|
+
<Select onValueChange={(value) => setMetadataIndex(parseInt(value))} defaultValue="0">
|
|
56
|
+
<SelectTrigger>
|
|
57
|
+
<SelectValue placeholder="Select a statement" />
|
|
58
|
+
</SelectTrigger>
|
|
59
|
+
<SelectContent>
|
|
60
|
+
{metadataStatements.map((data, index) => {
|
|
61
|
+
const id = data.statement.id;
|
|
62
|
+
const timestamp = formatTimestamp(data.statement.timestamp);
|
|
63
|
+
return (
|
|
64
|
+
<SelectItem value={String(index)} key={id}>
|
|
65
|
+
{`${id.split("cid:")[1].slice(0, 5)}...${id.slice(-4)} (${timestamp})`}
|
|
66
|
+
</SelectItem>
|
|
67
|
+
);
|
|
68
|
+
})}
|
|
69
|
+
</SelectContent>
|
|
70
|
+
</Select>
|
|
71
|
+
) : (
|
|
72
|
+
<p>{formatTimestamp(metadataObject.statement.timestamp)}</p>
|
|
73
|
+
),
|
|
74
|
+
offsetTopLabel: metadataStatements.length > 1 ? true : false,
|
|
75
|
+
},
|
|
76
|
+
...Object.entries(metadataObject.metadata).map(([key, value]) => {
|
|
77
|
+
const isAssetType = key === "assetType";
|
|
78
|
+
|
|
79
|
+
if (isAssetType) {
|
|
80
|
+
const resourceType = getResourceTypeFromDisplayType(displayType);
|
|
81
|
+
const item = {
|
|
82
|
+
label: "Asset Type",
|
|
83
|
+
value: value && <ResourceBadge type={resourceType} />, // Display badge if showing asset type
|
|
84
|
+
};
|
|
85
|
+
return item;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const item = {
|
|
89
|
+
label: formatKeyLabel(key),
|
|
90
|
+
value: value && <p>{String(value)}</p>,
|
|
91
|
+
};
|
|
92
|
+
return item;
|
|
93
|
+
}),
|
|
94
|
+
{
|
|
95
|
+
label: "Statement",
|
|
96
|
+
value: metadataObject.statement && (
|
|
97
|
+
<StatementVerficationIcon statement={metadataObject.statement} displayText={true} />
|
|
98
|
+
),
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
label: "Credential",
|
|
102
|
+
value: metadataObject.vc && (
|
|
103
|
+
<VCVerficationIcon
|
|
104
|
+
vc={metadataObject.vc}
|
|
105
|
+
displayText={true}
|
|
106
|
+
statementId={metadataObject.statement.id}
|
|
107
|
+
/>
|
|
108
|
+
),
|
|
109
|
+
},
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
return <SectionList data={data} />;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export type SourceDataType = "irohCollection" | "singleFile" | undefined;
|
|
116
|
+
|
|
117
|
+
const CustomSection = ({ node }: Props) => {
|
|
118
|
+
const [viewMode, setViewMode] = useState<"grid" | "list">("list");
|
|
119
|
+
const [sourceDetails, setSourceDetails] = useState<{
|
|
120
|
+
dataType: SourceDataType;
|
|
121
|
+
data: string | undefined;
|
|
122
|
+
sourceDataBlob: Blob | undefined;
|
|
123
|
+
sourceDataLanguage: string | null;
|
|
124
|
+
sourceDataLanguageExt: string | null;
|
|
125
|
+
sourceDataUrl: string | undefined;
|
|
126
|
+
}>({
|
|
127
|
+
dataType: undefined,
|
|
128
|
+
data: undefined,
|
|
129
|
+
sourceDataBlob: undefined,
|
|
130
|
+
sourceDataLanguage: null,
|
|
131
|
+
sourceDataLanguageExt: null,
|
|
132
|
+
sourceDataUrl: undefined,
|
|
133
|
+
});
|
|
134
|
+
const metadataLineage = node?.data.sidebarData?.metadataLineage;
|
|
135
|
+
const displayType = node.data.displayType;
|
|
136
|
+
const header = displayType || "Custom";
|
|
137
|
+
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
if (viewMode === "grid") setViewMode("list");
|
|
140
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
141
|
+
}, [node.id]);
|
|
142
|
+
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
let sourceDataType: SourceDataType;
|
|
145
|
+
let sourceData: string | undefined;
|
|
146
|
+
let sourceDataBlob: Blob | undefined;
|
|
147
|
+
let sourceDataLanguage: string | null = null;
|
|
148
|
+
let sourceDataLanguageExt: string | null = null;
|
|
149
|
+
let sourceDataUrl: string | undefined;
|
|
150
|
+
if (Array.isArray(node?.data.sidebarData?.sourceData)) {
|
|
151
|
+
sourceDataType = "irohCollection";
|
|
152
|
+
} else if (typeof node.data.sidebarData?.sourceData === "string") {
|
|
153
|
+
sourceDataType = "singleFile";
|
|
154
|
+
sourceData = node.data.sidebarData?.sourceData;
|
|
155
|
+
// base64 decoded
|
|
156
|
+
// TODO: handle blobs as raw bytes everywhere instead of base64 strings
|
|
157
|
+
// only base64 decode and try utf-8 parsing where necessary (e.g. FileViewer)
|
|
158
|
+
const bytes = Buffer.from(sourceData, "base64");
|
|
159
|
+
sourceDataLanguage = getLanguageFromContent(bytes);
|
|
160
|
+
sourceDataLanguageExt = sourceDataLanguage ? getExtensionFromLanguage(sourceDataLanguage) : null;
|
|
161
|
+
sourceDataBlob = new Blob([bytes]);
|
|
162
|
+
sourceDataUrl = URL.createObjectURL(sourceDataBlob);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
setSourceDetails({
|
|
166
|
+
dataType: sourceDataType,
|
|
167
|
+
data: sourceData,
|
|
168
|
+
sourceDataBlob: sourceDataBlob,
|
|
169
|
+
sourceDataLanguage: sourceDataLanguage,
|
|
170
|
+
sourceDataLanguageExt: sourceDataLanguageExt,
|
|
171
|
+
sourceDataUrl: sourceDataUrl,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// This needs to be done in a useEffect so we can revoke the URL when the component unmounts
|
|
175
|
+
return () => {
|
|
176
|
+
if (sourceDataUrl) URL.revokeObjectURL(sourceDataUrl);
|
|
177
|
+
};
|
|
178
|
+
}, [node.data.sidebarData?.sourceData]);
|
|
179
|
+
|
|
180
|
+
// Nothing to display. Hide the whole section
|
|
181
|
+
if (!sourceDetails.data && (!metadataLineage || metadataLineage.length === 0)) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const data = node.data.sidebarData?.sourceData;
|
|
186
|
+
|
|
187
|
+
return (
|
|
188
|
+
<div>
|
|
189
|
+
<SectionTitle
|
|
190
|
+
title={header === "Unknown" ? "Metadata" : header}
|
|
191
|
+
renderRightContent={() => {
|
|
192
|
+
return (
|
|
193
|
+
<div className="flex items-center gap-2">
|
|
194
|
+
{sourceDetails.dataType === "singleFile" && sourceDetails.data && (
|
|
195
|
+
<>
|
|
196
|
+
<FileViewer
|
|
197
|
+
filename={
|
|
198
|
+
(metadataLineage?.[0].metadata as unknown as { name: string }).name ??
|
|
199
|
+
"source"
|
|
200
|
+
}
|
|
201
|
+
content={sourceDetails.data}
|
|
202
|
+
language={sourceDetails.sourceDataLanguage || undefined}
|
|
203
|
+
decodeContent={true}
|
|
204
|
+
buttonText="View"
|
|
205
|
+
/>
|
|
206
|
+
{sourceDetails.sourceDataUrl && (
|
|
207
|
+
<Button
|
|
208
|
+
variant="tertiary"
|
|
209
|
+
size="sm"
|
|
210
|
+
prefix={<Download />}
|
|
211
|
+
href={sourceDetails.sourceDataUrl}
|
|
212
|
+
download={`${node.id}.${sourceDetails.sourceDataLanguageExt}`}
|
|
213
|
+
>
|
|
214
|
+
Download
|
|
215
|
+
</Button>
|
|
216
|
+
)}
|
|
217
|
+
</>
|
|
218
|
+
)}
|
|
219
|
+
{sourceDetails.dataType === "irohCollection" && (
|
|
220
|
+
<ListOrGridViewToggle
|
|
221
|
+
viewMode={viewMode}
|
|
222
|
+
onViewModeChange={setViewMode}
|
|
223
|
+
order={["list", "grid"]}
|
|
224
|
+
/>
|
|
225
|
+
)}
|
|
226
|
+
</div>
|
|
227
|
+
);
|
|
228
|
+
}}
|
|
229
|
+
/>
|
|
230
|
+
{metadataLineage && viewMode === "list" && (
|
|
231
|
+
<MetadataSection metadataStatements={metadataLineage} displayType={displayType} />
|
|
232
|
+
)}
|
|
233
|
+
{viewMode === "grid" && data && typeof data !== "string" && <IrohCollectionView data={data} />}
|
|
234
|
+
</div>
|
|
235
|
+
);
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
export default CustomSection;
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Button,
|
|
4
|
+
CodeBlock,
|
|
5
|
+
Dialog,
|
|
6
|
+
DialogContent,
|
|
7
|
+
DialogHeader,
|
|
8
|
+
DialogTitle,
|
|
9
|
+
DialogContainer,
|
|
10
|
+
} from "@eqtylab/equality";
|
|
11
|
+
import { ExternalLink } from "lucide-react";
|
|
12
|
+
|
|
13
|
+
import { Statement, StatementToJSON } from "@/explorer-component/integrityTypes/types";
|
|
14
|
+
|
|
15
|
+
import filetype from "magic-bytes.js";
|
|
16
|
+
|
|
17
|
+
interface BaseViewerProps {
|
|
18
|
+
children?: React.ReactNode;
|
|
19
|
+
buttonText?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type FileViewerProps =
|
|
23
|
+
| ({ statement: Statement } & BaseViewerProps)
|
|
24
|
+
| ({ filename: string; content: string; language?: string; decodeContent?: boolean } & BaseViewerProps);
|
|
25
|
+
|
|
26
|
+
interface ModalSettings {
|
|
27
|
+
filename: string;
|
|
28
|
+
showLineNumbers: boolean;
|
|
29
|
+
language: string;
|
|
30
|
+
content: string;
|
|
31
|
+
decodeContent: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const FileViewer = (props: FileViewerProps) => {
|
|
35
|
+
const settings = processProps(props);
|
|
36
|
+
|
|
37
|
+
const [decodedContent, setDecodedContent] = useState(settings.content);
|
|
38
|
+
const [formattedContent, setFormattedContent] = useState(settings.content);
|
|
39
|
+
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
40
|
+
|
|
41
|
+
const showModal = async () => {
|
|
42
|
+
setIsModalOpen(true);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const handleOk = () => {
|
|
46
|
+
setIsModalOpen(false);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (settings.decodeContent) {
|
|
51
|
+
try {
|
|
52
|
+
setDecodedContent(atob(settings.content));
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error("Failed to decode base64 content:", error);
|
|
55
|
+
setDecodedContent(settings.content);
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
setDecodedContent(settings.content);
|
|
59
|
+
}
|
|
60
|
+
}, [settings.content, settings.decodeContent]);
|
|
61
|
+
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (settings.language === "json") {
|
|
64
|
+
try {
|
|
65
|
+
const prettyJson = JSON.stringify(JSON.parse(decodedContent), null, 2);
|
|
66
|
+
setFormattedContent(prettyJson);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error("Failed to format JSON content:", error);
|
|
69
|
+
setFormattedContent(decodedContent);
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
setFormattedContent(decodedContent);
|
|
73
|
+
}
|
|
74
|
+
}, [decodedContent, settings.language]);
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<>
|
|
78
|
+
{React.isValidElement(props.children) && typeof props.children.type !== "string" ? (
|
|
79
|
+
React.cloneElement(props.children as React.ReactElement<any>, { onClick: showModal })
|
|
80
|
+
) : props.children ? (
|
|
81
|
+
props.children
|
|
82
|
+
) : (
|
|
83
|
+
<Button variant="tertiary" size="sm" onClick={showModal}>
|
|
84
|
+
{props.buttonText}
|
|
85
|
+
</Button>
|
|
86
|
+
)}
|
|
87
|
+
{settings.language === "html" && (
|
|
88
|
+
<Button
|
|
89
|
+
variant="tertiary"
|
|
90
|
+
size="sm"
|
|
91
|
+
prefix={<ExternalLink />}
|
|
92
|
+
onClick={() => {
|
|
93
|
+
const newWindow = window.open();
|
|
94
|
+
if (newWindow) {
|
|
95
|
+
newWindow.document.write(formattedContent);
|
|
96
|
+
newWindow.document.close();
|
|
97
|
+
}
|
|
98
|
+
}}
|
|
99
|
+
>
|
|
100
|
+
Open
|
|
101
|
+
</Button>
|
|
102
|
+
)}
|
|
103
|
+
|
|
104
|
+
<Dialog open={isModalOpen} onOpenChange={handleOk}>
|
|
105
|
+
<DialogContainer size="lg" className="h-dvh">
|
|
106
|
+
<DialogHeader>
|
|
107
|
+
<DialogTitle>{settings.filename}</DialogTitle>
|
|
108
|
+
</DialogHeader>
|
|
109
|
+
<DialogContent>
|
|
110
|
+
<CodeBlock
|
|
111
|
+
code={formattedContent}
|
|
112
|
+
// TODO: once syntax highlighting performance issues are resolved, re-enable for html
|
|
113
|
+
language={settings.language === "html" ? undefined : settings.language}
|
|
114
|
+
className="min-h-full"
|
|
115
|
+
/>
|
|
116
|
+
</DialogContent>
|
|
117
|
+
</DialogContainer>
|
|
118
|
+
</Dialog>
|
|
119
|
+
</>
|
|
120
|
+
);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
function processProps(props: FileViewerProps): ModalSettings {
|
|
124
|
+
if (typeof props === "object" && "content" in props && typeof props.content === "string") {
|
|
125
|
+
const language = (() => {
|
|
126
|
+
// 1st priority: explicit language prop
|
|
127
|
+
if (props.language) {
|
|
128
|
+
return props.language;
|
|
129
|
+
}
|
|
130
|
+
// 2nd priority: infer from file extension
|
|
131
|
+
const langFromExt = getLanguageFromFilename(props.filename);
|
|
132
|
+
if (langFromExt) {
|
|
133
|
+
return langFromExt;
|
|
134
|
+
}
|
|
135
|
+
// 3rd priority: magic bytes
|
|
136
|
+
const langFromMagicBytes = getLanguageFromContent(
|
|
137
|
+
Buffer.from(props.content, props.decodeContent ? "base64" : "utf-8"),
|
|
138
|
+
);
|
|
139
|
+
if (langFromMagicBytes) {
|
|
140
|
+
return langFromMagicBytes;
|
|
141
|
+
}
|
|
142
|
+
// Fallback to text if all else fails
|
|
143
|
+
return "text";
|
|
144
|
+
})();
|
|
145
|
+
const showLineNumbers = language !== "text";
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
language,
|
|
149
|
+
showLineNumbers,
|
|
150
|
+
content: props.content,
|
|
151
|
+
filename: props.filename,
|
|
152
|
+
decodeContent: props.decodeContent || false,
|
|
153
|
+
};
|
|
154
|
+
} else if (typeof props === "object" && "statement" in props) {
|
|
155
|
+
const content = JSON.stringify(StatementToJSON(props.statement), null, 2);
|
|
156
|
+
return {
|
|
157
|
+
language: "json",
|
|
158
|
+
showLineNumbers: true,
|
|
159
|
+
content,
|
|
160
|
+
decodeContent: false,
|
|
161
|
+
filename: "Statement",
|
|
162
|
+
};
|
|
163
|
+
} else {
|
|
164
|
+
return {
|
|
165
|
+
language: "text",
|
|
166
|
+
showLineNumbers: true,
|
|
167
|
+
content: "",
|
|
168
|
+
decodeContent: false,
|
|
169
|
+
filename: "No Data",
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const langMap: Record<string, string> = {
|
|
175
|
+
html: "html",
|
|
176
|
+
js: "javascript",
|
|
177
|
+
json: "json",
|
|
178
|
+
ts: "typescript",
|
|
179
|
+
py: "python",
|
|
180
|
+
r: "r",
|
|
181
|
+
rs: "rust",
|
|
182
|
+
sas: "sas",
|
|
183
|
+
yml: "yaml",
|
|
184
|
+
yaml: "yaml",
|
|
185
|
+
txt: "text",
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
export function getExtensionFromLanguage(language: string): string | null {
|
|
189
|
+
const entry = Object.entries(langMap).find(([_, lang]) => lang === language);
|
|
190
|
+
return entry ? entry[0] : null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function getLanguageFromFilename(fileName: string): string | null {
|
|
194
|
+
return langMap[fileName.split(".").pop() || ""] || null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function getLanguageFromContent(bytes: Uint8Array): string | null {
|
|
198
|
+
const fileTypes = filetype(bytes);
|
|
199
|
+
|
|
200
|
+
// Try using magic-bytes.js
|
|
201
|
+
const detectedLang =
|
|
202
|
+
fileTypes.length > 0 && fileTypes[0].extension ? getLanguageFromFilename(fileTypes[0].extension) : null;
|
|
203
|
+
|
|
204
|
+
if (detectedLang) {
|
|
205
|
+
return detectedLang;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Text formats not detected by magic-bytes.js
|
|
209
|
+
const text = new TextDecoder().decode(bytes);
|
|
210
|
+
|
|
211
|
+
// HTML: check for doctype or HTML tags
|
|
212
|
+
if (/^<!doctype\s+html/i.test(text) || /^<html/i.test(text)) {
|
|
213
|
+
return "html";
|
|
214
|
+
}
|
|
215
|
+
// Python: shebang or keywords
|
|
216
|
+
if (/^#!.*python/.test(text) || /\b(def|class|import|from)\b/.test(text)) {
|
|
217
|
+
return "python";
|
|
218
|
+
}
|
|
219
|
+
// JavaScript: import/export/function patterns
|
|
220
|
+
if (/\b(function|const|let|var|import|export)\b/.test(text)) {
|
|
221
|
+
return "javascript";
|
|
222
|
+
}
|
|
223
|
+
// YAML: starts with --- or contains key: value pattern
|
|
224
|
+
if (/^---/.test(text) || /^[A-Za-z0-9_-]+:\s/.test(text)) {
|
|
225
|
+
return "yaml";
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import SectionTitle from "@/explorer-component/components/sidebar/SectionTitle";
|
|
2
|
+
import { LineageNode } from "@/explorer-component/components/nodes/types";
|
|
3
|
+
import { SectionList } from "@/explorer-component/components/sidebar/SectionList";
|
|
4
|
+
import { formatTimestamp } from "@/explorer-component/utils/formatTimestamp";
|
|
5
|
+
import DisplayCode from "@/components/ui/display-code";
|
|
6
|
+
|
|
7
|
+
const GovernanceSection = ({ node }: { node: LineageNode }) => {
|
|
8
|
+
const governance = node.data.sidebarData?.governance;
|
|
9
|
+
|
|
10
|
+
if (!governance) return null;
|
|
11
|
+
|
|
12
|
+
const data = [
|
|
13
|
+
{
|
|
14
|
+
label: "Context",
|
|
15
|
+
value: <DisplayCode value={governance.context} />,
|
|
16
|
+
offsetTopLabel: true,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
label: "Registration ID",
|
|
20
|
+
value: <DisplayCode value={governance.id} />,
|
|
21
|
+
offsetTopLabel: true,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
label: "Registered By",
|
|
25
|
+
value: <DisplayCode value={governance.registeredBy} />,
|
|
26
|
+
offsetTopLabel: true,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
label: "Timestamp",
|
|
30
|
+
value: <p>{formatTimestamp(governance.timestamp)}</p>,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
label: "Subject",
|
|
34
|
+
value: <DisplayCode value={governance.subject} />,
|
|
35
|
+
offsetTopLabel: true,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
label: "Document",
|
|
39
|
+
value: <DisplayCode value={governance.document} />,
|
|
40
|
+
offsetTopLabel: true,
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div>
|
|
46
|
+
<SectionTitle title="Governance" />
|
|
47
|
+
<SectionList data={data} />
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export default GovernanceSection;
|