alerts web ui refactoring

This commit is contained in:
Henry Dollman
2024-10-16 13:48:36 -04:00
parent 02641ec007
commit abff85d61e
4 changed files with 78 additions and 91 deletions

View File

@@ -18,14 +18,16 @@ export default function () {
const alerts = useStore($alerts) const alerts = useStore($alerts)
const systems = useStore($systems) const systems = useStore($systems)
// todo: maybe remove active alert if changed
const activeAlerts = useMemo(() => { const activeAlerts = useMemo(() => {
if (!systems.length) { const activeAlerts = alerts.filter((alert) => {
return [] const active = alert.triggered && alert.name in alertInfo
if (!active) {
return false
} }
const activeAlerts = alerts.filter((alert) => alert.triggered && alert.name in alertInfo)
for (const alert of activeAlerts) {
alert.sysname = systems.find((system) => system.id === alert.system)?.name alert.sysname = systems.find((system) => system.id === alert.system)?.name
} return true
})
return activeAlerts return activeAlerts
}, [alerts]) }, [alerts])
@@ -61,21 +63,21 @@ export default function () {
</CardHeader> </CardHeader>
<CardContent className="max-sm:p-2"> <CardContent className="max-sm:p-2">
{activeAlerts.length > 0 && ( {activeAlerts.length > 0 && (
<div className="grid sm:grid-cols-2 md:grid-cols-3 gap-3 mb-1"> <div className="grid sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-3">
{activeAlerts.map((alert) => { {activeAlerts.map((alert) => {
const a = alertInfo[alert.name as keyof typeof alertInfo] const info = alertInfo[alert.name as keyof typeof alertInfo]
return ( return (
<Alert <Alert
key={alert.id} key={alert.id}
className="hover:-translate-y-[1px] duration-200 bg-transparent border-foreground/10 hover:shadow-md shadow-black" className="hover:-translate-y-[1px] duration-200 bg-transparent border-foreground/10 hover:shadow-md shadow-black"
> >
<a.icon className="h-4 w-4" /> <info.icon className="h-4 w-4" />
<AlertTitle className="mb-2"> <AlertTitle className="mb-2">
{alert.sysname} {a.name} {alert.sysname} {info.name}
</AlertTitle> </AlertTitle>
<AlertDescription> <AlertDescription>
Exceeds {alert.value} Exceeds {alert.value}
{a.unit} threshold in last {alert.min} min {info.unit} average in last {alert.min} min
</AlertDescription> </AlertDescription>
<Link <Link
href={`/system/${encodeURIComponent(alert.sysname!)}`} href={`/system/${encodeURIComponent(alert.sysname!)}`}

View File

@@ -8,15 +8,14 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from '@/components/ui/dialog' } from '@/components/ui/dialog'
import { BellIcon, CpuIcon, HardDriveIcon, MemoryStickIcon, ServerIcon } from 'lucide-react' import { BellIcon, ServerIcon } from 'lucide-react'
import { cn } from '@/lib/utils' import { alertInfo, cn } from '@/lib/utils'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Switch } from '@/components/ui/switch' import { Switch } from '@/components/ui/switch'
import { AlertRecord, SystemRecord } from '@/types' import { AlertRecord, SystemRecord } from '@/types'
import { lazy, Suspense, useMemo, useState } from 'react' import { lazy, Suspense, useMemo, useState } from 'react'
import { toast } from './ui/use-toast' import { toast } from './ui/use-toast'
import { Link } from './router' import { Link } from './router'
import { EthernetIcon, ThermometerIcon } from './ui/icons'
const Slider = lazy(() => import('./ui/slider')) const Slider = lazy(() => import('./ui/slider'))
@@ -29,19 +28,22 @@ const failedUpdateToast = () =>
export default function AlertsButton({ system }: { system: SystemRecord }) { export default function AlertsButton({ system }: { system: SystemRecord }) {
const alerts = useStore($alerts) const alerts = useStore($alerts)
const [opened, setOpened] = useState(false)
const active = useMemo(() => { const systemAlerts = alerts.filter((alert) => alert.system === system.id) as AlertRecord[]
return alerts.find((alert) => alert.system === system.id)
}, [alerts, system])
const systemAlerts = useMemo(() => { const active = systemAlerts.length > 0
return alerts.filter((alert) => alert.system === system.id) as AlertRecord[]
}, [alerts, system])
return ( return (
<Dialog> <Dialog>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button variant="ghost" size={'icon'} aria-label="Alerts" data-nolink> <Button
variant="ghost"
size={'icon'}
aria-label="Alerts"
data-nolink
onClick={() => setOpened(true)}
>
<BellIcon <BellIcon
className={cn('h-[1.2em] w-[1.2em] pointer-events-none', { className={cn('h-[1.2em] w-[1.2em] pointer-events-none', {
'fill-foreground': active, 'fill-foreground': active,
@@ -49,7 +51,12 @@ export default function AlertsButton({ system }: { system: SystemRecord }) {
/> />
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent className="max-h-full overflow-auto max-w-[35rem]"> <DialogContent
className="max-h-full overflow-auto max-w-[35rem]"
// onCloseAutoFocus={() => setOpened(false)}
>
{opened && (
<>
<DialogHeader> <DialogHeader>
<DialogTitle className="text-xl">{system.name} alerts</DialogTitle> <DialogTitle className="text-xl">{system.name} alerts</DialogTitle>
<DialogDescription className="mb-1"> <DialogDescription className="mb-1">
@@ -62,49 +69,24 @@ export default function AlertsButton({ system }: { system: SystemRecord }) {
</DialogHeader> </DialogHeader>
<div className="grid gap-3"> <div className="grid gap-3">
<AlertStatus system={system} alerts={systemAlerts} /> <AlertStatus system={system} alerts={systemAlerts} />
{Object.keys(alertInfo).map((key) => {
const alert = alertInfo[key as keyof typeof alertInfo]
return (
<AlertWithSlider <AlertWithSlider
key={key}
system={system} system={system}
alerts={systemAlerts} alerts={systemAlerts}
name="CPU" name={key}
title="CPU usage" title={alert.name}
description="Triggers when CPU usage exceeds a threshold." description={alert.desc}
Icon={CpuIcon} unit={alert.unit}
/> Icon={alert.icon}
<AlertWithSlider
system={system}
alerts={systemAlerts}
name="Memory"
title="Memory usage"
description="Triggers when memory usage exceeds a threshold."
Icon={MemoryStickIcon}
/>
<AlertWithSlider
system={system}
alerts={systemAlerts}
name="Disk"
title="Disk usage"
description="Triggers when root usage exceeds a threshold."
Icon={HardDriveIcon}
/>
<AlertWithSlider
system={system}
alerts={systemAlerts}
name="Bandwidth"
title="Bandwidth"
description="Triggers when combined up/down exceeds a threshold."
unit=" MB/s"
Icon={EthernetIcon}
/>
<AlertWithSlider
system={system}
alerts={systemAlerts}
name="Temperature"
title="Temperature"
description="Triggers when any sensor exceeds a threshold."
unit=" °C"
Icon={ThermometerIcon}
/> />
)
})}
</div> </div>
</>
)}
</DialogContent> </DialogContent>
</Dialog> </Dialog>
) )
@@ -113,9 +95,7 @@ export default function AlertsButton({ system }: { system: SystemRecord }) {
function AlertStatus({ system, alerts }: { system: SystemRecord; alerts: AlertRecord[] }) { function AlertStatus({ system, alerts }: { system: SystemRecord; alerts: AlertRecord[] }) {
const [pendingChange, setPendingChange] = useState(false) const [pendingChange, setPendingChange] = useState(false)
const alert = useMemo(() => { const alert = alerts.find((alert) => alert.name === 'Status')
return alerts.find((alert) => alert.name === 'Status')
}, [alerts])
return ( return (
<label <label
@@ -198,7 +178,7 @@ function AlertWithSlider({
return ( return (
<div className="rounded-lg border border-muted-foreground/15 hover:border-muted-foreground/20 transition-colors duration-100 group"> <div className="rounded-lg border border-muted-foreground/15 hover:border-muted-foreground/20 transition-colors duration-100 group">
<label <label
htmlFor={`v${key}`} htmlFor={`s${key}`}
className={cn('flex flex-row items-center justify-between gap-4 cursor-pointer p-4', { className={cn('flex flex-row items-center justify-between gap-4 cursor-pointer p-4', {
'pb-0': !!alert, 'pb-0': !!alert,
})} })}
@@ -210,7 +190,7 @@ function AlertWithSlider({
{!alert && <span className="block text-sm text-muted-foreground">{description}</span>} {!alert && <span className="block text-sm text-muted-foreground">{description}</span>}
</div> </div>
<Switch <Switch
id={`v${key}`} id={`s${key}`}
className={cn('transition-opacity', pendingChange && 'opacity-40')} className={cn('transition-opacity', pendingChange && 'opacity-40')}
checked={!!alert} checked={!!alert}
value={!!alert ? 'on' : 'off'} value={!!alert ? 'on' : 'off'}
@@ -243,16 +223,16 @@ function AlertWithSlider({
<div className="grid sm:grid-cols-2 mt-1.5 gap-5 px-4 pb-5 tabular-nums text-muted-foreground"> <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" />}> <Suspense fallback={<div className="h-10" />}>
<div> <div>
<label htmlFor={`v${key}`} className="text-sm block h-8"> <p id={`v${key}`} className="text-sm block h-8">
Average exceeds{' '} Average exceeds{' '}
<strong className="text-foreground"> <strong className="text-foreground">
{liveValue} {liveValue}
{unit} {unit}
</strong> </strong>
</label> </p>
<div className="flex gap-3"> <div className="flex gap-3">
<Slider <Slider
id={`v${key}`} aria-labelledby={`v${key}`}
defaultValue={[liveValue]} defaultValue={[liveValue]}
onValueCommit={(val) => { onValueCommit={(val) => {
pb.collection('alerts').update(alert.id, { pb.collection('alerts').update(alert.id, {
@@ -266,13 +246,13 @@ function AlertWithSlider({
</div> </div>
</div> </div>
<div> <div>
<label htmlFor={`t${key}`} className="text-sm block h-8"> <p id={`t${key}`} className="text-sm block h-8">
For <strong className="text-foreground">{liveMinutes}</strong> minute For <strong className="text-foreground">{liveMinutes}</strong> minute
{liveMinutes > 1 && 's'} {liveMinutes > 1 && 's'}
</label> </p>
<div className="flex gap-3"> <div className="flex gap-3">
<Slider <Slider
id={`t${key}`} aria-labelledby={`v${key}`}
defaultValue={[liveMinutes]} defaultValue={[liveMinutes]}
onValueCommit={(val) => { onValueCommit={(val) => {
pb.collection('alerts').update(alert.id, { pb.collection('alerts').update(alert.id, {

View File

@@ -303,25 +303,30 @@ export const alertInfo = {
name: 'CPU usage', name: 'CPU usage',
unit: '%', unit: '%',
icon: CpuIcon, icon: CpuIcon,
desc: 'Triggers when CPU usage exceeds a threshold.',
}, },
Memory: { Memory: {
name: 'Memory usage', name: 'Memory usage',
unit: '%', unit: '%',
icon: MemoryStickIcon, icon: MemoryStickIcon,
desc: 'Triggers when memory usage exceeds a threshold.',
}, },
Disk: { Disk: {
name: 'Disk usage', name: 'Disk usage',
unit: '%', unit: '%',
icon: HardDriveIcon, icon: HardDriveIcon,
desc: 'Triggers when root usage exceeds a threshold.',
}, },
Bandwidth: { Bandwidth: {
name: 'Bandwidth', name: 'Bandwidth',
unit: ' MB/s', unit: ' MB/s',
icon: EthernetIcon, icon: EthernetIcon,
desc: 'Triggers when combined up/down exceeds a threshold.',
}, },
Temperature: { Temperature: {
name: 'Temperature', name: 'Temperature',
unit: '°C', unit: '°C',
icon: ThermometerIcon, icon: ThermometerIcon,
desc: 'Triggers when any sensor exceeds a threshold.',
}, },
} }

View File

@@ -70,9 +70,9 @@ const App = () => {
$hubVersion.set(data.v) $hubVersion.set(data.v)
}) })
// get servers / alerts / settings // get servers / alerts / settings
updateSystemList()
updateAlerts()
updateUserSettings() updateUserSettings()
// get alerts after system list is loaded
updateSystemList().then(updateAlerts)
}, []) }, [])
// update favicon // update favicon