mirror of
https://github.com/henrygd/beszel.git
synced 2025-12-17 10:46:16 +01:00
271 lines
7.1 KiB
Go
271 lines
7.1 KiB
Go
import { $alerts, pb } from '@/lib/stores'
|
|
import { useStore } from '@nanostores/react'
|
|
import {
|
|
Dialog,
|
|
DialogTrigger,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog'
|
|
import { BellIcon, ServerIcon } from 'lucide-react'
|
|
import { alertInfo, cn } from '@/lib/utils'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Switch } from '@/components/ui/switch'
|
|
import { AlertRecord, SystemRecord } from '@/types'
|
|
import { lazy, Suspense, useMemo, useState } from 'react'
|
|
import { toast } from './ui/use-toast'
|
|
import { Link } from './router'
|
|
|
|
const Slider = lazy(() => import('./ui/slider'))
|
|
|
|
const failedUpdateToast = () =>
|
|
toast({
|
|
title: 'Failed to update alert',
|
|
description: 'Please check logs for more details.',
|
|
variant: 'destructive',
|
|
})
|
|
|
|
export default function AlertsButton({ system }: { system: SystemRecord }) {
|
|
const alerts = useStore($alerts)
|
|
const [opened, setOpened] = useState(false)
|
|
|
|
const systemAlerts = alerts.filter((alert) => alert.system === system.id) as AlertRecord[]
|
|
|
|
const active = systemAlerts.length > 0
|
|
|
|
return (
|
|
<Dialog>
|
|
<DialogTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size={'icon'}
|
|
aria-label="Alerts"
|
|
data-nolink
|
|
onClick={() => setOpened(true)}
|
|
>
|
|
<BellIcon
|
|
className={cn('h-[1.2em] w-[1.2em] pointer-events-none', {
|
|
'fill-foreground': active,
|
|
})}
|
|
/>
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent
|
|
className="max-h-full overflow-auto max-w-[35rem]"
|
|
// onCloseAutoFocus={() => setOpened(false)}
|
|
>
|
|
{opened && (
|
|
<>
|
|
<DialogHeader>
|
|
<DialogTitle className="text-xl">{system.name} alerts</DialogTitle>
|
|
<DialogDescription className="mb-1">
|
|
See{' '}
|
|
<Link href="/settings/notifications" className="link">
|
|
notification settings
|
|
</Link>{' '}
|
|
to configure how you receive alerts.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="grid gap-3">
|
|
<AlertStatus system={system} alerts={systemAlerts} />
|
|
{Object.keys(alertInfo).map((key) => {
|
|
const alert = alertInfo[key as keyof typeof alertInfo]
|
|
return (
|
|
<AlertWithSlider
|
|
key={key}
|
|
system={system}
|
|
alerts={systemAlerts}
|
|
name={key}
|
|
title={alert.name}
|
|
description={alert.desc}
|
|
unit={alert.unit}
|
|
Icon={alert.icon}
|
|
/>
|
|
)
|
|
})}
|
|
</div>
|
|
</>
|
|
)}
|
|
</DialogContent>
|
|
</Dialog>
|
|
)
|
|
}
|
|
|
|
function AlertStatus({ system, alerts }: { system: SystemRecord; alerts: AlertRecord[] }) {
|
|
const [pendingChange, setPendingChange] = useState(false)
|
|
|
|
const alert = alerts.find((alert) => alert.name === 'Status')
|
|
|
|
return (
|
|
<label
|
|
htmlFor="alert-status"
|
|
className="flex flex-row items-center justify-between gap-4 rounded-lg border border-muted-foreground/15 hover:border-muted-foreground/20 transition-colors duration-100 p-4 cursor-pointer"
|
|
>
|
|
<div className="grid gap-1 select-none">
|
|
<p className="font-semibold flex gap-3 items-center">
|
|
<ServerIcon className="h-4 w-4 opacity-85" /> System Status
|
|
</p>
|
|
<span className="block text-sm text-muted-foreground">
|
|
Triggers when status switches between up and down.
|
|
</span>
|
|
</div>
|
|
<Switch
|
|
id="alert-status"
|
|
className={cn('transition-opacity', pendingChange && 'opacity-40')}
|
|
checked={!!alert}
|
|
value={!!alert ? 'on' : 'off'}
|
|
onCheckedChange={async (active) => {
|
|
if (pendingChange) {
|
|
return
|
|
}
|
|
setPendingChange(true)
|
|
try {
|
|
if (!active && alert) {
|
|
await pb.collection('alerts').delete(alert.id)
|
|
} else if (active) {
|
|
pb.collection('alerts').create({
|
|
system: system.id,
|
|
user: pb.authStore.model!.id,
|
|
name: 'Status',
|
|
})
|
|
}
|
|
} catch (e) {
|
|
failedUpdateToast()
|
|
} finally {
|
|
setPendingChange(false)
|
|
}
|
|
}}
|
|
/>
|
|
</label>
|
|
)
|
|
}
|
|
|
|
function AlertWithSlider({
|
|
system,
|
|
alerts,
|
|
name,
|
|
title,
|
|
description,
|
|
unit = '%',
|
|
max = 99,
|
|
Icon,
|
|
}: {
|
|
system: SystemRecord
|
|
alerts: AlertRecord[]
|
|
name: string
|
|
title: string
|
|
description: string
|
|
unit?: string
|
|
max?: number
|
|
Icon: React.FC<React.SVGProps<SVGSVGElement>>
|
|
}) {
|
|
const [pendingChange, setPendingChange] = useState(false)
|
|
const [value, setValue] = useState(80)
|
|
const [min, setMin] = useState(10)
|
|
|
|
const key = name.replaceAll(' ', '-')
|
|
|
|
const alert = useMemo(() => {
|
|
const alert = alerts.find((alert) => alert.name === name)
|
|
if (alert) {
|
|
setValue(alert.value)
|
|
setMin(alert.min || 1)
|
|
}
|
|
return alert
|
|
}, [alerts])
|
|
|
|
const updateAlert = (obj: Partial<AlertRecord>) => {
|
|
obj.triggered = false
|
|
alert && pb.collection('alerts').update(alert.id, obj)
|
|
}
|
|
|
|
return (
|
|
<div className="rounded-lg border border-muted-foreground/15 hover:border-muted-foreground/20 transition-colors duration-100 group">
|
|
<label
|
|
htmlFor={`s${key}`}
|
|
className={cn('flex flex-row items-center justify-between gap-4 cursor-pointer p-4', {
|
|
'pb-0': !!alert,
|
|
})}
|
|
>
|
|
<div className="grid gap-1 select-none">
|
|
<p className="font-semibold flex gap-3 items-center capitalize">
|
|
<Icon className="h-4 w-4 opacity-85" /> {title}
|
|
</p>
|
|
{!alert && <span className="block text-sm text-muted-foreground">{description}</span>}
|
|
</div>
|
|
<Switch
|
|
id={`s${key}`}
|
|
className={cn('transition-opacity', pendingChange && 'opacity-40')}
|
|
checked={!!alert}
|
|
value={!!alert ? 'on' : 'off'}
|
|
onCheckedChange={async (active) => {
|
|
if (pendingChange) {
|
|
return
|
|
}
|
|
setPendingChange(true)
|
|
try {
|
|
if (!active && alert) {
|
|
await pb.collection('alerts').delete(alert.id)
|
|
} else if (active) {
|
|
pb.collection('alerts').create({
|
|
system: system.id,
|
|
user: pb.authStore.model!.id,
|
|
name,
|
|
value: value,
|
|
min: min,
|
|
})
|
|
}
|
|
} catch (e) {
|
|
failedUpdateToast()
|
|
} finally {
|
|
setPendingChange(false)
|
|
}
|
|
}}
|
|
/>
|
|
</label>
|
|
{alert && (
|
|
<div className="grid sm:grid-cols-2 mt-1.5 gap-5 px-4 pb-5 tabular-nums text-muted-foreground">
|
|
<Suspense fallback={<div className="h-10" />}>
|
|
<div>
|
|
<p id={`v${key}`} className="text-sm block h-8">
|
|
Average exceeds{' '}
|
|
<strong className="text-foreground">
|
|
{value}
|
|
{unit}
|
|
</strong>
|
|
</p>
|
|
<div className="flex gap-3">
|
|
<Slider
|
|
aria-labelledby={`v${key}`}
|
|
defaultValue={[value]}
|
|
onValueCommit={(val) => updateAlert({ value: val[0] })}
|
|
onValueChange={(val) => setValue(val[0])}
|
|
min={1}
|
|
max={max}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<p id={`t${key}`} className="text-sm block h-8">
|
|
For <strong className="text-foreground">{min}</strong> minute
|
|
{min > 1 && 's'}
|
|
</p>
|
|
<div className="flex gap-3">
|
|
<Slider
|
|
aria-labelledby={`v${key}`}
|
|
defaultValue={[min]}
|
|
onValueCommit={(val) => updateAlert({ min: val[0] })}
|
|
onValueChange={(val) => setMin(val[0])}
|
|
min={1}
|
|
max={60}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</Suspense>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|