From 4fc932d48a8f7b4e5d288616ce5bd43fbbc7ee41 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Fri, 27 Mar 2026 18:24:07 +0000 Subject: [PATCH] refactor(recovery): bound the primary workspace surface --- .../internal/subsystems/storage-recovery.md | 11 + .../src/components/Recovery/Recovery.tsx | 26 +-- .../RecoveryProtectedInventorySection.tsx | 112 ++++++++-- .../components/Recovery/RecoverySummary.tsx | 195 +++++++++--------- .../Recovery/__tests__/Recovery.test.tsx | 24 ++- 5 files changed, 225 insertions(+), 143 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md index b299c2b2e5..8b9e2cfe17 100644 --- a/docs/release-control/v6/internal/subsystems/storage-recovery.md +++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md @@ -443,6 +443,11 @@ recovery system instead of a single backup table metaphor. That top recovery frame must rely on solid elevated operator panels and border hierarchy rather than decorative gradients so the page reads like a monitoring workspace instead of a marketing-style dashboard shell. +That same framing must stay compact enough to keep the primary workspace in the +first scroll window. `RecoverySummary.tsx` and the workspace shell in +`Recovery.tsx` should compress posture, coverage, and mode framing into concise +operator panels instead of stacking multiple dashboard-like slabs that push the +main inventory or event table too far down the page. That same summary contract now also includes platform- and item-coverage framing through `frontend-modern/src/utils/recoverySummaryPresentation.ts`, so the first visible recovery overview explicitly shows which protected item @@ -458,6 +463,12 @@ first-class column in the main inventory grid rather than leaving platform as the only structural classifier beside the item label. Platform badges remain important supporting operator context, especially for Proxmox-heavy fleets, but the table frame itself must make protected item class explicit. +That same inventory contract must keep the protected-items grid operationally +bounded. The governed desktop recovery surface should not dump the entire +protected estate into one endless slab; it should page or otherwise bound the +primary inventory table so the workspace, filters, and adjacent activity panel +remain readable as one monitoring surface instead of dissolving into a raw list +dump. The same owned vocabulary applies to recovery events as well: `frontend-modern/src/utils/recoveryTablePresentation.ts` must keep the history-table `type` column labeled as `Item Type` within recovery surfaces so diff --git a/frontend-modern/src/components/Recovery/Recovery.tsx b/frontend-modern/src/components/Recovery/Recovery.tsx index 96cd3b7582..bd24dd84a7 100644 --- a/frontend-modern/src/components/Recovery/Recovery.tsx +++ b/frontend-modern/src/components/Recovery/Recovery.tsx @@ -488,17 +488,17 @@ const Recovery: Component = () => { -
-
+
+
-
+
Recovery Workspace
-
- Run recovery operations from one shared surface: protected inventory first, - event drill-in second, with the same item-first filters and route state. +
+ Protected inventory first, event drill-in second, with the same item-first + filters and route state.
@@ -515,10 +515,14 @@ const Recovery: Component = () => {
+
+
setWorkspaceView(value as RecoveryWorkspaceView)} ariaLabel="Recovery data view" + listClass="flex flex-wrap items-center gap-3" + tabClass="min-h-10 px-2 py-2.5 text-sm" tabs={[ { value: 'inventory', @@ -541,14 +545,6 @@ const Recovery: Component = () => { ]} />
-
- - Unified protection inventory across protected item types, with platform mix carried as supporting recovery context. - -
diff --git a/frontend-modern/src/components/Recovery/RecoveryProtectedInventorySection.tsx b/frontend-modern/src/components/Recovery/RecoveryProtectedInventorySection.tsx index c22fb87bea..c41d00b0cd 100644 --- a/frontend-modern/src/components/Recovery/RecoveryProtectedInventorySection.tsx +++ b/frontend-modern/src/components/Recovery/RecoveryProtectedInventorySection.tsx @@ -1,4 +1,4 @@ -import { For, Show, createMemo, createSignal } from 'solid-js'; +import { For, Show, createEffect, createMemo, createSignal } from 'solid-js'; import type { Accessor, Component } from 'solid-js'; import { Card } from '@/components/shared/Card'; @@ -83,11 +83,13 @@ interface RecoveryProtectedInventorySectionProps { } const availableOutcomes = ['all', 'success', 'warning', 'failed', 'running'] as const; +const PROTECTED_ITEMS_PAGE_SIZE = 24; export const RecoveryProtectedInventorySection: Component< RecoveryProtectedInventorySectionProps > = (props) => { const [protectedFiltersOpen, setProtectedFiltersOpen] = createSignal(false); + const [protectedPage, setProtectedPage] = createSignal(1); const [protectedSortCol, setProtectedSortCol] = createSignal('lastBackup'); const [protectedSortDir, setProtectedSortDir] = createSignal('desc'); @@ -158,25 +160,51 @@ export const RecoveryProtectedInventorySection: Component< return items; }); + const protectedTotalPages = createMemo(() => + Math.max(1, Math.ceil(sortedRollups().length / PROTECTED_ITEMS_PAGE_SIZE)), + ); + + const visibleRollups = createMemo(() => { + const start = (protectedPage() - 1) * PROTECTED_ITEMS_PAGE_SIZE; + return sortedRollups().slice(start, start + PROTECTED_ITEMS_PAGE_SIZE); + }); + + createEffect(() => { + props.queryFilter(); + props.platformFilter(); + props.itemTypeFilter(); + props.historyOutcomeFilter(); + props.protectedStaleOnly(); + protectedSortCol(); + protectedSortDir(); + setProtectedPage(1); + }); + + createEffect(() => { + const totalPages = protectedTotalPages(); + if (protectedPage() > totalPages) { + setProtectedPage(totalPages); + } + }); + return (
-
- Protected Items -
-
- Unified protection inventory across workloads, datasets, and other protected items, with platform mix carried as supporting context. +
Protected inventory
+
+ Item-first recovery inventory, with item class and latest-point posture kept visible + in the main grid instead of buried in drill-in.
- {props.filteredRollups().length} of {props.rollups().length} items shown + {sortedRollups().length} of {props.rollups().length} items shown 0}> @@ -193,7 +221,7 @@ export const RecoveryProtectedInventorySection: Component<
-
+
props.setQueryFilter(value)} placeholder={getRecoveryProtectedSearchPlaceholder()} class="w-full" + inputClass="py-2.5 text-sm" clearOnEscape history={{ storageKey: STORAGE_KEYS.RECOVERY_SEARCH_HISTORY, @@ -214,7 +243,8 @@ export const RecoveryProtectedInventorySection: Component< count: protectedActiveFilterCount(), }} showFilters={!props.isMobile || protectedFiltersOpen()} - toolbarClass="lg:flex-nowrap" + searchRowClass="flex w-full items-center gap-3" + toolbarClass="gap-3 lg:flex-nowrap" > {(itemType) => ( @@ -247,7 +278,8 @@ export const RecoveryProtectedInventorySection: Component< normalizeSourcePlatformQueryValue(event.currentTarget.value), ) } - selectClass="min-w-[10rem] max-w-[14rem]" + groupClass="gap-2 px-1.5 py-1" + selectClass="min-w-[10rem] max-w-[14rem] py-2 text-sm" > {(platform) => ( @@ -267,7 +299,8 @@ export const RecoveryProtectedInventorySection: Component< props.setHistoryOutcomeFilter(value); if (value !== 'all') props.setVerificationFilter('all'); }} - selectClass="min-w-[9rem]" + groupClass="gap-2 px-1.5 py-1" + selectClass="min-w-[9rem] py-2 text-sm" > {(outcome) => ( @@ -282,7 +315,7 @@ export const RecoveryProtectedInventorySection: Component< type="button" aria-pressed={props.protectedStaleOnly()} onClick={() => props.setProtectedStaleOnly((value) => !value)} - class={`rounded-md border px-2.5 py-1 text-xs font-medium transition-colors ${getRecoveryProtectedToggleClass( + class={`rounded-md border px-3 py-2 text-sm font-medium transition-colors ${getRecoveryProtectedToggleClass( props.protectedStaleOnly(), )}`} > @@ -333,7 +366,7 @@ export const RecoveryProtectedInventorySection: Component< ] as const ).map(([column, label]) => (