mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-22 19:23:31 +00:00
Unify Docker thresholds with platform tables
This commit is contained in:
@@ -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<string, string>([
|
||||
['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 <span class="text-xs font-medium text-gray-600 dark:text-gray-400">{groupKey}</span>;
|
||||
@@ -271,42 +375,8 @@ export function ResourceTable(props: ResourceTableProps) {
|
||||
</td>
|
||||
<For each={props.columns}>
|
||||
{(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<string, string>
|
||||
)[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) {
|
||||
<div class="relative flex justify-center">
|
||||
<input
|
||||
type="number"
|
||||
min="-1"
|
||||
max={
|
||||
metric === 'temperature'
|
||||
? 150
|
||||
: metric.includes('Read') ||
|
||||
metric.includes('Write') ||
|
||||
metric.includes('In') ||
|
||||
metric.includes('Out')
|
||||
? 10000
|
||||
: 100
|
||||
}
|
||||
min={bounds.min}
|
||||
max={bounds.max}
|
||||
value={isOff() ? '' : val()}
|
||||
placeholder={isOff() ? 'Off' : ''}
|
||||
disabled={isOff()}
|
||||
@@ -354,10 +415,9 @@ export function ResourceTable(props: ResourceTableProps) {
|
||||
class="absolute inset-0 w-full rounded cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-400"
|
||||
onClick={() => {
|
||||
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) {
|
||||
</Show>
|
||||
</td>
|
||||
{/* Metric columns - dynamically rendered based on resource type */}
|
||||
<For each={props.columns}>
|
||||
<For each={props.columns}>
|
||||
{(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<string, string>
|
||||
)[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) {
|
||||
}
|
||||
>
|
||||
<div class="flex items-center justify-center">
|
||||
<input
|
||||
type="number"
|
||||
min="-1"
|
||||
max={
|
||||
metric === 'temperature'
|
||||
? 200
|
||||
: metric.includes('disk') ||
|
||||
metric.includes('memory') ||
|
||||
metric.includes('cpu') ||
|
||||
metric === 'usage'
|
||||
? 100
|
||||
: 10000
|
||||
}
|
||||
<input
|
||||
type="number"
|
||||
min={bounds.min}
|
||||
max={bounds.max}
|
||||
value={thresholds()?.[metric] ?? ''}
|
||||
placeholder={isDisabled() ? 'Off' : ''}
|
||||
ref={(el) => {
|
||||
|
||||
@@ -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<Record<string, Resource[]>>((pr
|
||||
const resourceId = `docker:${host.id}/${containerId}`;
|
||||
const override = overridesMap.get(resourceId);
|
||||
|
||||
const defaults = props.dockerDefaults as Record<string, number | undefined>;
|
||||
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<Record<string, Resource[]>>((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<Record<string, Resource[]>>((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<Record<string, Resource[]>>((pr
|
||||
</Show>
|
||||
|
||||
<Show when={activeTab() === 'docker'}>
|
||||
{/* Docker Global Settings */}
|
||||
<Card padding="none">
|
||||
<div class="p-4 pb-2">
|
||||
<SectionHeader
|
||||
title="Docker global settings"
|
||||
description="Container behavior and policy thresholds"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
<div class="p-4 pt-2">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<label class="text-xs font-medium text-gray-700 dark:text-gray-200 min-w-[120px]">
|
||||
Restart Count:
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="20"
|
||||
value={props.dockerDefaults.restartCount}
|
||||
onInput={(e) => {
|
||||
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"
|
||||
/>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">restarts to trigger alert</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<label class="text-xs font-medium text-gray-700 dark:text-gray-200 min-w-[120px]">
|
||||
Restart Window:
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="60"
|
||||
max="1800"
|
||||
step="60"
|
||||
value={props.dockerDefaults.restartWindow}
|
||||
onInput={(e) => {
|
||||
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"
|
||||
/>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">seconds</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<label class="text-xs font-medium text-gray-700 dark:text-gray-200 min-w-[120px]">
|
||||
Memory Limit Warn:
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="100"
|
||||
value={props.dockerDefaults.memoryWarnPct}
|
||||
onInput={(e) => {
|
||||
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"
|
||||
/>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">% of container limit</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<label class="text-xs font-medium text-gray-700 dark:text-gray-200 min-w-[120px]">
|
||||
Memory Limit Critical:
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="100"
|
||||
value={props.dockerDefaults.memoryCriticalPct}
|
||||
onInput={(e) => {
|
||||
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"
|
||||
/>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">% of container limit</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<label class="text-xs font-medium text-gray-700 dark:text-gray-200 min-w-[120px]">
|
||||
Health Checks:
|
||||
</label>
|
||||
<span class="text-xs text-green-600 dark:text-green-400 font-medium">Always Enabled</span>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">(monitor container health status)</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<label class="text-xs font-medium text-gray-700 dark:text-gray-200 min-w-[120px]">
|
||||
OOM Detection:
|
||||
</label>
|
||||
<span class="text-xs text-green-600 dark:text-green-400 font-medium">Always Enabled</span>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">(alert on exit code 137)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Show when={hasSection('dockerHosts')}>
|
||||
<div ref={registerSection('dockerHosts')} class="scroll-mt-24">
|
||||
<ResourceTable
|
||||
@@ -1534,14 +1448,37 @@ const dockerContainersGroupedByHost = createMemo<Record<string, Resource[]>>((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}
|
||||
|
||||
Reference in New Issue
Block a user