mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-22 11:13:26 +00:00
Refine alerts thresholds layout
This commit is contained in:
@@ -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'}>
|
||||
|
||||
Reference in New Issue
Block a user