Refine alerts thresholds layout

This commit is contained in:
rcourtman
2025-10-07 19:46:54 +00:00
parent a78b9a6d78
commit ff1ceb99ee
2 changed files with 270 additions and 216 deletions
@@ -107,9 +107,6 @@ export function ResourceTable(props: ResourceTableProps) {
</a>
)}
</Show>
<Show when={meta.rawName && meta.rawName !== meta.displayName}>
<span class="text-xs text-gray-500 dark:text-gray-400">({meta.rawName})</span>
</Show>
<Show when={meta.clusterName}>
<span class="rounded px-2 py-0.5 text-[10px] font-medium whitespace-nowrap bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300">
{meta.clusterName}
@@ -153,6 +150,61 @@ export function ResourceTable(props: ResourceTableProps) {
);
};
const renderToggleBadge = (config: {
isEnabled: boolean;
disabled?: boolean;
size?: 'sm' | 'md';
onToggle?: () => void;
labelEnabled?: string;
labelDisabled?: string;
titleEnabled?: string;
titleDisabled?: string;
titleWhenDisabled?: string;
}) => {
const {
isEnabled,
disabled = false,
size = 'sm',
onToggle,
labelEnabled = 'Enabled',
labelDisabled = 'Disabled',
titleEnabled,
titleDisabled,
titleWhenDisabled,
} = config;
const basePadding = size === 'md' ? 'px-2.5 py-1' : 'px-2 py-0.5';
const baseClasses = `inline-flex items-center justify-center ${basePadding} text-xs font-semibold rounded-full border transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-blue-400`;
const stateClasses = isEnabled
? 'bg-emerald-500/10 text-emerald-700 border-emerald-500/50 hover:bg-emerald-500/15 dark:bg-emerald-500/20 dark:text-emerald-200 dark:border-emerald-400/40'
: 'bg-rose-500/10 text-rose-700 border-rose-500/50 hover:bg-rose-500/15 dark:bg-rose-500/20 dark:text-rose-200 dark:border-rose-400/40';
const disabledClasses = disabled
? 'opacity-60 cursor-not-allowed hover:bg-transparent dark:hover:bg-transparent'
: '';
const title = disabled
? titleWhenDisabled ?? titleDisabled ?? titleEnabled ?? ''
: isEnabled
? titleEnabled ?? ''
: titleDisabled ?? '';
return (
<button
type="button"
class={`${baseClasses} ${stateClasses} ${disabledClasses}`.trim()}
onClick={() => {
if (disabled) return;
onToggle?.();
}}
disabled={disabled}
aria-pressed={isEnabled}
title={title}
>
{isEnabled ? labelEnabled : labelDisabled}
</button>
);
};
return (
<Card
padding="none"
@@ -172,9 +224,6 @@ export function ResourceTable(props: ResourceTableProps) {
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Resource
</th>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Type
</th>
<For each={props.columns}>
{(column) => (
<th class="px-3 py-2 text-center text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
@@ -215,7 +264,7 @@ export function ResourceTable(props: ResourceTableProps) {
</div>
</Show>
</td>
<td class="p-1 px-2" colspan="2">
<td class="p-1 px-2">
<span class="text-sm font-bold text-blue-700 dark:text-blue-300">
Global Defaults
</span>
@@ -325,21 +374,19 @@ export function ResourceTable(props: ResourceTableProps) {
<Show when={props.showOfflineAlertsColumn}>
<td class="p-1 px-2 text-center align-middle">
<Show when={props.onToggleGlobalDisableOffline} fallback={<span class="text-sm text-gray-400">-</span>}>
<div class="flex items-center justify-center">
<TogglePrimitive
size="sm"
checked={!props.globalDisableOfflineFlag?.()}
onToggle={() => {
{(() => {
const offlineDisabled = props.globalDisableOfflineFlag?.() ?? false;
return renderToggleBadge({
isEnabled: !offlineDisabled,
size: 'md',
onToggle: () => {
props.onToggleGlobalDisableOffline?.();
props.setHasUnsavedChanges?.(true);
}}
checkedClass="bg-emerald-500/80 border-emerald-600/70 dark:bg-emerald-500/60 dark:border-emerald-500/70"
uncheckedClass="bg-rose-500/80 border-rose-600/70 dark:bg-rose-500/60 dark:border-rose-500/70"
class="my-[1px]"
title="Global offline alerts toggle"
ariaLabel="Global offline alerts toggle"
/>
</div>
},
titleEnabled: 'Offline alerts currently enabled globally. Click to disable.',
titleDisabled: 'Offline alerts currently disabled globally. Click to enable.',
});
})()}
</Show>
</td>
</Show>
@@ -362,7 +409,7 @@ export function ResourceTable(props: ResourceTableProps) {
{/* Node group header */}
<tr class="bg-gray-50 dark:bg-gray-700/50">
<td
colspan={props.columns.length + (props.showOfflineAlertsColumn ? 5 : 4)}
colspan={props.columns.length + (props.showOfflineAlertsColumn ? 4 : 3)}
class="p-1 px-2 text-xs font-medium text-gray-600 dark:text-gray-400"
>
{renderGroupHeader(nodeName, headerMeta)}
@@ -452,15 +499,6 @@ export function ResourceTable(props: ResourceTableProps) {
>
{resource.name}
</span>
<Show when={'vmid' in resource && resource.vmid}>
<span class="text-xs text-gray-500">({resource.vmid})</span>
</Show>
<Show when={resource.type === 'storage' && 'node' in resource && resource.node}>
<span class="text-xs text-gray-500">on {resource.node}</span>
</Show>
<Show when={resource.type === 'guest' && 'node' in resource && resource.node}>
<span class="text-xs text-gray-500">on {resource.node}</span>
</Show>
<Show when={resource.hasOverride || resource.disableConnectivity}>
<span class="text-xs px-1.5 py-0.5 bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 rounded">
Custom
@@ -473,7 +511,9 @@ export function ResourceTable(props: ResourceTableProps) {
<span
class={`text-sm font-medium ${resource.disabled ? 'text-gray-500 dark:text-gray-500' : 'text-gray-900 dark:text-gray-100'}`}
>
{resource.displayName || resource.name}
{resource.type === 'node'
? resource.name
: resource.displayName || resource.name}
</span>
}>
{(host) => (
@@ -489,13 +529,12 @@ export function ResourceTable(props: ResourceTableProps) {
}`}
title={`Open ${resource.displayName || resource.name} web interface`}
>
{resource.displayName || resource.name}
{resource.type === 'node'
? resource.name
: resource.displayName || resource.name}
</a>
)}
</Show>
<Show when={resource.rawName && resource.rawName !== resource.displayName}>
<span class="text-xs text-gray-500 dark:text-gray-400">({resource.rawName})</span>
</Show>
<Show when={resource.clusterName}>
<span class="rounded px-2 py-0.5 text-[10px] font-medium whitespace-nowrap bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300">
{resource.clusterName}
@@ -509,29 +548,8 @@ export function ResourceTable(props: ResourceTableProps) {
</div>
</Show>
</td>
<td class="p-1 px-2">
<span
class={`inline-block px-1.5 py-0.5 text-xs font-medium rounded ${
resource.type === 'pbs'
? 'bg-indigo-100 dark:bg-indigo-900/50 text-indigo-700 dark:text-indigo-300'
: resource.type === 'node'
? 'bg-purple-100 dark:bg-purple-900/50 text-purple-700 dark:text-purple-300'
: resource.type === 'storage'
? 'bg-orange-100 dark:bg-orange-900/50 text-orange-700 dark:text-orange-300'
: resource.type === 'dockerHost'
? 'bg-cyan-100 dark:bg-cyan-900/50 text-cyan-700 dark:text-cyan-300'
: resource.type === 'dockerContainer'
? 'bg-teal-100 dark:bg-teal-900/50 text-teal-700 dark:text-teal-300'
: resource.resourceType === 'VM'
? 'bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300'
: 'bg-green-100 dark:bg-green-900/50 text-green-700 dark:text-green-300'
}`}
>
{resource.resourceType}
</span>
</td>
{/* Metric columns - dynamically rendered based on resource type */}
<For each={props.columns}>
{/* Metric columns - dynamically rendered based on resource type */}
<For each={props.columns}>
{(column) => {
const normalizedColumn = column.trim().toLowerCase();
const metric = (
@@ -686,33 +704,24 @@ export function ResourceTable(props: ResourceTableProps) {
<Show when={props.showOfflineAlertsColumn}>
<td class="p-1 px-2 text-center align-middle">
<Show when={props.onToggleNodeConnectivity}>
<TogglePrimitive
size="sm"
checked={!resource.disableConnectivity}
disabled={props.globalDisableFlag?.() || props.globalDisableOfflineFlag?.()}
onToggle={() =>
!props.globalDisableFlag?.() &&
!props.globalDisableOfflineFlag?.() &&
props.onToggleNodeConnectivity?.(resource.id)
}
checkedClass="bg-emerald-500/80 border-emerald-600/70 dark:bg-emerald-500/60 dark:border-emerald-500/70"
uncheckedClass="bg-rose-500/80 border-rose-600/70 dark:bg-rose-500/60 dark:border-rose-500/70"
disabledClass="bg-slate-400/60 border-slate-500/70 dark:bg-slate-600/60 dark:border-slate-600/70 cursor-not-allowed opacity-60"
class="mx-auto my-[1px]"
title={
props.globalDisableFlag?.()
? 'Alerts disabled globally'
: props.globalDisableOfflineFlag?.()
? 'Offline alerts disabled globally'
: resource.disableConnectivity
? 'Offline alerts disabled for this resource'
: 'Offline alerts enabled'
}
ariaLabel={resource.disableConnectivity ? 'Offline alerts disabled for this resource' : 'Offline alerts enabled for this resource'}
/>
</Show>
</td>
</Show>
{(() => {
const disabledGlobally = props.globalDisableFlag?.() || props.globalDisableOfflineFlag?.();
const globalOfflineDisabled = props.globalDisableOfflineFlag?.() ?? false;
return renderToggleBadge({
isEnabled: !globalOfflineDisabled && !resource.disableConnectivity,
disabled: disabledGlobally,
onToggle: () => {
if (disabledGlobally) return;
props.onToggleNodeConnectivity?.(resource.id);
},
titleEnabled: 'Offline alerts enabled. Click to disable for this resource.',
titleDisabled: 'Offline alerts disabled. Click to enable for this resource.',
titleWhenDisabled: 'Offline alerts controlled globally',
});
})()}
</Show>
</td>
</Show>
{/* Actions column */}
<td class="p-1 px-2">
@@ -900,15 +909,6 @@ export function ResourceTable(props: ResourceTableProps) {
>
{resource.name}
</span>
<Show when={'vmid' in resource && resource.vmid}>
<span class="text-xs text-gray-500">({resource.vmid})</span>
</Show>
<Show when={resource.type === 'storage' && 'node' in resource && resource.node}>
<span class="text-xs text-gray-500">on {resource.node}</span>
</Show>
<Show when={resource.type === 'guest' && 'node' in resource && resource.node}>
<span class="text-xs text-gray-500">on {resource.node}</span>
</Show>
<Show when={resource.hasOverride || resource.disableConnectivity}>
<span class="text-xs px-1.5 py-0.5 bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 rounded">
Custom
@@ -921,7 +921,9 @@ export function ResourceTable(props: ResourceTableProps) {
<span
class={`text-sm font-medium ${resource.disabled ? 'text-gray-500 dark:text-gray-500' : 'text-gray-900 dark:text-gray-100'}`}
>
{resource.displayName || resource.name}
{resource.type === 'node'
? resource.name
: resource.displayName || resource.name}
</span>
}>
{(host) => (
@@ -937,13 +939,12 @@ export function ResourceTable(props: ResourceTableProps) {
}`}
title={`Open ${resource.displayName || resource.name} web interface`}
>
{resource.displayName || resource.name}
{resource.type === 'node'
? resource.name
: resource.displayName || resource.name}
</a>
)}
</Show>
<Show when={resource.rawName && resource.rawName !== resource.displayName}>
<span class="text-xs text-gray-500 dark:text-gray-400">({resource.rawName})</span>
</Show>
<Show when={resource.clusterName}>
<span class="rounded px-2 py-0.5 text-[10px] font-medium whitespace-nowrap bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300">
{resource.clusterName}
@@ -957,27 +958,6 @@ export function ResourceTable(props: ResourceTableProps) {
</div>
</Show>
</td>
<td class="p-1 px-2">
<span
class={`inline-block px-1.5 py-0.5 text-xs font-medium rounded ${
resource.type === 'pbs'
? 'bg-indigo-100 dark:bg-indigo-900/50 text-indigo-700 dark:text-indigo-300'
: resource.type === 'node'
? 'bg-purple-100 dark:bg-purple-900/50 text-purple-700 dark:text-purple-300'
: resource.type === 'storage'
? 'bg-orange-100 dark:bg-orange-900/50 text-orange-700 dark:text-orange-300'
: resource.type === 'dockerHost'
? 'bg-cyan-100 dark:bg-cyan-900/50 text-cyan-700 dark:text-cyan-300'
: resource.type === 'dockerContainer'
? 'bg-teal-100 dark:bg-teal-900/50 text-teal-700 dark:text-teal-300'
: resource.resourceType === 'VM'
? 'bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300'
: 'bg-green-100 dark:bg-green-900/50 text-green-700 dark:text-green-300'
}`}
>
{resource.resourceType}
</span>
</td>
{/* Metric columns - dynamically rendered based on resource type */}
<For each={props.columns}>
{(column) => {
@@ -1116,35 +1096,43 @@ export function ResourceTable(props: ResourceTableProps) {
<Show when={props.showOfflineAlertsColumn}>
<td class="p-1 px-2 text-center align-middle">
<Show when={props.onToggleNodeConnectivity}>
<div class="flex items-center justify-center">
<TogglePrimitive
size="sm"
checked={!resource.disableConnectivity}
disabled={props.globalDisableFlag?.() || props.globalDisableOfflineFlag?.()}
onToggle={() =>
!props.globalDisableFlag?.() &&
!props.globalDisableOfflineFlag?.() &&
props.onToggleNodeConnectivity?.(resource.id)
}
checkedClass="bg-emerald-500/80 border-emerald-600/70 dark:bg-emerald-500/60 dark:border-emerald-500/70"
uncheckedClass="bg-rose-500/80 border-rose-600/70 dark:bg-rose-500/60 dark:border-rose-500/70"
disabledClass="bg-slate-400/60 border-slate-500/70 dark:bg-slate-600/60 dark:border-slate-600/70 cursor-not-allowed opacity-60"
class="my-[1px]"
title={
props.globalDisableFlag?.()
? 'Alerts disabled globally'
: props.globalDisableOfflineFlag?.()
? 'Offline alerts disabled globally'
: resource.disableConnectivity
? 'Offline alerts disabled for this resource'
: 'Offline alerts enabled'
}
ariaLabel={resource.disableConnectivity ? 'Offline alerts disabled for this resource' : 'Offline alerts enabled for this resource'}
/>
</div>
{(() => {
const disabledGlobally = props.globalDisableFlag?.() || props.globalDisableOfflineFlag?.();
const isEnabled = !(resource.disableConnectivity || props.globalDisableOfflineFlag?.());
const label = isEnabled ? 'Enabled' : 'Disabled';
const baseClasses =
'inline-flex items-center justify-center px-2 py-0.5 text-xs font-semibold rounded-full border transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-blue-400';
const stateClasses = isEnabled
? 'bg-emerald-500/10 text-emerald-700 border-emerald-500/50 hover:bg-emerald-500/15 dark:bg-emerald-500/20 dark:text-emerald-200 dark:border-emerald-400/40'
: 'bg-rose-500/10 text-rose-700 border-rose-500/50 hover:bg-rose-500/15 dark:bg-rose-500/20 dark:text-rose-200 dark:border-rose-400/40';
const disabledClasses = disabledGlobally
? 'opacity-60 cursor-not-allowed hover:bg-transparent dark:hover:bg-transparent'
: '';
return (
<button
type="button"
class={`${baseClasses} ${stateClasses} ${disabledClasses}`.trim()}
onClick={() => {
if (disabledGlobally) return;
props.onToggleNodeConnectivity?.(resource.id);
}}
disabled={disabledGlobally}
aria-pressed={isEnabled}
title={
disabledGlobally
? 'Offline alerts controlled globally'
: isEnabled
? 'Offline alerts enabled. Click to disable for this resource.'
: 'Offline alerts disabled. Click to enable for this resource.'
}
>
{label}
</button>
);
})()}
</Show>
</td>
</Show>
</td>
</Show>
{/* Actions column */}
<td class="p-1 px-2">
@@ -1241,7 +1229,7 @@ export function ResourceTable(props: ResourceTableProps) {
>
<tr>
<td
colspan={props.columns.length + (props.showOfflineAlertsColumn ? 5 : 4)}
colspan={props.columns.length + (props.showOfflineAlertsColumn ? 4 : 3)}
class="px-4 py-8 text-center text-sm text-gray-500 dark:text-gray-400"
>
No {props.title.toLowerCase()} found
@@ -1252,7 +1240,7 @@ export function ResourceTable(props: ResourceTableProps) {
<Show when={!hasRows()}>
<tr>
<td
colspan={props.columns.length + (props.showOfflineAlertsColumn ? 5 : 4)}
colspan={props.columns.length + (props.showOfflineAlertsColumn ? 4 : 3)}
class="px-4 py-6 text-sm text-center text-gray-500 dark:text-gray-400"
>
{props.emptyMessage || 'No resources available.'}
@@ -208,6 +208,76 @@ export function ThresholdsTable(props: ThresholdsTableProps) {
};
// Process nodes with their overrides
const getFriendlyNodeName = (value: string, clusterName?: string): string => {
if (!value) return value;
const clusterLower = clusterName?.toLowerCase().trim();
const normalizeToken = (token?: string | null): string => {
if (!token) return '';
let result = token.replace(/\(.*?\)/g, ' ').replace(/\s+/g, ' ').trim();
if (clusterLower) {
result = result
.split(' ')
.filter((part) => part.toLowerCase() !== clusterLower)
.join(' ')
.trim();
}
if (!result) return '';
const firstWord = result.split(/\s+/)[0] || result;
const withoutDomain = firstWord.includes('.') ? firstWord.split('.')[0] ?? firstWord : firstWord;
return withoutDomain.trim();
};
const parentheticalMatch = value.match(/\(([^)]+)\)/);
const parentheticalRaw = parentheticalMatch?.[1]?.trim();
let base = normalizeToken(value);
if (!base) {
base = value.trim();
}
const parenthetical = normalizeToken(parentheticalRaw);
if (parenthetical && parenthetical.toLowerCase() !== base.toLowerCase()) {
return parenthetical;
}
return base;
};
const buildNodeHeaderMeta = (node: Node) => {
const originalDisplayName = node.displayName?.trim() || node.name;
const friendlyName = getFriendlyNodeName(originalDisplayName, node.clusterName);
const hostValue = node.host?.trim();
let host: string | undefined;
if (hostValue && hostValue !== '') {
host = hostValue.startsWith('http')
? hostValue
: `https://${hostValue.includes(':') ? hostValue : `${hostValue}:8006`}`;
} else if (node.name) {
host = `https://${node.name.includes(':') ? node.name : `${node.name}:8006`}`;
}
const headerMeta: GroupHeaderMeta = {
type: 'node',
displayName: friendlyName,
rawName: originalDisplayName,
host,
status: node.status,
clusterName: node.isClusterMember ? node.clusterName?.trim() || 'Cluster' : undefined,
isClusterMember: node.isClusterMember ?? false,
};
const keys = new Set<string>();
[node.name, originalDisplayName, friendlyName].forEach((value) => {
if (value && value.trim()) {
keys.add(value.trim());
}
});
return { headerMeta, keys };
};
const nodesWithOverrides = createMemo<Resource[]>((prev = []) => {
// If we're currently editing, return the previous value to avoid re-renders
if (editingId()) {
@@ -231,8 +301,10 @@ export function ThresholdsTable(props: ThresholdsTableProps) {
);
});
const displayName = node.displayName?.trim() || node.name;
const originalDisplayName = node.displayName?.trim() || node.name;
const friendlyName = getFriendlyNodeName(originalDisplayName, node.clusterName);
const rawName = node.name;
const sanitizedName = friendlyName || originalDisplayName || rawName.split('.')[0] || rawName;
// Build a best-effort management URL for the node
const hostValue = node.host?.trim() || rawName;
const normalizedHost = hostValue.startsWith('http://') || hostValue.startsWith('https://')
@@ -241,9 +313,9 @@ export function ThresholdsTable(props: ThresholdsTableProps) {
return {
id: node.id,
name: displayName,
displayName,
rawName,
name: sanitizedName,
displayName: sanitizedName,
rawName: originalDisplayName,
host: normalizedHost,
type: 'node' as const,
resourceType: 'Node',
@@ -550,36 +622,12 @@ const dockerContainersGroupedByHost = createMemo<Record<string, Resource[]>>((pr
const guestGroupHeaderMeta = createMemo<Record<string, GroupHeaderMeta>>(() => {
const meta: Record<string, GroupHeaderMeta> = {};
const nodeLookup = new Map((props.nodes ?? []).map((node) => [node.name, node]));
Object.keys(guestsGroupedByNode() ?? {}).forEach((nodeName) => {
const node = nodeLookup.get(nodeName);
if (!node) {
return;
}
const displayName = node.displayName?.trim() || node.name;
const hostValue = node.host?.trim();
let host: string | undefined;
if (hostValue && hostValue !== '') {
host = hostValue.startsWith('http')
? hostValue
: `https://${hostValue.includes(':') ? hostValue : `${hostValue}:8006`}`;
} else if (node.name) {
host = `https://${node.name.includes(':') ? node.name : `${node.name}:8006`}`;
}
meta[nodeName] = {
type: 'node',
displayName,
rawName: node.name,
host,
status: node.status,
clusterName: node.isClusterMember ? node.clusterName?.trim() || 'Cluster' : undefined,
isClusterMember: node.isClusterMember ?? false,
};
(props.nodes ?? []).forEach((node) => {
const { headerMeta, keys } = buildNodeHeaderMeta(node);
keys.forEach((key) => {
meta[key] = headerMeta;
});
});
return meta;
});
@@ -690,6 +738,23 @@ const dockerContainersGroupedByHost = createMemo<Record<string, Resource[]>>((pr
return storageDevices;
}, []);
const storageGroupedByNode = createMemo<Record<string, Resource[]>>(() => {
const grouped: Record<string, Resource[]> = {};
storageWithOverrides().forEach((storage) => {
const key = storage.node?.trim() || 'Unassigned';
if (!grouped[key]) {
grouped[key] = [];
}
grouped[key].push(storage);
});
Object.values(grouped).forEach((resources) => {
resources.sort((a, b) => a.name.localeCompare(b.name));
});
return grouped;
});
const summaryItems = createMemo(() => {
try {
const items = [
@@ -1175,11 +1240,44 @@ const dockerContainersGroupedByHost = createMemo<Record<string, Resource[]>>((pr
</div>
</Show>
<Show when={hasSection('guests')}>
<div ref={registerSection('guests')} class="scroll-mt-24">
<ResourceTable
title="VMs & Containers"
groupedResources={guestsGroupedByNode()}
groupHeaderMeta={guestGroupHeaderMeta()}
columns={['CPU %', 'Memory %', 'Disk %', 'Disk R MB/s', 'Disk W MB/s', 'Net In MB/s', 'Net Out MB/s']}
activeAlerts={props.activeAlerts}
emptyMessage="No VMs or containers match the current filters."
onEdit={startEditing}
onSaveEdit={saveEdit}
onCancelEdit={cancelEdit}
onRemoveOverride={removeOverride}
onToggleDisabled={toggleDisabled}
onToggleNodeConnectivity={toggleNodeConnectivity}
showOfflineAlertsColumn={true}
editingId={editingId}
editingThresholds={editingThresholds}
setEditingThresholds={setEditingThresholds}
formatMetricValue={formatMetricValue}
hasActiveAlert={hasActiveAlert}
globalDefaults={props.guestDefaults}
setGlobalDefaults={props.setGuestDefaults}
setHasUnsavedChanges={props.setHasUnsavedChanges}
globalDisableFlag={props.disableAllGuests}
onToggleGlobalDisable={() => props.setDisableAllGuests(!props.disableAllGuests())}
globalDisableOfflineFlag={props.disableAllGuestsOffline}
onToggleGlobalDisableOffline={() => props.setDisableAllGuestsOffline(!props.disableAllGuestsOffline())}
/>
</div>
</Show>
<Show when={hasSection('storage')}>
<div ref={registerSection('storage')} class="scroll-mt-24">
<ResourceTable
title="Storage Devices"
resources={storageWithOverrides()}
groupedResources={storageGroupedByNode()}
groupHeaderMeta={guestGroupHeaderMeta()}
columns={['Usage %']}
activeAlerts={props.activeAlerts}
emptyMessage="No storage devices match the current filters."
@@ -1247,38 +1345,6 @@ const dockerContainersGroupedByHost = createMemo<Record<string, Resource[]>>((pr
/>
</div>
</Show>
<Show when={hasSection('guests')}>
<div ref={registerSection('guests')} class="scroll-mt-24">
<ResourceTable
title="VMs & Containers"
groupedResources={guestsGroupedByNode()}
groupHeaderMeta={guestGroupHeaderMeta()}
columns={['CPU %', 'Memory %', 'Disk %', 'Disk R MB/s', 'Disk W MB/s', 'Net In MB/s', 'Net Out MB/s']}
activeAlerts={props.activeAlerts}
emptyMessage="No VMs or containers match the current filters."
onEdit={startEditing}
onSaveEdit={saveEdit}
onCancelEdit={cancelEdit}
onRemoveOverride={removeOverride}
onToggleDisabled={toggleDisabled}
onToggleNodeConnectivity={toggleNodeConnectivity}
showOfflineAlertsColumn={true}
editingId={editingId}
editingThresholds={editingThresholds}
setEditingThresholds={setEditingThresholds}
formatMetricValue={formatMetricValue}
hasActiveAlert={hasActiveAlert}
globalDefaults={props.guestDefaults}
setGlobalDefaults={props.setGuestDefaults}
setHasUnsavedChanges={props.setHasUnsavedChanges}
globalDisableFlag={props.disableAllGuests}
onToggleGlobalDisable={() => props.setDisableAllGuests(!props.disableAllGuests())}
globalDisableOfflineFlag={props.disableAllGuestsOffline}
onToggleGlobalDisableOffline={() => props.setDisableAllGuestsOffline(!props.disableAllGuestsOffline())}
/>
</div>
</Show>
</Show>
<Show when={activeTab() === 'docker'}>