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)); }
|
} catch (error) { toast.error(apiErrorMessage(error)); }
|
||||||
}, [activeRow, rememberSyncRun]);
|
}, [activeRow, rememberSyncRun]);
|
||||||
|
|
||||||
const catalogChanged = useCallback(async () => {
|
const catalogChanged = useCallback(async (run: CatalogSyncRun) => {
|
||||||
const databaseId = activeSyncRun?.databaseId;
|
const databaseId = run.databaseId;
|
||||||
if (databaseId) {
|
await Promise.all([
|
||||||
await Promise.all([
|
queryClient.invalidateQueries({ queryKey: ["catalog-tables", databaseId] }),
|
||||||
queryClient.invalidateQueries({ queryKey: ["catalog-tables", databaseId] }),
|
queryClient.invalidateQueries({ queryKey: ["catalog-columns", databaseId] }),
|
||||||
queryClient.invalidateQueries({ queryKey: ["catalog-relationships", databaseId] }),
|
queryClient.invalidateQueries({ queryKey: ["catalog-relationships", databaseId] }),
|
||||||
queryClient.invalidateQueries({ queryKey: ["catalog-sync-runs", databaseId] }),
|
queryClient.invalidateQueries({ queryKey: ["catalog-sync-runs", databaseId] }),
|
||||||
]);
|
]);
|
||||||
}
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
queryClient.invalidateQueries({ queryKey: DATABASE_QUERY_KEY }),
|
queryClient.invalidateQueries({ queryKey: DATABASE_QUERY_KEY }),
|
||||||
invalidateCatalogMetrics(),
|
invalidateCatalogMetrics(),
|
||||||
]);
|
]);
|
||||||
}, [activeSyncRun, invalidateCatalogMetrics, queryClient]);
|
}, [invalidateCatalogMetrics, queryClient]);
|
||||||
|
|
||||||
const formVisible = screen.kind !== "list" && screen.kind !== "tables" && screen.kind !== "relationships" && activeRow && draft;
|
const formVisible = screen.kind !== "list" && screen.kind !== "tables" && screen.kind !== "relationships" && activeRow && draft;
|
||||||
const tablesVisible = screen.kind === "tables" && activeRow?.configured && activeRow.id;
|
const tablesVisible = screen.kind === "tables" && activeRow?.configured && activeRow.id;
|
||||||
@@ -975,7 +974,7 @@ export function DatabaseManagementPage({
|
|||||||
onClose={() => setSyncDrawerOpen(false)}
|
onClose={() => setSyncDrawerOpen(false)}
|
||||||
onRunChange={rememberSyncRun}
|
onRunChange={rememberSyncRun}
|
||||||
onRunUpdate={updateTrackedSyncRun}
|
onRunUpdate={updateTrackedSyncRun}
|
||||||
onCatalogChanged={() => void catalogChanged()}
|
onCatalogChanged={(run) => void catalogChanged(run)}
|
||||||
/>
|
/>
|
||||||
<DescriptionGenerationDrawer
|
<DescriptionGenerationDrawer
|
||||||
open={descriptionGenerationDrawerOpen}
|
open={descriptionGenerationDrawerOpen}
|
||||||
@@ -1419,7 +1418,7 @@ export function DatabaseManagementPage({
|
|||||||
onClose={() => setSyncDrawerOpen(false)}
|
onClose={() => setSyncDrawerOpen(false)}
|
||||||
onRunChange={rememberSyncRun}
|
onRunChange={rememberSyncRun}
|
||||||
onRunUpdate={updateTrackedSyncRun}
|
onRunUpdate={updateTrackedSyncRun}
|
||||||
onCatalogChanged={() => void catalogChanged()}
|
onCatalogChanged={(run) => void catalogChanged(run)}
|
||||||
/>
|
/>
|
||||||
<DescriptionGenerationDrawer
|
<DescriptionGenerationDrawer
|
||||||
open={descriptionGenerationDrawerOpen}
|
open={descriptionGenerationDrawerOpen}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ interface Props {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onRunChange: (run: CatalogSyncRun) => void;
|
onRunChange: (run: CatalogSyncRun) => void;
|
||||||
onRunUpdate: (run: CatalogSyncRun) => void;
|
onRunUpdate: (run: CatalogSyncRun) => void;
|
||||||
onCatalogChanged: () => void;
|
onCatalogChanged: (run: CatalogSyncRun) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const phases: CatalogSyncPhase[] = [
|
const phases: CatalogSyncPhase[] = [
|
||||||
@@ -102,7 +102,7 @@ export function CatalogSyncDrawer({
|
|||||||
const runQuery = useQuery({
|
const runQuery = useQuery({
|
||||||
queryKey: ["catalog-sync-run", runId],
|
queryKey: ["catalog-sync-run", runId],
|
||||||
queryFn: () => getCatalogSyncRun(runId!),
|
queryFn: () => getCatalogSyncRun(runId!),
|
||||||
enabled: Boolean(runId && open),
|
enabled: Boolean(runId),
|
||||||
retry: false,
|
retry: false,
|
||||||
refetchInterval: (query) => terminal(query.state.data) ? false : 1_000,
|
refetchInterval: (query) => terminal(query.state.data) ? false : 1_000,
|
||||||
});
|
});
|
||||||
@@ -185,7 +185,7 @@ export function CatalogSyncDrawer({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!run || run.state !== "succeeded" || notifiedRun.current === run.id) return;
|
if (!run || run.state !== "succeeded" || notifiedRun.current === run.id) return;
|
||||||
notifiedRun.current = run.id;
|
notifiedRun.current = run.id;
|
||||||
onCatalogChanged();
|
onCatalogChanged(run);
|
||||||
}, [onCatalogChanged, run]);
|
}, [onCatalogChanged, run]);
|
||||||
|
|
||||||
const currentPhase = run ? phases.indexOf(run.phase) : -1;
|
const currentPhase = run ? phases.indexOf(run.phase) : -1;
|
||||||
|
|||||||
Reference in New Issue
Block a user