diff --git a/frontend-modern/src/features/platformPage/__tests__/platformOverviewLayout.guardrails.test.ts b/frontend-modern/src/features/platformPage/__tests__/platformOverviewLayout.guardrails.test.ts index 3f105d3b41..062c856a1a 100644 --- a/frontend-modern/src/features/platformPage/__tests__/platformOverviewLayout.guardrails.test.ts +++ b/frontend-modern/src/features/platformPage/__tests__/platformOverviewLayout.guardrails.test.ts @@ -28,7 +28,6 @@ import vsphereActivityTableSource from '@/features/vmware/VsphereActivityTable.t import vsphereAlertsTableSource from '@/features/vmware/VsphereAlertsTable.tsx?raw'; import vsphereDatastoresTableSource from '@/features/vmware/VsphereDatastoresTable.tsx?raw'; import vsphereHostsTableSource from '@/features/vmware/VsphereHostsTable.tsx?raw'; -import vsphereVirtualMachinesTableSource from '@/features/vmware/VsphereVirtualMachinesTable.tsx?raw'; const platformTableSources = [ proxmoxNodesTableSource, @@ -48,7 +47,6 @@ const platformTableSources = [ vsphereAlertsTableSource, vsphereDatastoresTableSource, vsphereHostsTableSource, - vsphereVirtualMachinesTableSource, ]; const platformToolbarTableSources = [ @@ -200,8 +198,10 @@ describe('platform overview layout guardrails', () => { expect(vmwarePageSurfaceSource).toContain('[] = [ - { value: 'all', label: 'All' }, - { - value: 'powered-on', - label: 'Powered on', - tone: 'success', - leading: platformChipStatusDot('bg-emerald-500'), - }, - { - value: 'attention', - label: 'Attention', - tone: 'warning', - leading: platformChipStatusDot('bg-amber-500'), - }, - { - value: 'powered-off', - label: 'Powered off', - tone: 'danger', - leading: platformChipStatusDot('bg-red-500'), - }, - { - value: 'suspended', - label: 'Suspended', - tone: 'warning', - leading: platformChipStatusDot('bg-amber-500'), - }, - { value: 'unknown', label: 'Unknown' }, -]; - -type VmwareVmGroup = { - key: string; - label: string; - cluster: string; - rows: Resource[]; -}; - -const finiteMetric = (value: number | undefined): number | undefined => - typeof value === 'number' && Number.isFinite(value) ? value : undefined; - -const metricFallback = () => ( -
- -
-); - -const vmName = (resource: Resource): string => - asTrimmedString(resource.displayName) || asTrimmedString(resource.name) || resource.id; - -const summarizeValues = ( - values: Array, - empty = '—', - visibleCount = 2, -): { label: string; title: string } => { - const compact = values - .map((value) => asTrimmedString(value)) - .filter((value): value is string => Boolean(value)); - if (compact.length === 0) return { label: empty, title: '' }; - const visible = compact.slice(0, visibleCount); - const suffix = compact.length > visible.length ? ` +${compact.length - visible.length}` : ''; - return { label: `${visible.join(', ')}${suffix}`, title: compact.join(', ') }; -}; - -const countSnapshotTree = (snapshots: ResourceVMwareSnapshot[] | undefined): number => - (snapshots ?? []).reduce( - (total, snapshot) => total + 1 + countSnapshotTree(snapshot.children), - 0, - ); - -const flattenSnapshotNames = (snapshots: ResourceVMwareSnapshot[] | undefined): string[] => { - const names: string[] = []; - for (const snapshot of snapshots ?? []) { - const name = - asTrimmedString(snapshot.name) || - asTrimmedString(snapshot.snapshot) || - (typeof snapshot.id === 'number' ? `Snapshot ${snapshot.id}` : ''); - if (name) names.push(snapshot.current ? `${name} (current)` : name); - names.push(...flattenSnapshotNames(snapshot.children)); - } - return names; -}; - -const networkAdapterName = (adapter: ResourceVMwareNetworkAdapter): string => - asTrimmedString(adapter.networkName) || - asTrimmedString(adapter.networkId) || - asTrimmedString(adapter.opaqueNetworkId) || - asTrimmedString(adapter.hostDevice) || - asTrimmedString(adapter.macAddress) || - asTrimmedString(adapter.label) || - ''; - -const networkAdapterTitle = (adapter: ResourceVMwareNetworkAdapter): string => - [ - asTrimmedString(adapter.label), - asTrimmedString(adapter.type), - networkAdapterName(adapter), - asTrimmedString(adapter.macAddress), - asTrimmedString(adapter.state), - ] - .filter(Boolean) - .join(' · '); - -const networkSummary = ( - adapters: ResourceVMwareNetworkAdapter[] | undefined, -): { label: string; title: string } => { - const names = (adapters ?? []).map(networkAdapterName).filter(Boolean); - const summary = summarizeValues(names, '—', 1); - const title = (adapters ?? []).map(networkAdapterTitle).filter(Boolean).join(' | '); - return { label: summary.label, title: title || summary.title }; -}; - -const virtualDiskAddress = (disk: ResourceVMwareVirtualDisk): string => { - const type = (asTrimmedString(disk.type) || '').toUpperCase(); - if (type === 'SCSI' && disk.scsiBus !== undefined && disk.scsiUnit !== undefined) { - return `SCSI ${disk.scsiBus}:${disk.scsiUnit}`; - } - if (type === 'SATA' && disk.sataBus !== undefined && disk.sataUnit !== undefined) { - return `SATA ${disk.sataBus}:${disk.sataUnit}`; - } - if (type === 'NVME' && disk.nvmeBus !== undefined && disk.nvmeUnit !== undefined) { - return `NVMe ${disk.nvmeBus}:${disk.nvmeUnit}`; - } - return type; -}; - -const virtualDiskTitle = (disk: ResourceVMwareVirtualDisk): string => - [ - asTrimmedString(disk.label) || asTrimmedString(disk.disk), - virtualDiskAddress(disk), - typeof disk.capacityBytes === 'number' && Number.isFinite(disk.capacityBytes) - ? formatBytes(disk.capacityBytes) - : '', - asTrimmedString(disk.datastoreName), - asTrimmedString(disk.vmdkFile), - ] - .filter(Boolean) - .join(' · '); - -const virtualDiskCount = (disks: ResourceVMwareVirtualDisk[] | undefined): number => - disks?.length ?? 0; - -const virtualDiskSummaryTitle = (disks: ResourceVMwareVirtualDisk[] | undefined): string => - (disks ?? []).map(virtualDiskTitle).filter(Boolean).join(' | '); - -const formatGuest = (resource: Resource): { label: string; detail: string; title: string } => { - const family = asTrimmedString(resource.vmware?.guestOsFamily); - const host = asTrimmedString(resource.vmware?.guestHostname); - const ips = summarizeValues(resource.vmware?.guestIpAddresses ?? [], '', 1); - return { - label: family || host || ips.label || '—', - detail: host || ips.label, - title: [family, host, ips.title].filter(Boolean).join(' | '), - }; -}; - -const vmGroupKey = (resource: Resource): string => - asTrimmedString(resource.vmware?.runtimeHostName) || - asTrimmedString(resource.parentName) || - 'Unassigned host'; - -const vmGroupCluster = (resource: Resource): string => - asTrimmedString(resource.vmware?.clusterName) || - asTrimmedString(resource.vmware?.computeResourceName) || - ''; - -const groupVirtualMachines = (vms: Resource[]): VmwareVmGroup[] => { - const groups: VmwareVmGroup[] = []; - const byKey = new Map(); - for (const vm of vms) { - const label = vmGroupKey(vm); - const key = label.toLowerCase(); - let group = byKey.get(key); - if (!group) { - group = { key, label, cluster: vmGroupCluster(vm), rows: [] }; - byKey.set(key, group); - groups.push(group); - } - if (!group.cluster) group.cluster = vmGroupCluster(vm); - group.rows.push(vm); - } - return groups; -}; - -export const VsphereVirtualMachinesTable: Component<{ - vms: Resource[]; - scope: Resource[]; - emptyIcon: JSX.Element; - emptyTitle: string; - emptyDescription: string; - showToolbar?: boolean; -}> = (props) => { - const tableState = createPlatformTableFilterState({ - resources: () => props.vms, - initialStatus: 'all' as VmwareVirtualMachineStatusFilter, - filter: filterVmwareVirtualMachines, - }); - const groups = createMemo(() => groupVirtualMachines(tableState.filtered())); - const drawer = createPlatformResourceDetailState({ idPrefix: 'vsphere-vm-drawer' }); - const resolveResourceLabel = createPlatformResourceLabelResolver(() => props.scope); - - return ( - 0} - fallback={ - - } - > -
- - - - - 0} - fallback={ - - } - > - - - - - - - VM - - - Power - - - - - - - - CPU - - - Memory - - - - - - - - - {(group) => ( - <> - - - {group.label} - - - {group.cluster} - - - - - - {(vm) => { - const meta = () => vm.vmware; - const name = () => vmName(vm); - const indicator = () => getSimpleStatusIndicator(vm.status); - const power = () => formatVmwarePowerState(meta()?.powerState); - const host = () => - asTrimmedString(meta()?.runtimeHostName) || - asTrimmedString(vm.parentName) || - '—'; - const cluster = () => - asTrimmedString(meta()?.clusterName) || - asTrimmedString(meta()?.computeResourceName) || - '—'; - const clusterServices = () => formatVmwareClusterServices(meta()); - const pool = () => asTrimmedString(meta()?.resourcePoolName) || '—'; - const guest = createMemo(() => formatGuest(vm)); - const network = createMemo(() => networkSummary(meta()?.networkAdapters)); - const datastores = createMemo(() => - summarizeValues(meta()?.datastoreNames ?? [], '—', 1), - ); - const disks = () => virtualDiskCount(meta()?.virtualDisks); - const diskTitle = () => virtualDiskSummaryTitle(meta()?.virtualDisks); - const snapshots = () => - Math.max( - 0, - meta()?.snapshotCount ?? countSnapshotTree(meta()?.snapshotTree), - ); - const snapshotTitle = () => - flattenSnapshotNames(meta()?.snapshotTree).join(', '); - const metricsKey = () => buildMetricKeyForUnifiedResource(vm); - const cpuPercent = () => finiteMetric(vm.cpu?.current); - const memoryTotal = () => finiteMetric(vm.memory?.total) ?? 0; - const memoryUsed = () => finiteMetric(vm.memory?.used) ?? 0; - const memoryPercentOnly = () => - memoryTotal() > 0 ? undefined : finiteMetric(vm.memory?.current); - const hasMemoryMetric = () => - memoryTotal() > 0 || memoryPercentOnly() !== undefined; - const canRenderMetrics = () => indicator().variant !== 'muted'; - const memoryTitle = () => - memoryTotal() > 0 - ? `${formatBytes(memoryUsed())} / ${formatBytes(memoryTotal())}` - : ''; - const detailRowId = () => drawer.detailRowId(vm); - const isExpanded = () => drawer.isExpanded(vm); - - return ( - <> - drawer.toggle(vm)} - onKeyDown={drawer.handleActivationKey(vm)} - tabIndex={0} - > - -
- - - {name()} - -
-
- -
- - - {power()} - -
-
- - - - - - - - - - - - - - - - -
- drawer.close(vm)} - /> - - ); - }} -
- - )} -
-
-
-
-
-
-
- ); -}; - -export default VsphereVirtualMachinesTable; diff --git a/frontend-modern/src/features/vmware/__tests__/VsphereVirtualMachinesTable.test.tsx b/frontend-modern/src/features/vmware/__tests__/VsphereVirtualMachinesTable.test.tsx deleted file mode 100644 index ddc5a67c84..0000000000 --- a/frontend-modern/src/features/vmware/__tests__/VsphereVirtualMachinesTable.test.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import { cleanup, fireEvent, render, screen, within } from '@solidjs/testing-library'; -import { afterEach, describe, expect, it, vi } from 'vitest'; - -import { VsphereVirtualMachinesTable } from '@/features/vmware/VsphereVirtualMachinesTable'; -import type { Resource } from '@/types/resource'; - -vi.mock('@/components/shared/responsive', () => ({ - ResponsiveMetricCell: () =>
metric
, -})); - -vi.mock('@/components/Workloads/StackedMemoryBar', () => ({ - StackedMemoryBar: () =>
memory
, -})); - -const makeVM = (overrides: Partial & Pick): Resource => - ({ - type: 'vm', - name: overrides.id, - displayName: overrides.id, - status: 'online', - platformType: 'vmware-vsphere', - platformScopes: ['vmware-vsphere'], - sourceType: 'api', - cpu: { current: 28 }, - memory: { current: 55, used: 4_400_000_000, total: 8_000_000_000 }, - vmware: { - entityType: 'vm', - managedObjectId: 'vm-201', - powerState: 'POWERED_ON', - overallStatus: 'green', - runtimeHostName: 'esxi-01.lab.local', - clusterName: 'Production Cluster', - resourcePoolName: 'Tier 1', - guestOsFamily: 'LINUX', - guestHostname: 'warehouse-api-01.internal', - guestIpAddresses: ['10.42.10.21'], - networkAdapters: [ - { - label: 'Network adapter 1', - type: 'VMXNET3', - macAddress: '00:50:56:aa:bb:cc', - networkName: 'VM Network', - state: 'CONNECTED', - }, - ], - datastoreNames: ['nvme-primary', 'backup-nfs'], - virtualDisks: [ - { - disk: '2000', - label: 'Hard disk 1', - type: 'SCSI', - scsiBus: 0, - scsiUnit: 1, - backingType: 'VMDK_FILE', - vmdkFile: '[nvme-primary] warehouse-api-01/warehouse-api-01.vmdk', - datastoreName: 'nvme-primary', - capacityBytes: 107374182400, - }, - ], - tools: { - runState: 'RUNNING', - versionStatus: 'CURRENT', - version: '12.4.0', - installType: 'OPEN_VM_TOOLS', - upgradePolicy: 'MANUAL', - autoUpdateSupported: true, - }, - snapshotTree: [ - { - snapshot: 'snapshot-201', - name: 'pre-upgrade', - quiesced: true, - children: [{ snapshot: 'snapshot-202', name: 'post-upgrade', current: true }], - }, - ], - }, - ...overrides, - }) as Resource; - -afterEach(() => { - cleanup(); -}); - -describe('VsphereVirtualMachinesTable', () => { - it('renders vCenter VM inventory fields instead of generic workload columns', async () => { - const warehouse = makeVM({ id: 'warehouse-api-01', name: 'warehouse-api-01' }); - const archive = makeVM({ - id: 'cold-archive-01', - name: 'cold-archive-01', - status: 'stopped', - vmware: { - entityType: 'vm', - managedObjectId: 'vm-301', - powerState: 'poweredOff', - runtimeHostName: 'esxi-03.lab.local', - clusterName: 'Archive Cluster', - resourcePoolName: 'Cold', - guestOsFamily: 'LINUX', - datastoreNames: ['archive-tier'], - snapshotCount: 0, - }, - }); - - render(() => ( - } - emptyTitle="No VMs" - emptyDescription="No VMs" - showToolbar={false} - /> - )); - - const table = screen.getByRole('table'); - expect(within(table).getByText('VM')).toBeInTheDocument(); - expect(within(table).getByText('Power')).toBeInTheDocument(); - expect(within(table).getByText('Host')).toBeInTheDocument(); - expect(within(table).getByText('Pool')).toBeInTheDocument(); - expect(within(table).getByText('Guest')).toBeInTheDocument(); - expect(within(table).getByText('Network')).toBeInTheDocument(); - expect(within(table).getByText('Disks')).toBeInTheDocument(); - expect(within(table).getByText('Snapshots')).toBeInTheDocument(); - expect(within(table).queryByRole('columnheader', { name: 'Tools' })).not.toBeInTheDocument(); - expect(within(table).queryByRole('columnheader', { name: 'Health' })).not.toBeInTheDocument(); - expect(within(table).queryByRole('columnheader', { name: 'ID' })).not.toBeInTheDocument(); - expect(within(table).queryByRole('columnheader', { name: 'Uptime' })).not.toBeInTheDocument(); - expect(within(table).queryByRole('columnheader', { name: 'Backup' })).not.toBeInTheDocument(); - expect(screen.getAllByText('esxi-01.lab.local')).toHaveLength(2); - expect(screen.getByText('Tier 1')).toBeInTheDocument(); - expect(screen.getByText('warehouse-api-01.internal')).toBeInTheDocument(); - expect(screen.getByText('VM Network')).toBeInTheDocument(); - expect(screen.getByText('nvme-primary +1')).toBeInTheDocument(); - expect(screen.getByTitle(/Hard disk 1 · SCSI 0:1 · 100 GB · nvme-primary/)).toHaveTextContent( - '1', - ); - expect(screen.getByTitle('pre-upgrade, post-upgrade (current)')).toHaveTextContent('2'); - - const row = screen.getByText('warehouse-api-01').closest('tr'); - expect(row).toHaveAttribute('aria-expanded', 'false'); - - await fireEvent.click(row!); - - expect(row).toHaveAttribute('aria-expanded', 'true'); - }); -});