add prettier config and format files site files

This commit is contained in:
Henry Dollman
2024-10-30 11:03:09 -04:00
parent 8827996553
commit 3505b215a2
75 changed files with 3096 additions and 3533 deletions

8
beszel/site/.prettierrc Normal file
View File

@@ -0,0 +1,8 @@
{
"trailingComma": "es5",
"useTabs": true,
"tabWidth": 2,
"semi": false,
"singleQuote": false,
"printWidth": 120
}

View File

@@ -1,4 +1,4 @@
import { Button } from '@/components/ui/button' import { Button } from "@/components/ui/button"
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -7,19 +7,19 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from '@/components/ui/dialog' } from "@/components/ui/dialog"
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip' import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip"
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Input } from '@/components/ui/input' import { Input } from "@/components/ui/input"
import { Label } from '@/components/ui/label' import { Label } from "@/components/ui/label"
import { $publicKey, pb } from '@/lib/stores' import { $publicKey, pb } from "@/lib/stores"
import { Copy, PlusIcon } from 'lucide-react' import { Copy, PlusIcon } from "lucide-react"
import { useState, useRef, MutableRefObject } from 'react' import { useState, useRef, MutableRefObject } from "react"
import { useStore } from '@nanostores/react' import { useStore } from "@nanostores/react"
import { cn, copyToClipboard, isReadOnlyUser } from '@/lib/utils' import { cn, copyToClipboard, isReadOnlyUser } from "@/lib/utils"
import { navigate } from './router' import { navigate } from "./router"
import { useTranslation } from 'react-i18next' import { useTranslation } from "react-i18next"
export function AddSystemButton({ className }: { className?: string }) { export function AddSystemButton({ className }: { className?: string }) {
const { t } = useTranslation() const { t } = useTranslation()
@@ -58,8 +58,8 @@ export function AddSystemButton({ className }: { className?: string }) {
data.users = pb.authStore.model!.id data.users = pb.authStore.model!.id
try { try {
setOpen(false) setOpen(false)
await pb.collection('systems').create(data) await pb.collection("systems").create(data)
navigate('/') navigate("/")
// console.log(record) // console.log(record)
} catch (e) { } catch (e) {
console.log(e) console.log(e)
@@ -71,73 +71,64 @@ export function AddSystemButton({ className }: { className?: string }) {
<DialogTrigger asChild> <DialogTrigger asChild>
<Button <Button
variant="outline" variant="outline"
className={cn('flex gap-1 max-xs:h-[2.4rem]', className, isReadOnlyUser() && 'hidden')} className={cn("flex gap-1 max-xs:h-[2.4rem]", className, isReadOnlyUser() && "hidden")}
> >
<PlusIcon className="h-4 w-4 -ml-1" /> <PlusIcon className="h-4 w-4 -ml-1" />
{t('add')} {t("add")}
<span className="hidden sm:inline">{t('system')}</span> <span className="hidden sm:inline">{t("system")}</span>
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent className="w-[90%] sm:max-w-[440px] rounded-lg"> <DialogContent className="w-[90%] sm:max-w-[440px] rounded-lg">
<Tabs defaultValue="docker"> <Tabs defaultValue="docker">
<DialogHeader> <DialogHeader>
<DialogTitle className="mb-2">{t('add_system.add_new_system')}</DialogTitle> <DialogTitle className="mb-2">{t("add_system.add_new_system")}</DialogTitle>
<TabsList className="grid w-full grid-cols-2"> <TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="docker">Docker</TabsTrigger> <TabsTrigger value="docker">Docker</TabsTrigger>
<TabsTrigger value="binary">{t('add_system.binary')}</TabsTrigger> <TabsTrigger value="binary">{t("add_system.binary")}</TabsTrigger>
</TabsList> </TabsList>
</DialogHeader> </DialogHeader>
{/* Docker */} {/* Docker */}
<TabsContent value="docker"> <TabsContent value="docker">
<DialogDescription className={'mb-4'}> <DialogDescription className={"mb-4"}>
{t('add_system.dialog_des_1')}{' '} {t("add_system.dialog_des_1")} <code className="bg-muted px-1 rounded-sm">docker-compose.yml</code>{" "}
<code className="bg-muted px-1 rounded-sm">docker-compose.yml</code>{' '} {t("add_system.dialog_des_2")}
{t('add_system.dialog_des_2')}
</DialogDescription> </DialogDescription>
</TabsContent> </TabsContent>
{/* Binary */} {/* Binary */}
<TabsContent value="binary"> <TabsContent value="binary">
<DialogDescription className={'mb-4'}> <DialogDescription className={"mb-4"}>
{t('add_system.dialog_des_1')}{' '} {t("add_system.dialog_des_1")} <code className="bg-muted px-1 rounded-sm">install command</code>{" "}
<code className="bg-muted px-1 rounded-sm">install command</code>{' '} {t("add_system.dialog_des_2")}
{t('add_system.dialog_des_2')}
</DialogDescription> </DialogDescription>
</TabsContent> </TabsContent>
<form onSubmit={handleSubmit as any}> <form onSubmit={handleSubmit as any}>
<div className="grid gap-3 mt-1 mb-4"> <div className="grid gap-3 mt-1 mb-4">
<div className="grid grid-cols-4 items-center gap-4"> <div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="name" className="text-right"> <Label htmlFor="name" className="text-right">
{t('add_system.name')} {t("add_system.name")}
</Label> </Label>
<Input id="name" name="name" className="col-span-3" required /> <Input id="name" name="name" className="col-span-3" required />
</div> </div>
<div className="grid grid-cols-4 items-center gap-4"> <div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="host" className="text-right"> <Label htmlFor="host" className="text-right">
{t('add_system.host_ip')} {t("add_system.host_ip")}
</Label> </Label>
<Input id="host" name="host" className="col-span-3" required /> <Input id="host" name="host" className="col-span-3" required />
</div> </div>
<div className="grid grid-cols-4 items-center gap-4"> <div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="port" className="text-right"> <Label htmlFor="port" className="text-right">
{t('add_system.port')} {t("add_system.port")}
</Label> </Label>
<Input <Input ref={port} name="port" id="port" defaultValue="45876" className="col-span-3" required />
ref={port}
name="port"
id="port"
defaultValue="45876"
className="col-span-3"
required
/>
</div> </div>
<div className="grid grid-cols-4 items-center gap-4 relative"> <div className="grid grid-cols-4 items-center gap-4 relative">
<Label htmlFor="pkey" className="text-right whitespace-pre"> <Label htmlFor="pkey" className="text-right whitespace-pre">
{t('add_system.public_key')} {t("add_system.public_key")}
</Label> </Label>
<Input readOnly id="pkey" value={publicKey} className="col-span-3" required></Input> <Input readOnly id="pkey" value={publicKey} className="col-span-3" required></Input>
<div <div
className={ className={
'h-6 w-24 bg-gradient-to-r from-transparent to-background to-65% absolute right-1 pointer-events-none' "h-6 w-24 bg-gradient-to-r from-transparent to-background to-65% absolute right-1 pointer-events-none"
} }
></div> ></div>
<TooltipProvider delayDuration={100}> <TooltipProvider delayDuration={100}>
@@ -145,7 +136,7 @@ export function AddSystemButton({ className }: { className?: string }) {
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button
type="button" type="button"
variant={'link'} variant={"link"}
className="absolute right-0" className="absolute right-0"
onClick={() => copyToClipboard(publicKey)} onClick={() => copyToClipboard(publicKey)}
> >
@@ -153,7 +144,7 @@ export function AddSystemButton({ className }: { className?: string }) {
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<p>{t('add_system.click_to_copy')}</p> <p>{t("add_system.click_to_copy")}</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
@@ -162,27 +153,19 @@ export function AddSystemButton({ className }: { className?: string }) {
{/* Docker */} {/* Docker */}
<TabsContent value="docker"> <TabsContent value="docker">
<DialogFooter className="flex justify-end gap-2 sm:w-[calc(100%+20px)] sm:-ml-[20px]"> <DialogFooter className="flex justify-end gap-2 sm:w-[calc(100%+20px)] sm:-ml-[20px]">
<Button <Button type="button" variant={"ghost"} onClick={() => copyDockerCompose(port.current.value)}>
type="button" {t("copy")} docker compose
variant={'ghost'}
onClick={() => copyDockerCompose(port.current.value)}
>
{t('copy')} docker compose
</Button> </Button>
<Button>{t('add_system.add_system')}</Button> <Button>{t("add_system.add_system")}</Button>
</DialogFooter> </DialogFooter>
</TabsContent> </TabsContent>
{/* Binary */} {/* Binary */}
<TabsContent value="binary"> <TabsContent value="binary">
<DialogFooter className="flex justify-end gap-2 sm:w-[calc(100%+20px)] sm:-ml-[20px]"> <DialogFooter className="flex justify-end gap-2 sm:w-[calc(100%+20px)] sm:-ml-[20px]">
<Button <Button type="button" variant={"ghost"} onClick={() => copyInstallCommand(port.current.value)}>
type="button" {t("copy")} linux {t("add_system.command")}
variant={'ghost'}
onClick={() => copyInstallCommand(port.current.value)}
>
{t('copy')} linux {t('add_system.command')}
</Button> </Button>
<Button>{t('add_system.add_system')}</Button> <Button>{t("add_system.add_system")}</Button>
</DialogFooter> </DialogFooter>
</TabsContent> </TabsContent>
</form> </form>

View File

@@ -1,6 +1,6 @@
import { memo, useState } from 'react' import { memo, useState } from "react"
import { useStore } from '@nanostores/react' import { useStore } from "@nanostores/react"
import { $alerts, $systems } from '@/lib/stores' import { $alerts, $systems } from "@/lib/stores"
import { import {
Dialog, Dialog,
DialogTrigger, DialogTrigger,
@@ -8,16 +8,16 @@ import {
DialogDescription, DialogDescription,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from '@/components/ui/dialog' } from "@/components/ui/dialog"
import { BellIcon, GlobeIcon, ServerIcon } from 'lucide-react' import { BellIcon, GlobeIcon, ServerIcon } from "lucide-react"
import { alertInfo, cn } from '@/lib/utils' import { alertInfo, cn } from "@/lib/utils"
import { Button } from '@/components/ui/button' import { Button } from "@/components/ui/button"
import { AlertRecord, SystemRecord } from '@/types' import { AlertRecord, SystemRecord } from "@/types"
import { Link } from '../router' import { Link } from "../router"
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Checkbox } from '../ui/checkbox' import { Checkbox } from "../ui/checkbox"
import { SystemAlert, SystemAlertGlobal } from './alerts-system' import { SystemAlert, SystemAlertGlobal } from "./alerts-system"
import { useTranslation } from 'react-i18next' import { useTranslation } from "react-i18next"
export default memo(function AlertsButton({ system }: { system: SystemRecord }) { export default memo(function AlertsButton({ system }: { system: SystemRecord }) {
const alerts = useStore($alerts) const alerts = useStore($alerts)
@@ -29,16 +29,10 @@ export default memo(function AlertsButton({ system }: { system: SystemRecord })
return ( return (
<Dialog> <Dialog>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button <Button variant="ghost" size="icon" aria-label="Alerts" data-nolink onClick={() => setOpened(true)}>
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-primary': active, "fill-primary": active,
})} })}
/> />
</Button> </Button>
@@ -57,7 +51,7 @@ function TheContent({
}) { }) {
const { t } = useTranslation() const { t } = useTranslation()
const [overwriteExisting, setOverwriteExisting] = useState<boolean | 'indeterminate'>(false) const [overwriteExisting, setOverwriteExisting] = useState<boolean | "indeterminate">(false)
const systems = $systems.get() const systems = $systems.get()
const data = Object.keys(alertInfo).map((key) => { const data = Object.keys(alertInfo).map((key) => {
@@ -72,13 +66,13 @@ function TheContent({
return ( return (
<> <>
<DialogHeader> <DialogHeader>
<DialogTitle className="text-xl">{t('alerts.title')}</DialogTitle> <DialogTitle className="text-xl">{t("alerts.title")}</DialogTitle>
<DialogDescription> <DialogDescription>
{t('alerts.subtitle_1')}{' '} {t("alerts.subtitle_1")}{" "}
<Link href="/settings/notifications" className="link"> <Link href="/settings/notifications" className="link">
{t('alerts.notification_settings')} {t("alerts.notification_settings")}
</Link>{' '} </Link>{" "}
{t('alerts.subtitle_2')} {t("alerts.subtitle_2")}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<Tabs defaultValue="system"> <Tabs defaultValue="system">
@@ -89,7 +83,7 @@ function TheContent({
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="global"> <TabsTrigger value="global">
<GlobeIcon className="mr-1.5 h-3.5 w-3.5" /> <GlobeIcon className="mr-1.5 h-3.5 w-3.5" />
{t('all_systems')} {t("all_systems")}
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="system"> <TabsContent value="system">
@@ -110,17 +104,11 @@ function TheContent({
checked={overwriteExisting} checked={overwriteExisting}
onCheckedChange={setOverwriteExisting} onCheckedChange={setOverwriteExisting}
/> />
{t('alerts.overwrite_existing_alerts')} {t("alerts.overwrite_existing_alerts")}
</label> </label>
<div className="grid gap-3"> <div className="grid gap-3">
{data.map((d) => ( {data.map((d) => (
<SystemAlertGlobal <SystemAlertGlobal key={d.key} data={d} overwrite={overwriteExisting} alerts={alerts} systems={systems} />
key={d.key}
data={d}
overwrite={overwriteExisting}
alerts={alerts}
systems={systems}
/>
))} ))}
</div> </div>
</TabsContent> </TabsContent>

View File

@@ -1,12 +1,12 @@
import { pb } from '@/lib/stores' import { pb } from "@/lib/stores"
import { alertInfo, cn } from '@/lib/utils' import { alertInfo, cn } from "@/lib/utils"
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, useRef, useState } from 'react' import { lazy, Suspense, useRef, useState } from "react"
import { toast } from '../ui/use-toast' import { toast } from "../ui/use-toast"
import { RecordOptions } from 'pocketbase' import { RecordOptions } from "pocketbase"
import { newQueue, Queue } from '@henrygd/queue' import { newQueue, Queue } from "@henrygd/queue"
import { useTranslation } from 'react-i18next' import { useTranslation } from "react-i18next"
interface AlertData { interface AlertData {
checked?: boolean checked?: boolean
@@ -18,15 +18,15 @@ interface AlertData {
system: SystemRecord system: SystemRecord
} }
const Slider = lazy(() => import('@/components/ui/slider')) const Slider = lazy(() => import("@/components/ui/slider"))
let queue: Queue let queue: Queue
const failedUpdateToast = () => const failedUpdateToast = () =>
toast({ toast({
title: 'Failed to update alert', title: "Failed to update alert",
description: 'Please check logs for more details.', description: "Please check logs for more details.",
variant: 'destructive', variant: "destructive",
}) })
export function SystemAlert({ export function SystemAlert({
@@ -43,11 +43,11 @@ export function SystemAlert({
data.updateAlert = async (checked: boolean, value: number, min: number) => { data.updateAlert = async (checked: boolean, value: number, min: number) => {
try { try {
if (alert && !checked) { if (alert && !checked) {
await pb.collection('alerts').delete(alert.id) await pb.collection("alerts").delete(alert.id)
} else if (alert && checked) { } else if (alert && checked) {
await pb.collection('alerts').update(alert.id, { value, min, triggered: false }) await pb.collection("alerts").update(alert.id, { value, min, triggered: false })
} else if (checked) { } else if (checked) {
pb.collection('alerts').create({ pb.collection("alerts").create({
system: system.id, system: system.id,
user: pb.authStore.model!.id, user: pb.authStore.model!.id,
name: data.key, name: data.key,
@@ -76,7 +76,7 @@ export function SystemAlertGlobal({
systems, systems,
}: { }: {
data: AlertData data: AlertData
overwrite: boolean | 'indeterminate' overwrite: boolean | "indeterminate"
alerts: AlertRecord[] alerts: AlertRecord[]
systems: SystemRecord[] systems: SystemRecord[]
}) { }) {
@@ -111,9 +111,7 @@ export function SystemAlertGlobal({
continue continue
} }
// find matching existing alert // find matching existing alert
const existingAlert = alerts.find( const existingAlert = alerts.find((alert) => alert.system === system.id && data.key === alert.name)
(alert) => alert.system === system.id && data.key === alert.name
)
// if first run, add system to set (alert already existed when global panel was opened) // if first run, add system to set (alert already existed when global panel was opened)
if (existingAlert && !populatedSet && !overwrite) { if (existingAlert && !populatedSet && !overwrite) {
set.add(system.id) set.add(system.id)
@@ -128,13 +126,13 @@ export function SystemAlertGlobal({
if (existingAlert) { if (existingAlert) {
// console.log('updating', system.name) // console.log('updating', system.name)
queue queue
.add(() => pb.collection('alerts').update(existingAlert.id, recordData, requestOptions)) .add(() => pb.collection("alerts").update(existingAlert.id, recordData, requestOptions))
.catch(failedUpdateToast) .catch(failedUpdateToast)
} else { } else {
// console.log('creating', system.name) // console.log('creating', system.name)
queue queue
.add(() => .add(() =>
pb.collection('alerts').create( pb.collection("alerts").create(
{ {
system: system.id, system: system.id,
user: pb.authStore.model!.id, user: pb.authStore.model!.id,
@@ -148,7 +146,7 @@ export function SystemAlertGlobal({
} }
} else if (existingAlert) { } else if (existingAlert) {
// console.log('deleting', system.name) // console.log('deleting', system.name)
queue.add(() => pb.collection('alerts').delete(existingAlert.id)).catch(failedUpdateToast) queue.add(() => pb.collection("alerts").delete(existingAlert.id)).catch(failedUpdateToast)
} }
} }
systemsWithExistingAlerts.current.populatedSet = true systemsWithExistingAlerts.current.populatedSet = true
@@ -162,7 +160,7 @@ function AlertContent({ data }: { data: AlertData }) {
const { key } = data const { key } = data
const hasSliders = !('single' in data.alert) const hasSliders = !("single" in data.alert)
const [checked, setChecked] = useState(data.checked || false) const [checked, setChecked] = useState(data.checked || false)
const [min, setMin] = useState(data.min || (hasSliders ? 10 : 0)) const [min, setMin] = useState(data.min || (hasSliders ? 10 : 0))
@@ -175,24 +173,21 @@ function AlertContent({ data }: { data: AlertData }) {
const Icon = alertInfo[key].icon const Icon = alertInfo[key].icon
const updateAlert = (c?: boolean) => const updateAlert = (c?: boolean) => data.updateAlert?.(c ?? checked, newValue.current, newMin.current)
data.updateAlert?.(c ?? checked, newValue.current, newMin.current)
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={`s${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': showSliders, "pb-0": showSliders,
})} })}
> >
<div className="grid gap-1 select-none"> <div className="grid gap-1 select-none">
<p className="font-semibold flex gap-3 items-center capitalize"> <p className="font-semibold flex gap-3 items-center capitalize">
<Icon className="h-4 w-4 opacity-85" /> {t(data.alert.name)} <Icon className="h-4 w-4 opacity-85" /> {t(data.alert.name)}
</p> </p>
{!showSliders && ( {!showSliders && <span className="block text-sm text-muted-foreground">{t(data.alert.desc)}</span>}
<span className="block text-sm text-muted-foreground">{t(data.alert.desc)}</span>
)}
</div> </div>
<Switch <Switch
id={`s${key}`} id={`s${key}`}
@@ -208,7 +203,7 @@ function AlertContent({ data }: { data: AlertData }) {
<Suspense fallback={<div className="h-10" />}> <Suspense fallback={<div className="h-10" />}>
<div> <div>
<p id={`v${key}`} className="text-sm block h-8"> <p id={`v${key}`} className="text-sm block h-8">
{t('alerts.average_exceeds')}{' '} {t("alerts.average_exceeds")}{" "}
<strong className="text-foreground"> <strong className="text-foreground">
{value} {value}
{data.alert.unit} {data.alert.unit}
@@ -227,7 +222,8 @@ function AlertContent({ data }: { data: AlertData }) {
</div> </div>
<div> <div>
<p id={`t${key}`} className="text-sm block h-8"> <p id={`t${key}`} className="text-sm block h-8">
{t('alerts.for')} <strong className="text-foreground">{min}</strong> {min > 1 ? t('alerts.minutes') : t('alerts.minute')} {t("alerts.for")} <strong className="text-foreground">{min}</strong>{" "}
{min > 1 ? t("alerts.minutes") : t("alerts.minute")}
</p> </p>
<div className="flex gap-3"> <div className="flex gap-3">
<Slider <Slider

View File

@@ -1,6 +1,6 @@
import { Area, AreaChart, CartesianGrid, YAxis } from 'recharts' import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from '@/components/ui/chart' import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
import { import {
useYAxisWidth, useYAxisWidth,
cn, cn,
@@ -8,10 +8,10 @@ import {
toFixedWithoutTrailingZeros, toFixedWithoutTrailingZeros,
decimalString, decimalString,
chartMargin, chartMargin,
} from '@/lib/utils' } from "@/lib/utils"
// import Spinner from '../spinner' // import Spinner from '../spinner'
import { ChartData } from '@/types' import { ChartData } from "@/types"
import { memo, useMemo } from 'react' import { memo, useMemo } from "react"
/** [label, key, color, opacity] */ /** [label, key, color, opacity] */
type DataKeys = [string, string, number, number] type DataKeys = [string, string, number, number]
@@ -21,14 +21,14 @@ const getNestedValue = (path: string, max = false, data: any): number | null =>
// a max value which doesn't exist, or the value was zero and omitted from the stats object. // a max value which doesn't exist, or the value was zero and omitted from the stats object.
// so we check if cpum is present. if so, return 0 to make sure the zero value is displayed. // so we check if cpum is present. if so, return 0 to make sure the zero value is displayed.
// if not, return null - there is no max data so do not display anything. // if not, return null - there is no max data so do not display anything.
return `stats.${path}${max ? 'm' : ''}` return `stats.${path}${max ? "m" : ""}`
.split('.') .split(".")
.reduce((acc: any, key: string) => acc?.[key] ?? (data.stats?.cpum ? 0 : null), data) .reduce((acc: any, key: string) => acc?.[key] ?? (data.stats?.cpum ? 0 : null), data)
} }
export default memo(function AreaChartDefault({ export default memo(function AreaChartDefault({
maxToggled = false, maxToggled = false,
unit = ' MB/s', unit = " MB/s",
chartName, chartName,
chartData, chartData,
}: { }: {
@@ -41,26 +41,26 @@ export default memo(function AreaChartDefault({
const { chartTime } = chartData const { chartTime } = chartData
const showMax = chartTime !== '1h' && maxToggled const showMax = chartTime !== "1h" && maxToggled
const dataKeys: DataKeys[] = useMemo(() => { const dataKeys: DataKeys[] = useMemo(() => {
// [label, key, color, opacity] // [label, key, color, opacity]
if (chartName === 'CPU Usage') { if (chartName === "CPU Usage") {
return [[chartName, 'cpu', 1, 0.4]] return [[chartName, "cpu", 1, 0.4]]
} else if (chartName === 'dio') { } else if (chartName === "dio") {
return [ return [
['Write', 'dw', 3, 0.3], ["Write", "dw", 3, 0.3],
['Read', 'dr', 1, 0.3], ["Read", "dr", 1, 0.3],
] ]
} else if (chartName === 'bw') { } else if (chartName === "bw") {
return [ return [
['Sent', 'ns', 5, 0.2], ["Sent", "ns", 5, 0.2],
['Received', 'nr', 2, 0.2], ["Received", "nr", 2, 0.2],
] ]
} else if (chartName.startsWith('efs')) { } else if (chartName.startsWith("efs")) {
return [ return [
['Write', `${chartName}.w`, 3, 0.3], ["Write", `${chartName}.w`, 3, 0.3],
['Read', `${chartName}.r`, 1, 0.3], ["Read", `${chartName}.r`, 1, 0.3],
] ]
} }
return [] return []
@@ -71,8 +71,8 @@ export default memo(function AreaChartDefault({
return ( return (
<div> <div>
<ChartContainer <ChartContainer
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', { className={cn("h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity", {
'opacity-100': yAxisWidth, "opacity-100": yAxisWidth,
})} })}
> >
<AreaChart accessibilityLayer data={chartData.systemStats} margin={chartMargin}> <AreaChart accessibilityLayer data={chartData.systemStats} margin={chartMargin}>

View File

@@ -1,26 +1,16 @@
import { import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
Select, import { $chartTime } from "@/lib/stores"
SelectContent, import { chartTimeData, cn } from "@/lib/utils"
SelectItem, import { ChartTimes } from "@/types"
SelectTrigger, import { useStore } from "@nanostores/react"
SelectValue, import { HistoryIcon } from "lucide-react"
} from '@/components/ui/select'
import { $chartTime } from '@/lib/stores'
import { chartTimeData, cn } from '@/lib/utils'
import { ChartTimes } from '@/types'
import { useStore } from '@nanostores/react'
import { HistoryIcon } from 'lucide-react'
export default function ChartTimeSelect({ className }: { className?: string }) { export default function ChartTimeSelect({ className }: { className?: string }) {
const chartTime = useStore($chartTime) const chartTime = useStore($chartTime)
return ( return (
<Select <Select defaultValue="1h" value={chartTime} onValueChange={(value: ChartTimes) => $chartTime.set(value)}>
defaultValue="1h" <SelectTrigger className={cn(className, "relative pl-10 pr-5")}>
value={chartTime}
onValueChange={(value: ChartTimes) => $chartTime.set(value)}
>
<SelectTrigger className={cn(className, 'relative pl-10 pr-5')}>
<HistoryIcon className="h-4 w-4 absolute left-4 top-1/2 -translate-y-1/2 opacity-85" /> <HistoryIcon className="h-4 w-4 absolute left-4 top-1/2 -translate-y-1/2 opacity-85" />
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>

View File

@@ -1,12 +1,6 @@
import { Area, AreaChart, CartesianGrid, YAxis } from 'recharts' import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
import { import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
ChartConfig, import { memo, useMemo } from "react"
ChartContainer,
ChartTooltip,
ChartTooltipContent,
xAxis,
} from '@/components/ui/chart'
import { memo, useMemo } from 'react'
import { import {
useYAxisWidth, useYAxisWidth,
cn, cn,
@@ -16,18 +10,18 @@ import {
toFixedFloat, toFixedFloat,
getSizeAndUnit, getSizeAndUnit,
toFixedWithoutTrailingZeros, toFixedWithoutTrailingZeros,
} from '@/lib/utils' } from "@/lib/utils"
// import Spinner from '../spinner' // import Spinner from '../spinner'
import { useStore } from '@nanostores/react' import { useStore } from "@nanostores/react"
import { $containerFilter } from '@/lib/stores' import { $containerFilter } from "@/lib/stores"
import { ChartData } from '@/types' import { ChartData } from "@/types"
import { Separator } from '../ui/separator' import { Separator } from "../ui/separator"
export default memo(function ContainerChart({ export default memo(function ContainerChart({
dataKey, dataKey,
chartData, chartData,
chartName, chartName,
unit = '%', unit = "%",
}: { }: {
dataKey: string dataKey: string
chartData: ChartData chartData: ChartData
@@ -39,7 +33,7 @@ export default memo(function ContainerChart({
const { containerData } = chartData const { containerData } = chartData
const isNetChart = chartName === 'net' const isNetChart = chartName === "net"
const chartConfig = useMemo(() => { const chartConfig = useMemo(() => {
let config = {} as Record< let config = {} as Record<
@@ -52,7 +46,7 @@ export default memo(function ContainerChart({
const totalUsage = {} as Record<string, number> const totalUsage = {} as Record<string, number>
for (let stats of containerData) { for (let stats of containerData) {
for (let key in stats) { for (let key in stats) {
if (!key || key === 'created') { if (!key || key === "created") {
continue continue
} }
if (!(key in totalUsage)) { if (!(key in totalUsage)) {
@@ -87,7 +81,7 @@ export default memo(function ContainerChart({
tickFormatter: (value: any) => string tickFormatter: (value: any) => string
} }
// tick formatter // tick formatter
if (chartName === 'cpu') { if (chartName === "cpu") {
obj.tickFormatter = (value) => { obj.tickFormatter = (value) => {
const val = toFixedWithoutTrailingZeros(value, 2) + unit const val = toFixedWithoutTrailingZeros(value, 2) + unit
return updateYAxisWidth(val) return updateYAxisWidth(val)
@@ -95,7 +89,7 @@ export default memo(function ContainerChart({
} else { } else {
obj.tickFormatter = (value) => { obj.tickFormatter = (value) => {
const { v, u } = getSizeAndUnit(value, false) const { v, u } = getSizeAndUnit(value, false)
return updateYAxisWidth(`${toFixedFloat(v, 2)}${u}${isNetChart ? '/s' : ''}`) return updateYAxisWidth(`${toFixedFloat(v, 2)}${u}${isNetChart ? "/s" : ""}`)
} }
} }
// tooltip formatter // tooltip formatter
@@ -134,8 +128,8 @@ export default memo(function ContainerChart({
return ( return (
<div> <div>
<ChartContainer <ChartContainer
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', { className={cn("h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity", {
'opacity-100': yAxisWidth, "opacity-100": yAxisWidth,
})} })}
> >
<AreaChart <AreaChart

View File

@@ -1,6 +1,6 @@
import { Area, AreaChart, CartesianGrid, YAxis } from 'recharts' import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from '@/components/ui/chart' import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
import { import {
useYAxisWidth, useYAxisWidth,
cn, cn,
@@ -9,9 +9,9 @@ import {
toFixedFloat, toFixedFloat,
chartMargin, chartMargin,
getSizeAndUnit, getSizeAndUnit,
} from '@/lib/utils' } from "@/lib/utils"
import { ChartData } from '@/types' import { ChartData } from "@/types"
import { memo } from 'react' import { memo } from "react"
export default memo(function DiskChart({ export default memo(function DiskChart({
dataKey, dataKey,
@@ -27,8 +27,8 @@ export default memo(function DiskChart({
return ( return (
<div> <div>
<ChartContainer <ChartContainer
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', { className={cn("h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity", {
'opacity-100': yAxisWidth, "opacity-100": yAxisWidth,
})} })}
> >
<AreaChart accessibilityLayer data={chartData.systemStats} margin={chartMargin}> <AreaChart accessibilityLayer data={chartData.systemStats} margin={chartMargin}>

View File

@@ -1,16 +1,9 @@
import { Area, AreaChart, CartesianGrid, YAxis } from 'recharts' import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from '@/components/ui/chart' import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
import { import { useYAxisWidth, cn, toFixedFloat, decimalString, formatShortDate, chartMargin } from "@/lib/utils"
useYAxisWidth, import { memo } from "react"
cn, import { ChartData } from "@/types"
toFixedFloat,
decimalString,
formatShortDate,
chartMargin,
} from '@/lib/utils'
import { memo } from 'react'
import { ChartData } from '@/types'
export default memo(function MemChart({ chartData }: { chartData: ChartData }) { export default memo(function MemChart({ chartData }: { chartData: ChartData }) {
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth() const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
@@ -23,8 +16,8 @@ export default memo(function MemChart({ chartData }: { chartData: ChartData }) {
<div> <div>
{/* {!yAxisSet && <Spinner />} */} {/* {!yAxisSet && <Spinner />} */}
<ChartContainer <ChartContainer
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', { className={cn("h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity", {
'opacity-100': yAxisWidth, "opacity-100": yAxisWidth,
})} })}
> >
<AreaChart accessibilityLayer data={chartData.systemStats} margin={chartMargin}> <AreaChart accessibilityLayer data={chartData.systemStats} margin={chartMargin}>
@@ -40,7 +33,7 @@ export default memo(function MemChart({ chartData }: { chartData: ChartData }) {
axisLine={false} axisLine={false}
tickFormatter={(value) => { tickFormatter={(value) => {
const val = toFixedFloat(value, 1) const val = toFixedFloat(value, 1)
return updateYAxisWidth(val + ' GB') return updateYAxisWidth(val + " GB")
}} }}
/> />
)} )}
@@ -54,7 +47,7 @@ export default memo(function MemChart({ chartData }: { chartData: ChartData }) {
// @ts-ignore // @ts-ignore
itemSorter={(a, b) => a.order - b.order} itemSorter={(a, b) => a.order - b.order}
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)} labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
contentFormatter={(item) => decimalString(item.value) + ' GB'} contentFormatter={(item) => decimalString(item.value) + " GB"}
// indicator="line" // indicator="line"
/> />
} }

View File

@@ -1,6 +1,6 @@
import { Area, AreaChart, CartesianGrid, YAxis } from 'recharts' import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from '@/components/ui/chart' import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
import { import {
useYAxisWidth, useYAxisWidth,
cn, cn,
@@ -8,9 +8,9 @@ import {
toFixedWithoutTrailingZeros, toFixedWithoutTrailingZeros,
decimalString, decimalString,
chartMargin, chartMargin,
} from '@/lib/utils' } from "@/lib/utils"
import { ChartData } from '@/types' import { ChartData } from "@/types"
import { memo } from 'react' import { memo } from "react"
export default memo(function SwapChart({ chartData }: { chartData: ChartData }) { export default memo(function SwapChart({ chartData }: { chartData: ChartData }) {
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth() const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
@@ -18,22 +18,19 @@ export default memo(function SwapChart({ chartData }: { chartData: ChartData })
return ( return (
<div> <div>
<ChartContainer <ChartContainer
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', { className={cn("h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity", {
'opacity-100': yAxisWidth, "opacity-100": yAxisWidth,
})} })}
> >
<AreaChart accessibilityLayer data={chartData.systemStats} margin={chartMargin}> <AreaChart accessibilityLayer data={chartData.systemStats} margin={chartMargin}>
<CartesianGrid vertical={false} /> <CartesianGrid vertical={false} />
<YAxis <YAxis
className="tracking-tighter" className="tracking-tighter"
domain={[ domain={[0, () => toFixedWithoutTrailingZeros(chartData.systemStats.at(-1)?.stats.s ?? 0.04, 2)]}
0,
() => toFixedWithoutTrailingZeros(chartData.systemStats.at(-1)?.stats.s ?? 0.04, 2),
]}
width={yAxisWidth} width={yAxisWidth}
tickLine={false} tickLine={false}
axisLine={false} axisLine={false}
tickFormatter={(value) => updateYAxisWidth(value + ' GB')} tickFormatter={(value) => updateYAxisWidth(value + " GB")}
/> />
{xAxis(chartData)} {xAxis(chartData)}
<ChartTooltip <ChartTooltip
@@ -42,7 +39,7 @@ export default memo(function SwapChart({ chartData }: { chartData: ChartData })
content={ content={
<ChartTooltipContent <ChartTooltipContent
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)} labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
contentFormatter={(item) => decimalString(item.value) + ' GB'} contentFormatter={(item) => decimalString(item.value) + " GB"}
// indicator="line" // indicator="line"
/> />
} }

View File

@@ -1,4 +1,4 @@
import { CartesianGrid, Line, LineChart, YAxis } from 'recharts' import { CartesianGrid, Line, LineChart, YAxis } from "recharts"
import { import {
ChartContainer, ChartContainer,
@@ -7,7 +7,7 @@ import {
ChartTooltip, ChartTooltip,
ChartTooltipContent, ChartTooltipContent,
xAxis, xAxis,
} from '@/components/ui/chart' } from "@/components/ui/chart"
import { import {
useYAxisWidth, useYAxisWidth,
cn, cn,
@@ -15,9 +15,9 @@ import {
toFixedWithoutTrailingZeros, toFixedWithoutTrailingZeros,
decimalString, decimalString,
chartMargin, chartMargin,
} from '@/lib/utils' } from "@/lib/utils"
import { ChartData } from '@/types' import { ChartData } from "@/types"
import { memo, useMemo } from 'react' import { memo, useMemo } from "react"
export default memo(function TemperatureChart({ chartData }: { chartData: ChartData }) { export default memo(function TemperatureChart({ chartData }: { chartData: ChartData }) {
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth() const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
@@ -53,19 +53,19 @@ export default memo(function TemperatureChart({ chartData }: { chartData: ChartD
return ( return (
<div> <div>
<ChartContainer <ChartContainer
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', { className={cn("h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity", {
'opacity-100': yAxisWidth, "opacity-100": yAxisWidth,
})} })}
> >
<LineChart accessibilityLayer data={newChartData.data} margin={chartMargin}> <LineChart accessibilityLayer data={newChartData.data} margin={chartMargin}>
<CartesianGrid vertical={false} /> <CartesianGrid vertical={false} />
<YAxis <YAxis
className="tracking-tighter" className="tracking-tighter"
domain={[0, 'auto']} domain={[0, "auto"]}
width={yAxisWidth} width={yAxisWidth}
tickFormatter={(value) => { tickFormatter={(value) => {
const val = toFixedWithoutTrailingZeros(value, 2) const val = toFixedWithoutTrailingZeros(value, 2)
return updateYAxisWidth(val + ' °C') return updateYAxisWidth(val + " °C")
}} }}
tickLine={false} tickLine={false}
axisLine={false} axisLine={false}
@@ -79,7 +79,7 @@ export default memo(function TemperatureChart({ chartData }: { chartData: ChartD
content={ content={
<ChartTooltipContent <ChartTooltipContent
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)} labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
contentFormatter={(item) => decimalString(item.value) + ' °C'} contentFormatter={(item) => decimalString(item.value) + " °C"}
// indicator="line" // indicator="line"
/> />
} }

View File

@@ -8,7 +8,7 @@ import {
Server, Server,
SettingsIcon, SettingsIcon,
UsersIcon, UsersIcon,
} from 'lucide-react' } from "lucide-react"
import { import {
CommandDialog, CommandDialog,
@@ -19,39 +19,33 @@ import {
CommandList, CommandList,
CommandSeparator, CommandSeparator,
CommandShortcut, CommandShortcut,
} from '@/components/ui/command' } from "@/components/ui/command"
import { useEffect } from 'react' import { useEffect } from "react"
import { useStore } from '@nanostores/react' import { useStore } from "@nanostores/react"
import { $systems } from '@/lib/stores' import { $systems } from "@/lib/stores"
import { isAdmin } from '@/lib/utils' import { isAdmin } from "@/lib/utils"
import { navigate } from './router' import { navigate } from "./router"
import { useTranslation } from 'react-i18next' import { useTranslation } from "react-i18next"
export default function CommandPalette({ export default function CommandPalette({ open, setOpen }: { open: boolean; setOpen: (open: boolean) => void }) {
open,
setOpen,
}: {
open: boolean
setOpen: (open: boolean) => void
}) {
const { t } = useTranslation() const { t } = useTranslation()
const systems = useStore($systems) const systems = useStore($systems)
useEffect(() => { useEffect(() => {
const down = (e: KeyboardEvent) => { const down = (e: KeyboardEvent) => {
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) { if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e.preventDefault() e.preventDefault()
setOpen(!open) setOpen(!open)
} }
} }
document.addEventListener('keydown', down) document.addEventListener("keydown", down)
return () => document.removeEventListener('keydown', down) return () => document.removeEventListener("keydown", down)
}, [open, setOpen]) }, [open, setOpen])
return ( return (
<CommandDialog open={open} onOpenChange={setOpen}> <CommandDialog open={open} onOpenChange={setOpen}>
<CommandInput placeholder={t('command.search')} /> <CommandInput placeholder={t("command.search")} />
<CommandList> <CommandList>
<CommandEmpty>No results found.</CommandEmpty> <CommandEmpty>No results found.</CommandEmpty>
{systems.length > 0 && ( {systems.length > 0 && (
@@ -74,106 +68,106 @@ export default function CommandPalette({
<CommandSeparator className="mb-1.5" /> <CommandSeparator className="mb-1.5" />
</> </>
)} )}
<CommandGroup heading={t('command.pages_settings')}> <CommandGroup heading={t("command.pages_settings")}>
<CommandItem <CommandItem
keywords={['home']} keywords={["home"]}
onSelect={() => { onSelect={() => {
navigate('/') navigate("/")
setOpen(false) setOpen(false)
}} }}
> >
<LayoutDashboard className="mr-2 h-4 w-4" /> <LayoutDashboard className="mr-2 h-4 w-4" />
<span>{t('command.dashboard')}</span> <span>{t("command.dashboard")}</span>
<CommandShortcut>{t('command.page')}</CommandShortcut> <CommandShortcut>{t("command.page")}</CommandShortcut>
</CommandItem> </CommandItem>
<CommandItem <CommandItem
onSelect={() => { onSelect={() => {
navigate('/settings/general') navigate("/settings/general")
setOpen(false) setOpen(false)
}} }}
> >
<SettingsIcon className="mr-2 h-4 w-4" /> <SettingsIcon className="mr-2 h-4 w-4" />
<span>{t('settings.settings')}</span> <span>{t("settings.settings")}</span>
<CommandShortcut>{t('settings.settings')}</CommandShortcut> <CommandShortcut>{t("settings.settings")}</CommandShortcut>
</CommandItem> </CommandItem>
<CommandItem <CommandItem
keywords={['alerts']} keywords={["alerts"]}
onSelect={() => { onSelect={() => {
navigate('/settings/notifications') navigate("/settings/notifications")
setOpen(false) setOpen(false)
}} }}
> >
<MailIcon className="mr-2 h-4 w-4" /> <MailIcon className="mr-2 h-4 w-4" />
<span>{t('settings.notifications.title')}</span> <span>{t("settings.notifications.title")}</span>
<CommandShortcut>{t('settings.settings')}</CommandShortcut> <CommandShortcut>{t("settings.settings")}</CommandShortcut>
</CommandItem> </CommandItem>
<CommandItem <CommandItem
keywords={['github']} keywords={["github"]}
onSelect={() => { onSelect={() => {
window.location.href = 'https://github.com/henrygd/beszel/blob/main/readme.md' window.location.href = "https://github.com/henrygd/beszel/blob/main/readme.md"
}} }}
> >
<Github className="mr-2 h-4 w-4" /> <Github className="mr-2 h-4 w-4" />
<span>{t('command.documentation')}</span> <span>{t("command.documentation")}</span>
<CommandShortcut>GitHub</CommandShortcut> <CommandShortcut>GitHub</CommandShortcut>
</CommandItem> </CommandItem>
</CommandGroup> </CommandGroup>
{isAdmin() && ( {isAdmin() && (
<> <>
<CommandSeparator className="mb-1.5" /> <CommandSeparator className="mb-1.5" />
<CommandGroup heading={t('command.admin')}> <CommandGroup heading={t("command.admin")}>
<CommandItem <CommandItem
keywords={['pocketbase']} keywords={["pocketbase"]}
onSelect={() => { onSelect={() => {
setOpen(false) setOpen(false)
window.open('/_/', '_blank') window.open("/_/", "_blank")
}} }}
> >
<UsersIcon className="mr-2 h-4 w-4" /> <UsersIcon className="mr-2 h-4 w-4" />
<span>{t('user_dm.users')}</span> <span>{t("user_dm.users")}</span>
<CommandShortcut>{t('command.admin')}</CommandShortcut> <CommandShortcut>{t("command.admin")}</CommandShortcut>
</CommandItem> </CommandItem>
<CommandItem <CommandItem
onSelect={() => { onSelect={() => {
setOpen(false) setOpen(false)
window.open('/_/#/logs', '_blank') window.open("/_/#/logs", "_blank")
}} }}
> >
<LogsIcon className="mr-2 h-4 w-4" /> <LogsIcon className="mr-2 h-4 w-4" />
<span>{t('user_dm.logs')}</span> <span>{t("user_dm.logs")}</span>
<CommandShortcut>{t('command.admin')}</CommandShortcut> <CommandShortcut>{t("command.admin")}</CommandShortcut>
</CommandItem> </CommandItem>
<CommandItem <CommandItem
onSelect={() => { onSelect={() => {
setOpen(false) setOpen(false)
window.open('/_/#/settings/backups', '_blank') window.open("/_/#/settings/backups", "_blank")
}} }}
> >
<DatabaseBackupIcon className="mr-2 h-4 w-4" /> <DatabaseBackupIcon className="mr-2 h-4 w-4" />
<span>{t('user_dm.backups')}</span> <span>{t("user_dm.backups")}</span>
<CommandShortcut>{t('command.admin')}</CommandShortcut> <CommandShortcut>{t("command.admin")}</CommandShortcut>
</CommandItem> </CommandItem>
<CommandItem <CommandItem
keywords={['oauth', 'oicd']} keywords={["oauth", "oicd"]}
onSelect={() => { onSelect={() => {
setOpen(false) setOpen(false)
window.open('/_/#/settings/auth-providers', '_blank') window.open("/_/#/settings/auth-providers", "_blank")
}} }}
> >
<LockKeyholeIcon className="mr-2 h-4 w-4" /> <LockKeyholeIcon className="mr-2 h-4 w-4" />
<span>{t('user_dm.auth_providers')}</span> <span>{t("user_dm.auth_providers")}</span>
<CommandShortcut>{t('command.admin')}</CommandShortcut> <CommandShortcut>{t("command.admin")}</CommandShortcut>
</CommandItem> </CommandItem>
<CommandItem <CommandItem
keywords={['email']} keywords={["email"]}
onSelect={() => { onSelect={() => {
setOpen(false) setOpen(false)
window.open('/_/#/settings/mail', '_blank') window.open("/_/#/settings/mail", "_blank")
}} }}
> >
<MailIcon className="mr-2 h-4 w-4" /> <MailIcon className="mr-2 h-4 w-4" />
<span>{t('command.SMTP_settings')}</span> <span>{t("command.SMTP_settings")}</span>
<CommandShortcut>{t('command.admin')}</CommandShortcut> <CommandShortcut>{t("command.admin")}</CommandShortcut>
</CommandItem> </CommandItem>
</CommandGroup> </CommandGroup>
</> </>

View File

@@ -1,7 +1,7 @@
import { useEffect, useMemo, useRef } from 'react' import { useEffect, useMemo, useRef } from "react"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from './ui/dialog' import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "./ui/dialog"
import { Textarea } from './ui/textarea' import { Textarea } from "./ui/textarea"
import { $copyContent } from '@/lib/stores' import { $copyContent } from "@/lib/stores"
export default function CopyToClipboard({ content }: { content: string }) { export default function CopyToClipboard({ content }: { content: string }) {
return ( return (
@@ -24,7 +24,7 @@ function CopyTextarea({ content }: { content: string }) {
const textareaRef = useRef<HTMLTextAreaElement>(null) const textareaRef = useRef<HTMLTextAreaElement>(null)
const rows = useMemo(() => { const rows = useMemo(() => {
return content.split('\n').length return content.split("\n").length
}, [content]) }, [content])
useEffect(() => { useEffect(() => {
@@ -34,7 +34,7 @@ function CopyTextarea({ content }: { content: string }) {
}, [textareaRef]) }, [textareaRef])
useEffect(() => { useEffect(() => {
return () => $copyContent.set('') return () => $copyContent.set("")
}, []) }, [])
return ( return (

View File

@@ -1,16 +1,11 @@
import { useEffect } from 'react' import { useEffect } from "react"
import { LanguagesIcon } from 'lucide-react' import { LanguagesIcon } from "lucide-react"
import { Button } from '@/components/ui/button' import { Button } from "@/components/ui/button"
import { import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
DropdownMenu, import { useTranslation } from "react-i18next"
DropdownMenuContent, import languages from "../lib/languages.json"
DropdownMenuItem, import { cn } from "@/lib/utils"
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { useTranslation } from 'react-i18next'
import languages from '../lib/languages.json'
import { cn } from '@/lib/utils'
export function LangToggle() { export function LangToggle() {
const { i18n } = useTranslation() const { i18n } = useTranslation()
@@ -22,7 +17,7 @@ export function LangToggle() {
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant={'ghost'} size="icon" className="hidden 450:flex"> <Button variant={"ghost"} size="icon" className="hidden 450:flex">
<LanguagesIcon className="absolute h-[1.2rem] w-[1.2rem] light:opacity-85" /> <LanguagesIcon className="absolute h-[1.2rem] w-[1.2rem] light:opacity-85" />
<span className="sr-only">Language</span> <span className="sr-only">Language</span>
</Button> </Button>
@@ -31,7 +26,7 @@ export function LangToggle() {
{languages.map(({ lang, label }) => ( {languages.map(({ lang, label }) => (
<DropdownMenuItem <DropdownMenuItem
key={lang} key={lang}
className={cn('pl-4', lang === i18n.language ? 'font-bold' : '')} className={cn("pl-4", lang === i18n.language ? "font-bold" : "")}
onClick={() => i18n.changeLanguage(lang)} onClick={() => i18n.changeLanguage(lang)}
> >
{label} {label}

View File

@@ -1,29 +1,20 @@
import { cn } from '@/lib/utils' import { cn } from "@/lib/utils"
import { buttonVariants } from '@/components/ui/button' import { buttonVariants } from "@/components/ui/button"
import { Input } from '@/components/ui/input' import { Input } from "@/components/ui/input"
import { Label } from '@/components/ui/label' import { Label } from "@/components/ui/label"
import { LoaderCircle, LockIcon, LogInIcon, MailIcon, UserIcon } from 'lucide-react' import { LoaderCircle, LockIcon, LogInIcon, MailIcon, UserIcon } from "lucide-react"
import { $authenticated, pb } from '@/lib/stores' import { $authenticated, pb } from "@/lib/stores"
import * as v from 'valibot' import * as v from "valibot"
import { toast } from '../ui/use-toast' import { toast } from "../ui/use-toast"
import { import { Dialog, DialogContent, DialogTrigger, DialogHeader, DialogTitle } from "@/components/ui/dialog"
Dialog, import { useCallback, useState } from "react"
DialogContent, import { AuthMethodsList, OAuth2AuthConfig } from "pocketbase"
DialogTrigger, import { Link } from "../router"
DialogHeader, import { useTranslation } from "react-i18next"
DialogTitle,
} from '@/components/ui/dialog'
import { useCallback, useState } from 'react'
import { AuthMethodsList, OAuth2AuthConfig } from 'pocketbase'
import { Link } from '../router'
import { useTranslation } from 'react-i18next'
const honeypot = v.literal('') const honeypot = v.literal("")
const emailSchema = v.pipe(v.string(), v.email('Invalid email address.')) const emailSchema = v.pipe(v.string(), v.email("Invalid email address."))
const passwordSchema = v.pipe( const passwordSchema = v.pipe(v.string(), v.minLength(10, "Password must be at least 10 characters."))
v.string(),
v.minLength(10, 'Password must be at least 10 characters.')
)
const LoginSchema = v.looseObject({ const LoginSchema = v.looseObject({
name: honeypot, name: honeypot,
@@ -37,9 +28,9 @@ const RegisterSchema = v.looseObject({
v.string(), v.string(),
v.regex( v.regex(
/^(?=.*[a-zA-Z])[a-zA-Z0-9_-]+$/, /^(?=.*[a-zA-Z])[a-zA-Z0-9_-]+$/,
'Invalid username. You may use alphanumeric characters, underscores, and hyphens.' "Invalid username. You may use alphanumeric characters, underscores, and hyphens."
), ),
v.minLength(3, 'Username must be at least 3 characters long.') v.minLength(3, "Username must be at least 3 characters long.")
), ),
email: emailSchema, email: emailSchema,
password: passwordSchema, password: passwordSchema,
@@ -48,9 +39,9 @@ const RegisterSchema = v.looseObject({
const showLoginFaliedToast = () => { const showLoginFaliedToast = () => {
toast({ toast({
title: 'Login attempt failed', title: "Login attempt failed",
description: 'Please check your credentials and try again', description: "Please check your credentials and try again",
variant: 'destructive', variant: "destructive",
}) })
} }
@@ -93,7 +84,7 @@ export function UserAuthForm({
if (isFirstRun) { if (isFirstRun) {
// check that passwords match // check that passwords match
if (password !== passwordConfirm) { if (password !== passwordConfirm) {
let msg = 'Passwords do not match' let msg = "Passwords do not match"
setErrors({ passwordConfirm: msg }) setErrors({ passwordConfirm: msg })
return return
} }
@@ -103,17 +94,17 @@ export function UserAuthForm({
passwordConfirm: password, passwordConfirm: password,
}) })
await pb.admins.authWithPassword(email, password) await pb.admins.authWithPassword(email, password)
await pb.collection('users').create({ await pb.collection("users").create({
username, username,
email, email,
password, password,
passwordConfirm: password, passwordConfirm: password,
role: 'admin', role: "admin",
verified: true, verified: true,
}) })
await pb.collection('users').authWithPassword(email, password) await pb.collection("users").authWithPassword(email, password)
} else { } else {
await pb.collection('users').authWithPassword(email, password) await pb.collection("users").authWithPassword(email, password)
} }
$authenticated.set(true) $authenticated.set(true)
} catch (e) { } catch (e) {
@@ -130,7 +121,7 @@ export function UserAuthForm({
} }
return ( return (
<div className={cn('grid gap-6', className)} {...props}> <div className={cn("grid gap-6", className)} {...props}>
{authMethods.emailPassword && ( {authMethods.emailPassword && (
<> <>
<form onSubmit={handleSubmit} onChange={() => setErrors({})}> <form onSubmit={handleSubmit} onChange={() => setErrors({})}>
@@ -154,9 +145,7 @@ export function UserAuthForm({
disabled={isLoading || isOauthLoading} disabled={isLoading || isOauthLoading}
className="pl-9" className="pl-9"
/> />
{errors?.username && ( {errors?.username && <p className="px-1 text-xs text-red-600">{errors.username}</p>}
<p className="px-1 text-xs text-red-600">{errors.username}</p>
)}
</div> </div>
)} )}
<div className="grid gap-1 relative"> <div className="grid gap-1 relative">
@@ -168,7 +157,7 @@ export function UserAuthForm({
id="email" id="email"
name="email" name="email"
required required
placeholder={isFirstRun ? 'email' : 'name@example.com'} placeholder={isFirstRun ? "email" : "name@example.com"}
type="email" type="email"
autoCapitalize="none" autoCapitalize="none"
autoComplete="email" autoComplete="email"
@@ -211,9 +200,7 @@ export function UserAuthForm({
disabled={isLoading || isOauthLoading} disabled={isLoading || isOauthLoading}
className="pl-9" className="pl-9"
/> />
{errors?.passwordConfirm && ( {errors?.passwordConfirm && <p className="px-1 text-xs text-red-600">{errors.passwordConfirm}</p>}
<p className="px-1 text-xs text-red-600">{errors.passwordConfirm}</p>
)}
</div> </div>
)} )}
<div className="sr-only"> <div className="sr-only">
@@ -227,7 +214,7 @@ export function UserAuthForm({
) : ( ) : (
<LogInIcon className="mr-2 h-4 w-4" /> <LogInIcon className="mr-2 h-4 w-4" />
)} )}
{isFirstRun ? t('auth.create_account') : t('auth.sign_in')} {isFirstRun ? t("auth.create_account") : t("auth.sign_in")}
</button> </button>
</div> </div>
</form> </form>
@@ -251,9 +238,9 @@ export function UserAuthForm({
<button <button
key={provider.name} key={provider.name}
type="button" type="button"
className={cn(buttonVariants({ variant: 'outline' }), { className={cn(buttonVariants({ variant: "outline" }), {
'justify-self-center': !authMethods.emailPassword, "justify-self-center": !authMethods.emailPassword,
'px-5': !authMethods.emailPassword, "px-5": !authMethods.emailPassword,
})} })}
onClick={() => { onClick={() => {
setIsOauthLoading(true) setIsOauthLoading(true)
@@ -266,9 +253,9 @@ export function UserAuthForm({
if (!authWindow) { if (!authWindow) {
setIsOauthLoading(false) setIsOauthLoading(false)
toast({ toast({
title: 'Error', title: "Error",
description: 'Please enable pop-ups for this site', description: "Please enable pop-ups for this site",
variant: 'destructive', variant: "destructive",
}) })
return return
} }
@@ -276,7 +263,7 @@ export function UserAuthForm({
authWindow.location.href = url authWindow.location.href = url
} }
} }
pb.collection('users') pb.collection("users")
.authWithOAuth2(oAuthOpts) .authWithOAuth2(oAuthOpts)
.then(() => { .then(() => {
$authenticated.set(pb.authStore.isValid) $authenticated.set(pb.authStore.isValid)
@@ -296,7 +283,7 @@ export function UserAuthForm({
src={`/static/${provider.name}.svg`} src={`/static/${provider.name}.svg`}
alt="" alt=""
onError={(e) => { onError={(e) => {
e.currentTarget.src = '/static/lock.svg' e.currentTarget.src = "/static/lock.svg"
}} }}
/> />
)} )}
@@ -310,26 +297,26 @@ export function UserAuthForm({
// only show GitHub button / dialog during onboarding // only show GitHub button / dialog during onboarding
<Dialog> <Dialog>
<DialogTrigger asChild> <DialogTrigger asChild>
<button type="button" className={cn(buttonVariants({ variant: 'outline' }))}> <button type="button" className={cn(buttonVariants({ variant: "outline" }))}>
<img className="mr-2 h-4 w-4 dark:invert" src="/static/github.svg" alt="" /> <img className="mr-2 h-4 w-4 dark:invert" src="/static/github.svg" alt="" />
<span className="translate-y-[1px]">GitHub</span> <span className="translate-y-[1px]">GitHub</span>
</button> </button>
</DialogTrigger> </DialogTrigger>
<DialogContent style={{ maxWidth: 440, width: '90%' }}> <DialogContent style={{ maxWidth: 440, width: "90%" }}>
<DialogHeader> <DialogHeader>
<DialogTitle>OAuth 2 / OIDC support</DialogTitle> <DialogTitle>OAuth 2 / OIDC support</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="text-primary/70 text-[0.95em] contents"> <div className="text-primary/70 text-[0.95em] contents">
<p>{t('auth.openid_des')}</p> <p>{t("auth.openid_des")}</p>
<p> <p>
{t('please_view_the')}{' '} {t("please_view_the")}{" "}
<a <a
href="https://github.com/henrygd/beszel/blob/main/readme.md#oauth--oidc-integration" href="https://github.com/henrygd/beszel/blob/main/readme.md#oauth--oidc-integration"
className={cn(buttonVariants({ variant: 'link' }), 'p-0 h-auto')} className={cn(buttonVariants({ variant: "link" }), "p-0 h-auto")}
> >
GitHub README GitHub README
</a>{' '} </a>{" "}
{t('for_instructions')} {t("for_instructions")}
</p> </p>
</div> </div>
</DialogContent> </DialogContent>
@@ -341,7 +328,7 @@ export function UserAuthForm({
href="/forgot-password" href="/forgot-password"
className="text-sm mx-auto hover:text-brand underline underline-offset-4 opacity-70 hover:opacity-100 transition-opacity" className="text-sm mx-auto hover:text-brand underline underline-offset-4 opacity-70 hover:opacity-100 transition-opacity"
> >
{t('auth.forgot_password')} {t("auth.forgot_password")}
</Link> </Link>
)} )}
</div> </div>

View File

@@ -1,27 +1,27 @@
import { LoaderCircle, MailIcon, SendHorizonalIcon } from 'lucide-react' import { LoaderCircle, MailIcon, SendHorizonalIcon } from "lucide-react"
import { Input } from '../ui/input' import { Input } from "../ui/input"
import { Label } from '../ui/label' import { Label } from "../ui/label"
import { useCallback, useState } from 'react' import { useCallback, useState } from "react"
import { toast } from '../ui/use-toast' import { toast } from "../ui/use-toast"
import { buttonVariants } from '../ui/button' import { buttonVariants } from "../ui/button"
import { cn } from '@/lib/utils' import { cn } from "@/lib/utils"
import { pb } from '@/lib/stores' import { pb } from "@/lib/stores"
import { Dialog, DialogHeader } from '../ui/dialog' import { Dialog, DialogHeader } from "../ui/dialog"
import { DialogContent, DialogTrigger, DialogTitle } from '../ui/dialog' import { DialogContent, DialogTrigger, DialogTitle } from "../ui/dialog"
import { useTranslation } from 'react-i18next' import { useTranslation } from "react-i18next"
const showLoginFaliedToast = () => { const showLoginFaliedToast = () => {
toast({ toast({
title: 'Login attempt failed', title: "Login attempt failed",
description: 'Please check your credentials and try again', description: "Please check your credentials and try again",
variant: 'destructive', variant: "destructive",
}) })
} }
export default function ForgotPassword() { export default function ForgotPassword() {
const { t } = useTranslation() const { t } = useTranslation()
const [isLoading, setIsLoading] = useState<boolean>(false) const [isLoading, setIsLoading] = useState<boolean>(false)
const [email, setEmail] = useState('') const [email, setEmail] = useState("")
const handleSubmit = useCallback( const handleSubmit = useCallback(
async (e: React.FormEvent<HTMLFormElement>) => { async (e: React.FormEvent<HTMLFormElement>) => {
@@ -29,16 +29,16 @@ export default function ForgotPassword() {
setIsLoading(true) setIsLoading(true)
try { try {
// console.log(email) // console.log(email)
await pb.collection('users').requestPasswordReset(email) await pb.collection("users").requestPasswordReset(email)
toast({ toast({
title: 'Password reset request received', title: "Password reset request received",
description: `Check ${email} for a reset link.`, description: `Check ${email} for a reset link.`,
}) })
} catch (e) { } catch (e) {
showLoginFaliedToast() showLoginFaliedToast()
} finally { } finally {
setIsLoading(false) setIsLoading(false)
setEmail('') setEmail("")
} }
}, },
[email] [email]
@@ -74,26 +74,22 @@ export default function ForgotPassword() {
) : ( ) : (
<SendHorizonalIcon className="mr-2 h-4 w-4" /> <SendHorizonalIcon className="mr-2 h-4 w-4" />
)} )}
{t('auth.reset_password')} {t("auth.reset_password")}
</button> </button>
</div> </div>
</form> </form>
<Dialog> <Dialog>
<DialogTrigger asChild> <DialogTrigger asChild>
<button className="text-sm mx-auto hover:text-brand underline underline-offset-4 opacity-70 hover:opacity-100 transition-opacity"> <button className="text-sm mx-auto hover:text-brand underline underline-offset-4 opacity-70 hover:opacity-100 transition-opacity">
{t('auth.command_line_instructions')} {t("auth.command_line_instructions")}
</button> </button>
</DialogTrigger> </DialogTrigger>
<DialogContent className="max-w-[33em]"> <DialogContent className="max-w-[33em]">
<DialogHeader> <DialogHeader>
<DialogTitle>{t('auth.command_line_instructions')}</DialogTitle> <DialogTitle>{t("auth.command_line_instructions")}</DialogTitle>
</DialogHeader> </DialogHeader>
<p className="text-primary/70 text-[0.95em] leading-relaxed"> <p className="text-primary/70 text-[0.95em] leading-relaxed">{t("auth.command_1")}</p>
{t('auth.command_1')} <p className="text-primary/70 text-[0.95em] leading-relaxed">{t("auth.command_2")}</p>
</p>
<p className="text-primary/70 text-[0.95em] leading-relaxed">
{t('auth.command_2')}
</p>
<code className="bg-muted rounded-sm py-0.5 px-2.5 mr-auto text-sm"> <code className="bg-muted rounded-sm py-0.5 px-2.5 mr-auto text-sm">
beszel admin update youremail@example.com newpassword beszel admin update youremail@example.com newpassword
</code> </code>

View File

@@ -1,12 +1,12 @@
import { UserAuthForm } from '@/components/login/auth-form' import { UserAuthForm } from "@/components/login/auth-form"
import { Logo } from '../logo' import { Logo } from "../logo"
import { useEffect, useMemo, useState } from 'react' import { useEffect, useMemo, useState } from "react"
import { pb } from '@/lib/stores' import { pb } from "@/lib/stores"
import { useStore } from '@nanostores/react' import { useStore } from "@nanostores/react"
import ForgotPassword from './forgot-pass-form' import ForgotPassword from "./forgot-pass-form"
import { $router } from '../router' import { $router } from "../router"
import { AuthMethodsList } from 'pocketbase' import { AuthMethodsList } from "pocketbase"
import { useTranslation } from 'react-i18next' import { useTranslation } from "react-i18next"
export default function () { export default function () {
const { t } = useTranslation() const { t } = useTranslation()
@@ -16,15 +16,15 @@ export default function () {
const [authMethods, setAuthMethods] = useState<AuthMethodsList>() const [authMethods, setAuthMethods] = useState<AuthMethodsList>()
useEffect(() => { useEffect(() => {
document.title = 'Login / Beszel' document.title = "Login / Beszel"
pb.send('/api/beszel/first-run', {}).then(({ firstRun }) => { pb.send("/api/beszel/first-run", {}).then(({ firstRun }) => {
setFirstRun(firstRun) setFirstRun(firstRun)
}) })
}, []) }, [])
useEffect(() => { useEffect(() => {
pb.collection('users') pb.collection("users")
.listAuthMethods() .listAuthMethods()
.then((methods) => { .then((methods) => {
setAuthMethods(methods) setAuthMethods(methods)
@@ -33,11 +33,11 @@ export default function () {
const subtitle = useMemo(() => { const subtitle = useMemo(() => {
if (isFirstRun) { if (isFirstRun) {
return t('auth.create') return t("auth.create")
} else if (page?.path === '/forgot-password') { } else if (page?.path === "/forgot-password") {
return t('auth.reset') return t("auth.reset")
} else { } else {
return t('auth.login') return t("auth.login")
} }
}, [isFirstRun, page]) }, [isFirstRun, page])
@@ -47,7 +47,7 @@ export default function () {
return ( return (
<div className="min-h-svh grid items-center py-12"> <div className="min-h-svh grid items-center py-12">
<div className="grid gap-5 w-full px-4 mx-auto" style={{ maxWidth: '22em' }}> <div className="grid gap-5 w-full px-4 mx-auto" style={{ maxWidth: "22em" }}>
<div className="text-center"> <div className="text-center">
<h1 className="mb-3"> <h1 className="mb-3">
<Logo className="h-7 fill-foreground mx-auto" /> <Logo className="h-7 fill-foreground mx-auto" />
@@ -55,7 +55,7 @@ export default function () {
</h1> </h1>
<p className="text-sm text-muted-foreground">{subtitle}</p> <p className="text-sm text-muted-foreground">{subtitle}</p>
</div> </div>
{page?.path === '/forgot-password' ? ( {page?.path === "/forgot-password" ? (
<ForgotPassword /> <ForgotPassword />
) : ( ) : (
<UserAuthForm isFirstRun={isFirstRun} authMethods={authMethods} /> <UserAuthForm isFirstRun={isFirstRun} authMethods={authMethods} />

View File

@@ -1,14 +1,9 @@
import { LaptopIcon, MoonStarIcon, SunIcon } from 'lucide-react' import { LaptopIcon, MoonStarIcon, SunIcon } from "lucide-react"
import { Button } from '@/components/ui/button' import { Button } from "@/components/ui/button"
import { import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
DropdownMenu, import { useTheme } from "@/components/theme-provider"
DropdownMenuContent, import { useTranslation } from "react-i18next"
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { useTheme } from '@/components/theme-provider'
import { useTranslation } from 'react-i18next'
export function ModeToggle() { export function ModeToggle() {
const { t } = useTranslation() const { t } = useTranslation()
@@ -17,24 +12,24 @@ export function ModeToggle() {
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant={'ghost'} size="icon"> <Button variant={"ghost"} size="icon">
<SunIcon className="h-[1.2rem] w-[1.2rem] dark:opacity-0" /> <SunIcon className="h-[1.2rem] w-[1.2rem] dark:opacity-0" />
<MoonStarIcon className="absolute h-[1.2rem] w-[1.2rem] opacity-0 dark:opacity-100" /> <MoonStarIcon className="absolute h-[1.2rem] w-[1.2rem] opacity-0 dark:opacity-100" />
<span className="sr-only">{t('themes.toggle_theme')}</span> <span className="sr-only">{t("themes.toggle_theme")}</span>
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent> <DropdownMenuContent>
<DropdownMenuItem onClick={() => setTheme('light')}> <DropdownMenuItem onClick={() => setTheme("light")}>
<SunIcon className="mr-2.5 h-4 w-4" /> <SunIcon className="mr-2.5 h-4 w-4" />
{t('themes.light')} {t("themes.light")}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('dark')}> <DropdownMenuItem onClick={() => setTheme("dark")}>
<MoonStarIcon className="mr-2.5 h-4 w-4" /> <MoonStarIcon className="mr-2.5 h-4 w-4" />
{t('themes.dark')} {t("themes.dark")}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('system')}> <DropdownMenuItem onClick={() => setTheme("system")}>
<LaptopIcon className="mr-2.5 h-4 w-4" /> <LaptopIcon className="mr-2.5 h-4 w-4" />
{t('themes.system')} {t("themes.system")}
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>

View File

@@ -1,5 +1,5 @@
import { useState, lazy, Suspense } from 'react' import { useState, lazy, Suspense } from "react"
import { Button, buttonVariants } from '@/components/ui/button' import { Button, buttonVariants } from "@/components/ui/button"
import { import {
DatabaseBackupIcon, DatabaseBackupIcon,
LockKeyholeIcon, LockKeyholeIcon,
@@ -10,14 +10,13 @@ import {
SettingsIcon, SettingsIcon,
UserIcon, UserIcon,
UsersIcon, UsersIcon,
} from 'lucide-react' } from "lucide-react"
import { TFunction } from 'i18next' import { Link } from "./router"
import { Link } from './router' import { LangToggle } from "./lang-toggle"
import { LangToggle } from './lang-toggle' import { ModeToggle } from "./mode-toggle"
import { ModeToggle } from './mode-toggle' import { Logo } from "./logo"
import { Logo } from './logo' import { pb } from "@/lib/stores"
import { pb } from '@/lib/stores' import { cn, isReadOnlyUser, isAdmin } from "@/lib/utils"
import { cn, isReadOnlyUser, isAdmin } from '@/lib/utils'
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuTrigger, DropdownMenuTrigger,
@@ -26,42 +25,41 @@ import {
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuGroup, DropdownMenuGroup,
DropdownMenuItem, DropdownMenuItem,
} from '@/components/ui/dropdown-menu' } from "@/components/ui/dropdown-menu"
import { AddSystemButton } from './add-system' import { AddSystemButton } from "./add-system"
import { useTranslation } from "react-i18next"
const CommandPalette = lazy(() => import('./command-palette.tsx')) const CommandPalette = lazy(() => import("./command-palette.tsx"))
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0 const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0
export default function Navbar(t: TFunction<'translation', undefined>) { export default function Navbar() {
const { t } = useTranslation()
return ( return (
<div className="flex items-center h-14 md:h-16 bg-card px-4 pr-3 sm:px-6 border bt-0 rounded-md my-4"> <div className="flex items-center h-14 md:h-16 bg-card px-4 pr-3 sm:px-6 border bt-0 rounded-md my-4">
<Link href="/" aria-label="Home" className={'p-2 pl-0'}> <Link href="/" aria-label="Home" className={"p-2 pl-0"}>
<Logo className="h-[1.3em] fill-foreground" /> <Logo className="h-[1.3em] fill-foreground" />
</Link> </Link>
<SearchButton /> <SearchButton />
<div className={'flex ml-auto items-center'}> <div className={"flex ml-auto items-center"}>
<LangToggle /> <LangToggle />
<ModeToggle /> <ModeToggle />
<Link <Link
href="/settings/general" href="/settings/general"
aria-label="Settings" aria-label="Settings"
className={cn('', buttonVariants({ variant: 'ghost', size: 'icon' }))} className={cn("", buttonVariants({ variant: "ghost", size: "icon" }))}
> >
<SettingsIcon className="h-[1.2rem] w-[1.2rem]" /> <SettingsIcon className="h-[1.2rem] w-[1.2rem]" />
</Link> </Link>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<button <button aria-label="User Actions" className={cn("", buttonVariants({ variant: "ghost", size: "icon" }))}>
aria-label="User Actions"
className={cn('', buttonVariants({ variant: 'ghost', size: 'icon' }))}
>
<UserIcon className="h-[1.2rem] w-[1.2rem]" /> <UserIcon className="h-[1.2rem] w-[1.2rem]" />
</button> </button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align={isReadOnlyUser() ? 'end' : 'center'} className="min-w-44"> <DropdownMenuContent align={isReadOnlyUser() ? "end" : "center"} className="min-w-44">
<DropdownMenuLabel>{pb.authStore.model?.email}</DropdownMenuLabel> <DropdownMenuLabel>{pb.authStore.model?.email}</DropdownMenuLabel>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuGroup> <DropdownMenuGroup>
@@ -70,31 +68,31 @@ export default function Navbar(t: TFunction<'translation', undefined>) {
<DropdownMenuItem asChild> <DropdownMenuItem asChild>
<a href="/_/" target="_blank"> <a href="/_/" target="_blank">
<UsersIcon className="mr-2.5 h-4 w-4" /> <UsersIcon className="mr-2.5 h-4 w-4" />
<span>{t('user_dm.users')}</span> <span>{t("user_dm.users")}</span>
</a> </a>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem asChild> <DropdownMenuItem asChild>
<a href="/_/#/collections?collectionId=2hz5ncl8tizk5nx" target="_blank"> <a href="/_/#/collections?collectionId=2hz5ncl8tizk5nx" target="_blank">
<ServerIcon className="mr-2.5 h-4 w-4" /> <ServerIcon className="mr-2.5 h-4 w-4" />
<span>{t('systems')}</span> <span>{t("systems")}</span>
</a> </a>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem asChild> <DropdownMenuItem asChild>
<a href="/_/#/logs" target="_blank"> <a href="/_/#/logs" target="_blank">
<LogsIcon className="mr-2.5 h-4 w-4" /> <LogsIcon className="mr-2.5 h-4 w-4" />
<span>{t('user_dm.logs')}</span> <span>{t("user_dm.logs")}</span>
</a> </a>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem asChild> <DropdownMenuItem asChild>
<a href="/_/#/settings/backups" target="_blank"> <a href="/_/#/settings/backups" target="_blank">
<DatabaseBackupIcon className="mr-2.5 h-4 w-4" /> <DatabaseBackupIcon className="mr-2.5 h-4 w-4" />
<span>{t('user_dm.backups')}</span> <span>{t("user_dm.backups")}</span>
</a> </a>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem asChild> <DropdownMenuItem asChild>
<a href="/_/#/settings/auth-providers" target="_blank"> <a href="/_/#/settings/auth-providers" target="_blank">
<LockKeyholeIcon className="mr-2.5 h-4 w-4" /> <LockKeyholeIcon className="mr-2.5 h-4 w-4" />
<span>{t('user_dm.auth_providers')}</span> <span>{t("user_dm.auth_providers")}</span>
</a> </a>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
@@ -103,7 +101,7 @@ export default function Navbar(t: TFunction<'translation', undefined>) {
</DropdownMenuGroup> </DropdownMenuGroup>
<DropdownMenuItem onSelect={() => pb.authStore.clear()}> <DropdownMenuItem onSelect={() => pb.authStore.clear()}>
<LogOutIcon className="mr-2.5 h-4 w-4" /> <LogOutIcon className="mr-2.5 h-4 w-4" />
<span>{t('user_dm.log_out')}</span> <span>{t("user_dm.log_out")}</span>
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
@@ -134,7 +132,7 @@ function SearchButton() {
Search Search
<span className="sr-only">Search</span> <span className="sr-only">Search</span>
<span className="flex items-center ml-3.5"> <span className="flex items-center ml-3.5">
<Kbd>{isMac ? '⌘' : 'Ctrl'}</Kbd> <Kbd>{isMac ? "⌘" : "Ctrl"}</Kbd>
<Kbd>K</Kbd> <Kbd>K</Kbd>
</span> </span>
</span> </span>

View File

@@ -1,11 +1,11 @@
import { createRouter } from '@nanostores/router' import { createRouter } from "@nanostores/router"
export const $router = createRouter( export const $router = createRouter(
{ {
home: '/', home: "/",
server: '/system/:name', server: "/system/:name",
settings: '/settings/:name?', settings: "/settings/:name?",
forgot_password: '/forgot-password', forgot_password: "/forgot-password",
}, },
{ links: false } { links: false }
) )

View File

@@ -1,17 +1,17 @@
import { Suspense, lazy, useEffect, useMemo, useState } from 'react' import { Suspense, lazy, useEffect, useMemo, useState } from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"
import { $alerts, $hubVersion, $systems, pb } from '@/lib/stores' import { $alerts, $hubVersion, $systems, pb } from "@/lib/stores"
import { useStore } from '@nanostores/react' import { useStore } from "@nanostores/react"
import { GithubIcon } from 'lucide-react' import { GithubIcon } from "lucide-react"
import { Separator } from '../ui/separator' import { Separator } from "../ui/separator"
import { alertInfo, updateRecordList, updateSystemList } from '@/lib/utils' import { alertInfo, updateRecordList, updateSystemList } from "@/lib/utils"
import { AlertRecord, SystemRecord } from '@/types' import { AlertRecord, SystemRecord } from "@/types"
import { Input } from '../ui/input' import { Input } from "../ui/input"
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import { Link } from '../router' import { Link } from "../router"
import { useTranslation } from 'react-i18next' import { useTranslation } from "react-i18next"
const SystemsTable = lazy(() => import('../systems-table/systems-table')) const SystemsTable = lazy(() => import("../systems-table/systems-table"))
export default function () { export default function () {
const { t } = useTranslation() const { t } = useTranslation()
@@ -35,21 +35,21 @@ export default function () {
}, [alerts]) }, [alerts])
useEffect(() => { useEffect(() => {
document.title = 'Dashboard / Beszel' document.title = "Dashboard / Beszel"
// make sure we have the latest list of systems // make sure we have the latest list of systems
updateSystemList() updateSystemList()
// subscribe to real time updates for systems / alerts // subscribe to real time updates for systems / alerts
pb.collection<SystemRecord>('systems').subscribe('*', (e) => { pb.collection<SystemRecord>("systems").subscribe("*", (e) => {
updateRecordList(e, $systems) updateRecordList(e, $systems)
}) })
// todo: add toast if new triggered alert comes in // todo: add toast if new triggered alert comes in
pb.collection<AlertRecord>('alerts').subscribe('*', (e) => { pb.collection<AlertRecord>("alerts").subscribe("*", (e) => {
updateRecordList(e, $alerts) updateRecordList(e, $alerts)
}) })
return () => { return () => {
pb.collection('systems').unsubscribe('*') pb.collection("systems").unsubscribe("*")
// pb.collection('alerts').unsubscribe('*') // pb.collection('alerts').unsubscribe('*')
} }
}, []) }, [])
@@ -61,7 +61,7 @@ export default function () {
<Card className="mb-4"> <Card className="mb-4">
<CardHeader className="pb-4 px-2 sm:px-6 max-sm:pt-5 max-sm:pb-1"> <CardHeader className="pb-4 px-2 sm:px-6 max-sm:pt-5 max-sm:pb-1">
<div className="px-2 sm:px-1"> <div className="px-2 sm:px-1">
<CardTitle>{t('home.active_alerts')}</CardTitle> <CardTitle>{t("home.active_alerts")}</CardTitle>
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="max-sm:p-2"> <CardContent className="max-sm:p-2">
@@ -79,7 +79,7 @@ export default function () {
{alert.sysname} {t(info.name)} {alert.sysname} {t(info.name)}
</AlertTitle> </AlertTitle>
<AlertDescription> <AlertDescription>
{t('home.active_des', { {t("home.active_des", {
value: alert.value, value: alert.value,
unit: info.unit, unit: info.unit,
minutes: alert.min, minutes: alert.min,
@@ -102,11 +102,11 @@ export default function () {
<CardHeader className="pb-5 px-2 sm:px-6 max-sm:pt-5 max-sm:pb-1"> <CardHeader className="pb-5 px-2 sm:px-6 max-sm:pt-5 max-sm:pb-1">
<div className="grid md:flex gap-3 w-full items-end"> <div className="grid md:flex gap-3 w-full items-end">
<div className="px-2 sm:px-1"> <div className="px-2 sm:px-1">
<CardTitle className="mb-2.5">{t('all_systems')}</CardTitle> <CardTitle className="mb-2.5">{t("all_systems")}</CardTitle>
<CardDescription>{t('home.subtitle_1')}</CardDescription> <CardDescription>{t("home.subtitle_1")}</CardDescription>
</div> </div>
<Input <Input
placeholder={t('filter')} placeholder={t("filter")}
onChange={(e) => setFilter(e.target.value)} onChange={(e) => setFilter(e.target.value)}
className="w-full md:w-56 lg:w-72 ml-auto px-4" className="w-full md:w-56 lg:w-72 ml-auto px-4"
/> />

View File

@@ -1,21 +1,21 @@
import { isAdmin } from '@/lib/utils' import { isAdmin } from "@/lib/utils"
import { Separator } from '@/components/ui/separator' import { Separator } from "@/components/ui/separator"
import { Button } from '@/components/ui/button' import { Button } from "@/components/ui/button"
import { redirectPage } from '@nanostores/router' import { redirectPage } from "@nanostores/router"
import { $router } from '@/components/router' import { $router } from "@/components/router"
import { AlertCircleIcon, FileSlidersIcon, LoaderCircleIcon } from 'lucide-react' import { AlertCircleIcon, FileSlidersIcon, LoaderCircleIcon } from "lucide-react"
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import { pb } from '@/lib/stores' import { pb } from "@/lib/stores"
import { useState } from 'react' import { useState } from "react"
import { Textarea } from '@/components/ui/textarea' import { Textarea } from "@/components/ui/textarea"
import { toast } from '@/components/ui/use-toast' import { toast } from "@/components/ui/use-toast"
import clsx from 'clsx' import clsx from "clsx"
import { useTranslation } from 'react-i18next' import { useTranslation } from "react-i18next"
export default function ConfigYaml() { export default function ConfigYaml() {
const { t } = useTranslation() const { t } = useTranslation()
const [configContent, setConfigContent] = useState<string>('') const [configContent, setConfigContent] = useState<string>("")
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const ButtonIcon = isLoading ? LoaderCircleIcon : FileSlidersIcon const ButtonIcon = isLoading ? LoaderCircleIcon : FileSlidersIcon
@@ -23,13 +23,13 @@ export default function ConfigYaml() {
async function fetchConfig() { async function fetchConfig() {
try { try {
setIsLoading(true) setIsLoading(true)
const { config } = await pb.send<{ config: string }>('/api/beszel/config-yaml', {}) const { config } = await pb.send<{ config: string }>("/api/beszel/config-yaml", {})
setConfigContent(config) setConfigContent(config)
} catch (error: any) { } catch (error: any) {
toast({ toast({
title: 'Error', title: "Error",
description: error.message, description: error.message,
variant: 'destructive', variant: "destructive",
}) })
} finally { } finally {
setIsLoading(false) setIsLoading(false)
@@ -37,33 +37,29 @@ export default function ConfigYaml() {
} }
if (!isAdmin()) { if (!isAdmin()) {
redirectPage($router, 'settings', { name: 'general' }) redirectPage($router, "settings", { name: "general" })
} }
return ( return (
<div> <div>
<div> <div>
<h3 className="text-xl font-medium mb-2">{t('settings.yaml_config.title')}</h3> <h3 className="text-xl font-medium mb-2">{t("settings.yaml_config.title")}</h3>
<p className="text-sm text-muted-foreground leading-relaxed"> <p className="text-sm text-muted-foreground leading-relaxed">{t("settings.yaml_config.subtitle")}</p>
{t('settings.yaml_config.subtitle')}
</p>
</div> </div>
<Separator className="my-4" /> <Separator className="my-4" />
<div className="space-y-2"> <div className="space-y-2">
<div className="mb-4"> <div className="mb-4">
<p className="text-sm text-muted-foreground leading-relaxed my-1"> <p className="text-sm text-muted-foreground leading-relaxed my-1">
{t('settings.yaml_config.des_1')}{' '} {t("settings.yaml_config.des_1")} <code className="bg-muted rounded-sm px-1 text-primary">config.yml</code>{" "}
<code className="bg-muted rounded-sm px-1 text-primary">config.yml</code> {t('settings.yaml_config.des_2')} {t("settings.yaml_config.des_2")}
</p>
<p className="text-sm text-muted-foreground leading-relaxed">
{t('settings.yaml_config.des_3')}
</p> </p>
<p className="text-sm text-muted-foreground leading-relaxed">{t("settings.yaml_config.des_3")}</p>
<Alert className="my-4 border-destructive text-destructive w-auto table md:pr-6"> <Alert className="my-4 border-destructive text-destructive w-auto table md:pr-6">
<AlertCircleIcon className="h-4 w-4 stroke-destructive" /> <AlertCircleIcon className="h-4 w-4 stroke-destructive" />
<AlertTitle>{t('settings.yaml_config.alert.title')}</AlertTitle> <AlertTitle>{t("settings.yaml_config.alert.title")}</AlertTitle>
<AlertDescription> <AlertDescription>
<p> <p>
{t('settings.yaml_config.alert.des_1')} <code>config.yml</code> {t('settings.yaml_config.alert.des_2')} {t("settings.yaml_config.alert.des_1")} <code>config.yml</code> {t("settings.yaml_config.alert.des_2")}
</p> </p>
</AlertDescription> </AlertDescription>
</Alert> </Alert>
@@ -73,20 +69,15 @@ export default function ConfigYaml() {
autoFocus autoFocus
defaultValue={configContent} defaultValue={configContent}
spellCheck="false" spellCheck="false"
rows={Math.min(25, configContent.split('\n').length)} rows={Math.min(25, configContent.split("\n").length)}
className="font-mono whitespace-pre" className="font-mono whitespace-pre"
/> />
)} )}
</div> </div>
<Separator className="my-5" /> <Separator className="my-5" />
<Button <Button type="button" className="mt-2 flex items-center gap-1" onClick={fetchConfig} disabled={isLoading}>
type="button" <ButtonIcon className={clsx("h-4 w-4 mr-0.5", isLoading && "animate-spin")} />
className="mt-2 flex items-center gap-1" {t("settings.export_configuration")}
onClick={fetchConfig}
disabled={isLoading}
>
<ButtonIcon className={clsx('h-4 w-4 mr-0.5', isLoading && 'animate-spin')} />
{t('settings.export_configuration')}
</Button> </Button>
</div> </div>
) )

View File

@@ -1,21 +1,15 @@
import { Button } from '@/components/ui/button' import { Button } from "@/components/ui/button"
import { Label } from '@/components/ui/label' import { Label } from "@/components/ui/label"
import { import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
Select, import { chartTimeData } from "@/lib/utils"
SelectContent, import { Separator } from "@/components/ui/separator"
SelectItem, import { LanguagesIcon, LoaderCircleIcon, SaveIcon } from "lucide-react"
SelectTrigger, import { UserSettings } from "@/types"
SelectValue, import { saveSettings } from "./layout"
} from '@/components/ui/select' import { useState, useEffect } from "react"
import { chartTimeData } from '@/lib/utils'
import { Separator } from '@/components/ui/separator'
import { LanguagesIcon, LoaderCircleIcon, SaveIcon } from 'lucide-react'
import { UserSettings } from '@/types'
import { saveSettings } from './layout'
import { useState, useEffect } from 'react'
// import { Input } from '@/components/ui/input' // import { Input } from '@/components/ui/input'
import { useTranslation } from 'react-i18next' import { useTranslation } from "react-i18next"
import languages from '../../../lib/languages.json' import languages from "../../../lib/languages.json"
export default function SettingsProfilePage({ userSettings }: { userSettings: UserSettings }) { export default function SettingsProfilePage({ userSettings }: { userSettings: UserSettings }) {
const { t, i18n } = useTranslation() const { t, i18n } = useTranslation()
@@ -38,10 +32,8 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
return ( return (
<div> <div>
<div> <div>
<h3 className="text-xl font-medium mb-2">{t('settings.general.title')}</h3> <h3 className="text-xl font-medium mb-2">{t("settings.general.title")}</h3>
<p className="text-sm text-muted-foreground leading-relaxed"> <p className="text-sm text-muted-foreground leading-relaxed">{t("settings.general.subtitle")}</p>
{t('settings.general.subtitle')}
</p>
</div> </div>
<Separator className="my-4" /> <Separator className="my-4" />
<form onSubmit={handleSubmit} className="space-y-5"> <form onSubmit={handleSubmit} className="space-y-5">
@@ -49,18 +41,18 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
<div className="mb-4"> <div className="mb-4">
<h3 className="mb-1 text-lg font-medium flex items-center gap-2"> <h3 className="mb-1 text-lg font-medium flex items-center gap-2">
<LanguagesIcon className="h-4 w-4" /> <LanguagesIcon className="h-4 w-4" />
{t('settings.general.language.title')} {t("settings.general.language.title")}
</h3> </h3>
<p className="text-sm text-muted-foreground leading-relaxed"> <p className="text-sm text-muted-foreground leading-relaxed">
{t('settings.general.language.subtitle_1')}{' '} {t("settings.general.language.subtitle_1")}{" "}
<a href="https://crowdin.com/project/beszel" className="link" target="_blank"> <a href="https://crowdin.com/project/beszel" className="link" target="_blank">
Crowdin Crowdin
</a>{' '} </a>{" "}
{t('settings.general.language.subtitle_2')} {t("settings.general.language.subtitle_2")}
</p> </p>
</div> </div>
<Label className="block" htmlFor="lang"> <Label className="block" htmlFor="lang">
{t('settings.general.language.preferred_language')} {t("settings.general.language.preferred_language")}
</Label> </Label>
<Select value={i18n.language} onValueChange={(lang: string) => i18n.changeLanguage(lang)}> <Select value={i18n.language} onValueChange={(lang: string) => i18n.changeLanguage(lang)}>
<SelectTrigger id="lang"> <SelectTrigger id="lang">
@@ -78,21 +70,15 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
<Separator /> <Separator />
<div className="space-y-2"> <div className="space-y-2">
<div className="mb-4"> <div className="mb-4">
<h3 className="mb-1 text-lg font-medium"> <h3 className="mb-1 text-lg font-medium">{t("settings.general.chart_options.title")}</h3>
{t('settings.general.chart_options.title')}
</h3>
<p className="text-sm text-muted-foreground leading-relaxed"> <p className="text-sm text-muted-foreground leading-relaxed">
{t('settings.general.chart_options.subtitle')} {t("settings.general.chart_options.subtitle")}
</p> </p>
</div> </div>
<Label className="block" htmlFor="chartTime"> <Label className="block" htmlFor="chartTime">
{t('settings.general.chart_options.default_time_period')} {t("settings.general.chart_options.default_time_period")}
</Label> </Label>
<Select <Select name="chartTime" key={userSettings.chartTime} defaultValue={userSettings.chartTime}>
name="chartTime"
key={userSettings.chartTime}
defaultValue={userSettings.chartTime}
>
<SelectTrigger id="chartTime"> <SelectTrigger id="chartTime">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
@@ -105,21 +91,13 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
</SelectContent> </SelectContent>
</Select> </Select>
<p className="text-[0.8rem] text-muted-foreground"> <p className="text-[0.8rem] text-muted-foreground">
{t('settings.general.chart_options.default_time_period_des')} {t("settings.general.chart_options.default_time_period_des")}
</p> </p>
</div> </div>
<Separator /> <Separator />
<Button <Button type="submit" className="flex items-center gap-1.5 disabled:opacity-100" disabled={isLoading}>
type="submit" {isLoading ? <LoaderCircleIcon className="h-4 w-4 animate-spin" /> : <SaveIcon className="h-4 w-4" />}
className="flex items-center gap-1.5 disabled:opacity-100" {t("settings.save_settings")}
disabled={isLoading}
>
{isLoading ? (
<LoaderCircleIcon className="h-4 w-4 animate-spin" />
) : (
<SaveIcon className="h-4 w-4" />
)}
{t('settings.save_settings')}
</Button> </Button>
</form> </form>
</div> </div>

View File

@@ -1,28 +1,28 @@
import { useEffect } from 'react' import { useEffect } from "react"
import { Separator } from '../../ui/separator' import { Separator } from "../../ui/separator"
import { SidebarNav } from './sidebar-nav.tsx' import { SidebarNav } from "./sidebar-nav.tsx"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card.tsx' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card.tsx"
import { useStore } from '@nanostores/react' import { useStore } from "@nanostores/react"
import { $router } from '@/components/router.tsx' import { $router } from "@/components/router.tsx"
import { redirectPage } from '@nanostores/router' import { redirectPage } from "@nanostores/router"
import { BellIcon, FileSlidersIcon, SettingsIcon } from 'lucide-react' import { BellIcon, FileSlidersIcon, SettingsIcon } from "lucide-react"
import { $userSettings, pb } from '@/lib/stores.ts' import { $userSettings, pb } from "@/lib/stores.ts"
import { toast } from '@/components/ui/use-toast.ts' import { toast } from "@/components/ui/use-toast.ts"
import { UserSettings } from '@/types.js' import { UserSettings } from "@/types.js"
import General from './general.tsx' import General from "./general.tsx"
import Notifications from './notifications.tsx' import Notifications from "./notifications.tsx"
import ConfigYaml from './config-yaml.tsx' import ConfigYaml from "./config-yaml.tsx"
import { isAdmin } from '@/lib/utils.ts' import { isAdmin } from "@/lib/utils.ts"
import { useTranslation } from 'react-i18next' import { useTranslation } from "react-i18next"
export async function saveSettings(newSettings: Partial<UserSettings>) { export async function saveSettings(newSettings: Partial<UserSettings>) {
try { try {
// get fresh copy of settings // get fresh copy of settings
const req = await pb.collection('user_settings').getFirstListItem('', { const req = await pb.collection("user_settings").getFirstListItem("", {
fields: 'id,settings', fields: "id,settings",
}) })
// update user settings // update user settings
const updatedSettings = await pb.collection('user_settings').update(req.id, { const updatedSettings = await pb.collection("user_settings").update(req.id, {
settings: { settings: {
...req.settings, ...req.settings,
...newSettings, ...newSettings,
@@ -30,15 +30,15 @@ export async function saveSettings(newSettings: Partial<UserSettings>) {
}) })
$userSettings.set(updatedSettings.settings) $userSettings.set(updatedSettings.settings)
toast({ toast({
title: 'Settings saved', title: "Settings saved",
description: 'Your user settings have been updated.', description: "Your user settings have been updated.",
}) })
} catch (e) { } catch (e) {
// console.error('update settings', e) // console.error('update settings', e)
toast({ toast({
title: 'Failed to save settings', title: "Failed to save settings",
description: 'Check logs for more details.', description: "Check logs for more details.",
variant: 'destructive', variant: "destructive",
}) })
} }
} }
@@ -48,21 +48,21 @@ export default function SettingsLayout() {
const sidebarNavItems = [ const sidebarNavItems = [
{ {
title: t('settings.general.title'), title: t("settings.general.title"),
href: '/settings/general', href: "/settings/general",
icon: SettingsIcon, icon: SettingsIcon,
}, },
{ {
title: t('settings.notifications.title'), title: t("settings.notifications.title"),
href: '/settings/notifications', href: "/settings/notifications",
icon: BellIcon, icon: BellIcon,
}, },
] ]
if (isAdmin()) { if (isAdmin()) {
sidebarNavItems.push({ sidebarNavItems.push({
title: t('settings.yaml_config.short_title'), title: t("settings.yaml_config.short_title"),
href: '/settings/config', href: "/settings/config",
icon: FileSlidersIcon, icon: FileSlidersIcon,
}) })
} }
@@ -70,18 +70,18 @@ export default function SettingsLayout() {
const page = useStore($router) const page = useStore($router)
useEffect(() => { useEffect(() => {
document.title = 'Settings / Beszel' document.title = "Settings / Beszel"
// redirect to account page if no page is specified // redirect to account page if no page is specified
if (page?.path === '/settings') { if (page?.path === "/settings") {
redirectPage($router, 'settings', { name: 'general' }) redirectPage($router, "settings", { name: "general" })
} }
}, []) }, [])
return ( return (
<Card className="pt-5 px-4 pb-8 sm:pt-6 sm:px-7"> <Card className="pt-5 px-4 pb-8 sm:pt-6 sm:px-7">
<CardHeader className="p-0"> <CardHeader className="p-0">
<CardTitle className="mb-1">{t('settings.settings')}</CardTitle> <CardTitle className="mb-1">{t("settings.settings")}</CardTitle>
<CardDescription>{t('settings.subtitle')}</CardDescription> <CardDescription>{t("settings.subtitle")}</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="p-0"> <CardContent className="p-0">
<Separator className="hidden md:block my-5" /> <Separator className="hidden md:block my-5" />
@@ -91,7 +91,7 @@ export default function SettingsLayout() {
</aside> </aside>
<div className="flex-1"> <div className="flex-1">
{/* @ts-ignore */} {/* @ts-ignore */}
<SettingsContent name={page?.params?.name ?? 'general'} /> <SettingsContent name={page?.params?.name ?? "general"} />
</div> </div>
</div> </div>
</CardContent> </CardContent>
@@ -103,11 +103,11 @@ function SettingsContent({ name }: { name: string }) {
const userSettings = useStore($userSettings) const userSettings = useStore($userSettings)
switch (name) { switch (name) {
case 'general': case "general":
return <General userSettings={userSettings} /> return <General userSettings={userSettings} />
case 'notifications': case "notifications":
return <Notifications userSettings={userSettings} /> return <Notifications userSettings={userSettings} />
case 'config': case "config":
return <ConfigYaml /> return <ConfigYaml />
} }
} }

View File

@@ -1,18 +1,18 @@
import { Button } from '@/components/ui/button' import { Button } from "@/components/ui/button"
import { Input } from '@/components/ui/input' import { Input } from "@/components/ui/input"
import { Label } from '@/components/ui/label' import { Label } from "@/components/ui/label"
import { pb } from '@/lib/stores' import { pb } from "@/lib/stores"
import { Separator } from '@/components/ui/separator' import { Separator } from "@/components/ui/separator"
import { Card } from '@/components/ui/card' import { Card } from "@/components/ui/card"
import { BellIcon, LoaderCircleIcon, PlusIcon, SaveIcon, Trash2Icon } from 'lucide-react' import { BellIcon, LoaderCircleIcon, PlusIcon, SaveIcon, Trash2Icon } from "lucide-react"
import { ChangeEventHandler, useEffect, useState } from 'react' import { ChangeEventHandler, useEffect, useState } from "react"
import { toast } from '@/components/ui/use-toast' import { toast } from "@/components/ui/use-toast"
import { InputTags } from '@/components/ui/input-tags' import { InputTags } from "@/components/ui/input-tags"
import { UserSettings } from '@/types' import { UserSettings } from "@/types"
import { saveSettings } from './layout' import { saveSettings } from "./layout"
import * as v from 'valibot' import * as v from "valibot"
import { isAdmin } from '@/lib/utils' import { isAdmin } from "@/lib/utils"
import { useTranslation } from 'react-i18next' import { useTranslation } from "react-i18next"
interface ShoutrrrUrlCardProps { interface ShoutrrrUrlCardProps {
url: string url: string
@@ -39,10 +39,10 @@ const SettingsNotificationsPage = ({ userSettings }: { userSettings: UserSetting
}, [userSettings]) }, [userSettings])
function addWebhook() { function addWebhook() {
setWebhooks([...webhooks, '']) setWebhooks([...webhooks, ""])
// focus on the new input // focus on the new input
queueMicrotask(() => { queueMicrotask(() => {
const inputs = document.querySelectorAll('#webhooks input') as NodeListOf<HTMLInputElement> const inputs = document.querySelectorAll("#webhooks input") as NodeListOf<HTMLInputElement>
inputs[inputs.length - 1]?.focus() inputs[inputs.length - 1]?.focus()
}) })
} }
@@ -61,9 +61,9 @@ const SettingsNotificationsPage = ({ userSettings }: { userSettings: UserSetting
await saveSettings(parsedData) await saveSettings(parsedData)
} catch (e: any) { } catch (e: any) {
toast({ toast({
title: 'Failed to save settings', title: "Failed to save settings",
description: e.message, description: e.message,
variant: 'destructive', variant: "destructive",
}) })
} }
setIsLoading(false) setIsLoading(false)
@@ -72,63 +72,51 @@ const SettingsNotificationsPage = ({ userSettings }: { userSettings: UserSetting
return ( return (
<div> <div>
<div> <div>
<h3 className="text-xl font-medium mb-2">{t('settings.notifications.title')}</h3> <h3 className="text-xl font-medium mb-2">{t("settings.notifications.title")}</h3>
<p className="text-sm text-muted-foreground leading-relaxed"> <p className="text-sm text-muted-foreground leading-relaxed">{t("settings.notifications.subtitle_1")}</p>
{t('settings.notifications.subtitle_1')}
</p>
<p className="text-sm text-muted-foreground mt-1.5 leading-relaxed"> <p className="text-sm text-muted-foreground mt-1.5 leading-relaxed">
{t('settings.notifications.subtitle_2')}{' '} {t("settings.notifications.subtitle_2")} <BellIcon className="inline h-4 w-4" />{" "}
<BellIcon className="inline h-4 w-4" /> {t('settings.notifications.subtitle_3')} {t("settings.notifications.subtitle_3")}
</p> </p>
</div> </div>
<Separator className="my-4" /> <Separator className="my-4" />
<div className="space-y-5"> <div className="space-y-5">
<div className="space-y-2"> <div className="space-y-2">
<div className="mb-4"> <div className="mb-4">
<h3 className="mb-1 text-lg font-medium"> <h3 className="mb-1 text-lg font-medium">{t("settings.notifications.email.title")}</h3>
{t('settings.notifications.email.title')}
</h3>
{isAdmin() && ( {isAdmin() && (
<p className="text-sm text-muted-foreground leading-relaxed"> <p className="text-sm text-muted-foreground leading-relaxed">
{t('settings.notifications.email.please')}{' '} {t("settings.notifications.email.please")}{" "}
<a href="/_/#/settings/mail" className="link" target="_blank"> <a href="/_/#/settings/mail" className="link" target="_blank">
{t('settings.notifications.email.configure_an_SMTP_server')} {t("settings.notifications.email.configure_an_SMTP_server")}
</a>{' '} </a>{" "}
{t('settings.notifications.email.to_ensure_alerts_are_delivered')}{' '} {t("settings.notifications.email.to_ensure_alerts_are_delivered")}{" "}
</p> </p>
)} )}
</div> </div>
<Label className="block" htmlFor="email"> <Label className="block" htmlFor="email">
{t('settings.notifications.email.to_email_s')} {t("settings.notifications.email.to_email_s")}
</Label> </Label>
<InputTags <InputTags
value={emails} value={emails}
onChange={setEmails} onChange={setEmails}
placeholder={t('settings.notifications.email.enter_email_address')} placeholder={t("settings.notifications.email.enter_email_address")}
className="w-full" className="w-full"
type="email" type="email"
id="email" id="email"
/> />
<p className="text-[0.8rem] text-muted-foreground"> <p className="text-[0.8rem] text-muted-foreground">{t("settings.notifications.email.des")}</p>
{t('settings.notifications.email.des')}
</p>
</div> </div>
<Separator /> <Separator />
<div className="space-y-3"> <div className="space-y-3">
<div> <div>
<h3 className="mb-1 text-lg font-medium"> <h3 className="mb-1 text-lg font-medium">{t("settings.notifications.webhook_push.title")}</h3>
{t('settings.notifications.webhook_push.title')}
</h3>
<p className="text-sm text-muted-foreground leading-relaxed"> <p className="text-sm text-muted-foreground leading-relaxed">
{t('settings.notifications.webhook_push.des_1')}{' '} {t("settings.notifications.webhook_push.des_1")}{" "}
<a <a href="https://containrrr.dev/shoutrrr/services/overview/" target="_blank" className="link">
href="https://containrrr.dev/shoutrrr/services/overview/"
target="_blank"
className="link"
>
Shoutrrr Shoutrrr
</a>{' '} </a>{" "}
{t('settings.notifications.webhook_push.des_2')} {t("settings.notifications.webhook_push.des_2")}
</p> </p>
</div> </div>
{webhooks.length > 0 && ( {webhooks.length > 0 && (
@@ -137,9 +125,7 @@ const SettingsNotificationsPage = ({ userSettings }: { userSettings: UserSetting
<ShoutrrrUrlCard <ShoutrrrUrlCard
key={index} key={index}
url={webhook} url={webhook}
onUrlChange={(e: React.ChangeEvent<HTMLInputElement>) => onUrlChange={(e: React.ChangeEvent<HTMLInputElement>) => updateWebhook(index, e.target.value)}
updateWebhook(index, e.target.value)
}
onRemove={() => removeWebhook(index)} onRemove={() => removeWebhook(index)}
/> />
))} ))}
@@ -153,7 +139,7 @@ const SettingsNotificationsPage = ({ userSettings }: { userSettings: UserSetting
onClick={addWebhook} onClick={addWebhook}
> >
<PlusIcon className="h-4 w-4 -ml-0.5" /> <PlusIcon className="h-4 w-4 -ml-0.5" />
{t('settings.notifications.webhook_push.add_url')} {t("settings.notifications.webhook_push.add_url")}
</Button> </Button>
</div> </div>
<Separator /> <Separator />
@@ -163,12 +149,8 @@ const SettingsNotificationsPage = ({ userSettings }: { userSettings: UserSetting
onClick={updateSettings} onClick={updateSettings}
disabled={isLoading} disabled={isLoading}
> >
{isLoading ? ( {isLoading ? <LoaderCircleIcon className="h-4 w-4 animate-spin" /> : <SaveIcon className="h-4 w-4" />}
<LoaderCircleIcon className="h-4 w-4 animate-spin" /> {t("settings.save_settings")}
) : (
<SaveIcon className="h-4 w-4" />
)}
{t('settings.save_settings')}
</Button> </Button>
</div> </div>
</div> </div>
@@ -180,17 +162,17 @@ const ShoutrrrUrlCard = ({ url, onUrlChange, onRemove }: ShoutrrrUrlCardProps) =
const sendTestNotification = async () => { const sendTestNotification = async () => {
setIsLoading(true) setIsLoading(true)
const res = await pb.send('/api/beszel/send-test-notification', { url }) const res = await pb.send("/api/beszel/send-test-notification", { url })
if ('err' in res && !res.err) { if ("err" in res && !res.err) {
toast({ toast({
title: 'Test notification sent', title: "Test notification sent",
description: 'Check your notification service', description: "Check your notification service",
}) })
} else { } else {
toast({ toast({
title: 'Error', title: "Error",
description: res.err ?? 'Failed to send test notification', description: res.err ?? "Failed to send test notification",
variant: 'destructive', variant: "destructive",
}) })
} }
setIsLoading(false) setIsLoading(false)
@@ -211,7 +193,7 @@ const ShoutrrrUrlCard = ({ url, onUrlChange, onRemove }: ShoutrrrUrlCardProps) =
type="button" type="button"
variant="outline" variant="outline"
className="w-20 md:w-28" className="w-20 md:w-28"
disabled={isLoading || url === ''} disabled={isLoading || url === ""}
onClick={sendTestNotification} onClick={sendTestNotification}
> >
{isLoading ? ( {isLoading ? (
@@ -222,14 +204,7 @@ const ShoutrrrUrlCard = ({ url, onUrlChange, onRemove }: ShoutrrrUrlCardProps) =
</span> </span>
)} )}
</Button> </Button>
<Button <Button type="button" variant="outline" size="icon" className="shrink-0" aria-label="Delete" onClick={onRemove}>
type="button"
variant="outline"
size="icon"
className="shrink-0"
aria-label="Delete"
onClick={onRemove}
>
<Trash2Icon className="h-4 w-4" /> <Trash2Icon className="h-4 w-4" />
</Button> </Button>
</div> </div>

View File

@@ -1,16 +1,10 @@
import React from 'react' import React from "react"
import { cn } from '@/lib/utils' import { cn } from "@/lib/utils"
import { buttonVariants } from '../../ui/button' import { buttonVariants } from "../../ui/button"
import { $router, Link, navigate } from '../../router' import { $router, Link, navigate } from "../../router"
import { useStore } from '@nanostores/react' import { useStore } from "@nanostores/react"
import { import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
Select, import { Separator } from "@/components/ui/separator"
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Separator } from '@/components/ui/separator'
interface SidebarNavProps extends React.HTMLAttributes<HTMLElement> { interface SidebarNavProps extends React.HTMLAttributes<HTMLElement> {
items: { items: {
@@ -46,16 +40,16 @@ export function SidebarNav({ className, items, ...props }: SidebarNavProps) {
</div> </div>
{/* Desktop View */} {/* Desktop View */}
<nav className={cn('hidden md:grid gap-1', className)} {...props}> <nav className={cn("hidden md:grid gap-1", className)} {...props}>
{items.map((item) => ( {items.map((item) => (
<Link <Link
key={item.href} key={item.href}
href={item.href} href={item.href}
className={cn( className={cn(
buttonVariants({ variant: 'ghost' }), buttonVariants({ variant: "ghost" }),
'flex items-center gap-3', "flex items-center gap-3",
page?.path === item.href ? 'bg-muted hover:bg-muted' : 'hover:bg-muted/50', page?.path === item.href ? "bg-muted hover:bg-muted" : "hover:bg-muted/50",
'justify-start' "justify-start"
)} )}
> >
{item.icon && <item.icon className="h-4 w-4" />} {item.icon && <item.icon className="h-4 w-4" />}

View File

@@ -1,40 +1,34 @@
import { $systems, pb, $chartTime, $containerFilter, $userSettings } from '@/lib/stores' import { $systems, pb, $chartTime, $containerFilter, $userSettings } from "@/lib/stores"
import { import { ChartData, ChartTimes, ContainerStatsRecord, SystemRecord, SystemStatsRecord } from "@/types"
ChartData, import React, { lazy, useCallback, useEffect, useMemo, useRef, useState } from "react"
ChartTimes, import { Card, CardHeader, CardTitle, CardDescription } from "../ui/card"
ContainerStatsRecord, import { useStore } from "@nanostores/react"
SystemRecord, import Spinner from "../spinner"
SystemStatsRecord, import { ClockArrowUp, CpuIcon, GlobeIcon, LayoutGridIcon, MonitorIcon, XIcon } from "lucide-react"
} from '@/types' import ChartTimeSelect from "../charts/chart-time-select"
import React, { lazy, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { chartTimeData, cn, getPbTimestamp, useLocalStorage } from "@/lib/utils"
import { Card, CardHeader, CardTitle, CardDescription } from '../ui/card' import { Separator } from "../ui/separator"
import { useStore } from '@nanostores/react' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip"
import Spinner from '../spinner' import { Button } from "../ui/button"
import { ClockArrowUp, CpuIcon, GlobeIcon, LayoutGridIcon, MonitorIcon, XIcon } from 'lucide-react' import { Input } from "../ui/input"
import ChartTimeSelect from '../charts/chart-time-select' import { ChartAverage, ChartMax, Rows, TuxIcon } from "../ui/icons"
import { chartTimeData, cn, getPbTimestamp, useLocalStorage } from '@/lib/utils' import { useIntersectionObserver } from "@/lib/use-intersection-observer"
import { Separator } from '../ui/separator' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip' import { timeTicks } from "d3-time"
import { Button } from '../ui/button' import { useTranslation } from "react-i18next"
import { Input } from '../ui/input'
import { ChartAverage, ChartMax, Rows, TuxIcon } from '../ui/icons'
import { useIntersectionObserver } from '@/lib/use-intersection-observer'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'
import { timeTicks } from 'd3-time'
import { useTranslation } from 'react-i18next'
const AreaChartDefault = lazy(() => import('../charts/area-chart')) const AreaChartDefault = lazy(() => import("../charts/area-chart"))
const ContainerChart = lazy(() => import('../charts/container-chart')) const ContainerChart = lazy(() => import("../charts/container-chart"))
const MemChart = lazy(() => import('../charts/mem-chart')) const MemChart = lazy(() => import("../charts/mem-chart"))
const DiskChart = lazy(() => import('../charts/disk-chart')) const DiskChart = lazy(() => import("../charts/disk-chart"))
const SwapChart = lazy(() => import('../charts/swap-chart')) const SwapChart = lazy(() => import("../charts/swap-chart"))
const TemperatureChart = lazy(() => import('../charts/temperature-chart')) const TemperatureChart = lazy(() => import("../charts/temperature-chart"))
const cache = new Map<string, any>() const cache = new Map<string, any>()
// create ticks and domain for charts // create ticks and domain for charts
function getTimeData(chartTime: ChartTimes, lastCreated: number) { function getTimeData(chartTime: ChartTimes, lastCreated: number) {
const cached = cache.get('td') const cached = cache.get("td")
if (cached && cached.chartTime === chartTime) { if (cached && cached.chartTime === chartTime) {
if (!lastCreated || cached.time >= lastCreated) { if (!lastCreated || cached.time >= lastCreated) {
return cached.data return cached.data
@@ -43,14 +37,12 @@ function getTimeData(chartTime: ChartTimes, lastCreated: number) {
const now = new Date() const now = new Date()
const startTime = chartTimeData[chartTime].getOffset(now) const startTime = chartTimeData[chartTime].getOffset(now)
const ticks = timeTicks(startTime, now, chartTimeData[chartTime].ticks ?? 12).map((date) => const ticks = timeTicks(startTime, now, chartTimeData[chartTime].ticks ?? 12).map((date) => date.getTime())
date.getTime()
)
const data = { const data = {
ticks, ticks,
domain: [chartTimeData[chartTime].getOffset(now).getTime(), now.getTime()], domain: [chartTimeData[chartTime].getOffset(now).getTime(), now.getTime()],
} }
cache.set('td', { time: now.getTime(), data, chartTime }) cache.set("td", { time: now.getTime(), data, chartTime })
return data return data
} }
@@ -79,20 +71,16 @@ function addEmptyValues<T extends SystemStatsRecord | ContainerStatsRecord>(
return modifiedRecords return modifiedRecords
} }
async function getStats<T>( async function getStats<T>(collection: string, system: SystemRecord, chartTime: ChartTimes): Promise<T[]> {
collection: string,
system: SystemRecord,
chartTime: ChartTimes
): Promise<T[]> {
const lastCached = cache.get(`${system.id}_${chartTime}_${collection}`)?.at(-1)?.created as number const lastCached = cache.get(`${system.id}_${chartTime}_${collection}`)?.at(-1)?.created as number
return await pb.collection<T>(collection).getFullList({ return await pb.collection<T>(collection).getFullList({
filter: pb.filter('system={:id} && created > {:created} && type={:type}', { filter: pb.filter("system={:id} && created > {:created} && type={:type}", {
id: system.id, id: system.id,
created: getPbTimestamp(chartTime, lastCached ? new Date(lastCached + 1000) : undefined), created: getPbTimestamp(chartTime, lastCached ? new Date(lastCached + 1000) : undefined),
type: chartTimeData[chartTime].type, type: chartTimeData[chartTime].type,
}), }),
fields: 'created,stats', fields: "created,stats",
sort: 'created', sort: "created",
}) })
} }
@@ -105,15 +93,15 @@ export default function SystemDetail({ name }: { name: string }) {
const cpuMaxStore = useState(false) const cpuMaxStore = useState(false)
const bandwidthMaxStore = useState(false) const bandwidthMaxStore = useState(false)
const diskIoMaxStore = useState(false) const diskIoMaxStore = useState(false)
const [grid, setGrid] = useLocalStorage('grid', true) const [grid, setGrid] = useLocalStorage("grid", true)
const [system, setSystem] = useState({} as SystemRecord) const [system, setSystem] = useState({} as SystemRecord)
const [systemStats, setSystemStats] = useState([] as SystemStatsRecord[]) const [systemStats, setSystemStats] = useState([] as SystemStatsRecord[])
const [containerData, setContainerData] = useState([] as ChartData['containerData']) const [containerData, setContainerData] = useState([] as ChartData["containerData"])
const netCardRef = useRef<HTMLDivElement>(null) const netCardRef = useRef<HTMLDivElement>(null)
const [containerFilterBar, setContainerFilterBar] = useState(null as null | JSX.Element) const [containerFilterBar, setContainerFilterBar] = useState(null as null | JSX.Element)
const [bottomSpacing, setBottomSpacing] = useState(0) const [bottomSpacing, setBottomSpacing] = useState(0)
const [chartLoading, setChartLoading] = useState(false) const [chartLoading, setChartLoading] = useState(false)
const isLongerChart = chartTime !== '1h' const isLongerChart = chartTime !== "1h"
useEffect(() => { useEffect(() => {
document.title = `${name} / Beszel` document.title = `${name} / Beszel`
@@ -123,7 +111,7 @@ export default function SystemDetail({ name }: { name: string }) {
setSystemStats([]) setSystemStats([])
setContainerData([]) setContainerData([])
setContainerFilterBar(null) setContainerFilterBar(null)
$containerFilter.set('') $containerFilter.set("")
cpuMaxStore[1](false) cpuMaxStore[1](false)
bandwidthMaxStore[1](false) bandwidthMaxStore[1](false)
diskIoMaxStore[1](false) diskIoMaxStore[1](false)
@@ -153,11 +141,11 @@ export default function SystemDetail({ name }: { name: string }) {
if (!system.id) { if (!system.id) {
return return
} }
pb.collection<SystemRecord>('systems').subscribe(system.id, (e) => { pb.collection<SystemRecord>("systems").subscribe(system.id, (e) => {
setSystem(e.record) setSystem(e.record)
}) })
return () => { return () => {
pb.collection('systems').unsubscribe(system.id) pb.collection("systems").unsubscribe(system.id)
} }
}, [system.id]) }, [system.id])
@@ -182,8 +170,8 @@ export default function SystemDetail({ name }: { name: string }) {
// loading: true // loading: true
setChartLoading(true) setChartLoading(true)
Promise.allSettled([ Promise.allSettled([
getStats<SystemStatsRecord>('system_stats', system, chartTime), getStats<SystemStatsRecord>("system_stats", system, chartTime),
getStats<ContainerStatsRecord>('container_stats', system, chartTime), getStats<ContainerStatsRecord>("container_stats", system, chartTime),
]).then(([systemStats, containerStats]) => { ]).then(([systemStats, containerStats]) => {
// loading: false // loading: false
setChartLoading(false) setChartLoading(false)
@@ -192,10 +180,8 @@ export default function SystemDetail({ name }: { name: string }) {
// make new system stats // make new system stats
const ss_cache_key = `${system.id}_${chartTime}_system_stats` const ss_cache_key = `${system.id}_${chartTime}_system_stats`
let systemData = (cache.get(ss_cache_key) || []) as SystemStatsRecord[] let systemData = (cache.get(ss_cache_key) || []) as SystemStatsRecord[]
if (systemStats.status === 'fulfilled' && systemStats.value.length) { if (systemStats.status === "fulfilled" && systemStats.value.length) {
systemData = systemData.concat( systemData = systemData.concat(addEmptyValues(systemData, systemStats.value, expectedInterval))
addEmptyValues(systemData, systemStats.value, expectedInterval)
)
if (systemData.length > 120) { if (systemData.length > 120) {
systemData = systemData.slice(-100) systemData = systemData.slice(-100)
} }
@@ -205,10 +191,8 @@ export default function SystemDetail({ name }: { name: string }) {
// make new container stats // make new container stats
const cs_cache_key = `${system.id}_${chartTime}_container_stats` const cs_cache_key = `${system.id}_${chartTime}_container_stats`
let containerData = (cache.get(cs_cache_key) || []) as ContainerStatsRecord[] let containerData = (cache.get(cs_cache_key) || []) as ContainerStatsRecord[]
if (containerStats.status === 'fulfilled' && containerStats.value.length) { if (containerStats.status === "fulfilled" && containerStats.value.length) {
containerData = containerData.concat( containerData = containerData.concat(addEmptyValues(containerData, containerStats.value, expectedInterval))
addEmptyValues(containerData, containerStats.value, expectedInterval)
)
if (containerData.length > 120) { if (containerData.length > 120) {
containerData = containerData.slice(-100) containerData = containerData.slice(-100)
} }
@@ -225,7 +209,7 @@ export default function SystemDetail({ name }: { name: string }) {
// make container stats for charts // make container stats for charts
const makeContainerData = useCallback((containers: ContainerStatsRecord[]) => { const makeContainerData = useCallback((containers: ContainerStatsRecord[]) => {
const containerData = [] as ChartData['containerData'] const containerData = [] as ChartData["containerData"]
for (let { created, stats } of containers) { for (let { created, stats } of containers) {
if (!created) { if (!created) {
// @ts-ignore add null value for gaps // @ts-ignore add null value for gaps
@@ -234,7 +218,7 @@ export default function SystemDetail({ name }: { name: string }) {
} }
created = new Date(created).getTime() created = new Date(created).getTime()
// @ts-ignore not dealing with this rn // @ts-ignore not dealing with this rn
let containerStats: ChartData['containerData'][0] = { created } let containerStats: ChartData["containerData"][0] = { created }
for (let container of stats) { for (let container of stats) {
containerStats[container.n] = container containerStats[container.n] = container
} }
@@ -251,7 +235,7 @@ export default function SystemDetail({ name }: { name: string }) {
let uptime: number | string = system.info.u let uptime: number | string = system.info.u
if (system.info.u < 172800) { if (system.info.u < 172800) {
const hours = Math.trunc(uptime / 3600) const hours = Math.trunc(uptime / 3600)
uptime = `${hours} hour${hours == 1 ? '' : 's'}` uptime = `${hours} hour${hours == 1 ? "" : "s"}`
} else { } else {
uptime = `${Math.trunc(system.info?.u / 86400)} days` uptime = `${Math.trunc(system.info?.u / 86400)} days`
} }
@@ -260,14 +244,14 @@ export default function SystemDetail({ name }: { name: string }) {
{ {
value: system.info.h, value: system.info.h,
Icon: MonitorIcon, Icon: MonitorIcon,
label: 'Hostname', label: "Hostname",
// hide if hostname is same as host or name // hide if hostname is same as host or name
hide: system.info.h === system.host || system.info.h === system.name, hide: system.info.h === system.host || system.info.h === system.name,
}, },
{ value: uptime, Icon: ClockArrowUp, label: 'Uptime' }, { value: uptime, Icon: ClockArrowUp, label: "Uptime" },
{ value: system.info.k, Icon: TuxIcon, label: 'Kernel' }, { value: system.info.k, Icon: TuxIcon, label: "Kernel" },
{ {
value: `${system.info.m} (${system.info.c}c${system.info.t ? `/${system.info.t}t` : ''})`, value: `${system.info.m} (${system.info.c}c${system.info.t ? `/${system.info.t}t` : ""})`,
Icon: CpuIcon, Icon: CpuIcon,
hide: !system.info.m, hide: !system.info.m,
}, },
@@ -286,7 +270,7 @@ export default function SystemDetail({ name }: { name: string }) {
return return
} }
const tooltipHeight = (Object.keys(containerData[0]).length - 11) * 17.8 - 40 const tooltipHeight = (Object.keys(containerData[0]).length - 11) * 17.8 - 40
const wrapperEl = document.getElementById('chartwrap') as HTMLDivElement const wrapperEl = document.getElementById("chartwrap") as HTMLDivElement
const wrapperRect = wrapperEl.getBoundingClientRect() const wrapperRect = wrapperEl.getBoundingClientRect()
const chartRect = netCardRef.current.getBoundingClientRect() const chartRect = netCardRef.current.getBoundingClientRect()
const distanceToBottom = wrapperRect.bottom - chartRect.bottom const distanceToBottom = wrapperRect.bottom - chartRect.bottom
@@ -298,7 +282,7 @@ export default function SystemDetail({ name }: { name: string }) {
} }
// if no data, show empty state // if no data, show empty state
const dataEmpty = !chartLoading && chartData.systemStats.length === 0; const dataEmpty = !chartLoading && chartData.systemStats.length === 0
return ( return (
<> <>
@@ -310,19 +294,19 @@ export default function SystemDetail({ name }: { name: string }) {
<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">
<div className="capitalize flex gap-2 items-center"> <div className="capitalize flex gap-2 items-center">
<span className={cn('relative flex h-3 w-3')}> <span className={cn("relative flex h-3 w-3")}>
{system.status === 'up' && ( {system.status === "up" && (
<span <span
className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75" className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"
style={{ animationDuration: '1.5s' }} style={{ animationDuration: "1.5s" }}
></span> ></span>
)} )}
<span <span
className={cn('relative inline-flex rounded-full h-3 w-3', { className={cn("relative inline-flex rounded-full h-3 w-3", {
'bg-green-500': system.status === 'up', "bg-green-500": system.status === "up",
'bg-red-500': system.status === 'down', "bg-red-500": system.status === "down",
'bg-primary/40': system.status === 'paused', "bg-primary/40": system.status === "paused",
'bg-yellow-500': system.status === 'pending', "bg-yellow-500": system.status === "pending",
})} })}
></span> ></span>
</span> </span>
@@ -361,7 +345,7 @@ export default function SystemDetail({ name }: { name: string }) {
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button
aria-label={t('monitor.toggle_grid')} aria-label={t("monitor.toggle_grid")}
variant="outline" variant="outline"
size="icon" size="icon"
className="hidden lg:flex p-0 text-primary" className="hidden lg:flex p-0 text-primary"
@@ -374,7 +358,7 @@ export default function SystemDetail({ name }: { name: string }) {
)} )}
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>{t('monitor.toggle_grid')}</TooltipContent> <TooltipContent>{t("monitor.toggle_grid")}</TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
</div> </div>
@@ -386,24 +370,21 @@ export default function SystemDetail({ name }: { name: string }) {
<ChartCard <ChartCard
empty={dataEmpty} empty={dataEmpty}
grid={grid} grid={grid}
title={t('monitor.total_cpu_usage')} title={t("monitor.total_cpu_usage")}
description={`${cpuMaxStore[0] && isLongerChart ? t('monitor.max_1_min') : t('monitor.average') } ${t('monitor.cpu_des')}`} description={`${cpuMaxStore[0] && isLongerChart ? t("monitor.max_1_min") : t("monitor.average")} ${t(
"monitor.cpu_des"
)}`}
cornerEl={isLongerChart ? <SelectAvgMax store={cpuMaxStore} /> : null} cornerEl={isLongerChart ? <SelectAvgMax store={cpuMaxStore} /> : null}
> >
<AreaChartDefault <AreaChartDefault chartData={chartData} chartName="CPU Usage" maxToggled={cpuMaxStore[0]} unit="%" />
chartData={chartData}
chartName="CPU Usage"
maxToggled={cpuMaxStore[0]}
unit="%"
/>
</ChartCard> </ChartCard>
{containerFilterBar && ( {containerFilterBar && (
<ChartCard <ChartCard
empty={dataEmpty} empty={dataEmpty}
grid={grid} grid={grid}
title={t('monitor.docker_cpu_usage')} title={t("monitor.docker_cpu_usage")}
description={t('monitor.docker_cpu_des')} description={t("monitor.docker_cpu_des")}
cornerEl={containerFilterBar} cornerEl={containerFilterBar}
> >
<ContainerChart chartData={chartData} dataKey="c" chartName="cpu" /> <ContainerChart chartData={chartData} dataKey="c" chartName="cpu" />
@@ -413,8 +394,8 @@ export default function SystemDetail({ name }: { name: string }) {
<ChartCard <ChartCard
empty={dataEmpty} empty={dataEmpty}
grid={grid} grid={grid}
title={t('monitor.total_memory_usage')} title={t("monitor.total_memory_usage")}
description={t('monitor.memory_des')} description={t("monitor.memory_des")}
> >
<MemChart chartData={chartData} /> <MemChart chartData={chartData} />
</ChartCard> </ChartCard>
@@ -423,15 +404,15 @@ export default function SystemDetail({ name }: { name: string }) {
<ChartCard <ChartCard
empty={dataEmpty} empty={dataEmpty}
grid={grid} grid={grid}
title={t('monitor.docker_memory_usage')} title={t("monitor.docker_memory_usage")}
description={t('monitor.docker_memory_des')} description={t("monitor.docker_memory_des")}
cornerEl={containerFilterBar} cornerEl={containerFilterBar}
> >
<ContainerChart chartData={chartData} chartName="mem" dataKey="m" unit=" MB" /> <ContainerChart chartData={chartData} chartName="mem" dataKey="m" unit=" MB" />
</ChartCard> </ChartCard>
)} )}
<ChartCard empty={dataEmpty} grid={grid} title={t('monitor.disk_space')} description={t('monitor.disk_des')}> <ChartCard empty={dataEmpty} grid={grid} title={t("monitor.disk_space")} description={t("monitor.disk_des")}>
<DiskChart <DiskChart
chartData={chartData} chartData={chartData}
dataKey="stats.du" dataKey="stats.du"
@@ -442,42 +423,34 @@ export default function SystemDetail({ name }: { name: string }) {
<ChartCard <ChartCard
empty={dataEmpty} empty={dataEmpty}
grid={grid} grid={grid}
title={t('monitor.disk_io')} title={t("monitor.disk_io")}
description={t('monitor.disk_io_des')} description={t("monitor.disk_io_des")}
cornerEl={isLongerChart ? <SelectAvgMax store={diskIoMaxStore} /> : null} cornerEl={isLongerChart ? <SelectAvgMax store={diskIoMaxStore} /> : null}
> >
<AreaChartDefault <AreaChartDefault chartData={chartData} maxToggled={diskIoMaxStore[0]} chartName="dio" />
chartData={chartData}
maxToggled={diskIoMaxStore[0]}
chartName="dio"
/>
</ChartCard> </ChartCard>
<ChartCard <ChartCard
empty={dataEmpty} empty={dataEmpty}
grid={grid} grid={grid}
title={t('monitor.bandwidth')} title={t("monitor.bandwidth")}
cornerEl={isLongerChart ? <SelectAvgMax store={bandwidthMaxStore} /> : null} cornerEl={isLongerChart ? <SelectAvgMax store={bandwidthMaxStore} /> : null}
description={t('monitor.bandwidth_des')} description={t("monitor.bandwidth_des")}
> >
<AreaChartDefault <AreaChartDefault chartData={chartData} maxToggled={bandwidthMaxStore[0]} chartName="bw" />
chartData={chartData}
maxToggled={bandwidthMaxStore[0]}
chartName="bw"
/>
</ChartCard> </ChartCard>
{containerFilterBar && containerData.length > 0 && ( {containerFilterBar && containerData.length > 0 && (
<div <div
ref={netCardRef} ref={netCardRef}
className={cn({ className={cn({
'col-span-full': !grid, "col-span-full": !grid,
})} })}
> >
<ChartCard <ChartCard
empty={dataEmpty} empty={dataEmpty}
title={t('monitor.docker_network_io')} title={t("monitor.docker_network_io")}
description={t('monitor.docker_network_io_des')} description={t("monitor.docker_network_io_des")}
cornerEl={containerFilterBar} cornerEl={containerFilterBar}
> >
{/* @ts-ignore */} {/* @ts-ignore */}
@@ -487,13 +460,23 @@ export default function SystemDetail({ name }: { name: string }) {
)} )}
{(systemStats.at(-1)?.stats.su ?? 0) > 0 && ( {(systemStats.at(-1)?.stats.su ?? 0) > 0 && (
<ChartCard empty={dataEmpty} grid={grid} title={t('monitor.swap_usage')} description={t('monitor.swap_des')}> <ChartCard
empty={dataEmpty}
grid={grid}
title={t("monitor.swap_usage")}
description={t("monitor.swap_des")}
>
<SwapChart chartData={chartData} /> <SwapChart chartData={chartData} />
</ChartCard> </ChartCard>
)} )}
{systemStats.at(-1)?.stats.t && ( {systemStats.at(-1)?.stats.t && (
<ChartCard empty={dataEmpty} grid={grid} title={t('monitor.temperature')} description={t('monitor.temperature_des')}> <ChartCard
empty={dataEmpty}
grid={grid}
title={t("monitor.temperature")}
description={t("monitor.temperature_des")}
>
<TemperatureChart chartData={chartData} /> <TemperatureChart chartData={chartData} />
</ChartCard> </ChartCard>
)} )}
@@ -508,8 +491,8 @@ export default function SystemDetail({ name }: { name: string }) {
<ChartCard <ChartCard
empty={dataEmpty} empty={dataEmpty}
grid={grid} grid={grid}
title={`${extraFsName} ${t('monitor.usage')}`} title={`${extraFsName} ${t("monitor.usage")}`}
description={`${t('monitor.disk_usage_of')} ${extraFsName}`} description={`${t("monitor.disk_usage_of")} ${extraFsName}`}
> >
<DiskChart <DiskChart
chartData={chartData} chartData={chartData}
@@ -521,7 +504,7 @@ export default function SystemDetail({ name }: { name: string }) {
empty={dataEmpty} empty={dataEmpty}
grid={grid} grid={grid}
title={`${extraFsName} I/O`} title={`${extraFsName} I/O`}
description={`${t('monitor.throughput_of')} ${extraFsName}`} description={`${t("monitor.throughput_of")} ${extraFsName}`}
cornerEl={isLongerChart ? <SelectAvgMax store={diskIoMaxStore} /> : null} cornerEl={isLongerChart ? <SelectAvgMax store={diskIoMaxStore} /> : null}
> >
<AreaChartDefault <AreaChartDefault
@@ -554,12 +537,7 @@ function ContainerFilterBar() {
return ( return (
<> <>
<Input <Input placeholder={t("filter")} className="pl-4 pr-8" value={containerFilter} onChange={handleChange} />
placeholder={t('filter')}
className="pl-4 pr-8"
value={containerFilter}
onChange={handleChange}
/>
{containerFilter && ( {containerFilter && (
<Button <Button
type="button" type="button"
@@ -567,7 +545,7 @@ function ContainerFilterBar() {
size="icon" size="icon"
aria-label="Clear" aria-label="Clear"
className="absolute right-1 top-1/2 -translate-y-1/2 h-7 w-7 text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100" className="absolute right-1 top-1/2 -translate-y-1/2 h-7 w-7 text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100"
onClick={() => $containerFilter.set('')} onClick={() => $containerFilter.set("")}
> >
<XIcon className="h-4 w-4" /> <XIcon className="h-4 w-4" />
</Button> </Button>
@@ -576,28 +554,24 @@ function ContainerFilterBar() {
) )
} }
function SelectAvgMax({ function SelectAvgMax({ store }: { store: [boolean, React.Dispatch<React.SetStateAction<boolean>>] }) {
store,
}: {
store: [boolean, React.Dispatch<React.SetStateAction<boolean>>]
}) {
const { t } = useTranslation() const { t } = useTranslation()
const [max, setMax] = store const [max, setMax] = store
const Icon = max ? ChartMax : ChartAverage const Icon = max ? ChartMax : ChartAverage
return ( return (
<Select value={max ? 'max' : 'avg'} onValueChange={(e) => setMax(e === 'max')}> <Select value={max ? "max" : "avg"} onValueChange={(e) => setMax(e === "max")}>
<SelectTrigger className="relative pl-10 pr-5"> <SelectTrigger className="relative pl-10 pr-5">
<Icon className="h-4 w-4 absolute left-4 top-1/2 -translate-y-1/2 opacity-85" /> <Icon className="h-4 w-4 absolute left-4 top-1/2 -translate-y-1/2 opacity-85" />
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem key="avg" value="avg"> <SelectItem key="avg" value="avg">
{t('monitor.average')} {t("monitor.average")}
</SelectItem> </SelectItem>
<SelectItem key="max" value="max"> <SelectItem key="max" value="max">
{t('monitor.max_1_min')} {t("monitor.max_1_min")}
</SelectItem> </SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
@@ -616,24 +590,17 @@ function ChartCard({
description: string description: string
children: React.ReactNode children: React.ReactNode
grid?: boolean grid?: boolean
empty?: boolean, empty?: boolean
cornerEl?: JSX.Element | null cornerEl?: JSX.Element | null
}) { }) {
const { isIntersecting, ref } = useIntersectionObserver() const { isIntersecting, ref } = useIntersectionObserver()
return ( return (
<Card <Card className={cn("pb-2 sm:pb-4 odd:last-of-type:col-span-full", { "col-span-full": !grid })} ref={ref}>
className={cn('pb-2 sm:pb-4 odd:last-of-type:col-span-full', { 'col-span-full': !grid })}
ref={ref}
>
<CardHeader className="pb-5 pt-4 relative space-y-1 max-sm:py-3 max-sm:px-4"> <CardHeader className="pb-5 pt-4 relative space-y-1 max-sm:py-3 max-sm:px-4">
<CardTitle className="text-xl sm:text-2xl">{title}</CardTitle> <CardTitle className="text-xl sm:text-2xl">{title}</CardTitle>
<CardDescription>{description}</CardDescription> <CardDescription>{description}</CardDescription>
{cornerEl && ( {cornerEl && <div className="relative py-1 block sm:w-44 sm:absolute sm:top-2.5 sm:right-3.5">{cornerEl}</div>}
<div className="relative py-1 block sm:w-44 sm:absolute sm:top-2.5 sm:right-3.5">
{cornerEl}
</div>
)}
</CardHeader> </CardHeader>
<div className="pl-0 w-[calc(100%-1.6em)] h-52 relative"> <div className="pl-0 w-[calc(100%-1.6em)] h-52 relative">
{<Spinner empty={empty} />} {<Spinner empty={empty} />}

View File

@@ -1,12 +1,12 @@
import { LoaderCircleIcon } from 'lucide-react' import { LoaderCircleIcon } from "lucide-react"
import { useTranslation } from 'react-i18next' import { useTranslation } from "react-i18next"
export default function (props: { empty?: boolean }) { export default function (props: { empty?: boolean }) {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<div className="flex flex-col items-center justify-center h-full absolute inset-0"> <div className="flex flex-col items-center justify-center h-full absolute inset-0">
<LoaderCircleIcon className="animate-spin h-10 w-10 opacity-60" /> <LoaderCircleIcon className="animate-spin h-10 w-10 opacity-60" />
{props.empty && <p className={'opacity-60 mt-2'}>{t('monitor.waiting_for')}</p>} {props.empty && <p className={"opacity-60 mt-2"}>{t("monitor.waiting_for")}</p>}
</div> </div>
) )
} }

View File

@@ -9,18 +9,11 @@ import {
getCoreRowModel, getCoreRowModel,
useReactTable, useReactTable,
Column, Column,
} from '@tanstack/react-table' } from "@tanstack/react-table"
import { import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import { Button, buttonVariants } from '@/components/ui/button' import { Button, buttonVariants } from "@/components/ui/button"
import { import {
DropdownMenu, DropdownMenu,
@@ -28,7 +21,7 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu' } from "@/components/ui/dropdown-menu"
import { import {
AlertDialog, AlertDialog,
@@ -40,9 +33,9 @@ import {
AlertDialogHeader, AlertDialogHeader,
AlertDialogTitle, AlertDialogTitle,
AlertDialogTrigger, AlertDialogTrigger,
} from '@/components/ui/alert-dialog' } from "@/components/ui/alert-dialog"
import { SystemRecord } from '@/types' import { SystemRecord } from "@/types"
import { import {
MoreHorizontalIcon, MoreHorizontalIcon,
ArrowUpDownIcon, ArrowUpDownIcon,
@@ -55,15 +48,15 @@ import {
HardDriveIcon, HardDriveIcon,
ServerIcon, ServerIcon,
CpuIcon, 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"
import { useStore } from '@nanostores/react' import { useStore } from "@nanostores/react"
import { cn, copyToClipboard, decimalString, isReadOnlyUser } from '@/lib/utils' import { cn, copyToClipboard, decimalString, isReadOnlyUser } from "@/lib/utils"
import AlertsButton from '../alerts/alert-button' import AlertsButton from "../alerts/alert-button"
import { navigate } from '../router' import { navigate } from "../router"
import { EthernetIcon } from '../ui/icons' import { EthernetIcon } from "../ui/icons"
import { useTranslation } from 'react-i18next' import { useTranslation } from "react-i18next"
function CellFormatter(info: CellContext<SystemRecord, unknown>) { function CellFormatter(info: CellContext<SystemRecord, unknown>) {
const val = info.getValue() as number const val = info.getValue() as number
@@ -73,8 +66,8 @@ function CellFormatter(info: CellContext<SystemRecord, unknown>) {
<span className="grow min-w-10 block bg-muted h-[1em] relative rounded-sm overflow-hidden"> <span className="grow min-w-10 block bg-muted h-[1em] relative rounded-sm overflow-hidden">
<span <span
className={cn( className={cn(
'absolute inset-0 w-full h-full origin-left', "absolute inset-0 w-full h-full origin-left",
(val < 65 && 'bg-green-500') || (val < 90 && 'bg-yellow-500') || 'bg-red-600' (val < 65 && "bg-green-500") || (val < 90 && "bg-yellow-500") || "bg-red-600"
)} )}
style={{ transform: `scalex(${val}%)` }} style={{ transform: `scalex(${val}%)` }}
></span> ></span>
@@ -83,18 +76,9 @@ function CellFormatter(info: CellContext<SystemRecord, unknown>) {
) )
} }
function sortableHeader( function sortableHeader(column: Column<SystemRecord, unknown>, name: string, Icon: any, hideSortIcon = false) {
column: Column<SystemRecord, unknown>,
name: string,
Icon: any,
hideSortIcon = false
) {
return ( return (
<Button <Button variant="ghost" className="h-9 px-3" onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}>
variant="ghost"
className="h-9 px-3"
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
>
<Icon className="mr-2 h-4 w-4" /> <Icon className="mr-2 h-4 w-4" />
{name} {name}
{!hideSortIcon && <ArrowUpDownIcon className="ml-2 h-4 w-4" />} {!hideSortIcon && <ArrowUpDownIcon className="ml-2 h-4 w-4" />}
@@ -112,7 +96,7 @@ export default function SystemsTable({ filter }: { filter?: string }) {
useEffect(() => { useEffect(() => {
if (filter !== undefined) { if (filter !== undefined) {
table.getColumn('name')?.setFilterValue(filter) table.getColumn("name")?.setFilterValue(filter)
} }
}, [filter]) }, [filter])
@@ -122,23 +106,23 @@ export default function SystemsTable({ filter }: { filter?: string }) {
// size: 200, // size: 200,
size: 200, size: 200,
minSize: 0, minSize: 0,
accessorKey: 'name', accessorKey: "name",
cell: (info) => { cell: (info) => {
const { status } = info.row.original const { status } = info.row.original
return ( return (
<span className="flex gap-0.5 items-center text-base md:pr-5"> <span className="flex gap-0.5 items-center text-base md:pr-5">
<span <span
className={cn('w-2 h-2 left-0 rounded-full', { className={cn("w-2 h-2 left-0 rounded-full", {
'bg-green-500': status === 'up', "bg-green-500": status === "up",
'bg-red-500': status === 'down', "bg-red-500": status === "down",
'bg-primary/40': status === 'paused', "bg-primary/40": status === "paused",
'bg-yellow-500': status === 'pending', "bg-yellow-500": status === "pending",
})} })}
style={{ marginBottom: '-1px' }} style={{ marginBottom: "-1px" }}
></span> ></span>
<Button <Button
data-nolink data-nolink
variant={'ghost'} variant={"ghost"}
className="text-primary/90 h-7 px-1.5 gap-1.5" className="text-primary/90 h-7 px-1.5 gap-1.5"
onClick={() => copyToClipboard(info.getValue() as string)} onClick={() => copyToClipboard(info.getValue() as string)}
> >
@@ -148,46 +132,44 @@ export default function SystemsTable({ filter }: { filter?: string }) {
</span> </span>
) )
}, },
header: ({ column }) => sortableHeader(column, t('systems_table.system'), ServerIcon), header: ({ column }) => sortableHeader(column, t("systems_table.system"), ServerIcon),
}, },
{ {
accessorKey: 'info.cpu', accessorKey: "info.cpu",
invertSorting: true, invertSorting: true,
cell: CellFormatter, cell: CellFormatter,
header: ({ column }) => sortableHeader(column, t('systems_table.cpu'), CpuIcon), header: ({ column }) => sortableHeader(column, t("systems_table.cpu"), CpuIcon),
}, },
{ {
accessorKey: 'info.mp', accessorKey: "info.mp",
invertSorting: true, invertSorting: true,
cell: CellFormatter, cell: CellFormatter,
header: ({ column }) => sortableHeader(column, t('systems_table.memory'), MemoryStickIcon), header: ({ column }) => sortableHeader(column, t("systems_table.memory"), MemoryStickIcon),
}, },
{ {
accessorKey: 'info.dp', accessorKey: "info.dp",
invertSorting: true, invertSorting: true,
cell: CellFormatter, cell: CellFormatter,
header: ({ column }) => sortableHeader(column, t('systems_table.disk'), HardDriveIcon), header: ({ column }) => sortableHeader(column, t("systems_table.disk"), HardDriveIcon),
}, },
{ {
accessorFn: (originalRow) => originalRow.info.b || 0, accessorFn: (originalRow) => originalRow.info.b || 0,
id: 'n', id: "n",
invertSorting: true, invertSorting: true,
size: 115, size: 115,
header: ({ column }) => sortableHeader(column, t('systems_table.net'), EthernetIcon), header: ({ column }) => sortableHeader(column, t("systems_table.net"), EthernetIcon),
cell: (info) => { cell: (info) => {
const val = info.getValue() as number const val = info.getValue() as number
return ( return (
<span className="tabular-nums whitespace-nowrap pl-1"> <span className="tabular-nums whitespace-nowrap pl-1">{decimalString(val, val >= 100 ? 1 : 2)} MB/s</span>
{decimalString(val, val >= 100 ? 1 : 2)} MB/s
</span>
) )
}, },
}, },
{ {
accessorKey: 'info.v', accessorKey: "info.v",
invertSorting: true, invertSorting: true,
size: 50, size: 50,
header: ({ column }) => sortableHeader(column, t('systems_table.agent'), WifiIcon, true), header: ({ column }) => sortableHeader(column, t("systems_table.agent"), WifiIcon, true),
cell: (info) => { cell: (info) => {
const version = info.getValue() as string const version = info.getValue() as string
if (!version || !hubVersion) { if (!version || !hubVersion) {
@@ -196,11 +178,8 @@ export default function SystemsTable({ filter }: { filter?: string }) {
return ( return (
<span className="flex gap-2 items-center md:pr-5 tabular-nums pl-1"> <span className="flex gap-2 items-center md:pr-5 tabular-nums pl-1">
<span <span
className={cn( className={cn("w-2 h-2 left-0 rounded-full", version === hubVersion ? "bg-green-500" : "bg-yellow-500")}
'w-2 h-2 left-0 rounded-full', style={{ marginBottom: "-1px" }}
version === hubVersion ? 'bg-green-500' : 'bg-yellow-500'
)}
style={{ marginBottom: '-1px' }}
></span> ></span>
<span>{info.getValue() as string}</span> <span>{info.getValue() as string}</span>
</span> </span>
@@ -208,70 +187,71 @@ export default function SystemsTable({ filter }: { filter?: string }) {
}, },
}, },
{ {
id: 'actions', id: "actions",
size: 120, size: 120,
// minSize: 0, // minSize: 0,
cell: ({ row }) => { cell: ({ row }) => {
const { id, name, status, host } = row.original const { id, name, status, host } = row.original
return ( return (
<div className={'flex justify-end items-center gap-1'}> <div className={"flex justify-end items-center gap-1"}>
<AlertsButton system={row.original} /> <AlertsButton system={row.original} />
<AlertDialog> <AlertDialog>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="ghost" size={'icon'} data-nolink> <Button variant="ghost" size={"icon"} data-nolink>
<span className="sr-only">{t('systems_table.open_menu')}</span> <span className="sr-only">{t("systems_table.open_menu")}</span>
<MoreHorizontalIcon className="w-5" /> <MoreHorizontalIcon className="w-5" />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuItem <DropdownMenuItem
className={cn(isReadOnlyUser() && 'hidden')} className={cn(isReadOnlyUser() && "hidden")}
onClick={() => { onClick={() => {
pb.collection('systems').update(id, { pb.collection("systems").update(id, {
status: status === 'paused' ? 'pending' : 'paused', status: status === "paused" ? "pending" : "paused",
}) })
}} }}
> >
{status === 'paused' ? ( {status === "paused" ? (
<> <>
<PlayCircleIcon className="mr-2.5 h-4 w-4" /> <PlayCircleIcon className="mr-2.5 h-4 w-4" />
{t('systems_table.resume')} {t("systems_table.resume")}
</> </>
) : ( ) : (
<> <>
<PauseCircleIcon className="mr-2.5 h-4 w-4" /> <PauseCircleIcon className="mr-2.5 h-4 w-4" />
{t('systems_table.pause')} {t("systems_table.pause")}
</> </>
)} )}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => copyToClipboard(host)}> <DropdownMenuItem onClick={() => copyToClipboard(host)}>
<CopyIcon className="mr-2.5 h-4 w-4" /> <CopyIcon className="mr-2.5 h-4 w-4" />
{t('systems_table.copy_host')} {t("systems_table.copy_host")}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator className={cn(isReadOnlyUser() && 'hidden')} /> <DropdownMenuSeparator className={cn(isReadOnlyUser() && "hidden")} />
<AlertDialogTrigger asChild> <AlertDialogTrigger asChild>
<DropdownMenuItem className={cn(isReadOnlyUser() && 'hidden')}> <DropdownMenuItem className={cn(isReadOnlyUser() && "hidden")}>
<Trash2Icon className="mr-2.5 h-4 w-4" /> <Trash2Icon className="mr-2.5 h-4 w-4" />
{t('systems_table.delete')} {t("systems_table.delete")}
</DropdownMenuItem> </DropdownMenuItem>
</AlertDialogTrigger> </AlertDialogTrigger>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>{t('systems_table.delete_confirm', { name })}</AlertDialogTitle> <AlertDialogTitle>{t("systems_table.delete_confirm", { name })}</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
{t('systems_table.delete_confirm_des_1')} <code className="bg-muted rounded-sm px-1">{name}</code> {t('systems_table.delete_confirm_des_2')} {t("systems_table.delete_confirm_des_1")} <code className="bg-muted rounded-sm px-1">{name}</code>{" "}
{t("systems_table.delete_confirm_des_2")}
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>{t('cancel')}</AlertDialogCancel> <AlertDialogCancel>{t("cancel")}</AlertDialogCancel>
<AlertDialogAction <AlertDialogAction
className={cn(buttonVariants({ variant: 'destructive' }))} className={cn(buttonVariants({ variant: "destructive" }))}
onClick={() => pb.collection('systems').delete(id)} onClick={() => pb.collection("systems").delete(id)}
> >
{t('continue')} {t("continue")}
</AlertDialogAction> </AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
@@ -311,9 +291,7 @@ export default function SystemsTable({ filter }: { filter?: string }) {
{headerGroup.headers.map((header) => { {headerGroup.headers.map((header) => {
return ( return (
<TableHead className="px-2" key={header.id}> <TableHead className="px-2" key={header.id}>
{header.isPlaceholder {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
? null
: flexRender(header.column.columnDef.header, header.getContext())}
</TableHead> </TableHead>
) )
})} })}
@@ -325,13 +303,13 @@ export default function SystemsTable({ filter }: { filter?: string }) {
table.getRowModel().rows.map((row) => ( table.getRowModel().rows.map((row) => (
<TableRow <TableRow
key={row.original.id} key={row.original.id}
data-state={row.getIsSelected() && 'selected'} data-state={row.getIsSelected() && "selected"}
className={cn('cursor-pointer transition-opacity', { className={cn("cursor-pointer transition-opacity", {
'opacity-50': row.original.status === 'paused', "opacity-50": row.original.status === "paused",
})} })}
onClick={(e) => { onClick={(e) => {
const target = e.target as HTMLElement const target = e.target as HTMLElement
if (!target.closest('[data-nolink]') && e.currentTarget.contains(target)) { if (!target.closest("[data-nolink]") && e.currentTarget.contains(target)) {
navigate(`/system/${encodeURIComponent(row.original.name)}`) navigate(`/system/${encodeURIComponent(row.original.name)}`)
} }
}} }}
@@ -340,12 +318,9 @@ export default function SystemsTable({ filter }: { filter?: string }) {
<TableCell <TableCell
key={cell.id} key={cell.id}
style={{ style={{
width: width: cell.column.getSize() === Number.MAX_SAFE_INTEGER ? "auto" : cell.column.getSize(),
cell.column.getSize() === Number.MAX_SAFE_INTEGER
? 'auto'
: cell.column.getSize(),
}} }}
className={cn('overflow-hidden relative', data.length > 10 ? 'py-2' : 'py-2.5')} className={cn("overflow-hidden relative", data.length > 10 ? "py-2" : "py-2.5")}
> >
{flexRender(cell.column.columnDef.cell, cell.getContext())} {flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell> </TableCell>
@@ -355,7 +330,7 @@ export default function SystemsTable({ filter }: { filter?: string }) {
) : ( ) : (
<TableRow> <TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center"> <TableCell colSpan={columns.length} className="h-24 text-center">
{t('systems_table.no_systems_found')} {t("systems_table.no_systems_found")}
</TableCell> </TableCell>
</TableRow> </TableRow>
)} )}

View File

@@ -1,6 +1,6 @@
import { createContext, useContext, useEffect, useState } from 'react' import { createContext, useContext, useEffect, useState } from "react"
type Theme = 'dark' | 'light' | 'system' type Theme = "dark" | "light" | "system"
type ThemeProviderProps = { type ThemeProviderProps = {
children: React.ReactNode children: React.ReactNode
@@ -14,7 +14,7 @@ type ThemeProviderState = {
} }
const initialState: ThemeProviderState = { const initialState: ThemeProviderState = {
theme: 'system', theme: "system",
setTheme: () => null, setTheme: () => null,
} }
@@ -22,23 +22,19 @@ const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
export function ThemeProvider({ export function ThemeProvider({
children, children,
defaultTheme = 'system', defaultTheme = "system",
storageKey = 'ui-theme', storageKey = "ui-theme",
...props ...props
}: ThemeProviderProps) { }: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>( const [theme, setTheme] = useState<Theme>(() => (localStorage.getItem(storageKey) as Theme) || defaultTheme)
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
)
useEffect(() => { useEffect(() => {
const root = window.document.documentElement const root = window.document.documentElement
root.classList.remove('light', 'dark') root.classList.remove("light", "dark")
if (theme === 'system') { if (theme === "system") {
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
? 'dark'
: 'light'
root.classList.add(systemTheme) root.classList.add(systemTheme)
return return

View File

@@ -1,8 +1,8 @@
import * as React from 'react' import * as React from "react"
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog' import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from '@/lib/utils' import { cn } from "@/lib/utils"
import { buttonVariants } from '@/components/ui/button' import { buttonVariants } from "@/components/ui/button"
const AlertDialog = AlertDialogPrimitive.Root const AlertDialog = AlertDialogPrimitive.Root
@@ -16,7 +16,7 @@ const AlertDialogOverlay = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay <AlertDialogPrimitive.Overlay
className={cn( className={cn(
'fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0', "fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className className
)} )}
{...props} {...props}
@@ -34,7 +34,7 @@ const AlertDialogContent = React.forwardRef<
<AlertDialogPrimitive.Content <AlertDialogPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg', "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className className
)} )}
{...props} {...props}
@@ -44,27 +44,20 @@ const AlertDialogContent = React.forwardRef<
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => ( const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn('flex flex-col space-y-2 text-center sm:text-left', className)} {...props} /> <div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
) )
AlertDialogHeader.displayName = 'AlertDialogHeader' AlertDialogHeader.displayName = "AlertDialogHeader"
const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => ( const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div <div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} {...props} />
className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)}
{...props}
/>
) )
AlertDialogFooter.displayName = 'AlertDialogFooter' AlertDialogFooter.displayName = "AlertDialogFooter"
const AlertDialogTitle = React.forwardRef< const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>, React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title> React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title <AlertDialogPrimitive.Title ref={ref} className={cn("text-lg font-semibold", className)} {...props} />
ref={ref}
className={cn('text-lg font-semibold', className)}
{...props}
/>
)) ))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
@@ -72,11 +65,7 @@ const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>, React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description> React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description <AlertDialogPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
ref={ref}
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
)) ))
AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName
@@ -94,7 +83,7 @@ const AlertDialogCancel = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel <AlertDialogPrimitive.Cancel
ref={ref} ref={ref}
className={cn(buttonVariants({ variant: 'outline' }), 'mt-2 sm:mt-0', className)} className={cn(buttonVariants({ variant: "outline" }), "mt-2 sm:mt-0", className)}
{...props} {...props}
/> />
)) ))

View File

@@ -1,7 +1,7 @@
import * as React from 'react' import * as React from "react"
// import { cva, type VariantProps } from 'class-variance-authority' // import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils' import { cn } from "@/lib/utils"
// const alertVariants = cva( // const alertVariants = cva(
// "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", // "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
@@ -29,31 +29,26 @@ const Alert = React.forwardRef<
ref={ref} ref={ref}
role="alert" role="alert"
className={cn( className={cn(
'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground bg-background text-foreground', "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground bg-background text-foreground",
className className
)} )}
{...props} {...props}
/> />
)) ))
Alert.displayName = 'Alert' Alert.displayName = "Alert"
const AlertTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>( const AlertTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => ( ({ className, ...props }, ref) => (
<h5 <h5 ref={ref} className={cn("mb-1 -mt-0.5 font-medium leading-tight tracking-tight", className)} {...props} />
ref={ref}
className={cn('mb-1 -mt-0.5 font-medium leading-tight tracking-tight', className)}
{...props}
/>
) )
) )
AlertTitle.displayName = 'AlertTitle' AlertTitle.displayName = "AlertTitle"
const AlertDescription = React.forwardRef< const AlertDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
HTMLParagraphElement, ({ className, ...props }, ref) => (
React.HTMLAttributes<HTMLParagraphElement> <div ref={ref} className={cn("text-sm [&_p]:leading-relaxed", className)} {...props} />
>(({ className, ...props }, ref) => ( )
<div ref={ref} className={cn('text-sm [&_p]:leading-relaxed', className)} {...props} /> )
)) AlertDescription.displayName = "AlertDescription"
AlertDescription.displayName = 'AlertDescription'
export { Alert, AlertTitle, AlertDescription } export { Alert, AlertTitle, AlertDescription }

View File

@@ -8,12 +8,9 @@ const badgeVariants = cva(
{ {
variants: { variants: {
variant: { variant: {
default: default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80", secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
secondary: destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground", outline: "text-foreground",
}, },
}, },
@@ -23,14 +20,10 @@ const badgeVariants = cva(
} }
) )
export interface BadgeProps export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) { function Badge({ className, variant, ...props }: BadgeProps) {
return ( return <div className={cn(badgeVariants({ variant }), className)} {...props} />
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
} }
export { Badge, badgeVariants } export { Badge, badgeVariants }

View File

@@ -1,31 +1,31 @@
import * as React from 'react' import * as React from "react"
import { Slot } from '@radix-ui/react-slot' import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from 'class-variance-authority' import { cva, type VariantProps } from "class-variance-authority"
import { cn } from '@/lib/utils' import { cn } from "@/lib/utils"
const buttonVariants = cva( const buttonVariants = cva(
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium 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', "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium 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",
{ {
variants: { variants: {
variant: { variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90', default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90', destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: 'hover:bg-accent hover:text-accent-foreground', ghost: "hover:bg-accent hover:text-accent-foreground",
link: 'text-primary underline-offset-4 hover:underline', link: "text-primary underline-offset-4 hover:underline",
}, },
size: { size: {
default: 'h-10 px-4 py-2', default: "h-10 px-4 py-2",
sm: 'h-9 rounded-md px-3', sm: "h-9 rounded-md px-3",
lg: 'h-11 rounded-md px-8', lg: "h-11 rounded-md px-8",
icon: 'h-10 w-10', icon: "h-10 w-10",
}, },
}, },
defaultVariants: { defaultVariants: {
variant: 'default', variant: "default",
size: 'default', size: "default",
}, },
} }
) )
@@ -38,12 +38,10 @@ export interface ButtonProps
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => { ({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button' const Comp = asChild ? Slot : "button"
return ( return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
<Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
)
} }
) )
Button.displayName = 'Button' Button.displayName = "Button"
export { Button, buttonVariants } export { Button, buttonVariants }

View File

@@ -2,78 +2,40 @@ import * as React from "react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
const Card = React.forwardRef< const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
HTMLDivElement, <div ref={ref} className={cn("rounded-lg border bg-card text-card-foreground shadow-sm", className)} {...props} />
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
className
)}
{...props}
/>
)) ))
Card.displayName = "Card" Card.displayName = "Card"
const CardHeader = React.forwardRef< const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
HTMLDivElement, ({ className, ...props }, ref) => (
React.HTMLAttributes<HTMLDivElement> <div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
>(({ className, ...props }, ref) => ( )
<div )
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader" CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef< const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
HTMLParagraphElement, ({ className, ...props }, ref) => (
React.HTMLAttributes<HTMLHeadingElement> <h3 ref={ref} className={cn("text-2xl font-semibold leading-none tracking-tight", className)} {...props} />
>(({ className, ...props }, ref) => ( )
<h3 )
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
CardTitle.displayName = "CardTitle" CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef< const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
HTMLParagraphElement, ({ className, ...props }, ref) => (
React.HTMLAttributes<HTMLParagraphElement> <p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
>(({ className, ...props }, ref) => ( )
<p )
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription" CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef< const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
HTMLDivElement, ({ className, ...props }, ref) => <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
React.HTMLAttributes<HTMLDivElement> )
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent" CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef< const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
HTMLDivElement, ({ className, ...props }, ref) => <div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
React.HTMLAttributes<HTMLDivElement> )
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter" CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@@ -1,20 +1,17 @@
import * as React from 'react' import * as React from "react"
import * as RechartsPrimitive from 'recharts' import * as RechartsPrimitive from "recharts"
import { chartTimeData, cn } from '@/lib/utils' import { chartTimeData, cn } from "@/lib/utils"
import { ChartData } from '@/types' import { ChartData } from "@/types"
// Format: { THEME_NAME: CSS_SELECTOR } // Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: '', dark: '.dark' } as const const THEMES = { light: "", dark: ".dark" } as const
export type ChartConfig = { export type ChartConfig = {
[k in string]: { [k in string]: {
label?: React.ReactNode label?: React.ReactNode
icon?: React.ComponentType icon?: React.ComponentType
} & ( } & ({ color?: string; theme?: never } | { color?: never; theme: Record<keyof typeof THEMES, string> })
| { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> }
)
} }
// type ChartContextProps = { // type ChartContextProps = {
@@ -35,13 +32,13 @@ export type ChartConfig = {
const ChartContainer = React.forwardRef< const ChartContainer = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.ComponentProps<'div'> & { React.ComponentProps<"div"> & {
// config: ChartConfig // config: ChartConfig
children: React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>['children'] children: React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>["children"]
} }
>(({ id, className, children, ...props }, ref) => { >(({ id, className, children, ...props }, ref) => {
const uniqueId = React.useId() const uniqueId = React.useId()
const chartId = `chart-${id || uniqueId.replace(/:/g, '')}` const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
return ( return (
//<ChartContext.Provider value={{ config }}> //<ChartContext.Provider value={{ config }}>
@@ -60,7 +57,7 @@ const ChartContainer = React.forwardRef<
//</ChartContext.Provider> //</ChartContext.Provider>
) )
}) })
ChartContainer.displayName = 'Chart' ChartContainer.displayName = "Chart"
// const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { // const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
// const colorConfig = Object.entries(config).filter(([_, config]) => config.theme || config.color) // const colorConfig = Object.entries(config).filter(([_, config]) => config.theme || config.color)
@@ -94,9 +91,9 @@ const ChartTooltip = RechartsPrimitive.Tooltip
const ChartTooltipContent = React.forwardRef< const ChartTooltipContent = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.ComponentProps<typeof RechartsPrimitive.Tooltip> & React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<'div'> & { React.ComponentProps<"div"> & {
hideLabel?: boolean hideLabel?: boolean
indicator?: 'line' | 'dot' | 'dashed' indicator?: "line" | "dot" | "dashed"
nameKey?: string nameKey?: string
labelKey?: string labelKey?: string
unit?: string unit?: string
@@ -109,7 +106,7 @@ const ChartTooltipContent = React.forwardRef<
active, active,
payload, payload,
className, className,
indicator = 'line', indicator = "line",
hideLabel = false, hideLabel = false,
label, label,
labelFormatter, labelFormatter,
@@ -144,21 +141,19 @@ const ChartTooltipContent = React.forwardRef<
} }
const [item] = payload const [item] = payload
const key = `${labelKey || item.name || 'value'}` const key = `${labelKey || item.name || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key) const itemConfig = getPayloadConfigFromPayload(config, item, key)
const value = !labelKey && typeof label === 'string' ? label : itemConfig?.label const value = !labelKey && typeof label === "string" ? label : itemConfig?.label
if (labelFormatter) { if (labelFormatter) {
return ( return <div className={cn("font-medium", labelClassName)}>{labelFormatter(value, payload)}</div>
<div className={cn('font-medium', labelClassName)}>{labelFormatter(value, payload)}</div>
)
} }
if (!value) { if (!value) {
return null return null
} }
return <div className={cn('font-medium', labelClassName)}>{value}</div> return <div className={cn("font-medium", labelClassName)}>{value}</div>
}, [label, labelFormatter, payload, hideLabel, labelClassName, config, labelKey]) }, [label, labelFormatter, payload, hideLabel, labelClassName, config, labelKey])
if (!active || !payload?.length) { if (!active || !payload?.length) {
@@ -172,14 +167,14 @@ const ChartTooltipContent = React.forwardRef<
<div <div
ref={ref} ref={ref}
className={cn( className={cn(
'grid min-w-[7rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl', "grid min-w-[7rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
className className
)} )}
> >
{!nestLabel ? tooltipLabel : null} {!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5"> <div className="grid gap-1.5">
{payload.map((item, index) => { {payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || 'value'}` const key = `${nameKey || item.name || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key) const itemConfig = getPayloadConfigFromPayload(config, item, key)
const indicatorColor = color || item.payload.fill || item.color const indicatorColor = color || item.payload.fill || item.color
@@ -187,8 +182,8 @@ const ChartTooltipContent = React.forwardRef<
<div <div
key={item?.name || item.dataKey} key={item?.name || item.dataKey}
className={cn( className={cn(
'flex w-full items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground', "flex w-full items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
indicator === 'dot' && 'items-center' indicator === "dot" && "items-center"
)} )}
> >
{formatter && item?.value !== undefined && item.name ? ( {formatter && item?.value !== undefined && item.name ? (
@@ -199,41 +194,35 @@ const ChartTooltipContent = React.forwardRef<
<itemConfig.icon /> <itemConfig.icon />
) : ( ) : (
<div <div
className={cn( className={cn("shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]", {
'shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]', "h-2.5 w-2.5": indicator === "dot",
{ "w-1": indicator === "line",
'h-2.5 w-2.5': indicator === 'dot', "w-0 border-[1.5px] border-dashed bg-transparent": indicator === "dashed",
'w-1': indicator === 'line', "my-0.5": nestLabel && indicator === "dashed",
'w-0 border-[1.5px] border-dashed bg-transparent': })}
indicator === 'dashed',
'my-0.5': nestLabel && indicator === 'dashed',
}
)}
style={ style={
{ {
'--color-bg': indicatorColor, "--color-bg": indicatorColor,
'--color-border': indicatorColor, "--color-border": indicatorColor,
} as React.CSSProperties } as React.CSSProperties
} }
/> />
)} )}
<div <div
className={cn( className={cn(
'flex flex-1 justify-between leading-none gap-2', "flex flex-1 justify-between leading-none gap-2",
nestLabel ? 'items-end' : 'items-center' nestLabel ? "items-end" : "items-center"
)} )}
> >
<div className="grid gap-1.5"> <div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null} {nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground"> <span className="text-muted-foreground">{itemConfig?.label || item.name}</span>
{itemConfig?.label || item.name}
</span>
</div> </div>
{item.value !== undefined && ( {item.value !== undefined && (
<span className="font-medium tabular-nums text-foreground"> <span className="font-medium tabular-nums text-foreground">
{content && typeof content === 'function' {content && typeof content === "function"
? content(item, key) ? content(item, key)
: item.value.toLocaleString() + (unit ? unit : '')} : item.value.toLocaleString() + (unit ? unit : "")}
</span> </span>
)} )}
</div> </div>
@@ -247,18 +236,18 @@ const ChartTooltipContent = React.forwardRef<
) )
} }
) )
ChartTooltipContent.displayName = 'ChartTooltip' ChartTooltipContent.displayName = "ChartTooltip"
const ChartLegend = RechartsPrimitive.Legend const ChartLegend = RechartsPrimitive.Legend
const ChartLegendContent = React.forwardRef< const ChartLegendContent = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.ComponentProps<'div'> & React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, 'payload' | 'verticalAlign'> & { Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean hideIcon?: boolean
nameKey?: string nameKey?: string
} }
>(({ className, payload, verticalAlign = 'bottom' }, ref) => { >(({ className, payload, verticalAlign = "bottom" }, ref) => {
// const { config } = useChart() // const { config } = useChart()
if (!payload?.length) { if (!payload?.length) {
@@ -269,8 +258,8 @@ const ChartLegendContent = React.forwardRef<
<div <div
ref={ref} ref={ref}
className={cn( className={cn(
'flex items-center justify-center gap-4 gap-y-1 flex-wrap', "flex items-center justify-center gap-4 gap-y-1 flex-wrap",
verticalAlign === 'top' ? 'pb-3' : 'pt-3', verticalAlign === "top" ? "pb-3" : "pt-3",
className className
)} )}
> >
@@ -283,7 +272,7 @@ const ChartLegendContent = React.forwardRef<
key={item.value} key={item.value}
className={cn( className={cn(
// 'flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground text-muted-foreground' // 'flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground text-muted-foreground'
'flex items-center gap-1.5 text-muted-foreground' "flex items-center gap-1.5 text-muted-foreground"
)} )}
> >
{/* {itemConfig?.icon && !hideIcon ? ( {/* {itemConfig?.icon && !hideIcon ? (
@@ -304,27 +293,27 @@ const ChartLegendContent = React.forwardRef<
</div> </div>
) )
}) })
ChartLegendContent.displayName = 'ChartLegend' ChartLegendContent.displayName = "ChartLegend"
// Helper to extract item config from a payload. // Helper to extract item config from a payload.
function getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string) { function getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string) {
if (typeof payload !== 'object' || payload === null) { if (typeof payload !== "object" || payload === null) {
return undefined return undefined
} }
const payloadPayload = const payloadPayload =
'payload' in payload && typeof payload.payload === 'object' && payload.payload !== null "payload" in payload && typeof payload.payload === "object" && payload.payload !== null
? payload.payload ? payload.payload
: undefined : undefined
let configLabelKey: string = key let configLabelKey: string = key
if (key in payload && typeof payload[key as keyof typeof payload] === 'string') { if (key in payload && typeof payload[key as keyof typeof payload] === "string") {
configLabelKey = payload[key as keyof typeof payload] as string configLabelKey = payload[key as keyof typeof payload] as string
} else if ( } else if (
payloadPayload && payloadPayload &&
key in payloadPayload && key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === 'string' typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
) { ) {
configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string
} }

View File

@@ -1,8 +1,8 @@
import * as React from 'react' import * as React from "react"
import * as CheckboxPrimitive from '@radix-ui/react-checkbox' import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from 'lucide-react' import { Check } from "lucide-react"
import { cn } from '@/lib/utils' import { cn } from "@/lib/utils"
const Checkbox = React.forwardRef< const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>, React.ElementRef<typeof CheckboxPrimitive.Root>,
@@ -11,12 +11,12 @@ const Checkbox = React.forwardRef<
<CheckboxPrimitive.Root <CheckboxPrimitive.Root
ref={ref} ref={ref}
className={cn( className={cn(
'peer h-4 w-4 shrink-0 rounded-[.3em] border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground', "peer h-4 w-4 shrink-0 rounded-[.3em] border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className className
)} )}
{...props} {...props}
> >
<CheckboxPrimitive.Indicator className={cn('flex items-center justify-center text-current')}> <CheckboxPrimitive.Indicator className={cn("flex items-center justify-center text-current")}>
<Check className="h-4 w-4" /> <Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator> </CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root> </CheckboxPrimitive.Root>

View File

@@ -1,10 +1,10 @@
import * as React from 'react' import * as React from "react"
import { DialogTitle, type DialogProps } from '@radix-ui/react-dialog' import { DialogTitle, type DialogProps } from "@radix-ui/react-dialog"
import { Command as CommandPrimitive } from 'cmdk' import { Command as CommandPrimitive } from "cmdk"
import { Search } from 'lucide-react' import { Search } from "lucide-react"
import { cn } from '@/lib/utils' import { cn } from "@/lib/utils"
import { Dialog, DialogContent } from '@/components/ui/dialog' import { Dialog, DialogContent } from "@/components/ui/dialog"
const Command = React.forwardRef< const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>, React.ElementRef<typeof CommandPrimitive>,
@@ -12,10 +12,7 @@ const Command = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<CommandPrimitive <CommandPrimitive
ref={ref} ref={ref}
className={cn( className={cn("flex h-full w-full flex-col overflow-hidden bg-popover text-popover-foreground", className)}
'flex h-full w-full flex-col overflow-hidden bg-popover text-popover-foreground',
className
)}
{...props} {...props}
/> />
)) ))
@@ -47,7 +44,7 @@ const CommandInput = React.forwardRef<
<CommandPrimitive.Input <CommandPrimitive.Input
ref={ref} ref={ref}
className={cn( className={cn(
'flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50', "flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className className
)} )}
{...props} {...props}
@@ -63,7 +60,7 @@ const CommandList = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<CommandPrimitive.List <CommandPrimitive.List
ref={ref} ref={ref}
className={cn('max-h-[300px] overflow-y-auto overflow-x-hidden', className)} className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
{...props} {...props}
/> />
)) ))
@@ -73,9 +70,7 @@ CommandList.displayName = CommandPrimitive.List.displayName
const CommandEmpty = React.forwardRef< const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>, React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty> React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => ( >((props, ref) => <CommandPrimitive.Empty ref={ref} className="py-6 text-center text-sm" {...props} />)
<CommandPrimitive.Empty ref={ref} className="py-6 text-center text-sm" {...props} />
))
CommandEmpty.displayName = CommandPrimitive.Empty.displayName CommandEmpty.displayName = CommandPrimitive.Empty.displayName
@@ -86,7 +81,7 @@ const CommandGroup = React.forwardRef<
<CommandPrimitive.Group <CommandPrimitive.Group
ref={ref} ref={ref}
className={cn( className={cn(
'overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground', "overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
className className
)} )}
{...props} {...props}
@@ -99,11 +94,7 @@ const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>, React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator> React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<CommandPrimitive.Separator <CommandPrimitive.Separator ref={ref} className={cn("-mx-1 h-px bg-border", className)} {...props} />
ref={ref}
className={cn('-mx-1 h-px bg-border', className)}
{...props}
/>
)) ))
CommandSeparator.displayName = CommandPrimitive.Separator.displayName CommandSeparator.displayName = CommandPrimitive.Separator.displayName
@@ -124,14 +115,9 @@ const CommandItem = React.forwardRef<
CommandItem.displayName = CommandPrimitive.Item.displayName CommandItem.displayName = CommandPrimitive.Item.displayName
const CommandShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => { const CommandShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
return ( return <span className={cn("ml-auto text-xs tracking-wide text-muted-foreground", className)} {...props} />
<span
className={cn('ml-auto text-xs tracking-wide text-muted-foreground', className)}
{...props}
/>
)
} }
CommandShortcut.displayName = 'CommandShortcut' CommandShortcut.displayName = "CommandShortcut"
export { export {
Command, Command,

View File

@@ -1,8 +1,8 @@
import * as React from 'react' import * as React from "react"
import * as DialogPrimitive from '@radix-ui/react-dialog' import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from 'lucide-react' import { X } from "lucide-react"
import { cn } from '@/lib/utils' import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root const Dialog = DialogPrimitive.Root
@@ -19,7 +19,7 @@ const DialogOverlay = React.forwardRef<
<DialogPrimitive.Overlay <DialogPrimitive.Overlay
ref={ref} ref={ref}
className={cn( className={cn(
'fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0', "fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className className
)} )}
{...props} {...props}
@@ -36,7 +36,7 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content <DialogPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-card p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg', "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-card p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className className
)} )}
{...props} {...props}
@@ -52,17 +52,14 @@ const DialogContent = React.forwardRef<
DialogContent.displayName = DialogPrimitive.Content.displayName DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => ( const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn('flex flex-col space-y-1.5 text-center sm:text-left', className)} {...props} /> <div className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)} {...props} />
) )
DialogHeader.displayName = 'DialogHeader' DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => ( const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div <div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} {...props} />
className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)}
{...props}
/>
) )
DialogFooter.displayName = 'DialogFooter' DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef< const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>, React.ElementRef<typeof DialogPrimitive.Title>,
@@ -70,7 +67,7 @@ const DialogTitle = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DialogPrimitive.Title <DialogPrimitive.Title
ref={ref} ref={ref}
className={cn('text-lg font-semibold leading-none tracking-tight', className)} className={cn("text-lg font-semibold leading-none tracking-tight", className)}
{...props} {...props}
/> />
)) ))
@@ -80,11 +77,7 @@ const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>, React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description> React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DialogPrimitive.Description <DialogPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
ref={ref}
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
)) ))
DialogDescription.displayName = DialogPrimitive.Description.displayName DialogDescription.displayName = DialogPrimitive.Description.displayName

View File

@@ -35,8 +35,7 @@ const DropdownMenuSubTrigger = React.forwardRef<
<ChevronRight className="ml-auto h-4 w-4" /> <ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger> </DropdownMenuPrimitive.SubTrigger>
)) ))
DropdownMenuSubTrigger.displayName = DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef< const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>, React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
@@ -51,8 +50,7 @@ const DropdownMenuSubContent = React.forwardRef<
{...props} {...props}
/> />
)) ))
DropdownMenuSubContent.displayName = DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef< const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>, React.ElementRef<typeof DropdownMenuPrimitive.Content>,
@@ -111,8 +109,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
{children} {children}
</DropdownMenuPrimitive.CheckboxItem> </DropdownMenuPrimitive.CheckboxItem>
)) ))
DropdownMenuCheckboxItem.displayName = DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef< const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>, React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
@@ -144,11 +141,7 @@ const DropdownMenuLabel = React.forwardRef<
>(({ className, inset, ...props }, ref) => ( >(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label <DropdownMenuPrimitive.Label
ref={ref} ref={ref}
className={cn( className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props} {...props}
/> />
)) ))
@@ -158,24 +151,12 @@ const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>, React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator> React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator <DropdownMenuPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
)) ))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({ const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
className, return <span className={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...props} />
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
} }
DropdownMenuShortcut.displayName = "DropdownMenuShortcut" DropdownMenuShortcut.displayName = "DropdownMenuShortcut"

View File

@@ -1,4 +1,4 @@
import { SVGProps } from 'react' import { SVGProps } from "react"
// linux-logo-bold from https://github.com/phosphor-icons/core (MIT license) // linux-logo-bold from https://github.com/phosphor-icons/core (MIT license)
export function TuxIcon(props: SVGProps<SVGSVGElement>) { export function TuxIcon(props: SVGProps<SVGSVGElement>) {
@@ -49,14 +49,7 @@ export function ChartMax(props: SVGProps<SVGSVGElement>) {
// Lucide https://github.com/lucide-icons/lucide (not in package for some reason) // Lucide https://github.com/lucide-icons/lucide (not in package for some reason)
export function EthernetIcon(props: SVGProps<SVGSVGElement>) { export function EthernetIcon(props: SVGProps<SVGSVGElement>) {
return ( return (
<svg <svg fill="none" stroke="currentColor" strokeLinecap="round" strokeWidth="2" viewBox="0 0 24 24" {...props}>
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeWidth="2"
viewBox="0 0 24 24"
{...props}
>
<path d="m15 20 3-3h2a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h2l3 3zM6 8v1m4-1v1m4-1v1m4-1v1" /> <path d="m15 20 3-3h2a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h2l3 3zM6 8v1m4-1v1m4-1v1m4-1v1" />
</svg> </svg>
) )

View File

@@ -1,27 +1,24 @@
import * as React from 'react' import * as React from "react"
import { Badge } from '@/components/ui/badge' import { Badge } from "@/components/ui/badge"
import { Button } from '@/components/ui/button' import { Button } from "@/components/ui/button"
import { XIcon } from 'lucide-react' import { XIcon } from "lucide-react"
import { type InputProps } from './input' import { type InputProps } from "./input"
import { cn } from '@/lib/utils' import { cn } from "@/lib/utils"
type InputTagsProps = Omit<InputProps, 'value' | 'onChange'> & { type InputTagsProps = Omit<InputProps, "value" | "onChange"> & {
value: string[] value: string[]
onChange: React.Dispatch<React.SetStateAction<string[]>> onChange: React.Dispatch<React.SetStateAction<string[]>>
} }
const InputTags = React.forwardRef<HTMLInputElement, InputTagsProps>( const InputTags = React.forwardRef<HTMLInputElement, InputTagsProps>(
({ className, value, onChange, ...props }, ref) => { ({ className, value, onChange, ...props }, ref) => {
const [pendingDataPoint, setPendingDataPoint] = React.useState('') const [pendingDataPoint, setPendingDataPoint] = React.useState("")
React.useEffect(() => { React.useEffect(() => {
if (pendingDataPoint.includes(',')) { if (pendingDataPoint.includes(",")) {
const newDataPoints = new Set([ const newDataPoints = new Set([...value, ...pendingDataPoint.split(",").map((chunk) => chunk.trim())])
...value,
...pendingDataPoint.split(',').map((chunk) => chunk.trim()),
])
onChange(Array.from(newDataPoints)) onChange(Array.from(newDataPoints))
setPendingDataPoint('') setPendingDataPoint("")
} }
}, [pendingDataPoint, onChange, value]) }, [pendingDataPoint, onChange, value])
@@ -29,14 +26,14 @@ const InputTags = React.forwardRef<HTMLInputElement, InputTagsProps>(
if (pendingDataPoint) { if (pendingDataPoint) {
const newDataPoints = new Set([...value, pendingDataPoint]) const newDataPoints = new Set([...value, pendingDataPoint])
onChange(Array.from(newDataPoints)) onChange(Array.from(newDataPoints))
setPendingDataPoint('') setPendingDataPoint("")
} }
} }
return ( return (
<div <div
className={cn( className={cn(
'bg-background min-h-10 flex w-full flex-wrap gap-2 rounded-md border border-input px-3 py-2 text-sm placeholder:text-muted-foreground has-[:focus-visible]:outline-none ring-offset-background has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-ring has-[:focus-visible]:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50', "bg-background min-h-10 flex w-full flex-wrap gap-2 rounded-md border border-input px-3 py-2 text-sm placeholder:text-muted-foreground has-[:focus-visible]:outline-none ring-offset-background has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-ring has-[:focus-visible]:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className className
)} )}
> >
@@ -60,10 +57,10 @@ const InputTags = React.forwardRef<HTMLInputElement, InputTagsProps>(
value={pendingDataPoint} value={pendingDataPoint}
onChange={(e) => setPendingDataPoint(e.target.value)} onChange={(e) => setPendingDataPoint(e.target.value)}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ',') { if (e.key === "Enter" || e.key === ",") {
e.preventDefault() e.preventDefault()
addPendingDataPoint() addPendingDataPoint()
} else if (e.key === 'Backspace' && pendingDataPoint.length === 0 && value.length > 0) { } else if (e.key === "Backspace" && pendingDataPoint.length === 0 && value.length > 0) {
e.preventDefault() e.preventDefault()
onChange(value.slice(0, -1)) onChange(value.slice(0, -1))
} }
@@ -76,6 +73,6 @@ const InputTags = React.forwardRef<HTMLInputElement, InputTagsProps>(
} }
) )
InputTags.displayName = 'InputTags' InputTags.displayName = "InputTags"
export { InputTags } export { InputTags }

View File

@@ -2,11 +2,9 @@ import * as React from "react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
export interface InputProps export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>( const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, type, ...props }, ref) => {
({ className, type, ...props }, ref) => {
return ( return (
<input <input
type={type} type={type}
@@ -18,8 +16,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
{...props} {...props}
/> />
) )
} })
)
Input.displayName = "Input" Input.displayName = "Input"
export { Input } export { Input }

View File

@@ -4,20 +4,13 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
const labelVariants = cva( const labelVariants = cva("text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70")
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef< const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>, React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<LabelPrimitive.Root <LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
)) ))
Label.displayName = LabelPrimitive.Root.displayName Label.displayName = LabelPrimitive.Root.displayName

View File

@@ -36,10 +36,7 @@ const SelectScrollUpButton = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton <SelectPrimitive.ScrollUpButton
ref={ref} ref={ref}
className={cn( className={cn("flex cursor-default items-center justify-center py-1", className)}
"flex cursor-default items-center justify-center py-1",
className
)}
{...props} {...props}
> >
<ChevronUp className="h-4 w-4" /> <ChevronUp className="h-4 w-4" />
@@ -53,17 +50,13 @@ const SelectScrollDownButton = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton <SelectPrimitive.ScrollDownButton
ref={ref} ref={ref}
className={cn( className={cn("flex cursor-default items-center justify-center py-1", className)}
"flex cursor-default items-center justify-center py-1",
className
)}
{...props} {...props}
> >
<ChevronDown className="h-4 w-4" /> <ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton> </SelectPrimitive.ScrollDownButton>
)) ))
SelectScrollDownButton.displayName = SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef< const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>, React.ElementRef<typeof SelectPrimitive.Content>,
@@ -101,11 +94,7 @@ const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>, React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label> React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SelectPrimitive.Label <SelectPrimitive.Label ref={ref} className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)} {...props} />
ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props}
/>
)) ))
SelectLabel.displayName = SelectPrimitive.Label.displayName SelectLabel.displayName = SelectPrimitive.Label.displayName
@@ -136,11 +125,7 @@ const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>, React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator> React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SelectPrimitive.Separator <SelectPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
)) ))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName SelectSeparator.displayName = SelectPrimitive.Separator.displayName

View File

@@ -6,24 +6,15 @@ import { cn } from "@/lib/utils"
const Separator = React.forwardRef< const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>, React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root> React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>( >(({ className, orientation = "horizontal", decorative = true, ...props }, ref) => (
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref
) => (
<SeparatorPrimitive.Root <SeparatorPrimitive.Root
ref={ref} ref={ref}
decorative={decorative} decorative={decorative}
orientation={orientation} orientation={orientation}
className={cn( className={cn("shrink-0 bg-border", orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]", className)}
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
)}
{...props} {...props}
/> />
) ))
)
Separator.displayName = SeparatorPrimitive.Root.displayName Separator.displayName = SeparatorPrimitive.Root.displayName
export { Separator } export { Separator }

View File

@@ -1,7 +1,7 @@
import * as React from 'react' import * as React from "react"
import * as SliderPrimitive from '@radix-ui/react-slider' import * as SliderPrimitive from "@radix-ui/react-slider"
import { cn } from '@/lib/utils' import { cn } from "@/lib/utils"
const Slider = React.forwardRef< const Slider = React.forwardRef<
React.ElementRef<typeof SliderPrimitive.Root>, React.ElementRef<typeof SliderPrimitive.Root>,
@@ -9,7 +9,7 @@ const Slider = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SliderPrimitive.Root <SliderPrimitive.Root
ref={ref} ref={ref}
className={cn('relative flex w-full touch-none select-none items-center', className)} className={cn("relative flex w-full touch-none select-none items-center", className)}
{...props} {...props}
> >
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary"> <SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">

View File

@@ -1,91 +1,72 @@
import * as React from 'react' import * as React from "react"
import { cn } from '@/lib/utils' import { cn } from "@/lib/utils"
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>( const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
({ className, ...props }, ref) => ( ({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto"> <div className="relative w-full overflow-auto">
<table ref={ref} className={cn('w-full caption-bottom text-sm', className)} {...props} /> <table ref={ref} className={cn("w-full caption-bottom text-sm", className)} {...props} />
</div> </div>
) )
) )
Table.displayName = 'Table' Table.displayName = "Table"
const TableHeader = React.forwardRef< const TableHeader = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
HTMLTableSectionElement, ({ className, ...props }, ref) => <thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
React.HTMLAttributes<HTMLTableSectionElement> )
>(({ className, ...props }, ref) => ( TableHeader.displayName = "TableHeader"
<thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} />
))
TableHeader.displayName = 'TableHeader'
const TableBody = React.forwardRef< const TableBody = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
HTMLTableSectionElement, ({ className, ...props }, ref) => (
React.HTMLAttributes<HTMLTableSectionElement> <tbody ref={ref} className={cn("[&_tr:last-child]:border-0", className)} {...props} />
>(({ className, ...props }, ref) => ( )
<tbody ref={ref} className={cn('[&_tr:last-child]:border-0', className)} {...props} /> )
)) TableBody.displayName = "TableBody"
TableBody.displayName = 'TableBody'
const TableFooter = React.forwardRef< const TableFooter = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
HTMLTableSectionElement, ({ className, ...props }, ref) => (
React.HTMLAttributes<HTMLTableSectionElement> <tfoot ref={ref} className={cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className)} {...props} />
>(({ className, ...props }, ref) => ( )
<tfoot )
ref={ref} TableFooter.displayName = "TableFooter"
className={cn('border-t bg-muted/50 font-medium [&>tr]:last:border-b-0', className)}
{...props}
/>
))
TableFooter.displayName = 'TableFooter'
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>( const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
({ className, ...props }, ref) => ( ({ className, ...props }, ref) => (
<tr <tr
ref={ref}
className={cn("border-b hover:bg-muted/40 dark:hover:bg-muted/30 data-[state=selected]:bg-muted", className)}
{...props}
/>
)
)
TableRow.displayName = "TableRow"
const TableHead = React.forwardRef<HTMLTableCellElement, React.ThHTMLAttributes<HTMLTableCellElement>>(
({ className, ...props }, ref) => (
<th
ref={ref} ref={ref}
className={cn( className={cn(
'border-b hover:bg-muted/40 dark:hover:bg-muted/30 data-[state=selected]:bg-muted', "h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
className className
)} )}
{...props} {...props}
/> />
) )
) )
TableRow.displayName = 'TableRow' TableHead.displayName = "TableHead"
const TableHead = React.forwardRef< const TableCell = React.forwardRef<HTMLTableCellElement, React.TdHTMLAttributes<HTMLTableCellElement>>(
HTMLTableCellElement, ({ className, ...props }, ref) => (
React.ThHTMLAttributes<HTMLTableCellElement> <td ref={ref} className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)} {...props} />
>(({ className, ...props }, ref) => ( )
<th )
ref={ref} TableCell.displayName = "TableCell"
className={cn(
'h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0',
className
)}
{...props}
/>
))
TableHead.displayName = 'TableHead'
const TableCell = React.forwardRef< const TableCaption = React.forwardRef<HTMLTableCaptionElement, React.HTMLAttributes<HTMLTableCaptionElement>>(
HTMLTableCellElement, ({ className, ...props }, ref) => (
React.TdHTMLAttributes<HTMLTableCellElement> <caption ref={ref} className={cn("mt-4 text-sm text-muted-foreground", className)} {...props} />
>(({ className, ...props }, ref) => ( )
<td )
ref={ref} TableCaption.displayName = "TableCaption"
className={cn('p-4 align-middle [&:has([role=checkbox])]:pr-0', className)}
{...props}
/>
))
TableCell.displayName = 'TableCell'
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption ref={ref} className={cn('mt-4 text-sm text-muted-foreground', className)} {...props} />
))
TableCaption.displayName = 'TableCaption'
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption } export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption }

View File

@@ -1,23 +1,21 @@
import * as React from 'react' import * as React from "react"
import { cn } from '@/lib/utils' import { cn } from "@/lib/utils"
export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {} export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>( const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(({ className, ...props }, ref) => {
({ className, ...props }, ref) => {
return ( return (
<textarea <textarea
className={cn( className={cn(
'flex min-h-14 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50', "flex min-h-14 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className className
)} )}
ref={ref} ref={ref}
{...props} {...props}
/> />
) )
} })
) Textarea.displayName = "Textarea"
Textarea.displayName = 'Textarea'
export { Textarea } export { Textarea }

View File

@@ -1,9 +1,9 @@
import * as React from 'react' import * as React from "react"
import * as ToastPrimitives from '@radix-ui/react-toast' import * as ToastPrimitives from "@radix-ui/react-toast"
import { cva, type VariantProps } from 'class-variance-authority' import { cva, type VariantProps } from "class-variance-authority"
import { X } from 'lucide-react' import { X } from "lucide-react"
import { cn } from '@/lib/utils' import { cn } from "@/lib/utils"
const ToastProvider = ToastPrimitives.Provider const ToastProvider = ToastPrimitives.Provider
@@ -14,7 +14,7 @@ const ToastViewport = React.forwardRef<
<ToastPrimitives.Viewport <ToastPrimitives.Viewport
ref={ref} ref={ref}
className={cn( className={cn(
'fixed top-0 z-[100] flex max-h-dvh w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]', "fixed top-0 z-[100] flex max-h-dvh w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className className
)} )}
{...props} {...props}
@@ -23,17 +23,16 @@ const ToastViewport = React.forwardRef<
ToastViewport.displayName = ToastPrimitives.Viewport.displayName ToastViewport.displayName = ToastPrimitives.Viewport.displayName
const toastVariants = cva( const toastVariants = cva(
'group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full', "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
{ {
variants: { variants: {
variant: { variant: {
default: 'border bg-background text-foreground', default: "border bg-background text-foreground",
destructive: destructive: "destructive group border-destructive bg-destructive text-destructive-foreground",
'destructive group border-destructive bg-destructive text-destructive-foreground',
}, },
}, },
defaultVariants: { defaultVariants: {
variant: 'default', variant: "default",
}, },
} }
) )
@@ -42,13 +41,7 @@ const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>, React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> & VariantProps<typeof toastVariants> React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> & VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => { >(({ className, variant, ...props }, ref) => {
return ( return <ToastPrimitives.Root ref={ref} className={cn(toastVariants({ variant }), className)} {...props} />
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
)
}) })
Toast.displayName = ToastPrimitives.Root.displayName Toast.displayName = ToastPrimitives.Root.displayName
@@ -59,7 +52,7 @@ const ToastAction = React.forwardRef<
<ToastPrimitives.Action <ToastPrimitives.Action
ref={ref} ref={ref}
className={cn( className={cn(
'inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive', "inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
className className
)} )}
{...props} {...props}
@@ -74,7 +67,7 @@ const ToastClose = React.forwardRef<
<ToastPrimitives.Close <ToastPrimitives.Close
ref={ref} ref={ref}
className={cn( className={cn(
'absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600', "absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className className
)} )}
toast-close="" toast-close=""
@@ -89,7 +82,7 @@ const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>, React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title> React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<ToastPrimitives.Title ref={ref} className={cn('text-sm font-semibold', className)} {...props} /> <ToastPrimitives.Title ref={ref} className={cn("text-sm font-semibold", className)} {...props} />
)) ))
ToastTitle.displayName = ToastPrimitives.Title.displayName ToastTitle.displayName = ToastPrimitives.Title.displayName
@@ -97,11 +90,7 @@ const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>, React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description> React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<ToastPrimitives.Description <ToastPrimitives.Description ref={ref} className={cn("text-sm opacity-90", className)} {...props} />
ref={ref}
className={cn('text-sm opacity-90', className)}
{...props}
/>
)) ))
ToastDescription.displayName = ToastPrimitives.Description.displayName ToastDescription.displayName = ToastPrimitives.Description.displayName

View File

@@ -1,11 +1,4 @@
import { import { Toast, ToastClose, ToastDescription, ToastProvider, ToastTitle, ToastViewport } from "@/components/ui/toast"
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from "@/components/ui/toast"
import { useToast } from "@/components/ui/use-toast" import { useToast } from "@/components/ui/use-toast"
export function Toaster() { export function Toaster() {
@@ -18,9 +11,7 @@ export function Toaster() {
<Toast key={id} {...props}> <Toast key={id} {...props}>
<div className="grid gap-1"> <div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>} {title && <ToastTitle>{title}</ToastTitle>}
{description && ( {description && <ToastDescription>{description}</ToastDescription>}
<ToastDescription>{description}</ToastDescription>
)}
</div> </div>
{action} {action}
<ToastClose /> <ToastClose />

View File

@@ -1,7 +1,7 @@
import * as React from 'react' import * as React from "react"
import * as TooltipPrimitive from '@radix-ui/react-tooltip' import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from '@/lib/utils' import { cn } from "@/lib/utils"
const TooltipProvider = TooltipPrimitive.Provider const TooltipProvider = TooltipPrimitive.Provider
@@ -17,7 +17,7 @@ const TooltipContent = React.forwardRef<
ref={ref} ref={ref}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
'z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', "z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className className
)} )}
{...props} {...props}

View File

@@ -1,10 +1,7 @@
// Inspired by react-hot-toast library // Inspired by react-hot-toast library
import * as React from "react" import * as React from "react"
import type { import type { ToastActionElement, ToastProps } from "@/components/ui/toast"
ToastActionElement,
ToastProps,
} from "@/components/ui/toast"
const TOAST_LIMIT = 1 const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000 const TOAST_REMOVE_DELAY = 1000000
@@ -83,9 +80,7 @@ export const reducer = (state: State, action: Action): State => {
case "UPDATE_TOAST": case "UPDATE_TOAST":
return { return {
...state, ...state,
toasts: state.toasts.map((t) => toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)),
t.id === action.toast.id ? { ...t, ...action.toast } : t
),
} }
case "DISMISS_TOAST": { case "DISMISS_TOAST": {

View File

@@ -68,7 +68,7 @@
font-style: normal; font-style: normal;
font-weight: 100 900; font-weight: 100 900;
font-display: swap; font-display: swap;
src: url('/static/InterVariable.woff2?v=4.0') format('woff2'); src: url("/static/InterVariable.woff2?v=4.0") format("woff2");
} }
@layer base { @layer base {

View File

@@ -1,37 +1,44 @@
import i18n from 'i18next'; import i18n from "i18next"
import { initReactI18next } from 'react-i18next'; import { initReactI18next } from "react-i18next"
import en from '../locales/en/translation.json'; import en from "../locales/en/translation.json"
import es from '../locales/es/translation.json'; import es from "../locales/es/translation.json"
import fr from '../locales/fr/translation.json'; import fr from "../locales/fr/translation.json"
import de from '../locales/de/translation.json'; import de from "../locales/de/translation.json"
import ru from '../locales/ru/translation.json'; import ru from "../locales/ru/translation.json"
import zhHans from '../locales/zh-CN/translation.json'; import zhHans from "../locales/zh-CN/translation.json"
import zhHant from '../locales/zh-HK/translation.json'; import zhHant from "../locales/zh-HK/translation.json"
// Custom language detector to use localStorage // Custom language detector to use localStorage
const languageDetector: any = { const languageDetector: any = {
type: 'languageDetector', type: "languageDetector",
async: true, async: true,
detect: (callback: (lng: string) => void) => { detect: (callback: (lng: string) => void) => {
const savedLanguage = localStorage.getItem('i18nextLng'); const savedLanguage = localStorage.getItem("i18nextLng")
const fallbackLanguage = (()=>{ const fallbackLanguage = (() => {
switch (navigator.language) { switch (navigator.language) {
case 'zh-CN': case 'zh-SG': case 'zh-MY': case 'zh': case 'zh-Hans': case "zh-CN":
return 'zh-CN'; case "zh-SG":
case 'zh-HK': case 'zh-TW': case 'zh-MO': case 'zh-Hant': case "zh-MY":
return 'zh-HK'; case "zh":
case "zh-Hans":
return "zh-CN"
case "zh-HK":
case "zh-TW":
case "zh-MO":
case "zh-Hant":
return "zh-HK"
default: default:
return navigator.language; return navigator.language
} }
})(); })()
callback(savedLanguage || fallbackLanguage); callback(savedLanguage || fallbackLanguage)
}, },
init: () => {}, init: () => {},
cacheUserLanguage: (lng: string) => { cacheUserLanguage: (lng: string) => {
localStorage.setItem('i18nextLng', lng); localStorage.setItem("i18nextLng", lng)
} },
}; }
i18n i18n
.use(languageDetector) .use(languageDetector)
@@ -44,14 +51,14 @@ i18n
de: { translation: de }, de: { translation: de },
ru: { translation: ru }, ru: { translation: ru },
// Chinese (Simplified) // Chinese (Simplified)
'zh-CN': { translation: zhHans }, "zh-CN": { translation: zhHans },
// Chinese (Traditional) // Chinese (Traditional)
'zh-HK': { translation: zhHant }, "zh-HK": { translation: zhHant },
}, },
fallbackLng: 'en', fallbackLng: "en",
interpolation: { interpolation: {
escapeValue: false escapeValue: false,
} },
}); })
export { i18n }; export { i18n }

View File

@@ -1,9 +1,9 @@
import PocketBase from 'pocketbase' import PocketBase from "pocketbase"
import { atom, map, WritableAtom } from 'nanostores' import { atom, map, WritableAtom } from "nanostores"
import { AlertRecord, ChartTimes, SystemRecord, UserSettings } from '@/types' import { AlertRecord, ChartTimes, SystemRecord, UserSettings } from "@/types"
/** PocketBase JS Client */ /** PocketBase JS Client */
export const pb = new PocketBase('/') export const pb = new PocketBase("/")
/** Store if user is authenticated */ /** Store if user is authenticated */
export const $authenticated = atom(pb.authStore.isValid) export const $authenticated = atom(pb.authStore.isValid)
@@ -15,18 +15,18 @@ export const $systems = atom([] as SystemRecord[])
export const $alerts = atom([] as AlertRecord[]) export const $alerts = atom([] as AlertRecord[])
/** SSH public key */ /** SSH public key */
export const $publicKey = atom('') export const $publicKey = atom("")
/** Beszel hub version */ /** Beszel hub version */
export const $hubVersion = atom('') export const $hubVersion = atom("")
/** Chart time period */ /** Chart time period */
export const $chartTime = atom('1h') as WritableAtom<ChartTimes> export const $chartTime = atom("1h") as WritableAtom<ChartTimes>
/** User settings */ /** User settings */
export const $userSettings = map<UserSettings>({ export const $userSettings = map<UserSettings>({
chartTime: '1h', chartTime: "1h",
emails: [pb.authStore.model?.email || ''], emails: [pb.authStore.model?.email || ""],
}) })
// update local storage on change // update local storage on change
$userSettings.subscribe((value) => { $userSettings.subscribe((value) => {
@@ -35,7 +35,7 @@ $userSettings.subscribe((value) => {
}) })
/** Container chart filter */ /** Container chart filter */
export const $containerFilter = atom('') export const $containerFilter = atom("")
/** Fallback copy to clipboard dialog content */ /** Fallback copy to clipboard dialog content */
export const $copyContent = atom('') export const $copyContent = atom("")

View File

@@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from "react"
// adapted from usehooks-ts/use-intersection-observer // adapted from usehooks-ts/use-intersection-observer
@@ -72,7 +72,7 @@ type IntersectionReturn = {
export function useIntersectionObserver({ export function useIntersectionObserver({
threshold = 0, threshold = 0,
root = null, root = null,
rootMargin = '0%', rootMargin = "0%",
freeze = true, freeze = true,
initialIsIntersecting = false, initialIsIntersecting = false,
onChange, onChange,
@@ -84,7 +84,7 @@ export function useIntersectionObserver({
entry: undefined, entry: undefined,
})) }))
const callbackRef = useRef<UseIntersectionObserverOptions['onChange']>() const callbackRef = useRef<UseIntersectionObserverOptions["onChange"]>()
callbackRef.current = onChange callbackRef.current = onChange
@@ -95,7 +95,7 @@ export function useIntersectionObserver({
if (!ref) return if (!ref) return
// Ensure the browser supports the Intersection Observer API // Ensure the browser supports the Intersection Observer API
if (!('IntersectionObserver' in window)) return if (!("IntersectionObserver" in window)) return
// Skip if frozen // Skip if frozen
if (frozen) return if (frozen) return
@@ -104,14 +104,11 @@ export function useIntersectionObserver({
const observer = new IntersectionObserver( const observer = new IntersectionObserver(
(entries: IntersectionObserverEntry[]): void => { (entries: IntersectionObserverEntry[]): void => {
const thresholds = Array.isArray(observer.thresholds) const thresholds = Array.isArray(observer.thresholds) ? observer.thresholds : [observer.thresholds]
? observer.thresholds
: [observer.thresholds]
entries.forEach((entry) => { entries.forEach((entry) => {
const isIntersecting = const isIntersecting =
entry.isIntersecting && entry.isIntersecting && thresholds.some((threshold) => entry.intersectionRatio >= threshold)
thresholds.some((threshold) => entry.intersectionRatio >= threshold)
setState({ isIntersecting, entry }) setState({ isIntersecting, entry })
@@ -149,13 +146,7 @@ export function useIntersectionObserver({
const prevRef = useRef<Element | null>(null) const prevRef = useRef<Element | null>(null)
useEffect(() => { useEffect(() => {
if ( if (!ref && state.entry?.target && !freeze && !frozen && prevRef.current !== state.entry.target) {
!ref &&
state.entry?.target &&
!freeze &&
!frozen &&
prevRef.current !== state.entry.target
) {
prevRef.current = state.entry.target prevRef.current = state.entry.target
setState({ isIntersecting: initialIsIntersecting, entry: undefined }) setState({ isIntersecting: initialIsIntersecting, entry: undefined })
} }

View File

@@ -1,14 +1,14 @@
import { toast } from '@/components/ui/use-toast' import { toast } from "@/components/ui/use-toast"
import { type ClassValue, clsx } from 'clsx' import { type ClassValue, clsx } from "clsx"
import { twMerge } from 'tailwind-merge' import { twMerge } from "tailwind-merge"
import { $alerts, $copyContent, $systems, $userSettings, pb } from './stores' import { $alerts, $copyContent, $systems, $userSettings, pb } from "./stores"
import { AlertRecord, ChartTimeData, ChartTimes, SystemRecord } from '@/types' import { AlertRecord, ChartTimeData, ChartTimes, SystemRecord } from "@/types"
import { RecordModel, RecordSubscription } from 'pocketbase' import { RecordModel, RecordSubscription } from "pocketbase"
import { WritableAtom } from 'nanostores' import { WritableAtom } from "nanostores"
import { timeDay, timeHour } from 'd3-time' import { timeDay, timeHour } from "d3-time"
import { useEffect, useState } from 'react' import { useEffect, useState } from "react"
import { CpuIcon, HardDriveIcon, MemoryStickIcon, ServerIcon } from 'lucide-react' import { CpuIcon, HardDriveIcon, MemoryStickIcon, ServerIcon } from "lucide-react"
import { EthernetIcon, ThermometerIcon } from '@/components/ui/icons' import { EthernetIcon, ThermometerIcon } from "@/components/ui/icons"
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)) return twMerge(clsx(inputs))
@@ -21,7 +21,7 @@ export async function copyToClipboard(content: string) {
await navigator.clipboard.writeText(content) await navigator.clipboard.writeText(content)
toast({ toast({
duration, duration,
description: 'Copied to clipboard', description: "Copied to clipboard",
}) })
} catch (e: any) { } catch (e: any) {
$copyContent.set(content) $copyContent.set(content)
@@ -29,22 +29,22 @@ export async function copyToClipboard(content: string) {
} }
const verifyAuth = () => { const verifyAuth = () => {
pb.collection('users') pb.collection("users")
.authRefresh() .authRefresh()
.catch(() => { .catch(() => {
pb.authStore.clear() pb.authStore.clear()
toast({ toast({
title: 'Failed to authenticate', title: "Failed to authenticate",
description: 'Please log in again', description: "Please log in again",
variant: 'destructive', variant: "destructive",
}) })
}) })
} }
export const updateSystemList = async () => { export const updateSystemList = async () => {
const records = await pb const records = await pb
.collection<SystemRecord>('systems') .collection<SystemRecord>("systems")
.getFullList({ sort: '+name', fields: 'id,name,host,info,status' }) .getFullList({ sort: "+name", fields: "id,name,host,info,status" })
if (records.length) { if (records.length) {
$systems.set(records) $systems.set(records)
} else { } else {
@@ -53,26 +53,26 @@ export const updateSystemList = async () => {
} }
export const updateAlerts = () => { export const updateAlerts = () => {
pb.collection('alerts') pb.collection("alerts")
.getFullList<AlertRecord>({ fields: 'id,name,system,value,min,triggered', sort: 'updated' }) .getFullList<AlertRecord>({ fields: "id,name,system,value,min,triggered", sort: "updated" })
.then((records) => { .then((records) => {
$alerts.set(records) $alerts.set(records)
}) })
} }
const hourWithMinutesFormatter = new Intl.DateTimeFormat(undefined, { const hourWithMinutesFormatter = new Intl.DateTimeFormat(undefined, {
hour: 'numeric', hour: "numeric",
minute: 'numeric', minute: "numeric",
}) })
export const hourWithMinutes = (timestamp: string) => { export const hourWithMinutes = (timestamp: string) => {
return hourWithMinutesFormatter.format(new Date(timestamp)) return hourWithMinutesFormatter.format(new Date(timestamp))
} }
const shortDateFormatter = new Intl.DateTimeFormat(undefined, { const shortDateFormatter = new Intl.DateTimeFormat(undefined, {
day: 'numeric', day: "numeric",
month: 'short', month: "short",
hour: 'numeric', hour: "numeric",
minute: 'numeric', minute: "numeric",
}) })
export const formatShortDate = (timestamp: string) => { export const formatShortDate = (timestamp: string) => {
// console.log('ts', timestamp) // console.log('ts', timestamp)
@@ -93,8 +93,8 @@ export const formatShortDate = (timestamp: string) => {
// } // }
const dayFormatter = new Intl.DateTimeFormat(undefined, { const dayFormatter = new Intl.DateTimeFormat(undefined, {
day: 'numeric', day: "numeric",
month: 'short', month: "short",
// dateStyle: 'medium', // dateStyle: 'medium',
}) })
export const formatDay = (timestamp: string) => { export const formatDay = (timestamp: string) => {
@@ -106,19 +106,16 @@ export const updateFavicon = (newIcon: string) => {
;(document.querySelector("link[rel='icon']") as HTMLLinkElement).href = `/static/${newIcon}` ;(document.querySelector("link[rel='icon']") as HTMLLinkElement).href = `/static/${newIcon}`
} }
export const isAdmin = () => pb.authStore.model?.role === 'admin' export const isAdmin = () => pb.authStore.model?.role === "admin"
export const isReadOnlyUser = () => pb.authStore.model?.role === 'readonly' export const isReadOnlyUser = () => pb.authStore.model?.role === "readonly"
// export const isDefaultUser = () => pb.authStore.model?.role === 'user' // export const isDefaultUser = () => pb.authStore.model?.role === 'user'
/** Update systems / alerts list when records change */ /** Update systems / alerts list when records change */
export function updateRecordList<T extends RecordModel>( export function updateRecordList<T extends RecordModel>(e: RecordSubscription<T>, $store: WritableAtom<T[]>) {
e: RecordSubscription<T>,
$store: WritableAtom<T[]>
) {
const curRecords = $store.get() const curRecords = $store.get()
const newRecords = [] const newRecords = []
// console.log('e', e) // console.log('e', e)
if (e.action === 'delete') { if (e.action === "delete") {
for (const server of curRecords) { for (const server of curRecords) {
if (server.id !== e.record.id) { if (server.id !== e.record.id) {
newRecords.push(server) newRecords.push(server)
@@ -143,51 +140,51 @@ export function updateRecordList<T extends RecordModel>(
export function getPbTimestamp(timeString: ChartTimes, d?: Date) { export function getPbTimestamp(timeString: ChartTimes, d?: Date) {
d ||= chartTimeData[timeString].getOffset(new Date()) d ||= chartTimeData[timeString].getOffset(new Date())
const year = d.getUTCFullYear() const year = d.getUTCFullYear()
const month = String(d.getUTCMonth() + 1).padStart(2, '0') const month = String(d.getUTCMonth() + 1).padStart(2, "0")
const day = String(d.getUTCDate()).padStart(2, '0') const day = String(d.getUTCDate()).padStart(2, "0")
const hours = String(d.getUTCHours()).padStart(2, '0') const hours = String(d.getUTCHours()).padStart(2, "0")
const minutes = String(d.getUTCMinutes()).padStart(2, '0') const minutes = String(d.getUTCMinutes()).padStart(2, "0")
const seconds = String(d.getUTCSeconds()).padStart(2, '0') const seconds = String(d.getUTCSeconds()).padStart(2, "0")
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}` return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
} }
export const chartTimeData: ChartTimeData = { export const chartTimeData: ChartTimeData = {
'1h': { "1h": {
type: '1m', type: "1m",
expectedInterval: 60_000, expectedInterval: 60_000,
label: '1 hour', label: "1 hour",
// ticks: 12, // ticks: 12,
format: (timestamp: string) => hourWithMinutes(timestamp), format: (timestamp: string) => hourWithMinutes(timestamp),
getOffset: (endTime: Date) => timeHour.offset(endTime, -1), getOffset: (endTime: Date) => timeHour.offset(endTime, -1),
}, },
'12h': { "12h": {
type: '10m', type: "10m",
expectedInterval: 60_000 * 10, expectedInterval: 60_000 * 10,
label: '12 hours', label: "12 hours",
ticks: 12, ticks: 12,
format: (timestamp: string) => hourWithMinutes(timestamp), format: (timestamp: string) => hourWithMinutes(timestamp),
getOffset: (endTime: Date) => timeHour.offset(endTime, -12), getOffset: (endTime: Date) => timeHour.offset(endTime, -12),
}, },
'24h': { "24h": {
type: '20m', type: "20m",
expectedInterval: 60_000 * 20, expectedInterval: 60_000 * 20,
label: '24 hours', label: "24 hours",
format: (timestamp: string) => hourWithMinutes(timestamp), format: (timestamp: string) => hourWithMinutes(timestamp),
getOffset: (endTime: Date) => timeHour.offset(endTime, -24), getOffset: (endTime: Date) => timeHour.offset(endTime, -24),
}, },
'1w': { "1w": {
type: '120m', type: "120m",
expectedInterval: 60_000 * 120, expectedInterval: 60_000 * 120,
label: '1 week', label: "1 week",
ticks: 7, ticks: 7,
format: (timestamp: string) => formatDay(timestamp), format: (timestamp: string) => formatDay(timestamp),
getOffset: (endTime: Date) => timeDay.offset(endTime, -7), getOffset: (endTime: Date) => timeDay.offset(endTime, -7),
}, },
'30d': { "30d": {
type: '480m', type: "480m",
expectedInterval: 60_000 * 480, expectedInterval: 60_000 * 480,
label: '30 days', label: "30 days",
ticks: 30, ticks: 30,
format: (timestamp: string) => formatDay(timestamp), format: (timestamp: string) => formatDay(timestamp),
getOffset: (endTime: Date) => timeDay.offset(endTime, -30), getOffset: (endTime: Date) => timeDay.offset(endTime, -30),
@@ -202,8 +199,8 @@ export function useYAxisWidth() {
function updateYAxisWidth(str: string) { function updateYAxisWidth(str: string) {
if (str.length > maxChars) { if (str.length > maxChars) {
maxChars = str.length maxChars = str.length
const div = document.createElement('div') const div = document.createElement("div")
div.className = 'text-xs tabular-nums tracking-tighter table sr-only' div.className = "text-xs tabular-nums tracking-tighter table sr-only"
div.innerHTML = str div.innerHTML = str
clearTimeout(timeout) clearTimeout(timeout)
timeout = setTimeout(() => { timeout = setTimeout(() => {
@@ -263,20 +260,18 @@ export const useLocalStorage = (key: string, defaultValue: any) => {
export async function updateUserSettings() { export async function updateUserSettings() {
try { try {
const req = await pb.collection('user_settings').getFirstListItem('', { fields: 'settings' }) const req = await pb.collection("user_settings").getFirstListItem("", { fields: "settings" })
$userSettings.set(req.settings) $userSettings.set(req.settings)
return return
} catch (e) { } catch (e) {
console.log('get settings', e) console.log("get settings", e)
} }
// create user settings if error fetching existing // create user settings if error fetching existing
try { try {
const createdSettings = await pb const createdSettings = await pb.collection("user_settings").create({ user: pb.authStore.model!.id })
.collection('user_settings')
.create({ user: pb.authStore.model!.id })
$userSettings.set(createdSettings.settings) $userSettings.set(createdSettings.settings)
} catch (e) { } catch (e) {
console.log('create settings', e) console.log("create settings", e)
} }
} }
@@ -290,51 +285,51 @@ export const getSizeAndUnit = (n: number, isGigabytes = true) => {
const sizeInGB = isGigabytes ? n : n / 1_000 const sizeInGB = isGigabytes ? n : n / 1_000
if (sizeInGB >= 1_000) { if (sizeInGB >= 1_000) {
return { v: sizeInGB / 1_000, u: ' TB' } return { v: sizeInGB / 1_000, u: " TB" }
} else if (sizeInGB >= 1) { } else if (sizeInGB >= 1) {
return { v: sizeInGB, u: ' GB' } return { v: sizeInGB, u: " GB" }
} }
return { v: n, u: ' MB' } return { v: n, u: " MB" }
} }
export const chartMargin = { top: 12 } export const chartMargin = { top: 12 }
export const alertInfo = { export const alertInfo = {
Status: { Status: {
name: 'alerts.info.status', name: "alerts.info.status",
unit: '', unit: "",
icon: ServerIcon, icon: ServerIcon,
desc: 'alerts.info.status_des', desc: "alerts.info.status_des",
single: true, single: true,
}, },
CPU: { CPU: {
name: 'alerts.info.cpu_usage', name: "alerts.info.cpu_usage",
unit: '%', unit: "%",
icon: CpuIcon, icon: CpuIcon,
desc: 'alerts.info.cpu_usage_des', desc: "alerts.info.cpu_usage_des",
}, },
Memory: { Memory: {
name: 'alerts.info.memory_usage', name: "alerts.info.memory_usage",
unit: '%', unit: "%",
icon: MemoryStickIcon, icon: MemoryStickIcon,
desc: 'alerts.info.memory_usage_des', desc: "alerts.info.memory_usage_des",
}, },
Disk: { Disk: {
name: 'alerts.info.disk_usage', name: "alerts.info.disk_usage",
unit: '%', unit: "%",
icon: HardDriveIcon, icon: HardDriveIcon,
desc: 'alerts.info.disk_usage_des', desc: "alerts.info.disk_usage_des",
}, },
Bandwidth: { Bandwidth: {
name: 'alerts.info.bandwidth', name: "alerts.info.bandwidth",
unit: ' MB/s', unit: " MB/s",
icon: EthernetIcon, icon: EthernetIcon,
desc: 'alerts.info.bandwidth_des', desc: "alerts.info.bandwidth_des",
}, },
Temperature: { Temperature: {
name: 'alerts.info.temperature', name: "alerts.info.temperature",
unit: '°C', unit: "°C",
icon: ThermometerIcon, icon: ThermometerIcon,
desc: 'alerts.info.temperature_des', desc: "alerts.info.temperature_des",
}, },
} }

View File

@@ -1,30 +1,21 @@
import './index.css' import "./index.css"
import { Suspense, lazy, useEffect, StrictMode } from 'react' import { Suspense, lazy, useEffect, StrictMode } from "react"
import ReactDOM from 'react-dom/client' import ReactDOM from "react-dom/client"
import Home from './components/routes/home.tsx' import Home from "./components/routes/home.tsx"
import { ThemeProvider } from './components/theme-provider.tsx' import { ThemeProvider } from "./components/theme-provider.tsx"
import { import { $authenticated, $systems, pb, $publicKey, $hubVersion, $copyContent } from "./lib/stores.ts"
$authenticated, import { updateUserSettings, updateAlerts, updateFavicon, updateSystemList } from "./lib/utils.ts"
$systems, import { useStore } from "@nanostores/react"
pb, import { Toaster } from "./components/ui/toaster.tsx"
$publicKey, import { $router } from "./components/router.tsx"
$hubVersion, import SystemDetail from "./components/routes/system.tsx"
$copyContent, import Navbar from "./components/navbar.tsx"
} from './lib/stores.ts' import "./lib/i18n.ts"
import { updateUserSettings, updateAlerts, updateFavicon, updateSystemList } from './lib/utils.ts'
import { useStore } from '@nanostores/react'
import { Toaster } from './components/ui/toaster.tsx'
import { $router } from './components/router.tsx'
import SystemDetail from './components/routes/system.tsx'
import './lib/i18n.ts'
import { useTranslation } from 'react-i18next'
import Navbar from './components/navbar.tsx'
// const ServerDetail = lazy(() => import('./components/routes/system.tsx')) // const ServerDetail = lazy(() => import('./components/routes/system.tsx'))
const LoginPage = lazy(() => import('./components/login/login.tsx')) const LoginPage = lazy(() => import("./components/login/login.tsx"))
const CopyToClipboardDialog = lazy(() => import('./components/copy-to-clipboard.tsx')) const CopyToClipboardDialog = lazy(() => import("./components/copy-to-clipboard.tsx"))
const Settings = lazy(() => import('./components/routes/settings/layout.tsx')) const Settings = lazy(() => import("./components/routes/settings/layout.tsx"))
const App = () => { const App = () => {
const page = useStore($router) const page = useStore($router)
@@ -37,7 +28,7 @@ const App = () => {
$authenticated.set(pb.authStore.isValid) $authenticated.set(pb.authStore.isValid)
}) })
// get version / public key // get version / public key
pb.send('/api/beszel/getkey', {}).then((data) => { pb.send("/api/beszel/getkey", {}).then((data) => {
$publicKey.set(data.key) $publicKey.set(data.key)
$hubVersion.set(data.v) $hubVersion.set(data.v)
}) })
@@ -46,34 +37,34 @@ const App = () => {
// get alerts after system list is loaded // get alerts after system list is loaded
updateSystemList().then(updateAlerts) updateSystemList().then(updateAlerts)
return () => updateFavicon('favicon.svg') return () => updateFavicon("favicon.svg")
}, []) }, [])
// update favicon // update favicon
useEffect(() => { useEffect(() => {
if (!systems.length || !authenticated) { if (!systems.length || !authenticated) {
updateFavicon('favicon.svg') updateFavicon("favicon.svg")
} else { } else {
let up = false let up = false
for (const system of systems) { for (const system of systems) {
if (system.status === 'down') { if (system.status === "down") {
updateFavicon('favicon-red.svg') updateFavicon("favicon-red.svg")
return return
} else if (system.status === 'up') { } else if (system.status === "up") {
up = true up = true
} }
} }
updateFavicon(up ? 'favicon-green.svg' : 'favicon.svg') updateFavicon(up ? "favicon-green.svg" : "favicon.svg")
} }
}, [systems]) }, [systems])
if (!page) { if (!page) {
return <h1 className="text-3xl text-center my-14">404</h1> return <h1 className="text-3xl text-center my-14">404</h1>
} else if (page.path === '/') { } else if (page.path === "/") {
return <Home /> return <Home />
} else if (page.route === 'server') { } else if (page.route === "server") {
return <SystemDetail name={page.params.name} /> return <SystemDetail name={page.params.name} />
} else if (page.route === 'settings') { } else if (page.route === "settings") {
return ( return (
<Suspense> <Suspense>
<Settings /> <Settings />
@@ -83,8 +74,6 @@ const App = () => {
} }
const Layout = () => { const Layout = () => {
const { t } = useTranslation()
const authenticated = useStore($authenticated) const authenticated = useStore($authenticated)
const copyContent = useStore($copyContent) const copyContent = useStore($copyContent)
@@ -98,7 +87,7 @@ const Layout = () => {
return ( return (
<> <>
<div className="container">{Navbar(t)}</div> <div className="container">{Navbar()}</div>
<div className="container mb-14 relative"> <div className="container mb-14 relative">
<App /> <App />
{copyContent && ( {copyContent && (
@@ -111,7 +100,7 @@ const Layout = () => {
) )
} }
ReactDOM.createRoot(document.getElementById('app')!).render( ReactDOM.createRoot(document.getElementById("app")!).render(
// strict mode in dev mounts / unmounts components twice // strict mode in dev mounts / unmounts components twice
// and breaks the clipboard dialog // and breaks the clipboard dialog
//<StrictMode> //<StrictMode>

View File

@@ -1,9 +1,9 @@
import { RecordModel } from 'pocketbase' import { RecordModel } from "pocketbase"
export interface SystemRecord extends RecordModel { export interface SystemRecord extends RecordModel {
name: string name: string
host: string host: string
status: 'up' | 'down' | 'paused' | 'pending' status: "up" | "down" | "paused" | "pending"
port: string port: string
info: SystemInfo info: SystemInfo
v: string v: string
@@ -132,11 +132,11 @@ export interface AlertRecord extends RecordModel {
// user: string // user: string
} }
export type ChartTimes = '1h' | '12h' | '24h' | '1w' | '30d' export type ChartTimes = "1h" | "12h" | "24h" | "1w" | "30d"
export interface ChartTimeData { export interface ChartTimeData {
[key: string]: { [key: string]: {
type: '1m' | '10m' | '20m' | '120m' | '480m' type: "1m" | "10m" | "20m" | "120m" | "480m"
expectedInterval: number expectedInterval: number
label: string label: string
ticks?: number ticks?: number
@@ -155,7 +155,7 @@ export type UserSettings = {
type ChartDataContainer = { type ChartDataContainer = {
created: number | null created: number | null
} & { } & {
[key: string]: key extends 'created' ? never : ContainerStats [key: string]: key extends "created" ? never : ContainerStats
} }
export interface ChartData { export interface ChartData {

View File

@@ -1,104 +1,99 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
module.exports = { module.exports = {
darkMode: ['class'], darkMode: ["class"],
content: [ content: ["./pages/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}", "./app/**/*.{ts,tsx}", "./src/**/*.{ts,tsx}"],
'./pages/**/*.{ts,tsx}', prefix: "",
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
prefix: '',
theme: { theme: {
container: { container: {
center: true, center: true,
padding: '1rem', padding: "1rem",
screens: { screens: {
'2xl': '1400px', "2xl": "1400px",
}, },
}, },
extend: { extend: {
fontFamily: { fontFamily: {
sans: 'Inter, sans-serif', sans: "Inter, sans-serif",
// body: ['Inter', 'sans-serif'], // body: ['Inter', 'sans-serif'],
// display: ['Inter', 'sans-serif'], // display: ['Inter', 'sans-serif'],
}, },
screens: { screens: {
xs: '425px', xs: "425px",
450: '450px', 450: "450px",
}, },
colors: { colors: {
green: { green: {
50: '#EBF9F0', 50: "#EBF9F0",
100: '#D8F3E1', 100: "#D8F3E1",
200: '#ADE6C0', 200: "#ADE6C0",
300: '#85DBA2', 300: "#85DBA2",
400: '#5ACE81', 400: "#5ACE81",
500: '#38BB63', 500: "#38BB63",
600: '#2D954F', 600: "#2D954F",
700: '#22723D', 700: "#22723D",
800: '#164B28', 800: "#164B28",
900: '#0C2715', 900: "#0C2715",
950: '#06140A', 950: "#06140A",
}, },
border: 'hsl(var(--border))', border: "hsl(var(--border))",
input: 'hsl(var(--input))', input: "hsl(var(--input))",
ring: 'hsl(var(--ring))', ring: "hsl(var(--ring))",
background: 'hsl(var(--background))', background: "hsl(var(--background))",
foreground: 'hsl(var(--foreground))', foreground: "hsl(var(--foreground))",
primary: { primary: {
DEFAULT: 'hsl(var(--primary))', DEFAULT: "hsl(var(--primary))",
foreground: 'hsl(var(--primary-foreground))', foreground: "hsl(var(--primary-foreground))",
}, },
secondary: { secondary: {
DEFAULT: 'hsl(var(--secondary))', DEFAULT: "hsl(var(--secondary))",
foreground: 'hsl(var(--secondary-foreground))', foreground: "hsl(var(--secondary-foreground))",
}, },
destructive: { destructive: {
DEFAULT: 'hsl(var(--destructive))', DEFAULT: "hsl(var(--destructive))",
foreground: 'hsl(var(--destructive-foreground))', foreground: "hsl(var(--destructive-foreground))",
}, },
muted: { muted: {
DEFAULT: 'hsl(var(--muted))', DEFAULT: "hsl(var(--muted))",
foreground: 'hsl(var(--muted-foreground))', foreground: "hsl(var(--muted-foreground))",
}, },
accent: { accent: {
DEFAULT: 'hsl(var(--accent))', DEFAULT: "hsl(var(--accent))",
foreground: 'hsl(var(--accent-foreground))', foreground: "hsl(var(--accent-foreground))",
}, },
popover: { popover: {
DEFAULT: 'hsl(var(--popover))', DEFAULT: "hsl(var(--popover))",
foreground: 'hsl(var(--popover-foreground))', foreground: "hsl(var(--popover-foreground))",
}, },
card: { card: {
DEFAULT: 'hsl(var(--card))', DEFAULT: "hsl(var(--card))",
foreground: 'hsl(var(--card-foreground))', foreground: "hsl(var(--card-foreground))",
}, },
}, },
borderRadius: { borderRadius: {
lg: 'var(--radius)', lg: "var(--radius)",
md: 'calc(var(--radius) - 2px)', md: "calc(var(--radius) - 2px)",
sm: 'calc(var(--radius) - 4px)', sm: "calc(var(--radius) - 4px)",
}, },
keyframes: { keyframes: {
'accordion-down': { "accordion-down": {
from: { height: '0' }, from: { height: "0" },
to: { height: 'var(--radix-accordion-content-height)' }, to: { height: "var(--radix-accordion-content-height)" },
}, },
'accordion-up': { "accordion-up": {
from: { height: 'var(--radix-accordion-content-height)' }, from: { height: "var(--radix-accordion-content-height)" },
to: { height: '0' }, to: { height: "0" },
}, },
}, },
animation: { animation: {
'accordion-down': 'accordion-down 0.2s ease-out', "accordion-down": "accordion-down 0.2s ease-out",
'accordion-up': 'accordion-up 0.2s ease-out', "accordion-up": "accordion-up 0.2s ease-out",
}, },
}, },
}, },
plugins: [ plugins: [
require('tailwindcss-animate'), require("tailwindcss-animate"),
function ({ addVariant }) { function ({ addVariant }) {
addVariant('light', '.light &') addVariant("light", ".light &")
}, },
], ],
} }

View File

@@ -1,15 +1,15 @@
import { defineConfig } from 'vite' import { defineConfig } from "vite"
import react from '@vitejs/plugin-react' import react from "@vitejs/plugin-react"
import path from 'path' import path from "path"
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
esbuild: { esbuild: {
legalComments: 'external', legalComments: "external",
}, },
resolve: { resolve: {
alias: { alias: {
'@': path.resolve(__dirname, './src'), "@": path.resolve(__dirname, "./src"),
}, },
}, },
}) })