import { useEffect, useMemo, useState } from "react"; import { useParams } from "react-router"; import { ArrowDownTrayIcon, PaperClipIcon } from "@heroicons/react/24/outline"; import type { RunArtifactEntry } from "@qltysh/fabro-api-client"; import { EmptyState, ErrorState, LoadingState } from "../components/state"; import { StageSidebar } from "../components/stage-sidebar"; import { formatBytes } from "../lib/format"; import { stageArtifactDownloadUrl } from "../lib/api-client"; import { useRunArtifacts, useRunStages } from "../lib/queries"; import { formatStageLabel, mapRunStagesToSidebarStages } from "../lib/stage-sidebar"; export const handle = { wide: true }; export default function RunArtifacts() { const { id } = useParams(); const stagesQuery = useRunStages(id); const artifactsQuery = useRunArtifacts(id); const stages = useMemo( () => mapRunStagesToSidebarStages(stagesQuery.data), [stagesQuery.data], ); return (
{renderBody(id!, artifactsQuery, stages)}
); } function renderBody( runId: string, artifactsQuery: ReturnType, stages: ReturnType, ) { if (artifactsQuery.error) { return ( void artifactsQuery.mutate()} /> ); } if (artifactsQuery.data === undefined) { return ; } const entries = artifactsQuery.data?.data ?? []; if (entries.length === 0) { return ( ); } return ; } interface StageGroup { key: string; stageId: string; retry: number; label: string; entries: RunArtifactEntry[]; totalBytes: number; } function groupArtifacts( entries: readonly RunArtifactEntry[], stages: ReturnType, ): StageGroup[] { const stageLabels = new Map(); for (const stage of stages) { stageLabels.set(stage.id, formatStageLabel(stage)); } const groups = new Map(); for (const entry of entries) { const key = `${entry.stage_id}#${entry.retry}`; const existing = groups.get(key); if (existing) { existing.entries.push(entry); existing.totalBytes += entry.size; } else { groups.set(key, { key, stageId: entry.stage_id, retry: entry.retry, label: stageLabels.get(entry.stage_id) ?? entry.node_slug, entries: [entry], totalBytes: entry.size, }); } } for (const group of groups.values()) { group.entries.sort((a, b) => a.relative_path.localeCompare(b.relative_path)); } return [...groups.values()].sort((a, b) => { const labelCmp = a.label.localeCompare(b.label); return labelCmp !== 0 ? labelCmp : a.retry - b.retry; }); } function ArtifactList({ runId, entries, stages, }: { runId: string; entries: readonly RunArtifactEntry[]; stages: ReturnType; }) { const groups = useMemo(() => groupArtifacts(entries, stages), [entries, stages]); const totalBytes = useMemo( () => entries.reduce((sum, entry) => sum + entry.size, 0), [entries], ); return (

{entries.length} {entries.length === 1 ? "artifact" : "artifacts"}

{formatBytes(totalBytes)} total
{groups.map((group) => ( ))}
); } function StageGroupCard({ runId, group }: { runId: string; group: StageGroup }) { return (

{group.label}

{group.retry > 0 && ( retry {group.retry} )}
{group.entries.length} {group.entries.length === 1 ? "file" : "files"} {" · "} {formatBytes(group.totalBytes)}
    {group.entries.map((entry) => ( ))}
); } function ArtifactRow({ runId, entry }: { runId: string; entry: RunArtifactEntry }) { const [href, setHref] = useState("#"); useEffect(() => { let active = true; void stageArtifactDownloadUrl( runId, entry.stage_id, entry.relative_path, entry.retry, ).then((url) => { if (active) setHref(url); }); return () => { active = false; }; }, [entry.relative_path, entry.retry, entry.stage_id, runId]); return (
  • {entry.relative_path} {formatBytes(entry.size)}
  • ); } function basename(path: string): string { const idx = path.lastIndexOf("/"); return idx >= 0 ? path.slice(idx + 1) : path; } function errorMessage(error: unknown): string | undefined { return error instanceof Error ? error.message : undefined; }