fix: refresh catalog after hidden sync completion
This commit is contained in:
@@ -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);
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user