mirror of
https://github.com/fankes/beszel.git
synced 2025-10-20 02:09:28 +08:00
alerts web ui refactoring
This commit is contained in:
@@ -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) {
|
||||||
const activeAlerts = alerts.filter((alert) => alert.triggered && alert.name in alertInfo)
|
return false
|
||||||
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!)}`}
|
||||||
|
@@ -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,62 +51,42 @@ export default function AlertsButton({ system }: { system: SystemRecord }) {
|
|||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-full overflow-auto max-w-[35rem]">
|
<DialogContent
|
||||||
<DialogHeader>
|
className="max-h-full overflow-auto max-w-[35rem]"
|
||||||
<DialogTitle className="text-xl">{system.name} alerts</DialogTitle>
|
// onCloseAutoFocus={() => setOpened(false)}
|
||||||
<DialogDescription className="mb-1">
|
>
|
||||||
See{' '}
|
{opened && (
|
||||||
<Link href="/settings/notifications" className="link">
|
<>
|
||||||
notification settings
|
<DialogHeader>
|
||||||
</Link>{' '}
|
<DialogTitle className="text-xl">{system.name} alerts</DialogTitle>
|
||||||
to configure how you receive alerts.
|
<DialogDescription className="mb-1">
|
||||||
</DialogDescription>
|
See{' '}
|
||||||
</DialogHeader>
|
<Link href="/settings/notifications" className="link">
|
||||||
<div className="grid gap-3">
|
notification settings
|
||||||
<AlertStatus system={system} alerts={systemAlerts} />
|
</Link>{' '}
|
||||||
<AlertWithSlider
|
to configure how you receive alerts.
|
||||||
system={system}
|
</DialogDescription>
|
||||||
alerts={systemAlerts}
|
</DialogHeader>
|
||||||
name="CPU"
|
<div className="grid gap-3">
|
||||||
title="CPU usage"
|
<AlertStatus system={system} alerts={systemAlerts} />
|
||||||
description="Triggers when CPU usage exceeds a threshold."
|
{Object.keys(alertInfo).map((key) => {
|
||||||
Icon={CpuIcon}
|
const alert = alertInfo[key as keyof typeof alertInfo]
|
||||||
/>
|
return (
|
||||||
<AlertWithSlider
|
<AlertWithSlider
|
||||||
system={system}
|
key={key}
|
||||||
alerts={systemAlerts}
|
system={system}
|
||||||
name="Memory"
|
alerts={systemAlerts}
|
||||||
title="Memory usage"
|
name={key}
|
||||||
description="Triggers when memory usage exceeds a threshold."
|
title={alert.name}
|
||||||
Icon={MemoryStickIcon}
|
description={alert.desc}
|
||||||
/>
|
unit={alert.unit}
|
||||||
<AlertWithSlider
|
Icon={alert.icon}
|
||||||
system={system}
|
/>
|
||||||
alerts={systemAlerts}
|
)
|
||||||
name="Disk"
|
})}
|
||||||
title="Disk usage"
|
</div>
|
||||||
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>
|
|
||||||
</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, {
|
||||||
|
@@ -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.',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
|
Reference in New Issue
Block a user