feat: add AI catalog description generation
This commit is contained in:
@@ -6,7 +6,7 @@ import {
|
||||
useState,
|
||||
} from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Plus, RefreshCw } from "lucide-react";
|
||||
import { History, Plus, RefreshCw } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { ApiError, apiErrorMessage } from "../api/client";
|
||||
@@ -15,8 +15,11 @@ import {
|
||||
deleteCatalogDatabase,
|
||||
deleteCatalogDatabaseMetadata,
|
||||
listCatalogDatabases,
|
||||
listDescriptionGenerationRuns,
|
||||
listMetadataGenerationModels,
|
||||
replaceCatalogDatabaseSecrets,
|
||||
startCatalogSync,
|
||||
startDescriptionGenerationRun,
|
||||
testCatalogDatabase,
|
||||
updateCatalogDatabase,
|
||||
type CatalogDatabase,
|
||||
@@ -26,12 +29,15 @@ import {
|
||||
type CatalogSyncRun,
|
||||
type DatabaseBinding,
|
||||
type DatabaseTransport,
|
||||
type DescriptionGenerationRun,
|
||||
} from "../api/catalog-databases";
|
||||
import { DatabaseGrid } from "./database-management/DatabaseGrid";
|
||||
import { DatabaseForm } from "./database-management/DatabaseForm";
|
||||
import { DatabaseTables } from "./database-management/DatabaseTables";
|
||||
import { DatabaseRelationships } from "./database-management/DatabaseRelationships";
|
||||
import { CatalogSyncDrawer } from "./database-management/CatalogSyncDrawer";
|
||||
import { MetadataGenerationModelSelector } from "./database-management/MetadataGenerationModelSelector";
|
||||
import { DescriptionGenerationDrawer } from "./database-management/DescriptionGenerationDrawer";
|
||||
import {
|
||||
configurationFingerprint,
|
||||
configurationFromDraft,
|
||||
@@ -46,6 +52,8 @@ import {
|
||||
} from "./database-management/model";
|
||||
|
||||
const DATABASE_QUERY_KEY = ["catalog-databases"] as const;
|
||||
const DESCRIPTION_GENERATION_HISTORY_QUERY_KEY = ["description-generation-runs", 50] as const;
|
||||
const DESCRIPTION_GENERATION_ACTIVE_STATUSES = ["queued", "running"] as const;
|
||||
const SYNC_STARTED_MESSAGES: Record<CatalogSyncScope, string> = {
|
||||
tables: "Table synchronization started",
|
||||
columns: "Column synchronization started",
|
||||
@@ -68,6 +76,12 @@ function isStaleError(error: unknown): boolean {
|
||||
return error instanceof ApiError && error.code === "database_stale";
|
||||
}
|
||||
|
||||
function isDescriptionGenerationActive(run?: DescriptionGenerationRun | null): boolean {
|
||||
return Boolean(run && DESCRIPTION_GENERATION_ACTIVE_STATUSES.some(
|
||||
(status) => status === run.status,
|
||||
));
|
||||
}
|
||||
|
||||
export function DatabaseManagementPage({
|
||||
canManage,
|
||||
canManageSecrets,
|
||||
@@ -86,8 +100,31 @@ export function DatabaseManagementPage({
|
||||
retry: false,
|
||||
});
|
||||
const rows = data ?? [];
|
||||
const metadataModelsQuery = useQuery({
|
||||
queryKey: ["metadata-generation-models"],
|
||||
queryFn: listMetadataGenerationModels,
|
||||
retry: false,
|
||||
});
|
||||
const metadataModels = metadataModelsQuery.data?.models ?? [];
|
||||
const configuredMetadataDefault = metadataModelsQuery.data?.default ?? "";
|
||||
const defaultMetadataModel = configuredMetadataDefault
|
||||
&& metadataModels.some((model) => model.id === configuredMetadataDefault)
|
||||
? configuredMetadataDefault
|
||||
: "";
|
||||
const descriptionGenerationRunsQuery = useQuery({
|
||||
queryKey: DESCRIPTION_GENERATION_HISTORY_QUERY_KEY,
|
||||
queryFn: () => listDescriptionGenerationRuns(50),
|
||||
enabled: canManage,
|
||||
retry: false,
|
||||
refetchInterval: 5_000,
|
||||
});
|
||||
const descriptionGenerationRuns = descriptionGenerationRunsQuery.data ?? [];
|
||||
const observedActiveDescriptionGenerationRun = descriptionGenerationRuns.find(
|
||||
(run) => isDescriptionGenerationActive(run),
|
||||
);
|
||||
|
||||
const [screen, setScreen] = useState<DatabaseScreen>({ kind: "list" });
|
||||
const [selectedMetadataModel, setSelectedMetadataModel] = useState("");
|
||||
const [draft, setDraft] = useState<DatabaseFormDraft | null>(null);
|
||||
const [baseline, setBaseline] = useState("");
|
||||
const [formSource, setFormSource] = useState<FormSource | null>(null);
|
||||
@@ -102,11 +139,17 @@ export function DatabaseManagementPage({
|
||||
});
|
||||
const [activeSyncRun, setActiveSyncRun] = useState<CatalogSyncRun | null>(null);
|
||||
const [syncDrawerOpen, setSyncDrawerOpen] = useState(false);
|
||||
const [activeDescriptionGenerationRun, setActiveDescriptionGenerationRun] = useState<DescriptionGenerationRun | null>(null);
|
||||
const [descriptionGenerationDrawerOpen, setDescriptionGenerationDrawerOpen] = useState(false);
|
||||
|
||||
const originRef = useRef<HTMLElement | null>(null);
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
const formHeadingRef = useRef<HTMLHeadingElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedMetadataModel(defaultMetadataModel);
|
||||
}, [defaultMetadataModel]);
|
||||
|
||||
const activeRow = screen.kind === "list"
|
||||
? undefined
|
||||
: rows.find((row) => row.workspaceId === screen.workspaceId);
|
||||
@@ -556,6 +599,36 @@ export function DatabaseManagementPage({
|
||||
setSyncDrawerOpen(true);
|
||||
}, [updateTrackedSyncRun]);
|
||||
|
||||
const rememberDescriptionGenerationRun = useCallback((run: DescriptionGenerationRun) => {
|
||||
setActiveDescriptionGenerationRun(run);
|
||||
queryClient.setQueryData<DescriptionGenerationRun[]>(
|
||||
DESCRIPTION_GENERATION_HISTORY_QUERY_KEY,
|
||||
(current = []) => [run, ...current.filter((item) => item.id !== run.id)],
|
||||
);
|
||||
setDescriptionGenerationDrawerOpen(true);
|
||||
}, [queryClient]);
|
||||
|
||||
const openDescriptionGenerationHistory = useCallback(() => {
|
||||
const run = observedActiveDescriptionGenerationRun
|
||||
?? descriptionGenerationRuns[0]
|
||||
?? activeDescriptionGenerationRun;
|
||||
if (!run) return;
|
||||
setActiveDescriptionGenerationRun(run);
|
||||
setDescriptionGenerationDrawerOpen(true);
|
||||
}, [activeDescriptionGenerationRun, descriptionGenerationRuns, observedActiveDescriptionGenerationRun]);
|
||||
|
||||
const descriptionGenerationTerminated = useCallback(async (run: DescriptionGenerationRun) => {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["catalog-tables", run.databaseId],
|
||||
exact: true,
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["catalog-columns", run.databaseId],
|
||||
}),
|
||||
]);
|
||||
}, [queryClient]);
|
||||
|
||||
const openSync = useCallback((row?: CatalogDatabase) => {
|
||||
const run = row?.activeSyncRun ?? activeSyncRun;
|
||||
if (!run) return;
|
||||
@@ -590,6 +663,26 @@ export function DatabaseManagementPage({
|
||||
}
|
||||
}, [queryClient, rememberSyncRun]);
|
||||
|
||||
const generateDatabaseDescriptions = useCallback(async (
|
||||
selected: CatalogDatabase[],
|
||||
scope: "all" | "missing",
|
||||
) => {
|
||||
const database = selected.length === 1 ? selected[0] : undefined;
|
||||
if (!database?.id || !selectedMetadataModel) return;
|
||||
try {
|
||||
const run = await startDescriptionGenerationRun(
|
||||
database.id,
|
||||
selectedMetadataModel,
|
||||
scope,
|
||||
);
|
||||
rememberDescriptionGenerationRun(run);
|
||||
toast.success(`${scope === "all" ? "Generate All" : "Generate Missing"} started for ${database.workspaceName}`);
|
||||
} catch (error) {
|
||||
toast.error(apiErrorMessage(error));
|
||||
throw error;
|
||||
}
|
||||
}, [rememberDescriptionGenerationRun, selectedMetadataModel]);
|
||||
|
||||
const deleteSelectedMetadata = useCallback(async (
|
||||
selected: CatalogDatabase[],
|
||||
target: CatalogDatabaseMetadataDeleteTarget,
|
||||
@@ -640,6 +733,16 @@ export function DatabaseManagementPage({
|
||||
: activeRow?.activeSyncRun && ["queued", "running", "awaiting_confirmation", "applying"].includes(activeRow.activeSyncRun.state)
|
||||
? activeRow.activeSyncRun
|
||||
: undefined;
|
||||
const selectedMetadataModelAvailable = Boolean(
|
||||
selectedMetadataModel
|
||||
&& metadataModels.some((model) => model.id === selectedMetadataModel),
|
||||
);
|
||||
const descriptionGenerationActive = isDescriptionGenerationActive(
|
||||
observedActiveDescriptionGenerationRun ?? activeDescriptionGenerationRun,
|
||||
);
|
||||
const descriptionGenerationHistoryAvailable = Boolean(
|
||||
descriptionGenerationRuns.length > 0 || activeDescriptionGenerationRun,
|
||||
);
|
||||
|
||||
return (
|
||||
<main aria-label="Database management" className="flex min-h-0 flex-1 flex-col overflow-hidden bg-background">
|
||||
@@ -651,21 +754,46 @@ export function DatabaseManagementPage({
|
||||
One database configuration for each repository workspace.
|
||||
</p>
|
||||
</div>
|
||||
{screen.kind === "list" ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button type="button" variant="outline" disabled={isFetching} onClick={() => void refreshList()}>
|
||||
<RefreshCw className={isFetching ? "animate-spin" : ""} /> Refresh
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
disabled={!canManage || availableWorkspaces.length === 0}
|
||||
title={availableWorkspaces.length === 0 ? "Every available workspace is already configured" : undefined}
|
||||
onClick={(event) => addDatabase(event.currentTarget)}
|
||||
>
|
||||
<Plus /> Add database
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="flex flex-wrap items-end justify-end gap-3">
|
||||
<MetadataGenerationModelSelector
|
||||
canManage={canManage}
|
||||
data={metadataModelsQuery.data}
|
||||
isError={metadataModelsQuery.isError}
|
||||
isLoading={metadataModelsQuery.isLoading}
|
||||
selectedModel={selectedMetadataModel}
|
||||
onSelectedModelChange={setSelectedMetadataModel}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
aria-label={observedActiveDescriptionGenerationRun
|
||||
? "Observe active description generation"
|
||||
: "View description generation history"}
|
||||
disabled={!canManage || !descriptionGenerationHistoryAvailable}
|
||||
title={!descriptionGenerationHistoryAvailable && !descriptionGenerationRunsQuery.isLoading
|
||||
? "No description generation runs yet"
|
||||
: undefined}
|
||||
onClick={openDescriptionGenerationHistory}
|
||||
>
|
||||
<History />
|
||||
{observedActiveDescriptionGenerationRun ? "Active run" : "Run history"}
|
||||
</Button>
|
||||
{screen.kind === "list" ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button type="button" variant="outline" disabled={isFetching} onClick={() => void refreshList()}>
|
||||
<RefreshCw className={isFetching ? "animate-spin" : ""} /> Refresh
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
disabled={!canManage || availableWorkspaces.length === 0}
|
||||
title={availableWorkspaces.length === 0 ? "Every available workspace is already configured" : undefined}
|
||||
onClick={(event) => addDatabase(event.currentTarget)}
|
||||
>
|
||||
<Plus /> Add database
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="relative flex min-h-0 flex-1">
|
||||
@@ -701,6 +829,9 @@ export function DatabaseManagementPage({
|
||||
onOpenSync={openSync}
|
||||
onTestSelected={testSelected}
|
||||
onSyncSelected={syncSelected}
|
||||
selectedMetadataModel={selectedMetadataModelAvailable ? selectedMetadataModel : null}
|
||||
descriptionGenerationActive={descriptionGenerationActive}
|
||||
onGenerateDescriptions={generateDatabaseDescriptions}
|
||||
onDeleteMetadataSelected={deleteSelectedMetadata}
|
||||
/>
|
||||
)}
|
||||
@@ -746,12 +877,15 @@ export function DatabaseManagementPage({
|
||||
database={activeRow}
|
||||
canManage={canManage}
|
||||
activeRun={currentActiveRun}
|
||||
selectedMetadataModel={selectedMetadataModelAvailable ? selectedMetadataModel : null}
|
||||
descriptionGenerationActive={descriptionGenerationActive}
|
||||
onBackToDatabases={showList}
|
||||
onOpenOverview={() => openOverview(activeRow)}
|
||||
onOpenRelationships={() => openRelationships(activeRow)}
|
||||
onNavigationStateChange={setTablesNavigationState}
|
||||
onRunStarted={rememberSyncRun}
|
||||
onOpenSync={() => openSync(activeRow)}
|
||||
onDescriptionGenerationRunStarted={rememberDescriptionGenerationRun}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -776,6 +910,16 @@ export function DatabaseManagementPage({
|
||||
onRunUpdate={updateTrackedSyncRun}
|
||||
onCatalogChanged={() => void catalogChanged()}
|
||||
/>
|
||||
<DescriptionGenerationDrawer
|
||||
open={descriptionGenerationDrawerOpen}
|
||||
run={activeDescriptionGenerationRun}
|
||||
modelLabel={metadataModels.find(
|
||||
(model) => model.id === activeDescriptionGenerationRun?.modelId,
|
||||
)?.label ?? activeDescriptionGenerationRun?.modelId ?? ""}
|
||||
onClose={() => setDescriptionGenerationDrawerOpen(false)}
|
||||
onRunUpdate={setActiveDescriptionGenerationRun}
|
||||
onTerminal={(run) => void descriptionGenerationTerminated(run)}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user