mirror of
https://github.com/fankes/beszel.git
synced 2025-10-20 18:29:29 +08:00
update alerts dialog and icon imports
This commit is contained in:
@@ -15,7 +15,7 @@ import ChartTimeSelect from '../charts/chart-time-select'
|
|||||||
import { chartTimeData, cn, getPbTimestamp, useLocalStorage } from '@/lib/utils'
|
import { chartTimeData, cn, getPbTimestamp, useLocalStorage } from '@/lib/utils'
|
||||||
import { Separator } from '../ui/separator'
|
import { Separator } from '../ui/separator'
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip'
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip'
|
||||||
import { Button, buttonVariants } from '../ui/button'
|
import { Button } from '../ui/button'
|
||||||
import { Input } from '../ui/input'
|
import { Input } from '../ui/input'
|
||||||
import { ChartAverage, ChartMax, Rows, TuxIcon } from '../ui/icons'
|
import { ChartAverage, ChartMax, Rows, TuxIcon } from '../ui/icons'
|
||||||
import { useIntersectionObserver } from '@/lib/use-intersection-observer'
|
import { useIntersectionObserver } from '@/lib/use-intersection-observer'
|
||||||
@@ -286,7 +286,7 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
<div id="chartwrap" className="grid gap-4 mb-10 overflow-x-clip">
|
<div id="chartwrap" className="grid gap-4 mb-10 overflow-x-clip">
|
||||||
{/* system info */}
|
{/* system info */}
|
||||||
<Card>
|
<Card>
|
||||||
<div className="grid lg:flex items-center gap-4 px-4 sm:px-6 pt-3 sm:pt-4 pb-5">
|
<div className="grid lg:flex gap-4 px-4 sm:px-6 pt-3 sm:pt-4 pb-5">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-[1.6rem] font-semibold mb-1.5">{system.name}</h1>
|
<h1 className="text-[1.6rem] font-semibold mb-1.5">{system.name}</h1>
|
||||||
<div className="flex flex-wrap items-center gap-3 gap-y-2 text-sm opacity-90">
|
<div className="flex flex-wrap items-center gap-3 gap-y-2 text-sm opacity-90">
|
||||||
|
@@ -44,17 +44,17 @@ import {
|
|||||||
|
|
||||||
import { SystemRecord } from '@/types'
|
import { SystemRecord } from '@/types'
|
||||||
import {
|
import {
|
||||||
MoreHorizontal,
|
MoreHorizontalIcon,
|
||||||
ArrowUpDown,
|
ArrowUpDownIcon,
|
||||||
Server,
|
MemoryStickIcon,
|
||||||
Cpu,
|
|
||||||
MemoryStick,
|
|
||||||
HardDrive,
|
|
||||||
CopyIcon,
|
CopyIcon,
|
||||||
PauseCircleIcon,
|
PauseCircleIcon,
|
||||||
PlayCircleIcon,
|
PlayCircleIcon,
|
||||||
Trash2Icon,
|
Trash2Icon,
|
||||||
WifiIcon,
|
WifiIcon,
|
||||||
|
HardDriveIcon,
|
||||||
|
ServerIcon,
|
||||||
|
CpuIcon,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { $hubVersion, $systems, pb } from '@/lib/stores'
|
import { $hubVersion, $systems, pb } from '@/lib/stores'
|
||||||
@@ -96,7 +96,7 @@ function sortableHeader(
|
|||||||
>
|
>
|
||||||
<Icon className="mr-2 h-4 w-4" />
|
<Icon className="mr-2 h-4 w-4" />
|
||||||
{name}
|
{name}
|
||||||
{!hideSortIcon && <ArrowUpDown className="ml-2 h-4 w-4" />}
|
{!hideSortIcon && <ArrowUpDownIcon className="ml-2 h-4 w-4" />}
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -145,22 +145,22 @@ export default function SystemsTable({ filter }: { filter?: string }) {
|
|||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
header: ({ column }) => sortableHeader(column, 'System', Server),
|
header: ({ column }) => sortableHeader(column, 'System', ServerIcon),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'info.cpu',
|
accessorKey: 'info.cpu',
|
||||||
cell: CellFormatter,
|
cell: CellFormatter,
|
||||||
header: ({ column }) => sortableHeader(column, 'CPU', Cpu),
|
header: ({ column }) => sortableHeader(column, 'CPU', CpuIcon),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'info.mp',
|
accessorKey: 'info.mp',
|
||||||
cell: CellFormatter,
|
cell: CellFormatter,
|
||||||
header: ({ column }) => sortableHeader(column, 'Memory', MemoryStick),
|
header: ({ column }) => sortableHeader(column, 'Memory', MemoryStickIcon),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'info.dp',
|
accessorKey: 'info.dp',
|
||||||
cell: CellFormatter,
|
cell: CellFormatter,
|
||||||
header: ({ column }) => sortableHeader(column, 'Disk', HardDrive),
|
header: ({ column }) => sortableHeader(column, 'Disk', HardDriveIcon),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'info.b',
|
accessorKey: 'info.b',
|
||||||
@@ -212,7 +212,7 @@ export default function SystemsTable({ filter }: { filter?: string }) {
|
|||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="ghost" size={'icon'} data-nolink>
|
<Button variant="ghost" size={'icon'} data-nolink>
|
||||||
<span className="sr-only">Open menu</span>
|
<span className="sr-only">Open menu</span>
|
||||||
<MoreHorizontal className="w-5" />
|
<MoreHorizontalIcon className="w-5" />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
|
@@ -8,7 +8,7 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from '@/components/ui/dialog'
|
} from '@/components/ui/dialog'
|
||||||
import { BellIcon } from 'lucide-react'
|
import { BellIcon, CpuIcon, HardDriveIcon, MemoryStickIcon, ServerIcon } from 'lucide-react'
|
||||||
import { cn } from '@/lib/utils'
|
import { 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'
|
||||||
@@ -16,6 +16,7 @@ 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'))
|
||||||
|
|
||||||
@@ -65,22 +66,25 @@ export default function AlertsButton({ system }: { system: SystemRecord }) {
|
|||||||
system={system}
|
system={system}
|
||||||
alerts={systemAlerts}
|
alerts={systemAlerts}
|
||||||
name="CPU"
|
name="CPU"
|
||||||
title="CPU Usage"
|
title="CPU usage"
|
||||||
description="Triggers when CPU usage exceeds a threshold."
|
description="Triggers when CPU usage exceeds a threshold."
|
||||||
|
Icon={CpuIcon}
|
||||||
/>
|
/>
|
||||||
<AlertWithSlider
|
<AlertWithSlider
|
||||||
system={system}
|
system={system}
|
||||||
alerts={systemAlerts}
|
alerts={systemAlerts}
|
||||||
name="Memory"
|
name="Memory"
|
||||||
title="Memory Usage"
|
title="Memory usage"
|
||||||
description="Triggers when memory usage exceeds a threshold."
|
description="Triggers when memory usage exceeds a threshold."
|
||||||
|
Icon={MemoryStickIcon}
|
||||||
/>
|
/>
|
||||||
<AlertWithSlider
|
<AlertWithSlider
|
||||||
system={system}
|
system={system}
|
||||||
alerts={systemAlerts}
|
alerts={systemAlerts}
|
||||||
name="Disk"
|
name="Disk"
|
||||||
title="Disk Usage"
|
title="Disk usage"
|
||||||
description="Triggers when root usage exceeds a threshold."
|
description="Triggers when root usage exceeds a threshold."
|
||||||
|
Icon={HardDriveIcon}
|
||||||
/>
|
/>
|
||||||
<AlertWithSlider
|
<AlertWithSlider
|
||||||
system={system}
|
system={system}
|
||||||
@@ -89,6 +93,7 @@ export default function AlertsButton({ system }: { system: SystemRecord }) {
|
|||||||
title="Bandwidth"
|
title="Bandwidth"
|
||||||
description="Triggers when combined up/down exceeds a threshold."
|
description="Triggers when combined up/down exceeds a threshold."
|
||||||
unit=" MB/s"
|
unit=" MB/s"
|
||||||
|
Icon={EthernetIcon}
|
||||||
/>
|
/>
|
||||||
<AlertWithSlider
|
<AlertWithSlider
|
||||||
system={system}
|
system={system}
|
||||||
@@ -97,6 +102,7 @@ export default function AlertsButton({ system }: { system: SystemRecord }) {
|
|||||||
title="Temperature"
|
title="Temperature"
|
||||||
description="Triggers when any sensor exceeds a threshold."
|
description="Triggers when any sensor exceeds a threshold."
|
||||||
unit=" °C"
|
unit=" °C"
|
||||||
|
Icon={ThermometerIcon}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
@@ -114,11 +120,13 @@ function AlertStatus({ system, alerts }: { system: SystemRecord; alerts: AlertRe
|
|||||||
return (
|
return (
|
||||||
<label
|
<label
|
||||||
htmlFor="alert-status"
|
htmlFor="alert-status"
|
||||||
className="flex flex-row items-center justify-between gap-4 rounded-lg border p-4 cursor-pointer"
|
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">
|
<div className="grid gap-1 select-none">
|
||||||
<p className="font-semibold">System Status</p>
|
<p className="font-semibold flex gap-3 items-center">
|
||||||
<span className="block text-sm text-foreground opacity-80">
|
<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.
|
Triggers when status switches between up and down.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -161,6 +169,7 @@ function AlertWithSlider({
|
|||||||
description,
|
description,
|
||||||
unit = '%',
|
unit = '%',
|
||||||
max = 99,
|
max = 99,
|
||||||
|
Icon,
|
||||||
}: {
|
}: {
|
||||||
system: SystemRecord
|
system: SystemRecord
|
||||||
alerts: AlertRecord[]
|
alerts: AlertRecord[]
|
||||||
@@ -169,32 +178,39 @@ function AlertWithSlider({
|
|||||||
description: string
|
description: string
|
||||||
unit?: string
|
unit?: string
|
||||||
max?: number
|
max?: number
|
||||||
|
Icon: React.FC<React.SVGProps<SVGSVGElement>>
|
||||||
}) {
|
}) {
|
||||||
const [pendingChange, setPendingChange] = useState(false)
|
const [pendingChange, setPendingChange] = useState(false)
|
||||||
const [liveValue, setLiveValue] = useState(80)
|
const [liveValue, setLiveValue] = useState(80)
|
||||||
|
const [liveMinutes, setLiveMinutes] = useState(10)
|
||||||
|
|
||||||
|
const key = name.replaceAll(' ', '-')
|
||||||
|
|
||||||
const alert = useMemo(() => {
|
const alert = useMemo(() => {
|
||||||
const alert = alerts.find((alert) => alert.name === name)
|
const alert = alerts.find((alert) => alert.name === name)
|
||||||
if (alert) {
|
if (alert) {
|
||||||
setLiveValue(alert.value)
|
setLiveValue(alert.value)
|
||||||
|
setLiveMinutes(alert.min || 1)
|
||||||
}
|
}
|
||||||
return alert
|
return alert
|
||||||
}, [alerts])
|
}, [alerts])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg border">
|
<div className="rounded-lg border border-muted-foreground/15 hover:border-muted-foreground/20 transition-colors duration-100 group">
|
||||||
<label
|
<label
|
||||||
htmlFor={`alert-${name}`}
|
htmlFor={`v${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,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div className="grid gap-1 select-none">
|
<div className="grid gap-1 select-none">
|
||||||
<p className="font-semibold">{title}</p>
|
<p className="font-semibold flex gap-3 items-center">
|
||||||
<span className="block text-sm text-foreground opacity-80">{description}</span>
|
<Icon className="h-4 w-4 opacity-85" /> {title}
|
||||||
|
</p>
|
||||||
|
{!alert && <span className="block text-sm text-muted-foreground">{description}</span>}
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
id={`alert-${name}`}
|
id={`v${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'}
|
||||||
@@ -212,6 +228,7 @@ function AlertWithSlider({
|
|||||||
user: pb.authStore.model!.id,
|
user: pb.authStore.model!.id,
|
||||||
name,
|
name,
|
||||||
value: liveValue,
|
value: liveValue,
|
||||||
|
min: liveMinutes,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -223,27 +240,52 @@ function AlertWithSlider({
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
{alert && (
|
{alert && (
|
||||||
<div className="flex mt-2 mb-3 gap-3 px-4">
|
<div className="grid sm:grid-cols-2 mt-1.5 gap-5 px-4 pb-5 tabular-nums text-muted-foreground">
|
||||||
<Suspense>
|
<Suspense fallback={<div className="h-10" />}>
|
||||||
|
<div>
|
||||||
|
<label htmlFor={`v${key}`} className="text-sm block h-8">
|
||||||
|
Exceeds{' '}
|
||||||
|
<strong className="text-foreground">
|
||||||
|
{liveValue}
|
||||||
|
{unit}
|
||||||
|
</strong>
|
||||||
|
</label>
|
||||||
|
<div className="flex gap-3">
|
||||||
<Slider
|
<Slider
|
||||||
|
id={`v${key}`}
|
||||||
defaultValue={[liveValue]}
|
defaultValue={[liveValue]}
|
||||||
onValueCommit={(val) => {
|
onValueCommit={(val) => {
|
||||||
pb.collection('alerts').update(alert.id, {
|
pb.collection('alerts').update(alert.id, {
|
||||||
value: val[0],
|
value: val[0],
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
onValueChange={(val) => {
|
onValueChange={(val) => setLiveValue(val[0])}
|
||||||
setLiveValue(val[0])
|
|
||||||
}}
|
|
||||||
min={1}
|
min={1}
|
||||||
max={max}
|
max={max}
|
||||||
// step={1}
|
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor={`t${key}`} className="text-sm block h-8">
|
||||||
|
For <strong className="text-foreground">{liveMinutes}</strong> minute
|
||||||
|
{liveMinutes > 1 && 's'}
|
||||||
|
</label>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<Slider
|
||||||
|
id={`t${key}`}
|
||||||
|
defaultValue={[liveMinutes]}
|
||||||
|
onValueCommit={(val) => {
|
||||||
|
pb.collection('alerts').update(alert.id, {
|
||||||
|
min: val[0],
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
onValueChange={(val) => setLiveMinutes(val[0])}
|
||||||
|
min={1}
|
||||||
|
max={60}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
<span className="tabular-nums tracking-tighter text-[.92em] shrink-0">
|
|
||||||
{liveValue}
|
|
||||||
{unit}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@@ -61,3 +61,15 @@ export function EthernetIcon(props: SVGProps<SVGSVGElement>) {
|
|||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phosphor MIT https://github.com/phosphor-icons/core
|
||||||
|
export function ThermometerIcon(props: SVGProps<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 256 256" {...props}>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M212 56a28 28 0 1 0 28 28 28 28 0 0 0-28-28m0 40a12 12 0 1 1 12-12 12 12 0 0 1-12 12m-60 50V40a32 32 0 0 0-64 0v106a56 56 0 1 0 64 0m-16-42h-32V40a16 16 0 0 1 32 0Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@@ -11,6 +11,7 @@ import { useEffect, useState } from 'react'
|
|||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs))
|
return twMerge(clsx(inputs))
|
||||||
}
|
}
|
||||||
|
// export const cn = clsx
|
||||||
|
|
||||||
export async function copyToClipboard(content: string) {
|
export async function copyToClipboard(content: string) {
|
||||||
const duration = 1500
|
const duration = 1500
|
||||||
@@ -51,7 +52,7 @@ export const updateSystemList = async () => {
|
|||||||
|
|
||||||
export const updateAlerts = () => {
|
export const updateAlerts = () => {
|
||||||
pb.collection('alerts')
|
pb.collection('alerts')
|
||||||
.getFullList<AlertRecord>({ fields: 'id,name,system,value' })
|
.getFullList<AlertRecord>({ fields: 'id,name,system,value,min' })
|
||||||
.then((records) => {
|
.then((records) => {
|
||||||
$alerts.set(records)
|
$alerts.set(records)
|
||||||
})
|
})
|
||||||
|
Reference in New Issue
Block a user