mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-24 04:33:17 +00:00
refactor(recovery): align summary and table hierarchy
This commit is contained in:
@@ -648,6 +648,10 @@ recovery-specific mini report. Recovery summary cards should prefer concise
|
||||
counts, bars, and badge-backed coverage cues over tall explanatory copy blocks
|
||||
so the protected-item table takes over the page at roughly the same visual
|
||||
density as the infrastructure and workloads monitoring surfaces.
|
||||
That same summary rule also applies within individual cards: recovery posture,
|
||||
freshness, attention, footprint, and history cards should favor compact rows
|
||||
and metric lists over stacked prose callouts so the summary strip reads like
|
||||
Pulse monitoring telemetry rather than a page-local narrative panel.
|
||||
That same inventory surface should also follow the established monitoring-table
|
||||
scan pattern in its first column. Protected-item rows should lead with a clear
|
||||
status cue, the primary item name, and compact badge-backed item/platform
|
||||
@@ -658,6 +662,11 @@ not just local row styling. When the protected-item page mixes healthy and
|
||||
problematic coverage, the table body should section attention items ahead of
|
||||
healthy coverage so operators can scan the recovery estate the way they scan
|
||||
other grouped monitoring tables in Pulse.
|
||||
That same table-shell contract must avoid duplicate framing once the view tabs
|
||||
already establish the active workspace. `RecoveryProtectedInventorySection.tsx`
|
||||
should keep page/count/sort orientation inside a slim table-shell status row
|
||||
and let the filter strip lead directly into the grid instead of reintroducing a
|
||||
second large inventory header card above the same table.
|
||||
That shared unified-resource dependency now also includes policy-governed
|
||||
resource metadata on the frontend decode path: storage and recovery surfaces
|
||||
that route through `frontend-modern/src/hooks/useUnifiedResources.ts` must
|
||||
|
||||
@@ -485,13 +485,13 @@ const Recovery: Component = () => {
|
||||
/>
|
||||
|
||||
<div ref={historySectionRef} class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="flex flex-col gap-2 rounded-md border border-border bg-surface px-3 py-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<Subtabs
|
||||
value={workspaceView()}
|
||||
onChange={(value) => setWorkspaceView(value as RecoveryWorkspaceView)}
|
||||
ariaLabel="Recovery data view"
|
||||
listClass="flex flex-wrap items-center gap-3"
|
||||
tabClass="min-h-9 px-2 py-2 text-sm"
|
||||
listClass="flex flex-wrap items-center gap-2"
|
||||
tabClass="min-h-8 px-2.5 py-1.5 text-sm"
|
||||
tabs={[
|
||||
{
|
||||
value: 'inventory',
|
||||
|
||||
@@ -239,15 +239,34 @@ export const RecoveryProtectedInventorySection: Component<
|
||||
<Card
|
||||
padding="none"
|
||||
tone="card"
|
||||
class="overflow-hidden border-border-subtle bg-surface shadow-[0_10px_24px_rgba(2,6,23,0.1)]"
|
||||
class="overflow-hidden border-border-subtle bg-surface"
|
||||
>
|
||||
<div class="border-b border-border-subtle/80 bg-surface px-4 py-3">
|
||||
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="text-sm font-semibold text-base-content">Protected inventory</div>
|
||||
<div class="flex flex-wrap items-center gap-2 text-xs">
|
||||
<span class="rounded-full border border-border-subtle bg-surface-hover/70 px-2.5 py-1 text-base-content">
|
||||
<div class="border-b border-border-subtle/80 bg-surface px-4 py-2.5">
|
||||
<div class="flex flex-col gap-2 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div class="flex flex-wrap items-center gap-3 text-[11px] text-muted">
|
||||
<span class="font-medium text-base-content">
|
||||
{sortedRollups().length} of {props.rollups().length} items shown
|
||||
</span>
|
||||
<span>
|
||||
Page {protectedPage()} / {protectedTotalPages()}
|
||||
</span>
|
||||
<span>
|
||||
Sort:{' '}
|
||||
<span class="font-medium text-base-content">
|
||||
{protectedSortCol() === 'lastBackup'
|
||||
? 'Latest point'
|
||||
: protectedSortCol() === 'item'
|
||||
? 'Item'
|
||||
: protectedSortCol() === 'type'
|
||||
? 'Item type'
|
||||
: protectedSortCol() === 'platform'
|
||||
? 'Platform'
|
||||
: 'Outcome'}
|
||||
</span>{' '}
|
||||
<span class="uppercase tracking-wide">{protectedSortDir()}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2 text-xs">
|
||||
<Show when={props.rollupsSummary().stale > 0}>
|
||||
<span class={`${getRecoveryRollupStatusPillClass('stale')} px-2.5 py-1`}>
|
||||
{props.rollupsSummary().stale} stale
|
||||
@@ -263,7 +282,7 @@ export const RecoveryProtectedInventorySection: Component<
|
||||
</div>
|
||||
|
||||
<Show when={!props.kioskMode}>
|
||||
<div class="border-b border-border-subtle/80 bg-surface-alt/35 px-4 py-4">
|
||||
<div class="border-b border-border-subtle/80 bg-surface px-4 py-3">
|
||||
<PageControls
|
||||
search={
|
||||
<SearchInput
|
||||
@@ -271,7 +290,7 @@ export const RecoveryProtectedInventorySection: Component<
|
||||
onChange={(value) => props.setQueryFilter(value)}
|
||||
placeholder={getRecoveryProtectedSearchPlaceholder()}
|
||||
class="w-full"
|
||||
inputClass="py-2.5 text-sm"
|
||||
inputClass="py-2 text-sm"
|
||||
clearOnEscape
|
||||
history={{
|
||||
storageKey: STORAGE_KEYS.RECOVERY_SEARCH_HISTORY,
|
||||
@@ -284,9 +303,9 @@ export const RecoveryProtectedInventorySection: Component<
|
||||
onToggle: () => setProtectedFiltersOpen((open) => !open),
|
||||
count: protectedActiveFilterCount(),
|
||||
}}
|
||||
showFilters={!props.isMobile || protectedFiltersOpen()}
|
||||
searchRowClass="flex w-full items-center gap-3"
|
||||
toolbarClass="gap-3 lg:flex-nowrap"
|
||||
showFilters={!props.isMobile || protectedFiltersOpen()}
|
||||
searchRowClass="flex w-full items-center gap-3"
|
||||
toolbarClass="gap-3 lg:flex-nowrap"
|
||||
>
|
||||
<LabeledFilterSelect
|
||||
id="recovery-item-type-filter"
|
||||
@@ -408,7 +427,7 @@ export const RecoveryProtectedInventorySection: Component<
|
||||
] as const
|
||||
).map(([column, label]) => (
|
||||
<TableHead
|
||||
class={`sticky top-0 z-[1] bg-surface-alt/95 px-3 py-3 whitespace-nowrap text-left text-[11px] sm:text-xs font-semibold uppercase tracking-[0.18em] cursor-pointer select-none hover:text-base-content transition-colors${
|
||||
class={`sticky top-0 z-[1] bg-surface-alt/95 px-3 py-2.5 whitespace-nowrap text-left text-[11px] sm:text-xs font-semibold uppercase tracking-[0.14em] cursor-pointer select-none hover:text-base-content transition-colors${
|
||||
column === 'type'
|
||||
? ' hidden md:table-cell w-[96px]'
|
||||
: column === 'platform'
|
||||
@@ -441,7 +460,7 @@ export const RecoveryProtectedInventorySection: Component<
|
||||
<For each={visibleGroupedRollups()}>
|
||||
{(group) => (
|
||||
<>
|
||||
<TableRow class="bg-surface-alt">
|
||||
<TableRow class="bg-surface-alt/75">
|
||||
<TableCell
|
||||
colspan={5}
|
||||
class="py-1 pr-3 pl-3 text-[12px] sm:text-sm font-semibold text-base-content"
|
||||
@@ -494,7 +513,7 @@ export const RecoveryProtectedInventorySection: Component<
|
||||
onClick={() => props.onSelectRollup(rollup.rollupId)}
|
||||
>
|
||||
<TableCell
|
||||
class="max-w-[420px] px-3 py-2.5 text-base-content"
|
||||
class="max-w-[420px] px-3 py-2 text-base-content"
|
||||
title={label}
|
||||
>
|
||||
<div class="flex min-w-0 flex-col gap-1.5">
|
||||
@@ -540,7 +559,7 @@ export const RecoveryProtectedInventorySection: Component<
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
<TableCell class="hidden md:table-cell whitespace-nowrap px-3 py-2.5">
|
||||
<TableCell class="hidden md:table-cell whitespace-nowrap px-3 py-2">
|
||||
<Show
|
||||
when={itemTypePresentation}
|
||||
fallback={<span class="text-muted">—</span>}
|
||||
@@ -551,7 +570,7 @@ export const RecoveryProtectedInventorySection: Component<
|
||||
</Show>
|
||||
</TableCell>
|
||||
|
||||
<TableCell class="hidden lg:table-cell whitespace-nowrap px-3 py-2.5">
|
||||
<TableCell class="hidden lg:table-cell whitespace-nowrap px-3 py-2">
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
<For each={platforms}>
|
||||
{(platform) => {
|
||||
@@ -567,7 +586,7 @@ export const RecoveryProtectedInventorySection: Component<
|
||||
</TableCell>
|
||||
|
||||
<TableCell
|
||||
class={`whitespace-nowrap px-3 py-2.5 ${getRecoveryRollupAgeTextClass(
|
||||
class={`whitespace-nowrap px-3 py-2 ${getRecoveryRollupAgeTextClass(
|
||||
rollup,
|
||||
nowMs,
|
||||
)}`}
|
||||
@@ -588,7 +607,7 @@ export const RecoveryProtectedInventorySection: Component<
|
||||
)}
|
||||
</TableCell>
|
||||
|
||||
<TableCell class="whitespace-nowrap px-3 py-2.5">
|
||||
<TableCell class="whitespace-nowrap px-3 py-2">
|
||||
<span
|
||||
class={`inline-flex rounded px-1.5 py-0.5 text-[10px] font-medium ${getRecoveryOutcomeBadgeClass(
|
||||
outcome,
|
||||
|
||||
@@ -58,6 +58,8 @@ describe('RecoverySummary', () => {
|
||||
expect(screen.getByText('Freshness')).toBeInTheDocument();
|
||||
expect(screen.getByText('Recent History')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('Attention Queue').length).toBeGreaterThan(0);
|
||||
expect(screen.getByText('Stale')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('Attention').length).toBeGreaterThan(0);
|
||||
expect(screen.getByText('Recovery Points')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('Item Types').length).toBeGreaterThan(0);
|
||||
expect(screen.getByText('Primary Item')).toBeInTheDocument();
|
||||
|
||||
@@ -4,7 +4,6 @@ import { SummaryPanel } from '@/components/shared/SummaryPanel';
|
||||
import { SummaryMetricCard } from '@/components/shared/SummaryMetricCard';
|
||||
import type { ProtectionRollup, RecoveryOutcome, RecoveryPointsSeriesBucket } from '@/types/recovery';
|
||||
import {
|
||||
buildRecoveryAttentionItems,
|
||||
buildRecoveryActivitySummary,
|
||||
buildRecoveryFreshnessBuckets,
|
||||
buildRecoveryItemCoverage,
|
||||
@@ -42,8 +41,6 @@ export const RecoverySummary: Component<RecoverySummaryProps> = (props) => {
|
||||
const itemCoverage = createMemo(() => buildRecoveryItemCoverage(props.rollups()));
|
||||
const platformCoverage = createMemo(() => buildRecoveryPlatformCoverage(props.rollups()));
|
||||
const activity = createMemo(() => buildRecoveryActivitySummary(props.series()));
|
||||
const attentionItems = createMemo(() => buildRecoveryAttentionItems(summary()));
|
||||
|
||||
const healthyCount = createMemo(() => postureSummary().healthy);
|
||||
const attentionCount = createMemo(() => postureSummary().attention);
|
||||
const recentWindowLabel = createMemo(() => {
|
||||
@@ -83,115 +80,147 @@ export const RecoverySummary: Component<RecoverySummaryProps> = (props) => {
|
||||
timeRangeLabels={RECOVERY_SUMMARY_TIME_RANGE_LABELS}
|
||||
testId="recovery-summary"
|
||||
>
|
||||
<SummaryMetricCard label="Recovery Posture" loaded={true} hasData={hasRollups()}>
|
||||
<div class="flex h-full flex-col gap-3">
|
||||
<div class="grid grid-cols-3 gap-3 border-b border-border-subtle pb-3 text-sm">
|
||||
<div>
|
||||
<div class="text-[11px] uppercase tracking-wide text-muted">Healthy</div>
|
||||
<div class="mt-1 text-2xl font-semibold text-emerald-600 dark:text-emerald-400">
|
||||
{healthyCount()}
|
||||
<div class="lg:col-span-2">
|
||||
<SummaryMetricCard label="Recovery Posture" loaded={true} hasData={hasRollups()}>
|
||||
<div class="grid h-full gap-3 lg:grid-cols-[minmax(0,1.4fr)_minmax(0,1fr)]">
|
||||
<div class="flex h-full flex-col gap-2.5">
|
||||
<div class="grid grid-cols-3 gap-2 border-b border-border-subtle pb-2.5 text-sm">
|
||||
<div>
|
||||
<div class="text-[11px] uppercase tracking-wide text-muted">Healthy</div>
|
||||
<div class="mt-1 text-xl font-semibold text-emerald-600 dark:text-emerald-400">
|
||||
{healthyCount()}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-[11px] uppercase tracking-wide text-muted">Attention</div>
|
||||
<div class="mt-1 text-xl font-semibold text-amber-600 dark:text-amber-400">
|
||||
{attentionCount()}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-[11px] uppercase tracking-wide text-muted">Protected</div>
|
||||
<div class="mt-1 text-xl font-semibold text-base-content">{summary().total}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div class="h-2 overflow-hidden rounded-full bg-surface-alt">
|
||||
<div class="flex h-full">
|
||||
<For each={postureSegments()}>
|
||||
{(segment) => (
|
||||
<div
|
||||
class={segment.color}
|
||||
style={{ width: `${Math.max(segment.percent, segment.count > 0 ? 4 : 0)}%` }}
|
||||
title={`${segment.label}: ${segment.count}`}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-x-3 gap-y-2">
|
||||
<For each={postureSegments()}>
|
||||
{(segment) => (
|
||||
<div class="flex items-center justify-between gap-3 text-sm">
|
||||
<div class="flex items-center gap-2 text-base-content">
|
||||
<span class={`h-2 w-2 rounded-full ${segment.color}`} />
|
||||
<span>{segment.label}</span>
|
||||
</div>
|
||||
<span class={`font-semibold tabular-nums ${segment.textColor}`}>
|
||||
{segment.count}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-t border-border-subtle pt-2.5">
|
||||
<div class="mb-2 flex items-center justify-between gap-3 text-[11px] font-medium uppercase tracking-wide text-muted">
|
||||
<span>Freshness</span>
|
||||
<Show when={summary().neverSucceeded > 0}>
|
||||
<span class="text-amber-600 dark:text-amber-400">
|
||||
{summary().neverSucceeded} never succeeded
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
<For each={freshnessBuckets()}>
|
||||
{(bucket) => (
|
||||
<div class="inline-flex items-center gap-2 rounded-md border border-border-subtle bg-surface-alt/35 px-2 py-1 text-xs">
|
||||
<span class={`h-2 w-2 rounded-full ${bucket.color}`} />
|
||||
<span class="text-base-content">{bucket.label}</span>
|
||||
<span class="tabular-nums text-base-content">{bucket.count}</span>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-[11px] uppercase tracking-wide text-muted">Attention</div>
|
||||
<div class="mt-1 text-2xl font-semibold text-amber-600 dark:text-amber-400">
|
||||
{attentionCount()}
|
||||
|
||||
<div class="border-t border-border-subtle pt-2.5 lg:border-t-0 lg:border-l lg:pl-3 lg:pt-0">
|
||||
<div class="mb-2 text-[11px] font-medium uppercase tracking-wide text-muted">
|
||||
Attention Queue
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-[11px] uppercase tracking-wide text-muted">Protected</div>
|
||||
<div class="mt-1 text-2xl font-semibold text-base-content">{summary().total}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div class="h-2.5 overflow-hidden rounded-full bg-surface-alt">
|
||||
<div class="flex h-full">
|
||||
<For each={postureSegments()}>
|
||||
{(segment) => (
|
||||
<div
|
||||
class={segment.color}
|
||||
style={{ width: `${Math.max(segment.percent, segment.count > 0 ? 4 : 0)}%` }}
|
||||
title={`${segment.label}: ${segment.count}`}
|
||||
/>
|
||||
<div class="flex h-full flex-col gap-2">
|
||||
<For
|
||||
each={[
|
||||
{
|
||||
label: 'Stale',
|
||||
count: summary().stale,
|
||||
detail: 'No successful point inside the active freshness threshold.',
|
||||
tone: 'amber',
|
||||
},
|
||||
{
|
||||
label: 'Never succeeded',
|
||||
count: summary().neverSucceeded,
|
||||
detail: 'Attempts exist but no successful recovery point has landed.',
|
||||
tone: 'rose',
|
||||
},
|
||||
{
|
||||
label: 'Attention',
|
||||
count: attentionCount(),
|
||||
detail: 'Protected items currently outside healthy recovery posture.',
|
||||
tone: 'amber',
|
||||
},
|
||||
{
|
||||
label: 'Running',
|
||||
count: postureSummary().running,
|
||||
detail: 'Recovery jobs are still in progress and not yet final.',
|
||||
tone: 'blue',
|
||||
},
|
||||
].filter((item) => item.count > 0)}
|
||||
>
|
||||
{(item) => (
|
||||
<div class="border-b border-border-subtle pb-2 last:border-b-0 last:pb-0">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-2 text-sm font-medium text-base-content">
|
||||
<span
|
||||
class={`h-2.5 w-2.5 rounded-full ${getRecoveryAttentionDotClass(item.tone)}`}
|
||||
/>
|
||||
<span>{item.label}</span>
|
||||
</div>
|
||||
<span
|
||||
class={`text-sm font-semibold tabular-nums ${
|
||||
item.tone === 'rose'
|
||||
? 'text-rose-600 dark:text-rose-400'
|
||||
: item.tone === 'blue'
|
||||
? 'text-blue-600 dark:text-blue-400'
|
||||
: 'text-amber-600 dark:text-amber-400'
|
||||
}`}
|
||||
>
|
||||
{item.count}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-1 text-xs leading-5 text-muted">{item.detail}</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-x-3 gap-y-2">
|
||||
<For each={postureSegments()}>
|
||||
{(segment) => (
|
||||
<div class="flex items-center justify-between gap-3 text-sm">
|
||||
<div class="flex items-center gap-2 text-base-content">
|
||||
<span class={`h-2 w-2 rounded-full ${segment.color}`} />
|
||||
<span>{segment.label}</span>
|
||||
</div>
|
||||
<span class={`font-semibold tabular-nums ${segment.textColor}`}>
|
||||
{segment.count}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-t border-border-subtle pt-3">
|
||||
<div class="mb-2 text-[11px] font-medium uppercase tracking-wide text-muted">
|
||||
Freshness
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<For each={freshnessBuckets()}>
|
||||
{(bucket) => (
|
||||
<div class="inline-flex items-center gap-2 rounded-md border border-border-subtle bg-surface-alt/35 px-2.5 py-1 text-xs">
|
||||
<span class={`h-2 w-2 rounded-full ${bucket.color}`} />
|
||||
<span class="text-base-content">{bucket.label}</span>
|
||||
<span class="tabular-nums text-base-content">{bucket.count}</span>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SummaryMetricCard>
|
||||
|
||||
<SummaryMetricCard
|
||||
label="Attention Queue"
|
||||
loaded={true}
|
||||
hasData={attentionItems().length > 0}
|
||||
emptyMessage="No active recovery risks."
|
||||
>
|
||||
<div class="grid gap-2">
|
||||
<For each={attentionItems().slice(0, 4)}>
|
||||
{(item) => (
|
||||
<div class="rounded-md border border-border-subtle bg-surface-alt/35 px-3 py-2">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class={`h-2.5 w-2.5 rounded-full ${getRecoveryAttentionDotClass(item.tone)}`}
|
||||
/>
|
||||
<span class="text-[11px] font-semibold uppercase tracking-[0.14em]">
|
||||
{item.label}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
class={`text-sm font-semibold tabular-nums ${
|
||||
item.tone === 'rose'
|
||||
? 'text-rose-600 dark:text-rose-400'
|
||||
: item.tone === 'blue'
|
||||
? 'text-blue-600 dark:text-blue-400'
|
||||
: 'text-amber-600 dark:text-amber-400'
|
||||
}`}
|
||||
>
|
||||
{item.count}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-1.5 text-sm leading-6 text-muted">{item.detail}</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</SummaryMetricCard>
|
||||
</SummaryMetricCard>
|
||||
</div>
|
||||
|
||||
<SummaryMetricCard label="Protected Footprint" loaded={true} hasData={hasRollups()}>
|
||||
<div class="flex h-full flex-col gap-3">
|
||||
<div class="flex h-full flex-col gap-2.5">
|
||||
<dl class="space-y-2 text-sm">
|
||||
<div class="flex items-center justify-between gap-3 border-b border-border-subtle pb-2">
|
||||
<dt class="text-[11px] uppercase tracking-wide text-muted">Item Types</dt>
|
||||
@@ -220,7 +249,7 @@ export const RecoverySummary: Component<RecoverySummaryProps> = (props) => {
|
||||
<div class="mb-2 text-[11px] font-medium uppercase tracking-wide text-muted">
|
||||
Item Types
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
<For each={itemCoverage().items.slice(0, 6)}>
|
||||
{(item) => (
|
||||
<div class="inline-flex items-center gap-2 text-[10px]">
|
||||
@@ -244,7 +273,7 @@ export const RecoverySummary: Component<RecoverySummaryProps> = (props) => {
|
||||
{platformCoverage().multiPlatformCount === 1 ? '' : 's'}
|
||||
</div>
|
||||
</Show>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
<For each={platformCoverage().items.slice(0, 6)}>
|
||||
{(item) => {
|
||||
const badge = getSourcePlatformBadge(item.key);
|
||||
@@ -270,7 +299,7 @@ export const RecoverySummary: Component<RecoverySummaryProps> = (props) => {
|
||||
hasData={activity().hasData}
|
||||
emptyMessage={props.seriesFailed?.() ? 'Trend data unavailable' : 'No recovery activity yet'}
|
||||
>
|
||||
<div class="flex h-full flex-col gap-3">
|
||||
<div class="flex h-full flex-col gap-2.5">
|
||||
<Show when={recentWindowLabel()}>
|
||||
<div class="text-xs text-muted">{recentWindowLabel()}</div>
|
||||
</Show>
|
||||
|
||||
@@ -206,6 +206,13 @@ describe('Recovery', () => {
|
||||
expect(await screen.findByRole('tab', { name: /protected items/i })).toBeInTheDocument();
|
||||
await screen.findByText('VM 123');
|
||||
expect(screen.getByText('Needs Attention')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Protected inventory')).not.toBeInTheDocument();
|
||||
expect(screen.getAllByText(/Page 1 \/ 1/i).length).toBeGreaterThan(0);
|
||||
expect(
|
||||
screen.getAllByText((_, element) =>
|
||||
(element?.textContent || '').replace(/\s+/g, ' ').includes('Sort: Latest point'),
|
||||
).length,
|
||||
).toBeGreaterThan(0);
|
||||
expect(screen.queryByText('Recovery Events')).not.toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByRole('table')).toHaveLength(1);
|
||||
|
||||
Reference in New Issue
Block a user