mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-20 01:01:20 +00:00
Canonicalize product table frames
This commit is contained in:
parent
c3b50001e0
commit
ef3d30e868
19 changed files with 337 additions and 238 deletions
|
|
@ -168,6 +168,12 @@ work extends shared components instead of creating new local variants.
|
|||
## Extension Points
|
||||
|
||||
1. Add shared primitives in `frontend-modern/src/components/shared/`
|
||||
Framed product table surfaces must consume the shared
|
||||
`TableCard` primitive instead of composing page-local `Card` border,
|
||||
background, and overflow classes. Feature owners may own the table data,
|
||||
filters, columns, and row behavior, but the outer product-table frame and
|
||||
its light/dark border treatment belong to frontend primitives so
|
||||
Infrastructure, Workloads, Storage, and Recovery do not drift visually.
|
||||
Shared monitored-system warning primitives under that path must stay compact
|
||||
app-shell pointers into the owned Pulse Pro billing surface. The shared
|
||||
banner may announce posture and route to the relevant billing tab, but
|
||||
|
|
|
|||
|
|
@ -224,6 +224,11 @@ regression protection.
|
|||
the shared `.grouped-table-row` CSS contract in `frontend-modern/src/index.css`,
|
||||
rather than local `bg-surface-alt` or hover-fill variants that drift between
|
||||
pages.
|
||||
Framed product-table cards on the same hot-path surfaces must consume the
|
||||
frontend-primitives-owned `TableCard` wrapper so Workloads,
|
||||
Infrastructure, Storage, and Recovery keep one border/background/overflow
|
||||
frame instead of introducing page-local table shells with different
|
||||
light/dark contrast.
|
||||
Infrastructure table responsive behavior belongs to that same hot-path
|
||||
owner. `frontend-modern/src/components/Infrastructure/useUnifiedResourceTableState.ts`
|
||||
must derive column visibility from the measured table surface width, with
|
||||
|
|
|
|||
|
|
@ -68,6 +68,11 @@ querying, and the operator-facing storage health presentation layer.
|
|||
|
||||
1. Add or change recovery-point persistence, rollups, or series derivation through `internal/recovery/`
|
||||
2. Add or change recovery page UX through `frontend-modern/src/components/Recovery/` and keep canonical route/query/filter state ownership in `frontend-modern/src/features/recovery/useRecoverySurfaceState.ts`
|
||||
Recovery table surfaces must consume the frontend-primitives-owned
|
||||
`TableCard` frame for protected-item, recovery-event, and adjacent
|
||||
table-fallback chrome. Storage/recovery may own table content, filters,
|
||||
columns, and rows, but it must not fork border/background/overflow table
|
||||
shells or reintroduce lighter open-sided Recovery table frames.
|
||||
Recovery inventory protection posture and recovery-event outcome filtering
|
||||
must stay separate in that owner: protected inventory uses the route-backed
|
||||
`state` query for health, stale, failed, warning, running, unknown, and
|
||||
|
|
|
|||
|
|
@ -337,7 +337,11 @@ AI-only summary payloads, or page-local heuristics.
|
|||
plus `frontend-modern/src/components/Infrastructure/useInfrastructureSummaryState.ts`
|
||||
must consume that scope through the shared page/group/entity interaction
|
||||
contract rather than inventing infrastructure-local summary filters or
|
||||
route-backed cluster hover state. Deliberate cluster focus must also stay
|
||||
route-backed cluster hover state. Host and service infrastructure table
|
||||
card frames must consume the frontend-primitives-owned `TableCard` wrapper;
|
||||
unified-resource ownership remains on resource identity, grouping, and row
|
||||
semantics rather than forking a table border/background shell. Deliberate
|
||||
cluster focus must also stay
|
||||
on the canonical infrastructure route through the shared `summaryGroup`
|
||||
query state, so pinned scope is shareable, reversible, and owned by the
|
||||
same route-backed summary contract as row focus. Infrastructure must stay
|
||||
|
|
|
|||
|
|
@ -122,6 +122,9 @@ describe('App architecture', () => {
|
|||
);
|
||||
expect(appStylesSource).toContain('tr.grouped-table-row > td');
|
||||
expect(appStylesSource).toContain('--color-grouped-table-row-bg');
|
||||
expect(appStylesSource).toContain('--color-grouped-table-row-bg: rgba(226, 232, 240, 0.72);');
|
||||
expect(appStylesSource).toContain('--color-grouped-table-row-bg: rgba(51, 65, 85, 0.58);');
|
||||
expect(appStylesSource).not.toContain('--color-grouped-table-row-bg: theme(');
|
||||
expect(appStylesSource).not.toContain('@keyframes pulse-brand-wordmark');
|
||||
expect(appStylesSource).not.toContain('text-shadow');
|
||||
expect(appLayoutSource).toContain("props.versionInfo()?.channel === 'rc'");
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { For } from 'solid-js';
|
||||
|
||||
import { ComponentErrorBoundary } from '@/components/ErrorBoundary';
|
||||
import { Card } from '@/components/shared/Card';
|
||||
import { SummaryTableCardHeader } from '@/components/shared/SummaryTableCardHeader';
|
||||
import { Table } from '@/components/shared/Table';
|
||||
import { TableCard } from '@/components/shared/TableCard';
|
||||
|
||||
import { getGuestColumnWidthStyle } from './guestRowModel';
|
||||
import type { DashboardState } from './useDashboardState';
|
||||
|
|
@ -60,10 +60,8 @@ export function DashboardWorkloadTable(props: DashboardWorkloadTableProps) {
|
|||
|
||||
return (
|
||||
<ComponentErrorBoundary name="Guest Table">
|
||||
<Card
|
||||
<TableCard
|
||||
ref={props.setTableRootRef}
|
||||
padding="none"
|
||||
tone="card"
|
||||
class="mb-4 rounded-md"
|
||||
data-summary-clear-surface
|
||||
data-testid="workloads-table-surface"
|
||||
|
|
@ -136,7 +134,7 @@ export function DashboardWorkloadTable(props: DashboardWorkloadTableProps) {
|
|||
workloadTableVisibleColumnIds={props.workloadTableVisibleColumnIds}
|
||||
/>
|
||||
</Table>
|
||||
</Card>
|
||||
</TableCard>
|
||||
</ComponentErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -794,6 +794,7 @@ describe('Dashboard performance contract', () => {
|
|||
expect(dashboardSource).toContain('clearPinnedSummaryScope={state.clearPinnedSummaryScope}');
|
||||
expect(dashboardWorkloadTableSource).toContain('data-summary-clear-surface');
|
||||
expect(dashboardWorkloadTableSource).toContain('data-testid="workloads-table-surface"');
|
||||
expect(dashboardWorkloadTableSource).toContain('TableCard');
|
||||
expect(dashboardWorkloadTableSource).toContain('SummaryTableCardHeader');
|
||||
expect(dashboardWorkloadTableSource).toContain('showClearSelection');
|
||||
expect(dashboardWorkloadTableSource).toContain('clearPinnedSummaryScope');
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ import type { Component } from 'solid-js';
|
|||
import type { Disk } from '@/types/api';
|
||||
import { formatBytes, formatSpeed, formatUptime, normalizeDiskArray } from '@/utils/format';
|
||||
import { formatTemperature } from '@/utils/temperature';
|
||||
import { Card } from '@/components/shared/Card';
|
||||
import { getInteractiveGroupedTableRowClass } from '@/components/shared/groupedTableRowPresentation';
|
||||
import { SummaryTableCardHeader } from '@/components/shared/SummaryTableCardHeader';
|
||||
import { TableCard } from '@/components/shared/TableCard';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
|
|
@ -63,7 +63,7 @@ export const UnifiedResourceHostTableCard: Component<UnifiedResourceHostTableCar
|
|||
|
||||
return (
|
||||
<Show when={table.showHostTable()}>
|
||||
<Card padding="none" tone="card" class="mb-0 overflow-hidden">
|
||||
<TableCard class="mb-0">
|
||||
<SummaryTableCardHeader
|
||||
title="Agent Infrastructure"
|
||||
showClearAction={table.showHostClearAction()}
|
||||
|
|
@ -676,7 +676,7 @@ export const UnifiedResourceHostTableCard: Component<UnifiedResourceHostTableCar
|
|||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</Card>
|
||||
</TableCard>
|
||||
</Show>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Show } from 'solid-js';
|
||||
import type { Component } from 'solid-js';
|
||||
import { Card } from '@/components/shared/Card';
|
||||
import { SummaryTableCardHeader } from '@/components/shared/SummaryTableCardHeader';
|
||||
import { TableCard } from '@/components/shared/TableCard';
|
||||
import {
|
||||
type UnifiedResourceTableProps,
|
||||
type UnifiedResourceTableState,
|
||||
|
|
@ -21,7 +21,7 @@ export const UnifiedResourceServiceInfrastructureCard: Component<
|
|||
|
||||
return (
|
||||
<Show when={table.sortedPBSResources().length > 0 || table.sortedPMGResources().length > 0}>
|
||||
<Card padding="none" tone="card" class="mb-0 overflow-hidden">
|
||||
<TableCard class="mb-0">
|
||||
<SummaryTableCardHeader
|
||||
title="Service Infrastructure"
|
||||
showClearAction={table.showServiceClearAction()}
|
||||
|
|
@ -29,7 +29,7 @@ export const UnifiedResourceServiceInfrastructureCard: Component<
|
|||
/>
|
||||
<UnifiedResourcePBSTableSection tableProps={tableProps} table={table} />
|
||||
<UnifiedResourcePMGTableSection tableProps={tableProps} table={table} />
|
||||
</Card>
|
||||
</TableCard>
|
||||
</Show>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -517,10 +517,12 @@ describe('UnifiedResourceTable performance contract', () => {
|
|||
expect(unifiedResourceHostTableCardSource).not.toContain('kind="scope"');
|
||||
expect(unifiedResourceHostTableCardSource).toContain('getInteractiveGroupedTableRowClass');
|
||||
expect(unifiedResourceHostTableCardSource).not.toContain('cursor-pointer bg-surface-alt');
|
||||
expect(unifiedResourceHostTableCardSource).toContain('TableCard');
|
||||
expect(unifiedResourceHostTableCardSource).toContain('SummaryTableCardHeader');
|
||||
expect(unifiedResourceHostTableCardSource).toContain(
|
||||
'onClear={tableProps.clearPinnedSummaryScope}',
|
||||
);
|
||||
expect(unifiedResourceServiceInfrastructureCardSource).toContain('TableCard');
|
||||
expect(unifiedResourceServiceInfrastructureCardSource).toContain('SummaryTableCardHeader');
|
||||
expect(unifiedResourceServiceInfrastructureCardSource).toContain('showClearAction');
|
||||
expect(unifiedResourceServiceInfrastructureCardSource).toContain(
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ import { RecoveryActivitySection } from '@/components/Recovery/RecoveryActivityS
|
|||
import { RecoveryHistorySection } from '@/components/Recovery/RecoveryHistorySection';
|
||||
import { RecoveryProtectedInventorySection } from '@/components/Recovery/RecoveryProtectedInventorySection';
|
||||
import { RecoverySummary } from '@/components/Recovery/RecoverySummary';
|
||||
import { Card } from '@/components/shared/Card';
|
||||
import { EmptyState } from '@/components/shared/EmptyState';
|
||||
import { PageHeader } from '@/components/shared/PageHeader';
|
||||
import { Subtabs } from '@/components/shared/Subtabs';
|
||||
import { TableCard } from '@/components/shared/TableCard';
|
||||
import { useRecoverySurfaceState } from '@/features/recovery/useRecoverySurfaceState';
|
||||
import type { ProtectedStateFilter } from '@/features/recovery/useRecoverySurfaceState';
|
||||
import { useBreakpoint } from '@/hooks/useBreakpoint';
|
||||
|
|
@ -669,11 +669,7 @@ const Recovery: Component = () => {
|
|||
{eventsActivity()}
|
||||
|
||||
<Show when={!recoveryPointsLoading() && recoveryPoints.response.error}>
|
||||
<Card
|
||||
padding="none"
|
||||
tone="card"
|
||||
class="overflow-hidden border-border-subtle bg-surface"
|
||||
>
|
||||
<TableCard>
|
||||
<div class="p-6">
|
||||
<EmptyState
|
||||
title={getRecoveryPointsFailureState().title}
|
||||
|
|
@ -683,7 +679,7 @@ const Recovery: Component = () => {
|
|||
)}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</TableCard>
|
||||
</Show>
|
||||
|
||||
<Show when={!recoveryPoints.response.error}>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
} from '@/components/shared/FilterToolbar';
|
||||
import { PageControls } from '@/components/shared/PageControls';
|
||||
import { SearchInput } from '@/components/shared/SearchInput';
|
||||
import { TableCard } from '@/components/shared/TableCard';
|
||||
import type { ColumnDef } from '@/hooks/useColumnVisibility';
|
||||
import { STORAGE_KEYS } from '@/utils/localStorage';
|
||||
import type { RecoveryOutcome, RecoveryPoint } from '@/types/recovery';
|
||||
|
|
@ -469,7 +470,7 @@ export const RecoveryHistorySection: Component<RecoveryHistorySectionProps> = (p
|
|||
</Card>
|
||||
</Show>
|
||||
|
||||
<Card padding="none" tone="card" class="overflow-hidden border-border-subtle bg-surface">
|
||||
<TableCard>
|
||||
<RecoveryHistoryTable
|
||||
clearSelectedPoint={clearSelectedPoint}
|
||||
currentPage={props.currentPage}
|
||||
|
|
@ -488,7 +489,7 @@ export const RecoveryHistorySection: Component<RecoveryHistorySectionProps> = (p
|
|||
toggleSelectedPoint={toggleSelectedPoint}
|
||||
totalPages={props.totalPages}
|
||||
/>
|
||||
</Card>
|
||||
</TableCard>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,7 +8,15 @@ import { PageControls } from '@/components/shared/PageControls';
|
|||
import { SearchInput } from '@/components/shared/SearchInput';
|
||||
import { StatusDot } from '@/components/shared/StatusDot';
|
||||
import { getSourcePlatformBadge } from '@/components/shared/sourcePlatformBadges';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/shared/Table';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/shared/Table';
|
||||
import { TableCard } from '@/components/shared/TableCard';
|
||||
import { STORAGE_KEYS } from '@/utils/localStorage';
|
||||
import { formatAbsoluteTime, formatRelativeTime } from '@/utils/format';
|
||||
import type { ProtectionRollup } from '@/types/recovery';
|
||||
|
|
@ -122,7 +130,8 @@ const getProtectionInsight = (
|
|||
|
||||
if (inventoryStatus === 'failed') return 'Latest protection event failed; open event details';
|
||||
if (inventoryStatus === 'warning') return 'Latest event completed with warnings; review details';
|
||||
if (inventoryStatus === 'running') return 'Protection event in progress; check events for completion';
|
||||
if (inventoryStatus === 'running')
|
||||
return 'Protection event in progress; check events for completion';
|
||||
return '';
|
||||
};
|
||||
|
||||
|
|
@ -174,8 +183,12 @@ export const RecoveryProtectedInventorySection: Component<
|
|||
return multiplier * leftLabel.localeCompare(rightLabel);
|
||||
}
|
||||
case 'type': {
|
||||
const leftType = getRecoveryItemTypePresentation(getRecoveryRollupItemTypeKey(left))?.label.toLowerCase();
|
||||
const rightType = getRecoveryItemTypePresentation(getRecoveryRollupItemTypeKey(right))?.label.toLowerCase();
|
||||
const leftType = getRecoveryItemTypePresentation(
|
||||
getRecoveryRollupItemTypeKey(left),
|
||||
)?.label.toLowerCase();
|
||||
const rightType = getRecoveryItemTypePresentation(
|
||||
getRecoveryRollupItemTypeKey(right),
|
||||
)?.label.toLowerCase();
|
||||
return multiplier * (leftType || '').localeCompare(rightType || '');
|
||||
}
|
||||
case 'platform': {
|
||||
|
|
@ -204,9 +217,7 @@ export const RecoveryProtectedInventorySection: Component<
|
|||
const leftTimestamp = getRecoveryRollupTimestampMs(left);
|
||||
const rightTimestamp = getRecoveryRollupTimestampMs(right);
|
||||
const naturalTieBreak =
|
||||
leftPriority === 4
|
||||
? leftTimestamp - rightTimestamp
|
||||
: rightTimestamp - leftTimestamp;
|
||||
leftPriority === 4 ? leftTimestamp - rightTimestamp : rightTimestamp - leftTimestamp;
|
||||
if (naturalTieBreak !== 0) return multiplier * naturalTieBreak;
|
||||
|
||||
const leftOutcome = normalizeRecoveryOutcome(left.lastOutcome);
|
||||
|
|
@ -337,9 +348,7 @@ export const RecoveryProtectedInventorySection: Component<
|
|||
<For each={availableProtectionStates}>
|
||||
{(state) => (
|
||||
<option value={state}>
|
||||
{state === 'all'
|
||||
? 'Any state'
|
||||
: getRecoveryRollupInventoryStatusLabel(state)}
|
||||
{state === 'all' ? 'Any state' : getRecoveryRollupInventoryStatusLabel(state)}
|
||||
</option>
|
||||
)}
|
||||
</For>
|
||||
|
|
@ -349,7 +358,7 @@ export const RecoveryProtectedInventorySection: Component<
|
|||
</Card>
|
||||
</Show>
|
||||
|
||||
<Card padding="none" tone="card" class="overflow-hidden border-border-subtle bg-surface">
|
||||
<TableCard>
|
||||
<Show when={props.loading() && props.filteredRollups().length === 0}>
|
||||
<div
|
||||
data-testid="recovery-protected-loading"
|
||||
|
|
@ -400,213 +409,216 @@ export const RecoveryProtectedInventorySection: Component<
|
|||
|
||||
<Show when={props.filteredRollups().length > 0}>
|
||||
<div class="overflow-x-auto bg-surface">
|
||||
<Table
|
||||
class={`w-full border-collapse whitespace-nowrap table-fixed ${
|
||||
props.isMobile ? 'min-w-full' : 'min-w-[640px]'
|
||||
}`}
|
||||
>
|
||||
<TableHeader>
|
||||
<TableRow class="bg-surface-alt/95 text-muted">
|
||||
{(
|
||||
[
|
||||
['item', getRecoveryArtifactColumnLabel('item', 'Item')],
|
||||
['type', 'Item Type'],
|
||||
['platform', getRecoveryArtifactColumnLabel('platform', 'Platform')],
|
||||
['lastBackup', 'Latest Point'],
|
||||
['outcome', 'Protection State'],
|
||||
] as const
|
||||
).map(([column, label]) => (
|
||||
<TableHead
|
||||
class={`sticky top-0 z-[1] bg-surface-alt/95 px-3 py-2 whitespace-nowrap text-left text-[11px] font-medium cursor-pointer select-none hover:text-base-content transition-colors${
|
||||
column === 'type'
|
||||
? ' hidden md:table-cell w-[96px]'
|
||||
: column === 'platform'
|
||||
? ' hidden lg:table-cell w-[110px]'
|
||||
: column === 'lastBackup'
|
||||
? ' w-[120px]'
|
||||
: column === 'outcome'
|
||||
? ' w-[116px]'
|
||||
: ''
|
||||
}`}
|
||||
onClick={() => toggleProtectedSort(column)}
|
||||
>
|
||||
<span class="inline-flex items-center gap-1">
|
||||
{label}
|
||||
<Show when={protectedSortCol() === column}>
|
||||
<svg class="h-3 w-3" viewBox="0 0 12 12" fill="currentColor">
|
||||
{protectedSortDir() === 'asc' ? (
|
||||
<path d="M6 3l3.5 5h-7z" />
|
||||
) : (
|
||||
<path d="M6 9l3.5-5h-7z" />
|
||||
)}
|
||||
</svg>
|
||||
</Show>
|
||||
</span>
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<For each={sortedRollups()}>
|
||||
{(rollup) => {
|
||||
const resourceIndex = props.resourcesById();
|
||||
const label = getRecoveryRollupItemLabel(rollup, resourceIndex);
|
||||
const secondaryLabel = getRecoveryRollupItemSecondaryLabel(rollup);
|
||||
const attemptMs = rollup.lastAttemptAt ? Date.parse(rollup.lastAttemptAt) : 0;
|
||||
const successMs = rollup.lastSuccessAt ? Date.parse(rollup.lastSuccessAt) : 0;
|
||||
const inventoryStatus = getRecoveryRollupInventoryStatus(rollup);
|
||||
const inventoryStatusLabel = getRecoveryRollupInventoryStatusLabel(inventoryStatus);
|
||||
const platforms = getRecoveryRollupPlatforms(rollup)
|
||||
.map((platform) => String(platform || '').trim())
|
||||
.filter(Boolean)
|
||||
.sort((left, right) =>
|
||||
getSourcePlatformLabel(left).localeCompare(getSourcePlatformLabel(right)),
|
||||
);
|
||||
const itemTypePresentation =
|
||||
getRecoveryItemTypePresentation(getRecoveryRollupItemTypeKey(rollup)) || null;
|
||||
const nowMs = Date.now();
|
||||
const neverSucceeded = inventoryStatus === 'never-succeeded';
|
||||
const protectionInsight = getProtectionInsight(rollup, inventoryStatus, nowMs);
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
class="cursor-pointer odd:bg-surface even:bg-surface-alt/35 transition-colors hover:bg-surface-hover/95"
|
||||
onClick={() => props.onSelectRollup(rollup.rollupId)}
|
||||
<Table
|
||||
class={`w-full border-collapse whitespace-nowrap table-fixed ${
|
||||
props.isMobile ? 'min-w-full' : 'min-w-[640px]'
|
||||
}`}
|
||||
>
|
||||
<TableHeader>
|
||||
<TableRow class="bg-surface-alt/95 text-muted">
|
||||
{(
|
||||
[
|
||||
['item', getRecoveryArtifactColumnLabel('item', 'Item')],
|
||||
['type', 'Item Type'],
|
||||
['platform', getRecoveryArtifactColumnLabel('platform', 'Platform')],
|
||||
['lastBackup', 'Latest Point'],
|
||||
['outcome', 'Protection State'],
|
||||
] as const
|
||||
).map(([column, label]) => (
|
||||
<TableHead
|
||||
class={`sticky top-0 z-[1] bg-surface-alt/95 px-3 py-2 whitespace-nowrap text-left text-[11px] font-medium cursor-pointer select-none hover:text-base-content transition-colors${
|
||||
column === 'type'
|
||||
? ' hidden md:table-cell w-[96px]'
|
||||
: column === 'platform'
|
||||
? ' hidden lg:table-cell w-[110px]'
|
||||
: column === 'lastBackup'
|
||||
? ' w-[120px]'
|
||||
: column === 'outcome'
|
||||
? ' w-[116px]'
|
||||
: ''
|
||||
}`}
|
||||
onClick={() => toggleProtectedSort(column)}
|
||||
>
|
||||
<TableCell
|
||||
class="max-w-[420px] px-3 py-1.5 text-base-content"
|
||||
title={label}
|
||||
<span class="inline-flex items-center gap-1">
|
||||
{label}
|
||||
<Show when={protectedSortCol() === column}>
|
||||
<svg class="h-3 w-3" viewBox="0 0 12 12" fill="currentColor">
|
||||
{protectedSortDir() === 'asc' ? (
|
||||
<path d="M6 3l3.5 5h-7z" />
|
||||
) : (
|
||||
<path d="M6 9l3.5-5h-7z" />
|
||||
)}
|
||||
</svg>
|
||||
</Show>
|
||||
</span>
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<For each={sortedRollups()}>
|
||||
{(rollup) => {
|
||||
const resourceIndex = props.resourcesById();
|
||||
const label = getRecoveryRollupItemLabel(rollup, resourceIndex);
|
||||
const secondaryLabel = getRecoveryRollupItemSecondaryLabel(rollup);
|
||||
const attemptMs = rollup.lastAttemptAt ? Date.parse(rollup.lastAttemptAt) : 0;
|
||||
const successMs = rollup.lastSuccessAt ? Date.parse(rollup.lastSuccessAt) : 0;
|
||||
const inventoryStatus = getRecoveryRollupInventoryStatus(rollup);
|
||||
const inventoryStatusLabel =
|
||||
getRecoveryRollupInventoryStatusLabel(inventoryStatus);
|
||||
const platforms = getRecoveryRollupPlatforms(rollup)
|
||||
.map((platform) => String(platform || '').trim())
|
||||
.filter(Boolean)
|
||||
.sort((left, right) =>
|
||||
getSourcePlatformLabel(left).localeCompare(getSourcePlatformLabel(right)),
|
||||
);
|
||||
const itemTypePresentation =
|
||||
getRecoveryItemTypePresentation(getRecoveryRollupItemTypeKey(rollup)) || null;
|
||||
const nowMs = Date.now();
|
||||
const neverSucceeded = inventoryStatus === 'never-succeeded';
|
||||
const protectionInsight = getProtectionInsight(rollup, inventoryStatus, nowMs);
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
class="cursor-pointer odd:bg-surface even:bg-surface-alt/35 transition-colors hover:bg-surface-hover/95"
|
||||
onClick={() => props.onSelectRollup(rollup.rollupId)}
|
||||
>
|
||||
<div class="flex min-w-0 flex-col gap-1">
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<StatusDot
|
||||
variant={getRecoveryRollupInventoryStatusVariant(inventoryStatus)}
|
||||
size="xs"
|
||||
pulse={inventoryStatus === 'running'}
|
||||
title={inventoryStatusLabel}
|
||||
ariaLabel={inventoryStatusLabel}
|
||||
/>
|
||||
<div class="flex min-w-0 items-baseline gap-1.5">
|
||||
<span class="truncate text-[13px] font-medium">{label}</span>
|
||||
<Show when={secondaryLabel}>
|
||||
<span class="shrink-0 text-[10px] font-mono tabular-nums text-muted">
|
||||
{secondaryLabel}
|
||||
<TableCell
|
||||
class="max-w-[420px] px-3 py-1.5 text-base-content"
|
||||
title={label}
|
||||
>
|
||||
<div class="flex min-w-0 flex-col gap-1">
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<StatusDot
|
||||
variant={getRecoveryRollupInventoryStatusVariant(inventoryStatus)}
|
||||
size="xs"
|
||||
pulse={inventoryStatus === 'running'}
|
||||
title={inventoryStatusLabel}
|
||||
ariaLabel={inventoryStatusLabel}
|
||||
/>
|
||||
<div class="flex min-w-0 items-baseline gap-1.5">
|
||||
<span class="truncate text-[13px] font-medium">{label}</span>
|
||||
<Show when={secondaryLabel}>
|
||||
<span class="shrink-0 text-[10px] font-mono tabular-nums text-muted">
|
||||
{secondaryLabel}
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={protectionInsight}>
|
||||
<div class="pl-4 text-[10px] leading-4 text-muted">
|
||||
{protectionInsight}
|
||||
</div>
|
||||
</Show>
|
||||
<div class="flex flex-wrap items-center gap-1.5 text-[10px] md:hidden">
|
||||
<Show when={itemTypePresentation?.label}>
|
||||
<span class={itemTypePresentation?.tableBadgeClasses}>
|
||||
{itemTypePresentation?.label}
|
||||
</span>
|
||||
</Show>
|
||||
<Show when={platforms.length > 0}>
|
||||
<For each={platforms.slice(0, 2)}>
|
||||
{(platform) => {
|
||||
const badge = getSourcePlatformBadge(platform);
|
||||
return (
|
||||
<span class={`${badge?.classes || ''} lg:hidden`}>
|
||||
{badge?.label || getSourcePlatformLabel(platform)}
|
||||
</span>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={protectionInsight}>
|
||||
<div class="pl-4 text-[10px] leading-4 text-muted">
|
||||
{protectionInsight}
|
||||
</div>
|
||||
</Show>
|
||||
<div class="flex flex-wrap items-center gap-1.5 text-[10px] md:hidden">
|
||||
<Show when={itemTypePresentation?.label}>
|
||||
<span class={itemTypePresentation?.tableBadgeClasses}>
|
||||
{itemTypePresentation?.label}
|
||||
</span>
|
||||
</Show>
|
||||
<Show when={platforms.length > 0}>
|
||||
<For each={platforms.slice(0, 2)}>
|
||||
{(platform) => {
|
||||
const badge = getSourcePlatformBadge(platform);
|
||||
return (
|
||||
<span class={`${badge?.classes || ''} lg:hidden`}>
|
||||
{badge?.label || getSourcePlatformLabel(platform)}
|
||||
</span>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableCell>
|
||||
|
||||
<TableCell class="hidden md:table-cell whitespace-nowrap px-3 py-1.5">
|
||||
<Show
|
||||
when={itemTypePresentation}
|
||||
fallback={<span class="text-muted">—</span>}
|
||||
>
|
||||
<span class={itemTypePresentation?.tableBadgeClasses}>
|
||||
{itemTypePresentation?.label}
|
||||
</span>
|
||||
</Show>
|
||||
</TableCell>
|
||||
|
||||
<TableCell class="hidden lg:table-cell whitespace-nowrap px-3 py-1.5">
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
<For each={platforms}>
|
||||
{(platform) => {
|
||||
const badge = getSourcePlatformBadge(platform);
|
||||
return (
|
||||
<span class={badge?.classes || ''}>
|
||||
{badge?.label || getSourcePlatformLabel(platform)}
|
||||
</span>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
<TableCell
|
||||
class={`whitespace-nowrap px-3 py-1.5 ${getRecoveryRollupAgeTextClass(
|
||||
rollup,
|
||||
nowMs,
|
||||
)}`}
|
||||
title={
|
||||
successMs > 0
|
||||
? formatAbsoluteTime(successMs)
|
||||
: attemptMs > 0
|
||||
? formatAbsoluteTime(attemptMs)
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<div class="flex flex-col">
|
||||
<span>
|
||||
{successMs > 0 ? (
|
||||
formatRelativeTime(successMs)
|
||||
) : neverSucceeded ? (
|
||||
<span class={getRecoverySpecialOutcomeTextClass('never')}>never</span>
|
||||
) : (
|
||||
'—'
|
||||
)}
|
||||
</span>
|
||||
<Show when={inventoryStatus !== 'healthy' && attemptMs > 0}>
|
||||
<span class="text-[10px] font-normal text-muted">
|
||||
Attempt {formatRelativeTime(attemptMs)}
|
||||
<TableCell class="hidden md:table-cell whitespace-nowrap px-3 py-1.5">
|
||||
<Show
|
||||
when={itemTypePresentation}
|
||||
fallback={<span class="text-muted">—</span>}
|
||||
>
|
||||
<span class={itemTypePresentation?.tableBadgeClasses}>
|
||||
{itemTypePresentation?.label}
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableCell>
|
||||
|
||||
<TableCell class="whitespace-nowrap px-3 py-1.5">
|
||||
<span
|
||||
class={`text-[11px] font-medium ${getRecoveryRollupInventoryStatusTextClass(
|
||||
inventoryStatus,
|
||||
<TableCell class="hidden lg:table-cell whitespace-nowrap px-3 py-1.5">
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
<For each={platforms}>
|
||||
{(platform) => {
|
||||
const badge = getSourcePlatformBadge(platform);
|
||||
return (
|
||||
<span class={badge?.classes || ''}>
|
||||
{badge?.label || getSourcePlatformLabel(platform)}
|
||||
</span>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
<TableCell
|
||||
class={`whitespace-nowrap px-3 py-1.5 ${getRecoveryRollupAgeTextClass(
|
||||
rollup,
|
||||
nowMs,
|
||||
)}`}
|
||||
title={
|
||||
successMs > 0
|
||||
? formatAbsoluteTime(successMs)
|
||||
: attemptMs > 0
|
||||
? formatAbsoluteTime(attemptMs)
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{inventoryStatusLabel}
|
||||
</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-2 border-t border-border bg-surface px-4 py-3 text-xs text-muted">
|
||||
<div>
|
||||
<Show
|
||||
when={sortedRollups().length > 0}
|
||||
fallback={<span>Showing 0 of 0 protected items</span>}
|
||||
>
|
||||
<span>Showing {sortedRollups().length} protected items</span>
|
||||
</Show>
|
||||
<div class="flex flex-col">
|
||||
<span>
|
||||
{successMs > 0 ? (
|
||||
formatRelativeTime(successMs)
|
||||
) : neverSucceeded ? (
|
||||
<span class={getRecoverySpecialOutcomeTextClass('never')}>
|
||||
never
|
||||
</span>
|
||||
) : (
|
||||
'—'
|
||||
)}
|
||||
</span>
|
||||
<Show when={inventoryStatus !== 'healthy' && attemptMs > 0}>
|
||||
<span class="text-[10px] font-normal text-muted">
|
||||
Attempt {formatRelativeTime(attemptMs)}
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
<TableCell class="whitespace-nowrap px-3 py-1.5">
|
||||
<span
|
||||
class={`text-[11px] font-medium ${getRecoveryRollupInventoryStatusTextClass(
|
||||
inventoryStatus,
|
||||
)}`}
|
||||
>
|
||||
{inventoryStatusLabel}
|
||||
</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-2 border-t border-border bg-surface px-4 py-3 text-xs text-muted">
|
||||
<div>
|
||||
<Show
|
||||
when={sortedRollups().length > 0}
|
||||
fallback={<span>Showing 0 of 0 protected items</span>}
|
||||
>
|
||||
<span>Showing {sortedRollups().length} protected items</span>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</Card>
|
||||
</TableCard>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Component, Show } from 'solid-js';
|
||||
import { Card } from '@/components/shared/Card';
|
||||
import { SummaryTableCardHeader } from '@/components/shared/SummaryTableCardHeader';
|
||||
import { TableCard } from '@/components/shared/TableCard';
|
||||
import { DiskList } from '@/components/Storage/DiskList';
|
||||
import StoragePoolsTable from '@/components/Storage/StoragePoolsTable';
|
||||
import { STORAGE_CONTENT_CARD_BODY_CLASS } from '@/features/storageBackups/storagePagePresentation';
|
||||
|
|
@ -59,11 +59,8 @@ export const StorageContentCard: Component<StorageContentCardProps> = (props) =>
|
|||
Boolean(props.focusedSummaryGroupId() || props.expandedPoolId() || props.selectedDiskId());
|
||||
|
||||
return (
|
||||
<Card
|
||||
<TableCard
|
||||
ref={props.setTableRootRef}
|
||||
padding="none"
|
||||
tone="card"
|
||||
class="overflow-hidden"
|
||||
data-summary-clear-surface
|
||||
data-testid="storage-content-surface"
|
||||
>
|
||||
|
|
@ -115,7 +112,7 @@ export const StorageContentCard: Component<StorageContentCardProps> = (props) =>
|
|||
onHoverChange={props.setHoveredStorageResourceId}
|
||||
/>
|
||||
</Show>
|
||||
</Card>
|
||||
</TableCard>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { JSX, splitProps, mergeProps } from 'solid-js';
|
|||
type Tone = 'default' | 'muted' | 'info' | 'success' | 'warning' | 'danger' | 'card' | 'glass';
|
||||
type Padding = 'none' | 'sm' | 'md' | 'lg';
|
||||
|
||||
type CardProps = {
|
||||
export type CardProps = {
|
||||
tone?: Tone;
|
||||
padding?: Padding;
|
||||
hoverable?: boolean;
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ import summaryRowActionButtonSource from '@/components/shared/SummaryRowActionBu
|
|||
import summaryInteractionA11ySource from '@/components/shared/summaryInteractionA11y.ts?raw';
|
||||
import summaryTableCardHeaderSource from '@/components/shared/SummaryTableCardHeader.tsx?raw';
|
||||
import summaryTableFocusSource from '@/components/shared/summaryTableFocus.ts?raw';
|
||||
import tableCardSource from '@/components/shared/TableCard.tsx?raw';
|
||||
import groupedTableRowPresentationSource from '@/components/shared/groupedTableRowPresentation.ts?raw';
|
||||
import infrastructureSummaryTableSource from '@/components/shared/InfrastructureSummaryTable.tsx?raw';
|
||||
import infrastructureSummaryTableRowSource from '@/components/shared/InfrastructureSummaryTableRow.tsx?raw';
|
||||
|
|
@ -118,15 +119,20 @@ import dashboardSelectionStateSource from '@/components/Dashboard/useDashboardSe
|
|||
import infrastructureSummarySource from '@/components/Infrastructure/InfrastructureSummary.tsx?raw';
|
||||
import infrastructureSummaryStateSource from '@/components/Infrastructure/useInfrastructureSummaryState.ts?raw';
|
||||
import unifiedResourceHostTableCardSource from '@/components/Infrastructure/UnifiedResourceHostTableCard.tsx?raw';
|
||||
import unifiedResourceServiceInfrastructureCardSource from '@/components/Infrastructure/UnifiedResourceServiceInfrastructureCard.tsx?raw';
|
||||
import unifiedResourcePBSTableSectionSource from '@/components/Infrastructure/UnifiedResourcePBSTableSection.tsx?raw';
|
||||
import unifiedResourcePMGTableSectionSource from '@/components/Infrastructure/UnifiedResourcePMGTableSection.tsx?raw';
|
||||
import nodeGroupHeaderSource from '@/components/shared/NodeGroupHeader.tsx?raw';
|
||||
import storageGroupRowSource from '@/components/Storage/StorageGroupRow.tsx?raw';
|
||||
import storageGroupPresentationSource from '@/features/storageBackups/groupPresentation.ts?raw';
|
||||
import storagePoolRowSource from '@/components/Storage/StoragePoolRow.tsx?raw';
|
||||
import storageContentCardSource from '@/components/Storage/StorageContentCard.tsx?raw';
|
||||
import diskListSource from '@/components/Storage/DiskList.tsx?raw';
|
||||
import storageSummarySource from '@/components/Storage/StorageSummary.tsx?raw';
|
||||
import workloadsSummarySource from '@/components/Workloads/WorkloadsSummary.tsx?raw';
|
||||
import recoveryComponentSource from '@/components/Recovery/Recovery.tsx?raw';
|
||||
import recoveryHistorySectionSource from '@/components/Recovery/RecoveryHistorySection.tsx?raw';
|
||||
import recoveryProtectedInventorySectionSource from '@/components/Recovery/RecoveryProtectedInventorySection.tsx?raw';
|
||||
import recoveryTablePresentationSource from '@/utils/recoveryTablePresentation.ts?raw';
|
||||
import resourceDetailDrawerOverviewSource from '@/components/Infrastructure/ResourceDetailDrawerOverviewTab.tsx?raw';
|
||||
import aiSettingsDialogsSource from '@/components/Settings/AISettingsDialogs.tsx?raw';
|
||||
|
|
@ -278,6 +284,28 @@ describe('shared primitive guardrails', () => {
|
|||
expect(storagePoolRowSource).not.toContain('Clear selection');
|
||||
});
|
||||
|
||||
it('keeps framed product table surfaces on the shared TableCard owner', () => {
|
||||
expect(tableCardSource).toContain("export const TABLE_CARD_FRAME_CLASS = 'overflow-hidden'");
|
||||
expect(tableCardSource).toContain("Omit<CardProps, 'border' | 'padding' | 'tone'>");
|
||||
expect(tableCardSource).toContain('border={true}');
|
||||
expect(tableCardSource).toContain('padding="none"');
|
||||
expect(tableCardSource).toContain('tone="card"');
|
||||
expect(tableCardSource).toContain('<Card');
|
||||
|
||||
for (const source of [
|
||||
dashboardWorkloadTableSource,
|
||||
unifiedResourceHostTableCardSource,
|
||||
unifiedResourceServiceInfrastructureCardSource,
|
||||
storageContentCardSource,
|
||||
recoveryComponentSource,
|
||||
recoveryHistorySectionSource,
|
||||
recoveryProtectedInventorySectionSource,
|
||||
]) {
|
||||
expect(source).toContain('TableCard');
|
||||
expect(source).not.toContain('overflow-hidden border-border-subtle bg-surface');
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps shared subtabs as one primitive and leaves shell styling to owning surfaces', () => {
|
||||
expect(subtabsSource).not.toContain("variant?: 'default' | 'control'");
|
||||
expect(subtabsSource).not.toContain('subtabsControlShellClass');
|
||||
|
|
@ -387,6 +415,13 @@ describe('shared primitive guardrails', () => {
|
|||
expect(frontendIndexCssSource).toContain('--color-summary-group-member-pinned-accent');
|
||||
expect(frontendIndexCssSource).toContain('tr.grouped-table-row > td');
|
||||
expect(frontendIndexCssSource).toContain('--color-grouped-table-row-bg');
|
||||
expect(frontendIndexCssSource).toContain(
|
||||
'--color-grouped-table-row-bg: rgba(226, 232, 240, 0.72);',
|
||||
);
|
||||
expect(frontendIndexCssSource).toContain(
|
||||
'--color-grouped-table-row-bg: rgba(51, 65, 85, 0.58);',
|
||||
);
|
||||
expect(frontendIndexCssSource).not.toContain('--color-grouped-table-row-bg: theme(');
|
||||
expect(groupedTableRowPresentationSource).toContain('GROUPED_TABLE_ROW_CLASS');
|
||||
expect(groupedTableRowPresentationSource).not.toContain('GROUPED_TABLE_ROW_DIVIDER_CLASS');
|
||||
|
||||
|
|
|
|||
23
frontend-modern/src/components/shared/TableCard.tsx
Normal file
23
frontend-modern/src/components/shared/TableCard.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { splitProps } from 'solid-js';
|
||||
|
||||
import { Card, type CardProps } from '@/components/shared/Card';
|
||||
|
||||
export type TableCardProps = Omit<CardProps, 'border' | 'padding' | 'tone'>;
|
||||
|
||||
export const TABLE_CARD_FRAME_CLASS = 'overflow-hidden';
|
||||
|
||||
export function TableCard(props: TableCardProps) {
|
||||
const [local, rest] = splitProps(props, ['class']);
|
||||
|
||||
return (
|
||||
<Card
|
||||
{...rest}
|
||||
border={true}
|
||||
padding="none"
|
||||
tone="card"
|
||||
class={`${TABLE_CARD_FRAME_CLASS} ${local.class ?? ''}`.trim()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default TableCard;
|
||||
|
|
@ -85,9 +85,10 @@
|
|||
--color-summary-group-member-pinned-bg: rgba(14, 165, 233, 0.04);
|
||||
--color-summary-group-member-pinned-bg-hover: rgba(14, 165, 233, 0.055);
|
||||
--color-summary-group-member-pinned-accent: rgba(14, 165, 233, 0.22);
|
||||
--color-grouped-table-row-bg: theme('colors.slate.200');
|
||||
--color-grouped-table-row-bg-hover: theme('colors.slate.300');
|
||||
--color-grouped-table-row-border: theme('colors.slate.300');
|
||||
/* Shared product-table subgroup band. It should read as body segmentation, not table chrome. */
|
||||
--color-grouped-table-row-bg: rgba(226, 232, 240, 0.72);
|
||||
--color-grouped-table-row-bg-hover: rgba(203, 213, 225, 0.82);
|
||||
--color-grouped-table-row-border: rgba(203, 213, 225, 0.9);
|
||||
}
|
||||
|
||||
.dark {
|
||||
|
|
@ -110,9 +111,9 @@
|
|||
--color-summary-group-member-pinned-bg: rgba(56, 189, 248, 0.07);
|
||||
--color-summary-group-member-pinned-bg-hover: rgba(56, 189, 248, 0.09);
|
||||
--color-summary-group-member-pinned-accent: rgba(56, 189, 248, 0.28);
|
||||
--color-grouped-table-row-bg: theme('colors.slate.900');
|
||||
--color-grouped-table-row-bg-hover: theme('colors.slate.800');
|
||||
--color-grouped-table-row-border: theme('colors.slate.700');
|
||||
--color-grouped-table-row-bg: rgba(51, 65, 85, 0.58);
|
||||
--color-grouped-table-row-bg-hover: rgba(51, 65, 85, 0.82);
|
||||
--color-grouped-table-row-border: rgba(71, 85, 105, 0.85);
|
||||
}
|
||||
|
||||
body {
|
||||
|
|
|
|||
|
|
@ -1107,8 +1107,17 @@ describe('frontend resource type boundaries', () => {
|
|||
"import { useRecoverySurfaceState } from '@/features/recovery/useRecoverySurfaceState';",
|
||||
);
|
||||
expect(recoveryComponentSource).toContain('useRecoverySurfaceState');
|
||||
expect(recoveryComponentSource).toContain('TableCard');
|
||||
expect(recoveryHistorySectionSource).toContain('useRecoveryHistorySectionState');
|
||||
expect(recoveryHistorySectionSource).toContain('RecoveryHistoryTable');
|
||||
expect(recoveryHistorySectionSource).toContain('TableCard');
|
||||
expect(recoveryHistorySectionSource).not.toContain(
|
||||
'overflow-hidden border-border-subtle bg-surface',
|
||||
);
|
||||
expect(recoveryProtectedInventorySectionSource).toContain('TableCard');
|
||||
expect(recoveryProtectedInventorySectionSource).not.toContain(
|
||||
'overflow-hidden border-border-subtle bg-surface',
|
||||
);
|
||||
expect(recoveryHistorySectionStateSource).toContain(
|
||||
'export function useRecoveryHistorySectionState',
|
||||
);
|
||||
|
|
@ -2050,6 +2059,7 @@ describe('frontend resource type boundaries', () => {
|
|||
expect(storageContentCardSource).toContain('useStorageContentCardModel');
|
||||
expect(storageContentCardSource).toContain('DiskList');
|
||||
expect(storageContentCardSource).toContain('StoragePoolsTable');
|
||||
expect(storageContentCardSource).toContain('TableCard');
|
||||
expect(storageContentCardSource).toContain('SummaryTableCardHeader');
|
||||
expect(storageContentCardSource).toContain('STORAGE_CONTENT_CARD_BODY_CLASS');
|
||||
expect(storageContentCardSource).not.toContain(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue