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 systems = useStore($systems)
// todo: maybe remove active alert if changed
const activeAlerts = useMemo(() => {
if (!systems.length) {
return []
const activeAlerts = alerts.filter((alert) => {
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
}
return true
})
return activeAlerts
}, [alerts])
@@ -61,21 +63,21 @@ export default function () {
</CardHeader>
<CardContent className="max-sm:p-2">
{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) => {
const a = alertInfo[alert.name as keyof typeof alertInfo]
const info = alertInfo[alert.name as keyof typeof alertInfo]
return (
<Alert
key={alert.id}
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">
{alert.sysname} {a.name}
{alert.sysname} {info.name}
</AlertTitle>
<AlertDescription>
Exceeds {alert.value}
{a.unit} threshold in last {alert.min} min
{info.unit} average in last {alert.min} min
</AlertDescription>
<Link
href={`/system/${encodeURIComponent(alert.sysname!)}`}

View File

@@ -8,15 +8,14 @@ import {
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { BellIcon, CpuIcon, HardDriveIcon, MemoryStickIcon, ServerIcon } from 'lucide-react'
import { cn } from '@/lib/utils'
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'
import { EthernetIcon, ThermometerIcon } from './ui/icons'
const Slider = lazy(() => import('./ui/slider'))
@@ -29,19 +28,22 @@ const failedUpdateToast = () =>
export default function AlertsButton({ system }: { system: SystemRecord }) {
const alerts = useStore($alerts)
const [opened, setOpened] = useState(false)
const active = useMemo(() => {
return alerts.find((alert) => alert.system === system.id)
}, [alerts, system])
const systemAlerts = alerts.filter((alert) => alert.system === system.id) as AlertRecord[]
const systemAlerts = useMemo(() => {
return alerts.filter((alert) => alert.system === system.id) as AlertRecord[]
}, [alerts, system])
const active = systemAlerts.length > 0
return (
<Dialog>
<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
className={cn('h-[1.2em] w-[1.2em] pointer-events-none', {
'fill-foreground': active,
@@ -49,7 +51,12 @@ export default function AlertsButton({ system }: { system: SystemRecord }) {
/>
</Button>
</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>
<DialogTitle className="text-xl">{system.name} alerts</DialogTitle>
<DialogDescription className="mb-1">
@@ -62,49 +69,24 @@ export default function AlertsButton({ system }: { system: SystemRecord }) {
</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="CPU"
title="CPU usage"
description="Triggers when CPU usage exceeds a threshold."
Icon={CpuIcon}
/>
<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}
name={key}
title={alert.name}
description={alert.desc}
unit={alert.unit}
Icon={alert.icon}
/>
)
})}
</div>
</>
)}
</DialogContent>
</Dialog>
)
@@ -113,9 +95,7 @@ export default function AlertsButton({ system }: { system: SystemRecord }) {
function AlertStatus({ system, alerts }: { system: SystemRecord; alerts: AlertRecord[] }) {
const [pendingChange, setPendingChange] = useState(false)
const alert = useMemo(() => {
return alerts.find((alert) => alert.name === 'Status')
}, [alerts])
const alert = alerts.find((alert) => alert.name === 'Status')
return (
<label
@@ -198,7 +178,7 @@ function AlertWithSlider({
return (
<div className="rounded-lg border border-muted-foreground/15 hover:border-muted-foreground/20 transition-colors duration-100 group">
<label
htmlFor={`v${key}`}
htmlFor={`s${key}`}
className={cn('flex flex-row items-center justify-between gap-4 cursor-pointer p-4', {
'pb-0': !!alert,
})}
@@ -210,7 +190,7 @@ function AlertWithSlider({
{!alert && <span className="block text-sm text-muted-foreground">{description}</span>}
</div>
<Switch
id={`v${key}`}
id={`s${key}`}
className={cn('transition-opacity', pendingChange && 'opacity-40')}
checked={!!alert}
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">
<Suspense fallback={<div className="h-10" />}>
<div>
<label htmlFor={`v${key}`} className="text-sm block h-8">
<p id={`v${key}`} className="text-sm block h-8">
Average exceeds{' '}
<strong className="text-foreground">
{liveValue}
{unit}
</strong>
</label>
</p>
<div className="flex gap-3">
<Slider
id={`v${key}`}
aria-labelledby={`v${key}`}
defaultValue={[liveValue]}
onValueCommit={(val) => {
pb.collection('alerts').update(alert.id, {
@@ -266,13 +246,13 @@ function AlertWithSlider({
</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
{liveMinutes > 1 && 's'}
</label>
</p>
<div className="flex gap-3">
<Slider
id={`t${key}`}
aria-labelledby={`v${key}`}
defaultValue={[liveMinutes]}
onValueCommit={(val) => {
pb.collection('alerts').update(alert.id, {

View File

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

View File

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