mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-23 20:25:56 +00:00
Delete VsphereVirtualMachinesTable
Now that VmwareOverview routes VMs through WorkloadsSurface, the bespoke VsphereVirtualMachinesTable + its test are dead code. Remove them and flip the platform-overview guardrail to assert the new shape (no VsphereVirtualMachinesTable, presence of WorkloadsSurface with forcedPlatform/forcedViewMode for vSphere). The shared formatVmwarePowerState / getVmwarePowerStateVariant helpers in vmwarePageModel stay; VsphereHostsTable still uses them for the ESXi host power column.
This commit is contained in:
+4
-4
@@ -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('<VsphereHostsTable');
|
||||
expect(vmwarePageSurfaceSource).toContain('<VsphereAlertsTable');
|
||||
expect(vmwarePageSurfaceSource).toContain("activeTab() === 'health'");
|
||||
expect(vmwarePageSurfaceSource).toContain('<VsphereVirtualMachinesTable');
|
||||
expect(vmwarePageSurfaceSource).not.toContain('<WorkloadsSurface');
|
||||
expect(vmwarePageSurfaceSource).not.toContain('<VsphereVirtualMachinesTable');
|
||||
expect(vmwarePageSurfaceSource).toContain('<WorkloadsSurface');
|
||||
expect(vmwarePageSurfaceSource).toContain("forcedPlatform={VMWARE_PLATFORM_FILTER}");
|
||||
expect(vmwarePageSurfaceSource).toContain("forcedViewMode=\"vm\"");
|
||||
expect(vmwarePageSurfaceSource).toContain('<VsphereDatastoresTable');
|
||||
expect(vmwarePageSurfaceSource).toContain('<VsphereNetworksTable');
|
||||
expect(vmwarePageSurfaceSource).toContain('<VsphereActivityTable');
|
||||
|
||||
@@ -1,559 +0,0 @@
|
||||
import { For, Show, createMemo, type Component, type JSX } from 'solid-js';
|
||||
import { StatusDot } from '@/components/shared/StatusDot';
|
||||
import { ResponsiveMetricCell } from '@/components/shared/responsive';
|
||||
import { TableCard } from '@/components/shared/TableCard';
|
||||
import { TableCardHeader } from '@/components/shared/TableCardHeader';
|
||||
import { StackedMemoryBar } from '@/components/Workloads/StackedMemoryBar';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/shared/Table';
|
||||
import { buildMetricKeyForUnifiedResource } from '@/utils/metricsKeys';
|
||||
import { formatBytes } from '@/utils/format';
|
||||
import { getSimpleStatusIndicator } from '@/utils/status';
|
||||
import { asTrimmedString } from '@/utils/stringUtils';
|
||||
import { formatVmwareClusterServices } from '@/utils/vmwareDisplay';
|
||||
import {
|
||||
PLATFORM_TABLE_BODY_CLASS,
|
||||
PLATFORM_TABLE_CARD_CLASS,
|
||||
PLATFORM_TABLE_HEADER_ROW_CLASS,
|
||||
PlatformTableEmptyState,
|
||||
PlatformTableToolbar,
|
||||
createPlatformTableFilterState,
|
||||
getPlatformTableCellClassForKind,
|
||||
getPlatformTableHeadClassForKind,
|
||||
platformChipStatusDot,
|
||||
type PlatformTableFilterOption,
|
||||
} from '@/features/platformPage/sharedPlatformPage';
|
||||
import {
|
||||
PlatformResourceDetailTableRow,
|
||||
createPlatformResourceDetailState,
|
||||
createPlatformResourceLabelResolver,
|
||||
getPlatformResourceDetailRowClass,
|
||||
} from '@/features/platformPage/PlatformResourceDetailTableRow';
|
||||
import type {
|
||||
Resource,
|
||||
ResourceVMwareNetworkAdapter,
|
||||
ResourceVMwareSnapshot,
|
||||
ResourceVMwareVirtualDisk,
|
||||
} from '@/types/resource';
|
||||
import {
|
||||
filterVmwareVirtualMachines,
|
||||
formatVmwarePowerState,
|
||||
getVmwarePowerStateVariant,
|
||||
type VmwareVirtualMachineStatusFilter,
|
||||
} from './vmwarePageModel';
|
||||
|
||||
const VSPHERE_VM_STATUS_OPTIONS: PlatformTableFilterOption<VmwareVirtualMachineStatusFilter>[] = [
|
||||
{ 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 = () => (
|
||||
<div class="flex justify-center">
|
||||
<span class="text-xs text-muted" aria-hidden="true">
|
||||
—
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
const vmName = (resource: Resource): string =>
|
||||
asTrimmedString(resource.displayName) || asTrimmedString(resource.name) || resource.id;
|
||||
|
||||
const summarizeValues = (
|
||||
values: Array<string | undefined>,
|
||||
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<string, VmwareVmGroup>();
|
||||
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 (
|
||||
<Show
|
||||
when={props.vms.length > 0}
|
||||
fallback={
|
||||
<PlatformTableEmptyState
|
||||
icon={props.emptyIcon}
|
||||
title={props.emptyTitle}
|
||||
description={props.emptyDescription}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div class="space-y-3">
|
||||
<Show when={props.showToolbar !== false}>
|
||||
<PlatformTableToolbar
|
||||
search={tableState.search}
|
||||
onSearchChange={tableState.setSearch}
|
||||
searchPlaceholder="Search vSphere VMs"
|
||||
status={tableState.status()}
|
||||
onStatusChange={tableState.setStatus}
|
||||
statusOptions={VSPHERE_VM_STATUS_OPTIONS}
|
||||
visible={tableState.visible()}
|
||||
total={tableState.total()}
|
||||
rowNoun="VMs"
|
||||
/>
|
||||
</Show>
|
||||
|
||||
<Show
|
||||
when={tableState.filtered().length > 0}
|
||||
fallback={
|
||||
<PlatformTableEmptyState
|
||||
icon={props.emptyIcon}
|
||||
title="No VMs match current filters"
|
||||
description="Adjust the search or power-state filter to see more vSphere VMs."
|
||||
/>
|
||||
}
|
||||
>
|
||||
<TableCard class={PLATFORM_TABLE_CARD_CLASS}>
|
||||
<TableCardHeader title="Virtual Machines" />
|
||||
<Table class="min-w-full table-fixed text-xs md:min-w-[1420px]">
|
||||
<TableHeader>
|
||||
<TableRow class={PLATFORM_TABLE_HEADER_ROW_CLASS}>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('name')} md:w-[18%]`}>
|
||||
VM
|
||||
</TableHead>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('text')} md:w-[8%]`}>
|
||||
Power
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[12%]`}
|
||||
>
|
||||
Host
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[10%]`}
|
||||
>
|
||||
Cluster
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('text')} hidden lg:table-cell md:w-[8%]`}
|
||||
>
|
||||
Pool
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('text')} hidden lg:table-cell md:w-[10%]`}
|
||||
>
|
||||
Guest
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('text')} hidden xl:table-cell md:w-[9%]`}
|
||||
>
|
||||
Network
|
||||
</TableHead>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('metric-bar')} md:w-[9%]`}>
|
||||
CPU
|
||||
</TableHead>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('metric-bar')} md:w-[11%]`}>
|
||||
Memory
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('text')} hidden xl:table-cell md:w-[8%]`}
|
||||
>
|
||||
Datastores
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('numeric-value')} hidden md:table-cell md:w-[6%]`}
|
||||
>
|
||||
Disks
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('numeric-value')} hidden md:table-cell md:w-[6%]`}
|
||||
>
|
||||
Snapshots
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody class={PLATFORM_TABLE_BODY_CLASS}>
|
||||
<For each={groups()}>
|
||||
{(group) => (
|
||||
<>
|
||||
<TableRow class="bg-surface-alt/70 text-[11px] font-semibold text-base-content">
|
||||
<TableCell colSpan={12} class="px-2 py-1">
|
||||
<span>{group.label}</span>
|
||||
<Show when={group.cluster}>
|
||||
<span class="ml-2 rounded border border-border bg-surface px-1.5 py-0.5 text-[10px] font-medium text-muted">
|
||||
{group.cluster}
|
||||
</span>
|
||||
</Show>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<For each={group.rows}>
|
||||
{(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 (
|
||||
<>
|
||||
<TableRow
|
||||
class={`${getPlatformResourceDetailRowClass(isExpanded())} text-[11px] sm:text-xs`}
|
||||
aria-controls={isExpanded() ? detailRowId() : undefined}
|
||||
aria-expanded={isExpanded() ? 'true' : 'false'}
|
||||
data-vsphere-vm-row={vm.id}
|
||||
onClick={() => drawer.toggle(vm)}
|
||||
onKeyDown={drawer.handleActivationKey(vm)}
|
||||
tabIndex={0}
|
||||
>
|
||||
<TableCell class={getPlatformTableCellClassForKind('name')}>
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<StatusDot
|
||||
size="sm"
|
||||
variant={indicator().variant}
|
||||
title={indicator().label}
|
||||
/>
|
||||
<span
|
||||
class="truncate font-medium text-base-content"
|
||||
title={
|
||||
[
|
||||
name(),
|
||||
meta()?.managedObjectId,
|
||||
meta()?.instanceUuid,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' · ') || name()
|
||||
}
|
||||
>
|
||||
{name()}
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class={getPlatformTableCellClassForKind('text')}>
|
||||
<div class="flex items-center gap-2">
|
||||
<StatusDot
|
||||
size="sm"
|
||||
variant={getVmwarePowerStateVariant(meta()?.powerState)}
|
||||
title={meta()?.powerState || 'unknown'}
|
||||
ariaHidden
|
||||
/>
|
||||
<span class="truncate text-base-content" title={power()}>
|
||||
{power()}
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content md:table-cell`}
|
||||
title={host()}
|
||||
>
|
||||
<span class="block truncate">{host()}</span>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content md:table-cell`}
|
||||
title={[cluster(), clusterServices()].filter(Boolean).join(' · ')}
|
||||
>
|
||||
<span class="block truncate">{cluster()}</span>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content lg:table-cell`}
|
||||
title={pool()}
|
||||
>
|
||||
<span class="block truncate">{pool()}</span>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content lg:table-cell`}
|
||||
title={guest().title}
|
||||
>
|
||||
<span class="block truncate">{guest().label}</span>
|
||||
<Show when={guest().detail && guest().detail !== guest().label}>
|
||||
<span class="block truncate text-[10px] text-muted">
|
||||
{guest().detail}
|
||||
</span>
|
||||
</Show>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content xl:table-cell`}
|
||||
title={network().title}
|
||||
>
|
||||
<span class="block truncate">{network().label}</span>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('metric-bar')} w-[20%] md:w-auto`}
|
||||
>
|
||||
<ResponsiveMetricCell
|
||||
class="w-full"
|
||||
value={cpuPercent() ?? 0}
|
||||
type="cpu"
|
||||
resourceId={metricsKey()}
|
||||
isRunning={canRenderMetrics() && cpuPercent() !== undefined}
|
||||
showMobile={false}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('metric-bar')} w-[20%] md:w-auto`}
|
||||
title={memoryTitle()}
|
||||
>
|
||||
<Show
|
||||
when={canRenderMetrics() && hasMemoryMetric()}
|
||||
fallback={metricFallback()}
|
||||
>
|
||||
<StackedMemoryBar
|
||||
used={memoryUsed()}
|
||||
total={memoryTotal()}
|
||||
percentOnly={memoryPercentOnly()}
|
||||
/>
|
||||
</Show>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content xl:table-cell`}
|
||||
title={datastores().title}
|
||||
>
|
||||
<span class="block truncate">{datastores().label}</span>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-base-content tabular-nums md:table-cell`}
|
||||
title={diskTitle()}
|
||||
>
|
||||
{disks()}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-base-content tabular-nums md:table-cell`}
|
||||
title={snapshotTitle()}
|
||||
>
|
||||
{snapshots()}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<PlatformResourceDetailTableRow
|
||||
resource={vm}
|
||||
open={isExpanded()}
|
||||
detailRowId={detailRowId()}
|
||||
colSpan={12}
|
||||
resolveResourceLabel={resolveResourceLabel}
|
||||
onClose={() => drawer.close(vm)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</>
|
||||
)}
|
||||
</For>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableCard>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
);
|
||||
};
|
||||
|
||||
export default VsphereVirtualMachinesTable;
|
||||
@@ -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: () => <div data-testid="metric-cell">metric</div>,
|
||||
}));
|
||||
|
||||
vi.mock('@/components/Workloads/StackedMemoryBar', () => ({
|
||||
StackedMemoryBar: () => <div data-testid="memory-cell">memory</div>,
|
||||
}));
|
||||
|
||||
const makeVM = (overrides: Partial<Resource> & Pick<Resource, 'id'>): 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(() => (
|
||||
<VsphereVirtualMachinesTable
|
||||
vms={[warehouse, archive]}
|
||||
scope={[warehouse, archive]}
|
||||
emptyIcon={<span />}
|
||||
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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user