mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-23 20:25:56 +00:00
Align vSphere tables with shared platform conventions
Strip the bespoke colored pill components (HealthPill, ToolsPill, StatusPill) from the vSphere VMs, Datastores, and Networks tables. Status now reads from the leading StatusDot in the name cell, the same way Proxmox, Docker, Kubernetes, and TrueNAS already convey it. The Tools and Health columns on VMs and the State column on Datastores and Networks are removed; the underlying data stays reachable through the row detail drawer. Drop the cluster-services inline subtitle on Hosts and VMs and the managedObjectId / datastoreUrl subtitles on the VMs, Datastores, and Networks name cells, so row density matches Proxmox and Docker. Move that information into the row title attribute. Replace the hand-rolled emerald progress bar in CapacityCell with the shared StackedDiskBar so datastore capacity colouring responds to fill thresholds rather than always rendering green. Promote powerStateVariant and formatPowerState into vmwarePageModel as getVmwarePowerStateVariant and formatVmwarePowerState; both Hosts and VMs tables now share them. Export platformChipStatusDot from sharedPlatformPage so vSphere filter chip definitions stop redeclaring the same helper, and add leading status dots to the Datastores, Networks, and Alerts chips that were missing them.
This commit is contained in:
@@ -138,7 +138,11 @@ export function PlatformErrorState(props: {
|
||||
// 'running' vs 'online', 'stopped' vs 'offline') collapse to one chip set.
|
||||
export type PlatformResourceStatusFilter = 'all' | 'online' | 'degraded' | 'offline';
|
||||
|
||||
const statusDot = (className: string) => <span class={`h-2 w-2 rounded-full ${className}`} />;
|
||||
export const platformChipStatusDot = (className: string) => (
|
||||
<span class={`h-2 w-2 rounded-full ${className}`} />
|
||||
);
|
||||
|
||||
const statusDot = platformChipStatusDot;
|
||||
|
||||
export const PLATFORM_STATUS_FILTER_OPTIONS: PlatformTableFilterOption<PlatformResourceStatusFilter>[] =
|
||||
[
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
createPlatformTableFilterState,
|
||||
getPlatformTableCellClassForKind,
|
||||
getPlatformTableHeadClassForKind,
|
||||
platformChipStatusDot,
|
||||
type PlatformTableFilterOption,
|
||||
} from '@/features/platformPage/sharedPlatformPage';
|
||||
import {
|
||||
@@ -38,15 +39,26 @@ import {
|
||||
|
||||
const VSPHERE_ACTIVITY_STATUS_OPTIONS: PlatformTableFilterOption<VmwareActivityStatusFilter>[] = [
|
||||
{ value: 'all', label: 'All' },
|
||||
{ value: 'tasks', label: 'Tasks', tone: 'success', leading: statusDot('bg-emerald-500') },
|
||||
{ value: 'events', label: 'Events', tone: 'info', leading: statusDot('bg-blue-500') },
|
||||
{ value: 'failed', label: 'Failed', tone: 'danger', leading: statusDot('bg-red-500') },
|
||||
{
|
||||
value: 'tasks',
|
||||
label: 'Tasks',
|
||||
tone: 'success',
|
||||
leading: platformChipStatusDot('bg-emerald-500'),
|
||||
},
|
||||
{
|
||||
value: 'events',
|
||||
label: 'Events',
|
||||
tone: 'info',
|
||||
leading: platformChipStatusDot('bg-blue-500'),
|
||||
},
|
||||
{
|
||||
value: 'failed',
|
||||
label: 'Failed',
|
||||
tone: 'danger',
|
||||
leading: platformChipStatusDot('bg-red-500'),
|
||||
},
|
||||
];
|
||||
|
||||
function statusDot(className: string): JSX.Element {
|
||||
return <span class={`h-2 w-2 rounded-full ${className}`} />;
|
||||
}
|
||||
|
||||
const formatResourceType = (type: ResourceType): string => {
|
||||
switch (type) {
|
||||
case 'agent':
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
createPlatformTableFilterState,
|
||||
getPlatformTableCellClassForKind,
|
||||
getPlatformTableHeadClassForKind,
|
||||
platformChipStatusDot,
|
||||
type PlatformTableFilterOption,
|
||||
} from '@/features/platformPage/sharedPlatformPage';
|
||||
import {
|
||||
@@ -37,9 +38,24 @@ import {
|
||||
|
||||
const VSPHERE_INCIDENT_STATUS_OPTIONS: PlatformTableFilterOption<VmwareIncidentSeverityFilter>[] = [
|
||||
{ value: 'all', label: 'All' },
|
||||
{ value: 'critical', label: 'Critical', tone: 'danger' },
|
||||
{ value: 'warning', label: 'Warning', tone: 'warning' },
|
||||
{ value: 'info', label: 'Info', tone: 'success' },
|
||||
{
|
||||
value: 'critical',
|
||||
label: 'Critical',
|
||||
tone: 'danger',
|
||||
leading: platformChipStatusDot('bg-red-500'),
|
||||
},
|
||||
{
|
||||
value: 'warning',
|
||||
label: 'Warning',
|
||||
tone: 'warning',
|
||||
leading: platformChipStatusDot('bg-amber-500'),
|
||||
},
|
||||
{
|
||||
value: 'info',
|
||||
label: 'Info',
|
||||
tone: 'success',
|
||||
leading: platformChipStatusDot('bg-emerald-500'),
|
||||
},
|
||||
];
|
||||
|
||||
const severityVariant = (severity: VmwareIncidentRow['severityBucket']): StatusIndicatorVariant => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { For, Show, createMemo, type Component, type JSX } from 'solid-js';
|
||||
import { StatusDot } from '@/components/shared/StatusDot';
|
||||
import { TableCard } from '@/components/shared/TableCard';
|
||||
import { TableCardHeader } from '@/components/shared/TableCardHeader';
|
||||
import { StackedDiskBar } from '@/components/Workloads/StackedDiskBar';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -10,7 +11,6 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/shared/Table';
|
||||
import { formatBytes, formatPercent } from '@/utils/format';
|
||||
import { getSimpleStatusIndicator } from '@/utils/status';
|
||||
import { asTrimmedString } from '@/utils/stringUtils';
|
||||
import {
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
createPlatformTableFilterState,
|
||||
getPlatformTableCellClassForKind,
|
||||
getPlatformTableHeadClassForKind,
|
||||
platformChipStatusDot,
|
||||
type PlatformTableFilterOption,
|
||||
} from '@/features/platformPage/sharedPlatformPage';
|
||||
import {
|
||||
@@ -33,16 +34,35 @@ import {
|
||||
import type { Resource } from '@/types/resource';
|
||||
import {
|
||||
filterVmwareDatastores,
|
||||
mapVmwareDatastoreStatus,
|
||||
type VmwareDatastoreStatusFilter,
|
||||
} from './vmwarePageModel';
|
||||
|
||||
const VSPHERE_DATASTORE_STATUS_OPTIONS: PlatformTableFilterOption<VmwareDatastoreStatusFilter>[] = [
|
||||
{ value: 'all', label: 'All' },
|
||||
{ value: 'accessible', label: 'Accessible', tone: 'success' },
|
||||
{ value: 'attention', label: 'Attention', tone: 'warning' },
|
||||
{ value: 'inaccessible', label: 'Inaccessible', tone: 'danger' },
|
||||
{ value: 'maintenance', label: 'Maintenance', tone: 'warning' },
|
||||
{
|
||||
value: 'accessible',
|
||||
label: 'Accessible',
|
||||
tone: 'success',
|
||||
leading: platformChipStatusDot('bg-emerald-500'),
|
||||
},
|
||||
{
|
||||
value: 'attention',
|
||||
label: 'Attention',
|
||||
tone: 'warning',
|
||||
leading: platformChipStatusDot('bg-amber-500'),
|
||||
},
|
||||
{
|
||||
value: 'inaccessible',
|
||||
label: 'Inaccessible',
|
||||
tone: 'danger',
|
||||
leading: platformChipStatusDot('bg-red-500'),
|
||||
},
|
||||
{
|
||||
value: 'maintenance',
|
||||
label: 'Maintenance',
|
||||
tone: 'warning',
|
||||
leading: platformChipStatusDot('bg-amber-500'),
|
||||
},
|
||||
{ value: 'unknown', label: 'Unknown' },
|
||||
];
|
||||
|
||||
@@ -52,14 +72,14 @@ const datastoreName = (resource: Resource): string =>
|
||||
const datastoreType = (resource: Resource): string =>
|
||||
asTrimmedString(resource.vmware?.datastoreType) ||
|
||||
asTrimmedString(resource.storage?.type)?.toUpperCase() ||
|
||||
'-';
|
||||
'—';
|
||||
|
||||
const compactList = (values: Array<string | undefined>): string[] =>
|
||||
values.map((value) => asTrimmedString(value)).filter((value): value is string => Boolean(value));
|
||||
|
||||
const summarizeValues = (
|
||||
values: string[],
|
||||
empty = '-',
|
||||
empty = '—',
|
||||
visibleCount = 2,
|
||||
): { label: string; title: string } => {
|
||||
if (values.length === 0) return { label: empty, title: '' };
|
||||
@@ -69,95 +89,40 @@ const summarizeValues = (
|
||||
};
|
||||
|
||||
const hostSummary = (resource: Resource): { label: string; title: string } =>
|
||||
summarizeValues(compactList(resource.storage?.nodes ?? []), '-', 2);
|
||||
summarizeValues(compactList(resource.storage?.nodes ?? []), '—', 2);
|
||||
|
||||
const consumerSummary = (resource: Resource): { label: string; title: string } => {
|
||||
const consumers = resource.storage?.topConsumers?.map((consumer) => consumer.name) ?? [];
|
||||
return summarizeValues(compactList(consumers), '-', 2);
|
||||
return summarizeValues(compactList(consumers), '—', 2);
|
||||
};
|
||||
|
||||
const consumerCount = (resource: Resource): number => resource.storage?.consumerCount ?? 0;
|
||||
|
||||
const capacityPercent = (resource: Resource): number | undefined => {
|
||||
const capacityDisk = (resource: Resource) => {
|
||||
const total = typeof resource.disk?.total === 'number' ? resource.disk.total : 0;
|
||||
const used = typeof resource.disk?.used === 'number' ? resource.disk.used : 0;
|
||||
const free =
|
||||
typeof resource.disk?.free === 'number' ? resource.disk.free : Math.max(0, total - used);
|
||||
const usage =
|
||||
typeof resource.disk?.current === 'number' && Number.isFinite(resource.disk.current)
|
||||
? resource.disk.current
|
||||
: total > 0
|
||||
? (used / total) * 100
|
||||
: 0;
|
||||
return { total, used, free, usage };
|
||||
};
|
||||
|
||||
const hasCapacityMetric = (resource: Resource): boolean => {
|
||||
if (typeof resource.disk?.current === 'number' && Number.isFinite(resource.disk.current)) {
|
||||
return resource.disk.current;
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
return (
|
||||
typeof resource.disk?.used === 'number' &&
|
||||
typeof resource.disk?.total === 'number' &&
|
||||
resource.disk.total > 0
|
||||
) {
|
||||
return (resource.disk.used / resource.disk.total) * 100;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const capacityLabel = (resource: Resource): string => {
|
||||
if (typeof resource.disk?.used === 'number' && typeof resource.disk?.total === 'number') {
|
||||
return `${formatBytes(resource.disk.used)} / ${formatBytes(resource.disk.total)}`;
|
||||
}
|
||||
const percent = capacityPercent(resource);
|
||||
return percent === undefined ? '-' : formatPercent(percent);
|
||||
};
|
||||
|
||||
const CapacityCell: Component<{ resource: Resource }> = (props) => {
|
||||
const percent = () => capacityPercent(props.resource);
|
||||
return (
|
||||
<div class="min-w-0">
|
||||
<div class="truncate text-base-content" title={capacityLabel(props.resource)}>
|
||||
{capacityLabel(props.resource)}
|
||||
</div>
|
||||
<Show when={percent() !== undefined}>
|
||||
<div class="mt-1 h-1.5 overflow-hidden rounded bg-surface-alt">
|
||||
<div
|
||||
class="h-full rounded bg-emerald-500"
|
||||
style={{ width: `${Math.max(0, Math.min(100, percent() ?? 0))}%` }}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const statusLabel = (resource: Resource): string => {
|
||||
switch (mapVmwareDatastoreStatus(resource)) {
|
||||
case 'accessible':
|
||||
return 'Accessible';
|
||||
case 'attention':
|
||||
return 'Attention';
|
||||
case 'inaccessible':
|
||||
return 'Inaccessible';
|
||||
case 'maintenance':
|
||||
return 'Maintenance';
|
||||
case 'unknown':
|
||||
return 'Unknown';
|
||||
}
|
||||
};
|
||||
|
||||
const statusPillClass = (resource: Resource): string => {
|
||||
switch (mapVmwareDatastoreStatus(resource)) {
|
||||
case 'accessible':
|
||||
return 'border-emerald-300/50 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300';
|
||||
case 'attention':
|
||||
case 'maintenance':
|
||||
return 'border-amber-300/50 bg-amber-500/10 text-amber-700 dark:text-amber-300';
|
||||
case 'inaccessible':
|
||||
return 'border-red-300/50 bg-red-500/10 text-red-700 dark:text-red-300';
|
||||
case 'unknown':
|
||||
return 'border-border bg-surface-alt text-muted';
|
||||
}
|
||||
};
|
||||
|
||||
const StatusPill: Component<{ resource: Resource }> = (props) => (
|
||||
<span
|
||||
class={`inline-flex items-center rounded border px-1.5 py-0.5 text-[10px] font-medium ${statusPillClass(
|
||||
props.resource,
|
||||
)}`}
|
||||
>
|
||||
{statusLabel(props.resource)}
|
||||
</span>
|
||||
);
|
||||
|
||||
export const VsphereDatastoresTable: Component<{
|
||||
datastores: Resource[];
|
||||
scope: Resource[];
|
||||
@@ -244,9 +209,6 @@ export const VsphereDatastoresTable: Component<{
|
||||
>
|
||||
Datacenter
|
||||
</TableHead>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('badge')} md:w-[8%]`}>
|
||||
State
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody class={PLATFORM_TABLE_BODY_CLASS}>
|
||||
@@ -257,7 +219,16 @@ export const VsphereDatastoresTable: Component<{
|
||||
const indicator = () => getSimpleStatusIndicator(datastore.status);
|
||||
const name = () => datastoreName(datastore);
|
||||
const datacenter = () =>
|
||||
asTrimmedString(datastore.vmware?.datacenterName) || '-';
|
||||
asTrimmedString(datastore.vmware?.datacenterName) || '—';
|
||||
const datastoreSubtitle = () =>
|
||||
asTrimmedString(datastore.vmware?.datastoreUrl) ||
|
||||
asTrimmedString(datastore.vmware?.folderName) ||
|
||||
asTrimmedString(datastore.vmware?.vcenterHost) ||
|
||||
'';
|
||||
const datastoreTitle = () =>
|
||||
[name(), datastoreSubtitle()].filter(Boolean).join(' · ') || name();
|
||||
const disk = createMemo(() => capacityDisk(datastore));
|
||||
const showCapacity = () => hasCapacityMetric(datastore);
|
||||
const detailRowId = () => drawer.detailRowId(datastore);
|
||||
const isExpanded = () => drawer.isExpanded(datastore);
|
||||
return (
|
||||
@@ -278,20 +249,12 @@ export const VsphereDatastoresTable: Component<{
|
||||
variant={indicator().variant}
|
||||
title={indicator().label}
|
||||
/>
|
||||
<div class="min-w-0">
|
||||
<div class="truncate font-medium text-base-content" title={name()}>
|
||||
{name()}
|
||||
</div>
|
||||
<div
|
||||
class="truncate text-[10px] text-muted"
|
||||
title={datastore.vmware?.datastoreUrl}
|
||||
>
|
||||
{datastore.vmware?.datastoreUrl ||
|
||||
datastore.vmware?.folderName ||
|
||||
datastore.vmware?.vcenterHost ||
|
||||
'vSphere datastore'}
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
class="truncate font-medium text-base-content"
|
||||
title={datastoreTitle()}
|
||||
>
|
||||
{name()}
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class={getPlatformTableCellClassForKind('text')}>
|
||||
@@ -300,7 +263,18 @@ export const VsphereDatastoresTable: Component<{
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell class={getPlatformTableCellClassForKind('metric-bar')}>
|
||||
<CapacityCell resource={datastore} />
|
||||
<Show
|
||||
when={showCapacity()}
|
||||
fallback={
|
||||
<div class="flex justify-center">
|
||||
<span class="text-xs text-muted" aria-hidden="true">
|
||||
—
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<StackedDiskBar aggregateDisk={disk()} />
|
||||
</Show>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content md:table-cell`}
|
||||
@@ -325,15 +299,12 @@ export const VsphereDatastoresTable: Component<{
|
||||
>
|
||||
<span class="block truncate">{datacenter()}</span>
|
||||
</TableCell>
|
||||
<TableCell class={getPlatformTableCellClassForKind('badge')}>
|
||||
<StatusPill resource={datastore} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<PlatformResourceDetailTableRow
|
||||
resource={datastore}
|
||||
open={isExpanded()}
|
||||
detailRowId={detailRowId()}
|
||||
colSpan={8}
|
||||
colSpan={7}
|
||||
resolveResourceLabel={resolveResourceLabel}
|
||||
onClose={() => drawer.close(datastore)}
|
||||
/>
|
||||
|
||||
@@ -15,6 +15,10 @@ import {
|
||||
import { getSimpleStatusIndicator } from '@/utils/status';
|
||||
import { asTrimmedString } from '@/utils/stringUtils';
|
||||
import { formatVmwareClusterServices } from '@/utils/vmwareDisplay';
|
||||
import {
|
||||
formatVmwarePowerState,
|
||||
getVmwarePowerStateVariant,
|
||||
} from './vmwarePageModel';
|
||||
import { buildMetricKeyForUnifiedResource } from '@/utils/metricsKeys';
|
||||
import {
|
||||
PLATFORM_TABLE_BODY_CLASS,
|
||||
@@ -60,25 +64,6 @@ const metricFallback = () => (
|
||||
</div>
|
||||
);
|
||||
|
||||
const powerStateVariant = (
|
||||
state: string | undefined,
|
||||
): 'success' | 'warning' | 'danger' | 'muted' => {
|
||||
const normalized = (state || '').trim().toUpperCase();
|
||||
if (normalized === 'POWERED_ON') return 'success';
|
||||
if (normalized === 'POWERED_OFF') return 'muted';
|
||||
if (normalized === 'SUSPENDED') return 'warning';
|
||||
return 'muted';
|
||||
};
|
||||
|
||||
const formatPowerState = (state: string | undefined): string => {
|
||||
const normalized = (state || '').trim();
|
||||
if (!normalized) return '—';
|
||||
return normalized
|
||||
.split('_')
|
||||
.map((part) => part.charAt(0) + part.slice(1).toLowerCase())
|
||||
.join(' ');
|
||||
};
|
||||
|
||||
export const VsphereHostsTable: Component<{
|
||||
hosts: Resource[];
|
||||
// Full vSphere scope so we can count VMs per host without spawning
|
||||
@@ -254,14 +239,9 @@ export const VsphereHostsTable: Component<{
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content md:table-cell`}
|
||||
title={[cluster(), clusterServices()].filter(Boolean).join(' | ')}
|
||||
title={[cluster(), clusterServices()].filter(Boolean).join(' · ')}
|
||||
>
|
||||
<span class="block truncate">{cluster()}</span>
|
||||
<Show when={clusterServices()}>
|
||||
<span class="block truncate text-[10px] text-muted">
|
||||
{clusterServices()}
|
||||
</span>
|
||||
</Show>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} hidden md:table-cell`}
|
||||
@@ -269,12 +249,12 @@ export const VsphereHostsTable: Component<{
|
||||
<div class="flex items-center gap-2">
|
||||
<StatusDot
|
||||
size="sm"
|
||||
variant={powerStateVariant(meta()?.powerState)}
|
||||
variant={getVmwarePowerStateVariant(meta()?.powerState)}
|
||||
title={meta()?.powerState || 'unknown'}
|
||||
ariaHidden
|
||||
/>
|
||||
<span class="text-base-content">
|
||||
{formatPowerState(meta()?.powerState)}
|
||||
{formatVmwarePowerState(meta()?.powerState)}
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
createPlatformTableFilterState,
|
||||
getPlatformTableCellClassForKind,
|
||||
getPlatformTableHeadClassForKind,
|
||||
platformChipStatusDot,
|
||||
type PlatformTableFilterOption,
|
||||
} from '@/features/platformPage/sharedPlatformPage';
|
||||
import {
|
||||
@@ -32,14 +33,23 @@ import {
|
||||
import type { Resource } from '@/types/resource';
|
||||
import {
|
||||
filterVmwareNetworks,
|
||||
mapVmwareNetworkStatus,
|
||||
type VmwareNetworkStatusFilter,
|
||||
} from './vmwarePageModel';
|
||||
|
||||
const VSPHERE_NETWORK_STATUS_OPTIONS: PlatformTableFilterOption<VmwareNetworkStatusFilter>[] = [
|
||||
{ value: 'all', label: 'All' },
|
||||
{ value: 'healthy', label: 'Healthy', tone: 'success' },
|
||||
{ value: 'attention', label: 'Attention', tone: 'warning' },
|
||||
{
|
||||
value: 'healthy',
|
||||
label: 'Healthy',
|
||||
tone: 'success',
|
||||
leading: platformChipStatusDot('bg-emerald-500'),
|
||||
},
|
||||
{
|
||||
value: 'attention',
|
||||
label: 'Attention',
|
||||
tone: 'warning',
|
||||
leading: platformChipStatusDot('bg-amber-500'),
|
||||
},
|
||||
{ value: 'unknown', label: 'Unknown' },
|
||||
];
|
||||
|
||||
@@ -47,14 +57,14 @@ const networkName = (resource: Resource): string =>
|
||||
asTrimmedString(resource.displayName) || asTrimmedString(resource.name) || resource.id;
|
||||
|
||||
const networkType = (resource: Resource): string =>
|
||||
asTrimmedString(resource.vmware?.networkType) || '-';
|
||||
asTrimmedString(resource.vmware?.networkType) || '—';
|
||||
|
||||
const compactList = (values: Array<string | undefined>): string[] =>
|
||||
values.map((value) => asTrimmedString(value)).filter((value): value is string => Boolean(value));
|
||||
|
||||
const summarizeValues = (
|
||||
values: string[],
|
||||
empty = '-',
|
||||
empty = '—',
|
||||
visibleCount = 2,
|
||||
): { label: string; title: string } => {
|
||||
if (values.length === 0) return { label: empty, title: '' };
|
||||
@@ -64,46 +74,14 @@ const summarizeValues = (
|
||||
};
|
||||
|
||||
const hostSummary = (resource: Resource): { label: string; title: string } =>
|
||||
summarizeValues(compactList(resource.vmware?.networkHostNames ?? []), '-', 2);
|
||||
summarizeValues(compactList(resource.vmware?.networkHostNames ?? []), '—', 2);
|
||||
|
||||
const vmSummary = (resource: Resource): { label: string; title: string } =>
|
||||
summarizeValues(compactList(resource.vmware?.networkVmNames ?? []), '-', 2);
|
||||
summarizeValues(compactList(resource.vmware?.networkVmNames ?? []), '—', 2);
|
||||
|
||||
const vmCount = (resource: Resource): number =>
|
||||
resource.vmware?.networkVmNames?.length ?? resource.vmware?.networkVmIds?.length ?? 0;
|
||||
|
||||
const statusLabel = (resource: Resource): string => {
|
||||
switch (mapVmwareNetworkStatus(resource)) {
|
||||
case 'healthy':
|
||||
return 'Healthy';
|
||||
case 'attention':
|
||||
return 'Attention';
|
||||
case 'unknown':
|
||||
return 'Unknown';
|
||||
}
|
||||
};
|
||||
|
||||
const statusPillClass = (resource: Resource): string => {
|
||||
switch (mapVmwareNetworkStatus(resource)) {
|
||||
case 'healthy':
|
||||
return 'border-emerald-300/50 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300';
|
||||
case 'attention':
|
||||
return 'border-amber-300/50 bg-amber-500/10 text-amber-700 dark:text-amber-300';
|
||||
case 'unknown':
|
||||
return 'border-border bg-surface-alt text-muted';
|
||||
}
|
||||
};
|
||||
|
||||
const StatusPill: Component<{ resource: Resource }> = (props) => (
|
||||
<span
|
||||
class={`inline-flex items-center rounded border px-1.5 py-0.5 text-[10px] font-medium ${statusPillClass(
|
||||
props.resource,
|
||||
)}`}
|
||||
>
|
||||
{statusLabel(props.resource)}
|
||||
</span>
|
||||
);
|
||||
|
||||
export const VsphereNetworksTable: Component<{
|
||||
networks: Resource[];
|
||||
scope: Resource[];
|
||||
@@ -187,9 +165,6 @@ export const VsphereNetworksTable: Component<{
|
||||
>
|
||||
Datacenter
|
||||
</TableHead>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('badge')} md:w-[8%]`}>
|
||||
State
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody class={PLATFORM_TABLE_BODY_CLASS}>
|
||||
@@ -199,7 +174,14 @@ export const VsphereNetworksTable: Component<{
|
||||
const vms = createMemo(() => vmSummary(network));
|
||||
const indicator = () => getSimpleStatusIndicator(network.status);
|
||||
const name = () => networkName(network);
|
||||
const datacenter = () => asTrimmedString(network.vmware?.datacenterName) || '-';
|
||||
const datacenter = () => asTrimmedString(network.vmware?.datacenterName) || '—';
|
||||
const networkSubtitle = () =>
|
||||
asTrimmedString(network.vmware?.managedObjectId) ||
|
||||
asTrimmedString(network.vmware?.folderName) ||
|
||||
asTrimmedString(network.vmware?.vcenterHost) ||
|
||||
'';
|
||||
const networkTitle = () =>
|
||||
[name(), networkSubtitle()].filter(Boolean).join(' · ') || name();
|
||||
const detailRowId = () => drawer.detailRowId(network);
|
||||
const isExpanded = () => drawer.isExpanded(network);
|
||||
return (
|
||||
@@ -220,20 +202,12 @@ export const VsphereNetworksTable: Component<{
|
||||
variant={indicator().variant}
|
||||
title={indicator().label}
|
||||
/>
|
||||
<div class="min-w-0">
|
||||
<div class="truncate font-medium text-base-content" title={name()}>
|
||||
{name()}
|
||||
</div>
|
||||
<div
|
||||
class="truncate text-[10px] text-muted"
|
||||
title={network.vmware?.managedObjectId}
|
||||
>
|
||||
{network.vmware?.managedObjectId ||
|
||||
network.vmware?.folderName ||
|
||||
network.vmware?.vcenterHost ||
|
||||
'vSphere network'}
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
class="truncate font-medium text-base-content"
|
||||
title={networkTitle()}
|
||||
>
|
||||
{name()}
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class={getPlatformTableCellClassForKind('text')}>
|
||||
@@ -264,15 +238,12 @@ export const VsphereNetworksTable: Component<{
|
||||
>
|
||||
<span class="block truncate">{datacenter()}</span>
|
||||
</TableCell>
|
||||
<TableCell class={getPlatformTableCellClassForKind('badge')}>
|
||||
<StatusPill resource={network} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<PlatformResourceDetailTableRow
|
||||
resource={network}
|
||||
open={isExpanded()}
|
||||
detailRowId={detailRowId()}
|
||||
colSpan={7}
|
||||
colSpan={6}
|
||||
resolveResourceLabel={resolveResourceLabel}
|
||||
onClose={() => drawer.close(network)}
|
||||
/>
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
createPlatformTableFilterState,
|
||||
getPlatformTableCellClassForKind,
|
||||
getPlatformTableHeadClassForKind,
|
||||
platformChipStatusDot,
|
||||
type PlatformTableFilterOption,
|
||||
} from '@/features/platformPage/sharedPlatformPage';
|
||||
import {
|
||||
@@ -38,12 +39,12 @@ import type {
|
||||
Resource,
|
||||
ResourceVMwareNetworkAdapter,
|
||||
ResourceVMwareSnapshot,
|
||||
ResourceVMwareTools,
|
||||
ResourceVMwareVirtualDisk,
|
||||
} from '@/types/resource';
|
||||
import {
|
||||
filterVmwareVirtualMachines,
|
||||
mapVmwareVirtualMachineStatus,
|
||||
formatVmwarePowerState,
|
||||
getVmwarePowerStateVariant,
|
||||
type VmwareVirtualMachineStatusFilter,
|
||||
} from './vmwarePageModel';
|
||||
|
||||
@@ -53,11 +54,26 @@ const VSPHERE_VM_STATUS_OPTIONS: PlatformTableFilterOption<VmwareVirtualMachineS
|
||||
value: 'powered-on',
|
||||
label: 'Powered on',
|
||||
tone: 'success',
|
||||
leading: statusDot('bg-emerald-500'),
|
||||
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: 'attention', label: 'Attention', tone: 'warning', leading: statusDot('bg-amber-500') },
|
||||
{ value: 'powered-off', label: 'Powered off', tone: 'danger', leading: statusDot('bg-red-500') },
|
||||
{ value: 'suspended', label: 'Suspended', tone: 'warning' },
|
||||
{ value: 'unknown', label: 'Unknown' },
|
||||
];
|
||||
|
||||
@@ -71,48 +87,20 @@ type VmwareVmGroup = {
|
||||
const finiteMetric = (value: number | undefined): number | undefined =>
|
||||
typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
||||
|
||||
function statusDot(className: string): JSX.Element {
|
||||
return <span class={`h-2 w-2 rounded-full ${className}`} />;
|
||||
}
|
||||
|
||||
const metricFallback = () => (
|
||||
<div class="flex justify-center">
|
||||
<span class="text-xs text-muted" aria-hidden="true">
|
||||
-
|
||||
—
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
const normalizeToken = (value: string | undefined): string =>
|
||||
(value || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[\s_-]/g, '');
|
||||
|
||||
const vmName = (resource: Resource): string =>
|
||||
asTrimmedString(resource.displayName) || asTrimmedString(resource.name) || resource.id;
|
||||
|
||||
const formatVmwarePowerState = (value: string | undefined): string => {
|
||||
const normalized = normalizeToken(value);
|
||||
if (normalized === 'poweredon' || normalized === 'on') return 'On';
|
||||
if (normalized === 'poweredoff' || normalized === 'off') return 'Off';
|
||||
if (normalized === 'suspended') return 'Suspended';
|
||||
return asTrimmedString(value) || 'Unknown';
|
||||
};
|
||||
|
||||
const powerStateVariant = (
|
||||
state: string | undefined,
|
||||
): 'success' | 'warning' | 'danger' | 'muted' => {
|
||||
const normalized = normalizeToken(state);
|
||||
if (normalized === 'poweredon' || normalized === 'on') return 'success';
|
||||
if (normalized === 'poweredoff' || normalized === 'off') return 'danger';
|
||||
if (normalized === 'suspended') return 'warning';
|
||||
return 'muted';
|
||||
};
|
||||
|
||||
const summarizeValues = (
|
||||
values: Array<string | undefined>,
|
||||
empty = '-',
|
||||
empty = '—',
|
||||
visibleCount = 2,
|
||||
): { label: string; title: string } => {
|
||||
const compact = values
|
||||
@@ -167,7 +155,7 @@ const networkSummary = (
|
||||
adapters: ResourceVMwareNetworkAdapter[] | undefined,
|
||||
): { label: string; title: string } => {
|
||||
const names = (adapters ?? []).map(networkAdapterName).filter(Boolean);
|
||||
const summary = summarizeValues(names, '-', 1);
|
||||
const summary = summarizeValues(names, '—', 1);
|
||||
const title = (adapters ?? []).map(networkAdapterTitle).filter(Boolean).join(' | ');
|
||||
return { label: summary.label, title: title || summary.title };
|
||||
};
|
||||
@@ -210,134 +198,12 @@ const formatGuest = (resource: Resource): { label: string; detail: string; title
|
||||
const host = asTrimmedString(resource.vmware?.guestHostname);
|
||||
const ips = summarizeValues(resource.vmware?.guestIpAddresses ?? [], '', 1);
|
||||
return {
|
||||
label: family || host || ips.label || '-',
|
||||
label: family || host || ips.label || '—',
|
||||
detail: host || ips.label,
|
||||
title: [family, host, ips.title].filter(Boolean).join(' | '),
|
||||
};
|
||||
};
|
||||
|
||||
const healthLabel = (resource: Resource): string => {
|
||||
const alarms = resource.vmware?.activeAlarmCount ?? 0;
|
||||
if (alarms > 0) return `${alarms} alarm${alarms === 1 ? '' : 's'}`;
|
||||
const overall = asTrimmedString(resource.vmware?.overallStatus);
|
||||
if (!overall) return 'Unknown';
|
||||
if (overall.toLowerCase() === 'green') return 'Healthy';
|
||||
return overall.charAt(0).toUpperCase() + overall.slice(1).toLowerCase();
|
||||
};
|
||||
|
||||
const healthPillClass = (resource: Resource): string => {
|
||||
const status = mapVmwareVirtualMachineStatus(resource);
|
||||
if (status === 'attention') {
|
||||
const overall = asTrimmedString(resource.vmware?.overallStatus)?.toLowerCase();
|
||||
if (overall === 'red') return 'border-red-300/50 bg-red-500/10 text-red-700 dark:text-red-300';
|
||||
return 'border-amber-300/50 bg-amber-500/10 text-amber-700 dark:text-amber-300';
|
||||
}
|
||||
if (status === 'powered-on') {
|
||||
return 'border-emerald-300/50 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300';
|
||||
}
|
||||
if (status === 'powered-off') {
|
||||
return 'border-border bg-surface-alt text-muted';
|
||||
}
|
||||
return 'border-border bg-surface-alt text-muted';
|
||||
};
|
||||
|
||||
const HealthPill: Component<{ resource: Resource }> = (props) => (
|
||||
<span
|
||||
class={`inline-flex items-center rounded border px-1.5 py-0.5 text-[10px] font-medium ${healthPillClass(
|
||||
props.resource,
|
||||
)}`}
|
||||
>
|
||||
{healthLabel(props.resource)}
|
||||
</span>
|
||||
);
|
||||
|
||||
const VMWARE_TOKEN_ACRONYMS: Record<string, string> = {
|
||||
API: 'API',
|
||||
OS: 'OS',
|
||||
VM: 'VM',
|
||||
VMDK: 'VMDK',
|
||||
};
|
||||
|
||||
const titleizeToken = (value: string | undefined): string =>
|
||||
(value || '')
|
||||
.split(/[\s_-]+/)
|
||||
.filter(Boolean)
|
||||
.map((part) => {
|
||||
const upper = part.toUpperCase();
|
||||
return (
|
||||
VMWARE_TOKEN_ACRONYMS[upper] ?? part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()
|
||||
);
|
||||
})
|
||||
.join(' ');
|
||||
|
||||
const toolsTitle = (tools: ResourceVMwareTools | undefined): string =>
|
||||
tools
|
||||
? [
|
||||
tools.runState ? `Run state: ${titleizeToken(tools.runState)}` : '',
|
||||
tools.versionStatus ? `Version status: ${titleizeToken(tools.versionStatus)}` : '',
|
||||
asTrimmedString(tools.version),
|
||||
tools.installType ? titleizeToken(tools.installType) : '',
|
||||
tools.upgradePolicy ? `Policy: ${titleizeToken(tools.upgradePolicy)}` : '',
|
||||
tools.guestRebootRequested ? 'Guest reboot requested' : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' | ')
|
||||
: '';
|
||||
|
||||
const toolsNeedsAttention = (tools: ResourceVMwareTools | undefined): boolean => {
|
||||
const runState = normalizeToken(tools?.runState);
|
||||
const versionStatus = normalizeToken(tools?.versionStatus);
|
||||
return Boolean(
|
||||
tools?.guestRebootRequested ||
|
||||
(runState && !['running', 'started'].includes(runState)) ||
|
||||
[
|
||||
'notinstalled',
|
||||
'tooold',
|
||||
'toonew',
|
||||
'old',
|
||||
'blacklisted',
|
||||
'unsupported',
|
||||
'supportedold',
|
||||
].includes(versionStatus),
|
||||
);
|
||||
};
|
||||
|
||||
const toolsLabel = (tools: ResourceVMwareTools | undefined): string => {
|
||||
if (!tools) return 'Unknown';
|
||||
if (tools.guestRebootRequested) return 'Reboot';
|
||||
const runState = normalizeToken(tools.runState);
|
||||
const versionStatus = normalizeToken(tools.versionStatus);
|
||||
if (['notinstalled'].includes(versionStatus)) return 'Missing';
|
||||
if (['tooold', 'old', 'supportedold'].includes(versionStatus)) return 'Old';
|
||||
if (runState && !['running', 'started'].includes(runState)) return titleizeToken(tools.runState);
|
||||
if (versionStatus && !['current', 'ok'].includes(versionStatus)) {
|
||||
return titleizeToken(tools.versionStatus);
|
||||
}
|
||||
if (runState) return titleizeToken(tools.runState);
|
||||
return 'Unknown';
|
||||
};
|
||||
|
||||
const toolsPillClass = (tools: ResourceVMwareTools | undefined): string => {
|
||||
if (toolsNeedsAttention(tools)) {
|
||||
return 'border-amber-300/50 bg-amber-500/10 text-amber-700 dark:text-amber-300';
|
||||
}
|
||||
if (normalizeToken(tools?.runState) === 'running') {
|
||||
return 'border-emerald-300/50 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300';
|
||||
}
|
||||
return 'border-border bg-surface-alt text-muted';
|
||||
};
|
||||
|
||||
const ToolsPill: Component<{ tools: ResourceVMwareTools | undefined }> = (props) => (
|
||||
<span
|
||||
class={`inline-flex items-center rounded border px-1.5 py-0.5 text-[10px] font-medium ${toolsPillClass(
|
||||
props.tools,
|
||||
)}`}
|
||||
title={toolsTitle(props.tools)}
|
||||
>
|
||||
{toolsLabel(props.tools)}
|
||||
</span>
|
||||
);
|
||||
|
||||
const vmGroupKey = (resource: Resource): string =>
|
||||
asTrimmedString(resource.vmware?.runtimeHostName) ||
|
||||
asTrimmedString(resource.parentName) ||
|
||||
@@ -421,7 +287,7 @@ export const VsphereVirtualMachinesTable: Component<{
|
||||
>
|
||||
<TableCard class={PLATFORM_TABLE_CARD_CLASS}>
|
||||
<TableCardHeader title="Virtual Machines" />
|
||||
<Table class="min-w-full table-fixed text-xs md:min-w-[1660px]">
|
||||
<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%]`}>
|
||||
@@ -476,12 +342,6 @@ export const VsphereVirtualMachinesTable: Component<{
|
||||
>
|
||||
Snapshots
|
||||
</TableHead>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('badge')} md:w-[7%]`}>
|
||||
Tools
|
||||
</TableHead>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('badge')} md:w-[8%]`}>
|
||||
Health
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody class={PLATFORM_TABLE_BODY_CLASS}>
|
||||
@@ -489,7 +349,7 @@ export const VsphereVirtualMachinesTable: Component<{
|
||||
{(group) => (
|
||||
<>
|
||||
<TableRow class="bg-surface-alt/70 text-[11px] font-semibold text-base-content">
|
||||
<TableCell colSpan={14} class="px-2 py-1">
|
||||
<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">
|
||||
@@ -507,17 +367,17 @@ export const VsphereVirtualMachinesTable: Component<{
|
||||
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 pool = () => asTrimmedString(meta()?.resourcePoolName) || '—';
|
||||
const guest = createMemo(() => formatGuest(vm));
|
||||
const network = createMemo(() => networkSummary(meta()?.networkAdapters));
|
||||
const datastores = createMemo(() =>
|
||||
summarizeValues(meta()?.datastoreNames ?? [], '-', 1),
|
||||
summarizeValues(meta()?.datastoreNames ?? [], '—', 1),
|
||||
);
|
||||
const disks = () => virtualDiskCount(meta()?.virtualDisks);
|
||||
const diskTitle = () => virtualDiskSummaryTitle(meta()?.virtualDisks);
|
||||
@@ -562,29 +422,27 @@ export const VsphereVirtualMachinesTable: Component<{
|
||||
variant={indicator().variant}
|
||||
title={indicator().label}
|
||||
/>
|
||||
<div class="min-w-0">
|
||||
<div
|
||||
class="truncate font-medium text-base-content"
|
||||
title={name()}
|
||||
>
|
||||
{name()}
|
||||
</div>
|
||||
<div
|
||||
class="truncate text-[10px] text-muted"
|
||||
title={meta()?.managedObjectId}
|
||||
>
|
||||
{meta()?.managedObjectId ||
|
||||
meta()?.instanceUuid ||
|
||||
'vSphere VM'}
|
||||
</div>
|
||||
</div>
|
||||
<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={powerStateVariant(meta()?.powerState)}
|
||||
variant={getVmwarePowerStateVariant(meta()?.powerState)}
|
||||
title={meta()?.powerState || 'unknown'}
|
||||
ariaHidden
|
||||
/>
|
||||
@@ -601,14 +459,9 @@ export const VsphereVirtualMachinesTable: Component<{
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content md:table-cell`}
|
||||
title={[cluster(), clusterServices()].filter(Boolean).join(' | ')}
|
||||
title={[cluster(), clusterServices()].filter(Boolean).join(' · ')}
|
||||
>
|
||||
<span class="block truncate">{cluster()}</span>
|
||||
<Show when={clusterServices()}>
|
||||
<span class="block truncate text-[10px] text-muted">
|
||||
{clusterServices()}
|
||||
</span>
|
||||
</Show>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content lg:table-cell`}
|
||||
@@ -678,18 +531,12 @@ export const VsphereVirtualMachinesTable: Component<{
|
||||
>
|
||||
{snapshots()}
|
||||
</TableCell>
|
||||
<TableCell class={getPlatformTableCellClassForKind('badge')}>
|
||||
<ToolsPill tools={meta()?.tools} />
|
||||
</TableCell>
|
||||
<TableCell class={getPlatformTableCellClassForKind('badge')}>
|
||||
<HealthPill resource={vm} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<PlatformResourceDetailTableRow
|
||||
resource={vm}
|
||||
open={isExpanded()}
|
||||
detailRowId={detailRowId()}
|
||||
colSpan={14}
|
||||
colSpan={12}
|
||||
resolveResourceLabel={resolveResourceLabel}
|
||||
onClose={() => drawer.close(vm)}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { cleanup, fireEvent, render, screen, within } from '@solidjs/testing-library';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
vi.mock('@/components/Workloads/StackedDiskBar', () => ({
|
||||
StackedDiskBar: () => <div data-testid="stacked-disk-bar" />,
|
||||
}));
|
||||
|
||||
import { VsphereDatastoresTable } from '@/features/vmware/VsphereDatastoresTable';
|
||||
import type { Resource } from '@/types/resource';
|
||||
@@ -75,10 +79,10 @@ describe('VsphereDatastoresTable', () => {
|
||||
expect(within(table).getByText('VMs')).toBeInTheDocument();
|
||||
expect(within(table).queryByText('Protection')).not.toBeInTheDocument();
|
||||
expect(within(table).queryByText('Growth (24h)')).not.toBeInTheDocument();
|
||||
expect(within(table).queryByRole('columnheader', { name: 'State' })).not.toBeInTheDocument();
|
||||
expect(screen.getAllByText('esxi-01.lab.local, esxi-02.lab.local')).toHaveLength(2);
|
||||
expect(screen.getAllByText('warehouse-api-01, etl-batch-01')).toHaveLength(2);
|
||||
expect(screen.getByText('Accessible')).toBeInTheDocument();
|
||||
expect(screen.getByText('Inaccessible')).toBeInTheDocument();
|
||||
expect(screen.getAllByTestId('stacked-disk-bar').length).toBeGreaterThan(0);
|
||||
|
||||
const row = screen.getByText('nvme-primary').closest('tr');
|
||||
expect(row).toHaveAttribute('aria-expanded', 'false');
|
||||
|
||||
@@ -65,12 +65,11 @@ describe('VsphereNetworksTable', () => {
|
||||
expect(within(table).getByText('Type')).toBeInTheDocument();
|
||||
expect(within(table).getByText('Hosts')).toBeInTheDocument();
|
||||
expect(within(table).getByText('Connected VMs')).toBeInTheDocument();
|
||||
expect(within(table).queryByRole('columnheader', { name: 'State' })).not.toBeInTheDocument();
|
||||
expect(screen.getByText('STANDARD_PORTGROUP')).toBeInTheDocument();
|
||||
expect(screen.getByText('DISTRIBUTED_PORTGROUP')).toBeInTheDocument();
|
||||
expect(screen.getByText('esxi-01.lab.local, esxi-02.lab.local')).toBeInTheDocument();
|
||||
expect(screen.getByText('warehouse-api-01, etl-batch-01')).toBeInTheDocument();
|
||||
expect(screen.getByText('Healthy')).toBeInTheDocument();
|
||||
expect(screen.getByText('Attention')).toBeInTheDocument();
|
||||
|
||||
const row = screen.getByText('VM Network').closest('tr');
|
||||
expect(row).toHaveAttribute('aria-expanded', 'false');
|
||||
|
||||
@@ -121,8 +121,8 @@ describe('VsphereVirtualMachinesTable', () => {
|
||||
expect(within(table).getByText('Network')).toBeInTheDocument();
|
||||
expect(within(table).getByText('Disks')).toBeInTheDocument();
|
||||
expect(within(table).getByText('Snapshots')).toBeInTheDocument();
|
||||
expect(within(table).getByText('Tools')).toBeInTheDocument();
|
||||
expect(within(table).getByText('Health')).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();
|
||||
@@ -134,8 +134,6 @@ describe('VsphereVirtualMachinesTable', () => {
|
||||
expect(screen.getByTitle(/Hard disk 1 · SCSI 0:1 · 100 GB · nvme-primary/)).toHaveTextContent(
|
||||
'1',
|
||||
);
|
||||
expect(screen.getByTitle(/Run state: Running/)).toHaveTextContent('Running');
|
||||
expect(screen.getByText('Healthy')).toBeInTheDocument();
|
||||
expect(screen.getByTitle('pre-upgrade, post-upgrade (current)')).toHaveTextContent('2');
|
||||
|
||||
const row = screen.getByText('warehouse-api-01').closest('tr');
|
||||
|
||||
@@ -283,6 +283,29 @@ export function mapVmwareIncidentSeverity(
|
||||
return 'info';
|
||||
}
|
||||
|
||||
export function normalizeVmwarePowerStateToken(value: string | undefined): string {
|
||||
return (value || '').trim().toLowerCase().replace(/[\s_-]/g, '');
|
||||
}
|
||||
|
||||
export function formatVmwarePowerState(value: string | undefined): string {
|
||||
const normalized = normalizeVmwarePowerStateToken(value);
|
||||
if (normalized === 'poweredon' || normalized === 'on') return 'On';
|
||||
if (normalized === 'poweredoff' || normalized === 'off') return 'Off';
|
||||
if (normalized === 'suspended') return 'Suspended';
|
||||
const trimmed = (value || '').trim();
|
||||
return trimmed || 'Unknown';
|
||||
}
|
||||
|
||||
export function getVmwarePowerStateVariant(
|
||||
state: string | undefined,
|
||||
): 'success' | 'warning' | 'danger' | 'muted' {
|
||||
const normalized = normalizeVmwarePowerStateToken(state);
|
||||
if (normalized === 'poweredon' || normalized === 'on') return 'success';
|
||||
if (normalized === 'poweredoff' || normalized === 'off') return 'danger';
|
||||
if (normalized === 'suspended') return 'warning';
|
||||
return 'muted';
|
||||
}
|
||||
|
||||
export function mapVmwareVirtualMachineStatus(
|
||||
resource: Resource,
|
||||
): Exclude<VmwareVirtualMachineStatusFilter, 'all'> {
|
||||
|
||||
Reference in New Issue
Block a user