mirror of
https://github.com/fankes/beszel.git
synced 2025-10-19 17:59:28 +08:00
add prettier config and format files site files
This commit is contained in:
8
beszel/site/.prettierrc
Normal file
8
beszel/site/.prettierrc
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"trailingComma": "es5",
|
||||
"useTabs": true,
|
||||
"tabWidth": 2,
|
||||
"semi": false,
|
||||
"singleQuote": false,
|
||||
"printWidth": 120
|
||||
}
|
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/index.css",
|
||||
"baseColor": "gray",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils"
|
||||
}
|
||||
}
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/index.css",
|
||||
"baseColor": "gray",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils"
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -7,19 +7,19 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog'
|
||||
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
} from "@/components/ui/dialog"
|
||||
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { $publicKey, pb } from '@/lib/stores'
|
||||
import { Copy, PlusIcon } from 'lucide-react'
|
||||
import { useState, useRef, MutableRefObject } from 'react'
|
||||
import { useStore } from '@nanostores/react'
|
||||
import { cn, copyToClipboard, isReadOnlyUser } from '@/lib/utils'
|
||||
import { navigate } from './router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { $publicKey, pb } from "@/lib/stores"
|
||||
import { Copy, PlusIcon } from "lucide-react"
|
||||
import { useState, useRef, MutableRefObject } from "react"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { cn, copyToClipboard, isReadOnlyUser } from "@/lib/utils"
|
||||
import { navigate } from "./router"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
export function AddSystemButton({ className }: { className?: string }) {
|
||||
const { t } = useTranslation()
|
||||
@@ -58,8 +58,8 @@ export function AddSystemButton({ className }: { className?: string }) {
|
||||
data.users = pb.authStore.model!.id
|
||||
try {
|
||||
setOpen(false)
|
||||
await pb.collection('systems').create(data)
|
||||
navigate('/')
|
||||
await pb.collection("systems").create(data)
|
||||
navigate("/")
|
||||
// console.log(record)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
@@ -71,73 +71,64 @@ export function AddSystemButton({ className }: { className?: string }) {
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
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" />
|
||||
{t('add')}
|
||||
<span className="hidden sm:inline">{t('system')}</span>
|
||||
{t("add")}
|
||||
<span className="hidden sm:inline">{t("system")}</span>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="w-[90%] sm:max-w-[440px] rounded-lg">
|
||||
<Tabs defaultValue="docker">
|
||||
<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">
|
||||
<TabsTrigger value="docker">Docker</TabsTrigger>
|
||||
<TabsTrigger value="binary">{t('add_system.binary')}</TabsTrigger>
|
||||
<TabsTrigger value="binary">{t("add_system.binary")}</TabsTrigger>
|
||||
</TabsList>
|
||||
</DialogHeader>
|
||||
{/* Docker */}
|
||||
<TabsContent value="docker">
|
||||
<DialogDescription className={'mb-4'}>
|
||||
{t('add_system.dialog_des_1')}{' '}
|
||||
<code className="bg-muted px-1 rounded-sm">docker-compose.yml</code>{' '}
|
||||
{t('add_system.dialog_des_2')}
|
||||
<DialogDescription className={"mb-4"}>
|
||||
{t("add_system.dialog_des_1")} <code className="bg-muted px-1 rounded-sm">docker-compose.yml</code>{" "}
|
||||
{t("add_system.dialog_des_2")}
|
||||
</DialogDescription>
|
||||
</TabsContent>
|
||||
{/* Binary */}
|
||||
<TabsContent value="binary">
|
||||
<DialogDescription className={'mb-4'}>
|
||||
{t('add_system.dialog_des_1')}{' '}
|
||||
<code className="bg-muted px-1 rounded-sm">install command</code>{' '}
|
||||
{t('add_system.dialog_des_2')}
|
||||
<DialogDescription className={"mb-4"}>
|
||||
{t("add_system.dialog_des_1")} <code className="bg-muted px-1 rounded-sm">install command</code>{" "}
|
||||
{t("add_system.dialog_des_2")}
|
||||
</DialogDescription>
|
||||
</TabsContent>
|
||||
<form onSubmit={handleSubmit as any}>
|
||||
<div className="grid gap-3 mt-1 mb-4">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="name" className="text-right">
|
||||
{t('add_system.name')}
|
||||
{t("add_system.name")}
|
||||
</Label>
|
||||
<Input id="name" name="name" className="col-span-3" required />
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="host" className="text-right">
|
||||
{t('add_system.host_ip')}
|
||||
{t("add_system.host_ip")}
|
||||
</Label>
|
||||
<Input id="host" name="host" className="col-span-3" required />
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="port" className="text-right">
|
||||
{t('add_system.port')}
|
||||
{t("add_system.port")}
|
||||
</Label>
|
||||
<Input
|
||||
ref={port}
|
||||
name="port"
|
||||
id="port"
|
||||
defaultValue="45876"
|
||||
className="col-span-3"
|
||||
required
|
||||
/>
|
||||
<Input ref={port} name="port" id="port" defaultValue="45876" className="col-span-3" required />
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4 relative">
|
||||
<Label htmlFor="pkey" className="text-right whitespace-pre">
|
||||
{t('add_system.public_key')}
|
||||
{t("add_system.public_key")}
|
||||
</Label>
|
||||
<Input readOnly id="pkey" value={publicKey} className="col-span-3" required></Input>
|
||||
<div
|
||||
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>
|
||||
<TooltipProvider delayDuration={100}>
|
||||
@@ -145,7 +136,7 @@ export function AddSystemButton({ className }: { className?: string }) {
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant={'link'}
|
||||
variant={"link"}
|
||||
className="absolute right-0"
|
||||
onClick={() => copyToClipboard(publicKey)}
|
||||
>
|
||||
@@ -153,7 +144,7 @@ export function AddSystemButton({ className }: { className?: string }) {
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{t('add_system.click_to_copy')}</p>
|
||||
<p>{t("add_system.click_to_copy")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
@@ -162,27 +153,19 @@ export function AddSystemButton({ className }: { className?: string }) {
|
||||
{/* Docker */}
|
||||
<TabsContent value="docker">
|
||||
<DialogFooter className="flex justify-end gap-2 sm:w-[calc(100%+20px)] sm:-ml-[20px]">
|
||||
<Button
|
||||
type="button"
|
||||
variant={'ghost'}
|
||||
onClick={() => copyDockerCompose(port.current.value)}
|
||||
>
|
||||
{t('copy')} docker compose
|
||||
<Button type="button" variant={"ghost"} onClick={() => copyDockerCompose(port.current.value)}>
|
||||
{t("copy")} docker compose
|
||||
</Button>
|
||||
<Button>{t('add_system.add_system')}</Button>
|
||||
<Button>{t("add_system.add_system")}</Button>
|
||||
</DialogFooter>
|
||||
</TabsContent>
|
||||
{/* Binary */}
|
||||
<TabsContent value="binary">
|
||||
<DialogFooter className="flex justify-end gap-2 sm:w-[calc(100%+20px)] sm:-ml-[20px]">
|
||||
<Button
|
||||
type="button"
|
||||
variant={'ghost'}
|
||||
onClick={() => copyInstallCommand(port.current.value)}
|
||||
>
|
||||
{t('copy')} linux {t('add_system.command')}
|
||||
<Button type="button" variant={"ghost"} onClick={() => copyInstallCommand(port.current.value)}>
|
||||
{t("copy")} linux {t("add_system.command")}
|
||||
</Button>
|
||||
<Button>{t('add_system.add_system')}</Button>
|
||||
<Button>{t("add_system.add_system")}</Button>
|
||||
</DialogFooter>
|
||||
</TabsContent>
|
||||
</form>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { memo, useState } from 'react'
|
||||
import { useStore } from '@nanostores/react'
|
||||
import { $alerts, $systems } from '@/lib/stores'
|
||||
import { memo, useState } from "react"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { $alerts, $systems } from "@/lib/stores"
|
||||
import {
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
@@ -8,16 +8,16 @@ import {
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { BellIcon, GlobeIcon, ServerIcon } from 'lucide-react'
|
||||
import { alertInfo, cn } from '@/lib/utils'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { AlertRecord, SystemRecord } from '@/types'
|
||||
import { Link } from '../router'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Checkbox } from '../ui/checkbox'
|
||||
import { SystemAlert, SystemAlertGlobal } from './alerts-system'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
} from "@/components/ui/dialog"
|
||||
import { BellIcon, GlobeIcon, ServerIcon } from "lucide-react"
|
||||
import { alertInfo, cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { AlertRecord, SystemRecord } from "@/types"
|
||||
import { Link } from "../router"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { Checkbox } from "../ui/checkbox"
|
||||
import { SystemAlert, SystemAlertGlobal } from "./alerts-system"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
export default memo(function AlertsButton({ system }: { system: SystemRecord }) {
|
||||
const alerts = useStore($alerts)
|
||||
@@ -29,16 +29,10 @@ export default memo(function AlertsButton({ system }: { system: SystemRecord })
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
aria-label="Alerts"
|
||||
data-nolink
|
||||
onClick={() => setOpened(true)}
|
||||
>
|
||||
<Button variant="ghost" size="icon" aria-label="Alerts" data-nolink onClick={() => setOpened(true)}>
|
||||
<BellIcon
|
||||
className={cn('h-[1.2em] w-[1.2em] pointer-events-none', {
|
||||
'fill-primary': active,
|
||||
className={cn("h-[1.2em] w-[1.2em] pointer-events-none", {
|
||||
"fill-primary": active,
|
||||
})}
|
||||
/>
|
||||
</Button>
|
||||
@@ -57,7 +51,7 @@ function TheContent({
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [overwriteExisting, setOverwriteExisting] = useState<boolean | 'indeterminate'>(false)
|
||||
const [overwriteExisting, setOverwriteExisting] = useState<boolean | "indeterminate">(false)
|
||||
const systems = $systems.get()
|
||||
|
||||
const data = Object.keys(alertInfo).map((key) => {
|
||||
@@ -72,13 +66,13 @@ function TheContent({
|
||||
return (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl">{t('alerts.title')}</DialogTitle>
|
||||
<DialogTitle className="text-xl">{t("alerts.title")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t('alerts.subtitle_1')}{' '}
|
||||
{t("alerts.subtitle_1")}{" "}
|
||||
<Link href="/settings/notifications" className="link">
|
||||
{t('alerts.notification_settings')}
|
||||
</Link>{' '}
|
||||
{t('alerts.subtitle_2')}
|
||||
{t("alerts.notification_settings")}
|
||||
</Link>{" "}
|
||||
{t("alerts.subtitle_2")}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Tabs defaultValue="system">
|
||||
@@ -89,7 +83,7 @@ function TheContent({
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="global">
|
||||
<GlobeIcon className="mr-1.5 h-3.5 w-3.5" />
|
||||
{t('all_systems')}
|
||||
{t("all_systems")}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="system">
|
||||
@@ -110,17 +104,11 @@ function TheContent({
|
||||
checked={overwriteExisting}
|
||||
onCheckedChange={setOverwriteExisting}
|
||||
/>
|
||||
{t('alerts.overwrite_existing_alerts')}
|
||||
{t("alerts.overwrite_existing_alerts")}
|
||||
</label>
|
||||
<div className="grid gap-3">
|
||||
{data.map((d) => (
|
||||
<SystemAlertGlobal
|
||||
key={d.key}
|
||||
data={d}
|
||||
overwrite={overwriteExisting}
|
||||
alerts={alerts}
|
||||
systems={systems}
|
||||
/>
|
||||
<SystemAlertGlobal key={d.key} data={d} overwrite={overwriteExisting} alerts={alerts} systems={systems} />
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
@@ -1,12 +1,12 @@
|
||||
import { pb } from '@/lib/stores'
|
||||
import { alertInfo, cn } from '@/lib/utils'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { AlertRecord, SystemRecord } from '@/types'
|
||||
import { lazy, Suspense, useRef, useState } from 'react'
|
||||
import { toast } from '../ui/use-toast'
|
||||
import { RecordOptions } from 'pocketbase'
|
||||
import { newQueue, Queue } from '@henrygd/queue'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { pb } from "@/lib/stores"
|
||||
import { alertInfo, cn } from "@/lib/utils"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { AlertRecord, SystemRecord } from "@/types"
|
||||
import { lazy, Suspense, useRef, useState } from "react"
|
||||
import { toast } from "../ui/use-toast"
|
||||
import { RecordOptions } from "pocketbase"
|
||||
import { newQueue, Queue } from "@henrygd/queue"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
interface AlertData {
|
||||
checked?: boolean
|
||||
@@ -18,15 +18,15 @@ interface AlertData {
|
||||
system: SystemRecord
|
||||
}
|
||||
|
||||
const Slider = lazy(() => import('@/components/ui/slider'))
|
||||
const Slider = lazy(() => import("@/components/ui/slider"))
|
||||
|
||||
let queue: Queue
|
||||
|
||||
const failedUpdateToast = () =>
|
||||
toast({
|
||||
title: 'Failed to update alert',
|
||||
description: 'Please check logs for more details.',
|
||||
variant: 'destructive',
|
||||
title: "Failed to update alert",
|
||||
description: "Please check logs for more details.",
|
||||
variant: "destructive",
|
||||
})
|
||||
|
||||
export function SystemAlert({
|
||||
@@ -43,11 +43,11 @@ export function SystemAlert({
|
||||
data.updateAlert = async (checked: boolean, value: number, min: number) => {
|
||||
try {
|
||||
if (alert && !checked) {
|
||||
await pb.collection('alerts').delete(alert.id)
|
||||
await pb.collection("alerts").delete(alert.id)
|
||||
} 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) {
|
||||
pb.collection('alerts').create({
|
||||
pb.collection("alerts").create({
|
||||
system: system.id,
|
||||
user: pb.authStore.model!.id,
|
||||
name: data.key,
|
||||
@@ -76,7 +76,7 @@ export function SystemAlertGlobal({
|
||||
systems,
|
||||
}: {
|
||||
data: AlertData
|
||||
overwrite: boolean | 'indeterminate'
|
||||
overwrite: boolean | "indeterminate"
|
||||
alerts: AlertRecord[]
|
||||
systems: SystemRecord[]
|
||||
}) {
|
||||
@@ -111,9 +111,7 @@ export function SystemAlertGlobal({
|
||||
continue
|
||||
}
|
||||
// find matching existing alert
|
||||
const existingAlert = alerts.find(
|
||||
(alert) => alert.system === system.id && data.key === alert.name
|
||||
)
|
||||
const existingAlert = alerts.find((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 (existingAlert && !populatedSet && !overwrite) {
|
||||
set.add(system.id)
|
||||
@@ -128,13 +126,13 @@ export function SystemAlertGlobal({
|
||||
if (existingAlert) {
|
||||
// console.log('updating', system.name)
|
||||
queue
|
||||
.add(() => pb.collection('alerts').update(existingAlert.id, recordData, requestOptions))
|
||||
.add(() => pb.collection("alerts").update(existingAlert.id, recordData, requestOptions))
|
||||
.catch(failedUpdateToast)
|
||||
} else {
|
||||
// console.log('creating', system.name)
|
||||
queue
|
||||
.add(() =>
|
||||
pb.collection('alerts').create(
|
||||
pb.collection("alerts").create(
|
||||
{
|
||||
system: system.id,
|
||||
user: pb.authStore.model!.id,
|
||||
@@ -148,7 +146,7 @@ export function SystemAlertGlobal({
|
||||
}
|
||||
} else if (existingAlert) {
|
||||
// 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
|
||||
@@ -162,7 +160,7 @@ function AlertContent({ data }: { data: AlertData }) {
|
||||
|
||||
const { key } = data
|
||||
|
||||
const hasSliders = !('single' in data.alert)
|
||||
const hasSliders = !("single" in data.alert)
|
||||
|
||||
const [checked, setChecked] = useState(data.checked || false)
|
||||
const [min, setMin] = useState(data.min || (hasSliders ? 10 : 0))
|
||||
@@ -175,24 +173,21 @@ function AlertContent({ data }: { data: AlertData }) {
|
||||
|
||||
const Icon = alertInfo[key].icon
|
||||
|
||||
const updateAlert = (c?: boolean) =>
|
||||
data.updateAlert?.(c ?? checked, newValue.current, newMin.current)
|
||||
const updateAlert = (c?: boolean) => data.updateAlert?.(c ?? checked, newValue.current, newMin.current)
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-muted-foreground/15 hover:border-muted-foreground/20 transition-colors duration-100 group">
|
||||
<label
|
||||
htmlFor={`s${key}`}
|
||||
className={cn('flex flex-row items-center justify-between gap-4 cursor-pointer p-4', {
|
||||
'pb-0': showSliders,
|
||||
className={cn("flex flex-row items-center justify-between gap-4 cursor-pointer p-4", {
|
||||
"pb-0": showSliders,
|
||||
})}
|
||||
>
|
||||
<div className="grid gap-1 select-none">
|
||||
<p className="font-semibold flex gap-3 items-center capitalize">
|
||||
<Icon className="h-4 w-4 opacity-85" /> {t(data.alert.name)}
|
||||
</p>
|
||||
{!showSliders && (
|
||||
<span className="block text-sm text-muted-foreground">{t(data.alert.desc)}</span>
|
||||
)}
|
||||
{!showSliders && <span className="block text-sm text-muted-foreground">{t(data.alert.desc)}</span>}
|
||||
</div>
|
||||
<Switch
|
||||
id={`s${key}`}
|
||||
@@ -208,7 +203,7 @@ function AlertContent({ data }: { data: AlertData }) {
|
||||
<Suspense fallback={<div className="h-10" />}>
|
||||
<div>
|
||||
<p id={`v${key}`} className="text-sm block h-8">
|
||||
{t('alerts.average_exceeds')}{' '}
|
||||
{t("alerts.average_exceeds")}{" "}
|
||||
<strong className="text-foreground">
|
||||
{value}
|
||||
{data.alert.unit}
|
||||
@@ -227,7 +222,8 @@ function AlertContent({ data }: { data: AlertData }) {
|
||||
</div>
|
||||
<div>
|
||||
<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>
|
||||
<div className="flex gap-3">
|
||||
<Slider
|
||||
|
@@ -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 {
|
||||
useYAxisWidth,
|
||||
cn,
|
||||
@@ -8,10 +8,10 @@ import {
|
||||
toFixedWithoutTrailingZeros,
|
||||
decimalString,
|
||||
chartMargin,
|
||||
} from '@/lib/utils'
|
||||
} from "@/lib/utils"
|
||||
// import Spinner from '../spinner'
|
||||
import { ChartData } from '@/types'
|
||||
import { memo, useMemo } from 'react'
|
||||
import { ChartData } from "@/types"
|
||||
import { memo, useMemo } from "react"
|
||||
|
||||
/** [label, key, color, opacity] */
|
||||
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.
|
||||
// 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.
|
||||
return `stats.${path}${max ? 'm' : ''}`
|
||||
.split('.')
|
||||
return `stats.${path}${max ? "m" : ""}`
|
||||
.split(".")
|
||||
.reduce((acc: any, key: string) => acc?.[key] ?? (data.stats?.cpum ? 0 : null), data)
|
||||
}
|
||||
|
||||
export default memo(function AreaChartDefault({
|
||||
maxToggled = false,
|
||||
unit = ' MB/s',
|
||||
unit = " MB/s",
|
||||
chartName,
|
||||
chartData,
|
||||
}: {
|
||||
@@ -41,26 +41,26 @@ export default memo(function AreaChartDefault({
|
||||
|
||||
const { chartTime } = chartData
|
||||
|
||||
const showMax = chartTime !== '1h' && maxToggled
|
||||
const showMax = chartTime !== "1h" && maxToggled
|
||||
|
||||
const dataKeys: DataKeys[] = useMemo(() => {
|
||||
// [label, key, color, opacity]
|
||||
if (chartName === 'CPU Usage') {
|
||||
return [[chartName, 'cpu', 1, 0.4]]
|
||||
} else if (chartName === 'dio') {
|
||||
if (chartName === "CPU Usage") {
|
||||
return [[chartName, "cpu", 1, 0.4]]
|
||||
} else if (chartName === "dio") {
|
||||
return [
|
||||
['Write', 'dw', 3, 0.3],
|
||||
['Read', 'dr', 1, 0.3],
|
||||
["Write", "dw", 3, 0.3],
|
||||
["Read", "dr", 1, 0.3],
|
||||
]
|
||||
} else if (chartName === 'bw') {
|
||||
} else if (chartName === "bw") {
|
||||
return [
|
||||
['Sent', 'ns', 5, 0.2],
|
||||
['Received', 'nr', 2, 0.2],
|
||||
["Sent", "ns", 5, 0.2],
|
||||
["Received", "nr", 2, 0.2],
|
||||
]
|
||||
} else if (chartName.startsWith('efs')) {
|
||||
} else if (chartName.startsWith("efs")) {
|
||||
return [
|
||||
['Write', `${chartName}.w`, 3, 0.3],
|
||||
['Read', `${chartName}.r`, 1, 0.3],
|
||||
["Write", `${chartName}.w`, 3, 0.3],
|
||||
["Read", `${chartName}.r`, 1, 0.3],
|
||||
]
|
||||
}
|
||||
return []
|
||||
@@ -71,8 +71,8 @@ export default memo(function AreaChartDefault({
|
||||
return (
|
||||
<div>
|
||||
<ChartContainer
|
||||
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
|
||||
'opacity-100': yAxisWidth,
|
||||
className={cn("h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity", {
|
||||
"opacity-100": yAxisWidth,
|
||||
})}
|
||||
>
|
||||
<AreaChart accessibilityLayer data={chartData.systemStats} margin={chartMargin}>
|
||||
|
@@ -1,26 +1,16 @@
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} 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'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } 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 }) {
|
||||
const chartTime = useStore($chartTime)
|
||||
|
||||
return (
|
||||
<Select
|
||||
defaultValue="1h"
|
||||
value={chartTime}
|
||||
onValueChange={(value: ChartTimes) => $chartTime.set(value)}
|
||||
>
|
||||
<SelectTrigger className={cn(className, 'relative pl-10 pr-5')}>
|
||||
<Select defaultValue="1h" 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" />
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
|
@@ -1,12 +1,6 @@
|
||||
import { Area, AreaChart, CartesianGrid, YAxis } from 'recharts'
|
||||
import {
|
||||
ChartConfig,
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
xAxis,
|
||||
} from '@/components/ui/chart'
|
||||
import { memo, useMemo } from 'react'
|
||||
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
|
||||
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
|
||||
import { memo, useMemo } from "react"
|
||||
import {
|
||||
useYAxisWidth,
|
||||
cn,
|
||||
@@ -16,18 +10,18 @@ import {
|
||||
toFixedFloat,
|
||||
getSizeAndUnit,
|
||||
toFixedWithoutTrailingZeros,
|
||||
} from '@/lib/utils'
|
||||
} from "@/lib/utils"
|
||||
// import Spinner from '../spinner'
|
||||
import { useStore } from '@nanostores/react'
|
||||
import { $containerFilter } from '@/lib/stores'
|
||||
import { ChartData } from '@/types'
|
||||
import { Separator } from '../ui/separator'
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { $containerFilter } from "@/lib/stores"
|
||||
import { ChartData } from "@/types"
|
||||
import { Separator } from "../ui/separator"
|
||||
|
||||
export default memo(function ContainerChart({
|
||||
dataKey,
|
||||
chartData,
|
||||
chartName,
|
||||
unit = '%',
|
||||
unit = "%",
|
||||
}: {
|
||||
dataKey: string
|
||||
chartData: ChartData
|
||||
@@ -39,7 +33,7 @@ export default memo(function ContainerChart({
|
||||
|
||||
const { containerData } = chartData
|
||||
|
||||
const isNetChart = chartName === 'net'
|
||||
const isNetChart = chartName === "net"
|
||||
|
||||
const chartConfig = useMemo(() => {
|
||||
let config = {} as Record<
|
||||
@@ -52,7 +46,7 @@ export default memo(function ContainerChart({
|
||||
const totalUsage = {} as Record<string, number>
|
||||
for (let stats of containerData) {
|
||||
for (let key in stats) {
|
||||
if (!key || key === 'created') {
|
||||
if (!key || key === "created") {
|
||||
continue
|
||||
}
|
||||
if (!(key in totalUsage)) {
|
||||
@@ -87,7 +81,7 @@ export default memo(function ContainerChart({
|
||||
tickFormatter: (value: any) => string
|
||||
}
|
||||
// tick formatter
|
||||
if (chartName === 'cpu') {
|
||||
if (chartName === "cpu") {
|
||||
obj.tickFormatter = (value) => {
|
||||
const val = toFixedWithoutTrailingZeros(value, 2) + unit
|
||||
return updateYAxisWidth(val)
|
||||
@@ -95,7 +89,7 @@ export default memo(function ContainerChart({
|
||||
} else {
|
||||
obj.tickFormatter = (value) => {
|
||||
const { v, u } = getSizeAndUnit(value, false)
|
||||
return updateYAxisWidth(`${toFixedFloat(v, 2)}${u}${isNetChart ? '/s' : ''}`)
|
||||
return updateYAxisWidth(`${toFixedFloat(v, 2)}${u}${isNetChart ? "/s" : ""}`)
|
||||
}
|
||||
}
|
||||
// tooltip formatter
|
||||
@@ -134,8 +128,8 @@ export default memo(function ContainerChart({
|
||||
return (
|
||||
<div>
|
||||
<ChartContainer
|
||||
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
|
||||
'opacity-100': yAxisWidth,
|
||||
className={cn("h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity", {
|
||||
"opacity-100": yAxisWidth,
|
||||
})}
|
||||
>
|
||||
<AreaChart
|
||||
|
@@ -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 {
|
||||
useYAxisWidth,
|
||||
cn,
|
||||
@@ -9,9 +9,9 @@ import {
|
||||
toFixedFloat,
|
||||
chartMargin,
|
||||
getSizeAndUnit,
|
||||
} from '@/lib/utils'
|
||||
import { ChartData } from '@/types'
|
||||
import { memo } from 'react'
|
||||
} from "@/lib/utils"
|
||||
import { ChartData } from "@/types"
|
||||
import { memo } from "react"
|
||||
|
||||
export default memo(function DiskChart({
|
||||
dataKey,
|
||||
@@ -27,8 +27,8 @@ export default memo(function DiskChart({
|
||||
return (
|
||||
<div>
|
||||
<ChartContainer
|
||||
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
|
||||
'opacity-100': yAxisWidth,
|
||||
className={cn("h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity", {
|
||||
"opacity-100": yAxisWidth,
|
||||
})}
|
||||
>
|
||||
<AreaChart accessibilityLayer data={chartData.systemStats} margin={chartMargin}>
|
||||
|
@@ -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 {
|
||||
useYAxisWidth,
|
||||
cn,
|
||||
toFixedFloat,
|
||||
decimalString,
|
||||
formatShortDate,
|
||||
chartMargin,
|
||||
} from '@/lib/utils'
|
||||
import { memo } from 'react'
|
||||
import { ChartData } from '@/types'
|
||||
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
|
||||
import { useYAxisWidth, cn, toFixedFloat, decimalString, formatShortDate, chartMargin } from "@/lib/utils"
|
||||
import { memo } from "react"
|
||||
import { ChartData } from "@/types"
|
||||
|
||||
export default memo(function MemChart({ chartData }: { chartData: ChartData }) {
|
||||
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
||||
@@ -23,8 +16,8 @@ export default memo(function MemChart({ chartData }: { chartData: ChartData }) {
|
||||
<div>
|
||||
{/* {!yAxisSet && <Spinner />} */}
|
||||
<ChartContainer
|
||||
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
|
||||
'opacity-100': yAxisWidth,
|
||||
className={cn("h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity", {
|
||||
"opacity-100": yAxisWidth,
|
||||
})}
|
||||
>
|
||||
<AreaChart accessibilityLayer data={chartData.systemStats} margin={chartMargin}>
|
||||
@@ -40,7 +33,7 @@ export default memo(function MemChart({ chartData }: { chartData: ChartData }) {
|
||||
axisLine={false}
|
||||
tickFormatter={(value) => {
|
||||
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
|
||||
itemSorter={(a, b) => a.order - b.order}
|
||||
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
||||
contentFormatter={(item) => decimalString(item.value) + ' GB'}
|
||||
contentFormatter={(item) => decimalString(item.value) + " GB"}
|
||||
// indicator="line"
|
||||
/>
|
||||
}
|
||||
|
@@ -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 {
|
||||
useYAxisWidth,
|
||||
cn,
|
||||
@@ -8,9 +8,9 @@ import {
|
||||
toFixedWithoutTrailingZeros,
|
||||
decimalString,
|
||||
chartMargin,
|
||||
} from '@/lib/utils'
|
||||
import { ChartData } from '@/types'
|
||||
import { memo } from 'react'
|
||||
} from "@/lib/utils"
|
||||
import { ChartData } from "@/types"
|
||||
import { memo } from "react"
|
||||
|
||||
export default memo(function SwapChart({ chartData }: { chartData: ChartData }) {
|
||||
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
||||
@@ -18,22 +18,19 @@ export default memo(function SwapChart({ chartData }: { chartData: ChartData })
|
||||
return (
|
||||
<div>
|
||||
<ChartContainer
|
||||
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
|
||||
'opacity-100': yAxisWidth,
|
||||
className={cn("h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity", {
|
||||
"opacity-100": yAxisWidth,
|
||||
})}
|
||||
>
|
||||
<AreaChart accessibilityLayer data={chartData.systemStats} margin={chartMargin}>
|
||||
<CartesianGrid vertical={false} />
|
||||
<YAxis
|
||||
className="tracking-tighter"
|
||||
domain={[
|
||||
0,
|
||||
() => toFixedWithoutTrailingZeros(chartData.systemStats.at(-1)?.stats.s ?? 0.04, 2),
|
||||
]}
|
||||
domain={[0, () => toFixedWithoutTrailingZeros(chartData.systemStats.at(-1)?.stats.s ?? 0.04, 2)]}
|
||||
width={yAxisWidth}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickFormatter={(value) => updateYAxisWidth(value + ' GB')}
|
||||
tickFormatter={(value) => updateYAxisWidth(value + " GB")}
|
||||
/>
|
||||
{xAxis(chartData)}
|
||||
<ChartTooltip
|
||||
@@ -42,7 +39,7 @@ export default memo(function SwapChart({ chartData }: { chartData: ChartData })
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
||||
contentFormatter={(item) => decimalString(item.value) + ' GB'}
|
||||
contentFormatter={(item) => decimalString(item.value) + " GB"}
|
||||
// indicator="line"
|
||||
/>
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { CartesianGrid, Line, LineChart, YAxis } from 'recharts'
|
||||
import { CartesianGrid, Line, LineChart, YAxis } from "recharts"
|
||||
|
||||
import {
|
||||
ChartContainer,
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
xAxis,
|
||||
} from '@/components/ui/chart'
|
||||
} from "@/components/ui/chart"
|
||||
import {
|
||||
useYAxisWidth,
|
||||
cn,
|
||||
@@ -15,9 +15,9 @@ import {
|
||||
toFixedWithoutTrailingZeros,
|
||||
decimalString,
|
||||
chartMargin,
|
||||
} from '@/lib/utils'
|
||||
import { ChartData } from '@/types'
|
||||
import { memo, useMemo } from 'react'
|
||||
} from "@/lib/utils"
|
||||
import { ChartData } from "@/types"
|
||||
import { memo, useMemo } from "react"
|
||||
|
||||
export default memo(function TemperatureChart({ chartData }: { chartData: ChartData }) {
|
||||
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
||||
@@ -53,19 +53,19 @@ export default memo(function TemperatureChart({ chartData }: { chartData: ChartD
|
||||
return (
|
||||
<div>
|
||||
<ChartContainer
|
||||
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
|
||||
'opacity-100': yAxisWidth,
|
||||
className={cn("h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity", {
|
||||
"opacity-100": yAxisWidth,
|
||||
})}
|
||||
>
|
||||
<LineChart accessibilityLayer data={newChartData.data} margin={chartMargin}>
|
||||
<CartesianGrid vertical={false} />
|
||||
<YAxis
|
||||
className="tracking-tighter"
|
||||
domain={[0, 'auto']}
|
||||
domain={[0, "auto"]}
|
||||
width={yAxisWidth}
|
||||
tickFormatter={(value) => {
|
||||
const val = toFixedWithoutTrailingZeros(value, 2)
|
||||
return updateYAxisWidth(val + ' °C')
|
||||
return updateYAxisWidth(val + " °C")
|
||||
}}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
@@ -79,7 +79,7 @@ export default memo(function TemperatureChart({ chartData }: { chartData: ChartD
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
||||
contentFormatter={(item) => decimalString(item.value) + ' °C'}
|
||||
contentFormatter={(item) => decimalString(item.value) + " °C"}
|
||||
// indicator="line"
|
||||
/>
|
||||
}
|
||||
|
@@ -8,7 +8,7 @@ import {
|
||||
Server,
|
||||
SettingsIcon,
|
||||
UsersIcon,
|
||||
} from 'lucide-react'
|
||||
} from "lucide-react"
|
||||
|
||||
import {
|
||||
CommandDialog,
|
||||
@@ -19,39 +19,33 @@ import {
|
||||
CommandList,
|
||||
CommandSeparator,
|
||||
CommandShortcut,
|
||||
} from '@/components/ui/command'
|
||||
import { useEffect } from 'react'
|
||||
import { useStore } from '@nanostores/react'
|
||||
import { $systems } from '@/lib/stores'
|
||||
import { isAdmin } from '@/lib/utils'
|
||||
import { navigate } from './router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
} from "@/components/ui/command"
|
||||
import { useEffect } from "react"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { $systems } from "@/lib/stores"
|
||||
import { isAdmin } from "@/lib/utils"
|
||||
import { navigate } from "./router"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
export default function CommandPalette({
|
||||
open,
|
||||
setOpen,
|
||||
}: {
|
||||
open: boolean
|
||||
setOpen: (open: boolean) => void
|
||||
}) {
|
||||
export default function CommandPalette({ open, setOpen }: { open: boolean; setOpen: (open: boolean) => void }) {
|
||||
const { t } = useTranslation()
|
||||
const systems = useStore($systems)
|
||||
|
||||
useEffect(() => {
|
||||
const down = (e: KeyboardEvent) => {
|
||||
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
|
||||
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault()
|
||||
setOpen(!open)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', down)
|
||||
return () => document.removeEventListener('keydown', down)
|
||||
document.addEventListener("keydown", down)
|
||||
return () => document.removeEventListener("keydown", down)
|
||||
}, [open, setOpen])
|
||||
|
||||
return (
|
||||
<CommandDialog open={open} onOpenChange={setOpen}>
|
||||
<CommandInput placeholder={t('command.search')} />
|
||||
<CommandInput placeholder={t("command.search")} />
|
||||
<CommandList>
|
||||
<CommandEmpty>No results found.</CommandEmpty>
|
||||
{systems.length > 0 && (
|
||||
@@ -74,106 +68,106 @@ export default function CommandPalette({
|
||||
<CommandSeparator className="mb-1.5" />
|
||||
</>
|
||||
)}
|
||||
<CommandGroup heading={t('command.pages_settings')}>
|
||||
<CommandGroup heading={t("command.pages_settings")}>
|
||||
<CommandItem
|
||||
keywords={['home']}
|
||||
keywords={["home"]}
|
||||
onSelect={() => {
|
||||
navigate('/')
|
||||
navigate("/")
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<LayoutDashboard className="mr-2 h-4 w-4" />
|
||||
<span>{t('command.dashboard')}</span>
|
||||
<CommandShortcut>{t('command.page')}</CommandShortcut>
|
||||
<span>{t("command.dashboard")}</span>
|
||||
<CommandShortcut>{t("command.page")}</CommandShortcut>
|
||||
</CommandItem>
|
||||
<CommandItem
|
||||
onSelect={() => {
|
||||
navigate('/settings/general')
|
||||
navigate("/settings/general")
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<SettingsIcon className="mr-2 h-4 w-4" />
|
||||
<span>{t('settings.settings')}</span>
|
||||
<CommandShortcut>{t('settings.settings')}</CommandShortcut>
|
||||
<span>{t("settings.settings")}</span>
|
||||
<CommandShortcut>{t("settings.settings")}</CommandShortcut>
|
||||
</CommandItem>
|
||||
<CommandItem
|
||||
keywords={['alerts']}
|
||||
keywords={["alerts"]}
|
||||
onSelect={() => {
|
||||
navigate('/settings/notifications')
|
||||
navigate("/settings/notifications")
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<MailIcon className="mr-2 h-4 w-4" />
|
||||
<span>{t('settings.notifications.title')}</span>
|
||||
<CommandShortcut>{t('settings.settings')}</CommandShortcut>
|
||||
<span>{t("settings.notifications.title")}</span>
|
||||
<CommandShortcut>{t("settings.settings")}</CommandShortcut>
|
||||
</CommandItem>
|
||||
<CommandItem
|
||||
keywords={['github']}
|
||||
keywords={["github"]}
|
||||
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" />
|
||||
<span>{t('command.documentation')}</span>
|
||||
<span>{t("command.documentation")}</span>
|
||||
<CommandShortcut>GitHub</CommandShortcut>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
{isAdmin() && (
|
||||
<>
|
||||
<CommandSeparator className="mb-1.5" />
|
||||
<CommandGroup heading={t('command.admin')}>
|
||||
<CommandGroup heading={t("command.admin")}>
|
||||
<CommandItem
|
||||
keywords={['pocketbase']}
|
||||
keywords={["pocketbase"]}
|
||||
onSelect={() => {
|
||||
setOpen(false)
|
||||
window.open('/_/', '_blank')
|
||||
window.open("/_/", "_blank")
|
||||
}}
|
||||
>
|
||||
<UsersIcon className="mr-2 h-4 w-4" />
|
||||
<span>{t('user_dm.users')}</span>
|
||||
<CommandShortcut>{t('command.admin')}</CommandShortcut>
|
||||
<span>{t("user_dm.users")}</span>
|
||||
<CommandShortcut>{t("command.admin")}</CommandShortcut>
|
||||
</CommandItem>
|
||||
<CommandItem
|
||||
onSelect={() => {
|
||||
setOpen(false)
|
||||
window.open('/_/#/logs', '_blank')
|
||||
window.open("/_/#/logs", "_blank")
|
||||
}}
|
||||
>
|
||||
<LogsIcon className="mr-2 h-4 w-4" />
|
||||
<span>{t('user_dm.logs')}</span>
|
||||
<CommandShortcut>{t('command.admin')}</CommandShortcut>
|
||||
<span>{t("user_dm.logs")}</span>
|
||||
<CommandShortcut>{t("command.admin")}</CommandShortcut>
|
||||
</CommandItem>
|
||||
<CommandItem
|
||||
onSelect={() => {
|
||||
setOpen(false)
|
||||
window.open('/_/#/settings/backups', '_blank')
|
||||
window.open("/_/#/settings/backups", "_blank")
|
||||
}}
|
||||
>
|
||||
<DatabaseBackupIcon className="mr-2 h-4 w-4" />
|
||||
<span>{t('user_dm.backups')}</span>
|
||||
<CommandShortcut>{t('command.admin')}</CommandShortcut>
|
||||
<span>{t("user_dm.backups")}</span>
|
||||
<CommandShortcut>{t("command.admin")}</CommandShortcut>
|
||||
</CommandItem>
|
||||
<CommandItem
|
||||
keywords={['oauth', 'oicd']}
|
||||
keywords={["oauth", "oicd"]}
|
||||
onSelect={() => {
|
||||
setOpen(false)
|
||||
window.open('/_/#/settings/auth-providers', '_blank')
|
||||
window.open("/_/#/settings/auth-providers", "_blank")
|
||||
}}
|
||||
>
|
||||
<LockKeyholeIcon className="mr-2 h-4 w-4" />
|
||||
<span>{t('user_dm.auth_providers')}</span>
|
||||
<CommandShortcut>{t('command.admin')}</CommandShortcut>
|
||||
<span>{t("user_dm.auth_providers")}</span>
|
||||
<CommandShortcut>{t("command.admin")}</CommandShortcut>
|
||||
</CommandItem>
|
||||
<CommandItem
|
||||
keywords={['email']}
|
||||
keywords={["email"]}
|
||||
onSelect={() => {
|
||||
setOpen(false)
|
||||
window.open('/_/#/settings/mail', '_blank')
|
||||
window.open("/_/#/settings/mail", "_blank")
|
||||
}}
|
||||
>
|
||||
<MailIcon className="mr-2 h-4 w-4" />
|
||||
<span>{t('command.SMTP_settings')}</span>
|
||||
<CommandShortcut>{t('command.admin')}</CommandShortcut>
|
||||
<span>{t("command.SMTP_settings")}</span>
|
||||
<CommandShortcut>{t("command.admin")}</CommandShortcut>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</>
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useMemo, useRef } from 'react'
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from './ui/dialog'
|
||||
import { Textarea } from './ui/textarea'
|
||||
import { $copyContent } from '@/lib/stores'
|
||||
import { useEffect, useMemo, useRef } from "react"
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "./ui/dialog"
|
||||
import { Textarea } from "./ui/textarea"
|
||||
import { $copyContent } from "@/lib/stores"
|
||||
|
||||
export default function CopyToClipboard({ content }: { content: string }) {
|
||||
return (
|
||||
@@ -24,7 +24,7 @@ function CopyTextarea({ content }: { content: string }) {
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||
|
||||
const rows = useMemo(() => {
|
||||
return content.split('\n').length
|
||||
return content.split("\n").length
|
||||
}, [content])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -34,7 +34,7 @@ function CopyTextarea({ content }: { content: string }) {
|
||||
}, [textareaRef])
|
||||
|
||||
useEffect(() => {
|
||||
return () => $copyContent.set('')
|
||||
return () => $copyContent.set("")
|
||||
}, [])
|
||||
|
||||
return (
|
||||
|
@@ -1,16 +1,11 @@
|
||||
import { useEffect } from 'react'
|
||||
import { LanguagesIcon } from 'lucide-react'
|
||||
import { useEffect } from "react"
|
||||
import { LanguagesIcon } from "lucide-react"
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import languages from '../lib/languages.json'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, 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() {
|
||||
const { i18n } = useTranslation()
|
||||
@@ -22,7 +17,7 @@ export function LangToggle() {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<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" />
|
||||
<span className="sr-only">Language</span>
|
||||
</Button>
|
||||
@@ -31,7 +26,7 @@ export function LangToggle() {
|
||||
{languages.map(({ lang, label }) => (
|
||||
<DropdownMenuItem
|
||||
key={lang}
|
||||
className={cn('pl-4', lang === i18n.language ? 'font-bold' : '')}
|
||||
className={cn("pl-4", lang === i18n.language ? "font-bold" : "")}
|
||||
onClick={() => i18n.changeLanguage(lang)}
|
||||
>
|
||||
{label}
|
||||
|
@@ -1,29 +1,20 @@
|
||||
import { cn } from '@/lib/utils'
|
||||
import { buttonVariants } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { LoaderCircle, LockIcon, LogInIcon, MailIcon, UserIcon } from 'lucide-react'
|
||||
import { $authenticated, pb } from '@/lib/stores'
|
||||
import * as v from 'valibot'
|
||||
import { toast } from '../ui/use-toast'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTrigger,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { AuthMethodsList, OAuth2AuthConfig } from 'pocketbase'
|
||||
import { Link } from '../router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { LoaderCircle, LockIcon, LogInIcon, MailIcon, UserIcon } from "lucide-react"
|
||||
import { $authenticated, pb } from "@/lib/stores"
|
||||
import * as v from "valibot"
|
||||
import { toast } from "../ui/use-toast"
|
||||
import { Dialog, DialogContent, DialogTrigger, DialogHeader, 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 emailSchema = v.pipe(v.string(), v.email('Invalid email address.'))
|
||||
const passwordSchema = v.pipe(
|
||||
v.string(),
|
||||
v.minLength(10, 'Password must be at least 10 characters.')
|
||||
)
|
||||
const honeypot = v.literal("")
|
||||
const emailSchema = v.pipe(v.string(), v.email("Invalid email address."))
|
||||
const passwordSchema = v.pipe(v.string(), v.minLength(10, "Password must be at least 10 characters."))
|
||||
|
||||
const LoginSchema = v.looseObject({
|
||||
name: honeypot,
|
||||
@@ -37,9 +28,9 @@ const RegisterSchema = v.looseObject({
|
||||
v.string(),
|
||||
v.regex(
|
||||
/^(?=.*[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,
|
||||
password: passwordSchema,
|
||||
@@ -48,9 +39,9 @@ const RegisterSchema = v.looseObject({
|
||||
|
||||
const showLoginFaliedToast = () => {
|
||||
toast({
|
||||
title: 'Login attempt failed',
|
||||
description: 'Please check your credentials and try again',
|
||||
variant: 'destructive',
|
||||
title: "Login attempt failed",
|
||||
description: "Please check your credentials and try again",
|
||||
variant: "destructive",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -93,7 +84,7 @@ export function UserAuthForm({
|
||||
if (isFirstRun) {
|
||||
// check that passwords match
|
||||
if (password !== passwordConfirm) {
|
||||
let msg = 'Passwords do not match'
|
||||
let msg = "Passwords do not match"
|
||||
setErrors({ passwordConfirm: msg })
|
||||
return
|
||||
}
|
||||
@@ -103,17 +94,17 @@ export function UserAuthForm({
|
||||
passwordConfirm: password,
|
||||
})
|
||||
await pb.admins.authWithPassword(email, password)
|
||||
await pb.collection('users').create({
|
||||
await pb.collection("users").create({
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
passwordConfirm: password,
|
||||
role: 'admin',
|
||||
role: "admin",
|
||||
verified: true,
|
||||
})
|
||||
await pb.collection('users').authWithPassword(email, password)
|
||||
await pb.collection("users").authWithPassword(email, password)
|
||||
} else {
|
||||
await pb.collection('users').authWithPassword(email, password)
|
||||
await pb.collection("users").authWithPassword(email, password)
|
||||
}
|
||||
$authenticated.set(true)
|
||||
} catch (e) {
|
||||
@@ -130,7 +121,7 @@ export function UserAuthForm({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('grid gap-6', className)} {...props}>
|
||||
<div className={cn("grid gap-6", className)} {...props}>
|
||||
{authMethods.emailPassword && (
|
||||
<>
|
||||
<form onSubmit={handleSubmit} onChange={() => setErrors({})}>
|
||||
@@ -154,9 +145,7 @@ export function UserAuthForm({
|
||||
disabled={isLoading || isOauthLoading}
|
||||
className="pl-9"
|
||||
/>
|
||||
{errors?.username && (
|
||||
<p className="px-1 text-xs text-red-600">{errors.username}</p>
|
||||
)}
|
||||
{errors?.username && <p className="px-1 text-xs text-red-600">{errors.username}</p>}
|
||||
</div>
|
||||
)}
|
||||
<div className="grid gap-1 relative">
|
||||
@@ -168,7 +157,7 @@ export function UserAuthForm({
|
||||
id="email"
|
||||
name="email"
|
||||
required
|
||||
placeholder={isFirstRun ? 'email' : 'name@example.com'}
|
||||
placeholder={isFirstRun ? "email" : "name@example.com"}
|
||||
type="email"
|
||||
autoCapitalize="none"
|
||||
autoComplete="email"
|
||||
@@ -211,9 +200,7 @@ export function UserAuthForm({
|
||||
disabled={isLoading || isOauthLoading}
|
||||
className="pl-9"
|
||||
/>
|
||||
{errors?.passwordConfirm && (
|
||||
<p className="px-1 text-xs text-red-600">{errors.passwordConfirm}</p>
|
||||
)}
|
||||
{errors?.passwordConfirm && <p className="px-1 text-xs text-red-600">{errors.passwordConfirm}</p>}
|
||||
</div>
|
||||
)}
|
||||
<div className="sr-only">
|
||||
@@ -227,7 +214,7 @@ export function UserAuthForm({
|
||||
) : (
|
||||
<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>
|
||||
</div>
|
||||
</form>
|
||||
@@ -251,9 +238,9 @@ export function UserAuthForm({
|
||||
<button
|
||||
key={provider.name}
|
||||
type="button"
|
||||
className={cn(buttonVariants({ variant: 'outline' }), {
|
||||
'justify-self-center': !authMethods.emailPassword,
|
||||
'px-5': !authMethods.emailPassword,
|
||||
className={cn(buttonVariants({ variant: "outline" }), {
|
||||
"justify-self-center": !authMethods.emailPassword,
|
||||
"px-5": !authMethods.emailPassword,
|
||||
})}
|
||||
onClick={() => {
|
||||
setIsOauthLoading(true)
|
||||
@@ -266,9 +253,9 @@ export function UserAuthForm({
|
||||
if (!authWindow) {
|
||||
setIsOauthLoading(false)
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Please enable pop-ups for this site',
|
||||
variant: 'destructive',
|
||||
title: "Error",
|
||||
description: "Please enable pop-ups for this site",
|
||||
variant: "destructive",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -276,7 +263,7 @@ export function UserAuthForm({
|
||||
authWindow.location.href = url
|
||||
}
|
||||
}
|
||||
pb.collection('users')
|
||||
pb.collection("users")
|
||||
.authWithOAuth2(oAuthOpts)
|
||||
.then(() => {
|
||||
$authenticated.set(pb.authStore.isValid)
|
||||
@@ -296,7 +283,7 @@ export function UserAuthForm({
|
||||
src={`/static/${provider.name}.svg`}
|
||||
alt=""
|
||||
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
|
||||
<Dialog>
|
||||
<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="" />
|
||||
<span className="translate-y-[1px]">GitHub</span>
|
||||
</button>
|
||||
</DialogTrigger>
|
||||
<DialogContent style={{ maxWidth: 440, width: '90%' }}>
|
||||
<DialogContent style={{ maxWidth: 440, width: "90%" }}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>OAuth 2 / OIDC support</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="text-primary/70 text-[0.95em] contents">
|
||||
<p>{t('auth.openid_des')}</p>
|
||||
<p>{t("auth.openid_des")}</p>
|
||||
<p>
|
||||
{t('please_view_the')}{' '}
|
||||
{t("please_view_the")}{" "}
|
||||
<a
|
||||
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
|
||||
</a>{' '}
|
||||
{t('for_instructions')}
|
||||
</a>{" "}
|
||||
{t("for_instructions")}
|
||||
</p>
|
||||
</div>
|
||||
</DialogContent>
|
||||
@@ -341,7 +328,7 @@ export function UserAuthForm({
|
||||
href="/forgot-password"
|
||||
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>
|
||||
)}
|
||||
</div>
|
||||
|
@@ -1,27 +1,27 @@
|
||||
import { LoaderCircle, MailIcon, SendHorizonalIcon } from 'lucide-react'
|
||||
import { Input } from '../ui/input'
|
||||
import { Label } from '../ui/label'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { toast } from '../ui/use-toast'
|
||||
import { buttonVariants } from '../ui/button'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { pb } from '@/lib/stores'
|
||||
import { Dialog, DialogHeader } from '../ui/dialog'
|
||||
import { DialogContent, DialogTrigger, DialogTitle } from '../ui/dialog'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { LoaderCircle, MailIcon, SendHorizonalIcon } from "lucide-react"
|
||||
import { Input } from "../ui/input"
|
||||
import { Label } from "../ui/label"
|
||||
import { useCallback, useState } from "react"
|
||||
import { toast } from "../ui/use-toast"
|
||||
import { buttonVariants } from "../ui/button"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { pb } from "@/lib/stores"
|
||||
import { Dialog, DialogHeader } from "../ui/dialog"
|
||||
import { DialogContent, DialogTrigger, DialogTitle } from "../ui/dialog"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
const showLoginFaliedToast = () => {
|
||||
toast({
|
||||
title: 'Login attempt failed',
|
||||
description: 'Please check your credentials and try again',
|
||||
variant: 'destructive',
|
||||
title: "Login attempt failed",
|
||||
description: "Please check your credentials and try again",
|
||||
variant: "destructive",
|
||||
})
|
||||
}
|
||||
|
||||
export default function ForgotPassword() {
|
||||
const { t } = useTranslation()
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false)
|
||||
const [email, setEmail] = useState('')
|
||||
const [email, setEmail] = useState("")
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
@@ -29,16 +29,16 @@ export default function ForgotPassword() {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
// console.log(email)
|
||||
await pb.collection('users').requestPasswordReset(email)
|
||||
await pb.collection("users").requestPasswordReset(email)
|
||||
toast({
|
||||
title: 'Password reset request received',
|
||||
title: "Password reset request received",
|
||||
description: `Check ${email} for a reset link.`,
|
||||
})
|
||||
} catch (e) {
|
||||
showLoginFaliedToast()
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
setEmail('')
|
||||
setEmail("")
|
||||
}
|
||||
},
|
||||
[email]
|
||||
@@ -74,26 +74,22 @@ export default function ForgotPassword() {
|
||||
) : (
|
||||
<SendHorizonalIcon className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
{t('auth.reset_password')}
|
||||
{t("auth.reset_password")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<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>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-[33em]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('auth.command_line_instructions')}</DialogTitle>
|
||||
<DialogTitle>{t("auth.command_line_instructions")}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<p className="text-primary/70 text-[0.95em] leading-relaxed">
|
||||
{t('auth.command_1')}
|
||||
</p>
|
||||
<p className="text-primary/70 text-[0.95em] leading-relaxed">
|
||||
{t('auth.command_2')}
|
||||
</p>
|
||||
<p className="text-primary/70 text-[0.95em] leading-relaxed">{t("auth.command_1")}</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">
|
||||
beszel admin update youremail@example.com newpassword
|
||||
</code>
|
||||
|
@@ -1,12 +1,12 @@
|
||||
import { UserAuthForm } from '@/components/login/auth-form'
|
||||
import { Logo } from '../logo'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { pb } from '@/lib/stores'
|
||||
import { useStore } from '@nanostores/react'
|
||||
import ForgotPassword from './forgot-pass-form'
|
||||
import { $router } from '../router'
|
||||
import { AuthMethodsList } from 'pocketbase'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { UserAuthForm } from "@/components/login/auth-form"
|
||||
import { Logo } from "../logo"
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { pb } from "@/lib/stores"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import ForgotPassword from "./forgot-pass-form"
|
||||
import { $router } from "../router"
|
||||
import { AuthMethodsList } from "pocketbase"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
export default function () {
|
||||
const { t } = useTranslation()
|
||||
@@ -16,15 +16,15 @@ export default function () {
|
||||
const [authMethods, setAuthMethods] = useState<AuthMethodsList>()
|
||||
|
||||
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)
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
pb.collection('users')
|
||||
pb.collection("users")
|
||||
.listAuthMethods()
|
||||
.then((methods) => {
|
||||
setAuthMethods(methods)
|
||||
@@ -33,11 +33,11 @@ export default function () {
|
||||
|
||||
const subtitle = useMemo(() => {
|
||||
if (isFirstRun) {
|
||||
return t('auth.create')
|
||||
} else if (page?.path === '/forgot-password') {
|
||||
return t('auth.reset')
|
||||
return t("auth.create")
|
||||
} else if (page?.path === "/forgot-password") {
|
||||
return t("auth.reset")
|
||||
} else {
|
||||
return t('auth.login')
|
||||
return t("auth.login")
|
||||
}
|
||||
}, [isFirstRun, page])
|
||||
|
||||
@@ -47,7 +47,7 @@ export default function () {
|
||||
|
||||
return (
|
||||
<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">
|
||||
<h1 className="mb-3">
|
||||
<Logo className="h-7 fill-foreground mx-auto" />
|
||||
@@ -55,7 +55,7 @@ export default function () {
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground">{subtitle}</p>
|
||||
</div>
|
||||
{page?.path === '/forgot-password' ? (
|
||||
{page?.path === "/forgot-password" ? (
|
||||
<ForgotPassword />
|
||||
) : (
|
||||
<UserAuthForm isFirstRun={isFirstRun} authMethods={authMethods} />
|
||||
|
@@ -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 {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { useTheme } from '@/components/theme-provider'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
||||
import { useTheme } from "@/components/theme-provider"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
export function ModeToggle() {
|
||||
const { t } = useTranslation()
|
||||
@@ -17,24 +12,24 @@ export function ModeToggle() {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant={'ghost'} size="icon">
|
||||
<Button variant={"ghost"} size="icon">
|
||||
<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" />
|
||||
<span className="sr-only">{t('themes.toggle_theme')}</span>
|
||||
<span className="sr-only">{t("themes.toggle_theme")}</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem onClick={() => setTheme('light')}>
|
||||
<DropdownMenuItem onClick={() => setTheme("light")}>
|
||||
<SunIcon className="mr-2.5 h-4 w-4" />
|
||||
{t('themes.light')}
|
||||
{t("themes.light")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme('dark')}>
|
||||
<DropdownMenuItem onClick={() => setTheme("dark")}>
|
||||
<MoonStarIcon className="mr-2.5 h-4 w-4" />
|
||||
{t('themes.dark')}
|
||||
{t("themes.dark")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme('system')}>
|
||||
<DropdownMenuItem onClick={() => setTheme("system")}>
|
||||
<LaptopIcon className="mr-2.5 h-4 w-4" />
|
||||
{t('themes.system')}
|
||||
{t("themes.system")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { useState, lazy, Suspense } from 'react'
|
||||
import { Button, buttonVariants } from '@/components/ui/button'
|
||||
import { useState, lazy, Suspense } from "react"
|
||||
import { Button, buttonVariants } from "@/components/ui/button"
|
||||
import {
|
||||
DatabaseBackupIcon,
|
||||
LockKeyholeIcon,
|
||||
@@ -10,14 +10,13 @@ import {
|
||||
SettingsIcon,
|
||||
UserIcon,
|
||||
UsersIcon,
|
||||
} from 'lucide-react'
|
||||
import { TFunction } from 'i18next'
|
||||
import { Link } from './router'
|
||||
import { LangToggle } from './lang-toggle'
|
||||
import { ModeToggle } from './mode-toggle'
|
||||
import { Logo } from './logo'
|
||||
import { pb } from '@/lib/stores'
|
||||
import { cn, isReadOnlyUser, isAdmin } from '@/lib/utils'
|
||||
} from "lucide-react"
|
||||
import { Link } from "./router"
|
||||
import { LangToggle } from "./lang-toggle"
|
||||
import { ModeToggle } from "./mode-toggle"
|
||||
import { Logo } from "./logo"
|
||||
import { pb } from "@/lib/stores"
|
||||
import { cn, isReadOnlyUser, isAdmin } from "@/lib/utils"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
@@ -26,42 +25,41 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { AddSystemButton } from './add-system'
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
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 (
|
||||
<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" />
|
||||
</Link>
|
||||
|
||||
<SearchButton />
|
||||
|
||||
<div className={'flex ml-auto items-center'}>
|
||||
<div className={"flex ml-auto items-center"}>
|
||||
<LangToggle />
|
||||
<ModeToggle />
|
||||
<Link
|
||||
href="/settings/general"
|
||||
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]" />
|
||||
</Link>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
aria-label="User Actions"
|
||||
className={cn('', buttonVariants({ variant: 'ghost', size: 'icon' }))}
|
||||
>
|
||||
<button aria-label="User Actions" className={cn("", buttonVariants({ variant: "ghost", size: "icon" }))}>
|
||||
<UserIcon className="h-[1.2rem] w-[1.2rem]" />
|
||||
</button>
|
||||
</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>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
@@ -70,31 +68,31 @@ export default function Navbar(t: TFunction<'translation', undefined>) {
|
||||
<DropdownMenuItem asChild>
|
||||
<a href="/_/" target="_blank">
|
||||
<UsersIcon className="mr-2.5 h-4 w-4" />
|
||||
<span>{t('user_dm.users')}</span>
|
||||
<span>{t("user_dm.users")}</span>
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<a href="/_/#/collections?collectionId=2hz5ncl8tizk5nx" target="_blank">
|
||||
<ServerIcon className="mr-2.5 h-4 w-4" />
|
||||
<span>{t('systems')}</span>
|
||||
<span>{t("systems")}</span>
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<a href="/_/#/logs" target="_blank">
|
||||
<LogsIcon className="mr-2.5 h-4 w-4" />
|
||||
<span>{t('user_dm.logs')}</span>
|
||||
<span>{t("user_dm.logs")}</span>
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<a href="/_/#/settings/backups" target="_blank">
|
||||
<DatabaseBackupIcon className="mr-2.5 h-4 w-4" />
|
||||
<span>{t('user_dm.backups')}</span>
|
||||
<span>{t("user_dm.backups")}</span>
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<a href="/_/#/settings/auth-providers" target="_blank">
|
||||
<LockKeyholeIcon className="mr-2.5 h-4 w-4" />
|
||||
<span>{t('user_dm.auth_providers')}</span>
|
||||
<span>{t("user_dm.auth_providers")}</span>
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
@@ -103,7 +101,7 @@ export default function Navbar(t: TFunction<'translation', undefined>) {
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuItem onSelect={() => pb.authStore.clear()}>
|
||||
<LogOutIcon className="mr-2.5 h-4 w-4" />
|
||||
<span>{t('user_dm.log_out')}</span>
|
||||
<span>{t("user_dm.log_out")}</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
@@ -134,7 +132,7 @@ function SearchButton() {
|
||||
Search
|
||||
<span className="sr-only">Search</span>
|
||||
<span className="flex items-center ml-3.5">
|
||||
<Kbd>{isMac ? '⌘' : 'Ctrl'}</Kbd>
|
||||
<Kbd>{isMac ? "⌘" : "Ctrl"}</Kbd>
|
||||
<Kbd>K</Kbd>
|
||||
</span>
|
||||
</span>
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import { createRouter } from '@nanostores/router'
|
||||
import { createRouter } from "@nanostores/router"
|
||||
|
||||
export const $router = createRouter(
|
||||
{
|
||||
home: '/',
|
||||
server: '/system/:name',
|
||||
settings: '/settings/:name?',
|
||||
forgot_password: '/forgot-password',
|
||||
home: "/",
|
||||
server: "/system/:name",
|
||||
settings: "/settings/:name?",
|
||||
forgot_password: "/forgot-password",
|
||||
},
|
||||
{ links: false }
|
||||
)
|
||||
|
@@ -1,17 +1,17 @@
|
||||
import { Suspense, lazy, useEffect, useMemo, useState } from 'react'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card'
|
||||
import { $alerts, $hubVersion, $systems, pb } from '@/lib/stores'
|
||||
import { useStore } from '@nanostores/react'
|
||||
import { GithubIcon } from 'lucide-react'
|
||||
import { Separator } from '../ui/separator'
|
||||
import { alertInfo, updateRecordList, updateSystemList } from '@/lib/utils'
|
||||
import { AlertRecord, SystemRecord } from '@/types'
|
||||
import { Input } from '../ui/input'
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||
import { Link } from '../router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Suspense, lazy, useEffect, useMemo, useState } from "react"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"
|
||||
import { $alerts, $hubVersion, $systems, pb } from "@/lib/stores"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { GithubIcon } from "lucide-react"
|
||||
import { Separator } from "../ui/separator"
|
||||
import { alertInfo, updateRecordList, updateSystemList } from "@/lib/utils"
|
||||
import { AlertRecord, SystemRecord } from "@/types"
|
||||
import { Input } from "../ui/input"
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
|
||||
import { Link } from "../router"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
const SystemsTable = lazy(() => import('../systems-table/systems-table'))
|
||||
const SystemsTable = lazy(() => import("../systems-table/systems-table"))
|
||||
|
||||
export default function () {
|
||||
const { t } = useTranslation()
|
||||
@@ -35,21 +35,21 @@ export default function () {
|
||||
}, [alerts])
|
||||
|
||||
useEffect(() => {
|
||||
document.title = 'Dashboard / Beszel'
|
||||
document.title = "Dashboard / Beszel"
|
||||
|
||||
// make sure we have the latest list of systems
|
||||
updateSystemList()
|
||||
|
||||
// subscribe to real time updates for systems / alerts
|
||||
pb.collection<SystemRecord>('systems').subscribe('*', (e) => {
|
||||
pb.collection<SystemRecord>("systems").subscribe("*", (e) => {
|
||||
updateRecordList(e, $systems)
|
||||
})
|
||||
// todo: add toast if new triggered alert comes in
|
||||
pb.collection<AlertRecord>('alerts').subscribe('*', (e) => {
|
||||
pb.collection<AlertRecord>("alerts").subscribe("*", (e) => {
|
||||
updateRecordList(e, $alerts)
|
||||
})
|
||||
return () => {
|
||||
pb.collection('systems').unsubscribe('*')
|
||||
pb.collection("systems").unsubscribe("*")
|
||||
// pb.collection('alerts').unsubscribe('*')
|
||||
}
|
||||
}, [])
|
||||
@@ -61,7 +61,7 @@ export default function () {
|
||||
<Card className="mb-4">
|
||||
<CardHeader className="pb-4 px-2 sm:px-6 max-sm:pt-5 max-sm:pb-1">
|
||||
<div className="px-2 sm:px-1">
|
||||
<CardTitle>{t('home.active_alerts')}</CardTitle>
|
||||
<CardTitle>{t("home.active_alerts")}</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="max-sm:p-2">
|
||||
@@ -79,7 +79,7 @@ export default function () {
|
||||
{alert.sysname} {t(info.name)}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t('home.active_des', {
|
||||
{t("home.active_des", {
|
||||
value: alert.value,
|
||||
unit: info.unit,
|
||||
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">
|
||||
<div className="grid md:flex gap-3 w-full items-end">
|
||||
<div className="px-2 sm:px-1">
|
||||
<CardTitle className="mb-2.5">{t('all_systems')}</CardTitle>
|
||||
<CardDescription>{t('home.subtitle_1')}</CardDescription>
|
||||
<CardTitle className="mb-2.5">{t("all_systems")}</CardTitle>
|
||||
<CardDescription>{t("home.subtitle_1")}</CardDescription>
|
||||
</div>
|
||||
<Input
|
||||
placeholder={t('filter')}
|
||||
placeholder={t("filter")}
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
className="w-full md:w-56 lg:w-72 ml-auto px-4"
|
||||
/>
|
||||
|
@@ -1,21 +1,21 @@
|
||||
import { isAdmin } from '@/lib/utils'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { redirectPage } from '@nanostores/router'
|
||||
import { $router } from '@/components/router'
|
||||
import { AlertCircleIcon, FileSlidersIcon, LoaderCircleIcon } from 'lucide-react'
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||
import { pb } from '@/lib/stores'
|
||||
import { useState } from 'react'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { toast } from '@/components/ui/use-toast'
|
||||
import clsx from 'clsx'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { isAdmin } from "@/lib/utils"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { redirectPage } from "@nanostores/router"
|
||||
import { $router } from "@/components/router"
|
||||
import { AlertCircleIcon, FileSlidersIcon, LoaderCircleIcon } from "lucide-react"
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
|
||||
import { pb } from "@/lib/stores"
|
||||
import { useState } from "react"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { toast } from "@/components/ui/use-toast"
|
||||
import clsx from "clsx"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
export default function ConfigYaml() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [configContent, setConfigContent] = useState<string>('')
|
||||
const [configContent, setConfigContent] = useState<string>("")
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const ButtonIcon = isLoading ? LoaderCircleIcon : FileSlidersIcon
|
||||
@@ -23,13 +23,13 @@ export default function ConfigYaml() {
|
||||
async function fetchConfig() {
|
||||
try {
|
||||
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)
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
title: "Error",
|
||||
description: error.message,
|
||||
variant: 'destructive',
|
||||
variant: "destructive",
|
||||
})
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
@@ -37,33 +37,29 @@ export default function ConfigYaml() {
|
||||
}
|
||||
|
||||
if (!isAdmin()) {
|
||||
redirectPage($router, 'settings', { name: 'general' })
|
||||
redirectPage($router, "settings", { name: "general" })
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<h3 className="text-xl font-medium mb-2">{t('settings.yaml_config.title')}</h3>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
{t('settings.yaml_config.subtitle')}
|
||||
</p>
|
||||
<h3 className="text-xl font-medium mb-2">{t("settings.yaml_config.title")}</h3>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">{t("settings.yaml_config.subtitle")}</p>
|
||||
</div>
|
||||
<Separator className="my-4" />
|
||||
<div className="space-y-2">
|
||||
<div className="mb-4">
|
||||
<p className="text-sm text-muted-foreground leading-relaxed my-1">
|
||||
{t('settings.yaml_config.des_1')}{' '}
|
||||
<code className="bg-muted rounded-sm px-1 text-primary">config.yml</code> {t('settings.yaml_config.des_2')}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
{t('settings.yaml_config.des_3')}
|
||||
{t("settings.yaml_config.des_1")} <code className="bg-muted rounded-sm px-1 text-primary">config.yml</code>{" "}
|
||||
{t("settings.yaml_config.des_2")}
|
||||
</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">
|
||||
<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>
|
||||
<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>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
@@ -73,20 +69,15 @@ export default function ConfigYaml() {
|
||||
autoFocus
|
||||
defaultValue={configContent}
|
||||
spellCheck="false"
|
||||
rows={Math.min(25, configContent.split('\n').length)}
|
||||
rows={Math.min(25, configContent.split("\n").length)}
|
||||
className="font-mono whitespace-pre"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Separator className="my-5" />
|
||||
<Button
|
||||
type="button"
|
||||
className="mt-2 flex items-center gap-1"
|
||||
onClick={fetchConfig}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<ButtonIcon className={clsx('h-4 w-4 mr-0.5', isLoading && 'animate-spin')} />
|
||||
{t('settings.export_configuration')}
|
||||
<Button type="button" className="mt-2 flex items-center gap-1" onClick={fetchConfig} disabled={isLoading}>
|
||||
<ButtonIcon className={clsx("h-4 w-4 mr-0.5", isLoading && "animate-spin")} />
|
||||
{t("settings.export_configuration")}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
|
@@ -1,21 +1,15 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
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 { Button } from "@/components/ui/button"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
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 { useTranslation } from 'react-i18next'
|
||||
import languages from '../../../lib/languages.json'
|
||||
import { useTranslation } from "react-i18next"
|
||||
import languages from "../../../lib/languages.json"
|
||||
|
||||
export default function SettingsProfilePage({ userSettings }: { userSettings: UserSettings }) {
|
||||
const { t, i18n } = useTranslation()
|
||||
@@ -38,10 +32,8 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<h3 className="text-xl font-medium mb-2">{t('settings.general.title')}</h3>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
{t('settings.general.subtitle')}
|
||||
</p>
|
||||
<h3 className="text-xl font-medium mb-2">{t("settings.general.title")}</h3>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">{t("settings.general.subtitle")}</p>
|
||||
</div>
|
||||
<Separator className="my-4" />
|
||||
<form onSubmit={handleSubmit} className="space-y-5">
|
||||
@@ -49,18 +41,18 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
|
||||
<div className="mb-4">
|
||||
<h3 className="mb-1 text-lg font-medium flex items-center gap-2">
|
||||
<LanguagesIcon className="h-4 w-4" />
|
||||
{t('settings.general.language.title')}
|
||||
{t("settings.general.language.title")}
|
||||
</h3>
|
||||
<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">
|
||||
Crowdin
|
||||
</a>{' '}
|
||||
{t('settings.general.language.subtitle_2')}
|
||||
</a>{" "}
|
||||
{t("settings.general.language.subtitle_2")}
|
||||
</p>
|
||||
</div>
|
||||
<Label className="block" htmlFor="lang">
|
||||
{t('settings.general.language.preferred_language')}
|
||||
{t("settings.general.language.preferred_language")}
|
||||
</Label>
|
||||
<Select value={i18n.language} onValueChange={(lang: string) => i18n.changeLanguage(lang)}>
|
||||
<SelectTrigger id="lang">
|
||||
@@ -78,21 +70,15 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
|
||||
<Separator />
|
||||
<div className="space-y-2">
|
||||
<div className="mb-4">
|
||||
<h3 className="mb-1 text-lg font-medium">
|
||||
{t('settings.general.chart_options.title')}
|
||||
</h3>
|
||||
<h3 className="mb-1 text-lg font-medium">{t("settings.general.chart_options.title")}</h3>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
{t('settings.general.chart_options.subtitle')}
|
||||
{t("settings.general.chart_options.subtitle")}
|
||||
</p>
|
||||
</div>
|
||||
<Label className="block" htmlFor="chartTime">
|
||||
{t('settings.general.chart_options.default_time_period')}
|
||||
{t("settings.general.chart_options.default_time_period")}
|
||||
</Label>
|
||||
<Select
|
||||
name="chartTime"
|
||||
key={userSettings.chartTime}
|
||||
defaultValue={userSettings.chartTime}
|
||||
>
|
||||
<Select name="chartTime" key={userSettings.chartTime} defaultValue={userSettings.chartTime}>
|
||||
<SelectTrigger id="chartTime">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
@@ -105,21 +91,13 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<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>
|
||||
</div>
|
||||
<Separator />
|
||||
<Button
|
||||
type="submit"
|
||||
className="flex items-center gap-1.5 disabled:opacity-100"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<LoaderCircleIcon className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<SaveIcon className="h-4 w-4" />
|
||||
)}
|
||||
{t('settings.save_settings')}
|
||||
<Button type="submit" className="flex items-center gap-1.5 disabled:opacity-100" disabled={isLoading}>
|
||||
{isLoading ? <LoaderCircleIcon className="h-4 w-4 animate-spin" /> : <SaveIcon className="h-4 w-4" />}
|
||||
{t("settings.save_settings")}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
|
@@ -1,28 +1,28 @@
|
||||
import { useEffect } from 'react'
|
||||
import { Separator } from '../../ui/separator'
|
||||
import { SidebarNav } from './sidebar-nav.tsx'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card.tsx'
|
||||
import { useStore } from '@nanostores/react'
|
||||
import { $router } from '@/components/router.tsx'
|
||||
import { redirectPage } from '@nanostores/router'
|
||||
import { BellIcon, FileSlidersIcon, SettingsIcon } from 'lucide-react'
|
||||
import { $userSettings, pb } from '@/lib/stores.ts'
|
||||
import { toast } from '@/components/ui/use-toast.ts'
|
||||
import { UserSettings } from '@/types.js'
|
||||
import General from './general.tsx'
|
||||
import Notifications from './notifications.tsx'
|
||||
import ConfigYaml from './config-yaml.tsx'
|
||||
import { isAdmin } from '@/lib/utils.ts'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useEffect } from "react"
|
||||
import { Separator } from "../../ui/separator"
|
||||
import { SidebarNav } from "./sidebar-nav.tsx"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card.tsx"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { $router } from "@/components/router.tsx"
|
||||
import { redirectPage } from "@nanostores/router"
|
||||
import { BellIcon, FileSlidersIcon, SettingsIcon } from "lucide-react"
|
||||
import { $userSettings, pb } from "@/lib/stores.ts"
|
||||
import { toast } from "@/components/ui/use-toast.ts"
|
||||
import { UserSettings } from "@/types.js"
|
||||
import General from "./general.tsx"
|
||||
import Notifications from "./notifications.tsx"
|
||||
import ConfigYaml from "./config-yaml.tsx"
|
||||
import { isAdmin } from "@/lib/utils.ts"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
export async function saveSettings(newSettings: Partial<UserSettings>) {
|
||||
try {
|
||||
// get fresh copy of settings
|
||||
const req = await pb.collection('user_settings').getFirstListItem('', {
|
||||
fields: 'id,settings',
|
||||
const req = await pb.collection("user_settings").getFirstListItem("", {
|
||||
fields: "id,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: {
|
||||
...req.settings,
|
||||
...newSettings,
|
||||
@@ -30,15 +30,15 @@ export async function saveSettings(newSettings: Partial<UserSettings>) {
|
||||
})
|
||||
$userSettings.set(updatedSettings.settings)
|
||||
toast({
|
||||
title: 'Settings saved',
|
||||
description: 'Your user settings have been updated.',
|
||||
title: "Settings saved",
|
||||
description: "Your user settings have been updated.",
|
||||
})
|
||||
} catch (e) {
|
||||
// console.error('update settings', e)
|
||||
toast({
|
||||
title: 'Failed to save settings',
|
||||
description: 'Check logs for more details.',
|
||||
variant: 'destructive',
|
||||
title: "Failed to save settings",
|
||||
description: "Check logs for more details.",
|
||||
variant: "destructive",
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -48,21 +48,21 @@ export default function SettingsLayout() {
|
||||
|
||||
const sidebarNavItems = [
|
||||
{
|
||||
title: t('settings.general.title'),
|
||||
href: '/settings/general',
|
||||
title: t("settings.general.title"),
|
||||
href: "/settings/general",
|
||||
icon: SettingsIcon,
|
||||
},
|
||||
{
|
||||
title: t('settings.notifications.title'),
|
||||
href: '/settings/notifications',
|
||||
title: t("settings.notifications.title"),
|
||||
href: "/settings/notifications",
|
||||
icon: BellIcon,
|
||||
},
|
||||
]
|
||||
|
||||
if (isAdmin()) {
|
||||
sidebarNavItems.push({
|
||||
title: t('settings.yaml_config.short_title'),
|
||||
href: '/settings/config',
|
||||
title: t("settings.yaml_config.short_title"),
|
||||
href: "/settings/config",
|
||||
icon: FileSlidersIcon,
|
||||
})
|
||||
}
|
||||
@@ -70,18 +70,18 @@ export default function SettingsLayout() {
|
||||
const page = useStore($router)
|
||||
|
||||
useEffect(() => {
|
||||
document.title = 'Settings / Beszel'
|
||||
document.title = "Settings / Beszel"
|
||||
// redirect to account page if no page is specified
|
||||
if (page?.path === '/settings') {
|
||||
redirectPage($router, 'settings', { name: 'general' })
|
||||
if (page?.path === "/settings") {
|
||||
redirectPage($router, "settings", { name: "general" })
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Card className="pt-5 px-4 pb-8 sm:pt-6 sm:px-7">
|
||||
<CardHeader className="p-0">
|
||||
<CardTitle className="mb-1">{t('settings.settings')}</CardTitle>
|
||||
<CardDescription>{t('settings.subtitle')}</CardDescription>
|
||||
<CardTitle className="mb-1">{t("settings.settings")}</CardTitle>
|
||||
<CardDescription>{t("settings.subtitle")}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<Separator className="hidden md:block my-5" />
|
||||
@@ -91,7 +91,7 @@ export default function SettingsLayout() {
|
||||
</aside>
|
||||
<div className="flex-1">
|
||||
{/* @ts-ignore */}
|
||||
<SettingsContent name={page?.params?.name ?? 'general'} />
|
||||
<SettingsContent name={page?.params?.name ?? "general"} />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -103,11 +103,11 @@ function SettingsContent({ name }: { name: string }) {
|
||||
const userSettings = useStore($userSettings)
|
||||
|
||||
switch (name) {
|
||||
case 'general':
|
||||
case "general":
|
||||
return <General userSettings={userSettings} />
|
||||
case 'notifications':
|
||||
case "notifications":
|
||||
return <Notifications userSettings={userSettings} />
|
||||
case 'config':
|
||||
case "config":
|
||||
return <ConfigYaml />
|
||||
}
|
||||
}
|
||||
|
@@ -1,18 +1,18 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { pb } from '@/lib/stores'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { BellIcon, LoaderCircleIcon, PlusIcon, SaveIcon, Trash2Icon } from 'lucide-react'
|
||||
import { ChangeEventHandler, useEffect, useState } from 'react'
|
||||
import { toast } from '@/components/ui/use-toast'
|
||||
import { InputTags } from '@/components/ui/input-tags'
|
||||
import { UserSettings } from '@/types'
|
||||
import { saveSettings } from './layout'
|
||||
import * as v from 'valibot'
|
||||
import { isAdmin } from '@/lib/utils'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { pb } from "@/lib/stores"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { Card } from "@/components/ui/card"
|
||||
import { BellIcon, LoaderCircleIcon, PlusIcon, SaveIcon, Trash2Icon } from "lucide-react"
|
||||
import { ChangeEventHandler, useEffect, useState } from "react"
|
||||
import { toast } from "@/components/ui/use-toast"
|
||||
import { InputTags } from "@/components/ui/input-tags"
|
||||
import { UserSettings } from "@/types"
|
||||
import { saveSettings } from "./layout"
|
||||
import * as v from "valibot"
|
||||
import { isAdmin } from "@/lib/utils"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
interface ShoutrrrUrlCardProps {
|
||||
url: string
|
||||
@@ -39,10 +39,10 @@ const SettingsNotificationsPage = ({ userSettings }: { userSettings: UserSetting
|
||||
}, [userSettings])
|
||||
|
||||
function addWebhook() {
|
||||
setWebhooks([...webhooks, ''])
|
||||
setWebhooks([...webhooks, ""])
|
||||
// focus on the new input
|
||||
queueMicrotask(() => {
|
||||
const inputs = document.querySelectorAll('#webhooks input') as NodeListOf<HTMLInputElement>
|
||||
const inputs = document.querySelectorAll("#webhooks input") as NodeListOf<HTMLInputElement>
|
||||
inputs[inputs.length - 1]?.focus()
|
||||
})
|
||||
}
|
||||
@@ -61,9 +61,9 @@ const SettingsNotificationsPage = ({ userSettings }: { userSettings: UserSetting
|
||||
await saveSettings(parsedData)
|
||||
} catch (e: any) {
|
||||
toast({
|
||||
title: 'Failed to save settings',
|
||||
title: "Failed to save settings",
|
||||
description: e.message,
|
||||
variant: 'destructive',
|
||||
variant: "destructive",
|
||||
})
|
||||
}
|
||||
setIsLoading(false)
|
||||
@@ -72,63 +72,51 @@ const SettingsNotificationsPage = ({ userSettings }: { userSettings: UserSetting
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<h3 className="text-xl font-medium mb-2">{t('settings.notifications.title')}</h3>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
{t('settings.notifications.subtitle_1')}
|
||||
</p>
|
||||
<h3 className="text-xl font-medium mb-2">{t("settings.notifications.title")}</h3>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">{t("settings.notifications.subtitle_1")}</p>
|
||||
<p className="text-sm text-muted-foreground mt-1.5 leading-relaxed">
|
||||
{t('settings.notifications.subtitle_2')}{' '}
|
||||
<BellIcon className="inline h-4 w-4" /> {t('settings.notifications.subtitle_3')}
|
||||
{t("settings.notifications.subtitle_2")} <BellIcon className="inline h-4 w-4" />{" "}
|
||||
{t("settings.notifications.subtitle_3")}
|
||||
</p>
|
||||
</div>
|
||||
<Separator className="my-4" />
|
||||
<div className="space-y-5">
|
||||
<div className="space-y-2">
|
||||
<div className="mb-4">
|
||||
<h3 className="mb-1 text-lg font-medium">
|
||||
{t('settings.notifications.email.title')}
|
||||
</h3>
|
||||
<h3 className="mb-1 text-lg font-medium">{t("settings.notifications.email.title")}</h3>
|
||||
{isAdmin() && (
|
||||
<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">
|
||||
{t('settings.notifications.email.configure_an_SMTP_server')}
|
||||
</a>{' '}
|
||||
{t('settings.notifications.email.to_ensure_alerts_are_delivered')}{' '}
|
||||
{t("settings.notifications.email.configure_an_SMTP_server")}
|
||||
</a>{" "}
|
||||
{t("settings.notifications.email.to_ensure_alerts_are_delivered")}{" "}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Label className="block" htmlFor="email">
|
||||
{t('settings.notifications.email.to_email_s')}
|
||||
{t("settings.notifications.email.to_email_s")}
|
||||
</Label>
|
||||
<InputTags
|
||||
value={emails}
|
||||
onChange={setEmails}
|
||||
placeholder={t('settings.notifications.email.enter_email_address')}
|
||||
placeholder={t("settings.notifications.email.enter_email_address")}
|
||||
className="w-full"
|
||||
type="email"
|
||||
id="email"
|
||||
/>
|
||||
<p className="text-[0.8rem] text-muted-foreground">
|
||||
{t('settings.notifications.email.des')}
|
||||
</p>
|
||||
<p className="text-[0.8rem] text-muted-foreground">{t("settings.notifications.email.des")}</p>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<h3 className="mb-1 text-lg font-medium">
|
||||
{t('settings.notifications.webhook_push.title')}
|
||||
</h3>
|
||||
<h3 className="mb-1 text-lg font-medium">{t("settings.notifications.webhook_push.title")}</h3>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
{t('settings.notifications.webhook_push.des_1')}{' '}
|
||||
<a
|
||||
href="https://containrrr.dev/shoutrrr/services/overview/"
|
||||
target="_blank"
|
||||
className="link"
|
||||
>
|
||||
{t("settings.notifications.webhook_push.des_1")}{" "}
|
||||
<a href="https://containrrr.dev/shoutrrr/services/overview/" target="_blank" className="link">
|
||||
Shoutrrr
|
||||
</a>{' '}
|
||||
{t('settings.notifications.webhook_push.des_2')}
|
||||
</a>{" "}
|
||||
{t("settings.notifications.webhook_push.des_2")}
|
||||
</p>
|
||||
</div>
|
||||
{webhooks.length > 0 && (
|
||||
@@ -137,9 +125,7 @@ const SettingsNotificationsPage = ({ userSettings }: { userSettings: UserSetting
|
||||
<ShoutrrrUrlCard
|
||||
key={index}
|
||||
url={webhook}
|
||||
onUrlChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
updateWebhook(index, e.target.value)
|
||||
}
|
||||
onUrlChange={(e: React.ChangeEvent<HTMLInputElement>) => updateWebhook(index, e.target.value)}
|
||||
onRemove={() => removeWebhook(index)}
|
||||
/>
|
||||
))}
|
||||
@@ -153,7 +139,7 @@ const SettingsNotificationsPage = ({ userSettings }: { userSettings: UserSetting
|
||||
onClick={addWebhook}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4 -ml-0.5" />
|
||||
{t('settings.notifications.webhook_push.add_url')}
|
||||
{t("settings.notifications.webhook_push.add_url")}
|
||||
</Button>
|
||||
</div>
|
||||
<Separator />
|
||||
@@ -163,12 +149,8 @@ const SettingsNotificationsPage = ({ userSettings }: { userSettings: UserSetting
|
||||
onClick={updateSettings}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<LoaderCircleIcon className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<SaveIcon className="h-4 w-4" />
|
||||
)}
|
||||
{t('settings.save_settings')}
|
||||
{isLoading ? <LoaderCircleIcon className="h-4 w-4 animate-spin" /> : <SaveIcon className="h-4 w-4" />}
|
||||
{t("settings.save_settings")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -180,17 +162,17 @@ const ShoutrrrUrlCard = ({ url, onUrlChange, onRemove }: ShoutrrrUrlCardProps) =
|
||||
|
||||
const sendTestNotification = async () => {
|
||||
setIsLoading(true)
|
||||
const res = await pb.send('/api/beszel/send-test-notification', { url })
|
||||
if ('err' in res && !res.err) {
|
||||
const res = await pb.send("/api/beszel/send-test-notification", { url })
|
||||
if ("err" in res && !res.err) {
|
||||
toast({
|
||||
title: 'Test notification sent',
|
||||
description: 'Check your notification service',
|
||||
title: "Test notification sent",
|
||||
description: "Check your notification service",
|
||||
})
|
||||
} else {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: res.err ?? 'Failed to send test notification',
|
||||
variant: 'destructive',
|
||||
title: "Error",
|
||||
description: res.err ?? "Failed to send test notification",
|
||||
variant: "destructive",
|
||||
})
|
||||
}
|
||||
setIsLoading(false)
|
||||
@@ -211,7 +193,7 @@ const ShoutrrrUrlCard = ({ url, onUrlChange, onRemove }: ShoutrrrUrlCardProps) =
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="w-20 md:w-28"
|
||||
disabled={isLoading || url === ''}
|
||||
disabled={isLoading || url === ""}
|
||||
onClick={sendTestNotification}
|
||||
>
|
||||
{isLoading ? (
|
||||
@@ -222,14 +204,7 @@ const ShoutrrrUrlCard = ({ url, onUrlChange, onRemove }: ShoutrrrUrlCardProps) =
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="shrink-0"
|
||||
aria-label="Delete"
|
||||
onClick={onRemove}
|
||||
>
|
||||
<Button type="button" variant="outline" size="icon" className="shrink-0" aria-label="Delete" onClick={onRemove}>
|
||||
<Trash2Icon className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
@@ -1,16 +1,10 @@
|
||||
import React from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { buttonVariants } from '../../ui/button'
|
||||
import { $router, Link, navigate } from '../../router'
|
||||
import { useStore } from '@nanostores/react'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "../../ui/button"
|
||||
import { $router, Link, navigate } from "../../router"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
|
||||
interface SidebarNavProps extends React.HTMLAttributes<HTMLElement> {
|
||||
items: {
|
||||
@@ -46,16 +40,16 @@ export function SidebarNav({ className, items, ...props }: SidebarNavProps) {
|
||||
</div>
|
||||
|
||||
{/* 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) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={cn(
|
||||
buttonVariants({ variant: 'ghost' }),
|
||||
'flex items-center gap-3',
|
||||
page?.path === item.href ? 'bg-muted hover:bg-muted' : 'hover:bg-muted/50',
|
||||
'justify-start'
|
||||
buttonVariants({ variant: "ghost" }),
|
||||
"flex items-center gap-3",
|
||||
page?.path === item.href ? "bg-muted hover:bg-muted" : "hover:bg-muted/50",
|
||||
"justify-start"
|
||||
)}
|
||||
>
|
||||
{item.icon && <item.icon className="h-4 w-4" />}
|
||||
|
@@ -1,40 +1,34 @@
|
||||
import { $systems, pb, $chartTime, $containerFilter, $userSettings } from '@/lib/stores'
|
||||
import {
|
||||
ChartData,
|
||||
ChartTimes,
|
||||
ContainerStatsRecord,
|
||||
SystemRecord,
|
||||
SystemStatsRecord,
|
||||
} from '@/types'
|
||||
import React, { lazy, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Card, CardHeader, CardTitle, CardDescription } from '../ui/card'
|
||||
import { useStore } from '@nanostores/react'
|
||||
import Spinner from '../spinner'
|
||||
import { ClockArrowUp, CpuIcon, GlobeIcon, LayoutGridIcon, MonitorIcon, XIcon } from 'lucide-react'
|
||||
import ChartTimeSelect from '../charts/chart-time-select'
|
||||
import { chartTimeData, cn, getPbTimestamp, useLocalStorage } from '@/lib/utils'
|
||||
import { Separator } from '../ui/separator'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip'
|
||||
import { Button } from '../ui/button'
|
||||
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'
|
||||
import { $systems, pb, $chartTime, $containerFilter, $userSettings } from "@/lib/stores"
|
||||
import { ChartData, ChartTimes, ContainerStatsRecord, SystemRecord, SystemStatsRecord } from "@/types"
|
||||
import React, { lazy, useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { Card, CardHeader, CardTitle, CardDescription } from "../ui/card"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import Spinner from "../spinner"
|
||||
import { ClockArrowUp, CpuIcon, GlobeIcon, LayoutGridIcon, MonitorIcon, XIcon } from "lucide-react"
|
||||
import ChartTimeSelect from "../charts/chart-time-select"
|
||||
import { chartTimeData, cn, getPbTimestamp, useLocalStorage } from "@/lib/utils"
|
||||
import { Separator } from "../ui/separator"
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip"
|
||||
import { Button } from "../ui/button"
|
||||
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 ContainerChart = lazy(() => import('../charts/container-chart'))
|
||||
const MemChart = lazy(() => import('../charts/mem-chart'))
|
||||
const DiskChart = lazy(() => import('../charts/disk-chart'))
|
||||
const SwapChart = lazy(() => import('../charts/swap-chart'))
|
||||
const TemperatureChart = lazy(() => import('../charts/temperature-chart'))
|
||||
const AreaChartDefault = lazy(() => import("../charts/area-chart"))
|
||||
const ContainerChart = lazy(() => import("../charts/container-chart"))
|
||||
const MemChart = lazy(() => import("../charts/mem-chart"))
|
||||
const DiskChart = lazy(() => import("../charts/disk-chart"))
|
||||
const SwapChart = lazy(() => import("../charts/swap-chart"))
|
||||
const TemperatureChart = lazy(() => import("../charts/temperature-chart"))
|
||||
|
||||
const cache = new Map<string, any>()
|
||||
|
||||
// create ticks and domain for charts
|
||||
function getTimeData(chartTime: ChartTimes, lastCreated: number) {
|
||||
const cached = cache.get('td')
|
||||
const cached = cache.get("td")
|
||||
if (cached && cached.chartTime === chartTime) {
|
||||
if (!lastCreated || cached.time >= lastCreated) {
|
||||
return cached.data
|
||||
@@ -43,14 +37,12 @@ function getTimeData(chartTime: ChartTimes, lastCreated: number) {
|
||||
|
||||
const now = new Date()
|
||||
const startTime = chartTimeData[chartTime].getOffset(now)
|
||||
const ticks = timeTicks(startTime, now, chartTimeData[chartTime].ticks ?? 12).map((date) =>
|
||||
date.getTime()
|
||||
)
|
||||
const ticks = timeTicks(startTime, now, chartTimeData[chartTime].ticks ?? 12).map((date) => date.getTime())
|
||||
const data = {
|
||||
ticks,
|
||||
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
|
||||
}
|
||||
|
||||
@@ -79,20 +71,16 @@ function addEmptyValues<T extends SystemStatsRecord | ContainerStatsRecord>(
|
||||
return modifiedRecords
|
||||
}
|
||||
|
||||
async function getStats<T>(
|
||||
collection: string,
|
||||
system: SystemRecord,
|
||||
chartTime: ChartTimes
|
||||
): Promise<T[]> {
|
||||
async function getStats<T>(collection: string, system: SystemRecord, chartTime: ChartTimes): Promise<T[]> {
|
||||
const lastCached = cache.get(`${system.id}_${chartTime}_${collection}`)?.at(-1)?.created as number
|
||||
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,
|
||||
created: getPbTimestamp(chartTime, lastCached ? new Date(lastCached + 1000) : undefined),
|
||||
type: chartTimeData[chartTime].type,
|
||||
}),
|
||||
fields: 'created,stats',
|
||||
sort: 'created',
|
||||
fields: "created,stats",
|
||||
sort: "created",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -105,15 +93,15 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
const cpuMaxStore = useState(false)
|
||||
const bandwidthMaxStore = 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 [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 [containerFilterBar, setContainerFilterBar] = useState(null as null | JSX.Element)
|
||||
const [bottomSpacing, setBottomSpacing] = useState(0)
|
||||
const [chartLoading, setChartLoading] = useState(false)
|
||||
const isLongerChart = chartTime !== '1h'
|
||||
const isLongerChart = chartTime !== "1h"
|
||||
|
||||
useEffect(() => {
|
||||
document.title = `${name} / Beszel`
|
||||
@@ -123,7 +111,7 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
setSystemStats([])
|
||||
setContainerData([])
|
||||
setContainerFilterBar(null)
|
||||
$containerFilter.set('')
|
||||
$containerFilter.set("")
|
||||
cpuMaxStore[1](false)
|
||||
bandwidthMaxStore[1](false)
|
||||
diskIoMaxStore[1](false)
|
||||
@@ -153,11 +141,11 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
if (!system.id) {
|
||||
return
|
||||
}
|
||||
pb.collection<SystemRecord>('systems').subscribe(system.id, (e) => {
|
||||
pb.collection<SystemRecord>("systems").subscribe(system.id, (e) => {
|
||||
setSystem(e.record)
|
||||
})
|
||||
return () => {
|
||||
pb.collection('systems').unsubscribe(system.id)
|
||||
pb.collection("systems").unsubscribe(system.id)
|
||||
}
|
||||
}, [system.id])
|
||||
|
||||
@@ -182,8 +170,8 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
// loading: true
|
||||
setChartLoading(true)
|
||||
Promise.allSettled([
|
||||
getStats<SystemStatsRecord>('system_stats', system, chartTime),
|
||||
getStats<ContainerStatsRecord>('container_stats', system, chartTime),
|
||||
getStats<SystemStatsRecord>("system_stats", system, chartTime),
|
||||
getStats<ContainerStatsRecord>("container_stats", system, chartTime),
|
||||
]).then(([systemStats, containerStats]) => {
|
||||
// loading: false
|
||||
setChartLoading(false)
|
||||
@@ -192,10 +180,8 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
// make new system stats
|
||||
const ss_cache_key = `${system.id}_${chartTime}_system_stats`
|
||||
let systemData = (cache.get(ss_cache_key) || []) as SystemStatsRecord[]
|
||||
if (systemStats.status === 'fulfilled' && systemStats.value.length) {
|
||||
systemData = systemData.concat(
|
||||
addEmptyValues(systemData, systemStats.value, expectedInterval)
|
||||
)
|
||||
if (systemStats.status === "fulfilled" && systemStats.value.length) {
|
||||
systemData = systemData.concat(addEmptyValues(systemData, systemStats.value, expectedInterval))
|
||||
if (systemData.length > 120) {
|
||||
systemData = systemData.slice(-100)
|
||||
}
|
||||
@@ -205,10 +191,8 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
// make new container stats
|
||||
const cs_cache_key = `${system.id}_${chartTime}_container_stats`
|
||||
let containerData = (cache.get(cs_cache_key) || []) as ContainerStatsRecord[]
|
||||
if (containerStats.status === 'fulfilled' && containerStats.value.length) {
|
||||
containerData = containerData.concat(
|
||||
addEmptyValues(containerData, containerStats.value, expectedInterval)
|
||||
)
|
||||
if (containerStats.status === "fulfilled" && containerStats.value.length) {
|
||||
containerData = containerData.concat(addEmptyValues(containerData, containerStats.value, expectedInterval))
|
||||
if (containerData.length > 120) {
|
||||
containerData = containerData.slice(-100)
|
||||
}
|
||||
@@ -225,7 +209,7 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
|
||||
// make container stats for charts
|
||||
const makeContainerData = useCallback((containers: ContainerStatsRecord[]) => {
|
||||
const containerData = [] as ChartData['containerData']
|
||||
const containerData = [] as ChartData["containerData"]
|
||||
for (let { created, stats } of containers) {
|
||||
if (!created) {
|
||||
// @ts-ignore add null value for gaps
|
||||
@@ -234,7 +218,7 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
}
|
||||
created = new Date(created).getTime()
|
||||
// @ts-ignore not dealing with this rn
|
||||
let containerStats: ChartData['containerData'][0] = { created }
|
||||
let containerStats: ChartData["containerData"][0] = { created }
|
||||
for (let container of stats) {
|
||||
containerStats[container.n] = container
|
||||
}
|
||||
@@ -251,7 +235,7 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
let uptime: number | string = system.info.u
|
||||
if (system.info.u < 172800) {
|
||||
const hours = Math.trunc(uptime / 3600)
|
||||
uptime = `${hours} hour${hours == 1 ? '' : 's'}`
|
||||
uptime = `${hours} hour${hours == 1 ? "" : "s"}`
|
||||
} else {
|
||||
uptime = `${Math.trunc(system.info?.u / 86400)} days`
|
||||
}
|
||||
@@ -260,14 +244,14 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
{
|
||||
value: system.info.h,
|
||||
Icon: MonitorIcon,
|
||||
label: 'Hostname',
|
||||
label: "Hostname",
|
||||
// hide if hostname is same as host or name
|
||||
hide: system.info.h === system.host || system.info.h === system.name,
|
||||
},
|
||||
{ value: uptime, Icon: ClockArrowUp, label: 'Uptime' },
|
||||
{ value: system.info.k, Icon: TuxIcon, label: 'Kernel' },
|
||||
{ value: uptime, Icon: ClockArrowUp, label: "Uptime" },
|
||||
{ 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,
|
||||
hide: !system.info.m,
|
||||
},
|
||||
@@ -286,7 +270,7 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
return
|
||||
}
|
||||
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 chartRect = netCardRef.current.getBoundingClientRect()
|
||||
const distanceToBottom = wrapperRect.bottom - chartRect.bottom
|
||||
@@ -298,7 +282,7 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
}
|
||||
|
||||
// if no data, show empty state
|
||||
const dataEmpty = !chartLoading && chartData.systemStats.length === 0;
|
||||
const dataEmpty = !chartLoading && chartData.systemStats.length === 0
|
||||
|
||||
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>
|
||||
<div className="flex flex-wrap items-center gap-3 gap-y-2 text-sm opacity-90">
|
||||
<div className="capitalize flex gap-2 items-center">
|
||||
<span className={cn('relative flex h-3 w-3')}>
|
||||
{system.status === 'up' && (
|
||||
<span className={cn("relative flex h-3 w-3")}>
|
||||
{system.status === "up" && (
|
||||
<span
|
||||
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
|
||||
className={cn('relative inline-flex rounded-full h-3 w-3', {
|
||||
'bg-green-500': system.status === 'up',
|
||||
'bg-red-500': system.status === 'down',
|
||||
'bg-primary/40': system.status === 'paused',
|
||||
'bg-yellow-500': system.status === 'pending',
|
||||
className={cn("relative inline-flex rounded-full h-3 w-3", {
|
||||
"bg-green-500": system.status === "up",
|
||||
"bg-red-500": system.status === "down",
|
||||
"bg-primary/40": system.status === "paused",
|
||||
"bg-yellow-500": system.status === "pending",
|
||||
})}
|
||||
></span>
|
||||
</span>
|
||||
@@ -361,7 +345,7 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
aria-label={t('monitor.toggle_grid')}
|
||||
aria-label={t("monitor.toggle_grid")}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="hidden lg:flex p-0 text-primary"
|
||||
@@ -374,7 +358,7 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t('monitor.toggle_grid')}</TooltipContent>
|
||||
<TooltipContent>{t("monitor.toggle_grid")}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
@@ -386,24 +370,21 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
<ChartCard
|
||||
empty={dataEmpty}
|
||||
grid={grid}
|
||||
title={t('monitor.total_cpu_usage')}
|
||||
description={`${cpuMaxStore[0] && isLongerChart ? t('monitor.max_1_min') : t('monitor.average') } ${t('monitor.cpu_des')}`}
|
||||
title={t("monitor.total_cpu_usage")}
|
||||
description={`${cpuMaxStore[0] && isLongerChart ? t("monitor.max_1_min") : t("monitor.average")} ${t(
|
||||
"monitor.cpu_des"
|
||||
)}`}
|
||||
cornerEl={isLongerChart ? <SelectAvgMax store={cpuMaxStore} /> : null}
|
||||
>
|
||||
<AreaChartDefault
|
||||
chartData={chartData}
|
||||
chartName="CPU Usage"
|
||||
maxToggled={cpuMaxStore[0]}
|
||||
unit="%"
|
||||
/>
|
||||
<AreaChartDefault chartData={chartData} chartName="CPU Usage" maxToggled={cpuMaxStore[0]} unit="%" />
|
||||
</ChartCard>
|
||||
|
||||
{containerFilterBar && (
|
||||
<ChartCard
|
||||
empty={dataEmpty}
|
||||
grid={grid}
|
||||
title={t('monitor.docker_cpu_usage')}
|
||||
description={t('monitor.docker_cpu_des')}
|
||||
title={t("monitor.docker_cpu_usage")}
|
||||
description={t("monitor.docker_cpu_des")}
|
||||
cornerEl={containerFilterBar}
|
||||
>
|
||||
<ContainerChart chartData={chartData} dataKey="c" chartName="cpu" />
|
||||
@@ -413,8 +394,8 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
<ChartCard
|
||||
empty={dataEmpty}
|
||||
grid={grid}
|
||||
title={t('monitor.total_memory_usage')}
|
||||
description={t('monitor.memory_des')}
|
||||
title={t("monitor.total_memory_usage")}
|
||||
description={t("monitor.memory_des")}
|
||||
>
|
||||
<MemChart chartData={chartData} />
|
||||
</ChartCard>
|
||||
@@ -423,15 +404,15 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
<ChartCard
|
||||
empty={dataEmpty}
|
||||
grid={grid}
|
||||
title={t('monitor.docker_memory_usage')}
|
||||
description={t('monitor.docker_memory_des')}
|
||||
title={t("monitor.docker_memory_usage")}
|
||||
description={t("monitor.docker_memory_des")}
|
||||
cornerEl={containerFilterBar}
|
||||
>
|
||||
<ContainerChart chartData={chartData} chartName="mem" dataKey="m" unit=" MB" />
|
||||
</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
|
||||
chartData={chartData}
|
||||
dataKey="stats.du"
|
||||
@@ -442,42 +423,34 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
<ChartCard
|
||||
empty={dataEmpty}
|
||||
grid={grid}
|
||||
title={t('monitor.disk_io')}
|
||||
description={t('monitor.disk_io_des')}
|
||||
title={t("monitor.disk_io")}
|
||||
description={t("monitor.disk_io_des")}
|
||||
cornerEl={isLongerChart ? <SelectAvgMax store={diskIoMaxStore} /> : null}
|
||||
>
|
||||
<AreaChartDefault
|
||||
chartData={chartData}
|
||||
maxToggled={diskIoMaxStore[0]}
|
||||
chartName="dio"
|
||||
/>
|
||||
<AreaChartDefault chartData={chartData} maxToggled={diskIoMaxStore[0]} chartName="dio" />
|
||||
</ChartCard>
|
||||
|
||||
<ChartCard
|
||||
empty={dataEmpty}
|
||||
grid={grid}
|
||||
title={t('monitor.bandwidth')}
|
||||
title={t("monitor.bandwidth")}
|
||||
cornerEl={isLongerChart ? <SelectAvgMax store={bandwidthMaxStore} /> : null}
|
||||
description={t('monitor.bandwidth_des')}
|
||||
description={t("monitor.bandwidth_des")}
|
||||
>
|
||||
<AreaChartDefault
|
||||
chartData={chartData}
|
||||
maxToggled={bandwidthMaxStore[0]}
|
||||
chartName="bw"
|
||||
/>
|
||||
<AreaChartDefault chartData={chartData} maxToggled={bandwidthMaxStore[0]} chartName="bw" />
|
||||
</ChartCard>
|
||||
|
||||
{containerFilterBar && containerData.length > 0 && (
|
||||
<div
|
||||
ref={netCardRef}
|
||||
className={cn({
|
||||
'col-span-full': !grid,
|
||||
"col-span-full": !grid,
|
||||
})}
|
||||
>
|
||||
<ChartCard
|
||||
empty={dataEmpty}
|
||||
title={t('monitor.docker_network_io')}
|
||||
description={t('monitor.docker_network_io_des')}
|
||||
title={t("monitor.docker_network_io")}
|
||||
description={t("monitor.docker_network_io_des")}
|
||||
cornerEl={containerFilterBar}
|
||||
>
|
||||
{/* @ts-ignore */}
|
||||
@@ -487,13 +460,23 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
)}
|
||||
|
||||
{(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} />
|
||||
</ChartCard>
|
||||
)}
|
||||
|
||||
{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} />
|
||||
</ChartCard>
|
||||
)}
|
||||
@@ -508,8 +491,8 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
<ChartCard
|
||||
empty={dataEmpty}
|
||||
grid={grid}
|
||||
title={`${extraFsName} ${t('monitor.usage')}`}
|
||||
description={`${t('monitor.disk_usage_of')} ${extraFsName}`}
|
||||
title={`${extraFsName} ${t("monitor.usage")}`}
|
||||
description={`${t("monitor.disk_usage_of")} ${extraFsName}`}
|
||||
>
|
||||
<DiskChart
|
||||
chartData={chartData}
|
||||
@@ -521,7 +504,7 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
empty={dataEmpty}
|
||||
grid={grid}
|
||||
title={`${extraFsName} I/O`}
|
||||
description={`${t('monitor.throughput_of')} ${extraFsName}`}
|
||||
description={`${t("monitor.throughput_of")} ${extraFsName}`}
|
||||
cornerEl={isLongerChart ? <SelectAvgMax store={diskIoMaxStore} /> : null}
|
||||
>
|
||||
<AreaChartDefault
|
||||
@@ -554,12 +537,7 @@ function ContainerFilterBar() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Input
|
||||
placeholder={t('filter')}
|
||||
className="pl-4 pr-8"
|
||||
value={containerFilter}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Input placeholder={t("filter")} className="pl-4 pr-8" value={containerFilter} onChange={handleChange} />
|
||||
{containerFilter && (
|
||||
<Button
|
||||
type="button"
|
||||
@@ -567,7 +545,7 @@ function ContainerFilterBar() {
|
||||
size="icon"
|
||||
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"
|
||||
onClick={() => $containerFilter.set('')}
|
||||
onClick={() => $containerFilter.set("")}
|
||||
>
|
||||
<XIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -576,28 +554,24 @@ function ContainerFilterBar() {
|
||||
)
|
||||
}
|
||||
|
||||
function SelectAvgMax({
|
||||
store,
|
||||
}: {
|
||||
store: [boolean, React.Dispatch<React.SetStateAction<boolean>>]
|
||||
}) {
|
||||
function SelectAvgMax({ store }: { store: [boolean, React.Dispatch<React.SetStateAction<boolean>>] }) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [max, setMax] = store
|
||||
const Icon = max ? ChartMax : ChartAverage
|
||||
|
||||
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">
|
||||
<Icon className="h-4 w-4 absolute left-4 top-1/2 -translate-y-1/2 opacity-85" />
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem key="avg" value="avg">
|
||||
{t('monitor.average')}
|
||||
{t("monitor.average")}
|
||||
</SelectItem>
|
||||
<SelectItem key="max" value="max">
|
||||
{t('monitor.max_1_min')}
|
||||
{t("monitor.max_1_min")}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
@@ -616,24 +590,17 @@ function ChartCard({
|
||||
description: string
|
||||
children: React.ReactNode
|
||||
grid?: boolean
|
||||
empty?: boolean,
|
||||
empty?: boolean
|
||||
cornerEl?: JSX.Element | null
|
||||
}) {
|
||||
const { isIntersecting, ref } = useIntersectionObserver()
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={cn('pb-2 sm:pb-4 odd:last-of-type:col-span-full', { 'col-span-full': !grid })}
|
||||
ref={ref}
|
||||
>
|
||||
<Card 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">
|
||||
<CardTitle className="text-xl sm:text-2xl">{title}</CardTitle>
|
||||
<CardDescription>{description}</CardDescription>
|
||||
{cornerEl && (
|
||||
<div className="relative py-1 block sm:w-44 sm:absolute sm:top-2.5 sm:right-3.5">
|
||||
{cornerEl}
|
||||
</div>
|
||||
)}
|
||||
{cornerEl && <div className="relative py-1 block sm:w-44 sm:absolute sm:top-2.5 sm:right-3.5">{cornerEl}</div>}
|
||||
</CardHeader>
|
||||
<div className="pl-0 w-[calc(100%-1.6em)] h-52 relative">
|
||||
{<Spinner empty={empty} />}
|
||||
|
@@ -1,12 +1,12 @@
|
||||
import { LoaderCircleIcon } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { LoaderCircleIcon } from "lucide-react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
export default function (props: { empty?: boolean }) {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full absolute inset-0">
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
@@ -9,18 +9,11 @@ import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
Column,
|
||||
} from '@tanstack/react-table'
|
||||
} from "@tanstack/react-table"
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table'
|
||||
import { 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 {
|
||||
DropdownMenu,
|
||||
@@ -28,7 +21,7 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
|
||||
import {
|
||||
AlertDialog,
|
||||
@@ -40,9 +33,9 @@ import {
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
} from "@/components/ui/alert-dialog"
|
||||
|
||||
import { SystemRecord } from '@/types'
|
||||
import { SystemRecord } from "@/types"
|
||||
import {
|
||||
MoreHorizontalIcon,
|
||||
ArrowUpDownIcon,
|
||||
@@ -55,15 +48,15 @@ import {
|
||||
HardDriveIcon,
|
||||
ServerIcon,
|
||||
CpuIcon,
|
||||
} from 'lucide-react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { $hubVersion, $systems, pb } from '@/lib/stores'
|
||||
import { useStore } from '@nanostores/react'
|
||||
import { cn, copyToClipboard, decimalString, isReadOnlyUser } from '@/lib/utils'
|
||||
import AlertsButton from '../alerts/alert-button'
|
||||
import { navigate } from '../router'
|
||||
import { EthernetIcon } from '../ui/icons'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
} from "lucide-react"
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { $hubVersion, $systems, pb } from "@/lib/stores"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { cn, copyToClipboard, decimalString, isReadOnlyUser } from "@/lib/utils"
|
||||
import AlertsButton from "../alerts/alert-button"
|
||||
import { navigate } from "../router"
|
||||
import { EthernetIcon } from "../ui/icons"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
function CellFormatter(info: CellContext<SystemRecord, unknown>) {
|
||||
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={cn(
|
||||
'absolute inset-0 w-full h-full origin-left',
|
||||
(val < 65 && 'bg-green-500') || (val < 90 && 'bg-yellow-500') || 'bg-red-600'
|
||||
"absolute inset-0 w-full h-full origin-left",
|
||||
(val < 65 && "bg-green-500") || (val < 90 && "bg-yellow-500") || "bg-red-600"
|
||||
)}
|
||||
style={{ transform: `scalex(${val}%)` }}
|
||||
></span>
|
||||
@@ -83,18 +76,9 @@ function CellFormatter(info: CellContext<SystemRecord, unknown>) {
|
||||
)
|
||||
}
|
||||
|
||||
function sortableHeader(
|
||||
column: Column<SystemRecord, unknown>,
|
||||
name: string,
|
||||
Icon: any,
|
||||
hideSortIcon = false
|
||||
) {
|
||||
function sortableHeader(column: Column<SystemRecord, unknown>, name: string, Icon: any, hideSortIcon = false) {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-9 px-3"
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
||||
>
|
||||
<Button variant="ghost" className="h-9 px-3" onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}>
|
||||
<Icon className="mr-2 h-4 w-4" />
|
||||
{name}
|
||||
{!hideSortIcon && <ArrowUpDownIcon className="ml-2 h-4 w-4" />}
|
||||
@@ -112,7 +96,7 @@ export default function SystemsTable({ filter }: { filter?: string }) {
|
||||
|
||||
useEffect(() => {
|
||||
if (filter !== undefined) {
|
||||
table.getColumn('name')?.setFilterValue(filter)
|
||||
table.getColumn("name")?.setFilterValue(filter)
|
||||
}
|
||||
}, [filter])
|
||||
|
||||
@@ -122,23 +106,23 @@ export default function SystemsTable({ filter }: { filter?: string }) {
|
||||
// size: 200,
|
||||
size: 200,
|
||||
minSize: 0,
|
||||
accessorKey: 'name',
|
||||
accessorKey: "name",
|
||||
cell: (info) => {
|
||||
const { status } = info.row.original
|
||||
return (
|
||||
<span className="flex gap-0.5 items-center text-base md:pr-5">
|
||||
<span
|
||||
className={cn('w-2 h-2 left-0 rounded-full', {
|
||||
'bg-green-500': status === 'up',
|
||||
'bg-red-500': status === 'down',
|
||||
'bg-primary/40': status === 'paused',
|
||||
'bg-yellow-500': status === 'pending',
|
||||
className={cn("w-2 h-2 left-0 rounded-full", {
|
||||
"bg-green-500": status === "up",
|
||||
"bg-red-500": status === "down",
|
||||
"bg-primary/40": status === "paused",
|
||||
"bg-yellow-500": status === "pending",
|
||||
})}
|
||||
style={{ marginBottom: '-1px' }}
|
||||
style={{ marginBottom: "-1px" }}
|
||||
></span>
|
||||
<Button
|
||||
data-nolink
|
||||
variant={'ghost'}
|
||||
variant={"ghost"}
|
||||
className="text-primary/90 h-7 px-1.5 gap-1.5"
|
||||
onClick={() => copyToClipboard(info.getValue() as string)}
|
||||
>
|
||||
@@ -148,46 +132,44 @@ export default function SystemsTable({ filter }: { filter?: string }) {
|
||||
</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,
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
id: 'n',
|
||||
id: "n",
|
||||
invertSorting: true,
|
||||
size: 115,
|
||||
header: ({ column }) => sortableHeader(column, t('systems_table.net'), EthernetIcon),
|
||||
header: ({ column }) => sortableHeader(column, t("systems_table.net"), EthernetIcon),
|
||||
cell: (info) => {
|
||||
const val = info.getValue() as number
|
||||
return (
|
||||
<span className="tabular-nums whitespace-nowrap pl-1">
|
||||
{decimalString(val, val >= 100 ? 1 : 2)} MB/s
|
||||
</span>
|
||||
<span className="tabular-nums whitespace-nowrap pl-1">{decimalString(val, val >= 100 ? 1 : 2)} MB/s</span>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'info.v',
|
||||
accessorKey: "info.v",
|
||||
invertSorting: true,
|
||||
size: 50,
|
||||
header: ({ column }) => sortableHeader(column, t('systems_table.agent'), WifiIcon, true),
|
||||
header: ({ column }) => sortableHeader(column, t("systems_table.agent"), WifiIcon, true),
|
||||
cell: (info) => {
|
||||
const version = info.getValue() as string
|
||||
if (!version || !hubVersion) {
|
||||
@@ -196,11 +178,8 @@ export default function SystemsTable({ filter }: { filter?: string }) {
|
||||
return (
|
||||
<span className="flex gap-2 items-center md:pr-5 tabular-nums pl-1">
|
||||
<span
|
||||
className={cn(
|
||||
'w-2 h-2 left-0 rounded-full',
|
||||
version === hubVersion ? 'bg-green-500' : 'bg-yellow-500'
|
||||
)}
|
||||
style={{ marginBottom: '-1px' }}
|
||||
className={cn("w-2 h-2 left-0 rounded-full", version === hubVersion ? "bg-green-500" : "bg-yellow-500")}
|
||||
style={{ marginBottom: "-1px" }}
|
||||
></span>
|
||||
<span>{info.getValue() as string}</span>
|
||||
</span>
|
||||
@@ -208,70 +187,71 @@ export default function SystemsTable({ filter }: { filter?: string }) {
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
id: "actions",
|
||||
size: 120,
|
||||
// minSize: 0,
|
||||
cell: ({ row }) => {
|
||||
const { id, name, status, host } = row.original
|
||||
return (
|
||||
<div className={'flex justify-end items-center gap-1'}>
|
||||
<div className={"flex justify-end items-center gap-1"}>
|
||||
<AlertsButton system={row.original} />
|
||||
<AlertDialog>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size={'icon'} data-nolink>
|
||||
<span className="sr-only">{t('systems_table.open_menu')}</span>
|
||||
<Button variant="ghost" size={"icon"} data-nolink>
|
||||
<span className="sr-only">{t("systems_table.open_menu")}</span>
|
||||
<MoreHorizontalIcon className="w-5" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
className={cn(isReadOnlyUser() && 'hidden')}
|
||||
className={cn(isReadOnlyUser() && "hidden")}
|
||||
onClick={() => {
|
||||
pb.collection('systems').update(id, {
|
||||
status: status === 'paused' ? 'pending' : 'paused',
|
||||
pb.collection("systems").update(id, {
|
||||
status: status === "paused" ? "pending" : "paused",
|
||||
})
|
||||
}}
|
||||
>
|
||||
{status === 'paused' ? (
|
||||
{status === "paused" ? (
|
||||
<>
|
||||
<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" />
|
||||
{t('systems_table.pause')}
|
||||
{t("systems_table.pause")}
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => copyToClipboard(host)}>
|
||||
<CopyIcon className="mr-2.5 h-4 w-4" />
|
||||
{t('systems_table.copy_host')}
|
||||
{t("systems_table.copy_host")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator className={cn(isReadOnlyUser() && 'hidden')} />
|
||||
<DropdownMenuSeparator className={cn(isReadOnlyUser() && "hidden")} />
|
||||
<AlertDialogTrigger asChild>
|
||||
<DropdownMenuItem className={cn(isReadOnlyUser() && 'hidden')}>
|
||||
<DropdownMenuItem className={cn(isReadOnlyUser() && "hidden")}>
|
||||
<Trash2Icon className="mr-2.5 h-4 w-4" />
|
||||
{t('systems_table.delete')}
|
||||
{t("systems_table.delete")}
|
||||
</DropdownMenuItem>
|
||||
</AlertDialogTrigger>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{t('systems_table.delete_confirm', { name })}</AlertDialogTitle>
|
||||
<AlertDialogTitle>{t("systems_table.delete_confirm", { name })}</AlertDialogTitle>
|
||||
<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>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>{t('cancel')}</AlertDialogCancel>
|
||||
<AlertDialogCancel>{t("cancel")}</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
className={cn(buttonVariants({ variant: 'destructive' }))}
|
||||
onClick={() => pb.collection('systems').delete(id)}
|
||||
className={cn(buttonVariants({ variant: "destructive" }))}
|
||||
onClick={() => pb.collection("systems").delete(id)}
|
||||
>
|
||||
{t('continue')}
|
||||
{t("continue")}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
@@ -311,9 +291,7 @@ export default function SystemsTable({ filter }: { filter?: string }) {
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead className="px-2" key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</TableHead>
|
||||
)
|
||||
})}
|
||||
@@ -325,13 +303,13 @@ export default function SystemsTable({ filter }: { filter?: string }) {
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.original.id}
|
||||
data-state={row.getIsSelected() && 'selected'}
|
||||
className={cn('cursor-pointer transition-opacity', {
|
||||
'opacity-50': row.original.status === 'paused',
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
className={cn("cursor-pointer transition-opacity", {
|
||||
"opacity-50": row.original.status === "paused",
|
||||
})}
|
||||
onClick={(e) => {
|
||||
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)}`)
|
||||
}
|
||||
}}
|
||||
@@ -340,12 +318,9 @@ export default function SystemsTable({ filter }: { filter?: string }) {
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
style={{
|
||||
width:
|
||||
cell.column.getSize() === Number.MAX_SAFE_INTEGER
|
||||
? 'auto'
|
||||
: cell.column.getSize(),
|
||||
width: 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())}
|
||||
</TableCell>
|
||||
@@ -355,7 +330,7 @@ export default function SystemsTable({ filter }: { filter?: string }) {
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
{t('systems_table.no_systems_found')}
|
||||
{t("systems_table.no_systems_found")}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
|
@@ -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 = {
|
||||
children: React.ReactNode
|
||||
@@ -14,7 +14,7 @@ type ThemeProviderState = {
|
||||
}
|
||||
|
||||
const initialState: ThemeProviderState = {
|
||||
theme: 'system',
|
||||
theme: "system",
|
||||
setTheme: () => null,
|
||||
}
|
||||
|
||||
@@ -22,23 +22,19 @@ const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
defaultTheme = 'system',
|
||||
storageKey = 'ui-theme',
|
||||
defaultTheme = "system",
|
||||
storageKey = "ui-theme",
|
||||
...props
|
||||
}: ThemeProviderProps) {
|
||||
const [theme, setTheme] = useState<Theme>(
|
||||
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
|
||||
)
|
||||
const [theme, setTheme] = useState<Theme>(() => (localStorage.getItem(storageKey) as Theme) || defaultTheme)
|
||||
|
||||
useEffect(() => {
|
||||
const root = window.document.documentElement
|
||||
|
||||
root.classList.remove('light', 'dark')
|
||||
root.classList.remove("light", "dark")
|
||||
|
||||
if (theme === 'system') {
|
||||
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
? 'dark'
|
||||
: 'light'
|
||||
if (theme === "system") {
|
||||
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
|
||||
|
||||
root.classList.add(systemTheme)
|
||||
return
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import * as React from 'react'
|
||||
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'
|
||||
import * as React from "react"
|
||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { buttonVariants } from '@/components/ui/button'
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "@/components/ui/button"
|
||||
|
||||
const AlertDialog = AlertDialogPrimitive.Root
|
||||
|
||||
@@ -16,7 +16,7 @@ const AlertDialogOverlay = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Overlay
|
||||
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
|
||||
)}
|
||||
{...props}
|
||||
@@ -34,7 +34,7 @@ const AlertDialogContent = React.forwardRef<
|
||||
<AlertDialogPrimitive.Content
|
||||
ref={ref}
|
||||
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
|
||||
)}
|
||||
{...props}
|
||||
@@ -44,27 +44,20 @@ const AlertDialogContent = React.forwardRef<
|
||||
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
|
||||
|
||||
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>) => (
|
||||
<div
|
||||
className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)}
|
||||
{...props}
|
||||
/>
|
||||
<div 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<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn('text-lg font-semibold', className)}
|
||||
{...props}
|
||||
/>
|
||||
<AlertDialogPrimitive.Title ref={ref} className={cn("text-lg font-semibold", className)} {...props} />
|
||||
))
|
||||
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
|
||||
|
||||
@@ -72,11 +65,7 @@ const AlertDialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn('text-sm text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
<AlertDialogPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
|
||||
))
|
||||
AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName
|
||||
|
||||
@@ -94,7 +83,7 @@ const AlertDialogCancel = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Cancel
|
||||
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}
|
||||
/>
|
||||
))
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react'
|
||||
import * as React from "react"
|
||||
// import { cva, type VariantProps } from 'class-variance-authority'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
// 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",
|
||||
@@ -29,31 +29,26 @@ const Alert = React.forwardRef<
|
||||
ref={ref}
|
||||
role="alert"
|
||||
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
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Alert.displayName = 'Alert'
|
||||
Alert.displayName = "Alert"
|
||||
|
||||
const AlertTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<h5
|
||||
ref={ref}
|
||||
className={cn('mb-1 -mt-0.5 font-medium leading-tight tracking-tight', className)}
|
||||
{...props}
|
||||
/>
|
||||
<h5 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<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn('text-sm [&_p]:leading-relaxed', className)} {...props} />
|
||||
))
|
||||
AlertDescription.displayName = 'AlertDescription'
|
||||
const AlertDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("text-sm [&_p]:leading-relaxed", className)} {...props} />
|
||||
)
|
||||
)
|
||||
AlertDescription.displayName = "AlertDescription"
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription }
|
||||
|
@@ -4,33 +4,26 @@ import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
||||
secondary:
|
||||
"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",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
||||
secondary: "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",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
)
|
||||
return <div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
|
@@ -1,31 +1,31 @@
|
||||
import * as React from 'react'
|
||||
import { Slot } from '@radix-ui/react-slot'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
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: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
||||
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
||||
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: 'h-10 px-4 py-2',
|
||||
sm: 'h-9 rounded-md px-3',
|
||||
lg: 'h-11 rounded-md px-8',
|
||||
icon: 'h-10 w-10',
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -38,12 +38,10 @@ export interface ButtonProps
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : 'button'
|
||||
return (
|
||||
<Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
|
||||
)
|
||||
const Comp = asChild ? Slot : "button"
|
||||
return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
|
||||
}
|
||||
)
|
||||
Button.displayName = 'Button'
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants }
|
||||
|
@@ -2,78 +2,40 @@ import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
const Card = React.forwardRef<HTMLDivElement, 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"
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
|
||||
)
|
||||
)
|
||||
CardHeader.displayName = "CardHeader"
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-2xl font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<h3 ref={ref} className={cn("text-2xl font-semibold leading-none tracking-tight", className)} {...props} />
|
||||
)
|
||||
)
|
||||
CardTitle.displayName = "CardTitle"
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
|
||||
)
|
||||
)
|
||||
CardDescription.displayName = "CardDescription"
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
))
|
||||
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
)
|
||||
CardContent.displayName = "CardContent"
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => <div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
|
||||
)
|
||||
CardFooter.displayName = "CardFooter"
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||
|
@@ -1,20 +1,17 @@
|
||||
import * as React from 'react'
|
||||
import * as RechartsPrimitive from 'recharts'
|
||||
import * as React from "react"
|
||||
import * as RechartsPrimitive from "recharts"
|
||||
|
||||
import { chartTimeData, cn } from '@/lib/utils'
|
||||
import { ChartData } from '@/types'
|
||||
import { chartTimeData, cn } from "@/lib/utils"
|
||||
import { ChartData } from "@/types"
|
||||
|
||||
// Format: { THEME_NAME: CSS_SELECTOR }
|
||||
const THEMES = { light: '', dark: '.dark' } as const
|
||||
const THEMES = { light: "", dark: ".dark" } as const
|
||||
|
||||
export type ChartConfig = {
|
||||
[k in string]: {
|
||||
label?: React.ReactNode
|
||||
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 = {
|
||||
@@ -35,13 +32,13 @@ export type ChartConfig = {
|
||||
|
||||
const ChartContainer = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<'div'> & {
|
||||
React.ComponentProps<"div"> & {
|
||||
// config: ChartConfig
|
||||
children: React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>['children']
|
||||
children: React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>["children"]
|
||||
}
|
||||
>(({ id, className, children, ...props }, ref) => {
|
||||
const uniqueId = React.useId()
|
||||
const chartId = `chart-${id || uniqueId.replace(/:/g, '')}`
|
||||
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
|
||||
|
||||
return (
|
||||
//<ChartContext.Provider value={{ config }}>
|
||||
@@ -60,7 +57,7 @@ const ChartContainer = React.forwardRef<
|
||||
//</ChartContext.Provider>
|
||||
)
|
||||
})
|
||||
ChartContainer.displayName = 'Chart'
|
||||
ChartContainer.displayName = "Chart"
|
||||
|
||||
// const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
||||
// const colorConfig = Object.entries(config).filter(([_, config]) => config.theme || config.color)
|
||||
@@ -94,9 +91,9 @@ const ChartTooltip = RechartsPrimitive.Tooltip
|
||||
const ChartTooltipContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
||||
React.ComponentProps<'div'> & {
|
||||
React.ComponentProps<"div"> & {
|
||||
hideLabel?: boolean
|
||||
indicator?: 'line' | 'dot' | 'dashed'
|
||||
indicator?: "line" | "dot" | "dashed"
|
||||
nameKey?: string
|
||||
labelKey?: string
|
||||
unit?: string
|
||||
@@ -109,7 +106,7 @@ const ChartTooltipContent = React.forwardRef<
|
||||
active,
|
||||
payload,
|
||||
className,
|
||||
indicator = 'line',
|
||||
indicator = "line",
|
||||
hideLabel = false,
|
||||
label,
|
||||
labelFormatter,
|
||||
@@ -144,21 +141,19 @@ const ChartTooltipContent = React.forwardRef<
|
||||
}
|
||||
|
||||
const [item] = payload
|
||||
const key = `${labelKey || item.name || 'value'}`
|
||||
const key = `${labelKey || item.name || "value"}`
|
||||
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) {
|
||||
return (
|
||||
<div className={cn('font-medium', labelClassName)}>{labelFormatter(value, payload)}</div>
|
||||
)
|
||||
return <div className={cn("font-medium", labelClassName)}>{labelFormatter(value, payload)}</div>
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
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])
|
||||
|
||||
if (!active || !payload?.length) {
|
||||
@@ -172,14 +167,14 @@ const ChartTooltipContent = React.forwardRef<
|
||||
<div
|
||||
ref={ref}
|
||||
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
|
||||
)}
|
||||
>
|
||||
{!nestLabel ? tooltipLabel : null}
|
||||
<div className="grid gap-1.5">
|
||||
{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 indicatorColor = color || item.payload.fill || item.color
|
||||
|
||||
@@ -187,8 +182,8 @@ const ChartTooltipContent = React.forwardRef<
|
||||
<div
|
||||
key={item?.name || item.dataKey}
|
||||
className={cn(
|
||||
'flex w-full items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground',
|
||||
indicator === 'dot' && 'items-center'
|
||||
"flex w-full items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
|
||||
indicator === "dot" && "items-center"
|
||||
)}
|
||||
>
|
||||
{formatter && item?.value !== undefined && item.name ? (
|
||||
@@ -199,41 +194,35 @@ const ChartTooltipContent = React.forwardRef<
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
<div
|
||||
className={cn(
|
||||
'shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]',
|
||||
{
|
||||
'h-2.5 w-2.5': indicator === 'dot',
|
||||
'w-1': indicator === 'line',
|
||||
'w-0 border-[1.5px] border-dashed bg-transparent':
|
||||
indicator === 'dashed',
|
||||
'my-0.5': nestLabel && indicator === 'dashed',
|
||||
}
|
||||
)}
|
||||
className={cn("shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]", {
|
||||
"h-2.5 w-2.5": indicator === "dot",
|
||||
"w-1": indicator === "line",
|
||||
"w-0 border-[1.5px] border-dashed bg-transparent": indicator === "dashed",
|
||||
"my-0.5": nestLabel && indicator === "dashed",
|
||||
})}
|
||||
style={
|
||||
{
|
||||
'--color-bg': indicatorColor,
|
||||
'--color-border': indicatorColor,
|
||||
"--color-bg": indicatorColor,
|
||||
"--color-border": indicatorColor,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-1 justify-between leading-none gap-2',
|
||||
nestLabel ? 'items-end' : 'items-center'
|
||||
"flex flex-1 justify-between leading-none gap-2",
|
||||
nestLabel ? "items-end" : "items-center"
|
||||
)}
|
||||
>
|
||||
<div className="grid gap-1.5">
|
||||
{nestLabel ? tooltipLabel : null}
|
||||
<span className="text-muted-foreground">
|
||||
{itemConfig?.label || item.name}
|
||||
</span>
|
||||
<span className="text-muted-foreground">{itemConfig?.label || item.name}</span>
|
||||
</div>
|
||||
{item.value !== undefined && (
|
||||
<span className="font-medium tabular-nums text-foreground">
|
||||
{content && typeof content === 'function'
|
||||
{content && typeof content === "function"
|
||||
? content(item, key)
|
||||
: item.value.toLocaleString() + (unit ? unit : '')}
|
||||
: item.value.toLocaleString() + (unit ? unit : "")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -247,18 +236,18 @@ const ChartTooltipContent = React.forwardRef<
|
||||
)
|
||||
}
|
||||
)
|
||||
ChartTooltipContent.displayName = 'ChartTooltip'
|
||||
ChartTooltipContent.displayName = "ChartTooltip"
|
||||
|
||||
const ChartLegend = RechartsPrimitive.Legend
|
||||
|
||||
const ChartLegendContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<'div'> &
|
||||
Pick<RechartsPrimitive.LegendProps, 'payload' | 'verticalAlign'> & {
|
||||
React.ComponentProps<"div"> &
|
||||
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
||||
hideIcon?: boolean
|
||||
nameKey?: string
|
||||
}
|
||||
>(({ className, payload, verticalAlign = 'bottom' }, ref) => {
|
||||
>(({ className, payload, verticalAlign = "bottom" }, ref) => {
|
||||
// const { config } = useChart()
|
||||
|
||||
if (!payload?.length) {
|
||||
@@ -269,8 +258,8 @@ const ChartLegendContent = React.forwardRef<
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex items-center justify-center gap-4 gap-y-1 flex-wrap',
|
||||
verticalAlign === 'top' ? 'pb-3' : 'pt-3',
|
||||
"flex items-center justify-center gap-4 gap-y-1 flex-wrap",
|
||||
verticalAlign === "top" ? "pb-3" : "pt-3",
|
||||
className
|
||||
)}
|
||||
>
|
||||
@@ -283,7 +272,7 @@ const ChartLegendContent = React.forwardRef<
|
||||
key={item.value}
|
||||
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 text-muted-foreground'
|
||||
"flex items-center gap-1.5 text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{/* {itemConfig?.icon && !hideIcon ? (
|
||||
@@ -304,27 +293,27 @@ const ChartLegendContent = React.forwardRef<
|
||||
</div>
|
||||
)
|
||||
})
|
||||
ChartLegendContent.displayName = 'ChartLegend'
|
||||
ChartLegendContent.displayName = "ChartLegend"
|
||||
|
||||
// Helper to extract item config from a payload.
|
||||
function getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string) {
|
||||
if (typeof payload !== 'object' || payload === null) {
|
||||
if (typeof payload !== "object" || payload === null) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const payloadPayload =
|
||||
'payload' in payload && typeof payload.payload === 'object' && payload.payload !== null
|
||||
"payload" in payload && typeof payload.payload === "object" && payload.payload !== null
|
||||
? payload.payload
|
||||
: undefined
|
||||
|
||||
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
|
||||
} else if (
|
||||
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
|
||||
}
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import * as React from 'react'
|
||||
import * as CheckboxPrimitive from '@radix-ui/react-checkbox'
|
||||
import { Check } from 'lucide-react'
|
||||
import * as React from "react"
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||
import { Check } from "lucide-react"
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Checkbox = React.forwardRef<
|
||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||
@@ -11,12 +11,12 @@ const Checkbox = React.forwardRef<
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
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
|
||||
)}
|
||||
{...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" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import * as React from 'react'
|
||||
import { DialogTitle, type DialogProps } from '@radix-ui/react-dialog'
|
||||
import { Command as CommandPrimitive } from 'cmdk'
|
||||
import { Search } from 'lucide-react'
|
||||
import * as React from "react"
|
||||
import { DialogTitle, type DialogProps } from "@radix-ui/react-dialog"
|
||||
import { Command as CommandPrimitive } from "cmdk"
|
||||
import { Search } from "lucide-react"
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Dialog, DialogContent } from '@/components/ui/dialog'
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Dialog, DialogContent } from "@/components/ui/dialog"
|
||||
|
||||
const Command = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive>,
|
||||
@@ -12,10 +12,7 @@ const Command = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex h-full w-full flex-col overflow-hidden bg-popover text-popover-foreground',
|
||||
className
|
||||
)}
|
||||
className={cn("flex h-full w-full flex-col overflow-hidden bg-popover text-popover-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
@@ -47,7 +44,7 @@ const CommandInput = React.forwardRef<
|
||||
<CommandPrimitive.Input
|
||||
ref={ref}
|
||||
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
|
||||
)}
|
||||
{...props}
|
||||
@@ -63,7 +60,7 @@ const CommandList = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.List
|
||||
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}
|
||||
/>
|
||||
))
|
||||
@@ -73,9 +70,7 @@ CommandList.displayName = CommandPrimitive.List.displayName
|
||||
const CommandEmpty = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Empty>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
||||
>((props, ref) => (
|
||||
<CommandPrimitive.Empty ref={ref} className="py-6 text-center text-sm" {...props} />
|
||||
))
|
||||
>((props, ref) => <CommandPrimitive.Empty ref={ref} className="py-6 text-center text-sm" {...props} />)
|
||||
|
||||
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
|
||||
|
||||
@@ -86,7 +81,7 @@ const CommandGroup = React.forwardRef<
|
||||
<CommandPrimitive.Group
|
||||
ref={ref}
|
||||
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
|
||||
)}
|
||||
{...props}
|
||||
@@ -99,11 +94,7 @@ const CommandSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn('-mx-1 h-px bg-border', className)}
|
||||
{...props}
|
||||
/>
|
||||
<CommandPrimitive.Separator ref={ref} className={cn("-mx-1 h-px bg-border", className)} {...props} />
|
||||
))
|
||||
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
|
||||
|
||||
@@ -124,14 +115,9 @@ const CommandItem = React.forwardRef<
|
||||
CommandItem.displayName = CommandPrimitive.Item.displayName
|
||||
|
||||
const CommandShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn('ml-auto text-xs tracking-wide text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
return <span className={cn("ml-auto text-xs tracking-wide text-muted-foreground", className)} {...props} />
|
||||
}
|
||||
CommandShortcut.displayName = 'CommandShortcut'
|
||||
CommandShortcut.displayName = "CommandShortcut"
|
||||
|
||||
export {
|
||||
Command,
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import * as React from 'react'
|
||||
import * as DialogPrimitive from '@radix-ui/react-dialog'
|
||||
import { X } from 'lucide-react'
|
||||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { X } from "lucide-react"
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Dialog = DialogPrimitive.Root
|
||||
|
||||
@@ -19,7 +19,7 @@ const DialogOverlay = React.forwardRef<
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
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
|
||||
)}
|
||||
{...props}
|
||||
@@ -36,7 +36,7 @@ const DialogContent = React.forwardRef<
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
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
|
||||
)}
|
||||
{...props}
|
||||
@@ -52,17 +52,14 @@ const DialogContent = React.forwardRef<
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||
|
||||
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>) => (
|
||||
<div
|
||||
className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)}
|
||||
{...props}
|
||||
/>
|
||||
<div 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<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
@@ -70,7 +67,7 @@ const DialogTitle = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
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}
|
||||
/>
|
||||
))
|
||||
@@ -80,11 +77,7 @@ const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn('text-sm text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
<DialogPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
|
||||
))
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||
|
||||
|
@@ -17,182 +17,163 @@ const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
||||
|
||||
const DropdownMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
}
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto h-4 w-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto h-4 w-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
))
|
||||
DropdownMenuSubTrigger.displayName =
|
||||
DropdownMenuPrimitive.SubTrigger.displayName
|
||||
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg 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-[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
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg 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-[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
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuSubContent.displayName =
|
||||
DropdownMenuPrimitive.SubContent.displayName
|
||||
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md 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-[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
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md 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-[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
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
))
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
}
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
))
|
||||
DropdownMenuCheckboxItem.displayName =
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName
|
||||
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName
|
||||
|
||||
const DropdownMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Circle className="h-2 w-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Circle className="h-2 w-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
))
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
}
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
||||
|
||||
const DropdownMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
<DropdownMenuPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />
|
||||
))
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
||||
|
||||
const DropdownMenuShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return <span className={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...props} />
|
||||
}
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { SVGProps } from 'react'
|
||||
import { SVGProps } from "react"
|
||||
|
||||
// linux-logo-bold from https://github.com/phosphor-icons/core (MIT license)
|
||||
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)
|
||||
export function EthernetIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 24 24"
|
||||
{...props}
|
||||
>
|
||||
<svg 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" />
|
||||
</svg>
|
||||
)
|
||||
|
@@ -1,27 +1,24 @@
|
||||
import * as React from 'react'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { XIcon } from 'lucide-react'
|
||||
import { type InputProps } from './input'
|
||||
import { cn } from '@/lib/utils'
|
||||
import * as React from "react"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { XIcon } from "lucide-react"
|
||||
import { type InputProps } from "./input"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
type InputTagsProps = Omit<InputProps, 'value' | 'onChange'> & {
|
||||
type InputTagsProps = Omit<InputProps, "value" | "onChange"> & {
|
||||
value: string[]
|
||||
onChange: React.Dispatch<React.SetStateAction<string[]>>
|
||||
}
|
||||
|
||||
const InputTags = React.forwardRef<HTMLInputElement, InputTagsProps>(
|
||||
({ className, value, onChange, ...props }, ref) => {
|
||||
const [pendingDataPoint, setPendingDataPoint] = React.useState('')
|
||||
const [pendingDataPoint, setPendingDataPoint] = React.useState("")
|
||||
|
||||
React.useEffect(() => {
|
||||
if (pendingDataPoint.includes(',')) {
|
||||
const newDataPoints = new Set([
|
||||
...value,
|
||||
...pendingDataPoint.split(',').map((chunk) => chunk.trim()),
|
||||
])
|
||||
if (pendingDataPoint.includes(",")) {
|
||||
const newDataPoints = new Set([...value, ...pendingDataPoint.split(",").map((chunk) => chunk.trim())])
|
||||
onChange(Array.from(newDataPoints))
|
||||
setPendingDataPoint('')
|
||||
setPendingDataPoint("")
|
||||
}
|
||||
}, [pendingDataPoint, onChange, value])
|
||||
|
||||
@@ -29,14 +26,14 @@ const InputTags = React.forwardRef<HTMLInputElement, InputTagsProps>(
|
||||
if (pendingDataPoint) {
|
||||
const newDataPoints = new Set([...value, pendingDataPoint])
|
||||
onChange(Array.from(newDataPoints))
|
||||
setPendingDataPoint('')
|
||||
setPendingDataPoint("")
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
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
|
||||
)}
|
||||
>
|
||||
@@ -60,10 +57,10 @@ const InputTags = React.forwardRef<HTMLInputElement, InputTagsProps>(
|
||||
value={pendingDataPoint}
|
||||
onChange={(e) => setPendingDataPoint(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ',') {
|
||||
if (e.key === "Enter" || e.key === ",") {
|
||||
e.preventDefault()
|
||||
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()
|
||||
onChange(value.slice(0, -1))
|
||||
}
|
||||
@@ -76,6 +73,6 @@ const InputTags = React.forwardRef<HTMLInputElement, InputTagsProps>(
|
||||
}
|
||||
)
|
||||
|
||||
InputTags.displayName = 'InputTags'
|
||||
InputTags.displayName = "InputTags"
|
||||
|
||||
export { InputTags }
|
||||
|
@@ -2,24 +2,21 @@ import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium 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
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium 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
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Input.displayName = "Input"
|
||||
|
||||
export { Input }
|
||||
|
@@ -4,20 +4,13 @@ import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const labelVariants = cva(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
)
|
||||
const labelVariants = cva("text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70")
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
|
||||
))
|
||||
Label.displayName = LabelPrimitive.Root.displayName
|
||||
|
||||
|
@@ -11,148 +11,133 @@ const SelectGroup = SelectPrimitive.Group
|
||||
const SelectValue = SelectPrimitive.Value
|
||||
|
||||
const SelectTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
))
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
||||
|
||||
const SelectScrollUpButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
ref={ref}
|
||||
className={cn("flex cursor-default items-center justify-center py-1", className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
))
|
||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
||||
|
||||
const SelectScrollDownButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
ref={ref}
|
||||
className={cn("flex cursor-default items-center justify-center py-1", className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
))
|
||||
SelectScrollDownButton.displayName =
|
||||
SelectPrimitive.ScrollDownButton.displayName
|
||||
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName
|
||||
|
||||
const SelectContent = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
>(({ className, children, position = "popper", ...props }, ref) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md 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-[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",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md 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-[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",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
))
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName
|
||||
|
||||
const SelectLabel = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
<SelectPrimitive.Label ref={ref} className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)} {...props} />
|
||||
))
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
||||
|
||||
const SelectItem = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
))
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName
|
||||
|
||||
const SelectSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
<SelectPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />
|
||||
))
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectScrollUpButton,
|
||||
SelectScrollDownButton,
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectScrollUpButton,
|
||||
SelectScrollDownButton,
|
||||
}
|
||||
|
@@ -4,26 +4,17 @@ import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Separator = React.forwardRef<
|
||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||
>(
|
||||
(
|
||||
{ className, orientation = "horizontal", decorative = true, ...props },
|
||||
ref
|
||||
) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"shrink-0 bg-border",
|
||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||
>(({ className, orientation = "horizontal", decorative = true, ...props }, ref) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn("shrink-0 bg-border", orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName
|
||||
|
||||
export { Separator }
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react'
|
||||
import * as SliderPrimitive from '@radix-ui/react-slider'
|
||||
import * as React from "react"
|
||||
import * as SliderPrimitive from "@radix-ui/react-slider"
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Slider = React.forwardRef<
|
||||
React.ElementRef<typeof SliderPrimitive.Root>,
|
||||
@@ -9,7 +9,7 @@ const Slider = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SliderPrimitive.Root
|
||||
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}
|
||||
>
|
||||
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
|
||||
|
@@ -4,23 +4,23 @@ import * as SwitchPrimitives from "@radix-ui/react-switch"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Switch = React.forwardRef<
|
||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
))
|
||||
Switch.displayName = SwitchPrimitives.Root.displayName
|
||||
|
||||
|
@@ -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>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<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>
|
||||
)
|
||||
)
|
||||
Table.displayName = 'Table'
|
||||
Table.displayName = "Table"
|
||||
|
||||
const TableHeader = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} />
|
||||
))
|
||||
TableHeader.displayName = 'TableHeader'
|
||||
const TableHeader = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
|
||||
({ className, ...props }, ref) => <thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
||||
)
|
||||
TableHeader.displayName = "TableHeader"
|
||||
|
||||
const TableBody = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tbody ref={ref} className={cn('[&_tr:last-child]:border-0', className)} {...props} />
|
||||
))
|
||||
TableBody.displayName = 'TableBody'
|
||||
const TableBody = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<tbody ref={ref} className={cn("[&_tr:last-child]:border-0", className)} {...props} />
|
||||
)
|
||||
)
|
||||
TableBody.displayName = "TableBody"
|
||||
|
||||
const TableFooter = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tfoot
|
||||
ref={ref}
|
||||
className={cn('border-t bg-muted/50 font-medium [&>tr]:last:border-b-0', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableFooter.displayName = 'TableFooter'
|
||||
const TableFooter = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<tfoot ref={ref} 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>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<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}
|
||||
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
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
TableRow.displayName = 'TableRow'
|
||||
TableHead.displayName = "TableHead"
|
||||
|
||||
const TableHead = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<th
|
||||
ref={ref}
|
||||
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<HTMLTableCellElement, React.TdHTMLAttributes<HTMLTableCellElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<td ref={ref} className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)} {...props} />
|
||||
)
|
||||
)
|
||||
TableCell.displayName = "TableCell"
|
||||
|
||||
const TableCell = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<td
|
||||
ref={ref}
|
||||
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'
|
||||
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 }
|
||||
|
@@ -6,47 +6,47 @@ import { cn } from "@/lib/utils"
|
||||
const Tabs = TabsPrimitive.Root
|
||||
|
||||
const TabsList = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||
React.ElementRef<typeof TabsPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsList.displayName = TabsPrimitive.List.displayName
|
||||
|
||||
const TabsTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
||||
|
||||
const TabsContent = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
<TabsPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName
|
||||
|
||||
|
@@ -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> {}
|
||||
|
||||
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
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',
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Textarea.displayName = 'Textarea'
|
||||
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
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",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Textarea.displayName = "Textarea"
|
||||
|
||||
export { Textarea }
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import * as React from 'react'
|
||||
import * as ToastPrimitives from '@radix-ui/react-toast'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
import { X } from 'lucide-react'
|
||||
import * as React from "react"
|
||||
import * as ToastPrimitives from "@radix-ui/react-toast"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { X } from "lucide-react"
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const ToastProvider = ToastPrimitives.Provider
|
||||
|
||||
@@ -14,7 +14,7 @@ const ToastViewport = React.forwardRef<
|
||||
<ToastPrimitives.Viewport
|
||||
ref={ref}
|
||||
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
|
||||
)}
|
||||
{...props}
|
||||
@@ -23,17 +23,16 @@ const ToastViewport = React.forwardRef<
|
||||
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
|
||||
|
||||
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: {
|
||||
variant: {
|
||||
default: 'border bg-background text-foreground',
|
||||
destructive:
|
||||
'destructive group border-destructive bg-destructive text-destructive-foreground',
|
||||
default: "border bg-background text-foreground",
|
||||
destructive: "destructive group border-destructive bg-destructive text-destructive-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -42,13 +41,7 @@ const Toast = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> & VariantProps<typeof toastVariants>
|
||||
>(({ className, variant, ...props }, ref) => {
|
||||
return (
|
||||
<ToastPrimitives.Root
|
||||
ref={ref}
|
||||
className={cn(toastVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
return <ToastPrimitives.Root ref={ref} className={cn(toastVariants({ variant }), className)} {...props} />
|
||||
})
|
||||
Toast.displayName = ToastPrimitives.Root.displayName
|
||||
|
||||
@@ -59,7 +52,7 @@ const ToastAction = React.forwardRef<
|
||||
<ToastPrimitives.Action
|
||||
ref={ref}
|
||||
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
|
||||
)}
|
||||
{...props}
|
||||
@@ -74,7 +67,7 @@ const ToastClose = React.forwardRef<
|
||||
<ToastPrimitives.Close
|
||||
ref={ref}
|
||||
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
|
||||
)}
|
||||
toast-close=""
|
||||
@@ -89,7 +82,7 @@ const ToastTitle = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
||||
>(({ 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
|
||||
|
||||
@@ -97,11 +90,7 @@ const ToastDescription = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Description
|
||||
ref={ref}
|
||||
className={cn('text-sm opacity-90', className)}
|
||||
{...props}
|
||||
/>
|
||||
<ToastPrimitives.Description ref={ref} className={cn("text-sm opacity-90", className)} {...props} />
|
||||
))
|
||||
ToastDescription.displayName = ToastPrimitives.Description.displayName
|
||||
|
||||
|
@@ -1,33 +1,24 @@
|
||||
import {
|
||||
Toast,
|
||||
ToastClose,
|
||||
ToastDescription,
|
||||
ToastProvider,
|
||||
ToastTitle,
|
||||
ToastViewport,
|
||||
} from "@/components/ui/toast"
|
||||
import { Toast, ToastClose, ToastDescription, ToastProvider, ToastTitle, ToastViewport } from "@/components/ui/toast"
|
||||
import { useToast } from "@/components/ui/use-toast"
|
||||
|
||||
export function Toaster() {
|
||||
const { toasts } = useToast()
|
||||
const { toasts } = useToast()
|
||||
|
||||
return (
|
||||
<ToastProvider>
|
||||
{toasts.map(function ({ id, title, description, action, ...props }) {
|
||||
return (
|
||||
<Toast key={id} {...props}>
|
||||
<div className="grid gap-1">
|
||||
{title && <ToastTitle>{title}</ToastTitle>}
|
||||
{description && (
|
||||
<ToastDescription>{description}</ToastDescription>
|
||||
)}
|
||||
</div>
|
||||
{action}
|
||||
<ToastClose />
|
||||
</Toast>
|
||||
)
|
||||
})}
|
||||
<ToastViewport />
|
||||
</ToastProvider>
|
||||
)
|
||||
return (
|
||||
<ToastProvider>
|
||||
{toasts.map(function ({ id, title, description, action, ...props }) {
|
||||
return (
|
||||
<Toast key={id} {...props}>
|
||||
<div className="grid gap-1">
|
||||
{title && <ToastTitle>{title}</ToastTitle>}
|
||||
{description && <ToastDescription>{description}</ToastDescription>}
|
||||
</div>
|
||||
{action}
|
||||
<ToastClose />
|
||||
</Toast>
|
||||
)
|
||||
})}
|
||||
<ToastViewport />
|
||||
</ToastProvider>
|
||||
)
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react'
|
||||
import * as TooltipPrimitive from '@radix-ui/react-tooltip'
|
||||
import * as React from "react"
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider
|
||||
|
||||
@@ -17,7 +17,7 @@ const TooltipContent = React.forwardRef<
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
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
|
||||
)}
|
||||
{...props}
|
||||
|
@@ -1,130 +1,125 @@
|
||||
// Inspired by react-hot-toast library
|
||||
import * as React from "react"
|
||||
|
||||
import type {
|
||||
ToastActionElement,
|
||||
ToastProps,
|
||||
} from "@/components/ui/toast"
|
||||
import type { ToastActionElement, ToastProps } from "@/components/ui/toast"
|
||||
|
||||
const TOAST_LIMIT = 1
|
||||
const TOAST_REMOVE_DELAY = 1000000
|
||||
|
||||
type ToasterToast = ToastProps & {
|
||||
id: string
|
||||
title?: React.ReactNode
|
||||
description?: React.ReactNode
|
||||
action?: ToastActionElement
|
||||
id: string
|
||||
title?: React.ReactNode
|
||||
description?: React.ReactNode
|
||||
action?: ToastActionElement
|
||||
}
|
||||
|
||||
const actionTypes = {
|
||||
ADD_TOAST: "ADD_TOAST",
|
||||
UPDATE_TOAST: "UPDATE_TOAST",
|
||||
DISMISS_TOAST: "DISMISS_TOAST",
|
||||
REMOVE_TOAST: "REMOVE_TOAST",
|
||||
ADD_TOAST: "ADD_TOAST",
|
||||
UPDATE_TOAST: "UPDATE_TOAST",
|
||||
DISMISS_TOAST: "DISMISS_TOAST",
|
||||
REMOVE_TOAST: "REMOVE_TOAST",
|
||||
} as const
|
||||
|
||||
let count = 0
|
||||
|
||||
function genId() {
|
||||
count = (count + 1) % Number.MAX_SAFE_INTEGER
|
||||
return count.toString()
|
||||
count = (count + 1) % Number.MAX_SAFE_INTEGER
|
||||
return count.toString()
|
||||
}
|
||||
|
||||
type ActionType = typeof actionTypes
|
||||
|
||||
type Action =
|
||||
| {
|
||||
type: ActionType["ADD_TOAST"]
|
||||
toast: ToasterToast
|
||||
}
|
||||
| {
|
||||
type: ActionType["UPDATE_TOAST"]
|
||||
toast: Partial<ToasterToast>
|
||||
}
|
||||
| {
|
||||
type: ActionType["DISMISS_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
| {
|
||||
type: ActionType["REMOVE_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
| {
|
||||
type: ActionType["ADD_TOAST"]
|
||||
toast: ToasterToast
|
||||
}
|
||||
| {
|
||||
type: ActionType["UPDATE_TOAST"]
|
||||
toast: Partial<ToasterToast>
|
||||
}
|
||||
| {
|
||||
type: ActionType["DISMISS_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
| {
|
||||
type: ActionType["REMOVE_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
|
||||
interface State {
|
||||
toasts: ToasterToast[]
|
||||
toasts: ToasterToast[]
|
||||
}
|
||||
|
||||
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
||||
|
||||
const addToRemoveQueue = (toastId: string) => {
|
||||
if (toastTimeouts.has(toastId)) {
|
||||
return
|
||||
}
|
||||
if (toastTimeouts.has(toastId)) {
|
||||
return
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
toastTimeouts.delete(toastId)
|
||||
dispatch({
|
||||
type: "REMOVE_TOAST",
|
||||
toastId: toastId,
|
||||
})
|
||||
}, TOAST_REMOVE_DELAY)
|
||||
const timeout = setTimeout(() => {
|
||||
toastTimeouts.delete(toastId)
|
||||
dispatch({
|
||||
type: "REMOVE_TOAST",
|
||||
toastId: toastId,
|
||||
})
|
||||
}, TOAST_REMOVE_DELAY)
|
||||
|
||||
toastTimeouts.set(toastId, timeout)
|
||||
toastTimeouts.set(toastId, timeout)
|
||||
}
|
||||
|
||||
export const reducer = (state: State, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case "ADD_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||
}
|
||||
switch (action.type) {
|
||||
case "ADD_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||
}
|
||||
|
||||
case "UPDATE_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === action.toast.id ? { ...t, ...action.toast } : t
|
||||
),
|
||||
}
|
||||
case "UPDATE_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)),
|
||||
}
|
||||
|
||||
case "DISMISS_TOAST": {
|
||||
const { toastId } = action
|
||||
case "DISMISS_TOAST": {
|
||||
const { toastId } = action
|
||||
|
||||
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||
// but I'll keep it here for simplicity
|
||||
if (toastId) {
|
||||
addToRemoveQueue(toastId)
|
||||
} else {
|
||||
state.toasts.forEach((toast) => {
|
||||
addToRemoveQueue(toast.id)
|
||||
})
|
||||
}
|
||||
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||
// but I'll keep it here for simplicity
|
||||
if (toastId) {
|
||||
addToRemoveQueue(toastId)
|
||||
} else {
|
||||
state.toasts.forEach((toast) => {
|
||||
addToRemoveQueue(toast.id)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === toastId || toastId === undefined
|
||||
? {
|
||||
...t,
|
||||
open: false,
|
||||
}
|
||||
: t
|
||||
),
|
||||
}
|
||||
}
|
||||
case "REMOVE_TOAST":
|
||||
if (action.toastId === undefined) {
|
||||
return {
|
||||
...state,
|
||||
toasts: [],
|
||||
}
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||
}
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === toastId || toastId === undefined
|
||||
? {
|
||||
...t,
|
||||
open: false,
|
||||
}
|
||||
: t
|
||||
),
|
||||
}
|
||||
}
|
||||
case "REMOVE_TOAST":
|
||||
if (action.toastId === undefined) {
|
||||
return {
|
||||
...state,
|
||||
toasts: [],
|
||||
}
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const listeners: Array<(state: State) => void> = []
|
||||
@@ -132,61 +127,61 @@ const listeners: Array<(state: State) => void> = []
|
||||
let memoryState: State = { toasts: [] }
|
||||
|
||||
function dispatch(action: Action) {
|
||||
memoryState = reducer(memoryState, action)
|
||||
listeners.forEach((listener) => {
|
||||
listener(memoryState)
|
||||
})
|
||||
memoryState = reducer(memoryState, action)
|
||||
listeners.forEach((listener) => {
|
||||
listener(memoryState)
|
||||
})
|
||||
}
|
||||
|
||||
type Toast = Omit<ToasterToast, "id">
|
||||
|
||||
function toast({ ...props }: Toast) {
|
||||
const id = genId()
|
||||
const id = genId()
|
||||
|
||||
const update = (props: ToasterToast) =>
|
||||
dispatch({
|
||||
type: "UPDATE_TOAST",
|
||||
toast: { ...props, id },
|
||||
})
|
||||
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
|
||||
const update = (props: ToasterToast) =>
|
||||
dispatch({
|
||||
type: "UPDATE_TOAST",
|
||||
toast: { ...props, id },
|
||||
})
|
||||
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
|
||||
|
||||
dispatch({
|
||||
type: "ADD_TOAST",
|
||||
toast: {
|
||||
...props,
|
||||
id,
|
||||
open: true,
|
||||
onOpenChange: (open) => {
|
||||
if (!open) dismiss()
|
||||
},
|
||||
},
|
||||
})
|
||||
dispatch({
|
||||
type: "ADD_TOAST",
|
||||
toast: {
|
||||
...props,
|
||||
id,
|
||||
open: true,
|
||||
onOpenChange: (open) => {
|
||||
if (!open) dismiss()
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
id: id,
|
||||
dismiss,
|
||||
update,
|
||||
}
|
||||
return {
|
||||
id: id,
|
||||
dismiss,
|
||||
update,
|
||||
}
|
||||
}
|
||||
|
||||
function useToast() {
|
||||
const [state, setState] = React.useState<State>(memoryState)
|
||||
const [state, setState] = React.useState<State>(memoryState)
|
||||
|
||||
React.useEffect(() => {
|
||||
listeners.push(setState)
|
||||
return () => {
|
||||
const index = listeners.indexOf(setState)
|
||||
if (index > -1) {
|
||||
listeners.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}, [state])
|
||||
React.useEffect(() => {
|
||||
listeners.push(setState)
|
||||
return () => {
|
||||
const index = listeners.indexOf(setState)
|
||||
if (index > -1) {
|
||||
listeners.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}, [state])
|
||||
|
||||
return {
|
||||
...state,
|
||||
toast,
|
||||
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
toast,
|
||||
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
||||
}
|
||||
}
|
||||
|
||||
export { useToast, toast }
|
||||
|
@@ -68,7 +68,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 100 900;
|
||||
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 {
|
||||
|
@@ -1,57 +1,64 @@
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import i18n from "i18next"
|
||||
import { initReactI18next } from "react-i18next"
|
||||
|
||||
import en from '../locales/en/translation.json';
|
||||
import es from '../locales/es/translation.json';
|
||||
import fr from '../locales/fr/translation.json';
|
||||
import de from '../locales/de/translation.json';
|
||||
import ru from '../locales/ru/translation.json';
|
||||
import zhHans from '../locales/zh-CN/translation.json';
|
||||
import zhHant from '../locales/zh-HK/translation.json';
|
||||
import en from "../locales/en/translation.json"
|
||||
import es from "../locales/es/translation.json"
|
||||
import fr from "../locales/fr/translation.json"
|
||||
import de from "../locales/de/translation.json"
|
||||
import ru from "../locales/ru/translation.json"
|
||||
import zhHans from "../locales/zh-CN/translation.json"
|
||||
import zhHant from "../locales/zh-HK/translation.json"
|
||||
|
||||
// Custom language detector to use localStorage
|
||||
const languageDetector: any = {
|
||||
type: 'languageDetector',
|
||||
async: true,
|
||||
detect: (callback: (lng: string) => void) => {
|
||||
const savedLanguage = localStorage.getItem('i18nextLng');
|
||||
const fallbackLanguage = (()=>{
|
||||
switch (navigator.language) {
|
||||
case 'zh-CN': case 'zh-SG': case 'zh-MY': case 'zh': case 'zh-Hans':
|
||||
return 'zh-CN';
|
||||
case 'zh-HK': case 'zh-TW': case 'zh-MO': case 'zh-Hant':
|
||||
return 'zh-HK';
|
||||
default:
|
||||
return navigator.language;
|
||||
}
|
||||
})();
|
||||
callback(savedLanguage || fallbackLanguage);
|
||||
},
|
||||
init: () => {},
|
||||
cacheUserLanguage: (lng: string) => {
|
||||
localStorage.setItem('i18nextLng', lng);
|
||||
}
|
||||
};
|
||||
type: "languageDetector",
|
||||
async: true,
|
||||
detect: (callback: (lng: string) => void) => {
|
||||
const savedLanguage = localStorage.getItem("i18nextLng")
|
||||
const fallbackLanguage = (() => {
|
||||
switch (navigator.language) {
|
||||
case "zh-CN":
|
||||
case "zh-SG":
|
||||
case "zh-MY":
|
||||
case "zh":
|
||||
case "zh-Hans":
|
||||
return "zh-CN"
|
||||
case "zh-HK":
|
||||
case "zh-TW":
|
||||
case "zh-MO":
|
||||
case "zh-Hant":
|
||||
return "zh-HK"
|
||||
default:
|
||||
return navigator.language
|
||||
}
|
||||
})()
|
||||
callback(savedLanguage || fallbackLanguage)
|
||||
},
|
||||
init: () => {},
|
||||
cacheUserLanguage: (lng: string) => {
|
||||
localStorage.setItem("i18nextLng", lng)
|
||||
},
|
||||
}
|
||||
|
||||
i18n
|
||||
.use(languageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources: {
|
||||
en: { translation: en },
|
||||
es: { translation: es },
|
||||
fr: { translation: fr },
|
||||
de: { translation: de },
|
||||
ru: { translation: ru },
|
||||
// Chinese (Simplified)
|
||||
'zh-CN': { translation: zhHans },
|
||||
// Chinese (Traditional)
|
||||
'zh-HK': { translation: zhHant },
|
||||
},
|
||||
fallbackLng: 'en',
|
||||
interpolation: {
|
||||
escapeValue: false
|
||||
}
|
||||
});
|
||||
.use(languageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources: {
|
||||
en: { translation: en },
|
||||
es: { translation: es },
|
||||
fr: { translation: fr },
|
||||
de: { translation: de },
|
||||
ru: { translation: ru },
|
||||
// Chinese (Simplified)
|
||||
"zh-CN": { translation: zhHans },
|
||||
// Chinese (Traditional)
|
||||
"zh-HK": { translation: zhHant },
|
||||
},
|
||||
fallbackLng: "en",
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
})
|
||||
|
||||
export { i18n };
|
||||
export { i18n }
|
||||
|
@@ -1,30 +1,30 @@
|
||||
[
|
||||
{
|
||||
"lang": "en",
|
||||
"label": "English"
|
||||
},
|
||||
{
|
||||
"lang": "es",
|
||||
"label": "Español"
|
||||
},
|
||||
{
|
||||
"lang": "fr",
|
||||
"label": "Français"
|
||||
},
|
||||
{
|
||||
"lang": "de",
|
||||
"label": "Deutsch"
|
||||
},
|
||||
{
|
||||
"lang": "ru",
|
||||
"label": "Русский"
|
||||
},
|
||||
{
|
||||
"lang": "zh-CN",
|
||||
"label": "简体中文"
|
||||
},
|
||||
{
|
||||
"lang": "zh-HK",
|
||||
"label": "繁體中文"
|
||||
}
|
||||
]
|
||||
{
|
||||
"lang": "en",
|
||||
"label": "English"
|
||||
},
|
||||
{
|
||||
"lang": "es",
|
||||
"label": "Español"
|
||||
},
|
||||
{
|
||||
"lang": "fr",
|
||||
"label": "Français"
|
||||
},
|
||||
{
|
||||
"lang": "de",
|
||||
"label": "Deutsch"
|
||||
},
|
||||
{
|
||||
"lang": "ru",
|
||||
"label": "Русский"
|
||||
},
|
||||
{
|
||||
"lang": "zh-CN",
|
||||
"label": "简体中文"
|
||||
},
|
||||
{
|
||||
"lang": "zh-HK",
|
||||
"label": "繁體中文"
|
||||
}
|
||||
]
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import PocketBase from 'pocketbase'
|
||||
import { atom, map, WritableAtom } from 'nanostores'
|
||||
import { AlertRecord, ChartTimes, SystemRecord, UserSettings } from '@/types'
|
||||
import PocketBase from "pocketbase"
|
||||
import { atom, map, WritableAtom } from "nanostores"
|
||||
import { AlertRecord, ChartTimes, SystemRecord, UserSettings } from "@/types"
|
||||
|
||||
/** PocketBase JS Client */
|
||||
export const pb = new PocketBase('/')
|
||||
export const pb = new PocketBase("/")
|
||||
|
||||
/** Store if user is authenticated */
|
||||
export const $authenticated = atom(pb.authStore.isValid)
|
||||
@@ -15,18 +15,18 @@ export const $systems = atom([] as SystemRecord[])
|
||||
export const $alerts = atom([] as AlertRecord[])
|
||||
|
||||
/** SSH public key */
|
||||
export const $publicKey = atom('')
|
||||
export const $publicKey = atom("")
|
||||
|
||||
/** Beszel hub version */
|
||||
export const $hubVersion = atom('')
|
||||
export const $hubVersion = atom("")
|
||||
|
||||
/** Chart time period */
|
||||
export const $chartTime = atom('1h') as WritableAtom<ChartTimes>
|
||||
export const $chartTime = atom("1h") as WritableAtom<ChartTimes>
|
||||
|
||||
/** User settings */
|
||||
export const $userSettings = map<UserSettings>({
|
||||
chartTime: '1h',
|
||||
emails: [pb.authStore.model?.email || ''],
|
||||
chartTime: "1h",
|
||||
emails: [pb.authStore.model?.email || ""],
|
||||
})
|
||||
// update local storage on change
|
||||
$userSettings.subscribe((value) => {
|
||||
@@ -35,7 +35,7 @@ $userSettings.subscribe((value) => {
|
||||
})
|
||||
|
||||
/** Container chart filter */
|
||||
export const $containerFilter = atom('')
|
||||
export const $containerFilter = atom("")
|
||||
|
||||
/** Fallback copy to clipboard dialog content */
|
||||
export const $copyContent = atom('')
|
||||
export const $copyContent = atom("")
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
|
||||
// adapted from usehooks-ts/use-intersection-observer
|
||||
|
||||
@@ -72,7 +72,7 @@ type IntersectionReturn = {
|
||||
export function useIntersectionObserver({
|
||||
threshold = 0,
|
||||
root = null,
|
||||
rootMargin = '0%',
|
||||
rootMargin = "0%",
|
||||
freeze = true,
|
||||
initialIsIntersecting = false,
|
||||
onChange,
|
||||
@@ -84,7 +84,7 @@ export function useIntersectionObserver({
|
||||
entry: undefined,
|
||||
}))
|
||||
|
||||
const callbackRef = useRef<UseIntersectionObserverOptions['onChange']>()
|
||||
const callbackRef = useRef<UseIntersectionObserverOptions["onChange"]>()
|
||||
|
||||
callbackRef.current = onChange
|
||||
|
||||
@@ -95,7 +95,7 @@ export function useIntersectionObserver({
|
||||
if (!ref) return
|
||||
|
||||
// Ensure the browser supports the Intersection Observer API
|
||||
if (!('IntersectionObserver' in window)) return
|
||||
if (!("IntersectionObserver" in window)) return
|
||||
|
||||
// Skip if frozen
|
||||
if (frozen) return
|
||||
@@ -104,14 +104,11 @@ export function useIntersectionObserver({
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries: IntersectionObserverEntry[]): void => {
|
||||
const thresholds = Array.isArray(observer.thresholds)
|
||||
? observer.thresholds
|
||||
: [observer.thresholds]
|
||||
const thresholds = Array.isArray(observer.thresholds) ? observer.thresholds : [observer.thresholds]
|
||||
|
||||
entries.forEach((entry) => {
|
||||
const isIntersecting =
|
||||
entry.isIntersecting &&
|
||||
thresholds.some((threshold) => entry.intersectionRatio >= threshold)
|
||||
entry.isIntersecting && thresholds.some((threshold) => entry.intersectionRatio >= threshold)
|
||||
|
||||
setState({ isIntersecting, entry })
|
||||
|
||||
@@ -149,13 +146,7 @@ export function useIntersectionObserver({
|
||||
const prevRef = useRef<Element | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!ref &&
|
||||
state.entry?.target &&
|
||||
!freeze &&
|
||||
!frozen &&
|
||||
prevRef.current !== state.entry.target
|
||||
) {
|
||||
if (!ref && state.entry?.target && !freeze && !frozen && prevRef.current !== state.entry.target) {
|
||||
prevRef.current = state.entry.target
|
||||
setState({ isIntersecting: initialIsIntersecting, entry: undefined })
|
||||
}
|
||||
|
@@ -1,14 +1,14 @@
|
||||
import { toast } from '@/components/ui/use-toast'
|
||||
import { type ClassValue, clsx } from 'clsx'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { $alerts, $copyContent, $systems, $userSettings, pb } from './stores'
|
||||
import { AlertRecord, ChartTimeData, ChartTimes, SystemRecord } from '@/types'
|
||||
import { RecordModel, RecordSubscription } from 'pocketbase'
|
||||
import { WritableAtom } from 'nanostores'
|
||||
import { timeDay, timeHour } from 'd3-time'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { CpuIcon, HardDriveIcon, MemoryStickIcon, ServerIcon } from 'lucide-react'
|
||||
import { EthernetIcon, ThermometerIcon } from '@/components/ui/icons'
|
||||
import { toast } from "@/components/ui/use-toast"
|
||||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import { $alerts, $copyContent, $systems, $userSettings, pb } from "./stores"
|
||||
import { AlertRecord, ChartTimeData, ChartTimes, SystemRecord } from "@/types"
|
||||
import { RecordModel, RecordSubscription } from "pocketbase"
|
||||
import { WritableAtom } from "nanostores"
|
||||
import { timeDay, timeHour } from "d3-time"
|
||||
import { useEffect, useState } from "react"
|
||||
import { CpuIcon, HardDriveIcon, MemoryStickIcon, ServerIcon } from "lucide-react"
|
||||
import { EthernetIcon, ThermometerIcon } from "@/components/ui/icons"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
@@ -21,7 +21,7 @@ export async function copyToClipboard(content: string) {
|
||||
await navigator.clipboard.writeText(content)
|
||||
toast({
|
||||
duration,
|
||||
description: 'Copied to clipboard',
|
||||
description: "Copied to clipboard",
|
||||
})
|
||||
} catch (e: any) {
|
||||
$copyContent.set(content)
|
||||
@@ -29,22 +29,22 @@ export async function copyToClipboard(content: string) {
|
||||
}
|
||||
|
||||
const verifyAuth = () => {
|
||||
pb.collection('users')
|
||||
pb.collection("users")
|
||||
.authRefresh()
|
||||
.catch(() => {
|
||||
pb.authStore.clear()
|
||||
toast({
|
||||
title: 'Failed to authenticate',
|
||||
description: 'Please log in again',
|
||||
variant: 'destructive',
|
||||
title: "Failed to authenticate",
|
||||
description: "Please log in again",
|
||||
variant: "destructive",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export const updateSystemList = async () => {
|
||||
const records = await pb
|
||||
.collection<SystemRecord>('systems')
|
||||
.getFullList({ sort: '+name', fields: 'id,name,host,info,status' })
|
||||
.collection<SystemRecord>("systems")
|
||||
.getFullList({ sort: "+name", fields: "id,name,host,info,status" })
|
||||
if (records.length) {
|
||||
$systems.set(records)
|
||||
} else {
|
||||
@@ -53,26 +53,26 @@ export const updateSystemList = async () => {
|
||||
}
|
||||
|
||||
export const updateAlerts = () => {
|
||||
pb.collection('alerts')
|
||||
.getFullList<AlertRecord>({ fields: 'id,name,system,value,min,triggered', sort: 'updated' })
|
||||
pb.collection("alerts")
|
||||
.getFullList<AlertRecord>({ fields: "id,name,system,value,min,triggered", sort: "updated" })
|
||||
.then((records) => {
|
||||
$alerts.set(records)
|
||||
})
|
||||
}
|
||||
|
||||
const hourWithMinutesFormatter = new Intl.DateTimeFormat(undefined, {
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
})
|
||||
export const hourWithMinutes = (timestamp: string) => {
|
||||
return hourWithMinutesFormatter.format(new Date(timestamp))
|
||||
}
|
||||
|
||||
const shortDateFormatter = new Intl.DateTimeFormat(undefined, {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
day: "numeric",
|
||||
month: "short",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
})
|
||||
export const formatShortDate = (timestamp: string) => {
|
||||
// console.log('ts', timestamp)
|
||||
@@ -93,8 +93,8 @@ export const formatShortDate = (timestamp: string) => {
|
||||
// }
|
||||
|
||||
const dayFormatter = new Intl.DateTimeFormat(undefined, {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
day: "numeric",
|
||||
month: "short",
|
||||
// dateStyle: 'medium',
|
||||
})
|
||||
export const formatDay = (timestamp: string) => {
|
||||
@@ -106,19 +106,16 @@ export const updateFavicon = (newIcon: string) => {
|
||||
;(document.querySelector("link[rel='icon']") as HTMLLinkElement).href = `/static/${newIcon}`
|
||||
}
|
||||
|
||||
export const isAdmin = () => pb.authStore.model?.role === 'admin'
|
||||
export const isReadOnlyUser = () => pb.authStore.model?.role === 'readonly'
|
||||
export const isAdmin = () => pb.authStore.model?.role === "admin"
|
||||
export const isReadOnlyUser = () => pb.authStore.model?.role === "readonly"
|
||||
// export const isDefaultUser = () => pb.authStore.model?.role === 'user'
|
||||
|
||||
/** Update systems / alerts list when records change */
|
||||
export function updateRecordList<T extends RecordModel>(
|
||||
e: RecordSubscription<T>,
|
||||
$store: WritableAtom<T[]>
|
||||
) {
|
||||
export function updateRecordList<T extends RecordModel>(e: RecordSubscription<T>, $store: WritableAtom<T[]>) {
|
||||
const curRecords = $store.get()
|
||||
const newRecords = []
|
||||
// console.log('e', e)
|
||||
if (e.action === 'delete') {
|
||||
if (e.action === "delete") {
|
||||
for (const server of curRecords) {
|
||||
if (server.id !== e.record.id) {
|
||||
newRecords.push(server)
|
||||
@@ -143,51 +140,51 @@ export function updateRecordList<T extends RecordModel>(
|
||||
export function getPbTimestamp(timeString: ChartTimes, d?: Date) {
|
||||
d ||= chartTimeData[timeString].getOffset(new Date())
|
||||
const year = d.getUTCFullYear()
|
||||
const month = String(d.getUTCMonth() + 1).padStart(2, '0')
|
||||
const day = String(d.getUTCDate()).padStart(2, '0')
|
||||
const hours = String(d.getUTCHours()).padStart(2, '0')
|
||||
const minutes = String(d.getUTCMinutes()).padStart(2, '0')
|
||||
const seconds = String(d.getUTCSeconds()).padStart(2, '0')
|
||||
const month = String(d.getUTCMonth() + 1).padStart(2, "0")
|
||||
const day = String(d.getUTCDate()).padStart(2, "0")
|
||||
const hours = String(d.getUTCHours()).padStart(2, "0")
|
||||
const minutes = String(d.getUTCMinutes()).padStart(2, "0")
|
||||
const seconds = String(d.getUTCSeconds()).padStart(2, "0")
|
||||
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||
}
|
||||
|
||||
export const chartTimeData: ChartTimeData = {
|
||||
'1h': {
|
||||
type: '1m',
|
||||
"1h": {
|
||||
type: "1m",
|
||||
expectedInterval: 60_000,
|
||||
label: '1 hour',
|
||||
label: "1 hour",
|
||||
// ticks: 12,
|
||||
format: (timestamp: string) => hourWithMinutes(timestamp),
|
||||
getOffset: (endTime: Date) => timeHour.offset(endTime, -1),
|
||||
},
|
||||
'12h': {
|
||||
type: '10m',
|
||||
"12h": {
|
||||
type: "10m",
|
||||
expectedInterval: 60_000 * 10,
|
||||
label: '12 hours',
|
||||
label: "12 hours",
|
||||
ticks: 12,
|
||||
format: (timestamp: string) => hourWithMinutes(timestamp),
|
||||
getOffset: (endTime: Date) => timeHour.offset(endTime, -12),
|
||||
},
|
||||
'24h': {
|
||||
type: '20m',
|
||||
"24h": {
|
||||
type: "20m",
|
||||
expectedInterval: 60_000 * 20,
|
||||
label: '24 hours',
|
||||
label: "24 hours",
|
||||
format: (timestamp: string) => hourWithMinutes(timestamp),
|
||||
getOffset: (endTime: Date) => timeHour.offset(endTime, -24),
|
||||
},
|
||||
'1w': {
|
||||
type: '120m',
|
||||
"1w": {
|
||||
type: "120m",
|
||||
expectedInterval: 60_000 * 120,
|
||||
label: '1 week',
|
||||
label: "1 week",
|
||||
ticks: 7,
|
||||
format: (timestamp: string) => formatDay(timestamp),
|
||||
getOffset: (endTime: Date) => timeDay.offset(endTime, -7),
|
||||
},
|
||||
'30d': {
|
||||
type: '480m',
|
||||
"30d": {
|
||||
type: "480m",
|
||||
expectedInterval: 60_000 * 480,
|
||||
label: '30 days',
|
||||
label: "30 days",
|
||||
ticks: 30,
|
||||
format: (timestamp: string) => formatDay(timestamp),
|
||||
getOffset: (endTime: Date) => timeDay.offset(endTime, -30),
|
||||
@@ -202,8 +199,8 @@ export function useYAxisWidth() {
|
||||
function updateYAxisWidth(str: string) {
|
||||
if (str.length > maxChars) {
|
||||
maxChars = str.length
|
||||
const div = document.createElement('div')
|
||||
div.className = 'text-xs tabular-nums tracking-tighter table sr-only'
|
||||
const div = document.createElement("div")
|
||||
div.className = "text-xs tabular-nums tracking-tighter table sr-only"
|
||||
div.innerHTML = str
|
||||
clearTimeout(timeout)
|
||||
timeout = setTimeout(() => {
|
||||
@@ -263,20 +260,18 @@ export const useLocalStorage = (key: string, defaultValue: any) => {
|
||||
|
||||
export async function updateUserSettings() {
|
||||
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)
|
||||
return
|
||||
} catch (e) {
|
||||
console.log('get settings', e)
|
||||
console.log("get settings", e)
|
||||
}
|
||||
// create user settings if error fetching existing
|
||||
try {
|
||||
const createdSettings = await pb
|
||||
.collection('user_settings')
|
||||
.create({ user: pb.authStore.model!.id })
|
||||
const createdSettings = await pb.collection("user_settings").create({ user: pb.authStore.model!.id })
|
||||
$userSettings.set(createdSettings.settings)
|
||||
} 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
|
||||
|
||||
if (sizeInGB >= 1_000) {
|
||||
return { v: sizeInGB / 1_000, u: ' TB' }
|
||||
return { v: sizeInGB / 1_000, u: " TB" }
|
||||
} 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 alertInfo = {
|
||||
Status: {
|
||||
name: 'alerts.info.status',
|
||||
unit: '',
|
||||
name: "alerts.info.status",
|
||||
unit: "",
|
||||
icon: ServerIcon,
|
||||
desc: 'alerts.info.status_des',
|
||||
desc: "alerts.info.status_des",
|
||||
single: true,
|
||||
},
|
||||
CPU: {
|
||||
name: 'alerts.info.cpu_usage',
|
||||
unit: '%',
|
||||
name: "alerts.info.cpu_usage",
|
||||
unit: "%",
|
||||
icon: CpuIcon,
|
||||
desc: 'alerts.info.cpu_usage_des',
|
||||
desc: "alerts.info.cpu_usage_des",
|
||||
},
|
||||
Memory: {
|
||||
name: 'alerts.info.memory_usage',
|
||||
unit: '%',
|
||||
name: "alerts.info.memory_usage",
|
||||
unit: "%",
|
||||
icon: MemoryStickIcon,
|
||||
desc: 'alerts.info.memory_usage_des',
|
||||
desc: "alerts.info.memory_usage_des",
|
||||
},
|
||||
Disk: {
|
||||
name: 'alerts.info.disk_usage',
|
||||
unit: '%',
|
||||
name: "alerts.info.disk_usage",
|
||||
unit: "%",
|
||||
icon: HardDriveIcon,
|
||||
desc: 'alerts.info.disk_usage_des',
|
||||
desc: "alerts.info.disk_usage_des",
|
||||
},
|
||||
Bandwidth: {
|
||||
name: 'alerts.info.bandwidth',
|
||||
unit: ' MB/s',
|
||||
name: "alerts.info.bandwidth",
|
||||
unit: " MB/s",
|
||||
icon: EthernetIcon,
|
||||
desc: 'alerts.info.bandwidth_des',
|
||||
desc: "alerts.info.bandwidth_des",
|
||||
},
|
||||
Temperature: {
|
||||
name: 'alerts.info.temperature',
|
||||
unit: '°C',
|
||||
name: "alerts.info.temperature",
|
||||
unit: "°C",
|
||||
icon: ThermometerIcon,
|
||||
desc: 'alerts.info.temperature_des',
|
||||
desc: "alerts.info.temperature_des",
|
||||
},
|
||||
}
|
||||
|
@@ -1,194 +1,194 @@
|
||||
{
|
||||
"all_systems": "Alle Systeme",
|
||||
"filter": "Filtern...",
|
||||
"copy": "Kopieren",
|
||||
"add": "Hinzufügen",
|
||||
"system": "System",
|
||||
"systems": "Systeme",
|
||||
"cancel": "Abbrechen",
|
||||
"continue": "Fortsetzen",
|
||||
"home": {
|
||||
"active_alerts": "Aktive Warnungen",
|
||||
"active_des": "Überschreitet {{value}}{{unit}} Durchschnitt in den letzten {{minutes}} Minuten",
|
||||
"subtitle_1": "In Echtzeit aktualisiert. Drücken Sie",
|
||||
"subtitle_2": "um die Befehlsübersicht zu öffnen."
|
||||
},
|
||||
"systems_table": {
|
||||
"system": "System",
|
||||
"memory": "Speicher",
|
||||
"cpu": "CPU",
|
||||
"disk": "Festplatte",
|
||||
"net": "Netzwerk",
|
||||
"agent": "Agent",
|
||||
"no_systems_found": "Keine Systeme gefunden.",
|
||||
"open_menu": "Menü öffnen",
|
||||
"resume": "Fortsetzen",
|
||||
"pause": "Pause",
|
||||
"copy_host": "Host kopieren",
|
||||
"delete": "Löschen",
|
||||
"delete_confirm": "Sind Sie sicher, dass Sie {{name}} löschen möchten?",
|
||||
"delete_confirm_des_1": "Diese Aktion kann nicht rückgängig gemacht werden. Dies wird alle aktuellen Aufzeichnungen für",
|
||||
"delete_confirm_des_2": "dauerhaft aus der Datenbank löschen."
|
||||
},
|
||||
"alerts": {
|
||||
"title": "Warnungen",
|
||||
"subtitle_1": "Siehe",
|
||||
"notification_settings": "Benachrichtigungseinstellungen",
|
||||
"subtitle_2": "um zu konfigurieren, wie Sie Warnungen erhalten.",
|
||||
"overwrite_existing_alerts": "Bestehende Warnungen überschreiben",
|
||||
"info": {
|
||||
"status": "Status",
|
||||
"status_des": "Löst aus, wenn der Status zwischen oben und unten wechselt.",
|
||||
"cpu_usage": "CPU-Auslastung",
|
||||
"cpu_usage_des": "Löst aus, wenn die CPU-Auslastung einen Schwellenwert überschreitet.",
|
||||
"memory_usage": "Speicherauslastung",
|
||||
"memory_usage_des": "Löst aus, wenn die Speicherauslastung einen Schwellenwert überschreitet.",
|
||||
"disk_usage": "Festplattennutzung",
|
||||
"disk_usage_des": "Löst aus, wenn die Nutzung einer Festplatte einen Schwellenwert überschreitet.",
|
||||
"bandwidth": "Bandbreite",
|
||||
"bandwidth_des": "Löst aus, wenn die kombinierte Auf-/Abwärtsbandbreite einen Schwellenwert überschreitet.",
|
||||
"temperature": "Temperatur",
|
||||
"temperature_des": "Löst aus, wenn ein Sensor einen Schwellenwert überschreitet."
|
||||
},
|
||||
"average_exceeds": "Durchschnitt überschreitet",
|
||||
"for": "Für",
|
||||
"minute": "Minute",
|
||||
"minutes": "Minuten"
|
||||
},
|
||||
"settings": {
|
||||
"settings": "Einstellungen",
|
||||
"subtitle": "Anzeige- und Benachrichtigungseinstellungen verwalten.",
|
||||
"save_settings": "Einstellungen speichern",
|
||||
"export_configuration": "Konfiguration exportieren",
|
||||
"general": {
|
||||
"title": "Allgemein",
|
||||
"subtitle": "Allgemeine Anwendungsoptionen ändern.",
|
||||
"language": {
|
||||
"title": "Sprache",
|
||||
"subtitle_1": "Möchten Sie uns helfen, unsere Übersetzungen noch besser zu machen? Schauen Sie sich",
|
||||
"subtitle_2": "für weitere Details an.",
|
||||
"preferred_language": "Bevorzugte Sprache"
|
||||
},
|
||||
"chart_options": {
|
||||
"title": "Diagrammoptionen",
|
||||
"subtitle": "Anzeigeoptionen für Diagramme anpassen.",
|
||||
"default_time_period": "Standardzeitraum",
|
||||
"default_time_period_des": "Legt den Standardzeitraum für Diagramme fest, wenn ein System angezeigt wird."
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Benachrichtigungen",
|
||||
"subtitle_1": "Konfigurieren Sie, wie Sie Warnbenachrichtigungen erhalten.",
|
||||
"subtitle_2": "Suchen Sie stattdessen nach dem Ort, an dem Sie Warnungen erstellen können? Klicken Sie auf die Glocke",
|
||||
"subtitle_3": "Symbole in der Systemtabelle.",
|
||||
"email": {
|
||||
"title": "E-Mail-Benachrichtigungen",
|
||||
"please": "Bitte",
|
||||
"configure_an_SMTP_server": "konfigurieren Sie einen SMTP-Server",
|
||||
"to_ensure_alerts_are_delivered": "um sicherzustellen, dass Warnungen zugestellt werden.",
|
||||
"to_email_s": "An E-Mail(s)",
|
||||
"enter_email_address": "E-Mail-Adresse eingeben...",
|
||||
"des": "Adresse mit der Eingabetaste oder dem Komma speichern. Leer lassen, um E-Mail-Benachrichtigungen zu deaktivieren."
|
||||
},
|
||||
"webhook_push": {
|
||||
"title": "Webhook / Push-Benachrichtigungen",
|
||||
"des_1": "Beszel verwendet",
|
||||
"des_2": "um sich mit beliebten Benachrichtigungsdiensten zu integrieren.",
|
||||
"add_url": "URL hinzufügen"
|
||||
}
|
||||
},
|
||||
"yaml_config": {
|
||||
"short_title": "YAML-Konfig",
|
||||
"title": "YAML-Konfiguration",
|
||||
"subtitle": "Aktuelle Systemkonfiguration exportieren.",
|
||||
"des_1": "Systeme können in einer",
|
||||
"des_2": "Datei im Datenverzeichnis verwaltet werden.",
|
||||
"des_3": "Bei jedem Neustart werden die Systeme in der Datenbank aktualisiert, um den in der Datei definierten Systemen zu entsprechen.",
|
||||
"alert": {
|
||||
"title": "Achtung - potenzieller Datenverlust",
|
||||
"des_1": "Bestehende Systeme, die nicht in",
|
||||
"des_2": "definiert sind, werden gelöscht. Bitte machen Sie regelmäßige Backups."
|
||||
}
|
||||
},
|
||||
"language": "Sprache"
|
||||
},
|
||||
"user_dm": {
|
||||
"users": "Benutzer",
|
||||
"logs": "Protokolle",
|
||||
"backups": "Backups",
|
||||
"auth_providers": "Authentifizierungsanbieter",
|
||||
"log_out": "Abmelden"
|
||||
},
|
||||
"themes": {
|
||||
"toggle_theme": "Thema wechseln",
|
||||
"light": "Hell",
|
||||
"dark": "Dunkel",
|
||||
"system": "System"
|
||||
},
|
||||
"add_system": {
|
||||
"add_new_system": "Neues System hinzufügen",
|
||||
"binary": "Binär",
|
||||
"dialog_des_1": "Der Agent muss auf dem System laufen, um eine Verbindung herzustellen. Kopieren Sie den",
|
||||
"dialog_des_2": "für den Agenten unten.",
|
||||
"name": "Name",
|
||||
"host_ip": "Host / IP",
|
||||
"port": "Port",
|
||||
"public_key": "Öffentlicher Schlüssel",
|
||||
"click_to_copy": "Zum Kopieren klicken",
|
||||
"command": "Befehl",
|
||||
"add_system": "System hinzufügen"
|
||||
},
|
||||
"command": {
|
||||
"search": "Nach Systemen oder Einstellungen suchen...",
|
||||
"pages_settings": "Seiten / Einstellungen",
|
||||
"dashboard": "Dashboard",
|
||||
"documentation": "Dokumentation",
|
||||
"SMTP_settings": "SMTP-Einstellungen",
|
||||
"page": "Seite",
|
||||
"admin": "Admin"
|
||||
},
|
||||
"monitor": {
|
||||
"toggle_grid": "Raster umschalten",
|
||||
"average": "Durchschnitt",
|
||||
"max_1_min": "Max 1 Min",
|
||||
"total_cpu_usage": "Gesamte CPU-Auslastung",
|
||||
"cpu_des": "Systemweite CPU-Auslastung",
|
||||
"docker_cpu_usage": "Docker-CPU-Auslastung",
|
||||
"docker_cpu_des": "Durchschnittliche CPU-Auslastung der Container",
|
||||
"total_memory_usage": "Gesamte Speicherauslastung",
|
||||
"memory_des": "Genaue Nutzung zum aufgezeichneten Zeitpunkt",
|
||||
"docker_memory_usage": "Docker-Speicherauslastung",
|
||||
"docker_memory_des": "Speichernutzung der Docker-Container",
|
||||
"disk_space": "Festplattenspeicher",
|
||||
"disk_des": "Nutzung der Root-Partition",
|
||||
"disk_io": "Festplatten-I/O",
|
||||
"disk_io_des": "Durchsatz des Root-Dateisystems",
|
||||
"bandwidth": "Bandbreite",
|
||||
"bandwidth_des": "Netzwerkverkehr der öffentlichen Schnittstellen",
|
||||
"docker_network_io": "Docker-Netzwerk-I/O",
|
||||
"docker_network_io_des": "Netzwerkverkehr der Docker-Container",
|
||||
"swap_usage": "Swap-Nutzung",
|
||||
"swap_des": "Vom System genutzter Swap-Speicher",
|
||||
"temperature": "Temperatur",
|
||||
"temperature_des": "Temperaturen der System-Sensoren",
|
||||
"usage": "Nutzung",
|
||||
"disk_usage_of": "Festplattennutzung von",
|
||||
"throughput_of": "Durchsatz von",
|
||||
"waiting_for": "Warten auf genügend Datensätze zur Anzeige"
|
||||
},
|
||||
"auth": {
|
||||
"login": "Bitte melden Sie sich bei Ihrem Konto an",
|
||||
"reset": "E-Mail-Adresse eingeben, um das Passwort zurückzusetzen",
|
||||
"create": "Bitte erstellen Sie ein Administratorkonto",
|
||||
"create_account": "Konto erstellen",
|
||||
"sign_in": "Anmelden",
|
||||
"openid_des": "Beszel unterstützt OpenID Connect und viele OAuth2-Authentifizierungsanbieter.",
|
||||
"please_view_the": "Bitte sehen Sie sich die",
|
||||
"for_instructions": "für Anweisungen an.",
|
||||
"forgot_password": "Passwort vergessen?",
|
||||
"reset_password": "Passwort zurücksetzen",
|
||||
"command_line_instructions": "Befehlszeilenanweisungen",
|
||||
"command_1": "Wenn Sie das Passwort für Ihr Administratorkonto verloren haben, können Sie es mit dem folgenden Befehl zurücksetzen.",
|
||||
"command_2": "Melden Sie sich dann im Backend an und setzen Sie das Passwort Ihres Benutzerkontos in der Benutzertabelle zurück."
|
||||
}
|
||||
}
|
||||
"all_systems": "Alle Systeme",
|
||||
"filter": "Filtern...",
|
||||
"copy": "Kopieren",
|
||||
"add": "Hinzufügen",
|
||||
"system": "System",
|
||||
"systems": "Systeme",
|
||||
"cancel": "Abbrechen",
|
||||
"continue": "Fortsetzen",
|
||||
"home": {
|
||||
"active_alerts": "Aktive Warnungen",
|
||||
"active_des": "Überschreitet {{value}}{{unit}} Durchschnitt in den letzten {{minutes}} Minuten",
|
||||
"subtitle_1": "In Echtzeit aktualisiert. Drücken Sie",
|
||||
"subtitle_2": "um die Befehlsübersicht zu öffnen."
|
||||
},
|
||||
"systems_table": {
|
||||
"system": "System",
|
||||
"memory": "Speicher",
|
||||
"cpu": "CPU",
|
||||
"disk": "Festplatte",
|
||||
"net": "Netzwerk",
|
||||
"agent": "Agent",
|
||||
"no_systems_found": "Keine Systeme gefunden.",
|
||||
"open_menu": "Menü öffnen",
|
||||
"resume": "Fortsetzen",
|
||||
"pause": "Pause",
|
||||
"copy_host": "Host kopieren",
|
||||
"delete": "Löschen",
|
||||
"delete_confirm": "Sind Sie sicher, dass Sie {{name}} löschen möchten?",
|
||||
"delete_confirm_des_1": "Diese Aktion kann nicht rückgängig gemacht werden. Dies wird alle aktuellen Aufzeichnungen für",
|
||||
"delete_confirm_des_2": "dauerhaft aus der Datenbank löschen."
|
||||
},
|
||||
"alerts": {
|
||||
"title": "Warnungen",
|
||||
"subtitle_1": "Siehe",
|
||||
"notification_settings": "Benachrichtigungseinstellungen",
|
||||
"subtitle_2": "um zu konfigurieren, wie Sie Warnungen erhalten.",
|
||||
"overwrite_existing_alerts": "Bestehende Warnungen überschreiben",
|
||||
"info": {
|
||||
"status": "Status",
|
||||
"status_des": "Löst aus, wenn der Status zwischen oben und unten wechselt.",
|
||||
"cpu_usage": "CPU-Auslastung",
|
||||
"cpu_usage_des": "Löst aus, wenn die CPU-Auslastung einen Schwellenwert überschreitet.",
|
||||
"memory_usage": "Speicherauslastung",
|
||||
"memory_usage_des": "Löst aus, wenn die Speicherauslastung einen Schwellenwert überschreitet.",
|
||||
"disk_usage": "Festplattennutzung",
|
||||
"disk_usage_des": "Löst aus, wenn die Nutzung einer Festplatte einen Schwellenwert überschreitet.",
|
||||
"bandwidth": "Bandbreite",
|
||||
"bandwidth_des": "Löst aus, wenn die kombinierte Auf-/Abwärtsbandbreite einen Schwellenwert überschreitet.",
|
||||
"temperature": "Temperatur",
|
||||
"temperature_des": "Löst aus, wenn ein Sensor einen Schwellenwert überschreitet."
|
||||
},
|
||||
"average_exceeds": "Durchschnitt überschreitet",
|
||||
"for": "Für",
|
||||
"minute": "Minute",
|
||||
"minutes": "Minuten"
|
||||
},
|
||||
"settings": {
|
||||
"settings": "Einstellungen",
|
||||
"subtitle": "Anzeige- und Benachrichtigungseinstellungen verwalten.",
|
||||
"save_settings": "Einstellungen speichern",
|
||||
"export_configuration": "Konfiguration exportieren",
|
||||
"general": {
|
||||
"title": "Allgemein",
|
||||
"subtitle": "Allgemeine Anwendungsoptionen ändern.",
|
||||
"language": {
|
||||
"title": "Sprache",
|
||||
"subtitle_1": "Möchten Sie uns helfen, unsere Übersetzungen noch besser zu machen? Schauen Sie sich",
|
||||
"subtitle_2": "für weitere Details an.",
|
||||
"preferred_language": "Bevorzugte Sprache"
|
||||
},
|
||||
"chart_options": {
|
||||
"title": "Diagrammoptionen",
|
||||
"subtitle": "Anzeigeoptionen für Diagramme anpassen.",
|
||||
"default_time_period": "Standardzeitraum",
|
||||
"default_time_period_des": "Legt den Standardzeitraum für Diagramme fest, wenn ein System angezeigt wird."
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Benachrichtigungen",
|
||||
"subtitle_1": "Konfigurieren Sie, wie Sie Warnbenachrichtigungen erhalten.",
|
||||
"subtitle_2": "Suchen Sie stattdessen nach dem Ort, an dem Sie Warnungen erstellen können? Klicken Sie auf die Glocke",
|
||||
"subtitle_3": "Symbole in der Systemtabelle.",
|
||||
"email": {
|
||||
"title": "E-Mail-Benachrichtigungen",
|
||||
"please": "Bitte",
|
||||
"configure_an_SMTP_server": "konfigurieren Sie einen SMTP-Server",
|
||||
"to_ensure_alerts_are_delivered": "um sicherzustellen, dass Warnungen zugestellt werden.",
|
||||
"to_email_s": "An E-Mail(s)",
|
||||
"enter_email_address": "E-Mail-Adresse eingeben...",
|
||||
"des": "Adresse mit der Eingabetaste oder dem Komma speichern. Leer lassen, um E-Mail-Benachrichtigungen zu deaktivieren."
|
||||
},
|
||||
"webhook_push": {
|
||||
"title": "Webhook / Push-Benachrichtigungen",
|
||||
"des_1": "Beszel verwendet",
|
||||
"des_2": "um sich mit beliebten Benachrichtigungsdiensten zu integrieren.",
|
||||
"add_url": "URL hinzufügen"
|
||||
}
|
||||
},
|
||||
"yaml_config": {
|
||||
"short_title": "YAML-Konfig",
|
||||
"title": "YAML-Konfiguration",
|
||||
"subtitle": "Aktuelle Systemkonfiguration exportieren.",
|
||||
"des_1": "Systeme können in einer",
|
||||
"des_2": "Datei im Datenverzeichnis verwaltet werden.",
|
||||
"des_3": "Bei jedem Neustart werden die Systeme in der Datenbank aktualisiert, um den in der Datei definierten Systemen zu entsprechen.",
|
||||
"alert": {
|
||||
"title": "Achtung - potenzieller Datenverlust",
|
||||
"des_1": "Bestehende Systeme, die nicht in",
|
||||
"des_2": "definiert sind, werden gelöscht. Bitte machen Sie regelmäßige Backups."
|
||||
}
|
||||
},
|
||||
"language": "Sprache"
|
||||
},
|
||||
"user_dm": {
|
||||
"users": "Benutzer",
|
||||
"logs": "Protokolle",
|
||||
"backups": "Backups",
|
||||
"auth_providers": "Authentifizierungsanbieter",
|
||||
"log_out": "Abmelden"
|
||||
},
|
||||
"themes": {
|
||||
"toggle_theme": "Thema wechseln",
|
||||
"light": "Hell",
|
||||
"dark": "Dunkel",
|
||||
"system": "System"
|
||||
},
|
||||
"add_system": {
|
||||
"add_new_system": "Neues System hinzufügen",
|
||||
"binary": "Binär",
|
||||
"dialog_des_1": "Der Agent muss auf dem System laufen, um eine Verbindung herzustellen. Kopieren Sie den",
|
||||
"dialog_des_2": "für den Agenten unten.",
|
||||
"name": "Name",
|
||||
"host_ip": "Host / IP",
|
||||
"port": "Port",
|
||||
"public_key": "Öffentlicher Schlüssel",
|
||||
"click_to_copy": "Zum Kopieren klicken",
|
||||
"command": "Befehl",
|
||||
"add_system": "System hinzufügen"
|
||||
},
|
||||
"command": {
|
||||
"search": "Nach Systemen oder Einstellungen suchen...",
|
||||
"pages_settings": "Seiten / Einstellungen",
|
||||
"dashboard": "Dashboard",
|
||||
"documentation": "Dokumentation",
|
||||
"SMTP_settings": "SMTP-Einstellungen",
|
||||
"page": "Seite",
|
||||
"admin": "Admin"
|
||||
},
|
||||
"monitor": {
|
||||
"toggle_grid": "Raster umschalten",
|
||||
"average": "Durchschnitt",
|
||||
"max_1_min": "Max 1 Min",
|
||||
"total_cpu_usage": "Gesamte CPU-Auslastung",
|
||||
"cpu_des": "Systemweite CPU-Auslastung",
|
||||
"docker_cpu_usage": "Docker-CPU-Auslastung",
|
||||
"docker_cpu_des": "Durchschnittliche CPU-Auslastung der Container",
|
||||
"total_memory_usage": "Gesamte Speicherauslastung",
|
||||
"memory_des": "Genaue Nutzung zum aufgezeichneten Zeitpunkt",
|
||||
"docker_memory_usage": "Docker-Speicherauslastung",
|
||||
"docker_memory_des": "Speichernutzung der Docker-Container",
|
||||
"disk_space": "Festplattenspeicher",
|
||||
"disk_des": "Nutzung der Root-Partition",
|
||||
"disk_io": "Festplatten-I/O",
|
||||
"disk_io_des": "Durchsatz des Root-Dateisystems",
|
||||
"bandwidth": "Bandbreite",
|
||||
"bandwidth_des": "Netzwerkverkehr der öffentlichen Schnittstellen",
|
||||
"docker_network_io": "Docker-Netzwerk-I/O",
|
||||
"docker_network_io_des": "Netzwerkverkehr der Docker-Container",
|
||||
"swap_usage": "Swap-Nutzung",
|
||||
"swap_des": "Vom System genutzter Swap-Speicher",
|
||||
"temperature": "Temperatur",
|
||||
"temperature_des": "Temperaturen der System-Sensoren",
|
||||
"usage": "Nutzung",
|
||||
"disk_usage_of": "Festplattennutzung von",
|
||||
"throughput_of": "Durchsatz von",
|
||||
"waiting_for": "Warten auf genügend Datensätze zur Anzeige"
|
||||
},
|
||||
"auth": {
|
||||
"login": "Bitte melden Sie sich bei Ihrem Konto an",
|
||||
"reset": "E-Mail-Adresse eingeben, um das Passwort zurückzusetzen",
|
||||
"create": "Bitte erstellen Sie ein Administratorkonto",
|
||||
"create_account": "Konto erstellen",
|
||||
"sign_in": "Anmelden",
|
||||
"openid_des": "Beszel unterstützt OpenID Connect und viele OAuth2-Authentifizierungsanbieter.",
|
||||
"please_view_the": "Bitte sehen Sie sich die",
|
||||
"for_instructions": "für Anweisungen an.",
|
||||
"forgot_password": "Passwort vergessen?",
|
||||
"reset_password": "Passwort zurücksetzen",
|
||||
"command_line_instructions": "Befehlszeilenanweisungen",
|
||||
"command_1": "Wenn Sie das Passwort für Ihr Administratorkonto verloren haben, können Sie es mit dem folgenden Befehl zurücksetzen.",
|
||||
"command_2": "Melden Sie sich dann im Backend an und setzen Sie das Passwort Ihres Benutzerkontos in der Benutzertabelle zurück."
|
||||
}
|
||||
}
|
||||
|
@@ -1,193 +1,193 @@
|
||||
{
|
||||
"all_systems": "All Systems",
|
||||
"filter": "Filter...",
|
||||
"copy": "Copy",
|
||||
"add": "Add",
|
||||
"system": "System",
|
||||
"systems": "Systems",
|
||||
"cancel": "Cancel",
|
||||
"continue": "Continue",
|
||||
"home": {
|
||||
"active_alerts": "Active Alerts",
|
||||
"active_des": "Exceeds {{value}}{{unit}} average in last {{minutes}} minutes",
|
||||
"subtitle_1": "Updated in real time. Click on a system to view information."
|
||||
},
|
||||
"systems_table": {
|
||||
"system": "System",
|
||||
"memory": "Memory",
|
||||
"cpu": "CPU",
|
||||
"disk": "Disk",
|
||||
"net": "Net",
|
||||
"agent": "Agent",
|
||||
"no_systems_found": "No systems found.",
|
||||
"open_menu": "Open menu",
|
||||
"resume": "Resume",
|
||||
"pause": "Pause",
|
||||
"copy_host": "Copy host",
|
||||
"delete": "Delete",
|
||||
"delete_confirm": "Are you sure you want to delete {{name}}?",
|
||||
"delete_confirm_des_1": "This action cannot be undone. This will permanently delete all current records for",
|
||||
"delete_confirm_des_2": "from the database."
|
||||
},
|
||||
"alerts": {
|
||||
"title": "Alerts",
|
||||
"subtitle_1": "See",
|
||||
"notification_settings": "notification settings",
|
||||
"subtitle_2": "to configure how you receive alerts.",
|
||||
"overwrite_existing_alerts": "Overwrite existing alerts",
|
||||
"info": {
|
||||
"status": "Status",
|
||||
"status_des": "Triggers when status switches between up and down.",
|
||||
"cpu_usage": "CPU Usage",
|
||||
"cpu_usage_des": "Triggers when CPU usage exceeds a threshold.",
|
||||
"memory_usage": "Memory Usage",
|
||||
"memory_usage_des": "Triggers when memory usage exceeds a threshold.",
|
||||
"disk_usage": "Disk Usage",
|
||||
"disk_usage_des": "Triggers when usage of any disk exceeds a threshold.",
|
||||
"bandwidth": "Bandwidth",
|
||||
"bandwidth_des": "Triggers when combined up/down exceeds a threshold.",
|
||||
"temperature": "Temperature",
|
||||
"temperature_des": "Triggers when any sensor exceeds a threshold."
|
||||
},
|
||||
"average_exceeds": "Average exceeds",
|
||||
"for": "For",
|
||||
"minute": "minute",
|
||||
"minutes": "minutes"
|
||||
},
|
||||
"settings": {
|
||||
"settings": "Settings",
|
||||
"subtitle": "Manage display and notification preferences.",
|
||||
"save_settings": "Save Settings",
|
||||
"export_configuration": "Export configuration",
|
||||
"general": {
|
||||
"title": "General",
|
||||
"subtitle": "Change general application options.",
|
||||
"language": {
|
||||
"title": "Language",
|
||||
"subtitle_1": "Want to help us make our translations even better? Check out",
|
||||
"subtitle_2": "for more details.",
|
||||
"preferred_language": "Preferred Language"
|
||||
},
|
||||
"chart_options": {
|
||||
"title": "Chart options",
|
||||
"subtitle": "Adjust display options for charts.",
|
||||
"default_time_period": "Default time period",
|
||||
"default_time_period_des": "Sets the default time range for charts when a system is viewed."
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Notifications",
|
||||
"subtitle_1": "Configure how you receive alert notifications.",
|
||||
"subtitle_2": "Looking instead for where to create alerts? Click the bell",
|
||||
"subtitle_3": "icons in the systems table.",
|
||||
"email": {
|
||||
"title": "Email notifications",
|
||||
"please": "Please",
|
||||
"configure_an_SMTP_server": "configure an SMTP server",
|
||||
"to_ensure_alerts_are_delivered": "to ensure alerts are delivered.",
|
||||
"to_email_s": "To email(s)",
|
||||
"enter_email_address": "Enter email address...",
|
||||
"des": "Save address using enter key or comma. Leave blank to disable email notifications."
|
||||
},
|
||||
"webhook_push": {
|
||||
"title": "Webhook / Push notifications",
|
||||
"des_1": "Beszel uses",
|
||||
"des_2": "to integrate with popular notification services.",
|
||||
"add_url": "Add URL"
|
||||
}
|
||||
},
|
||||
"yaml_config": {
|
||||
"short_title": "YAML Config",
|
||||
"title": "YAML Configuration",
|
||||
"subtitle": "Export your current systems configuration.",
|
||||
"des_1": "Systems may be managed in a",
|
||||
"des_2": "file inside your data directory.",
|
||||
"des_3": "On each restart, systems in the database will be updated to match the systems defined in the file.",
|
||||
"alert": {
|
||||
"title": "Caution - potential data loss",
|
||||
"des_1": "Existing systems not defined in",
|
||||
"des_2": "will be deleted. Please make regular backups."
|
||||
}
|
||||
},
|
||||
"language": "Language"
|
||||
},
|
||||
"user_dm": {
|
||||
"users": "Users",
|
||||
"logs": "Logs",
|
||||
"backups": "Backups",
|
||||
"auth_providers": "Auth Providers",
|
||||
"log_out": "Log Out"
|
||||
},
|
||||
"themes": {
|
||||
"toggle_theme": "Toggle theme",
|
||||
"light": "Light",
|
||||
"dark": "Dark",
|
||||
"system": "System"
|
||||
},
|
||||
"add_system": {
|
||||
"add_new_system": "Add New System",
|
||||
"binary": "Binary",
|
||||
"dialog_des_1": "The agent must be running on the system to connect. Copy the",
|
||||
"dialog_des_2": "for the agent below.",
|
||||
"name": "Name",
|
||||
"host_ip": "Host / IP",
|
||||
"port": "Port",
|
||||
"public_key": "Public Key",
|
||||
"click_to_copy": "Click to copy",
|
||||
"command": "command",
|
||||
"add_system": "Add system"
|
||||
},
|
||||
"command": {
|
||||
"search": "Search for systems or settings...",
|
||||
"pages_settings": "Pages / Settings",
|
||||
"dashboard": "Dashboard",
|
||||
"documentation": "Documentation",
|
||||
"SMTP_settings": "SMTP settings",
|
||||
"page": "Page",
|
||||
"admin": "Admin"
|
||||
},
|
||||
"monitor": {
|
||||
"toggle_grid": "Toggle grid",
|
||||
"average": "Average",
|
||||
"max_1_min": "Max 1 min ",
|
||||
"total_cpu_usage": "Total CPU Usage",
|
||||
"cpu_des": "system-wide CPU utilization",
|
||||
"docker_cpu_usage": "Docker CPU Usage",
|
||||
"docker_cpu_des": "Average CPU utilization of containers",
|
||||
"total_memory_usage": "Total Memory Usage",
|
||||
"memory_des": "Precise utilization at the recorded time",
|
||||
"docker_memory_usage": "Docker Memory Usage",
|
||||
"docker_memory_des": "Memory usage of docker containers",
|
||||
"disk_space": "Disk Space",
|
||||
"disk_des": "Usage of root partition",
|
||||
"disk_io": "Disk I/O",
|
||||
"disk_io_des": "Throughput of root filesystem",
|
||||
"bandwidth": "Bandwidth",
|
||||
"bandwidth_des": "Network traffic of public interfaces",
|
||||
"docker_network_io": "Docker Network I/O",
|
||||
"docker_network_io_des": "Network traffic of docker containers",
|
||||
"swap_usage": "Swap Usage",
|
||||
"swap_des": "Swap space used by the system",
|
||||
"temperature": "Temperature",
|
||||
"temperature_des": "Temperatures of system sensors",
|
||||
"usage": "Usage",
|
||||
"disk_usage_of": "Disk usage of",
|
||||
"throughput_of": "Throughput of",
|
||||
"waiting_for": "Waiting for enough records to display"
|
||||
},
|
||||
"auth": {
|
||||
"login": "Please sign in to your account",
|
||||
"reset": "Enter email address to reset password",
|
||||
"create": "Please create an admin account",
|
||||
"create_account": "Create account",
|
||||
"sign_in": "Sign in",
|
||||
"openid_des": "Beszel supports OpenID Connect and many OAuth2 authentication providers.",
|
||||
"please_view_the": "Please view the",
|
||||
"for_instructions": "for instructions.",
|
||||
"forgot_password": "Forgot password?",
|
||||
"reset_password": "Reset Password",
|
||||
"command_line_instructions": "Command line instructions",
|
||||
"command_1": "If you've lost the password to your admin account, you may reset it using the following command.",
|
||||
"command_2": "Then log into the backend and reset your user account password in the users table."
|
||||
}
|
||||
"all_systems": "All Systems",
|
||||
"filter": "Filter...",
|
||||
"copy": "Copy",
|
||||
"add": "Add",
|
||||
"system": "System",
|
||||
"systems": "Systems",
|
||||
"cancel": "Cancel",
|
||||
"continue": "Continue",
|
||||
"home": {
|
||||
"active_alerts": "Active Alerts",
|
||||
"active_des": "Exceeds {{value}}{{unit}} average in last {{minutes}} minutes",
|
||||
"subtitle_1": "Updated in real time. Click on a system to view information."
|
||||
},
|
||||
"systems_table": {
|
||||
"system": "System",
|
||||
"memory": "Memory",
|
||||
"cpu": "CPU",
|
||||
"disk": "Disk",
|
||||
"net": "Net",
|
||||
"agent": "Agent",
|
||||
"no_systems_found": "No systems found.",
|
||||
"open_menu": "Open menu",
|
||||
"resume": "Resume",
|
||||
"pause": "Pause",
|
||||
"copy_host": "Copy host",
|
||||
"delete": "Delete",
|
||||
"delete_confirm": "Are you sure you want to delete {{name}}?",
|
||||
"delete_confirm_des_1": "This action cannot be undone. This will permanently delete all current records for",
|
||||
"delete_confirm_des_2": "from the database."
|
||||
},
|
||||
"alerts": {
|
||||
"title": "Alerts",
|
||||
"subtitle_1": "See",
|
||||
"notification_settings": "notification settings",
|
||||
"subtitle_2": "to configure how you receive alerts.",
|
||||
"overwrite_existing_alerts": "Overwrite existing alerts",
|
||||
"info": {
|
||||
"status": "Status",
|
||||
"status_des": "Triggers when status switches between up and down.",
|
||||
"cpu_usage": "CPU Usage",
|
||||
"cpu_usage_des": "Triggers when CPU usage exceeds a threshold.",
|
||||
"memory_usage": "Memory Usage",
|
||||
"memory_usage_des": "Triggers when memory usage exceeds a threshold.",
|
||||
"disk_usage": "Disk Usage",
|
||||
"disk_usage_des": "Triggers when usage of any disk exceeds a threshold.",
|
||||
"bandwidth": "Bandwidth",
|
||||
"bandwidth_des": "Triggers when combined up/down exceeds a threshold.",
|
||||
"temperature": "Temperature",
|
||||
"temperature_des": "Triggers when any sensor exceeds a threshold."
|
||||
},
|
||||
"average_exceeds": "Average exceeds",
|
||||
"for": "For",
|
||||
"minute": "minute",
|
||||
"minutes": "minutes"
|
||||
},
|
||||
"settings": {
|
||||
"settings": "Settings",
|
||||
"subtitle": "Manage display and notification preferences.",
|
||||
"save_settings": "Save Settings",
|
||||
"export_configuration": "Export configuration",
|
||||
"general": {
|
||||
"title": "General",
|
||||
"subtitle": "Change general application options.",
|
||||
"language": {
|
||||
"title": "Language",
|
||||
"subtitle_1": "Want to help us make our translations even better? Check out",
|
||||
"subtitle_2": "for more details.",
|
||||
"preferred_language": "Preferred Language"
|
||||
},
|
||||
"chart_options": {
|
||||
"title": "Chart options",
|
||||
"subtitle": "Adjust display options for charts.",
|
||||
"default_time_period": "Default time period",
|
||||
"default_time_period_des": "Sets the default time range for charts when a system is viewed."
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Notifications",
|
||||
"subtitle_1": "Configure how you receive alert notifications.",
|
||||
"subtitle_2": "Looking instead for where to create alerts? Click the bell",
|
||||
"subtitle_3": "icons in the systems table.",
|
||||
"email": {
|
||||
"title": "Email notifications",
|
||||
"please": "Please",
|
||||
"configure_an_SMTP_server": "configure an SMTP server",
|
||||
"to_ensure_alerts_are_delivered": "to ensure alerts are delivered.",
|
||||
"to_email_s": "To email(s)",
|
||||
"enter_email_address": "Enter email address...",
|
||||
"des": "Save address using enter key or comma. Leave blank to disable email notifications."
|
||||
},
|
||||
"webhook_push": {
|
||||
"title": "Webhook / Push notifications",
|
||||
"des_1": "Beszel uses",
|
||||
"des_2": "to integrate with popular notification services.",
|
||||
"add_url": "Add URL"
|
||||
}
|
||||
},
|
||||
"yaml_config": {
|
||||
"short_title": "YAML Config",
|
||||
"title": "YAML Configuration",
|
||||
"subtitle": "Export your current systems configuration.",
|
||||
"des_1": "Systems may be managed in a",
|
||||
"des_2": "file inside your data directory.",
|
||||
"des_3": "On each restart, systems in the database will be updated to match the systems defined in the file.",
|
||||
"alert": {
|
||||
"title": "Caution - potential data loss",
|
||||
"des_1": "Existing systems not defined in",
|
||||
"des_2": "will be deleted. Please make regular backups."
|
||||
}
|
||||
},
|
||||
"language": "Language"
|
||||
},
|
||||
"user_dm": {
|
||||
"users": "Users",
|
||||
"logs": "Logs",
|
||||
"backups": "Backups",
|
||||
"auth_providers": "Auth Providers",
|
||||
"log_out": "Log Out"
|
||||
},
|
||||
"themes": {
|
||||
"toggle_theme": "Toggle theme",
|
||||
"light": "Light",
|
||||
"dark": "Dark",
|
||||
"system": "System"
|
||||
},
|
||||
"add_system": {
|
||||
"add_new_system": "Add New System",
|
||||
"binary": "Binary",
|
||||
"dialog_des_1": "The agent must be running on the system to connect. Copy the",
|
||||
"dialog_des_2": "for the agent below.",
|
||||
"name": "Name",
|
||||
"host_ip": "Host / IP",
|
||||
"port": "Port",
|
||||
"public_key": "Public Key",
|
||||
"click_to_copy": "Click to copy",
|
||||
"command": "command",
|
||||
"add_system": "Add system"
|
||||
},
|
||||
"command": {
|
||||
"search": "Search for systems or settings...",
|
||||
"pages_settings": "Pages / Settings",
|
||||
"dashboard": "Dashboard",
|
||||
"documentation": "Documentation",
|
||||
"SMTP_settings": "SMTP settings",
|
||||
"page": "Page",
|
||||
"admin": "Admin"
|
||||
},
|
||||
"monitor": {
|
||||
"toggle_grid": "Toggle grid",
|
||||
"average": "Average",
|
||||
"max_1_min": "Max 1 min ",
|
||||
"total_cpu_usage": "Total CPU Usage",
|
||||
"cpu_des": "system-wide CPU utilization",
|
||||
"docker_cpu_usage": "Docker CPU Usage",
|
||||
"docker_cpu_des": "Average CPU utilization of containers",
|
||||
"total_memory_usage": "Total Memory Usage",
|
||||
"memory_des": "Precise utilization at the recorded time",
|
||||
"docker_memory_usage": "Docker Memory Usage",
|
||||
"docker_memory_des": "Memory usage of docker containers",
|
||||
"disk_space": "Disk Space",
|
||||
"disk_des": "Usage of root partition",
|
||||
"disk_io": "Disk I/O",
|
||||
"disk_io_des": "Throughput of root filesystem",
|
||||
"bandwidth": "Bandwidth",
|
||||
"bandwidth_des": "Network traffic of public interfaces",
|
||||
"docker_network_io": "Docker Network I/O",
|
||||
"docker_network_io_des": "Network traffic of docker containers",
|
||||
"swap_usage": "Swap Usage",
|
||||
"swap_des": "Swap space used by the system",
|
||||
"temperature": "Temperature",
|
||||
"temperature_des": "Temperatures of system sensors",
|
||||
"usage": "Usage",
|
||||
"disk_usage_of": "Disk usage of",
|
||||
"throughput_of": "Throughput of",
|
||||
"waiting_for": "Waiting for enough records to display"
|
||||
},
|
||||
"auth": {
|
||||
"login": "Please sign in to your account",
|
||||
"reset": "Enter email address to reset password",
|
||||
"create": "Please create an admin account",
|
||||
"create_account": "Create account",
|
||||
"sign_in": "Sign in",
|
||||
"openid_des": "Beszel supports OpenID Connect and many OAuth2 authentication providers.",
|
||||
"please_view_the": "Please view the",
|
||||
"for_instructions": "for instructions.",
|
||||
"forgot_password": "Forgot password?",
|
||||
"reset_password": "Reset Password",
|
||||
"command_line_instructions": "Command line instructions",
|
||||
"command_1": "If you've lost the password to your admin account, you may reset it using the following command.",
|
||||
"command_2": "Then log into the backend and reset your user account password in the users table."
|
||||
}
|
||||
}
|
||||
|
@@ -1,194 +1,194 @@
|
||||
{
|
||||
"all_systems": "Todos los sistemas",
|
||||
"filter": "Filtrar...",
|
||||
"copy": "Copiar",
|
||||
"add": "Agregar",
|
||||
"system": "Sistema",
|
||||
"systems": "Sistemas",
|
||||
"cancel": "Cancelar",
|
||||
"continue": "Continuar",
|
||||
"home": {
|
||||
"active_alerts": "Alertas activas",
|
||||
"active_des": "Excede el promedio de {{value}}{{unit}} en los últimos {{minutes}} minutos",
|
||||
"subtitle_1": "Actualizado en tiempo real. Presione",
|
||||
"subtitle_2": "para abrir la paleta de comandos."
|
||||
},
|
||||
"systems_table": {
|
||||
"system": "Sistema",
|
||||
"memory": "Memoria",
|
||||
"cpu": "CPU",
|
||||
"disk": "Disco",
|
||||
"net": "Red",
|
||||
"agent": "Agente",
|
||||
"no_systems_found": "No se encontraron sistemas.",
|
||||
"open_menu": "Abrir menú",
|
||||
"resume": "Reanudar",
|
||||
"pause": "Pausar",
|
||||
"copy_host": "Copiar host",
|
||||
"delete": "Eliminar",
|
||||
"delete_confirm": "¿Estás seguro de que quieres eliminar {{name}}?",
|
||||
"delete_confirm_des_1": "Esta acción no se puede deshacer. Esto eliminará permanentemente todos los registros actuales para",
|
||||
"delete_confirm_des_2": "de la base de datos."
|
||||
},
|
||||
"alerts": {
|
||||
"title": "Alertas",
|
||||
"subtitle_1": "Ver",
|
||||
"notification_settings": "configuraciones de notificación",
|
||||
"subtitle_2": "para configurar cómo recibes las alertas.",
|
||||
"overwrite_existing_alerts": "Sobrescribir alertas existentes",
|
||||
"info": {
|
||||
"status": "Estado",
|
||||
"status_des": "Se activa cuando el estado cambia entre arriba y abajo.",
|
||||
"cpu_usage": "Uso de CPU",
|
||||
"cpu_usage_des": "Se activa cuando el uso de la CPU supera un umbral.",
|
||||
"memory_usage": "Uso de memoria",
|
||||
"memory_usage_des": "Se activa cuando el uso de la memoria supera un umbral.",
|
||||
"disk_usage": "Uso de disco",
|
||||
"disk_usage_des": "Se activa cuando el uso de cualquier disco supera un umbral.",
|
||||
"bandwidth": "Ancho de banda",
|
||||
"bandwidth_des": "Se activa cuando el combinado arriba/abajo supera un umbral.",
|
||||
"temperature": "Temperatura",
|
||||
"temperature_des": "Se activa cuando cualquier sensor supera un umbral."
|
||||
},
|
||||
"average_exceeds": "Promedio excede",
|
||||
"for": "Por",
|
||||
"minute": "minuto",
|
||||
"minutes": "minutos"
|
||||
},
|
||||
"settings": {
|
||||
"settings": "Configuraciones",
|
||||
"subtitle": "Administre las preferencias de visualización y notificación.",
|
||||
"save_settings": "Guardar configuraciones",
|
||||
"export_configuration": "Exportar configuración",
|
||||
"general": {
|
||||
"title": "General",
|
||||
"subtitle": "Cambie las opciones generales de la aplicación.",
|
||||
"language": {
|
||||
"title": "Idioma",
|
||||
"subtitle_1": "¿Quiere ayudarnos a mejorar nuestras traducciones? Consulte",
|
||||
"subtitle_2": "para más detalles.",
|
||||
"preferred_language": "Idioma preferido"
|
||||
},
|
||||
"chart_options": {
|
||||
"title": "Opciones de gráficos",
|
||||
"subtitle": "Ajuste las opciones de visualización para los gráficos.",
|
||||
"default_time_period": "Período de tiempo predeterminado",
|
||||
"default_time_period_des": "Establezca el rango de tiempo predeterminado para los gráficos cuando se visualiza un sistema."
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Notificaciones",
|
||||
"subtitle_1": "Configure cómo recibe las notificaciones de alerta.",
|
||||
"subtitle_2": "¿Busca en su lugar dónde crear alertas? Haga clic en el icono de campana",
|
||||
"subtitle_3": "en la tabla de sistemas.",
|
||||
"email": {
|
||||
"title": "Notificaciones por correo electrónico",
|
||||
"please": "Por favor",
|
||||
"configure_an_SMTP_server": "configure un servidor SMTP",
|
||||
"to_ensure_alerts_are_delivered": "para asegurarse de que se entreguen las alertas.",
|
||||
"to_email_s": "A correo electrónico(s)",
|
||||
"enter_email_address": "Ingrese la dirección de correo electrónico...",
|
||||
"des": "Guarde la dirección presionando Enter o usando una coma. Deje en blanco para desactivar las notificaciones por correo electrónico."
|
||||
},
|
||||
"webhook_push": {
|
||||
"title": "Notificaciones Webhook/Push",
|
||||
"des_1": "Beszel utiliza",
|
||||
"des_2": "para integrarse con populares servicios de notificación.",
|
||||
"add_url": "Agregar URL"
|
||||
}
|
||||
},
|
||||
"yaml_config": {
|
||||
"short_title": "Configuración YAML",
|
||||
"title": "Configuración YAML",
|
||||
"subtitle": "Exporta tu configuración actual de sistemas.",
|
||||
"des_1": "Los sistemas pueden gestionarse en un",
|
||||
"des_2": "archivo dentro de tu directorio de datos.",
|
||||
"des_3": "En cada reinicio, los sistemas de la base de datos se actualizarán para coincidir con los sistemas definidos en el archivo.",
|
||||
"alert": {
|
||||
"title": "Advertencia - posible pérdida de datos",
|
||||
"des_1": "Los sistemas existentes no definidos en",
|
||||
"des_2": "serán eliminados. Por favor, haz copias de seguridad regulares."
|
||||
}
|
||||
},
|
||||
"language": "Idioma"
|
||||
},
|
||||
"user_dm": {
|
||||
"users": "Usuarios",
|
||||
"logs": "Registros",
|
||||
"backups": "Respaldos",
|
||||
"auth_providers": "Proveedores de autenticación",
|
||||
"log_out": "Cerrar sesión"
|
||||
},
|
||||
"themes": {
|
||||
"toggle_theme": "Alternar tema",
|
||||
"light": "Claro",
|
||||
"dark": "Oscuro",
|
||||
"system": "Sistema"
|
||||
},
|
||||
"add_system": {
|
||||
"add_new_system": "Agregar nuevo sistema",
|
||||
"binary": "Binario",
|
||||
"dialog_des_1": "El agente debe estar ejecutándose en el sistema para conectarse. Copia el",
|
||||
"dialog_des_2": "para el agente a continuación.",
|
||||
"name": "Nombre",
|
||||
"host_ip": "Host/IP",
|
||||
"port": "Puerto",
|
||||
"public_key": "Clave pública",
|
||||
"click_to_copy": "Haz clic para copiar",
|
||||
"command": "comando",
|
||||
"add_system": "Agregar sistema"
|
||||
},
|
||||
"command": {
|
||||
"search": "Buscar sistemas o configuraciones...",
|
||||
"pages_settings": "Páginas/Configuraciones",
|
||||
"dashboard": "Panel de control",
|
||||
"documentation": "Documentación",
|
||||
"SMTP_settings": "Configuraciones SMTP",
|
||||
"page": "Página",
|
||||
"admin": "Administrador"
|
||||
},
|
||||
"monitor": {
|
||||
"toggle_grid": "Alternar cuadrícula",
|
||||
"average": "Promedio",
|
||||
"max_1_min": "Máx. 1 min ",
|
||||
"total_cpu_usage": "Uso total de CPU",
|
||||
"cpu_des": "Utilización de CPU de todo el sistema",
|
||||
"docker_cpu_usage": "Uso de CPU de Docker",
|
||||
"docker_cpu_des": "Uso promedio de CPU de los contenedores",
|
||||
"total_memory_usage": "Uso total de memoria",
|
||||
"memory_des": "Utilización precisa en el momento registrado",
|
||||
"docker_memory_usage": "Uso de memoria de Docker",
|
||||
"docker_memory_des": "Uso de memoria de los contenedores de Docker",
|
||||
"disk_space": "Espacio en disco",
|
||||
"disk_des": "Uso de la partición raíz",
|
||||
"disk_io": "E/S de disco",
|
||||
"disk_io_des": "Rendimiento de la raíz del sistema de archivos",
|
||||
"bandwidth": "Ancho de banda",
|
||||
"bandwidth_des": "Tráfico de red de interfaces públicas",
|
||||
"docker_network_io": "E/S de red de Docker",
|
||||
"docker_network_io_des": "Tráfico de red de los contenedores de Docker",
|
||||
"swap_usage": "Uso de intercambio",
|
||||
"swap_des": "Espacio de intercambio utilizado por el sistema",
|
||||
"temperature": "Temperatura",
|
||||
"temperature_des": "Temperaturas de los sensores del sistema",
|
||||
"usage": "Uso",
|
||||
"disk_usage_of": "Uso de disco de",
|
||||
"throughput_of": "Rendimiento de",
|
||||
"waiting_for": "Esperando suficientes registros para mostrar"
|
||||
},
|
||||
"auth": {
|
||||
"login": "Por favor, inicie sesión en su cuenta",
|
||||
"reset": "Ingrese la dirección de correo electrónico para restablecer la contraseña",
|
||||
"create": "Por favor, cree una cuenta de administrador",
|
||||
"create_account": "Crear cuenta",
|
||||
"sign_in": "Iniciar sesión",
|
||||
"openid_des": "Beszel admite OpenID Connect y muchos proveedores de autenticación OAuth2.",
|
||||
"please_view_the": "Por favor, consulte el",
|
||||
"for_instructions": "para obtener instrucciones.",
|
||||
"forgot_password": "¿Olvidó su contraseña?",
|
||||
"reset_password": "Restablecer contraseña",
|
||||
"command_line_instructions": "Instrucciones de línea de comandos",
|
||||
"command_1": "Si ha perdido la contraseña de su cuenta de administrador, puede restablecerla usando el siguiente comando.",
|
||||
"command_2": "Luego inicie sesión en el backend y restablezca la contraseña de su cuenta de usuario en la tabla de usuarios."
|
||||
}
|
||||
}
|
||||
"all_systems": "Todos los sistemas",
|
||||
"filter": "Filtrar...",
|
||||
"copy": "Copiar",
|
||||
"add": "Agregar",
|
||||
"system": "Sistema",
|
||||
"systems": "Sistemas",
|
||||
"cancel": "Cancelar",
|
||||
"continue": "Continuar",
|
||||
"home": {
|
||||
"active_alerts": "Alertas activas",
|
||||
"active_des": "Excede el promedio de {{value}}{{unit}} en los últimos {{minutes}} minutos",
|
||||
"subtitle_1": "Actualizado en tiempo real. Presione",
|
||||
"subtitle_2": "para abrir la paleta de comandos."
|
||||
},
|
||||
"systems_table": {
|
||||
"system": "Sistema",
|
||||
"memory": "Memoria",
|
||||
"cpu": "CPU",
|
||||
"disk": "Disco",
|
||||
"net": "Red",
|
||||
"agent": "Agente",
|
||||
"no_systems_found": "No se encontraron sistemas.",
|
||||
"open_menu": "Abrir menú",
|
||||
"resume": "Reanudar",
|
||||
"pause": "Pausar",
|
||||
"copy_host": "Copiar host",
|
||||
"delete": "Eliminar",
|
||||
"delete_confirm": "¿Estás seguro de que quieres eliminar {{name}}?",
|
||||
"delete_confirm_des_1": "Esta acción no se puede deshacer. Esto eliminará permanentemente todos los registros actuales para",
|
||||
"delete_confirm_des_2": "de la base de datos."
|
||||
},
|
||||
"alerts": {
|
||||
"title": "Alertas",
|
||||
"subtitle_1": "Ver",
|
||||
"notification_settings": "configuraciones de notificación",
|
||||
"subtitle_2": "para configurar cómo recibes las alertas.",
|
||||
"overwrite_existing_alerts": "Sobrescribir alertas existentes",
|
||||
"info": {
|
||||
"status": "Estado",
|
||||
"status_des": "Se activa cuando el estado cambia entre arriba y abajo.",
|
||||
"cpu_usage": "Uso de CPU",
|
||||
"cpu_usage_des": "Se activa cuando el uso de la CPU supera un umbral.",
|
||||
"memory_usage": "Uso de memoria",
|
||||
"memory_usage_des": "Se activa cuando el uso de la memoria supera un umbral.",
|
||||
"disk_usage": "Uso de disco",
|
||||
"disk_usage_des": "Se activa cuando el uso de cualquier disco supera un umbral.",
|
||||
"bandwidth": "Ancho de banda",
|
||||
"bandwidth_des": "Se activa cuando el combinado arriba/abajo supera un umbral.",
|
||||
"temperature": "Temperatura",
|
||||
"temperature_des": "Se activa cuando cualquier sensor supera un umbral."
|
||||
},
|
||||
"average_exceeds": "Promedio excede",
|
||||
"for": "Por",
|
||||
"minute": "minuto",
|
||||
"minutes": "minutos"
|
||||
},
|
||||
"settings": {
|
||||
"settings": "Configuraciones",
|
||||
"subtitle": "Administre las preferencias de visualización y notificación.",
|
||||
"save_settings": "Guardar configuraciones",
|
||||
"export_configuration": "Exportar configuración",
|
||||
"general": {
|
||||
"title": "General",
|
||||
"subtitle": "Cambie las opciones generales de la aplicación.",
|
||||
"language": {
|
||||
"title": "Idioma",
|
||||
"subtitle_1": "¿Quiere ayudarnos a mejorar nuestras traducciones? Consulte",
|
||||
"subtitle_2": "para más detalles.",
|
||||
"preferred_language": "Idioma preferido"
|
||||
},
|
||||
"chart_options": {
|
||||
"title": "Opciones de gráficos",
|
||||
"subtitle": "Ajuste las opciones de visualización para los gráficos.",
|
||||
"default_time_period": "Período de tiempo predeterminado",
|
||||
"default_time_period_des": "Establezca el rango de tiempo predeterminado para los gráficos cuando se visualiza un sistema."
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Notificaciones",
|
||||
"subtitle_1": "Configure cómo recibe las notificaciones de alerta.",
|
||||
"subtitle_2": "¿Busca en su lugar dónde crear alertas? Haga clic en el icono de campana",
|
||||
"subtitle_3": "en la tabla de sistemas.",
|
||||
"email": {
|
||||
"title": "Notificaciones por correo electrónico",
|
||||
"please": "Por favor",
|
||||
"configure_an_SMTP_server": "configure un servidor SMTP",
|
||||
"to_ensure_alerts_are_delivered": "para asegurarse de que se entreguen las alertas.",
|
||||
"to_email_s": "A correo electrónico(s)",
|
||||
"enter_email_address": "Ingrese la dirección de correo electrónico...",
|
||||
"des": "Guarde la dirección presionando Enter o usando una coma. Deje en blanco para desactivar las notificaciones por correo electrónico."
|
||||
},
|
||||
"webhook_push": {
|
||||
"title": "Notificaciones Webhook/Push",
|
||||
"des_1": "Beszel utiliza",
|
||||
"des_2": "para integrarse con populares servicios de notificación.",
|
||||
"add_url": "Agregar URL"
|
||||
}
|
||||
},
|
||||
"yaml_config": {
|
||||
"short_title": "Configuración YAML",
|
||||
"title": "Configuración YAML",
|
||||
"subtitle": "Exporta tu configuración actual de sistemas.",
|
||||
"des_1": "Los sistemas pueden gestionarse en un",
|
||||
"des_2": "archivo dentro de tu directorio de datos.",
|
||||
"des_3": "En cada reinicio, los sistemas de la base de datos se actualizarán para coincidir con los sistemas definidos en el archivo.",
|
||||
"alert": {
|
||||
"title": "Advertencia - posible pérdida de datos",
|
||||
"des_1": "Los sistemas existentes no definidos en",
|
||||
"des_2": "serán eliminados. Por favor, haz copias de seguridad regulares."
|
||||
}
|
||||
},
|
||||
"language": "Idioma"
|
||||
},
|
||||
"user_dm": {
|
||||
"users": "Usuarios",
|
||||
"logs": "Registros",
|
||||
"backups": "Respaldos",
|
||||
"auth_providers": "Proveedores de autenticación",
|
||||
"log_out": "Cerrar sesión"
|
||||
},
|
||||
"themes": {
|
||||
"toggle_theme": "Alternar tema",
|
||||
"light": "Claro",
|
||||
"dark": "Oscuro",
|
||||
"system": "Sistema"
|
||||
},
|
||||
"add_system": {
|
||||
"add_new_system": "Agregar nuevo sistema",
|
||||
"binary": "Binario",
|
||||
"dialog_des_1": "El agente debe estar ejecutándose en el sistema para conectarse. Copia el",
|
||||
"dialog_des_2": "para el agente a continuación.",
|
||||
"name": "Nombre",
|
||||
"host_ip": "Host/IP",
|
||||
"port": "Puerto",
|
||||
"public_key": "Clave pública",
|
||||
"click_to_copy": "Haz clic para copiar",
|
||||
"command": "comando",
|
||||
"add_system": "Agregar sistema"
|
||||
},
|
||||
"command": {
|
||||
"search": "Buscar sistemas o configuraciones...",
|
||||
"pages_settings": "Páginas/Configuraciones",
|
||||
"dashboard": "Panel de control",
|
||||
"documentation": "Documentación",
|
||||
"SMTP_settings": "Configuraciones SMTP",
|
||||
"page": "Página",
|
||||
"admin": "Administrador"
|
||||
},
|
||||
"monitor": {
|
||||
"toggle_grid": "Alternar cuadrícula",
|
||||
"average": "Promedio",
|
||||
"max_1_min": "Máx. 1 min ",
|
||||
"total_cpu_usage": "Uso total de CPU",
|
||||
"cpu_des": "Utilización de CPU de todo el sistema",
|
||||
"docker_cpu_usage": "Uso de CPU de Docker",
|
||||
"docker_cpu_des": "Uso promedio de CPU de los contenedores",
|
||||
"total_memory_usage": "Uso total de memoria",
|
||||
"memory_des": "Utilización precisa en el momento registrado",
|
||||
"docker_memory_usage": "Uso de memoria de Docker",
|
||||
"docker_memory_des": "Uso de memoria de los contenedores de Docker",
|
||||
"disk_space": "Espacio en disco",
|
||||
"disk_des": "Uso de la partición raíz",
|
||||
"disk_io": "E/S de disco",
|
||||
"disk_io_des": "Rendimiento de la raíz del sistema de archivos",
|
||||
"bandwidth": "Ancho de banda",
|
||||
"bandwidth_des": "Tráfico de red de interfaces públicas",
|
||||
"docker_network_io": "E/S de red de Docker",
|
||||
"docker_network_io_des": "Tráfico de red de los contenedores de Docker",
|
||||
"swap_usage": "Uso de intercambio",
|
||||
"swap_des": "Espacio de intercambio utilizado por el sistema",
|
||||
"temperature": "Temperatura",
|
||||
"temperature_des": "Temperaturas de los sensores del sistema",
|
||||
"usage": "Uso",
|
||||
"disk_usage_of": "Uso de disco de",
|
||||
"throughput_of": "Rendimiento de",
|
||||
"waiting_for": "Esperando suficientes registros para mostrar"
|
||||
},
|
||||
"auth": {
|
||||
"login": "Por favor, inicie sesión en su cuenta",
|
||||
"reset": "Ingrese la dirección de correo electrónico para restablecer la contraseña",
|
||||
"create": "Por favor, cree una cuenta de administrador",
|
||||
"create_account": "Crear cuenta",
|
||||
"sign_in": "Iniciar sesión",
|
||||
"openid_des": "Beszel admite OpenID Connect y muchos proveedores de autenticación OAuth2.",
|
||||
"please_view_the": "Por favor, consulte el",
|
||||
"for_instructions": "para obtener instrucciones.",
|
||||
"forgot_password": "¿Olvidó su contraseña?",
|
||||
"reset_password": "Restablecer contraseña",
|
||||
"command_line_instructions": "Instrucciones de línea de comandos",
|
||||
"command_1": "Si ha perdido la contraseña de su cuenta de administrador, puede restablecerla usando el siguiente comando.",
|
||||
"command_2": "Luego inicie sesión en el backend y restablezca la contraseña de su cuenta de usuario en la tabla de usuarios."
|
||||
}
|
||||
}
|
||||
|
@@ -1,194 +1,194 @@
|
||||
{
|
||||
"all_systems": "Tous les systèmes",
|
||||
"filter": "Filtrer...",
|
||||
"copy": "Copier",
|
||||
"add": "Ajouter",
|
||||
"system": "Système",
|
||||
"systems": "Systèmes",
|
||||
"cancel": "Annuler",
|
||||
"continue": "Continuer",
|
||||
"home": {
|
||||
"active_alerts": "Alertes actives",
|
||||
"active_des": "Dépasse {{value}}{{unit}} en moyenne au cours des {{minutes}} dernières minutes",
|
||||
"subtitle_1": "Mis à jour en temps réel. Appuyez sur",
|
||||
"subtitle_2": "pour ouvrir la palette de commandes."
|
||||
},
|
||||
"systems_table": {
|
||||
"system": "Système",
|
||||
"memory": "Mémoire",
|
||||
"cpu": "CPU",
|
||||
"disk": "Disque",
|
||||
"net": "Réseau",
|
||||
"agent": "Agent",
|
||||
"no_systems_found": "Aucun système trouvé.",
|
||||
"open_menu": "Ouvrir le menu",
|
||||
"resume": "Reprendre",
|
||||
"pause": "Pause",
|
||||
"copy_host": "Copier l'hôte",
|
||||
"delete": "Supprimer",
|
||||
"delete_confirm": "Êtes-vous sûr de vouloir supprimer {{name}}?",
|
||||
"delete_confirm_des_1": "Cette action est irréversible. Cela supprimera définitivement tous les enregistrements actuels de",
|
||||
"delete_confirm_des_2": "de la base de données."
|
||||
},
|
||||
"alerts": {
|
||||
"title": "Alertes",
|
||||
"subtitle_1": "Voir",
|
||||
"notification_settings": "paramètres de notification",
|
||||
"subtitle_2": "pour configurer comment vous recevez les alertes.",
|
||||
"overwrite_existing_alerts": "Écraser les alertes existantes",
|
||||
"info": {
|
||||
"status": "Statut",
|
||||
"status_des": "Déclenchement lorsque le statut passe de haut en bas.",
|
||||
"cpu_usage": "Utilisation du CPU",
|
||||
"cpu_usage_des": "Déclenchement lorsque l'utilisation du CPU dépasse un seuil.",
|
||||
"memory_usage": "Utilisation de la mémoire",
|
||||
"memory_usage_des": "Déclenchement lorsque l'utilisation de la mémoire dépasse un seuil.",
|
||||
"disk_usage": "Utilisation du disque",
|
||||
"disk_usage_des": "Déclenchement lorsque l'utilisation de n'importe quel disque dépasse un seuil.",
|
||||
"bandwidth": "Bande passante",
|
||||
"bandwidth_des": "Déclenchement lorsque le total montant/descendant dépasse un seuil.",
|
||||
"temperature": "Température",
|
||||
"temperature_des": "Déclenchement lorsque n'importe quel capteur dépasse un seuil."
|
||||
},
|
||||
"average_exceeds": "La moyenne dépasse",
|
||||
"for": "Pour",
|
||||
"minute": "minute",
|
||||
"minutes": "minutes"
|
||||
},
|
||||
"settings": {
|
||||
"settings": "Paramètres",
|
||||
"subtitle": "Gérer les préférences d'affichage et de notification.",
|
||||
"save_settings": "Enregistrer les paramètres",
|
||||
"export_configuration": "Exporter la configuration",
|
||||
"general": {
|
||||
"title": "Général",
|
||||
"subtitle": "Modifier les options générales de l'application.",
|
||||
"language": {
|
||||
"title": "Langue",
|
||||
"subtitle_1": "Vous voulez nous aider à améliorer nos traductions? Consultez",
|
||||
"subtitle_2": "pour plus de détails.",
|
||||
"preferred_language": "Langue préférée"
|
||||
},
|
||||
"chart_options": {
|
||||
"title": "Options de graphique",
|
||||
"subtitle": "Ajuster les options d'affichage pour les graphiques.",
|
||||
"default_time_period": "Période de temps par défaut",
|
||||
"default_time_period_des": "Définit la plage de temps par défaut pour les graphiques lorsqu'un système est consulté."
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Notifications",
|
||||
"subtitle_1": "Configurer comment vous recevez les notifications d'alerte.",
|
||||
"subtitle_2": "Vous cherchez plutôt où créer des alertes? Cliquez sur la cloche",
|
||||
"subtitle_3": "icônes dans le tableau des systèmes.",
|
||||
"email": {
|
||||
"title": "Notifications par email",
|
||||
"please": "Veuillez",
|
||||
"configure_an_SMTP_server": "configurer un serveur SMTP",
|
||||
"to_ensure_alerts_are_delivered": "pour garantir la livraison des alertes.",
|
||||
"to_email_s": "À email(s)",
|
||||
"enter_email_address": "Entrez l'adresse email...",
|
||||
"des": "Enregistrez l'adresse en utilisant la touche entrée ou la virgule. Laissez vide pour désactiver les notifications par email."
|
||||
},
|
||||
"webhook_push": {
|
||||
"title": "Notifications Webhook / Push",
|
||||
"des_1": "Beszel utilise",
|
||||
"des_2": "pour s'intégrer avec des services de notification populaires.",
|
||||
"add_url": "Ajouter une URL"
|
||||
}
|
||||
},
|
||||
"yaml_config": {
|
||||
"short_title": "Config YAML",
|
||||
"title": "Configuration YAML",
|
||||
"subtitle": "Exporter la configuration actuelle de vos systèmes.",
|
||||
"des_1": "Les systèmes peuvent être gérés dans un fichier",
|
||||
"des_2": "à l'intérieur de votre répertoire de données.",
|
||||
"des_3": "À chaque redémarrage, les systèmes dans la base de données seront mis à jour pour correspondre aux systèmes définis dans le fichier.",
|
||||
"alert": {
|
||||
"title": "Attention - perte de données potentielle",
|
||||
"des_1": "Les systèmes existants non définis dans",
|
||||
"des_2": "seront supprimés. Veuillez faire des sauvegardes régulières."
|
||||
}
|
||||
},
|
||||
"language": "Langue"
|
||||
},
|
||||
"user_dm": {
|
||||
"users": "Utilisateurs",
|
||||
"logs": "Journaux",
|
||||
"backups": "Sauvegardes",
|
||||
"auth_providers": "Fournisseurs d'authentification",
|
||||
"log_out": "Déconnexion"
|
||||
},
|
||||
"themes": {
|
||||
"toggle_theme": "Changer de thème",
|
||||
"light": "Clair",
|
||||
"dark": "Sombre",
|
||||
"system": "Système"
|
||||
},
|
||||
"add_system": {
|
||||
"add_new_system": "Ajouter un nouveau système",
|
||||
"binary": "Binaire",
|
||||
"dialog_des_1": "L'agent doit être en cours d'exécution sur le système pour se connecter. Copiez le",
|
||||
"dialog_des_2": "pour l'agent ci-dessous.",
|
||||
"name": "Nom",
|
||||
"host_ip": "Hôte / IP",
|
||||
"port": "Port",
|
||||
"public_key": "Clé publique",
|
||||
"click_to_copy": "Cliquez pour copier",
|
||||
"command": "commande",
|
||||
"add_system": "Ajouter un système"
|
||||
},
|
||||
"command": {
|
||||
"search": "Rechercher des systèmes ou des paramètres...",
|
||||
"pages_settings": "Pages / Paramètres",
|
||||
"dashboard": "Tableau de bord",
|
||||
"documentation": "Documentation",
|
||||
"SMTP_settings": "Paramètres SMTP",
|
||||
"page": "Page",
|
||||
"admin": "Admin"
|
||||
},
|
||||
"monitor": {
|
||||
"toggle_grid": "Changer de grille",
|
||||
"average": "Moyenne",
|
||||
"max_1_min": "Max 1 min",
|
||||
"total_cpu_usage": "Utilisation totale du CPU",
|
||||
"cpu_des": "utilisation du CPU à l'échelle du système",
|
||||
"docker_cpu_usage": "Utilisation du CPU Docker",
|
||||
"docker_cpu_des": "Utilisation moyenne du CPU des conteneurs",
|
||||
"total_memory_usage": "Utilisation totale de la mémoire",
|
||||
"memory_des": "Utilisation précise au moment enregistré",
|
||||
"docker_memory_usage": "Utilisation de la mémoire Docker",
|
||||
"docker_memory_des": "Utilisation de la mémoire des conteneurs Docker",
|
||||
"disk_space": "Espace disque",
|
||||
"disk_des": "Utilisation de la partition racine",
|
||||
"disk_io": "E/S disque",
|
||||
"disk_io_des": "Débit du système de fichiers racine",
|
||||
"bandwidth": "Bande passante",
|
||||
"bandwidth_des": "Trafic réseau des interfaces publiques",
|
||||
"docker_network_io": "E/S réseau Docker",
|
||||
"docker_network_io_des": "Trafic réseau des conteneurs Docker",
|
||||
"swap_usage": "Utilisation du swap",
|
||||
"swap_des": "Espace swap utilisé par le système",
|
||||
"temperature": "Température",
|
||||
"temperature_des": "Températures des capteurs du système",
|
||||
"usage": "Utilisation",
|
||||
"disk_usage_of": "Utilisation du disque de",
|
||||
"throughput_of": "Débit de",
|
||||
"waiting_for": "En attente de suffisamment d'enregistrements pour afficher"
|
||||
},
|
||||
"auth": {
|
||||
"login": "Veuillez vous connecter à votre compte",
|
||||
"reset": "Entrez l'adresse e-mail pour réinitialiser le mot de passe",
|
||||
"create": "Veuillez créer un compte administrateur",
|
||||
"create_account": "Créer un compte",
|
||||
"sign_in": "Se connecter",
|
||||
"openid_des": "Beszel prend en charge OpenID Connect et de nombreux fournisseurs d'authentification OAuth2.",
|
||||
"please_view_the": "Veuillez consulter le",
|
||||
"for_instructions": "pour les instructions.",
|
||||
"forgot_password": "Mot de passe oublié ?",
|
||||
"reset_password": "Réinitialiser le mot de passe",
|
||||
"command_line_instructions": "Instructions en ligne de commande",
|
||||
"command_1": "Si vous avez perdu le mot de passe de votre compte administrateur, vous pouvez le réinitialiser en utilisant la commande suivante.",
|
||||
"command_2": "Ensuite, connectez-vous au backend et réinitialisez le mot de passe de votre compte utilisateur dans la table des utilisateurs."
|
||||
}
|
||||
}
|
||||
"all_systems": "Tous les systèmes",
|
||||
"filter": "Filtrer...",
|
||||
"copy": "Copier",
|
||||
"add": "Ajouter",
|
||||
"system": "Système",
|
||||
"systems": "Systèmes",
|
||||
"cancel": "Annuler",
|
||||
"continue": "Continuer",
|
||||
"home": {
|
||||
"active_alerts": "Alertes actives",
|
||||
"active_des": "Dépasse {{value}}{{unit}} en moyenne au cours des {{minutes}} dernières minutes",
|
||||
"subtitle_1": "Mis à jour en temps réel. Appuyez sur",
|
||||
"subtitle_2": "pour ouvrir la palette de commandes."
|
||||
},
|
||||
"systems_table": {
|
||||
"system": "Système",
|
||||
"memory": "Mémoire",
|
||||
"cpu": "CPU",
|
||||
"disk": "Disque",
|
||||
"net": "Réseau",
|
||||
"agent": "Agent",
|
||||
"no_systems_found": "Aucun système trouvé.",
|
||||
"open_menu": "Ouvrir le menu",
|
||||
"resume": "Reprendre",
|
||||
"pause": "Pause",
|
||||
"copy_host": "Copier l'hôte",
|
||||
"delete": "Supprimer",
|
||||
"delete_confirm": "Êtes-vous sûr de vouloir supprimer {{name}}?",
|
||||
"delete_confirm_des_1": "Cette action est irréversible. Cela supprimera définitivement tous les enregistrements actuels de",
|
||||
"delete_confirm_des_2": "de la base de données."
|
||||
},
|
||||
"alerts": {
|
||||
"title": "Alertes",
|
||||
"subtitle_1": "Voir",
|
||||
"notification_settings": "paramètres de notification",
|
||||
"subtitle_2": "pour configurer comment vous recevez les alertes.",
|
||||
"overwrite_existing_alerts": "Écraser les alertes existantes",
|
||||
"info": {
|
||||
"status": "Statut",
|
||||
"status_des": "Déclenchement lorsque le statut passe de haut en bas.",
|
||||
"cpu_usage": "Utilisation du CPU",
|
||||
"cpu_usage_des": "Déclenchement lorsque l'utilisation du CPU dépasse un seuil.",
|
||||
"memory_usage": "Utilisation de la mémoire",
|
||||
"memory_usage_des": "Déclenchement lorsque l'utilisation de la mémoire dépasse un seuil.",
|
||||
"disk_usage": "Utilisation du disque",
|
||||
"disk_usage_des": "Déclenchement lorsque l'utilisation de n'importe quel disque dépasse un seuil.",
|
||||
"bandwidth": "Bande passante",
|
||||
"bandwidth_des": "Déclenchement lorsque le total montant/descendant dépasse un seuil.",
|
||||
"temperature": "Température",
|
||||
"temperature_des": "Déclenchement lorsque n'importe quel capteur dépasse un seuil."
|
||||
},
|
||||
"average_exceeds": "La moyenne dépasse",
|
||||
"for": "Pour",
|
||||
"minute": "minute",
|
||||
"minutes": "minutes"
|
||||
},
|
||||
"settings": {
|
||||
"settings": "Paramètres",
|
||||
"subtitle": "Gérer les préférences d'affichage et de notification.",
|
||||
"save_settings": "Enregistrer les paramètres",
|
||||
"export_configuration": "Exporter la configuration",
|
||||
"general": {
|
||||
"title": "Général",
|
||||
"subtitle": "Modifier les options générales de l'application.",
|
||||
"language": {
|
||||
"title": "Langue",
|
||||
"subtitle_1": "Vous voulez nous aider à améliorer nos traductions? Consultez",
|
||||
"subtitle_2": "pour plus de détails.",
|
||||
"preferred_language": "Langue préférée"
|
||||
},
|
||||
"chart_options": {
|
||||
"title": "Options de graphique",
|
||||
"subtitle": "Ajuster les options d'affichage pour les graphiques.",
|
||||
"default_time_period": "Période de temps par défaut",
|
||||
"default_time_period_des": "Définit la plage de temps par défaut pour les graphiques lorsqu'un système est consulté."
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Notifications",
|
||||
"subtitle_1": "Configurer comment vous recevez les notifications d'alerte.",
|
||||
"subtitle_2": "Vous cherchez plutôt où créer des alertes? Cliquez sur la cloche",
|
||||
"subtitle_3": "icônes dans le tableau des systèmes.",
|
||||
"email": {
|
||||
"title": "Notifications par email",
|
||||
"please": "Veuillez",
|
||||
"configure_an_SMTP_server": "configurer un serveur SMTP",
|
||||
"to_ensure_alerts_are_delivered": "pour garantir la livraison des alertes.",
|
||||
"to_email_s": "À email(s)",
|
||||
"enter_email_address": "Entrez l'adresse email...",
|
||||
"des": "Enregistrez l'adresse en utilisant la touche entrée ou la virgule. Laissez vide pour désactiver les notifications par email."
|
||||
},
|
||||
"webhook_push": {
|
||||
"title": "Notifications Webhook / Push",
|
||||
"des_1": "Beszel utilise",
|
||||
"des_2": "pour s'intégrer avec des services de notification populaires.",
|
||||
"add_url": "Ajouter une URL"
|
||||
}
|
||||
},
|
||||
"yaml_config": {
|
||||
"short_title": "Config YAML",
|
||||
"title": "Configuration YAML",
|
||||
"subtitle": "Exporter la configuration actuelle de vos systèmes.",
|
||||
"des_1": "Les systèmes peuvent être gérés dans un fichier",
|
||||
"des_2": "à l'intérieur de votre répertoire de données.",
|
||||
"des_3": "À chaque redémarrage, les systèmes dans la base de données seront mis à jour pour correspondre aux systèmes définis dans le fichier.",
|
||||
"alert": {
|
||||
"title": "Attention - perte de données potentielle",
|
||||
"des_1": "Les systèmes existants non définis dans",
|
||||
"des_2": "seront supprimés. Veuillez faire des sauvegardes régulières."
|
||||
}
|
||||
},
|
||||
"language": "Langue"
|
||||
},
|
||||
"user_dm": {
|
||||
"users": "Utilisateurs",
|
||||
"logs": "Journaux",
|
||||
"backups": "Sauvegardes",
|
||||
"auth_providers": "Fournisseurs d'authentification",
|
||||
"log_out": "Déconnexion"
|
||||
},
|
||||
"themes": {
|
||||
"toggle_theme": "Changer de thème",
|
||||
"light": "Clair",
|
||||
"dark": "Sombre",
|
||||
"system": "Système"
|
||||
},
|
||||
"add_system": {
|
||||
"add_new_system": "Ajouter un nouveau système",
|
||||
"binary": "Binaire",
|
||||
"dialog_des_1": "L'agent doit être en cours d'exécution sur le système pour se connecter. Copiez le",
|
||||
"dialog_des_2": "pour l'agent ci-dessous.",
|
||||
"name": "Nom",
|
||||
"host_ip": "Hôte / IP",
|
||||
"port": "Port",
|
||||
"public_key": "Clé publique",
|
||||
"click_to_copy": "Cliquez pour copier",
|
||||
"command": "commande",
|
||||
"add_system": "Ajouter un système"
|
||||
},
|
||||
"command": {
|
||||
"search": "Rechercher des systèmes ou des paramètres...",
|
||||
"pages_settings": "Pages / Paramètres",
|
||||
"dashboard": "Tableau de bord",
|
||||
"documentation": "Documentation",
|
||||
"SMTP_settings": "Paramètres SMTP",
|
||||
"page": "Page",
|
||||
"admin": "Admin"
|
||||
},
|
||||
"monitor": {
|
||||
"toggle_grid": "Changer de grille",
|
||||
"average": "Moyenne",
|
||||
"max_1_min": "Max 1 min",
|
||||
"total_cpu_usage": "Utilisation totale du CPU",
|
||||
"cpu_des": "utilisation du CPU à l'échelle du système",
|
||||
"docker_cpu_usage": "Utilisation du CPU Docker",
|
||||
"docker_cpu_des": "Utilisation moyenne du CPU des conteneurs",
|
||||
"total_memory_usage": "Utilisation totale de la mémoire",
|
||||
"memory_des": "Utilisation précise au moment enregistré",
|
||||
"docker_memory_usage": "Utilisation de la mémoire Docker",
|
||||
"docker_memory_des": "Utilisation de la mémoire des conteneurs Docker",
|
||||
"disk_space": "Espace disque",
|
||||
"disk_des": "Utilisation de la partition racine",
|
||||
"disk_io": "E/S disque",
|
||||
"disk_io_des": "Débit du système de fichiers racine",
|
||||
"bandwidth": "Bande passante",
|
||||
"bandwidth_des": "Trafic réseau des interfaces publiques",
|
||||
"docker_network_io": "E/S réseau Docker",
|
||||
"docker_network_io_des": "Trafic réseau des conteneurs Docker",
|
||||
"swap_usage": "Utilisation du swap",
|
||||
"swap_des": "Espace swap utilisé par le système",
|
||||
"temperature": "Température",
|
||||
"temperature_des": "Températures des capteurs du système",
|
||||
"usage": "Utilisation",
|
||||
"disk_usage_of": "Utilisation du disque de",
|
||||
"throughput_of": "Débit de",
|
||||
"waiting_for": "En attente de suffisamment d'enregistrements pour afficher"
|
||||
},
|
||||
"auth": {
|
||||
"login": "Veuillez vous connecter à votre compte",
|
||||
"reset": "Entrez l'adresse e-mail pour réinitialiser le mot de passe",
|
||||
"create": "Veuillez créer un compte administrateur",
|
||||
"create_account": "Créer un compte",
|
||||
"sign_in": "Se connecter",
|
||||
"openid_des": "Beszel prend en charge OpenID Connect et de nombreux fournisseurs d'authentification OAuth2.",
|
||||
"please_view_the": "Veuillez consulter le",
|
||||
"for_instructions": "pour les instructions.",
|
||||
"forgot_password": "Mot de passe oublié ?",
|
||||
"reset_password": "Réinitialiser le mot de passe",
|
||||
"command_line_instructions": "Instructions en ligne de commande",
|
||||
"command_1": "Si vous avez perdu le mot de passe de votre compte administrateur, vous pouvez le réinitialiser en utilisant la commande suivante.",
|
||||
"command_2": "Ensuite, connectez-vous au backend et réinitialisez le mot de passe de votre compte utilisateur dans la table des utilisateurs."
|
||||
}
|
||||
}
|
||||
|
@@ -1,194 +1,194 @@
|
||||
{
|
||||
"all_systems": "Все системы",
|
||||
"filter": "Фильтр...",
|
||||
"copy": "Копировать",
|
||||
"add": "Добавить",
|
||||
"system": "Система",
|
||||
"systems": "Системы",
|
||||
"cancel": "Отмена",
|
||||
"continue": "Продолжить",
|
||||
"home": {
|
||||
"active_alerts": "Активные предупреждения",
|
||||
"active_des": "Превышает {{value}}{{unit}} в среднем за последние {{minutes}} минут",
|
||||
"subtitle_1": "Обновляется в реальном времени. Нажмите",
|
||||
"subtitle_2": "чтобы открыть палитру команд."
|
||||
},
|
||||
"systems_table": {
|
||||
"system": "Система",
|
||||
"memory": "Память",
|
||||
"cpu": "ЦП",
|
||||
"disk": "Диск",
|
||||
"net": "Сеть",
|
||||
"agent": "Агент",
|
||||
"no_systems_found": "Систем не найдено.",
|
||||
"open_menu": "Открыть меню",
|
||||
"resume": "Возобновить",
|
||||
"pause": "Пауза",
|
||||
"copy_host": "Копировать хост",
|
||||
"delete": "Удалить",
|
||||
"delete_confirm": "Вы уверены, что хотите удалить {{name}}?",
|
||||
"delete_confirm_des_1": "Это действие нельзя отменить. Это навсегда удалит все текущие записи для",
|
||||
"delete_confirm_des_2": "из базы данных."
|
||||
},
|
||||
"alerts": {
|
||||
"title": "Предупреждения",
|
||||
"subtitle_1": "См.",
|
||||
"notification_settings": "настройки уведомлений",
|
||||
"subtitle_2": "чтобы настроить, как вы получаете предупреждения.",
|
||||
"overwrite_existing_alerts": "Перезаписать существующие предупреждения",
|
||||
"info": {
|
||||
"status": "Статус",
|
||||
"status_des": "Срабатывает, когда статус переключается между вверх и вниз.",
|
||||
"cpu_usage": "Использование ЦП",
|
||||
"cpu_usage_des": "Срабатывает, когда использование ЦП превышает порог.",
|
||||
"memory_usage": "Использование памяти",
|
||||
"memory_usage_des": "Срабатывает, когда использование памяти превышает порог.",
|
||||
"disk_usage": "Использование диска",
|
||||
"disk_usage_des": "Срабатывает, когда использование любого диска превышает порог.",
|
||||
"bandwidth": "Пропускная способность",
|
||||
"bandwidth_des": "Срабатывает, когда суммарная загрузка/выгрузка превышает порог.",
|
||||
"temperature": "Температура",
|
||||
"temperature_des": "Срабатывает, когда любой датчик превышает порог."
|
||||
},
|
||||
"average_exceeds": "Среднее значение превышает",
|
||||
"for": "За",
|
||||
"minute": "минуту",
|
||||
"minutes": "минут"
|
||||
},
|
||||
"settings": {
|
||||
"settings": "Настройки",
|
||||
"subtitle": "Управление предпочтениями отображения и уведомлений.",
|
||||
"save_settings": "Сохранить настройки",
|
||||
"export_configuration": "Экспорт конфигурации",
|
||||
"general": {
|
||||
"title": "Общие",
|
||||
"subtitle": "Изменить общие параметры приложения.",
|
||||
"language": {
|
||||
"title": "Язык",
|
||||
"subtitle_1": "Хотите помочь нам улучшить наши переводы? Ознакомьтесь с",
|
||||
"subtitle_2": "для получения дополнительной информации.",
|
||||
"preferred_language": "Предпочитаемый язык"
|
||||
},
|
||||
"chart_options": {
|
||||
"title": "Параметры диаграммы",
|
||||
"subtitle": "Настроить параметры отображения для диаграмм.",
|
||||
"default_time_period": "Период по умолчанию",
|
||||
"default_time_period_des": "Устанавливает диапазон времени по умолчанию для диаграмм при просмотре системы."
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Уведомления",
|
||||
"subtitle_1": "Настройте, как вы получаете уведомления о предупреждениях.",
|
||||
"subtitle_2": "Ищете, где создать предупреждения? Нажмите на колокольчик",
|
||||
"subtitle_3": "значки в таблице систем.",
|
||||
"email": {
|
||||
"title": "Уведомления по электронной почте",
|
||||
"please": "Пожалуйста",
|
||||
"configure_an_SMTP_server": "настройте SMTP-сервер",
|
||||
"to_ensure_alerts_are_delivered": "чтобы гарантировать доставку предупреждений.",
|
||||
"to_email_s": "На электронную почту(ы)",
|
||||
"enter_email_address": "Введите адрес электронной почты...",
|
||||
"des": "Сохраните адрес, используя клавишу ввода или запятую. Оставьте пустым, чтобы отключить уведомления по электронной почте."
|
||||
},
|
||||
"webhook_push": {
|
||||
"title": "Webhook / Push уведомления",
|
||||
"des_1": "Beszel использует",
|
||||
"des_2": "для интеграции с популярными сервисами уведомлений.",
|
||||
"add_url": "Добавить URL"
|
||||
}
|
||||
},
|
||||
"yaml_config": {
|
||||
"short_title": "YAML Конфиг",
|
||||
"title": "YAML Конфигурация",
|
||||
"subtitle": "Экспорт текущей конфигурации ваших систем.",
|
||||
"des_1": "Системы могут управляться в",
|
||||
"des_2": "файле в вашем каталоге данных.",
|
||||
"des_3": "При каждом перезапуске системы в базе данных будут обновлены, чтобы соответствовать системам, определенным в файле.",
|
||||
"alert": {
|
||||
"title": "Внимание - возможная потеря данных",
|
||||
"des_1": "Существующие системы, не определенные в",
|
||||
"des_2": "будут удалены. Пожалуйста, делайте регулярные резервные копии."
|
||||
}
|
||||
},
|
||||
"language": "Язык"
|
||||
},
|
||||
"user_dm": {
|
||||
"users": "Пользователи",
|
||||
"logs": "Журналы",
|
||||
"backups": "Резервные копии",
|
||||
"auth_providers": "Поставщики аутентификации",
|
||||
"log_out": "Выйти"
|
||||
},
|
||||
"themes": {
|
||||
"toggle_theme": "Переключить тему",
|
||||
"light": "Светлая",
|
||||
"dark": "Темная",
|
||||
"system": "Система"
|
||||
},
|
||||
"add_system": {
|
||||
"add_new_system": "Добавить новую систему",
|
||||
"binary": "Бинарный",
|
||||
"dialog_des_1": "Агент должен работать на системе для подключения. Скопируйте",
|
||||
"dialog_des_2": "для агента ниже.",
|
||||
"name": "Имя",
|
||||
"host_ip": "Хост / IP",
|
||||
"port": "Порт",
|
||||
"public_key": "Публичный ключ",
|
||||
"click_to_copy": "Нажмите, чтобы скопировать",
|
||||
"command": "команда",
|
||||
"add_system": "Добавить систему"
|
||||
},
|
||||
"command": {
|
||||
"search": "Поиск систем или настроек...",
|
||||
"pages_settings": "Страницы / Настройки",
|
||||
"dashboard": "Панель управления",
|
||||
"documentation": "Документация",
|
||||
"SMTP_settings": "Настройки SMTP",
|
||||
"page": "Страница",
|
||||
"admin": "Админ"
|
||||
},
|
||||
"monitor": {
|
||||
"toggle_grid": "Переключить сетку",
|
||||
"average": "Среднее",
|
||||
"max_1_min": "Макс 1 мин",
|
||||
"total_cpu_usage": "Общее использование ЦП",
|
||||
"cpu_des": "системное использование ЦП",
|
||||
"docker_cpu_usage": "Использование ЦП Docker",
|
||||
"docker_cpu_des": "Среднее использование ЦП контейнеров",
|
||||
"total_memory_usage": "Общее использование памяти",
|
||||
"memory_des": "Точное использование на момент записи",
|
||||
"docker_memory_usage": "Использование памяти Docker",
|
||||
"docker_memory_des": "Использование памяти контейнеров Docker",
|
||||
"disk_space": "Место на диске",
|
||||
"disk_des": "Использование корневого раздела",
|
||||
"disk_io": "Дисковый ввод/вывод",
|
||||
"disk_io_des": "Пропускная способность корневой файловой системы",
|
||||
"bandwidth": "Пропускная способность",
|
||||
"bandwidth_des": "Сетевой трафик публичных интерфейсов",
|
||||
"docker_network_io": "Сетевой ввод/вывод Docker",
|
||||
"docker_network_io_des": "Сетевой трафик контейнеров Docker",
|
||||
"swap_usage": "Использование swap",
|
||||
"swap_des": "Использование swap пространства системой",
|
||||
"temperature": "Температура",
|
||||
"temperature_des": "Температуры датчиков системы",
|
||||
"usage": "Использование",
|
||||
"disk_usage_of": "Использование диска",
|
||||
"throughput_of": "Пропускная способность",
|
||||
"waiting_for": "Ожидание достаточного количества записей для отображения"
|
||||
},
|
||||
"auth": {
|
||||
"login": "Пожалуйста, войдите в свою учетную запись",
|
||||
"reset": "Введите адрес электронной почты для сброса пароля",
|
||||
"create": "Пожалуйста, создайте учетную запись администратора",
|
||||
"create_account": "Создать учетную запись",
|
||||
"sign_in": "Войти",
|
||||
"openid_des": "Beszel поддерживает OpenID Connect и многих поставщиков аутентификации OAuth2.",
|
||||
"please_view_the": "Пожалуйста, ознакомьтесь с",
|
||||
"for_instructions": "для получения инструкций.",
|
||||
"forgot_password": "Забыли пароль?",
|
||||
"reset_password": "Сбросить пароль",
|
||||
"command_line_instructions": "Инструкции по командной строке",
|
||||
"command_1": "Если вы потеряли пароль от своей учетной записи администратора, вы можете сбросить его, используя следующую команду.",
|
||||
"command_2": "Затем войдите в бэкэнд и сбросьте пароль своей учетной записи пользователя в таблице пользователей."
|
||||
}
|
||||
}
|
||||
"all_systems": "Все системы",
|
||||
"filter": "Фильтр...",
|
||||
"copy": "Копировать",
|
||||
"add": "Добавить",
|
||||
"system": "Система",
|
||||
"systems": "Системы",
|
||||
"cancel": "Отмена",
|
||||
"continue": "Продолжить",
|
||||
"home": {
|
||||
"active_alerts": "Активные предупреждения",
|
||||
"active_des": "Превышает {{value}}{{unit}} в среднем за последние {{minutes}} минут",
|
||||
"subtitle_1": "Обновляется в реальном времени. Нажмите",
|
||||
"subtitle_2": "чтобы открыть палитру команд."
|
||||
},
|
||||
"systems_table": {
|
||||
"system": "Система",
|
||||
"memory": "Память",
|
||||
"cpu": "ЦП",
|
||||
"disk": "Диск",
|
||||
"net": "Сеть",
|
||||
"agent": "Агент",
|
||||
"no_systems_found": "Систем не найдено.",
|
||||
"open_menu": "Открыть меню",
|
||||
"resume": "Возобновить",
|
||||
"pause": "Пауза",
|
||||
"copy_host": "Копировать хост",
|
||||
"delete": "Удалить",
|
||||
"delete_confirm": "Вы уверены, что хотите удалить {{name}}?",
|
||||
"delete_confirm_des_1": "Это действие нельзя отменить. Это навсегда удалит все текущие записи для",
|
||||
"delete_confirm_des_2": "из базы данных."
|
||||
},
|
||||
"alerts": {
|
||||
"title": "Предупреждения",
|
||||
"subtitle_1": "См.",
|
||||
"notification_settings": "настройки уведомлений",
|
||||
"subtitle_2": "чтобы настроить, как вы получаете предупреждения.",
|
||||
"overwrite_existing_alerts": "Перезаписать существующие предупреждения",
|
||||
"info": {
|
||||
"status": "Статус",
|
||||
"status_des": "Срабатывает, когда статус переключается между вверх и вниз.",
|
||||
"cpu_usage": "Использование ЦП",
|
||||
"cpu_usage_des": "Срабатывает, когда использование ЦП превышает порог.",
|
||||
"memory_usage": "Использование памяти",
|
||||
"memory_usage_des": "Срабатывает, когда использование памяти превышает порог.",
|
||||
"disk_usage": "Использование диска",
|
||||
"disk_usage_des": "Срабатывает, когда использование любого диска превышает порог.",
|
||||
"bandwidth": "Пропускная способность",
|
||||
"bandwidth_des": "Срабатывает, когда суммарная загрузка/выгрузка превышает порог.",
|
||||
"temperature": "Температура",
|
||||
"temperature_des": "Срабатывает, когда любой датчик превышает порог."
|
||||
},
|
||||
"average_exceeds": "Среднее значение превышает",
|
||||
"for": "За",
|
||||
"minute": "минуту",
|
||||
"minutes": "минут"
|
||||
},
|
||||
"settings": {
|
||||
"settings": "Настройки",
|
||||
"subtitle": "Управление предпочтениями отображения и уведомлений.",
|
||||
"save_settings": "Сохранить настройки",
|
||||
"export_configuration": "Экспорт конфигурации",
|
||||
"general": {
|
||||
"title": "Общие",
|
||||
"subtitle": "Изменить общие параметры приложения.",
|
||||
"language": {
|
||||
"title": "Язык",
|
||||
"subtitle_1": "Хотите помочь нам улучшить наши переводы? Ознакомьтесь с",
|
||||
"subtitle_2": "для получения дополнительной информации.",
|
||||
"preferred_language": "Предпочитаемый язык"
|
||||
},
|
||||
"chart_options": {
|
||||
"title": "Параметры диаграммы",
|
||||
"subtitle": "Настроить параметры отображения для диаграмм.",
|
||||
"default_time_period": "Период по умолчанию",
|
||||
"default_time_period_des": "Устанавливает диапазон времени по умолчанию для диаграмм при просмотре системы."
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Уведомления",
|
||||
"subtitle_1": "Настройте, как вы получаете уведомления о предупреждениях.",
|
||||
"subtitle_2": "Ищете, где создать предупреждения? Нажмите на колокольчик",
|
||||
"subtitle_3": "значки в таблице систем.",
|
||||
"email": {
|
||||
"title": "Уведомления по электронной почте",
|
||||
"please": "Пожалуйста",
|
||||
"configure_an_SMTP_server": "настройте SMTP-сервер",
|
||||
"to_ensure_alerts_are_delivered": "чтобы гарантировать доставку предупреждений.",
|
||||
"to_email_s": "На электронную почту(ы)",
|
||||
"enter_email_address": "Введите адрес электронной почты...",
|
||||
"des": "Сохраните адрес, используя клавишу ввода или запятую. Оставьте пустым, чтобы отключить уведомления по электронной почте."
|
||||
},
|
||||
"webhook_push": {
|
||||
"title": "Webhook / Push уведомления",
|
||||
"des_1": "Beszel использует",
|
||||
"des_2": "для интеграции с популярными сервисами уведомлений.",
|
||||
"add_url": "Добавить URL"
|
||||
}
|
||||
},
|
||||
"yaml_config": {
|
||||
"short_title": "YAML Конфиг",
|
||||
"title": "YAML Конфигурация",
|
||||
"subtitle": "Экспорт текущей конфигурации ваших систем.",
|
||||
"des_1": "Системы могут управляться в",
|
||||
"des_2": "файле в вашем каталоге данных.",
|
||||
"des_3": "При каждом перезапуске системы в базе данных будут обновлены, чтобы соответствовать системам, определенным в файле.",
|
||||
"alert": {
|
||||
"title": "Внимание - возможная потеря данных",
|
||||
"des_1": "Существующие системы, не определенные в",
|
||||
"des_2": "будут удалены. Пожалуйста, делайте регулярные резервные копии."
|
||||
}
|
||||
},
|
||||
"language": "Язык"
|
||||
},
|
||||
"user_dm": {
|
||||
"users": "Пользователи",
|
||||
"logs": "Журналы",
|
||||
"backups": "Резервные копии",
|
||||
"auth_providers": "Поставщики аутентификации",
|
||||
"log_out": "Выйти"
|
||||
},
|
||||
"themes": {
|
||||
"toggle_theme": "Переключить тему",
|
||||
"light": "Светлая",
|
||||
"dark": "Темная",
|
||||
"system": "Система"
|
||||
},
|
||||
"add_system": {
|
||||
"add_new_system": "Добавить новую систему",
|
||||
"binary": "Бинарный",
|
||||
"dialog_des_1": "Агент должен работать на системе для подключения. Скопируйте",
|
||||
"dialog_des_2": "для агента ниже.",
|
||||
"name": "Имя",
|
||||
"host_ip": "Хост / IP",
|
||||
"port": "Порт",
|
||||
"public_key": "Публичный ключ",
|
||||
"click_to_copy": "Нажмите, чтобы скопировать",
|
||||
"command": "команда",
|
||||
"add_system": "Добавить систему"
|
||||
},
|
||||
"command": {
|
||||
"search": "Поиск систем или настроек...",
|
||||
"pages_settings": "Страницы / Настройки",
|
||||
"dashboard": "Панель управления",
|
||||
"documentation": "Документация",
|
||||
"SMTP_settings": "Настройки SMTP",
|
||||
"page": "Страница",
|
||||
"admin": "Админ"
|
||||
},
|
||||
"monitor": {
|
||||
"toggle_grid": "Переключить сетку",
|
||||
"average": "Среднее",
|
||||
"max_1_min": "Макс 1 мин",
|
||||
"total_cpu_usage": "Общее использование ЦП",
|
||||
"cpu_des": "системное использование ЦП",
|
||||
"docker_cpu_usage": "Использование ЦП Docker",
|
||||
"docker_cpu_des": "Среднее использование ЦП контейнеров",
|
||||
"total_memory_usage": "Общее использование памяти",
|
||||
"memory_des": "Точное использование на момент записи",
|
||||
"docker_memory_usage": "Использование памяти Docker",
|
||||
"docker_memory_des": "Использование памяти контейнеров Docker",
|
||||
"disk_space": "Место на диске",
|
||||
"disk_des": "Использование корневого раздела",
|
||||
"disk_io": "Дисковый ввод/вывод",
|
||||
"disk_io_des": "Пропускная способность корневой файловой системы",
|
||||
"bandwidth": "Пропускная способность",
|
||||
"bandwidth_des": "Сетевой трафик публичных интерфейсов",
|
||||
"docker_network_io": "Сетевой ввод/вывод Docker",
|
||||
"docker_network_io_des": "Сетевой трафик контейнеров Docker",
|
||||
"swap_usage": "Использование swap",
|
||||
"swap_des": "Использование swap пространства системой",
|
||||
"temperature": "Температура",
|
||||
"temperature_des": "Температуры датчиков системы",
|
||||
"usage": "Использование",
|
||||
"disk_usage_of": "Использование диска",
|
||||
"throughput_of": "Пропускная способность",
|
||||
"waiting_for": "Ожидание достаточного количества записей для отображения"
|
||||
},
|
||||
"auth": {
|
||||
"login": "Пожалуйста, войдите в свою учетную запись",
|
||||
"reset": "Введите адрес электронной почты для сброса пароля",
|
||||
"create": "Пожалуйста, создайте учетную запись администратора",
|
||||
"create_account": "Создать учетную запись",
|
||||
"sign_in": "Войти",
|
||||
"openid_des": "Beszel поддерживает OpenID Connect и многих поставщиков аутентификации OAuth2.",
|
||||
"please_view_the": "Пожалуйста, ознакомьтесь с",
|
||||
"for_instructions": "для получения инструкций.",
|
||||
"forgot_password": "Забыли пароль?",
|
||||
"reset_password": "Сбросить пароль",
|
||||
"command_line_instructions": "Инструкции по командной строке",
|
||||
"command_1": "Если вы потеряли пароль от своей учетной записи администратора, вы можете сбросить его, используя следующую команду.",
|
||||
"command_2": "Затем войдите в бэкэнд и сбросьте пароль своей учетной записи пользователя в таблице пользователей."
|
||||
}
|
||||
}
|
||||
|
@@ -1,194 +1,194 @@
|
||||
{
|
||||
"all_systems": "所有服务器",
|
||||
"filter": "筛选...",
|
||||
"copy": "复制",
|
||||
"add": "添加",
|
||||
"system": "服务器",
|
||||
"systems": "服务器",
|
||||
"cancel": "取消",
|
||||
"continue": "继续",
|
||||
"home": {
|
||||
"active_alerts": "活动警报",
|
||||
"active_des": "在过去 {{minutes}} 分钟内超过 {{value}}{{unit}} 平均值",
|
||||
"subtitle_1": "实时更新。按",
|
||||
"subtitle_2": "打开命令面板。"
|
||||
},
|
||||
"systems_table": {
|
||||
"system": "服务器",
|
||||
"memory": "内存",
|
||||
"cpu": "CPU",
|
||||
"disk": "磁盘",
|
||||
"net": "网络",
|
||||
"agent": "客户端",
|
||||
"no_systems_found": "未找到服务器。",
|
||||
"open_menu": "打开菜单",
|
||||
"resume": "恢复",
|
||||
"pause": "暂停",
|
||||
"copy_host": "复制主机",
|
||||
"delete": "删除",
|
||||
"delete_confirm": "您确定要删除 {{name}} 吗?",
|
||||
"delete_confirm_des_1": "此操作无法撤消。这将永久从数据库中删除",
|
||||
"delete_confirm_des_2": "的所有记录。"
|
||||
},
|
||||
"alerts": {
|
||||
"title": "警报",
|
||||
"subtitle_1": "查看",
|
||||
"notification_settings": "通知设置",
|
||||
"subtitle_2": "配置如何接收警报。",
|
||||
"overwrite_existing_alerts": "覆盖现有警报",
|
||||
"info": {
|
||||
"status": "状态",
|
||||
"status_des": "状态在 在线/离线 之间切换时触发。",
|
||||
"cpu_usage": "CPU 使用率",
|
||||
"cpu_usage_des": "当 CPU 使用率超过阈值时触发。",
|
||||
"memory_usage": "内存使用率",
|
||||
"memory_usage_des": "当内存使用率超过阈值时触发。",
|
||||
"disk_usage": "磁盘使用率",
|
||||
"disk_usage_des": "当任何磁盘的使用率超过阈值时触发。",
|
||||
"bandwidth": "带宽",
|
||||
"bandwidth_des": "当组合的 上行/下行 流量超过阈值时触发。",
|
||||
"temperature": "温度",
|
||||
"temperature_des": "当任何传感器超过阈值时触发。"
|
||||
},
|
||||
"average_exceeds": "平均值超过",
|
||||
"for": "持续",
|
||||
"minute": "分钟",
|
||||
"minutes": "分钟"
|
||||
},
|
||||
"settings": {
|
||||
"settings": "设置",
|
||||
"subtitle": "管理显示和通知偏好。",
|
||||
"save_settings": "保存设置",
|
||||
"export_configuration": "导出配置",
|
||||
"general": {
|
||||
"title": "常规",
|
||||
"subtitle": "更改通用应用程序选项。",
|
||||
"language": {
|
||||
"title": "语言",
|
||||
"subtitle_1": "想帮助我们改进翻译吗?查看",
|
||||
"subtitle_2": "了解更多详情。",
|
||||
"preferred_language": "首选语言"
|
||||
},
|
||||
"chart_options": {
|
||||
"title": "图表选项",
|
||||
"subtitle": "调整图表的显示选项。",
|
||||
"default_time_period": "默认时间段",
|
||||
"default_time_period_des": "设置查看服务器时图表的默认时间范围。"
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"title": "通知",
|
||||
"subtitle_1": "设置如何接收警报通知。",
|
||||
"subtitle_2": "正在寻找创建警报的位置?点击服务器列表中的",
|
||||
"subtitle_3": "图标。",
|
||||
"email": {
|
||||
"title": "电子邮件通知",
|
||||
"please": "配置",
|
||||
"configure_an_SMTP_server": "SMTP 服务器",
|
||||
"to_ensure_alerts_are_delivered": "以确保邮件可以成功发送。",
|
||||
"to_email_s": "收件人邮箱",
|
||||
"enter_email_address": "输入邮箱地址...",
|
||||
"des": "使用 Enter 键或逗号保存地址。留空以禁用电子邮件通知。"
|
||||
},
|
||||
"webhook_push": {
|
||||
"title": "Webhook / 推送通知",
|
||||
"des_1": "Beszel 使用",
|
||||
"des_2": "与流行的通知服务集成。",
|
||||
"add_url": "添加 URL"
|
||||
}
|
||||
},
|
||||
"yaml_config": {
|
||||
"short_title": "YAML 配置",
|
||||
"title": "YAML 配置",
|
||||
"subtitle": "导出当前系统配置。",
|
||||
"des_1": "系统使用",
|
||||
"des_2": "配置文件储存管理数据",
|
||||
"des_3": "每次重启时,数据库中的系统将更新以匹配文件中定义的系统。",
|
||||
"alert": {
|
||||
"title": "警告 - 潜在数据丢失",
|
||||
"des_1": "未在",
|
||||
"des_2": "中定义的现有系统将被删除。请定期备份。"
|
||||
}
|
||||
},
|
||||
"language": "语言"
|
||||
},
|
||||
"user_dm": {
|
||||
"users": "用户",
|
||||
"logs": "日志",
|
||||
"backups": "备份",
|
||||
"auth_providers": "身份验证提供者",
|
||||
"log_out": "登出"
|
||||
},
|
||||
"themes": {
|
||||
"toggle_theme": "切换主题",
|
||||
"light": "浅色",
|
||||
"dark": "深色",
|
||||
"system": "系统"
|
||||
},
|
||||
"add_system": {
|
||||
"add_new_system": "添加新服务器",
|
||||
"binary": "二进制文件",
|
||||
"dialog_des_1": "客户端必须在服务器上运行才能连接。复制",
|
||||
"dialog_des_2": "到目标服务器以安装。",
|
||||
"name": "名称",
|
||||
"host_ip": "主机/IP",
|
||||
"port": "端口",
|
||||
"public_key": "公钥",
|
||||
"click_to_copy": "点击复制",
|
||||
"command": "命令",
|
||||
"add_system": "添加服务器"
|
||||
},
|
||||
"command": {
|
||||
"search": "在设置或系统中搜索...",
|
||||
"pages_settings": "页面 / 设置",
|
||||
"dashboard": "仪表盘",
|
||||
"documentation": "文档",
|
||||
"SMTP_settings": "SMTP 设置",
|
||||
"page": "页面",
|
||||
"admin": "管理员"
|
||||
},
|
||||
"monitor": {
|
||||
"toggle_grid": "切换布局",
|
||||
"average": "平均值",
|
||||
"max_1_min": "1 分钟最大",
|
||||
"total_cpu_usage": "总 CPU 使用率",
|
||||
"cpu_des": "系统范围的 CPU 利用率",
|
||||
"docker_cpu_usage": "Docker CPU 使用率",
|
||||
"docker_cpu_des": "Docker 的平均 CPU 利用率",
|
||||
"total_memory_usage": "总内存使用率",
|
||||
"memory_des": "记录时间点的精确利用率",
|
||||
"docker_memory_usage": "Docker 内存使用率",
|
||||
"docker_memory_des": "Docker 容器的内存使用率",
|
||||
"disk_space": "磁盘空间",
|
||||
"disk_des": "根分区的使用情况",
|
||||
"disk_io": "磁盘 I/O",
|
||||
"disk_io_des": "根文件系统的吞吐量",
|
||||
"bandwidth": "带宽",
|
||||
"bandwidth_des": "公共接口的网络流量",
|
||||
"docker_network_io": "Docker 网络 I/O",
|
||||
"docker_network_io_des": "Docker 容器的网络流量",
|
||||
"swap_usage": "交换空间使用率",
|
||||
"swap_des": "系统使用的交换空间",
|
||||
"temperature": "温度",
|
||||
"temperature_des": "系统传感器的温度",
|
||||
"usage": "使用率",
|
||||
"disk_usage_of": "的磁盘使用率",
|
||||
"throughput_of": "的吞吐量",
|
||||
"waiting_for": "等待足够的记录以显示"
|
||||
},
|
||||
"auth": {
|
||||
"login": "请登入你的账户",
|
||||
"reset": "输入邮箱来重设密码",
|
||||
"create": "请创建管理员账户",
|
||||
"create_account": "创建账户",
|
||||
"sign_in": "登入",
|
||||
"openid_des": "Beszel 支持 OpenID Connect 和许多 OAuth2 认证提供商。",
|
||||
"please_view_the": "请检视",
|
||||
"for_instructions": "以获取更多信息。",
|
||||
"forgot_password": "忘记密码?",
|
||||
"reset_password": "重设密码",
|
||||
"command_line_instructions": "了解命令行指令",
|
||||
"command_1": "如果您忘记了管理员账户的密码,可以使用以下命令重置。",
|
||||
"command_2": "然后登录到后台,在用户表中重置您的用户账户密码。"
|
||||
}
|
||||
}
|
||||
"all_systems": "所有服务器",
|
||||
"filter": "筛选...",
|
||||
"copy": "复制",
|
||||
"add": "添加",
|
||||
"system": "服务器",
|
||||
"systems": "服务器",
|
||||
"cancel": "取消",
|
||||
"continue": "继续",
|
||||
"home": {
|
||||
"active_alerts": "活动警报",
|
||||
"active_des": "在过去 {{minutes}} 分钟内超过 {{value}}{{unit}} 平均值",
|
||||
"subtitle_1": "实时更新。按",
|
||||
"subtitle_2": "打开命令面板。"
|
||||
},
|
||||
"systems_table": {
|
||||
"system": "服务器",
|
||||
"memory": "内存",
|
||||
"cpu": "CPU",
|
||||
"disk": "磁盘",
|
||||
"net": "网络",
|
||||
"agent": "客户端",
|
||||
"no_systems_found": "未找到服务器。",
|
||||
"open_menu": "打开菜单",
|
||||
"resume": "恢复",
|
||||
"pause": "暂停",
|
||||
"copy_host": "复制主机",
|
||||
"delete": "删除",
|
||||
"delete_confirm": "您确定要删除 {{name}} 吗?",
|
||||
"delete_confirm_des_1": "此操作无法撤消。这将永久从数据库中删除",
|
||||
"delete_confirm_des_2": "的所有记录。"
|
||||
},
|
||||
"alerts": {
|
||||
"title": "警报",
|
||||
"subtitle_1": "查看",
|
||||
"notification_settings": "通知设置",
|
||||
"subtitle_2": "配置如何接收警报。",
|
||||
"overwrite_existing_alerts": "覆盖现有警报",
|
||||
"info": {
|
||||
"status": "状态",
|
||||
"status_des": "状态在 在线/离线 之间切换时触发。",
|
||||
"cpu_usage": "CPU 使用率",
|
||||
"cpu_usage_des": "当 CPU 使用率超过阈值时触发。",
|
||||
"memory_usage": "内存使用率",
|
||||
"memory_usage_des": "当内存使用率超过阈值时触发。",
|
||||
"disk_usage": "磁盘使用率",
|
||||
"disk_usage_des": "当任何磁盘的使用率超过阈值时触发。",
|
||||
"bandwidth": "带宽",
|
||||
"bandwidth_des": "当组合的 上行/下行 流量超过阈值时触发。",
|
||||
"temperature": "温度",
|
||||
"temperature_des": "当任何传感器超过阈值时触发。"
|
||||
},
|
||||
"average_exceeds": "平均值超过",
|
||||
"for": "持续",
|
||||
"minute": "分钟",
|
||||
"minutes": "分钟"
|
||||
},
|
||||
"settings": {
|
||||
"settings": "设置",
|
||||
"subtitle": "管理显示和通知偏好。",
|
||||
"save_settings": "保存设置",
|
||||
"export_configuration": "导出配置",
|
||||
"general": {
|
||||
"title": "常规",
|
||||
"subtitle": "更改通用应用程序选项。",
|
||||
"language": {
|
||||
"title": "语言",
|
||||
"subtitle_1": "想帮助我们改进翻译吗?查看",
|
||||
"subtitle_2": "了解更多详情。",
|
||||
"preferred_language": "首选语言"
|
||||
},
|
||||
"chart_options": {
|
||||
"title": "图表选项",
|
||||
"subtitle": "调整图表的显示选项。",
|
||||
"default_time_period": "默认时间段",
|
||||
"default_time_period_des": "设置查看服务器时图表的默认时间范围。"
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"title": "通知",
|
||||
"subtitle_1": "设置如何接收警报通知。",
|
||||
"subtitle_2": "正在寻找创建警报的位置?点击服务器列表中的",
|
||||
"subtitle_3": "图标。",
|
||||
"email": {
|
||||
"title": "电子邮件通知",
|
||||
"please": "配置",
|
||||
"configure_an_SMTP_server": "SMTP 服务器",
|
||||
"to_ensure_alerts_are_delivered": "以确保邮件可以成功发送。",
|
||||
"to_email_s": "收件人邮箱",
|
||||
"enter_email_address": "输入邮箱地址...",
|
||||
"des": "使用 Enter 键或逗号保存地址。留空以禁用电子邮件通知。"
|
||||
},
|
||||
"webhook_push": {
|
||||
"title": "Webhook / 推送通知",
|
||||
"des_1": "Beszel 使用",
|
||||
"des_2": "与流行的通知服务集成。",
|
||||
"add_url": "添加 URL"
|
||||
}
|
||||
},
|
||||
"yaml_config": {
|
||||
"short_title": "YAML 配置",
|
||||
"title": "YAML 配置",
|
||||
"subtitle": "导出当前系统配置。",
|
||||
"des_1": "系统使用",
|
||||
"des_2": "配置文件储存管理数据",
|
||||
"des_3": "每次重启时,数据库中的系统将更新以匹配文件中定义的系统。",
|
||||
"alert": {
|
||||
"title": "警告 - 潜在数据丢失",
|
||||
"des_1": "未在",
|
||||
"des_2": "中定义的现有系统将被删除。请定期备份。"
|
||||
}
|
||||
},
|
||||
"language": "语言"
|
||||
},
|
||||
"user_dm": {
|
||||
"users": "用户",
|
||||
"logs": "日志",
|
||||
"backups": "备份",
|
||||
"auth_providers": "身份验证提供者",
|
||||
"log_out": "登出"
|
||||
},
|
||||
"themes": {
|
||||
"toggle_theme": "切换主题",
|
||||
"light": "浅色",
|
||||
"dark": "深色",
|
||||
"system": "系统"
|
||||
},
|
||||
"add_system": {
|
||||
"add_new_system": "添加新服务器",
|
||||
"binary": "二进制文件",
|
||||
"dialog_des_1": "客户端必须在服务器上运行才能连接。复制",
|
||||
"dialog_des_2": "到目标服务器以安装。",
|
||||
"name": "名称",
|
||||
"host_ip": "主机/IP",
|
||||
"port": "端口",
|
||||
"public_key": "公钥",
|
||||
"click_to_copy": "点击复制",
|
||||
"command": "命令",
|
||||
"add_system": "添加服务器"
|
||||
},
|
||||
"command": {
|
||||
"search": "在设置或系统中搜索...",
|
||||
"pages_settings": "页面 / 设置",
|
||||
"dashboard": "仪表盘",
|
||||
"documentation": "文档",
|
||||
"SMTP_settings": "SMTP 设置",
|
||||
"page": "页面",
|
||||
"admin": "管理员"
|
||||
},
|
||||
"monitor": {
|
||||
"toggle_grid": "切换布局",
|
||||
"average": "平均值",
|
||||
"max_1_min": "1 分钟最大",
|
||||
"total_cpu_usage": "总 CPU 使用率",
|
||||
"cpu_des": "系统范围的 CPU 利用率",
|
||||
"docker_cpu_usage": "Docker CPU 使用率",
|
||||
"docker_cpu_des": "Docker 的平均 CPU 利用率",
|
||||
"total_memory_usage": "总内存使用率",
|
||||
"memory_des": "记录时间点的精确利用率",
|
||||
"docker_memory_usage": "Docker 内存使用率",
|
||||
"docker_memory_des": "Docker 容器的内存使用率",
|
||||
"disk_space": "磁盘空间",
|
||||
"disk_des": "根分区的使用情况",
|
||||
"disk_io": "磁盘 I/O",
|
||||
"disk_io_des": "根文件系统的吞吐量",
|
||||
"bandwidth": "带宽",
|
||||
"bandwidth_des": "公共接口的网络流量",
|
||||
"docker_network_io": "Docker 网络 I/O",
|
||||
"docker_network_io_des": "Docker 容器的网络流量",
|
||||
"swap_usage": "交换空间使用率",
|
||||
"swap_des": "系统使用的交换空间",
|
||||
"temperature": "温度",
|
||||
"temperature_des": "系统传感器的温度",
|
||||
"usage": "使用率",
|
||||
"disk_usage_of": "的磁盘使用率",
|
||||
"throughput_of": "的吞吐量",
|
||||
"waiting_for": "等待足够的记录以显示"
|
||||
},
|
||||
"auth": {
|
||||
"login": "请登入你的账户",
|
||||
"reset": "输入邮箱来重设密码",
|
||||
"create": "请创建管理员账户",
|
||||
"create_account": "创建账户",
|
||||
"sign_in": "登入",
|
||||
"openid_des": "Beszel 支持 OpenID Connect 和许多 OAuth2 认证提供商。",
|
||||
"please_view_the": "请检视",
|
||||
"for_instructions": "以获取更多信息。",
|
||||
"forgot_password": "忘记密码?",
|
||||
"reset_password": "重设密码",
|
||||
"command_line_instructions": "了解命令行指令",
|
||||
"command_1": "如果您忘记了管理员账户的密码,可以使用以下命令重置。",
|
||||
"command_2": "然后登录到后台,在用户表中重置您的用户账户密码。"
|
||||
}
|
||||
}
|
||||
|
@@ -1,194 +1,194 @@
|
||||
{
|
||||
"all_systems": "所有伺服器",
|
||||
"filter": "篩選...",
|
||||
"copy": "複製",
|
||||
"add": "新增",
|
||||
"system": "伺服器",
|
||||
"systems": "伺服器",
|
||||
"cancel": "取消",
|
||||
"continue": "繼續",
|
||||
"home": {
|
||||
"active_alerts": "活動警報",
|
||||
"active_des": "在過去 {{minutes}} 分鐘內超過 {{value}}{{unit}} 平均值",
|
||||
"subtitle_1": "即時更新。按",
|
||||
"subtitle_2": "打開指令面板。"
|
||||
},
|
||||
"systems_table": {
|
||||
"system": "伺服器",
|
||||
"memory": "記憶體",
|
||||
"cpu": "CPU",
|
||||
"disk": "磁碟",
|
||||
"net": "網絡",
|
||||
"agent": "客戶端",
|
||||
"no_systems_found": "未找到伺服器。",
|
||||
"open_menu": "打開選單",
|
||||
"resume": "恢復",
|
||||
"pause": "暫停",
|
||||
"copy_host": "複製主機",
|
||||
"delete": "刪除",
|
||||
"delete_confirm": "您確定要刪除 {{name}} 嗎?",
|
||||
"delete_confirm_des_1": "此操作無法撤銷。這將永久從資料庫中刪除",
|
||||
"delete_confirm_des_2": "的所有記錄。"
|
||||
},
|
||||
"alerts": {
|
||||
"title": "警報",
|
||||
"subtitle_1": "查看",
|
||||
"notification_settings": "通知設定",
|
||||
"subtitle_2": "配置如何接收警報。",
|
||||
"overwrite_existing_alerts": "覆蓋現有警報",
|
||||
"info": {
|
||||
"status": "狀態",
|
||||
"status_des": "狀態在 在線/離線 之間切換時觸發。",
|
||||
"cpu_usage": "CPU 使用率",
|
||||
"cpu_usage_des": "當 CPU 使用率超過閾值時觸發。",
|
||||
"memory_usage": "記憶體使用率",
|
||||
"memory_usage_des": "當記憶體使用率超過閾值時觸發。",
|
||||
"disk_usage": "磁碟使用率",
|
||||
"disk_usage_des": "當任何磁碟的使用率超過閾值時觸發。",
|
||||
"bandwidth": "頻寬",
|
||||
"bandwidth_des": "當組合的 上行/下行 流量超過閾值時觸發。",
|
||||
"temperature": "溫度",
|
||||
"temperature_des": "當任何感應器超過閾值時觸發。"
|
||||
},
|
||||
"average_exceeds": "平均值超過",
|
||||
"for": "持續",
|
||||
"minute": "分鐘",
|
||||
"minutes": "分鐘"
|
||||
},
|
||||
"settings": {
|
||||
"settings": "設定",
|
||||
"subtitle": "管理顯示和通知偏好。",
|
||||
"save_settings": "儲存設定",
|
||||
"export_configuration": "匯出配置",
|
||||
"general": {
|
||||
"title": "一般",
|
||||
"subtitle": "更改通用應用程式選項。",
|
||||
"language": {
|
||||
"title": "語言",
|
||||
"subtitle_1": "想幫助我們改進翻譯嗎?查看",
|
||||
"subtitle_2": "了解更多詳情。",
|
||||
"preferred_language": "首選語言"
|
||||
},
|
||||
"chart_options": {
|
||||
"title": "圖表選項",
|
||||
"subtitle": "調整圖表的顯示選項。",
|
||||
"default_time_period": "預設時間段",
|
||||
"default_time_period_des": "設定查看伺服器時圖表的預設時間範圍。"
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"title": "通知",
|
||||
"subtitle_1": "設定如何接收警報通知。",
|
||||
"subtitle_2": "正在尋找建立警報的位置?點擊伺服器列表中的",
|
||||
"subtitle_3": "圖示。",
|
||||
"email": {
|
||||
"title": "電郵通知",
|
||||
"please": "配置",
|
||||
"configure_an_SMTP_server": "SMTP 伺服器",
|
||||
"to_ensure_alerts_are_delivered": "以確保郵件可以成功發送。",
|
||||
"to_email_s": "收件人電郵",
|
||||
"enter_email_address": "輸入電郵地址...",
|
||||
"des": "使用 Enter 鍵或逗號儲存地址。留空以禁用電郵通知。"
|
||||
},
|
||||
"webhook_push": {
|
||||
"title": "Webhook / 推送通知",
|
||||
"des_1": "Beszel 使用",
|
||||
"des_2": "與流行的通知服務整合。",
|
||||
"add_url": "新增 URL"
|
||||
}
|
||||
},
|
||||
"yaml_config": {
|
||||
"short_title": "YAML 配置",
|
||||
"title": "YAML 配置",
|
||||
"subtitle": "匯出當前系統配置。",
|
||||
"des_1": "系統使用",
|
||||
"des_2": "配置檔案儲存管理數據",
|
||||
"des_3": "每次重啟時,資料庫中的系統將更新以匹配檔案中定義的系統。",
|
||||
"alert": {
|
||||
"title": "警告 - 潛在數據丟失",
|
||||
"des_1": "未在",
|
||||
"des_2": "中定義的現有系統將被刪除。請定期備份。"
|
||||
}
|
||||
},
|
||||
"language": "語言"
|
||||
},
|
||||
"user_dm": {
|
||||
"users": "用戶",
|
||||
"logs": "日誌",
|
||||
"backups": "備份",
|
||||
"auth_providers": "身份驗證提供者",
|
||||
"log_out": "登出"
|
||||
},
|
||||
"themes": {
|
||||
"toggle_theme": "切換主題",
|
||||
"light": "淺色",
|
||||
"dark": "深色",
|
||||
"system": "系統"
|
||||
},
|
||||
"add_system": {
|
||||
"add_new_system": "新增新伺服器",
|
||||
"binary": "二進制檔案",
|
||||
"dialog_des_1": "客戶端必須在伺服器上運行才能連接。複製",
|
||||
"dialog_des_2": "到目標伺服器以安裝。",
|
||||
"name": "名稱",
|
||||
"host_ip": "主機/IP",
|
||||
"port": "端口",
|
||||
"public_key": "公鑰",
|
||||
"click_to_copy": "點擊複製",
|
||||
"command": "指令",
|
||||
"add_system": "新增伺服器"
|
||||
},
|
||||
"command": {
|
||||
"search": "在設定或系統中搜尋...",
|
||||
"pages_settings": "頁面 / 設定",
|
||||
"dashboard": "儀表板",
|
||||
"documentation": "文件",
|
||||
"SMTP_settings": "SMTP 設定",
|
||||
"page": "頁面",
|
||||
"admin": "管理員"
|
||||
},
|
||||
"monitor": {
|
||||
"toggle_grid": "切換佈局",
|
||||
"average": "平均值",
|
||||
"max_1_min": "1 分鐘最大",
|
||||
"total_cpu_usage": "總 CPU 使用率",
|
||||
"cpu_des": "系統範圍的 CPU 利用率",
|
||||
"docker_cpu_usage": "Docker CPU 使用率",
|
||||
"docker_cpu_des": "Docker 的平均 CPU 利用率",
|
||||
"total_memory_usage": "總記憶體使用率",
|
||||
"memory_des": "記錄時間點的精確利用率",
|
||||
"docker_memory_usage": "Docker 記憶體使用率",
|
||||
"docker_memory_des": "Docker 容器的記憶體使用率",
|
||||
"disk_space": "磁碟空間",
|
||||
"disk_des": "根分區的使用情況",
|
||||
"disk_io": "磁碟 I/O",
|
||||
"disk_io_des": "根檔案系統的吞吐量",
|
||||
"bandwidth": "頻寬",
|
||||
"bandwidth_des": "公共介面的網絡流量",
|
||||
"docker_network_io": "Docker 網絡 I/O",
|
||||
"docker_network_io_des": "Docker 容器的網絡流量",
|
||||
"swap_usage": "交換空間使用率",
|
||||
"swap_des": "系統使用的交換空間",
|
||||
"temperature": "溫度",
|
||||
"temperature_des": "系統感應器的溫度",
|
||||
"usage": "使用率",
|
||||
"disk_usage_of": "的磁碟使用率",
|
||||
"throughput_of": "的吞吐量",
|
||||
"waiting_for": "等待足夠的記錄以顯示"
|
||||
},
|
||||
"auth": {
|
||||
"login": "請登入你的賬戶",
|
||||
"reset": "輸入電郵來重設密碼",
|
||||
"create": "請創建管理員賬戶",
|
||||
"create_account": "創建賬戶",
|
||||
"sign_in": "登入",
|
||||
"openid_des": "Beszel 支持 OpenID Connect 和許多 OAuth2 認證提供商。",
|
||||
"please_view_the": "請檢視",
|
||||
"for_instructions": "以獲取更多信息。",
|
||||
"forgot_password": "忘記密碼?",
|
||||
"reset_password": "重設密碼",
|
||||
"command_line_instructions": "了解命令行指令",
|
||||
"command_1": "如果您忘記了管理員賬戶的密碼,可以使用以下命令重置。",
|
||||
"command_2": "然後登入到後台,在用戶表中重置您的用戶賬戶密碼。"
|
||||
}
|
||||
}
|
||||
"all_systems": "所有伺服器",
|
||||
"filter": "篩選...",
|
||||
"copy": "複製",
|
||||
"add": "新增",
|
||||
"system": "伺服器",
|
||||
"systems": "伺服器",
|
||||
"cancel": "取消",
|
||||
"continue": "繼續",
|
||||
"home": {
|
||||
"active_alerts": "活動警報",
|
||||
"active_des": "在過去 {{minutes}} 分鐘內超過 {{value}}{{unit}} 平均值",
|
||||
"subtitle_1": "即時更新。按",
|
||||
"subtitle_2": "打開指令面板。"
|
||||
},
|
||||
"systems_table": {
|
||||
"system": "伺服器",
|
||||
"memory": "記憶體",
|
||||
"cpu": "CPU",
|
||||
"disk": "磁碟",
|
||||
"net": "網絡",
|
||||
"agent": "客戶端",
|
||||
"no_systems_found": "未找到伺服器。",
|
||||
"open_menu": "打開選單",
|
||||
"resume": "恢復",
|
||||
"pause": "暫停",
|
||||
"copy_host": "複製主機",
|
||||
"delete": "刪除",
|
||||
"delete_confirm": "您確定要刪除 {{name}} 嗎?",
|
||||
"delete_confirm_des_1": "此操作無法撤銷。這將永久從資料庫中刪除",
|
||||
"delete_confirm_des_2": "的所有記錄。"
|
||||
},
|
||||
"alerts": {
|
||||
"title": "警報",
|
||||
"subtitle_1": "查看",
|
||||
"notification_settings": "通知設定",
|
||||
"subtitle_2": "配置如何接收警報。",
|
||||
"overwrite_existing_alerts": "覆蓋現有警報",
|
||||
"info": {
|
||||
"status": "狀態",
|
||||
"status_des": "狀態在 在線/離線 之間切換時觸發。",
|
||||
"cpu_usage": "CPU 使用率",
|
||||
"cpu_usage_des": "當 CPU 使用率超過閾值時觸發。",
|
||||
"memory_usage": "記憶體使用率",
|
||||
"memory_usage_des": "當記憶體使用率超過閾值時觸發。",
|
||||
"disk_usage": "磁碟使用率",
|
||||
"disk_usage_des": "當任何磁碟的使用率超過閾值時觸發。",
|
||||
"bandwidth": "頻寬",
|
||||
"bandwidth_des": "當組合的 上行/下行 流量超過閾值時觸發。",
|
||||
"temperature": "溫度",
|
||||
"temperature_des": "當任何感應器超過閾值時觸發。"
|
||||
},
|
||||
"average_exceeds": "平均值超過",
|
||||
"for": "持續",
|
||||
"minute": "分鐘",
|
||||
"minutes": "分鐘"
|
||||
},
|
||||
"settings": {
|
||||
"settings": "設定",
|
||||
"subtitle": "管理顯示和通知偏好。",
|
||||
"save_settings": "儲存設定",
|
||||
"export_configuration": "匯出配置",
|
||||
"general": {
|
||||
"title": "一般",
|
||||
"subtitle": "更改通用應用程式選項。",
|
||||
"language": {
|
||||
"title": "語言",
|
||||
"subtitle_1": "想幫助我們改進翻譯嗎?查看",
|
||||
"subtitle_2": "了解更多詳情。",
|
||||
"preferred_language": "首選語言"
|
||||
},
|
||||
"chart_options": {
|
||||
"title": "圖表選項",
|
||||
"subtitle": "調整圖表的顯示選項。",
|
||||
"default_time_period": "預設時間段",
|
||||
"default_time_period_des": "設定查看伺服器時圖表的預設時間範圍。"
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"title": "通知",
|
||||
"subtitle_1": "設定如何接收警報通知。",
|
||||
"subtitle_2": "正在尋找建立警報的位置?點擊伺服器列表中的",
|
||||
"subtitle_3": "圖示。",
|
||||
"email": {
|
||||
"title": "電郵通知",
|
||||
"please": "配置",
|
||||
"configure_an_SMTP_server": "SMTP 伺服器",
|
||||
"to_ensure_alerts_are_delivered": "以確保郵件可以成功發送。",
|
||||
"to_email_s": "收件人電郵",
|
||||
"enter_email_address": "輸入電郵地址...",
|
||||
"des": "使用 Enter 鍵或逗號儲存地址。留空以禁用電郵通知。"
|
||||
},
|
||||
"webhook_push": {
|
||||
"title": "Webhook / 推送通知",
|
||||
"des_1": "Beszel 使用",
|
||||
"des_2": "與流行的通知服務整合。",
|
||||
"add_url": "新增 URL"
|
||||
}
|
||||
},
|
||||
"yaml_config": {
|
||||
"short_title": "YAML 配置",
|
||||
"title": "YAML 配置",
|
||||
"subtitle": "匯出當前系統配置。",
|
||||
"des_1": "系統使用",
|
||||
"des_2": "配置檔案儲存管理數據",
|
||||
"des_3": "每次重啟時,資料庫中的系統將更新以匹配檔案中定義的系統。",
|
||||
"alert": {
|
||||
"title": "警告 - 潛在數據丟失",
|
||||
"des_1": "未在",
|
||||
"des_2": "中定義的現有系統將被刪除。請定期備份。"
|
||||
}
|
||||
},
|
||||
"language": "語言"
|
||||
},
|
||||
"user_dm": {
|
||||
"users": "用戶",
|
||||
"logs": "日誌",
|
||||
"backups": "備份",
|
||||
"auth_providers": "身份驗證提供者",
|
||||
"log_out": "登出"
|
||||
},
|
||||
"themes": {
|
||||
"toggle_theme": "切換主題",
|
||||
"light": "淺色",
|
||||
"dark": "深色",
|
||||
"system": "系統"
|
||||
},
|
||||
"add_system": {
|
||||
"add_new_system": "新增新伺服器",
|
||||
"binary": "二進制檔案",
|
||||
"dialog_des_1": "客戶端必須在伺服器上運行才能連接。複製",
|
||||
"dialog_des_2": "到目標伺服器以安裝。",
|
||||
"name": "名稱",
|
||||
"host_ip": "主機/IP",
|
||||
"port": "端口",
|
||||
"public_key": "公鑰",
|
||||
"click_to_copy": "點擊複製",
|
||||
"command": "指令",
|
||||
"add_system": "新增伺服器"
|
||||
},
|
||||
"command": {
|
||||
"search": "在設定或系統中搜尋...",
|
||||
"pages_settings": "頁面 / 設定",
|
||||
"dashboard": "儀表板",
|
||||
"documentation": "文件",
|
||||
"SMTP_settings": "SMTP 設定",
|
||||
"page": "頁面",
|
||||
"admin": "管理員"
|
||||
},
|
||||
"monitor": {
|
||||
"toggle_grid": "切換佈局",
|
||||
"average": "平均值",
|
||||
"max_1_min": "1 分鐘最大",
|
||||
"total_cpu_usage": "總 CPU 使用率",
|
||||
"cpu_des": "系統範圍的 CPU 利用率",
|
||||
"docker_cpu_usage": "Docker CPU 使用率",
|
||||
"docker_cpu_des": "Docker 的平均 CPU 利用率",
|
||||
"total_memory_usage": "總記憶體使用率",
|
||||
"memory_des": "記錄時間點的精確利用率",
|
||||
"docker_memory_usage": "Docker 記憶體使用率",
|
||||
"docker_memory_des": "Docker 容器的記憶體使用率",
|
||||
"disk_space": "磁碟空間",
|
||||
"disk_des": "根分區的使用情況",
|
||||
"disk_io": "磁碟 I/O",
|
||||
"disk_io_des": "根檔案系統的吞吐量",
|
||||
"bandwidth": "頻寬",
|
||||
"bandwidth_des": "公共介面的網絡流量",
|
||||
"docker_network_io": "Docker 網絡 I/O",
|
||||
"docker_network_io_des": "Docker 容器的網絡流量",
|
||||
"swap_usage": "交換空間使用率",
|
||||
"swap_des": "系統使用的交換空間",
|
||||
"temperature": "溫度",
|
||||
"temperature_des": "系統感應器的溫度",
|
||||
"usage": "使用率",
|
||||
"disk_usage_of": "的磁碟使用率",
|
||||
"throughput_of": "的吞吐量",
|
||||
"waiting_for": "等待足夠的記錄以顯示"
|
||||
},
|
||||
"auth": {
|
||||
"login": "請登入你的賬戶",
|
||||
"reset": "輸入電郵來重設密碼",
|
||||
"create": "請創建管理員賬戶",
|
||||
"create_account": "創建賬戶",
|
||||
"sign_in": "登入",
|
||||
"openid_des": "Beszel 支持 OpenID Connect 和許多 OAuth2 認證提供商。",
|
||||
"please_view_the": "請檢視",
|
||||
"for_instructions": "以獲取更多信息。",
|
||||
"forgot_password": "忘記密碼?",
|
||||
"reset_password": "重設密碼",
|
||||
"command_line_instructions": "了解命令行指令",
|
||||
"command_1": "如果您忘記了管理員賬戶的密碼,可以使用以下命令重置。",
|
||||
"command_2": "然後登入到後台,在用戶表中重置您的用戶賬戶密碼。"
|
||||
}
|
||||
}
|
||||
|
@@ -1,30 +1,21 @@
|
||||
import './index.css'
|
||||
import { Suspense, lazy, useEffect, StrictMode } from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import Home from './components/routes/home.tsx'
|
||||
import { ThemeProvider } from './components/theme-provider.tsx'
|
||||
import {
|
||||
$authenticated,
|
||||
$systems,
|
||||
pb,
|
||||
$publicKey,
|
||||
$hubVersion,
|
||||
$copyContent,
|
||||
} from './lib/stores.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'
|
||||
import "./index.css"
|
||||
import { Suspense, lazy, useEffect, StrictMode } from "react"
|
||||
import ReactDOM from "react-dom/client"
|
||||
import Home from "./components/routes/home.tsx"
|
||||
import { ThemeProvider } from "./components/theme-provider.tsx"
|
||||
import { $authenticated, $systems, pb, $publicKey, $hubVersion, $copyContent } from "./lib/stores.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 Navbar from "./components/navbar.tsx"
|
||||
import "./lib/i18n.ts"
|
||||
|
||||
// const ServerDetail = lazy(() => import('./components/routes/system.tsx'))
|
||||
const LoginPage = lazy(() => import('./components/login/login.tsx'))
|
||||
const CopyToClipboardDialog = lazy(() => import('./components/copy-to-clipboard.tsx'))
|
||||
const Settings = lazy(() => import('./components/routes/settings/layout.tsx'))
|
||||
const LoginPage = lazy(() => import("./components/login/login.tsx"))
|
||||
const CopyToClipboardDialog = lazy(() => import("./components/copy-to-clipboard.tsx"))
|
||||
const Settings = lazy(() => import("./components/routes/settings/layout.tsx"))
|
||||
|
||||
const App = () => {
|
||||
const page = useStore($router)
|
||||
@@ -37,7 +28,7 @@ const App = () => {
|
||||
$authenticated.set(pb.authStore.isValid)
|
||||
})
|
||||
// get version / public key
|
||||
pb.send('/api/beszel/getkey', {}).then((data) => {
|
||||
pb.send("/api/beszel/getkey", {}).then((data) => {
|
||||
$publicKey.set(data.key)
|
||||
$hubVersion.set(data.v)
|
||||
})
|
||||
@@ -46,34 +37,34 @@ const App = () => {
|
||||
// get alerts after system list is loaded
|
||||
updateSystemList().then(updateAlerts)
|
||||
|
||||
return () => updateFavicon('favicon.svg')
|
||||
return () => updateFavicon("favicon.svg")
|
||||
}, [])
|
||||
|
||||
// update favicon
|
||||
useEffect(() => {
|
||||
if (!systems.length || !authenticated) {
|
||||
updateFavicon('favicon.svg')
|
||||
updateFavicon("favicon.svg")
|
||||
} else {
|
||||
let up = false
|
||||
for (const system of systems) {
|
||||
if (system.status === 'down') {
|
||||
updateFavicon('favicon-red.svg')
|
||||
if (system.status === "down") {
|
||||
updateFavicon("favicon-red.svg")
|
||||
return
|
||||
} else if (system.status === 'up') {
|
||||
} else if (system.status === "up") {
|
||||
up = true
|
||||
}
|
||||
}
|
||||
updateFavicon(up ? 'favicon-green.svg' : 'favicon.svg')
|
||||
updateFavicon(up ? "favicon-green.svg" : "favicon.svg")
|
||||
}
|
||||
}, [systems])
|
||||
|
||||
if (!page) {
|
||||
return <h1 className="text-3xl text-center my-14">404</h1>
|
||||
} else if (page.path === '/') {
|
||||
} else if (page.path === "/") {
|
||||
return <Home />
|
||||
} else if (page.route === 'server') {
|
||||
} else if (page.route === "server") {
|
||||
return <SystemDetail name={page.params.name} />
|
||||
} else if (page.route === 'settings') {
|
||||
} else if (page.route === "settings") {
|
||||
return (
|
||||
<Suspense>
|
||||
<Settings />
|
||||
@@ -83,8 +74,6 @@ const App = () => {
|
||||
}
|
||||
|
||||
const Layout = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const authenticated = useStore($authenticated)
|
||||
const copyContent = useStore($copyContent)
|
||||
|
||||
@@ -98,7 +87,7 @@ const Layout = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="container">{Navbar(t)}</div>
|
||||
<div className="container">{Navbar()}</div>
|
||||
<div className="container mb-14 relative">
|
||||
<App />
|
||||
{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
|
||||
// and breaks the clipboard dialog
|
||||
//<StrictMode>
|
||||
|
10
beszel/site/src/types.d.ts
vendored
10
beszel/site/src/types.d.ts
vendored
@@ -1,9 +1,9 @@
|
||||
import { RecordModel } from 'pocketbase'
|
||||
import { RecordModel } from "pocketbase"
|
||||
|
||||
export interface SystemRecord extends RecordModel {
|
||||
name: string
|
||||
host: string
|
||||
status: 'up' | 'down' | 'paused' | 'pending'
|
||||
status: "up" | "down" | "paused" | "pending"
|
||||
port: string
|
||||
info: SystemInfo
|
||||
v: string
|
||||
@@ -132,11 +132,11 @@ export interface AlertRecord extends RecordModel {
|
||||
// user: string
|
||||
}
|
||||
|
||||
export type ChartTimes = '1h' | '12h' | '24h' | '1w' | '30d'
|
||||
export type ChartTimes = "1h" | "12h" | "24h" | "1w" | "30d"
|
||||
|
||||
export interface ChartTimeData {
|
||||
[key: string]: {
|
||||
type: '1m' | '10m' | '20m' | '120m' | '480m'
|
||||
type: "1m" | "10m" | "20m" | "120m" | "480m"
|
||||
expectedInterval: number
|
||||
label: string
|
||||
ticks?: number
|
||||
@@ -155,7 +155,7 @@ export type UserSettings = {
|
||||
type ChartDataContainer = {
|
||||
created: number | null
|
||||
} & {
|
||||
[key: string]: key extends 'created' ? never : ContainerStats
|
||||
[key: string]: key extends "created" ? never : ContainerStats
|
||||
}
|
||||
|
||||
export interface ChartData {
|
||||
|
@@ -1,104 +1,99 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
darkMode: ['class'],
|
||||
content: [
|
||||
'./pages/**/*.{ts,tsx}',
|
||||
'./components/**/*.{ts,tsx}',
|
||||
'./app/**/*.{ts,tsx}',
|
||||
'./src/**/*.{ts,tsx}',
|
||||
],
|
||||
prefix: '',
|
||||
darkMode: ["class"],
|
||||
content: ["./pages/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}", "./app/**/*.{ts,tsx}", "./src/**/*.{ts,tsx}"],
|
||||
prefix: "",
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: '1rem',
|
||||
padding: "1rem",
|
||||
screens: {
|
||||
'2xl': '1400px',
|
||||
"2xl": "1400px",
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: 'Inter, sans-serif',
|
||||
sans: "Inter, sans-serif",
|
||||
// body: ['Inter', 'sans-serif'],
|
||||
// display: ['Inter', 'sans-serif'],
|
||||
},
|
||||
screens: {
|
||||
xs: '425px',
|
||||
450: '450px',
|
||||
xs: "425px",
|
||||
450: "450px",
|
||||
},
|
||||
colors: {
|
||||
green: {
|
||||
50: '#EBF9F0',
|
||||
100: '#D8F3E1',
|
||||
200: '#ADE6C0',
|
||||
300: '#85DBA2',
|
||||
400: '#5ACE81',
|
||||
500: '#38BB63',
|
||||
600: '#2D954F',
|
||||
700: '#22723D',
|
||||
800: '#164B28',
|
||||
900: '#0C2715',
|
||||
950: '#06140A',
|
||||
50: "#EBF9F0",
|
||||
100: "#D8F3E1",
|
||||
200: "#ADE6C0",
|
||||
300: "#85DBA2",
|
||||
400: "#5ACE81",
|
||||
500: "#38BB63",
|
||||
600: "#2D954F",
|
||||
700: "#22723D",
|
||||
800: "#164B28",
|
||||
900: "#0C2715",
|
||||
950: "#06140A",
|
||||
},
|
||||
border: 'hsl(var(--border))',
|
||||
input: 'hsl(var(--input))',
|
||||
ring: 'hsl(var(--ring))',
|
||||
background: 'hsl(var(--background))',
|
||||
foreground: 'hsl(var(--foreground))',
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
background: "hsl(var(--background))",
|
||||
foreground: "hsl(var(--foreground))",
|
||||
primary: {
|
||||
DEFAULT: 'hsl(var(--primary))',
|
||||
foreground: 'hsl(var(--primary-foreground))',
|
||||
DEFAULT: "hsl(var(--primary))",
|
||||
foreground: "hsl(var(--primary-foreground))",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: 'hsl(var(--secondary))',
|
||||
foreground: 'hsl(var(--secondary-foreground))',
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))",
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: 'hsl(var(--destructive))',
|
||||
foreground: 'hsl(var(--destructive-foreground))',
|
||||
DEFAULT: "hsl(var(--destructive))",
|
||||
foreground: "hsl(var(--destructive-foreground))",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: 'hsl(var(--muted))',
|
||||
foreground: 'hsl(var(--muted-foreground))',
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
foreground: "hsl(var(--muted-foreground))",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: 'hsl(var(--accent))',
|
||||
foreground: 'hsl(var(--accent-foreground))',
|
||||
DEFAULT: "hsl(var(--accent))",
|
||||
foreground: "hsl(var(--accent-foreground))",
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: 'hsl(var(--popover))',
|
||||
foreground: 'hsl(var(--popover-foreground))',
|
||||
DEFAULT: "hsl(var(--popover))",
|
||||
foreground: "hsl(var(--popover-foreground))",
|
||||
},
|
||||
card: {
|
||||
DEFAULT: 'hsl(var(--card))',
|
||||
foreground: 'hsl(var(--card-foreground))',
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: 'var(--radius)',
|
||||
md: 'calc(var(--radius) - 2px)',
|
||||
sm: 'calc(var(--radius) - 4px)',
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
},
|
||||
keyframes: {
|
||||
'accordion-down': {
|
||||
from: { height: '0' },
|
||||
to: { height: 'var(--radix-accordion-content-height)' },
|
||||
"accordion-down": {
|
||||
from: { height: "0" },
|
||||
to: { height: "var(--radix-accordion-content-height)" },
|
||||
},
|
||||
'accordion-up': {
|
||||
from: { height: 'var(--radix-accordion-content-height)' },
|
||||
to: { height: '0' },
|
||||
"accordion-up": {
|
||||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
to: { height: "0" },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
'accordion-down': 'accordion-down 0.2s ease-out',
|
||||
'accordion-up': 'accordion-up 0.2s ease-out',
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
require('tailwindcss-animate'),
|
||||
require("tailwindcss-animate"),
|
||||
function ({ addVariant }) {
|
||||
addVariant('light', '.light &')
|
||||
addVariant("light", ".light &")
|
||||
},
|
||||
],
|
||||
}
|
||||
|
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
|
@@ -1,15 +1,15 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import path from 'path'
|
||||
import { defineConfig } from "vite"
|
||||
import react from "@vitejs/plugin-react"
|
||||
import path from "path"
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
esbuild: {
|
||||
legalComments: 'external',
|
||||
legalComments: "external",
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
Reference in New Issue
Block a user