alerts for cpu, memory, and disk

This commit is contained in:
Henry Dollman
2024-07-22 16:14:55 -04:00
parent b1d994a0ff
commit c060e294f9
8 changed files with 335 additions and 93 deletions

Binary file not shown.

View File

@@ -17,6 +17,7 @@
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slider": "^1.2.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.0",
"@radix-ui/react-toast": "^1.2.1",

View File

@@ -13,9 +13,18 @@ import { cn, isAdmin } from '@/lib/utils'
import { Button } from '@/components/ui/button'
import { Switch } from '@/components/ui/switch'
import { AlertRecord, SystemRecord } from '@/types'
import { useMemo, useState } from 'react'
import { lazy, Suspense, useMemo, useState } from 'react'
import { toast } from './ui/use-toast'
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)
@@ -38,7 +47,7 @@ export default function AlertsButton({ system }: { system: SystemRecord }) {
/>
</Button>
</DialogTrigger>
<DialogContent>
<DialogContent className="max-h-full overflow-auto">
<DialogHeader>
<DialogTitle className="mb-1">Alerts for {system.name}</DialogTitle>
<DialogDescription>
@@ -54,38 +63,57 @@ export default function AlertsButton({ system }: { system: SystemRecord }) {
to ensure alerts are delivered.{' '}
</span>
)}
Webhook delivery and more alert options will be added in the future.
</DialogDescription>
</DialogHeader>
<Alert system={system} alerts={systemAlerts} />
<div className="grid gap-3">
<AlertStatus system={system} alerts={systemAlerts} />
<AlertWithSlider
system={system}
alerts={systemAlerts}
name="CPU"
title="CPU usage"
description="Triggers when CPU usage exceeds a threshold."
/>
<AlertWithSlider
system={system}
alerts={systemAlerts}
name="Memory"
title="Memory usage"
description="Triggers when memory usage exceeds a threshold."
/>
<AlertWithSlider
system={system}
alerts={systemAlerts}
name="Disk"
title="Disk usage"
description="Triggers when disk usage exceeds a threshold."
/>
</div>
</DialogContent>
</Dialog>
)
}
function Alert({ system, alerts }: { system: SystemRecord; alerts: AlertRecord[] }) {
function AlertStatus({ system, alerts }: { system: SystemRecord; alerts: AlertRecord[] }) {
const [pendingChange, setPendingChange] = useState(false)
const alert = useMemo(() => {
return alerts.find((alert) => alert.name === 'status')
return alerts.find((alert) => alert.name === 'Status')
}, [alerts])
return (
<label
htmlFor="status"
htmlFor="alert-status"
className="space-y-2 flex flex-row items-center justify-between rounded-lg border p-4 cursor-pointer"
>
<div className="grid gap-0.5 select-none">
<p className="font-medium text-base">System status</p>
<span
id=":r3m:-form-item-description"
className="block text-[0.8rem] text-foreground opacity-80"
>
<p className="font-medium text-[1.05em]">System status</p>
<span className="block text-[0.85em] text-foreground opacity-80">
Triggers when status switches between up and down.
</span>
</div>
<Switch
id="status"
id="alert-status"
className={cn('transition-opacity', pendingChange && 'opacity-40')}
checked={!!alert}
value={!!alert ? 'on' : 'off'}
@@ -101,15 +129,11 @@ function Alert({ system, alerts }: { system: SystemRecord; alerts: AlertRecord[]
pb.collection('alerts').create({
system: system.id,
user: pb.authStore.model!.id,
name: 'status',
name: 'Status',
})
}
} catch (e) {
toast({
title: 'Failed to update alert',
description: 'Please check logs for more details.',
variant: 'destructive',
})
failedUpdateToast()
} finally {
setPendingChange(false)
}
@@ -118,3 +142,93 @@ function Alert({ system, alerts }: { system: SystemRecord; alerts: AlertRecord[]
</label>
)
}
function AlertWithSlider({
system,
alerts,
name,
title,
description,
}: {
system: SystemRecord
alerts: AlertRecord[]
name: string
title: string
description: string
}) {
const [pendingChange, setPendingChange] = useState(false)
const [liveValue, setLiveValue] = useState(50)
const alert = useMemo(() => {
const alert = alerts.find((alert) => alert.name === name)
if (alert) {
setLiveValue(alert.value)
}
return alert
}, [alerts])
return (
<div className="rounded-lg border">
<label
htmlFor={`alert-${name}`}
className={cn('space-y-2 flex flex-row items-center justify-between cursor-pointer p-4', {
'pb-0': !!alert,
})}
>
<div className="grid gap-0.5 select-none">
<p className="font-medium text-[1.05em]">{title}</p>
<span className="block text-[0.85em] text-foreground opacity-80">{description}</span>
</div>
<Switch
id={`alert-${name}`}
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: liveValue,
})
}
} catch (e) {
failedUpdateToast()
} finally {
setPendingChange(false)
}
}}
/>
</label>
{alert && (
<div className="flex mt-2 mb-3 gap-3 px-4">
<Suspense>
<Slider
defaultValue={[liveValue]}
onValueCommit={(val) => {
pb.collection('alerts').update(alert.id, {
value: val[0],
})
}}
onValueChange={(val) => {
setLiveValue(val[0])
}}
min={10}
max={99}
// step={1}
/>
</Suspense>
<span className="tabular-nums tracking-tighter text-[.92em]">{liveValue}%</span>
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,23 @@
import * as React from 'react'
import * as SliderPrimitive from '@radix-ui/react-slider'
import { cn } from '@/lib/utils'
const Slider = React.forwardRef<
React.ElementRef<typeof SliderPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
>(({ className, ...props }, ref) => (
<SliderPrimitive.Root
ref={ref}
className={cn('relative flex w-full touch-none select-none items-center', className)}
{...props}
>
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
<SliderPrimitive.Range className="absolute h-full bg-primary" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
</SliderPrimitive.Root>
))
Slider.displayName = SliderPrimitive.Root.displayName
export default Slider

View File

@@ -45,7 +45,7 @@ export const updateSystemList = async () => {
export const updateAlerts = () => {
pb.collection('alerts')
.getFullList<AlertRecord>({ fields: 'id,name,system' })
.getFullList<AlertRecord>({ fields: 'id,name,system,value' })
.then((records) => {
$alerts.set(records)
})