mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 06:32:03 +00:00
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.
This commit is contained in:
1 parent
eec52534d5
commit
848e88d58e
2 files changed
+52
-8
No files matched your search
@@ -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(
|
||||
<QueryClientProvider client={client}>
|
||||
<I18nProvider initialLocale="en">
|
||||
<RecoveryReadinessCard />
|
||||
</I18nProvider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
const start = html.indexOf('data-testid="recovery-readiness-loading"');
|
||||
expect(start).toBeGreaterThan(-1);
|
||||
expect(html.slice(start).match(/<li/g)?.length).toBe(
|
||||
READINESS_SIGNALS.length,
|
||||
);
|
||||
});
|
||||
|
||||
it("renders every signal with its state and sentence", () => {
|
||||
pathname.current = "/admin";
|
||||
const html = render(READINESS);
|
||||
|
||||
@@ -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()}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader2
|
||||
className="text-muted-foreground size-4 animate-spin motion-reduce:animate-none"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="text-muted-foreground text-sm">
|
||||
// Holds the list's height while it loads. The card opens the backups
|
||||
// page, and a one-line spinner here pushed every card below it down
|
||||
// by the whole list once the signals landed.
|
||||
<div data-testid="recovery-readiness-loading">
|
||||
<span className="sr-only">
|
||||
{t("admin.recoveryReadiness.loading")}
|
||||
</span>
|
||||
<ul className="divide-border divide-y" aria-hidden="true">
|
||||
{READINESS_SIGNAL_SLOTS.map((slot) => (
|
||||
<li
|
||||
key={slot}
|
||||
className="flex items-start gap-3 py-3 first:pt-0 last:pb-0"
|
||||
>
|
||||
<Skeleton className="mt-0.5 size-4 shrink-0 rounded-full" />
|
||||
<div className="min-w-0 flex-1 space-y-1">
|
||||
<Skeleton className="h-5 w-40 max-w-full" />
|
||||
<Skeleton className="h-5 w-full" />
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</SettingsCard>
|
||||
|
||||
Reference in new issue
Block a user