feat: refine metadata catalog workflows
This commit is contained in:
@@ -10,9 +10,11 @@ import { ApiError, apiErrorMessage } from "../../api/client";
|
||||
import {
|
||||
consolidateCatalogDescriptions,
|
||||
listCatalogColumns,
|
||||
startCatalogSync,
|
||||
startDescriptionGenerationRun,
|
||||
updateCatalogColumnMetadata,
|
||||
type CatalogColumn,
|
||||
type CatalogSyncRun,
|
||||
type CatalogTable,
|
||||
type DescriptionGenerationRun,
|
||||
type SensitiveDataSuggestionRequest,
|
||||
@@ -25,10 +27,12 @@ import { NO_METADATA_GENERATION_LLM_MODEL_MESSAGE } from "./MetadataGenerationMo
|
||||
interface Props {
|
||||
databaseId: string;
|
||||
table: CatalogTable;
|
||||
databaseVersion: number;
|
||||
canManage: boolean;
|
||||
selectedMetadataModel: string | null;
|
||||
descriptionGenerationActive: boolean;
|
||||
onDescriptionGenerationRunStarted: (run: DescriptionGenerationRun) => void;
|
||||
onCatalogSyncRunStarted?: (run: CatalogSyncRun) => void;
|
||||
onNavigationStateChange: (state: DatabaseNavigationState) => void;
|
||||
onSuggestSensitive: (selection: SensitiveDataSuggestionRequest, scopeLabel: string) => Promise<void>;
|
||||
catalogOperationActive?: boolean;
|
||||
@@ -88,10 +92,12 @@ function SensitiveCell({ data, context }: ICellRendererParams<CatalogColumn, unk
|
||||
export function DatabaseColumns({
|
||||
databaseId,
|
||||
table,
|
||||
databaseVersion,
|
||||
canManage,
|
||||
selectedMetadataModel,
|
||||
descriptionGenerationActive,
|
||||
onDescriptionGenerationRunStarted,
|
||||
onCatalogSyncRunStarted,
|
||||
onNavigationStateChange,
|
||||
onSuggestSensitive,
|
||||
catalogOperationActive = false,
|
||||
@@ -285,15 +291,33 @@ export function DatabaseColumns({
|
||||
};
|
||||
|
||||
type FleetColumnAction =
|
||||
| "sync-columns"
|
||||
| "generate-descriptions"
|
||||
| "consolidate-descriptions"
|
||||
| "suggest-sensitive"
|
||||
| "save-sensitive";
|
||||
|
||||
const fleetActions: readonly FleetActionOption<FleetColumnAction>[] = [
|
||||
{
|
||||
id: "sync-columns",
|
||||
label: "Synchronize columns for this table",
|
||||
group: "Synchronization",
|
||||
scopeLabel: "Selected table",
|
||||
runLabel: "Synchronize",
|
||||
disabled: !canManage || selectedIds.length === 0 || catalogOperationActive || busy,
|
||||
disabledReason: !canManage
|
||||
? "You do not have permission to synchronize the catalog."
|
||||
: selectedIds.length === 0
|
||||
? "Select at least one column."
|
||||
: catalogOperationActive
|
||||
? "Wait for the active catalog operation to finish."
|
||||
: busy
|
||||
? "Another action is running."
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
id: "generate-descriptions",
|
||||
label: "Generate descriptions",
|
||||
label: "Generate column descriptions",
|
||||
group: "Descriptions",
|
||||
runLabel: "Generate",
|
||||
disabled: !canManage || selectedIds.length === 0 || !selectedMetadataModel || descriptionGenerationActive || catalogOperationActive || busy,
|
||||
@@ -311,7 +335,7 @@ export function DatabaseColumns({
|
||||
},
|
||||
{
|
||||
id: "consolidate-descriptions",
|
||||
label: "Move generated to Description",
|
||||
label: "Copy generated descriptions to Description",
|
||||
group: "Descriptions",
|
||||
runLabel: "Move",
|
||||
disabled: !canManage || selectedIds.length === 0 || catalogOperationActive || busy,
|
||||
@@ -345,14 +369,14 @@ export function DatabaseColumns({
|
||||
},
|
||||
{
|
||||
id: "save-sensitive",
|
||||
label: "Save sensitive-field changes",
|
||||
label: "Save sensitive field changes",
|
||||
group: "Sensitive data",
|
||||
runLabel: "Save",
|
||||
disabled: !canManage || changedSensitiveColumns.length === 0 || catalogOperationActive || busy,
|
||||
disabledReason: !canManage
|
||||
? "You do not have permission to update sensitive fields."
|
||||
: changedSensitiveColumns.length === 0
|
||||
? "No sensitive-field changes are waiting to be saved."
|
||||
? "No sensitive field changes to save."
|
||||
: catalogOperationActive
|
||||
? "Wait for the active catalog operation to finish."
|
||||
: busy
|
||||
@@ -362,7 +386,8 @@ export function DatabaseColumns({
|
||||
];
|
||||
|
||||
const runFleetAction = async (action: FleetColumnAction) => {
|
||||
if (action === "generate-descriptions") await generateDescriptions();
|
||||
if (action === "sync-columns") await synchronizeColumns();
|
||||
else if (action === "generate-descriptions") await generateDescriptions();
|
||||
else if (action === "consolidate-descriptions") await consolidateDescriptions();
|
||||
else if (action === "suggest-sensitive") await suggestSensitive();
|
||||
else await saveSensitive();
|
||||
@@ -373,6 +398,22 @@ export function DatabaseColumns({
|
||||
setSelectedIds([]);
|
||||
};
|
||||
|
||||
const synchronizeColumns = async () => {
|
||||
if (selectedIds.length === 0) return;
|
||||
setBusy(true);
|
||||
try {
|
||||
const run = await startCatalogSync(databaseId, databaseVersion, "columns", [table.id]);
|
||||
onCatalogSyncRunStarted?.(run);
|
||||
gridRef.current?.api.deselectAll();
|
||||
setSelectedIds([]);
|
||||
toast.success("Column synchronization started");
|
||||
} catch (error) {
|
||||
toast.error(apiErrorMessage(error));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const columns = useMemo<ColDef<CatalogColumn>[]>(() => [
|
||||
{ field: "ordinalPosition", headerName: "#", width: 64, maxWidth: 64, filter: "agNumberColumnFilter" },
|
||||
{ field: "sensitive", headerName: "Sensitive", headerTooltip: "Mark this column as sensitive; this does not select it for actions.", minWidth: 110, width: 110, sortable: false, filter: false, resizable: false, cellRenderer: SensitiveCell },
|
||||
@@ -398,7 +439,7 @@ export function DatabaseColumns({
|
||||
<p className="font-semibold text-destructive">Catalog columns could not be loaded.</p>
|
||||
<p className="mt-1 leading-5 text-muted-foreground">{apiErrorMessage(error)}</p>
|
||||
<Button type="button" variant="outline" className="mt-4" disabled={isFetching} onClick={() => void refetch()}>
|
||||
<RefreshCw className={isFetching ? "animate-spin" : ""} aria-hidden="true" /> Retry columns
|
||||
<RefreshCw className={isFetching ? "animate-spin" : ""} aria-hidden="true" /> Retry loading columns
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -456,8 +497,9 @@ export function DatabaseColumns({
|
||||
<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} 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 || busy} onClick={() => void consolidateDescriptions()}>Move generated description to Description</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 || !selectedIds.length || catalogOperationActive || busy} onClick={() => void synchronizeColumns()}>Synchronize columns for this table</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 || !selectedMetadataModel || descriptionGenerationActive || busy} onClick={() => void generateDescriptions()}>Generate {selectedIds.length === 1 ? "column description" : "column 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 || busy} onClick={() => void consolidateDescriptions()}>Copy generated descriptions to Description</Menu.Item>
|
||||
</Menu.Popup>
|
||||
</Menu.Positioner>
|
||||
</Menu.Portal>
|
||||
|
||||
@@ -110,6 +110,7 @@ test("Fleet columns distinguish a failed query from an empty catalog and retry i
|
||||
presentation="fleet"
|
||||
databaseId={database.id!}
|
||||
table={table}
|
||||
databaseVersion={database.version}
|
||||
canManage
|
||||
selectedMetadataModel={null}
|
||||
descriptionGenerationActive={false}
|
||||
@@ -124,7 +125,7 @@ test("Fleet columns distinguish a failed query from an empty catalog and retry i
|
||||
expect(alert).toHaveTextContent("The database catalog is unavailable.");
|
||||
expect(screen.queryByText(/No columns synchronized/i)).not.toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Retry columns" }));
|
||||
await user.click(screen.getByRole("button", { name: "Retry loading columns" }));
|
||||
await waitFor(() => expect(requests).toBe(2));
|
||||
await waitFor(() => expect(screen.queryByText("Catalog columns could not be loaded.")).not.toBeInTheDocument());
|
||||
});
|
||||
|
||||
@@ -281,7 +281,7 @@ export function DatabaseForm({
|
||||
disabled={readOnly || busy}
|
||||
onChange={(event) => onTransportChange(event.target.value as DatabaseTransport)}
|
||||
>
|
||||
<option value="postgres_direct">Direct PostgreSQL</option>
|
||||
<option value="postgres_direct">Direct PostgreSQL connection</option>
|
||||
<option value="rest_api">REST API</option>
|
||||
<option value="ssh_tunnel">SSH tunnel</option>
|
||||
</select>
|
||||
|
||||
@@ -149,13 +149,13 @@ function accessSummary(row: CatalogDatabase): { label: string; detail: string }
|
||||
return { label: "SSH", detail: endpoint(row.binding.sshHost, row.binding.sshPort) };
|
||||
}
|
||||
return {
|
||||
label: "Direct PostgreSQL",
|
||||
label: "Direct PostgreSQL connection",
|
||||
detail: endpoint(row.binding.host, row.binding.port),
|
||||
};
|
||||
}
|
||||
|
||||
function coverageStatus(total: number, complete: number): { label: string; detail: string; tone: StateTone } {
|
||||
if (total === 0) return { label: "Not started", detail: "No catalog items", tone: "danger" };
|
||||
if (total === 0) return { label: "Not started", detail: "No catalog objects", tone: "danger" };
|
||||
if (complete >= total) return { label: "Complete", detail: `${complete}/${total}`, tone: "success" };
|
||||
if (complete > 0) return { label: "Partial", detail: `${complete}/${total}`, tone: "warning" };
|
||||
return { label: "Not started", detail: `0/${total}`, tone: "danger" };
|
||||
@@ -172,7 +172,7 @@ function CatalogStatusCells({ row, metrics, sensitiveRun }: { row: CatalogDataba
|
||||
const descriptions = coverageStatus(metrics?.descriptionTargets ?? 0, metrics?.describedTargets ?? 0);
|
||||
const sensitiveProcessed = sensitiveRun ? sensitiveRun.suggestedSensitive + sensitiveRun.suggestedNonSensitive : 0;
|
||||
const sensitive = !sensitiveRun
|
||||
? { label: "Not started", detail: "No analysis run", tone: "danger" as const }
|
||||
? { label: "Not started", detail: "No analysis has been run", tone: "danger" as const }
|
||||
: sensitiveRun.status === "completed" && sensitiveProcessed >= sensitiveRun.total
|
||||
? { label: "Complete", detail: `${sensitiveProcessed}/${sensitiveRun.total}`, tone: "success" as const }
|
||||
: ["failed", "interrupted"].includes(sensitiveRun.status)
|
||||
@@ -608,7 +608,7 @@ export function DatabaseGrid({
|
||||
() => onGenerateDescriptions(selectedRows, pendingGenerationScope),
|
||||
)}
|
||||
>
|
||||
{pendingGenerationScope === "all" ? "Generate All" : "Generate Missing"}
|
||||
{pendingGenerationScope === "all" ? "Generate all descriptions" : "Generate missing descriptions"}
|
||||
</Button>
|
||||
</div>
|
||||
) : pendingDelete ? (
|
||||
@@ -688,14 +688,14 @@ export function DatabaseGrid({
|
||||
disabled={!canGenerateDescriptions}
|
||||
onClick={() => setPendingGenerationScope("all")}
|
||||
>
|
||||
Generate All
|
||||
Generate all 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={!canGenerateDescriptions}
|
||||
onClick={() => setPendingGenerationScope("missing")}
|
||||
>
|
||||
Generate Missing
|
||||
Generate missing descriptions
|
||||
</Menu.Item>
|
||||
<Menu.Separator className="my-1 h-px bg-border" />
|
||||
<Menu.Item
|
||||
|
||||
@@ -389,7 +389,7 @@ export function DatabaseRelationships({
|
||||
</optgroup>
|
||||
<optgroup label="Synchronization">
|
||||
<option value="relationships">Synchronize physical relationships</option>
|
||||
<option value="all">Synchronize the full schema for this database</option>
|
||||
<option value="all">Synchronize the full database schema</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
<Button type="button" size="sm" disabled={!selectedAction || Boolean(selectedActionUnavailable) || busy} aria-describedby={selectedActionUnavailable ? "relationship-action-reason" : undefined} onClick={() => void runSelectedAction()}><Play aria-hidden="true" />{busy ? selectedAction === "rebuild-generated" ? "Rebuilding…" : "Running…" : "Run action"}</Button>
|
||||
@@ -414,7 +414,7 @@ export function DatabaseRelationships({
|
||||
{addUnavailable ? <span id="relationship-add-reason" className="sr-only">{addUnavailable}</span> : null}
|
||||
{selectedActionUnavailable ? <span id="relationship-action-reason" className="basis-full text-xs text-muted-foreground" role="status">{selectedActionUnavailable}</span> : null}
|
||||
</div>
|
||||
) : <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>}
|
||||
) : <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 />Synchronize relationships</Button>}
|
||||
</div>
|
||||
{!bindingReady ? <div className="border-b border-border bg-muted/35 px-4 py-3 text-sm text-muted-foreground">{presentation === "fleet" ? "Test the current database binding in database details before synchronizing relationships." : "Test the current database binding from Overview before synchronizing relationships."}</div> : null}
|
||||
{fleetQueryError ?? <div className="relative min-h-[280px] flex-1">
|
||||
@@ -430,7 +430,7 @@ export function DatabaseRelationships({
|
||||
title="Add relationship"
|
||||
description="Connect one source column to one primary-key target column."
|
||||
onClose={closeAdd}
|
||||
closeLabel="Close add relationship"
|
||||
closeLabel="Close relationship form"
|
||||
busy={busy}
|
||||
footer={(
|
||||
<>
|
||||
|
||||
@@ -330,13 +330,12 @@ export function DatabaseTables({
|
||||
| "sync-columns"
|
||||
| "consolidate-descriptions"
|
||||
| "suggest-sensitive"
|
||||
| "clear-columns"
|
||||
| "clear-relationships";
|
||||
| "clear-columns";
|
||||
|
||||
const fleetActions: readonly FleetActionOption<FleetTableAction>[] = [
|
||||
{
|
||||
id: "generate-descriptions",
|
||||
label: "Generate descriptions",
|
||||
label: "Generate table description",
|
||||
group: "Descriptions",
|
||||
runLabel: "Generate",
|
||||
disabled: !canManage || selectedIds.length === 0 || !selectedMetadataModel || descriptionGenerationActive || busy !== null || Boolean(currentRun),
|
||||
@@ -372,7 +371,7 @@ export function DatabaseTables({
|
||||
},
|
||||
{
|
||||
id: "consolidate-descriptions",
|
||||
label: "Move generated to Description",
|
||||
label: "Copy generated descriptions to Description",
|
||||
group: "Descriptions",
|
||||
runLabel: "Move",
|
||||
disabled: !canManage || selectedIds.length === 0 || busy !== null || Boolean(currentRun),
|
||||
@@ -386,22 +385,24 @@ export function DatabaseTables({
|
||||
},
|
||||
{
|
||||
id: "sync-tables",
|
||||
label: "Synchronize tables",
|
||||
label: "Synchronize database tables",
|
||||
group: "Synchronization",
|
||||
scopeLabel: "All tables in this database",
|
||||
scopeLabel: "Selected database",
|
||||
runLabel: "Synchronize",
|
||||
disabled: !canManage || !bindingReady || busy !== null || Boolean(currentRun),
|
||||
disabled: !canManage || selectedIds.length === 0 || !bindingReady || busy !== null || Boolean(currentRun),
|
||||
disabledReason: !canManage
|
||||
? "You do not have permission to synchronize the catalog."
|
||||
: !bindingReady
|
||||
? "Test the current database binding first."
|
||||
: busy !== null || Boolean(currentRun)
|
||||
? "Wait for the active catalog operation to finish."
|
||||
: undefined,
|
||||
: selectedIds.length === 0
|
||||
? "Select at least one table."
|
||||
: !bindingReady
|
||||
? "Test the current database binding first."
|
||||
: busy !== null || Boolean(currentRun)
|
||||
? "Wait for the active catalog operation to finish."
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
id: "sync-columns",
|
||||
label: "Synchronize columns",
|
||||
label: "Synchronize columns for selected tables",
|
||||
group: "Synchronization",
|
||||
runLabel: "Synchronize",
|
||||
disabled: !canManage || selectedIds.length === 0 || !bindingReady || busy !== null || Boolean(currentRun),
|
||||
@@ -435,22 +436,7 @@ export function DatabaseTables({
|
||||
},
|
||||
{
|
||||
id: "clear-columns",
|
||||
label: "Clear catalog columns",
|
||||
group: "Catalog cleanup",
|
||||
tone: "destructive",
|
||||
runLabel: "Review cleanup",
|
||||
disabled: !canManage || selectedIds.length === 0 || busy !== null || Boolean(currentRun),
|
||||
disabledReason: !canManage
|
||||
? "You do not have permission to clear catalog metadata."
|
||||
: selectedIds.length === 0
|
||||
? "Select at least one table."
|
||||
: busy !== null || Boolean(currentRun)
|
||||
? "Wait for the active catalog operation to finish."
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
id: "clear-relationships",
|
||||
label: "Clear catalog relationships",
|
||||
label: "Remove synchronized columns",
|
||||
group: "Catalog cleanup",
|
||||
tone: "destructive",
|
||||
runLabel: "Review cleanup",
|
||||
@@ -473,7 +459,6 @@ export function DatabaseTables({
|
||||
else if (action === "sync-columns") await synchronize("columns", selectedIds);
|
||||
else if (action === "suggest-sensitive") await suggestSensitive();
|
||||
else if (action === "clear-columns") setPendingDelete("columns");
|
||||
else setPendingDelete("relationships");
|
||||
};
|
||||
|
||||
const clearSelection = () => {
|
||||
@@ -544,10 +529,12 @@ export function DatabaseTables({
|
||||
<DatabaseColumns
|
||||
databaseId={databaseId}
|
||||
table={activeTable}
|
||||
databaseVersion={database.version}
|
||||
canManage={canManage}
|
||||
selectedMetadataModel={selectedMetadataModel}
|
||||
descriptionGenerationActive={descriptionGenerationActive}
|
||||
onDescriptionGenerationRunStarted={onDescriptionGenerationRunStarted}
|
||||
onCatalogSyncRunStarted={onRunStarted}
|
||||
onNavigationStateChange={setColumnNavigation}
|
||||
onSuggestSensitive={onSuggestSensitive}
|
||||
catalogOperationActive={Boolean(currentRun)}
|
||||
@@ -594,7 +581,7 @@ export function DatabaseTables({
|
||||
rowHeight={44}
|
||||
headerHeight={38}
|
||||
animateRows={false}
|
||||
overlayNoRowsTemplate="No catalog tables. Test the binding, then sync tables."
|
||||
overlayNoRowsTemplate="No catalog tables. Synchronize the schema from the database view."
|
||||
/>
|
||||
</div>
|
||||
</div>}
|
||||
@@ -655,10 +642,12 @@ export function DatabaseTables({
|
||||
<DatabaseColumns
|
||||
databaseId={databaseId}
|
||||
table={activeTable}
|
||||
databaseVersion={database.version}
|
||||
canManage={canManage}
|
||||
selectedMetadataModel={selectedMetadataModel}
|
||||
descriptionGenerationActive={descriptionGenerationActive}
|
||||
onDescriptionGenerationRunStarted={onDescriptionGenerationRunStarted}
|
||||
onCatalogSyncRunStarted={onRunStarted}
|
||||
onNavigationStateChange={setColumnNavigation}
|
||||
onSuggestSensitive={onSuggestSensitive}
|
||||
onCatalogMetricsChanged={onCatalogMetricsChanged}
|
||||
@@ -710,19 +699,11 @@ export function DatabaseTables({
|
||||
<div className="mr-auto min-w-56">
|
||||
<p id="table-cleanup-confirmation" className="text-sm font-semibold">
|
||||
{presentation === "fleet"
|
||||
? `Clear catalog ${pendingDelete} for ${selectedIds.length} table${selectedIds.length === 1 ? "" : "s"}?`
|
||||
: pendingDelete === "columns"
|
||||
? `Delete all catalog columns for ${selectedIds.length} table${selectedIds.length === 1 ? "" : "s"}?`
|
||||
: `Delete all relationships for ${selectedIds.length} table${selectedIds.length === 1 ? "" : "s"}?`}
|
||||
? `Remove synchronized columns for ${selectedIds.length} table${selectedIds.length === 1 ? "" : "s"}?`
|
||||
: `Remove synchronized columns for ${selectedIds.length} table${selectedIds.length === 1 ? "" : "s"}?`}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{presentation === "fleet"
|
||||
? pendingDelete === "columns"
|
||||
? "Only synchronized catalog columns for these tables will be cleared. The source database is not modified."
|
||||
: "Only synchronized catalog relationships for these tables will be cleared. The source database is not modified."
|
||||
: pendingDelete === "columns"
|
||||
? "Only columns belonging to the selected catalog tables will be removed."
|
||||
: "All incoming and outgoing relationships for the selected catalog tables will be removed."}
|
||||
Only synchronized catalog columns for these tables will be removed. The source database is not modified.
|
||||
</p>
|
||||
</div>
|
||||
<Button type="button" variant="ghost" disabled={busy !== null} onClick={closeDeleteConfirmation}>Cancel</Button>
|
||||
@@ -733,7 +714,7 @@ export function DatabaseTables({
|
||||
disabled={busy !== null}
|
||||
onClick={() => void deleteMetadata(pendingDelete)}
|
||||
>
|
||||
<Trash2 />{presentation === "fleet" ? "Clear catalog" : pendingDelete === "columns" ? "Delete columns" : "Delete relationships"}
|
||||
<Trash2 />Remove synchronized columns
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
@@ -760,13 +741,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 || !selectedMetadataModel || descriptionGenerationActive || busy !== null || Boolean(currentRun)} onClick={() => void generateDescriptions()}>Generate table description</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 || !selectedMetadataModel || descriptionGenerationActive || busy !== null || Boolean(currentRun)} onClick={() => void generateColumnDescriptions()}>Generate descriptions for all 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 || !bindingReady || busy !== null || Boolean(currentRun)} onClick={() => void synchronize("columns", selectedIds)}>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.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)} onClick={() => void synchronize("tables")}>Synchronize database tables</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)} onClick={() => void synchronize("columns", selectedIds)}>Synchronize columns for selected tables</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()}>Copy generated descriptions 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" disabled={!canManage || busy !== null || Boolean(currentRun)} onClick={() => setPendingDelete("columns")}>Delete all columns</Menu.Item>
|
||||
<Menu.Item className="rounded-md px-3 py-2 text-sm text-destructive outline-none data-[highlighted]:bg-destructive/10 data-[disabled]:opacity-45" disabled={!canManage || busy !== null || Boolean(currentRun)} onClick={() => setPendingDelete("relationships")}>Delete all relationships</Menu.Item>
|
||||
<Menu.Item className="rounded-md px-3 py-2 text-sm text-destructive outline-none data-[highlighted]:bg-destructive/10 data-[disabled]:opacity-45" disabled={!canManage || busy !== null || Boolean(currentRun)} onClick={() => setPendingDelete("columns")}>Remove synchronized columns</Menu.Item>
|
||||
</Menu.Popup>
|
||||
</Menu.Positioner>
|
||||
</Menu.Portal>
|
||||
@@ -799,12 +780,11 @@ export function DatabaseTables({
|
||||
<input ref={searchInputRef} 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 tables" placeholder="Search" value={search} onChange={(event) => setSearch(event.target.value)} />
|
||||
<span aria-live="polite" className="whitespace-nowrap text-xs tabular-nums text-muted-foreground">{search.trim() ? `${displayedCount} of ${data.length}` : displayedCount}</span>
|
||||
<Button type="button" variant="outline" disabled={isFetching || busy !== null} onClick={() => void refetch()}><RefreshCw className={isFetching ? "animate-spin" : ""} />Refresh</Button>
|
||||
<Button type="button" disabled={!canManage || !bindingReady || busy !== null || Boolean(currentRun)} title={!bindingReady ? "Test the current database binding before synchronizing tables" : undefined} onClick={() => void synchronize("tables")}><RefreshCw className={busy === "sync" ? "animate-spin" : ""} />Sync tables</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 tables.</div> : null}
|
||||
{!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 columns.</div> : null}
|
||||
{fleetQueryError ?? <div className="relative min-h-[280px] flex-1">
|
||||
<div className={`thot-database-grid ${presentation === "fleet" ? "thot-fleet-ledger-grid " : ""}ag-theme-alpine absolute inset-0 h-full w-full`}>
|
||||
<AgGridReact<CatalogTable>
|
||||
@@ -825,7 +805,7 @@ export function DatabaseTables({
|
||||
rowHeight={44}
|
||||
headerHeight={38}
|
||||
animateRows={false}
|
||||
overlayNoRowsTemplate="No catalog tables. Test the binding, then sync tables."
|
||||
overlayNoRowsTemplate="No catalog tables. Synchronize the schema from the database view."
|
||||
/>
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
@@ -8,7 +8,7 @@ const actions = [
|
||||
{ id: "sync", label: "Synchronize tables", group: "Synchronization", scopeLabel: "All tables in this database" },
|
||||
{
|
||||
id: "generate",
|
||||
label: "Generate descriptions",
|
||||
label: "Generate column descriptions",
|
||||
group: "Descriptions",
|
||||
disabled: true,
|
||||
disabledReason: "Choose a metadata model first",
|
||||
|
||||
@@ -27,4 +27,10 @@ test("uses operational body text for every available-command list", () => {
|
||||
expect(styles).toMatch(
|
||||
/\.thot-fleet-action-selector__select\s*\{[^}]*font-size:\s*0\.9375rem;/s,
|
||||
);
|
||||
expect(styles).toMatch(
|
||||
/\.thot-fleet-action-selector\s*\{[^}]*flex:\s*0 0 auto;/s,
|
||||
);
|
||||
expect(styles).toMatch(
|
||||
/\.thot-fleet-action-selector__select\s*\{[^}]*width:\s*max-content;/s,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -381,7 +381,8 @@
|
||||
.thot-fleet-action-selector {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
max-width: 100%;
|
||||
flex: 0 0 auto;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
@@ -402,9 +403,10 @@
|
||||
}
|
||||
|
||||
.thot-fleet-action-selector__select {
|
||||
flex: 1 1 13rem;
|
||||
width: min(100%, 22rem);
|
||||
min-width: 13rem;
|
||||
flex: 0 0 auto;
|
||||
width: max-content;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
height: 2rem;
|
||||
padding: 0 2rem 0 0.625rem;
|
||||
border: 1px solid oklch(var(--input));
|
||||
@@ -613,8 +615,8 @@
|
||||
|
||||
@media (min-width: 48rem) {
|
||||
.thot-fleet-grid-toolbar > .thot-fleet-action-selector {
|
||||
flex: 1 1 33rem;
|
||||
min-width: 33rem;
|
||||
flex: 0 0 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -963,9 +965,6 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.thot-fleet-action-selector__select {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 44rem) and (min-width: 48rem) {
|
||||
|
||||
@@ -12,8 +12,13 @@ describe("history action placement", () => {
|
||||
expect(fleetHeader).not.toContain("Sensitive history");
|
||||
});
|
||||
|
||||
test("database views expose metadata histories beside database commands", () => {
|
||||
for (const file of ["DatabaseForm.tsx", "DatabaseTables.tsx", "DatabaseRelationships.tsx"]) {
|
||||
test("database tables do not own metadata history actions", () => {
|
||||
for (const file of ["DatabaseTables.tsx"]) {
|
||||
const source = componentSource(file);
|
||||
expect(source, file).not.toContain("Description history");
|
||||
expect(source, file).not.toContain("Sensitive history");
|
||||
}
|
||||
for (const file of ["DatabaseForm.tsx", "DatabaseRelationships.tsx"]) {
|
||||
const source = componentSource(file);
|
||||
expect(source, file).toContain("Description history");
|
||||
expect(source, file).toContain("Sensitive history");
|
||||
|
||||
Reference in New Issue
Block a user