mirror of
https://github.com/henrygd/beszel.git
synced 2025-12-17 10:46:16 +01:00
ctrl k & i18n
This commit is contained in:
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user