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 (
+
+ );
+};
+
+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) => (
+
+);
+
+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,