From 62a0027ff89fdbf474d5f400f370c7a16b5d7568 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sat, 23 May 2026 09:04:34 +0100 Subject: [PATCH] Prune vSphere drawer sections to operator signal The VIRTUAL HARDWARE section was rendering 20+ rows including capability toggles (CPU/memory hot-add) and boot configuration (EFI legacy boot, boot delay, boot retry, boot order, default setup mode) that operators don't act on from a monitoring surface. The GUEST section dumped Host/Instance/BIOS UUIDs that are API-shaped identifiers, not operator scan candidates. The TOOLS section padded out with install metadata (install type, upgrade policy, auto-update capability, attempt count, reboot components / time) that's the same kind of noise. Trim each section to the rows an operator actually reads: - Hardware: Guest OS, Hardware version, CPU topology, Memory size, Upgrade attention, Upgrade error, Instant clone frozen (when true), Enter setup mode (when true). Capability toggles and default boot config stay in the raw payload. - Guest: Guest OS, Guest hostname, Guest IPs. UUIDs and datastoreUrl drop. - State: Connection state replaces the redundant Power row (power is already on the workload row and SYSTEM card). - Tools: Run state, Version status (only when not current), Version, Guest reboot (only when requested), Last install error. Install metadata stays in the raw payload. Drop the now-unused formatMilliseconds and bootOrderLabel helpers and their ResourceVMwareBootDevice import. Test assertions are updated to match the trimmed shape with comments explaining the operator-signal principle. --- ...urceDetailDrawer.identity-runtime.test.tsx | 10 +- .../resourceDetailDrawerVmwareModel.test.ts | 35 +--- .../resourceDetailDrawerVmwareModel.ts | 191 ++++-------------- 3 files changed, 57 insertions(+), 179 deletions(-) diff --git a/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.identity-runtime.test.tsx b/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.identity-runtime.test.tsx index 5a22c14cd7..fe8a8e0225 100644 --- a/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.identity-runtime.test.tsx +++ b/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.identity-runtime.test.tsx @@ -554,13 +554,13 @@ describe('ResourceDetailDrawer runtime and identity cards', () => { expect(within(section).getByText('Pending')).toBeInTheDocument(); expect(within(section).getByText('CPU topology')).toBeInTheDocument(); expect(within(section).getByText('4 vCPU · 2 cores/socket')).toBeInTheDocument(); - expect(within(section).getByText('Boot order')).toBeInTheDocument(); - expect(within(section).getByText('Disk 2000 -> Ethernet 4000')).toBeInTheDocument(); + // Boot order, install type, and the default-case Version status row no + // longer render: they live in the raw API payload and the drawer keeps + // hardware/tools sections focused on actionable operator state. + expect(within(section).queryByText('Boot order')).toBeNull(); + expect(within(section).queryByText('Open VM Tools')).toBeNull(); expect(within(section).getByText('Run state')).toBeInTheDocument(); expect(within(section).getByText('Running')).toBeInTheDocument(); - expect(within(section).getByText('Version status')).toBeInTheDocument(); - expect(within(section).getByText('Current')).toBeInTheDocument(); - expect(within(section).getByText('Open VM Tools')).toBeInTheDocument(); expect(within(section).getByText('Guest reboot')).toBeInTheDocument(); expect(within(section).getByText('Requested')).toBeInTheDocument(); expect(within(section).getByText('Hard disk 1')).toBeInTheDocument(); diff --git a/frontend-modern/src/components/Infrastructure/__tests__/resourceDetailDrawerVmwareModel.test.ts b/frontend-modern/src/components/Infrastructure/__tests__/resourceDetailDrawerVmwareModel.test.ts index a8b441397c..30b349b52e 100644 --- a/frontend-modern/src/components/Infrastructure/__tests__/resourceDetailDrawerVmwareModel.test.ts +++ b/frontend-modern/src/components/Infrastructure/__tests__/resourceDetailDrawerVmwareModel.test.ts @@ -162,43 +162,30 @@ describe('resourceDetailDrawerVmwareModel', () => { (section) => section.id === 'hardware', ); + // Hardware section is deliberately pruned to operator-actionable rows. + // Capability toggles (CPU/memory hot-add) and boot configuration (EFI + // legacy boot, boot delay, boot retry, boot order, setup mode default) + // stay in the raw API payload; the drawer only surfaces them when + // they're in an attention state (Upgrade status pending, + // Instant clone frozen, Enter setup mode toggled). expect(hardware?.rows).toEqual([ { label: 'Guest OS', value: 'Ubuntu 64' }, { label: 'Hardware version', value: 'VMX 20' }, - { label: 'Upgrade status', value: 'Pending', tone: 'warning' }, - { label: 'Upgrade policy', value: 'After Clean Shutdown' }, - { label: 'Upgrade target', value: 'VMX 21' }, - { label: 'Instant clone frozen', value: 'No', tone: 'default' }, { label: 'CPU topology', value: '4 vCPU · 2 cores/socket' }, - { label: 'CPU hot-add', value: 'Yes' }, - { label: 'CPU hot-remove', value: 'No' }, { label: 'Memory size', value: '8 GB' }, - { label: 'Memory hot-add', value: 'Yes' }, - { label: 'Memory hot-add increment', value: '256 MB' }, - { label: 'Memory hot-add limit', value: '16 GB' }, - { label: 'Boot type', value: 'EFI' }, - { label: 'EFI legacy boot', value: 'No' }, - { label: 'Boot network protocol', value: 'IPv4' }, - { label: 'Boot delay', value: '5000 ms' }, - { label: 'Boot retry', value: 'Yes' }, - { label: 'Boot retry delay', value: '10000 ms' }, - { label: 'Enter setup mode', value: 'No', tone: 'default' }, - { label: 'Boot order', value: 'Disk 2000 -> Ethernet 4000' }, + { label: 'Upgrade status', value: 'Pending', tone: 'warning' }, ]); const tools = buildVMwareDetailSections('vm', vmware).find((section) => section.id === 'tools'); + // Tools section is pruned to operator-actionable rows: Run state, + // Version status (only when not current), Version, Guest reboot (only + // when requested), and Last install error. Install metadata stays in + // the raw payload. expect(tools?.rows).toEqual([ { label: 'Run state', value: 'Running', tone: 'default' }, - { label: 'Version status', value: 'Current', tone: 'default' }, { label: 'Version', value: '12.4.0' }, - { label: 'Install type', value: 'Open VM Tools' }, - { label: 'Upgrade policy', value: 'Manual' }, - { label: 'Auto update supported', value: 'Yes' }, - { label: 'Install attempts', value: '1' }, { label: 'Guest reboot', value: 'Requested', tone: 'warning' }, - { label: 'Reboot components', value: 'drivers', tone: 'warning' }, - { label: 'Reboot requested at', value: '2026-03-30 18:20 UTC', tone: 'warning' }, ]); const disks = buildVMwareDetailSections('vm', vmware).find((section) => section.id === 'disks'); diff --git a/frontend-modern/src/components/Infrastructure/resourceDetailDrawerVmwareModel.ts b/frontend-modern/src/components/Infrastructure/resourceDetailDrawerVmwareModel.ts index 0a25f87eeb..8091e15a2f 100644 --- a/frontend-modern/src/components/Infrastructure/resourceDetailDrawerVmwareModel.ts +++ b/frontend-modern/src/components/Infrastructure/resourceDetailDrawerVmwareModel.ts @@ -1,6 +1,5 @@ import type { ResourceType, - ResourceVMwareBootDevice, ResourceVMwareHardware, ResourceVMwareMeta, ResourceVMwareNetworkAdapter, @@ -93,11 +92,6 @@ const formatMiB = (value?: number): string => { return formatCapacityBytes(value * 1024 * 1024).replace(/\.0 ([A-Z]+)/, ' $1'); }; -const formatMilliseconds = (value?: number): string => { - if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) return ''; - return `${value} ms`; -}; - const countSnapshotTree = (snapshots?: ResourceVMwareSnapshot[]): number => (snapshots ?? []).reduce( (total, snapshot) => total + 1 + countSnapshotTree(snapshot.children), @@ -216,18 +210,6 @@ const hardwareSummary = (hardware?: ResourceVMwareHardware): string => { return formatEnumLabel(hardware.version); }; -const bootDeviceLabel = (device: ResourceVMwareBootDevice): string => { - const type = formatEnumLabel(device.type); - const details = [ - asTrimmedString(device.nic), - ...(device.disks ?? []).map((disk) => asTrimmedString(disk)), - ].filter(Boolean); - return details.length > 0 ? `${type} ${details.join(', ')}` : type; -}; - -const bootOrderLabel = (devices?: ResourceVMwareBootDevice[]): string => - (devices ?? []).map(bootDeviceLabel).filter(Boolean).join(' -> '); - const cpuTopologyLabel = (vmware: ResourceVMwareMeta): string => { const parts = []; if ( @@ -250,6 +232,9 @@ const cpuTopologyLabel = (vmware: ResourceVMwareMeta): string => { const hardwareRows = (vmware?: ResourceVMwareMeta): ResourceDetailDrawerVMwareRow[] => { if (!vmware?.hardware) return []; const hardware = vmware.hardware; + const upgradeStatus = asTrimmedString(hardware.upgradeStatus).toUpperCase(); + const upgradeAttention = + Boolean(upgradeStatus) && !['NONE', 'OK'].includes(upgradeStatus); return filterNonEmptyRows([ { label: 'Guest OS', @@ -259,21 +244,24 @@ const hardwareRows = (vmware?: ResourceVMwareMeta): ResourceDetailDrawerVMwareRo label: 'Hardware version', value: formatEnumLabel(hardware.version), }, + { + label: 'CPU topology', + value: cpuTopologyLabel(vmware), + }, + { + label: 'Memory size', + value: formatMiB(vmware?.memorySizeMib), + }, + // Upgrade and clone status only surface when something is actionable; + // when everything is in the default state these rows resolve to empty + // strings and filterNonEmptyRows drops them. Capability toggles + // (CPU/memory hot-add, boot config) live in the raw API and stay out + // of the operator drawer because they don't change minute to minute + // and operators don't act on them from Pulse. { label: 'Upgrade status', - value: formatEnumLabel(hardware.upgradeStatus), - tone: getWarningTone( - Boolean(asTrimmedString(hardware.upgradeStatus)) && - !['NONE', 'OK'].includes(asTrimmedString(hardware.upgradeStatus).toUpperCase()), - ), - }, - { - label: 'Upgrade policy', - value: formatEnumLabel(hardware.upgradePolicy), - }, - { - label: 'Upgrade target', - value: formatEnumLabel(hardware.upgradeVersion), + value: upgradeAttention ? formatEnumLabel(hardware.upgradeStatus) : '', + tone: getWarningTone(upgradeAttention), }, { label: 'Upgrade error', @@ -282,70 +270,14 @@ const hardwareRows = (vmware?: ResourceVMwareMeta): ResourceDetailDrawerVMwareRo }, { label: 'Instant clone frozen', - value: formatBoolLabel(hardware.instantCloneFrozen), + value: hardware.instantCloneFrozen === true ? formatBoolLabel(true) : '', tone: getWarningTone(hardware.instantCloneFrozen === true), }, - { - label: 'CPU topology', - value: cpuTopologyLabel(vmware), - }, - { - label: 'CPU hot-add', - value: formatBoolLabel(hardware.cpuHotAddEnabled), - }, - { - label: 'CPU hot-remove', - value: formatBoolLabel(hardware.cpuHotRemoveEnabled), - }, - { - label: 'Memory size', - value: formatMiB(vmware?.memorySizeMib), - }, - { - label: 'Memory hot-add', - value: formatBoolLabel(hardware.memoryHotAddEnabled), - }, - { - label: 'Memory hot-add increment', - value: formatMiB(hardware.memoryHotAddIncrementMib), - }, - { - label: 'Memory hot-add limit', - value: formatMiB(hardware.memoryHotAddLimitMib), - }, - { - label: 'Boot type', - value: formatEnumLabel(hardware.bootType), - }, - { - label: 'EFI legacy boot', - value: formatBoolLabel(hardware.efiLegacyBoot), - }, - { - label: 'Boot network protocol', - value: formatEnumLabel(hardware.bootNetworkProtocol), - }, - { - label: 'Boot delay', - value: formatMilliseconds(hardware.bootDelayMilliseconds), - }, - { - label: 'Boot retry', - value: formatBoolLabel(hardware.bootRetry), - }, - { - label: 'Boot retry delay', - value: formatMilliseconds(hardware.bootRetryDelayMilliseconds), - }, { label: 'Enter setup mode', - value: formatBoolLabel(hardware.enterSetupMode), + value: hardware.enterSetupMode === true ? formatBoolLabel(true) : '', tone: getWarningTone(hardware.enterSetupMode === true), }, - { - label: 'Boot order', - value: bootOrderLabel(hardware.bootDevices), - }, ]); }; @@ -363,6 +295,14 @@ const toolsSummary = (tools?: ResourceVMwareTools): string => { const toolsRows = (tools?: ResourceVMwareTools): ResourceDetailDrawerVMwareRow[] => { if (!tools) return []; + const versionStatus = asTrimmedString(tools.versionStatus).toUpperCase(); + const versionAttention = + Boolean(versionStatus) && !['CURRENT', 'OK'].includes(versionStatus); + // Drawer surfaces what an operator scans for: is Tools running, is its + // version current, has the guest asked for a reboot, did the last install + // error. Install metadata (install type, upgrade policy, auto-update + // capability, attempt count, reboot components / time) stays in the raw + // API; we only surface it when something is actionable. return filterNonEmptyRows([ { label: 'Run state', @@ -374,55 +314,18 @@ const toolsRows = (tools?: ResourceVMwareTools): ResourceDetailDrawerVMwareRow[] }, { label: 'Version status', - value: formatEnumLabel(tools.versionStatus), - tone: getWarningTone( - Boolean(asTrimmedString(tools.versionStatus)) && - !['CURRENT', 'OK'].includes(asTrimmedString(tools.versionStatus).toUpperCase()), - ), + value: versionAttention ? formatEnumLabel(tools.versionStatus) : '', + tone: getWarningTone(versionAttention), }, { label: 'Version', value: asTrimmedString(tools.version), }, - { - label: 'Install type', - value: formatEnumLabel(tools.installType), - }, - { - label: 'Upgrade policy', - value: formatEnumLabel(tools.upgradePolicy), - }, - { - label: 'Auto update supported', - value: formatBoolLabel(tools.autoUpdateSupported), - }, - { - label: 'Install attempts', - value: - typeof tools.installAttemptCount === 'number' && Number.isFinite(tools.installAttemptCount) - ? String(tools.installAttemptCount) - : '', - }, { label: 'Guest reboot', - value: - tools.guestRebootRequested === undefined - ? '' - : tools.guestRebootRequested - ? 'Requested' - : 'Not requested', + value: tools.guestRebootRequested === true ? 'Requested' : '', tone: getWarningTone(tools.guestRebootRequested === true), }, - { - label: 'Reboot components', - value: (tools.guestRebootComponents ?? []).filter(Boolean).join(', '), - tone: getWarningTone((tools.guestRebootComponents ?? []).length > 0), - }, - { - label: 'Reboot requested at', - value: formatSnapshotDate(tools.guestRebootRequestTime), - tone: getWarningTone(Boolean(tools.guestRebootRequestTime)), - }, { label: 'Last install error', value: asTrimmedString(tools.errorMessage), @@ -595,13 +498,13 @@ export const buildVMwareDetailSections = ( value: asTrimmedString(vmware.overallStatus), tone: getStatusTone(vmware.overallStatus), }, - { - label: 'Power', - value: asTrimmedString(vmware.powerState), - }, + // Power state is already conveyed by the workload row's status dot and + // the SYSTEM card. We only resurface Connection state here because it + // matters when an ESXi host is disconnected and the row dot alone can't + // tell you why. { label: 'Connection state', - value: asTrimmedString(vmware.connectionState), + value: formatEnumLabel(vmware.connectionState), }, { label: 'Datastore type', @@ -662,19 +565,11 @@ export const buildVMwareDetailSections = ( }, ]); + // UUID fields (host / instance / BIOS) and datastoreUrl are API-shaped + // identifiers an operator never types or compares from the drawer; they + // are still available in the raw resource payload. Keep guest identity + // human-readable. const guestRows = filterNonEmptyRows([ - { - label: 'Host UUID', - value: asTrimmedString(vmware.hostUuid), - }, - { - label: 'Instance UUID', - value: asTrimmedString(vmware.instanceUuid), - }, - { - label: 'BIOS UUID', - value: asTrimmedString(vmware.biosUuid), - }, { label: 'Guest OS', value: asTrimmedString(vmware.guestOsFamily), @@ -687,10 +582,6 @@ export const buildVMwareDetailSections = ( label: 'Guest IPs', value: (vmware.guestIpAddresses ?? []).filter(Boolean).join(', '), }, - { - label: 'Datastore URL', - value: asTrimmedString(vmware.datastoreUrl), - }, ]); const networkRows =