rpki/ui/rpki-explorer/src/pages/ExportsPage.tsx

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