mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 02:55:51 +00:00
fix(frontend): emphasize scoped group members in tables
This commit is contained in:
@@ -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}
|
||||
/>
|
||||
|
||||
+3
@@ -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()}
|
||||
|
||||
+2
@@ -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');
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user