feat: add AI catalog description generation

This commit is contained in:
Codex
2026-08-29 16:42:56 +02:00
parent b0afba81ca
commit 376dd5a09d
76 changed files with 14860 additions and 102 deletions
@@ -8,14 +8,17 @@ import { toast } from "sonner";
import { Button } from "../../components/ui/button";
import { ApiError, apiErrorMessage } from "../../api/client";
import {
consolidateCatalogDescriptions,
deleteCatalogTableMetadata,
listCatalogTables,
startCatalogSync,
startDescriptionGenerationRun,
updateCatalogTableMetadata,
type CatalogDatabase,
type CatalogSyncRun,
type CatalogTable,
type CatalogTableMetadataDeleteTarget,
type DescriptionGenerationRun,
} from "../../api/catalog-databases";
import type { DatabaseNavigationState } from "./model";
import { DatabaseColumns } from "./DatabaseColumns";
@@ -24,12 +27,15 @@ interface Props {
database: CatalogDatabase;
canManage: boolean;
activeRun?: CatalogSyncRun;
selectedMetadataModel: string | null;
descriptionGenerationActive: boolean;
onBackToDatabases: () => void;
onOpenOverview: () => void;
onOpenRelationships: () => void;
onNavigationStateChange: (state: DatabaseNavigationState) => void;
onRunStarted: (run: CatalogSyncRun) => void;
onOpenSync: () => void;
onDescriptionGenerationRunStarted: (run: DescriptionGenerationRun) => void;
}
interface TableGridContext {
@@ -55,12 +61,15 @@ export function DatabaseTables({
database,
canManage,
activeRun,
selectedMetadataModel,
descriptionGenerationActive,
onBackToDatabases,
onOpenOverview,
onOpenRelationships,
onNavigationStateChange,
onRunStarted,
onOpenSync,
onDescriptionGenerationRunStarted,
}: Props) {
const databaseId = database.id!;
const queryClient = useQueryClient();
@@ -77,7 +86,7 @@ export function DatabaseTables({
const [editorVersion, setEditorVersion] = useState<number | null>(null);
const [stale, setStale] = useState(false);
const [staleBannerOpen, setStaleBannerOpen] = useState(true);
const [busy, setBusy] = useState<"sync" | "save" | "delete" | null>(null);
const [busy, setBusy] = useState<"sync" | "save" | "delete" | "consolidate" | "generate" | null>(null);
const [pendingDelete, setPendingDelete] = useState<CatalogTableMetadataDeleteTarget | null>(null);
const [columnNavigation, setColumnNavigation] = useState<DatabaseNavigationState>({ dirty: false, busy: false });
const gridRef = useRef<AgGridReact<CatalogTable>>(null);
@@ -203,6 +212,34 @@ export function DatabaseTables({
toast.error(apiErrorMessage(error));
} finally { setBusy(null); }
};
const consolidateDescriptions = async () => {
setBusy("consolidate");
try {
const counts = await consolidateCatalogDescriptions(databaseId, "tables", selectedIds);
await queryClient.invalidateQueries({ queryKey, exact: true });
gridRef.current?.api.deselectAll();
setSelectedIds([]);
toast.success(`Copied ${counts.copied} description${counts.copied === 1 ? "" : "s"}; skipped ${counts.skipped}`);
} catch (error) {
toast.error(apiErrorMessage(error));
} finally { setBusy(null); }
};
const generateDescriptions = async () => {
if (selectedIds.length === 0 || !selectedMetadataModel) return;
setBusy("generate");
try {
const run = await startDescriptionGenerationRun(
databaseId,
selectedMetadataModel,
"selected_tables",
selectedIds,
);
onDescriptionGenerationRunStarted(run);
toast.success(`Description generation started for ${selectedIds.length} table${selectedIds.length === 1 ? "" : "s"}`);
} catch (error) {
toast.error(apiErrorMessage(error));
} finally { setBusy(null); }
};
const columns = useMemo<ColDef<CatalogTable>[]>(() => [
{ field: "name", headerName: "Name", minWidth: 250, flex: 1, cellClass: "font-mono text-xs" },
@@ -257,7 +294,16 @@ export function DatabaseTables({
</nav>
</div>
{tableSection === "columns" ? (
<DatabaseColumns databaseId={databaseId} table={activeTable} canManage={canManage} onNavigationStateChange={setColumnNavigation} onSync={() => void synchronize("columns", [activeTable.id])} />
<DatabaseColumns
databaseId={databaseId}
table={activeTable}
canManage={canManage}
selectedMetadataModel={selectedMetadataModel}
descriptionGenerationActive={descriptionGenerationActive}
onDescriptionGenerationRunStarted={onDescriptionGenerationRunStarted}
onNavigationStateChange={setColumnNavigation}
onSync={() => void synchronize("columns", [activeTable.id])}
/>
) : (
<div className="min-h-0 overflow-y-auto px-4 py-5">
<p className="text-sm text-muted-foreground">Only review metadata can be changed.</p>
@@ -334,6 +380,13 @@ export function DatabaseTables({
<Menu.Portal>
<Menu.Positioner side="bottom" align="start" sideOffset={4}>
<Menu.Popup className="z-50 min-w-64 rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-none">
<Menu.Item
className="rounded-md px-3 py-2 text-sm outline-none data-[highlighted]:bg-muted data-[disabled]:opacity-45"
disabled={!canManage || !selectedMetadataModel || descriptionGenerationActive || busy !== null || Boolean(currentRun)}
onClick={() => void generateDescriptions()}
>
Generate {selectedIds.length === 1 ? "description" : "descriptions"}
</Menu.Item>
<Menu.Item
className="rounded-md px-3 py-2 text-sm outline-none data-[highlighted]:bg-muted data-[disabled]:opacity-45"
disabled={!canManage || !bindingReady || busy !== null || Boolean(currentRun)}
@@ -341,6 +394,13 @@ export function DatabaseTables({
>
Synchronize columns
</Menu.Item>
<Menu.Item
className="rounded-md px-3 py-2 text-sm outline-none data-[highlighted]:bg-muted data-[disabled]:opacity-45"
disabled={!canManage || busy !== null || Boolean(currentRun)}
onClick={() => void consolidateDescriptions()}
>
Move generated description to Description
</Menu.Item>
<Menu.Separator className="my-1 h-px bg-border" />
<Menu.Item
className="rounded-md px-3 py-2 text-sm text-destructive outline-none data-[highlighted]:bg-destructive/10 data-[disabled]:opacity-45"