mirror of
https://github.com/fankes/beszel.git
synced 2025-10-19 17:59:28 +08:00
ctrl k & i18n
This commit is contained in:
Binary file not shown.
@@ -31,11 +31,14 @@
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.0.0",
|
||||
"d3-time": "^3.1.0",
|
||||
"i18next": "^23.16.4",
|
||||
"i18next-browser-languagedetector": "^8.0.0",
|
||||
"lucide-react": "^0.452.0",
|
||||
"nanostores": "^0.11.3",
|
||||
"pocketbase": "^0.21.5",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-i18next": "^15.1.0",
|
||||
"recharts": "^2.13.0",
|
||||
"tailwind-merge": "^2.5.4",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
|
@@ -24,8 +24,11 @@ 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()
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const port = useRef() as MutableRefObject<HTMLInputElement>
|
||||
const publicKey = useStore($publicKey)
|
||||
@@ -74,41 +77,40 @@ export function AddSystemButton({ className }: { className?: string }) {
|
||||
className={cn('flex gap-1 max-xs:h-[2.4rem]', className, isReadOnlyUser() && 'hidden')}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4 -ml-1" />
|
||||
Add <span className="hidden xs:inline">System</span>
|
||||
{t('add')} <span className="hidden xs:inline">{t('system')}</span>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="w-[90%] sm:max-w-[425px] rounded-lg">
|
||||
<Tabs defaultValue="docker">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="mb-2">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">Binary</TabsTrigger>
|
||||
<TabsTrigger value="binary">{t('add_system.binary')}</TabsTrigger>
|
||||
</TabsList>
|
||||
</DialogHeader>
|
||||
<TabsContent value="docker">
|
||||
<DialogDescription className={'mb-4'}>
|
||||
The agent must be running on the system to connect. Copy the{' '}
|
||||
<code className="bg-muted px-1 rounded-sm">docker-compose.yml</code> for the agent
|
||||
below.
|
||||
{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>
|
||||
<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">
|
||||
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">
|
||||
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">
|
||||
Port
|
||||
{t('add_system.port')}
|
||||
</Label>
|
||||
<Input
|
||||
ref={port}
|
||||
@@ -121,7 +123,7 @@ export function AddSystemButton({ className }: { className?: string }) {
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4 relative">
|
||||
<Label htmlFor="pkey" className="text-right whitespace-pre">
|
||||
Public Key
|
||||
{t('add_system.public_key')}
|
||||
</Label>
|
||||
<Input readOnly id="pkey" value={publicKey} className="col-span-3" required></Input>
|
||||
<div
|
||||
@@ -142,7 +144,7 @@ export function AddSystemButton({ className }: { className?: string }) {
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Click to copy</p>
|
||||
<p>{t('add_system.click_to_copy')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
@@ -154,34 +156,34 @@ export function AddSystemButton({ className }: { className?: string }) {
|
||||
variant={'ghost'}
|
||||
onClick={() => copyDockerCompose(port.current.value)}
|
||||
>
|
||||
Copy docker compose
|
||||
{t('copy')} docker compose
|
||||
</Button>
|
||||
<Button>Add system</Button>
|
||||
<Button>{t('add_system.add_system')}</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</TabsContent>
|
||||
<TabsContent value="binary">
|
||||
<DialogDescription className={'mb-4'}>
|
||||
The agent must be running on the system to connect. Copy the{' '}
|
||||
<code className="bg-muted px-1 rounded-sm">install command</code> for the agent below.
|
||||
{t('add_system.dialog_des_1')}{' '}
|
||||
<code className="bg-muted px-1 rounded-sm">install command</code> {t('add_system.dialog_des_2')}
|
||||
</DialogDescription>
|
||||
<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">
|
||||
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">
|
||||
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">
|
||||
Port
|
||||
{t('add_system.port')}
|
||||
</Label>
|
||||
<Input
|
||||
ref={port}
|
||||
@@ -194,7 +196,7 @@ export function AddSystemButton({ className }: { className?: string }) {
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4 relative">
|
||||
<Label htmlFor="pkey" className="text-right whitespace-pre">
|
||||
Public Key
|
||||
{t('add_system.public_key')}
|
||||
</Label>
|
||||
<Input readOnly id="pkey" value={publicKey} className="col-span-3" required></Input>
|
||||
<div
|
||||
@@ -215,7 +217,7 @@ export function AddSystemButton({ className }: { className?: string }) {
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Click to copy</p>
|
||||
<p>{t('add_system.click_to_copy')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
@@ -227,14 +229,14 @@ export function AddSystemButton({ className }: { className?: string }) {
|
||||
variant={'ghost'}
|
||||
onClick={() => copyInstallCommand(port.current.value)}
|
||||
>
|
||||
Copy linux command
|
||||
{t('copy')} linux {t('add_system.command')}
|
||||
</Button>
|
||||
<Button>Add system</Button>
|
||||
<Button>{t('add_system.add_system')}</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</DialogContent>
|
||||
</Dialog >
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
@@ -17,6 +17,7 @@ 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)
|
||||
@@ -54,6 +55,8 @@ function TheContent({
|
||||
}: {
|
||||
data: { system: SystemRecord; alerts: AlertRecord[]; systemAlerts: AlertRecord[] }
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [overwriteExisting, setOverwriteExisting] = useState<boolean | 'indeterminate'>(false)
|
||||
const systems = $systems.get()
|
||||
|
||||
@@ -69,13 +72,13 @@ function TheContent({
|
||||
return (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl">Alerts</DialogTitle>
|
||||
<DialogTitle className="text-xl">{t('alerts.title')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
See{' '}
|
||||
{t('alerts.subtitle_1')}{' '}
|
||||
<Link href="/settings/notifications" className="link">
|
||||
notification settings
|
||||
{t('alerts.notification_settings')}
|
||||
</Link>{' '}
|
||||
to configure how you receive alerts.
|
||||
{t('alerts.subtitle_2')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Tabs defaultValue="system">
|
||||
@@ -86,7 +89,7 @@ function TheContent({
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="global">
|
||||
<GlobeIcon className="mr-1.5 h-3.5 w-3.5" />
|
||||
All systems
|
||||
{t('all_systems')}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="system">
|
||||
@@ -107,7 +110,7 @@ function TheContent({
|
||||
checked={overwriteExisting}
|
||||
onCheckedChange={setOverwriteExisting}
|
||||
/>
|
||||
Overwrite existing alerts
|
||||
{t('alerts.overwrite_existing_alerts')}
|
||||
</label>
|
||||
<div className="grid gap-3">
|
||||
{data.map((d) => (
|
||||
|
42
beszel/site/src/components/lang-toggle.tsx
Normal file
42
beszel/site/src/components/lang-toggle.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { useEffect } from 'react'
|
||||
import { GlobeIcon, Languages } 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'
|
||||
|
||||
export function LangToggle() {
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.lang = i18n.language;
|
||||
}, [i18n.language]);
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant={'ghost'} size="icon">
|
||||
<GlobeIcon className="absolute h-[1.2rem] w-[1.2rem]" />
|
||||
<span className="sr-only">Language</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
{languages.map(({ lang, label }) => (
|
||||
<DropdownMenuItem
|
||||
key={lang}
|
||||
className={lang === i18n.language ? 'font-bold' : ''}
|
||||
onClick={() => i18n.changeLanguage(lang)}
|
||||
>
|
||||
{label}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
@@ -8,8 +8,10 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { useTheme } from '@/components/theme-provider'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export function ModeToggle() {
|
||||
const { t } = useTranslation()
|
||||
const { setTheme } = useTheme()
|
||||
|
||||
return (
|
||||
@@ -18,21 +20,21 @@ export function ModeToggle() {
|
||||
<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">Toggle theme</span>
|
||||
<span className="sr-only">{t('themes.toggle_theme')}</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem onClick={() => setTheme('light')}>
|
||||
<SunIcon className="mr-2.5 h-4 w-4" />
|
||||
Light
|
||||
{t('themes.light')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme('dark')}>
|
||||
<MoonStarIcon className="mr-2.5 h-4 w-4" />
|
||||
Dark
|
||||
{t('themes.dark')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme('system')}>
|
||||
<LaptopIcon className="mr-2.5 h-4 w-4" />
|
||||
System
|
||||
{t('themes.system')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
@@ -9,10 +9,15 @@ 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 isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
|
||||
|
||||
export default function () {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const hubVersion = useStore($hubVersion)
|
||||
const [filter, setFilter] = useState<string>()
|
||||
const alerts = useStore($alerts)
|
||||
@@ -58,7 +63,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>Active Alerts</CardTitle>
|
||||
<CardTitle>{t('home.active_alerts')}</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="max-sm:p-2">
|
||||
@@ -76,8 +81,11 @@ export default function () {
|
||||
{alert.sysname} {info.name}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
Exceeds {alert.value}
|
||||
{info.unit} average in last {alert.min} min
|
||||
{t('active_des', {
|
||||
value: alert.value,
|
||||
unit: info.unit,
|
||||
minutes: alert.min
|
||||
})}
|
||||
</AlertDescription>
|
||||
<Link
|
||||
href={`/system/${encodeURIComponent(alert.sysname!)}`}
|
||||
@@ -96,17 +104,17 @@ 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">All Systems</CardTitle>
|
||||
<CardTitle className="mb-2.5">{t('all_systems')}</CardTitle>
|
||||
<CardDescription>
|
||||
Updated in real time. Press{' '}
|
||||
{t('home.subtitle_1')}{' '}
|
||||
<kbd className="pointer-events-none inline-flex h-5 select-none items-center gap-0.5 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground opacity-100">
|
||||
<span className="text-xs">⌘</span>K
|
||||
<span className="text-xs">{isMac ? '⌘' : "Ctrl"}</span>K
|
||||
</kbd>{' '}
|
||||
to open the command palette.
|
||||
{t('home.subtitle_2')}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Input
|
||||
placeholder="Filter..."
|
||||
placeholder={t('filter')}
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
className="w-full md:w-56 lg:w-80 ml-auto px-4"
|
||||
/>
|
||||
|
@@ -12,10 +12,18 @@ import { Separator } from '@/components/ui/separator'
|
||||
import { LoaderCircleIcon, SaveIcon } from 'lucide-react'
|
||||
import { UserSettings } from '@/types'
|
||||
import { saveSettings } from './layout'
|
||||
import { useState } from 'react'
|
||||
import { useState, useEffect } from 'react'
|
||||
// import { Input } from '@/components/ui/input'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import languages from '../../../lib/languages.json'
|
||||
|
||||
export default function SettingsProfilePage({ userSettings }: { userSettings: UserSettings }) {
|
||||
const { t, i18n } = useTranslation()
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.lang = i18n.language;
|
||||
}, [i18n.language]);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
@@ -30,46 +38,49 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<h3 className="text-xl font-medium mb-2">General</h3>
|
||||
<h3 className="text-xl font-medium mb-2">{t('settings.general.title')}</h3>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
Change general application options.
|
||||
{t('settings.general.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
<Separator className="my-4" />
|
||||
<form onSubmit={handleSubmit} className="space-y-5">
|
||||
{/* <Separator />
|
||||
<div className="space-y-2">
|
||||
<div className="mb-4">
|
||||
<h3 className="mb-1 text-lg font-medium">Language</h3>
|
||||
<h3 className="mb-1 text-lg font-medium">{t('settings.general.language.title')}</h3>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
Internationalization will be added in a future release. Please see the{' '}
|
||||
{t('settings.general.language.subtitle_1')}{' '}
|
||||
<a href="#" className="link" target="_blank">
|
||||
discussion on GitHub
|
||||
Crowdin
|
||||
</a>{' '}
|
||||
for more details.
|
||||
{t('settings.general.language.subtitle_2')}
|
||||
</p>
|
||||
</div>
|
||||
<Label className="block" htmlFor="lang">
|
||||
Preferred language
|
||||
{t('settings.general.language.preferred_language')}
|
||||
</Label>
|
||||
<Select defaultValue="en">
|
||||
<Select defaultValue={i18n.language} onValueChange={(lang: string) => i18n.changeLanguage(lang)}>
|
||||
<SelectTrigger id="lang">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="en">English</SelectItem>
|
||||
{languages.map((lang) => (
|
||||
<SelectItem key={lang.lang} value={lang.lang}>
|
||||
{lang.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div> */}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="mb-4">
|
||||
<h3 className="mb-1 text-lg font-medium">Chart options</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">
|
||||
Adjust display options for charts.
|
||||
{t('settings.general.chart_options.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
<Label className="block" htmlFor="chartTime">
|
||||
Default time period
|
||||
{t('settings.general.chart_options.default_time_period')}
|
||||
</Label>
|
||||
<Select
|
||||
name="chartTime"
|
||||
@@ -88,7 +99,7 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-[0.8rem] text-muted-foreground">
|
||||
Sets the default time range for charts when a system is viewed.
|
||||
{t('settings.general.chart_options.default_time_period_des')}
|
||||
</p>
|
||||
</div>
|
||||
<Separator />
|
||||
@@ -102,7 +113,7 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
|
||||
) : (
|
||||
<SaveIcon className="h-4 w-4" />
|
||||
)}
|
||||
Save settings
|
||||
{t('settings.save_settings')}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
|
@@ -13,27 +13,7 @@ import General from './general.tsx'
|
||||
import Notifications from './notifications.tsx'
|
||||
import ConfigYaml from './config-yaml.tsx'
|
||||
import { isAdmin } from '@/lib/utils.ts'
|
||||
|
||||
const sidebarNavItems = [
|
||||
{
|
||||
title: 'General',
|
||||
href: '/settings/general',
|
||||
icon: SettingsIcon,
|
||||
},
|
||||
{
|
||||
title: 'Notifications',
|
||||
href: '/settings/notifications',
|
||||
icon: BellIcon,
|
||||
},
|
||||
]
|
||||
|
||||
if (isAdmin()) {
|
||||
sidebarNavItems.push({
|
||||
title: 'YAML Config',
|
||||
href: '/settings/config',
|
||||
icon: FileSlidersIcon,
|
||||
})
|
||||
}
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export async function saveSettings(newSettings: Partial<UserSettings>) {
|
||||
try {
|
||||
@@ -64,6 +44,29 @@ export async function saveSettings(newSettings: Partial<UserSettings>) {
|
||||
}
|
||||
|
||||
export default function SettingsLayout() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const sidebarNavItems = [
|
||||
{
|
||||
title: t('settings.general.title'),
|
||||
href: '/settings/general',
|
||||
icon: SettingsIcon,
|
||||
},
|
||||
{
|
||||
title: t('settings.notifications.title'),
|
||||
href: '/settings/notifications',
|
||||
icon: BellIcon,
|
||||
},
|
||||
]
|
||||
|
||||
if (isAdmin()) {
|
||||
sidebarNavItems.push({
|
||||
title: 'YAML Config',
|
||||
href: '/settings/config',
|
||||
icon: FileSlidersIcon,
|
||||
})
|
||||
}
|
||||
|
||||
const page = useStore($router)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -77,8 +80,8 @@ export default function SettingsLayout() {
|
||||
return (
|
||||
<Card className="pt-5 px-4 pb-8 sm:pt-6 sm:px-7">
|
||||
<CardHeader className="p-0">
|
||||
<CardTitle className="mb-1">Settings</CardTitle>
|
||||
<CardDescription>Manage display and notification preferences.</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" />
|
||||
|
@@ -12,6 +12,7 @@ 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
|
||||
@@ -25,6 +26,8 @@ const NotificationSchema = v.object({
|
||||
})
|
||||
|
||||
const SettingsNotificationsPage = ({ userSettings }: { userSettings: UserSettings }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [webhooks, setWebhooks] = useState(userSettings.webhooks ?? [])
|
||||
const [emails, setEmails] = useState<string[]>(userSettings.emails ?? [])
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
@@ -69,13 +72,13 @@ const SettingsNotificationsPage = ({ userSettings }: { userSettings: UserSetting
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<h3 className="text-xl font-medium mb-2">Notifications</h3>
|
||||
<h3 className="text-xl font-medium mb-2">{t('settings.notifications.title')}</h3>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
Configure how you receive alert notifications.
|
||||
{t('settings.notifications.subtitle_1')}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground mt-1.5 leading-relaxed">
|
||||
Looking instead for where to create alerts? Click the bell{' '}
|
||||
<BellIcon className="inline h-4 w-4" /> icons in the systems table.
|
||||
{t('settings.notifications.subtitle_2')}{' '}
|
||||
<BellIcon className="inline h-4 w-4" /> {t('settings.notifications.subtitle_3')}
|
||||
</p>
|
||||
</div>
|
||||
<Separator className="my-4" />
|
||||
@@ -161,7 +164,7 @@ const SettingsNotificationsPage = ({ userSettings }: { userSettings: UserSetting
|
||||
) : (
|
||||
<SaveIcon className="h-4 w-4" />
|
||||
)}
|
||||
Save settings
|
||||
{t('settings.save_settings')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -21,6 +21,7 @@ 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'))
|
||||
@@ -374,9 +375,8 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
<ChartCard
|
||||
grid={grid}
|
||||
title="Total CPU Usage"
|
||||
description={`${
|
||||
cpuMaxStore[0] && isLongerChart ? 'Max 1 min ' : 'Average'
|
||||
} system-wide CPU utilization`}
|
||||
description={`${cpuMaxStore[0] && isLongerChart ? 'Max 1 min ' : 'Average'
|
||||
} system-wide CPU utilization`}
|
||||
cornerEl={isLongerChart ? <SelectAvgMax store={cpuMaxStore} /> : null}
|
||||
>
|
||||
<AreaChartDefault
|
||||
@@ -525,6 +525,8 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
}
|
||||
|
||||
function ContainerFilterBar() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const containerFilter = useStore($containerFilter)
|
||||
|
||||
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
@@ -534,7 +536,7 @@ function ContainerFilterBar() {
|
||||
return (
|
||||
<>
|
||||
<Input
|
||||
placeholder="Filter..."
|
||||
placeholder={t('filter')}
|
||||
className="pl-4 pr-8"
|
||||
value={containerFilter}
|
||||
onChange={handleChange}
|
||||
|
@@ -63,6 +63,7 @@ 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
|
||||
@@ -102,6 +103,8 @@ function sortableHeader(
|
||||
}
|
||||
|
||||
export default function SystemsTable({ filter }: { filter?: string }) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const data = useStore($systems)
|
||||
const hubVersion = useStore($hubVersion)
|
||||
const [sorting, setSorting] = useState<SortingState>([])
|
||||
@@ -145,32 +148,32 @@ export default function SystemsTable({ filter }: { filter?: string }) {
|
||||
</span>
|
||||
)
|
||||
},
|
||||
header: ({ column }) => sortableHeader(column, 'System', ServerIcon),
|
||||
header: ({ column }) => sortableHeader(column, t('systems_table.system'), ServerIcon),
|
||||
},
|
||||
{
|
||||
accessorKey: 'info.cpu',
|
||||
invertSorting: true,
|
||||
cell: CellFormatter,
|
||||
header: ({ column }) => sortableHeader(column, 'CPU', CpuIcon),
|
||||
header: ({ column }) => sortableHeader(column, t('systems_table.cpu'), CpuIcon),
|
||||
},
|
||||
{
|
||||
accessorKey: 'info.mp',
|
||||
invertSorting: true,
|
||||
cell: CellFormatter,
|
||||
header: ({ column }) => sortableHeader(column, 'Memory', MemoryStickIcon),
|
||||
header: ({ column }) => sortableHeader(column, t('systems_table.memory'), MemoryStickIcon),
|
||||
},
|
||||
{
|
||||
accessorKey: 'info.dp',
|
||||
invertSorting: true,
|
||||
cell: CellFormatter,
|
||||
header: ({ column }) => sortableHeader(column, 'Disk', HardDriveIcon),
|
||||
header: ({ column }) => sortableHeader(column, t('systems_table.disk'), HardDriveIcon),
|
||||
},
|
||||
{
|
||||
accessorFn: (originalRow) => originalRow.info.b || 0,
|
||||
id: 'n',
|
||||
invertSorting: true,
|
||||
size: 115,
|
||||
header: ({ column }) => sortableHeader(column, 'Net', EthernetIcon),
|
||||
header: ({ column }) => sortableHeader(column, t('systems_table.net'), EthernetIcon),
|
||||
cell: (info) => {
|
||||
const val = info.getValue() as number
|
||||
return (
|
||||
@@ -184,7 +187,7 @@ export default function SystemsTable({ filter }: { filter?: string }) {
|
||||
accessorKey: 'info.v',
|
||||
invertSorting: true,
|
||||
size: 50,
|
||||
header: ({ column }) => sortableHeader(column, 'Agent', WifiIcon, true),
|
||||
header: ({ column }) => sortableHeader(column, t('systems_table.agent'), WifiIcon, true),
|
||||
cell: (info) => {
|
||||
const version = info.getValue() as string
|
||||
if (!version || !hubVersion) {
|
||||
@@ -217,7 +220,7 @@ export default function SystemsTable({ filter }: { filter?: string }) {
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size={'icon'} data-nolink>
|
||||
<span className="sr-only">Open menu</span>
|
||||
<span className="sr-only">{t('systems_table.open_menu')}</span>
|
||||
<MoreHorizontalIcon className="w-5" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
@@ -233,44 +236,42 @@ export default function SystemsTable({ filter }: { filter?: string }) {
|
||||
{status === 'paused' ? (
|
||||
<>
|
||||
<PlayCircleIcon className="mr-2.5 h-4 w-4" />
|
||||
Resume
|
||||
{t('systems_table.resume')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<PauseCircleIcon className="mr-2.5 h-4 w-4" />
|
||||
Pause
|
||||
{t('systems_table.pause')}
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => copyToClipboard(host)}>
|
||||
<CopyIcon className="mr-2.5 h-4 w-4" />
|
||||
Copy host
|
||||
{t('systems_table.copy_host')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator className={cn(isReadOnlyUser() && 'hidden')} />
|
||||
<AlertDialogTrigger asChild>
|
||||
<DropdownMenuItem className={cn(isReadOnlyUser() && 'hidden')}>
|
||||
<Trash2Icon className="mr-2.5 h-4 w-4" />
|
||||
Delete
|
||||
{t('systems_table.delete')}
|
||||
</DropdownMenuItem>
|
||||
</AlertDialogTrigger>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you sure you want to delete {name}?</AlertDialogTitle>
|
||||
<AlertDialogTitle>{t('systems_table.delete_confirm', { name })}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete all current records
|
||||
for <code className="bg-muted rounded-sm px-1">{name}</code> from the
|
||||
database.
|
||||
{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>Cancel</AlertDialogCancel>
|
||||
<AlertDialogCancel>{t('cancel')}</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
className={cn(buttonVariants({ variant: 'destructive' }))}
|
||||
onClick={() => pb.collection('systems').delete(id)}
|
||||
>
|
||||
Continue
|
||||
{t('continue')}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
@@ -354,7 +355,7 @@ export default function SystemsTable({ filter }: { filter?: string }) {
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
No systems found
|
||||
{t('systems_table.no_systems_found')}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
|
22
beszel/site/src/lib/i18n.ts
Normal file
22
beszel/site/src/lib/i18n.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
|
||||
import en from '../locales/en/translation.json';
|
||||
import es from '../locales/es/translation.json';
|
||||
|
||||
i18n
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources: {
|
||||
en: { translation: en },
|
||||
es: { translation: es }
|
||||
},
|
||||
fallbackLng: 'en',
|
||||
interpolation: {
|
||||
escapeValue: false
|
||||
}
|
||||
});
|
||||
|
||||
export { i18n };
|
30
beszel/site/src/lib/languages.json
Normal file
30
beszel/site/src/lib/languages.json
Normal file
@@ -0,0 +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-Hans",
|
||||
"label": "简体中文"
|
||||
},
|
||||
{
|
||||
"lang": "zh-Hant",
|
||||
"label": "繁體中文"
|
||||
}
|
||||
]
|
94
beszel/site/src/locales/en/translation.json
Normal file
94
beszel/site/src/locales/en/translation.json
Normal file
@@ -0,0 +1,94 @@
|
||||
{
|
||||
"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. Press",
|
||||
"subtitle_2": "to open the command palette."
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"settings": {
|
||||
"settings": "Settings",
|
||||
"subtitle": "Manage display and notification preferences.",
|
||||
"save_settings": "Save Settings",
|
||||
"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."
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
94
beszel/site/src/locales/es/translation.json
Normal file
94
beszel/site/src/locales/es/translation.json
Normal file
@@ -0,0 +1,94 @@
|
||||
{
|
||||
"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": "configuración de notificaciones",
|
||||
"subtitle_2": "para configurar cómo recibe las alertas.",
|
||||
"overwrite_existing_alerts": "Sobrescribir alertas existentes"
|
||||
},
|
||||
"settings": {
|
||||
"settings": "Configuración",
|
||||
"subtitle": "Administrar las preferencias de visualización y notificación.",
|
||||
"save_settings": "Guardar configuración",
|
||||
"general": {
|
||||
"title": "General",
|
||||
"subtitle": "Cambiar las opciones generales de la aplicación.",
|
||||
"language": {
|
||||
"title": "Idioma",
|
||||
"subtitle_1": "¿Quieres ayudarnos a mejorar nuestras traducciones? Consulta",
|
||||
"subtitle_2": "para más detalles.",
|
||||
"preferred_language": "Idioma preferido"
|
||||
},
|
||||
"chart_options": {
|
||||
"title": "Opciones de gráfico",
|
||||
"subtitle": "Ajustar las opciones de visualización para los gráficos.",
|
||||
"default_time_period": "Periodo de tiempo predeterminado",
|
||||
"default_time_period_des": "Establece 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 dónde crear alertas? Haga clic en el icono de campana",
|
||||
"subtitle_3": "en la tabla de sistemas."
|
||||
},
|
||||
"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. Copie 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": "Haga clic para copiar",
|
||||
"command": "comando",
|
||||
"add_system": "Agregar sistema"
|
||||
}
|
||||
}
|
@@ -11,6 +11,7 @@ import {
|
||||
$hubVersion,
|
||||
$copyContent,
|
||||
} from './lib/stores.ts'
|
||||
import { LangToggle } from './components/lang-toggle.tsx'
|
||||
import { ModeToggle } from './components/mode-toggle.tsx'
|
||||
import {
|
||||
cn,
|
||||
@@ -48,6 +49,9 @@ import { $router, Link } from './components/router.tsx'
|
||||
import SystemDetail from './components/routes/system.tsx'
|
||||
import { AddSystemButton } from './components/add-system.tsx'
|
||||
|
||||
import './lib/i18n.ts'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
// const ServerDetail = lazy(() => import('./components/routes/system.tsx'))
|
||||
const CommandPalette = lazy(() => import('./components/command-palette.tsx'))
|
||||
const LoginPage = lazy(() => import('./components/login/login.tsx'))
|
||||
@@ -111,6 +115,8 @@ const App = () => {
|
||||
}
|
||||
|
||||
const Layout = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const authenticated = useStore($authenticated)
|
||||
const copyContent = useStore($copyContent)
|
||||
|
||||
@@ -131,6 +137,7 @@ const Layout = () => {
|
||||
</Link>
|
||||
|
||||
<div className={'flex ml-auto items-center'}>
|
||||
<LangToggle />
|
||||
<ModeToggle />
|
||||
<Link
|
||||
href="/settings/general"
|
||||
@@ -157,31 +164,31 @@ const Layout = () => {
|
||||
<DropdownMenuItem asChild>
|
||||
<a href="/_/" target="_blank">
|
||||
<UsersIcon className="mr-2.5 h-4 w-4" />
|
||||
<span>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>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>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>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>Auth providers</span>
|
||||
<span>{t('user_dm.auth_providers')}</span>
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
@@ -190,7 +197,7 @@ const Layout = () => {
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuItem onSelect={() => pb.authStore.clear()}>
|
||||
<LogOutIcon className="mr-2.5 h-4 w-4" />
|
||||
<span>Log out</span>
|
||||
<span>{t('user_dm.log_out')}</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
Reference in New Issue
Block a user