From 848e88d58ebfea4b7d919c8bedbd4c7f23cce337 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Wed, 11 Nov 2026 22:11:37 +0100 Subject: [PATCH] fix(admin): hold the recovery readiness list's height while it loads `/admin/backups` opens with the recovery readiness card, which showed a one-line spinner until its seven signals arrived as one block, so the backups and off-host cards below jumped down by the whole list after they had drawn. The loading state now draws one placeholder row per signal in the list's own row shape, with the loading text kept for screen readers. --- .../recovery-readiness-card.test.tsx | 22 ++++++++++- .../admin/recovery-readiness-card.tsx | 38 +++++++++++++++---- 2 files changed, 52 insertions(+), 8 deletions(-) diff --git a/src/components/admin/__tests__/recovery-readiness-card.test.tsx b/src/components/admin/__tests__/recovery-readiness-card.test.tsx index 3d130ed3b..ac09f58fa 100644 --- a/src/components/admin/__tests__/recovery-readiness-card.test.tsx +++ b/src/components/admin/__tests__/recovery-readiness-card.test.tsx @@ -24,7 +24,10 @@ import type { ReadinessSignal, RecoveryReadiness, } from "@/components/admin/use-recovery-readiness"; -import { READINESS_FIX_PATHS } from "@/lib/recovery/readiness-model"; +import { + READINESS_FIX_PATHS, + READINESS_SIGNALS, +} from "@/lib/recovery/readiness-model"; import { primeLocales } from "@/__tests__/helpers/prime-locales"; primeLocales("de"); @@ -97,6 +100,23 @@ function row(html: string, id: string): string { } describe("RecoveryReadinessCard", () => { + it("holds one row per signal while it loads, so the cards below stay put", () => { + // A read that has not answered: no data, no error. + const client = new QueryClient(); + const html = renderToStaticMarkup( + + + + + , + ); + const start = html.indexOf('data-testid="recovery-readiness-loading"'); + expect(start).toBeGreaterThan(-1); + expect(html.slice(start).match(/
  • { pathname.current = "/admin"; const html = render(READINESS); diff --git a/src/components/admin/recovery-readiness-card.tsx b/src/components/admin/recovery-readiness-card.tsx index d77ae64d1..34189912b 100644 --- a/src/components/admin/recovery-readiness-card.tsx +++ b/src/components/admin/recovery-readiness-card.tsx @@ -24,7 +24,6 @@ import { CheckCircle2, Clock, LifeBuoy, - Loader2, MinusCircle, XCircle, } from "lucide-react"; @@ -34,6 +33,7 @@ import { SettingsCard } from "@/components/settings/settings-card"; import { Badge } from "@/components/ui/badge"; import { InlineAction } from "@/components/ui/inline-action"; import { QueryErrorRow } from "@/components/ui/query-error-row"; +import { Skeleton } from "@/components/ui/skeleton"; import { useFormatters, useTranslations } from "@/lib/i18n/context"; import { cn } from "@/lib/utils"; @@ -44,6 +44,17 @@ import { type RecoveryReadiness, } from "./use-recovery-readiness"; +/** One placeholder row per readiness signal the card always lists. */ +const READINESS_SIGNAL_SLOTS = [ + "database_dump", + "weekly_copy", + "offhost_copies", + "restore_drill", + "offhost_setup", + "dump_retention", + "recovery_kit", +] as const satisfies readonly ReadinessSignal["id"][]; + const STATE_ICON: Record< ReadinessState, React.ComponentType<{ className?: string; "aria-hidden"?: boolean }> @@ -194,14 +205,27 @@ export function RecoveryReadinessCard() { onRetry={() => void refetch()} /> ) : ( -
    -