diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 4d145efeb0..9cef461a0a 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -3034,7 +3034,7 @@ Agent`), with the plain-language source phrase available through accessible than receiving an unfiltered copy of the estate. Opaque unified-resource ids are not part of that visible search vocabulary and must not retain a node when the normalized guest table has no corresponding match. -8. Keep summary interaction identity on one shared helper. Pages that expose row-hover, group-hover, or route-focus-driven summary emphasis, today the Workloads and Storage tables through `useSummaryPageInteractionState` in `frontend-modern/src/components/shared/summaryTableFocus.ts`, must derive page/group/entity scope through `frontend-modern/src/components/shared/summaryCardInteraction.ts` rather than letting each surface read `hovered || focused` from its own page-local ID source. Hovering or pinning a storage pool-group header, or a workload group header on a surface that enables inline group drawers (the Proxmox and VMware surfaces pass `groupNodeDrawerMode="disabled"` today), must scope the matching rows through that same shared contract instead of forking a page-local summary filter path. No summary card or chart strip renders today, and the bridge carries no chart-hover input; one that returns must consume that same resolved scope, and the chart under the pointer must enter its hovered series into `resolveSummaryScopeState` as a preview input that outranks row hover instead of keeping a chart-local hover island. Recovery is explicitly outside this interaction dialect: its retired posture-card strip must not return with row/group/chart hover behavior without a separate governed product decision. +8. Keep summary interaction identity on one shared helper. Pages that expose row-hover, group-hover, or route-focus-driven summary emphasis, today the Workloads and Storage tables through `useSummaryPageInteractionState` in `frontend-modern/src/components/shared/summaryTableFocus.ts`, must derive page/group/entity scope through `frontend-modern/src/components/shared/summaryCardInteraction.ts` rather than letting each surface read `hovered || focused` from its own page-local ID source. Hovering or pinning a storage pool-group header must scope the matching rows through that same shared contract instead of forking a page-local summary filter path. Workloads grouped host rows are static identity dividers and pass no group scope to the bridge; a grouped table that wants group hover or pin again needs a demand-ledger entry and must use this shared scope rather than reviving a Workloads-local path. No summary card or chart strip renders today, and the bridge carries no chart-hover input; one that returns must consume that same resolved scope, and the chart under the pointer must enter its hovered series into `resolveSummaryScopeState` as a preview input that outranks row hover instead of keeping a chart-local hover island. Recovery is explicitly outside this interaction dialect: its retired posture-card strip must not return with row/group/chart hover behavior without a separate governed product decision. 9. Keep any page summary page-scoped when table rows enter contextual focus. No infrastructure, workloads, or storage summary cards render today. If one returns, route-backed row selection may add a focused label and shared series emphasis, but the summary must continue to render the page-level series set instead of collapsing down to the selected row or replacing the global trend view with row-local empty states. 10. Keep contextual row focus on the shared primitives. Same-route table drill-ins must reuse `frontend-modern/src/components/shared/contextualFocus.ts` for local scroll preservation, off-screen reveal, inline-detail lookup, and deliberate inline-detail reveal, and must resolve the active series through `frontend-modern/src/components/shared/summaryCardInteraction.ts` behind `frontend-modern/src/components/shared/summaryTableFocus.ts`, instead of rebuilding page-local `Set` filters, focused-label scans, drawer-aware scroll math, or ad hoc scroll restoration in each surface. `frontend-modern/src/components/shared/summaryTableFocus.ts` measures and scrolls rows only for deliberate focus, a focused row's inline detail or a pinned group header, so it must not keep window-level scroll or resize listeners or re-measure the hovered row on hover changes and scroll frames; an off-screen affordance for the active row needs a rendered consumer before that cost returns. 11. 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. Inline table detail rows on platform, workload, and infrastructure tables must compose `frontend-modern/src/components/shared/InlineDetailTableRow.tsx` for the full-width row, surface-alt cell, detail padding, and row-click containment instead of rebuilding page-local `TableRow` / `TableCell` / `div` shells around each drawer. 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. diff --git a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md index 669568ae86..bd8c8d6b60 100644 --- a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md +++ b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md @@ -1092,11 +1092,13 @@ change may globally weaken the Task 03 lifecycle-state idempotency invariant. mounted, report errors out of band, and must not rely on Solid `createResource` in a way that can bubble into the route-level Suspense fallback or the setup/welcome shell. - Workload row drawer expansion, local row focus, and summary group pinning - are local interaction state. Inbound Workloads deep links may hydrate - `resource` and `summaryGroup` into the focused row or group, but opening, - closing, or clearing an inline drawer must not write those params back to - route state, schedule router navigation, or trip the app-shell fallback. + Workload row drawer expansion and local row focus are local interaction + state. Inbound Workloads deep links may hydrate `resource` into the focused + row, but opening, closing, or clearing an inline drawer must not write that + param back to route state, schedule router navigation, or trip the + app-shell fallback. Workloads has no group focus, so it reads no + `summaryGroup` param: row expansion stopped writing it and no link builder + passes one, so its hydration was removed with the group pin. URL synchronization for Workloads stays limited to filter-owned scope in `frontend-modern/src/components/Workloads/useWorkloadUrlSync.ts`. The Workloads page may show a bounded partial-inventory banner when a @@ -1473,13 +1475,16 @@ change may globally weaken the Task 03 lifecycle-state idempotency invariant. `TableCardHeader` inside both the populated and filtered-empty table card. A platform page must not place a parallel floating title above its filters; the title/count band belongs to the same framed table surface as the rows. - `WorkloadPanel` owns the mutually exclusive host/guest drawer handoff: - clicking a grouped host row while a guest drawer is open must clear the - selected guest and keep/focus the host summary group so the node drawer can - replace the guest drawer. Platform pages that render a dedicated host - table above an embedded Workloads table may disable the grouped host drawer - through the explicit Workloads surface option, but standalone Workloads - must keep the inline grouped-row drawer as the default. + `WorkloadPanel` renders only the guest drawer. Grouped host rows are + static dividers: no click handler, hover preview, group pin, or inline + node drawer. They keep `data-summary-group-id` so a click on a divider + does not clear an open guest drawer. Host details open from the platform + page's own hosts table (`ProxmoxNodesTable` mounts `NodeDrawer`; + `VsphereHostsTable` owns its detail row). The former + `groupNodeDrawerMode` option defaulted to an inline grouped-row drawer + that only the retired standalone Workloads page used; both production + mounts passed `disabled`, so the option, the inline `NodeDrawer` mount + and the Workloads group hover/pin scope were removed. Compact icon headers inside `WorkloadTableHeader.tsx` may stay visually dense for responsive workload tables, but the icon must be decorative and the column label must remain present through an `sr-only` label so the @@ -2003,7 +2008,10 @@ because both remaining production mounts (Proxmox and vSphere overview) set it unconditionally, so the non-compact path no longer rendered anywhere. A new embedded consumer that needs host stats beside its workloads must render its own hosts table rather than -re-growing metric cells in the shared group row. +re-growing metric cells in the shared group row. The same applies to host +interaction: the group row has no drawer, hover preview, or pin, and the +Workloads state no longer builds per-group summary scopes for every +filtered-guest change. WorkloadsSurface stays monitoring-first. It must not render a persistent aggregate banner just because running VMs or system containers lack an @@ -2066,7 +2074,7 @@ the hovered timestamp, so density does not remove point-in-time metric detail. Proxmox node thermals follow that same drawer-only rule: temperature remains a node-context metric, not a universal Workloads table column, and the node detail drawer may add one compact `Thermals` history group beside utilization and I/O -only after a grouped node row is selected. Host-agent CPU temperature must be +only after a node's drawer is opened from the Proxmox nodes table. Host-agent CPU temperature must be persisted into the same metrics-store history stream as other agent metrics, with current node temperature displayed only as a drawer-local current reading while history is still accumulating; it must not become a synthetic history @@ -2106,7 +2114,7 @@ still reads nothing extra. The Proxmox node drawer overview should follow the existing guest drawer compact detail-section pattern and expose node-specific context such as platform, kernel, hardware, raw capacity, telemetry, and thermal facts rather than -repeating the metric cells already visible in the grouped table row. +repeating the metric cells already visible in the Proxmox nodes table row. Object drawers across workload, node, Docker, and unified-resource surfaces must keep their default render bounded to active attention plus a small additive context projection. Full provider-specific support workflows remain @@ -2415,16 +2423,19 @@ nothing renders: the retired `Jump to row` visibility check did exactly that, costing 128 `getComputedStyle` calls across eight row hovers and 320 across ten scroll steps on the 12-node mock storage table, and none once removed. That same hot-path contract also owns the row-emphasis paint. Workload guest -and group rows and storage pool, group, and disk rows must expose +rows and storage pool, group, and disk rows must expose summary-linked activity through the shared `data-summary-row-active` marker and let the shared frontend primitive render the emphasis, instead of layering lane-local row-fill classes that diverge across pages or wash out inline metric bars. -`frontend-modern/src/components/Workloads/useWorkloadSelectionState.ts` must -write workload selection back into the workloads route through the shared -same-path route-state scheduler, but the actual shell-position handoff for -query-only row focus must go through `frontend-modern/src/utils/appShellScrollRestoration.ts` -plus the root `frontend-modern/src/App.tsx` shell so opening a focused -workload does not look like a full page reload. The shared same-path scheduler must also own cleanup +`frontend-modern/src/components/Workloads/useWorkloadSelectionState.ts` keeps +workload selection local and never writes it back into the route; an inbound +`resource` link only hydrates it. Local row focus keeps the scroll position +through `preserveScrollableAncestorVerticalOffset` in +`frontend-modern/src/components/shared/contextualFocus.ts` and reveals the +opened detail through `summaryTableFocus.ts`, so opening a focused workload +does not look like a full page reload. The root `frontend-modern/src/App.tsx` +shell applies a pending `frontend-modern/src/utils/appShellScrollRestoration.ts` +restore only when the route itself changes. The shared same-path scheduler must also own cleanup for every deferred scroll-restore timeout and animation frame it creates, so route-state cleanup cannot leave hot-path replay work running after the owning surface unmounts. @@ -2739,11 +2750,12 @@ can still lose the aggregate to a live agent while its own filesystem rows remain; such a guest stays on the Proxmox rule. The agent's OS, addresses and physical disk health carry no stale cue yet. Guest, node, and Docker-host drawer headers follow the same frontend-primitives dependency -boundary for collapse: Workloads owns which inline row is selected and the -close handler, while +boundary for collapse: the owning table (Workloads for guests, the Proxmox +nodes table for nodes, the Docker hosts table for Docker hosts) owns which +inline row is selected and the close handler, while `frontend-modern/src/components/shared/ObjectDrawerHeader.tsx` owns the full-width semantic header button, phone target height, focus treatment, and -collapse chevron. `WorkloadPanel` must pass the grouped node close handler into +collapse chevron. `ProxmoxNodesTable` must pass its node close handler into `NodeDrawer`, the Docker hosts table must pass its close handler into `DockerHostDrawer`, and those shells must not fall back to a small icon-only collapse target or wrap interactive header actions inside a local button. diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md index 1067ba5fd9..222886010a 100644 --- a/docs/release-control/v6/internal/subsystems/storage-recovery.md +++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md @@ -980,10 +980,11 @@ states. This heading alignment changes neither workload backup scope nor the storage/recovery evidence carried by the adjacent Backup column and Backups tab. The guest table's grouped node rows are identity-only dividers. Node stats and -the node drawer stay with the Proxmox nodes table above it, so `ProxmoxPageSurface` -passes `groupNodeDrawerMode: 'disabled'` and no host-metric option to the -embedded Workloads state. Those dividers carry no backup or recovery evidence; -the per-guest Backup column remains the overview's protection signal. +the node drawer stay with the Proxmox nodes table above it: the embedded +Workloads table has no grouped-row drawer, hover preview, or group pin, and +`ProxmoxPageSurface` passes no host-metric option to the embedded Workloads +state. Those dividers carry no backup or recovery evidence; the per-guest +Backup column remains the overview's protection signal. The overview's node-row action selects the shared workload node scope and reveals the guest section. The title names the selected node and reports the filtered guest count, including zero. Activating the selected node again or diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md index abf7035f3a..a289178f34 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -3060,10 +3060,11 @@ canonical WebSocket snapshot, including the public `vmware-vsphere` alias for the raw `vmware` source, rather than degrading a source-scoped page to periodic REST refreshes. VMware Overview passes that same source-scoped snapshot into the embedded Workloads state, so hosts and VMs share one inventory generation -and one explicit refresh path. Beyond disabling the grouped host drawer, it -passes no option that adds host metrics to grouped rows: the shared Workloads -group row carries host identity only, and per-host stats stay in the page's -own hosts table. +and one explicit refresh path. It passes no option that adds host metrics to +grouped rows: the shared Workloads group row carries host identity only, with +no drawer, hover preview, or group pin, and per-host stats and details stay in +the page's own hosts table. The Workloads route model no longer parses a +`summaryGroup` param, because no group focus remains to hydrate. Its page-owned workload toolbar consumes the complete shared `getWorkloadsMetricFilterProps` binding, so vSphere VMs expose the same Bars, Trends, Details, History, range, and first-use discovery contract as every diff --git a/frontend-modern/browser-verification/db4ee39cb5db006c.json b/frontend-modern/browser-verification/db4ee39cb5db006c.json new file mode 100644 index 0000000000..e5361be148 --- /dev/null +++ b/frontend-modern/browser-verification/db4ee39cb5db006c.json @@ -0,0 +1,71 @@ +{ + "version": 1, + "base_sha": "1a094646d7f21217eda051af6ba0252e208f283e", + "verified_at": "2026-10-07T22:40:00Z", + "result": "passed", + "changed_paths": [ + "frontend-modern/src/components/Workloads/GuestRow.tsx", + "frontend-modern/src/components/Workloads/WorkloadPanel.tsx", + "frontend-modern/src/components/Workloads/WorkloadsSurface.tsx", + "frontend-modern/src/components/Workloads/WorkloadsTable.tsx", + "frontend-modern/src/components/Workloads/guestRowModel.tsx", + "frontend-modern/src/components/Workloads/useWorkloadSelectionState.ts", + "frontend-modern/src/components/Workloads/useWorkloadsState.ts", + "frontend-modern/src/components/Workloads/workloadSelectionModel.ts", + "frontend-modern/src/components/Workloads/workloadSelectors.ts", + "frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx", + "frontend-modern/src/features/vmware/VmwarePageSurface.tsx", + "frontend-modern/src/routing/resourceLinks.ts" + ], + "content_sha256": { + "frontend-modern/src/components/Workloads/GuestRow.tsx": "b345738d55505aeffb16b20f104f8010008e9fcc73fafe084c7e75dbc8138e1a", + "frontend-modern/src/components/Workloads/WorkloadPanel.tsx": "a8b2e107f2143d01f3cb038d29526a9d38d35dcc6df5cdac076c02aa143b3e5f", + "frontend-modern/src/components/Workloads/WorkloadsSurface.tsx": "e48d1be3998e7e3ece47d9eb01673872e74f0f46e0cc242142898af3f2eea80d", + "frontend-modern/src/components/Workloads/WorkloadsTable.tsx": "2742be27628c947aac825ca70af4f19b7518931d3629e6bf3b9068cc98eae19e", + "frontend-modern/src/components/Workloads/guestRowModel.tsx": "e7bd98fc4a091ee66595b59d2407b7ea9cca9e52f1df2b7527ff41cfdecc99b9", + "frontend-modern/src/components/Workloads/useWorkloadSelectionState.ts": "2c99c812cb19b6c31b376ec5db56c3ec16403d8556fbe5f9028fd9018e310915", + "frontend-modern/src/components/Workloads/useWorkloadsState.ts": "5ac8410704ad9fbcf8237291432a30d16a31527b38fd34f0179eb57b90b92ca4", + "frontend-modern/src/components/Workloads/workloadSelectionModel.ts": "82b8f740e51630aba916d7ae617cd57213f4c910d17e843ea378384b63f9ffe9", + "frontend-modern/src/components/Workloads/workloadSelectors.ts": "3b39341dc6e068db4bc84a5745f6a8508454b0019a84c7abfd8c6ee826d8ed93", + "frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx": "f2ec48c3f9932d4e17410cda69c2cc4452c8d11fe3d6d120940cbe8ced40c6bd", + "frontend-modern/src/features/vmware/VmwarePageSurface.tsx": "561645e9b339738484a53a0c044721d69831ce6812d721d826233443efb0f0e1", + "frontend-modern/src/routing/resourceLinks.ts": "7b3a287fb80c7aa66aea6a40a3433662f99460888e734950ea3b455a04f3d53a" + }, + "routes": [ + "/proxmox/overview", + "/proxmox/overview?resource=Analytics%3Apve31%3A675", + "/proxmox/overview?summaryGroup=Analytics-pve31", + "/vmware/overview", + "/vmware/overview?summaryGroup=etl-batch-01-esxi-01.lab.local" + ], + "viewports": [ + { + "width": 1440, + "height": 900 + }, + { + "width": 390, + "height": 844 + } + ], + "states": [ + "Embedded build of this exact tree (fingerprinted runtime files checked by sha256 on the test host) on a mock DEMO_MODE stack on pulse-dev; baseline is an embedded build of origin/main 36d3f8c557 run with the same probe script (group interaction code is unchanged on main since)", + "Proxmox overview grouped by node: 8 group dividers, static (no aria-expanded, cursor auto, no inline node drawer in the guests table), same as baseline", + "vSphere overview grouped by host: 18 group dividers in the current mock estate, static, same as baseline", + "Group-row attributes this change removes are absent: data-summary-group-series-count (baseline 18) and data-summary-row-active (baseline \"false\"); data-summary-group-id kept", + "Hand-typed ?summaryGroup=Analytics-pve31 on Proxmox: baseline pinned 18 member rows and highlighted the group row; now 0 rows pinned and no group highlight. vSphere ?summaryGroup= pinned 0 rows on both", + "Proxmox nodes table NodeDrawer still opens from its own row toggle (aria-expanded true, one inline node detail row)", + "?resource= deep link opens the matching guest drawer", + "Phone 390x844 Proxmox: tapping a group divider pins nothing and opens no node drawer; tapping a guest opens its drawer", + "No page errors at either width" + ], + "interactions": [ + "Hover the first group divider (right half, away from the node link): 0 preview member rows", + "Click the first group divider: 0 pinned rows, 0 inline node drawers", + "Open the first guest drawer, then click a group divider: drawer stays open 5/5 on Proxmox and 5/5 on vSphere (baseline the same); Escape closes it", + "Hover a guest row: exactly one guest row carries data-summary-row-active=true", + "Toggle the first Proxmox nodes table row disclosure: NodeDrawer opens, then closes", + "Load the ?summaryGroup= and ?resource= URLs above and count pinned rows and open drawers", + "Phone width: tap a group divider, then tap a guest row" + ] +} diff --git a/frontend-modern/src/components/Workloads/GuestRow.tsx b/frontend-modern/src/components/Workloads/GuestRow.tsx index fbdfce3e53..633d0eea25 100644 --- a/frontend-modern/src/components/Workloads/GuestRow.tsx +++ b/frontend-modern/src/components/Workloads/GuestRow.tsx @@ -338,11 +338,6 @@ export function GuestRow(props: GuestRowProps) { } data-workload-alert-accent={alertAccentTone()} 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'} on:click={rowActionProps.onClick ? nativeRowClickTarget : undefined} on:mousedown={rowActionProps.onClick ? selectionGuard.onMouseDown : undefined} diff --git a/frontend-modern/src/components/Workloads/WorkloadPanel.tsx b/frontend-modern/src/components/Workloads/WorkloadPanel.tsx index de8a3cab1d..75ae46bb24 100644 --- a/frontend-modern/src/components/Workloads/WorkloadPanel.tsx +++ b/frontend-modern/src/components/Workloads/WorkloadPanel.tsx @@ -5,26 +5,18 @@ import { GROUPED_TABLE_ROW_BADGE_CLASS, getGroupedTableRowCellClass, getGroupedTableRowClass, - getInteractiveGroupedTableRowClass, } from '@/components/shared/groupedTableRowPresentation'; import { InlineDetailTableRow } from '@/components/shared/InlineDetailTableRow'; import { NodeGroupHeader } from '@/components/shared/NodeGroupHeader'; -import { createSummaryInteractiveRowPreviewHandlers } from '@/components/shared/summaryInteractionA11y'; import { buildSummaryDisclosureControlsId } from '@/components/shared/summaryInteractionA11y'; import { TableBody, TableCell, TableRow } from '@/components/shared/Table'; import { getAlertsForResource, getAlertStyles } from '@/utils/alerts'; import { guestOverrideIdCandidates } from '@/features/alerts/guestOverrideIdentity'; import { isNodeOnline } from '@/utils/status'; import { getCanonicalWorkloadId, getWorkloadMetadataIdCandidates } from '@/utils/workloads'; -import { - resolveSummaryGroupMemberInteractionState, - type SummarySeriesGroupScope, -} from '@/components/shared/summaryCardInteraction'; import { GuestDrawer } from './GuestDrawer'; import { GuestRow } from './GuestRow'; -import { NodeDrawer } from './NodeDrawer'; -import { buildWorkloadSummaryGroupScope } from './workloadSelectors'; import { getWorkloadGuestMetadataRecord } from './workloadGuestMetadataRecord'; import type { WorkloadsState } from './useWorkloadsState'; @@ -34,27 +26,19 @@ type WorkloadPanelProps = Pick< | 'alertsEnabled' | 'bottomSpacerHeight' | 'getGroupLabel' - | 'getNodeTemperatureThresholds' | 'groupedGuests' | 'groupedWindowing' | 'groupLabelBadges' | 'guestMetadata' | 'guestParentNodeMap' - | 'groupNodeDrawerMode' | 'groupingMode' | 'handleCustomUrlUpdate' | 'handleTagClick' - | 'activeSummaryWorkloadGroupScope' | 'activeSummaryWorkloadId' - | 'focusedSummaryWorkloadGroupScope' - | 'focusedSummaryWorkloadGroupId' - | 'hoveredSummaryWorkloadGroupScope' | 'nestedWorkloadContextByGuestId' | 'nodeByInstance' | 'search' | 'selectedGuestId' - | 'setFocusedWorkloadGroupScope' - | 'setHoveredWorkloadGroupScope' | 'setHoveredWorkloadId' | 'setSelectedGuestId' | 'setTableBodyRef' @@ -100,52 +84,12 @@ export function WorkloadPanel(props: WorkloadPanelProps) { ); const groupGuestIds = createMemo(() => groupGuests().map(getCanonicalWorkloadId)); const node = () => props.nodeByInstance()[groupKey]; - const groupSummaryScope = createMemo(() => { - if (props.groupingMode() !== 'grouped') { - return null; - } - return buildWorkloadSummaryGroupScope( - groupKey, - fullGroupGuests(), - props.getGroupLabel(groupKey, fullGroupGuests()), - ); - }); - const isSummaryGroupHighlighted = createMemo( - () => props.activeSummaryWorkloadGroupScope()?.id === groupKey, - ); - const shouldShowNodeDrawer = createMemo( - () => - props.groupNodeDrawerMode() === 'inline' && - Boolean(node()) && - props.focusedSummaryWorkloadGroupId() === groupKey && - props.selectedGuestId() === null, - ); - const canOpenNodeDrawer = () => props.groupNodeDrawerMode() === 'inline'; - const handleGroupHoverChange = (next: SummarySeriesGroupScope | null) => { - props.setHoveredWorkloadGroupScope(next); - }; - const handleGroupFocusToggle = () => { - if (!canOpenNodeDrawer()) return; - const scope = groupSummaryScope(); - const selectedGuestId = props.selectedGuestId(); - const nextFocusedScope = - scope && - props.focusedSummaryWorkloadGroupId() === scope.id && - selectedGuestId === null - ? null - : scope; - if (nextFocusedScope && selectedGuestId !== null) { - props.setSelectedGuestId(null); - } - props.setFocusedWorkloadGroupScope(nextFocusedScope); - }; - const groupRowInteraction = createSummaryInteractiveRowPreviewHandlers({ - onPreview: () => handleGroupHoverChange(groupSummaryScope()), - onPreviewClear: () => handleGroupHoverChange(null), - }); return ( <> + {/* Group rows are identity-only dividers: host details open from the + platform page's own hosts table. data-summary-group-id keeps a click + on a divider from clearing an open guest drawer. */} + td>div]:flex-nowrap' - : 'select-none duration-150 [&>td>div]:flex-nowrap' - } - trProps={{ - 'aria-expanded': canOpenNodeDrawer() - ? shouldShowNodeDrawer() - ? 'true' - : 'false' - : undefined, - 'data-summary-group-id': groupKey, - 'data-summary-group-series-count': String( - groupSummaryScope()?.seriesIds.length ?? 0, - ), - 'data-summary-row-active': isSummaryGroupHighlighted() ? 'true' : 'false', - onClick: canOpenNodeDrawer() ? handleGroupFocusToggle : undefined, - ...(canOpenNodeDrawer() ? groupRowInteraction : {}), - }} + trClass="select-none duration-150 [&>td>div]:flex-nowrap" + trProps={{ 'data-summary-group-id': groupKey }} /> - - - props.setFocusedWorkloadGroupScope(null)} - temperatureThresholds={props.getNodeTemperatureThresholds(node()!)} - alerts={getAlertsForResource( - [node()!.id], - props.activeAlerts, - props.alertsEnabled(), - node()!.name, - )} - /> - - }> {(keyedGuestId) => { @@ -284,11 +180,6 @@ 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()} metricDisplayMode={props.workloadMetricDisplayMode()} metricHoverMode={props.workloadMetricHoverMode()} diff --git a/frontend-modern/src/components/Workloads/WorkloadsSurface.tsx b/frontend-modern/src/components/Workloads/WorkloadsSurface.tsx index fe11021d39..945bbde267 100644 --- a/frontend-modern/src/components/Workloads/WorkloadsSurface.tsx +++ b/frontend-modern/src/components/Workloads/WorkloadsSurface.tsx @@ -173,9 +173,7 @@ export function WorkloadsSurface(props: WorkloadsSurfaceComponentProps) { props.memoryDisplayBasis ? state.workloadMemoryDisplayBasis : undefined } forcedPlatform={props.forcedPlatform} - pinnedSelectionActive={() => - Boolean(state.selectedGuestId() || state.focusedSummaryWorkloadGroupId()) - } + pinnedSelectionActive={() => Boolean(state.selectedGuestId())} onClearPinnedSelection={state.clearPinnedSummaryScope} /> @@ -193,29 +191,21 @@ export function WorkloadsSurface(props: WorkloadsSurfaceComponentProps) { alertsEnabled={state.alertsEnabled} bottomSpacerHeight={state.bottomSpacerHeight} getGroupLabel={state.getGroupLabel} - getNodeTemperatureThresholds={state.getNodeTemperatureThresholds} groupedGuests={state.groupedGuests} groupedWindowing={state.groupedWindowing} groupLabelBadges={state.groupLabelBadges} guestMetadata={state.guestMetadata} guestParentNodeMap={state.guestParentNodeMap} - groupNodeDrawerMode={state.groupNodeDrawerMode} groupingMode={state.groupingMode} handleCustomUrlUpdate={state.handleCustomUrlUpdate} handleSort={state.handleSort} handleTagClick={state.handleTagClick} - activeSummaryWorkloadGroupScope={state.activeSummaryWorkloadGroupScope} activeSummaryWorkloadId={state.activeSummaryWorkloadId} - focusedSummaryWorkloadGroupScope={state.focusedSummaryWorkloadGroupScope} - focusedSummaryWorkloadGroupId={state.focusedSummaryWorkloadGroupId} - hoveredSummaryWorkloadGroupScope={state.hoveredSummaryWorkloadGroupScope} isMobile={state.isMobile} nestedWorkloadContextByGuestId={state.nestedWorkloadContextByGuestId} nodeByInstance={state.nodeByInstance} search={state.search} selectedGuestId={state.selectedGuestId} - setFocusedWorkloadGroupScope={state.setFocusedWorkloadGroupScope} - setHoveredWorkloadGroupScope={state.setHoveredWorkloadGroupScope} setHoveredWorkloadId={state.setHoveredWorkloadId} setSelectedGuestId={state.setSelectedGuestId} setTableRootRef={state.setTableRootRef} diff --git a/frontend-modern/src/components/Workloads/WorkloadsTable.tsx b/frontend-modern/src/components/Workloads/WorkloadsTable.tsx index 5193c87d1c..82c4bb13cb 100644 --- a/frontend-modern/src/components/Workloads/WorkloadsTable.tsx +++ b/frontend-modern/src/components/Workloads/WorkloadsTable.tsx @@ -18,29 +18,21 @@ type WorkloadsTableProps = Pick< | 'alertsEnabled' | 'bottomSpacerHeight' | 'getGroupLabel' - | 'getNodeTemperatureThresholds' | 'groupedGuests' | 'groupedWindowing' | 'groupLabelBadges' | 'guestMetadata' | 'guestParentNodeMap' - | 'groupNodeDrawerMode' | 'groupingMode' | 'handleCustomUrlUpdate' | 'handleSort' | 'handleTagClick' - | 'activeSummaryWorkloadGroupScope' | 'activeSummaryWorkloadId' - | 'focusedSummaryWorkloadGroupScope' - | 'focusedSummaryWorkloadGroupId' - | 'hoveredSummaryWorkloadGroupScope' | 'isMobile' | 'nestedWorkloadContextByGuestId' | 'nodeByInstance' | 'search' | 'selectedGuestId' - | 'setFocusedWorkloadGroupScope' - | 'setHoveredWorkloadGroupScope' | 'setHoveredWorkloadId' | 'setSelectedGuestId' | 'setTableRootRef' @@ -133,27 +125,19 @@ export function WorkloadsTable(props: WorkloadsTableProps) { alertsEnabled={props.alertsEnabled} bottomSpacerHeight={props.bottomSpacerHeight} getGroupLabel={props.getGroupLabel} - getNodeTemperatureThresholds={props.getNodeTemperatureThresholds} groupedGuests={props.groupedGuests} groupedWindowing={props.groupedWindowing} groupLabelBadges={props.groupLabelBadges} guestMetadata={props.guestMetadata} guestParentNodeMap={props.guestParentNodeMap} - groupNodeDrawerMode={props.groupNodeDrawerMode} groupingMode={props.groupingMode} handleCustomUrlUpdate={props.handleCustomUrlUpdate} handleTagClick={props.handleTagClick} - activeSummaryWorkloadGroupScope={props.activeSummaryWorkloadGroupScope} activeSummaryWorkloadId={props.activeSummaryWorkloadId} - focusedSummaryWorkloadGroupScope={props.focusedSummaryWorkloadGroupScope} - focusedSummaryWorkloadGroupId={props.focusedSummaryWorkloadGroupId} - hoveredSummaryWorkloadGroupScope={props.hoveredSummaryWorkloadGroupScope} nestedWorkloadContextByGuestId={props.nestedWorkloadContextByGuestId} nodeByInstance={props.nodeByInstance} search={props.search} selectedGuestId={props.selectedGuestId} - setFocusedWorkloadGroupScope={props.setFocusedWorkloadGroupScope} - setHoveredWorkloadGroupScope={props.setHoveredWorkloadGroupScope} setHoveredWorkloadId={props.setHoveredWorkloadId} setSelectedGuestId={props.setSelectedGuestId} setTableBodyRef={props.setTableBodyRef} diff --git a/frontend-modern/src/components/Workloads/__tests__/WorkloadsSurface.performance.contract.test.tsx b/frontend-modern/src/components/Workloads/__tests__/WorkloadsSurface.performance.contract.test.tsx index c481cfd535..7e7dde7a22 100644 --- a/frontend-modern/src/components/Workloads/__tests__/WorkloadsSurface.performance.contract.test.tsx +++ b/frontend-modern/src/components/Workloads/__tests__/WorkloadsSurface.performance.contract.test.tsx @@ -438,28 +438,23 @@ describe('Workloads platform-page embed contract', () => { expect(nodesTableSource).toContain("series: 'nodes',"); }); - it('lets platform pages move grouped host drawers to a dedicated host table owner', async () => { + it('keeps grouped host rows as dividers and host drawers on the platform hosts table', async () => { const stateSource = (await import('../useWorkloadsState.ts?raw')).default; - expect(stateSource).toContain("groupNodeDrawerMode?: 'inline' | 'disabled';"); - expect(stateSource).toContain( - "groupNodeDrawerMode: () => props.groupNodeDrawerMode ?? 'inline',", - ); + expect(stateSource).not.toContain('groupNodeDrawerMode'); + expect(stateSource).not.toContain('summaryGroupScopes'); - const tableSource = (await import('../WorkloadsTable.tsx?raw')).default; - expect(tableSource).toContain(`| 'groupNodeDrawerMode'`); - expect(tableSource).toContain('groupNodeDrawerMode={props.groupNodeDrawerMode}'); + const selectionSource = (await import('../useWorkloadSelectionState.ts?raw')).default; + expect(selectionSource).not.toContain('hoveredGroupScope'); + expect(selectionSource).not.toContain('focusedGroupScope'); const panelSource = (await import('../WorkloadPanel.tsx?raw')).default; - expect(panelSource).toContain("props.groupNodeDrawerMode() === 'inline'"); - expect(panelSource).toContain( - 'onClick: canOpenNodeDrawer() ? handleGroupFocusToggle : undefined', - ); + expect(panelSource).not.toContain('NodeDrawer'); + expect(panelSource).not.toContain('handleGroupFocusToggle'); + expect(panelSource).not.toContain('createSummaryInteractiveRowPreviewHandlers'); + expect(panelSource).toContain("trProps={{ 'data-summary-group-id': groupKey }}"); - const proxmoxPageSource = (await import('../../../features/proxmox/ProxmoxPageSurface.tsx?raw')) - .default; const proxmoxNodesSource = (await import('../../../features/proxmox/ProxmoxNodesTable.tsx?raw')) .default; - expect(proxmoxPageSource).toContain('groupNodeDrawerMode="disabled"'); expect(proxmoxNodesSource).toContain('NodeDrawer'); expect(proxmoxNodesSource).toContain('data-inline-node-detail-for={node.id}'); }); @@ -1301,20 +1296,11 @@ describe('Workloads performance contract', () => { expect(workloadsSource).not.toContain('createMemo(() => getCanonicalWorkloadId(guest()))'); expect(workloadPanelSource).toContain('groupGuests().map(getCanonicalWorkloadId)'); expect(workloadPanelSource).toContain('groupGuestById().get(keyedGuestId)'); - expect(workloadPanelSource).toContain('buildWorkloadSummaryGroupScope'); expect(workloadPanelSource).toContain('data-summary-group-id'); - expect(workloadPanelSource).toContain('setHoveredWorkloadGroupScope'); - expect(workloadPanelSource).toContain('getGroupedTableRowClass'); - expect(workloadPanelSource).toContain('getInteractiveGroupedTableRowClass'); + expect(workloadPanelSource).not.toContain('setHoveredWorkloadGroupScope'); expect(workloadPanelSource).toContain('getGroupedTableRowCellClass'); - expect(workloadPanelSource).toContain('? getInteractiveGroupedTableRowClass()'); - expect(workloadPanelSource).toContain(': getGroupedTableRowClass()'); - expect(workloadPanelSource).toContain( - 'onClick={canOpenNodeDrawer() ? handleGroupFocusToggle : undefined}', - ); - expect(workloadPanelSource).toContain( - '{...(canOpenNodeDrawer() ? groupRowInteraction : {})}', - ); + expect(workloadPanelSource).toContain('class={getGroupedTableRowClass()}'); + expect(workloadPanelSource).not.toContain('getInteractiveGroupedTableRowClass'); expect(workloadPanelSource).not.toContain('cursor-pointer bg-surface-alt'); expect(workloadsWorkloadTableSource).not.toContain( 'createMemo(() => getCanonicalWorkloadId(guest()))', @@ -1593,11 +1579,10 @@ describe('Workloads performance contract', () => { expect(nodeDrawerModelSource).toContain('HOST_METRICS_HISTORY_GROUPS'); expect(nodeDrawerModelSource).toContain("group.id !== 'disk-io'"); expect(nodeDrawerModelSource).not.toContain("id: 'thermals'"); - expect(workloadPanelSource).toContain('NodeDrawer'); - expect(workloadPanelSource).toContain('data-inline-node-detail-for'); - expect(workloadPanelSource).toContain('const selectedGuestId = props.selectedGuestId()'); - expect(workloadPanelSource).toContain('props.setSelectedGuestId(null)'); - expect(workloadPanelSource).toContain('selectedGuestId === null'); + expect(workloadPanelSource).not.toContain('NodeDrawer'); + expect(workloadPanelSource).not.toContain('data-inline-node-detail-for'); + expect(workloadPanelSource).toContain('data-inline-detail-for={guestId()}'); + expect(workloadPanelSource).toContain('onClose={() => props.setSelectedGuestId(null)}'); expect(guestDrawerSource).not.toContain('const guestId = () =>'); expect(guestDrawerSource).not.toContain('const infrastructureHref = () =>'); expect(guestDrawerSource).not.toContain('Filesystems'); @@ -1720,19 +1705,15 @@ describe('Workloads performance contract', () => { expect(workloadPanelSource).toContain('NodeGroupHeader'); expect(workloadPanelSource).toContain('GuestDrawer'); expect(workloadPanelSource).toContain('groupGuests().map(getCanonicalWorkloadId)'); - expect(workloadPanelSource).toContain('createSummaryInteractiveRowPreviewHandlers'); - expect(workloadPanelSource).toContain('resolveSummaryGroupMemberInteractionState'); - expect(workloadPanelSource).toContain('getInteractiveGroupedTableRowClass'); + expect(workloadPanelSource).not.toContain('createSummaryInteractiveRowPreviewHandlers'); + expect(workloadPanelSource).not.toContain('resolveSummaryGroupMemberInteractionState'); expect(workloadPanelSource).toContain('getGroupedTableRowCellClass'); expect(workloadPanelSource).not.toContain('style={{'); expect(workloadPanelSource).not.toContain('style={'); expect(workloadPanelSource).not.toContain('kind="scope"'); expect(workloadPanelSource).not.toContain('leadingAction={'); - expect(workloadsSelectionStateSource).toContain('activeSummaryWorkloadGroupScope'); - expect(workloadsSelectionStateSource).toContain('focusedSummaryWorkloadGroupScope'); - expect(workloadsSelectionStateSource).toContain('setHoveredWorkloadGroupScope'); - expect(workloadsWorkloadTableSource).toContain('focusedSummaryWorkloadGroupScope'); - expect(workloadsWorkloadTableSource).toContain('hoveredSummaryWorkloadGroupScope'); + expect(workloadsSelectionStateSource).not.toContain('WorkloadGroupScope'); + expect(workloadsWorkloadTableSource).not.toContain('SummaryWorkloadGroupScope'); expect(workloadPanelSource).not.toContain('TableHead'); }); diff --git a/frontend-modern/src/components/Workloads/__tests__/useWorkloadSelectionState.test.ts b/frontend-modern/src/components/Workloads/__tests__/useWorkloadSelectionState.test.ts index c405cd7b80..cbf7f1b844 100644 --- a/frontend-modern/src/components/Workloads/__tests__/useWorkloadSelectionState.test.ts +++ b/frontend-modern/src/components/Workloads/__tests__/useWorkloadSelectionState.test.ts @@ -2,7 +2,6 @@ import { renderHook } from '@solidjs/testing-library'; import { createSignal } from 'solid-js'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import type { SummarySeriesGroupScope } from '@/components/shared/summaryCardInteraction'; import type { WorkloadGuest } from '@/types/workloads'; import { resolveWorkloadResourceSelection } from '../workloadSelectionModel'; @@ -10,7 +9,6 @@ import { useWorkloadSelectionState } from '../useWorkloadSelectionState'; let locationSearch = '?resource=cluster-a:node-1:101'; const navigateSpy = vi.fn(); -const emptySummaryGroupScopes = () => new Map(); vi.mock('@solidjs/router', () => ({ useLocation: () => ({ @@ -46,15 +44,11 @@ describe('useWorkloadSelectionState', () => { const { result } = renderHook(() => useWorkloadSelectionState({ filteredGuests, - summaryGroupScopes: emptySummaryGroupScopes, }), ); expect(result.selectedGuestId()).toBe('cluster-a:node-1:101'); - expect(resolveWorkloadResourceSelection(locationSearch)).toEqual({ - resourceId: 'cluster-a:node-1:101', - summaryGroupId: null, - }); + expect(resolveWorkloadResourceSelection(locationSearch)).toBe('cluster-a:node-1:101'); }); it('clears stale hovered workload ids when filtered guests change', () => { @@ -71,7 +65,6 @@ describe('useWorkloadSelectionState', () => { const { result } = renderHook(() => useWorkloadSelectionState({ filteredGuests, - summaryGroupScopes: emptySummaryGroupScopes, }), ); @@ -89,15 +82,13 @@ describe('useWorkloadSelectionState', () => { const { result } = renderHook(() => useWorkloadSelectionState({ filteredGuests, - summaryGroupScopes: emptySummaryGroupScopes, }), ); expect(result.selectedGuestId()).toBe('app-container:truenas-main:nextcloud'); - expect(resolveWorkloadResourceSelection(locationSearch)).toEqual({ - resourceId: 'app-container:truenas-main:nextcloud', - summaryGroupId: null, - }); + expect(resolveWorkloadResourceSelection(locationSearch)).toBe( + 'app-container:truenas-main:nextcloud', + ); }); it('opens workload row selection locally without route navigation', () => { @@ -107,7 +98,6 @@ describe('useWorkloadSelectionState', () => { const { result } = renderHook(() => useWorkloadSelectionState({ filteredGuests, - summaryGroupScopes: emptySummaryGroupScopes, }), ); @@ -118,40 +108,28 @@ describe('useWorkloadSelectionState', () => { expect(navigateSpy).not.toHaveBeenCalled(); }); - it('clears pinned workload scope locally without mutating route filters', () => { + it('clears the pinned workload row locally without mutating route filters', () => { locationSearch = - '?type=app-container&platform=truenas&agent=truenas-main&resource=app-container%3Atruenas-main%3Anextcloud&summaryGroup=docker-host%3Atruenas-main'; + '?type=app-container&platform=truenas&agent=truenas-main&resource=app-container%3Atruenas-main%3Anextcloud'; const [filteredGuests] = createSignal([]); - const summaryScopes = () => - new Map([ - [ - 'docker-host:truenas-main', - { - id: 'docker-host:truenas-main', - label: 'TrueNAS Main (4 workloads)', - seriesIds: ['app-container:truenas-main:nextcloud'], - }, - ], - ]); const { result } = renderHook(() => useWorkloadSelectionState({ filteredGuests, - summaryGroupScopes: summaryScopes, }), ); + expect(result.selectedGuestId()).toBe('app-container:truenas-main:nextcloud'); result.clearPinnedSummaryScope(); vi.runAllTimers(); expect(result.selectedGuestId()).toBeNull(); - expect(result.focusedSummaryWorkloadGroupId()).toBeNull(); expect(navigateSpy).not.toHaveBeenCalled(); }); it('routes Escape through workload scope clearing and additional page reset work', () => { locationSearch = - '?type=app-container&platform=truenas&agent=truenas-main&resource=app-container%3Atruenas-main%3Anextcloud&summaryGroup=docker-host%3Atruenas-main'; + '?type=app-container&platform=truenas&agent=truenas-main&resource=app-container%3Atruenas-main%3Anextcloud'; const [filteredGuests] = createSignal([]); const clearAdditionalPageStateOnEscape = vi.fn(); @@ -159,7 +137,6 @@ describe('useWorkloadSelectionState', () => { useWorkloadSelectionState({ clearAdditionalPageStateOnEscape, filteredGuests, - summaryGroupScopes: emptySummaryGroupScopes, }), ); @@ -177,7 +154,6 @@ describe('useWorkloadSelectionState', () => { const { result } = renderHook(() => useWorkloadSelectionState({ filteredGuests, - summaryGroupScopes: emptySummaryGroupScopes, }), ); @@ -231,7 +207,6 @@ describe('useWorkloadSelectionState', () => { const { result } = renderHook(() => useWorkloadSelectionState({ filteredGuests, - summaryGroupScopes: emptySummaryGroupScopes, }), ); @@ -284,7 +259,8 @@ describe('useWorkloadSelectionState', () => { ).toBe(true); }); - it('tracks hovered workload groups without letting them override entity selection outside scope', () => { + it('ignores a summaryGroup param because grouped rows no longer pin a group scope', () => { + locationSearch = '?summaryGroup=cluster-b&resource=cluster-a:node-1:101'; const [filteredGuests] = createSignal([ { id: 'cluster-a:node-1:101', @@ -294,87 +270,18 @@ describe('useWorkloadSelectionState', () => { node: 'node-1', vmid: 101, } as unknown as WorkloadGuest, - { - id: 'cluster-b:node-2:202', - name: 'guest-2', - status: 'running', - instance: 'cluster-b', - node: 'node-2', - vmid: 202, - } as unknown as WorkloadGuest, ]); - const groupScope: SummarySeriesGroupScope = { - id: 'cluster-b', - label: 'Cluster B (1 workload)', - seriesIds: ['cluster-b:node-2:202'], - }; const { result } = renderHook(() => useWorkloadSelectionState({ filteredGuests, - summaryGroupScopes: emptySummaryGroupScopes, }), ); + vi.runAllTimers(); expect(result.selectedGuestId()).toBe('cluster-a:node-1:101'); - result.setHoveredWorkloadGroupScope(groupScope); - - expect(result.activeSummaryWorkloadGroupScope()?.id).toBe('cluster-b'); - expect(result.activeSummaryWorkloadId()).toBeNull(); - }); - - it('shows the pinned-scope fallback only after a pinned workload group scrolls out of view', () => { - locationSearch = '?summaryGroup=cluster-a'; - const [filteredGuests] = createSignal([ - { - id: 'cluster-a:node-1:101', - name: 'guest-1', - status: 'running', - instance: 'cluster-a', - node: 'node-1', - vmid: 101, - } as unknown as WorkloadGuest, - ]); - const summaryScopes = () => - new Map([ - [ - 'cluster-a', - { - id: 'cluster-a', - label: 'Cluster A (1 workload)', - seriesIds: ['cluster-a:node-1:101'], - }, - ], - ]); - - let top = 120; - const { result } = renderHook(() => - useWorkloadSelectionState({ - filteredGuests, - summaryGroupScopes: summaryScopes, - }), - ); - - const tableWrapper = document.createElement('div'); - const groupRow = document.createElement('div'); - groupRow.dataset.summaryGroupId = 'cluster-a'; - groupRow.getBoundingClientRect = vi.fn(() => ({ - top, - bottom: top + 40, - left: 0, - right: 320, - width: 320, - height: 40, - x: 0, - y: top, - toJSON: () => ({}), - })) as unknown as typeof groupRow.getBoundingClientRect; - tableWrapper.appendChild(groupRow); - document.body.appendChild(tableWrapper); - - result.setTableWrapperRef(tableWrapper as HTMLDivElement); - - expect(result.focusedSummaryWorkloadGroupId()).toBe('cluster-a'); + expect(result.activeSummaryWorkloadId()).toBe('cluster-a:node-1:101'); + expect(Object.keys(result).filter((key) => /group/i.test(key))).toEqual([]); }); it('clears row focus locally without leaving behind an inferred agent filter', () => { @@ -384,7 +291,6 @@ describe('useWorkloadSelectionState', () => { const { result } = renderHook(() => useWorkloadSelectionState({ filteredGuests, - summaryGroupScopes: emptySummaryGroupScopes, }), ); diff --git a/frontend-modern/src/components/Workloads/__tests__/workloadSelectionModel.test.ts b/frontend-modern/src/components/Workloads/__tests__/workloadSelectionModel.test.ts index 3c96e87f6f..16dd0e903b 100644 --- a/frontend-modern/src/components/Workloads/__tests__/workloadSelectionModel.test.ts +++ b/frontend-modern/src/components/Workloads/__tests__/workloadSelectionModel.test.ts @@ -9,28 +9,18 @@ import { describe('workloadSelectionModel', () => { it('resolves workloads resource deep links into focused guest ids without inventing filters', () => { - expect(resolveWorkloadResourceSelection('?resource=cluster-a:node-1:101')).toEqual({ - resourceId: 'cluster-a:node-1:101', - summaryGroupId: null, - }); + expect(resolveWorkloadResourceSelection('?resource=cluster-a:node-1:101')).toBe( + 'cluster-a:node-1:101', + ); expect( resolveWorkloadResourceSelection( '?type=app-container&resource=app-container:truenas-main:nextcloud', ), - ).toEqual({ - resourceId: 'app-container:truenas-main:nextcloud', - summaryGroupId: null, - }); + ).toBe('app-container:truenas-main:nextcloud'); expect( resolveWorkloadResourceSelection('?resource=app-container:docker-main:container-123'), - ).toEqual({ - resourceId: 'app-container:docker-main:container-123', - summaryGroupId: null, - }); - expect(resolveWorkloadResourceSelection('?resource=guest-1')).toEqual({ - resourceId: 'guest-1', - summaryGroupId: null, - }); + ).toBe('app-container:docker-main:container-123'); + expect(resolveWorkloadResourceSelection('?resource=guest-1')).toBe('guest-1'); expect(resolveWorkloadResourceSelection('')).toBeNull(); }); @@ -50,10 +40,7 @@ describe('workloadSelectionModel', () => { expect(workloadsHasHoveredWorkload(guests, 'cluster-a:node-1:102')).toBe(false); }); - it('resolves summary group deep links separately from local row expansion', () => { - expect(resolveWorkloadResourceSelection('?summaryGroup=docker-host%3Atruenas-main')).toEqual({ - resourceId: null, - summaryGroupId: 'docker-host:truenas-main', - }); + it('ignores summary group params, which no longer pin a workload group', () => { + expect(resolveWorkloadResourceSelection('?summaryGroup=docker-host%3Atruenas-main')).toBeNull(); }); }); diff --git a/frontend-modern/src/components/Workloads/__tests__/workloadSelectors.branchcov.test.ts b/frontend-modern/src/components/Workloads/__tests__/workloadSelectors.branchcov.test.ts index 7367dba77e..ad485174fe 100644 --- a/frontend-modern/src/components/Workloads/__tests__/workloadSelectors.branchcov.test.ts +++ b/frontend-modern/src/components/Workloads/__tests__/workloadSelectors.branchcov.test.ts @@ -1,9 +1,6 @@ import { describe, expect, it } from 'vitest'; -import type { Node } from '@/types/api'; import type { WorkloadGuest } from '@/types/workloads'; import { - buildWorkloadSummaryGroupScope, - buildWorkloadSummaryGroupScopeMap, createWorkloadSortComparator, filterWorkloads, getDiskUsagePercent, @@ -38,26 +35,6 @@ const makeGuest = (i: number, overrides?: Partial): WorkloadGuest ...overrides, }); -const makeNode = (id: string, instance: string, name: string): Node => ({ - id, - name, - displayName: name, - instance, - host: `${name}.local`, - status: 'online', - type: 'pve', - cpu: 0, - memory: { total: 1, used: 0, free: 1, usage: 0 }, - disk: { total: 1, used: 0, free: 1, usage: 0 }, - uptime: 1, - loadAverage: [0, 0, 0], - kernelVersion: 'test', - pveVersion: 'test', - cpuInfo: { model: 'test', cores: 1, sockets: 1, mhz: '1' }, - lastSeen: new Date().toISOString(), - connectionHealth: 'online', -}); - describe('workloadSelectors (branch coverage)', () => { describe('createWorkloadSortComparator', () => { it('sorts by memory usage in both directions and falls back to 0 for null memory', () => { @@ -703,139 +680,6 @@ describe('workloadSelectors (branch coverage)', () => { }); }); - describe('buildWorkloadSummaryGroupScope', () => { - it('returns null when all guests produce empty canonical IDs', () => { - const guest = makeGuest(1, { id: '', type: 'app-container', workloadType: 'app-container' }); - expect(buildWorkloadSummaryGroupScope('grp', [guest], { type: 'T', name: 'N' })).toBeNull(); - }); - - it('builds singular label for a single guest', () => { - const guest = makeGuest(1, { - id: 'cid-1', - type: 'app-container', - workloadType: 'app-container', - }); - const scope = buildWorkloadSummaryGroupScope('grp', [guest], { - type: 'Container', - name: 'frigate', - }); - expect(scope).not.toBeNull(); - expect(scope!.label).toBe('frigate · Container (1 workload)'); - expect(scope!.seriesIds).toEqual(['cid-1']); - }); - - it('builds plural label and deduplicates canonical IDs for multiple guests', () => { - const guests = [ - makeGuest(1, { id: 'dup-id', type: 'app-container', workloadType: 'app-container' }), - makeGuest(2, { id: 'dup-id', type: 'app-container', workloadType: 'app-container' }), - ]; - const scope = buildWorkloadSummaryGroupScope('grp', guests, { type: 'C', name: 'host' }); - expect(scope).not.toBeNull(); - expect(scope!.label).toBe('host · C (2 workloads)'); - expect(scope!.seriesIds).toEqual(['dup-id']); // deduped - }); - - it('uses workload count alone when label name and type are both empty', () => { - const guest = makeGuest(1, { - id: 'x-1', - type: 'app-container', - workloadType: 'app-container', - }); - const scope = buildWorkloadSummaryGroupScope('grp', [guest], { type: '', name: '' }); - expect(scope).not.toBeNull(); - expect(scope!.label).toBe('1 workload'); - }); - - it('trims the groupId for the scope id', () => { - const guest = makeGuest(1, { - id: 'x-2', - type: 'app-container', - workloadType: 'app-container', - }); - const scope = buildWorkloadSummaryGroupScope(' padded-grp ', [guest], { - type: 'T', - name: 'N', - }); - expect(scope).not.toBeNull(); - expect(scope!.id).toBe('padded-grp'); - }); - }); - - describe('buildWorkloadSummaryGroupScopeMap', () => { - it('returns an empty map when groupingMode is flat', () => { - const result = buildWorkloadSummaryGroupScopeMap({ - guests: [makeGuest(1)], - nodes: [], - groupingMode: 'flat', - sortComparator: null, - }); - expect(result.size).toBe(0); - }); - - it('resolves node display name via buildNodeByInstance for the group label', () => { - const guests = [ - makeGuest(1, { type: 'vm', workloadType: 'vm', instance: 'cluster-a', node: 'node-a' }), - ]; - const nodes = [makeNode('n1', 'cluster-a', 'node-a')]; - - const result = buildWorkloadSummaryGroupScopeMap({ - guests, - nodes, - groupingMode: 'grouped', - sortComparator: null, - }); - - // groupKey = 'cluster-a-node-a' matches node via instance-name key - // getNodeDisplayName -> 'node-a' -> label includes it - const scope = result.get('cluster-a-node-a'); - expect(scope).toBeDefined(); - expect(scope!.label).toBe('node-a (1 workload)'); - expect(scope!.seriesIds).toEqual(['cluster-a:node-a:101']); - }); - - it('falls back to lowercase badge key when exact-case lookup misses', () => { - const guests = [ - makeGuest(1, { - id: 'c-1', - name: 'frigate', - type: 'app-container', - workloadType: 'app-container', - contextLabel: 'Frigate', // capital F -> groupKey 'app-container:Frigate' - }), - ]; - - const result = buildWorkloadSummaryGroupScopeMap({ - guests, - nodes: [], - groupingMode: 'grouped', - sortComparator: null, - groupLabelBadges: { - 'app-container:frigate': { label: 'Docker' }, // lowercase key - }, - }); - - // badge found via lowercase fallback -> label includes 'Docker' - const scope = result.get('app-container:Frigate'); - expect(scope).toBeDefined(); - expect(scope!.label).toBe('Frigate · Docker (1 workload)'); - }); - - it('omits groups whose scope is null (empty canonical IDs)', () => { - const guests = [ - makeGuest(1, { id: '', type: 'app-container', workloadType: 'app-container' }), - ]; - - const result = buildWorkloadSummaryGroupScopeMap({ - guests, - nodes: [], - groupingMode: 'grouped', - sortComparator: null, - }); - - expect(result.size).toBe(0); - }); - }); - describe('groupWorkloads', () => { it('sorts the single flat group when a comparator is provided', () => { const guests = [ diff --git a/frontend-modern/src/components/Workloads/__tests__/workloadSelectors.test.ts b/frontend-modern/src/components/Workloads/__tests__/workloadSelectors.test.ts index 40c23da99e..22c4df6e88 100644 --- a/frontend-modern/src/components/Workloads/__tests__/workloadSelectors.test.ts +++ b/frontend-modern/src/components/Workloads/__tests__/workloadSelectors.test.ts @@ -8,7 +8,6 @@ import { getDiskUsagePercent, getWorkloadGroupKey, getWorkloadGroupLabel, - buildWorkloadSummaryGroupScopeMap, groupWorkloads, selectVisibleWorkloadInventory, } from '@/components/Workloads/workloadSelectors'; @@ -946,32 +945,6 @@ describe('workloadSelectors', () => { type: 'LXC', }); }); - - it('passes group identity badges into summary group scope labels', () => { - const guests = [ - makeGuest(1, { - id: 'container-1', - name: 'frigate', - type: 'app-container', - workloadType: 'app-container', - contextLabel: 'frigate-host', - }), - ]; - - const scopes = buildWorkloadSummaryGroupScopeMap({ - guests, - nodes: [], - groupingMode: 'grouped', - sortComparator: null, - groupLabelBadges: { - 'app-container:frigate-host': { label: 'LXC' }, - }, - }); - - expect(scopes.get('app-container:frigate-host')?.label).toBe( - 'frigate-host · LXC (1 workload)', - ); - }); }); describe('workloadNodeScopeId', () => { diff --git a/frontend-modern/src/components/Workloads/__tests__/workloadUrlSyncModel.test.ts b/frontend-modern/src/components/Workloads/__tests__/workloadUrlSyncModel.test.ts index e8a5de58f3..745f923c74 100644 --- a/frontend-modern/src/components/Workloads/__tests__/workloadUrlSyncModel.test.ts +++ b/frontend-modern/src/components/Workloads/__tests__/workloadUrlSyncModel.test.ts @@ -22,7 +22,6 @@ describe('workloadUrlSyncModel', () => { cluster: '', agent: 'node-a', resource: 'guest-1', - summaryGroup: '', }); }); diff --git a/frontend-modern/src/components/Workloads/guestRowModel.tsx b/frontend-modern/src/components/Workloads/guestRowModel.tsx index df232cfa02..eefec02a58 100644 --- a/frontend-modern/src/components/Workloads/guestRowModel.tsx +++ b/frontend-modern/src/components/Workloads/guestRowModel.tsx @@ -1,7 +1,6 @@ import type { JSX } from 'solid-js'; import type { ColumnDef } from '@/hooks/useColumnVisibility'; -import type { SummaryGroupMemberInteractionState } from '@/components/shared/summaryCardInteraction'; import { PLATFORM_TABLE_NARROW_IDENTITY_WIDTH_PERCENT, getPlatformTableWeightedColumnWidthStyle, @@ -118,7 +117,6 @@ export interface GuestRowProps { onClick?: () => void; isExpanded?: boolean; isSummaryHighlighted?: boolean; - summaryGroupMemberState?: SummaryGroupMemberInteractionState; ioEmphasis?: WorkloadIOEmphasis; metricDisplayMode?: 'bars' | 'sparklines'; metricHoverMode?: WorkloadsMetricHoverMode; diff --git a/frontend-modern/src/components/Workloads/useWorkloadSelectionState.ts b/frontend-modern/src/components/Workloads/useWorkloadSelectionState.ts index 65e1f05faf..6ee13e7d68 100644 --- a/frontend-modern/src/components/Workloads/useWorkloadSelectionState.ts +++ b/frontend-modern/src/components/Workloads/useWorkloadSelectionState.ts @@ -1,24 +1,18 @@ import { useLocation } from '@solidjs/router'; -import { createEffect, createMemo, createSignal, type Accessor } from 'solid-js'; +import { createEffect, createSignal, type Accessor } from 'solid-js'; import { preserveScrollableAncestorVerticalOffset } from '@/components/shared/contextualFocus'; import { useSummaryPageInteractionState } from '@/components/shared/summaryTableFocus'; -import { - isSummarySeriesInGroupScope, - type SummarySeriesGroupScope, -} from '@/components/shared/summaryCardInteraction'; import type { WorkloadGuest } from '@/types/workloads'; import { capturePendingAppShellRestoreTop } from '@/utils/appShellScrollRestoration'; import { workloadsHasHoveredWorkload, - workloadsHasVisibleWorkloadGroupScope, resolveWorkloadResourceSelection, } from './workloadSelectionModel'; interface UseWorkloadsSelectionStateOptions { clearAdditionalPageStateOnEscape?: () => void; filteredGuests: Accessor; - summaryGroupScopes: Accessor>; routeStateEnabled?: Accessor; } @@ -26,42 +20,25 @@ export function useWorkloadSelectionState(options: UseWorkloadsSelectionStateOpt const location = useLocation(); const [selectedGuestId, setSelectedGuestIdRaw] = createSignal(null); - const [selectedWorkloadGroupId, setSelectedWorkloadGroupIdRaw] = createSignal( - null, - ); const [hoveredWorkloadId, setHoveredWorkloadId] = createSignal(null); - const [hoveredWorkloadGroupScope, setHoveredWorkloadGroupScope] = - createSignal(null); const [handledResourceId, setHandledResourceId] = createSignal(null); - const [handledWorkloadGroupId, setHandledWorkloadGroupId] = createSignal(null); const [revealedGuestId, setRevealedGuestId] = createSignal(null); const [tableWrapperRef, setTableWrapperRefSignal] = createSignal( undefined, ); const [tableBodyRef, setTableBodyRef] = createSignal(null); - const focusedWorkloadGroupScope = createMemo(() => { - const selectedGroupId = selectedWorkloadGroupId(); - if (!selectedGroupId) { - return null; - } - return options.summaryGroupScopes().get(selectedGroupId) ?? null; - }); const clearPinnedSummaryScope = () => { preserveScrollableAncestorVerticalOffset(tableWrapperRef(), () => { setSelectedGuestIdRaw(null); - setSelectedWorkloadGroupIdRaw(null); }); }; const summaryInteraction = useSummaryPageInteractionState({ clearPinnedScope: clearPinnedSummaryScope, hoveredSeriesId: hoveredWorkloadId, - hoveredGroupScope: hoveredWorkloadGroupScope, focusedSeriesId: selectedGuestId, - focusedGroupId: selectedWorkloadGroupId, - focusedGroupScope: focusedWorkloadGroupScope, onEscapeClear: () => { clearPinnedSummaryScope(); options.clearAdditionalPageStateOnEscape?.(); @@ -89,50 +66,20 @@ export function useWorkloadSelectionState(options: UseWorkloadsSelectionStateOpt const setSelectedGuestId = (id: string | null) => { capturePendingAppShellRestoreTop(); - const activeFocusedGroupScope = focusedWorkloadGroupScope(); - const nextGroupScope = - activeFocusedGroupScope && !isSummarySeriesInGroupScope(activeFocusedGroupScope, id) - ? null - : activeFocusedGroupScope; - preserveScrollableAncestorVerticalOffset(tableWrapperRef(), () => { - setSelectedGuestIdRaw(id); - setSelectedWorkloadGroupIdRaw(nextGroupScope?.id ?? null); - }); - }; - - const setFocusedWorkloadGroupScopeState = (scope: SummarySeriesGroupScope | null) => { - preserveScrollableAncestorVerticalOffset(tableWrapperRef(), () => { - setSelectedWorkloadGroupIdRaw(scope?.id ?? null); - if (scope && !isSummarySeriesInGroupScope(scope, selectedGuestId())) { - setSelectedGuestIdRaw(null); - } - }); - }; - - const setFocusedWorkloadGroupScope = (scope: SummarySeriesGroupScope | null) => { - setFocusedWorkloadGroupScopeState(scope); + setSelectedGuestIdState(id); }; createEffect(() => { if (options.routeStateEnabled && !options.routeStateEnabled()) return; - const selection = resolveWorkloadResourceSelection(location.search); - if (!selection) { + const resourceId = resolveWorkloadResourceSelection(location.search); + if (!resourceId) { if (handledResourceId() !== null) { setSelectedGuestIdState(null); setHandledResourceId(null); } - if (handledWorkloadGroupId() !== null) { - setSelectedWorkloadGroupIdRaw(null); - setHandledWorkloadGroupId(null); - } return; } - const { resourceId, summaryGroupId } = selection; - if (summaryGroupId !== handledWorkloadGroupId()) { - setSelectedWorkloadGroupIdRaw(summaryGroupId); - setHandledWorkloadGroupId(summaryGroupId); - } if (resourceId !== handledResourceId()) { setSelectedGuestIdState(resourceId); setHandledResourceId(resourceId); @@ -155,50 +102,13 @@ export function useWorkloadSelectionState(options: UseWorkloadsSelectionStateOpt } }); - createEffect(() => { - const groupScope = hoveredWorkloadGroupScope(); - if (!groupScope) { - return; - } - if (!workloadsHasVisibleWorkloadGroupScope(options.filteredGuests(), groupScope)) { - setHoveredWorkloadGroupScope(null); - } - }); - - createEffect(() => { - const selectedGroupId = selectedWorkloadGroupId(); - if (!selectedGroupId) { - return; - } - if (!focusedWorkloadGroupScope()) { - setFocusedWorkloadGroupScope(null); - } - }); - - createEffect(() => { - const selectedId = selectedGuestId(); - const groupScope = focusedWorkloadGroupScope(); - if (!selectedId || !groupScope) { - return; - } - if (!isSummarySeriesInGroupScope(groupScope, selectedId)) { - setSelectedGuestId(null); - } - }); - return { - activeSummaryWorkloadGroupScope: summaryInteraction.activeGroupScope, activeSummaryWorkloadId: summaryInteraction.activeSeriesId, clearPinnedSummaryScope, - focusedSummaryWorkloadGroupScope: focusedWorkloadGroupScope, hoveredWorkloadId, - hoveredSummaryWorkloadGroupScope: hoveredWorkloadGroupScope, - focusedSummaryWorkloadGroupId: selectedWorkloadGroupId, revealedGuestId, selectedGuestId, setClearSurfaceRootRef, - setFocusedWorkloadGroupScope, - setHoveredWorkloadGroupScope, setHoveredWorkloadId, setSelectedGuestId, setTableBodyRef, diff --git a/frontend-modern/src/components/Workloads/useWorkloadsState.ts b/frontend-modern/src/components/Workloads/useWorkloadsState.ts index 8b5b954709..4565c12f1a 100644 --- a/frontend-modern/src/components/Workloads/useWorkloadsState.ts +++ b/frontend-modern/src/components/Workloads/useWorkloadsState.ts @@ -5,7 +5,6 @@ import { type RuntimeInventorySourcesResponse, } from '@/api/runtimeInventorySources'; import { recordWorkloadHistoryActivity } from '@/api/workloadHistoryActivity'; -import { nodeOverrideIdCandidates } from '@/features/alerts/alertOverridesModel'; import type { VM, Container, Node } from '@/types/api'; import type { Resource } from '@/types/resource'; import type { ViewMode, WorkloadGuest, WorkloadType } from '@/types/workloads'; @@ -28,7 +27,6 @@ import { getCanonicalWorkloadId } from '@/utils/workloads'; import { nodeFromResource } from '@/utils/resourceStateAdapters'; import { STORAGE_KEYS } from '@/utils/localStorage'; import { - buildWorkloadSummaryGroupScopeMap, createWorkloadSortComparator, filterWorkloads, selectVisibleWorkloadInventory, @@ -105,10 +103,6 @@ export interface WorkloadsSurfaceProps { }>; resourceSnapshotRefetch?: () => Promise; statusModeStorageScope?: string; - // Default Workloads behavior owns grouped host row drawers inline. Platform - // pages with a dedicated host table can disable that drawer so host details - // open from the host-owned table instead of the embedded guest table. - groupNodeDrawerMode?: 'inline' | 'disabled'; // When a platform page owns the metric display mode + sparkline range // (so the same toggle drives both the page's hosts table and this // embedded workloads surface), pass the accessors + change handlers. @@ -423,9 +417,6 @@ export function useWorkloadsState(props: WorkloadsSurfaceProps) { reconnect(); }; - const getNodeTemperatureThresholds = (node: Node) => - alertsActivation.getMetricThresholds('node', 'temperature', nodeOverrideIdCandidates(node)); - createEffect(() => { if (!workloadsEnabled() || props.inventorySourcesQuery) return; const handle = window.setInterval(() => { @@ -480,29 +471,14 @@ export function useWorkloadsState(props: WorkloadsSurfaceProps) { const groupLabelBadges = createMemo>( () => props.groupLabelBadges ?? {}, ); - const summaryGroupScopes = createMemo(() => - buildWorkloadSummaryGroupScopeMap({ - guests: filteredGuests(), - nodes: infrastructureNodes(), - groupingMode: groupingMode(), - sortComparator: guestSortComparator(), - groupLabelBadges: groupLabelBadges(), - }), - ); const { - activeSummaryWorkloadGroupScope, activeSummaryWorkloadId, clearPinnedSummaryScope, - focusedSummaryWorkloadGroupScope, - focusedSummaryWorkloadGroupId, - hoveredSummaryWorkloadGroupScope, hoveredWorkloadId, revealedGuestId, selectedGuestId, setClearSurfaceRootRef, - setFocusedWorkloadGroupScope, - setHoveredWorkloadGroupScope, setHoveredWorkloadId, setSelectedGuestId, setTableBodyRef, @@ -515,7 +491,6 @@ export function useWorkloadsState(props: WorkloadsSurfaceProps) { resetWorkloadRouteFilters(); }, filteredGuests, - summaryGroupScopes, routeStateEnabled: props.routeStateEnabled, }); @@ -606,7 +581,6 @@ export function useWorkloadsState(props: WorkloadsSurfaceProps) { allGuests, inventoryStats, isScrollToTopVisible, - activeSummaryWorkloadGroupScope, activeSummaryWorkloadId, clearPinnedSummaryScope, bottomSpacerHeight, @@ -623,10 +597,7 @@ export function useWorkloadsState(props: WorkloadsSurfaceProps) { workloadsInfrastructureEmptyState, workloadsLoadingState, filteredGuests, - focusedSummaryWorkloadGroupScope, - focusedSummaryWorkloadGroupId, getGroupLabel, - getNodeTemperatureThresholds, groupedGuests, groupedWindowing, guestMetadata, @@ -638,7 +609,6 @@ export function useWorkloadsState(props: WorkloadsSurfaceProps) { handleTagClick, hasInfrastructureSources, hostFilterConfig, - hoveredSummaryWorkloadGroupScope, infrastructureSourceStateReady, infrastructureNodes, initialDataReceived, @@ -667,9 +637,7 @@ export function useWorkloadsState(props: WorkloadsSurfaceProps) { selectedNode, setContainerRuntime, setClearSurfaceRootRef, - setFocusedWorkloadGroupScope, setGroupingMode, - setHoveredWorkloadGroupScope, setHoveredWorkloadId, setSearch, setSelectedCluster, @@ -725,7 +693,6 @@ export function useWorkloadsState(props: WorkloadsSurfaceProps) { workloadsNoInventoryState, ws, groupingMode, - groupNodeDrawerMode: () => props.groupNodeDrawerMode ?? 'inline', groupLabelBadges, } as const; } diff --git a/frontend-modern/src/components/Workloads/workloadSelectionModel.ts b/frontend-modern/src/components/Workloads/workloadSelectionModel.ts index 7c0b452153..2d8b591832 100644 --- a/frontend-modern/src/components/Workloads/workloadSelectionModel.ts +++ b/frontend-modern/src/components/Workloads/workloadSelectionModel.ts @@ -1,36 +1,12 @@ import { parseWorkloadsLinkSearch } from '@/routing/resourceLinks'; -import type { SummarySeriesGroupScope } from '@/components/shared/summaryCardInteraction'; import type { WorkloadGuest } from '@/types/workloads'; import { getCanonicalWorkloadId } from '@/utils/workloads'; -export interface WorkloadsResourceSelection { - resourceId: string | null; - summaryGroupId: string | null; -} - -export const resolveWorkloadResourceSelection = ( - search: string, -): WorkloadsResourceSelection | null => { - const { resource: resourceId, summaryGroup: summaryGroupId } = parseWorkloadsLinkSearch(search); - if (!resourceId && !summaryGroupId) return null; - - return { - resourceId: resourceId || null, - summaryGroupId: summaryGroupId || null, - }; -}; +/** Resolves the inbound `resource` deep link to the guest whose drawer opens. */ +export const resolveWorkloadResourceSelection = (search: string): string | null => + parseWorkloadsLinkSearch(search).resource || null; export const workloadsHasHoveredWorkload = ( filteredGuests: WorkloadGuest[], hoveredId: string, ): boolean => filteredGuests.some((guest) => getCanonicalWorkloadId(guest) === hoveredId); - -export const workloadsHasVisibleWorkloadGroupScope = ( - filteredGuests: WorkloadGuest[], - groupScope: SummarySeriesGroupScope, -): boolean => { - const filteredGuestIds = new Set( - filteredGuests.map((guest) => getCanonicalWorkloadId(guest)).filter(Boolean), - ); - return groupScope.seriesIds.some((seriesId) => filteredGuestIds.has(seriesId)); -}; diff --git a/frontend-modern/src/components/Workloads/workloadSelectors.ts b/frontend-modern/src/components/Workloads/workloadSelectors.ts index 30cd597b3b..c1896da8fe 100644 --- a/frontend-modern/src/components/Workloads/workloadSelectors.ts +++ b/frontend-modern/src/components/Workloads/workloadSelectors.ts @@ -1,19 +1,15 @@ -import type { Node } from '@/types/api'; import type { WorkloadGuest, ViewMode, WorkloadType } from '@/types/workloads'; import { getWorkloadInfoValue, type IODistributionStats, type WorkloadIOEmphasis, } from './guestRowModel'; -import type { SummarySeriesGroupScope } from '@/components/shared/summaryCardInteraction'; import { parseFilterStack, evaluateFilterStack, splitSearchExclusions } from '@/utils/searchQuery'; import { normalizeSourcePlatformQueryValue } from '@/utils/sourcePlatforms'; import { OFFLINE_HEALTH_STATUSES } from '@/utils/status'; import { hasFailedAttachedAvailabilityCheck } from '@/utils/availabilityProbePresentation'; -import { getNodeDisplayName } from '@/utils/nodes'; import { isContainerWorkloadViewMode, - getCanonicalWorkloadId, getWorkloadCPUPercent, resolveWorkloadType, workloadMatchesPlatformScope, @@ -21,7 +17,6 @@ import { } from '@/utils/workloads'; import { getWorkloadTypePresentation } from '@/utils/workloadTypePresentation'; import { - buildNodeByInstance, getKubernetesContextKey, getWorkloadHostHintCandidates, workloadHostScopeId, @@ -417,29 +412,6 @@ export const getWorkloadGroupLabel = ( return { type: '', name: context }; }; -export const buildWorkloadSummaryGroupScope = ( - groupId: string, - guests: WorkloadGuest[], - label: { type: string; name: string }, -): SummarySeriesGroupScope | null => { - const seriesIds = Array.from( - new Set(guests.map((guest) => getCanonicalWorkloadId(guest)).filter(Boolean)), - ); - if (seriesIds.length === 0) { - return null; - } - - const summaryLabelParts = [label.name.trim(), label.type.trim()].filter(Boolean); - const scopeLabel = summaryLabelParts.join(' · '); - const workloadCountLabel = `${guests.length} workload${guests.length === 1 ? '' : 's'}`; - - return { - id: groupId.trim(), - label: scopeLabel ? `${scopeLabel} (${workloadCountLabel})` : workloadCountLabel, - seriesIds, - }; -}; - export const groupWorkloads = ( guests: WorkloadGuest[], mode: 'grouped' | 'flat', @@ -471,46 +443,6 @@ export const groupWorkloads = ( return groups; }; -export const buildWorkloadSummaryGroupScopeMap = ({ - guests, - nodes, - groupingMode, - sortComparator, - groupLabelBadges, -}: { - guests: WorkloadGuest[]; - nodes: Node[]; - groupingMode: 'grouped' | 'flat'; - sortComparator: ((a: WorkloadGuest, b: WorkloadGuest) => number) | null; - groupLabelBadges?: Record; -}): Map => { - if (groupingMode !== 'grouped') { - return new Map(); - } - - const grouped = groupWorkloads(guests, groupingMode, sortComparator); - const nodeByInstance = buildNodeByInstance(nodes); - const scopes = new Map(); - for (const [groupKey, groupGuests] of Object.entries(grouped)) { - const node = nodeByInstance[groupKey]; - const badge = groupLabelBadges?.[groupKey] ?? groupLabelBadges?.[groupKey.toLowerCase()]; - const scope = buildWorkloadSummaryGroupScope( - groupKey, - groupGuests, - getWorkloadGroupLabel( - groupKey, - groupGuests, - node ? getNodeDisplayName(node) : null, - badge?.label, - ), - ); - if (scope) { - scopes.set(scope.id, scope); - } - } - return scopes; -}; - export const computeWorkloadStats = (guests: WorkloadGuest[]): WorkloadStats => { let running = 0; let degraded = 0; diff --git a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts index c90210a1e6..a9da0806f2 100644 --- a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts +++ b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts @@ -1356,8 +1356,6 @@ describe('shared primitive guardrails', () => { expect(summaryCardInteractionSource).toContain('resolveSummaryScopeState'); expect(workloadSelectionStateSource).toContain('preserveScrollableAncestorVerticalOffset'); - expect(workloadSelectionStateSource).toContain('hoveredWorkloadGroupScope'); - expect(workloadSelectionStateSource).toContain('activeSummaryWorkloadGroupScope'); expect(workloadSelectionStateSource).not.toContain('const scrollTop = scroller?.scrollTop'); expect(summaryTableFocusSource).toContain('export function useSummaryTableFocusBridge'); @@ -1404,7 +1402,6 @@ describe('shared primitive guardrails', () => { expect(groupedTableRowPresentationSource).not.toContain('GROUPED_TABLE_ROW_DIVIDER_CLASS'); 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'); @@ -1422,7 +1419,7 @@ describe('shared primitive guardrails', () => { expect(storageGroupPresentationSource).toContain('getGroupedTableRowCellClass'); expect(nodeGroupHeaderSource).toContain('getGroupedTableRowClass'); expect(nodeGroupHeaderSource).toContain('getGroupedTableRowCellClass'); - expect(workloadPanelSource).toContain('getInteractiveGroupedTableRowClass'); + expect(workloadPanelSource).toContain('class={getGroupedTableRowClass()}'); expect(workloadPanelSource).toContain('getGroupedTableRowCellClass'); expect(alertHistoryTableGroupRowSource).toContain('getGroupedTableRowClass'); expect(alertHistoryTableGroupRowSource).toContain('getGroupedTableRowCellClass'); @@ -1728,7 +1725,6 @@ describe('shared primitive guardrails', () => { expect(storageGroupRowSource).not.toContain('hideWhenRowTappableOnMobile={false}'); expect(storageGroupRowSource).toContain('props.onToggle();'); - expect(workloadPanelSource).toContain('createSummaryInteractiveRowPreviewHandlers'); expect(workloadPanelSource).not.toContain('kind="scope"'); expect(storageGroupRowSource).not.toContain('kind="scope"'); diff --git a/frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx b/frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx index fa0a193e29..e0d72b7bbb 100644 --- a/frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx +++ b/frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx @@ -494,7 +494,6 @@ function ProxmoxOverview(props: ProxmoxOverviewProps) { // surface, so label it with the platform vocabulary instead of 'Info'. columnLabelOverrides: { info: 'ID' }, statusModeStorageScope: PROXMOX_WORKLOAD_STATUS_STORAGE_SCOPE, - groupNodeDrawerMode: 'disabled', metricDisplayMode: props.metricDisplayMode, onMetricDisplayModeChange: props.setMetricDisplayMode, metricHistoryRange: props.metricHistoryRange, @@ -617,12 +616,7 @@ function ProxmoxOverview(props: ProxmoxOverviewProps) { memoryDisplayBasis={workloadsState.workloadMemoryDisplayBasis} setMemoryDisplayBasis={props.setMemoryDisplayBasis} forcedPlatform={PROXMOX_PLATFORM_FILTER} - pinnedSelectionActive={() => - Boolean( - workloadsState.selectedGuestId() || - workloadsState.focusedSummaryWorkloadGroupId(), - ) - } + pinnedSelectionActive={() => Boolean(workloadsState.selectedGuestId())} onClearPinnedSelection={workloadsState.clearPinnedSummaryScope} /> @@ -636,7 +630,6 @@ function ProxmoxOverview(props: ProxmoxOverviewProps) { forcedPlatform={PROXMOX_PLATFORM_FILTER} excludedWorkloadTypes={PROXMOX_WORKLOAD_EXCLUDED_TYPES} showNestedExcludedWorkloads - groupNodeDrawerMode="disabled" suppressFilterToolbar emptyStateTitle="No Proxmox workloads" emptyStateDescription="Proxmox VMs and LXCs appear here when inventory is available." diff --git a/frontend-modern/src/features/proxmox/__tests__/ProxmoxPageSurface.contract.test.tsx b/frontend-modern/src/features/proxmox/__tests__/ProxmoxPageSurface.contract.test.tsx index 63964ff67a..e3c47a6883 100644 --- a/frontend-modern/src/features/proxmox/__tests__/ProxmoxPageSurface.contract.test.tsx +++ b/frontend-modern/src/features/proxmox/__tests__/ProxmoxPageSurface.contract.test.tsx @@ -214,16 +214,6 @@ describe('ProxmoxPageSurface contract', () => { ); }); - it('leaves host details to the nodes table instead of the embedded guest table', () => { - setResources([makeResource({ id: 'vm-1', type: 'vm' })]); - - renderSurface(); - - expect(mockWorkloadsOptions).toHaveBeenCalledWith( - expect.objectContaining({ groupNodeDrawerMode: 'disabled' }), - ); - }); - afterEach(() => { cleanup(); vi.clearAllMocks(); diff --git a/frontend-modern/src/features/proxmox/__tests__/proxmoxDrawerPlacement.contract.test.ts b/frontend-modern/src/features/proxmox/__tests__/proxmoxDrawerPlacement.contract.test.ts index 311fe4dfda..268e04a426 100644 --- a/frontend-modern/src/features/proxmox/__tests__/proxmoxDrawerPlacement.contract.test.ts +++ b/frontend-modern/src/features/proxmox/__tests__/proxmoxDrawerPlacement.contract.test.ts @@ -2,23 +2,14 @@ import { describe, expect, it } from 'vitest'; import workloadsStateSource from '@/components/Workloads/useWorkloadsState.ts?raw'; import workloadPanelSource from '@/components/Workloads/WorkloadPanel.tsx?raw'; -import workloadsTableSource from '@/components/Workloads/WorkloadsTable.tsx?raw'; import proxmoxNodesTableSource from '../ProxmoxNodesTable.tsx?raw'; import proxmoxPageSurfaceSource from '../ProxmoxPageSurface.tsx?raw'; describe('Proxmox drawer placement contract', () => { it('keeps host details owned by the Proxmox host table instead of the embedded guest table', () => { - expect(workloadsStateSource).toContain("groupNodeDrawerMode?: 'inline' | 'disabled';"); - expect(workloadsStateSource).toContain( - "groupNodeDrawerMode: () => props.groupNodeDrawerMode ?? 'inline',", - ); - expect(workloadsTableSource).toContain(`| 'groupNodeDrawerMode'`); - expect(workloadsTableSource).toContain('groupNodeDrawerMode={props.groupNodeDrawerMode}'); - expect(workloadPanelSource).toContain("props.groupNodeDrawerMode() === 'inline'"); - expect(workloadPanelSource).toContain( - 'onClick: canOpenNodeDrawer() ? handleGroupFocusToggle : undefined', - ); - expect(proxmoxPageSurfaceSource).toContain('groupNodeDrawerMode="disabled"'); + expect(workloadsStateSource).not.toContain('groupNodeDrawerMode'); + expect(workloadPanelSource).not.toContain('NodeDrawer'); + expect(proxmoxPageSurfaceSource).not.toContain('groupNodeDrawerMode'); expect(proxmoxNodesTableSource).toContain('NodeDrawer'); expect(proxmoxNodesTableSource).toContain('data-inline-node-detail-for={node.id}'); }); diff --git a/frontend-modern/src/features/vmware/VmwarePageSurface.tsx b/frontend-modern/src/features/vmware/VmwarePageSurface.tsx index 1a34ea8be5..ee925fd5f3 100644 --- a/frontend-modern/src/features/vmware/VmwarePageSurface.tsx +++ b/frontend-modern/src/features/vmware/VmwarePageSurface.tsx @@ -475,7 +475,6 @@ function VmwareOverview(props: VmwareOverviewProps) { columnVisibilityStorageScope: VMWARE_WORKLOAD_COLUMN_VISIBILITY_SCOPE, additionalDefaultHiddenColumnIds: [...VMWARE_WORKLOAD_DEFAULT_HIDDEN_COLUMN_IDS], inventorySourcesQuery: props.inventorySourcesQuery, - groupNodeDrawerMode: 'disabled', metricDisplayMode: props.metricDisplayMode, onMetricDisplayModeChange: props.setMetricDisplayMode, metricHistoryRange: props.metricHistoryRange, @@ -527,11 +526,7 @@ function VmwareOverview(props: VmwareOverviewProps) { platformFilter={undefined} {...getWorkloadsMetricFilterProps(workloadsState)} forcedPlatform={VMWARE_PLATFORM_FILTER} - pinnedSelectionActive={() => - Boolean( - workloadsState.selectedGuestId() || workloadsState.focusedSummaryWorkloadGroupId(), - ) - } + pinnedSelectionActive={() => Boolean(workloadsState.selectedGuestId())} onClearPinnedSelection={workloadsState.clearPinnedSummaryScope} /> @@ -544,7 +539,6 @@ function VmwareOverview(props: VmwareOverviewProps) { useWorkloads forcedPlatform={VMWARE_PLATFORM_FILTER} forcedViewMode="vm" - groupNodeDrawerMode="disabled" suppressFilterToolbar emptyStateTitle="No vSphere VMs" emptyStateDescription="Virtual machines appear here once the vCenter connection enumerates them." diff --git a/frontend-modern/src/routing/__tests__/resourceLinks.branchcov0712b.test.ts b/frontend-modern/src/routing/__tests__/resourceLinks.branchcov0712b.test.ts index 24b72a7620..7607b3c072 100644 --- a/frontend-modern/src/routing/__tests__/resourceLinks.branchcov0712b.test.ts +++ b/frontend-modern/src/routing/__tests__/resourceLinks.branchcov0712b.test.ts @@ -28,22 +28,20 @@ describe('buildWorkloadsRouteSearch branch coverage', () => { expect(buildWorkloadsRouteSearch({})).toBe(''); }); - it('emits the runtime, namespace, cluster, and summaryGroup params the sibling suite omits', () => { + it('emits the runtime, namespace, and cluster params the sibling suite omits', () => { expect( buildWorkloadsRouteSearch({ runtime: 'containerd', namespace: 'kube-system', cluster: 'prod-us-east', - summaryGroup: 'compute', }), - ).toBe('?runtime=containerd&namespace=kube-system&cluster=prod-us-east&summaryGroup=compute'); + ).toBe('?runtime=containerd&namespace=kube-system&cluster=prod-us-east'); }); it('verifies the omitted query-param keys map to the canonical names', () => { expect(WORKLOADS_QUERY_PARAMS.runtime).toBe('runtime'); expect(WORKLOADS_QUERY_PARAMS.namespace).toBe('namespace'); expect(WORKLOADS_QUERY_PARAMS.cluster).toBe('cluster'); - expect(WORKLOADS_QUERY_PARAMS.summaryGroup).toBe('summaryGroup'); }); it('canonicalizes the "host" type alias to "agent" via canonicalizeWorkloadFilterType', () => { @@ -69,10 +67,9 @@ describe('buildWorkloadsRouteSearch branch coverage', () => { cluster: ' cl-1 ', agent: ' ag-1 ', resource: ' res-1 ', - summaryGroup: ' sg-1 ', }), ).toBe( - '?type=pod&platform=kubernetes&runtime=cri-o&context=ctx-1&namespace=ns-1&cluster=cl-1&agent=ag-1&resource=res-1&summaryGroup=sg-1', + '?type=pod&platform=kubernetes&runtime=cri-o&context=ctx-1&namespace=ns-1&cluster=cl-1&agent=ag-1&resource=res-1', ); }); @@ -87,7 +84,6 @@ describe('buildWorkloadsRouteSearch branch coverage', () => { cluster: '', agent: null, resource: undefined, - summaryGroup: ' ', }), ).toBe(''); }); diff --git a/frontend-modern/src/routing/__tests__/resourceLinks.test.ts b/frontend-modern/src/routing/__tests__/resourceLinks.test.ts index a79c26e118..362c39eb37 100644 --- a/frontend-modern/src/routing/__tests__/resourceLinks.test.ts +++ b/frontend-modern/src/routing/__tests__/resourceLinks.test.ts @@ -248,7 +248,6 @@ describe('resource link routing contract', () => { cluster: '', agent: 'worker-1', resource: 'cluster-a:worker-1:101', - summaryGroup: '', }); expect(WORKLOADS_QUERY_PARAMS.type).toBe('type'); @@ -259,7 +258,7 @@ describe('resource link routing contract', () => { expect(WORKLOADS_QUERY_PARAMS.cluster).toBe('cluster'); expect(WORKLOADS_QUERY_PARAMS.agent).toBe('agent'); expect(WORKLOADS_QUERY_PARAMS.resource).toBe('resource'); - expect(WORKLOADS_QUERY_PARAMS.summaryGroup).toBe('summaryGroup'); + expect(WORKLOADS_QUERY_PARAMS).not.toHaveProperty('summaryGroup'); }); it('canonicalizes legacy workloads type aliases when building route search', () => { diff --git a/frontend-modern/src/routing/resourceLinks.ts b/frontend-modern/src/routing/resourceLinks.ts index d9e8e13101..594afd361a 100644 --- a/frontend-modern/src/routing/resourceLinks.ts +++ b/frontend-modern/src/routing/resourceLinks.ts @@ -13,7 +13,6 @@ export const WORKLOADS_QUERY_PARAMS = { // Canonical v6 agent filter query param. agent: 'agent', resource: 'resource', - summaryGroup: 'summaryGroup', } as const; export const STANDALONE_PATH = '/standalone'; @@ -156,7 +155,6 @@ type WorkloadsLinkOptions = { cluster?: string | null; agent?: string | null; resource?: string | null; - summaryGroup?: string | null; }; type DockerLinkOptions = { @@ -210,7 +208,6 @@ export const parseWorkloadsLinkSearch = (search: string) => { cluster: normalizeQueryValue(params.get(WORKLOADS_QUERY_PARAMS.cluster)), agent: normalizeQueryValue(params.get(WORKLOADS_QUERY_PARAMS.agent)), resource: normalizeQueryValue(params.get(WORKLOADS_QUERY_PARAMS.resource)), - summaryGroup: normalizeQueryValue(params.get(WORKLOADS_QUERY_PARAMS.summaryGroup)), }; }; @@ -298,7 +295,6 @@ export const buildWorkloadsRouteSearch = (options: WorkloadsLinkOptions = {}): s const cluster = normalizeQueryValue(options.cluster); const agent = normalizeQueryValue(options.agent); const resource = normalizeQueryValue(options.resource); - const summaryGroup = normalizeQueryValue(options.summaryGroup); if (type) params.set(WORKLOADS_QUERY_PARAMS.type, type); if (platform) params.set(WORKLOADS_QUERY_PARAMS.platform, platform); if (runtime) params.set(WORKLOADS_QUERY_PARAMS.runtime, runtime); @@ -307,7 +303,6 @@ export const buildWorkloadsRouteSearch = (options: WorkloadsLinkOptions = {}): s if (cluster) params.set(WORKLOADS_QUERY_PARAMS.cluster, cluster); if (agent) params.set(WORKLOADS_QUERY_PARAMS.agent, agent); if (resource) params.set(WORKLOADS_QUERY_PARAMS.resource, resource); - if (summaryGroup) params.set(WORKLOADS_QUERY_PARAMS.summaryGroup, summaryGroup); return serializedRouteSearch(params); }; diff --git a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts index a2de774cb6..c4fbf37a5e 100644 --- a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts +++ b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts @@ -1414,7 +1414,7 @@ describe('frontend resource type boundaries', () => { expect(storageGroupRowSource).toContain('buildStorageGroupRowPresentation'); expect(storageGroupRowSource).toContain('STORAGE_GROUP_ROW_CLASS'); expect(storageGroupRowSource).not.toContain('cursor-pointer select-none bg-surface-alt'); - expect(workloadPanelSource).toContain('getInteractiveGroupedTableRowClass'); + expect(workloadPanelSource).toContain('getGroupedTableRowClass()'); expect(storageGroupPresentationSource).toContain( 'export const getStorageGroupHealthCountPresentation', ); diff --git a/frontend-modern/src/utils/__tests__/temperatureDisplayGuardrails.test.ts b/frontend-modern/src/utils/__tests__/temperatureDisplayGuardrails.test.ts index 37c1175902..974e67ce47 100644 --- a/frontend-modern/src/utils/__tests__/temperatureDisplayGuardrails.test.ts +++ b/frontend-modern/src/utils/__tests__/temperatureDisplayGuardrails.test.ts @@ -1,16 +1,14 @@ import { describe, expect, it } from 'vitest'; import nodeDrawerOverviewSource from '@/components/Workloads/NodeDrawerOverview.tsx?raw'; -import workloadPanelSource from '@/components/Workloads/WorkloadPanel.tsx?raw'; import temperatureGaugeSource from '@/components/shared/TemperatureGauge.tsx?raw'; import dockerHostDrawerOverviewSource from '@/features/docker/DockerHostDrawerOverview.tsx?raw'; +import proxmoxNodesTableSource from '@/features/proxmox/ProxmoxNodesTable.tsx?raw'; import agentsMachinesTableSource from '@/features/standalone/AgentsMachinesTable.tsx?raw'; describe('temperature display guardrails', () => { it('keeps colored temperature displays threshold-aware', () => { - expect(workloadPanelSource).toContain( - 'temperatureThresholds={props.getNodeTemperatureThresholds(node()!)}', - ); + expect(proxmoxNodesTableSource).toContain('temperatureThresholds={temperatureThresholds()}'); expect(nodeDrawerOverviewSource).toContain('getTemperatureTextClass(value, thresholds)'); expect(dockerHostDrawerOverviewSource).toContain('temperatureThresholds()'); expect(agentsMachinesTableSource).toContain('getAgentMachineTemperatureMetric'); @@ -19,7 +17,6 @@ describe('temperature display guardrails', () => { }); it('does not reintroduce the previous local temperature severity calls', () => { - expect(workloadPanelSource).not.toContain('getTemperatureTextClass(temperature)}'); expect(nodeDrawerOverviewSource).not.toContain('getTemperatureTextClass(value)'); expect(dockerHostDrawerOverviewSource).not.toContain('getTemperatureTextClass(temperature)'); expect(agentsMachinesTableSource).not.toContain('');