refactor(recovery): strengthen inventory workspace treatment

This commit is contained in:
rcourtman
2026-03-27 18:41:30 +00:00
parent 2a2d2ba93c
commit 9b4cd6471e
3 changed files with 87 additions and 20 deletions
@@ -473,6 +473,11 @@ 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.
That same protected-inventory surface should carry compact operator orientation
inside the table shell itself. `RecoveryProtectedInventorySection.tsx` should
expose the current bounded range, page, and sort state near the primary grid,
and the first column should carry enough secondary item metadata to read as a
monitored inventory row rather than a bare export line.
That same hierarchy rule also applies to the activity panel below the primary
workspace. `RecoveryActivitySection.tsx` should read as timeline analysis for
the selected history window, not as a second copy of the page-level posture
@@ -169,6 +169,31 @@ export const RecoveryProtectedInventorySection: Component<
return sortedRollups().slice(start, start + PROTECTED_ITEMS_PAGE_SIZE);
});
const pageStart = createMemo(() =>
sortedRollups().length === 0 ? 0 : (protectedPage() - 1) * PROTECTED_ITEMS_PAGE_SIZE + 1,
);
const pageEnd = createMemo(() =>
Math.min(protectedPage() * PROTECTED_ITEMS_PAGE_SIZE, sortedRollups().length),
);
const protectedSortLabel = createMemo(() => {
switch (protectedSortCol()) {
case 'item':
return 'Item';
case 'type':
return 'Item Type';
case 'platform':
return 'Platform';
case 'lastBackup':
return 'Latest Point';
case 'outcome':
return 'Outcome';
default:
return 'Latest Point';
}
});
createEffect(() => {
props.queryFilter();
props.platformFilter();
@@ -325,6 +350,27 @@ export const RecoveryProtectedInventorySection: Component<
</div>
</Show>
<Show when={sortedRollups().length > 0}>
<div class="border-b border-border-subtle/80 bg-surface-alt/20 px-4 py-3">
<div class="flex flex-col gap-2 lg:flex-row lg:items-center lg:justify-between">
<div class="flex flex-wrap items-center gap-2 text-xs">
<span class="rounded-full border border-border-subtle bg-surface px-2.5 py-1 text-base-content">
Showing {pageStart()} - {pageEnd()}
</span>
<span class="rounded-full border border-border-subtle bg-surface px-2.5 py-1 text-base-content">
Page {protectedPage()} / {protectedTotalPages()}
</span>
<span class="rounded-full border border-border-subtle bg-surface px-2.5 py-1 text-base-content">
Sorted by {protectedSortLabel()}
</span>
</div>
<div class="text-xs text-muted">
Open any item to drill directly into its recovery events.
</div>
</div>
</div>
</Show>
<Show when={props.loading() && props.filteredRollups().length === 0}>
<div class="px-6 py-6 text-sm text-muted">
{getRecoveryProtectedItemsLoadingState().text}
@@ -427,7 +473,7 @@ export const RecoveryProtectedInventorySection: Component<
onClick={() => props.onSelectRollup(rollup.rollupId)}
>
<TableCell
class={`relative max-w-[420px] truncate whitespace-nowrap px-3 py-3 text-base-content ${
class={`relative max-w-[420px] px-3 py-3 text-base-content ${
issueTone === 'rose' || issueTone === 'blue' ? 'font-medium' : ''
}`}
title={label}
@@ -435,18 +481,32 @@ export const RecoveryProtectedInventorySection: Component<
<Show when={issueTone !== 'none'}>
<span class={`absolute inset-y-0 left-0 w-0.5 ${issueRailClass}`} />
</Show>
<div class="flex items-center gap-2">
<span class="truncate">{label}</span>
<Show when={neverSucceeded}>
<span class={getRecoveryRollupStatusPillClass('never-succeeded')}>
{getRecoveryRollupStatusPillLabel('never-succeeded')}
</span>
</Show>
<Show when={!neverSucceeded && stale}>
<span class={getRecoveryRollupStatusPillClass('stale')}>
{getRecoveryRollupStatusPillLabel('stale')}
</span>
</Show>
<div class="flex min-w-0 flex-col gap-1.5 pl-0.5">
<div class="flex min-w-0 items-center gap-2">
<span class="truncate text-sm font-medium">{label}</span>
<Show when={neverSucceeded}>
<span class={getRecoveryRollupStatusPillClass('never-succeeded')}>
{getRecoveryRollupStatusPillLabel('never-succeeded')}
</span>
</Show>
<Show when={!neverSucceeded && stale}>
<span class={getRecoveryRollupStatusPillClass('stale')}>
{getRecoveryRollupStatusPillLabel('stale')}
</span>
</Show>
</div>
<div class="flex flex-wrap items-center gap-x-2 gap-y-1 text-[11px] text-muted">
<Show when={itemTypePresentation?.label}>
<span>{itemTypePresentation?.label}</span>
</Show>
<Show when={platforms.length > 0}>
<span>
{platforms
.map((platform) => getSourcePlatformLabel(platform))
.join(' • ')}
</span>
</Show>
</div>
</div>
</TableCell>
@@ -456,7 +516,7 @@ export const RecoveryProtectedInventorySection: Component<
fallback={<span class="text-muted">—</span>}
>
<span
class={`inline-flex min-w-[4.5rem] justify-center rounded px-1.5 py-px text-[10px] font-medium ${
class={`inline-flex min-w-[4.75rem] justify-center rounded-full border px-2 py-1 text-[10px] font-medium ${
itemTypePresentation?.badgeClasses || 'bg-surface-alt text-base-content'
}`}
>
@@ -525,9 +585,7 @@ export const RecoveryProtectedInventorySection: Component<
fallback={<span>Showing 0 of 0 protected items</span>}
>
<span>
Showing {(protectedPage() - 1) * PROTECTED_ITEMS_PAGE_SIZE + 1} -{' '}
{Math.min(protectedPage() * PROTECTED_ITEMS_PAGE_SIZE, sortedRollups().length)} of{' '}
{sortedRollups().length} protected items
Showing {pageStart()} - {pageEnd()} of {sortedRollups().length} protected items
</span>
</Show>
</div>
@@ -213,7 +213,7 @@ describe('Recovery', () => {
expect(within(inventoryTable).getByText('Item')).toBeInTheDocument();
expect(within(inventoryTable).getByText('Item Type')).toBeInTheDocument();
expect(within(inventoryTable).getByText('Platform')).toBeInTheDocument();
expect(within(inventoryTable).getByText('VM')).toBeInTheDocument();
expect(within(inventoryTable).getAllByText('VM').length).toBeGreaterThan(0);
expect(screen.queryByText('Backups By Date')).not.toBeInTheDocument();
fireEvent.click(await screen.findByText('VM 123'));
@@ -742,13 +742,17 @@ describe('Recovery', () => {
render(() => <Recovery />);
expect(await screen.findByText('Showing 1 - 24 of 26 protected items')).toBeInTheDocument();
expect(screen.getByText('Page 1 / 2')).toBeInTheDocument();
expect(screen.getAllByText('Page 1 / 2').length).toBeGreaterThan(0);
expect(screen.getByText('Sorted by Latest Point')).toBeInTheDocument();
expect(
screen.getByText('Open any item to drill directly into its recovery events.'),
).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: 'Next' }));
await waitFor(() => {
expect(screen.getByText('Showing 25 - 26 of 26 protected items')).toBeInTheDocument();
expect(screen.getByText('Page 2 / 2')).toBeInTheDocument();
expect(screen.getAllByText('Page 2 / 2').length).toBeGreaterThan(0);
});
});