mirror of
https://github.com/rajnandan1/kener.git
synced 2026-08-07 07:14:56 +00:00
feat(monitors): implement toggle functionality for monitor status and visibility, implements #739
This commit is contained in:
@@ -10,6 +10,14 @@ An open-source status page application providing real-time monitoring, uptime tr
|
||||
A single check against a service, with a unique tag, a type (API, Ping, TCP, DNS, SSL, SQL, Heartbeat, GameDig, Group, gRPC, None), and a status (ACTIVE or otherwise).
|
||||
_Avoid_: Check, probe, service
|
||||
|
||||
**Inactive Monitor**:
|
||||
A monitor that is not checked at all: the scheduler drops its job and no monitoring data is collected until it is made ACTIVE again. Independent of visibility (see Hidden Monitor).
|
||||
_Avoid_: Disabled monitor, paused monitor
|
||||
|
||||
**Hidden Monitor**:
|
||||
A monitor excluded from all status pages while remaining fully checked and alerted. Independent of ACTIVE/INACTIVE.
|
||||
_Avoid_: Invisible monitor, private monitor
|
||||
|
||||
**Group Monitor**:
|
||||
A monitor whose status is derived from other monitors via a weighted score (UP=1, DEGRADED=0.5, DOWN=0; maintenance counts as UP). A group cannot contain another group.
|
||||
_Avoid_: Monitor group, composite monitor
|
||||
|
||||
@@ -14,15 +14,20 @@
|
||||
import XIcon from "@lucide/svelte/icons/x";
|
||||
import { Spinner } from "$lib/components/ui/spinner/index.js";
|
||||
import * as Item from "$lib/components/ui/item/index.js";
|
||||
import { Switch } from "$lib/components/ui/switch/index.js";
|
||||
import type { MonitorRecord } from "$lib/server/types/db.js";
|
||||
import { resolve } from "$app/paths";
|
||||
import { page } from "$app/state";
|
||||
import clientResolver from "$lib/client/resolver.js";
|
||||
import { GetInitials } from "$lib/clientTools.js";
|
||||
import { onMount } from "svelte";
|
||||
import { toast } from "svelte-sonner";
|
||||
|
||||
let monitors = $state<MonitorRecord[]>([]);
|
||||
let loading = $state(true);
|
||||
let error = $state<string | null>(null);
|
||||
let toggling = $state<Record<string, boolean>>({});
|
||||
const canWrite = $derived(page.data.userPermissions?.includes("monitors.write") ?? false);
|
||||
let showFilters = $state(false);
|
||||
let statusFilter = $state("ALL");
|
||||
let searchQuery = $state("");
|
||||
@@ -93,6 +98,38 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleMonitorField(monitor: MonitorRecord, field: "status" | "is_hidden", checked: boolean) {
|
||||
const key = `${monitor.id}:${field}`;
|
||||
if (toggling[key]) return;
|
||||
|
||||
const previous = field === "status" ? monitor.status || "INACTIVE" : monitor.is_hidden || "NO";
|
||||
const next = field === "status" ? (checked ? "ACTIVE" : "INACTIVE") : checked ? "YES" : "NO";
|
||||
if (previous === next) return;
|
||||
|
||||
// Optimistic flip; rolled back on failure
|
||||
monitor[field] = next;
|
||||
toggling[key] = true;
|
||||
try {
|
||||
const response = await fetch(clientResolver(resolve, "/manage/api"), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ action: "storeMonitorData", data: { ...monitor, [field]: next } })
|
||||
});
|
||||
const result = await response.json();
|
||||
if (result.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
const stateLabel =
|
||||
field === "status" ? (next === "ACTIVE" ? "active" : "inactive") : next === "YES" ? "hidden" : "visible";
|
||||
toast.success(`${monitor.name} is now ${stateLabel}`);
|
||||
} catch (e) {
|
||||
monitor[field] = previous;
|
||||
toast.error(e instanceof Error ? e.message : "Failed to update monitor");
|
||||
} finally {
|
||||
delete toggling[key];
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
fetchMonitors();
|
||||
});
|
||||
@@ -212,14 +249,30 @@
|
||||
<Badge variant="secondary">{data.monitor_type}</Badge>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Badge variant={(data.status || "INACTIVE") === "ACTIVE" ? "default" : "destructive"}>
|
||||
{data.status || "INACTIVE"}
|
||||
</Badge>
|
||||
<div class="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={(data.status || "INACTIVE") === "ACTIVE"}
|
||||
disabled={!canWrite || !!toggling[`${data.id}:status`]}
|
||||
aria-label="Toggle status for {data.name}"
|
||||
onCheckedChange={(checked) => toggleMonitorField(data, "status", checked)}
|
||||
/>
|
||||
<span class="text-muted-foreground text-xs">
|
||||
{(data.status || "INACTIVE") === "ACTIVE" ? "Active" : "Inactive"}
|
||||
</span>
|
||||
</div>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Badge variant={data.is_hidden === "YES" ? "destructive" : "outline"}>
|
||||
{data.is_hidden === "YES" ? "YES" : "NO"}
|
||||
</Badge>
|
||||
<div class="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={data.is_hidden === "YES"}
|
||||
disabled={!canWrite || !!toggling[`${data.id}:is_hidden`]}
|
||||
aria-label="Toggle status page visibility for {data.name}"
|
||||
onCheckedChange={(checked) => toggleMonitorField(data, "is_hidden", checked)}
|
||||
/>
|
||||
<span class="text-muted-foreground text-xs">
|
||||
{data.is_hidden === "YES" ? "Hidden" : "Visible"}
|
||||
</span>
|
||||
</div>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<span class="text-muted-foreground text-xs">{data.cron || "-"}</span>
|
||||
|
||||
Reference in New Issue
Block a user