mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-24 20:22:53 +00:00
Improve alerts defaults and websocket cleanup
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Show, createSignal, createContext, useContext, createEffect, onMount, onCleanup } from 'solid-js';
|
||||
import { Show, createSignal, createContext, useContext, createEffect, onMount, onCleanup, getOwner, runWithOwner } from 'solid-js';
|
||||
import { getGlobalWebSocketStore } from './stores/websocket-global';
|
||||
import { Dashboard } from './components/Dashboard/Dashboard';
|
||||
import StorageComponent from './components/Storage/Storage';
|
||||
@@ -46,6 +46,9 @@ export const useDarkMode = () => {
|
||||
};
|
||||
|
||||
function App() {
|
||||
const owner = getOwner();
|
||||
const acquireWsStore = () => (owner ? runWithOwner(owner, () => getGlobalWebSocketStore()) : getGlobalWebSocketStore());
|
||||
|
||||
// Simple auth state
|
||||
const [isLoading, setIsLoading] = createSignal(true);
|
||||
const [needsAuth, setNeedsAuth] = createSignal(false);
|
||||
@@ -189,7 +192,7 @@ function App() {
|
||||
setHasAuth(false);
|
||||
setNeedsAuth(false);
|
||||
// Initialize WebSocket immediately since no auth needed
|
||||
setWsStore(getGlobalWebSocketStore());
|
||||
setWsStore(acquireWsStore());
|
||||
|
||||
// Load theme preference from server for cross-device sync
|
||||
// Only use server preference if no local preference exists
|
||||
@@ -237,7 +240,7 @@ function App() {
|
||||
});
|
||||
setNeedsAuth(false);
|
||||
// Initialize WebSocket for proxy auth users
|
||||
setWsStore(getGlobalWebSocketStore());
|
||||
setWsStore(acquireWsStore());
|
||||
|
||||
// Load theme preference from server for cross-device sync
|
||||
// Only use server preference if no local preference exists
|
||||
@@ -294,7 +297,7 @@ function App() {
|
||||
} else {
|
||||
setNeedsAuth(false);
|
||||
// Only initialize WebSocket after successful auth check
|
||||
setWsStore(getGlobalWebSocketStore());
|
||||
setWsStore(acquireWsStore());
|
||||
|
||||
// Load theme preference from server for cross-device sync
|
||||
// Only use server preference if no local preference exists
|
||||
@@ -324,7 +327,7 @@ function App() {
|
||||
console.error('Auth check error:', error);
|
||||
// On error, try to proceed without auth
|
||||
setNeedsAuth(false);
|
||||
setWsStore(getGlobalWebSocketStore());
|
||||
setWsStore(acquireWsStore());
|
||||
|
||||
// Theme is already applied on initialization, no need to reapply
|
||||
} finally {
|
||||
|
||||
@@ -71,10 +71,7 @@ export function ResourceTable(props: ResourceTableProps) {
|
||||
<div class="px-4 py-3 border-b border-gray-200 dark:border-gray-700">
|
||||
<SectionHeader title={props.title} size="sm" />
|
||||
</div>
|
||||
<div class="overflow-x-auto" style="scrollbar-width: none; -ms-overflow-style: none;">
|
||||
<style>{`
|
||||
.overflow-x-auto::-webkit-scrollbar { display: none; }
|
||||
`}</style>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full">
|
||||
<thead>
|
||||
<tr class="bg-gray-50 dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700">
|
||||
|
||||
@@ -624,210 +624,257 @@ export function ThresholdsTable(props: ThresholdsTableProps) {
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-200 dark:border-gray-700 p-4 space-y-4">
|
||||
{/* Threshold table */}
|
||||
{/* Threshold inputs in a responsive layout */}
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-3">
|
||||
Default thresholds for all resources. Individual resources can override these values below.
|
||||
<span class="ml-2 text-blue-600 dark:text-blue-400">Enter 0 or -1 to disable specific alerts.</span>
|
||||
</p>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="border-b border-gray-200 dark:border-gray-700">
|
||||
<th class="text-left py-2 px-3 text-xs font-medium text-gray-600 dark:text-gray-400">Resource Type</th>
|
||||
<th class="text-center px-3 text-xs font-medium text-gray-600 dark:text-gray-400">CPU<br/><span class="text-[10px] font-normal text-gray-500 dark:text-gray-500">%</span></th>
|
||||
<th class="text-center px-3 text-xs font-medium text-gray-600 dark:text-gray-400">Memory<br/><span class="text-[10px] font-normal text-gray-500 dark:text-gray-500">%</span></th>
|
||||
<th class="text-center px-3 text-xs font-medium text-gray-600 dark:text-gray-400">Disk<br/><span class="text-[10px] font-normal text-gray-500 dark:text-gray-500">%</span></th>
|
||||
<th class="text-center px-3 text-xs font-medium text-gray-600 dark:text-gray-400">Storage<br/><span class="text-[10px] font-normal text-gray-500 dark:text-gray-500">%</span></th>
|
||||
<th class="text-center px-3 text-xs font-medium text-gray-600 dark:text-gray-400">Disk Read<br/><span class="text-[10px] font-normal text-gray-500 dark:text-gray-500">MB/s</span></th>
|
||||
<th class="text-center px-3 text-xs font-medium text-gray-600 dark:text-gray-400">Disk Write<br/><span class="text-[10px] font-normal text-gray-500 dark:text-gray-500">MB/s</span></th>
|
||||
<th class="text-center px-3 text-xs font-medium text-gray-600 dark:text-gray-400">Net In<br/><span class="text-[10px] font-normal text-gray-500 dark:text-gray-500">MB/s</span></th>
|
||||
<th class="text-center px-3 text-xs font-medium text-gray-600 dark:text-gray-400">Net Out<br/><span class="text-[10px] font-normal text-gray-500 dark:text-gray-500">MB/s</span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="border-b border-gray-100 dark:border-gray-700/50">
|
||||
<td class="p-1 px-2 font-medium text-gray-700 dark:text-gray-300 text-sm">VMs & Containers</td>
|
||||
<td class="text-center p-1 px-2">
|
||||
<input
|
||||
type="number"
|
||||
min="-1"
|
||||
max="100"
|
||||
value={props.guestDefaults.cpu || 0}
|
||||
title="Enter 0 or -1 to disable CPU alerts"
|
||||
onInput={(e) => {
|
||||
props.setGuestDefaults((prev) => ({...prev, cpu: parseInt(e.currentTarget.value) || 0}));
|
||||
props.setHasUnsavedChanges(true);
|
||||
}}
|
||||
class="w-16 px-2 py-1 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 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
</td>
|
||||
<td class="text-center p-1 px-2">
|
||||
<input
|
||||
type="number"
|
||||
min="-1"
|
||||
max="100"
|
||||
value={props.guestDefaults.memory || 0}
|
||||
onInput={(e) => {
|
||||
props.setGuestDefaults((prev) => ({...prev, memory: parseInt(e.currentTarget.value) || 0}));
|
||||
props.setHasUnsavedChanges(true);
|
||||
}}
|
||||
class="w-16 px-2 py-1 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 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
</td>
|
||||
<td class="text-center p-1 px-2">
|
||||
<input
|
||||
type="number"
|
||||
min="-1"
|
||||
max="100"
|
||||
value={props.guestDefaults.disk || 0}
|
||||
onInput={(e) => {
|
||||
props.setGuestDefaults((prev) => ({...prev, disk: parseInt(e.currentTarget.value) || 0}));
|
||||
props.setHasUnsavedChanges(true);
|
||||
}}
|
||||
class="w-16 px-2 py-1 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 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
</td>
|
||||
<td class="text-center p-1 px-2"><span class="text-gray-400 dark:text-gray-500">-</span></td>
|
||||
<td class="text-center p-1 px-2">
|
||||
<input
|
||||
type="number"
|
||||
min="-1"
|
||||
max="10000"
|
||||
value={props.guestDefaults.diskRead || 0}
|
||||
onInput={(e) => {
|
||||
props.setGuestDefaults((prev) => ({...prev, diskRead: parseInt(e.currentTarget.value) || 0}));
|
||||
props.setHasUnsavedChanges(true);
|
||||
}}
|
||||
class="w-16 px-2 py-1 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 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
</td>
|
||||
<td class="text-center p-1 px-2">
|
||||
<input
|
||||
type="number"
|
||||
min="-1"
|
||||
max="10000"
|
||||
value={props.guestDefaults.diskWrite || 0}
|
||||
onInput={(e) => {
|
||||
props.setGuestDefaults((prev) => ({...prev, diskWrite: parseInt(e.currentTarget.value) || 0}));
|
||||
props.setHasUnsavedChanges(true);
|
||||
}}
|
||||
class="w-16 px-2 py-1 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 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
</td>
|
||||
<td class="text-center p-1 px-2">
|
||||
<input
|
||||
type="number"
|
||||
min="-1"
|
||||
max="10000"
|
||||
value={props.guestDefaults.networkIn || 0}
|
||||
onInput={(e) => {
|
||||
props.setGuestDefaults((prev) => ({...prev, networkIn: parseInt(e.currentTarget.value) || 0}));
|
||||
props.setHasUnsavedChanges(true);
|
||||
}}
|
||||
class="w-16 px-2 py-1 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 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
</td>
|
||||
<td class="text-center p-1 px-2">
|
||||
<input
|
||||
type="number"
|
||||
min="-1"
|
||||
max="10000"
|
||||
value={props.guestDefaults.networkOut || 0}
|
||||
onInput={(e) => {
|
||||
props.setGuestDefaults((prev) => ({...prev, networkOut: parseInt(e.currentTarget.value) || 0}));
|
||||
props.setHasUnsavedChanges(true);
|
||||
}}
|
||||
class="w-16 px-2 py-1 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 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b border-gray-100 dark:border-gray-700/50">
|
||||
<td class="p-1 px-2 font-medium text-gray-700 dark:text-gray-300 text-sm">Proxmox Nodes</td>
|
||||
<td class="text-center p-1 px-2">
|
||||
<input
|
||||
type="number"
|
||||
min="-1"
|
||||
max="100"
|
||||
value={props.nodeDefaults.cpu || 0}
|
||||
onInput={(e) => {
|
||||
props.setNodeDefaults((prev) => ({...prev, cpu: parseInt(e.currentTarget.value) || 0}));
|
||||
props.setHasUnsavedChanges(true);
|
||||
}}
|
||||
class="w-16 px-2 py-1 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 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
</td>
|
||||
<td class="text-center p-1 px-2">
|
||||
<input
|
||||
type="number"
|
||||
min="-1"
|
||||
max="100"
|
||||
value={props.nodeDefaults.memory || 0}
|
||||
onInput={(e) => {
|
||||
props.setNodeDefaults((prev) => ({...prev, memory: parseInt(e.currentTarget.value) || 0}));
|
||||
props.setHasUnsavedChanges(true);
|
||||
}}
|
||||
class="w-16 px-2 py-1 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 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
</td>
|
||||
<td class="text-center p-1 px-2">
|
||||
<input
|
||||
type="number"
|
||||
min="-1"
|
||||
max="100"
|
||||
value={props.nodeDefaults.disk || 0}
|
||||
onInput={(e) => {
|
||||
props.setNodeDefaults((prev) => ({...prev, disk: parseInt(e.currentTarget.value) || 0}));
|
||||
props.setHasUnsavedChanges(true);
|
||||
}}
|
||||
class="w-16 px-2 py-1 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 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
</td>
|
||||
<td class="text-center p-1 px-2"><span class="text-gray-400 dark:text-gray-500">-</span></td>
|
||||
<td class="text-center p-1 px-2"><span class="text-gray-400 dark:text-gray-500">-</span></td>
|
||||
<td class="text-center p-1 px-2"><span class="text-gray-400 dark:text-gray-500">-</span></td>
|
||||
<td class="text-center p-1 px-2"><span class="text-gray-400 dark:text-gray-500">-</span></td>
|
||||
<td class="text-center p-1 px-2"><span class="text-gray-400 dark:text-gray-500">-</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="p-1 px-2 font-medium text-gray-700 dark:text-gray-300 text-sm">Storage</td>
|
||||
<td class="text-center p-1 px-2"><span class="text-gray-400 dark:text-gray-500">-</span></td>
|
||||
<td class="text-center p-1 px-2"><span class="text-gray-400 dark:text-gray-500">-</span></td>
|
||||
<td class="text-center p-1 px-2"><span class="text-gray-400 dark:text-gray-500">-</span></td>
|
||||
<td class="text-center p-1 px-2">
|
||||
<input
|
||||
type="number"
|
||||
min="-1"
|
||||
max="100"
|
||||
value={props.storageDefault()}
|
||||
onInput={(e) => {
|
||||
props.setStorageDefault(parseInt(e.currentTarget.value) || 0);
|
||||
props.setHasUnsavedChanges(true);
|
||||
}}
|
||||
class="w-16 px-2 py-1 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 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
</td>
|
||||
<td class="text-center p-1 px-2"><span class="text-gray-400 dark:text-gray-500">-</span></td>
|
||||
<td class="text-center p-1 px-2"><span class="text-gray-400 dark:text-gray-500">-</span></td>
|
||||
<td class="text-center p-1 px-2"><span class="text-gray-400 dark:text-gray-500">-</span></td>
|
||||
<td class="text-center p-1 px-2"><span class="text-gray-400 dark:text-gray-500">-</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<div class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800/60 p-3">
|
||||
<h4 class="text-xs font-semibold text-gray-600 dark:text-gray-300 uppercase tracking-wide mb-3">
|
||||
VMs & Containers
|
||||
</h4>
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
<div class="space-y-1">
|
||||
<label for="guest-cpu" class="text-xs font-medium text-gray-600 dark:text-gray-300 flex items-center justify-between">
|
||||
<span>CPU</span>
|
||||
<span class="text-[10px] font-normal text-gray-400">%</span>
|
||||
</label>
|
||||
<input
|
||||
id="guest-cpu"
|
||||
type="number"
|
||||
min="-1"
|
||||
max="100"
|
||||
value={props.guestDefaults.cpu ?? 0}
|
||||
title="Enter 0 or -1 to disable CPU alerts"
|
||||
onInput={(e) => {
|
||||
const value = parseInt(e.currentTarget.value, 10);
|
||||
props.setGuestDefaults((prev) => ({ ...prev, cpu: Number.isNaN(value) ? 0 : value }));
|
||||
props.setHasUnsavedChanges(true);
|
||||
}}
|
||||
class="w-full px-3 py-1.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 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label for="guest-memory" class="text-xs font-medium text-gray-600 dark:text-gray-300 flex items-center justify-between">
|
||||
<span>Memory</span>
|
||||
<span class="text-[10px] font-normal text-gray-400">%</span>
|
||||
</label>
|
||||
<input
|
||||
id="guest-memory"
|
||||
type="number"
|
||||
min="-1"
|
||||
max="100"
|
||||
value={props.guestDefaults.memory ?? 0}
|
||||
onInput={(e) => {
|
||||
const value = parseInt(e.currentTarget.value, 10);
|
||||
props.setGuestDefaults((prev) => ({ ...prev, memory: Number.isNaN(value) ? 0 : value }));
|
||||
props.setHasUnsavedChanges(true);
|
||||
}}
|
||||
class="w-full px-3 py-1.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 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label for="guest-disk" class="text-xs font-medium text-gray-600 dark:text-gray-300 flex items-center justify-between">
|
||||
<span>Disk</span>
|
||||
<span class="text-[10px] font-normal text-gray-400">%</span>
|
||||
</label>
|
||||
<input
|
||||
id="guest-disk"
|
||||
type="number"
|
||||
min="-1"
|
||||
max="100"
|
||||
value={props.guestDefaults.disk ?? 0}
|
||||
onInput={(e) => {
|
||||
const value = parseInt(e.currentTarget.value, 10);
|
||||
props.setGuestDefaults((prev) => ({ ...prev, disk: Number.isNaN(value) ? 0 : value }));
|
||||
props.setHasUnsavedChanges(true);
|
||||
}}
|
||||
class="w-full px-3 py-1.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 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label for="guest-disk-read" class="text-xs font-medium text-gray-600 dark:text-gray-300 flex items-center justify-between">
|
||||
<span>Disk Read</span>
|
||||
<span class="text-[10px] font-normal text-gray-400">MB/s</span>
|
||||
</label>
|
||||
<input
|
||||
id="guest-disk-read"
|
||||
type="number"
|
||||
min="-1"
|
||||
max="10000"
|
||||
value={props.guestDefaults.diskRead ?? 0}
|
||||
onInput={(e) => {
|
||||
const value = parseInt(e.currentTarget.value, 10);
|
||||
props.setGuestDefaults((prev) => ({ ...prev, diskRead: Number.isNaN(value) ? 0 : value }));
|
||||
props.setHasUnsavedChanges(true);
|
||||
}}
|
||||
class="w-full px-3 py-1.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 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label for="guest-disk-write" class="text-xs font-medium text-gray-600 dark:text-gray-300 flex items-center justify-between">
|
||||
<span>Disk Write</span>
|
||||
<span class="text-[10px] font-normal text-gray-400">MB/s</span>
|
||||
</label>
|
||||
<input
|
||||
id="guest-disk-write"
|
||||
type="number"
|
||||
min="-1"
|
||||
max="10000"
|
||||
value={props.guestDefaults.diskWrite ?? 0}
|
||||
onInput={(e) => {
|
||||
const value = parseInt(e.currentTarget.value, 10);
|
||||
props.setGuestDefaults((prev) => ({ ...prev, diskWrite: Number.isNaN(value) ? 0 : value }));
|
||||
props.setHasUnsavedChanges(true);
|
||||
}}
|
||||
class="w-full px-3 py-1.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 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label for="guest-network-in" class="text-xs font-medium text-gray-600 dark:text-gray-300 flex items-center justify-between">
|
||||
<span>Net In</span>
|
||||
<span class="text-[10px] font-normal text-gray-400">MB/s</span>
|
||||
</label>
|
||||
<input
|
||||
id="guest-network-in"
|
||||
type="number"
|
||||
min="-1"
|
||||
max="10000"
|
||||
value={props.guestDefaults.networkIn ?? 0}
|
||||
onInput={(e) => {
|
||||
const value = parseInt(e.currentTarget.value, 10);
|
||||
props.setGuestDefaults((prev) => ({ ...prev, networkIn: Number.isNaN(value) ? 0 : value }));
|
||||
props.setHasUnsavedChanges(true);
|
||||
}}
|
||||
class="w-full px-3 py-1.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 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label for="guest-network-out" class="text-xs font-medium text-gray-600 dark:text-gray-300 flex items-center justify-between">
|
||||
<span>Net Out</span>
|
||||
<span class="text-[10px] font-normal text-gray-400">MB/s</span>
|
||||
</label>
|
||||
<input
|
||||
id="guest-network-out"
|
||||
type="number"
|
||||
min="-1"
|
||||
max="10000"
|
||||
value={props.guestDefaults.networkOut ?? 0}
|
||||
onInput={(e) => {
|
||||
const value = parseInt(e.currentTarget.value, 10);
|
||||
props.setGuestDefaults((prev) => ({ ...prev, networkOut: Number.isNaN(value) ? 0 : value }));
|
||||
props.setHasUnsavedChanges(true);
|
||||
}}
|
||||
class="w-full px-3 py-1.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 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800/60 p-3">
|
||||
<h4 class="text-xs font-semibold text-gray-600 dark:text-gray-300 uppercase tracking-wide mb-3">
|
||||
Proxmox Nodes
|
||||
</h4>
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
<div class="space-y-1">
|
||||
<label for="node-cpu" class="text-xs font-medium text-gray-600 dark:text-gray-300 flex items-center justify-between">
|
||||
<span>CPU</span>
|
||||
<span class="text-[10px] font-normal text-gray-400">%</span>
|
||||
</label>
|
||||
<input
|
||||
id="node-cpu"
|
||||
type="number"
|
||||
min="-1"
|
||||
max="100"
|
||||
value={props.nodeDefaults.cpu ?? 0}
|
||||
onInput={(e) => {
|
||||
const value = parseInt(e.currentTarget.value, 10);
|
||||
props.setNodeDefaults((prev) => ({ ...prev, cpu: Number.isNaN(value) ? 0 : value }));
|
||||
props.setHasUnsavedChanges(true);
|
||||
}}
|
||||
class="w-full px-3 py-1.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 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label for="node-memory" class="text-xs font-medium text-gray-600 dark:text-gray-300 flex items-center justify-between">
|
||||
<span>Memory</span>
|
||||
<span class="text-[10px] font-normal text-gray-400">%</span>
|
||||
</label>
|
||||
<input
|
||||
id="node-memory"
|
||||
type="number"
|
||||
min="-1"
|
||||
max="100"
|
||||
value={props.nodeDefaults.memory ?? 0}
|
||||
onInput={(e) => {
|
||||
const value = parseInt(e.currentTarget.value, 10);
|
||||
props.setNodeDefaults((prev) => ({ ...prev, memory: Number.isNaN(value) ? 0 : value }));
|
||||
props.setHasUnsavedChanges(true);
|
||||
}}
|
||||
class="w-full px-3 py-1.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 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-1 sm:col-span-2">
|
||||
<label for="node-disk" class="text-xs font-medium text-gray-600 dark:text-gray-300 flex items-center justify-between">
|
||||
<span>Disk</span>
|
||||
<span class="text-[10px] font-normal text-gray-400">%</span>
|
||||
</label>
|
||||
<input
|
||||
id="node-disk"
|
||||
type="number"
|
||||
min="-1"
|
||||
max="100"
|
||||
value={props.nodeDefaults.disk ?? 0}
|
||||
onInput={(e) => {
|
||||
const value = parseInt(e.currentTarget.value, 10);
|
||||
props.setNodeDefaults((prev) => ({ ...prev, disk: Number.isNaN(value) ? 0 : value }));
|
||||
props.setHasUnsavedChanges(true);
|
||||
}}
|
||||
class="w-full px-3 py-1.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 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800/60 p-3 md:col-span-2">
|
||||
<h4 class="text-xs font-semibold text-gray-600 dark:text-gray-300 uppercase tracking-wide mb-3">
|
||||
Storage
|
||||
</h4>
|
||||
<div class="grid gap-3 sm:grid-cols-3 lg:grid-cols-4">
|
||||
<div class="space-y-1">
|
||||
<label for="storage-usage" class="text-xs font-medium text-gray-600 dark:text-gray-300 flex items-center justify-between">
|
||||
<span>Usage</span>
|
||||
<span class="text-[10px] font-normal text-gray-400">%</span>
|
||||
</label>
|
||||
<input
|
||||
id="storage-usage"
|
||||
type="number"
|
||||
min="-1"
|
||||
max="100"
|
||||
value={props.storageDefault()}
|
||||
onInput={(e) => {
|
||||
const value = parseInt(e.currentTarget.value, 10);
|
||||
props.setStorageDefault(Number.isNaN(value) ? 0 : value);
|
||||
props.setHasUnsavedChanges(true);
|
||||
}}
|
||||
class="w-full px-3 py-1.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 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Alert delay settings per resource type */}
|
||||
<div class="pt-3 border-t border-gray-200 dark:border-gray-700">
|
||||
<div class="mb-2">
|
||||
|
||||
@@ -66,6 +66,75 @@ interface UIEmailConfig {
|
||||
rateLimit: number;
|
||||
}
|
||||
|
||||
interface QuietHoursConfig {
|
||||
enabled: boolean;
|
||||
start: string;
|
||||
end: string;
|
||||
timezone: string;
|
||||
days: Record<string, boolean>;
|
||||
}
|
||||
|
||||
interface CooldownConfig {
|
||||
enabled: boolean;
|
||||
minutes: number;
|
||||
maxAlerts: number;
|
||||
}
|
||||
|
||||
interface GroupingConfig {
|
||||
enabled: boolean;
|
||||
window: number;
|
||||
maxGroupSize?: number;
|
||||
byNode?: boolean;
|
||||
byGuest?: boolean;
|
||||
}
|
||||
|
||||
interface EscalationConfig {
|
||||
enabled: boolean;
|
||||
timeToEscalate?: number;
|
||||
levels: Array<{
|
||||
level?: number;
|
||||
destinations?: string[];
|
||||
after?: number;
|
||||
notify?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
const getLocalTimezone = () => Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
|
||||
|
||||
const createDefaultQuietHours = (): QuietHoursConfig => ({
|
||||
enabled: false,
|
||||
start: '22:00',
|
||||
end: '08:00',
|
||||
timezone: getLocalTimezone(),
|
||||
days: {
|
||||
monday: true,
|
||||
tuesday: true,
|
||||
wednesday: true,
|
||||
thursday: true,
|
||||
friday: true,
|
||||
saturday: false,
|
||||
sunday: false
|
||||
}
|
||||
});
|
||||
|
||||
const createDefaultCooldown = (): CooldownConfig => ({
|
||||
enabled: true,
|
||||
minutes: 30,
|
||||
maxAlerts: 3
|
||||
});
|
||||
|
||||
const createDefaultGrouping = (): GroupingConfig => ({
|
||||
enabled: true,
|
||||
window: 5,
|
||||
byNode: true,
|
||||
byGuest: false
|
||||
});
|
||||
|
||||
const createDefaultEscalation = (): EscalationConfig => ({
|
||||
enabled: false,
|
||||
levels: []
|
||||
});
|
||||
|
||||
export function Alerts() {
|
||||
const { state, activeAlerts, updateAlert } = useWebSocket();
|
||||
const [activeTab, setActiveTab] = createSignal<AlertTab>('overview');
|
||||
@@ -107,39 +176,13 @@ export function Alerts() {
|
||||
});
|
||||
|
||||
// Schedule configuration state moved to parent to persist across tab changes
|
||||
const [scheduleQuietHours, setScheduleQuietHours] = createSignal({
|
||||
enabled: false,
|
||||
start: '22:00',
|
||||
end: '08:00',
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC',
|
||||
days: {
|
||||
monday: true,
|
||||
tuesday: true,
|
||||
wednesday: true,
|
||||
thursday: true,
|
||||
friday: true,
|
||||
saturday: false,
|
||||
sunday: false
|
||||
} as Record<string, boolean>
|
||||
});
|
||||
const [scheduleQuietHours, setScheduleQuietHours] = createSignal<QuietHoursConfig>(createDefaultQuietHours());
|
||||
|
||||
const [scheduleCooldown, setScheduleCooldown] = createSignal({
|
||||
enabled: true,
|
||||
minutes: 30,
|
||||
maxAlerts: 3
|
||||
});
|
||||
const [scheduleCooldown, setScheduleCooldown] = createSignal<CooldownConfig>(createDefaultCooldown());
|
||||
|
||||
const [scheduleGrouping, setScheduleGrouping] = createSignal({
|
||||
enabled: true,
|
||||
window: 5,
|
||||
byNode: true,
|
||||
byGuest: false
|
||||
});
|
||||
const [scheduleGrouping, setScheduleGrouping] = createSignal<GroupingConfig>(createDefaultGrouping());
|
||||
|
||||
const [scheduleEscalation, setScheduleEscalation] = createSignal({
|
||||
enabled: false,
|
||||
levels: [] as Array<{ after: number; notify: string }>
|
||||
});
|
||||
const [scheduleEscalation, setScheduleEscalation] = createSignal<EscalationConfig>(createDefaultEscalation());
|
||||
|
||||
// Set up destinationsRef.emailConfig function immediately
|
||||
destinationsRef.emailConfig = () => {
|
||||
@@ -1211,39 +1254,6 @@ function DestinationsTab(props: DestinationsTabProps) {
|
||||
|
||||
// History Tab - Alert history
|
||||
|
||||
interface QuietHoursConfig {
|
||||
enabled: boolean;
|
||||
start: string;
|
||||
end: string;
|
||||
timezone: string;
|
||||
days: Record<string, boolean>;
|
||||
}
|
||||
|
||||
interface CooldownConfig {
|
||||
enabled: boolean;
|
||||
minutes: number;
|
||||
maxAlerts: number;
|
||||
}
|
||||
|
||||
interface GroupingConfig {
|
||||
enabled: boolean;
|
||||
window: number;
|
||||
maxGroupSize?: number;
|
||||
byNode?: boolean;
|
||||
byGuest?: boolean;
|
||||
}
|
||||
|
||||
interface EscalationConfig {
|
||||
enabled: boolean;
|
||||
timeToEscalate?: number;
|
||||
levels: Array<{
|
||||
level?: number;
|
||||
destinations?: string[];
|
||||
after?: number;
|
||||
notify?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
// Schedule Tab - Quiet hours, cooldown, and grouping
|
||||
interface ScheduleTabProps {
|
||||
hasUnsavedChanges: () => boolean;
|
||||
@@ -1268,6 +1278,13 @@ function ScheduleTab(props: ScheduleTabProps) {
|
||||
const setGrouping = props.setGrouping;
|
||||
const escalation = props.escalation;
|
||||
const setEscalation = props.setEscalation;
|
||||
const resetToDefaults = () => {
|
||||
setQuietHours(createDefaultQuietHours());
|
||||
setCooldown(createDefaultCooldown());
|
||||
setGrouping(createDefaultGrouping());
|
||||
setEscalation(createDefaultEscalation());
|
||||
props.setHasUnsavedChanges(true);
|
||||
};
|
||||
|
||||
const timezones = [
|
||||
'UTC',
|
||||
@@ -1295,10 +1312,23 @@ function ScheduleTab(props: ScheduleTabProps) {
|
||||
|
||||
return (
|
||||
<div class="space-y-6">
|
||||
<SectionHeader
|
||||
label="Notification Schedule"
|
||||
title="Control when alerts are allowed to fire and how they are grouped."
|
||||
/>
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<SectionHeader
|
||||
label="Notification Schedule"
|
||||
title="Control when alerts are allowed to fire and how they are grouped."
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={resetToDefaults}
|
||||
class="inline-flex items-center gap-1 self-start rounded-md border border-gray-300 px-3 py-1.5 text-xs font-medium text-gray-600 transition-colors hover:bg-gray-50 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
title="Restore quiet hours, cooldown, grouping, and escalation settings to their defaults"
|
||||
>
|
||||
<svg class="h-3 w-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
Reset defaults
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-6 lg:grid-cols-2">
|
||||
{/* Quiet Hours */}
|
||||
|
||||
Reference in New Issue
Block a user