ctrl k & i18n

This commit is contained in:
Arsfy
2024-10-28 13:37:21 +08:00
parent b7176fc8f3
commit 376e8d4621
17 changed files with 441 additions and 114 deletions

Binary file not shown.

View File

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

View File

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

View File

@@ -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) => (

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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 };

View 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": "繁體中文"
}
]

View 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"
}
}

View 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"
}
}

View File

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