@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,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;