Remove the never-mounted unified resource table

PlatformResourceTable was the only renderer of UnifiedResourceTable, and
nothing imports it. Every platform page (Proxmox, Docker, Kubernetes,
TrueNAS, vSphere, Machines) renders its own native tables, and
/infrastructure only shows the opening-workspace placeholder. A
production build of main already tree-shook the whole cluster (none of
its strings ship), yet it still carried 11 source files, 7 test files,
registry ownership in two subsystems and contract rules for a table
nobody sees.

Removed: PlatformResourceTable and its PLATFORM_STATUS_FILTER_OPTIONS;
UnifiedResourceTable with its host, service, PBS and PMG cards, source
badge cell, state hook, viewport sync and models; and
ClusterDeployBanner, which only the host card rendered (the live cluster
deploy flow is the Settings ClusterAgentDeployDialog). Their tests go
with them.

Coverage for live code those tests also exercised is kept: the detail
mapper no-fetch boundary and the online boolean sensor case move to
resourceDetailMappers.test.ts, the ResourceFacetSummary render cases
move to their own test file, and both files become registered proofs for
the policies that govern their subjects. Guardrail tests, the shared
template registry, registry.json, status.json evidence and the
performance, unified-resources, frontend-primitives and cloud-paid
contracts drop the deleted paths and the rules that only governed them.

Rollup used to name the large shared chunk after sharedPlatformPage; it
now names it MetricBar, so the bundle baseline entry is re-keyed at its
measured size.
This commit is contained in:
rcourtman
2026-10-06 20:13:36 +01:00
parent 9aaac4d3e5
commit 2265d28242
35 changed files with 261 additions and 5970 deletions
+1 -16
View File
@@ -95,11 +95,6 @@
"path": "cmd/pulse-agent/service_windows.go",
"kind": "file"
},
{
"repo": "pulse",
"path": "frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.workloads-link.test.tsx",
"kind": "file"
},
{
"repo": "pulse",
"path": "frontend-modern/src/hooks/__tests__/useUnifiedResources.test.ts",
@@ -5666,11 +5661,6 @@
"performance-and-scalability"
],
"evidence": [
{
"repo": "pulse",
"path": "frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.performance.contract.test.tsx",
"kind": "file"
},
{
"repo": "pulse",
"path": "frontend-modern/src/components/Infrastructure/infrastructureSelectors.ts",
@@ -5681,11 +5671,6 @@
"path": "frontend-modern/src/components/Infrastructure/resourceDetailMappers.ts",
"kind": "file"
},
{
"repo": "pulse",
"path": "frontend-modern/src/components/Infrastructure/UnifiedResourceTable.tsx",
"kind": "file"
},
{
"repo": "pulse",
"path": "frontend-modern/src/components/Workloads/__tests__/workloadInventorySourceIssues.test.ts",
@@ -5923,7 +5908,7 @@
},
{
"repo": "pulse",
"path": "frontend-modern/src/components/Infrastructure/UnifiedResourceTable.tsx",
"path": "frontend-modern/src/hooks/useUnifiedResources.ts",
"kind": "file"
},
{
@@ -1168,9 +1168,8 @@ hands-on Patrol modes, issue investigation, verified fixes, and longer history`.
aliases such as `/settings/workloads/docker` or nested
`/settings/infrastructure/platforms/*` paths. Each platform page must
remain chrome-only: routing plus sub-tab navigation that embeds the
canonical `WorkloadsSurface`, `StorageSurface`, `RecoverySurface`, or
`UnifiedResourceTable` in `embedded tableOnly` mode with a forced
platform/source filter. The shell must not introduce dashboard cards,
canonical `WorkloadsSurface`, `StorageSurface`, or `RecoverySurface` in
`embedded tableOnly` mode with a forced platform/source filter. The shell must not introduce dashboard cards,
bespoke per-family tables, synthetic placeholder data, or reintroduce
Infrastructure as a primary navigation entry without a governed contract
decision recorded by the frontend-primitives owner.
@@ -3276,9 +3276,9 @@ default` instead of fusing provider and badge text such as
Platform-first top-level pages registered through
`frontend-modern/src/App.tsx` must stay chrome-only and route through the
canonical app shell: each per-platform surface owns navigation and sub-tab
chrome, then embeds the canonical `WorkloadsSurface`, `StorageSurface`,
`RecoverySurface`, or `UnifiedResourceTable` in `embedded tableOnly` mode
with a forced platform or source filter. Per-platform features must not
chrome, then embeds the canonical `WorkloadsSurface`, `StorageSurface`, or
`RecoverySurface` in `embedded tableOnly` mode with a forced platform or
source filter. Per-platform features must not
fork their own table primitives, header layouts, or summary cards when a
shared canonical surface already exists; new shared platform-page
primitives live under `frontend-modern/src/features/platformPage/` so the
@@ -6284,16 +6284,10 @@ layering on the default. Tables therefore state the padding they want in their
own classes or presentation constants instead of carrying `!px-*` overrides to
beat the primitive. `Table.test.tsx` pins the base, single-side, important,
`p-*`, prefixed-only and reactive cases.
That same shared table boundary now owns CSP-safe sizing for infrastructure
tables and metric bars. `frontend-modern/src/components/Infrastructure/useUnifiedResourceTableState.ts`
and `frontend-modern/src/components/Infrastructure/unifiedResourceTableStateModel.ts`
must express table layout and column sizing as shared class/attribute
presentation instead of inline `style=` maps, and
`frontend-modern/src/components/shared/ProgressBar.tsx` must render fill width
through DOM attributes rather than inline width styles. Infrastructure host and
service tables may still vary by breakpoint and column family, but they must do
so through the shared presentation owner instead of lane-local style objects
that break the public demo CSP.
That same shared table boundary now owns CSP-safe fill rendering for metric
bars: `frontend-modern/src/components/shared/ProgressBar.tsx` must render fill
width through DOM attributes rather than inline width styles that break the
public demo CSP.
That same shared-boundary rule applies to summary density. The shared compact
mode on `SummaryPanel.tsx` and `SummaryMetricCard.tsx` exists for genuinely
dense monitoring surfaces, but pages that are trying to align with the normal
@@ -474,15 +474,6 @@ in-place click and chevron activation over a selection.
64. `frontend-modern/src/components/Workloads/__tests__/workloadRouteStateModel.test.ts`
65. `frontend-modern/src/components/Workloads/__tests__/workloadUrlSyncModel.test.ts`
66. `frontend-modern/src/components/Workloads/__tests__/workloadTopology.test.ts`
67. `frontend-modern/src/components/Infrastructure/UnifiedResourceTable.tsx`
68. `frontend-modern/src/components/Infrastructure/unifiedResourceTableStateModel.ts`
69. `frontend-modern/src/components/Infrastructure/useUnifiedResourceTableState.ts`
70. `frontend-modern/src/components/Infrastructure/useUnifiedResourceTableViewportSync.ts`
71. `frontend-modern/src/components/Infrastructure/UnifiedResourceHostTableCard.tsx`
72. `frontend-modern/src/components/Infrastructure/UnifiedResourcePBSTableSection.tsx`
73. `frontend-modern/src/components/Infrastructure/UnifiedResourcePMGTableSection.tsx`
74. `frontend-modern/src/components/Infrastructure/UnifiedResourceServiceInfrastructureCard.tsx`
75. `frontend-modern/src/components/Infrastructure/unifiedResourceTableModel.ts`
76. `frontend-modern/src/components/Infrastructure/infrastructureSelectors.ts`
77. `frontend-modern/src/components/Infrastructure/resourceDetailMappers.ts`
78. `frontend-modern/src/components/Workloads/__tests__/WorkloadsSurface.performance.contract.test.tsx`
@@ -504,7 +495,6 @@ in-place click and chevron activation over a selection.
94. `frontend-modern/src/components/Workloads/__tests__/workloadMetricHistoryModel.test.ts`
95. `frontend-modern/src/components/Workloads/GuestDrawer.test.tsx`
96. `frontend-modern/src/components/Workloads/__tests__/useGroupedTableWindowing.test.ts`
97. `frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.performance.contract.test.tsx`
98. `frontend-modern/src/components/Workloads/useWorkloadViewportSync.ts`
99. `frontend-modern/src/components/Workloads/__tests__/useWorkloadViewportSync.test.tsx`
100. `frontend-modern/src/utils/workloadsSummaryCache.ts`
@@ -548,16 +538,7 @@ or a measured fleet performance improvement.
1. `frontend-modern/src/components/Infrastructure/infrastructureSelectors.ts` shared with `unified-resources`: the infrastructure selector pipeline is both a canonical unified-resource consumer surface and a fleet-scale performance hot-path boundary.
2. `frontend-modern/src/components/Infrastructure/resourceDetailMappers.ts` shared with `unified-resources`: resource detail mappers are both a canonical unified-resource consumer surface and a fleet-scale performance hot-path boundary.
3. `frontend-modern/src/components/Infrastructure/UnifiedResourceHostTableCard.tsx` shared with `unified-resources`: the unified resource host table card is both a canonical unified-resource consumer surface and a fleet-scale performance hot-path boundary.
4. `frontend-modern/src/components/Infrastructure/UnifiedResourcePBSTableSection.tsx` shared with `unified-resources`: the unified resource PBS section is both a canonical unified-resource consumer surface and a fleet-scale performance hot-path boundary.
5. `frontend-modern/src/components/Infrastructure/UnifiedResourcePMGTableSection.tsx` shared with `unified-resources`: the unified resource PMG section is both a canonical unified-resource consumer surface and a fleet-scale performance hot-path boundary.
6. `frontend-modern/src/components/Infrastructure/UnifiedResourceServiceInfrastructureCard.tsx` shared with `unified-resources`: the unified resource service infrastructure card is both a canonical unified-resource consumer surface and a fleet-scale performance hot-path boundary.
7. `frontend-modern/src/components/Infrastructure/UnifiedResourceTable.tsx` shared with `unified-resources`: the unified resource table is both a canonical unified-resource consumer surface and a fleet-scale performance hot-path boundary.
8. `frontend-modern/src/components/Infrastructure/unifiedResourceTableModel.ts` shared with `unified-resources`: unified resource service row shaping and I/O emphasis are both a canonical unified-resource consumer surface and a fleet-scale performance hot-path boundary.
9. `frontend-modern/src/components/Infrastructure/unifiedResourceTableStateModel.ts` shared with `unified-resources`: unified resource table state derivation, sort-cycle policy, service sorting, and responsive column layout are both a canonical unified-resource consumer surface and a fleet-scale performance hot-path boundary.
10. `frontend-modern/src/components/Infrastructure/useTableWindowing.ts` shared with `frontend-primitives`: the shared bounded table-window controller is both a canonical frontend rendering primitive and a fleet-scale scrolling hot-path boundary.
10. `frontend-modern/src/components/Infrastructure/useUnifiedResourceTableState.ts` shared with `unified-resources`: unified resource table state, grouping, and windowing are both a canonical unified-resource consumer surface and a fleet-scale performance hot-path boundary.
11. `frontend-modern/src/components/Infrastructure/useUnifiedResourceTableViewportSync.ts` shared with `unified-resources`: unified resource table viewport sync and selected-row reveal are both a canonical unified-resource consumer surface and a fleet-scale performance hot-path boundary.
12. `frontend-modern/src/features/platformPage/PlatformWindowedList.tsx` shared with `frontend-primitives`: the shared bounded list renderer is both a canonical platform-page primitive and a fleet-scale mounted-DOM performance boundary.
13. `frontend-modern/src/features/platformPage/PlatformWindowedRows.tsx` shared with `frontend-primitives`: the shared bounded table-row renderer is both a canonical platform-page primitive and a fleet-scale mounted-DOM performance boundary.
14. `frontend-modern/src/features/platformPage/usePlatformWindowedItems.ts` shared with `frontend-primitives`: the platform windowing controller is both a canonical frontend scroll primitive and a directional-runway performance hot path.
@@ -1060,16 +1041,6 @@ change may globally weaken the Task 03 lifecycle-state idempotency invariant.
`/auth/trial-activate` and `POST /api/license/trial/start` from public-path
and CSRF inventories must stay as constant-time route-table absence rather
than replacing the old callback with persistence-backed router probes.
Infrastructure host-table health explanations follow the same hot-path
rule. `UnifiedResourceHostTableCard.tsx` may render compact issue labels
from the already-materialized resource incident and storage-risk fields, but
it must not introduce per-row API reads, broad resource scans, storage
topology recomputation, or layout-measuring work just to explain warning or
degraded rows. Infrastructure table source derivation follows the same
bounded-work rule: `unifiedResourceTableStateModel.ts` must read the
already-materialized top-level `resource.sources` array before legacy
`platformData.sources` hints, and must not reconstruct platform identity by
scanning sibling resources or making row-time API calls.
5. Extend workload hot-path filter, sort, grouping, and stats math through `frontend-modern/src/components/Workloads/workloadSelectors.ts`, and extend workload identity, discovery routing, and node-topology helpers through `frontend-modern/src/components/Workloads/workloadTopology.ts`, rather than duplicating selector or topology logic in `frontend-modern/src/components/Workloads/WorkloadsSurface.tsx`
Platform-owned Workloads surfaces may force a platform scope, but that
scope must be applied before deriving host, Kubernetes context/namespace,
@@ -1749,22 +1720,16 @@ without contention or cross-delivery.
2. Update this contract when a new protected hot path is adopted
3. Route runtime changes through the explicit performance proof policies in `registry.json`; default fallback proof routing is not allowed
4. Record the evidence source for any claimed performance improvement
5. Keep wide-desktop infrastructure table layout proof on the shared owner.
Changes to `frontend-modern/src/components/Infrastructure/unifiedResourceTableStateModel.ts`
that affect host, PBS, or PMG desktop column widths must ship with shared
model verification plus desktop Playwright proof that full-width shells
distribute surplus width across peer columns instead of stretching only the
`Resource` column.
6. Keep entitlement and runtime capability lookups on shared router/settings
5. Keep entitlement and runtime capability lookups on shared router/settings
hot paths bounded and request-local. AI control-level clamping in
`internal/api/router.go` may consult the already-wired runtime entitlement
service, but it must not add broad persistence scans, metrics fan-out, or
external network calls to protected settings or chat request paths.
7. Initialize and cache each organization RBAC manager once per router
6. Initialize and cache each organization RBAC manager once per router
lifecycle. Request handlers may reuse the cached manager but must not reopen
or remigrate SQLite on each role or assignment request, and shutdown must
close the owned manager set.
8. Guest Overview filesystem provenance is local presentation over the already
7. Guest Overview filesystem provenance is local presentation over the already
supplied snapshot: no additional query, timer, poll, History fetch or guest
command may be added to establish it. Its existing linear filesystem mapping
may label retained values and omit current utilization bars; it must preserve
@@ -1823,8 +1788,8 @@ definitions with four concurrent command or REST executions. Group/subgroup
ordering and numeric, boolean, or timestamp rendering remain one bounded
sort-and-map over that collection and stay outside table-row and virtualized
viewport hot paths.
`UnifiedResourceTable.performance.contract.test.tsx` pins the resource-local
projection and no-fetch boundary.
`resourceDetailMappers.test.ts` pins the resource-local projection and no-fetch
boundary.
### Workload refreshes retain one coherent paged generation
@@ -2717,15 +2682,6 @@ re-accumulating discovery heuristics back into the performance hot-path mapper.
That mapper may add simple host-agent thermals rows from already-loaded sensor
metadata, including `thermalState`, but it must not perform additional resource
lookups, history reads, or polling work to decorate the drawer.
The PBS service-table hot path now follows that same split: raw job arrays may
cross the transport boundary, but
`frontend-modern/src/components/Infrastructure/unifiedResourceTableModel.ts`
must collapse them into one shared `Activity` presentation via the canonical
service-model helpers, while
`frontend-modern/src/components/Infrastructure/UnifiedResourcePBSTableSection.tsx`
stays a thin render shell. Future running-task visibility changes must extend
through the shared model and accepted performance proof file instead of adding
per-row PBS status parsing or inline job scans in the table render path.
The Workloads disk list now follows the same pattern: the shell stays in
`frontend-modern/src/components/Workloads/DiskList.tsx`, while disk-row
presentation derivations and fallback tooltip/runtime wiring live in
@@ -2889,48 +2845,11 @@ precedence, reinterpret ratios above an arbitrary threshold, or replace an
established provider history series; source authority and canonical identity
convergence belong upstream in the unified-resource and monitoring contracts.
The unified resource table hot path is now also governed as explicit
performance-owned runtime, with shared ownership against the unified-resource
consumer boundary. The remaining performance work is no longer top-level
ownership ambiguity on the main Infrastructure or Workloads tables.
The table's reactive runtime, grouping, and row-windowing owner now lives in
`frontend-modern/src/components/Infrastructure/useUnifiedResourceTableState.ts`,
while pure table-state derivation, service sorting, sort-cycle policy, and
responsive column layout now live in
`frontend-modern/src/components/Infrastructure/unifiedResourceTableStateModel.ts`,
and viewport-sync plus selected-row reveal behavior now live in
`frontend-modern/src/components/Infrastructure/useUnifiedResourceTableViewportSync.ts`,
so future hot-path table-state changes must not fold selector derivation,
layout policy, and scroll coordination back into one mixed owner or the render
shell.
The same shared layout policy now treats the phone branch as a zero-overflow
fixed-width table rather than preserving a 640-pixel intrinsic floor. The
shared state model must select one identity track fixed at 30 percent plus five to seven
source-relevant health or activity tracks, normalize those widths to the
available container, and keep the document and table shell at scroll-width
parity. That denser phone projection remains pure model/class work: it must not
add row-time measurement, duplicate tables, viewport listeners, or per-row
responsive signals to the infrastructure hot path.
That same hot-path boundary now also owns CSP-safe table sizing. Infrastructure
host, PBS, and PMG table shells must take their layout and column sizing from
the shared presentation owner in
`frontend-modern/src/components/Infrastructure/unifiedResourceTableStateModel.ts`
and apply those values through classes and width/height attributes, not inline
`style=` maps on the live table DOM. Future hot-path table work must not
reintroduce inline width, min/max width, or row-height styles into the render
shell just to land a local layout tweak.
That same shared sizing contract now also owns wide-desktop width
distribution. In full-width shells, host, PBS, and PMG tables must keep an
explicit desktop `Resource` column width in the shared presentation owner so
surplus width is redistributed across peer metric, source, uptime, and action
columns instead of being dumped into the first column and wasting operator
visible table density.
That hot-path contract now includes policy badge rendering on resource rows.
Policy-rich table rows must only surface a single inline summary chip for
blocking `local-only`/`restricted` posture; non-blocking `sensitive` +
Resource table rows that surface policy posture must show at most a single
inline summary chip for blocking `local-only`/`restricted` posture; non-blocking `sensitive` +
`local-first` and redaction-only metadata belongs in Data Handling, detail, and
AI/governance surfaces instead of spending default row visual budget.
Agentless availability evidence belongs on that same bounded row path.
Agentless availability evidence belongs on the bounded row path.
Any resource row carrying an `AvailabilityData` facet—whether a standalone
`network-endpoint` or a known guest that inherited the facet through explicit
link or IP correlation—may replace otherwise empty host metric slots with one
@@ -2960,40 +2879,28 @@ The summary API feeding that hot path must also normalize mixed-resolution
history into equal-time summary buckets before it reaches the shell/runtime
owners, so long-range cards do not bunch recent higher-resolution samples at
the right edge.
It now also includes compact resource-facet summary chips rendered next to
policy metadata, and table-row uses must set an explicit visible chip limit
with overflow disclosure instead of allowing mock-rich resource rows to wrap
an unbounded badge list. Those chips must stay within the same bounded
windowing and mounted-row budget proved by
`UnifiedResourceTable.performance.contract.test.tsx`.
The same facet summary contract applies to the service-resource rows inside
the unified table as well, so PBS and PMG entries must keep the same bounded
presentation and verification surface as the primary fleet rows. The shared
`ResourceFacetSummary` component now owns that chip rendering path, so any
future summary changes must preserve the same bounded row budget instead of
forking separate table-only presentation logic. That component now also
Compact resource-facet summary chips render through the shared
`ResourceFacetSummary` component, whose production consumer is the detail
drawer's change history. A table-row use must set an explicit visible chip
limit with overflow disclosure instead of letting mock-rich rows wrap an
unbounded badge list, and must stay within the table's bounded windowing and
mounted-row budget rather than forking separate table-only presentation
logic. That component now also
consumes the shared `frontend-modern/src/utils/resourceChangePresentation.ts`
label helper for canonical change kinds, source types, and adapter provenance
so the chip wording stays consistent without adding extra hot-path branching.
Host, PBS, and PMG summary rows also declare themselves as the compact touch
disclosure target through `SummaryRowActionButton`; the shared primitive hides
the redundant visible mobile chevron while preserving accessible disclosure
semantics, so provider sections do not pay a separate cell-width or tap-target
cost for an action already owned by the whole row.
The same infrastructure hot path now also depends on the shared
`frontend-modern/src/components/shared/ProgressBar.tsx` primitive for metric
fill rendering. Performance-sensitive metric bars may vary by value and color,
but they must render width through shared attribute-driven progress geometry
instead of per-row inline width styles that break the hosted demo CSP.
The default table hot path now scopes those summary chips to timeline and
change-provenance badges only. Generic capability and relationship badges are
removed from the default row surface entirely until the underlying data is
proven populated, which preserves the fleet-table scan path and avoids
spending hot-path visual budget on model nouns that do not yet clear the
product bar.
Row summaries now also prefer canonical `facetCounts` on each resource when
they are available, so the hot path can stay within the same budget while
still reading totals from the shared resource contract. The drawer history
Those summary chips are scoped to timeline and change-provenance badges only.
Generic capability and relationship badges stay off the default surface until
the underlying data is proven populated, so no surface spends visual budget on
model nouns that do not yet clear the product bar.
Facet summaries fall back to canonical `facetCounts` on each resource when no
fetched facet counts are available, reading totals from the shared resource
contract. The drawer history
surface reuses the same governed resource route helpers for relationship and
related-resource links, so cross-resource navigation stays within the existing
infrastructure surface rather than branching into custom detail-only routing.
@@ -3007,23 +2914,17 @@ investigation-first product contract instead of rendering low-signal generic
facet sections by default.
Governance metadata such as sensitivity and routing scope may be visible in
the table, but it must remain on the same bounded row-windowing and mounted-row
budget proved by `UnifiedResourceTable.performance.contract.test.tsx` rather
than creating a separate unbounded rendering path for policy-rich fleets.
Infrastructure cluster-header presentation must stay inside that same bounded
row model. `unifiedResourceTableStateModel.ts` owns whether the compact
`Cluster` chip is rendered, including suppressing it when the visible group
name already ends in `Cluster`, so label cleanup does not add a second
row-rendering path or per-page DOM workaround.
The shared table and detail drawer now also render governed resource labels
through the shared identity/display contract, which routes policy-aware
resources through the canonical policy-aware helper and suppresses the raw
alternate name when policy requires governed handling. That keeps the
policy-aware label path inside the same hot-row rendering budget instead of
adding a second display branch for redacted fleets, and the proof for that
behavior lives in `UnifiedResourceTable.performance.contract.test.tsx`.
The shared table now also passes the same canonical resource-label resolver
into the detail drawer so related-resource chips in the timeline/history path
can resolve through the canonical catalog without adding a separate
budget rather than creating a separate unbounded rendering path for
policy-rich fleets.
The detail drawer now also renders governed resource labels through the
shared identity/display contract, which routes policy-aware resources through
the canonical policy-aware helper and suppresses the raw alternate name when
policy requires governed handling. That keeps the policy-aware label path
inside the same rendering budget instead of adding a second display branch for
redacted fleets.
Platform tables pass the canonical resource-label resolver into the detail
drawer through `PlatformResourceDetailTableRow` so related-resource chips in
the timeline/history path can resolve through the canonical catalog without adding a separate
detail-only lookup branch to the hot-row path.
The same detail drawer also uses that resolver for correlation dependency and
dependent chips, so the investigation path does not fall back to raw IDs in
@@ -153,62 +153,6 @@
"unified-resources"
]
},
{
"path": "frontend-modern/src/components/Infrastructure/UnifiedResourceHostTableCard.tsx",
"rationale": "the unified resource host table card is both a canonical unified-resource consumer surface and a fleet-scale performance hot-path boundary",
"subsystems": [
"performance-and-scalability",
"unified-resources"
]
},
{
"path": "frontend-modern/src/components/Infrastructure/UnifiedResourcePBSTableSection.tsx",
"rationale": "the unified resource PBS section is both a canonical unified-resource consumer surface and a fleet-scale performance hot-path boundary",
"subsystems": [
"performance-and-scalability",
"unified-resources"
]
},
{
"path": "frontend-modern/src/components/Infrastructure/UnifiedResourcePMGTableSection.tsx",
"rationale": "the unified resource PMG section is both a canonical unified-resource consumer surface and a fleet-scale performance hot-path boundary",
"subsystems": [
"performance-and-scalability",
"unified-resources"
]
},
{
"path": "frontend-modern/src/components/Infrastructure/UnifiedResourceServiceInfrastructureCard.tsx",
"rationale": "the unified resource service infrastructure card is both a canonical unified-resource consumer surface and a fleet-scale performance hot-path boundary",
"subsystems": [
"performance-and-scalability",
"unified-resources"
]
},
{
"path": "frontend-modern/src/components/Infrastructure/UnifiedResourceTable.tsx",
"rationale": "the unified resource table is both a canonical unified-resource consumer surface and a fleet-scale performance hot-path boundary",
"subsystems": [
"performance-and-scalability",
"unified-resources"
]
},
{
"path": "frontend-modern/src/components/Infrastructure/unifiedResourceTableModel.ts",
"rationale": "unified resource service row shaping and I/O emphasis are both a canonical unified-resource consumer surface and a fleet-scale performance hot-path boundary",
"subsystems": [
"performance-and-scalability",
"unified-resources"
]
},
{
"path": "frontend-modern/src/components/Infrastructure/unifiedResourceTableStateModel.ts",
"rationale": "unified resource table state derivation, sort-cycle policy, service sorting, and responsive column layout are both a canonical unified-resource consumer surface and a fleet-scale performance hot-path boundary",
"subsystems": [
"performance-and-scalability",
"unified-resources"
]
},
{
"path": "frontend-modern/src/components/Infrastructure/useTableWindowing.ts",
"rationale": "the shared bounded table-window controller is both a canonical frontend rendering primitive and a fleet-scale scrolling hot-path boundary",
@@ -217,22 +161,6 @@
"performance-and-scalability"
]
},
{
"path": "frontend-modern/src/components/Infrastructure/useUnifiedResourceTableState.ts",
"rationale": "unified resource table state, grouping, and windowing are both a canonical unified-resource consumer surface and a fleet-scale performance hot-path boundary",
"subsystems": [
"performance-and-scalability",
"unified-resources"
]
},
{
"path": "frontend-modern/src/components/Infrastructure/useUnifiedResourceTableViewportSync.ts",
"rationale": "unified resource table viewport sync and selected-row reveal are both a canonical unified-resource consumer surface and a fleet-scale performance hot-path boundary",
"subsystems": [
"performance-and-scalability",
"unified-resources"
]
},
{
"path": "frontend-modern/src/components/Settings/AgentIntegrationsPanel.tsx",
"rationale": "the External agents settings panel is the optional settings-shell projection of Pulse MCP onboarding, the AI runtime connected-agent onboarding surface, and a presentation consumer of the shared agent capabilities frontend client",
@@ -7117,16 +7045,7 @@
"owned_files": [
"frontend-modern/src/components/Infrastructure/infrastructureSelectors.ts",
"frontend-modern/src/components/Infrastructure/resourceDetailMappers.ts",
"frontend-modern/src/components/Infrastructure/UnifiedResourceHostTableCard.tsx",
"frontend-modern/src/components/Infrastructure/UnifiedResourcePBSTableSection.tsx",
"frontend-modern/src/components/Infrastructure/UnifiedResourcePMGTableSection.tsx",
"frontend-modern/src/components/Infrastructure/UnifiedResourceServiceInfrastructureCard.tsx",
"frontend-modern/src/components/Infrastructure/UnifiedResourceTable.tsx",
"frontend-modern/src/components/Infrastructure/unifiedResourceTableModel.ts",
"frontend-modern/src/components/Infrastructure/unifiedResourceTableStateModel.ts",
"frontend-modern/src/components/Infrastructure/useTableWindowing.ts",
"frontend-modern/src/components/Infrastructure/useUnifiedResourceTableState.ts",
"frontend-modern/src/components/Infrastructure/useUnifiedResourceTableViewportSync.ts",
"frontend-modern/src/components/Workloads/DiskList.tsx",
"frontend-modern/src/components/Workloads/diskListModel.ts",
"frontend-modern/src/components/Workloads/EnhancedCPUBar.tsx",
@@ -7286,16 +7205,7 @@
"match_files": [
"frontend-modern/src/components/Infrastructure/infrastructureSelectors.ts",
"frontend-modern/src/components/Infrastructure/resourceDetailMappers.ts",
"frontend-modern/src/components/Infrastructure/UnifiedResourceHostTableCard.tsx",
"frontend-modern/src/components/Infrastructure/UnifiedResourcePBSTableSection.tsx",
"frontend-modern/src/components/Infrastructure/UnifiedResourcePMGTableSection.tsx",
"frontend-modern/src/components/Infrastructure/UnifiedResourceServiceInfrastructureCard.tsx",
"frontend-modern/src/components/Infrastructure/UnifiedResourceTable.tsx",
"frontend-modern/src/components/Infrastructure/unifiedResourceTableModel.ts",
"frontend-modern/src/components/Infrastructure/unifiedResourceTableStateModel.ts",
"frontend-modern/src/components/Infrastructure/useTableWindowing.ts",
"frontend-modern/src/components/Infrastructure/useUnifiedResourceTableState.ts",
"frontend-modern/src/components/Infrastructure/useUnifiedResourceTableViewportSync.ts",
"frontend-modern/src/components/Workloads/DiskList.tsx",
"frontend-modern/src/components/Workloads/diskListModel.ts",
"frontend-modern/src/components/Workloads/EnhancedCPUBar.tsx",
@@ -7365,8 +7275,7 @@
"test_prefixes": [],
"exact_files": [
"frontend-modern/src/components/Infrastructure/__tests__/infrastructureSelectors.test.ts",
"frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.performance.contract.test.tsx",
"frontend-modern/src/components/Infrastructure/__tests__/unifiedResourceTableStateModel.test.ts",
"frontend-modern/src/components/Infrastructure/__tests__/resourceDetailMappers.test.ts",
"frontend-modern/src/components/Infrastructure/__tests__/useTableWindowing.test.ts",
"frontend-modern/src/components/Workloads/__tests__/AvailabilityProbeStatusCard.test.tsx",
"frontend-modern/src/components/Workloads/__tests__/DiskList.test.tsx",
@@ -8110,19 +8019,10 @@
"frontend-modern/src/components/Infrastructure/ResourceFacetSummary.tsx",
"frontend-modern/src/components/Infrastructure/ResourceOperatorStateSection.tsx",
"frontend-modern/src/components/Infrastructure/ResourcePolicySummary.tsx",
"frontend-modern/src/components/Infrastructure/UnifiedResourceHostTableCard.tsx",
"frontend-modern/src/components/Infrastructure/UnifiedResourcePBSTableSection.tsx",
"frontend-modern/src/components/Infrastructure/UnifiedResourcePMGTableSection.tsx",
"frontend-modern/src/components/Infrastructure/UnifiedResourceServiceInfrastructureCard.tsx",
"frontend-modern/src/components/Infrastructure/UnifiedResourceTable.tsx",
"frontend-modern/src/components/Infrastructure/unifiedResourceTableModel.ts",
"frontend-modern/src/components/Infrastructure/unifiedResourceTableStateModel.ts",
"frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDerivedState.ts",
"frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDockerActionsState.ts",
"frontend-modern/src/components/Infrastructure/useResourceDetailDrawerHistoryState.ts",
"frontend-modern/src/components/Infrastructure/useResourceDetailDrawerState.ts",
"frontend-modern/src/components/Infrastructure/useUnifiedResourceTableState.ts",
"frontend-modern/src/components/Infrastructure/useUnifiedResourceTableViewportSync.ts",
"frontend-modern/src/components/Kubernetes/K8sDeploymentsDrawer.tsx",
"frontend-modern/src/components/Kubernetes/K8sNamespacesDrawer.tsx",
"frontend-modern/src/features/docker/DockerAlertsTable.tsx",
@@ -8318,19 +8218,10 @@
"frontend-modern/src/components/Infrastructure/ResourceFacetSummary.tsx",
"frontend-modern/src/components/Infrastructure/ResourceOperatorStateSection.tsx",
"frontend-modern/src/components/Infrastructure/ResourcePolicySummary.tsx",
"frontend-modern/src/components/Infrastructure/UnifiedResourceHostTableCard.tsx",
"frontend-modern/src/components/Infrastructure/UnifiedResourcePBSTableSection.tsx",
"frontend-modern/src/components/Infrastructure/UnifiedResourcePMGTableSection.tsx",
"frontend-modern/src/components/Infrastructure/UnifiedResourceServiceInfrastructureCard.tsx",
"frontend-modern/src/components/Infrastructure/UnifiedResourceTable.tsx",
"frontend-modern/src/components/Infrastructure/unifiedResourceTableModel.ts",
"frontend-modern/src/components/Infrastructure/unifiedResourceTableStateModel.ts",
"frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDerivedState.ts",
"frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDockerActionsState.ts",
"frontend-modern/src/components/Infrastructure/useResourceDetailDrawerHistoryState.ts",
"frontend-modern/src/components/Infrastructure/useResourceDetailDrawerState.ts",
"frontend-modern/src/components/Infrastructure/useUnifiedResourceTableState.ts",
"frontend-modern/src/components/Infrastructure/useUnifiedResourceTableViewportSync.ts",
"frontend-modern/src/components/Kubernetes/K8sDeploymentsDrawer.tsx",
"frontend-modern/src/components/Kubernetes/K8sNamespacesDrawer.tsx",
"frontend-modern/src/features/docker/DockerAlertsTable.tsx",
@@ -8419,11 +8310,9 @@
"frontend-modern/src/components/Infrastructure/__tests__/resourceDetailDrawerOperationalModel.test.ts",
"frontend-modern/src/components/Infrastructure/__tests__/resourceDetailDrawerTrueNASModel.test.ts",
"frontend-modern/src/components/Infrastructure/__tests__/resourceDetailMappers.test.ts",
"frontend-modern/src/components/Infrastructure/__tests__/ResourceFacetSummary.test.tsx",
"frontend-modern/src/components/Infrastructure/__tests__/ResourceOperatorStateSection.test.ts",
"frontend-modern/src/components/Infrastructure/__tests__/ResourcePolicySummary.test.tsx",
"frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.performance.contract.test.tsx",
"frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.workloads-link.test.tsx",
"frontend-modern/src/components/Infrastructure/__tests__/unifiedResourceTableStateModel.test.ts",
"frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts",
"frontend-modern/src/features/docker/__tests__/DockerAlertsTable.test.tsx",
"frontend-modern/src/features/docker/__tests__/dockerContainerLifecycleActions.test.ts",
@@ -826,12 +826,6 @@ Compact Coverage age cells use the existing metadata-size text and reclaim numer
49. `frontend-modern/src/components/Infrastructure/ResourceCorrelationSummary.tsx`
50. `frontend-modern/src/components/Infrastructure/ResourceOperatorStateSection.tsx`
50a. `frontend-modern/src/components/Infrastructure/ResourcePolicySummary.tsx`
51. `frontend-modern/src/components/Infrastructure/UnifiedResourceHostTableCard.tsx`
52. `frontend-modern/src/components/Infrastructure/UnifiedResourcePBSTableSection.tsx`
53. `frontend-modern/src/components/Infrastructure/UnifiedResourcePMGTableSection.tsx`
54. `frontend-modern/src/components/Infrastructure/UnifiedResourceServiceInfrastructureCard.tsx`
55. `frontend-modern/src/components/Infrastructure/unifiedResourceTableModel.ts`
56. `frontend-modern/src/components/Infrastructure/unifiedResourceTableStateModel.ts`
57. `frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDerivedState.ts`
58. `frontend-modern/src/components/Infrastructure/resourceDetailDrawerServiceModel.ts`
59. `frontend-modern/src/components/Infrastructure/resourceDetailDrawerVmwareModel.ts`
@@ -841,8 +835,6 @@ Compact Coverage age cells use the existing metadata-size text and reclaim numer
62. `frontend-modern/src/components/Infrastructure/useResourceDetailDrawerHistoryState.ts`
63. `frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDockerActionsState.ts`
64. `frontend-modern/src/components/Infrastructure/useResourceDetailDrawerState.ts`
65. `frontend-modern/src/components/Infrastructure/useUnifiedResourceTableState.ts`
66. `frontend-modern/src/components/Infrastructure/useUnifiedResourceTableViewportSync.ts`
67. `frontend-modern/src/components/Discovery/discoveryReadiness.ts`
68. `frontend-modern/src/components/Discovery/DiscoveryTab.tsx`
69. `frontend-modern/src/components/Discovery/useDiscoveryTabState.ts`
@@ -1706,15 +1698,6 @@ container inventory table.
1. `frontend-modern/src/components/Infrastructure/infrastructureSelectors.ts` shared with `performance-and-scalability`: the infrastructure selector pipeline is both a canonical unified-resource consumer surface and a fleet-scale performance hot-path boundary.
2. `frontend-modern/src/components/Infrastructure/resourceDetailMappers.ts` shared with `performance-and-scalability`: resource detail mappers are both a canonical unified-resource consumer surface and a fleet-scale performance hot-path boundary.
3. `frontend-modern/src/components/Infrastructure/UnifiedResourceHostTableCard.tsx` shared with `performance-and-scalability`: the unified resource host table card is both a canonical unified-resource consumer surface and a fleet-scale performance hot-path boundary.
4. `frontend-modern/src/components/Infrastructure/UnifiedResourcePBSTableSection.tsx` shared with `performance-and-scalability`: the unified resource PBS section is both a canonical unified-resource consumer surface and a fleet-scale performance hot-path boundary.
5. `frontend-modern/src/components/Infrastructure/UnifiedResourcePMGTableSection.tsx` shared with `performance-and-scalability`: the unified resource PMG section is both a canonical unified-resource consumer surface and a fleet-scale performance hot-path boundary.
6. `frontend-modern/src/components/Infrastructure/UnifiedResourceServiceInfrastructureCard.tsx` shared with `performance-and-scalability`: the unified resource service infrastructure card is both a canonical unified-resource consumer surface and a fleet-scale performance hot-path boundary.
7. `frontend-modern/src/components/Infrastructure/UnifiedResourceTable.tsx` shared with `performance-and-scalability`: the unified resource table is both a canonical unified-resource consumer surface and a fleet-scale performance hot-path boundary.
8. `frontend-modern/src/components/Infrastructure/unifiedResourceTableModel.ts` shared with `performance-and-scalability`: unified resource service row shaping and I/O emphasis are both a canonical unified-resource consumer surface and a fleet-scale performance hot-path boundary.
9. `frontend-modern/src/components/Infrastructure/unifiedResourceTableStateModel.ts` shared with `performance-and-scalability`: unified resource table state derivation, sort-cycle policy, service sorting, and responsive column layout are both a canonical unified-resource consumer surface and a fleet-scale performance hot-path boundary.
10. `frontend-modern/src/components/Infrastructure/useUnifiedResourceTableState.ts` shared with `performance-and-scalability`: unified resource table state, grouping, and windowing are both a canonical unified-resource consumer surface and a fleet-scale performance hot-path boundary.
11. `frontend-modern/src/components/Infrastructure/useUnifiedResourceTableViewportSync.ts` shared with `performance-and-scalability`: unified resource table viewport sync and selected-row reveal are both a canonical unified-resource consumer surface and a fleet-scale performance hot-path boundary.
12. `frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx` shared with `storage-recovery`: Proxmox backup server table rows are both a storage/recovery backup-health surface and a unified-resource platform-table consumer boundary.
The table is composed on the Proxmox Backups tab and must not be duplicated
on Overview. The Backups placement keeps the canonical PBS row and drawer
@@ -2347,18 +2330,7 @@ provider identity or governed safe-summary posture when that context helps
an operator, but the rendered labels must stay product-neutral and use
`Analysis`, `Analysis Reasoning`, and `Safe Summary` rather than reviving
generic `AI` or `AI-Safe` branding inside the resource drawer or discovery
shell. 14. Keep the operator-facing unified resource table width-aware at the table
surface, not just at the browser viewport. `frontend-modern/src/components/Infrastructure/UnifiedResourceTable.tsx`
must route its root ref through `frontend-modern/src/components/Infrastructure/useUnifiedResourceTableState.ts`,
and `frontend-modern/src/components/Infrastructure/unifiedResourceTableStateModel.ts`
owns the column-priority breakpoints for host and service infrastructure
rows. When the app shell leaves tablet-sized space during live resize, the
table hides lower-priority metadata first. At phone width, the state model
must remove the old 640-pixel floor, preserve identity at exactly 30 percent
of the table, and allocate the remaining width across the bounded
source-relevant health and activity columns. Both the document and table
shell must remain free of horizontal overflow; desktop and tablet stages
retain their existing complete column contracts. 15. Keep shared policy-posture framing on the unified-resource card owner.
shell. 15. Keep shared policy-posture framing on the unified-resource card owner.
`frontend-modern/src/components/Infrastructure/ResourcePolicySummary.tsx`
may accept caller-owned subtitle or resource-count wording when Patrol or
another shared surface needs to explain how the same governed policy counts
@@ -2570,67 +2542,13 @@ application resource-provider or WebSocket lifecycle.
counts or capacity must use the shared `AnimatedNumber` primitive rather
than page-local counter state, so readout motion stays presentation-only and
canonical unified-resource identity and scope stay unchanged.
12. Keep infrastructure chart hover non-destructive to the unified-resource
table. If the hovered resource row is already visible in
`frontend-modern/src/components/Infrastructure/UnifiedResourceTable.tsx`,
the row may highlight in place through the shared active-resource id; if it
is off-screen, the page must offer an explicit `Jump to row` affordance
rather than auto-scrolling or collapsing the table on hover.
12a. Keep infrastructure summary visibility as display preference, not a
12. Keep infrastructure summary visibility as display preference, not a
unified-resource filter. Platform/runtime pages and shared infrastructure
summary consumers may hide or restore chart sections through shared
presentation controls, but those controls must not mutate resource
identity, table membership, source scope, or summary-hover state. The
retired top-level `/infrastructure` page and its saved-view/route-state
machinery must not be reintroduced for this purpose.
13. Keep infrastructure cluster headers as canonical summary scope. Grouped
headers in `frontend-modern/src/components/Infrastructure/UnifiedResourceHostTableCard.tsx`
must publish cluster scope from the same `ResourceGroup` / unified-resource
ids that power the table rows, and
`frontend-modern/src/components/Infrastructure/useInfrastructureSummaryState.ts`
must consume that scope through the shared page/group/entity interaction
contract rather than inventing infrastructure-local summary filters or
route-backed cluster hover state. Host and service infrastructure table
card frames must consume the frontend-primitives-owned `TableCard` wrapper;
unified-resource ownership remains on resource identity, grouping, and row
semantics rather than forking a table border/background shell. Deliberate
cluster focus must also stay
on the canonical infrastructure route through the shared `summaryGroup`
query state, so pinned scope is shareable, reversible, and owned by the
same route-backed summary contract as row focus. Infrastructure must stay
row-first here: the pinned cluster header remains the visible scoped
state, and explicit clearing belongs to the shared infrastructure table
card header action plus the shared `Escape` reset path rather than a
search-row fallback widget, page-level scope strip, or a second
scope/pinned pill inside the cluster row chrome. Background whitespace
clearing may remain a convenience, but infrastructure must not rely on it
as the only reversible control.
14. Keep infrastructure row emphasis on the shared frontend presentation
contract. Host, PBS, and PMG table sections may decide whether a resource
is contextually active, but they must expose that state through
`data-summary-row-active` and rely on the shared row presentation owned by
`frontend-modern/src/index.css` instead of provider-specific background
classes that drift across resource tables or hide inline metric bars.
Cluster-member rows must also expose shared preview-versus-pinned group
emphasis through `data-summary-group-member-active`, so the whole cluster
block reads as the active scope without inventing a second infrastructure-
local outline or banner treatment.
Static grouped cluster-header emphasis must route through
`frontend-modern/src/components/shared/groupedTableRowPresentation.ts` and
the shared `.grouped-table-row` CSS contract in `frontend-modern/src/index.css`,
rather than infrastructure-local background or hover-fill classes.
Summary-linked infrastructure rows and cluster headers must also route
pointer preview and focus preview through
`frontend-modern/src/components/shared/summaryInteractionA11y.ts`, while
deliberate expand/scope ownership must route through
`frontend-modern/src/components/shared/SummaryRowActionButton.tsx`, so the
unified-resource table does not fork mouse-only hover logic, focusable-row
button shims, touch-hostile synthetic hover, or provider-specific control
handling across host, PBS, and PMG sections. Those three resource rows are
themselves the compact touch disclosure target and must explicitly enable
the shared mobile-chevron suppression; their accessible row/button state
remains available without spending a visible summary-cell slot on a
duplicate control.
15. Keep infrastructure search aligned with the governed display label. Shared
infrastructure filtering through
`frontend-modern/src/components/Infrastructure/infrastructureSelectors.ts`
@@ -4015,8 +3933,6 @@ when the opened drawer would otherwise fall below the fold. That reveal must
scroll only enough of the infrastructure table to keep the row header plus the
start of the detail visible, not leave the drawer clipped and not hard-center
the selected row.
`useUnifiedResourceTableViewportSync.ts` must stay viewport-only; it may not
grow a second selected-row reveal path or a resource-local centering rule.
That same unified-resource boundary now also owns stored metrics-target
continuity for provider-backed resources. When registry rebuild cannot derive a
fresh metrics target from raw source facets, `internal/unifiedresources/registry.go`
@@ -4392,9 +4308,8 @@ raw backup, sync, verify, prune, and garbage job arrays travel through the
unified-resource metadata contract in `frontend-modern/src/types/resource.ts`
and `frontend-modern/src/components/Infrastructure/resourceDetailMappers.ts`,
`resourceDetailDrawerServiceModel.ts` owns active-task status classification and
shared activity wording, and both
`frontend-modern/src/components/Infrastructure/UnifiedResourcePBSTableSection.tsx`
and `frontend-modern/src/components/Infrastructure/ResourceDetailDrawerOverviewTab.tsx`
shared activity wording, and
`frontend-modern/src/components/Infrastructure/ResourceDetailDrawerOverviewTab.tsx`
must render from that shared projection instead of rescanning raw job arrays or
inventing local PBS status heuristics,
`resourceDetailDrawerIdentityModel.ts` owns the pure identity-card,
@@ -4562,30 +4477,9 @@ The shared node-state adapter also routes Proxmox cluster labels through that
same helper, so infrastructure summary projections keep the same canonical
cluster name as the rest of the unified resource model instead of rewriting
the label locally.
The unified resource table now routes reactive table-state composition,
grouping, and row-windowing through
`frontend-modern/src/components/Infrastructure/useUnifiedResourceTableState.ts`,
while pure table-state derivation, service sorting, sort-cycle policy, and
responsive column layout now route through
`frontend-modern/src/components/Infrastructure/unifiedResourceTableStateModel.ts`,
and viewport reveal plus scroll synchronization now route through
`frontend-modern/src/components/Infrastructure/useUnifiedResourceTableViewportSync.ts`,
so the shared consumer model is no longer interleaving selector derivation,
layout policy, and DOM viewport coordination inside one mixed state boundary.
That viewport controller must consume
`frontend-modern/src/components/shared/windowedPageScroll.ts` for scroll-
ancestor selection, wheel normalization, and listener lifecycle. Wheel may
prewarm the bounded row runway, but touch remains compositor-native and updates
the runway only through the passive native page-scroll event; unified-resource
tables must not register their own touch listener.
The mobile shell class from that shared state model now uses `min-w-full` for
the phone stage and restores the existing wider-stage floor above it. Mobile
column weights must sum to the available table width, retain the prioritized
identity and operational set, and avoid both local and document-level
horizontal scrolling. This is a phone projection only; wider stages keep the
established desktop distribution.
The native provider rollout now applies the same projection to Proxmox backup,
coverage, recoverable, Ceph, Mail Gateway, and replication rows; Docker native
Native provider tables keep a phone projection: prioritized identity and
operational columns sized to the available width, with no local or
document-level horizontal scrolling, for Proxmox backup, coverage, recoverable, Ceph, Mail Gateway, and replication rows; Docker native
and Swarm inventories; Kubernetes native inventories; TrueNAS systems and
workflows; vSphere hosts and workflows; and Standalone machines. The shared
`platform-table-phone-hidden` marker removes secondary or duplicate tracks at
@@ -4595,17 +4489,6 @@ demotion while promoting identity to a 40-percent track. Docker rows whose full
labels, image references, or placement values cannot fit now expose those
values in the same touch- and keyboard-operable inline detail pattern used by
the other platform tables rather than relying on hover titles.
That same unified-resource consumer contract now also owns CSP-safe table
presentation for infrastructure rows. Host, PBS, and PMG table sections must
consume the shared column presentation owner and render canonical table sizing
through classes plus DOM width/height attributes rather than lane-local inline
style objects, so the same unified-resource dataset can reach the public demo
without transport-specific DOM drift.
That same consumer contract now also owns full-width desktop balance for the
infrastructure tables. The shared column presentation owner must publish an
explicit desktop `Resource` width for host, PBS, and PMG sections so wide
shells redistribute surplus width across the remaining columns instead of
turning the first column into blank filler that hides metric density.
The canonical unified-resource change and relationship presenters now also
share the same elapsed-time and "ago" wording utilities, so `observed`,
`last seen`, and `ago` fragments stay consistent without each formatter
+2 -2
View File
@@ -7,8 +7,8 @@
"index": {
"baselineGzip": 150212
},
"sharedPlatformPage": {
"baselineGzip": 120645
"MetricBar": {
"baselineGzip": 120887
},
"Alerts": {
"baselineGzip": 120045
@@ -0,0 +1,93 @@
{
"version": 1,
"base_sha": "9aaac4d3e502c01913fb3637d0997b0ce03a8b7b",
"verified_at": "2026-10-06T19:05:00Z",
"result": "passed",
"changed_paths": [
"frontend-modern/src/components/Infrastructure/ClusterDeployBanner.tsx",
"frontend-modern/src/components/Infrastructure/UnifiedResourceHostTableCard.tsx",
"frontend-modern/src/components/Infrastructure/UnifiedResourcePBSTableSection.tsx",
"frontend-modern/src/components/Infrastructure/UnifiedResourcePMGTableSection.tsx",
"frontend-modern/src/components/Infrastructure/UnifiedResourceServiceInfrastructureCard.tsx",
"frontend-modern/src/components/Infrastructure/UnifiedResourceSourceBadgeCell.tsx",
"frontend-modern/src/components/Infrastructure/UnifiedResourceTable.tsx",
"frontend-modern/src/components/Infrastructure/unifiedResourceTableModel.ts",
"frontend-modern/src/components/Infrastructure/unifiedResourceTableStateModel.ts",
"frontend-modern/src/components/Infrastructure/useUnifiedResourceTableState.ts",
"frontend-modern/src/components/Infrastructure/useUnifiedResourceTableViewportSync.ts",
"frontend-modern/src/features/platformPage/sharedPlatformPage.tsx"
],
"content_sha256": {
"frontend-modern/src/components/Infrastructure/ClusterDeployBanner.tsx": "deleted",
"frontend-modern/src/components/Infrastructure/UnifiedResourceHostTableCard.tsx": "deleted",
"frontend-modern/src/components/Infrastructure/UnifiedResourcePBSTableSection.tsx": "deleted",
"frontend-modern/src/components/Infrastructure/UnifiedResourcePMGTableSection.tsx": "deleted",
"frontend-modern/src/components/Infrastructure/UnifiedResourceServiceInfrastructureCard.tsx": "deleted",
"frontend-modern/src/components/Infrastructure/UnifiedResourceSourceBadgeCell.tsx": "deleted",
"frontend-modern/src/components/Infrastructure/UnifiedResourceTable.tsx": "deleted",
"frontend-modern/src/components/Infrastructure/unifiedResourceTableModel.ts": "deleted",
"frontend-modern/src/components/Infrastructure/unifiedResourceTableStateModel.ts": "deleted",
"frontend-modern/src/components/Infrastructure/useUnifiedResourceTableState.ts": "deleted",
"frontend-modern/src/components/Infrastructure/useUnifiedResourceTableViewportSync.ts": "deleted",
"frontend-modern/src/features/platformPage/sharedPlatformPage.tsx": "3232e48158a03f69e98474731da49ceb5dd050e99fa167972849b7ccb61d8643"
},
"routes": [
"/proxmox/overview",
"/proxmox/storage",
"/proxmox/replication",
"/proxmox/backups",
"/proxmox/ceph",
"/proxmox/mail",
"/docker/overview",
"/docker/images",
"/docker/storage",
"/docker/networks",
"/docker/swarm",
"/kubernetes/overview",
"/kubernetes/nodes",
"/kubernetes/workloads",
"/kubernetes/services",
"/kubernetes/storage",
"/kubernetes/configuration",
"/kubernetes/events",
"/truenas/overview",
"/truenas/storage",
"/truenas/services",
"/truenas/apps",
"/truenas/vms",
"/truenas/shares",
"/truenas/protection",
"/vmware/overview",
"/vmware/storage",
"/vmware/networks",
"/vmware/health",
"/vmware/activity",
"/standalone/machines",
"/standalone/availability"
],
"viewports": [
{
"width": 1440,
"height": 900
},
{
"width": 390,
"height": 844
}
],
"states": [
"production vite build embedded in a private mock-mode backend (50 Proxmox nodes, Docker, Swarm, 3 Kubernetes clusters, TrueNAS, vSphere, agents)",
"every platform sub-tab mounts its page root and populated tables at desktop and phone width with zero document horizontal overflow",
"no element with data-testid infrastructure-table-surface renders on any route, before or after the change",
"no-match search empty state on Machines and Docker images",
"Machines and Docker images inline row detail open",
"before/after pair: the same row-detail interaction on the origin/main build shows the identical 8 pre-existing style-src-attr CSP violations from ResourceDetailDrawer overflow-anchor styles, so none come from this change"
],
"interactions": [
"signed in with the hot-dev credentials",
"visited every platform sub-tab discovered from each platform nav at 1440x900 and 390x844",
"typed a row-name prefix into the shared platform toolbar search on Machines and Docker images and confirmed the row count narrowed",
"typed a non-matching query and confirmed the no-match empty state, then cleared it and confirmed every row returned",
"clicked the first row to open the inline resource detail"
]
}
@@ -118,15 +118,6 @@
{
"path": "src/components/Alerts/AlertResourceTableRow.tsx"
},
{
"path": "src/components/Infrastructure/UnifiedResourceHostTableCard.tsx"
},
{
"path": "src/components/Infrastructure/UnifiedResourcePBSTableSection.tsx"
},
{
"path": "src/components/Infrastructure/UnifiedResourcePMGTableSection.tsx"
},
{
"path": "src/features/docker/DockerNativeTableShared.tsx"
},
@@ -212,9 +203,6 @@
"export": "InlineDetailTableRow"
},
"requiredConsumers": [
{ "path": "src/components/Infrastructure/UnifiedResourceHostTableCard.tsx" },
{ "path": "src/components/Infrastructure/UnifiedResourcePBSTableSection.tsx" },
{ "path": "src/components/Infrastructure/UnifiedResourcePMGTableSection.tsx" },
{ "path": "src/components/Workloads/WorkloadPanel.tsx" },
{ "path": "src/features/docker/DockerAlertsTable.tsx" },
{ "path": "src/features/docker/DockerHostsTable.tsx" },
@@ -413,8 +401,6 @@
"export": "TableCard"
},
"requiredConsumers": [
{ "path": "src/components/Infrastructure/UnifiedResourceHostTableCard.tsx" },
{ "path": "src/components/Infrastructure/UnifiedResourceServiceInfrastructureCard.tsx" },
{ "path": "src/components/Storage/StorageContentCard.tsx" },
{ "path": "src/components/Workloads/WorkloadsSurface.tsx" },
{ "path": "src/components/Workloads/WorkloadsTable.tsx" },
@@ -435,8 +421,6 @@
"export": "TableCardHeader"
},
"requiredConsumers": [
{ "path": "src/components/Infrastructure/UnifiedResourceHostTableCard.tsx" },
{ "path": "src/components/Infrastructure/UnifiedResourceServiceInfrastructureCard.tsx" },
{ "path": "src/components/Storage/StorageContentCard.tsx" },
{ "path": "src/features/platformPage/sharedPlatformPage.tsx" }
],
@@ -1,74 +0,0 @@
import { Component, Show, createMemo } from 'solid-js';
import type { ResourceGroup } from '@/components/Infrastructure/infrastructureSelectors';
import InfoIcon from 'lucide-solid/icons/info';
import RocketIcon from 'lucide-solid/icons/rocket';
interface ClusterDeployBannerProps {
group: ResourceGroup;
onDeploy: (clusterId: string, clusterName: string) => void;
}
/**
* Compact inline banner shown in cluster group headers when reachable PVE
* nodes lack a Pulse Agent and a source agent is available to deploy from.
*/
export const ClusterDeployBanner: Component<ClusterDeployBannerProps> = (props) => {
// Visibility conditions:
// 1. Non-empty cluster name (not Standalone)
// 2. At least one resource has platformType === 'proxmox-pve'
// 3. At least one resource has agent?.agentId (source agent exists)
// 4. At least one *reachable* PVE node does NOT have an agent
//
// Offline nodes are excluded: we can't deploy a Pulse Unified Agent to
// a node we can't reach, and a node going temporarily offline is not
// the same situation as a never-onboarded cluster peer.
const deployInfo = createMemo(() => {
const resources = props.group.resources;
const cluster = props.group.cluster;
if (!cluster) return null;
// Only consider actual PVE node resources (not VMs/containers).
const pveNodes = resources.filter(
(r) => r.type === 'agent' && r.platformType === 'proxmox-pve',
);
if (pveNodes.length === 0) return null;
const hasSourceAgent = pveNodes.some((r) => r.agent?.agentId);
if (!hasSourceAgent) return null;
const deployableCount = pveNodes.filter(
(r) => !r.agent?.agentId && r.status !== 'offline',
).length;
if (deployableCount === 0) return null;
return { cluster, deployableCount };
});
return (
<Show when={deployInfo()}>
{(info) => (
<div class="flex items-center gap-2 mt-1">
<div class="flex items-center gap-1.5 text-[11px] text-blue-600 dark:text-blue-400">
<InfoIcon class="w-3 h-3" />
<span>
{info().deployableCount} node{info().deployableCount !== 1 ? 's' : ''} ready for Pulse
Agent
</span>
</div>
<button
type="button"
onClick={(e) => {
e.stopPropagation();
props.onDeploy(info().cluster, info().cluster);
}}
class="inline-flex items-center gap-1 rounded-sm px-1.5 py-0.5 text-[10px] font-medium text-blue-700 bg-blue-100 hover:bg-blue-200 dark:text-blue-300 dark:bg-blue-900/25 dark:hover:bg-blue-800 transition-colors"
>
<RocketIcon class="w-2.5 h-2.5" />
Review & Deploy
</button>
</div>
)}
</Show>
);
};
@@ -1,799 +0,0 @@
import { For, Show, createMemo } from 'solid-js';
import type { Component } from 'solid-js';
import {
formatBytes,
formatSpeed,
formatObservedSpeed,
formatUptime,
getResourceDiskSummary,
normalizeDiskArray,
} from '@/utils/format';
import { formatTemperature, getTemperatureTextClass } from '@/utils/temperature';
import {
GROUPED_TABLE_ROW_BADGE_CLASS,
GROUPED_TABLE_ROW_META_CLASS,
getGroupedTableRowCellClass,
getInteractiveGroupedTableRowClass,
} from '@/components/shared/groupedTableRowPresentation';
import { InlineDetailTableRow } from '@/components/shared/InlineDetailTableRow';
import { TableCardHeader } from '@/components/shared/TableCardHeader';
import { TableCard } from '@/components/shared/TableCard';
import { hostOverrideIdCandidates } from '@/features/alerts/alertOverridesModel';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/shared/Table';
import { StatusDot } from '@/components/shared/StatusDot';
import { ResponsiveMetricCell } from '@/components/shared/responsive';
import { StackedDiskBar } from '@/components/Workloads/StackedDiskBar';
import { StackedMemoryBar } from '@/components/Workloads/StackedMemoryBar';
import { buildMetricKeyForUnifiedResource } from '@/utils/metricsKeys';
import {
getInfrastructureSystemIdentityBadges,
getInfrastructureSystemTitleBadges,
getPlatformBadge,
getSourceBadge,
getUnifiedSourceBadges,
} from '@/utils/resourceBadgePresentation';
import { getAgentStatusIndicator } from '@/utils/status';
import { getPreferredInfrastructureDisplayName } from '@/utils/resourceIdentity';
import {
getResourcePolicyTableBadges,
shouldShowResourceAlternateName,
} from '@/utils/resourcePolicyPresentation';
import { getAvailabilityProbePresentation } from '@/utils/availabilityProbePresentation';
import { ResourceDetailDrawer } from './ResourceDetailDrawer';
import { getResourceHealthIssuePresentation } from './resourceHealthPresentation';
import { ClusterDeployBanner } from './ClusterDeployBanner';
import { ResourceFacetSummary } from './ResourceFacetSummary';
import { UnifiedResourceSourceBadgeCell } from './UnifiedResourceSourceBadgeCell';
import type { SummarySeriesGroupScope } from '@/components/shared/summaryCardInteraction';
import { resolveSummaryGroupMemberInteractionState } from '@/components/shared/summaryCardInteraction';
import {
buildSummaryDisclosureControlsId,
createSummaryInteractiveRowPreviewHandlers,
} from '@/components/shared/summaryInteractionA11y';
import { SummaryRowActionButton } from '@/components/shared/SummaryRowActionButton';
import { ResourceNameWithWebInterfaceLink } from '@/components/shared/WebInterfaceLink';
import {
type UnifiedResourceTableProps,
type UnifiedResourceTableState,
} from './useUnifiedResourceTableState';
import { shouldShowClusterGroupTypeLabel } from './unifiedResourceTableStateModel';
import { getOutlierEmphasis, isResourceOnline } from './unifiedResourceTableModel';
import { type Resource, getCpuPercent, getMemoryPercent } from '@/types/resource';
interface UnifiedResourceHostTableCardProps {
tableProps: UnifiedResourceTableProps;
table: UnifiedResourceTableState;
}
type ThermalPressurePresentation = {
label: string;
title: string;
className: string;
};
const getThermalPressurePresentation = (resource: Resource): ThermalPressurePresentation | null => {
const thermalState = resource.agent?.sensors?.thermalState;
if (!thermalState) return null;
const pressure = thermalState?.pressure?.trim().toLowerCase();
if (!pressure) return null;
const label =
pressure === 'nominal' ? 'Nominal' : pressure === 'constrained' ? 'Constrained' : 'Unknown';
const className =
pressure === 'nominal'
? 'text-emerald-600 dark:text-emerald-400'
: pressure === 'constrained'
? 'text-amber-600 dark:text-amber-400'
: 'text-slate-500 dark:text-slate-400';
const limits = Object.entries(thermalState.limitsPercent ?? {})
.filter(([, value]) => Number.isFinite(value) && value < 100)
.sort(([a], [b]) => a.localeCompare(b))
.map(([key, value]) => `${key.replace(/_/g, ' ')} ${Math.round(value)}%`);
const source = thermalState.source ? ` via ${thermalState.source}` : '';
const limitSummary = limits.length > 0 ? ` · limits: ${limits.join(', ')}` : '';
return {
label,
className,
title: `Thermal pressure ${label.toLowerCase()}${source}${limitSummary}`,
};
};
export const UnifiedResourceHostTableCard: Component<UnifiedResourceHostTableCardProps> = (
props,
) => {
const { table, tableProps } = props;
return (
<Show when={table.showHostTable()}>
<TableCard class="mb-0">
<TableCardHeader
title="Agent Infrastructure"
showClearAction={table.showHostClearAction()}
onClear={tableProps.clearPinnedSummaryScope}
/>
<Table class={`whitespace-nowrap ${table.tableShellClass()}`}>
<TableHeader>
<TableRow class="bg-surface-alt text-muted border-b border-border">
<TableHead
class={`text-left pl-2 sm:pl-3 ${table.resourceColumn().className}`}
width={table.resourceColumn().width}
onClick={() => table.handleSort('name')}
>
{table.headerLabels().resource} {table.renderSortIndicator('name')}
</TableHead>
<TableHead
class={table.metricColumn().className}
width={table.metricColumn().width}
onClick={() => table.handleSort('cpu')}
>
{table.headerLabels().cpu} {table.renderSortIndicator('cpu')}
</TableHead>
<TableHead
class={table.metricColumn().className}
width={table.metricColumn().width}
onClick={() => table.handleSort('memory')}
>
{table.headerLabels().memory} {table.renderSortIndicator('memory')}
</TableHead>
<TableHead
class={table.metricColumn().className}
width={table.metricColumn().width}
onClick={() => table.handleSort('disk')}
>
{table.headerLabels().disk} {table.renderSortIndicator('disk')}
</TableHead>
<TableHead
classList={{ hidden: !table.isMobile() && !table.isVisible('secondary') }}
class={table.ioColumn().className}
width={table.ioColumn().width}
onClick={() => table.handleSort('network')}
>
{table.headerLabels().network} {table.renderSortIndicator('network')}
</TableHead>
<TableHead
classList={{ hidden: table.isMobile() || !table.isHostDiskIoVisible() }}
class={table.ioColumn().className}
width={table.ioColumn().width}
onClick={() => table.handleSort('diskio')}
>
{table.headerLabels().diskIo} {table.renderSortIndicator('diskio')}
</TableHead>
<TableHead
classList={{ hidden: table.isMobile() || !table.isVisible('secondary') }}
class={table.sourceColumn().className}
width={table.sourceColumn().width}
onClick={() => table.handleSort('source')}
>
{table.headerLabels().source} {table.renderSortIndicator('source')}
</TableHead>
<TableHead
classList={{ hidden: !table.isMobile() && !table.isVisible('supplementary') }}
class={table.uptimeColumn().className}
width={table.uptimeColumn().width}
onClick={() => table.handleSort('uptime')}
>
{table.headerLabels().uptime} {table.renderSortIndicator('uptime')}
</TableHead>
<TableHead
classList={{ hidden: table.isMobile() || !table.isVisible('supplementary') }}
class={table.tempColumn().className}
width={table.tempColumn().width}
onClick={() => table.handleSort('temp')}
>
{table.headerLabels().temp} {table.renderSortIndicator('temp')}
</TableHead>
</TableRow>
</TableHeader>
<TableBody ref={table.setHostBodyRef}>
<Show when={table.hostWindowing.isWindowed() && table.hostTopSpacerHeight() > 0}>
<TableRow aria-hidden="true">
<TableCell colspan={9} class="border-0 p-0" height={table.hostTopSpacerHeight()} />
</TableRow>
</Show>
<For each={table.visibleHostTableItems()}>
{(item) => {
if (item.type === 'header') {
const group = item.group;
const groupSummaryScope = createMemo<SummarySeriesGroupScope | null>(() =>
table.buildHostSummaryGroupScope(group),
);
const isSummaryGroupHighlighted = createMemo(
() => tableProps.activeSummaryGroupScope?.id === groupSummaryScope()?.id,
);
const handleGroupHoverChange = (next: SummarySeriesGroupScope | null) => {
tableProps.onGroupHoverChange?.(next);
};
const handleGroupFocusToggle = () => {
const nextScope = groupSummaryScope();
tableProps.onGroupFocusChange?.(
nextScope && tableProps.focusedSummaryGroupId === nextScope.id
? null
: (nextScope?.id ?? null),
);
};
const groupRowInteraction = createSummaryInteractiveRowPreviewHandlers({
onPreview: () => handleGroupHoverChange(groupSummaryScope()),
onPreviewClear: () => handleGroupHoverChange(null),
});
return (
<TableRow
class={getInteractiveGroupedTableRowClass()}
data-summary-group-id={groupSummaryScope()?.id ?? undefined}
data-summary-group-series-count={String(
groupSummaryScope()?.seriesIds.length ?? 0,
)}
data-summary-row-active={isSummaryGroupHighlighted() ? 'true' : 'false'}
onClick={handleGroupFocusToggle}
{...groupRowInteraction}
>
<TableCell colspan={9} class={getGroupedTableRowCellClass()}>
<div class="flex items-center gap-2">
<Show
when={group.cluster}
fallback={<span class="text-muted">Standalone</span>}
>
<span>{group.cluster}</span>
<Show when={shouldShowClusterGroupTypeLabel(group.cluster)}>
<span class={GROUPED_TABLE_ROW_BADGE_CLASS}>Cluster</span>
</Show>
</Show>
<span class={GROUPED_TABLE_ROW_META_CLASS}>
{group.resources.length}{' '}
{group.resources.length === 1 ? 'resource' : 'resources'}
</span>
</div>
<Show when={tableProps.onDeployCluster}>
<ClusterDeployBanner
group={group}
onDeploy={tableProps.onDeployCluster!}
/>
</Show>
</TableCell>
</TableRow>
);
}
const resource = item.resource;
const isExpanded = createMemo(() => tableProps.expandedResourceId === resource.id);
const isHighlighted = createMemo(
() => tableProps.highlightedResourceId === resource.id,
);
const displayName = createMemo(() =>
getPreferredInfrastructureDisplayName(resource),
);
const summaryGroupMemberState = createMemo(() =>
resolveSummaryGroupMemberInteractionState({
seriesId: resource.id,
hoveredGroupScope: tableProps.hoveredSummaryGroupScope,
focusedGroupScope: tableProps.focusedSummaryGroupScope,
}),
);
const statusIndicator = createMemo(() =>
getAgentStatusIndicator({ status: resource.status }),
);
const healthIssue = createMemo(() => getResourceHealthIssuePresentation(resource));
const metricsKey = createMemo(() => buildMetricKeyForUnifiedResource(resource));
const alertResourceIdCandidates = createMemo(() =>
hostOverrideIdCandidates(resource),
);
const cpuThresholds = createMemo(() =>
table.getMetricThresholds('agent', 'cpu', alertResourceIdCandidates()),
);
const memoryThresholds = createMemo(() =>
table.getMetricThresholds('agent', 'memory', alertResourceIdCandidates()),
);
const diskThresholds = createMemo(() =>
table.getMetricThresholds('agent', 'disk', alertResourceIdCandidates()),
);
const temperatureThresholds = createMemo(() =>
table.getMetricThresholds('node', 'temperature', alertResourceIdCandidates()),
);
const detailControlsId = createMemo(() =>
buildSummaryDisclosureControlsId(resource.id),
);
const cpuPercentValue = createMemo(() =>
resource.cpu ? Math.round(getCpuPercent(resource)) : null,
);
const memoryPercentValue = createMemo(() =>
resource.memory ? Math.round(getMemoryPercent(resource)) : null,
);
const diskSummary = createMemo(() => getResourceDiskSummary(resource));
const diskPercentValue = createMemo(() => {
const summary = diskSummary();
return summary ? Math.round(summary.usage) : null;
});
const memorySublabel = createMemo(() => {
if (
!resource.memory ||
resource.memory.used === undefined ||
resource.memory.total === undefined
)
return undefined;
return `${formatBytes(resource.memory.used)}/${formatBytes(resource.memory.total)}`;
});
const diskSublabel = createMemo(() => {
const summary = diskSummary();
if (!summary) return undefined;
return `${formatBytes(summary.used)}/${formatBytes(summary.total)}`;
});
const networkTotal = createMemo(
() => (resource.network?.rxBytes ?? 0) + (resource.network?.txBytes ?? 0),
);
const networkEmphasis = createMemo(() =>
getOutlierEmphasis(networkTotal(), table.ioScale().network),
);
const diskIOTotal = createMemo(() =>
resource.diskIO?.readRate !== undefined &&
resource.diskIO?.writeRate !== undefined
? resource.diskIO.readRate + resource.diskIO.writeRate
: NaN,
);
const diskIOEmphasis = createMemo(() =>
getOutlierEmphasis(diskIOTotal(), table.ioScale().diskIO),
);
const rowClass = createMemo(() => {
const baseHover =
'cursor-pointer transition-all duration-200 relative group hover:bg-surface-hover';
if (isExpanded()) {
return 'cursor-pointer transition-all duration-200 relative z-10 group bg-blue-50 dark:bg-blue-900/25';
}
let className = baseHover;
if (!isResourceOnline(resource)) {
className += ' opacity-60';
}
return className;
});
const platformBadge = createMemo(() => getPlatformBadge(resource.platformType));
const sourceBadge = createMemo(() => getSourceBadge(resource.sourceType));
const unifiedSources = createMemo(() => table.getUnifiedSources(resource));
const sourceBadges = createMemo(() => getUnifiedSourceBadges(unifiedSources()));
const systemBadges = createMemo(() =>
getInfrastructureSystemIdentityBadges(resource),
);
const systemTitleBadges = createMemo(() =>
getInfrastructureSystemTitleBadges(systemBadges(), sourceBadges()),
);
const policyBadges = createMemo(() =>
getResourcePolicyTableBadges(resource.policy),
);
const availabilityProbe = createMemo(() =>
getAvailabilityProbePresentation(resource),
);
const thermalPressure = createMemo(() => getThermalPressurePresentation(resource));
const resourceRowInteraction = createSummaryInteractiveRowPreviewHandlers({
onPreview: () => tableProps.onHoverChange?.(resource.id),
onPreviewClear: () => tableProps.onHoverChange?.(null),
});
return (
<>
<TableRow
data-row-id={resource.id}
data-summary-series-id={resource.id}
data-summary-group-member-active={
summaryGroupMemberState() !== 'default'
? summaryGroupMemberState()
: undefined
}
data-summary-row-active={
(tableProps.hoveredResourceId === resource.id || isHighlighted()) &&
!isExpanded()
? 'true'
: 'false'
}
class={`${rowClass()} h-8`}
onClick={() => table.toggleExpand(resource.id)}
{...resourceRowInteraction}
>
<TableCell
class={`pr-1.5 sm:pr-2 py-0.5 align-middle overflow-hidden pl-2 sm:pl-3 ${table.resourceColumn().className}`}
width={table.resourceColumn().width}
>
<div class="flex items-center gap-1.5 min-w-0">
<SummaryRowActionButton
kind="disclosure"
subjectLabel={displayName()}
expanded={isExpanded()}
controlsId={detailControlsId()}
hideWhenRowTappableOnMobile
onAction={() => table.toggleExpand(resource.id)}
onPreviewClear={() => tableProps.onHoverChange?.(null)}
/>
<StatusDot
variant={statusIndicator().variant}
title={healthIssue()?.title || statusIndicator().label}
ariaLabel={healthIssue()?.title || statusIndicator().label}
size="xs"
/>
<div class="flex min-w-0 flex-1 flex-col">
<div class="flex min-w-0 items-center gap-1">
<ResourceNameWithWebInterfaceLink
name={displayName()}
url={resource.customUrl}
class="min-w-0 flex-1"
nameClass="block min-w-0 flex-1 truncate font-medium text-[11px] text-base-content select-text"
/>
<Show when={healthIssue()}>
{(issue) => (
<span
class="hidden shrink-0 whitespace-nowrap rounded-sm bg-amber-100 px-1 text-[9px] font-medium text-amber-700 dark:bg-amber-900/25 dark:text-amber-300 lg:inline"
title={issue().title}
>
{issue().compactLabel}
</span>
)}
</Show>
<Show when={availabilityProbe()}>
{(probe) => (
<span
class={`hidden shrink-0 whitespace-nowrap rounded-sm px-1 text-[9px] font-medium lg:inline ${probe().toneClassName}`}
title={probe().detailLabel}
>
{probe().methodLabel}
</span>
)}
</Show>
<Show when={shouldShowResourceAlternateName(resource)}>
<span class="hidden min-w-0 max-w-[28%] shrink truncate text-[9px] text-muted lg:inline">
({resource.name})
</span>
</Show>
<For each={policyBadges()}>
{(badge) => (
<span
class={`${badge.className} max-w-[44%] shrink-0 overflow-hidden px-1`}
title={badge.title}
>
<span class="min-w-0 truncate">{badge.label}</span>
</span>
)}
</For>
<Show when={policyBadges().length === 0}>
<ResourceFacetSummary
recentChanges={resource.recentChanges}
counts={resource.facetCounts}
maxVisibleBadges={1}
class="hidden max-w-[48%] shrink-0 flex-nowrap! overflow-hidden lg:flex"
/>
</Show>
</div>
</div>
</div>
</TableCell>
<TableCell>
<Show
when={cpuPercentValue() !== null}
fallback={
<div class="flex justify-center">
<span class="text-xs" aria-hidden="true">
—
</span>
</div>
}
>
<ResponsiveMetricCell
class="w-full"
value={cpuPercentValue() ?? 0}
type="cpu"
resourceId={table.isMobile() ? undefined : metricsKey()}
isRunning={isResourceOnline(resource)}
showMobile={false}
thresholds={cpuThresholds()}
/>
</Show>
</TableCell>
<TableCell>
<Show
when={memoryPercentValue() !== null}
fallback={
<div class="flex justify-center">
<span class="text-xs" aria-hidden="true">
—
</span>
</div>
}
>
<div class="w-full" title={memorySublabel()}>
<StackedMemoryBar
used={resource.memory?.used ?? 0}
total={resource.memory?.total ?? 0}
percentOnly={
!resource.memory?.total && resource.memory?.current != null
? resource.memory.current
: undefined
}
swapUsed={resource.agent?.memory?.swapUsed ?? 0}
swapTotal={resource.agent?.memory?.swapTotal ?? 0}
thresholds={memoryThresholds()}
/>
</div>
</Show>
</TableCell>
<TableCell>
<Show
when={diskPercentValue() !== null}
fallback={
<div class="flex justify-center">
<span class="text-xs" aria-hidden="true">
—
</span>
</div>
}
>
<div class="w-full" title={diskSublabel()}>
<StackedDiskBar
disks={normalizeDiskArray(resource.agent?.disks)}
aggregateDisk={diskSummary()}
thresholds={diskThresholds()}
/>
</div>
</Show>
</TableCell>
<TableCell
classList={{ hidden: !table.isMobile() && !table.isVisible('secondary') }}
>
<Show
when={resource.network}
fallback={
<Show
when={availabilityProbe()}
fallback={
<div class="text-center">
<span class="text-xs text-slate-400" aria-hidden="true">
—
</span>
</div>
}
>
{(probe) => (
<div
class="mx-auto inline-flex max-w-full min-w-0 items-baseline justify-center gap-1 whitespace-nowrap text-[11px] leading-tight"
title={probe().detailLabel}
>
<Show
when={probe().targetLabel}
fallback={
<span
class={`font-medium tabular-nums ${probe().toneClassName}`}
>
{probe().resultLabel}
</span>
}
>
{(targetLabel) => (
<>
<span class="min-w-0 truncate text-muted">
{targetLabel()}:
</span>
<span
class={`shrink-0 font-medium tabular-nums ${probe().toneClassName}`}
>
{probe().resultLabel}
</span>
</>
)}
</Show>
<span
class={`hidden shrink-0 text-[9px] xl:inline ${probe().toneClassName}`}
>
{probe().freshnessLabel}
</span>
</div>
)}
</Show>
}
>
<div
class={
table.layoutMode() === 'wide'
? 'grid w-full grid-cols-[0.75rem_minmax(0,1fr)_0.75rem_minmax(0,1fr)] items-center gap-x-1 text-[11px] tabular-nums'
: 'grid w-full grid-cols-[0.75rem_minmax(0,1fr)] items-center gap-x-1 text-[10px] leading-tight tabular-nums'
}
>
<span class="inline-flex w-3 justify-center text-emerald-500">↓</span>
<span
class={`min-w-0 overflow-hidden text-ellipsis whitespace-nowrap ${networkEmphasis().className}`}
title={
networkEmphasis().showOutlierHint
? `${formatSpeed(resource.network!.rxBytes)} (Top outlier)`
: formatSpeed(resource.network!.rxBytes)
}
>
{formatSpeed(resource.network!.rxBytes)}
</span>
<span class="inline-flex w-3 justify-center text-orange-400">↑</span>
<span
class={`min-w-0 overflow-hidden text-ellipsis whitespace-nowrap ${networkEmphasis().className}`}
title={
networkEmphasis().showOutlierHint
? `${formatSpeed(resource.network!.txBytes)} (Top outlier)`
: formatSpeed(resource.network!.txBytes)
}
>
{formatSpeed(resource.network!.txBytes)}
</span>
</div>
</Show>
</TableCell>
<TableCell
classList={{
hidden: table.isMobile() || !table.isHostDiskIoVisible(),
}}
>
<Show
when={resource.diskIO}
fallback={
<div class="text-center">
<span class="text-xs text-slate-400" aria-hidden="true">
—
</span>
</div>
}
>
<div
class={
table.layoutMode() === 'wide'
? 'grid w-full grid-cols-[0.75rem_minmax(0,1fr)_0.75rem_minmax(0,1fr)] items-center gap-x-1 text-[11px] tabular-nums'
: 'grid w-full grid-cols-[0.75rem_minmax(0,1fr)] items-center gap-x-1 text-[10px] leading-tight tabular-nums'
}
>
<span class="inline-flex w-3 justify-center font-mono text-blue-500">
R
</span>
<span
class={`min-w-0 overflow-hidden text-ellipsis whitespace-nowrap ${diskIOEmphasis().className}`}
title={
diskIOEmphasis().showOutlierHint
? `${formatObservedSpeed(resource.diskIO!.readRate)} (Top outlier)`
: formatObservedSpeed(resource.diskIO!.readRate)
}
>
{formatObservedSpeed(resource.diskIO!.readRate)}
</span>
<span class="inline-flex w-3 justify-center font-mono text-amber-500">
W
</span>
<span
class={`min-w-0 overflow-hidden text-ellipsis whitespace-nowrap ${diskIOEmphasis().className}`}
title={
diskIOEmphasis().showOutlierHint
? `${formatObservedSpeed(resource.diskIO!.writeRate)} (Top outlier)`
: formatObservedSpeed(resource.diskIO!.writeRate)
}
>
{formatObservedSpeed(resource.diskIO!.writeRate)}
</span>
</div>
</Show>
</TableCell>
<TableCell
classList={{ hidden: table.isMobile() || !table.isVisible('secondary') }}
>
<UnifiedResourceSourceBadgeCell
unifiedBadges={systemBadges()}
platformBadge={platformBadge()}
sourceBadge={sourceBadge()}
titleBadges={systemTitleBadges()}
layoutMode={table.layoutMode()}
/>
</TableCell>
<TableCell
classList={{
hidden: !table.isMobile() && !table.isVisible('supplementary'),
}}
>
<div class="flex justify-center">
<Show
when={resource.uptime}
fallback={
<span class="text-xs text-slate-400" aria-hidden="true">
—
</span>
}
>
<span class="text-xs text-base-content whitespace-nowrap">
{formatUptime(resource.uptime ?? 0)}
</span>
</Show>
</div>
</TableCell>
<TableCell
classList={{
hidden: table.isMobile() || !table.isVisible('supplementary'),
}}
>
<div class="flex justify-center">
<Show
when={resource.temperature != null}
fallback={
<Show
when={thermalPressure()}
fallback={
<span class="text-xs text-slate-400" aria-hidden="true">
—
</span>
}
>
{(thermal) => (
<span
class={`text-[11px] whitespace-nowrap font-medium ${thermal().className}`}
title={thermal().title}
>
{thermal().label}
</span>
)}
</Show>
}
>
<span
class={`text-xs whitespace-nowrap font-medium ${getTemperatureTextClass(
resource.temperature,
temperatureThresholds(),
)}`}
>
{formatTemperature(resource.temperature)}
</span>
</Show>
</div>
</TableCell>
</TableRow>
<Show when={isExpanded()}>
<InlineDetailTableRow
cellId={detailControlsId()}
cellClass="border-border-subtle shadow-inner"
colspan={9}
contentClass="px-4 py-4"
data-inline-detail-for={resource.id}
>
<ResourceDetailDrawer
resource={resource}
resolveResourceLabel={table.resolveResourceLabel}
onClose={() => tableProps.onExpandedResourceChange(null)}
/>
</InlineDetailTableRow>
</Show>
</>
);
}}
</For>
<Show when={table.hostWindowing.isWindowed() && table.hostBottomSpacerHeight() > 0}>
<TableRow aria-hidden="true">
<TableCell
colspan={9}
class="border-0 p-0"
height={table.hostBottomSpacerHeight()}
/>
</TableRow>
</Show>
</TableBody>
</Table>
</TableCard>
</Show>
);
};
@@ -1,353 +0,0 @@
import { Show, createMemo } from 'solid-js';
import type { Component } from 'solid-js';
import { formatUptime } from '@/utils/format';
import { InlineDetailTableRow } from '@/components/shared/InlineDetailTableRow';
import { StatusDot } from '@/components/shared/StatusDot';
import {
buildSummaryDisclosureControlsId,
createSummaryInteractiveRowPreviewHandlers,
} from '@/components/shared/summaryInteractionA11y';
import { SummaryRowActionButton } from '@/components/shared/SummaryRowActionButton';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/shared/Table';
import {
getInfrastructureSystemIdentityBadges,
getInfrastructureSystemTitleBadges,
getPlatformBadge,
getSourceBadge,
getUnifiedSourceBadges,
} from '@/utils/resourceBadgePresentation';
import { getAgentStatusIndicator } from '@/utils/status';
import { getServiceHealthSummaryPresentation } from '@/utils/serviceHealthPresentation';
import { getPreferredInfrastructureDisplayName } from '@/utils/resourceIdentity';
import { shouldShowResourceAlternateName } from '@/utils/resourcePolicyPresentation';
import { ResourceDetailDrawer } from './ResourceDetailDrawer';
import { ResourceFacetSummary } from './ResourceFacetSummary';
import { UnifiedResourceSourceBadgeCell } from './UnifiedResourceSourceBadgeCell';
import {
type UnifiedResourceTableProps,
type UnifiedResourceTableState,
} from './useUnifiedResourceTableState';
import { getPBSTableRow, isResourceOnline } from './unifiedResourceTableModel';
import { buildServiceDetailLinks } from './serviceDetailLinks';
import { ResourceNameWithWebInterfaceLink } from '@/components/shared/WebInterfaceLink';
import { PlatformWindowedRows } from '@/features/platformPage/PlatformWindowedRows';
interface UnifiedResourcePBSTableSectionProps {
tableProps: UnifiedResourceTableProps;
table: UnifiedResourceTableState;
}
export const UnifiedResourcePBSTableSection: Component<UnifiedResourcePBSTableSectionProps> = (
props,
) => {
const { table, tableProps } = props;
const isServiceMobileColumnVisible = (priority: 'primary' | 'secondary') =>
table.isMobile() || table.isServiceVisible(priority);
return (
<Show when={table.sortedPBSResources().length > 0}>
<div class="border-b border-border bg-surface-hover px-3 py-2 text-[11px] font-semibold uppercase tracking-wide text-muted">
PBS Services
</div>
<Table class={`whitespace-nowrap ${table.tableShellClass()}`}>
<TableHeader>
<TableRow class="bg-surface-alt text-muted border-b border-border">
<TableHead
class={`text-left pl-2 sm:pl-3 ${table.serviceResourceColumn().className}`}
width={table.serviceResourceColumn().width}
>
{table.headerLabels().resource}
</TableHead>
<TableHead
classList={{ hidden: !isServiceMobileColumnVisible('primary') }}
class={table.serviceCountColumn().className}
width={table.serviceCountColumn().width}
>
{table.headerLabels().datastores}
</TableHead>
<TableHead
classList={{ hidden: !isServiceMobileColumnVisible('secondary') }}
class={table.serviceCountColumn().className}
width={table.serviceCountColumn().width}
>
{table.headerLabels().activity}
</TableHead>
<TableHead
class={table.serviceHealthColumn().className}
width={table.serviceHealthColumn().width}
>
{table.headerLabels().health}
</TableHead>
<TableHead
classList={{ hidden: !isServiceMobileColumnVisible('secondary') }}
class={table.serviceSourceColumn().className}
width={table.serviceSourceColumn().width}
>
{table.headerLabels().source}
</TableHead>
<TableHead
classList={{ hidden: !table.isServiceVisible('supplementary') }}
class={table.uptimeColumn().className}
width={table.uptimeColumn().width}
>
{table.headerLabels().uptime}
</TableHead>
<TableHead
class={table.serviceActionColumn().className}
width={table.serviceActionColumn().width}
>
{table.headerLabels().action}
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<PlatformWindowedRows items={table.sortedPBSResources} estimatedRowHeight={40}>
{(resource) => {
const isExpanded = createMemo(() => tableProps.expandedResourceId === resource.id);
const isHighlighted = createMemo(
() => tableProps.highlightedResourceId === resource.id,
);
const displayName = createMemo(() => getPreferredInfrastructureDisplayName(resource));
const serviceLink = createMemo(() => buildServiceDetailLinks(resource)[0] ?? null);
const statusIndicator = createMemo(() =>
getAgentStatusIndicator({ status: resource.status }),
);
const pbsRow = createMemo(() => getPBSTableRow(resource));
const platformBadge = createMemo(() => getPlatformBadge(resource.platformType));
const sourceBadge = createMemo(() => getSourceBadge(resource.sourceType));
const unifiedSources = createMemo(() => table.getUnifiedSources(resource));
const sourceBadges = createMemo(() => getUnifiedSourceBadges(unifiedSources()));
const systemBadges = createMemo(() =>
getInfrastructureSystemIdentityBadges(resource),
);
const systemTitleBadges = createMemo(() =>
getInfrastructureSystemTitleBadges(systemBadges(), sourceBadges()),
);
const healthClass = createMemo(
() =>
getServiceHealthSummaryPresentation(resource.status, pbsRow()?.health).textClass,
);
const activityClass = createMemo(() =>
(pbsRow()?.activeTaskCount || 0) > 0
? 'text-[11px] font-semibold text-emerald-700 dark:text-emerald-300'
: 'text-xs text-base-content',
);
const rowClass = createMemo(() => {
const baseBorder = 'border-b border-border-subtle';
const baseHover = `cursor-pointer transition-all duration-200 relative hover:shadow-xs group ${baseBorder}`;
if (isExpanded()) {
return `cursor-pointer transition-all duration-200 relative hover:shadow-xs z-10 group bg-blue-50 dark:bg-blue-900/25 ${baseBorder}`;
}
let className = baseHover;
if (!isResourceOnline(resource)) {
className += ' opacity-60';
}
return className;
});
const detailControlsId = createMemo(() =>
buildSummaryDisclosureControlsId(resource.id),
);
const resourceRowInteraction = createSummaryInteractiveRowPreviewHandlers({
onPreview: () => tableProps.onHoverChange?.(resource.id),
onPreviewClear: () => tableProps.onHoverChange?.(null),
});
return (
<>
<TableRow
data-summary-series-id={resource.id}
data-summary-row-active={
(tableProps.hoveredResourceId === resource.id || isHighlighted()) &&
!isExpanded()
? 'true'
: 'false'
}
class={`${rowClass()} h-8`}
onClick={() => table.toggleExpand(resource.id)}
{...resourceRowInteraction}
>
<TableCell class="pr-1.5 sm:pr-2 py-0.5 align-middle overflow-hidden pl-2 sm:pl-3">
<div class="flex items-center gap-1.5 min-w-0">
<SummaryRowActionButton
kind="disclosure"
subjectLabel={displayName()}
expanded={isExpanded()}
controlsId={detailControlsId()}
hideWhenRowTappableOnMobile
onAction={() => table.toggleExpand(resource.id)}
onPreviewClear={() => tableProps.onHoverChange?.(null)}
/>
<StatusDot
variant={statusIndicator().variant}
title={statusIndicator().label}
ariaLabel={statusIndicator().label}
size="xs"
/>
<ResourceNameWithWebInterfaceLink
name={displayName()}
url={resource.customUrl}
class="min-w-0 flex-1"
nameClass="block min-w-0 flex-1 truncate font-medium text-[11px] text-base-content select-text"
/>
<Show when={shouldShowResourceAlternateName(resource)}>
<span class="hidden min-w-0 max-w-[35%] shrink truncate text-[9px] text-muted lg:inline">
({resource.name})
</span>
</Show>
<ResourceFacetSummary
recentChanges={resource.recentChanges}
counts={resource.facetCounts}
maxVisibleBadges={1}
class="hidden max-w-[48%] shrink-0 flex-nowrap! overflow-hidden lg:flex"
/>
</div>
</TableCell>
<TableCell classList={{ hidden: !isServiceMobileColumnVisible('primary') }}>
<div class="flex justify-center">
<Show
when={pbsRow()?.datastores != null}
fallback={
<span class="text-xs text-slate-400" aria-hidden="true">
—
</span>
}
>
<span class="text-xs text-base-content">{pbsRow()!.datastores}</span>
</Show>
</div>
</TableCell>
<TableCell classList={{ hidden: !isServiceMobileColumnVisible('secondary') }}>
<div class="flex justify-center">
<Show
when={pbsRow()?.activity}
fallback={
<span class="text-xs text-slate-400" aria-hidden="true">
—
</span>
}
>
<div
class="flex flex-col items-center leading-tight"
title={[pbsRow()!.activity, pbsRow()!.activityDetail]
.filter(Boolean)
.join(' · ')}
>
<span class={activityClass()}>{pbsRow()!.activity}</span>
<Show when={pbsRow()?.activityDetail}>
<span class="text-[10px] text-muted">{pbsRow()!.activityDetail}</span>
</Show>
</div>
</Show>
</div>
</TableCell>
<TableCell>
<div class="flex justify-center">
<Show
when={pbsRow()?.health}
fallback={
<span class="text-xs text-slate-400" aria-hidden="true">
—
</span>
}
>
<span class={`text-xs font-medium ${healthClass()}`}>
{pbsRow()!.health}
</span>
</Show>
</div>
</TableCell>
<TableCell classList={{ hidden: !isServiceMobileColumnVisible('secondary') }}>
<UnifiedResourceSourceBadgeCell
unifiedBadges={systemBadges()}
platformBadge={platformBadge()}
sourceBadge={sourceBadge()}
titleBadges={systemTitleBadges()}
layoutMode={table.layoutMode()}
/>
</TableCell>
<TableCell
classList={{
hidden: !table.isServiceVisible('supplementary'),
}}
>
<div class="flex justify-center">
<Show
when={resource.uptime}
fallback={
<span class="text-xs text-slate-400" aria-hidden="true">
—
</span>
}
>
<span class="text-xs text-base-content whitespace-nowrap">
{formatUptime(resource.uptime ?? 0)}
</span>
</Show>
</div>
</TableCell>
<TableCell>
<div class="flex justify-center">
<Show
when={serviceLink()}
fallback={
<span class="text-xs text-slate-400" aria-hidden="true">
—
</span>
}
>
{(link) => (
<a
href={link().href}
class="inline-flex items-center rounded-sm border border-blue-200 bg-blue-50 px-1.5 py-0.5 text-[10px] font-medium text-blue-700 transition-colors hover:bg-blue-100 dark:border-blue-700 dark:bg-blue-900/25 dark:text-blue-200 dark:hover:bg-blue-800"
title={link().label}
aria-label={link().ariaLabel}
onClick={(event) => event.stopPropagation()}
>
{link().compactLabel}
</a>
)}
</Show>
</div>
</TableCell>
</TableRow>
<Show when={isExpanded()}>
<InlineDetailTableRow
cellId={detailControlsId()}
cellClass="border-border-subtle shadow-inner"
colspan={7}
contentClass="px-4 py-4"
data-inline-detail-for={resource.id}
>
<ResourceDetailDrawer
resource={resource}
resolveResourceLabel={table.resolveResourceLabel}
onClose={() => tableProps.onExpandedResourceChange(null)}
/>
</InlineDetailTableRow>
</Show>
</>
);
}}
</PlatformWindowedRows>
</TableBody>
</Table>
</Show>
);
};
@@ -1,393 +0,0 @@
import { Show, createMemo } from 'solid-js';
import type { Component } from 'solid-js';
import { formatUptime } from '@/utils/format';
import { InlineDetailTableRow } from '@/components/shared/InlineDetailTableRow';
import { StatusDot } from '@/components/shared/StatusDot';
import {
buildSummaryDisclosureControlsId,
createSummaryInteractiveRowPreviewHandlers,
} from '@/components/shared/summaryInteractionA11y';
import { SummaryRowActionButton } from '@/components/shared/SummaryRowActionButton';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/shared/Table';
import {
getInfrastructureSystemIdentityBadges,
getInfrastructureSystemTitleBadges,
getPlatformBadge,
getSourceBadge,
getUnifiedSourceBadges,
} from '@/utils/resourceBadgePresentation';
import { getAgentStatusIndicator } from '@/utils/status';
import { getServiceHealthSummaryPresentation } from '@/utils/serviceHealthPresentation';
import { getPreferredInfrastructureDisplayName } from '@/utils/resourceIdentity';
import { shouldShowResourceAlternateName } from '@/utils/resourcePolicyPresentation';
import { ResourceDetailDrawer } from './ResourceDetailDrawer';
import { ResourceFacetSummary } from './ResourceFacetSummary';
import { UnifiedResourceSourceBadgeCell } from './UnifiedResourceSourceBadgeCell';
import {
type UnifiedResourceTableProps,
type UnifiedResourceTableState,
} from './useUnifiedResourceTableState';
import { getPMGTableRow, isResourceOnline } from './unifiedResourceTableModel';
import { buildServiceDetailLinks } from './serviceDetailLinks';
import { ResourceNameWithWebInterfaceLink } from '@/components/shared/WebInterfaceLink';
import { PlatformWindowedRows } from '@/features/platformPage/PlatformWindowedRows';
interface UnifiedResourcePMGTableSectionProps {
tableProps: UnifiedResourceTableProps;
table: UnifiedResourceTableState;
}
export const UnifiedResourcePMGTableSection: Component<UnifiedResourcePMGTableSectionProps> = (
props,
) => {
const { table, tableProps } = props;
const isServiceMobileColumnVisible = (priority: 'primary' | 'secondary') =>
table.isMobile() || table.isServiceVisible(priority);
return (
<Show when={table.sortedPMGResources().length > 0}>
<div class="border-b border-border bg-surface-hover px-3 py-2 text-[11px] font-semibold uppercase tracking-wide text-muted">
PMG Services
</div>
<Table class={`whitespace-nowrap ${table.tableShellClass()}`}>
<TableHeader>
<TableRow class="bg-surface-alt text-muted border-b border-border">
<TableHead
class={`text-left pl-2 sm:pl-3 ${table.serviceResourceColumn().className}`}
width={table.serviceResourceColumn().width}
>
{table.headerLabels().resource}
</TableHead>
<TableHead
classList={{ hidden: !isServiceMobileColumnVisible('primary') }}
class={table.serviceQueueColumn().className}
width={table.serviceQueueColumn().width}
>
{table.headerLabels().queue}
</TableHead>
<TableHead
classList={{ hidden: !isServiceMobileColumnVisible('secondary') }}
class={table.serviceQueueColumn().className}
width={table.serviceQueueColumn().width}
>
{table.headerLabels().deferred}
</TableHead>
<TableHead
classList={{ hidden: !table.isServiceVisible('detailed') }}
class={table.serviceQueueColumn().className}
width={table.serviceQueueColumn().width}
>
{table.headerLabels().hold}
</TableHead>
<TableHead
classList={{ hidden: !isServiceMobileColumnVisible('secondary') }}
class={table.serviceCountColumn().className}
width={table.serviceCountColumn().width}
>
{table.headerLabels().nodes}
</TableHead>
<TableHead
class={table.serviceHealthColumn().className}
width={table.serviceHealthColumn().width}
>
{table.headerLabels().health}
</TableHead>
<TableHead
classList={{ hidden: !table.isServiceVisible('secondary') }}
class={table.serviceSourceColumn().className}
width={table.serviceSourceColumn().width}
>
{table.headerLabels().source}
</TableHead>
<TableHead
classList={{ hidden: !table.isServiceVisible('supplementary') }}
class={table.uptimeColumn().className}
width={table.uptimeColumn().width}
>
{table.headerLabels().uptime}
</TableHead>
<TableHead
class={table.serviceActionColumn().className}
width={table.serviceActionColumn().width}
>
{table.headerLabels().action}
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<PlatformWindowedRows items={table.sortedPMGResources} estimatedRowHeight={40}>
{(resource) => {
const isExpanded = createMemo(() => tableProps.expandedResourceId === resource.id);
const isHighlighted = createMemo(
() => tableProps.highlightedResourceId === resource.id,
);
const displayName = createMemo(() => getPreferredInfrastructureDisplayName(resource));
const serviceLink = createMemo(() => buildServiceDetailLinks(resource)[0] ?? null);
const statusIndicator = createMemo(() =>
getAgentStatusIndicator({ status: resource.status }),
);
const pmgRow = createMemo(() => getPMGTableRow(resource));
const platformBadge = createMemo(() => getPlatformBadge(resource.platformType));
const sourceBadge = createMemo(() => getSourceBadge(resource.sourceType));
const unifiedSources = createMemo(() => table.getUnifiedSources(resource));
const sourceBadges = createMemo(() => getUnifiedSourceBadges(unifiedSources()));
const systemBadges = createMemo(() =>
getInfrastructureSystemIdentityBadges(resource),
);
const systemTitleBadges = createMemo(() =>
getInfrastructureSystemTitleBadges(systemBadges(), sourceBadges()),
);
const healthClass = createMemo(
() =>
getServiceHealthSummaryPresentation(resource.status, pmgRow()?.health).textClass,
);
const queueClass = createMemo(() =>
(pmgRow()?.deferred || 0) + (pmgRow()?.hold || 0) > 0
? 'text-amber-600 dark:text-amber-400'
: 'text-base-content',
);
const rowClass = createMemo(() => {
const baseBorder = 'border-b border-border-subtle';
const baseHover = `cursor-pointer transition-all duration-200 relative hover:shadow-xs group ${baseBorder}`;
if (isExpanded()) {
return `cursor-pointer transition-all duration-200 relative hover:shadow-xs z-10 group bg-blue-50 dark:bg-blue-900/25 ${baseBorder}`;
}
let className = baseHover;
if (!isResourceOnline(resource)) {
className += ' opacity-60';
}
return className;
});
const detailControlsId = createMemo(() =>
buildSummaryDisclosureControlsId(resource.id),
);
const resourceRowInteraction = createSummaryInteractiveRowPreviewHandlers({
onPreview: () => tableProps.onHoverChange?.(resource.id),
onPreviewClear: () => tableProps.onHoverChange?.(null),
});
return (
<>
<TableRow
data-summary-series-id={resource.id}
data-summary-row-active={
(tableProps.hoveredResourceId === resource.id || isHighlighted()) &&
!isExpanded()
? 'true'
: 'false'
}
class={`${rowClass()} h-8`}
onClick={() => table.toggleExpand(resource.id)}
{...resourceRowInteraction}
>
<TableCell class="pr-1.5 sm:pr-2 py-0.5 align-middle overflow-hidden pl-2 sm:pl-3">
<div class="flex items-center gap-1.5 min-w-0">
<SummaryRowActionButton
kind="disclosure"
subjectLabel={displayName()}
expanded={isExpanded()}
controlsId={detailControlsId()}
hideWhenRowTappableOnMobile
onAction={() => table.toggleExpand(resource.id)}
onPreviewClear={() => tableProps.onHoverChange?.(null)}
/>
<StatusDot
variant={statusIndicator().variant}
title={statusIndicator().label}
ariaLabel={statusIndicator().label}
size="xs"
/>
<ResourceNameWithWebInterfaceLink
name={displayName()}
url={resource.customUrl}
class="min-w-0 flex-1"
nameClass="block min-w-0 flex-1 truncate font-medium text-[11px] text-base-content select-text"
/>
<Show when={shouldShowResourceAlternateName(resource)}>
<span class="hidden min-w-0 max-w-[35%] shrink truncate text-[9px] text-muted lg:inline">
({resource.name})
</span>
</Show>
<ResourceFacetSummary
recentChanges={resource.recentChanges}
counts={resource.facetCounts}
maxVisibleBadges={1}
class="hidden max-w-[48%] shrink-0 flex-nowrap! overflow-hidden lg:flex"
/>
</div>
</TableCell>
<TableCell classList={{ hidden: !isServiceMobileColumnVisible('primary') }}>
<div class="flex justify-center">
<Show
when={pmgRow()?.queue != null}
fallback={
<span class="text-xs text-slate-400" aria-hidden="true">
—
</span>
}
>
<span class={`text-xs font-medium ${queueClass()}`}>
{pmgRow()!.queue}
</span>
</Show>
</div>
</TableCell>
<TableCell classList={{ hidden: !isServiceMobileColumnVisible('secondary') }}>
<div class="flex justify-center">
<Show
when={pmgRow()?.deferred != null}
fallback={
<span class="text-xs text-slate-400" aria-hidden="true">
—
</span>
}
>
<span class="text-xs text-base-content">{pmgRow()!.deferred}</span>
</Show>
</div>
</TableCell>
<TableCell
classList={{
hidden: !table.isServiceVisible('detailed'),
}}
>
<div class="flex justify-center">
<Show
when={pmgRow()?.hold != null}
fallback={
<span class="text-xs text-slate-400" aria-hidden="true">
—
</span>
}
>
<span class="text-xs text-base-content">{pmgRow()!.hold}</span>
</Show>
</div>
</TableCell>
<TableCell classList={{ hidden: !isServiceMobileColumnVisible('secondary') }}>
<div class="flex justify-center">
<Show
when={pmgRow()?.nodes != null}
fallback={
<span class="text-xs text-slate-400" aria-hidden="true">
—
</span>
}
>
<span class="text-xs text-base-content">{pmgRow()!.nodes}</span>
</Show>
</div>
</TableCell>
<TableCell>
<div class="flex justify-center">
<Show
when={pmgRow()?.health}
fallback={
<span class="text-xs text-slate-400" aria-hidden="true">
—
</span>
}
>
<span class={`text-xs font-medium ${healthClass()}`}>
{pmgRow()!.health}
</span>
</Show>
</div>
</TableCell>
<TableCell classList={{ hidden: !table.isServiceVisible('secondary') }}>
<UnifiedResourceSourceBadgeCell
unifiedBadges={systemBadges()}
platformBadge={platformBadge()}
sourceBadge={sourceBadge()}
titleBadges={systemTitleBadges()}
layoutMode={table.layoutMode()}
/>
</TableCell>
<TableCell
classList={{
hidden: !table.isServiceVisible('supplementary'),
}}
>
<div class="flex justify-center">
<Show
when={resource.uptime}
fallback={
<span class="text-xs text-slate-400" aria-hidden="true">
—
</span>
}
>
<span class="text-xs text-base-content whitespace-nowrap">
{formatUptime(resource.uptime ?? 0)}
</span>
</Show>
</div>
</TableCell>
<TableCell>
<div class="flex justify-center">
<Show
when={serviceLink()}
fallback={
<span class="text-xs text-slate-400" aria-hidden="true">
—
</span>
}
>
{(link) => (
<a
href={link().href}
class="inline-flex items-center rounded-sm border border-blue-200 bg-blue-50 px-1.5 py-0.5 text-[10px] font-medium text-blue-700 transition-colors hover:bg-blue-100 dark:border-blue-700 dark:bg-blue-900/25 dark:text-blue-200 dark:hover:bg-blue-800"
title={link().label}
aria-label={link().ariaLabel}
onClick={(event) => event.stopPropagation()}
>
{link().compactLabel}
</a>
)}
</Show>
</div>
</TableCell>
</TableRow>
<Show when={isExpanded()}>
<InlineDetailTableRow
cellId={detailControlsId()}
cellClass="border-border-subtle shadow-inner"
colspan={9}
contentClass="px-4 py-4"
data-inline-detail-for={resource.id}
>
<ResourceDetailDrawer
resource={resource}
resolveResourceLabel={table.resolveResourceLabel}
onClose={() => tableProps.onExpandedResourceChange(null)}
/>
</InlineDetailTableRow>
</Show>
</>
);
}}
</PlatformWindowedRows>
</TableBody>
</Table>
</Show>
);
};
@@ -1,35 +0,0 @@
import { Show } from 'solid-js';
import type { Component } from 'solid-js';
import { TableCardHeader } from '@/components/shared/TableCardHeader';
import { TableCard } from '@/components/shared/TableCard';
import {
type UnifiedResourceTableProps,
type UnifiedResourceTableState,
} from './useUnifiedResourceTableState';
import { UnifiedResourcePBSTableSection } from './UnifiedResourcePBSTableSection';
import { UnifiedResourcePMGTableSection } from './UnifiedResourcePMGTableSection';
interface UnifiedResourceServiceInfrastructureCardProps {
tableProps: UnifiedResourceTableProps;
table: UnifiedResourceTableState;
}
export const UnifiedResourceServiceInfrastructureCard: Component<
UnifiedResourceServiceInfrastructureCardProps
> = (props) => {
const { table, tableProps } = props;
return (
<Show when={table.sortedPBSResources().length > 0 || table.sortedPMGResources().length > 0}>
<TableCard class="mb-0">
<TableCardHeader
title="Service Infrastructure"
showClearAction={table.showServiceClearAction()}
onClear={tableProps.clearPinnedSummaryScope}
/>
<UnifiedResourcePBSTableSection tableProps={tableProps} table={table} />
<UnifiedResourcePMGTableSection tableProps={tableProps} table={table} />
</TableCard>
</Show>
);
};
@@ -1,72 +0,0 @@
import { For, Show, createMemo } from 'solid-js';
import type { Component } from 'solid-js';
import type { ResourceBadge } from '@/utils/resourceBadgePresentation';
import type { UnifiedResourceTableLayoutMode } from './unifiedResourceTableStateModel';
interface UnifiedResourceSourceBadgeCellProps {
unifiedBadges: ResourceBadge[];
platformBadge: ResourceBadge | null;
sourceBadge: ResourceBadge | null;
titleBadges?: ResourceBadge[];
layoutMode: UnifiedResourceTableLayoutMode;
}
const getVisibleSourceBadgeLimit = (layoutMode: UnifiedResourceTableLayoutMode): number =>
layoutMode === 'wide' ? 3 : 2;
export const UnifiedResourceSourceBadgeCell: Component<UnifiedResourceSourceBadgeCellProps> = (
props,
) => {
const badges = createMemo(() => {
if (props.unifiedBadges.length > 0) return props.unifiedBadges;
return [props.platformBadge, props.platformBadge ? null : props.sourceBadge].filter(
(badge): badge is ResourceBadge => Boolean(badge),
);
});
const titleBadges = createMemo(() =>
props.titleBadges && props.titleBadges.length > 0 ? props.titleBadges : badges(),
);
const visibleBadges = createMemo(() =>
badges().slice(0, getVisibleSourceBadgeLimit(props.layoutMode)),
);
const hiddenBadges = createMemo(() => badges().slice(visibleBadges().length));
const hiddenBadgeCount = createMemo(() => Math.max(0, badges().length - visibleBadges().length));
const hiddenBadgeLabel = createMemo(() =>
hiddenBadgeCount() === 1 ? `+${hiddenBadges()[0]?.label ?? ''}` : `+${hiddenBadgeCount()}`,
);
const title = createMemo(() =>
titleBadges()
.map((badge) => badge.title ?? badge.label)
.join(', '),
);
return (
<div
class="flex min-w-0 max-w-full items-center justify-center gap-1 overflow-hidden"
aria-label={title() ? `System: ${title()}` : undefined}
title={title()}
>
<For each={visibleBadges()}>
{(badge) => (
<span
class={`${badge.classes} min-w-0 max-w-full overflow-hidden px-1`}
title={badge.title}
>
<span class="min-w-0 truncate">{badge.label}</span>
</span>
)}
</For>
<Show when={hiddenBadgeCount() > 0}>
<span
class="inline-flex min-w-0 max-w-full items-center overflow-hidden rounded-sm bg-surface-alt px-1 py-0.5 text-[10px] font-medium text-muted"
aria-label={`Additional systems: ${hiddenBadges()
.map((badge) => badge.title ?? badge.label)
.join(', ')}`}
title={title()}
>
<span class="min-w-0 truncate">{hiddenBadgeLabel()}</span>
</span>
</Show>
</div>
);
};
@@ -1,25 +0,0 @@
import type { Component } from 'solid-js';
import {
useUnifiedResourceTableState,
type UnifiedResourceTableProps,
} from './useUnifiedResourceTableState';
import { UnifiedResourceHostTableCard } from './UnifiedResourceHostTableCard';
import { UnifiedResourceServiceInfrastructureCard } from './UnifiedResourceServiceInfrastructureCard';
export const UnifiedResourceTable: Component<UnifiedResourceTableProps> = (props) => {
const table = useUnifiedResourceTableState(props);
return (
<div
ref={table.setTableRootRef}
class="space-y-4"
data-summary-clear-surface
data-testid="infrastructure-table-surface"
>
<UnifiedResourceHostTableCard table={table} tableProps={props} />
<UnifiedResourceServiceInfrastructureCard table={table} tableProps={props} />
</div>
);
};
export default UnifiedResourceTable;
@@ -1,235 +0,0 @@
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
import { render, screen, fireEvent, cleanup } from '@solidjs/testing-library';
/* ── hoisted mocks ────────────────────────────────────────────── */
vi.mock('lucide-solid/icons/info', () => ({
default: () => <span data-testid="info-icon" />,
}));
vi.mock('lucide-solid/icons/rocket', () => ({
default: () => <span data-testid="rocket-icon" />,
}));
/* ── component import (after mocks) ──────────────────────────── */
import { ClusterDeployBanner } from '../ClusterDeployBanner';
import type { ResourceGroup } from '../infrastructureSelectors';
import type { Resource } from '@/types/resource';
/* ── helpers ──────────────────────────────────────────────────── */
/** Minimal PVE node resource with optional agent and overridable status */
function makePveNode(
id: string,
agentId?: string,
status: Resource['status'] = 'online',
): Resource {
return {
id,
type: 'agent',
name: id,
displayName: id,
platformId: 'pve1',
platformType: 'proxmox-pve',
sourceType: 'api',
status,
agent: agentId ? { agentId } : undefined,
} as Resource;
}
/** Non-PVE resource (e.g. a VM) */
function makeVm(id: string): Resource {
return {
id,
type: 'vm',
name: id,
displayName: id,
platformId: 'pve1',
platformType: 'proxmox-pve',
sourceType: 'api',
status: 'running',
} as Resource;
}
/** Non-PVE platform node (e.g. k8s) with optional agent */
function makeK8sNode(id: string, agentId?: string): Resource {
return {
id,
type: 'k8s-node',
name: id,
displayName: id,
platformId: 'k8s1',
platformType: 'kubernetes',
sourceType: 'agent',
status: 'online',
agent: agentId ? { agentId } : undefined,
} as Resource;
}
function makeGroup(cluster: string, resources: Resource[]): ResourceGroup {
return { cluster, resources };
}
/* ── lifecycle ────────────────────────────────────────────────── */
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
cleanup();
});
/* ── tests ────────────────────────────────────────────────────── */
describe('ClusterDeployBanner', () => {
describe('visibility conditions', () => {
it('renders nothing when group has no cluster name', () => {
const group = makeGroup('', [makePveNode('node1', 'agent-1'), makePveNode('node2')]);
render(() => <ClusterDeployBanner group={group} onDeploy={vi.fn()} />);
expect(screen.queryByText('Review & Deploy')).not.toBeInTheDocument();
});
it('renders nothing when there are no PVE nodes', () => {
const group = makeGroup('my-cluster', [makeVm('vm1'), makeK8sNode('k8s-node1')]);
render(() => <ClusterDeployBanner group={group} onDeploy={vi.fn()} />);
expect(screen.queryByText('Review & Deploy')).not.toBeInTheDocument();
});
it('renders nothing when no PVE node has a source agent', () => {
const group = makeGroup('my-cluster', [
makePveNode('node1'), // no agent
makePveNode('node2'), // no agent
]);
render(() => <ClusterDeployBanner group={group} onDeploy={vi.fn()} />);
expect(screen.queryByText('Review & Deploy')).not.toBeInTheDocument();
});
it('renders nothing when all PVE nodes already have agents', () => {
const group = makeGroup('my-cluster', [
makePveNode('node1', 'agent-1'),
makePveNode('node2', 'agent-2'),
]);
render(() => <ClusterDeployBanner group={group} onDeploy={vi.fn()} />);
expect(screen.queryByText('Review & Deploy')).not.toBeInTheDocument();
});
it('renders when there is a mix of monitored and unmonitored PVE nodes', () => {
const group = makeGroup('my-cluster', [
makePveNode('node1', 'agent-1'), // source agent
makePveNode('node2'), // unmonitored
]);
render(() => <ClusterDeployBanner group={group} onDeploy={vi.fn()} />);
expect(screen.getByText('1 node ready for Pulse Agent')).toBeInTheDocument();
expect(screen.getByText('Review & Deploy')).toBeInTheDocument();
});
it('does not count offline no-agent nodes (cannot deploy to an unreachable host)', () => {
const group = makeGroup('my-cluster', [
makePveNode('node1', 'agent-1'), // source agent
makePveNode('offline-node', undefined, 'offline'), // offline, no agent
]);
render(() => <ClusterDeployBanner group={group} onDeploy={vi.fn()} />);
expect(screen.queryByText(/ready for Pulse Agent/)).not.toBeInTheDocument();
expect(screen.queryByText('Review & Deploy')).not.toBeInTheDocument();
});
it('still counts online no-agent nodes when other no-agent nodes are offline', () => {
const group = makeGroup('my-cluster', [
makePveNode('node1', 'agent-1'),
makePveNode('online-no-agent'),
makePveNode('offline-no-agent', undefined, 'offline'),
]);
render(() => <ClusterDeployBanner group={group} onDeploy={vi.fn()} />);
expect(screen.getByText('1 node ready for Pulse Agent')).toBeInTheDocument();
});
});
describe('deployable count display', () => {
it('shows singular "node" for 1 deployable', () => {
const group = makeGroup('cluster-a', [makePveNode('node1', 'agent-1'), makePveNode('node2')]);
render(() => <ClusterDeployBanner group={group} onDeploy={vi.fn()} />);
expect(screen.getByText('1 node ready for Pulse Agent')).toBeInTheDocument();
});
it('shows plural "nodes" for multiple deployable', () => {
const group = makeGroup('cluster-a', [
makePveNode('node1', 'agent-1'),
makePveNode('node2'),
makePveNode('node3'),
makePveNode('node4'),
]);
render(() => <ClusterDeployBanner group={group} onDeploy={vi.fn()} />);
expect(screen.getByText('3 nodes ready for Pulse Agent')).toBeInTheDocument();
});
});
describe('deploy button', () => {
it('calls onDeploy with cluster name when clicked', () => {
const onDeploy = vi.fn();
const group = makeGroup('prod-cluster', [
makePveNode('node1', 'agent-1'),
makePveNode('node2'),
]);
render(() => <ClusterDeployBanner group={group} onDeploy={onDeploy} />);
fireEvent.click(screen.getByText('Review & Deploy'));
expect(onDeploy).toHaveBeenCalledTimes(1);
expect(onDeploy).toHaveBeenCalledWith('prod-cluster', 'prod-cluster');
});
it('stops event propagation on click', () => {
const parentHandler = vi.fn();
const group = makeGroup('my-cluster', [
makePveNode('node1', 'agent-1'),
makePveNode('node2'),
]);
render(() => (
<div onClick={parentHandler}>
<ClusterDeployBanner group={group} onDeploy={vi.fn()} />
</div>
));
fireEvent.click(screen.getByText('Review & Deploy'));
expect(parentHandler).not.toHaveBeenCalled();
});
});
describe('edge cases', () => {
it('ignores VMs when computing PVE node counts', () => {
const group = makeGroup('my-cluster', [
makePveNode('node1', 'agent-1'),
makeVm('vm1'),
makePveNode('node2'),
]);
render(() => <ClusterDeployBanner group={group} onDeploy={vi.fn()} />);
expect(screen.getByText('1 node ready for Pulse Agent')).toBeInTheDocument();
});
it('ignores non-PVE platform nodes', () => {
const group = makeGroup('my-cluster', [
makePveNode('pve-node1', 'agent-1'),
makeK8sNode('k8s-node1'),
makePveNode('pve-node2'),
]);
render(() => <ClusterDeployBanner group={group} onDeploy={vi.fn()} />);
expect(screen.getByText('1 node ready for Pulse Agent')).toBeInTheDocument();
});
it('does not treat non-PVE agent as a PVE source agent', () => {
// A k8s node with an agent should NOT satisfy the source-agent requirement for PVE
const group = makeGroup('my-cluster', [
makePveNode('pve-node1'), // PVE node, no agent
makePveNode('pve-node2'), // PVE node, no agent
makeK8sNode('k8s-node1', 'k8s-agent-1'), // non-PVE node with agent
]);
render(() => <ClusterDeployBanner group={group} onDeploy={vi.fn()} />);
expect(screen.queryByText('Review & Deploy')).not.toBeInTheDocument();
});
it('renders icons', () => {
const group = makeGroup('my-cluster', [
makePveNode('node1', 'agent-1'),
makePveNode('node2'),
]);
render(() => <ClusterDeployBanner group={group} onDeploy={vi.fn()} />);
expect(screen.getByTestId('info-icon')).toBeInTheDocument();
expect(screen.getByTestId('rocket-icon')).toBeInTheDocument();
});
});
});
@@ -0,0 +1,69 @@
import { describe, expect, it } from 'vitest';
import { render } from '@solidjs/testing-library';
import { ResourceFacetSummary } from '@/components/Infrastructure/ResourceFacetSummary';
describe('ResourceFacetSummary', () => {
it('renders timeline-only badges for canonical resource counts', () => {
const { getByText, queryByText } = render(() => (
<ResourceFacetSummary
counts={{
recentChanges: 3,
recentChangeKinds: {
restart: 2,
config_update: 1,
metric_anomaly: 1,
},
recentChangeSourceTypes: {
platform_event: 1,
pulse_diff: 2,
},
recentChangeSourceAdapters: {
docker_adapter: 2,
proxmox_adapter: 1,
},
}}
recentChanges={[]}
/>
));
expect(getByText('Timeline 3')).toBeInTheDocument();
expect(getByText('Restart 2')).toBeInTheDocument();
expect(getByText('Config update 1')).toBeInTheDocument();
expect(getByText('Anomaly 1')).toBeInTheDocument();
expect(getByText('Platform event 1')).toBeInTheDocument();
expect(getByText('Pulse diff 2')).toBeInTheDocument();
expect(getByText('Docker adapter 2')).toBeInTheDocument();
expect(getByText('Proxmox adapter 1')).toBeInTheDocument();
expect(queryByText('Capabilities 1')).toBeNull();
expect(queryByText('Relationships 1')).toBeNull();
});
it('collapses badges behind an overflow chip when a visible limit is set', () => {
const { getByText, queryByText } = render(() => (
<ResourceFacetSummary
counts={{
recentChanges: 3,
recentChangeKinds: {
restart: 2,
config_update: 1,
metric_anomaly: 1,
},
recentChangeSourceTypes: {
platform_event: 1,
pulse_diff: 2,
},
recentChangeSourceAdapters: {
docker_adapter: 2,
proxmox_adapter: 1,
},
}}
recentChanges={[]}
maxVisibleBadges={1}
/>
));
expect(getByText('Timeline 3')).toBeInTheDocument();
expect(getByText('+7')).toBeInTheDocument();
expect(queryByText('Config update 1')).toBeNull();
});
});
@@ -1,103 +0,0 @@
import { describe, expect, it, vi } from 'vitest';
import { render } from '@solidjs/testing-library';
import type { Resource } from '@/types/resource';
import { UnifiedResourceTable } from '@/components/Infrastructure/UnifiedResourceTable';
if (typeof globalThis.ResizeObserver === 'undefined') {
globalThis.ResizeObserver = class ResizeObserver {
observe() {}
unobserve() {}
disconnect() {}
} as unknown as typeof ResizeObserver;
}
vi.mock('@/hooks/useBreakpoint', () => ({
useBreakpoint: () => ({
isMobile: () => false,
isVisible: () => true,
}),
}));
vi.mock('@/components/shared/responsive', () => ({
ResponsiveMetricCell: () => <div data-testid="metric-cell">metric</div>,
}));
vi.mock('@/components/Infrastructure/ResourceDetailDrawer', () => ({
ResourceDetailDrawer: () => <div data-testid="resource-drawer">drawer</div>,
}));
const resource: Resource = {
id: 'resource-1',
type: 'agent',
name: 'sensitive-host',
displayName: 'Sensitive Host',
platformId: 'platform-1',
platformType: 'agent',
sourceType: 'agent',
status: 'online',
lastSeen: Date.now(),
platformData: { sources: ['agent'] },
policy: {
sensitivity: 'restricted',
routing: {
scope: 'local-only',
redact: ['hostname', 'alias'],
},
},
aiSafeSummary: 'restricted host summary safe for remote AI consumption',
recentChanges: [
{
id: 'change-1',
observedAt: new Date().toISOString(),
resourceId: 'resource-1',
kind: 'state_transition',
sourceType: 'pulse_diff',
confidence: 'high',
},
],
};
describe('UnifiedResourceTable governance presentation', () => {
it('surfaces blocking policy posture in the resource row', () => {
const { getByText, queryByText } = render(() => (
<UnifiedResourceTable
resources={[resource]}
expandedResourceId={null}
onExpandedResourceChange={vi.fn()}
groupingMode="flat"
/>
));
expect(getByText('Local Only')).toBeInTheDocument();
expect(queryByText('Restricted')).toBeNull();
expect(getByText('Sensitive Host')).toBeInTheDocument();
expect(queryByText('restricted host summary safe for remote AI consumption')).toBeNull();
expect(queryByText('(sensitive-host)')).toBeNull();
});
it('surfaces resource facet counts in the resource row', () => {
const facetResource: Resource = {
...resource,
policy: {
sensitivity: 'sensitive',
routing: {
scope: 'local-first',
redact: ['hostname'],
},
},
};
const { getByText, queryByText } = render(() => (
<UnifiedResourceTable
resources={[facetResource]}
expandedResourceId={null}
onExpandedResourceChange={vi.fn()}
groupingMode="flat"
/>
));
expect(getByText('Timeline 1')).toBeInTheDocument();
expect(queryByText('Capabilities 2')).toBeNull();
expect(queryByText('Relationships 1')).toBeNull();
});
});
@@ -1,293 +0,0 @@
import { describe, expect, it, vi } from 'vitest';
import { fireEvent, render, screen, within } from '@solidjs/testing-library';
import type { Resource } from '@/types/resource';
import { UnifiedResourceTable } from '@/components/Infrastructure/UnifiedResourceTable';
const responsiveMetricCellMock = vi.fn();
if (typeof globalThis.ResizeObserver === 'undefined') {
globalThis.ResizeObserver = class ResizeObserver {
observe() {}
unobserve() {}
disconnect() {}
} as unknown as typeof ResizeObserver;
}
vi.mock('@/hooks/useBreakpoint', () => ({
useBreakpoint: () => ({
isMobile: () => false,
isVisible: () => true,
}),
}));
vi.mock('@/components/shared/responsive', () => ({
ResponsiveMetricCell: (props: unknown) => {
responsiveMetricCellMock(props);
return <div data-testid="metric-cell">metric</div>;
},
}));
vi.mock('@/components/Infrastructure/ResourceDetailDrawer', () => ({
ResourceDetailDrawer: () => <div data-testid="resource-drawer">drawer</div>,
}));
const baseResource = (overrides: Partial<Resource>): Resource => ({
id: 'resource-1',
type: 'agent',
name: 'pve1',
displayName: 'pve1',
platformId: 'pve1',
platformType: 'proxmox-pve',
sourceType: 'api',
status: 'online',
lastSeen: Date.now(),
platformData: { sources: ['proxmox'] },
...overrides,
});
describe('UnifiedResourceTable workloads links', () => {
it('passes canonical metrics keys to metric cells for docker-host resources', async () => {
responsiveMetricCellMock.mockClear();
render(() => (
<UnifiedResourceTable
resources={[
baseResource({
id: 'hash-docker-resource',
type: 'docker-host',
name: 'Tower',
displayName: 'Tower',
platformType: 'docker',
sourceType: 'agent',
customUrl: 'https://tower.internal:9443',
metricsTarget: {
resourceType: 'docker-host',
resourceId: 'docker-host-1',
},
platformData: {
sources: ['docker'],
docker: {
hostSourceId: 'docker-host-1',
},
},
cpu: { current: 0.21 },
memory: { current: 0.52, total: 8, used: 4 },
disk: { current: 0.33 },
}),
]}
expandedResourceId={null}
onExpandedResourceChange={vi.fn()}
groupingMode="flat"
/>
));
expect(responsiveMetricCellMock).toHaveBeenCalled();
expect(
responsiveMetricCellMock.mock.calls.some(
([props]) => (props as { resourceId?: string }).resourceId === 'dockerHost:docker-host-1',
),
).toBe(true);
expect(screen.getByText('Tower').tagName).toBe('SPAN');
expect(screen.getByRole('link', { name: 'Open web interface for Tower' })).toHaveAttribute(
'href',
'https://tower.internal:9443',
);
});
it('no longer renders host-row workloads cross-jump links in the platform-first layout', () => {
const resources: Resource[] = [
baseResource({
id: 'node-1',
type: 'agent',
platformData: {
sources: ['proxmox'],
proxmox: { nodeName: 'pve1' },
},
}),
baseResource({
id: 'k8s-cluster-1',
type: 'k8s-cluster',
name: 'cluster-a',
displayName: 'cluster-a',
clusterId: 'cluster-a',
platformType: 'kubernetes',
sourceType: 'api',
platformData: {
sources: ['kubernetes'],
kubernetes: {
clusterName: 'cluster-a',
},
},
}),
];
const { queryAllByRole } = render(() => (
<UnifiedResourceTable
resources={resources}
expandedResourceId={null}
onExpandedResourceChange={vi.fn()}
groupingMode="flat"
/>
));
// Legacy cross-jump from a host row to /workloads?... was retired with
// the standalone Workloads page; the platform pages expose workloads as
// adjacent sub-tabs instead.
expect(queryAllByRole('link', { name: /view workloads/i })).toHaveLength(0);
});
it('renders PBS and PMG resources in a dedicated service table with service-native columns', async () => {
const onExpandedResourceChange = vi.fn();
const resources: Resource[] = [
baseResource({
id: 'pbs-1',
type: 'pbs',
name: 'pbs-main',
displayName: 'pbs-main',
customUrl: 'https://pbs-main.internal:8007',
platformType: 'proxmox-pbs',
sourceType: 'api',
platformData: {
sources: ['pbs'],
pbs: {
datastoreCount: 2,
backupJobCount: 2,
syncJobCount: 1,
backupJobs: [
{
id: 'backup-nightly',
store: 'fast',
type: 'vm',
vmid: '100',
lastBackup: '',
nextRun: '',
status: 'running',
error: '',
},
{
id: 'backup-weekly',
store: 'archive',
type: 'ct',
vmid: '200',
lastBackup: '',
nextRun: '',
status: 'ok',
error: '',
},
],
syncJobs: [
{
id: 'sync-remote',
store: 'fast',
remote: 'offsite',
status: 'queued',
lastSync: '',
nextRun: '',
error: '',
},
],
},
},
recentChanges: [
{
id: 'pbs-change-1',
observedAt: new Date().toISOString(),
resourceId: 'pbs-1',
kind: 'state_transition',
sourceType: 'pulse_diff',
confidence: 'high',
},
],
}),
baseResource({
id: 'pmg-1',
type: 'pmg',
name: 'pmg-main',
displayName: 'pmg-main',
customUrl: 'https://pmg-main.internal:8006',
platformType: 'proxmox-pmg',
sourceType: 'api',
platformData: {
sources: ['pmg'],
pmg: {
queueTotal: 519,
nodeCount: 1,
},
},
recentChanges: [
{
id: 'pmg-change-1',
observedAt: new Date().toISOString(),
resourceId: 'pmg-1',
kind: 'metric_anomaly',
sourceType: 'heuristic',
confidence: 'medium',
},
],
}),
];
const { getByText, getByRole, getAllByText } = render(() => (
<UnifiedResourceTable
resources={resources}
expandedResourceId={null}
onExpandedResourceChange={onExpandedResourceChange}
groupingMode="flat"
/>
));
expect(getByText('Service Infrastructure')).toBeInTheDocument();
expect(getByText('PBS Services')).toBeInTheDocument();
expect(getByText('PMG Services')).toBeInTheDocument();
// Service Infrastructure dual-table renders the compact column label.
expect(getByText('Stores')).toBeInTheDocument();
expect(getByText('Activity')).toBeInTheDocument();
// Compact layout uses 'Action' for the action column header.
expect(getAllByText('Action').length).toBeGreaterThan(0);
expect(getByText('Queue')).toBeInTheDocument();
// Compact layout abbreviates Deferred to 'Def'.
expect(getByText('Def')).toBeInTheDocument();
expect(getByText('Hold')).toBeInTheDocument();
expect(getByText('Nodes')).toBeInTheDocument();
expect(getByText('pbs-main').tagName).toBe('SPAN');
expect(getByText('pmg-main').tagName).toBe('SPAN');
expect(getByRole('link', { name: 'Open web interface for pbs-main' })).toHaveAttribute(
'href',
'https://pbs-main.internal:8007',
);
expect(getByRole('link', { name: 'Open web interface for pmg-main' })).toHaveAttribute(
'href',
'https://pmg-main.internal:8006',
);
const pbsRow = getByText('pbs-main').closest('tr');
expect(pbsRow).toBeTruthy();
if (pbsRow) {
const row = within(pbsRow);
expect(row.getByText('2')).toBeInTheDocument();
expect(row.getByText('2 active')).toBeInTheDocument();
expect(row.getByText('3 total')).toBeInTheDocument();
expect(row.getByText('Timeline 1')).toBeInTheDocument();
expect(row.queryByText('Capabilities 1')).toBeNull();
expect(row.queryByText('Relationships 1')).toBeNull();
}
const pmgRow = getByText('pmg-main').closest('tr');
expect(pmgRow).toBeTruthy();
if (pmgRow) {
const row = within(pmgRow);
expect(row.getByText('519')).toBeInTheDocument();
expect(row.getByText('1')).toBeInTheDocument();
expect(row.getByText('Timeline 1')).toBeInTheDocument();
expect(row.queryByText('Capabilities 1')).toBeNull();
expect(row.queryByText('Relationships 1')).toBeNull();
}
const pmgLink = getByRole('link', { name: /open pmg thresholds/i });
expect(pmgLink).toHaveTextContent('Thresholds');
expect(pmgLink).toHaveAttribute('href', '/alerts/thresholds/mail-gateway');
pmgLink.addEventListener('click', (event) => event.preventDefault());
await fireEvent.click(pmgLink);
expect(onExpandedResourceChange).not.toHaveBeenCalled();
});
});
@@ -8,6 +8,8 @@ import {
toAgentFromResource,
toNodeFromProxmox,
} from '@/components/Infrastructure/resourceDetailMappers';
import resourceDetailMappersSource from '@/components/Infrastructure/resourceDetailMappers.ts?raw';
import resourceDetailDiscoveryModelSource from '@/components/Infrastructure/resourceDetailDiscoveryModel.ts?raw';
import type { Resource } from '@/types/resource';
const createHybridHostResource = (): Resource =>
@@ -304,6 +306,41 @@ describe('resourceDetailMappers', () => {
},
]);
});
it('formats an online boolean metric with an OK status', () => {
expect(
buildCustomSensorRows({
custom: [
{
id: 'service_online',
name: 'Checkout',
group: 'Main server',
subgroup: 'Services',
kind: 'boolean',
value: 1,
status: 'ok',
observedAt: '2026-07-30T20:00:00Z',
},
],
}),
).toEqual([
{
label: 'Main server / Services / Checkout',
value: 'Online',
valueTitle: 'Online · OK',
},
]);
});
});
describe('detail row projection boundary', () => {
it('formats temperature, GPU, power, fan and custom rows from the selected resource payload only', () => {
expect(resourceDetailMappersSource).not.toContain('fetch(');
expect(resourceDetailMappersSource).not.toContain('nvidia-smi');
expect(resourceDetailMappersSource).not.toContain('powercap');
expect(resourceDetailMappersSource).not.toContain('export const toDiscoveryConfig');
expect(resourceDetailDiscoveryModelSource).toContain('export const toDiscoveryConfig');
});
});
describe('toNodeFromProxmox', () => {
@@ -1,340 +0,0 @@
import { describe, expect, it } from 'vitest';
import type { Resource } from '@/types/resource';
import type { IODistributionStats } from '@/components/Infrastructure/infrastructureSelectors';
import {
getOutlierEmphasis,
getPBSTableRow,
getPMGTableRow,
} from '@/components/Infrastructure/unifiedResourceTableModel';
/**
* Branch-coverage suite for the still-uncovered exported functions in
* unifiedResourceTableModel.ts: getPBSTableRow, getPMGTableRow, getOutlierEmphasis.
* Each test drives a specific branch (guard, ternary arm, optional-chain outcome)
* and asserts on the concrete returned shape — never truthiness alone.
*/
const makeResource = (overrides: Partial<Resource> & { id: string }): Resource =>
({
type: 'agent',
name: `name-${overrides.id}`,
displayName: `Display ${overrides.id}`,
platformId: `platform-${overrides.id}`,
platformType: 'proxmox-pve',
sourceType: 'hybrid',
status: 'online',
lastSeen: 1_700_000_000_000,
platformData: {},
...overrides,
}) as unknown as Resource;
const makeStats = (overrides: Partial<IODistributionStats> = {}): IODistributionStats => ({
median: 0,
mad: 0,
max: 100,
p97: 90,
p99: 95,
count: 10,
...overrides,
});
describe('getPBSTableRow', () => {
it('returns null when resource.type is not pbs (guard)', () => {
expect(getPBSTableRow(makeResource({ id: 'a', type: 'pmg' }))).toBeNull();
expect(getPBSTableRow(makeResource({ id: 'b', type: 'agent' }))).toBeNull();
});
it('nulls every derived field when platformData has no pbs block (optional-chain miss)', () => {
const row = getPBSTableRow(
makeResource({ id: 'pbs-empty', type: 'pbs', status: 'online', platformData: {} }),
);
expect(row).toEqual({
datastores: null,
jobs: null,
activity: null,
activityDetail: null,
activeTaskCount: 0,
health: null,
tone: 'ok',
});
});
it('surfaces a positive datastoreCount and jobs total, trims health, and derives tone from health', () => {
const row = getPBSTableRow(
makeResource({
id: 'pbs-full',
type: 'pbs',
status: 'degraded',
platformData: {
pbs: { datastoreCount: 3, backupJobCount: 2, connectionHealth: ' Healthy ' },
},
}),
);
expect(row).toEqual({
datastores: 3,
jobs: 2,
activity: '2 jobs',
activityDetail: null,
activeTaskCount: 0,
health: 'Healthy',
tone: 'ok',
});
});
it('nulls datastores/jobs for zero counts and maps whitespace health + offline status to a warning tone', () => {
const row = getPBSTableRow(
makeResource({
id: 'pbs-zero',
type: 'pbs',
status: 'offline',
platformData: { pbs: { datastoreCount: 0, connectionHealth: ' ' } },
}),
);
expect(row).toEqual({
datastores: null,
jobs: null,
activity: null,
activityDetail: null,
activeTaskCount: 0,
health: null,
tone: 'warning',
});
});
});
describe('getPMGTableRow', () => {
it('returns null when resource.type is not pmg (guard)', () => {
expect(getPMGTableRow(makeResource({ id: 'a', type: 'pbs' }))).toBeNull();
expect(getPMGTableRow(makeResource({ id: 'b', type: 'agent' }))).toBeNull();
});
it('nulls every derived field when platformData has no pmg block (optional-chain miss)', () => {
const row = getPMGTableRow(
makeResource({ id: 'pmg-empty', type: 'pmg', status: 'online', platformData: {} }),
);
expect(row).toEqual({
queue: null,
deferred: null,
hold: null,
nodes: null,
health: null,
tone: 'ok',
});
});
it('reports positive queue/deferred/hold/nodes and forces a warning tone when backlog > 0', () => {
const row = getPMGTableRow(
makeResource({
id: 'pmg-full',
type: 'pmg',
status: 'online',
platformData: {
pmg: {
queueTotal: 12,
queueDeferred: 3,
queueHold: 2,
nodeCount: 4,
connectionHealth: 'degraded',
},
},
}),
);
expect(row).toEqual({
queue: 12,
deferred: 3,
hold: 2,
nodes: 4,
health: 'degraded',
tone: 'warning',
});
});
it('nulls zero counts, trims whitespace health to null, and derives tone from status when backlog is 0', () => {
const row = getPMGTableRow(
makeResource({
id: 'pmg-zero',
type: 'pmg',
status: 'warning',
platformData: {
pmg: {
queueTotal: 5,
queueDeferred: 0,
queueHold: 0,
nodeCount: 0,
connectionHealth: ' ',
},
},
}),
);
expect(row).toEqual({
queue: 5,
deferred: null,
hold: null,
nodes: null,
health: null,
tone: 'warning',
});
});
it('nulls queue when queueTotal is explicitly 0 with a present pmg block', () => {
const row = getPMGTableRow(
makeResource({
id: 'pmg-no-queue',
type: 'pmg',
status: 'online',
platformData: { pmg: { queueTotal: 0, nodeCount: 1 } },
}),
);
expect(row).toEqual({
queue: null,
deferred: null,
hold: null,
nodes: 1,
health: null,
tone: 'ok',
});
});
});
describe('getOutlierEmphasis', () => {
describe('guard branch (!isFinite(value) || value <= 0 || stats.max <= 0)', () => {
it('mutes a non-finite value (NaN)', () => {
expect(getOutlierEmphasis(Number.NaN, makeStats({ max: 100 }))).toEqual({
className: 'text-muted',
showOutlierHint: false,
});
});
it('mutes a non-finite value (Infinity)', () => {
expect(getOutlierEmphasis(Number.POSITIVE_INFINITY, makeStats({ max: 100 }))).toEqual({
className: 'text-muted',
showOutlierHint: false,
});
});
it('mutes a value of exactly 0', () => {
expect(getOutlierEmphasis(0, makeStats({ max: 100 }))).toEqual({
className: 'text-muted',
showOutlierHint: false,
});
});
it('mutes a negative value', () => {
expect(getOutlierEmphasis(-5, makeStats({ max: 100 }))).toEqual({
className: 'text-muted',
showOutlierHint: false,
});
});
it('mutes when stats.max is 0', () => {
expect(getOutlierEmphasis(50, makeStats({ max: 0 }))).toEqual({
className: 'text-muted',
showOutlierHint: false,
});
});
it('mutes when stats.max is negative', () => {
expect(getOutlierEmphasis(50, makeStats({ max: -1 }))).toEqual({
className: 'text-muted',
showOutlierHint: false,
});
});
});
describe('small-sample branch (stats.count < 4)', () => {
it('emphasizes when value/max ratio >= 0.995 (exact boundary)', () => {
expect(getOutlierEmphasis(99.5, makeStats({ max: 100, count: 3 }))).toEqual({
className: 'text-base-content font-medium',
showOutlierHint: true,
});
});
it('emphasizes a ratio of 1.0', () => {
expect(getOutlierEmphasis(100, makeStats({ max: 100, count: 3 }))).toEqual({
className: 'text-base-content font-medium',
showOutlierHint: true,
});
});
it('mutes when value/max ratio < 0.995', () => {
expect(getOutlierEmphasis(80, makeStats({ max: 100, count: 3 }))).toEqual({
className: 'text-muted',
showOutlierHint: false,
});
});
});
describe('mad-positive branch (stats.count >= 4, stats.mad > 0)', () => {
it('returns semibold for a severe outlier (modifiedZ >= 6.5 and value >= p99)', () => {
// modifiedZ = 0.6745 * (200 - 50) / 2 = 50.5875 (>= 6.5); value 200 >= p99 180
const stats = makeStats({ median: 50, mad: 2, max: 200, p97: 150, p99: 180, count: 10 });
expect(getOutlierEmphasis(200, stats)).toEqual({
className: 'text-base-content font-semibold',
showOutlierHint: true,
});
});
it('returns medium when modifiedZ is in [5.5, 6.5) and value >= p97 but < p99', () => {
// modifiedZ = 0.6745 * (90 - 0) / 10 = 6.0705 (in [5.5, 6.5)); value 90 >= p97 80, < p99 200
const stats = makeStats({ median: 0, mad: 10, max: 300, p97: 80, p99: 200, count: 10 });
expect(getOutlierEmphasis(90, stats)).toEqual({
className: 'text-base-content font-medium',
showOutlierHint: true,
});
});
it('mutes a non-outlier whose modifiedZ is below 5.5', () => {
// modifiedZ = 0.6745 * (55 - 50) / 10 = 0.337 (< 5.5)
const stats = makeStats({ median: 50, mad: 10, max: 200, p97: 80, p99: 180, count: 10 });
expect(getOutlierEmphasis(55, stats)).toEqual({
className: 'text-muted',
showOutlierHint: false,
});
});
it('falls through to medium when modifiedZ >= 6.5 but value < p99 (skips severe, hits moderate)', () => {
// modifiedZ = 0.6745 * (70 - 50) / 2 = 6.745 (>= 6.5); value 70 >= p99 80 is FALSE;
// value 70 >= p97 60 is TRUE -> moderate path.
const stats = makeStats({ median: 50, mad: 2, max: 200, p97: 60, p99: 80, count: 10 });
expect(getOutlierEmphasis(70, stats)).toEqual({
className: 'text-base-content font-medium',
showOutlierHint: true,
});
});
});
describe('mad-zero fallback branch (stats.count >= 4, stats.mad <= 0)', () => {
it('returns semibold when value >= p99', () => {
const stats = makeStats({ mad: 0, max: 300, p97: 120, p99: 150, count: 10 });
expect(getOutlierEmphasis(200, stats)).toEqual({
className: 'text-base-content font-semibold',
showOutlierHint: true,
});
});
it('returns medium when value >= p97 but < p99', () => {
const stats = makeStats({ mad: 0, max: 300, p97: 120, p99: 150, count: 10 });
expect(getOutlierEmphasis(130, stats)).toEqual({
className: 'text-base-content font-medium',
showOutlierHint: true,
});
});
it('mutes a positive value below p97', () => {
const stats = makeStats({ mad: 0, max: 300, p97: 120, p99: 150, count: 10 });
expect(getOutlierEmphasis(50, stats)).toEqual({
className: 'text-muted',
showOutlierHint: false,
});
});
it('honors an explicitly negative mad identically to mad === 0', () => {
// Defensive: mad <= 0 takes the fallback path; -1 must behave like 0.
const stats = makeStats({ mad: -1, max: 300, p97: 120, p99: 150, count: 10 });
expect(getOutlierEmphasis(200, stats)).toEqual({
className: 'text-base-content font-semibold',
showOutlierHint: true,
});
});
});
});
@@ -1,173 +0,0 @@
import { describe, expect, it } from 'vitest';
import type { Resource } from '@/types/resource';
import {
UNIFIED_RESOURCE_TABLE_DEFAULT_LAYOUT_WIDTH,
buildHostRowIndexById,
buildHostTableItems,
getHostRevealTargetIndex,
getHostSpacerHeights,
getNextUnifiedResourceTableSortState,
normalizeUnifiedResourceTableLayoutWidth,
shouldShowClusterGroupTypeLabel,
sortServiceResources,
} from '@/components/Infrastructure/unifiedResourceTableStateModel';
/**
* Branch-coverage additions for unifiedResourceTableStateModel.ts targeting the
* arms left cold by the existing happy-path spec
* (unifiedResourceTableStateModel.test.ts).
*
* Each test drives a specific measured-cold branch (default fallback, map miss,
* nullish coalescing RHS, non-windowed early return, ternary consequent) and
* asserts on the concrete returned value — never a tautology.
*
* The makeResource fixture mirrors the existing spec's builder so import style
* and field shape stay consistent.
*/
const makeResource = (id: string, overrides: Partial<Resource> = {}): Resource =>
({
id,
type: 'agent',
name: `name-${id}`,
displayName: `Display ${id}`,
platformId: `platform-${id}`,
platformType: 'proxmox-pve',
sourceType: 'api',
status: 'online',
lastSeen: Date.now(),
platformData: { sources: ['proxmox'] },
...overrides,
}) as Resource;
describe('normalizeUnifiedResourceTableLayoutWidth — default-fallback arm', () => {
// Drives branch 6 arm 0 (lines 69-71): the trailing
// `return UNIFIED_RESOURCE_TABLE_DEFAULT_LAYOUT_WIDTH;` reached only when the
// width argument is invalid AND the fallback is also invalid. The existing
// spec only exercises a valid width or an invalid width with a valid
// fallback, so the final default return is never reached.
it('falls back to the default constant when the width is absent and the fallback is NaN', () => {
expect(normalizeUnifiedResourceTableLayoutWidth(null, Number.NaN)).toBe(
UNIFIED_RESOURCE_TABLE_DEFAULT_LAYOUT_WIDTH,
);
});
it('falls back to the default constant when the width is absent and the fallback is 0', () => {
expect(normalizeUnifiedResourceTableLayoutWidth(null, 0)).toBe(
UNIFIED_RESOURCE_TABLE_DEFAULT_LAYOUT_WIDTH,
);
});
});
describe('getHostRevealTargetIndex — map-miss returns null', () => {
// Drives branch 31 arm 0 (line 150: `rowIndexById.get(targetId) ?? null`):
// the LHS of `??` is undefined because targetId is absent from the map. The
// existing spec always resolves a present id, so the nullish-coalescing RHS
// is never taken.
const items = buildHostTableItems(
[{ cluster: 'cluster-a', resources: [makeResource('a'), makeResource('b')] }],
'grouped',
);
const rowIndexById = buildHostRowIndexById(items);
it('returns null when expandedResourceId is not present in the row index', () => {
// rowIndexById only holds 'a' (1) and 'b' (2); 'missing' is absent.
expect(getHostRevealTargetIndex(rowIndexById, 'missing', null)).toBeNull();
});
it('returns null when revealedResourceId is the chosen target but absent from the row index', () => {
// expandedResourceId is null so revealedResourceId 'ghost' wins the chain,
// then misses the map lookup.
expect(getHostRevealTargetIndex(rowIndexById, null, null, 'ghost')).toBeNull();
});
});
describe('shouldShowClusterGroupTypeLabel — nullish clusterLabel arm', () => {
// Drives branch 35 arm 0 (line 166: `(clusterLabel ?? '')`): the RHS of `??`
// is only evaluated when clusterLabel is null/undefined. The existing spec
// passes only concrete strings, so the coalescing RHS stays cold.
it('returns false for a null clusterLabel without throwing', () => {
expect(shouldShowClusterGroupTypeLabel(null)).toBe(false);
});
it('returns false for an undefined clusterLabel without throwing', () => {
expect(shouldShowClusterGroupTypeLabel(undefined)).toBe(false);
});
});
describe('getHostSpacerHeights — non-windowed early-return arm', () => {
// Drives branch 38 arm 0 (lines 177-179): the `if (!isWindowed)` consequent
// that returns `{ top: 0, bottom: 0 }`. The existing spec always calls with
// isWindowed = true, so the zero-spacer early return is never produced.
it('returns zero spacers regardless of window bounds when windowing is disabled', () => {
const result = getHostSpacerHeights(100, 5, 20, false);
expect(result).toEqual({ top: 0, bottom: 0 });
});
it('returns zero spacers even when a custom row height is supplied', () => {
// Discriminator: a custom estimatedRowHeight (40) would otherwise scale the
// top spacer to 5 * 40 = 200; the early return suppresses it entirely.
const result = getHostSpacerHeights(100, 5, 20, false, 40);
expect(result).toEqual({ top: 0, bottom: 0 });
expect(result.top).not.toBe(200);
});
});
describe('getNextUnifiedResourceTableSortState — asc default for name/source', () => {
// Drives branch 43 arm 0 (line 204: the `'asc'` consequent of
// `nextKey === 'name' || nextKey === 'source' ? 'asc' : 'desc'`). The existing
// spec only enters the function with currentKey === nextKey (so it never
// reaches line 204) or with nextKey 'cpu' (which takes the 'desc' alternate).
it('initializes a name sort to ascending when switching from a different key', () => {
expect(getNextUnifiedResourceTableSortState('default', 'asc', 'name')).toEqual({
key: 'name',
direction: 'asc',
});
});
it('initializes a source sort to ascending when switching from a different key', () => {
// Also drives the second operand of the `||` (`nextKey === 'source'`) true.
expect(getNextUnifiedResourceTableSortState('cpu', 'desc', 'source')).toEqual({
key: 'source',
direction: 'asc',
});
});
});
describe('sortServiceResources — uncovered exported function', () => {
// Drives fn 15 (sortServiceResources, line 217): the only exported function
// the existing spec never invokes. It must (a) filter to the requested
// service type and (b) delegate to sortResources with 'default'/'asc', which
// ranks online resources first then by display name. Assertions are made on
// the concrete id sequence so they cannot pass against an unfiltered or
// unsorted list.
const services: Resource[] = [
makeResource('zebra', { type: 'pbs', displayName: 'Zeta', status: 'offline' }),
makeResource('alpha', { type: 'pbs', displayName: 'Alpha', status: 'online' }),
makeResource('mango', { type: 'pbs', displayName: 'Mango', status: 'online' }),
makeResource('pmg-only', { type: 'pmg', displayName: 'Should Be Filtered', status: 'online' }),
makeResource('agent-only', { type: 'agent', displayName: 'Also Filtered', status: 'online' }),
];
it('returns only pbs resources, online-first then by display name', () => {
const result = sortServiceResources(services, 'pbs');
expect(result.map((r) => r.id)).toEqual(['alpha', 'mango', 'zebra']);
expect(result.every((r) => r.type === 'pbs')).toBe(true);
});
it('returns only pmg resources when type is pmg', () => {
const result = sortServiceResources(services, 'pmg');
expect(result.map((r) => r.id)).toEqual(['pmg-only']);
expect(result.every((r) => r.type === 'pmg')).toBe(true);
});
it('returns an empty array when the input list is empty', () => {
expect(sortServiceResources([], 'pbs')).toEqual([]);
});
it('does not mutate the input array', () => {
const snapshot = services.map((r) => r.id);
sortServiceResources(services, 'pbs');
expect(services.map((r) => r.id)).toEqual(snapshot);
});
});
@@ -1,226 +0,0 @@
import { describe, expect, it } from 'vitest';
import type { Resource } from '@/types/resource';
import type { ResourceGroup } from '@/components/Infrastructure/infrastructureSelectors';
import {
buildHostRowIndexById,
buildHostTableItems,
buildResourceLabelById,
getHostRevealTargetIndex,
getHostSpacerHeights,
getNextUnifiedResourceTableSortState,
getUnifiedResourceTableColumnPresentations,
getUnifiedResourceTableHeaderLabels,
getUnifiedResourceTableLayoutMode,
getUnifiedResourceTableShellClass,
getUnifiedResourceTableSortIndicator,
getUnifiedSources,
getVisibleHostTableItems,
isUnifiedResourceHostDiskIoVisible,
isUnifiedResourceServiceColumnVisible,
isUnifiedResourceTableColumnVisible,
normalizeUnifiedResourceTableLayoutWidth,
shouldShowClusterGroupTypeLabel,
shouldShowUnifiedResourceHostTable,
shouldUseUnifiedResourceTableMobileLayout,
} from '@/components/Infrastructure/unifiedResourceTableStateModel';
const makeResource = (id: string, overrides: Partial<Resource> = {}): Resource =>
({
id,
type: 'agent',
name: `name-${id}`,
displayName: `Display ${id}`,
platformId: `platform-${id}`,
platformType: 'proxmox-pve',
sourceType: 'api',
status: 'online',
lastSeen: Date.now(),
platformData: { sources: ['proxmox'] },
...overrides,
}) as Resource;
describe('unifiedResourceTableStateModel', () => {
it('builds resource labels from the canonical preferred display name', () => {
const labels = buildResourceLabelById([
makeResource('a', { displayName: 'Alpha' }),
makeResource('b', { displayName: 'Beta' }),
]);
expect(labels.get('a')).toBe('Alpha');
expect(labels.get('b')).toBe('Beta');
});
it('builds host table items with group headers only in grouped mode', () => {
const groups: ResourceGroup[] = [
{
cluster: 'cluster-a',
resources: [makeResource('a'), makeResource('b')],
},
];
expect(buildHostTableItems(groups, 'grouped').map((item) => item.type)).toEqual([
'header',
'row',
'row',
]);
expect(buildHostTableItems(groups, 'flat').map((item) => item.type)).toEqual(['row', 'row']);
});
it('suppresses duplicate cluster type labels when the group name already ends with cluster', () => {
expect(shouldShowClusterGroupTypeLabel('Core Fabric')).toBe(true);
expect(shouldShowClusterGroupTypeLabel('Production Cluster')).toBe(false);
expect(shouldShowClusterGroupTypeLabel('production cluster')).toBe(false);
expect(shouldShowClusterGroupTypeLabel('')).toBe(false);
});
it('tracks reveal indexes only for row items', () => {
const items = buildHostTableItems(
[{ cluster: 'cluster-a', resources: [makeResource('a'), makeResource('b')] }],
'grouped',
);
const rowIndexById = buildHostRowIndexById(items);
expect(getHostRevealTargetIndex(rowIndexById, 'b', null)).toBe(2);
expect(getHostRevealTargetIndex(rowIndexById, null, 'a')).toBe(1);
expect(getHostRevealTargetIndex(rowIndexById, null, null)).toBeNull();
});
it('computes visible items and spacer heights from the active window', () => {
const items = buildHostTableItems(
[{ cluster: '', resources: [makeResource('a'), makeResource('b'), makeResource('c')] }],
'flat',
);
expect(getVisibleHostTableItems(items, false, 1, 2)).toEqual(items);
expect(
getVisibleHostTableItems(items, true, 1, 3).map((item) =>
item.type === 'row' ? item.resource.id : item.type,
),
).toEqual(['b', 'c']);
expect(getHostSpacerHeights(items.length, 1, 3, true, 40)).toEqual({
top: 40,
bottom: 0,
});
});
it('cycles sort state through asc, desc, then default', () => {
expect(getNextUnifiedResourceTableSortState('default', 'asc', 'cpu')).toEqual({
key: 'cpu',
direction: 'desc',
});
expect(getNextUnifiedResourceTableSortState('cpu', 'desc', 'cpu')).toEqual({
key: 'default',
direction: 'asc',
});
expect(getNextUnifiedResourceTableSortState('name', 'asc', 'name')).toEqual({
key: 'name',
direction: 'desc',
});
expect(getUnifiedResourceTableSortIndicator('name', 'asc', 'name')).toBe('▲');
expect(getUnifiedResourceTableSortIndicator('cpu', 'desc', 'cpu')).toBe('▼');
expect(getUnifiedResourceTableSortIndicator('default', 'asc', 'cpu')).toBeNull();
});
it('derives responsive column presentations and host-table visibility as pure layout policy', () => {
const mobileColumns = getUnifiedResourceTableColumnPresentations('mobile');
const narrowTabletColumns = getUnifiedResourceTableColumnPresentations('tablet', 760);
const tabletColumns = getUnifiedResourceTableColumnPresentations('tablet', 820);
const compactColumns = getUnifiedResourceTableColumnPresentations('compact');
const wideColumns = getUnifiedResourceTableColumnPresentations('wide');
expect(getUnifiedResourceTableShellClass('mobile')).toContain('table-fixed');
expect(getUnifiedResourceTableShellClass('mobile')).toContain('min-w-full');
expect(getUnifiedResourceTableShellClass('mobile')).not.toContain('min-w-[640px]');
expect(getUnifiedResourceTableShellClass('compact')).toContain('min-w-[640px]');
expect(getUnifiedResourceTableShellClass('wide')).toContain('min-w-[640px]');
expect(getUnifiedResourceTableShellClass('wide')).not.toContain('min-w-max');
// Mobile keeps the prioritized column set but preserves a readable width
// floor inside the shared horizontal-scroll shell. Wider modes keep all
// host columns visible while compressing their tracks before any
// lower-priority columns are dropped.
expect(mobileColumns.resourceColumn.width).toBe('34%');
expect(mobileColumns.metricColumn.width).toBe('14%');
expect(mobileColumns.ioColumn.width).toBe('12%');
expect(mobileColumns.uptimeColumn.width).toBe('12%');
expect(mobileColumns.serviceResourceColumn.width).toBe('28%');
expect(mobileColumns.serviceSourceColumn.width).toBe('10%');
expect(mobileColumns.serviceCountColumn.width).toBe('11%');
expect(mobileColumns.serviceHealthColumn.width).toBe('13%');
expect(mobileColumns.serviceActionColumn.width).toBe('14%');
expect(narrowTabletColumns.resourceColumn.width).toBe('34%');
expect(narrowTabletColumns.ioColumn.width).toBe('14%');
expect(narrowTabletColumns.sourceColumn.width).toBe('13%');
expect(isUnifiedResourceHostDiskIoVisible(799)).toBe(false);
expect(isUnifiedResourceHostDiskIoVisible(800)).toBe(true);
expect(tabletColumns.resourceColumn.width).toBe('28%');
expect(tabletColumns.serviceResourceColumn.width).toBe('24%');
expect(tabletColumns.metricColumn.width).toBe('12%');
expect(tabletColumns.ioColumn.width).toBe('12%');
expect(tabletColumns.sourceColumn.width).toBe('12%');
expect(tabletColumns.serviceSourceColumn.width).toBe('8%');
expect(compactColumns.resourceColumn.width).toBe('16%');
expect(compactColumns.serviceResourceColumn.width).toBe('18%');
expect(compactColumns.metricColumn.width).toBe('10%');
expect(compactColumns.sourceColumn.width).toBe('13%');
expect(compactColumns.serviceSourceColumn.width).toBe('9.5%');
expect(compactColumns.tempColumn.width).toBe('6.5%');
expect(wideColumns.resourceColumn.width).toBe('18%');
expect(wideColumns.serviceResourceColumn.width).toBe('18%');
expect(wideColumns.serviceSourceColumn.width).toBe('10%');
expect(wideColumns.ioColumn.width).toBe('12.5%');
expect(wideColumns.serviceActionColumn.width).toBe('16%');
expect(getUnifiedResourceTableHeaderLabels('wide').memory).toBe('Memory');
expect(getUnifiedResourceTableHeaderLabels('wide').source).toBe('System');
expect(getUnifiedResourceTableHeaderLabels('compact').memory).toBe('Mem');
expect(getUnifiedResourceTableHeaderLabels('compact').source).toBe('System');
expect(getUnifiedResourceTableHeaderLabels('tablet').network).toBe('Net');
expect(getUnifiedResourceTableHeaderLabels('tablet').source).toBe('Sys');
expect(getUnifiedResourceTableHeaderLabels('mobile').datastores).toBe('Store');
expect(shouldShowUnifiedResourceHostTable(0, 0)).toBe(true);
expect(shouldShowUnifiedResourceHostTable(0, 2)).toBe(false);
expect(shouldShowUnifiedResourceHostTable(3, 2)).toBe(true);
});
it('derives infrastructure table breakpoints from the measured table surface width', () => {
expect(normalizeUnifiedResourceTableLayoutWidth(820.4)).toBe(820);
expect(normalizeUnifiedResourceTableLayoutWidth(null, 700)).toBe(700);
expect(getUnifiedResourceTableLayoutMode(699)).toBe('mobile');
expect(getUnifiedResourceTableLayoutMode(700)).toBe('tablet');
expect(getUnifiedResourceTableLayoutMode(899)).toBe('tablet');
expect(getUnifiedResourceTableLayoutMode(900)).toBe('compact');
expect(getUnifiedResourceTableLayoutMode(1159)).toBe('compact');
expect(getUnifiedResourceTableLayoutMode(1160)).toBe('wide');
expect(shouldUseUnifiedResourceTableMobileLayout(699)).toBe(true);
expect(shouldUseUnifiedResourceTableMobileLayout(700)).toBe(false);
expect(isUnifiedResourceTableColumnVisible('primary', 640)).toBe(true);
expect(isUnifiedResourceTableColumnVisible('secondary', 699)).toBe(false);
expect(isUnifiedResourceTableColumnVisible('secondary', 700)).toBe(true);
expect(isUnifiedResourceTableColumnVisible('supplementary', 899)).toBe(false);
expect(isUnifiedResourceTableColumnVisible('supplementary', 900)).toBe(true);
expect(isUnifiedResourceTableColumnVisible('detailed', 1159)).toBe(false);
expect(isUnifiedResourceTableColumnVisible('detailed', 1160)).toBe(true);
expect(isUnifiedResourceServiceColumnVisible('primary', 499)).toBe(false);
expect(isUnifiedResourceServiceColumnVisible('primary', 500)).toBe(true);
expect(isUnifiedResourceServiceColumnVisible('secondary', 579)).toBe(false);
expect(isUnifiedResourceServiceColumnVisible('secondary', 580)).toBe(true);
expect(isUnifiedResourceServiceColumnVisible('supplementary', 639)).toBe(false);
expect(isUnifiedResourceServiceColumnVisible('supplementary', 640)).toBe(true);
expect(isUnifiedResourceServiceColumnVisible('detailed', 899)).toBe(false);
expect(isUnifiedResourceServiceColumnVisible('detailed', 900)).toBe(true);
});
it('reads unified source tags from platform data without hook state', () => {
expect(
getUnifiedSources(makeResource('a', { platformData: { sources: ['proxmox', 'agent'] } })),
).toEqual(['proxmox', 'agent']);
expect(
getUnifiedSources(
makeResource('b', {
sources: ['agent', 'docker'],
platformData: { sources: ['kubernetes'] },
}),
),
).toEqual(['agent', 'docker']);
expect(getUnifiedSources(makeResource('c', { platformData: {} }))).toEqual([]);
});
});
@@ -1,108 +0,0 @@
import type { Resource } from '@/types/resource';
import { getServiceHealthSummaryPresentation } from '@/utils/serviceHealthPresentation';
import type { IODistributionStats } from '@/components/Infrastructure/infrastructureSelectors';
import type { PBSPlatformData, PMGPlatformData } from './resourceDetailMappers';
import { getPbsActivitySummary, getPbsJobTotal } from './resourceDetailDrawerServiceModel';
type PBSServiceData = PBSPlatformData;
type PMGServiceData = PMGPlatformData;
export type PBSTableRow = {
datastores: number | null;
jobs: number | null;
activity: string | null;
activityDetail: string | null;
activeTaskCount: number;
health: string | null;
tone: ReturnType<typeof getServiceHealthSummaryPresentation>['tone'];
};
export type PMGTableRow = {
queue: number | null;
deferred: number | null;
hold: number | null;
nodes: number | null;
health: string | null;
tone: ReturnType<typeof getServiceHealthSummaryPresentation>['tone'];
};
export interface IOEmphasis {
className: string;
showOutlierHint: boolean;
}
export const isResourceOnline = (resource: Resource) => {
const status = resource.status?.toLowerCase();
return status !== 'offline' && status !== 'stopped';
};
export const getPBSTableRow = (resource: Resource): PBSTableRow | null => {
if (resource.type !== 'pbs') return null;
const platformData = resource.platformData as
{ pbs?: PBSServiceData; pmg?: PMGServiceData } | undefined;
const pbs = platformData?.pbs;
const totalJobs = getPbsJobTotal(pbs);
const activitySummary = getPbsActivitySummary(pbs);
const health = pbs?.connectionHealth?.trim() || null;
return {
datastores: (pbs?.datastoreCount || 0) > 0 ? pbs?.datastoreCount || 0 : null,
jobs: totalJobs > 0 ? totalJobs : null,
activity: activitySummary.label,
activityDetail: activitySummary.detail,
activeTaskCount: activitySummary.activeTaskCount,
health,
tone: getServiceHealthSummaryPresentation(resource.status, health).tone,
};
};
export const getPMGTableRow = (resource: Resource): PMGTableRow | null => {
if (resource.type !== 'pmg') return null;
const platformData = resource.platformData as
{ pbs?: PBSServiceData; pmg?: PMGServiceData } | undefined;
const pmg = platformData?.pmg;
const health = pmg?.connectionHealth?.trim() || null;
const backlog = (pmg?.queueDeferred || 0) + (pmg?.queueHold || 0);
return {
queue: (pmg?.queueTotal || 0) > 0 ? pmg?.queueTotal || 0 : null,
deferred: (pmg?.queueDeferred || 0) > 0 ? pmg?.queueDeferred || 0 : null,
hold: (pmg?.queueHold || 0) > 0 ? pmg?.queueHold || 0 : null,
nodes: (pmg?.nodeCount || 0) > 0 ? pmg?.nodeCount || 0 : null,
health,
tone:
backlog > 0 ? 'warning' : getServiceHealthSummaryPresentation(resource.status, health).tone,
};
};
export const getOutlierEmphasis = (value: number, stats: IODistributionStats): IOEmphasis => {
if (!Number.isFinite(value) || value <= 0 || stats.max <= 0) {
return { className: 'text-muted', showOutlierHint: false };
}
if (stats.count < 4) {
const ratio = value / stats.max;
if (ratio >= 0.995) {
return { className: 'text-base-content font-medium', showOutlierHint: true };
}
return { className: 'text-muted', showOutlierHint: false };
}
if (stats.mad > 0) {
const modifiedZ = (0.6745 * (value - stats.median)) / stats.mad;
if (modifiedZ >= 6.5 && value >= stats.p99) {
return { className: 'text-base-content font-semibold', showOutlierHint: true };
}
if (modifiedZ >= 5.5 && value >= stats.p97) {
return { className: 'text-base-content font-medium', showOutlierHint: true };
}
return { className: 'text-muted', showOutlierHint: false };
}
if (value >= stats.p99)
return { className: 'text-base-content font-semibold', showOutlierHint: true };
if (value >= stats.p97)
return { className: 'text-base-content font-medium', showOutlierHint: true };
if (value > 0) return { className: 'text-muted', showOutlierHint: false };
return { className: 'text-muted', showOutlierHint: false };
};
@@ -1,449 +0,0 @@
import type { Resource } from '@/types/resource';
import type { ColumnPriority } from '@/hooks/useBreakpoint';
import { getPreferredInfrastructureDisplayName } from '@/utils/resourceIdentity';
import {
sortResources,
type ResourceGroup,
} from '@/components/Infrastructure/infrastructureSelectors';
export type UnifiedResourceTableSortKey =
| 'default'
| 'name'
| 'uptime'
| 'cpu'
| 'memory'
| 'disk'
| 'network'
| 'diskio'
| 'source'
| 'temp';
export type UnifiedResourceTableSortDirection = 'asc' | 'desc';
export type HostTableHeaderItem = {
type: 'header';
group: ResourceGroup;
};
export type HostTableResourceItem = {
type: 'row';
group: ResourceGroup;
resource: Resource;
};
export type HostTableItem = HostTableHeaderItem | HostTableResourceItem;
export const HOST_TABLE_ESTIMATED_ROW_HEIGHT = 40;
export const HOST_TABLE_WINDOW_SIZE = 137;
export const UNIFIED_RESOURCE_TABLE_DEFAULT_LAYOUT_WIDTH = 1024;
export const UNIFIED_RESOURCE_TABLE_MOBILE_LAYOUT_WIDTH = 700;
export const UNIFIED_RESOURCE_TABLE_TABLET_DISK_IO_LAYOUT_WIDTH = 800;
export const UNIFIED_RESOURCE_TABLE_COMPACT_LAYOUT_WIDTH = 900;
export const UNIFIED_RESOURCE_TABLE_WIDE_LAYOUT_WIDTH = 1160;
export const UNIFIED_RESOURCE_TABLE_COLUMN_BREAKPOINTS: Record<ColumnPriority, number> = {
essential: 0,
primary: 640,
secondary: UNIFIED_RESOURCE_TABLE_MOBILE_LAYOUT_WIDTH,
supplementary: UNIFIED_RESOURCE_TABLE_COMPACT_LAYOUT_WIDTH,
detailed: UNIFIED_RESOURCE_TABLE_WIDE_LAYOUT_WIDTH,
};
export const UNIFIED_RESOURCE_SERVICE_COLUMN_BREAKPOINTS: Record<ColumnPriority, number> = {
essential: 0,
primary: 500,
secondary: 580,
supplementary: 640,
detailed: 900,
};
export type UnifiedResourceTableLayoutMode = 'mobile' | 'tablet' | 'compact' | 'wide';
export const normalizeUnifiedResourceTableLayoutWidth = (
width: number | null | undefined,
fallback: number = UNIFIED_RESOURCE_TABLE_DEFAULT_LAYOUT_WIDTH,
): number => {
if (typeof width === 'number' && Number.isFinite(width) && width > 0) {
return Math.round(width);
}
if (Number.isFinite(fallback) && fallback > 0) {
return Math.round(fallback);
}
return UNIFIED_RESOURCE_TABLE_DEFAULT_LAYOUT_WIDTH;
};
export const getUnifiedResourceTableLayoutMode = (
layoutWidth: number,
): UnifiedResourceTableLayoutMode => {
const width = normalizeUnifiedResourceTableLayoutWidth(layoutWidth);
if (width < UNIFIED_RESOURCE_TABLE_MOBILE_LAYOUT_WIDTH) return 'mobile';
if (width < UNIFIED_RESOURCE_TABLE_COMPACT_LAYOUT_WIDTH) return 'tablet';
if (width < UNIFIED_RESOURCE_TABLE_WIDE_LAYOUT_WIDTH) return 'compact';
return 'wide';
};
export const shouldUseUnifiedResourceTableMobileLayout = (layoutWidth: number): boolean =>
getUnifiedResourceTableLayoutMode(layoutWidth) === 'mobile';
export const isUnifiedResourceTableColumnVisible = (
priority: ColumnPriority,
layoutWidth: number,
): boolean =>
normalizeUnifiedResourceTableLayoutWidth(layoutWidth) >=
UNIFIED_RESOURCE_TABLE_COLUMN_BREAKPOINTS[priority];
export const isUnifiedResourceServiceColumnVisible = (
priority: ColumnPriority,
layoutWidth: number,
): boolean =>
normalizeUnifiedResourceTableLayoutWidth(layoutWidth) >=
UNIFIED_RESOURCE_SERVICE_COLUMN_BREAKPOINTS[priority];
export const isUnifiedResourceHostDiskIoVisible = (layoutWidth: number): boolean =>
normalizeUnifiedResourceTableLayoutWidth(layoutWidth) >=
UNIFIED_RESOURCE_TABLE_TABLET_DISK_IO_LAYOUT_WIDTH;
export const buildResourceLabelById = (resources: Resource[]): Map<string, string> => {
const map = new Map<string, string>();
for (const resource of resources) {
map.set(resource.id, getPreferredInfrastructureDisplayName(resource));
}
return map;
};
export const buildHostTableItems = (
groupedResources: ResourceGroup[],
groupingMode: 'grouped' | 'flat' | undefined,
): HostTableItem[] => {
const items: HostTableItem[] = [];
const showGroupHeaders = groupingMode === 'grouped';
for (const group of groupedResources) {
if (showGroupHeaders) {
items.push({ type: 'header', group });
}
for (const resource of group.resources) {
items.push({ type: 'row', group, resource });
}
}
return items;
};
export const buildHostRowIndexById = (items: HostTableItem[]): Map<string, number> => {
const map = new Map<string, number>();
items.forEach((item, index) => {
if (item.type === 'row') {
map.set(item.resource.id, index);
}
});
return map;
};
export const getHostRevealTargetIndex = (
rowIndexById: Map<string, number>,
expandedResourceId: string | null,
highlightedResourceId: string | null | undefined,
revealedResourceId?: string | null,
): number | null => {
const targetId = expandedResourceId ?? revealedResourceId ?? highlightedResourceId ?? null;
if (!targetId) return null;
return rowIndexById.get(targetId) ?? null;
};
export const getVisibleHostTableItems = (
items: HostTableItem[],
isWindowed: boolean,
startIndex: number,
endIndex: number,
): HostTableItem[] => {
if (!isWindowed) return items;
return items.slice(startIndex, endIndex);
};
export const shouldShowClusterGroupTypeLabel = (
clusterLabel: string | null | undefined,
): boolean => {
const normalized = (clusterLabel ?? '').trim().replace(/\s+/g, ' ').toLowerCase();
return Boolean(normalized) && !/\bcluster$/.test(normalized);
};
export const getHostSpacerHeights = (
totalCount: number,
startIndex: number,
endIndex: number,
isWindowed: boolean,
estimatedRowHeight: number = HOST_TABLE_ESTIMATED_ROW_HEIGHT,
): { top: number; bottom: number } => {
if (!isWindowed) {
return { top: 0, bottom: 0 };
}
return {
top: startIndex * estimatedRowHeight,
bottom: Math.max(0, (totalCount - endIndex) * estimatedRowHeight),
};
};
export const getNextUnifiedResourceTableSortState = (
currentKey: UnifiedResourceTableSortKey,
currentDirection: UnifiedResourceTableSortDirection,
nextKey: Exclude<UnifiedResourceTableSortKey, 'default'>,
): {
key: UnifiedResourceTableSortKey;
direction: UnifiedResourceTableSortDirection;
} => {
if (currentKey === nextKey) {
if (currentDirection === 'asc') {
return { key: nextKey, direction: 'desc' };
}
return { key: 'default', direction: 'asc' };
}
return {
key: nextKey,
direction: nextKey === 'name' || nextKey === 'source' ? 'asc' : 'desc',
};
};
export const getUnifiedResourceTableSortIndicator = (
activeKey: UnifiedResourceTableSortKey,
activeDirection: UnifiedResourceTableSortDirection,
key: UnifiedResourceTableSortKey,
): '▲' | '▼' | null => {
if (activeKey !== key) return null;
return activeDirection === 'asc' ? '▲' : '▼';
};
export const sortServiceResources = (services: Resource[], type: 'pbs' | 'pmg'): Resource[] =>
sortResources(
services.filter((resource) => resource.type === type),
'default',
'asc',
);
export const shouldShowUnifiedResourceHostTable = (
primaryResourceCount: number,
serviceResourceCount: number,
): boolean => primaryResourceCount > 0 || serviceResourceCount === 0;
export const getUnifiedSources = (resource: Resource): string[] => {
if (Array.isArray(resource.sources) && resource.sources.length > 0) {
return resource.sources;
}
const platformData = resource.platformData as { sources?: string[] } | undefined;
return platformData?.sources ?? [];
};
export interface UnifiedResourceTableColumnPresentation {
className: string;
width?: string | number;
}
export type UnifiedResourceTableColumnPresentations = {
resourceColumn: UnifiedResourceTableColumnPresentation;
serviceResourceColumn: UnifiedResourceTableColumnPresentation;
metricColumn: UnifiedResourceTableColumnPresentation;
ioColumn: UnifiedResourceTableColumnPresentation;
sourceColumn: UnifiedResourceTableColumnPresentation;
serviceSourceColumn: UnifiedResourceTableColumnPresentation;
uptimeColumn: UnifiedResourceTableColumnPresentation;
tempColumn: UnifiedResourceTableColumnPresentation;
serviceCountColumn: UnifiedResourceTableColumnPresentation;
serviceQueueColumn: UnifiedResourceTableColumnPresentation;
serviceHealthColumn: UnifiedResourceTableColumnPresentation;
serviceActionColumn: UnifiedResourceTableColumnPresentation;
};
export type UnifiedResourceTableHeaderLabels = {
resource: string;
cpu: string;
memory: string;
disk: string;
network: string;
diskIo: string;
source: string;
uptime: string;
temp: string;
datastores: string;
activity: string;
queue: string;
deferred: string;
hold: string;
nodes: string;
health: string;
action: string;
};
const buildUnifiedResourceTableColumnPresentation = (
className: string,
width?: string | number,
): UnifiedResourceTableColumnPresentation => ({
className,
width,
});
export const getUnifiedResourceTableShellClass = (
layoutMode: UnifiedResourceTableLayoutMode,
): string =>
`table-fixed ${layoutMode === 'mobile' ? 'min-w-full' : 'min-w-[640px] sm:min-w-full'}${
layoutMode === 'wide' ? '' : ' text-[11px] sm:text-xs'
}`;
export const getUnifiedResourceTableHeaderLabels = (
layoutMode: UnifiedResourceTableLayoutMode,
): UnifiedResourceTableHeaderLabels => {
if (layoutMode === 'wide') {
return {
resource: 'Resource',
cpu: 'CPU',
memory: 'Memory',
disk: 'Disk',
network: 'Net I/O',
diskIo: 'Disk I/O',
source: 'System',
uptime: 'Uptime',
temp: 'Temp',
datastores: 'Datastores',
activity: 'Activity',
queue: 'Queue',
deferred: 'Deferred',
hold: 'Hold',
nodes: 'Nodes',
health: 'Health',
action: 'Action',
};
}
if (layoutMode === 'compact') {
return {
resource: 'Resource',
cpu: 'CPU',
memory: 'Mem',
disk: 'Disk',
network: 'Net I/O',
diskIo: 'Disk I/O',
source: 'System',
uptime: 'Up',
temp: 'Temp',
datastores: 'Stores',
activity: 'Activity',
queue: 'Queue',
deferred: 'Def',
hold: 'Hold',
nodes: 'Nodes',
health: 'Health',
action: 'Action',
};
}
return {
resource: 'Resource',
cpu: 'CPU',
memory: 'Mem',
disk: 'Disk',
network: 'Net',
diskIo: 'I/O',
source: 'Sys',
uptime: 'Up',
temp: 'Temp',
datastores: 'Store',
activity: 'Jobs',
queue: 'Queue',
deferred: 'Def',
hold: 'Hold',
nodes: 'Nodes',
health: 'Health',
action: 'Action',
};
};
export const getUnifiedResourceTableColumnPresentations = (
layoutMode: UnifiedResourceTableLayoutMode,
layoutWidth?: number,
): UnifiedResourceTableColumnPresentations => {
if (layoutMode === 'mobile') {
// Mobile widths are percentages so the prioritized host columns fill the
// viewport without horizontal scroll. Hidden columns keep placeholder
// widths that never render.
return {
resourceColumn: buildUnifiedResourceTableColumnPresentation('', '34%'),
serviceResourceColumn: buildUnifiedResourceTableColumnPresentation('', '28%'),
metricColumn: buildUnifiedResourceTableColumnPresentation('', '14%'),
ioColumn: buildUnifiedResourceTableColumnPresentation('', '12%'),
sourceColumn: buildUnifiedResourceTableColumnPresentation('', '20%'),
serviceSourceColumn: buildUnifiedResourceTableColumnPresentation('', '10%'),
uptimeColumn: buildUnifiedResourceTableColumnPresentation('', '12%'),
tempColumn: buildUnifiedResourceTableColumnPresentation('', '15%'),
serviceCountColumn: buildUnifiedResourceTableColumnPresentation('', '11%'),
serviceQueueColumn: buildUnifiedResourceTableColumnPresentation('', '8%'),
serviceHealthColumn: buildUnifiedResourceTableColumnPresentation('', '13%'),
serviceActionColumn: buildUnifiedResourceTableColumnPresentation('', '14%'),
};
}
if (layoutMode === 'tablet') {
if (typeof layoutWidth === 'number' && isUnifiedResourceHostDiskIoVisible(layoutWidth)) {
return {
resourceColumn: buildUnifiedResourceTableColumnPresentation('', '28%'),
serviceResourceColumn: buildUnifiedResourceTableColumnPresentation('', '24%'),
metricColumn: buildUnifiedResourceTableColumnPresentation('', '12%'),
ioColumn: buildUnifiedResourceTableColumnPresentation('', '12%'),
sourceColumn: buildUnifiedResourceTableColumnPresentation('', '12%'),
serviceSourceColumn: buildUnifiedResourceTableColumnPresentation('', '8%'),
uptimeColumn: buildUnifiedResourceTableColumnPresentation('', '7%'),
tempColumn: buildUnifiedResourceTableColumnPresentation('', '6%'),
serviceCountColumn: buildUnifiedResourceTableColumnPresentation('', '8%'),
serviceQueueColumn: buildUnifiedResourceTableColumnPresentation('', '7.5%'),
serviceHealthColumn: buildUnifiedResourceTableColumnPresentation('', '14%'),
serviceActionColumn: buildUnifiedResourceTableColumnPresentation('', '13%'),
};
}
return {
resourceColumn: buildUnifiedResourceTableColumnPresentation('', '34%'),
serviceResourceColumn: buildUnifiedResourceTableColumnPresentation('', '24%'),
metricColumn: buildUnifiedResourceTableColumnPresentation('', '13%'),
ioColumn: buildUnifiedResourceTableColumnPresentation('', '14%'),
sourceColumn: buildUnifiedResourceTableColumnPresentation('', '13%'),
serviceSourceColumn: buildUnifiedResourceTableColumnPresentation('', '8%'),
uptimeColumn: buildUnifiedResourceTableColumnPresentation('', '7%'),
tempColumn: buildUnifiedResourceTableColumnPresentation('', '6%'),
serviceCountColumn: buildUnifiedResourceTableColumnPresentation('', '8%'),
serviceQueueColumn: buildUnifiedResourceTableColumnPresentation('', '7.5%'),
serviceHealthColumn: buildUnifiedResourceTableColumnPresentation('', '14%'),
serviceActionColumn: buildUnifiedResourceTableColumnPresentation('', '13%'),
};
}
if (layoutMode === 'compact') {
return {
resourceColumn: buildUnifiedResourceTableColumnPresentation('', '16%'),
serviceResourceColumn: buildUnifiedResourceTableColumnPresentation('', '18%'),
metricColumn: buildUnifiedResourceTableColumnPresentation('', '10%'),
ioColumn: buildUnifiedResourceTableColumnPresentation('', '12.5%'),
sourceColumn: buildUnifiedResourceTableColumnPresentation('', '13%'),
serviceSourceColumn: buildUnifiedResourceTableColumnPresentation('', '9.5%'),
uptimeColumn: buildUnifiedResourceTableColumnPresentation('', '7.5%'),
tempColumn: buildUnifiedResourceTableColumnPresentation('', '6.5%'),
serviceCountColumn: buildUnifiedResourceTableColumnPresentation('', '9.5%'),
serviceQueueColumn: buildUnifiedResourceTableColumnPresentation('', '9.5%'),
serviceHealthColumn: buildUnifiedResourceTableColumnPresentation('', '14%'),
serviceActionColumn: buildUnifiedResourceTableColumnPresentation('', '12%'),
};
}
return {
resourceColumn: buildUnifiedResourceTableColumnPresentation('', '18%'),
serviceResourceColumn: buildUnifiedResourceTableColumnPresentation('', '18%'),
metricColumn: buildUnifiedResourceTableColumnPresentation('', '10.5%'),
ioColumn: buildUnifiedResourceTableColumnPresentation('', '12.5%'),
sourceColumn: buildUnifiedResourceTableColumnPresentation('', '10%'),
serviceSourceColumn: buildUnifiedResourceTableColumnPresentation('', '10%'),
uptimeColumn: buildUnifiedResourceTableColumnPresentation('', '8%'),
tempColumn: buildUnifiedResourceTableColumnPresentation('', '7.5%'),
serviceCountColumn: buildUnifiedResourceTableColumnPresentation('', '8.5%'),
serviceQueueColumn: buildUnifiedResourceTableColumnPresentation('', '8.5%'),
serviceHealthColumn: buildUnifiedResourceTableColumnPresentation('', '14%'),
serviceActionColumn: buildUnifiedResourceTableColumnPresentation('', '16%'),
};
};
@@ -1,267 +0,0 @@
import { createEffect, createMemo, createSignal, onCleanup } from 'solid-js';
import type { Resource } from '@/types/resource';
import type { ColumnPriority } from '@/hooks/useBreakpoint';
import { useBreakpoint } from '@/hooks/useBreakpoint';
import { useAlertsActivation } from '@/stores/alertsActivation';
import type { AlertThresholdScope, DisplayMetricType } from '@/utils/metricThresholds';
import {
buildInfrastructureSummaryGroupScope,
splitPrimaryAndServiceResources,
sortResources,
groupResources,
computeIOScale,
} from '@/components/Infrastructure/infrastructureSelectors';
import type { SummarySeriesGroupScope } from '@/components/shared/summaryCardInteraction';
import { useTableWindowing } from './useTableWindowing';
import { useUnifiedResourceTableViewportSync } from './useUnifiedResourceTableViewportSync';
import {
buildHostRowIndexById,
buildHostTableItems,
buildResourceLabelById,
getHostRevealTargetIndex,
getHostSpacerHeights,
getNextUnifiedResourceTableSortState,
getUnifiedResourceTableColumnPresentations,
getUnifiedResourceTableHeaderLabels,
getUnifiedResourceTableLayoutMode,
isUnifiedResourceHostDiskIoVisible,
isUnifiedResourceServiceColumnVisible,
isUnifiedResourceTableColumnVisible,
getUnifiedResourceTableShellClass,
getUnifiedResourceTableSortIndicator,
getUnifiedSources,
getVisibleHostTableItems,
HOST_TABLE_ESTIMATED_ROW_HEIGHT,
HOST_TABLE_WINDOW_SIZE,
normalizeUnifiedResourceTableLayoutWidth,
shouldShowUnifiedResourceHostTable,
shouldUseUnifiedResourceTableMobileLayout,
sortServiceResources,
type HostTableItem,
type UnifiedResourceTableSortKey as SortKey,
} from './unifiedResourceTableStateModel';
export interface UnifiedResourceTableProps {
resources: Resource[];
expandedResourceId: string | null;
clearPinnedSummaryScope?: () => void;
highlightedResourceId?: string | null;
revealedResourceId?: string | null;
hoveredResourceId?: string | null;
activeSummaryGroupScope?: SummarySeriesGroupScope | null;
hoveredSummaryGroupScope?: SummarySeriesGroupScope | null;
focusedSummaryGroupScope?: SummarySeriesGroupScope | null;
focusedSummaryGroupId?: string | null;
onExpandedResourceChange: (id: string | null) => void;
onHoverChange?: (id: string | null) => void;
onGroupFocusChange?: (groupId: string | null) => void;
onGroupHoverChange?: (scope: SummarySeriesGroupScope | null) => void;
groupingMode?: 'grouped' | 'flat';
onDeployCluster?: (clusterId: string, clusterName: string) => void;
setTableRootRef?: (element: HTMLDivElement | undefined) => void;
}
export function useUnifiedResourceTableState(props: UnifiedResourceTableProps) {
const breakpoint = useBreakpoint();
const alertsActivation = useAlertsActivation();
const [tableContainerWidth, setTableContainerWidth] = createSignal<number | null>(null);
const [tableRootElement, setTableRootElement] = createSignal<HTMLDivElement | null>(null);
const [sortKey, setSortKey] = createSignal<SortKey>('default');
const [sortDirection, setSortDirection] = createSignal<'asc' | 'desc'>('asc');
const updateTableContainerWidth = (width: number | null | undefined) => {
if (typeof width === 'number' && Number.isFinite(width) && width > 0) {
setTableContainerWidth(Math.round(width));
}
};
const setTableRootRef = (element: HTMLDivElement | undefined) => {
setTableRootElement(element ?? null);
props.setTableRootRef?.(element);
if (element) {
updateTableContainerWidth(element.clientWidth);
} else {
setTableContainerWidth(null);
}
};
createEffect(() => {
const element = tableRootElement();
if (!element || typeof ResizeObserver === 'undefined') return;
const observer = new ResizeObserver((entries) => {
const entryWidth = entries[0]?.contentRect.width;
updateTableContainerWidth(entryWidth ?? element.clientWidth);
});
observer.observe(element);
onCleanup(() => observer.disconnect());
});
const fallbackViewportWidth = () => {
const widthAccessor = breakpoint.width as (() => number) | undefined;
const width = widthAccessor?.();
return normalizeUnifiedResourceTableLayoutWidth(
width,
typeof window !== 'undefined' ? window.innerWidth : undefined,
);
};
const tableLayoutWidth = createMemo(() =>
normalizeUnifiedResourceTableLayoutWidth(tableContainerWidth(), fallbackViewportWidth()),
);
const layoutMode = createMemo(() => getUnifiedResourceTableLayoutMode(tableLayoutWidth()));
const isMobile = createMemo(() => shouldUseUnifiedResourceTableMobileLayout(tableLayoutWidth()));
const isHostDiskIoVisible = createMemo(() =>
isUnifiedResourceHostDiskIoVisible(tableLayoutWidth()),
);
const isVisible = (priority: ColumnPriority) =>
isUnifiedResourceTableColumnVisible(priority, tableLayoutWidth());
const isServiceVisible = (priority: ColumnPriority) =>
isUnifiedResourceServiceColumnVisible(priority, tableLayoutWidth());
const split = createMemo(() => splitPrimaryAndServiceResources(props.resources));
const primaryResources = createMemo(() => split().primaryResources);
const serviceResources = createMemo(() => split().services);
const primaryResourceIds = createMemo(
() => new Set(primaryResources().map((resource) => resource.id)),
);
const serviceResourceIds = createMemo(
() => new Set(serviceResources().map((resource) => resource.id)),
);
const sortedResources = createMemo(() =>
sortResources(primaryResources(), sortKey(), sortDirection()),
);
const resourceLabelById = createMemo(() => buildResourceLabelById(props.resources));
const resolveResourceLabel = (resourceId: string): string | undefined =>
resourceLabelById().get(resourceId);
const groupedResources = createMemo(() =>
groupResources(sortedResources(), props.groupingMode ?? 'grouped'),
);
const hostTableItems = createMemo<HostTableItem[]>(() =>
buildHostTableItems(groupedResources(), props.groupingMode),
);
const hostRowIndexById = createMemo(() => buildHostRowIndexById(hostTableItems()));
const hostRevealTargetIndex = createMemo<number | null>(() =>
getHostRevealTargetIndex(
hostRowIndexById(),
props.expandedResourceId,
props.highlightedResourceId,
props.revealedResourceId,
),
);
const hostWindowing = useTableWindowing({
totalCount: () => hostTableItems().length,
windowSize: HOST_TABLE_WINDOW_SIZE,
revealIndex: hostRevealTargetIndex,
});
const visibleHostTableItems = createMemo(() =>
getVisibleHostTableItems(
hostTableItems(),
hostWindowing.isWindowed(),
hostWindowing.startIndex(),
hostWindowing.endIndex(),
),
);
const hostSpacerHeights = createMemo(() =>
getHostSpacerHeights(
hostTableItems().length,
hostWindowing.startIndex(),
hostWindowing.endIndex(),
hostWindowing.isWindowed(),
HOST_TABLE_ESTIMATED_ROW_HEIGHT,
),
);
const viewportSync = useUnifiedResourceTableViewportSync({
totalCount: () => hostTableItems().length,
estimatedRowHeight: HOST_TABLE_ESTIMATED_ROW_HEIGHT,
hostWindowing,
});
const sortedPBSResources = createMemo(() => sortServiceResources(serviceResources(), 'pbs'));
const sortedPMGResources = createMemo(() => sortServiceResources(serviceResources(), 'pmg'));
const ioScale = createMemo(() => computeIOScale(primaryResources()));
const handleSort = (key: Exclude<SortKey, 'default'>) => {
const nextSort = getNextUnifiedResourceTableSortState(sortKey(), sortDirection(), key);
setSortKey(nextSort.key);
setSortDirection(nextSort.direction);
};
const renderSortIndicator = (key: SortKey) =>
getUnifiedResourceTableSortIndicator(sortKey(), sortDirection(), key);
const getMetricThresholds = (
scope: AlertThresholdScope,
metric: DisplayMetricType,
resourceIds?: string | string[],
) => alertsActivation.getMetricThresholds(scope, metric, resourceIds);
const toggleExpand = (resourceId: string) => {
props.onExpandedResourceChange(props.expandedResourceId === resourceId ? null : resourceId);
};
const tableShellClass = createMemo(() => getUnifiedResourceTableShellClass(layoutMode()));
const columnPresentations = createMemo(() =>
getUnifiedResourceTableColumnPresentations(layoutMode(), tableLayoutWidth()),
);
const headerLabels = createMemo(() => getUnifiedResourceTableHeaderLabels(layoutMode()));
const showHostTable = createMemo(() =>
shouldShowUnifiedResourceHostTable(primaryResources().length, serviceResources().length),
);
const showHostClearAction = createMemo(() =>
Boolean(
props.focusedSummaryGroupId ||
(props.expandedResourceId && primaryResourceIds().has(props.expandedResourceId)),
),
);
const showServiceClearAction = createMemo(() =>
Boolean(props.expandedResourceId && serviceResourceIds().has(props.expandedResourceId)),
);
return {
layoutMode,
isMobile,
isHostDiskIoVisible,
isVisible,
isServiceVisible,
getMetricThresholds,
handleSort,
renderSortIndicator,
resolveResourceLabel,
setTableRootRef,
visibleHostTableItems,
hostTopSpacerHeight: () => hostSpacerHeights().top,
hostBottomSpacerHeight: () => hostSpacerHeights().bottom,
hostWindowing,
sortedPBSResources,
sortedPMGResources,
ioScale,
...viewportSync,
tableShellClass,
headerLabels,
resourceColumn: () => columnPresentations().resourceColumn,
serviceResourceColumn: () => columnPresentations().serviceResourceColumn,
metricColumn: () => columnPresentations().metricColumn,
ioColumn: () => columnPresentations().ioColumn,
sourceColumn: () => columnPresentations().sourceColumn,
serviceSourceColumn: () => columnPresentations().serviceSourceColumn,
uptimeColumn: () => columnPresentations().uptimeColumn,
tempColumn: () => columnPresentations().tempColumn,
showHostTable,
showHostClearAction,
showServiceClearAction,
serviceCountColumn: () => columnPresentations().serviceCountColumn,
serviceQueueColumn: () => columnPresentations().serviceQueueColumn,
serviceHealthColumn: () => columnPresentations().serviceHealthColumn,
serviceActionColumn: () => columnPresentations().serviceActionColumn,
toggleExpand,
buildHostSummaryGroupScope: buildInfrastructureSummaryGroupScope,
getUnifiedSources,
};
}
export type UnifiedResourceTableState = ReturnType<typeof useUnifiedResourceTableState>;
@@ -1,78 +0,0 @@
import { createEffect, createSignal, onCleanup, untrack, type Accessor } from 'solid-js';
import {
bindWindowedPageScrollEvents,
findWindowedPageScrollContainer,
isWindowedSurfaceHidden,
wheelDeltaInPixels,
} from '@/components/shared/windowedPageScroll';
import { useTableWindowing } from './useTableWindowing';
interface UseUnifiedResourceTableViewportSyncOptions {
totalCount: Accessor<number>;
estimatedRowHeight: number;
hostWindowing: ReturnType<typeof useTableWindowing>;
}
export function useUnifiedResourceTableViewportSync(
options: UseUnifiedResourceTableViewportSyncOptions,
) {
const { totalCount, estimatedRowHeight, hostWindowing } = options;
const [hostBodyRef, setHostBodyRef] = createSignal<HTMLTableSectionElement | null>(null);
const syncHostWindowToViewport = (projectedScrollDelta = 0) => {
if (!hostWindowing.isWindowed() || typeof window === 'undefined') return;
const body = hostBodyRef();
if (!body || isWindowedSurfaceHidden(body)) return;
const rect = body.getBoundingClientRect();
const scrollContainer = findWindowedPageScrollContainer(body);
if (scrollContainer) {
const containerRect = scrollContainer.getBoundingClientRect();
hostWindowing.onScroll(
Math.max(0, containerRect.top - rect.top + projectedScrollDelta),
scrollContainer.clientHeight || window.innerHeight,
estimatedRowHeight,
);
return;
}
hostWindowing.onScroll(
Math.max(0, -rect.top + projectedScrollDelta),
window.innerHeight,
estimatedRowHeight,
);
};
createEffect(() => {
if (typeof window === 'undefined') return;
totalCount();
if (!hostWindowing.isWindowed()) return;
if (!hostBodyRef()) return;
const body = hostBodyRef()!;
const scrollContainer = findWindowedPageScrollContainer(body);
const scrollTarget: HTMLElement | Window = scrollContainer ?? window;
const viewportHeight = () => scrollContainer?.clientHeight || window.innerHeight;
const handleViewportChange = () => syncHostWindowToViewport();
const handleWheel = (event: Event) => {
const wheelEvent = event as WheelEvent;
if (wheelEvent.deltaY === 0) return;
syncHostWindowToViewport(wheelDeltaInPixels(wheelEvent, viewportHeight()));
};
// The initial measurement reads the current window bounds and may move the
// window. Keep those reads outside this setup effect's dependency graph;
// otherwise a far-away reveal target and the viewport can repeatedly move
// the window back and forth until Solid exhausts the call stack.
untrack(handleViewportChange);
onCleanup(
bindWindowedPageScrollEvents({
scrollTarget,
onScroll: handleViewportChange,
onWheel: handleWheel,
onResize: handleViewportChange,
}),
);
});
return {
setHostBodyRef,
};
}
@@ -101,7 +101,6 @@ import summaryTableFocusSource from '@/components/shared/summaryTableFocus.ts?ra
import tableCardSource from '@/components/shared/TableCard.tsx?raw';
import groupedTableModeSegmentedControlSource from '@/components/shared/GroupedTableModeSegmentedControl.tsx?raw';
import groupedTableRowPresentationSource from '@/components/shared/groupedTableRowPresentation.ts?raw';
import unifiedResourceTableViewportSyncSource from '@/components/Infrastructure/useUnifiedResourceTableViewportSync.ts?raw';
import storagePoolsTableWindowingSource from '@/components/Storage/useStoragePoolsTableWindowing.ts?raw';
import workloadViewportSyncSource from '@/components/Workloads/useWorkloadViewportSync.ts?raw';
import platformWindowedItemsSource from '@/features/platformPage/usePlatformWindowedItems.ts?raw';
@@ -223,10 +222,6 @@ import vsphereHostsTableSource from '@/features/vmware/VsphereHostsTable.tsx?raw
import availabilityChecksTableSource from '@/features/standalone/AvailabilityChecksTable.tsx?raw';
import agentsMachinesTableSource from '@/features/standalone/AgentsMachinesTable.tsx?raw';
import agentMachineTableModelSource from '@/features/standalone/agentMachineTableModel.ts?raw';
import unifiedResourceHostTableCardSource from '@/components/Infrastructure/UnifiedResourceHostTableCard.tsx?raw';
import unifiedResourceServiceInfrastructureCardSource from '@/components/Infrastructure/UnifiedResourceServiceInfrastructureCard.tsx?raw';
import unifiedResourcePBSTableSectionSource from '@/components/Infrastructure/UnifiedResourcePBSTableSection.tsx?raw';
import unifiedResourcePMGTableSectionSource from '@/components/Infrastructure/UnifiedResourcePMGTableSection.tsx?raw';
import proxmoxMailGatewayDrawerSource from '@/features/proxmox/ProxmoxMailGatewayDrawer.tsx?raw';
import swarmServicesDrawerSource from '@/components/Docker/SwarmServicesDrawer.tsx?raw';
import k8sDeploymentsDrawerSource from '@/components/Kubernetes/K8sDeploymentsDrawer.tsx?raw';
@@ -1057,8 +1052,6 @@ describe('shared primitive guardrails', () => {
expect(registeredRule?.canonical?.path).toBe('src/components/shared/TableCardHeader.tsx');
expect(registeredRule?.canonical?.export).toBe('TableCardHeader');
expect(registeredRule?.requiredConsumers?.map((consumer) => consumer.path)).toEqual([
'src/components/Infrastructure/UnifiedResourceHostTableCard.tsx',
'src/components/Infrastructure/UnifiedResourceServiceInfrastructureCard.tsx',
'src/components/Storage/StorageContentCard.tsx',
'src/features/platformPage/sharedPlatformPage.tsx',
]);
@@ -1080,11 +1073,7 @@ describe('shared primitive guardrails', () => {
expect(tableCardHeaderSource).toContain('TABLE_CARD_HEADER_CLASS');
expect(tableCardHeaderSource).not.toContain('Pinned to');
expect(tableCardHeaderSource).not.toContain('Scoped to');
for (const source of [
unifiedResourceHostTableCardSource,
unifiedResourceServiceInfrastructureCardSource,
storageContentCardSource,
]) {
for (const source of [storageContentCardSource]) {
expect(source).toContain('TableCardHeader');
expect(source).not.toContain('SummaryTableCardHeader');
}
@@ -1116,8 +1105,6 @@ describe('shared primitive guardrails', () => {
expect(registeredRule?.canonical?.path).toBe('src/components/shared/TableCard.tsx');
expect(registeredRule?.canonical?.export).toBe('TableCard');
expect(registeredRule?.requiredConsumers?.map((consumer) => consumer.path)).toEqual([
'src/components/Infrastructure/UnifiedResourceHostTableCard.tsx',
'src/components/Infrastructure/UnifiedResourceServiceInfrastructureCard.tsx',
'src/components/Storage/StorageContentCard.tsx',
'src/components/Workloads/WorkloadsSurface.tsx',
'src/components/Workloads/WorkloadsTable.tsx',
@@ -1149,8 +1136,6 @@ describe('shared primitive guardrails', () => {
workloadsSurfaceSource,
alertHistoryTableSectionSource,
sharedPlatformPageSource,
unifiedResourceHostTableCardSource,
unifiedResourceServiceInfrastructureCardSource,
storageContentCardSource,
]) {
expect(source).toContain('TableCard');
@@ -1394,13 +1379,7 @@ describe('shared primitive guardrails', () => {
expect(guestRowStateSource).not.toContain('bg-sky-50/70');
expect(guestRowStateSource).not.toContain('ring-sky-400/25');
for (const source of [
storagePoolRowSource,
diskListSource,
unifiedResourceHostTableCardSource,
unifiedResourcePBSTableSectionSource,
unifiedResourcePMGTableSectionSource,
]) {
for (const source of [storagePoolRowSource, diskListSource]) {
expect(source).toContain('data-summary-row-active');
expect(source).not.toContain('bg-sky-50/70');
expect(source).not.toContain('ring-sky-400/25');
@@ -1416,8 +1395,6 @@ describe('shared primitive guardrails', () => {
expect(nodeGroupHeaderSource).toContain('getGroupedTableRowCellClass');
expect(workloadPanelSource).toContain('getInteractiveGroupedTableRowClass');
expect(workloadPanelSource).toContain('getGroupedTableRowCellClass');
expect(unifiedResourceHostTableCardSource).toContain('getInteractiveGroupedTableRowClass');
expect(unifiedResourceHostTableCardSource).toContain('getGroupedTableRowCellClass');
expect(alertHistoryTableGroupRowSource).toContain('getGroupedTableRowClass');
expect(alertHistoryTableGroupRowSource).toContain('getGroupedTableRowCellClass');
expect(alertHistoryTableGroupRowSource).not.toContain('class="bg-surface-alt"');
@@ -1426,7 +1403,6 @@ describe('shared primitive guardrails', () => {
expect(infrastructureSourceManagerSource).toContain('getGroupedTableRowClass');
expect(infrastructureSourceManagerSource).toContain('getGroupedTableRowCellClass');
expect(infrastructureSourceManagerSource).not.toContain('bg-base hover:bg-base');
expect(unifiedResourceHostTableCardSource).toContain('data-summary-group-member-active');
});
it('routes Proxmox node version presentation through the shared formatter', () => {
@@ -1463,7 +1439,6 @@ describe('shared primitive guardrails', () => {
expect(windowedPageScrollSource).not.toContain("addEventListener('touch");
for (const source of [
platformWindowedItemsSource,
unifiedResourceTableViewportSyncSource,
storagePoolsTableWindowingSource,
workloadViewportSyncSource,
]) {
@@ -1487,9 +1462,6 @@ describe('shared primitive guardrails', () => {
expect(tableCardHeaderSource).toContain('TABLE_CARD_HEADER_CLASS');
for (const source of [
unifiedResourceHostTableCardSource,
unifiedResourcePBSTableSectionSource,
unifiedResourcePMGTableSectionSource,
alertHistoryTableSectionSource,
workloadsTableSource,
storagePoolsTableSource,
@@ -1509,7 +1481,6 @@ describe('shared primitive guardrails', () => {
}
for (const source of [
unifiedResourceHostTableCardSource,
alertHistoryTableSectionSource,
workloadsTableSource,
storageContentCardSource,
@@ -1517,9 +1488,7 @@ describe('shared primitive guardrails', () => {
expect(source).toContain('<TableCard');
}
for (const source of [unifiedResourceHostTableCardSource, storageContentCardSource]) {
expect(source).toContain('TableCardHeader');
}
expect(storageContentCardSource).toContain('TableCardHeader');
expect(alertHistoryTableSectionSource).not.toContain(
'overflow-hidden rounded-sm border border-border',
@@ -1718,14 +1687,7 @@ describe('shared primitive guardrails', () => {
expect(platformResourceDetailTableRowSource).toContain('SummaryRowActionButton');
expect(platformResourceDetailTableRowSource).toContain('hideWhenRowTappableOnMobile');
for (const source of [
guestRowSource,
storagePoolRowSource,
diskListSource,
unifiedResourceHostTableCardSource,
unifiedResourcePBSTableSectionSource,
unifiedResourcePMGTableSectionSource,
]) {
for (const source of [guestRowSource, storagePoolRowSource, diskListSource]) {
expect(source).toContain('createSummaryInteractiveRowPreviewHandlers');
expect(source).toContain('SummaryRowActionButton');
expect(source).toContain('hideWhenRowTappableOnMobile');
@@ -1740,7 +1702,6 @@ describe('shared primitive guardrails', () => {
expect(workloadPanelSource).toContain('createSummaryInteractiveRowPreviewHandlers');
expect(workloadPanelSource).not.toContain('kind="scope"');
expect(storageGroupRowSource).not.toContain('kind="scope"');
expect(unifiedResourceHostTableCardSource).not.toContain('kind="scope"');
expect(nodeGroupHeaderSource).toContain('ResourceNameWithWebInterfaceLink');
expect(webInterfaceLinkSource).toContain('event.stopPropagation()');
@@ -5394,9 +5355,6 @@ describe('shared primitive guardrails', () => {
'src/features/proxmox/ProxmoxNodesTable.tsx',
'src/components/Alerts/AlertResourceGroupHeader.tsx',
'src/components/Alerts/AlertResourceTableRow.tsx',
'src/components/Infrastructure/UnifiedResourceHostTableCard.tsx',
'src/components/Infrastructure/UnifiedResourcePBSTableSection.tsx',
'src/components/Infrastructure/UnifiedResourcePMGTableSection.tsx',
'src/features/docker/DockerNativeTableShared.tsx',
'src/features/docker/DockerContainersTable.tsx',
'src/features/kubernetes/KubernetesClustersTable.tsx',
@@ -5413,9 +5371,6 @@ describe('shared primitive guardrails', () => {
expect(proxmoxNodesTableSource).toContain('ResourceNameWithWebInterfaceLink');
expect(alertResourceGroupHeaderSource).toContain('ResourceNameWithWebInterfaceLink');
expect(alertResourceTableRowSource).toContain('ResourceNameWithWebInterfaceLink');
expect(unifiedResourceHostTableCardSource).toContain('ResourceNameWithWebInterfaceLink');
expect(unifiedResourcePBSTableSectionSource).toContain('ResourceNameWithWebInterfaceLink');
expect(unifiedResourcePMGTableSectionSource).toContain('ResourceNameWithWebInterfaceLink');
expect(nodeGroupHeaderSource).not.toContain('target="_blank"');
expect(guestRowSource).not.toContain('target="_blank"');
expect(agentsMachinesTableSource).not.toContain('target="_blank"');
@@ -7766,9 +7721,6 @@ describe('shared primitive guardrails', () => {
expect(inlineDetailTableRowSource).toContain('.focus({ preventScroll: true })');
expect(registeredRule?.requiredConsumers?.map((consumer) => consumer.path)).toEqual(
expect.arrayContaining([
'src/components/Infrastructure/UnifiedResourceHostTableCard.tsx',
'src/components/Infrastructure/UnifiedResourcePBSTableSection.tsx',
'src/components/Infrastructure/UnifiedResourcePMGTableSection.tsx',
'src/components/Workloads/WorkloadPanel.tsx',
'src/features/docker/DockerHostsTable.tsx',
'src/features/docker/DockerNetworksTable.tsx',
@@ -7796,9 +7748,6 @@ describe('shared primitive guardrails', () => {
dockerHostsTableSource,
proxmoxCoverageTableSource,
proxmoxNodesTableSource,
unifiedResourceHostTableCardSource,
unifiedResourcePBSTableSectionSource,
unifiedResourcePMGTableSectionSource,
workloadPanelSource,
]) {
expect(source).toContain('InlineDetailTableRow');
@@ -235,7 +235,6 @@ describe('platform overview layout guardrails', () => {
expect(source).not.toContain('const [search, ');
}
expect(sharedPlatformPageSource).toContain('searchSuggestions={tableState.searchSuggestions}');
expect(platformSearchSuggestionsSource).toContain('buildPlatformResourceSearchSuggestions');
expect(platformSearchSuggestionsSource).not.toContain('platformData');
});
@@ -36,7 +36,6 @@ import { TableCardHeader } from '@/components/shared/TableCardHeader';
import { useActiveHorizontalRailItemVisibility } from '@/components/shared/useActiveHorizontalRailItemVisibility';
import { useBreakpoint } from '@/hooks/useBreakpoint';
import { usePersistentSignal } from '@/hooks/usePersistentSignal';
import { UnifiedResourceTable } from '@/components/Infrastructure/UnifiedResourceTable';
import type { Resource } from '@/types/resource';
import { formatBytes, formatRelativeTime, formatUptime } from '@/utils/format';
import { useRelativeTimeNow } from '@/utils/relativeTimeClock';
@@ -1103,14 +1102,6 @@ export type PlatformResourceStatusFilter = 'all' | 'online' | 'degraded' | 'offl
const statusDot = filterChipStatusDot;
export const PLATFORM_STATUS_FILTER_OPTIONS: PlatformTableFilterOption<PlatformResourceStatusFilter>[] =
[
{ value: 'all', label: 'All' },
{ value: 'online', label: 'Online', tone: 'success', leading: statusDot('bg-emerald-500') },
{ value: 'degraded', label: 'Degraded', tone: 'warning', leading: statusDot('bg-amber-500') },
{ value: 'offline', label: 'Offline', tone: 'danger', leading: statusDot('bg-red-500') },
];
export const PLATFORM_HEALTH_FILTER_OPTIONS: PlatformTableFilterOption<PlatformResourceStatusFilter>[] =
[
{ value: 'all', label: 'All' },
@@ -1455,70 +1446,3 @@ export function PlatformTableToolbar<T extends string | number>(props: {
/>
);
}
export const PlatformResourceTable: Component<{
resources: Resource[];
emptyIcon: JSX.Element;
emptyTitle: string;
emptyDescription: string;
groupingMode?: 'grouped' | 'flat';
searchPlaceholder?: string;
}> = (props) => {
const [expandedResourceId, setExpandedResourceId] = createSignal<string | null>(null);
const tableState = createPlatformTableFilterState({
resources: () => props.resources,
initialStatus: 'all' as PlatformResourceStatusFilter,
filter: filterPlatformResources,
});
return (
<Show
when={props.resources.length > 0}
fallback={
<PlatformTableEmptyState
icon={props.emptyIcon}
title={props.emptyTitle}
description={props.emptyDescription}
/>
}
>
<div class="space-y-3">
<PlatformTableToolbar
search={tableState.search}
onSearchChange={tableState.setSearch}
searchPlaceholder={props.searchPlaceholder ?? 'Search rows'}
searchSuggestions={tableState.searchSuggestions}
status={tableState.status()}
onStatusChange={tableState.setStatus}
statusOptions={withPlatformStatusCounts(
PLATFORM_STATUS_FILTER_OPTIONS,
tableState.countForStatus,
)}
visible={tableState.visible()}
total={tableState.total()}
rowNoun="rows"
hasActiveFilters={tableState.hasActiveFilters()}
onResetFilters={tableState.resetFilters}
/>
<Show
when={tableState.filtered().length > 0}
fallback={
<PlatformTableEmptyState
icon={props.emptyIcon}
title="No rows match current filters"
description="Adjust the search or status filter to see more rows."
actions={<PlatformTableResetFiltersButton onReset={tableState.resetFilters} />}
/>
}
>
<UnifiedResourceTable
resources={tableState.filtered()}
expandedResourceId={expandedResourceId()}
onExpandedResourceChange={setExpandedResourceId}
groupingMode={props.groupingMode ?? 'grouped'}
/>
</Show>
</div>
</Show>
);
};
@@ -261,11 +261,6 @@ import resourceDetailDrawerOperationalModelSource from '@/components/Infrastruct
import resourceDetailDrawerServiceModelSource from '@/components/Infrastructure/resourceDetailDrawerServiceModel.ts?raw';
import resourceDetailDrawerDockerActionsStateSource from '@/components/Infrastructure/useResourceDetailDrawerDockerActionsState.ts?raw';
import resourceDetailDrawerStateSource from '@/components/Infrastructure/useResourceDetailDrawerState.ts?raw';
import unifiedResourceTableSource from '@/components/Infrastructure/UnifiedResourceTable.tsx?raw';
import unifiedResourceTableStateSource from '@/components/Infrastructure/useUnifiedResourceTableState.ts?raw';
import unifiedResourceTableViewportSyncSource from '@/components/Infrastructure/useUnifiedResourceTableViewportSync.ts?raw';
import unifiedResourceTableStateModelSource from '@/components/Infrastructure/unifiedResourceTableStateModel.ts?raw';
import unifiedResourceTableModelSource from '@/components/Infrastructure/unifiedResourceTableModel.ts?raw';
import useUnifiedResourcesSource from '@/hooks/useUnifiedResources.ts?raw';
import useWorkloadsSource from '@/hooks/useWorkloads.ts?raw';
import findingsPanelSource from '@/components/AI/FindingsPanel.tsx?raw';
@@ -2499,52 +2494,6 @@ describe('frontend resource type boundaries', () => {
expect(resourceDetailMappersSource).not.toContain('export const toDiscoveryConfig');
expect(resourceDetailMappersSource).not.toContain('export const normalizeHealthLabel');
expect(resourceDetailMappersSource).not.toContain('export const healthToneClass');
expect(unifiedResourceTableSource).toContain('useUnifiedResourceTableState');
expect(unifiedResourceTableSource).toContain('UnifiedResourceHostTableCard');
expect(unifiedResourceTableSource).toContain('UnifiedResourceServiceInfrastructureCard');
expect(unifiedResourceTableSource).not.toContain('const split = createMemo(() =>');
expect(unifiedResourceTableSource).not.toContain('const sortedPBSResources = createMemo(() =>');
expect(unifiedResourceTableSource).not.toContain(
'const resourceColumnStyle = createMemo(() =>',
);
expect(unifiedResourceTableSource).not.toContain('getServiceHealthSummaryPresentation');
expect(unifiedResourceTableSource).not.toContain('const getOutlierEmphasis =');
expect(unifiedResourceTableSource).not.toContain('const summarizeServiceHealthTone =');
expect(unifiedResourceTableStateSource).toContain(
'export function useUnifiedResourceTableState',
);
expect(unifiedResourceTableStateSource).toContain("from './unifiedResourceTableStateModel'");
expect(unifiedResourceTableStateSource).toContain('buildHostTableItems');
expect(unifiedResourceTableStateSource).toContain('getUnifiedResourceTableColumnPresentations');
expect(unifiedResourceTableStateSource).toContain('getUnifiedResourceTableShellClass');
expect(unifiedResourceTableStateSource).toContain('useTableWindowing');
expect(unifiedResourceTableStateSource).toContain('useUnifiedResourceTableViewportSync');
expect(unifiedResourceTableStateSource).not.toContain(
'const resourceColumnStyle = createMemo(() =>',
);
expect(unifiedResourceTableStateSource).not.toContain(
"const showGroupHeaders = props.groupingMode === 'grouped'",
);
expect(unifiedResourceTableStateSource).not.toContain('const items: HostTableItem[] = [];');
expect(unifiedResourceTableStateSource).not.toContain('window.addEventListener');
expect(unifiedResourceTableStateSource).not.toContain('getBoundingClientRect');
expect(unifiedResourceTableStateModelSource).toContain('export const buildHostTableItems');
expect(unifiedResourceTableStateModelSource).toContain(
'export const getUnifiedResourceTableColumnPresentations',
);
expect(unifiedResourceTableStateModelSource).toContain(
'export const getUnifiedResourceTableShellClass',
);
expect(unifiedResourceTableStateModelSource).toContain(
'export const getNextUnifiedResourceTableSortState',
);
expect(unifiedResourceTableModelSource).toContain('getServiceHealthSummaryPresentation');
expect(unifiedResourceTableModelSource).toContain('export const getOutlierEmphasis');
expect(unifiedResourceTableViewportSyncSource).toContain('bindWindowedPageScrollEvents');
expect(unifiedResourceTableViewportSyncSource).not.toContain('window.addEventListener');
expect(unifiedResourceTableViewportSyncSource).toContain('getBoundingClientRect');
expect(unifiedResourceTableViewportSyncSource).toContain('hostWindowing.onScroll');
expect(unifiedResourceTableViewportSyncSource).not.toContain('scrollIntoView');
expect(discoveryTabSource).toContain('getDiscoverySuggestedURLReason');
expect(discoveryTabSource).toContain('getDiscoveryObservedSourceLabel');
expect(discoveryTabSource).toContain('getDiscoveryAnalysisProviderBadgeClass');
@@ -378,11 +378,11 @@ class SubsystemLookupTest(unittest.TestCase):
match["verification_requirement"]["exact_files"],
)
def test_lookup_paths_assigns_unified_resource_table_state_model_to_shared_infrastructure_hot_path(
def test_lookup_paths_assigns_resource_detail_mappers_to_shared_infrastructure_hot_path(
self,
) -> None:
result = lookup_paths(
["frontend-modern/src/components/Infrastructure/unifiedResourceTableStateModel.ts"]
["frontend-modern/src/components/Infrastructure/resourceDetailMappers.ts"]
)
self.assertEqual(result["unowned_runtime_files"], [])
self.assertEqual(
@@ -426,7 +426,7 @@ class SubsystemLookupTest(unittest.TestCase):
"resource-consumers",
)
self.assertIn(
"frontend-modern/src/components/Infrastructure/__tests__/unifiedResourceTableStateModel.test.ts",
"frontend-modern/src/components/Infrastructure/__tests__/resourceDetailMappers.test.ts",
unified_resources_match["verification_requirement"]["exact_files"],
)
@@ -136,7 +136,7 @@ const PLATFORM_PAGES: readonly PlatformPageCase[] = [
];
// Every populated inventory tab — embedded canonical Workloads/Storage and
// UnifiedResourceTable-backed infrastructure views — owns an operator search
// platform-native infrastructure tables — owns an operator search
// input. Overview stacks are summary surfaces and intentionally suppress their
// table toolbars. Keep each route as an independent browser test so one slow
// navigation cannot consume the time budget or state of the remaining routes.