mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-23 20:25:56 +00:00
Remove the unreachable host metric cells from grouped workload rows (#2622)
Both production mounts of the Workloads surface, the Proxmox and vSphere overviews, set `compactGroupHeaders` unconditionally. The non-compact branch in `WorkloadPanel` (per-column node CPU, memory, disk and I/O bars or sparklines, uptime, version, temperature) and the `NodeGroupHeader` column mode and inline host facts that only it reached never rendered. They were left over from the retired standalone Workloads page. This removes the option, the dead renderers and the `NodeGroupHeader` props that only served them. Grouped host rows keep the shape both pages already render: one spanning cell with status, linked name, cluster and agent badges. Host stats stay in each page's own hosts table. Verification - No other production consumer: `WorkloadsSurface` → `WorkloadsTable` → `WorkloadPanel` is entered only from `ProxmoxPageSurface` and `VmwarePageSurface`; no references in pulse-pro, pulse-enterprise or pulse-mobile. - Browser pass on a production build embedded in a private mock backend, `/proxmox/overview` and `/vmware` at 1440x900 and 390x844, Bars and Trends modes: every grouped host row is one cell spanning the guest table's visible columns, with no metric cells or host facts; group-row clicks open nothing; guest drawers still open; no page errors. - The rewritten `NodeGroupHeader` test fails against the old component (its default facts render `PVE 9.1.9`). - `tsc`, full `npm run lint`, vitest over Workloads, shared, features, hooks, utils, Storage and app tests (only the Mac-only `agentInstallCommand` failures, unrelated), bundle budgets, and all release-control guards pass.
This commit is contained in:
@@ -2911,9 +2911,9 @@ Agent`), with the plain-language source phrase available through accessible
|
||||
`pveVersion` or a Pulse Agent report whose OS identity resolves to Unraid or
|
||||
Proxmox VE. They must omit the version rather than showing unrelated
|
||||
collector OS versions, such as Debian 12, beside an API-backed PVE badge.
|
||||
Shared row primitives that render Proxmox node identity, including
|
||||
`frontend-modern/src/components/shared/NodeGroupHeader.tsx`, must route raw
|
||||
PVE manager payloads through
|
||||
Surfaces that render Proxmox node versions, including the Proxmox page
|
||||
model at `frontend-modern/src/features/proxmox/proxmoxPageModel.ts`, must
|
||||
route raw PVE manager payloads through
|
||||
`frontend-modern/src/utils/proxmoxVersion.ts` rather than inlining
|
||||
page-local parsing or falling back to unrelated agent OS versions.
|
||||
System title metadata must apply the same identity rule: once the primary
|
||||
@@ -4395,12 +4395,14 @@ The shared table chrome now allows `TableCardHeader` to expose a right-aligned
|
||||
action slot, currently used by the Workloads/Proxmox metric display control.
|
||||
That slot belongs to the table header band and must not reintroduce nested
|
||||
cards or page-local toolbar wrappers inside `TableCard`. Proxmox host grouping
|
||||
also extends the shared `NodeGroupHeader` row pattern: host metrics may align
|
||||
with workload table columns, but the shared primitive owns the header/table
|
||||
shell boundary rather than platform pages copying their own card headers.
|
||||
Compact PVE version text in that header must come from the shared Proxmox
|
||||
version formatter so raw `pve-manager/...` payloads and platform-page host
|
||||
version cells stay consistent.
|
||||
also extends the shared `NodeGroupHeader` row pattern, which owns the
|
||||
header/table shell boundary rather than platform pages copying their own card
|
||||
headers. `frontend-modern/src/components/shared/NodeGroupHeader.tsx` renders
|
||||
node identity only (status dot, linked name, cluster and agent badges); in
|
||||
table-row mode that identity sits in one cell spanning the table. It carries no
|
||||
per-column cell renderer and no inline
|
||||
version, temperature, or uptime facts: host metrics and versions belong to the
|
||||
platform page's host table, so a group row must not duplicate them.
|
||||
Mobile navigation now recognizes `proxmox` as a first-class platform tab in
|
||||
the shared priority model so app-shell ordering remains centralized.
|
||||
|
||||
|
||||
@@ -1955,22 +1955,20 @@ query persistence once per resource, or run on the steady-state alert hot path;
|
||||
ancestor lookup remains bounded by canonical hierarchy depth for each active
|
||||
alert actually evaluated.
|
||||
|
||||
The embedded WorkloadsSurface exposes a `compactGroupHeaders` prop on
|
||||
`frontend-modern/src/components/Workloads/useWorkloadsState.ts` that
|
||||
platform pages owning their own hosts table (Proxmox overview today) set
|
||||
to strip per-host metric cells out of the `NodeGroupHeader` rows in
|
||||
grouped mode. The flag is threaded through
|
||||
`frontend-modern/src/components/Workloads/WorkloadsSurface.tsx`,
|
||||
`frontend-modern/src/components/Workloads/WorkloadsTable.tsx`, and
|
||||
`frontend-modern/src/components/Workloads/WorkloadPanel.tsx`; when set,
|
||||
`WorkloadPanel` calls `NodeGroupHeader` without `columns` /
|
||||
`renderColumnCell` so the existing colspan layout renders just the
|
||||
status dot, linked node name, cluster badge, and agent badge — the
|
||||
duplicate CPU / Memory / Disk / uptime / temperature / version stats
|
||||
the top-of-page hosts table already owns are dropped. New embedded
|
||||
platform-page consumers that render their own hosts summary must set
|
||||
this flag; standalone Workloads surfaces (no top hosts table) keep the
|
||||
original verbose group rows by default.
|
||||
Grouped host rows in the embedded WorkloadsSurface are identity-only
|
||||
dividers. `frontend-modern/src/components/Workloads/WorkloadPanel.tsx`
|
||||
renders each grouped node through `NodeGroupHeader` as one cell spanning
|
||||
the table: status dot, linked node name, cluster badge, and agent badge.
|
||||
Per-host CPU, memory, disk, network and disk I/O, uptime, temperature,
|
||||
and version stats belong to the platform page's own hosts table (the
|
||||
Proxmox nodes table today), so the workloads table must not render them
|
||||
per column or inline in the group row. The former `compactGroupHeaders`
|
||||
option and the per-column node metric cells it switched off were removed
|
||||
because both remaining production mounts (Proxmox and vSphere overview)
|
||||
set it unconditionally, so the non-compact path no longer rendered
|
||||
anywhere. A new embedded consumer that needs host
|
||||
stats beside its workloads must render its own hosts table rather than
|
||||
re-growing metric cells in the shared group row.
|
||||
|
||||
WorkloadsSurface stays monitoring-first. It must not render a persistent
|
||||
aggregate banner just because running VMs or system containers lack an
|
||||
|
||||
@@ -970,6 +970,11 @@ the page supplies it through the Workloads `tableTitle` slot and the shared
|
||||
states. This heading alignment changes neither workload backup scope nor the
|
||||
storage/recovery evidence carried by the adjacent Backup column and Backups
|
||||
tab.
|
||||
The guest table's grouped node rows are identity-only dividers. Node stats and
|
||||
the node drawer stay with the Proxmox nodes table above it, so `ProxmoxPageSurface`
|
||||
passes `groupNodeDrawerMode: 'disabled'` and no host-metric option to the
|
||||
embedded Workloads state. Those dividers carry no backup or recovery evidence;
|
||||
the per-guest Backup column remains the overview's protection signal.
|
||||
The overview's node-row action selects the shared workload node scope and
|
||||
reveals the guest section. The title names the selected node and reports the
|
||||
filtered guest count, including zero. Activating the selected node again or
|
||||
|
||||
@@ -2973,7 +2973,10 @@ canonical WebSocket snapshot, including the public `vmware-vsphere` alias for
|
||||
the raw `vmware` source, rather than degrading a source-scoped page to periodic
|
||||
REST refreshes. VMware Overview passes that same source-scoped snapshot into
|
||||
the embedded Workloads state, so hosts and VMs share one inventory generation
|
||||
and one explicit refresh path.
|
||||
and one explicit refresh path. Beyond disabling the grouped host drawer, it
|
||||
passes no option that adds host metrics to grouped rows: the shared Workloads
|
||||
group row carries host identity only, and per-host stats stay in the page's
|
||||
own hosts table.
|
||||
Its page-owned workload toolbar consumes the complete shared
|
||||
`getWorkloadsMetricFilterProps` binding, so vSphere VMs expose the same Bars,
|
||||
Trends, Details, History, range, and first-use discovery contract as every
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"version": 1,
|
||||
"base_sha": "c3adf0c7b9fa7f4283f7fd832a806659b009b51b",
|
||||
"verified_at": "2026-10-06T21:45:33Z",
|
||||
"result": "passed",
|
||||
"changed_paths": [
|
||||
"frontend-modern/src/components/Workloads/WorkloadPanel.tsx",
|
||||
"frontend-modern/src/components/Workloads/WorkloadsSurface.tsx",
|
||||
"frontend-modern/src/components/Workloads/WorkloadsTable.tsx",
|
||||
"frontend-modern/src/components/Workloads/useWorkloadsState.ts",
|
||||
"frontend-modern/src/components/shared/NodeGroupHeader.tsx",
|
||||
"frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx",
|
||||
"frontend-modern/src/features/vmware/VmwarePageSurface.tsx"
|
||||
],
|
||||
"content_sha256": {
|
||||
"frontend-modern/src/components/Workloads/WorkloadPanel.tsx": "67c2c51ffceb96893f968dc141dbce54f0affe66fb389ffeeba8c8c94988968c",
|
||||
"frontend-modern/src/components/Workloads/WorkloadsSurface.tsx": "e49032445f566b4e6a4329314f4ada41ed6f7652ba52e9e3fc413047a49f7878",
|
||||
"frontend-modern/src/components/Workloads/WorkloadsTable.tsx": "a340532f9e3e0e28d83e9a0fb369c8be973acb0dea7b321fd646345ff924db38",
|
||||
"frontend-modern/src/components/Workloads/useWorkloadsState.ts": "a29034c5149b7323e86bc2fe795d9b18db805b1ad32e32013fb71547256d61f5",
|
||||
"frontend-modern/src/components/shared/NodeGroupHeader.tsx": "e435c1e6ab04b85a022459cf19a67645bc8339f15ef986ffcb5b42ba27ce667f",
|
||||
"frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx": "80c860be3bee37251c16d21044074267e48991b9cb39b87d335cb2155066ec96",
|
||||
"frontend-modern/src/features/vmware/VmwarePageSurface.tsx": "6c74063a298a259ce5f60485174529e9bffbe7293ae0710f53dc95d6f3c11265"
|
||||
},
|
||||
"routes": ["/proxmox/overview", "/vmware"],
|
||||
"viewports": [
|
||||
{
|
||||
"width": 1440,
|
||||
"height": 900
|
||||
},
|
||||
{
|
||||
"width": 390,
|
||||
"height": 844
|
||||
}
|
||||
],
|
||||
"states": [
|
||||
"Production vite build of this tree (rebased on c3adf0c7b9) embedded in a private DEMO_MODE mock backend (50 PVE nodes, 929 guests, 7 ESXi hosts), headless Chromium via Playwright; desktop 1440x900 and phone 390x844.",
|
||||
"Proxmox Overview guest table, Grouped layout: every grouped node row (8 desktop, 2 phone in the rendered window) is one td whose colspan equals the guest table's visible header count (8 desktop, 5 phone), with status dot, linked node name, cluster badge and Agent badge only; no data-workload-col cells, no PVE version, temperature or uptime text.",
|
||||
"vSphere Overview VM table: the 7 host group rows render as one spanning cell (colspan 6 desktop, 5 phone) with host name and cluster badge, unchanged shape.",
|
||||
"Bars and Trends metric modes both inspected: Trends adds four sparklines per guest row (Proxmox 140 to 560 images, vSphere 18 to 72) while each grouped host row keeps exactly its one status-dot image.",
|
||||
"Screenshots of grouped rows in both modes and of open guest drawers inspected at both viewports; no page errors.",
|
||||
"This is the shape both pages already rendered on main, where compactGroupHeaders was always set; it is not a check of host drawers, which both pages disable."
|
||||
],
|
||||
"interactions": [
|
||||
"Opened the View popover (desktop) and the Filters popover (phone) and toggled Bars to Trends and back.",
|
||||
"Clicked a grouped host row on each page: no inline node drawer row appears and nothing else changes.",
|
||||
"Clicked the first guest row under a group on each page: its inline guest drawer opens.",
|
||||
"Scrolled the first grouped host row to the viewport centre before each capture."
|
||||
]
|
||||
}
|
||||
@@ -1,10 +1,6 @@
|
||||
import { createMemo, For, Show } from 'solid-js';
|
||||
|
||||
import { ComponentErrorBoundary } from '@/components/ErrorBoundary';
|
||||
import { EnhancedCPUBar } from '@/components/Workloads/EnhancedCPUBar';
|
||||
import { MetricMiniSparkline } from '@/components/Workloads/MetricMiniSparkline';
|
||||
import { StackedDiskBar } from '@/components/Workloads/StackedDiskBar';
|
||||
import { StackedMemoryBar } from '@/components/Workloads/StackedMemoryBar';
|
||||
import {
|
||||
GROUPED_TABLE_ROW_BADGE_CLASS,
|
||||
getGroupedTableRowCellClass,
|
||||
@@ -16,12 +12,9 @@ import { NodeGroupHeader } from '@/components/shared/NodeGroupHeader';
|
||||
import { createSummaryInteractiveRowPreviewHandlers } from '@/components/shared/summaryInteractionA11y';
|
||||
import { buildSummaryDisclosureControlsId } from '@/components/shared/summaryInteractionA11y';
|
||||
import { TableBody, TableCell, TableRow } from '@/components/shared/Table';
|
||||
import type { Node } from '@/types/api';
|
||||
import { getAlertsForResource, getAlertStyles } from '@/utils/alerts';
|
||||
import { guestOverrideIdCandidates } from '@/features/alerts/guestOverrideIdentity';
|
||||
import { formatSpeed, formatUptime } from '@/utils/format';
|
||||
import { isNodeOnline } from '@/utils/status';
|
||||
import { formatTemperature, getCpuTemperature, getTemperatureTextClass } from '@/utils/temperature';
|
||||
import { getCanonicalWorkloadId, getWorkloadMetadataIdCandidates } from '@/utils/workloads';
|
||||
import {
|
||||
resolveSummaryGroupMemberInteractionState,
|
||||
@@ -34,14 +27,12 @@ import { NodeDrawer } from './NodeDrawer';
|
||||
import { buildWorkloadSummaryGroupScope } from './workloadSelectors';
|
||||
import { getWorkloadGuestMetadataRecord } from './workloadGuestMetadataRecord';
|
||||
import type { WorkloadsState } from './useWorkloadsState';
|
||||
import type { WorkloadTableMetric } from './workloadMetricHistoryModel';
|
||||
|
||||
type WorkloadPanelProps = Pick<
|
||||
WorkloadsState,
|
||||
| 'activeAlerts'
|
||||
| 'alertsEnabled'
|
||||
| 'bottomSpacerHeight'
|
||||
| 'compactGroupHeaders'
|
||||
| 'getGroupLabel'
|
||||
| 'getNodeTemperatureThresholds'
|
||||
| 'groupedGuests'
|
||||
@@ -58,7 +49,6 @@ type WorkloadPanelProps = Pick<
|
||||
| 'focusedSummaryWorkloadGroupScope'
|
||||
| 'focusedSummaryWorkloadGroupId'
|
||||
| 'hoveredSummaryWorkloadGroupScope'
|
||||
| 'isMobile'
|
||||
| 'nestedWorkloadContextByGuestId'
|
||||
| 'nodeByInstance'
|
||||
| 'search'
|
||||
@@ -80,289 +70,9 @@ type WorkloadPanelProps = Pick<
|
||||
| 'workloadTableLayoutMode'
|
||||
| 'workloadTableVisibleColumnIds'
|
||||
| 'workloadColumnWidths'
|
||||
| 'workloadTableVisibleColumns'
|
||||
>;
|
||||
|
||||
const GROUP_NODE_METRIC_CELL_CLASS = 'px-1.5 sm:px-2 py-0.5 align-middle';
|
||||
const GROUP_NODE_NAME_CELL_CLASS = getGroupedTableRowCellClass(
|
||||
'py-1! pl-2! sm:pl-3! pr-1.5! sm:pr-2!',
|
||||
);
|
||||
|
||||
const getGroupNodeColumnCellClass = (_columnId: string, isNameColumn: boolean): string => {
|
||||
if (isNameColumn) return GROUP_NODE_NAME_CELL_CLASS;
|
||||
return GROUP_NODE_METRIC_CELL_CLASS;
|
||||
};
|
||||
|
||||
const normalizeMetricPercent = (value: number | null | undefined): number => {
|
||||
if (typeof value !== 'number' || !Number.isFinite(value)) return 0;
|
||||
return value <= 1 ? Math.max(0, value * 100) : Math.max(0, value);
|
||||
};
|
||||
|
||||
const formatMetricPercent = (value: number | null | undefined): string =>
|
||||
`${Math.round(normalizeMetricPercent(value))}%`;
|
||||
|
||||
const getUsedPercent = (used?: number | null, total?: number | null): number => {
|
||||
if (typeof used !== 'number' || typeof total !== 'number' || total <= 0) return 0;
|
||||
return normalizeMetricPercent((used / total) * 100);
|
||||
};
|
||||
|
||||
const getGroupNodeMetricKey = (node: Node): string => node.linkedAgentId || node.id || node.name;
|
||||
|
||||
const getGroupNodePveVersion = (node: Node): string => {
|
||||
const version = (node.pveVersion || '').trim();
|
||||
if (!version || version.toLowerCase() === 'unknown') return '';
|
||||
return (
|
||||
version.match(/pve-manager\/([^/\s]+)/i)?.[1] || version.match(/\d+(?:\.\d+)+/)?.[0] || version
|
||||
);
|
||||
};
|
||||
|
||||
const renderGroupNodeEmptyCell = () => (
|
||||
<div class="text-center">
|
||||
<span class="text-xs text-slate-400" aria-hidden="true">
|
||||
—
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
export function WorkloadPanel(props: WorkloadPanelProps) {
|
||||
const renderGroupNodeSparkline = (
|
||||
node: Node,
|
||||
metric: WorkloadTableMetric,
|
||||
valueLabel: string,
|
||||
title: string,
|
||||
unit = '%',
|
||||
valueLabelMode: 'inline' | 'tooltip' | 'hidden' = 'inline',
|
||||
formatValue?: (value: number) => string,
|
||||
) => (
|
||||
<MetricMiniSparkline
|
||||
series={props.workloadMetricHistory.getNodeMetricSeries(node, metric)}
|
||||
valueLabel={valueLabel}
|
||||
valueLabelMode={valueLabelMode}
|
||||
title={title}
|
||||
unit={unit}
|
||||
formatValue={formatValue}
|
||||
/>
|
||||
);
|
||||
|
||||
const renderGroupNodeRatePair = (
|
||||
leftLabel: string,
|
||||
leftClass: string,
|
||||
leftValue: number | null | undefined,
|
||||
rightLabel: string,
|
||||
rightClass: string,
|
||||
rightValue: number | null | undefined,
|
||||
) => {
|
||||
const left = Math.max(0, leftValue ?? 0);
|
||||
const right = Math.max(0, rightValue ?? 0);
|
||||
if (left <= 0 && right <= 0) return renderGroupNodeEmptyCell();
|
||||
|
||||
return (
|
||||
<div class="grid w-full min-w-0 grid-cols-[0.75rem_minmax(0,1fr)_0.75rem_minmax(0,1fr)] items-center gap-x-1 overflow-hidden text-[11px] tabular-nums">
|
||||
<span class={`inline-flex w-3 justify-center ${leftClass}`}>{leftLabel}</span>
|
||||
<span
|
||||
class="block min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-muted"
|
||||
title={formatSpeed(left)}
|
||||
>
|
||||
{formatSpeed(left)}
|
||||
</span>
|
||||
<span class={`inline-flex w-3 justify-center ${rightClass}`}>{rightLabel}</span>
|
||||
<span
|
||||
class="block min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-muted"
|
||||
title={formatSpeed(right)}
|
||||
>
|
||||
{formatSpeed(right)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderGroupNodeInfoCell = (node: Node) => {
|
||||
const version = getGroupNodePveVersion(node);
|
||||
const temperature = getCpuTemperature(node.temperature);
|
||||
if (!version && temperature === null) return renderGroupNodeEmptyCell();
|
||||
|
||||
return (
|
||||
<div class="flex items-center justify-center gap-2 text-[10px] font-medium text-muted">
|
||||
<Show when={version}>
|
||||
<span title="Proxmox VE version">PVE {version}</span>
|
||||
</Show>
|
||||
<Show when={temperature !== null}>
|
||||
<span
|
||||
class={getTemperatureTextClass(temperature, props.getNodeTemperatureThresholds(node))}
|
||||
title="CPU temperature"
|
||||
>
|
||||
{formatTemperature(temperature)}
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderGroupNodeColumnCell = (columnId: string, node: Node) => {
|
||||
switch (columnId) {
|
||||
case 'type':
|
||||
return (
|
||||
<div class="flex justify-center">
|
||||
<span
|
||||
class="inline-flex items-center px-1 py-0.5 text-[10px] font-medium rounded-sm whitespace-nowrap bg-orange-100 text-orange-700 dark:bg-orange-900 dark:text-orange-300"
|
||||
title="Proxmox VE node"
|
||||
>
|
||||
PVE
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
case 'info':
|
||||
case 'vmid':
|
||||
return renderGroupNodeInfoCell(node);
|
||||
case 'cpu':
|
||||
if (props.workloadMetricDisplayMode() === 'sparklines') {
|
||||
return renderGroupNodeSparkline(
|
||||
node,
|
||||
'cpu',
|
||||
formatMetricPercent(node.cpu),
|
||||
`${node.name} CPU history`,
|
||||
'%',
|
||||
'inline',
|
||||
formatMetricPercent,
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div class="h-4">
|
||||
<EnhancedCPUBar
|
||||
usage={normalizeMetricPercent(node.cpu)}
|
||||
loadAverage={node.loadAverage?.[0]}
|
||||
cores={props.isMobile() ? undefined : node.cpuInfo?.cores}
|
||||
model={node.cpuInfo?.model}
|
||||
resourceId={getGroupNodeMetricKey(node)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
case 'memory':
|
||||
if (props.workloadMetricDisplayMode() === 'sparklines') {
|
||||
return renderGroupNodeSparkline(
|
||||
node,
|
||||
'memory',
|
||||
node.memory?.usageUnavailable
|
||||
? 'N/A'
|
||||
: formatMetricPercent(getUsedPercent(node.memory?.used, node.memory?.total)),
|
||||
`${node.name} memory history`,
|
||||
'%',
|
||||
'inline',
|
||||
formatMetricPercent,
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div class="h-4">
|
||||
<StackedMemoryBar
|
||||
used={node.memory?.used || 0}
|
||||
total={node.memory?.total || 0}
|
||||
unavailable={node.memory?.usageUnavailable === true}
|
||||
balloon={node.memory?.balloon || 0}
|
||||
swapUsed={node.memory?.swapUsed || 0}
|
||||
swapTotal={node.memory?.swapTotal || 0}
|
||||
resourceId={getGroupNodeMetricKey(node)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
case 'disk':
|
||||
if (props.workloadMetricDisplayMode() === 'sparklines') {
|
||||
return renderGroupNodeSparkline(
|
||||
node,
|
||||
'disk',
|
||||
formatMetricPercent(node.disk?.usage),
|
||||
`${node.name} disk usage history`,
|
||||
'%',
|
||||
'inline',
|
||||
formatMetricPercent,
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div class="h-4">
|
||||
<StackedDiskBar
|
||||
aggregateDisk={{
|
||||
total: node.disk?.total || 0,
|
||||
used: node.disk?.used || 0,
|
||||
free:
|
||||
node.disk?.free ?? Math.max(0, (node.disk?.total || 0) - (node.disk?.used || 0)),
|
||||
usage: node.disk?.usage || 0,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
case 'uptime':
|
||||
return (
|
||||
<div class="flex justify-center">
|
||||
<Show when={(node.uptime ?? 0) > 0} fallback={renderGroupNodeEmptyCell()}>
|
||||
<span
|
||||
class={`text-xs whitespace-nowrap ${
|
||||
node.uptime > 0 && node.uptime < 3600 ? 'text-orange-500' : 'text-muted'
|
||||
}`}
|
||||
>
|
||||
{formatUptime(node.uptime, props.isMobile())}
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
case 'node':
|
||||
case 'context':
|
||||
return (
|
||||
<div class="flex justify-center">
|
||||
<span class="text-xs text-muted truncate max-w-[120px]" title={node.name}>
|
||||
{node.name}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
case 'os':
|
||||
return (
|
||||
<div class="flex justify-center">
|
||||
<span class="text-xs text-muted">PVE</span>
|
||||
</div>
|
||||
);
|
||||
case 'netIo':
|
||||
if (props.workloadMetricDisplayMode() === 'sparklines') {
|
||||
return renderGroupNodeSparkline(
|
||||
node,
|
||||
'netIo',
|
||||
`${formatSpeed(node.networkIn ?? 0)} / ${formatSpeed(node.networkOut ?? 0)}`,
|
||||
`${node.name} network I/O history`,
|
||||
'B/s',
|
||||
'tooltip',
|
||||
formatSpeed,
|
||||
);
|
||||
}
|
||||
return renderGroupNodeRatePair(
|
||||
'↓',
|
||||
'text-emerald-500',
|
||||
node.networkIn,
|
||||
'↑',
|
||||
'text-orange-400',
|
||||
node.networkOut,
|
||||
);
|
||||
case 'diskIo':
|
||||
if (props.workloadMetricDisplayMode() === 'sparklines') {
|
||||
return renderGroupNodeSparkline(
|
||||
node,
|
||||
'diskIo',
|
||||
`${formatSpeed(node.diskRead ?? 0)} / ${formatSpeed(node.diskWrite ?? 0)}`,
|
||||
`${node.name} disk I/O history`,
|
||||
'B/s',
|
||||
'tooltip',
|
||||
formatSpeed,
|
||||
);
|
||||
}
|
||||
return renderGroupNodeRatePair(
|
||||
'R',
|
||||
'font-mono text-blue-500',
|
||||
node.diskRead,
|
||||
'W',
|
||||
'font-mono text-amber-500',
|
||||
node.diskWrite,
|
||||
);
|
||||
default:
|
||||
return renderGroupNodeEmptyCell();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TableBody ref={props.setTableBodyRef} class="divide-y divide-border">
|
||||
<Show when={props.groupedWindowing.isWindowed() && props.topSpacerHeight() > 0}>
|
||||
@@ -485,22 +195,8 @@ export function WorkloadPanel(props: WorkloadPanelProps) {
|
||||
>
|
||||
<NodeGroupHeader
|
||||
node={node()!}
|
||||
temperatureThresholds={props.getNodeTemperatureThresholds(node()!)}
|
||||
renderAs="tr"
|
||||
colspan={props.totalColumns()}
|
||||
columns={
|
||||
props.compactGroupHeaders() ? undefined : props.workloadTableVisibleColumns()
|
||||
}
|
||||
columnCellClass={
|
||||
props.compactGroupHeaders() ? undefined : getGroupNodeColumnCellClass
|
||||
}
|
||||
renderColumnCell={
|
||||
props.compactGroupHeaders() ? undefined : renderGroupNodeColumnCell
|
||||
}
|
||||
showFactsInName={
|
||||
!props.compactGroupHeaders() &&
|
||||
!props.workloadTableVisibleColumnIds().includes('info')
|
||||
}
|
||||
trClass={
|
||||
canOpenNodeDrawer()
|
||||
? 'cursor-pointer select-none duration-150 [&>td>div]:flex-nowrap'
|
||||
|
||||
@@ -192,7 +192,6 @@ export function WorkloadsSurface(props: WorkloadsSurfaceComponentProps) {
|
||||
activeAlerts={state.activeAlerts}
|
||||
alertsEnabled={state.alertsEnabled}
|
||||
bottomSpacerHeight={state.bottomSpacerHeight}
|
||||
compactGroupHeaders={state.compactGroupHeaders}
|
||||
getGroupLabel={state.getGroupLabel}
|
||||
getNodeTemperatureThresholds={state.getNodeTemperatureThresholds}
|
||||
groupedGuests={state.groupedGuests}
|
||||
|
||||
@@ -17,7 +17,6 @@ type WorkloadsTableProps = Pick<
|
||||
| 'activeAlerts'
|
||||
| 'alertsEnabled'
|
||||
| 'bottomSpacerHeight'
|
||||
| 'compactGroupHeaders'
|
||||
| 'getGroupLabel'
|
||||
| 'getNodeTemperatureThresholds'
|
||||
| 'groupedGuests'
|
||||
@@ -133,7 +132,6 @@ export function WorkloadsTable(props: WorkloadsTableProps) {
|
||||
activeAlerts={props.activeAlerts}
|
||||
alertsEnabled={props.alertsEnabled}
|
||||
bottomSpacerHeight={props.bottomSpacerHeight}
|
||||
compactGroupHeaders={props.compactGroupHeaders}
|
||||
getGroupLabel={props.getGroupLabel}
|
||||
getNodeTemperatureThresholds={props.getNodeTemperatureThresholds}
|
||||
groupedGuests={props.groupedGuests}
|
||||
@@ -150,7 +148,6 @@ export function WorkloadsTable(props: WorkloadsTableProps) {
|
||||
focusedSummaryWorkloadGroupScope={props.focusedSummaryWorkloadGroupScope}
|
||||
focusedSummaryWorkloadGroupId={props.focusedSummaryWorkloadGroupId}
|
||||
hoveredSummaryWorkloadGroupScope={props.hoveredSummaryWorkloadGroupScope}
|
||||
isMobile={props.isMobile}
|
||||
nestedWorkloadContextByGuestId={props.nestedWorkloadContextByGuestId}
|
||||
nodeByInstance={props.nodeByInstance}
|
||||
search={props.search}
|
||||
@@ -171,7 +168,6 @@ export function WorkloadsTable(props: WorkloadsTableProps) {
|
||||
workloadMetricHistory={props.workloadMetricHistory}
|
||||
workloadTableLayoutMode={props.workloadTableLayoutMode}
|
||||
workloadTableVisibleColumnIds={props.workloadTableVisibleColumnIds}
|
||||
workloadTableVisibleColumns={props.workloadTableVisibleColumns}
|
||||
workloadColumnWidths={props.workloadColumnWidths}
|
||||
/>
|
||||
</Table>
|
||||
|
||||
-26
@@ -454,32 +454,6 @@ describe('Workloads platform-page embed contract', () => {
|
||||
expect(nodesTableSource).toContain('isSparklineMode');
|
||||
});
|
||||
|
||||
it('exposes compactGroupHeaders so platform pages can strip duplicate host stats from group rows', async () => {
|
||||
const stateSource = (await import('../useWorkloadsState.ts?raw')).default;
|
||||
expect(stateSource).toContain('compactGroupHeaders?: boolean;');
|
||||
expect(stateSource).toContain('compactGroupHeaders: () => props.compactGroupHeaders === true,');
|
||||
|
||||
const tableSource = (await import('../WorkloadsTable.tsx?raw')).default;
|
||||
expect(tableSource).toContain(`| 'compactGroupHeaders'`);
|
||||
expect(tableSource).toContain('compactGroupHeaders={props.compactGroupHeaders}');
|
||||
|
||||
const panelSource = (await import('../WorkloadPanel.tsx?raw')).default;
|
||||
expect(panelSource).toContain(`| 'compactGroupHeaders'`);
|
||||
// When the flag is set the panel falls back to NodeGroupHeader's
|
||||
// colspan layout: no per-column metric cells, no inline node facts.
|
||||
expect(panelSource).toContain(
|
||||
'props.compactGroupHeaders() ? undefined : props.workloadTableVisibleColumns()',
|
||||
);
|
||||
expect(panelSource).toContain(
|
||||
'props.compactGroupHeaders() ? undefined : renderGroupNodeColumnCell',
|
||||
);
|
||||
expect(panelSource).toContain('!props.compactGroupHeaders() &&');
|
||||
|
||||
const proxmoxSource = (await import('../../../features/proxmox/ProxmoxPageSurface.tsx?raw'))
|
||||
.default;
|
||||
expect(proxmoxSource).toContain('compactGroupHeaders');
|
||||
});
|
||||
|
||||
it('lets platform pages move grouped host drawers to a dedicated host table owner', async () => {
|
||||
const stateSource = (await import('../useWorkloadsState.ts?raw')).default;
|
||||
expect(stateSource).toContain("groupNodeDrawerMode?: 'inline' | 'disabled';");
|
||||
|
||||
@@ -105,12 +105,6 @@ export interface WorkloadsSurfaceProps {
|
||||
}>;
|
||||
resourceSnapshotRefetch?: () => Promise<unknown>;
|
||||
statusModeStorageScope?: string;
|
||||
// Platform pages that render their own hosts table above the embedded
|
||||
// workloads surface (e.g. Proxmox overview) own the per-host CPU / Memory
|
||||
// / Disk / Temperature / uptime / version stats. Setting
|
||||
// `compactGroupHeaders` strips those stats from the NodeGroupHeader rows
|
||||
// in grouped mode so the section dividers don't duplicate the info.
|
||||
compactGroupHeaders?: boolean;
|
||||
// Default Workloads behavior owns grouped host row drawers inline. Platform
|
||||
// pages with a dedicated host table can disable that drawer so host details
|
||||
// open from the host-owned table instead of the embedded guest table.
|
||||
@@ -728,7 +722,6 @@ export function useWorkloadsState(props: WorkloadsSurfaceProps) {
|
||||
workloadsNoInventoryState,
|
||||
ws,
|
||||
groupingMode,
|
||||
compactGroupHeaders: () => props.compactGroupHeaders === true,
|
||||
groupNodeDrawerMode: () => props.groupNodeDrawerMode ?? 'inline',
|
||||
groupLabelBadges,
|
||||
} as const;
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { Component, For, Show, type JSX } from 'solid-js';
|
||||
import { Component, Show, type JSX } from 'solid-js';
|
||||
import type { Node } from '@/types/api';
|
||||
import type { MetricDisplayThresholds } from '@/utils/metricThresholds';
|
||||
import { getNodeDisplayName, getNodeExternalUrl, hasAlternateDisplayName } from '@/utils/nodes';
|
||||
import { StatusDot } from '@/components/shared/StatusDot';
|
||||
import { getNodeStatusIndicator } from '@/utils/status';
|
||||
import { formatUptime } from '@/utils/format';
|
||||
import { formatTemperature, getCpuTemperature, getTemperatureTextClass } from '@/utils/temperature';
|
||||
import { formatProxmoxVersion } from '@/utils/proxmoxVersion';
|
||||
import {
|
||||
GROUPED_TABLE_ROW_BADGE_CLASS,
|
||||
getGroupedTableRowCellClass,
|
||||
@@ -17,13 +13,8 @@ import { ResourceNameWithWebInterfaceLink } from './WebInterfaceLink';
|
||||
interface NodeGroupHeaderProps {
|
||||
node: Node;
|
||||
colspan?: number;
|
||||
columns?: Array<{ id: string }>;
|
||||
columnCellClass?: (columnId: string, isNameColumn: boolean) => string;
|
||||
leadingAction?: JSX.Element;
|
||||
renderColumnCell?: (columnId: string, node: Node) => JSX.Element;
|
||||
renderAs?: 'tr' | 'div';
|
||||
showFactsInName?: boolean;
|
||||
temperatureThresholds?: MetricDisplayThresholds | null;
|
||||
trClass?: string;
|
||||
trProps?: JSX.HTMLAttributes<HTMLTableRowElement> &
|
||||
Partial<Record<`data-${string}`, string | undefined>>;
|
||||
@@ -35,12 +26,6 @@ export const NodeGroupHeader: Component<NodeGroupHeaderProps> = (props) => {
|
||||
const nodeUrl = () => getNodeExternalUrl(props.node);
|
||||
const displayName = () => getNodeDisplayName(props.node);
|
||||
const showActualName = () => hasAlternateDisplayName(props.node);
|
||||
const pveVersion = () => formatProxmoxVersion(props.node.pveVersion);
|
||||
const cpuTemperature = () => getCpuTemperature(props.node.temperature);
|
||||
const hasNodeFacts = () =>
|
||||
Boolean(pveVersion()) ||
|
||||
cpuTemperature() !== null ||
|
||||
(typeof props.node.uptime === 'number' && props.node.uptime > 0);
|
||||
|
||||
const InnerContent = () => (
|
||||
<div
|
||||
@@ -84,25 +69,6 @@ export const NodeGroupHeader: Component<NodeGroupHeaderProps> = (props) => {
|
||||
Agent
|
||||
</span>
|
||||
</Show>
|
||||
|
||||
<Show when={props.showFactsInName !== false && hasNodeFacts()}>
|
||||
<span class="hidden sm:inline-flex flex-wrap items-center gap-3 text-[10px] font-medium text-muted">
|
||||
<Show when={pveVersion()}>
|
||||
<span title="Proxmox VE version">PVE {pveVersion()}</span>
|
||||
</Show>
|
||||
<Show when={cpuTemperature() !== null}>
|
||||
<span
|
||||
class={getTemperatureTextClass(cpuTemperature(), props.temperatureThresholds)}
|
||||
title="CPU temperature"
|
||||
>
|
||||
{formatTemperature(cpuTemperature())}
|
||||
</span>
|
||||
</Show>
|
||||
<Show when={typeof props.node.uptime === 'number' && props.node.uptime > 0}>
|
||||
<span title="Node uptime">{formatUptime(props.node.uptime, true)}</span>
|
||||
</Show>
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -118,38 +84,9 @@ export const NodeGroupHeader: Component<NodeGroupHeaderProps> = (props) => {
|
||||
}
|
||||
>
|
||||
<tr class={getGroupedTableRowClass(props.trClass)} {...props.trProps}>
|
||||
<Show
|
||||
when={(props.columns?.length ?? 0) > 0 && Boolean(props.renderColumnCell)}
|
||||
fallback={
|
||||
<td colspan={props.colspan} class={getGroupedTableRowCellClass()}>
|
||||
<InnerContent />
|
||||
</td>
|
||||
}
|
||||
>
|
||||
<For each={props.columns}>
|
||||
{(column) => {
|
||||
const isNameColumn = () => column.id === 'name';
|
||||
return (
|
||||
<td
|
||||
data-workload-col={column.id}
|
||||
class={
|
||||
props.columnCellClass?.(column.id, isNameColumn()) ??
|
||||
(isNameColumn()
|
||||
? getGroupedTableRowCellClass()
|
||||
: 'px-1.5 sm:px-2 py-0.5 align-middle')
|
||||
}
|
||||
>
|
||||
<Show
|
||||
when={isNameColumn()}
|
||||
fallback={props.renderColumnCell?.(column.id, props.node)}
|
||||
>
|
||||
<InnerContent />
|
||||
</Show>
|
||||
</td>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</Show>
|
||||
<td colspan={props.colspan} class={getGroupedTableRowCellClass()}>
|
||||
<InnerContent />
|
||||
</td>
|
||||
</tr>
|
||||
</Show>
|
||||
);
|
||||
|
||||
@@ -216,6 +216,7 @@ import proxmoxCoverageTableSource from '@/features/proxmox/ProxmoxCoverageTable.
|
||||
import proxmoxMailGatewayTableSource from '@/features/proxmox/ProxmoxMailGatewayTable.tsx?raw';
|
||||
import proxmoxNodesTableSource from '@/features/proxmox/ProxmoxNodesTable.tsx?raw';
|
||||
import proxmoxHostTableModelSource from '@/features/proxmox/proxmoxHostTableModel.ts?raw';
|
||||
import proxmoxPageModelSource from '@/features/proxmox/proxmoxPageModel.ts?raw';
|
||||
import proxmoxRecoverableTableSource from '@/features/proxmox/ProxmoxRecoverableTable.tsx?raw';
|
||||
import proxmoxReplicationTableSource from '@/features/proxmox/ProxmoxReplicationTable.tsx?raw';
|
||||
import vsphereHostsTableSource from '@/features/vmware/VsphereHostsTable.tsx?raw';
|
||||
@@ -1406,9 +1407,9 @@ describe('shared primitive guardrails', () => {
|
||||
});
|
||||
|
||||
it('routes Proxmox node version presentation through the shared formatter', () => {
|
||||
expect(nodeGroupHeaderSource).toContain("from '@/utils/proxmoxVersion'");
|
||||
expect(nodeGroupHeaderSource).toContain('formatProxmoxVersion(props.node.pveVersion)');
|
||||
expect(nodeGroupHeaderSource).not.toContain('pve-manager\\/');
|
||||
expect(proxmoxPageModelSource).toContain("from '@/utils/proxmoxVersion'");
|
||||
expect(proxmoxPageModelSource).toContain('formatProxmoxVersion(resource.proxmox?.pveVersion)');
|
||||
expect(proxmoxPageModelSource).not.toContain('pve-manager\\/');
|
||||
|
||||
expect(proxmoxVersionSource).toContain('formatProxmoxVersion');
|
||||
expect(proxmoxVersionSource).toContain('pve-manager\\/');
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { render, screen } from '@solidjs/testing-library';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { NodeGroupHeader } from '@/components/shared/NodeGroupHeader';
|
||||
import type { Node } from '@/types/api';
|
||||
import { temperatureStore } from '@/utils/temperature';
|
||||
|
||||
const makeNode = (overrides: Partial<Node> = {}): Node => ({
|
||||
id: 'cluster-a-pve1',
|
||||
@@ -34,18 +33,11 @@ const makeNode = (overrides: Partial<Node> = {}): Node => ({
|
||||
});
|
||||
|
||||
describe('NodeGroupHeader', () => {
|
||||
beforeEach(() => {
|
||||
temperatureStore.setUnit('celsius');
|
||||
});
|
||||
|
||||
it('renders compact infrastructure facts in the grouped workload header', () => {
|
||||
it('renders node identity in the grouped workload header', () => {
|
||||
render(() => <NodeGroupHeader node={makeNode()} />);
|
||||
|
||||
expect(screen.getByText('pve1')).toBeInTheDocument();
|
||||
expect(screen.getByText('homelab')).toBeInTheDocument();
|
||||
expect(screen.getByText('PVE 9.1.9')).toBeInTheDocument();
|
||||
expect(screen.getByText('62°C')).toBeInTheDocument();
|
||||
expect(screen.getByText('2h')).toBeInTheDocument();
|
||||
|
||||
const link = screen.getByRole('link', { name: 'Open web interface for pve1' });
|
||||
expect(link).toHaveAttribute('href', 'https://pve1:8006');
|
||||
@@ -53,20 +45,21 @@ describe('NodeGroupHeader', () => {
|
||||
expect(link).toHaveAttribute('rel', 'noopener noreferrer');
|
||||
});
|
||||
|
||||
it('colors grouped header temperatures from configured thresholds', () => {
|
||||
render(() => (
|
||||
<NodeGroupHeader
|
||||
node={makeNode({
|
||||
temperature: {
|
||||
available: true,
|
||||
cpuPackage: 76,
|
||||
lastUpdate: '2026-01-01T00:00:00Z',
|
||||
},
|
||||
})}
|
||||
temperatureThresholds={{ warning: 80, critical: 85 }}
|
||||
/>
|
||||
it('spans the table in one cell and leaves host stats to the platform host table', () => {
|
||||
const { container } = render(() => (
|
||||
<table>
|
||||
<tbody>
|
||||
<NodeGroupHeader node={makeNode()} renderAs="tr" colspan={9} />
|
||||
</tbody>
|
||||
</table>
|
||||
));
|
||||
|
||||
expect(screen.getByText('76°C')).toHaveClass('text-green-600');
|
||||
const cells = container.querySelectorAll('tr > td');
|
||||
expect(cells).toHaveLength(1);
|
||||
expect(cells[0]).toHaveAttribute('colspan', '9');
|
||||
expect(cells[0]).toHaveTextContent('pve1');
|
||||
expect(screen.queryByText('PVE 9.1.9')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/°[CF]/)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('2h')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -495,7 +495,6 @@ function ProxmoxOverview(props: ProxmoxOverviewProps) {
|
||||
// surface, so label it with the platform vocabulary instead of 'Info'.
|
||||
columnLabelOverrides: { info: 'ID' },
|
||||
statusModeStorageScope: PROXMOX_WORKLOAD_STATUS_STORAGE_SCOPE,
|
||||
compactGroupHeaders: true,
|
||||
groupNodeDrawerMode: 'disabled',
|
||||
metricDisplayMode: props.metricDisplayMode,
|
||||
onMetricDisplayModeChange: props.setMetricDisplayMode,
|
||||
@@ -638,7 +637,6 @@ function ProxmoxOverview(props: ProxmoxOverviewProps) {
|
||||
forcedPlatform={PROXMOX_PLATFORM_FILTER}
|
||||
excludedWorkloadTypes={PROXMOX_WORKLOAD_EXCLUDED_TYPES}
|
||||
showNestedExcludedWorkloads
|
||||
compactGroupHeaders
|
||||
groupNodeDrawerMode="disabled"
|
||||
suppressFilterToolbar
|
||||
emptyStateTitle="No Proxmox workloads"
|
||||
|
||||
@@ -214,6 +214,16 @@ describe('ProxmoxPageSurface contract', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('leaves host details to the nodes table instead of the embedded guest table', () => {
|
||||
setResources([makeResource({ id: 'vm-1', type: 'vm' })]);
|
||||
|
||||
renderSurface();
|
||||
|
||||
expect(mockWorkloadsOptions).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ groupNodeDrawerMode: 'disabled' }),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -474,7 +474,6 @@ function VmwareOverview(props: VmwareOverviewProps) {
|
||||
statusModeStorageScope: VMWARE_WORKLOAD_STATUS_STORAGE_SCOPE,
|
||||
columnVisibilityStorageScope: VMWARE_WORKLOAD_COLUMN_VISIBILITY_SCOPE,
|
||||
additionalDefaultHiddenColumnIds: [...VMWARE_WORKLOAD_DEFAULT_HIDDEN_COLUMN_IDS],
|
||||
compactGroupHeaders: true,
|
||||
inventorySourcesQuery: props.inventorySourcesQuery,
|
||||
groupNodeDrawerMode: 'disabled',
|
||||
metricDisplayMode: props.metricDisplayMode,
|
||||
@@ -545,7 +544,6 @@ function VmwareOverview(props: VmwareOverviewProps) {
|
||||
useWorkloads
|
||||
forcedPlatform={VMWARE_PLATFORM_FILTER}
|
||||
forcedViewMode="vm"
|
||||
compactGroupHeaders
|
||||
groupNodeDrawerMode="disabled"
|
||||
suppressFilterToolbar
|
||||
emptyStateTitle="No vSphere VMs"
|
||||
|
||||
@@ -2,15 +2,15 @@ import { describe, expect, it } from 'vitest';
|
||||
|
||||
import nodeDrawerOverviewSource from '@/components/Workloads/NodeDrawerOverview.tsx?raw';
|
||||
import workloadPanelSource from '@/components/Workloads/WorkloadPanel.tsx?raw';
|
||||
import nodeGroupHeaderSource from '@/components/shared/NodeGroupHeader.tsx?raw';
|
||||
import temperatureGaugeSource from '@/components/shared/TemperatureGauge.tsx?raw';
|
||||
import dockerHostDrawerOverviewSource from '@/features/docker/DockerHostDrawerOverview.tsx?raw';
|
||||
import agentsMachinesTableSource from '@/features/standalone/AgentsMachinesTable.tsx?raw';
|
||||
|
||||
describe('temperature display guardrails', () => {
|
||||
it('keeps colored temperature displays threshold-aware', () => {
|
||||
expect(nodeGroupHeaderSource).toContain('temperatureThresholds');
|
||||
expect(workloadPanelSource).toContain('getNodeTemperatureThresholds(node)');
|
||||
expect(workloadPanelSource).toContain(
|
||||
'temperatureThresholds={props.getNodeTemperatureThresholds(node()!)}',
|
||||
);
|
||||
expect(nodeDrawerOverviewSource).toContain('getTemperatureTextClass(value, thresholds)');
|
||||
expect(dockerHostDrawerOverviewSource).toContain('temperatureThresholds()');
|
||||
expect(agentsMachinesTableSource).toContain('getAgentMachineTemperatureMetric');
|
||||
@@ -19,7 +19,6 @@ describe('temperature display guardrails', () => {
|
||||
});
|
||||
|
||||
it('does not reintroduce the previous local temperature severity calls', () => {
|
||||
expect(nodeGroupHeaderSource).not.toContain('getTemperatureTextClass(cpuTemperature())');
|
||||
expect(workloadPanelSource).not.toContain('getTemperatureTextClass(temperature)}');
|
||||
expect(nodeDrawerOverviewSource).not.toContain('getTemperatureTextClass(value)');
|
||||
expect(dockerHostDrawerOverviewSource).not.toContain('getTemperatureTextClass(temperature)');
|
||||
|
||||
Reference in New Issue
Block a user