import { t } from "@lingui/core/macro" import { Trans, Plural } from "@lingui/react/macro" import { $alerts, $systems, pb } from "@/lib/stores" import { alertInfo, cn } from "@/lib/utils" import { Switch } from "@/components/ui/switch" import { AlertInfo, AlertRecord, SystemRecord } from "@/types" import { lazy, Suspense, useMemo, useState } from "react" import { toast } from "../ui/use-toast" import { BatchService } from "pocketbase" import { getSemaphore } from "@henrygd/semaphore" interface AlertData { checked?: boolean val?: number min?: number updateAlert?: (checked: boolean, value: number, min: number) => void name: keyof typeof alertInfo alert: AlertInfo system: SystemRecord } const Slider = lazy(() => import("@/components/ui/slider")) const failedUpdateToast = () => toast({ title: t`Failed to update alert`, description: t`Please check logs for more details.`, variant: "destructive", }) export function SystemAlert({ system, systemAlerts, data, }: { system: SystemRecord systemAlerts: AlertRecord[] data: AlertData }) { const alert = systemAlerts.find((alert) => alert.name === data.name) data.updateAlert = async (checked: boolean, value: number, min: number) => { try { if (alert && !checked) { await pb.collection("alerts").delete(alert.id) } else if (alert && checked) { await pb.collection("alerts").update(alert.id, { value, min, triggered: false }) } else if (checked) { pb.collection("alerts").create({ system: system.id, user: pb.authStore.record!.id, name: data.name, value: value, min: min, }) } } catch (e) { failedUpdateToast() } } if (alert) { data.checked = true data.val = alert.value data.min = alert.min || 1 } return } export const SystemAlertGlobal = ({ data, overwrite }: { data: AlertData; overwrite: boolean | "indeterminate" }) => { data.checked = false data.val = data.min = 0 // set of system ids that have an alert for this name when the component is mounted const existingAlertsSystems = useMemo(() => { const map = new Set() const alerts = $alerts.get() for (const alert of alerts) { if (alert.name === data.name) { map.add(alert.system) } } return map }, []) data.updateAlert = async (checked: boolean, value: number, min: number) => { const sem = getSemaphore("alerts") await sem.acquire() try { // if another update is waiting behind, don't start this one if (sem.size() > 1) { return } const recordData: Partial = { value, min, triggered: false, } const batch = batchWrapper("alerts", 25) const systems = $systems.get() const currentAlerts = $alerts.get() // map of current alerts with this name right now by system id const currentAlertsSystems = new Map() for (const alert of currentAlerts) { if (alert.name === data.name) { currentAlertsSystems.set(alert.system, alert) } } if (overwrite) { existingAlertsSystems.clear() } const processSystem = async (system: SystemRecord): Promise => { const existingAlert = existingAlertsSystems.has(system.id) if (!overwrite && existingAlert) { return } const currentAlert = currentAlertsSystems.get(system.id) // delete existing alert if unchecked if (!checked && currentAlert) { return batch.remove(currentAlert.id) } if (checked && currentAlert) { // update existing alert if checked return batch.update(currentAlert.id, recordData) } if (checked) { // create new alert if checked and not existing return batch.create({ system: system.id, user: pb.authStore.record!.id, name: data.name, ...recordData, }) } } // make sure current system is updated in the first batch await processSystem(data.system) for (const system of systems) { if (system.id === data.system.id) { continue } if (sem.size() > 1) { return } await processSystem(system) } await batch.send() } finally { sem.release() } } return } /** * Creates a wrapper for performing batch operations on a specified collection. */ function batchWrapper(collection: string, batchSize: number) { let batch: BatchService | undefined let count = 0 const create = async >(options: T) => { batch ||= pb.createBatch() batch.collection(collection).create(options) if (++count >= batchSize) { await send() } } const update = async >(id: string, data: T) => { batch ||= pb.createBatch() batch.collection(collection).update(id, data) if (++count >= batchSize) { await send() } } const remove = async (id: string) => { batch ||= pb.createBatch() batch.collection(collection).delete(id) if (++count >= batchSize) { await send() } } const send = async () => { if (count) { await batch?.send({ requestKey: null }) batch = undefined count = 0 } } return { update, remove, send, create, } } function AlertContent({ data }: { data: AlertData }) { const { name } = data const singleDescription = data.alert.singleDesc?.() const [checked, setChecked] = useState(data.checked || false) const [min, setMin] = useState(data.min || 10) const [value, setValue] = useState(data.val || (singleDescription ? 0 : 80)) const Icon = alertInfo[name].icon return (
{checked && (
}> {!singleDescription && (

Average exceeds{" "} {value} {data.alert.unit}

{ data.updateAlert?.(true, val[0], min) }} onValueChange={(val) => { setValue(val[0]) }} min={1} max={alertInfo[name].max ?? 99} />
)}

{singleDescription && ( <> {singleDescription} {` `} )} For {min}{" "}

{ data.updateAlert?.(true, value, min[0]) }} onValueChange={(val) => { setMin(val[0]) }} min={1} max={60} />
)}
) }