fix(frontend): emphasize scoped group members in tables

This commit is contained in:
rcourtman
2026-04-02 23:04:49 +01:00
parent 01e4633a5e
commit 67eb19633b
24 changed files with 276 additions and 4 deletions
@@ -169,7 +169,7 @@ work extends shared components instead of creating new local variants.
the shared `SummaryScopeBar.tsx` reset path. Workloads, infrastructure,
and storage must not rebuild row-as-button keyboard handling or trailing
one-off expand columns once the shared action primitive exists.
12. 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.
12. 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.
13. 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
@@ -177,7 +177,11 @@ regression protection.
scope/pinned pill buttons that compete with the summary shell. Workloads,
infrastructure, and storage do not rebuild mouse-only hover branches,
focusable-row toggles, or touch-hostile synthetic hover behavior inside
individual row renderers.
individual row renderers. The same hot path must also carry block-level
group feedback through one shared row-state contract: when a summary group
is previewed or pinned, member rows should take a restrained shared
preview/pinned wash via `data-summary-group-member-active` rather than
per-surface outlines, secondary buttons, or full-strength row fills.
28. Keep summary-card hover emphasis on one bounded rendering budget: when a summary row is active, shared sparkline and density-map primitives must promote the selected series and demote background series through the same active-series ID rather than layering a second page-local highlight pass, so zoom-range and hover scrubbing stay visually coherent without reintroducing multi-series overdraw on the hot summary cards. Density maps on that hot path must stay overview-first under focus: preserve the multi-entity heatmap rows, layer focused-entity detail inside the card, and avoid swapping transient hover into a separate single-series chart path.
## Forbidden Paths
@@ -199,7 +199,10 @@ querying, and the operator-facing storage health presentation layer.
`frontend-modern/src/components/shared/summaryScopePresentation.ts`
contract so touch users and mouse users see the same explicit, reversible
scope state instead of relying on hidden route state or storage-local
badges.
badges. When that scope is a storage pool group, member pool rows should
expose shared `data-summary-group-member-active="preview|pinned"` state so
the grouped block reads as one scoped set without adding storage-local
outlines, pill buttons, or heavy full-row fills.
14. Keep storage summary remount caches versioned with the chart contract.
`frontend-modern/src/components/Storage/StorageSummary.tsx` may keep a
bounded in-memory cache for same-tab remounts, but its cache key must carry
@@ -228,6 +228,10 @@ assembly branch.
`data-summary-row-active` and rely on the shared row presentation owned by
`frontend-modern/src/index.css` instead of provider-specific background
classes that drift across resource tables or hide inline metric bars.
Cluster-member rows must also expose shared preview-versus-pinned group
emphasis through `data-summary-group-member-active`, so the whole cluster
block reads as the active scope without inventing a second infrastructure-
local outline or banner treatment.
Summary-linked infrastructure rows and cluster headers must also route
pointer preview and focus preview through
`frontend-modern/src/components/shared/summaryInteractionA11y.ts`, while
@@ -133,7 +133,9 @@ export function Dashboard(props: DashboardProps) {
handleTagClick={state.handleTagClick}
activeSummaryWorkloadGroupScope={state.activeSummaryWorkloadGroupScope}
activeSummaryWorkloadId={state.activeSummaryWorkloadId}
focusedSummaryWorkloadGroupScope={state.focusedSummaryWorkloadGroupScope}
focusedSummaryWorkloadGroupId={state.focusedSummaryWorkloadGroupId}
hoveredSummaryWorkloadGroupScope={state.hoveredSummaryWorkloadGroupScope}
isMobile={state.isMobile}
mobileVisibleColumnIds={state.mobileVisibleColumnIds}
mobileVisibleColumns={state.mobileVisibleColumns}
@@ -22,7 +22,9 @@ type DashboardWorkloadTableProps = Pick<
| 'handleTagClick'
| 'activeSummaryWorkloadGroupScope'
| 'activeSummaryWorkloadId'
| 'focusedSummaryWorkloadGroupScope'
| 'focusedSummaryWorkloadGroupId'
| 'hoveredSummaryWorkloadGroupScope'
| 'isMobile'
| 'mobileVisibleColumnIds'
| 'mobileVisibleColumns'
@@ -83,7 +85,9 @@ export function DashboardWorkloadTable(props: DashboardWorkloadTableProps) {
handleTagClick={props.handleTagClick}
activeSummaryWorkloadGroupScope={props.activeSummaryWorkloadGroupScope}
activeSummaryWorkloadId={props.activeSummaryWorkloadId}
focusedSummaryWorkloadGroupScope={props.focusedSummaryWorkloadGroupScope}
focusedSummaryWorkloadGroupId={props.focusedSummaryWorkloadGroupId}
hoveredSummaryWorkloadGroupScope={props.hoveredSummaryWorkloadGroupScope}
mobileVisibleColumnIds={props.mobileVisibleColumnIds}
nodeByInstance={props.nodeByInstance}
search={props.search}
@@ -110,6 +110,11 @@ export function GuestRow(props: GuestRowProps) {
style={rowStyle()}
data-guest-id={guestId()}
data-summary-series-id={guestId()}
data-summary-group-member-active={
props.summaryGroupMemberState && props.summaryGroupMemberState !== 'default'
? props.summaryGroupMemberState
: undefined
}
data-summary-row-active={props.isSummaryHighlighted && !props.isExpanded ? 'true' : 'false'}
onClick={props.onClick}
{...interactiveRowHandlers}
@@ -10,7 +10,10 @@ import { TableBody, TableCell, TableRow } from '@/components/shared/Table';
import { getAlertStyles } from '@/utils/alerts';
import { isNodeOnline } from '@/utils/status';
import { getCanonicalWorkloadId } from '@/utils/workloads';
import type { SummarySeriesGroupScope } from '@/components/shared/summaryCardInteraction';
import {
resolveSummaryGroupMemberInteractionState,
type SummarySeriesGroupScope,
} from '@/components/shared/summaryCardInteraction';
import { GuestDrawer } from './GuestDrawer';
import { GuestRow } from './GuestRow';
@@ -32,7 +35,9 @@ type WorkloadPanelProps = Pick<
| 'handleTagClick'
| 'activeSummaryWorkloadGroupScope'
| 'activeSummaryWorkloadId'
| 'focusedSummaryWorkloadGroupScope'
| 'focusedSummaryWorkloadGroupId'
| 'hoveredSummaryWorkloadGroupScope'
| 'mobileVisibleColumnIds'
| 'nodeByInstance'
| 'search'
@@ -182,6 +187,11 @@ export function WorkloadPanel(props: WorkloadPanelProps) {
}
isExpanded={props.selectedGuestId() === guestId()}
isSummaryHighlighted={props.activeSummaryWorkloadId() === guestId()}
summaryGroupMemberState={resolveSummaryGroupMemberInteractionState({
seriesId: guestId(),
hoveredGroupScope: props.hoveredSummaryWorkloadGroupScope(),
focusedGroupScope: props.focusedSummaryWorkloadGroupScope(),
})}
ioEmphasis={props.workloadIOEmphasis()}
onHoverChange={props.setHoveredWorkloadId}
/>
@@ -873,11 +873,14 @@ describe('Dashboard performance contract', () => {
expect(workloadPanelSource).toContain('GuestDrawer');
expect(workloadPanelSource).toContain('createMemo(() => getCanonicalWorkloadId(guest()))');
expect(workloadPanelSource).toContain('createSummaryInteractiveRowPreviewHandlers');
expect(workloadPanelSource).toContain('resolveSummaryGroupMemberInteractionState');
expect(workloadPanelSource).not.toContain('kind="scope"');
expect(workloadPanelSource).not.toContain('leadingAction={');
expect(dashboardSelectionStateSource).toContain('activeSummaryWorkloadGroupScope');
expect(dashboardSelectionStateSource).toContain('focusedSummaryWorkloadGroupScope');
expect(dashboardSelectionStateSource).toContain('setHoveredWorkloadGroupScope');
expect(dashboardWorkloadTableSource).toContain('focusedSummaryWorkloadGroupScope');
expect(dashboardWorkloadTableSource).toContain('hoveredSummaryWorkloadGroupScope');
expect(dashboardSource).toContain('hoveredGroupScope={state.hoveredSummaryWorkloadGroupScope()}');
expect(dashboardSource).toContain('focusedGroupScope={state.focusedSummaryWorkloadGroupScope()}');
expect(workloadPanelSource).not.toContain('TableHead');
@@ -1,4 +1,5 @@
import type { ColumnDef } from '@/hooks/useColumnVisibility';
import type { SummaryGroupMemberInteractionState } from '@/components/shared/summaryCardInteraction';
import type { WorkloadGuest, ViewMode } from '@/types/workloads';
import { createVisibleCanonicalTypeColumn } from '@/utils/typeColumnDefinition';
@@ -42,6 +43,7 @@ export interface GuestRowProps {
onClick?: () => void;
isExpanded?: boolean;
isSummaryHighlighted?: boolean;
summaryGroupMemberState?: SummaryGroupMemberInteractionState;
ioEmphasis?: WorkloadIOEmphasis;
onHoverChange?: (guestId: string | null) => void;
}
@@ -33,6 +33,7 @@ import { buildWorkloadsHref } from './workloadsLink';
import { ClusterDeployBanner } from './ClusterDeployBanner';
import { ResourceFacetSummary } from './ResourceFacetSummary';
import type { SummarySeriesGroupScope } from '@/components/shared/summaryCardInteraction';
import { resolveSummaryGroupMemberInteractionState } from '@/components/shared/summaryCardInteraction';
import {
buildSummaryDisclosureControlsId,
createSummaryInteractiveRowPreviewHandlers,
@@ -221,6 +222,13 @@ export const UnifiedResourceHostTableCard: Component<UnifiedResourceHostTableCar
const displayName = createMemo(() =>
getPreferredInfrastructureDisplayName(resource),
);
const summaryGroupMemberState = createMemo(() =>
resolveSummaryGroupMemberInteractionState({
seriesId: resource.id,
hoveredGroupScope: tableProps.hoveredSummaryGroupScope,
focusedGroupScope: tableProps.focusedSummaryGroupScope,
}),
);
const statusIndicator = createMemo(() =>
getAgentStatusIndicator({ status: resource.status }),
);
@@ -306,6 +314,11 @@ export const UnifiedResourceHostTableCard: Component<UnifiedResourceHostTableCar
<TableRow
data-row-id={resource.id}
data-summary-series-id={resource.id}
data-summary-group-member-active={
summaryGroupMemberState() !== 'default'
? summaryGroupMemberState()
: undefined
}
data-summary-row-active={
(tableProps.hoveredResourceId === resource.id || isHighlighted()) &&
!isExpanded()
@@ -37,6 +37,8 @@ export interface UnifiedResourceTableProps {
revealedResourceId?: string | null;
hoveredResourceId?: string | null;
activeSummaryGroupScope?: SummarySeriesGroupScope | null;
hoveredSummaryGroupScope?: SummarySeriesGroupScope | null;
focusedSummaryGroupScope?: SummarySeriesGroupScope | null;
focusedSummaryGroupId?: string | null;
onExpandedResourceChange: (id: string | null) => void;
onHoverChange?: (id: string | null) => void;
@@ -148,6 +148,8 @@ const Storage: Component = () => {
getRecordAlertState={getRecordAlertState}
isLoadingPools={isLoadingPools}
activeSummaryGroupScope={activeSummaryStorageGroupScope}
hoveredSummaryGroupScope={hoveredSummaryStorageGroupScope}
focusedSummaryGroupScope={focusedSummaryStorageGroupScope}
focusedSummaryGroupId={focusedSummaryStorageGroupId}
onGroupFocusChange={setFocusedStorageGroupScope}
onGroupHoverChange={setHoveredStorageGroupScope}
@@ -30,6 +30,8 @@ type StorageContentCardProps = {
getRecordAlertState: (recordId: string) => StorageAlertRowState;
isLoadingPools: () => boolean;
activeSummaryGroupScope: () => SummarySeriesGroupScope | null;
hoveredSummaryGroupScope: () => SummarySeriesGroupScope | null;
focusedSummaryGroupScope: () => SummarySeriesGroupScope | null;
focusedSummaryGroupId: () => string | null;
onGroupFocusChange: (scope: SummarySeriesGroupScope | null) => void;
onGroupHoverChange: (scope: SummarySeriesGroupScope | null) => void;
@@ -78,6 +80,8 @@ export const StorageContentCard: Component<StorageContentCardProps> = (props) =>
getRecordAlertState={props.getRecordAlertState}
isLoading={props.isLoadingPools()}
activeSummaryGroupScope={props.activeSummaryGroupScope()}
hoveredSummaryGroupScope={props.hoveredSummaryGroupScope()}
focusedSummaryGroupScope={props.focusedSummaryGroupScope()}
focusedSummaryGroupId={props.focusedSummaryGroupId()}
onGroupFocusChange={props.onGroupFocusChange}
onGroupHoverChange={props.onGroupHoverChange}
@@ -29,6 +29,7 @@ import {
STORAGE_POOL_ROW_USAGE_WRAP_CLASS,
} from '@/features/storageBackups/storagePoolRowPresentation';
import type { Resource } from '@/types/resource';
import type { SummaryGroupMemberInteractionState } from '@/components/shared/summaryCardInteraction';
import { EnhancedStorageBar } from './EnhancedStorageBar';
import { StoragePoolDetail } from './StoragePoolDetail';
import {
@@ -42,6 +43,7 @@ interface StoragePoolRowProps {
summarySeriesId: string;
expanded: boolean;
summaryHighlighted?: boolean;
summaryGroupMemberState?: SummaryGroupMemberInteractionState;
onToggleExpand: () => void;
onHoverChange?: (recordId: string | null) => void;
rowClass: string;
@@ -73,6 +75,11 @@ export const StoragePoolRow: Component<StoragePoolRowProps> = (props) => {
onClick={props.onToggleExpand}
{...interactiveRowHandlers}
data-summary-series-id={props.summarySeriesId}
data-summary-group-member-active={
props.summaryGroupMemberState && props.summaryGroupMemberState !== 'default'
? props.summaryGroupMemberState
: undefined
}
data-summary-row-active={props.summaryHighlighted && !props.expanded ? 'true' : 'false'}
{...props.alertDataAttrs}
>
@@ -19,6 +19,7 @@ import { StoragePoolRow } from './StoragePoolRow';
import type { StorageGroupedRecords, StorageGroupKey } from './useStorageModel';
import { useStoragePoolsTableModel } from './useStoragePoolsTableModel';
import type { SummarySeriesGroupScope } from '@/components/shared/summaryCardInteraction';
import { resolveSummaryGroupMemberInteractionState } from '@/components/shared/summaryCardInteraction';
import { buildStorageSummaryGroupScope } from './storageSummaryGroups';
type StoragePoolsTableProps = {
@@ -34,6 +35,8 @@ type StoragePoolsTableProps = {
getRecordAlertState: (recordId: string) => StorageAlertRowState;
isLoading: boolean;
activeSummaryGroupScope?: SummarySeriesGroupScope | null;
hoveredSummaryGroupScope?: SummarySeriesGroupScope | null;
focusedSummaryGroupScope?: SummarySeriesGroupScope | null;
focusedSummaryGroupId?: string | null;
onGroupFocusChange?: (scope: SummarySeriesGroupScope | null) => void;
onGroupHoverChange?: (scope: SummarySeriesGroupScope | null) => void;
@@ -108,6 +111,11 @@ export const StoragePoolsTable: Component<StoragePoolsTableProps> = (props) => {
props.highlightedSummarySeriesId ===
resolveStorageRecordMetricResourceId(record())
}
summaryGroupMemberState={resolveSummaryGroupMemberInteractionState({
seriesId: resolveStorageRecordMetricResourceId(record()),
hoveredGroupScope: props.hoveredSummaryGroupScope,
focusedGroupScope: props.focusedSummaryGroupScope,
})}
onToggleExpand={() => model.togglePool(record().id)}
onHoverChange={props.onHoverChange}
rowClass={rowModel().rowClass}
@@ -782,6 +782,61 @@ describe('Storage', () => {
).not.toBeInTheDocument();
});
it('uses shared preview and pinned group-member emphasis for storage pool rows', async () => {
hookResources = [
buildStorageResource('storage-1', 'Node-Store', 'pve1'),
buildStorageResource('storage-2', 'Edge-Store', 'pve2'),
];
mockLocationSearch = '?group=node';
navigateSpy.mockImplementation((nextPath: string) => {
mockLocationSearch = nextPath.includes('?') ? nextPath.slice(nextPath.indexOf('?')) : '';
});
render(() => <Storage />);
await waitFor(() => {
expect(
document.querySelector('tr[data-summary-group-id="storage:node:pve1"]'),
).toBeTruthy();
});
const groupRow = document.querySelector(
'tr[data-summary-group-id="storage:node:pve1"]',
) as HTMLTableRowElement | null;
expect(groupRow).not.toBeNull();
if (!groupRow) {
return;
}
fireEvent.pointerEnter(groupRow, { pointerType: 'mouse' });
await waitFor(() => {
expect(
document.querySelectorAll('tr[data-summary-group-member-active="preview"]'),
).toHaveLength(1);
});
fireEvent.pointerLeave(groupRow, { pointerType: 'mouse' });
await waitFor(() => {
expect(
document.querySelectorAll('tr[data-summary-group-member-active="preview"]'),
).toHaveLength(0);
});
fireEvent.click(groupRow);
await waitFor(() => {
expect(navigateSpy).toHaveBeenCalledWith(
'/storage?group=node&summaryGroup=storage%3Anode%3Apve1',
ROUTE_STATE_REPLACE_OPTIONS,
);
expect(
document.querySelectorAll('tr[data-summary-group-member-active="pinned"]'),
).toHaveLength(1);
});
});
it('shows ceph summary card and pool expand chevron', async () => {
hookResources = [
buildStorageResource('storage-ceph', 'Ceph-Pool-1', 'pve1', {
@@ -291,6 +291,7 @@ describe('shared primitive guardrails', () => {
expect(summaryCardInteractionSource).toContain('chartHoveredSeriesId');
expect(summaryCardInteractionSource).toContain('SummarySeriesGroupScope');
expect(summaryCardInteractionSource).toContain('resolveSummaryGroupScope');
expect(summaryCardInteractionSource).toContain('resolveSummaryGroupMemberInteractionState');
expect(summaryCardInteractionSource).toContain('resolveSummaryScopeState');
expect(interactiveSparklineModelSource).toContain('onHoverSyncChange');
expect(densityMapModelSource).toContain('onHoverSyncChange');
@@ -384,8 +385,12 @@ describe('shared primitive guardrails', () => {
expect(frontendIndexCssSource).toContain("tr[data-summary-row-active='true'] > td");
expect(frontendIndexCssSource).toContain('--color-summary-row-bg');
expect(frontendIndexCssSource).toContain('--color-summary-row-accent');
expect(frontendIndexCssSource).toContain("tr[data-summary-group-member-active='preview'] > td");
expect(frontendIndexCssSource).toContain("tr[data-summary-group-member-active='pinned'] > td");
expect(frontendIndexCssSource).toContain('--color-summary-group-member-pinned-accent');
expect(guestRowSource).toContain('data-summary-row-active');
expect(guestRowSource).toContain('data-summary-group-member-active');
expect(guestRowStateSource).not.toContain('bg-sky-50/70');
expect(guestRowStateSource).not.toContain('ring-sky-400/25');
@@ -402,6 +407,9 @@ describe('shared primitive guardrails', () => {
expect(source).not.toContain('bg-blue-100 dark:bg-blue-800');
expect(source).not.toContain('ring-blue-300 dark:ring-blue-600');
}
expect(storagePoolRowSource).toContain('data-summary-group-member-active');
expect(unifiedResourceHostTableCardSource).toContain('data-summary-group-member-active');
});
it('keeps summary-linked row input semantics on the shared interaction helper', () => {
@@ -9,6 +9,7 @@ import {
filterSummarySeriesByGroupScope,
resolveSummaryActiveSeriesId,
resolveSummaryCardInteractionState,
resolveSummaryGroupMemberInteractionState,
resolveSummaryScopeState,
type SummarySeriesGroupScope,
} from '@/components/shared/summaryCardInteraction';
@@ -164,6 +165,43 @@ describe('summaryCardInteraction', () => {
});
});
it('resolves group-member emphasis from hovered and pinned group scope', () => {
const hoveredGroupScope: SummarySeriesGroupScope = {
id: 'cluster-a',
label: 'Cluster A (2 workloads)',
seriesIds: ['alpha', 'beta'],
};
const focusedGroupScope: SummarySeriesGroupScope = {
id: 'cluster-b',
label: 'Cluster B (2 workloads)',
seriesIds: ['gamma', 'delta'],
};
expect(
resolveSummaryGroupMemberInteractionState({
seriesId: 'alpha',
hoveredGroupScope,
focusedGroupScope,
}),
).toBe('preview');
expect(
resolveSummaryGroupMemberInteractionState({
seriesId: 'gamma',
hoveredGroupScope,
focusedGroupScope,
}),
).toBe('pinned');
expect(
resolveSummaryGroupMemberInteractionState({
seriesId: 'omega',
hoveredGroupScope,
focusedGroupScope,
}),
).toBe('default');
});
it('builds consistent scope-bar presentation for page, group, and entity states', () => {
const groupScope: SummarySeriesGroupScope = {
id: 'cluster-a',
@@ -1,4 +1,5 @@
export type SummaryCardInteractionState = 'default' | 'active' | 'inactive';
export type SummaryGroupMemberInteractionState = 'default' | 'preview' | 'pinned';
export type SummaryScopeKind = 'page' | 'group' | 'entity';
export type SummaryScopeSource = 'page' | 'preview' | 'pinned';
@@ -68,6 +69,29 @@ export const resolveSummaryGroupScope = (options: {
return normalizeSummarySeriesGroupScope(options.focusedGroupScope);
};
export const resolveSummaryGroupMemberInteractionState = (options: {
seriesId?: string | null;
hoveredGroupScope?: SummarySeriesGroupScope | null;
focusedGroupScope?: SummarySeriesGroupScope | null;
}): SummaryGroupMemberInteractionState => {
const normalizedSeriesId = normalizeSeriesId(options.seriesId);
if (!normalizedSeriesId) {
return 'default';
}
const hoveredGroupScope = normalizeSummarySeriesGroupScope(options.hoveredGroupScope);
if (isSummarySeriesInGroupScope(hoveredGroupScope, normalizedSeriesId)) {
return 'preview';
}
const focusedGroupScope = normalizeSummarySeriesGroupScope(options.focusedGroupScope);
if (isSummarySeriesInGroupScope(focusedGroupScope, normalizedSeriesId)) {
return 'pinned';
}
return 'default';
};
export const resolveSummaryScopeState = (options: {
chartHoveredSeriesId?: string | null;
hoveredSeriesId?: string | null;
@@ -338,6 +338,8 @@ export function InfrastructurePageSurface() {
resources={filteredResources()}
expandedResourceId={expandedResourceId()}
activeSummaryGroupScope={activeSummaryResourceGroupScope()}
hoveredSummaryGroupScope={hoveredSummaryResourceGroupScope()}
focusedSummaryGroupScope={focusedSummaryResourceGroupScope()}
focusedSummaryGroupId={focusedSummaryResourceGroupId()}
hoveredResourceId={activeSummaryResourceId()}
highlightedResourceId={highlightedResourceId()}
@@ -46,6 +46,8 @@ describe('InfrastructurePageSurface guardrails', () => {
expect(infrastructurePageSurfaceSource).toContain('hoveredGroupScope={hoveredSummaryResourceGroupScope()}');
expect(infrastructurePageSurfaceSource).toContain('focusedGroupScope={focusedSummaryResourceGroupScope()}');
expect(infrastructurePageSurfaceSource).toContain('activeSummaryGroupScope={activeSummaryResourceGroupScope()}');
expect(infrastructurePageSurfaceSource).toContain('hoveredSummaryGroupScope={hoveredSummaryResourceGroupScope()}');
expect(infrastructurePageSurfaceSource).toContain('focusedSummaryGroupScope={focusedSummaryResourceGroupScope()}');
expect(infrastructurePageSurfaceSource).toContain('onGroupHoverChange={setHoveredResourceGroupScope}');
expect(infrastructurePageSurfaceSource).toContain('setTableRootRef={setSummaryTableRootRef}');
expect(infrastructurePageSurfaceSource).toContain('SummaryScopeBar');
+35
View File
@@ -62,6 +62,11 @@
--color-summary-row-bg-hover: rgba(14, 165, 233, 0.08);
--color-summary-row-border: rgba(14, 165, 233, 0.16);
--color-summary-row-accent: rgba(14, 165, 233, 0.42);
--color-summary-group-member-preview-bg: rgba(14, 165, 233, 0.025);
--color-summary-group-member-preview-bg-hover: rgba(14, 165, 233, 0.04);
--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);
}
.dark {
@@ -79,6 +84,11 @@
--color-summary-row-bg-hover: rgba(56, 189, 248, 0.12);
--color-summary-row-border: rgba(56, 189, 248, 0.18);
--color-summary-row-accent: rgba(56, 189, 248, 0.5);
--color-summary-group-member-preview-bg: rgba(56, 189, 248, 0.045);
--color-summary-group-member-preview-bg-hover: rgba(56, 189, 248, 0.065);
--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);
}
body {
@@ -163,6 +173,31 @@
@apply border-b border-border hover:bg-surface-hover;
}
tr[data-summary-group-member-active='preview'] > td,
tr[data-summary-group-member-active='preview'] > th {
background-color: var(--color-summary-group-member-preview-bg);
}
tr[data-summary-group-member-active='preview']:hover > td,
tr[data-summary-group-member-active='preview']:hover > th {
background-color: var(--color-summary-group-member-preview-bg-hover);
}
tr[data-summary-group-member-active='pinned'] > td,
tr[data-summary-group-member-active='pinned'] > th {
background-color: var(--color-summary-group-member-pinned-bg);
}
tr[data-summary-group-member-active='pinned']:hover > td,
tr[data-summary-group-member-active='pinned']:hover > th {
background-color: var(--color-summary-group-member-pinned-bg-hover);
}
tr[data-summary-group-member-active='pinned'] > td:first-child,
tr[data-summary-group-member-active='pinned'] > th:first-child {
box-shadow: inset 1px 0 0 var(--color-summary-group-member-pinned-accent);
}
tr[data-summary-row-active='true'] > td,
tr[data-summary-row-active='true'] > th {
background-color: var(--color-summary-row-bg);
@@ -894,6 +894,9 @@ test.describe.serial("Summary hover selection", () => {
await matchedGroupRow.hover();
await expect(matchedGroupRow).toHaveAttribute("data-summary-row-active", "true");
await expect(page.locator('tr[data-summary-group-member-active="preview"]')).toHaveCount(
matchedGroupSeriesCount,
);
await expect
.poll(() => readRenderedSeriesCounts(workloadsSummary))
.toEqual(new Array(4).fill(matchedGroupSeriesCount));
@@ -901,6 +904,7 @@ test.describe.serial("Summary hover selection", () => {
await page.mouse.move(1, 1);
await expect(matchedGroupRow).toHaveAttribute("data-summary-row-active", "false");
await expect(page.locator('tr[data-summary-group-member-active="preview"]')).toHaveCount(0);
await expect
.poll(() => readRenderedSeriesCounts(workloadsSummary))
.toEqual(resolvedBaselineCounts);
@@ -964,6 +968,9 @@ test.describe.serial("Summary hover selection", () => {
await matchedGroupRow.hover();
await expect(matchedGroupRow).toHaveAttribute("data-summary-row-active", "true");
await expect(page.locator('tr[data-summary-group-member-active="preview"]')).toHaveCount(
matchedGroupSeriesCount,
);
await expect
.poll(() => readRenderedSeriesCounts(infrastructureSummary))
.toEqual(new Array(4).fill(matchedGroupSeriesCount));
@@ -973,6 +980,7 @@ test.describe.serial("Summary hover selection", () => {
await page.mouse.move(1, 1);
await expect(matchedGroupRow).toHaveAttribute("data-summary-row-active", "false");
await expect(page.locator('tr[data-summary-group-member-active="preview"]')).toHaveCount(0);
await expect
.poll(() => readRenderedSeriesCounts(infrastructureSummary))
.toEqual(resolvedBaselineCounts);
@@ -1053,14 +1061,21 @@ test.describe.serial("Summary hover selection", () => {
await matchedGroupRow.hover();
await expect(scopeBar).toContainText("Preview");
await expect(page.locator('tr[data-summary-group-member-active="preview"]')).toHaveCount(
matchedGroupSeriesCount,
);
await page.mouse.move(1, 1);
await expect(scopeBar).toContainText("All");
await expect(page.locator('tr[data-summary-group-member-active="preview"]')).toHaveCount(0);
await matchedGroupRow.click();
await expect
.poll(() => new URL(page.url()).searchParams.get("summaryGroup"))
.toBe(matchedGroupId);
await expect(scopeBar).toContainText("Pinned");
await expect(page.locator('tr[data-summary-group-member-active="preview"]')).toHaveCount(
matchedGroupSeriesCount,
);
await expect(
scopeBar.getByRole("button", { name: "Reset pinned scope" }),
).toBeVisible();
@@ -1072,6 +1087,10 @@ test.describe.serial("Summary hover selection", () => {
);
await page.mouse.move(1, 1);
await expect(page.locator('tr[data-summary-group-member-active="preview"]')).toHaveCount(0);
await expect(page.locator('tr[data-summary-group-member-active="pinned"]')).toHaveCount(
matchedGroupSeriesCount,
);
await expect
.poll(() => readRenderedSeriesCounts(summary))
.toEqual(new Array(4).fill(matchedGroupSeriesCount));
@@ -1081,6 +1100,7 @@ test.describe.serial("Summary hover selection", () => {
.poll(() => new URL(page.url()).searchParams.get("summaryGroup"))
.toBeNull();
await expect(scopeBar).toContainText("All");
await expect(page.locator('tr[data-summary-group-member-active="pinned"]')).toHaveCount(0);
await page.mouse.move(1, 1);
await expect
.poll(() => readRenderedSeriesCounts(summary))
@@ -1119,6 +1139,7 @@ test.describe.serial("Summary hover selection", () => {
let matchedGroupRow: import("@playwright/test").Locator | null = null;
let matchedGroupId = "";
let matchedGroupSeriesCount = 0;
for (let index = 0; index < groupRowCount; index += 1) {
const row = groupRows.nth(index);
if (!(await row.isVisible())) {
@@ -1135,25 +1156,34 @@ test.describe.serial("Summary hover selection", () => {
}
matchedGroupRow = row;
matchedGroupId = groupId;
matchedGroupSeriesCount = seriesCount;
break;
}
expect(matchedGroupRow).not.toBeNull();
expect(matchedGroupId).not.toBe("");
expect(matchedGroupSeriesCount).toBeGreaterThan(0);
if (!matchedGroupRow) {
return;
}
await matchedGroupRow.hover();
await expect(storageScopeBar).toContainText("Preview");
await expect(page.locator('tr[data-summary-group-member-active="preview"]')).toHaveCount(
matchedGroupSeriesCount,
);
await page.mouse.move(1, 1);
await expect(storageScopeBar).toContainText("All");
await expect(page.locator('tr[data-summary-group-member-active="preview"]')).toHaveCount(0);
await matchedGroupRow.click();
await expect
.poll(() => new URL(page.url()).searchParams.get("summaryGroup"))
.toBe(matchedGroupId);
await expect(storageScopeBar).toContainText("Pinned");
await expect(page.locator('tr[data-summary-group-member-active="preview"]')).toHaveCount(
matchedGroupSeriesCount,
);
await expect(page.locator("tr[data-row-id]")).toHaveCount(baselineVisibleRows);
await expect
@@ -1161,11 +1191,16 @@ test.describe.serial("Summary hover selection", () => {
.not.toEqual(baselineCounts);
await page.mouse.move(1, 1);
await expect(page.locator('tr[data-summary-group-member-active="preview"]')).toHaveCount(0);
await expect(page.locator('tr[data-summary-group-member-active="pinned"]')).toHaveCount(
matchedGroupSeriesCount,
);
await expect(page.locator("tr[data-row-id]")).toHaveCount(baselineVisibleRows);
await storageScopeBar
.getByRole("button", { name: "Reset pinned scope" })
.click();
await expect(page.locator('tr[data-summary-group-member-active="pinned"]')).toHaveCount(0);
await expect
.poll(() => new URL(page.url()).searchParams.get("summaryGroup"))
.toBeNull();