diff --git a/private-web/e2e/backups.spec.ts b/private-web/e2e/backups.spec.ts index f484a51b..d57a12e4 100644 --- a/private-web/e2e/backups.spec.ts +++ b/private-web/e2e/backups.spec.ts @@ -600,6 +600,7 @@ test.describe("server backup capabilities", () => { sql, }) => { const group = await seedServerGroup(sql, { name: "snap-caps-group" }); + await seedServerGroupBackupConfig(sql, { groupId: group.id, status: "ready" }); const device = await seedDevice(sql); const server = await seedServer(sql, { name: "snap-caps-srv", @@ -633,6 +634,7 @@ test.describe("server backup capabilities", () => { sql, }) => { const group = await seedServerGroup(sql, { name: "no-snap-group" }); + await seedServerGroupBackupConfig(sql, { groupId: group.id, status: "ready" }); const server = await seedServer(sql, { name: "no-snap-srv", groupId: group.id, @@ -652,6 +654,7 @@ test.describe("server backup capabilities", () => { sql, }) => { const group = await seedServerGroup(sql, { name: "inflight-group" }); + await seedServerGroupBackupConfig(sql, { groupId: group.id, status: "ready" }); const device = await seedDevice(sql); const server = await seedServer(sql, { name: "inflight-srv", @@ -683,11 +686,41 @@ test.describe("server backup capabilities", () => { await expect(backups.getByText(/kfreshsnap/)).toBeVisible(); }); + test("with no group backup config, capabilities are collapsed behind a message", async ({ + page, + sql, + }) => { + const group = await seedServerGroup(sql, { name: "unconfigured-group" }); + const server = await seedServer(sql, { + name: "unconfigured-srv", + groupId: group.id, + }); + // A declared capability, but the group has NO backup config. + await seedServerBackupCapability(sql, { + serverId: server.id, + type: "tamanu-postgres", + }); + + await page.goto(`/servers/${server.id}`); + const backups = page.locator("#backups"); + // The message explains the toggles are inert. + await expect(backups.getByText(/aren't set up for this group/i)).toBeVisible(); + // The toggle is collapsed (not visible) until expanded. + const toggle = backups.getByRole("switch", { + name: /enable tamanu-postgres backups/i, + }); + await expect(toggle).toBeHidden(); + // …but still reachable: expanding reveals it. + await backups.getByRole("button", { name: /show backup types/i }).click(); + await expect(toggle).toBeVisible(); + }); + test("toggling a capability switch flips enabled in the DB", async ({ page, sql, }) => { const group = await seedServerGroup(sql, { name: "caps-group" }); + await seedServerGroupBackupConfig(sql, { groupId: group.id, status: "ready" }); const server = await seedServer(sql, { name: "caps-srv", groupId: group.id, diff --git a/private-web/src/routes/ServerDetail.tsx b/private-web/src/routes/ServerDetail.tsx index 12f0b9ff..d5202f98 100644 --- a/private-web/src/routes/ServerDetail.tsx +++ b/private-web/src/routes/ServerDetail.tsx @@ -6,6 +6,7 @@ import { Box, Button, Chip, + Collapse, Dialog, DialogActions, DialogContent, @@ -1631,10 +1632,16 @@ function SiblingServers({ ); } +/// The all-zero group id, used to query backup config for an ungrouped server: +/// it always resolves to "no config" rather than erroring on a missing group. +const NIL_UUID = "00000000-0000-0000-0000-000000000000"; + /// Per-(server, type) backup capabilities with an admin-only enable toggle. /// Reads `backups.capabilities`; the switch calls `backups.set_capability` and /// refetches. Capabilities are advertised by bestool, so a server with none yet -/// renders an explicit empty state rather than disappearing. +/// renders an explicit empty state rather than disappearing. When the group has +/// no active backup config the toggles are greyed + collapsed behind a message, +/// since they have no effect until backups are set up. function BackupCapabilitiesSection({ serverId, groupId, @@ -1650,6 +1657,41 @@ function BackupCapabilitiesSection({ { server_id: serverId }, [serverId], ); + // Whether the group has an *active* (ready) backup config. Ungrouped servers + // query the nil group, which always returns no config. While this is loading + // we optimistically treat the section as active to avoid a grey→normal flash. + const config = useApi( + "backups", + "get", + { server_group_id: groupId ?? NIL_UUID }, + [groupId], + ); + const inactive = + config.status === "ok" && + !(config.data != null && config.data.status === "ready"); + const inactiveMessage = !groupId + ? "This server isn't in a group, so backups can't be configured for it." + : config.status === "ok" && config.data == null + ? "Backups aren't set up for this group yet, so these settings have no effect." + : "Backups for this group are still being set up, so these settings have no effect yet."; + + const [showInactive, setShowInactive] = useState(false); + + const rows = ( + }> + {caps.status === "ok" && + caps.data.map((cap) => ( + + ))} + + ); + return ( )} + + {inactive && ( + + Set up + + ) : undefined + } + > + {inactiveMessage} + + )} + {caps.status === "loading" || caps.status === "idle" ? ( ) : caps.status === "error" ? ( @@ -1679,18 +1743,32 @@ function BackupCapabilitiesSection({ No backup types registered for this server. + ) : inactive ? ( + // Collapsed + greyed: the toggles still work (they record intent for + // when backups are set up), but it's clear they're dormant right now. + <> + + + {rows} + + ) : ( - }> - {caps.data.map((cap) => ( - - ))} - + rows )} );