diff --git a/frontend/src/shell/DatabaseManagementPage.sync-cache.test.tsx b/frontend/src/shell/DatabaseManagementPage.sync-cache.test.tsx new file mode 100644 index 00000000..fb8c73a3 --- /dev/null +++ b/frontend/src/shell/DatabaseManagementPage.sync-cache.test.tsx @@ -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( + + + + , + ); + + 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); +}); diff --git a/frontend/src/shell/DatabaseManagementPage.tsx b/frontend/src/shell/DatabaseManagementPage.tsx index 5d4a1c3d..faa834e0 100644 --- a/frontend/src/shell/DatabaseManagementPage.tsx +++ b/frontend/src/shell/DatabaseManagementPage.tsx @@ -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)} /> setSyncDrawerOpen(false)} onRunChange={rememberSyncRun} onRunUpdate={updateTrackedSyncRun} - onCatalogChanged={() => void catalogChanged()} + onCatalogChanged={(run) => void catalogChanged(run)} /> 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;