diff --git a/frontend-modern/src/components/Alerts/ResourceTable.tsx b/frontend-modern/src/components/Alerts/ResourceTable.tsx index c7a3fb6e2..a5cfaf33a 100644 --- a/frontend-modern/src/components/Alerts/ResourceTable.tsx +++ b/frontend-modern/src/components/Alerts/ResourceTable.tsx @@ -84,6 +84,110 @@ export function ResourceTable(props: ResourceTableProps) { const [activeMetricInput, setActiveMetricInput] = createSignal<{ resourceId: string; metric: string } | null>(null); + const normalizeMetricKey = (column: string): string => { + const key = column.trim().toLowerCase(); + const mapped = ( + new Map([ + ['cpu %', 'cpu'], + ['memory %', 'memory'], + ['disk %', 'disk'], + ['disk r mb/s', 'diskRead'], + ['disk w mb/s', 'diskWrite'], + ['net in mb/s', 'networkIn'], + ['net out mb/s', 'networkOut'], + ['usage %', 'usage'], + ['temp °c', 'temperature'], + ['temperature °c', 'temperature'], + ['temperature', 'temperature'], + ['restart count', 'restartCount'], + ['restart window', 'restartWindow'], + ['restart window (s)', 'restartWindow'], + ['memory warn %', 'memoryWarnPct'], + ['memory critical %', 'memoryCriticalPct'], + ]) + ).get(key); + if (mapped) { + return mapped; + } + + return key + .replace(' %', '') + .replace(' °c', '') + .replace(' mb/s', '') + .replace('disk r', 'diskRead') + .replace('disk w', 'diskWrite') + .replace('net in', 'networkIn') + .replace('net out', 'networkOut'); + }; + + const metricBounds = (metric: string): { min: number; max: number } => { + if (metric === 'temperature') { + return { min: -1, max: 150 }; + } + if (['diskRead', 'diskWrite', 'networkIn', 'networkOut'].includes(metric)) { + return { min: -1, max: 10000 }; + } + if (['cpu', 'memory', 'disk', 'usage', 'memoryWarnPct', 'memoryCriticalPct'].includes(metric)) { + return { min: -1, max: 100 }; + } + if (metric === 'restartCount') { + return { min: -1, max: 50 }; + } + if (metric === 'restartWindow') { + return { min: -1, max: 86400 }; + } + return { min: -1, max: 10000 }; + }; + + const getEnabledDefaultValue = (metric: string): number => { + if (['diskRead', 'diskWrite', 'networkIn', 'networkOut'].includes(metric)) { + return 100; + } + if (metric === 'temperature') { + return 80; + } + if (metric === 'restartCount') { + return 3; + } + if (metric === 'restartWindow') { + return 300; + } + if (metric === 'memoryWarnPct') { + return 90; + } + if (metric === 'memoryCriticalPct') { + return 95; + } + return 80; + }; + + const resourceSupportsMetric = (resourceType: string | undefined, metric: string): boolean => { + if (!resourceType) return true; + if ( + resourceType === 'node' && + ['diskRead', 'diskWrite', 'networkIn', 'networkOut'].includes(metric) + ) { + return false; + } + if (resourceType === 'pbs') { + return ['cpu', 'memory'].includes(metric); + } + if (resourceType === 'storage') { + return metric === 'usage'; + } + if (resourceType === 'dockerContainer') { + return [ + 'cpu', + 'memory', + 'restartCount', + 'restartWindow', + 'memoryWarnPct', + 'memoryCriticalPct', + ].includes(metric); + } + return true; + }; + const renderGroupHeader = (groupKey: string, meta?: GroupHeaderMeta) => { if (!meta || meta.type !== 'node') { return {groupKey}; @@ -271,42 +375,8 @@ export function ResourceTable(props: ResourceTableProps) { {(column) => { - const normalizedColumn = column.trim().toLowerCase(); - const metric = ( - { - 'cpu %': 'cpu', - 'memory %': 'memory', - 'disk %': 'disk', - 'disk r mb/s': 'diskRead', - 'disk w mb/s': 'diskWrite', - 'net in mb/s': 'networkIn', - 'net out mb/s': 'networkOut', - 'usage %': 'usage', - 'temp °c': 'temperature', - 'temperature °c': 'temperature', - temperature: 'temperature', - } as Record - )[normalizedColumn] - ?? normalizedColumn - .replace(' %', '') - .replace(' °c', '') - .replace(' mb/s', '') - .replace('disk r', 'diskRead') - .replace('disk w', 'diskWrite') - .replace('net in', 'networkIn') - .replace('net out', 'networkOut'); - - // Get default value when enabling a disabled metric - const getEnabledDefault = (m: string): number => { - if (m.includes('Read') || m.includes('Write') || m.includes('In') || m.includes('Out')) { - return 100; // 100 MB/s for I/O metrics - } - if (m === 'temperature') { - return 80; // 80°C for temperature - } - return 80; // 80% for percentage metrics - }; - + const metric = normalizeMetricKey(column); + const bounds = metricBounds(metric); const val = () => props.globalDefaults?.[metric] ?? 0; const isOff = () => val() === -1; @@ -315,17 +385,8 @@ export function ResourceTable(props: ResourceTableProps) {
{ if (!props.setGlobalDefaults) return; - const enabledValue = getEnabledDefault(metric); props.setGlobalDefaults((prev) => ({ ...prev, - [metric]: enabledValue, + [metric]: getEnabledDefaultValue(metric), })); props.setHasUnsavedChanges?.(true); }} @@ -549,53 +609,11 @@ export function ResourceTable(props: ResourceTableProps) { {/* Metric columns - dynamically rendered based on resource type */} - + {(column) => { - const normalizedColumn = column.trim().toLowerCase(); - const metric = ( - { - 'cpu %': 'cpu', - 'memory %': 'memory', - 'disk %': 'disk', - 'disk r mb/s': 'diskRead', - 'disk w mb/s': 'diskWrite', - 'net in mb/s': 'networkIn', - 'net out mb/s': 'networkOut', - 'usage %': 'usage', - 'temp °c': 'temperature', - 'temperature °c': 'temperature', - temperature: 'temperature', - } as Record - )[normalizedColumn] - ?? normalizedColumn - .replace(' %', '') - .replace(' °c', '') - .replace(' mb/s', '') - .replace('disk r', 'diskRead') - .replace('disk w', 'diskWrite') - .replace('net in', 'networkIn') - .replace('net out', 'networkOut'); - - // Check if this metric applies to this resource type - const showMetric = () => { - if ( - resource.type === 'node' && - ['diskRead', 'diskWrite', 'networkIn', 'networkOut'].includes( - metric, - ) - ) { - return false; - } - if (resource.type === 'pbs') { - // PBS only has CPU and Memory metrics - return ['cpu', 'memory'].includes(metric); - } - if (resource.type === 'storage') { - return metric === 'usage'; - } - return true; - }; - + const metric = normalizeMetricKey(column); + const showMetric = () => resourceSupportsMetric(resource.type, metric); + const bounds = metricBounds(metric); const isDisabled = () => thresholds()?.[metric] === -1; const openMetricEditor = (e: MouseEvent) => { @@ -636,19 +654,10 @@ export function ResourceTable(props: ResourceTableProps) { } >
- { diff --git a/frontend-modern/src/components/Alerts/ThresholdsTable.tsx b/frontend-modern/src/components/Alerts/ThresholdsTable.tsx index 9fc35c435..84a8c201f 100644 --- a/frontend-modern/src/components/Alerts/ThresholdsTable.tsx +++ b/frontend-modern/src/components/Alerts/ThresholdsTable.tsx @@ -178,7 +178,14 @@ export function ThresholdsTable(props: ThresholdsTableProps) { if (value <= 0) return 'Off'; // Percentage-based metrics - if (metric === 'cpu' || metric === 'memory' || metric === 'disk' || metric === 'usage') { + if ( + metric === 'cpu' || + metric === 'memory' || + metric === 'disk' || + metric === 'usage' || + metric === 'memoryWarnPct' || + metric === 'memoryCriticalPct' + ) { return `${value}%`; } @@ -187,6 +194,14 @@ export function ThresholdsTable(props: ThresholdsTableProps) { return `${value}°C`; } + if (metric === 'restartWindow') { + return `${value}s`; + } + + if (metric === 'restartCount') { + return String(value); + } + // MB/s metrics if ( metric === 'diskRead' || @@ -438,13 +453,14 @@ const dockerContainersGroupedByHost = createMemo>((pr const resourceId = `docker:${host.id}/${containerId}`; const override = overridesMap.get(resourceId); + const defaults = props.dockerDefaults as Record; const hasCustomThresholds = override?.thresholds && Object.keys(override.thresholds).some((key) => { const k = key as keyof typeof override.thresholds; return ( override.thresholds[k] !== undefined && - override.thresholds[k] !== (props.guestDefaults as any)[k] + override.thresholds[k] !== defaults?.[k as keyof typeof defaults] ); }); @@ -480,7 +496,7 @@ const dockerContainersGroupedByHost = createMemo>((pr disabled: override?.disabled || false, disableConnectivity: override?.disableConnectivity || false, thresholds: override?.thresholds || {}, - defaults: props.guestDefaults, + defaults: props.dockerDefaults, hostId: host.id, image: container.image, }; @@ -512,7 +528,7 @@ const dockerContainersGroupedByHost = createMemo>((pr disabled: override.disabled || false, disableConnectivity: override.disableConnectivity || false, thresholds: override.thresholds || {}, - defaults: props.guestDefaults, + defaults: props.dockerDefaults, }); }); @@ -1384,108 +1400,6 @@ const dockerContainersGroupedByHost = createMemo>((pr - {/* Docker Global Settings */} - -
- -
-
-
-
- - { - const value = parseInt(e.currentTarget.value, 10); - props.setDockerDefaults((prev) => ({ ...prev, restartCount: Number.isNaN(value) ? 3 : value })); - props.setHasUnsavedChanges(true); - }} - class="w-16 px-2 py-1 text-xs text-center border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700" - /> - restarts to trigger alert -
-
- - { - const value = parseInt(e.currentTarget.value, 10); - props.setDockerDefaults((prev) => ({ ...prev, restartWindow: Number.isNaN(value) ? 300 : value })); - props.setHasUnsavedChanges(true); - }} - class="w-16 px-2 py-1 text-xs text-center border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700" - /> - seconds -
-
- - { - const value = parseInt(e.currentTarget.value, 10); - props.setDockerDefaults((prev) => ({ ...prev, memoryWarnPct: Number.isNaN(value) ? 90 : value })); - props.setHasUnsavedChanges(true); - }} - class="w-16 px-2 py-1 text-xs text-center border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700" - /> - % of container limit -
-
- - { - const value = parseInt(e.currentTarget.value, 10); - props.setDockerDefaults((prev) => ({ ...prev, memoryCriticalPct: Number.isNaN(value) ? 95 : value })); - props.setHasUnsavedChanges(true); - }} - class="w-16 px-2 py-1 text-xs text-center border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700" - /> - % of container limit -
-
- - Always Enabled - (monitor container health status) -
-
- - Always Enabled - (alert on exit code 137) -
-
-
-
-
>((pr setEditingThresholds={setEditingThresholds} formatMetricValue={formatMetricValue} hasActiveAlert={hasActiveAlert} - globalDefaults={{ cpu: props.dockerDefaults.cpu, memory: props.dockerDefaults.memory }} + globalDefaults={{ + cpu: props.dockerDefaults.cpu, + memory: props.dockerDefaults.memory, + restartCount: props.dockerDefaults.restartCount, + restartWindow: props.dockerDefaults.restartWindow, + memoryWarnPct: props.dockerDefaults.memoryWarnPct, + memoryCriticalPct: props.dockerDefaults.memoryCriticalPct, + }} setGlobalDefaults={(value) => { - if (typeof value === 'function') { - const newValue = value({ cpu: props.dockerDefaults.cpu, memory: props.dockerDefaults.memory }); - props.setDockerDefaults((prev) => ({ ...prev, cpu: newValue.cpu ?? prev.cpu, memory: newValue.memory ?? prev.memory })); - } else { - props.setDockerDefaults((prev) => ({ ...prev, cpu: value.cpu ?? prev.cpu, memory: value.memory ?? prev.memory })); - } + const current = { + cpu: props.dockerDefaults.cpu, + memory: props.dockerDefaults.memory, + restartCount: props.dockerDefaults.restartCount, + restartWindow: props.dockerDefaults.restartWindow, + memoryWarnPct: props.dockerDefaults.memoryWarnPct, + memoryCriticalPct: props.dockerDefaults.memoryCriticalPct, + }; + const next = + typeof value === 'function' + ? value(current) + : { ...current, ...value }; + + props.setDockerDefaults((prev) => ({ + ...prev, + cpu: next.cpu ?? prev.cpu, + memory: next.memory ?? prev.memory, + restartCount: next.restartCount ?? prev.restartCount, + restartWindow: next.restartWindow ?? prev.restartWindow, + memoryWarnPct: next.memoryWarnPct ?? prev.memoryWarnPct, + memoryCriticalPct: next.memoryCriticalPct ?? prev.memoryCriticalPct, + })); }} setHasUnsavedChanges={props.setHasUnsavedChanges} globalDisableFlag={props.disableAllDockerContainers}