feat: complete catalog fleet management workflow
This commit is contained in:
@@ -16,6 +16,7 @@ import {
|
||||
type CatalogSyncPhase,
|
||||
type CatalogSyncRun,
|
||||
} from "../../api/catalog-databases";
|
||||
import { FleetLedgerDrawer } from "./FleetLedgerShell";
|
||||
|
||||
interface Props {
|
||||
databaseId: string | null;
|
||||
@@ -101,7 +102,7 @@ export function CatalogSyncDrawer({
|
||||
const runQuery = useQuery({
|
||||
queryKey: ["catalog-sync-run", runId],
|
||||
queryFn: () => getCatalogSyncRun(runId!),
|
||||
enabled: Boolean(runId),
|
||||
enabled: Boolean(runId && open),
|
||||
retry: false,
|
||||
refetchInterval: (query) => terminal(query.state.data) ? false : 1_000,
|
||||
});
|
||||
@@ -113,6 +114,12 @@ export function CatalogSyncDrawer({
|
||||
retry: false,
|
||||
refetchInterval: run && !terminal(run) ? 2_000 : false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const latestRun = historyQuery.data?.[0];
|
||||
if (!open || runId || !latestRun) return;
|
||||
onRunChange(latestRun);
|
||||
}, [historyQuery.data, onRunChange, open, runId]);
|
||||
const eventQuery = useQuery({
|
||||
queryKey: ["catalog-sync-events", runId],
|
||||
queryFn: () => listCatalogSyncEvents(runId!, lastSequence.current),
|
||||
@@ -218,25 +225,74 @@ export function CatalogSyncDrawer({
|
||||
}
|
||||
};
|
||||
|
||||
if (!open) return null;
|
||||
return (
|
||||
<aside
|
||||
aria-label="Schema synchronization"
|
||||
className="fixed inset-y-2 right-0 z-40 flex w-full max-w-[440px] flex-col border-l border-border bg-background shadow-2xl sm:inset-y-4"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-4 border-b border-border px-5 py-4">
|
||||
<div>
|
||||
<p className="thot-label">Schema synchronization</p>
|
||||
<h2 className="mt-1 font-heading text-xl font-semibold">{run ? `${stateLabel(run)[0].toUpperCase()}${stateLabel(run).slice(1)}` : "Loading"}</h2>
|
||||
{run ? <p className="mt-1 text-sm text-muted-foreground">{run.scope} · {elapsed(run, now)}</p> : null}
|
||||
const historySection = (
|
||||
<section className="thot-catalog-drawer__history mt-5" aria-label="Synchronization history">
|
||||
<h3 className="thot-label mb-2">Recent runs</h3>
|
||||
{historyQuery.isLoading ? <p className="text-sm text-muted-foreground">Loading recent runs…</p> : null}
|
||||
{historyQuery.isError ? (
|
||||
<div className="rounded-md border border-destructive/30 bg-destructive/5 p-3 text-sm" role="alert">
|
||||
<p className="font-semibold text-destructive">Synchronization history could not be loaded.</p>
|
||||
<Button type="button" variant="outline" size="sm" className="mt-3" disabled={historyQuery.isFetching} onClick={() => void historyQuery.refetch()}>
|
||||
<RotateCcw className={historyQuery.isFetching ? "animate-spin" : ""} aria-hidden="true" /> Retry history
|
||||
</Button>
|
||||
</div>
|
||||
<Button type="button" variant="ghost" size="icon-lg" aria-label="Close synchronization drawer" onClick={onClose}>
|
||||
<X aria-hidden="true" />
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
{!historyQuery.isLoading && !historyQuery.isError && (historyQuery.data ?? []).length === 0 ? (
|
||||
<div className="rounded-md border border-dashed border-border bg-muted/20 px-3 py-4 text-sm text-muted-foreground">
|
||||
No synchronization runs yet.
|
||||
</div>
|
||||
) : null}
|
||||
{(historyQuery.data ?? []).length > 0 ? (
|
||||
<div className="divide-y divide-border rounded-md border border-border">
|
||||
{(historyQuery.data ?? []).slice(0, 5).map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
aria-current={item.id === run?.id ? "true" : undefined}
|
||||
className="flex w-full items-center justify-between gap-3 px-3 py-2 text-left text-sm hover:bg-muted/50 aria-[current=true]:bg-primary/8"
|
||||
onClick={() => onRunChange(item)}
|
||||
>
|
||||
<span>{item.scope}</span>
|
||||
<span className="text-xs text-muted-foreground">{stateLabel(item)} · {new Date(item.createdAt).toLocaleString()}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-y-auto px-5 py-5">
|
||||
{runQuery.isLoading ? <p className="text-sm text-muted-foreground">Loading synchronization…</p> : null}
|
||||
if (!open) return null;
|
||||
const footer = run ? (
|
||||
<>
|
||||
{run.state === "awaiting_confirmation" ? (
|
||||
<Button type="button" disabled={action !== null} onClick={() => void perform("confirm")}>
|
||||
{action === "confirm" ? <LoaderCircle className="animate-spin" /> : <Check />} Confirm removals
|
||||
</Button>
|
||||
) : null}
|
||||
{["queued", "running", "awaiting_confirmation"].includes(run.state) ? (
|
||||
<Button type="button" variant="outline" disabled={action !== null} onClick={() => void perform("cancel")}>Cancel</Button>
|
||||
) : null}
|
||||
{["failed", "cancelled", "interrupted"].includes(run.state) ? (
|
||||
<Button type="button" disabled={action !== null} onClick={() => void perform("retry")}>
|
||||
<RotateCcw className={action === "retry" ? "animate-spin" : ""} /> Retry
|
||||
</Button>
|
||||
) : null}
|
||||
</>
|
||||
) : undefined;
|
||||
return (
|
||||
<FleetLedgerDrawer
|
||||
open
|
||||
ariaLabel="Schema synchronization"
|
||||
eyebrow="Schema synchronization"
|
||||
title={run ? `${stateLabel(run)[0].toUpperCase()}${stateLabel(run).slice(1)}` : "Synchronization history"}
|
||||
description={run ? `${run.scope} · ${elapsed(run, now)}` : undefined}
|
||||
onClose={onClose}
|
||||
closeLabel="Close synchronization drawer"
|
||||
busy={action !== null}
|
||||
bodyClassName="thot-catalog-drawer__history-layout"
|
||||
footer={footer}
|
||||
>
|
||||
{runQuery.isLoading ? <p className="text-sm text-muted-foreground">Loading synchronization…</p> : null}
|
||||
{run ? (
|
||||
<>
|
||||
<section aria-labelledby="synchronization-steps-heading">
|
||||
@@ -365,38 +421,22 @@ export function CatalogSyncDrawer({
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mt-5" aria-label="Synchronization history">
|
||||
<h3 className="thot-label mb-2">Recent runs</h3>
|
||||
<div className="divide-y divide-border rounded-md border border-border">
|
||||
{(historyQuery.data ?? []).slice(0, 5).map((item) => (
|
||||
<button key={item.id} type="button" className="flex w-full items-center justify-between gap-3 px-3 py-2 text-left text-sm hover:bg-muted/50" onClick={() => onRunChange(item)}>
|
||||
<span>{item.scope}</span>
|
||||
<span className="text-xs text-muted-foreground">{stateLabel(item)} · {new Date(item.createdAt).toLocaleString()}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
{historySection}
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{run ? (
|
||||
<div className="flex justify-end gap-2 border-t border-border px-5 py-4">
|
||||
{run.state === "awaiting_confirmation" ? (
|
||||
<Button type="button" disabled={action !== null} onClick={() => void perform("confirm")}>
|
||||
{action === "confirm" ? <LoaderCircle className="animate-spin" /> : <Check />} Confirm removals
|
||||
</Button>
|
||||
) : null}
|
||||
{["queued", "running", "awaiting_confirmation"].includes(run.state) ? (
|
||||
<Button type="button" variant="outline" disabled={action !== null} onClick={() => void perform("cancel")}>Cancel</Button>
|
||||
) : null}
|
||||
{["failed", "cancelled", "interrupted"].includes(run.state) ? (
|
||||
<Button type="button" disabled={action !== null} onClick={() => void perform("retry")}>
|
||||
<RotateCcw className={action === "retry" ? "animate-spin" : ""} /> Retry
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</aside>
|
||||
) : (
|
||||
<>
|
||||
<div className="rounded-lg border border-dashed border-primary/30 bg-primary/5 px-5 py-6 text-sm">
|
||||
<p className="font-semibold">{runQuery.isError ? "This synchronization run could not be loaded." : historyQuery.isLoading ? "Loading synchronization history…" : "No run selected."}</p>
|
||||
<p className="mt-1 text-muted-foreground">Choose a recent run to inspect its steps and event log.</p>
|
||||
{runQuery.isError ? (
|
||||
<Button type="button" variant="outline" size="sm" className="mt-3" disabled={runQuery.isFetching} onClick={() => void runQuery.refetch()}>
|
||||
<RotateCcw className={runQuery.isFetching ? "animate-spin" : ""} aria-hidden="true" /> Retry run
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
{historySection}
|
||||
</>
|
||||
)}
|
||||
</FleetLedgerDrawer>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user