From 72334c42d039bd12c74fd195c4c885b6c4b57b5e Mon Sep 17 00:00:00 2001 From: henrygd Date: Sun, 24 Aug 2025 19:57:28 -0400 Subject: [PATCH] refactor: add @/lib/alerts --- .../src/components/alerts-history-columns.tsx | 3 +- .../src/components/alerts/alerts-sheet.tsx | 3 +- beszel/site/src/components/routes/home.tsx | 3 +- .../settings/alerts-history-data-table.tsx | 3 +- beszel/site/src/lib/alerts.ts | 170 +++++++++++++++++ beszel/site/src/lib/utils.ts | 178 +----------------- beszel/site/src/main.tsx | 9 +- 7 files changed, 184 insertions(+), 185 deletions(-) create mode 100644 beszel/site/src/lib/alerts.ts diff --git a/beszel/site/src/components/alerts-history-columns.tsx b/beszel/site/src/components/alerts-history-columns.tsx index b145cf0..4bddce7 100644 --- a/beszel/site/src/components/alerts-history-columns.tsx +++ b/beszel/site/src/components/alerts-history-columns.tsx @@ -2,7 +2,8 @@ import { ColumnDef } from "@tanstack/react-table" import { AlertsHistoryRecord } from "@/types" import { Button } from "@/components/ui/button" import { Badge } from "@/components/ui/badge" -import { alertInfo, formatShortDate, toFixedFloat, formatDuration, cn } from "@/lib/utils" +import { formatShortDate, toFixedFloat, formatDuration, cn } from "@/lib/utils" +import { alertInfo } from "@/lib/alerts" import { Trans } from "@lingui/react/macro" import { t } from "@lingui/core/macro" diff --git a/beszel/site/src/components/alerts/alerts-sheet.tsx b/beszel/site/src/components/alerts/alerts-sheet.tsx index 4135c02..3f2d3dc 100644 --- a/beszel/site/src/components/alerts/alerts-sheet.tsx +++ b/beszel/site/src/components/alerts/alerts-sheet.tsx @@ -1,7 +1,8 @@ import { t } from "@lingui/core/macro" import { Trans, Plural } from "@lingui/react/macro" import { $alerts, $systems, pb } from "@/lib/stores" -import { alertInfo, cn, debounce } from "@/lib/utils" +import { cn, debounce } from "@/lib/utils" +import { alertInfo } from "@/lib/alerts" import { Switch } from "@/components/ui/switch" import { AlertInfo, AlertRecord, SystemRecord } from "@/types" import { lazy, memo, Suspense, useMemo, useState } from "react" diff --git a/beszel/site/src/components/routes/home.tsx b/beszel/site/src/components/routes/home.tsx index 4bcba00..72316f6 100644 --- a/beszel/site/src/components/routes/home.tsx +++ b/beszel/site/src/components/routes/home.tsx @@ -4,12 +4,13 @@ import { $alerts, $systems, pb } from "@/lib/stores" import { useStore } from "@nanostores/react" import { GithubIcon } from "lucide-react" import { Separator } from "../ui/separator" -import { alertInfo, getSystemNameFromId, updateRecordList, updateSystemList } from "@/lib/utils" +import { getSystemNameFromId, updateRecordList, updateSystemList } from "@/lib/utils" import { AlertRecord, SystemRecord } from "@/types" import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" import { $router, Link } from "../router" import { Plural, Trans, useLingui } from "@lingui/react/macro" import { getPagePath } from "@nanostores/router" +import { alertInfo } from "@/lib/alerts" const SystemsTable = lazy(() => import("../systems-table/systems-table")) diff --git a/beszel/site/src/components/routes/settings/alerts-history-data-table.tsx b/beszel/site/src/components/routes/settings/alerts-history-data-table.tsx index ac5c141..30bca2b 100644 --- a/beszel/site/src/components/routes/settings/alerts-history-data-table.tsx +++ b/beszel/site/src/components/routes/settings/alerts-history-data-table.tsx @@ -1,5 +1,6 @@ import { pb } from "@/lib/stores" -import { alertInfo, cn, formatDuration, formatShortDate } from "@/lib/utils" +import { cn, formatDuration, formatShortDate } from "@/lib/utils" +import { alertInfo } from "@/lib/alerts" import { AlertsHistoryRecord } from "@/types" import { getCoreRowModel, diff --git a/beszel/site/src/lib/alerts.ts b/beszel/site/src/lib/alerts.ts new file mode 100644 index 0000000..256a1e1 --- /dev/null +++ b/beszel/site/src/lib/alerts.ts @@ -0,0 +1,170 @@ +import type { AlertInfo, AlertRecord } from "@/types" +import type { RecordSubscription } from "pocketbase" +import { pb, $alerts } from "@/lib/stores" +import { EthernetIcon } from "@/components/ui/icons" +import { ServerIcon, CpuIcon, MemoryStickIcon, HardDriveIcon, ThermometerIcon, HourglassIcon } from "lucide-react" +import { t } from "@lingui/core/macro" + +/** Alert info for each alert type */ +export const alertInfo: Record = { + Status: { + name: () => t`Status`, + unit: "", + icon: ServerIcon, + desc: () => t`Triggers when status switches between up and down`, + /** "for x minutes" is appended to desc when only one value */ + singleDesc: () => t`System` + " " + t`Down`, + }, + CPU: { + name: () => t`CPU Usage`, + unit: "%", + icon: CpuIcon, + desc: () => t`Triggers when CPU usage exceeds a threshold`, + }, + Memory: { + name: () => t`Memory Usage`, + unit: "%", + icon: MemoryStickIcon, + desc: () => t`Triggers when memory usage exceeds a threshold`, + }, + Disk: { + name: () => t`Disk Usage`, + unit: "%", + icon: HardDriveIcon, + desc: () => t`Triggers when usage of any disk exceeds a threshold`, + }, + Bandwidth: { + name: () => t`Bandwidth`, + unit: " MB/s", + icon: EthernetIcon, + desc: () => t`Triggers when combined up/down exceeds a threshold`, + max: 125, + }, + Temperature: { + name: () => t`Temperature`, + unit: "°C", + icon: ThermometerIcon, + desc: () => t`Triggers when any sensor exceeds a threshold`, + }, + LoadAvg1: { + name: () => t`Load Average 1m`, + unit: "", + icon: HourglassIcon, + max: 100, + min: 0.1, + start: 10, + step: 0.1, + desc: () => t`Triggers when 1 minute load average exceeds a threshold`, + }, + LoadAvg5: { + name: () => t`Load Average 5m`, + unit: "", + icon: HourglassIcon, + max: 100, + min: 0.1, + start: 10, + step: 0.1, + desc: () => t`Triggers when 5 minute load average exceeds a threshold`, + }, + LoadAvg15: { + name: () => t`Load Average 15m`, + unit: "", + icon: HourglassIcon, + min: 0.1, + max: 100, + start: 10, + step: 0.1, + desc: () => t`Triggers when 15 minute load average exceeds a threshold`, + }, +} as const + +/** Helper to manage user alerts */ +export const alertManager = (() => { + const collection = pb.collection("alerts") + let unsub: () => void + + /** Fields to fetch from alerts collection */ + const fields = "id,name,system,value,min,triggered" + + /** Fetch alerts from collection */ + async function fetchAlerts(): Promise { + return await collection.getFullList({ fields, sort: "updated" }) + } + + /** Format alerts into a map of system id to alert name to alert record */ + function add(alerts: AlertRecord[]) { + for (const alert of alerts) { + const systemId = alert.system + const systemAlerts = $alerts.get()[systemId] ?? new Map() + const newAlerts = new Map(systemAlerts) + newAlerts.set(alert.name, alert) + $alerts.setKey(systemId, newAlerts) + } + } + + function remove(alerts: Pick[]) { + for (const alert of alerts) { + const systemId = alert.system + const systemAlerts = $alerts.get()[systemId] + const newAlerts = new Map(systemAlerts) + newAlerts.delete(alert.name) + $alerts.setKey(systemId, newAlerts) + } + } + + const actionFns = { + create: add, + update: add, + delete: remove, + } + + // batch alert updates to prevent unnecessary re-renders when adding many alerts at once + const batchUpdate = (() => { + const batch = new Map>() + let timeout: ReturnType + + return (data: RecordSubscription) => { + const { record } = data + batch.set(`${record.system}${record.name}`, data) + clearTimeout(timeout!) + timeout = setTimeout(() => { + const groups = { create: [], update: [], delete: [] } as Record + for (const { action, record } of batch.values()) { + groups[action]?.push(record) + } + for (const key in groups) { + if (groups[key].length) { + actionFns[key as keyof typeof actionFns]?.(groups[key]) + } + } + batch.clear() + }, 50) + } + })() + + async function subscribe() { + unsub = await collection.subscribe("*", batchUpdate, { fields }) + } + + function unsubscribe() { + unsub?.() + } + + async function refresh() { + const records = await fetchAlerts() + add(records) + } + + return { + /** Add alerts to store */ + add, + /** Remove alerts from store */ + remove, + /** Subscribe to alerts */ + subscribe, + /** Unsubscribe from alerts */ + unsubscribe, + /** Refresh alerts with latest data from hub */ + refresh, + } +})() diff --git a/beszel/site/src/lib/utils.ts b/beszel/site/src/lib/utils.ts index d29820b..678494b 100644 --- a/beszel/site/src/lib/utils.ts +++ b/beszel/site/src/lib/utils.ts @@ -3,22 +3,11 @@ import { toast } from "@/components/ui/use-toast" import { type ClassValue, clsx } from "clsx" import { twMerge } from "tailwind-merge" import { $alerts, $copyContent, $systems, $userSettings, pb } from "./stores" -import { - AlertInfo, - AlertRecord, - ChartTimeData, - ChartTimes, - FingerprintRecord, - SemVer, - SystemRecord, - UserSettings, -} from "@/types" +import type { ChartTimeData, ChartTimes, FingerprintRecord, SemVer, SystemRecord, UserSettings } from "@/types" import { RecordModel, RecordSubscription } from "pocketbase" import { WritableAtom } from "nanostores" import { timeDay, timeHour } from "d3-time" import { useEffect, useState } from "react" -import { CpuIcon, HardDriveIcon, MemoryStickIcon, ServerIcon } from "lucide-react" -import { EthernetIcon, HourglassIcon, ThermometerIcon } from "@/components/ui/icons" import { prependBasePath } from "@/components/router" import { MeterState, Unit } from "./enums" @@ -360,79 +349,6 @@ export async function updateUserSettings() { export const chartMargin = { top: 12 } -/** Alert info for each alert type */ -export const alertInfo: Record = { - Status: { - name: () => t`Status`, - unit: "", - icon: ServerIcon, - desc: () => t`Triggers when status switches between up and down`, - /** "for x minutes" is appended to desc when only one value */ - singleDesc: () => t`System` + " " + t`Down`, - }, - CPU: { - name: () => t`CPU Usage`, - unit: "%", - icon: CpuIcon, - desc: () => t`Triggers when CPU usage exceeds a threshold`, - }, - Memory: { - name: () => t`Memory Usage`, - unit: "%", - icon: MemoryStickIcon, - desc: () => t`Triggers when memory usage exceeds a threshold`, - }, - Disk: { - name: () => t`Disk Usage`, - unit: "%", - icon: HardDriveIcon, - desc: () => t`Triggers when usage of any disk exceeds a threshold`, - }, - Bandwidth: { - name: () => t`Bandwidth`, - unit: " MB/s", - icon: EthernetIcon, - desc: () => t`Triggers when combined up/down exceeds a threshold`, - max: 125, - }, - Temperature: { - name: () => t`Temperature`, - unit: "°C", - icon: ThermometerIcon, - desc: () => t`Triggers when any sensor exceeds a threshold`, - }, - LoadAvg1: { - name: () => t`Load Average 1m`, - unit: "", - icon: HourglassIcon, - max: 100, - min: 0.1, - start: 10, - step: 0.1, - desc: () => t`Triggers when 1 minute load average exceeds a threshold`, - }, - LoadAvg5: { - name: () => t`Load Average 5m`, - unit: "", - icon: HourglassIcon, - max: 100, - min: 0.1, - start: 10, - step: 0.1, - desc: () => t`Triggers when 5 minute load average exceeds a threshold`, - }, - LoadAvg15: { - name: () => t`Load Average 15m`, - unit: "", - icon: HourglassIcon, - min: 0.1, - max: 100, - start: 10, - step: 0.1, - desc: () => t`Triggers when 15 minute load average exceeds a threshold`, - }, -} as const - /** * Retuns value of system host, truncating full path if socket. * @example @@ -532,95 +448,3 @@ export const getSystemNameFromId = (() => { return sysName } })() - -// TODO: reorganize this utils file into more specific files -/** Helper to manage user alerts */ -export const alertManager = (() => { - const collection = pb.collection("alerts") - let unsub: () => void - - /** Fields to fetch from alerts collection */ - const fields = "id,name,system,value,min,triggered" - - /** Fetch alerts from collection */ - async function fetchAlerts(): Promise { - return await collection.getFullList({ fields, sort: "updated" }) - } - - /** Format alerts into a map of system id to alert name to alert record */ - function add(alerts: AlertRecord[]) { - for (const alert of alerts) { - const systemId = alert.system - const systemAlerts = $alerts.get()[systemId] ?? new Map() - const newAlerts = new Map(systemAlerts) - newAlerts.set(alert.name, alert) - $alerts.setKey(systemId, newAlerts) - } - } - - function remove(alerts: Pick[]) { - for (const alert of alerts) { - const systemId = alert.system - const systemAlerts = $alerts.get()[systemId] - const newAlerts = new Map(systemAlerts) - newAlerts.delete(alert.name) - $alerts.setKey(systemId, newAlerts) - } - } - - const actionFns = { - create: add, - update: add, - delete: remove, - } - - // batch alert updates to prevent unnecessary re-renders when adding many alerts at once - const batchUpdate = (() => { - const batch = new Map>() - let timeout: ReturnType - - return (data: RecordSubscription) => { - const { record } = data - batch.set(`${record.system}${record.name}`, data) - clearTimeout(timeout!) - timeout = setTimeout(() => { - const groups = { create: [], update: [], delete: [] } as Record - for (const { action, record } of batch.values()) { - groups[action]?.push(record) - } - for (const key in groups) { - if (groups[key].length) { - actionFns[key as keyof typeof actionFns]?.(groups[key]) - } - } - batch.clear() - }, 50) - } - })() - - async function subscribe() { - unsub = await collection.subscribe("*", batchUpdate, { fields }) - } - - function unsubscribe() { - unsub?.() - } - - async function refresh() { - const records = await fetchAlerts() - add(records) - } - - return { - /** Add alerts to store */ - add, - /** Remove alerts from store */ - remove, - /** Subscribe to alerts */ - subscribe, - /** Unsubscribe from alerts */ - unsubscribe, - /** Refresh alerts with latest data from hub */ - refresh, - } -})() diff --git a/beszel/site/src/main.tsx b/beszel/site/src/main.tsx index 7d371fd..e519aa2 100644 --- a/beszel/site/src/main.tsx +++ b/beszel/site/src/main.tsx @@ -6,7 +6,7 @@ import { Home } from "./components/routes/home.tsx" import { ThemeProvider } from "./components/theme-provider.tsx" import { DirectionProvider } from "@radix-ui/react-direction" import { $authenticated, $systems, pb, $publicKey, $copyContent, $direction } from "./lib/stores.ts" -import { updateUserSettings, updateFavicon, updateSystemList, alertManager } from "./lib/utils.ts" +import { updateUserSettings, updateFavicon, updateSystemList } from "./lib/utils.ts" import { useStore } from "@nanostores/react" import { Toaster } from "./components/ui/toaster.tsx" import { $router } from "./components/router.tsx" @@ -14,8 +14,9 @@ import SystemDetail from "./components/routes/system.tsx" import Navbar from "./components/navbar.tsx" import { I18nProvider } from "@lingui/react" import { i18n } from "@lingui/core" -import { getLocale, dynamicActivate } from "./lib/i18n.ts" -import { SystemStatus } from "./lib/enums.ts" +import { getLocale, dynamicActivate } from "./lib/i18n" +import { SystemStatus } from "./lib/enums" +import { alertManager } from "./lib/alerts" // const ServerDetail = lazy(() => import('./components/routes/system.tsx')) const LoginPage = lazy(() => import("./components/login/login.tsx")) @@ -61,7 +62,7 @@ const App = memo(() => { if (system.status === SystemStatus.Down) { updateFavicon("favicon-red.svg") return - } + } if (system.status === SystemStatus.Up) { up = true }