RegistryUI recipes
Job runs table
The latest background job runs with their status, attempts and error, and a button to retry a failed run.
It loads its data on the server through the block's client; the jobs block must be installed (npx better-supabase add jobs). Its page on bettersupabase.com is /recipes/job-runs-table.
Web, with shadcn components:
npx shadcn add alert badge button card empty skeleton tabletype JobRunsState =
| { readonly kind: "ok"; readonly runs: readonly JobRunRow[] }
| { readonly kind: "error" }
| { readonly kind: "no-organization" };
async function loadJobRunsState(): Promise<JobRunsState> {
try {
const runs = await loadJobRuns();
return runs ? { kind: "ok", runs } : { kind: "no-organization" };
} catch (error) {
unstable_rethrow(error);
return { kind: "error" };
}
}
// oxlint-disable-next-line typescript/require-await -- a Server Action must be async, and refresh() returns nothing to await
async function retry() {
"use server";
refresh();
}
type Translate = Awaited<ReturnType<typeof getExtracted>>;
function JobStatusBadge({ status, t }: { status: JobRunStatus; t: Translate }) {
switch (status) {
case "queued": {
return <Badge variant="info-light">{t("Queued")}</Badge>;
}
case "scheduled": {
return <Badge variant="secondary">{t("Scheduled")}</Badge>;
}
case "running": {
return <Badge variant="primary-light">{t("Running")}</Badge>;
}
case "retrying": {
return <Badge variant="warning-light">{t("Retrying")}</Badge>;
}
case "succeeded": {
return <Badge variant="success-light">{t("Succeeded")}</Badge>;
}
case "dead": {
return <Badge variant="destructive-light">{t("Failed")}</Badge>;
}
default: {
const never: never = status;
return never;
}
}
}
/**
* Render inside `<Suspense>`: the organization's latest background jobs across
* every queue, with a retry button on failed runs. Gated by `audit.read`, since
* it reads with the database owner.
*/
export async function JobRunsTable() {
const [session, t, format] = await Promise.all([
getSession(),
getExtracted("jobs"),
getFormatter(),
]);
if (!can(session, JOBS_PERMISSION)) {
forbidden();
}
const state = await loadJobRunsState();
if (state.kind === "no-organization") {
return null;
}
const when = (iso: string) =>
format.dateTime(new Date(iso), { dateStyle: "medium", timeStyle: "short" });
return (
<Card data-recipe="job-runs-table">
<CardHeader>
<CardTitle>{t("Job runs")}</CardTitle>
<CardDescription>
{t(
"Background work queued for this organization, newest first. Failed jobs retry with backoff before they stop.",
)}
</CardDescription>
{state.kind === "ok" && state.runs.length > 0 ? (
<CardAction>
<EnqueueTestJobButton variant="outline" />
</CardAction>
) : null}
</CardHeader>
<CardContent>
{state.kind === "error" ? (
<Alert variant="destructive">
<TriangleAlertIcon />
<AlertTitle>{t("The job runs didn't load")}</AlertTitle>
<AlertDescription>{t("Try again in a moment.")}</AlertDescription>
<AlertAction>
<form action={retry}>
<Button size="sm" variant="outline" type="submit">
{t("Retry")}
</Button>
</form>
</AlertAction>
</Alert>
) : state.runs.length === 0 ? (
<Empty className="border">
<EmptyHeader>
<EmptyMedia variant="icon">
<ListChecksIcon />
</EmptyMedia>
<EmptyTitle>{t("No job runs yet")}</EmptyTitle>
<EmptyDescription>
{t(
"Jobs are queued when the app hands work to the background, such as a webhook delivery or an import. Queue a test job to see one here.",
)}
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<EnqueueTestJobButton />
</EmptyContent>
</Empty>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>{t("Job")}</TableHead>
<TableHead>{t("Status")}</TableHead>
<TableHead>{t("Attempts")}</TableHead>
<TableHead>{t("Queued")}</TableHead>
<TableHead>{t("Finished")}</TableHead>
<TableHead className="text-right">{t("Actions")}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{state.runs.map((run) => (
<TableRow key={`${run.queue}:${String(run.id)}`}>
<TableCell>
<div className="flex flex-col gap-1">
<span className="font-mono text-sm">{run.queue}</span>
<span
className="max-w-xs truncate text-xs text-muted-foreground"
title={run.lastError ?? undefined}
>
{run.lastError ?? `#${String(run.id)}`}
</span>
</div>
</TableCell>
<TableCell>
<JobStatusBadge status={run.status} t={t} />
</TableCell>
<TableCell className="tabular-nums">{run.attempts}</TableCell>
<TableCell className="text-xs text-muted-foreground">
{when(run.enqueuedAt)}
</TableCell>
<TableCell className="text-xs text-muted-foreground">
{run.finishedAt ? when(run.finishedAt) : "–"}
</TableCell>
<TableCell className="text-right">
{run.status === "dead" ? (
<RetryJobButton queue={run.queue} id={run.id} />
) : null}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
);
}
export function JobRunsTableSkeleton() {
return <Skeleton className="h-40 rounded-xl" aria-busy="true" />;
}Last updated on