/** * Copyright (c) 2026-present, Goldman Sachs * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * Developer-only diagnostic page for measuring the size and shape of the * DataSpace analytics artifact returned by the Depot server. Used to * investigate Chrome tab OOM crashes when loading large multi execution * context DataSpaces. * * Route: /dev/dataspace-inspector * * The page reuses the application's existing depot client (so it inherits * the same auth / cookies / Envoy origin headers), fetches: * GET /generations/{groupId}/{artifactId}/{versionId}/types/dataSpace-analytics?elementPath={path} * (the existing per-type endpoint, now narrowed via the `elementPath` * query parameter for QUERY-1054) and reports total uncompressed payload * bytes plus a per-file breakdown for that one DataSpace. * * Also supports loading inputs from a saved query id (hits the engine * query server `/pure/v1/query/{id}` endpoint and pulls groupId / * artifactId / versionId + the dataspace path from the saved query's * `taggedValues` / `executionContext`), and an inline JSON viewer (Monaco) * for the parsed response and individual file contents. */ import { observer } from 'mobx-react-lite'; import { useState } from 'react'; import { StoreProjectData } from '@finos/legend-server-depot'; import { LogEvent } from '@finos/legend-shared'; import { CodeEditor } from '@finos/legend-lego/code-editor'; import { CODE_EDITOR_LANGUAGE } from '@finos/legend-code-editor'; import { LEGEND_QUERY_APP_EVENT } from '../__lib__/LegendQueryEvent.js'; import { useLegendQueryApplicationStore, useLegendQueryBaseStore, } from './LegendQueryFrameworkProvider.js'; import { DATA_SPACE_ELEMENT_CLASSIFIER_PATH } from '@finos/legend-extension-dsl-data-space/graph'; const MB = 1024 * 1024; const fmtMB = (bytes: number): string => `${(bytes / MB).toFixed(2)} MB`; const QUERY_PROFILE_PATH = 'meta::pure::profiles::query'; const QUERY_PROFILE_TAG_DATA_SPACE = 'dataSpace'; interface DataSpaceOption { groupId: string; artifactId: string; versionId: string; path: string; label: string; } interface FileSizeRow { path: string; type: string; filePath: string; bytes: number; } interface InspectionResult { mode: 'parsed' | 'streamed' | 'streamed-headers'; totalBytes: number; fileCount: number; rows: FileSizeRow[]; fetchMs: number; sizingMs: number; streamedBodyBytes?: number | undefined; streamedCompressedBytes?: number | undefined; streamedContentEncoding?: string | undefined; streamedUrl?: string | undefined; // Only populated in parsed mode — used by the inline JSON viewer. rawFiles?: unknown[] | undefined; } interface JsonViewerState { title: string; content: string; } /** * Pretty-print arbitrary JSON-ish input. If the input is already a string * that happens to be valid JSON (depot returns `content` as a string for * files like `AnalyticsResult.json`), reparse and re-stringify so the * viewer gets a properly indented, foldable tree instead of a long * single-line escaped string. */ const prettyJson = (value: unknown): string => { if (typeof value === 'string') { const trimmed = value.trim(); if ( (trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']')) ) { try { return JSON.stringify(JSON.parse(value), null, 2); } catch { // fall through, return raw string } } return value; } try { return JSON.stringify(value, null, 2); } catch { return String(value); } }; const inputStyle: React.CSSProperties = { padding: '0.4rem 0.6rem', background: '#2a2a2a', color: '#eee', border: '1px solid #444', borderRadius: 4, fontFamily: 'monospace', }; const thStyle: React.CSSProperties = { padding: '0.4rem 0.6rem', textAlign: 'left', borderBottom: '1px solid #444', }; const tdStyle: React.CSSProperties = { padding: '0.35rem 0.6rem', borderBottom: '1px solid #2a2a2a', fontFamily: 'monospace', fontSize: '0.85rem', }; const Row: React.FC<{ label: string; value: string }> = ({ label, value }) => ( {label} {value} ); const JsonViewerModal: React.FC<{ title: string; content: string; onClose: () => void; }> = ({ title, content, onClose }) => (
e.stopPropagation()} >
{title} ({content.length.toLocaleString()} chars)
); export const DataSpaceArtifactInspector = observer(() => { const applicationStore = useLegendQueryApplicationStore(); const depotServerClient = useLegendQueryBaseStore().depotServerClient; const [groupId, setGroupId] = useState(''); const [artifactId, setArtifactId] = useState(''); const [versionId, setVersionId] = useState(''); const [elementPath, setElementPath] = useState(''); const [savedQueryId, setSavedQueryId] = useState(''); const [loadingQuery, setLoadingQuery] = useState(false); const [queryLoadError, setQueryLoadError] = useState( undefined, ); const [queryLoadInfo, setQueryLoadInfo] = useState( undefined, ); const [dataSpaceOptions, setDataSpaceOptions] = useState( [], ); const [loadingDataSpaces, setLoadingDataSpaces] = useState(false); const [dataSpacesError, setDataSpacesError] = useState( undefined, ); const [selectedDataSpaceKey, setSelectedDataSpaceKey] = useState(''); const [dataSpaceFilter, setDataSpaceFilter] = useState(''); const [running, setRunning] = useState(false); const [error, setError] = useState(undefined); const [result, setResult] = useState(undefined); const [viewer, setViewer] = useState(undefined); const elementPathMissing = !elementPath.trim(); const canRun = !running && !!groupId && !!artifactId && !!versionId && !elementPathMissing; /** * Load groupId / artifactId / versionId / elementPath from a saved * query. Hits the engine query server `/pure/v1/query/{id}` endpoint * directly (no graphManager dependency required for a diagnostic page). */ const loadFromSavedQuery = async (): Promise => { const id = savedQueryId.trim(); if (!id) { return; } setLoadingQuery(true); setQueryLoadError(undefined); setQueryLoadInfo(undefined); try { const config = applicationStore.config; const base = config.engineQueryServerUrl ?? config.engineServerUrl; if (!base) { throw new Error('Engine (query) server URL is not configured'); } const url = `${base}/pure/v1/query/${encodeURIComponent(id)}`; const response = await fetch(url, { method: 'GET', credentials: 'include', headers: { Accept: 'application/json' }, }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const body = (await response.json()) as { groupId?: string; artifactId?: string; versionId?: string; taggedValues?: { profile?: string; tag?: string; value?: string; }[]; executionContext?: { _type?: string; dataSpacePath?: string; }; }; const nextGroupId = body.groupId ?? ''; const nextArtifactId = body.artifactId ?? ''; const nextVersionId = body.versionId ?? ''; // Prefer the executionContext.dataSpacePath (set when the saved // query targets a dataspace exec context); fall back to the legacy // `dataSpace` tagged value. const fromExecutionContext = body.executionContext?.dataSpacePath?.trim() ?? ''; const fromTaggedValues = (body.taggedValues ?? []).find( (t) => t.profile === QUERY_PROFILE_PATH && t.tag === QUERY_PROFILE_TAG_DATA_SPACE && typeof t.value === 'string' && t.value.length > 0, )?.value; const nextElementPath = fromExecutionContext || (fromTaggedValues ?? ''); if (!nextGroupId || !nextArtifactId || !nextVersionId) { throw new Error( 'Saved query is missing groupId / artifactId / versionId', ); } if (!nextElementPath) { throw new Error( 'Saved query has no dataspace tagged value or dataspace execution context — cannot infer elementPath. Fill it in manually.', ); } setGroupId(nextGroupId); setArtifactId(nextArtifactId); setVersionId(nextVersionId); setElementPath(nextElementPath); setQueryLoadInfo( `Loaded ${nextGroupId}:${nextArtifactId}:${nextVersionId} → ${nextElementPath}`, ); } catch (e) { setQueryLoadError(e instanceof Error ? e.message : String(e)); applicationStore.logService.error( LogEvent.create(LEGEND_QUERY_APP_EVENT.GENERIC_FAILURE), e, ); } finally { setLoadingQuery(false); } }; /** * Load every DataSpace registered in depot (latest version of each) * into a dropdown. Uses the lightweight `summary` classifier endpoint * so we only get `{groupId, artifactId, versionId, path}` rows, not * full entity content — safe to call with thousands of entries. */ const loadAllDataSpaces = async (): Promise => { setLoadingDataSpaces(true); setDataSpacesError(undefined); try { const base = depotServerClient.baseUrl; if (!base) { throw new Error('Depot server baseUrl is not configured'); } const url = `${base}/classifiers/${encodeURIComponent( DATA_SPACE_ELEMENT_CLASSIFIER_PATH, )}?summary=true&latest=true`; const response = await fetch(url, { method: 'GET', credentials: 'include', headers: { Accept: 'application/json' }, }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const body = (await response.json()) as { groupId?: string; artifactId?: string; versionId?: string; path?: string; }[]; const options: DataSpaceOption[] = body .map((e): DataSpaceOption | undefined => { const { groupId: g, artifactId: a, versionId: v, path: p } = e; if (!g || !a || !v || !p) { return undefined; } return { groupId: g, artifactId: a, versionId: v, path: p, label: `${p} \u2014 ${g}:${a}:${v}`, }; }) .filter((o): o is DataSpaceOption => o !== undefined) .sort((a, b) => a.label.localeCompare(b.label)); setDataSpaceOptions(options); } catch (e) { setDataSpacesError(e instanceof Error ? e.message : String(e)); applicationStore.logService.error( LogEvent.create(LEGEND_QUERY_APP_EVENT.GENERIC_FAILURE), e, ); } finally { setLoadingDataSpaces(false); } }; const applyDataSpaceOption = (key: string): void => { setSelectedDataSpaceKey(key); if (!key) { return; } const option = dataSpaceOptions.find( (o) => `${o.groupId}:${o.artifactId}:${o.versionId}:${o.path}` === key, ); if (!option) { return; } setGroupId(option.groupId); setArtifactId(option.artifactId); setVersionId(option.versionId); setElementPath(option.path); }; const filteredDataSpaceOptions = (() => { const f = dataSpaceFilter.trim().toLowerCase(); if (!f) { return dataSpaceOptions; } return dataSpaceOptions.filter((o) => o.label.toLowerCase().includes(f)); })(); const run = async (): Promise => { setRunning(true); setError(undefined); setResult(undefined); try { const project = StoreProjectData.serialization.fromJson( await depotServerClient.getProject(groupId.trim(), artifactId.trim()), ); const fetchStart = performance.now(); const files = await depotServerClient.getGenerationFilesByType( project, versionId.trim(), 'dataSpace-analytics', elementPath.trim(), ); const fetchMs = performance.now() - fetchStart; const sizingStart = performance.now(); const rows: FileSizeRow[] = files .map((f) => { const elemPath = (f as { path?: unknown }).path; const type = (f as { type?: unknown }).type; const file = (f as { file?: { path?: unknown; content?: unknown } }) .file; const filePath = file?.path; const content = file?.content; let bytes = 0; try { bytes = typeof content === 'string' ? content.length : JSON.stringify(content).length; } catch { bytes = -1; } return { path: typeof elemPath === 'string' ? elemPath : elementPath.trim(), type: typeof type === 'string' ? type : '', filePath: typeof filePath === 'string' ? filePath : '', bytes, }; }) .sort((a, b) => b.bytes - a.bytes); const sizingMs = performance.now() - sizingStart; const totalBytes = rows.reduce((sum, r) => sum + Math.max(0, r.bytes), 0); setResult({ mode: 'parsed', totalBytes, fileCount: rows.length, rows, fetchMs, sizingMs, rawFiles: files, }); } catch (e) { setError(e instanceof Error ? e.message : String(e)); applicationStore.logService.error( LogEvent.create(LEGEND_QUERY_APP_EVENT.GENERIC_FAILURE), e, ); } finally { setRunning(false); } }; /** * Streaming variant that NEVER parses the JSON. Uses fetch() directly, * reads the response as a byte stream, and counts bytes as they arrive. * Memory footprint stays flat regardless of payload size, so this works * even on the OOM-causing payload where the parsed variant would crash. */ const runStreaming = async (): Promise => { setRunning(true); setError(undefined); setResult(undefined); try { const base = depotServerClient.baseUrl; if (!base) { throw new Error('Depot server baseUrl is not configured'); } const url = `${base}/generations/${encodeURIComponent(groupId.trim())}` + `/${encodeURIComponent(artifactId.trim())}` + `/${encodeURIComponent(versionId.trim())}` + `/types/dataSpace-analytics` + `?elementPath=${encodeURIComponent(elementPath.trim())}`; const fetchStart = performance.now(); // `credentials: 'include'` mirrors what AbstractServerClient does so // we inherit the user's session cookies. const response = await fetch(url, { method: 'GET', credentials: 'include', headers: { Accept: 'application/json' }, }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const contentEncoding = response.headers.get('content-encoding') ?? ''; const compressedHeader = response.headers.get('content-length'); const compressedBytes = compressedHeader ? Number(compressedHeader) : undefined; const reader = response.body?.getReader(); if (!reader) { throw new Error('Response body is not readable as a stream'); } let streamedBodyBytes = 0; let chunk = await reader.read(); while (!chunk.done) { streamedBodyBytes += chunk.value.byteLength; chunk = await reader.read(); } const fetchMs = performance.now() - fetchStart; setResult({ mode: 'streamed', totalBytes: streamedBodyBytes, fileCount: 0, rows: [], fetchMs, sizingMs: 0, streamedBodyBytes, streamedCompressedBytes: compressedBytes, streamedContentEncoding: contentEncoding, streamedUrl: url, }); } catch (e) { setError(e instanceof Error ? e.message : String(e)); applicationStore.logService.error( LogEvent.create(LEGEND_QUERY_APP_EVENT.GENERIC_FAILURE), e, ); } finally { setRunning(false); } }; /** * Streaming variant that walks the response with a tiny JSON state * machine, recognises top-level array element boundaries, and for each * element keeps only: * - its total byte length (from where it started to where it ended) * - its first 64 KB of text (enough to regex out `path`) * Content (the large field) is read past and dropped on the fly, so peak * heap stays roughly element-bounded, not response-bounded. Works on * OOM-causing payloads because we never materialize the full body. */ const runStreamingWithHeaders = async (): Promise => { setRunning(true); setError(undefined); setResult(undefined); try { const base = depotServerClient.baseUrl; if (!base) { throw new Error('Depot server baseUrl is not configured'); } const url = `${base}/generations/${encodeURIComponent(groupId.trim())}` + `/${encodeURIComponent(artifactId.trim())}` + `/${encodeURIComponent(versionId.trim())}` + `/types/dataSpace-analytics` + `?elementPath=${encodeURIComponent(elementPath.trim())}`; const fetchStart = performance.now(); const response = await fetch(url, { method: 'GET', credentials: 'include', headers: { Accept: 'application/json' }, }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const contentEncoding = response.headers.get('content-encoding') ?? ''; const compressedHeader = response.headers.get('content-length'); const compressedBytes = compressedHeader ? Number(compressedHeader) : undefined; const reader = response.body?.getReader(); if (!reader) { throw new Error('Response body is not readable as a stream'); } // Cap element header buffer at 64 KB. `StoredFileGeneration.path` // and `type` reliably appear before the (potentially huge) // `file.content` payload, so 64 KB is overkill but safe. const MAX_HEADER_BYTES = 64 * 1024; const decoder = new TextDecoder('utf-8'); let streamedBodyBytes = 0; // JSON state machine let depth = 0; // brace/bracket depth across all containers let inString = false; let escape = false; let inElement = false; // true while inside a top-level array element let elementByteStart = 0; let elementHeader = ''; // capped to MAX_HEADER_BYTES per element const rows: FileSizeRow[] = []; const onElementComplete = ( startByte: number, endByte: number, headerText: string, ): void => { const pathMatch = /"path"\s*:\s*"(?(?:[^"\\]|\\.)*)"/.exec( headerText, ); const filePath = pathMatch?.groups?.value ?? ''; rows.push({ path: elementPath.trim(), type: '', filePath, bytes: endByte - startByte, }); }; let chunk = await reader.read(); while (!chunk.done) { const { value } = chunk; const chunkBytes = value.byteLength; const chunkText = decoder.decode(value, { stream: true }); const chunkStartByte = streamedBodyBytes; for (let i = 0; i < chunkText.length; i++) { const ch = chunkText[i]; if (inString) { if (escape) { escape = false; } else if (ch === '\\') { escape = true; } else if (ch === '"') { inString = false; } } else if (ch === '"') { inString = true; } else if (ch === '{' || ch === '[') { if (depth === 1 && ch === '{') { // Entering a top-level array element inElement = true; elementByteStart = chunkStartByte + i; // approximate (UTF-8/16 mismatch is OK for sizing) elementHeader = ''; } depth++; } else if (ch === '}' || ch === ']') { depth--; if (depth === 1 && inElement && ch === '}') { const elementByteEnd = chunkStartByte + i + 1; onElementComplete( elementByteStart, elementByteEnd, elementHeader, ); inElement = false; elementHeader = ''; } } if (inElement && elementHeader.length < MAX_HEADER_BYTES) { elementHeader += ch; } } streamedBodyBytes += chunkBytes; chunk = await reader.read(); } const fetchMs = performance.now() - fetchStart; rows.sort((a, b) => b.bytes - a.bytes); const totalBytes = rows.reduce((sum, r) => sum + Math.max(0, r.bytes), 0); setResult({ mode: 'streamed-headers', totalBytes, fileCount: rows.length, rows, fetchMs, sizingMs: 0, streamedBodyBytes, streamedCompressedBytes: compressedBytes, streamedContentEncoding: contentEncoding, streamedUrl: url, }); } catch (e) { setError(e instanceof Error ? e.message : String(e)); applicationStore.logService.error( LogEvent.create(LEGEND_QUERY_APP_EVENT.GENERIC_FAILURE), e, ); } finally { setRunning(false); } }; /** * Stream the full response straight to a file on disk. Uses the File * System Access API (`showSaveFilePicker`) so chunks are flushed to the * filesystem as they arrive and the entire body never sits in memory at * once — safe for the OOM-causing payload. * * Falls back to building a Blob and triggering an click for * browsers without the File System Access API. Note: the fallback path * DOES hold the full response in memory and will OOM on huge payloads; * a warning is logged in that case. */ const runStreamDownload = async (): Promise => { setRunning(true); setError(undefined); setResult(undefined); try { const base = depotServerClient.baseUrl; if (!base) { throw new Error('Depot server baseUrl is not configured'); } const url = `${base}/generations/${encodeURIComponent(groupId.trim())}` + `/${encodeURIComponent(artifactId.trim())}` + `/${encodeURIComponent(versionId.trim())}` + `/types/dataSpace-analytics` + `?elementPath=${encodeURIComponent(elementPath.trim())}`; const suggestedName = `dataSpace-analytics_${groupId.trim()}_${artifactId.trim()}_${versionId.trim()}_${elementPath.trim().replace(/::/g, '_')}.json`; // File System Access API is Chromium-only as of 2026. Probe before use. const showSaveFilePicker = ( window as unknown as { showSaveFilePicker?: (opts?: unknown) => Promise<{ createWritable: () => Promise<{ write: (chunk: Uint8Array) => Promise; close: () => Promise; }>; }>; } ).showSaveFilePicker; const fetchStart = performance.now(); const response = await fetch(url, { method: 'GET', credentials: 'include', headers: { Accept: 'application/json' }, }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const contentEncoding = response.headers.get('content-encoding') ?? ''; const compressedHeader = response.headers.get('content-length'); const compressedBytes = compressedHeader ? Number(compressedHeader) : undefined; let streamedBodyBytes = 0; if (showSaveFilePicker) { // Streaming path — bytes flushed to disk per chunk. const handle = await showSaveFilePicker({ suggestedName, types: [ { description: 'JSON file', accept: { 'application/json': ['.json'] }, }, ], }); const writable = await handle.createWritable(); const reader = response.body?.getReader(); if (!reader) { await writable.close(); throw new Error('Response body is not readable as a stream'); } let chunk = await reader.read(); while (!chunk.done) { await writable.write(chunk.value); streamedBodyBytes += chunk.value.byteLength; chunk = await reader.read(); } await writable.close(); } else { // Fallback: blob + anchor click. WARNING: holds full body in memory. // eslint-disable-next-line no-console console.warn( '[DataSpaceArtifactInspector] showSaveFilePicker not available; ' + 'falling back to blob download. This holds the full body in memory.', ); const blob = await response.blob(); streamedBodyBytes = blob.size; const objectUrl = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = objectUrl; a.download = suggestedName; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(objectUrl); } const fetchMs = performance.now() - fetchStart; setResult({ mode: 'streamed', totalBytes: streamedBodyBytes, fileCount: 0, rows: [], fetchMs, sizingMs: 0, streamedBodyBytes, streamedCompressedBytes: compressedBytes, streamedContentEncoding: contentEncoding, streamedUrl: url, }); } catch (e) { // AbortError from the user cancelling the save dialog is not a real // error — silently no-op. if (e instanceof DOMException && e.name === 'AbortError') { return; } setError(e instanceof Error ? e.message : String(e)); applicationStore.logService.error( LogEvent.create(LEGEND_QUERY_APP_EVENT.GENERIC_FAILURE), e, ); } finally { setRunning(false); } }; const openFullResponseViewer = (): void => { if (!result?.rawFiles) { return; } setViewer({ title: 'Full parsed response (StoredFileGeneration[])', content: prettyJson(result.rawFiles), }); }; const openFileViewer = (index: number): void => { const entry = result?.rawFiles?.[index] as | { path?: string; type?: string; file?: { path?: string; content?: unknown }; } | undefined; if (!entry) { return; } const filePath = entry.file?.path ?? entry.path ?? ''; setViewer({ title: `File: ${filePath}`, content: prettyJson(entry.file?.content), }); }; return (

DataSpace Artifact Inspector

Diagnostic tool: fetches the per-element depot analytics endpoint for a single DataSpace and reports total uncompressed payload size plus a per-file breakdown. Use this to identify oversized partition files that may cause Chrome tab OOM.

Load inputs from a saved query (optional)

Paste a Legend saved query id; we'll fetch{' '} /pure/v1/query/<id> and pre-fill groupId, artifactId, versionId, and the dataspace path from the query's tagged values / execution context.

setSavedQueryId(e.target.value)} placeholder="e.g. 65f1c0a8b1c0d20012345678" style={inputStyle} />
{queryLoadError && (
{queryLoadError}
)} {queryLoadInfo && (
{queryLoadInfo}
)}

Pick from all DataSpaces (optional)

Load every DataSpace registered in depot (latest version of each) via{' '} /classifiers/meta::pure::metamodel::dataSpace::DataSpace?summary=true&latest=true , then pick one to pre-fill all four inputs.

setDataSpaceFilter(e.target.value)} placeholder="filter…" disabled={dataSpaceOptions.length === 0} style={{ ...inputStyle, flex: '0 0 220px' }} />
{dataSpacesError && (
{dataSpacesError}
)}

Inputs

setGroupId(e.target.value)} placeholder="com.groupId" style={inputStyle} /> setArtifactId(e.target.value)} placeholder="my-artifactId" style={inputStyle} /> setVersionId(e.target.value)} placeholder="1.0.0 or master-SNAPSHOT" style={inputStyle} /> setElementPath(e.target.value)} placeholder="com::path::to::MyDataSpace (required)" style={{ ...inputStyle, borderColor: elementPathMissing ? '#a33' : '#444', }} aria-required="true" aria-invalid={elementPathMissing} />
{elementPathMissing && (
elementPath is required — this endpoint fetches files for a single DataSpace.
)} {error && (
{error}
)} {result && (

Summary ( {result.mode === 'streamed' ? 'streaming mode — size only' : result.mode === 'streamed-headers' ? 'streaming mode — per-file headers' : 'parsed mode'} )

{result.mode === 'streamed' || result.mode === 'streamed-headers' ? ( <> '} /> '} /> ' } /> {result.mode === 'streamed-headers' && ( <> {result.fileCount > 0 && ( )} {result.rows[0] && ( )} )} ) : ( <> {result.fileCount > 0 && ( )} {result.rows[0] && ( )} )}
{result.mode === 'parsed' && result.rawFiles && ( )}

Per-file breakdown (descending)

{result.mode === 'streamed' ? (

Per-file breakdown is only available in parsed or streamed-headers mode. Raw streaming mode only measures total bytes.

) : ( {result.mode === 'parsed' && ( )} {result.rows.map((r, i) => { // In parsed mode, the per-row index in the descending // size table doesn't match the position in `rawFiles`. // Match by the inner `file.path` (the actual file path) — // the outer `path` field on `StoredFileGeneration` is the // element path and is the same for every row. const rawIndex = result.mode === 'parsed' && result.rawFiles ? result.rawFiles.findIndex( (f) => (f as { file?: { path?: unknown } }).file?.path === r.filePath, ) : -1; return ( {result.mode === 'parsed' && ( )} ); })}
# DataSpace path Type File Size % of totalView
{i + 1} {r.path} {r.type} {r.filePath} {fmtMB(r.bytes)} {result.totalBytes ? `${((r.bytes / result.totalBytes) * 100).toFixed(1)}%` : '—'} {rawIndex >= 0 ? ( ) : ( '—' )}
)}
)} {viewer && ( setViewer(undefined)} /> )}
); });