feat: complete catalog fleet management workflow
This commit is contained in:
@@ -14,9 +14,11 @@ import {
|
||||
createCatalogDatabase,
|
||||
deleteCatalogDatabase,
|
||||
deleteCatalogDatabaseMetadata,
|
||||
getCatalogMetrics,
|
||||
listCatalogDatabases,
|
||||
listDescriptionGenerationRuns,
|
||||
listMetadataGenerationModels,
|
||||
listSensitiveDataSuggestionRuns,
|
||||
replaceCatalogDatabaseSecrets,
|
||||
startCatalogSync,
|
||||
startDescriptionGenerationRun,
|
||||
@@ -34,15 +36,27 @@ import {
|
||||
type DescriptionGenerationRun,
|
||||
type SensitiveDataSuggestion,
|
||||
type SensitiveDataSuggestionRequest,
|
||||
type SensitiveDataSuggestionRun,
|
||||
} from "../api/catalog-databases";
|
||||
import { DatabaseGrid } from "./database-management/DatabaseGrid";
|
||||
import { DatabaseForm } from "./database-management/DatabaseForm";
|
||||
import { databaseFormTitle } 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 { SensitiveDataSuggestionHistoryDrawer } from "./database-management/SensitiveDataSuggestionHistoryDrawer";
|
||||
import { SensitiveDataReviewDrawer } from "./database-management/SensitiveDataReviewDrawer";
|
||||
import {
|
||||
FleetLedgerBreadcrumb,
|
||||
FleetLedgerContent,
|
||||
FleetLedgerDrawer,
|
||||
FleetLedgerHeader,
|
||||
FleetLedgerKpiStrip,
|
||||
FleetLedgerRealStatus,
|
||||
FleetLedgerShell,
|
||||
} from "./database-management/FleetLedgerShell";
|
||||
import {
|
||||
configurationFingerprint,
|
||||
configurationFromDraft,
|
||||
@@ -58,6 +72,7 @@ import {
|
||||
|
||||
const DATABASE_QUERY_KEY = ["catalog-databases"] as const;
|
||||
const DESCRIPTION_GENERATION_HISTORY_QUERY_KEY = ["description-generation-runs", 50] as const;
|
||||
const SENSITIVE_DATA_SUGGESTION_HISTORY_QUERY_KEY = ["sensitive-data-suggestion-runs", 50] as const;
|
||||
const DESCRIPTION_GENERATION_ACTIVE_STATUSES = ["queued", "running"] as const;
|
||||
const SYNC_STARTED_MESSAGES: Record<CatalogSyncScope, string> = {
|
||||
tables: "Table synchronization started",
|
||||
@@ -70,6 +85,7 @@ interface Props {
|
||||
canManage: boolean;
|
||||
canManageSecrets: boolean;
|
||||
onNavigationStateChange?: (state: DatabaseNavigationState) => void;
|
||||
presentation?: "legacy" | "fleet";
|
||||
}
|
||||
|
||||
interface FormSource {
|
||||
@@ -91,6 +107,7 @@ export function DatabaseManagementPage({
|
||||
canManage,
|
||||
canManageSecrets,
|
||||
onNavigationStateChange,
|
||||
presentation = "legacy",
|
||||
}: Props) {
|
||||
const queryClient = useQueryClient();
|
||||
const {
|
||||
@@ -127,6 +144,17 @@ export function DatabaseManagementPage({
|
||||
const observedActiveDescriptionGenerationRun = descriptionGenerationRuns.find(
|
||||
(run) => isDescriptionGenerationActive(run),
|
||||
);
|
||||
const sensitiveDataSuggestionRunsQuery = useQuery({
|
||||
queryKey: SENSITIVE_DATA_SUGGESTION_HISTORY_QUERY_KEY,
|
||||
queryFn: () => listSensitiveDataSuggestionRuns(50),
|
||||
enabled: canManage,
|
||||
retry: false,
|
||||
refetchInterval: 5_000,
|
||||
});
|
||||
const sensitiveDataSuggestionRuns = sensitiveDataSuggestionRunsQuery.data ?? [];
|
||||
const observedActiveSensitiveDataSuggestionRun = sensitiveDataSuggestionRuns.find(
|
||||
(run) => run.status === "running",
|
||||
);
|
||||
|
||||
const [screen, setScreen] = useState<DatabaseScreen>({ kind: "list" });
|
||||
const [selectedMetadataModel, setSelectedMetadataModel] = useState("");
|
||||
@@ -142,10 +170,14 @@ export function DatabaseManagementPage({
|
||||
dirty: false,
|
||||
busy: false,
|
||||
});
|
||||
const [tableNestedNavigationActive, setTableNestedNavigationActive] = useState(false);
|
||||
const [activeSyncRun, setActiveSyncRun] = useState<CatalogSyncRun | null>(null);
|
||||
const [syncHistoryDatabaseId, setSyncHistoryDatabaseId] = useState<string | null>(null);
|
||||
const [syncDrawerOpen, setSyncDrawerOpen] = useState(false);
|
||||
const [activeDescriptionGenerationRun, setActiveDescriptionGenerationRun] = useState<DescriptionGenerationRun | null>(null);
|
||||
const [descriptionGenerationDrawerOpen, setDescriptionGenerationDrawerOpen] = useState(false);
|
||||
const [activeSensitiveDataSuggestionRun, setActiveSensitiveDataSuggestionRun] = useState<SensitiveDataSuggestionRun | null>(null);
|
||||
const [sensitiveDataSuggestionHistoryDrawerOpen, setSensitiveDataSuggestionHistoryDrawerOpen] = useState(false);
|
||||
const [sensitiveReview, setSensitiveReview] = useState<{
|
||||
databaseId: string;
|
||||
scopeLabel: string;
|
||||
@@ -163,6 +195,20 @@ export function DatabaseManagementPage({
|
||||
const activeRow = screen.kind === "list"
|
||||
? undefined
|
||||
: rows.find((row) => row.workspaceId === screen.workspaceId);
|
||||
const metricsDatabaseId = (screen.kind === "tables" || screen.kind === "relationships")
|
||||
? activeRow?.id
|
||||
: undefined;
|
||||
const catalogMetricsQuery = useQuery({
|
||||
queryKey: ["catalog-metrics", metricsDatabaseId ?? "global"],
|
||||
queryFn: () => getCatalogMetrics(metricsDatabaseId),
|
||||
enabled: canManage && (metricsDatabaseId === undefined || Boolean(activeRow?.id)),
|
||||
retry: false,
|
||||
refetchInterval: presentation === "fleet" ? 15_000 : false,
|
||||
});
|
||||
const invalidateCatalogMetrics = useCallback(
|
||||
() => queryClient.invalidateQueries({ queryKey: ["catalog-metrics"] }),
|
||||
[queryClient],
|
||||
);
|
||||
const availableWorkspaces = useMemo(
|
||||
() => rows.filter((row) => row.workspaceAvailable && !row.configured),
|
||||
[rows],
|
||||
@@ -235,6 +281,7 @@ export function DatabaseManagementPage({
|
||||
setStaleBannerOpen(true);
|
||||
setPartialSecretFailure(null);
|
||||
setTablesNavigationState({ dirty: false, busy: false });
|
||||
setTableNestedNavigationActive(false);
|
||||
restoreListFocus();
|
||||
}, [restoreListFocus]);
|
||||
|
||||
@@ -298,6 +345,7 @@ export function DatabaseManagementPage({
|
||||
setStale(false);
|
||||
setPartialSecretFailure(null);
|
||||
setTablesNavigationState({ dirty: false, busy: false });
|
||||
setTableNestedNavigationActive(false);
|
||||
setScreen({ kind: "tables", workspaceId: row.workspaceId });
|
||||
}, []);
|
||||
|
||||
@@ -590,10 +638,11 @@ export function DatabaseManagementPage({
|
||||
try {
|
||||
const result = await refetch();
|
||||
if (result.error) throw result.error;
|
||||
await invalidateCatalogMetrics();
|
||||
} catch (error) {
|
||||
toast.error(apiErrorMessage(error));
|
||||
}
|
||||
}, [isFetching, refetch]);
|
||||
}, [invalidateCatalogMetrics, isFetching, refetch]);
|
||||
|
||||
const updateTrackedSyncRun = useCallback((run: CatalogSyncRun) => {
|
||||
setActiveSyncRun(run);
|
||||
@@ -606,6 +655,7 @@ export function DatabaseManagementPage({
|
||||
|
||||
const rememberSyncRun = useCallback((run: CatalogSyncRun) => {
|
||||
updateTrackedSyncRun(run);
|
||||
setSyncHistoryDatabaseId(run.databaseId);
|
||||
setSyncDrawerOpen(true);
|
||||
}, [updateTrackedSyncRun]);
|
||||
|
||||
@@ -622,11 +672,20 @@ export function DatabaseManagementPage({
|
||||
const run = observedActiveDescriptionGenerationRun
|
||||
?? descriptionGenerationRuns[0]
|
||||
?? activeDescriptionGenerationRun;
|
||||
if (!run) return;
|
||||
setActiveDescriptionGenerationRun(run);
|
||||
if (run) setActiveDescriptionGenerationRun(run);
|
||||
setSensitiveDataSuggestionHistoryDrawerOpen(false);
|
||||
setDescriptionGenerationDrawerOpen(true);
|
||||
}, [activeDescriptionGenerationRun, descriptionGenerationRuns, observedActiveDescriptionGenerationRun]);
|
||||
|
||||
const openSensitiveDataSuggestionHistory = useCallback(() => {
|
||||
const run = observedActiveSensitiveDataSuggestionRun
|
||||
?? sensitiveDataSuggestionRuns[0]
|
||||
?? activeSensitiveDataSuggestionRun;
|
||||
if (run) setActiveSensitiveDataSuggestionRun(run);
|
||||
setDescriptionGenerationDrawerOpen(false);
|
||||
setSensitiveDataSuggestionHistoryDrawerOpen(true);
|
||||
}, [activeSensitiveDataSuggestionRun, observedActiveSensitiveDataSuggestionRun, sensitiveDataSuggestionRuns]);
|
||||
|
||||
const descriptionGenerationTerminated = useCallback(async (run: DescriptionGenerationRun) => {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({
|
||||
@@ -636,15 +695,19 @@ export function DatabaseManagementPage({
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["catalog-columns", run.databaseId],
|
||||
}),
|
||||
invalidateCatalogMetrics(),
|
||||
]);
|
||||
}, [queryClient]);
|
||||
}, [invalidateCatalogMetrics, queryClient]);
|
||||
|
||||
const openSync = useCallback((row?: CatalogDatabase) => {
|
||||
const run = row?.activeSyncRun ?? activeSyncRun;
|
||||
if (!run) return;
|
||||
const databaseId = row?.id ?? activeSyncRun?.databaseId ?? activeRow?.id ?? null;
|
||||
if (!databaseId) return;
|
||||
const run = row?.activeSyncRun
|
||||
?? (activeSyncRun?.databaseId === databaseId ? activeSyncRun : null);
|
||||
setActiveSyncRun(run);
|
||||
setSyncHistoryDatabaseId(databaseId);
|
||||
setSyncDrawerOpen(true);
|
||||
}, [activeSyncRun]);
|
||||
}, [activeRow?.id, activeSyncRun]);
|
||||
|
||||
const testSelected = useCallback(async (selected: CatalogDatabase[]) => {
|
||||
try {
|
||||
@@ -708,13 +771,22 @@ export function DatabaseManagementPage({
|
||||
}
|
||||
try {
|
||||
const result = await suggestSensitiveFields(database.id, selectedMetadataModel, selection);
|
||||
if (result.run) {
|
||||
setActiveSensitiveDataSuggestionRun(result.run);
|
||||
queryClient.setQueryData<SensitiveDataSuggestionRun[]>(
|
||||
SENSITIVE_DATA_SUGGESTION_HISTORY_QUERY_KEY,
|
||||
(current = []) => [result.run, ...current.filter((item) => item.id !== result.run.id)],
|
||||
);
|
||||
}
|
||||
setSensitiveReview({ databaseId: database.id, scopeLabel, suggestions: result.suggestions });
|
||||
toast.success(`Prepared ${result.suggestions.length} sensitive-field suggestion${result.suggestions.length === 1 ? "" : "s"} for review`);
|
||||
} catch (error) {
|
||||
toast.error(apiErrorMessage(error));
|
||||
throw error;
|
||||
} finally {
|
||||
await queryClient.invalidateQueries({ queryKey: SENSITIVE_DATA_SUGGESTION_HISTORY_QUERY_KEY });
|
||||
}
|
||||
}, [selectedMetadataModel]);
|
||||
}, [queryClient, selectedMetadataModel]);
|
||||
|
||||
const suggestDatabaseSensitiveFields = useCallback(async (selected: CatalogDatabase[]) => {
|
||||
if (selected.length !== 1) {
|
||||
@@ -751,8 +823,13 @@ export function DatabaseManagementPage({
|
||||
}),
|
||||
} : null);
|
||||
const databaseId = sensitiveReview?.databaseId;
|
||||
if (databaseId) void queryClient.invalidateQueries({ queryKey: ["catalog-columns", databaseId] });
|
||||
}, [queryClient, sensitiveReview?.databaseId]);
|
||||
if (databaseId) {
|
||||
void Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: ["catalog-columns", databaseId] }),
|
||||
invalidateCatalogMetrics(),
|
||||
]);
|
||||
}
|
||||
}, [invalidateCatalogMetrics, queryClient, sensitiveReview?.databaseId]);
|
||||
|
||||
const deleteSelectedMetadata = useCallback(async (
|
||||
selected: CatalogDatabase[],
|
||||
@@ -766,15 +843,18 @@ export function DatabaseManagementPage({
|
||||
queryClient.invalidateQueries({ queryKey: ["catalog-columns", databaseId] }),
|
||||
queryClient.invalidateQueries({ queryKey: ["catalog-relationships", databaseId] }),
|
||||
]));
|
||||
await queryClient.invalidateQueries({ queryKey: DATABASE_QUERY_KEY });
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: DATABASE_QUERY_KEY }),
|
||||
invalidateCatalogMetrics(),
|
||||
]);
|
||||
toast.success(target === "tables"
|
||||
? `Deleted ${counts.tables} catalog tables, ${counts.columns} columns, and ${counts.relationships} relationships`
|
||||
: `Deleted ${counts.relationships} catalog relationships`);
|
||||
? `Cleared ${counts.tables} catalog tables, ${counts.columns} columns, and ${counts.relationships} relationships. The source database was not changed.`
|
||||
: `Cleared ${counts.relationships} catalog relationships. The source database was not changed.`);
|
||||
} catch (error) {
|
||||
toast.error(apiErrorMessage(error));
|
||||
throw error;
|
||||
}
|
||||
}, [queryClient]);
|
||||
}, [invalidateCatalogMetrics, queryClient]);
|
||||
|
||||
const syncDatabase = useCallback(async (scope: CatalogSyncScope) => {
|
||||
if (!activeRow?.id || !activeRow.configured) return;
|
||||
@@ -793,13 +873,16 @@ export function DatabaseManagementPage({
|
||||
queryClient.invalidateQueries({ queryKey: ["catalog-sync-runs", databaseId] }),
|
||||
]);
|
||||
}
|
||||
await queryClient.invalidateQueries({ queryKey: DATABASE_QUERY_KEY });
|
||||
}, [activeSyncRun, queryClient]);
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: DATABASE_QUERY_KEY }),
|
||||
invalidateCatalogMetrics(),
|
||||
]);
|
||||
}, [activeSyncRun, invalidateCatalogMetrics, queryClient]);
|
||||
|
||||
const formVisible = screen.kind !== "list" && screen.kind !== "tables" && screen.kind !== "relationships" && activeRow && draft;
|
||||
const tablesVisible = screen.kind === "tables" && activeRow?.configured && activeRow.id;
|
||||
const relationshipsVisible = screen.kind === "relationships" && activeRow?.configured && activeRow.id;
|
||||
const currentActiveRun = activeSyncRun && ["queued", "running", "awaiting_confirmation", "applying"].includes(activeSyncRun.state)
|
||||
const currentActiveRun = activeSyncRun && activeSyncRun.databaseId === activeRow?.id && ["queued", "running", "awaiting_confirmation", "applying"].includes(activeSyncRun.state)
|
||||
? activeSyncRun
|
||||
: activeRow?.activeSyncRun && ["queued", "running", "awaiting_confirmation", "applying"].includes(activeRow.activeSyncRun.state)
|
||||
? activeRow.activeSyncRun
|
||||
@@ -811,13 +894,292 @@ export function DatabaseManagementPage({
|
||||
const descriptionGenerationActive = isDescriptionGenerationActive(
|
||||
observedActiveDescriptionGenerationRun ?? activeDescriptionGenerationRun,
|
||||
);
|
||||
const descriptionGenerationHistoryAvailable = Boolean(
|
||||
descriptionGenerationRuns.length > 0 || activeDescriptionGenerationRun,
|
||||
const fleetMetadataModelControl = (
|
||||
<MetadataGenerationModelSelector
|
||||
canManage={canManage}
|
||||
data={metadataModelsQuery.data}
|
||||
isError={metadataModelsQuery.isError}
|
||||
isLoading={metadataModelsQuery.isLoading}
|
||||
selectedModel={selectedMetadataModel}
|
||||
onSelectedModelChange={setSelectedMetadataModel}
|
||||
/>
|
||||
);
|
||||
|
||||
const metrics = catalogMetricsQuery.data;
|
||||
const metricsUnavailable = catalogMetricsQuery.isError;
|
||||
const metricsValue = (value?: number) => (
|
||||
catalogMetricsQuery.isLoading ? "…" : metricsUnavailable || value === undefined ? "—" : value.toLocaleString("en-US")
|
||||
);
|
||||
const latestCatalogUpdate = metrics?.updatedAt
|
||||
? new Date(metrics.updatedAt).toLocaleString()
|
||||
: "No synchronized metadata";
|
||||
const operationLabel = currentActiveRun
|
||||
? `Synchronization: ${currentActiveRun.phase.replaceAll("_", " ")}`
|
||||
: descriptionGenerationActive
|
||||
? "Description generation active"
|
||||
: observedActiveSensitiveDataSuggestionRun
|
||||
? "Sensitive analysis active"
|
||||
: "Idle";
|
||||
const operationTone = currentActiveRun || descriptionGenerationActive || observedActiveSensitiveDataSuggestionRun
|
||||
? "warning" as const
|
||||
: "neutral" as const;
|
||||
const fleetBreadcrumb = screen.kind === "tables" || screen.kind === "relationships"
|
||||
? [
|
||||
{ id: "databases", label: "Databases", onSelect: showList },
|
||||
{ id: "database", label: activeRow?.workspaceName ?? "Database" },
|
||||
{ id: screen.kind, label: screen.kind === "tables" ? "Tables" : "Relationships", current: true },
|
||||
]
|
||||
: [{ id: "databases", label: "Databases", current: true }];
|
||||
const catalogDrawers = (
|
||||
<>
|
||||
<CatalogSyncDrawer
|
||||
databaseId={syncHistoryDatabaseId}
|
||||
runId={activeSyncRun?.databaseId === syncHistoryDatabaseId ? activeSyncRun.id : null}
|
||||
open={syncDrawerOpen && Boolean(syncHistoryDatabaseId)}
|
||||
onClose={() => setSyncDrawerOpen(false)}
|
||||
onRunChange={rememberSyncRun}
|
||||
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)}
|
||||
/>
|
||||
<SensitiveDataSuggestionHistoryDrawer
|
||||
open={sensitiveDataSuggestionHistoryDrawerOpen}
|
||||
run={activeSensitiveDataSuggestionRun}
|
||||
modelLabel={metadataModels.find(
|
||||
(model) => model.id === activeSensitiveDataSuggestionRun?.modelId,
|
||||
)?.label ?? activeSensitiveDataSuggestionRun?.modelId ?? ""}
|
||||
onClose={() => setSensitiveDataSuggestionHistoryDrawerOpen(false)}
|
||||
onRunUpdate={setActiveSensitiveDataSuggestionRun}
|
||||
/>
|
||||
<SensitiveDataReviewDrawer
|
||||
open={Boolean(sensitiveReview)}
|
||||
databaseId={sensitiveReview?.databaseId ?? null}
|
||||
scopeLabel={sensitiveReview?.scopeLabel ?? ""}
|
||||
suggestions={sensitiveReview?.suggestions ?? []}
|
||||
canManage={canManage}
|
||||
onClose={() => setSensitiveReview(null)}
|
||||
onSaved={sensitiveColumnsSaved}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
if (presentation === "fleet") {
|
||||
const fleetFormMode = formVisible ? screen.kind as DatabaseFormMode : null;
|
||||
const fleetDrawer = formVisible && fleetFormMode ? (
|
||||
<FleetLedgerDrawer
|
||||
open
|
||||
eyebrow="Catalog configuration"
|
||||
title={databaseFormTitle(
|
||||
fleetFormMode,
|
||||
fleetFormMode === "add" && Boolean(screen.kind === "add" && screen.workspaceLocked),
|
||||
"drawer",
|
||||
)}
|
||||
description={`${activeRow.workspaceName} · ${activeRow.workspaceId}`}
|
||||
onClose={backToList}
|
||||
closeLabel="Close database panel"
|
||||
busy={busy}
|
||||
width="wide"
|
||||
>
|
||||
<DatabaseForm
|
||||
presentation="drawer"
|
||||
mode={fleetFormMode}
|
||||
row={activeRow}
|
||||
availableWorkspaces={availableWorkspaces}
|
||||
workspaceLocked={fleetFormMode === "add" && Boolean(screen.kind === "add" && screen.workspaceLocked)}
|
||||
draft={draft}
|
||||
dirty={dirty}
|
||||
canManage={canManage}
|
||||
canManageSecrets={canManageSecrets}
|
||||
busyAction={busyAction}
|
||||
stale={stale}
|
||||
staleBannerOpen={staleBannerOpen}
|
||||
partialSecretFailure={partialSecretFailure}
|
||||
headingRef={formHeadingRef}
|
||||
onBack={backToList}
|
||||
onWorkspaceChange={changeWorkspace}
|
||||
onFieldChange={changeField}
|
||||
onTransportChange={changeTransport}
|
||||
onBindingChange={changeBinding}
|
||||
onSecretChange={changeSecret}
|
||||
onSave={() => void save()}
|
||||
onTest={() => void testConnection()}
|
||||
onDelete={() => void remove()}
|
||||
onReloadLatest={() => void reloadLatest()}
|
||||
onKeepEditing={() => setStaleBannerOpen(false)}
|
||||
onRetrySecrets={() => void retrySecrets()}
|
||||
onOpenTables={(origin) => openTables(activeRow, origin)}
|
||||
onOpenRelationships={() => openRelationships(activeRow)}
|
||||
onSync={(scope) => void syncDatabase(scope)}
|
||||
onOpenSync={() => openSync(activeRow)}
|
||||
activeSyncRun={currentActiveRun}
|
||||
/>
|
||||
</FleetLedgerDrawer>
|
||||
) : null;
|
||||
|
||||
const fleetContent = screen.kind === "tables" && tablesVisible ? (
|
||||
<DatabaseTables
|
||||
presentation="fleet"
|
||||
database={activeRow}
|
||||
canManage={canManage}
|
||||
activeRun={currentActiveRun}
|
||||
selectedMetadataModel={selectedMetadataModelAvailable ? selectedMetadataModel : null}
|
||||
descriptionGenerationActive={descriptionGenerationActive}
|
||||
onBackToDatabases={showList}
|
||||
onOpenOverview={() => openOverview(activeRow)}
|
||||
onOpenRelationships={() => openRelationships(activeRow)}
|
||||
onNavigationStateChange={setTablesNavigationState}
|
||||
onNestedNavigationChange={setTableNestedNavigationActive}
|
||||
onRunStarted={rememberSyncRun}
|
||||
onOpenSync={() => openSync(activeRow)}
|
||||
onDescriptionGenerationRunStarted={rememberDescriptionGenerationRun}
|
||||
onSuggestSensitive={suggestActiveDatabaseSensitiveFields}
|
||||
onCatalogMetricsChanged={invalidateCatalogMetrics}
|
||||
metadataModelControl={fleetMetadataModelControl}
|
||||
/>
|
||||
) : screen.kind === "relationships" && relationshipsVisible ? (
|
||||
<DatabaseRelationships
|
||||
presentation="fleet"
|
||||
database={{ ...activeRow, activeSyncRun: currentActiveRun }}
|
||||
canManage={canManage}
|
||||
onBackToDatabases={showList}
|
||||
onOpenOverview={() => openOverview(activeRow)}
|
||||
onOpenTables={() => openTables(activeRow)}
|
||||
onRunStarted={rememberSyncRun}
|
||||
onOpenSync={() => openSync(activeRow)}
|
||||
/>
|
||||
) : isError && rows.length === 0 ? (
|
||||
<div className="grid h-full place-items-center p-6">
|
||||
<div className="max-w-lg rounded-lg border border-destructive/30 bg-destructive/8 p-5 text-sm" role="alert">
|
||||
<p className="font-semibold text-destructive">The database catalog is unavailable.</p>
|
||||
<p className="mt-1 leading-5 text-muted-foreground">Verify the catalog service and database migrations, then try again.</p>
|
||||
<Button type="button" variant="outline" className="mt-4" disabled={isFetching} onClick={() => void refreshList()}>
|
||||
<RefreshCw /> Try again
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<DatabaseGrid
|
||||
presentation="fleet"
|
||||
rows={rows}
|
||||
isLoading={isLoading}
|
||||
isFetching={isFetching}
|
||||
search={search}
|
||||
canManage={canManage}
|
||||
searchInputRef={searchInputRef}
|
||||
onSearchChange={setSearch}
|
||||
onView={viewRow}
|
||||
onOpenTables={(row, origin) => openTables(row, origin)}
|
||||
onEdit={editRow}
|
||||
onDelete={deleteRow}
|
||||
onOpenSync={openSync}
|
||||
onTestSelected={testSelected}
|
||||
onSyncSelected={syncSelected}
|
||||
selectedMetadataModel={selectedMetadataModelAvailable ? selectedMetadataModel : null}
|
||||
descriptionGenerationActive={descriptionGenerationActive}
|
||||
onGenerateDescriptions={generateDatabaseDescriptions}
|
||||
onSuggestSensitive={suggestDatabaseSensitiveFields}
|
||||
onDeleteMetadataSelected={deleteSelectedMetadata}
|
||||
metadataModelControl={fleetMetadataModelControl}
|
||||
onRefresh={refreshList}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<main aria-label="Database management" className="flex min-h-0 flex-1 overflow-hidden bg-background">
|
||||
<FleetLedgerShell
|
||||
header={(
|
||||
<FleetLedgerHeader
|
||||
title="Database management"
|
||||
status={(
|
||||
<FleetLedgerRealStatus
|
||||
title="Catalog status"
|
||||
items={[
|
||||
{
|
||||
id: "catalog",
|
||||
label: "Catalog",
|
||||
value: isLoading ? "Loading" : isError ? "Unavailable" : "Available",
|
||||
detail: isError ? "Retry from the current grid" : `${rows.length} workspace${rows.length === 1 ? "" : "s"}`,
|
||||
tone: isError ? "danger" : isLoading ? "warning" : "success",
|
||||
},
|
||||
{ id: "operation", label: "Operations", value: operationLabel, tone: operationTone },
|
||||
{ id: "updated", label: "Metadata updated", value: latestCatalogUpdate, tone: "info" },
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
actions={(
|
||||
<>
|
||||
<Button type="button" variant="outline" size="sm" className="thot-fleet-nav-action thot-fleet-nav-action--below" data-tooltip="Description history" data-fleet-action="secondary" onClick={openDescriptionGenerationHistory}>
|
||||
<History aria-hidden="true" /> Description history
|
||||
{observedActiveDescriptionGenerationRun ? <span className="size-1.5 rounded-full bg-warning" aria-hidden="true" /> : null}
|
||||
</Button>
|
||||
<Button type="button" variant="outline" size="sm" className="thot-fleet-nav-action thot-fleet-nav-action--below" data-tooltip="Sensitive history" data-fleet-action="secondary" onClick={openSensitiveDataSuggestionHistory}>
|
||||
<History aria-hidden="true" /> Sensitive history
|
||||
{observedActiveSensitiveDataSuggestionRun ? <span className="size-1.5 rounded-full bg-warning" aria-hidden="true" /> : null}
|
||||
</Button>
|
||||
{(screen.kind === "list" || formVisible) ? (
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
className="thot-fleet-nav-action thot-fleet-nav-action--below"
|
||||
data-tooltip="Add database"
|
||||
data-fleet-action="primary"
|
||||
disabled={!canManage || availableWorkspaces.length === 0}
|
||||
title={availableWorkspaces.length === 0 ? "Every available workspace is already configured" : undefined}
|
||||
onClick={(event) => addDatabase(event.currentTarget)}
|
||||
>
|
||||
<Plus aria-hidden="true" /> Add database
|
||||
</Button>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
kpis={(
|
||||
<FleetLedgerKpiStrip
|
||||
title={metricsDatabaseId ? "Database summary" : "Fleet summary"}
|
||||
items={[
|
||||
{ id: "tables", label: "Tables", value: metricsValue(metrics?.tables) },
|
||||
{ id: "columns", label: "Columns", value: metricsValue(metrics?.columns) },
|
||||
{ id: "sensitive", label: "Sensitive", value: metricsValue(metrics?.sensitiveColumns) },
|
||||
{ id: "relationships", label: "Relationships", value: metricsValue(metrics?.relationships) },
|
||||
{ id: "coverage", label: "Description coverage", value: metrics ? `${metrics.descriptionCoverage}%` : metricsValue() },
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
navigation={(
|
||||
<FleetLedgerBreadcrumb
|
||||
items={fleetBreadcrumb}
|
||||
back={screen.kind === "relationships" || (screen.kind === "tables" && !tableNestedNavigationActive)
|
||||
? { label: "Back to databases", onBack: showList, disabled: navigationBusy }
|
||||
: undefined}
|
||||
/>
|
||||
)}
|
||||
children={<FleetLedgerContent busy={isLoading || isFetching}>{fleetContent}</FleetLedgerContent>}
|
||||
drawer={(
|
||||
<>
|
||||
{fleetDrawer}
|
||||
{catalogDrawers}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main aria-label="Database management" className="flex min-h-0 flex-1 flex-col overflow-hidden bg-background">
|
||||
<header className="flex flex-wrap items-center justify-between gap-4 border-b border-border bg-card px-4 py-4 sm:px-5">
|
||||
<header className="grid gap-4 border-b border-border bg-card px-4 py-4 sm:px-5">
|
||||
<div>
|
||||
<p className="thot-label mb-1">Administration</p>
|
||||
<h1 className="font-heading text-xl font-semibold tracking-tight">Database management</h1>
|
||||
@@ -825,43 +1187,76 @@ export function DatabaseManagementPage({
|
||||
One database configuration for each repository workspace.
|
||||
</p>
|
||||
</div>
|
||||
<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}
|
||||
<div
|
||||
role="group"
|
||||
aria-label="Database management controls"
|
||||
className="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between"
|
||||
>
|
||||
<div
|
||||
role="group"
|
||||
aria-label="Metadata description controls"
|
||||
className="flex flex-wrap items-end gap-3"
|
||||
>
|
||||
<History />
|
||||
{observedActiveDescriptionGenerationRun ? "Active run" : "Run history"}
|
||||
</Button>
|
||||
<MetadataGenerationModelSelector
|
||||
canManage={canManage}
|
||||
data={metadataModelsQuery.data}
|
||||
isError={metadataModelsQuery.isError}
|
||||
isLoading={metadataModelsQuery.isLoading}
|
||||
selectedModel={selectedMetadataModel}
|
||||
onSelectedModelChange={setSelectedMetadataModel}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="whitespace-nowrap disabled:opacity-70"
|
||||
aria-label="Run descriptions generation history"
|
||||
disabled={!canManage}
|
||||
title={observedActiveDescriptionGenerationRun
|
||||
? "Description generation is active"
|
||||
: "View description generation history"}
|
||||
onClick={openDescriptionGenerationHistory}
|
||||
>
|
||||
<History />
|
||||
Run descriptions generation history
|
||||
{observedActiveDescriptionGenerationRun ? (
|
||||
<span aria-hidden="true" className="size-1.5 rounded-full bg-primary" />
|
||||
) : null}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="whitespace-nowrap disabled:opacity-70"
|
||||
aria-label="Run sensitive suggestions history"
|
||||
disabled={!canManage}
|
||||
title={observedActiveSensitiveDataSuggestionRun
|
||||
? "Sensitive suggestion generation is active"
|
||||
: "View sensitive suggestion history"}
|
||||
onClick={openSensitiveDataSuggestionHistory}
|
||||
>
|
||||
<History />
|
||||
Run sensitive suggestions history
|
||||
{observedActiveSensitiveDataSuggestionRun ? (
|
||||
<span aria-hidden="true" className="size-1.5 rounded-full bg-primary" />
|
||||
) : null}
|
||||
</Button>
|
||||
</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
|
||||
role="group"
|
||||
aria-label="Database management actions"
|
||||
className="flex flex-wrap items-center gap-2 sm:justify-end"
|
||||
>
|
||||
<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>
|
||||
@@ -959,6 +1354,7 @@ export function DatabaseManagementPage({
|
||||
onOpenSync={() => openSync(activeRow)}
|
||||
onDescriptionGenerationRunStarted={rememberDescriptionGenerationRun}
|
||||
onSuggestSensitive={suggestActiveDatabaseSensitiveFields}
|
||||
onCatalogMetricsChanged={invalidateCatalogMetrics}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -975,9 +1371,9 @@ export function DatabaseManagementPage({
|
||||
) : null}
|
||||
</div>
|
||||
<CatalogSyncDrawer
|
||||
databaseId={activeSyncRun?.databaseId ?? activeRow?.id ?? null}
|
||||
runId={activeSyncRun?.id ?? null}
|
||||
open={syncDrawerOpen && Boolean(activeSyncRun)}
|
||||
databaseId={syncHistoryDatabaseId}
|
||||
runId={activeSyncRun?.databaseId === syncHistoryDatabaseId ? activeSyncRun.id : null}
|
||||
open={syncDrawerOpen && Boolean(syncHistoryDatabaseId)}
|
||||
onClose={() => setSyncDrawerOpen(false)}
|
||||
onRunChange={rememberSyncRun}
|
||||
onRunUpdate={updateTrackedSyncRun}
|
||||
@@ -993,6 +1389,15 @@ export function DatabaseManagementPage({
|
||||
onRunUpdate={setActiveDescriptionGenerationRun}
|
||||
onTerminal={(run) => void descriptionGenerationTerminated(run)}
|
||||
/>
|
||||
<SensitiveDataSuggestionHistoryDrawer
|
||||
open={sensitiveDataSuggestionHistoryDrawerOpen}
|
||||
run={activeSensitiveDataSuggestionRun}
|
||||
modelLabel={metadataModels.find(
|
||||
(model) => model.id === activeSensitiveDataSuggestionRun?.modelId,
|
||||
)?.label ?? activeSensitiveDataSuggestionRun?.modelId ?? ""}
|
||||
onClose={() => setSensitiveDataSuggestionHistoryDrawerOpen(false)}
|
||||
onRunUpdate={setActiveSensitiveDataSuggestionRun}
|
||||
/>
|
||||
<SensitiveDataReviewDrawer
|
||||
open={Boolean(sensitiveReview)}
|
||||
databaseId={sensitiveReview?.databaseId ?? null}
|
||||
|
||||
Reference in New Issue
Block a user