fix: refresh catalog after hidden sync completion

This commit is contained in:
Codex
2026-09-02 23:23:00 +02:00
parent a50475d687
commit 7b87e95427
3 changed files with 185 additions and 15 deletions
@@ -0,0 +1,171 @@
import { render, screen, waitFor, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { http, HttpResponse } from "msw";
import { server } from "../test/msw";
import type {
CatalogDatabase,
CatalogSyncRun,
CatalogTable,
} from "../api/catalog-databases";
import { Toaster } from "../components/ui/sonner";
import { DatabaseManagementPage } from "./DatabaseManagementPage";
const databaseId = "11111111-1111-4111-8111-111111111111";
const database: CatalogDatabase = {
id: databaseId,
workspaceId: "psd-clinical",
workspaceName: "Policlinico San Donato",
workspaceAvailable: true,
workspaceRevision: { commit: "a".repeat(40), blob: "b".repeat(40) },
workspaceEvidence: { sourceType: "filesystem", state: "materialized_current_revision" },
runtimeBinding: {
transport: "postgres_direct",
configurationState: "ready",
sessionTransportSupported: true,
},
configured: true,
engine: "postgres",
databaseName: "warehouse",
schema: "datawarehouse",
version: 3,
createdAt: "2026-08-27T08:00:00Z",
updatedAt: "2026-08-27T09:00:00Z",
binding: {
transport: "postgres_direct",
host: "db.internal",
port: 5432,
username: "reader",
},
connectionStatus: "reachable",
testedVersion: 3,
secrets: {
password: false,
apiKey: false,
sshPrivateKey: false,
sshPrivateKeyPassphrase: false,
sshKnownHosts: false,
tlsCa: false,
},
};
const patientsTable: CatalogTable = {
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
databaseId,
name: "patients",
sourceComment: "Patients imported from the clinical source",
description: null,
generatedDescription: null,
version: 1,
createdAt: "2026-08-27T08:00:00Z",
updatedAt: "2026-08-27T09:00:00Z",
};
const queuedRun: CatalogSyncRun = {
id: "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
databaseId,
scope: "tables",
tableIds: [],
state: "queued",
phase: "queued",
requestedDatabaseVersion: 3,
plannedDiff: null,
confirmationToken: null,
counts: {},
errorCode: null,
errorMessage: null,
cancelRequested: false,
createdAt: "2026-08-27T10:00:00Z",
startedAt: null,
updatedAt: "2026-08-27T10:00:00Z",
finishedAt: null,
heartbeatAt: null,
};
const completedRun: CatalogSyncRun = {
...queuedRun,
state: "succeeded",
phase: "completed",
counts: { tables: 1 },
startedAt: "2026-08-27T10:00:00Z",
updatedAt: "2026-08-27T10:00:01Z",
finishedAt: "2026-08-27T10:00:01Z",
};
test("refreshes database metadata when a synchronization completes with its drawer closed", async () => {
const user = userEvent.setup();
let synchronized = false;
let tableReads = 0;
server.use(
http.get("/api/catalog/databases", () => HttpResponse.json([database])),
http.get("/api/catalog/databases/:databaseId/tables", () => {
tableReads += 1;
return HttpResponse.json(synchronized ? [patientsTable] : []);
}),
http.post("/api/catalog/databases/:databaseId/sync-runs", () => (
HttpResponse.json(queuedRun, { status: 202 })
)),
http.get("/api/catalog/sync-runs/:runId", () => (
HttpResponse.json(synchronized ? completedRun : queuedRun)
)),
http.get("/api/catalog/sync-runs/:runId/events-list", () => HttpResponse.json([])),
http.get("/api/catalog/databases/:databaseId/sync-runs", () => (
HttpResponse.json([synchronized ? completedRun : queuedRun])
)),
);
const client = new QueryClient({
defaultOptions: { queries: { retry: false, staleTime: 30_000 } },
});
render(
<QueryClientProvider client={client}>
<DatabaseManagementPage
canManage
canManageSecrets
presentation="fleet"
/>
<Toaster duration={Infinity} />
</QueryClientProvider>,
);
const initialRow = await screen.findByRole("row", { name: /Policlinico San Donato/ });
await user.click(within(initialRow).getByRole("button", {
name: "View tables for Policlinico San Donato",
}));
expect(await screen.findByRole("button", { name: "Back to databases" })).toBeVisible();
await waitFor(() => expect(tableReads).toBe(1));
await user.click(screen.getByRole("button", { name: "Back to databases" }));
client.setQueryData(["catalog-columns", databaseId, patientsTable.id], []);
client.setQueryData(["catalog-columns", "another-database", patientsTable.id], []);
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" }),
"sync-tables",
);
await user.click(screen.getByRole("button", { name: "Run action" }));
const synchronizationDrawer = await screen.findByRole("dialog", {
name: "Schema synchronization",
});
await user.click(within(synchronizationDrawer).getByRole("button", {
name: "Close synchronization drawer",
}));
synchronized = true;
const restoredRow = await screen.findByRole("row", { name: /Policlinico San Donato/ });
await user.click(within(restoredRow).getByRole("button", {
name: "View tables for Policlinico San Donato",
}));
expect(await screen.findByRole("row", { name: /patients/ }, { timeout: 3_000 })).toBeVisible();
expect(tableReads).toBeGreaterThanOrEqual(2);
expect(client.getQueryState(["catalog-columns", databaseId, patientsTable.id])?.isInvalidated)
.toBe(true);
expect(client.getQueryState(["catalog-columns", "another-database", patientsTable.id])?.isInvalidated)
.toBe(false);
});
+11 -12
View File
@@ -899,20 +899,19 @@ export function DatabaseManagementPage({
} catch (error) { toast.error(apiErrorMessage(error)); }
}, [activeRow, rememberSyncRun]);
const catalogChanged = useCallback(async () => {
const databaseId = activeSyncRun?.databaseId;
if (databaseId) {
await Promise.all([
queryClient.invalidateQueries({ queryKey: ["catalog-tables", databaseId] }),
queryClient.invalidateQueries({ queryKey: ["catalog-relationships", databaseId] }),
queryClient.invalidateQueries({ queryKey: ["catalog-sync-runs", databaseId] }),
]);
}
const catalogChanged = useCallback(async (run: CatalogSyncRun) => {
const databaseId = run.databaseId;
await Promise.all([
queryClient.invalidateQueries({ queryKey: ["catalog-tables", databaseId] }),
queryClient.invalidateQueries({ queryKey: ["catalog-columns", databaseId] }),
queryClient.invalidateQueries({ queryKey: ["catalog-relationships", databaseId] }),
queryClient.invalidateQueries({ queryKey: ["catalog-sync-runs", databaseId] }),
]);
await Promise.all([
queryClient.invalidateQueries({ queryKey: DATABASE_QUERY_KEY }),
invalidateCatalogMetrics(),
]);
}, [activeSyncRun, invalidateCatalogMetrics, queryClient]);
}, [invalidateCatalogMetrics, queryClient]);
const formVisible = screen.kind !== "list" && screen.kind !== "tables" && screen.kind !== "relationships" && activeRow && draft;
const tablesVisible = screen.kind === "tables" && activeRow?.configured && activeRow.id;
@@ -975,7 +974,7 @@ export function DatabaseManagementPage({
onClose={() => setSyncDrawerOpen(false)}
onRunChange={rememberSyncRun}
onRunUpdate={updateTrackedSyncRun}
onCatalogChanged={() => void catalogChanged()}
onCatalogChanged={(run) => void catalogChanged(run)}
/>
<DescriptionGenerationDrawer
open={descriptionGenerationDrawerOpen}
@@ -1419,7 +1418,7 @@ export function DatabaseManagementPage({
onClose={() => setSyncDrawerOpen(false)}
onRunChange={rememberSyncRun}
onRunUpdate={updateTrackedSyncRun}
onCatalogChanged={() => void catalogChanged()}
onCatalogChanged={(run) => void catalogChanged(run)}
/>
<DescriptionGenerationDrawer
open={descriptionGenerationDrawerOpen}
@@ -25,7 +25,7 @@ interface Props {
onClose: () => void;
onRunChange: (run: CatalogSyncRun) => void;
onRunUpdate: (run: CatalogSyncRun) => void;
onCatalogChanged: () => void;
onCatalogChanged: (run: CatalogSyncRun) => void;
}
const phases: CatalogSyncPhase[] = [
@@ -102,7 +102,7 @@ export function CatalogSyncDrawer({
const runQuery = useQuery({
queryKey: ["catalog-sync-run", runId],
queryFn: () => getCatalogSyncRun(runId!),
enabled: Boolean(runId && open),
enabled: Boolean(runId),
retry: false,
refetchInterval: (query) => terminal(query.state.data) ? false : 1_000,
});
@@ -185,7 +185,7 @@ export function CatalogSyncDrawer({
useEffect(() => {
if (!run || run.state !== "succeeded" || notifiedRun.current === run.id) return;
notifiedRun.current = run.id;
onCatalogChanged();
onCatalogChanged(run);
}, [onCatalogChanged, run]);
const currentPhase = run ? phases.indexOf(run.phase) : -1;