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.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)}
Download
);
}
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;
}