feat: implement metadata catalog database management
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { AgGridReact } from "ag-grid-react";
|
||||
import type { ColDef } from "ag-grid-community";
|
||||
import { ArrowLeft, RefreshCw } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { apiErrorMessage } from "../../api/client";
|
||||
import {
|
||||
listCatalogRelationships,
|
||||
startCatalogSync,
|
||||
type CatalogDatabase,
|
||||
type CatalogRelationship,
|
||||
type CatalogSyncRun,
|
||||
} from "../../api/catalog-databases";
|
||||
|
||||
interface Props {
|
||||
database: CatalogDatabase;
|
||||
canManage: boolean;
|
||||
onBackToDatabases: () => void;
|
||||
onOpenOverview: () => void;
|
||||
onOpenTables: () => void;
|
||||
onRunStarted: (run: CatalogSyncRun) => void;
|
||||
onOpenSync: () => void;
|
||||
}
|
||||
|
||||
export function DatabaseRelationships({
|
||||
database,
|
||||
canManage,
|
||||
onBackToDatabases,
|
||||
onOpenOverview,
|
||||
onOpenTables,
|
||||
onRunStarted,
|
||||
onOpenSync,
|
||||
}: Props) {
|
||||
const databaseId = database.id!;
|
||||
const { data = [], isLoading, isFetching, refetch } = useQuery({
|
||||
queryKey: ["catalog-relationships", databaseId],
|
||||
queryFn: () => listCatalogRelationships(databaseId),
|
||||
retry: false,
|
||||
});
|
||||
const [search, setSearch] = useState("");
|
||||
const [busy, setBusy] = useState(false);
|
||||
const bindingReady = database.connectionStatus === "reachable" && database.testedVersion === database.version;
|
||||
|
||||
const synchronize = async (scope: "relationships" | "all") => {
|
||||
setBusy(true);
|
||||
try {
|
||||
const run = await startCatalogSync(databaseId, database.version, scope);
|
||||
onRunStarted(run);
|
||||
toast.success(scope === "all" ? "Full schema synchronization started" : "Relationship synchronization started");
|
||||
} catch (error) { toast.error(apiErrorMessage(error)); } finally { setBusy(false); }
|
||||
};
|
||||
|
||||
const columns = useMemo<ColDef<CatalogRelationship>[]>(() => [
|
||||
{ field: "constraintName", headerName: "Constraint", minWidth: 220, flex: 1, cellClass: "font-mono text-xs" },
|
||||
{ field: "sourceTableName", headerName: "Source table", minWidth: 200, flex: 1 },
|
||||
{ headerName: "Source columns", minWidth: 200, flex: 1, valueGetter: ({ data: row }) => row?.columns.map((pair) => pair.sourceColumnName).join(", ") ?? "" },
|
||||
{ field: "targetTableName", headerName: "Target table", minWidth: 200, flex: 1 },
|
||||
{ headerName: "Target columns", minWidth: 200, flex: 1, valueGetter: ({ data: row }) => row?.columns.map((pair) => pair.targetColumnName).join(", ") ?? "" },
|
||||
{ field: "updateRule", headerName: "On update", minWidth: 125, width: 125 },
|
||||
{ field: "deleteRule", headerName: "On delete", minWidth: 125, width: 125 },
|
||||
], []);
|
||||
|
||||
return (
|
||||
<section aria-label={`Relationships for ${database.workspaceName}`} className="flex min-h-0 flex-1 flex-col overflow-hidden bg-background">
|
||||
<div className="px-4 pt-5 sm:px-5">
|
||||
<Button type="button" variant="ghost" className="-ml-2 mb-4" disabled={busy} onClick={onBackToDatabases}><ArrowLeft />Back to databases</Button>
|
||||
<div className="flex flex-wrap items-start justify-between gap-4 border-b border-border pb-5">
|
||||
<div>
|
||||
<p className="thot-label mb-1">Database management</p>
|
||||
<h2 className="font-heading text-2xl font-semibold tracking-tight">{database.workspaceName}</h2>
|
||||
<p className="mt-1 text-sm text-muted-foreground">{database.databaseName} · {database.schema}</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{database.activeSyncRun ? <Button type="button" variant="outline" onClick={onOpenSync}><RefreshCw className="animate-spin" />View sync</Button> : null}
|
||||
<Button type="button" disabled={!canManage || !bindingReady || busy || Boolean(database.activeSyncRun)} onClick={() => void synchronize("all")}><RefreshCw />Sync all</Button>
|
||||
</div>
|
||||
</div>
|
||||
<nav aria-label="Database sections" className="flex gap-1 border-b border-border" role="tablist">
|
||||
<button type="button" role="tab" aria-selected="false" className="border-b-2 border-transparent px-3 py-2 text-sm font-semibold text-muted-foreground hover:text-foreground" onClick={onOpenOverview}>Overview</button>
|
||||
<button type="button" role="tab" aria-selected="false" className="border-b-2 border-transparent px-3 py-2 text-sm font-semibold text-muted-foreground hover:text-foreground" onClick={onOpenTables}>Tables</button>
|
||||
<button type="button" role="tab" aria-selected="true" className="border-b-2 border-primary px-3 py-2 text-sm font-semibold text-foreground">Relationships</button>
|
||||
</nav>
|
||||
</div>
|
||||
<div className="mx-3 mb-4 mt-4 flex min-h-0 flex-1 flex-col overflow-hidden rounded-md border border-border bg-card sm:mx-5">
|
||||
<div className="flex min-h-12 flex-wrap items-center gap-3 border-b border-border px-3 py-2">
|
||||
<span className="thot-label whitespace-nowrap">Physical relationships</span>
|
||||
<input className="h-8 min-w-40 flex-1 rounded-md border border-input bg-background px-2.5 text-sm outline-none focus:border-primary/60 focus:ring-3 focus:ring-ring/15" aria-label="Search relationships" placeholder="Search" value={search} onChange={(event) => setSearch(event.target.value)} />
|
||||
<span className="text-xs tabular-nums text-muted-foreground">{data.length}</span>
|
||||
<Button type="button" variant="outline" disabled={isFetching || busy} onClick={() => void refetch()}><RefreshCw className={isFetching ? "animate-spin" : ""} />Refresh</Button>
|
||||
<Button type="button" disabled={!canManage || !bindingReady || busy || Boolean(database.activeSyncRun)} title={!bindingReady ? "Test the current database binding before synchronizing relationships" : undefined} onClick={() => void synchronize("relationships")}><RefreshCw />Sync relationships</Button>
|
||||
</div>
|
||||
{!bindingReady ? <div className="border-b border-border bg-muted/35 px-4 py-3 text-sm text-muted-foreground">Test the current database binding from Overview before synchronizing relationships.</div> : null}
|
||||
<div className="relative min-h-[280px] flex-1">
|
||||
<div className="thot-database-grid ag-theme-alpine absolute inset-0 h-full w-full">
|
||||
<AgGridReact<CatalogRelationship> rowData={data} columnDefs={columns} loading={isLoading} quickFilterText={search} defaultColDef={{ sortable: true, filter: true, resizable: true }} getRowId={({ data: row }) => row.id} rowHeight={44} headerHeight={38} animateRows={false} overlayNoRowsTemplate="No physical relationships synchronized for this database." />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user