114 lines
3.8 KiB
TypeScript
114 lines
3.8 KiB
TypeScript
import { useQuery } from "@tanstack/react-query";
|
|
import { exportDownloadUrl, listExports } from "../api/service";
|
|
import type { ExportJobRecord } from "../api/schemas";
|
|
import { DataTable, type Column } from "../components/DataTable";
|
|
import { PageHeader } from "../components/PageHeader";
|
|
import { Panel } from "../components/Panel";
|
|
import { StatusPill } from "../components/StatusPill";
|
|
import { ErrorBlock, Notice } from "../components/StateBlock";
|
|
import { formatBytes, formatInt, formatRelative, formatUtc } from "../lib/format";
|
|
import { useRun, runErrorTitle } from "../lib/useRun";
|
|
|
|
/**
|
|
* Export job history for the active run. Jobs are held in the query service
|
|
* process memory, so the list resets when the service restarts.
|
|
*/
|
|
export default function ExportsPage() {
|
|
const { runId, runQuery } = useRun();
|
|
|
|
const exportsQuery = useQuery({
|
|
queryKey: ["exports", runId],
|
|
queryFn: () => listExports(runId, { limit: 50 }),
|
|
refetchInterval: (query) => {
|
|
const jobs = query.state.data?.items ?? [];
|
|
return jobs.some((job) => job.status === "running") ? 2000 : false;
|
|
},
|
|
});
|
|
|
|
if (runQuery.isError) {
|
|
return (
|
|
<div className="page">
|
|
<PageHeader title="Exports" />
|
|
<ErrorBlock
|
|
error={runQuery.error}
|
|
onRetry={() => runQuery.refetch()}
|
|
title={runErrorTitle(runId, runQuery.error)}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const columns: Column<ExportJobRecord>[] = [
|
|
{
|
|
key: "job",
|
|
header: "Job",
|
|
render: (job) => <span className="mono text-small">{job.jobId.slice(0, 12)}…</span>,
|
|
},
|
|
{ key: "scope", header: "Scope", render: (job) => <span className="chip">{job.scope}</span> },
|
|
{
|
|
key: "target",
|
|
header: "Target",
|
|
render: (job) => (
|
|
<span className="mono text-small">{job.repoId ?? job.ppId ?? "object set"}</span>
|
|
),
|
|
},
|
|
{ key: "status", header: "Status", render: (job) => <StatusPill status={job.status} /> },
|
|
{
|
|
key: "objects",
|
|
header: "Objects",
|
|
numeric: true,
|
|
render: (job) => formatInt(job.objectCount),
|
|
},
|
|
{ key: "bytes", header: "Size", numeric: true, render: (job) => formatBytes(job.bytesWritten) },
|
|
{
|
|
key: "created",
|
|
header: "Created",
|
|
render: (job) => <span title={formatUtc(job.createdAt)}>{formatRelative(job.createdAt)}</span>,
|
|
},
|
|
{
|
|
key: "error",
|
|
header: "Error",
|
|
render: (job) =>
|
|
job.error ? <span className="text-small" title={job.error}>{job.error}</span> : <span className="text-faint">—</span>,
|
|
},
|
|
{
|
|
key: "download",
|
|
header: "",
|
|
render: (job) =>
|
|
job.status === "complete" ? (
|
|
<a className="btn small primary" href={exportDownloadUrl(runId, job.jobId)} download>
|
|
Download
|
|
</a>
|
|
) : null,
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div className="page">
|
|
<PageHeader
|
|
title="Exports"
|
|
subtitle="Evidence bundles (raw object tarballs) produced by the query service"
|
|
/>
|
|
<Notice kind="info">
|
|
Export jobs live in the query service process memory and are cleared on
|
|
restart. Creating exports requires the service to run with --repo-bytes-db.
|
|
New exports can be started from an object, publication point or repository
|
|
page.
|
|
</Notice>
|
|
<Panel title="Jobs" flush>
|
|
<DataTable
|
|
columns={columns}
|
|
rows={exportsQuery.data?.items ?? []}
|
|
rowKey={(job) => job.jobId}
|
|
loading={exportsQuery.isPending}
|
|
error={exportsQuery.isError ? exportsQuery.error : undefined}
|
|
onRetry={() => exportsQuery.refetch()}
|
|
emptyTitle="No export jobs yet"
|
|
emptyHint="Start an export from an object detail page (object set) to see it here."
|
|
caption="Export jobs"
|
|
/>
|
|
</Panel>
|
|
</div>
|
|
);
|
|
}
|