From f5cb20c8c6a3f18b4a201a8274c09381d1908d1c Mon Sep 17 00:00:00 2001 From: Camiel van Schoonhoven Date: Thu, 12 Mar 2026 14:19:25 -0700 Subject: [PATCH] Artifact Visualization --- package-lock.json | 26 +- package.json | 3 + .../ArtifactVisualizer/ArtifactVisualizer.tsx | 251 ++++++++++++++++++ .../ArtifactVisualizer/CsvVisualizer.tsx | 70 +++++ .../ArtifactVisualizer/ImageVisualizer.tsx | 10 + .../ArtifactVisualizer/JsonVisualizer.tsx | 25 ++ .../ArtifactVisualizer/ParquetVisualizer.tsx | 49 ++++ .../ArtifactVisualizer/TableVisualizer.tsx | 94 +++++++ .../ArtifactVisualizer/TextVisualizer.tsx | 39 +++ .../ArtifactVisualizer/useArtifactFetch.tsx | 29 ++ .../IOCell/ArtifactVisualizer/utils.ts | 20 ++ .../TaskOverview/IOSection/IOCell/IOCell.tsx | 55 +++- src/services/executionService.ts | 14 + 13 files changed, 673 insertions(+), 12 deletions(-) create mode 100644 src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/ArtifactVisualizer.tsx create mode 100644 src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/CsvVisualizer.tsx create mode 100644 src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/ImageVisualizer.tsx create mode 100644 src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/JsonVisualizer.tsx create mode 100644 src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/ParquetVisualizer.tsx create mode 100644 src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/TableVisualizer.tsx create mode 100644 src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/TextVisualizer.tsx create mode 100644 src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/useArtifactFetch.tsx create mode 100644 src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/utils.ts diff --git a/package-lock.json b/package-lock.json index ab29a7176..e29c8826e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,6 +41,7 @@ "@tanstack/react-router": "1.168.4", "@tanstack/router-devtools": "1.166.11", "@types/gapi": "^0.0.47", + "@types/papaparse": "^5.5.2", "@xyflow/react": "^12.10.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -51,10 +52,12 @@ "eslint-plugin-simple-import-sort": "^12.1.1", "fast-deep-equal": "^3.1.3", "gh-pages": "^6.3.0", + "hyparquet": "^1.25.1", "js-yaml": "^4.1.1", "localforage": "^1.10.0", "lucide-react": "^0.577.0", "nanoid": "^5.1.7", + "papaparse": "^5.5.3", "pyodide": "^0.29.3", "random-words": "^2.0.1", "react": "^19.2.4", @@ -5121,12 +5124,20 @@ "version": "25.5.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", - "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~7.18.0" } }, + "node_modules/@types/papaparse": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.5.2.tgz", + "integrity": "sha512-gFnFp/JMzLHCwRf7tQHrNnfhN4eYBVYYI897CGX4MY1tzY9l2aLkVyx2IlKZ/SAqDbB3I1AOZW5gTMGGsqWliA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/react": { "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", @@ -8200,6 +8211,12 @@ "dev": true, "license": "MIT" }, + "node_modules/hyparquet": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/hyparquet/-/hyparquet-1.25.1.tgz", + "integrity": "sha512-CXcN/u6RdQqsK8IphUptpAEqY8IzgwzHY+MuXX+2wpoWTumfxPVr6JYbbywsNsiAl9aEbM5sRtxkwRBa22b49w==", + "license": "MIT" + }, "node_modules/idn-hostname": { "version": "15.1.8", "resolved": "https://registry.npmjs.org/idn-hostname/-/idn-hostname-15.1.8.tgz", @@ -10007,6 +10024,12 @@ "dev": true, "license": "BlueOak-1.0.0" }, + "node_modules/papaparse": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.3.tgz", + "integrity": "sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==", + "license": "MIT" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -12417,7 +12440,6 @@ "version": "7.18.2", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", - "devOptional": true, "license": "MIT" }, "node_modules/universalify": { diff --git a/package.json b/package.json index bc4b533bb..3fa031ffe 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "@tanstack/react-router": "1.168.4", "@tanstack/router-devtools": "1.166.11", "@types/gapi": "^0.0.47", + "@types/papaparse": "^5.5.2", "@xyflow/react": "^12.10.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -91,10 +92,12 @@ "eslint-plugin-simple-import-sort": "^12.1.1", "fast-deep-equal": "^3.1.3", "gh-pages": "^6.3.0", + "hyparquet": "^1.25.1", "js-yaml": "^4.1.1", "localforage": "^1.10.0", "lucide-react": "^0.577.0", "nanoid": "^5.1.7", + "papaparse": "^5.5.3", "pyodide": "^0.29.3", "random-words": "^2.0.1", "react": "^19.2.4", diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/ArtifactVisualizer.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/ArtifactVisualizer.tsx new file mode 100644 index 000000000..ee7ef78d0 --- /dev/null +++ b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/ArtifactVisualizer.tsx @@ -0,0 +1,251 @@ +import { useSuspenseQuery } from "@tanstack/react-query"; +import { useState } from "react"; + +import type { ArtifactNodeResponse } from "@/api/types.gen"; +import { SuspenseWrapper } from "@/components/shared/SuspenseWrapper"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Icon } from "@/components/ui/icon"; +import { BlockStack, InlineStack } from "@/components/ui/layout"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Text } from "@/components/ui/typography"; +import { cn } from "@/lib/utils"; +import { useBackend } from "@/providers/BackendProvider"; +import { getArtifactSignedUrl } from "@/services/executionService"; +import { HOURS } from "@/utils/constants"; + +import ArtifactURI from "../ArtifactURI"; +import { CsvVisualizerRemote, CsvVisualizerValue } from "./CsvVisualizer"; +import ImageVisualizer from "./ImageVisualizer"; +import { JsonVisualizerRemote, JsonVisualizerValue } from "./JsonVisualizer"; +import ParquetVisualizer from "./ParquetVisualizer"; +import { TextVisualizerRemote, TextVisualizerValue } from "./TextVisualizer"; + +const VISUALIZABLE_TYPES = new Set([ + "text", + "image", + "jsonobject", + "jsonarray", + "csv", + "tsv", + "apacheparquet", +]); + +type ArtifactVisualizerProps = { + artifact: ArtifactNodeResponse; + name: string; + type: string; + value?: string; +}; + +const ArtifactVisualizer = ({ + artifact, + name, + type, + value, +}: ArtifactVisualizerProps) => { + const [isFullscreen, setIsFullscreen] = useState(false); + + const normalizedType = type?.toLowerCase().replace(/\s/g, "") ?? "text"; + + const handleOpenChange = (open: boolean) => { + if (!open) setIsFullscreen(false); + }; + + if (!VISUALIZABLE_TYPES.has(normalizedType) && !value) return null; + + const artifactData = artifact.artifact_data; + const isJson = + normalizedType === "jsonobject" || normalizedType === "jsonarray"; + + return ( + + + {value ? ( + + ) : ( + + )} + + + {!isJson && ( + + )} + + + {name} + {!!type && ( + + {type} + + )} + + {!!artifactData?.uri && ( + + )} + + + + Artifact visualization for {name} + + +
+ {value ? ( + + ) : ( + }> + + + )} +
+
+
+ ); +}; + +interface InlineContentProps { + type: string; + name: string; + value: string; + remoteLink?: string | null; + isFullscreen: boolean; +} + +const InlineContent = ({ + type, + name, + value, + remoteLink, + isFullscreen, +}: InlineContentProps) => { + switch (type) { + case "csv": + case "tsv": + return ( + + ); + case "jsonobject": + case "jsonarray": + return ; + case "text": + default: + return ; + } +}; + +interface PreviewContentProps { + artifactId: string; + type: string; + name: string; + isFullscreen: boolean; +} + +const PreviewContent = ({ + artifactId, + type, + name, + isFullscreen, +}: PreviewContentProps) => { + const { backendUrl } = useBackend(); + + const { data } = useSuspenseQuery({ + queryKey: ["artifact-signed-url", artifactId], + queryFn: () => getArtifactSignedUrl(artifactId, backendUrl), + staleTime: 24 * HOURS, + retry: false, + }); + + const signedUrl = data?.signed_url; + if (!signedUrl) return null; + + switch (type) { + case "text": + return ; + case "image": + return ; + case "csv": + case "tsv": + return ( + + ); + case "apacheparquet": + return ( + + ); + case "jsonobject": + case "jsonarray": + return ; + default: + return null; + } +}; + +export default ArtifactVisualizer; + +const SKELETON_ROWS = 6; + +const PreviewSkeleton = () => ( + + + {Array.from({ length: 3 }, (_, i) => ( + + ))} + + {Array.from({ length: SKELETON_ROWS }, (_, i) => ( + + + + + + ))} + +); diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/CsvVisualizer.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/CsvVisualizer.tsx new file mode 100644 index 000000000..6af224cc2 --- /dev/null +++ b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/CsvVisualizer.tsx @@ -0,0 +1,70 @@ +import { Paragraph } from "@/components/ui/typography"; + +import TableVisualizer from "./TableVisualizer"; +import { useArtifactFetch } from "./useArtifactFetch"; +import { type ArtifactTableData, parseCsv } from "./utils"; + +interface CsvVisualizerValueProps { + value: string; + remoteLink?: string | null; + isFullscreen: boolean; +} + +interface CsvVisualizerRemoteProps { + signedUrl: string; + isFullscreen: boolean; +} + +const CsvContent = ({ + data, + remoteLink, + isFullscreen, +}: { + data: ArtifactTableData; + remoteLink?: string | null; + isFullscreen: boolean; +}) => { + if (data.headers.length === 0) { + return ( + + No data + + ); + } + + return ( + + ); +}; + +export const CsvVisualizerValue = ({ + value, + remoteLink, + isFullscreen, +}: CsvVisualizerValueProps) => ( + +); + +export const CsvVisualizerRemote = ({ + signedUrl, + isFullscreen, +}: CsvVisualizerRemoteProps) => { + const data = useArtifactFetch("csv", signedUrl, async (r) => + parseCsv(await r.text()), + ); + return ( + + ); +}; diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/ImageVisualizer.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/ImageVisualizer.tsx new file mode 100644 index 000000000..c91a978b4 --- /dev/null +++ b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/ImageVisualizer.tsx @@ -0,0 +1,10 @@ +interface ImageVisualizerProps { + src: string; + name: string; +} + +const ImageVisualizer = ({ src, name }: ImageVisualizerProps) => ( + {name} +); + +export default ImageVisualizer; diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/JsonVisualizer.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/JsonVisualizer.tsx new file mode 100644 index 000000000..6b500da32 --- /dev/null +++ b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/JsonVisualizer.tsx @@ -0,0 +1,25 @@ +import IOCodeViewer from "../IOCodeViewer"; +import { useArtifactFetch } from "./useArtifactFetch"; + +interface JsonVisualizerValueProps { + name: string; + value: string; +} + +interface JsonVisualizerRemoteProps { + name: string; + signedUrl: string; +} + +export const JsonVisualizerValue = ({ + name, + value, +}: JsonVisualizerValueProps) => ; + +export const JsonVisualizerRemote = ({ + name, + signedUrl, +}: JsonVisualizerRemoteProps) => { + const content = useArtifactFetch("json", signedUrl, (r) => r.text()); + return ; +}; diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/ParquetVisualizer.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/ParquetVisualizer.tsx new file mode 100644 index 000000000..c37c6619c --- /dev/null +++ b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/ParquetVisualizer.tsx @@ -0,0 +1,49 @@ +import { parquetReadObjects } from "hyparquet"; + +import { Paragraph } from "@/components/ui/typography"; + +import TableVisualizer from "./TableVisualizer"; +import { useArtifactFetch } from "./useArtifactFetch"; +import { MAX_PREVIEW_ROWS } from "./utils"; + +interface ParquetVisualizerProps { + signedUrl: string; + isFullscreen: boolean; +} + +const ParquetVisualizer = ({ + signedUrl, + isFullscreen, +}: ParquetVisualizerProps) => { + const data = useArtifactFetch("parquet", signedUrl, async (response) => { + const arrayBuffer = await response.arrayBuffer(); + const objects = await parquetReadObjects({ + file: arrayBuffer, + rowEnd: MAX_PREVIEW_ROWS + 1, + }); + + if (objects.length === 0) return { headers: [], rows: [] }; + const headers = Object.keys(objects[0]); + const rows = objects.map((obj) => headers.map((h) => obj[String(h)])); + + return { headers, rows }; + }); + + if (data.headers.length === 0) { + return ( + + No data + + ); + } + + return ( + + ); +}; + +export default ParquetVisualizer; diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/TableVisualizer.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/TableVisualizer.tsx new file mode 100644 index 000000000..8c67e3ea2 --- /dev/null +++ b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/TableVisualizer.tsx @@ -0,0 +1,94 @@ +import { BlockStack, InlineStack } from "@/components/ui/layout"; +import { Link } from "@/components/ui/link"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { Paragraph } from "@/components/ui/typography"; + +import { + type ArtifactTableData, + DEFAULT_PREVIEW_ROWS, + MAX_PREVIEW_ROWS, +} from "./utils"; + +interface TableVisualizerProps { + data: ArtifactTableData; + remoteLink?: string | null; + isFullscreen: boolean; +} + +const TableVisualizer = ({ + data, + remoteLink, + isFullscreen, +}: TableVisualizerProps) => { + const displayedRows = isFullscreen + ? data.rows.slice(0, MAX_PREVIEW_ROWS) + : data.rows.slice(0, DEFAULT_PREVIEW_ROWS); + + const isShowingAllRows = displayedRows.length >= data.rows.length; + + return ( + + + + + {isShowingAllRows + ? `Showing all ${displayedRows.length} rows` + : `Showing first ${displayedRows.length} rows`} + + {!isShowingAllRows && remoteLink && ( + + See all + + )} + + + ); +}; + +export default TableVisualizer; + +interface ArtifactTableProps { + headers: string[]; + rows: string[][]; +} + +const ArtifactTable = ({ headers, rows }: ArtifactTableProps) => ( + + + + {headers.map((h) => ( + + {h} + + ))} + + + + {rows.map((row, i) => ( + + {row.map((cell, j) => ( + + {String(cell)} + + ))} + + ))} + +
+); diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/TextVisualizer.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/TextVisualizer.tsx new file mode 100644 index 000000000..abd908a64 --- /dev/null +++ b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/TextVisualizer.tsx @@ -0,0 +1,39 @@ +import { BlockStack } from "@/components/ui/layout"; +import { Paragraph, Text } from "@/components/ui/typography"; + +import { useArtifactFetch } from "./useArtifactFetch"; + +interface TextVisualizerValueProps { + value: string; +} + +interface TextVisualizerRemoteProps { + signedUrl: string; +} + +const TextContent = ({ content }: { content: string }) => { + if (content.length === 0) { + return ( + + No data + + ); + } + + return ( + + {content} + + ); +}; + +export const TextVisualizerValue = ({ value }: TextVisualizerValueProps) => ( + +); + +export const TextVisualizerRemote = ({ + signedUrl, +}: TextVisualizerRemoteProps) => { + const content = useArtifactFetch("text", signedUrl, (r) => r.text()); + return ; +}; diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/useArtifactFetch.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/useArtifactFetch.tsx new file mode 100644 index 000000000..744d2a30d --- /dev/null +++ b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/useArtifactFetch.tsx @@ -0,0 +1,29 @@ +import { useSuspenseQuery } from "@tanstack/react-query"; + +import { HOURS } from "@/utils/constants"; + +/** + * Fetches artifact content from a signed URL using suspense mode. + * Loading and error states are handled by the nearest SuspenseWrapper. + */ +export function useArtifactFetch( + queryKey: string, + signedUrl: string, + transform: (response: Response) => Promise, +): T { + const { data } = useSuspenseQuery({ + queryKey: [`artifact-${queryKey}`, signedUrl], + queryFn: async () => { + const response = await fetch(signedUrl); + if (!response.ok) { + throw new Error(`(${response.status}) Failed to fetch artifact.`); + } + + return transform(response); + }, + staleTime: 24 * HOURS, + retry: false, + }); + + return data; +} diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/utils.ts b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/utils.ts new file mode 100644 index 000000000..3fc26fe5e --- /dev/null +++ b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/utils.ts @@ -0,0 +1,20 @@ +import Papa from "papaparse"; + +export type ArtifactTableData = { headers: string[]; rows: string[][] }; + +export const DEFAULT_PREVIEW_ROWS = 10; +export const MAX_PREVIEW_ROWS = 30; + +export function parseCsv(text: string, delimiter?: string): ArtifactTableData { + const result = Papa.parse(text, { + delimiter, + header: false, + preview: MAX_PREVIEW_ROWS + 1, + skipEmptyLines: true, + }); + + if (result.data.length === 0) return { headers: [], rows: [] }; + + const [headers, ...rows] = result.data; + return { headers, rows }; +} diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/IOCell.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/IOCell.tsx index a15c330e6..a3e3c7462 100644 --- a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/IOCell.tsx +++ b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/IOCell.tsx @@ -5,6 +5,7 @@ import { Text } from "@/components/ui/typography"; import { formatBytes } from "@/utils/string"; import ArtifactURI from "./ArtifactURI"; +import ArtifactVisualizer from "./ArtifactVisualizer/ArtifactVisualizer"; interface IOCellProps { name: string; @@ -16,6 +17,7 @@ const IOCell = ({ name, type, artifact }: IOCellProps) => { const artifactData = artifact?.artifact_data; const inlineValue = artifactData?.value; const hasInlineValue = canShowInlineValue(inlineValue); + const hasDetails = Boolean(artifactData?.uri || hasInlineValue); const artifactType = type ?? artifact?.type_name ?? (artifactData?.is_dir ? "Directory" : "Any"); @@ -45,18 +47,51 @@ const IOCell = ({ name, type, artifact }: IOCellProps) => { - {hasInlineValue && ( - - {inlineValue} - - )} + + {hasInlineValue && ( + + {inlineValue} + + )} + + {!artifactData?.uri && artifact && hasDetails && ( + + )} + {!!artifactData?.uri && ( - + + + + {artifact && hasDetails && ( + + )} + )} ); diff --git a/src/services/executionService.ts b/src/services/executionService.ts index 48d49a29a..ae6e7946f 100644 --- a/src/services/executionService.ts +++ b/src/services/executionService.ts @@ -3,6 +3,7 @@ import { useQuery } from "@tanstack/react-query"; import { getGraphExecutionStateApiExecutionsIdStateGet } from "@/api/sdk.gen"; import type { GetArtifactsApiExecutionsIdArtifactsGetResponse, + GetArtifactSignedUrlResponse, GetContainerExecutionStateResponse, GetExecutionInfoResponse, PipelineRunResponse, @@ -113,6 +114,19 @@ export const fetchExecutionStatusLight = rateLimit( }, ); +export const getArtifactSignedUrl = async ( + artifactId: string, + backendUrl: string, +): Promise => { + const response = await fetch( + `${backendUrl}/api/artifacts/${artifactId}/signed_artifact_url`, + ); + if (!response.ok) { + throw new Error(`(${response.status}) Failed to get signed URL.`); + } + return response.json(); +}; + export const getExecutionArtifacts = async ( executionId: string, backendUrl: string,