update alerts dialog and icon imports

This commit is contained in:
Henry Dollman
2024-10-14 17:53:49 -04:00
parent 1b0dffc1ab
commit a57498f8f7
5 changed files with 102 additions and 47 deletions

View File

@@ -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">

View File

@@ -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">

View File

@@ -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" />}>
<Slider <div>
defaultValue={[liveValue]} <label htmlFor={`v${key}`} className="text-sm block h-8">
onValueCommit={(val) => { Exceeds{' '}
pb.collection('alerts').update(alert.id, { <strong className="text-foreground">
value: val[0], {liveValue}
}) {unit}
}} </strong>
onValueChange={(val) => { </label>
setLiveValue(val[0]) <div className="flex gap-3">
}} <Slider
min={1} id={`v${key}`}
max={max} defaultValue={[liveValue]}
// step={1} onValueCommit={(val) => {
/> pb.collection('alerts').update(alert.id, {
value: val[0],
})
}}
onValueChange={(val) => setLiveValue(val[0])}
min={1}
max={max}
/>
</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>

View File

@@ -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>
)
}

View File

@@ -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)
}) })