This commit is contained in:
@@ -496,7 +496,7 @@ test.each(synchronizationScopes)(
|
||||
},
|
||||
);
|
||||
|
||||
test("starts Generate Missing for one configured database without confirmation", async () => {
|
||||
test("discloses source sampling before starting Generate Missing", async () => {
|
||||
const user = userEvent.setup();
|
||||
const queuedRun = makeDescriptionGenerationRun({ scope: "missing", total: 3 });
|
||||
let startBody: unknown;
|
||||
@@ -519,6 +519,15 @@ test("starts Generate Missing for one configured database without confirmation",
|
||||
await user.click(screen.getByRole("button", { name: "Actions" }));
|
||||
await user.click(await screen.findByRole("menuitem", { name: "Generate Missing" }));
|
||||
|
||||
expect(screen.getByText("Generate missing descriptions for Policlinico San Donato?")).toBeVisible();
|
||||
expect(screen.getByRole("note", {
|
||||
name: "Metadata generation source data disclosure",
|
||||
})).toHaveTextContent(
|
||||
"When available, up to five real source rows and five representative values from columns not marked sensitive are sent to the selected provider.",
|
||||
);
|
||||
expect(startBody).toBeUndefined();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Generate Missing" }));
|
||||
await waitFor(() => expect(startBody).toEqual({
|
||||
modelId: "local-qwen",
|
||||
scope: "missing",
|
||||
@@ -527,6 +536,44 @@ test("starts Generate Missing for one configured database without confirmation",
|
||||
expect(await screen.findByRole("dialog", { name: "Description generation" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("keeps Fleet Generate Missing behind the source-data disclosure", async () => {
|
||||
const user = userEvent.setup();
|
||||
let generationStarts = 0;
|
||||
server.use(
|
||||
http.get("/api/catalog/metadata-generation/models", () => HttpResponse.json({
|
||||
models: [{ id: "local-qwen", label: "Local Qwen" }],
|
||||
default: "local-qwen",
|
||||
})),
|
||||
http.post("/api/catalog/databases/:databaseId/description-generation-runs", () => {
|
||||
generationStarts += 1;
|
||||
return HttpResponse.json(makeDescriptionGenerationRun({ scope: "missing" }), { status: 202 });
|
||||
}),
|
||||
);
|
||||
renderPage({ rows: [makeDatabase()], presentation: "fleet" });
|
||||
|
||||
const databaseRow = await screen.findByRole("row", { name: /Policlinico San Donato/ });
|
||||
await user.click(within(databaseRow).getByRole("checkbox", { name: /toggle row selection/i }));
|
||||
await user.selectOptions(
|
||||
screen.getByRole("combobox", { name: "Batch action" }),
|
||||
"generate-missing",
|
||||
);
|
||||
await waitFor(() => expect(screen.getByRole("combobox", {
|
||||
name: "Metadata description model",
|
||||
})).toHaveValue("local-qwen"));
|
||||
await user.click(screen.getByRole("button", { name: "Run action" }));
|
||||
|
||||
expect(screen.getByRole("note", {
|
||||
name: "Metadata generation source data disclosure",
|
||||
})).toBeVisible();
|
||||
expect(generationStarts).toBe(0);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Cancel" }));
|
||||
expect(screen.queryByRole("note", {
|
||||
name: "Metadata generation source data disclosure",
|
||||
})).not.toBeInTheDocument();
|
||||
expect(generationStarts).toBe(0);
|
||||
});
|
||||
|
||||
test("confirms Generate All replacement, supports cancel, and sends the database-wide scope", async () => {
|
||||
const user = userEvent.setup();
|
||||
const queuedRun = makeDescriptionGenerationRun({ scope: "all", total: 6 });
|
||||
@@ -552,6 +599,9 @@ test("confirms Generate All replacement, supports cancel, and sends the database
|
||||
|
||||
expect(screen.getByText("Replace generated descriptions for Policlinico San Donato?")).toBeVisible();
|
||||
expect(screen.getByText(/existing generated descriptions for eligible tables and columns will be replaced/i)).toBeVisible();
|
||||
expect(screen.getByRole("note", {
|
||||
name: "Metadata generation source data disclosure",
|
||||
})).toHaveTextContent(/real source rows.*selected provider/i);
|
||||
expect(startBody).toBeUndefined();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Cancel" }));
|
||||
@@ -587,6 +637,7 @@ test("keeps the database selected and safely explains when no descriptions are e
|
||||
await user.click(within(databaseRow).getByRole("checkbox", { name: /toggle row selection/i }));
|
||||
await user.click(screen.getByRole("button", { name: "Actions" }));
|
||||
await user.click(await screen.findByRole("menuitem", { name: "Generate Missing" }));
|
||||
await user.click(screen.getByRole("button", { name: "Generate Missing" }));
|
||||
|
||||
expect(await screen.findByText(
|
||||
"No eligible catalog tables or columns need description generation.",
|
||||
@@ -648,7 +699,9 @@ test.each([
|
||||
await user.click(await screen.findByRole("menuitem", {
|
||||
name: scope === "all" ? "Generate All" : "Generate Missing",
|
||||
}));
|
||||
if (scope === "all") await user.click(screen.getByRole("button", { name: "Generate All" }));
|
||||
await user.click(screen.getByRole("button", {
|
||||
name: scope === "all" ? "Generate All" : "Generate Missing",
|
||||
}));
|
||||
|
||||
expect(await screen.findByRole("heading", {
|
||||
name: status === "completed" ? "Completed" : "Completed with errors",
|
||||
@@ -752,6 +805,7 @@ test("disables database-wide generation while a description generation is active
|
||||
await user.click(within(databaseRow).getByRole("checkbox", { name: /toggle row selection/i }));
|
||||
await user.click(screen.getByRole("button", { name: "Actions" }));
|
||||
await user.click(await screen.findByRole("menuitem", { name: "Generate Missing" }));
|
||||
await user.click(screen.getByRole("button", { name: "Generate Missing" }));
|
||||
expect(await screen.findByRole("dialog", { name: "Description generation" })).toBeVisible();
|
||||
|
||||
databaseRow = await screen.findByRole("row", { name: /Policlinico San Donato/ });
|
||||
|
||||
@@ -214,7 +214,9 @@ export function DatabaseGrid({
|
||||
const [selectedRows, setSelectedRows] = useState<CatalogDatabase[]>([]);
|
||||
const [action, setAction] = useState<"test" | "sync" | "generate" | "suggest" | "delete" | null>(null);
|
||||
const [pendingDelete, setPendingDelete] = useState<CatalogDatabaseMetadataDeleteTarget | null>(null);
|
||||
const [pendingGenerateAll, setPendingGenerateAll] = useState(false);
|
||||
const [pendingGenerationScope, setPendingGenerationScope] = useState<
|
||||
Extract<DescriptionGenerationScope, "all" | "missing"> | null
|
||||
>(null);
|
||||
const context = useMemo<DatabaseGridContext>(
|
||||
() => ({ canManage, presentation, onView, onOpenTables, onEdit, onDelete, onOpenSync }),
|
||||
[canManage, presentation, onView, onOpenTables, onEdit, onDelete, onOpenSync],
|
||||
@@ -376,10 +378,10 @@ export function DatabaseGrid({
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [pendingDelete]);
|
||||
useEffect(() => {
|
||||
if (!pendingGenerateAll) return;
|
||||
const timer = window.setTimeout(() => document.getElementById("database-generate-all-confirm-button")?.focus(), 0);
|
||||
if (!pendingGenerationScope) return;
|
||||
const timer = window.setTimeout(() => document.getElementById("database-generation-confirm-button")?.focus(), 0);
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [pendingGenerateAll]);
|
||||
}, [pendingGenerationScope]);
|
||||
const closeDeleteConfirmation = () => {
|
||||
setPendingDelete(null);
|
||||
window.setTimeout(() => {
|
||||
@@ -387,8 +389,8 @@ export function DatabaseGrid({
|
||||
else actionsTriggerRef.current?.focus();
|
||||
}, 0);
|
||||
};
|
||||
const closeGenerateAllConfirmation = () => {
|
||||
setPendingGenerateAll(false);
|
||||
const closeGenerationConfirmation = () => {
|
||||
setPendingGenerationScope(null);
|
||||
window.setTimeout(() => {
|
||||
if (presentation === "fleet") actionSelectRef.current?.focus();
|
||||
else actionsTriggerRef.current?.focus();
|
||||
@@ -407,16 +409,17 @@ export function DatabaseGrid({
|
||||
setSelectedRows([]);
|
||||
}
|
||||
setPendingDelete(null);
|
||||
setPendingGenerateAll(false);
|
||||
setPendingGenerationScope(null);
|
||||
if (kind === "delete") window.setTimeout(() => searchInputRef.current?.focus(), 0);
|
||||
} catch {
|
||||
// The page-level operation owns safe error feedback. Preserve the selection for retry.
|
||||
if (kind === "generate") setPendingGenerationScope(null);
|
||||
} finally { setAction(null); }
|
||||
};
|
||||
|
||||
const runFleetAction = async (selectedAction: DatabaseFleetAction) => {
|
||||
if (selectedAction === "generate-all") {
|
||||
setPendingGenerateAll(true);
|
||||
if (selectedAction === "generate-all" || selectedAction === "generate-missing") {
|
||||
setPendingGenerationScope(selectedAction === "generate-all" ? "all" : "missing");
|
||||
return;
|
||||
}
|
||||
if (selectedAction === "clear-tables" || selectedAction === "clear-relationships") {
|
||||
@@ -432,10 +435,6 @@ export function DatabaseGrid({
|
||||
await perform("sync", () => onSyncSelected(selectedRows, scope));
|
||||
return;
|
||||
}
|
||||
if (selectedAction === "generate-missing") {
|
||||
await perform("generate", () => onGenerateDescriptions(selectedRows, "missing"));
|
||||
return;
|
||||
}
|
||||
await perform("suggest", () => onSuggestSensitive(selectedRows), false);
|
||||
};
|
||||
|
||||
@@ -452,35 +451,47 @@ export function DatabaseGrid({
|
||||
: "flex min-h-12 flex-wrap items-center gap-3 border-b border-border px-3 py-2"}
|
||||
>
|
||||
{selectedRows.length > 0 ? (
|
||||
pendingGenerateAll ? (
|
||||
pendingGenerationScope ? (
|
||||
<div
|
||||
role="group"
|
||||
aria-labelledby="database-generate-all-confirmation"
|
||||
aria-labelledby="database-generation-confirmation"
|
||||
className="flex min-w-0 flex-1 flex-wrap items-center gap-2"
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Escape" && action === null) closeGenerateAllConfirmation();
|
||||
if (event.key === "Escape" && action === null) closeGenerationConfirmation();
|
||||
}}
|
||||
>
|
||||
<div className="mr-auto min-w-56">
|
||||
<p id="database-generate-all-confirmation" className="text-sm font-semibold">
|
||||
Replace generated descriptions for {selectedRows[0]?.workspaceName}?
|
||||
<p id="database-generation-confirmation" className="text-sm font-semibold">
|
||||
{pendingGenerationScope === "all"
|
||||
? `Replace generated descriptions for ${selectedRows[0]?.workspaceName}?`
|
||||
: `Generate missing descriptions for ${selectedRows[0]?.workspaceName}?`}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Existing generated descriptions for eligible tables and columns will be replaced.
|
||||
{pendingGenerationScope === "all" ? (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Existing generated descriptions for eligible tables and columns will be replaced.
|
||||
</p>
|
||||
) : null}
|
||||
<p
|
||||
role="note"
|
||||
aria-label="Metadata generation source data disclosure"
|
||||
className="mt-1 text-xs font-medium text-amber-800 dark:text-amber-300"
|
||||
>
|
||||
When available, up to five real source rows and five representative values from
|
||||
columns not marked sensitive are sent to the selected provider.
|
||||
</p>
|
||||
</div>
|
||||
<Button type="button" variant="ghost" disabled={action !== null} onClick={closeGenerateAllConfirmation}>Cancel</Button>
|
||||
<Button type="button" variant="ghost" disabled={action !== null} onClick={closeGenerationConfirmation}>Cancel</Button>
|
||||
<Button
|
||||
id="database-generate-all-confirm-button"
|
||||
id="database-generation-confirm-button"
|
||||
type="button"
|
||||
variant="destructive"
|
||||
variant={pendingGenerationScope === "all" ? "destructive" : "default"}
|
||||
disabled={action !== null}
|
||||
onClick={() => void perform(
|
||||
"generate",
|
||||
() => onGenerateDescriptions(selectedRows, "all"),
|
||||
() => onGenerateDescriptions(selectedRows, pendingGenerationScope),
|
||||
)}
|
||||
>
|
||||
Generate All
|
||||
{pendingGenerationScope === "all" ? "Generate All" : "Generate Missing"}
|
||||
</Button>
|
||||
</div>
|
||||
) : pendingDelete ? (
|
||||
@@ -564,17 +575,14 @@ export function DatabaseGrid({
|
||||
<Menu.Item
|
||||
className="rounded-md px-3 py-2 text-sm outline-none data-[highlighted]:bg-muted data-[disabled]:opacity-45"
|
||||
disabled={!canGenerateDescriptions}
|
||||
onClick={() => setPendingGenerateAll(true)}
|
||||
onClick={() => setPendingGenerationScope("all")}
|
||||
>
|
||||
Generate All
|
||||
</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={() => void perform(
|
||||
"generate",
|
||||
() => onGenerateDescriptions(selectedRows, "missing"),
|
||||
)}
|
||||
onClick={() => setPendingGenerationScope("missing")}
|
||||
>
|
||||
Generate Missing
|
||||
</Menu.Item>
|
||||
@@ -649,7 +657,7 @@ export function DatabaseGrid({
|
||||
selectionColumnDef={{ width: 44, maxWidth: 44, pinned: "left" }}
|
||||
onSelectionChanged={({ api }) => {
|
||||
if (pendingDelete && action === null) setPendingDelete(null);
|
||||
if (pendingGenerateAll && action === null) setPendingGenerateAll(false);
|
||||
if (pendingGenerationScope && action === null) setPendingGenerationScope(null);
|
||||
setSelectedRows(api.getSelectedRows());
|
||||
}}
|
||||
rowHeight={44}
|
||||
|
||||
Reference in New Issue
Block a user