From e4514dfd8b539ab325692c97a0969ced9db1571a Mon Sep 17 00:00:00 2001 From: rcourtman Date: Wed, 29 Apr 2026 10:24:51 +0100 Subject: [PATCH] Canonicalize grouped table row presentation --- .../subsystems/frontend-primitives.md | 2 +- .../Alerts/AlertResourceGroupHeader.tsx | 9 ++++---- .../Alerts/AlertResourceTableDesktop.tsx | 7 +++++-- .../components/Dashboard/WorkloadPanel.tsx | 10 ++++++--- .../Dashboard.performance.contract.test.tsx | 2 ++ .../UnifiedResourceHostTableCard.tsx | 13 ++++++++---- ...esourceTable.performance.contract.test.tsx | 1 + .../Settings/InfrastructureSourceManager.tsx | 14 +++++++------ .../src/components/shared/NodeGroupHeader.tsx | 21 ++++++++++--------- .../SharedPrimitives.guardrails.test.ts | 11 +++++++++- .../shared/groupedTableRowPresentation.ts | 8 +++++++ .../alerts/AlertHistoryTableGroupRow.tsx | 10 ++++++--- .../storageBackups/groupPresentation.ts | 7 +++++-- .../pages/__tests__/Alerts.helpers.test.ts | 2 ++ .../frontendResourceTypeBoundaries.test.ts | 2 +- .../recoveryTablePresentation.test.ts | 2 +- .../src/utils/recoveryTablePresentation.ts | 10 +++++---- 17 files changed, 89 insertions(+), 42 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 9c9f37890..8be6f6d1e 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -507,7 +507,7 @@ work extends shared components instead of creating new local variants. reset action stays as one compact header-level `Clear` control with an accessible `Clear selection` label, not a second page-level scope strip, search-row accessory, or filter-bar badge. -13. Keep summary-linked table row emphasis on the shared primitive contract. Workloads, infrastructure, and storage rows that mirror the active summary entity must expose that state through `data-summary-row-active` and let the shared presentation in `frontend-modern/src/index.css` render the row emphasis, rather than carrying page-local sky or blue fill classes inside each row renderer. Group-scoped preview and pin must use that same shared presentation boundary: child rows that belong to a hovered or pinned summary group should expose `data-summary-group-member-active="preview|pinned"` so the block-level emphasis stays subtle, consistent, and reversible instead of each table inventing its own outline, badge, or full-strength fill treatment. Static grouped row headers on workloads, infrastructure, storage, recovery, and future grouped tables must use `frontend-modern/src/components/shared/groupedTableRowPresentation.ts` plus the `.grouped-table-row` CSS contract in `frontend-modern/src/index.css`, rather than rebuilding local `bg-surface-alt` variants with subtly different light/dark behavior or page-local left-accent markers. Storage-backed reusable row presenters under `frontend-modern/src/features/storageBackups/` must also keep row height and alert accents on class/data-attribute presentation instead of runtime inline style maps, so the shared table contract stays CSP-safe on both steady-state and alert-highlighted routes. +13. Keep summary-linked table row emphasis on the shared primitive contract. Workloads, infrastructure, and storage rows that mirror the active summary entity must expose that state through `data-summary-row-active` and let the shared presentation in `frontend-modern/src/index.css` render the row emphasis, rather than carrying page-local sky or blue fill classes inside each row renderer. Group-scoped preview and pin must use that same shared presentation boundary: child rows that belong to a hovered or pinned summary group should expose `data-summary-group-member-active="preview|pinned"` so the block-level emphasis stays subtle, consistent, and reversible instead of each table inventing its own outline, badge, or full-strength fill treatment. Static grouped row headers on workloads, infrastructure, storage, recovery, and future grouped tables must use `frontend-modern/src/components/shared/groupedTableRowPresentation.ts` plus the `.grouped-table-row` CSS contract in `frontend-modern/src/index.css`, rather than rebuilding local `bg-surface-alt` variants with subtly different light/dark behavior or page-local left-accent markers. That shared grouped-table primitive owns the subgroup cell padding, typography, small metadata, and badge treatment as well as the row background token, so a future adjustment to the subgroup visual language changes every grouped product table from one owner. Storage-backed reusable row presenters under `frontend-modern/src/features/storageBackups/` must also keep row height and alert accents on class/data-attribute presentation instead of runtime inline style maps, so the shared table contract stays CSP-safe on both steady-state and alert-highlighted routes. 14. Keep retained-value data loading honest at the ownership boundary. Helpers that prevent a feature surface from falling through the app-level Suspense boundary during in-flight refresh should stay feature-local until multiple diff --git a/frontend-modern/src/components/Alerts/AlertResourceGroupHeader.tsx b/frontend-modern/src/components/Alerts/AlertResourceGroupHeader.tsx index 5fca953bb..c4c7459f9 100644 --- a/frontend-modern/src/components/Alerts/AlertResourceGroupHeader.tsx +++ b/frontend-modern/src/components/Alerts/AlertResourceGroupHeader.tsx @@ -1,5 +1,6 @@ import { Show } from 'solid-js'; +import { GROUPED_TABLE_ROW_BADGE_CLASS } from '@/components/shared/groupedTableRowPresentation'; import type { GroupHeaderMeta } from '@/features/alerts/thresholds/tableTypes'; interface AlertResourceGroupHeaderProps { @@ -13,12 +14,12 @@ export function AlertResourceGroupHeader(props: AlertResourceGroupHeaderProps) { return ( {groupLabel()}} + fallback={{groupLabel()}} >
{groupLabel()}} + fallback={{groupLabel()}} > {(host) => ( e.stopPropagation()} - class="text-sm font-medium text-base-content transition-colors duration-150 hover:text-sky-600 dark:hover:text-sky-400" + class="text-base-content transition-colors duration-150 hover:text-sky-600 dark:hover:text-sky-400" title={`Open ${groupLabel()} web interface`} > {groupLabel()} @@ -34,7 +35,7 @@ export function AlertResourceGroupHeader(props: AlertResourceGroupHeaderProps) { )} - + {props.meta?.clusterName} diff --git a/frontend-modern/src/components/Alerts/AlertResourceTableDesktop.tsx b/frontend-modern/src/components/Alerts/AlertResourceTableDesktop.tsx index 7dae2aba8..de5a50244 100644 --- a/frontend-modern/src/components/Alerts/AlertResourceTableDesktop.tsx +++ b/frontend-modern/src/components/Alerts/AlertResourceTableDesktop.tsx @@ -13,7 +13,10 @@ import { } from '@/components/shared/Table'; import { SectionHeader } from '@/components/shared/SectionHeader'; import { HelpIcon } from '@/components/shared/HelpIcon'; -import { getGroupedTableRowClass } from '@/components/shared/groupedTableRowPresentation'; +import { + getGroupedTableRowCellClass, + getGroupedTableRowClass, +} from '@/components/shared/groupedTableRowPresentation'; import { AlertResourceTableRow } from './AlertResourceTableRow'; import { AlertResourceGroupHeader } from './AlertResourceGroupHeader'; import { @@ -445,7 +448,7 @@ export function AlertResourceTableDesktop(props: AlertResourceTableDesktopProps) {(() => { const label = props.getGroupLabel(groupKey(), fullGroupGuests()); @@ -127,7 +131,7 @@ export function WorkloadPanel(props: WorkloadPanelProps) {
{label.name} - + {label.type} diff --git a/frontend-modern/src/components/Dashboard/__tests__/Dashboard.performance.contract.test.tsx b/frontend-modern/src/components/Dashboard/__tests__/Dashboard.performance.contract.test.tsx index 385fff15e..bdf980468 100644 --- a/frontend-modern/src/components/Dashboard/__tests__/Dashboard.performance.contract.test.tsx +++ b/frontend-modern/src/components/Dashboard/__tests__/Dashboard.performance.contract.test.tsx @@ -775,6 +775,7 @@ describe('Dashboard performance contract', () => { expect(workloadPanelSource).toContain('data-summary-group-id'); expect(workloadPanelSource).toContain('setHoveredWorkloadGroupScope'); expect(workloadPanelSource).toContain('getInteractiveGroupedTableRowClass'); + expect(workloadPanelSource).toContain('getGroupedTableRowCellClass'); expect(workloadPanelSource).not.toContain('cursor-pointer bg-surface-alt'); expect(dashboardWorkloadTableSource).not.toContain( 'createMemo(() => getCanonicalWorkloadId(guest()))', @@ -985,6 +986,7 @@ describe('Dashboard performance contract', () => { expect(workloadPanelSource).toContain('createSummaryInteractiveRowPreviewHandlers'); expect(workloadPanelSource).toContain('resolveSummaryGroupMemberInteractionState'); expect(workloadPanelSource).toContain('getInteractiveGroupedTableRowClass'); + expect(workloadPanelSource).toContain('getGroupedTableRowCellClass'); expect(workloadPanelSource).not.toContain('style={{'); expect(workloadPanelSource).not.toContain('style={'); expect(workloadPanelSource).not.toContain('kind="scope"'); diff --git a/frontend-modern/src/components/Infrastructure/UnifiedResourceHostTableCard.tsx b/frontend-modern/src/components/Infrastructure/UnifiedResourceHostTableCard.tsx index 6c5de4d42..7c34593ab 100644 --- a/frontend-modern/src/components/Infrastructure/UnifiedResourceHostTableCard.tsx +++ b/frontend-modern/src/components/Infrastructure/UnifiedResourceHostTableCard.tsx @@ -3,7 +3,12 @@ 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 { getInteractiveGroupedTableRowClass } from '@/components/shared/groupedTableRowPresentation'; +import { + GROUPED_TABLE_ROW_BADGE_CLASS, + GROUPED_TABLE_ROW_META_CLASS, + getGroupedTableRowCellClass, + getInteractiveGroupedTableRowClass, +} from '@/components/shared/groupedTableRowPresentation'; import { TableCardHeader } from '@/components/shared/TableCardHeader'; import { TableCard } from '@/components/shared/TableCard'; import { @@ -193,7 +198,7 @@ export const UnifiedResourceHostTableCard: Component
{group.cluster} - + Cluster - + {group.resources.length}{' '} {group.resources.length === 1 ? 'resource' : 'resources'} diff --git a/frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.performance.contract.test.tsx b/frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.performance.contract.test.tsx index a8a83fac0..497dc065b 100644 --- a/frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.performance.contract.test.tsx +++ b/frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.performance.contract.test.tsx @@ -516,6 +516,7 @@ describe('UnifiedResourceTable performance contract', () => { expect(unifiedResourceHostTableCardSource).not.toContain('kind="scope"'); expect(unifiedResourceHostTableCardSource).toContain('getInteractiveGroupedTableRowClass'); + expect(unifiedResourceHostTableCardSource).toContain('getGroupedTableRowCellClass'); expect(unifiedResourceHostTableCardSource).not.toContain('cursor-pointer bg-surface-alt'); expect(unifiedResourceHostTableCardSource).toContain('TableCard'); expect(unifiedResourceHostTableCardSource).toContain('TableCardHeader'); diff --git a/frontend-modern/src/components/Settings/InfrastructureSourceManager.tsx b/frontend-modern/src/components/Settings/InfrastructureSourceManager.tsx index 51a8b47f6..1457205d5 100644 --- a/frontend-modern/src/components/Settings/InfrastructureSourceManager.tsx +++ b/frontend-modern/src/components/Settings/InfrastructureSourceManager.tsx @@ -18,7 +18,10 @@ import { TableHeader, TableRow, } from '@/components/shared/Table'; -import { getGroupedTableRowClass } from '@/components/shared/groupedTableRowPresentation'; +import { + getGroupedTableRowCellClass, + getGroupedTableRowClass, +} from '@/components/shared/groupedTableRowPresentation'; import { connectionAgentVersionPresentation, fleetSignalClassName, @@ -619,7 +622,6 @@ export const InfrastructureSourceManager: Component groupedDiscoveredRows().get(product.type) ?? []; const groupRowClass = () => getGroupedTableRowClass('border-b border-border-subtle'); - const groupLabelClass = () => 'text-[15px] font-semibold text-base-content'; return ( <> @@ -627,18 +629,18 @@ export const InfrastructureSourceManager: Component - +
- {product.label} + {product.label}
} > - +
- {product.label} + {product.label}