feat: add storage device overrides to threshold table

- Added Storage column to threshold table for individual storage thresholds
- Storage devices now appear in their own "Storage" group
- Each storage device can have its own usage threshold override
- Storage shows "-" for CPU/Memory/Disk/IO metrics (not applicable)
- Added orange badge styling for storage device type
- Updated Override interface to support storage type and usage threshold
- Fixed colspan values for new 13-column layout

This allows users to set different thresholds for different storage pools
(e.g. tighter monitoring for system storage vs bulk storage)
This commit is contained in:
Pulse Monitor
2025-08-21 11:01:10 +00:00
parent f2d5231c25
commit 88b972f8fd
2 changed files with 150 additions and 69 deletions
@@ -1,10 +1,10 @@
import { createSignal, createMemo, For, Show, onMount, onCleanup } from 'solid-js';
import type { VM, Container, Node, Alert } from '@/types/api';
import type { VM, Container, Node, Alert, Storage } from '@/types/api';
interface Override {
id: string;
name: string;
type: 'guest' | 'node';
type: 'guest' | 'node' | 'storage';
resourceType?: string;
vmid?: number;
node?: string;
@@ -19,6 +19,7 @@ interface Override {
diskWrite?: number;
networkIn?: number;
networkOut?: number;
usage?: number; // For storage devices
};
}
@@ -41,6 +42,7 @@ interface ThresholdsTableProps {
setRawOverridesConfig: (config: Record<string, any>) => void;
allGuests: () => (VM | Container)[];
nodes: Node[];
storage: Storage[];
guestDefaults: SimpleThresholds;
setGuestDefaults: (value: Record<string, number> | ((prev: Record<string, number>) => Record<string, number>)) => void;
nodeDefaults: SimpleThresholds;
@@ -105,7 +107,7 @@ export function ThresholdsTable(props: ThresholdsTableProps) {
if (value === undefined || value === null) return '0';
// Percentage-based metrics
if (metric === 'cpu' || metric === 'memory' || metric === 'disk') {
if (metric === 'cpu' || metric === 'memory' || metric === 'disk' || metric === 'usage') {
return `${value}%`;
}
@@ -208,8 +210,31 @@ export function ThresholdsTable(props: ThresholdsTableProps) {
};
});
// Process storage devices
const storageDevices = props.storage.map(storage => {
const override = overridesMap.get(storage.id);
// Storage only has usage threshold
const hasCustomThresholds = override?.thresholds?.usage !== undefined &&
override.thresholds.usage !== props.storageDefault();
return {
id: storage.id,
name: storage.name,
type: 'storage' as const,
resourceType: 'Storage',
node: storage.node,
instance: storage.instance,
status: storage.status,
hasOverride: hasCustomThresholds || false,
disabled: override?.disabled || false,
thresholds: override?.thresholds || {},
defaults: { usage: props.storageDefault() }
};
});
// Combine and filter
const allResources = [...guests, ...nodes];
const allResources = [...guests, ...nodes, ...storageDevices];
if (search) {
return allResources.filter(r =>
@@ -227,7 +252,14 @@ export function ThresholdsTable(props: ThresholdsTableProps) {
const groups: Record<string, any[]> = {};
resourcesWithOverrides().forEach(resource => {
const groupKey = resource.type === 'node' ? 'Nodes' : ('node' in resource ? resource.node : 'Unknown');
let groupKey: string;
if (resource.type === 'node') {
groupKey = 'Nodes';
} else if (resource.type === 'storage') {
groupKey = 'Storage';
} else {
groupKey = 'node' in resource ? resource.node : 'Unknown';
}
if (!groups[groupKey]) {
groups[groupKey] = [];
@@ -293,7 +325,7 @@ export function ThresholdsTable(props: ThresholdsTableProps) {
const override: Override = {
id: resourceId,
name: resource.name,
type: resource.type,
type: resource.type as 'guest' | 'node' | 'storage',
resourceType: resource.resourceType,
vmid: 'vmid' in resource ? resource.vmid : undefined,
node: 'node' in resource ? resource.node : undefined,
@@ -841,6 +873,9 @@ export function ThresholdsTable(props: ThresholdsTableProps) {
<th class="px-3 py-2 text-center text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Disk %
</th>
<th class="px-3 py-2 text-center text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Storage %
</th>
<th class="px-3 py-2 text-center text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Disk R<br/><span class="text-[10px] font-normal">MB/s</span>
</th>
@@ -872,7 +907,7 @@ export function ThresholdsTable(props: ThresholdsTableProps) {
<>
{/* Group header */}
<tr class="bg-gray-50 dark:bg-gray-700/50">
<td colspan="12" class="px-3 py-1 text-xs font-medium text-gray-500 dark:text-gray-400">
<td colspan="13" class="px-3 py-1 text-xs font-medium text-gray-500 dark:text-gray-400">
{groupName}
</td>
</tr>
@@ -893,6 +928,10 @@ export function ThresholdsTable(props: ThresholdsTableProps) {
metric === 'networkIn' || metric === 'networkOut')) {
return false;
}
// Storage only has usage metric
if (resource.type === 'storage') {
return metric === 'usage';
}
return true;
};
const isOverridden = (metric: string) => {
@@ -925,6 +964,7 @@ export function ThresholdsTable(props: ThresholdsTableProps) {
<td class="px-3 py-1.5">
<span class={`inline-block px-1.5 py-0.5 text-xs font-medium rounded ${
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.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'
}`}>
@@ -943,72 +983,111 @@ export function ThresholdsTable(props: ThresholdsTableProps) {
</span>
</td>
<td class="px-3 py-1.5 text-center">
<Show when={isEditing()} fallback={
<MetricValueWithHeat
resourceId={resource.id}
metric="cpu"
value={displayValue('cpu')}
isOverridden={isOverridden('cpu')}
/>
<Show when={resource.type === 'storage'} fallback={
<Show when={isEditing()} fallback={
<MetricValueWithHeat
resourceId={resource.id}
metric="cpu"
value={displayValue('cpu')}
isOverridden={isOverridden('cpu')}
/>
}>
<input
type="number"
min="0"
max="100"
value={thresholds().cpu || ''}
onInput={(e) => setEditingThresholds({
...editingThresholds(),
cpu: parseInt(e.currentTarget.value) || undefined
})}
class="w-14 px-1 py-0.5 text-sm text-center border border-gray-300 dark:border-gray-600 rounded
bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
/>
</Show>
}>
<input
type="number"
min="0"
max="100"
value={thresholds().cpu || ''}
onInput={(e) => setEditingThresholds({
...editingThresholds(),
cpu: parseInt(e.currentTarget.value) || undefined
})}
class="w-14 px-1 py-0.5 text-sm text-center border border-gray-300 dark:border-gray-600 rounded
bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
/>
<span class="text-sm text-gray-400 dark:text-gray-500">-</span>
</Show>
</td>
<td class="px-3 py-1.5 text-center">
<Show when={isEditing()} fallback={
<MetricValueWithHeat
resourceId={resource.id}
metric="memory"
value={displayValue('memory')}
isOverridden={isOverridden('memory')}
/>
<Show when={resource.type === 'storage'} fallback={
<Show when={isEditing()} fallback={
<MetricValueWithHeat
resourceId={resource.id}
metric="memory"
value={displayValue('memory')}
isOverridden={isOverridden('memory')}
/>
}>
<input
type="number"
min="0"
max="100"
value={thresholds().memory || ''}
onInput={(e) => setEditingThresholds({
...editingThresholds(),
memory: parseInt(e.currentTarget.value) || undefined
})}
class="w-14 px-1 py-0.5 text-sm text-center border border-gray-300 dark:border-gray-600 rounded
bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
/>
</Show>
}>
<input
type="number"
min="0"
max="100"
value={thresholds().memory || ''}
onInput={(e) => setEditingThresholds({
...editingThresholds(),
memory: parseInt(e.currentTarget.value) || undefined
})}
class="w-14 px-1 py-0.5 text-sm text-center border border-gray-300 dark:border-gray-600 rounded
bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
/>
<span class="text-sm text-gray-400 dark:text-gray-500">-</span>
</Show>
</td>
<td class="px-3 py-1.5 text-center">
<Show when={isEditing()} fallback={
<MetricValueWithHeat
resourceId={resource.id}
metric="disk"
value={displayValue('disk')}
isOverridden={isOverridden('disk')}
/>
<Show when={resource.type === 'storage'} fallback={
<Show when={isEditing()} fallback={
<MetricValueWithHeat
resourceId={resource.id}
metric="disk"
value={displayValue('disk')}
isOverridden={isOverridden('disk')}
/>
}>
<input
type="number"
min="0"
max="100"
value={thresholds().disk || ''}
onInput={(e) => setEditingThresholds({
...editingThresholds(),
disk: parseInt(e.currentTarget.value) || undefined
})}
class="w-14 px-1 py-0.5 text-sm text-center border border-gray-300 dark:border-gray-600 rounded
bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
/>
</Show>
}>
<input
type="number"
min="0"
max="100"
value={thresholds().disk || ''}
onInput={(e) => setEditingThresholds({
...editingThresholds(),
disk: parseInt(e.currentTarget.value) || undefined
})}
class="w-14 px-1 py-0.5 text-sm text-center border border-gray-300 dark:border-gray-600 rounded
bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
/>
<span class="text-sm text-gray-400 dark:text-gray-500">-</span>
</Show>
</td>
<td class="px-3 py-1.5 text-center">
<Show when={resource.type === 'storage'} fallback={
<span class="text-sm text-gray-400 dark:text-gray-500">-</span>
}>
<Show when={isEditing()} fallback={
<MetricValueWithHeat
resourceId={resource.id}
metric="usage"
value={displayValue('usage')}
isOverridden={isOverridden('usage')}
/>
}>
<input
type="number"
min="0"
max="100"
value={thresholds().usage || ''}
onInput={(e) => setEditingThresholds({
...editingThresholds(),
usage: parseInt(e.currentTarget.value) || undefined
})}
class="w-14 px-1 py-0.5 text-sm text-center border border-gray-300 dark:border-gray-600 rounded
bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
/>
</Show>
</Show>
</td>
<td class="px-3 py-1.5 text-center">
@@ -1226,7 +1305,7 @@ export function ThresholdsTable(props: ThresholdsTableProps) {
</For>
}>
<tr>
<td colspan="12" class="px-4 py-8 text-center text-sm text-gray-500 dark:text-gray-400">
<td colspan="13" class="px-4 py-8 text-center text-sm text-gray-500 dark:text-gray-400">
No resources found
</td>
</tr>
+6 -4
View File
@@ -50,12 +50,12 @@ interface ScheduleRef {
interface Override {
id: string; // Full ID (e.g. "Main-node1-105" for guest, "node-node1" for node)
name: string; // Display name
type: 'guest' | 'node';
resourceType?: string; // VM, CT, or Node
type: 'guest' | 'node' | 'storage';
resourceType?: string; // VM, CT, Node, or Storage
vmid?: number; // Only for guests
node?: string; // Node name (for guests), undefined for nodes themselves
node?: string; // Node name (for guests and storage), undefined for nodes themselves
instance?: string;
disabled?: boolean; // Completely disable alerts for this guest
disabled?: boolean; // Completely disable alerts for this guest/storage
disableConnectivity?: boolean; // For nodes - disable offline/connectivity alerts
thresholds: {
cpu?: number;
@@ -65,6 +65,7 @@ interface Override {
diskWrite?: number;
networkIn?: number;
networkOut?: number;
usage?: number; // For storage devices
};
}
@@ -849,6 +850,7 @@ function ThresholdsTab(props: ThresholdsTabProps) {
setRawOverridesConfig={props.setRawOverridesConfig}
allGuests={props.allGuests}
nodes={props.state.nodes || []}
storage={props.state.storage || []}
guestDefaults={props.guestDefaults()}
setGuestDefaults={props.setGuestDefaults}
nodeDefaults={props.nodeDefaults()}