diff --git a/web/src/app/dashboard/backfill/page.tsx b/web/src/app/dashboard/backfill/page.tsx index 89e92bc..4f7d91d 100644 --- a/web/src/app/dashboard/backfill/page.tsx +++ b/web/src/app/dashboard/backfill/page.tsx @@ -5,7 +5,9 @@ import { useCallback, useEffect, useState } from "react"; import { useCurrentUser } from "@/hooks/use-current-user"; import { createBackfillJob, getBackfillJobs, getLexicons } from "@/lib/api"; import type { BackfillJob } from "@/types/backfill"; +import { CheckCircle2, Circle, Loader2 } from "lucide-react"; import { SiteHeader } from "@/components/site-header"; +import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Combobox, @@ -27,6 +29,12 @@ import { } from "@/components/ui/responsive-dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet"; import { Table, TableBody, @@ -36,10 +44,55 @@ import { TableRow, } from "@/components/ui/table"; +const STAGES = [ + "pending", + "discovering_repos", + "resolving_pds", + "fetching_records", + "completed", + "failed", +] as const; + +const STAGE_LABELS: Record = { + pending: "Pending", + discovering_repos: "Discovering repos", + resolving_pds: "Resolving PDS", + fetching_records: "Fetching records", + completed: "Completed", + failed: "Failed", +}; + +function stageBadge(stage: string) { + switch (stage) { + case "completed": + return ( + + completed + + ); + case "failed": + return failed; + case "pending": + return pending; + default: + return ( + + {STAGE_LABELS[stage] ?? stage} + + ); + } +} + +function stageIndex(stage: string): number { + const idx = STAGES.indexOf(stage as (typeof STAGES)[number]); + return idx === -1 ? 0 : idx; +} + export default function BackfillPage() { const { hasPermission } = useCurrentUser(); const [jobs, setJobs] = useState([]); const [error, setError] = useState(null); + const [selectedJobId, setSelectedJobId] = useState(null); const load = useCallback(() => { getBackfillJobs() @@ -51,12 +104,13 @@ export default function BackfillPage() { load(); }, [load]); - // Auto-refresh every 5 seconds useEffect(() => { const interval = setInterval(load, 5000); return () => clearInterval(interval); }, [load]); + const selectedJob = jobs.find((j) => j.id === selectedJobId) ?? null; + return ( <> @@ -93,7 +147,11 @@ export default function BackfillPage() { )} {jobs.map((job) => ( - + setSelectedJobId(job.id)} + > {job.id.slice(0, 8)} @@ -103,9 +161,7 @@ export default function BackfillPage() { {job.did ?? "All"} - - - + {stageBadge(job.stage)} {job.started_at ? new Date(job.started_at).toLocaleString() @@ -116,53 +172,165 @@ export default function BackfillPage() { + + { + if (!open) setSelectedJobId(null); + }} + > + + {selectedJob && } + + ); } -function StageDisplay({ job }: { job: BackfillJob }) { - const repos = job.total_repos?.toLocaleString() ?? "0"; - const processed = job.processed_repos?.toLocaleString() ?? "0"; - const records = job.total_records?.toLocaleString() ?? "0"; +function JobDetail({ job }: { job: BackfillJob }) { + const current = stageIndex(job.stage); - switch (job.stage) { - case "pending": - return Pending; - case "discovering_repos": - return ( - - Discovering repos… -
{repos} found -
- ); - case "resolving_pds": - return ( - - Resolving PDS… ({processed} / {repos}) - - ); - case "fetching_records": - return ( - - Fetching records… ({processed} / {repos} repos, {records} records) - - ); - case "completed": - return ( - - Completed — {repos} repos, {records} records - - ); - case "failed": - return ( - - Failed{job.error ? ` — ${job.error}` : ""} + return ( + <> + + + Backfill Details + + +
+
+
+ Job ID +

{job.id}

+
+
+ Collection +

{job.collection ?? "All"}

+
+
+ DID +

{job.did ?? "All"}

+
+
+ Created +

+ {new Date(job.created_at).toLocaleString()} +

+
+
+ Started +

+ {job.started_at + ? new Date(job.started_at).toLocaleString() + : "--"} +

+
+ {job.completed_at && ( +
+ Completed +

+ {new Date(job.completed_at).toLocaleString()} +

+
+ )} +
+ + {job.error && ( +
+ Error +
+ {job.error} +
+
+ )} + +
+ Progress +
+ = stageIndex("discovering_repos")} + value={job.total_repos?.toLocaleString()} + suffix="repos found" + /> + = stageIndex("resolving_pds")} + value={ + current >= stageIndex("resolving_pds") + ? `${job.processed_repos?.toLocaleString() ?? "0"} / ${job.total_repos?.toLocaleString() ?? "0"}` + : undefined + } + suffix="resolved" + /> + = stageIndex("fetching_records")} + value={ + current >= stageIndex("fetching_records") + ? `${job.processed_repos?.toLocaleString() ?? "0"} / ${job.total_repos?.toLocaleString() ?? "0"} repos` + : undefined + } + suffix={ + current >= stageIndex("fetching_records") + ? `${job.total_records?.toLocaleString() ?? "0"} records` + : undefined + } + /> +
+
+
+ + ); +} + +function ProgressRow({ + label, + active, + reached, + value, + suffix, +}: { + label: string; + active: boolean; + reached: boolean; + value?: string; + suffix?: string; +}) { + const done = reached && !active; + + return ( +
+ + {active ? ( + + ) : done ? ( + + ) : ( + + )} + + {label} + {reached && value && ( + + {value} + {suffix ? ` · ${suffix}` : ""} - ); - default: - return {job.stage}; - } + )} +
+ ); } function CreateDialog({ onSuccess }: { onSuccess: () => void }) {