migrate to lingui

This commit is contained in:
Henry Dollman
2024-11-01 20:31:57 -04:00
parent 856683610a
commit a93ff63605
76 changed files with 14410 additions and 3837 deletions

View File

@@ -9,17 +9,17 @@ import { AlertRecord, SystemRecord } from "@/types"
import { Input } from "../ui/input"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import { Link } from "../router"
import { useTranslation } from "react-i18next"
import { Plural, t, Trans } from "@lingui/macro"
import { useLingui } from "@lingui/react"
const SystemsTable = lazy(() => import("../systems-table/systems-table"))
export default function () {
const { t } = useTranslation()
export default function Home() {
const hubVersion = useStore($hubVersion)
const [filter, setFilter] = useState<string>()
const alerts = useStore($alerts)
const systems = useStore($systems)
const { _ } = useLingui()
// todo: maybe remove active alert if changed
const activeAlerts = useMemo(() => {
@@ -35,7 +35,7 @@ export default function () {
}, [alerts])
useEffect(() => {
document.title = "Dashboard / Beszel"
document.title = t`Dashboard` + " / Beszel"
// make sure we have the latest list of systems
updateSystemList()
@@ -61,7 +61,9 @@ export default function () {
<Card className="mb-4">
<CardHeader className="pb-4 px-2 sm:px-6 max-sm:pt-5 max-sm:pb-1">
<div className="px-2 sm:px-1">
<CardTitle>{t("home.active_alerts")}</CardTitle>
<CardTitle>
<Trans>Active Alerts</Trans>
</CardTitle>
</div>
</CardHeader>
<CardContent className="max-sm:p-2">
@@ -76,16 +78,13 @@ export default function () {
>
<info.icon className="h-4 w-4" />
<AlertTitle>
{alert.sysname} {t(info.name)}
{alert.sysname} {info.name().toLowerCase().replace("cpu", "CPU")}
</AlertTitle>
<AlertDescription>
{t("home.active_des", {
value: alert.value,
unit: info.unit,
})}
{t("minutes", {
count: alert.min,
})}
<Trans>
Exceeds {alert.value}
{info.unit} in last <Plural value={alert.min} one="# minute" other="# minutes" />
</Trans>
</AlertDescription>
<Link
href={`/system/${encodeURIComponent(alert.sysname!)}`}
@@ -104,11 +103,15 @@ 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-5 w-full items-end">
<div className="px-2 sm:px-1">
<CardTitle className="mb-2.5">{t("all_systems")}</CardTitle>
<CardDescription>{t("home.subtitle")}</CardDescription>
<CardTitle className="mb-2.5">
<Trans>All Systems</Trans>
</CardTitle>
<CardDescription>
<Trans>Updated in real time. Click on a system to view information.</Trans>
</CardDescription>
</div>
<Input
placeholder={t("filter")}
placeholder={_(t`Filter...`)}
onChange={(e) => setFilter(e.target.value)}
className="w-full md:w-56 lg:w-72 ms-auto px-4"
/>

View File

@@ -10,11 +10,9 @@ import { useState } from "react"
import { Textarea } from "@/components/ui/textarea"
import { toast } from "@/components/ui/use-toast"
import clsx from "clsx"
import { useTranslation } from "react-i18next"
import { Trans, t } from "@lingui/macro"
export default function ConfigYaml() {
const { t } = useTranslation()
const [configContent, setConfigContent] = useState<string>("")
const [isLoading, setIsLoading] = useState(false)
@@ -27,7 +25,7 @@ export default function ConfigYaml() {
setConfigContent(config)
} catch (error: any) {
toast({
title: "Error",
title: t`Error`,
description: error.message,
variant: "destructive",
})
@@ -43,23 +41,37 @@ export default function ConfigYaml() {
return (
<div>
<div>
<h3 className="text-xl font-medium mb-2">{t("settings.yaml_config.title")}</h3>
<p className="text-sm text-muted-foreground leading-relaxed">{t("settings.yaml_config.subtitle")}</p>
<h3 className="text-xl font-medium mb-2">
<Trans>YAML Configuration</Trans>
</h3>
<p className="text-sm text-muted-foreground leading-relaxed">
<Trans>Export your current systems configuration.</Trans>
</p>
</div>
<Separator className="my-4" />
<div className="space-y-2">
<div className="mb-4">
<p className="text-sm text-muted-foreground leading-relaxed my-1">
{t("settings.yaml_config.des_1")} <code className="bg-muted rounded-sm px-1 text-primary">config.yml</code>{" "}
{t("settings.yaml_config.des_2")}
<Trans>
Systems may be managed in a <code className="bg-muted rounded-sm px-1 text-primary">config.yml</code> file
inside your data directory.
</Trans>
</p>
<p className="text-sm text-muted-foreground leading-relaxed">
<Trans>
On each restart, systems in the database will be updated to match the systems defined in the file.
</Trans>
</p>
<p className="text-sm text-muted-foreground leading-relaxed">{t("settings.yaml_config.des_3")}</p>
<Alert className="my-4 border-destructive text-destructive w-auto table md:pe-6">
<AlertCircleIcon className="h-4 w-4 stroke-destructive" />
<AlertTitle>{t("settings.yaml_config.alert.title")}</AlertTitle>
<AlertTitle>
<Trans>Caution - potential data loss</Trans>
</AlertTitle>
<AlertDescription>
<p>
{t("settings.yaml_config.alert.des_1")} <code>config.yml</code> {t("settings.yaml_config.alert.des_2")}
<Trans>
Existing systems not defined in <code>config.yml</code> will be deleted. Please make regular backups.
</Trans>
</p>
</AlertDescription>
</Alert>
@@ -78,7 +90,7 @@ export default function ConfigYaml() {
<Separator className="my-5" />
<Button type="button" className="mt-2 flex items-center gap-1" onClick={fetchConfig} disabled={isLoading}>
<ButtonIcon className={clsx("h-4 w-4 me-0.5", isLoading && "animate-spin")} />
{t("settings.export_configuration")}
<Trans>Export configuration</Trans>
</Button>
</div>
)

View File

@@ -6,20 +6,16 @@ import { Separator } from "@/components/ui/separator"
import { LanguagesIcon, LoaderCircleIcon, SaveIcon } from "lucide-react"
import { UserSettings } from "@/types"
import { saveSettings } from "./layout"
import { useState, useEffect } from "react"
// import { Input } from '@/components/ui/input'
import { useTranslation } from "react-i18next"
import { useState } from "react"
import { Trans } from "@lingui/macro"
import languages from "../../../lib/languages.json"
import { setLang } from "@/lib/i18n"
import { dynamicActivate } from "@/lib/i18n"
import { useLingui } from "@lingui/react"
// import { setLang } from "@/lib/i18n"
export default function SettingsProfilePage({ userSettings }: { userSettings: UserSettings }) {
const { t, i18n } = useTranslation()
useEffect(() => {
document.documentElement.lang = i18n.language
}, [i18n.language])
const [isLoading, setIsLoading] = useState(false)
const { i18n } = useLingui()
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
@@ -33,8 +29,12 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
return (
<div>
<div>
<h3 className="text-xl font-medium mb-2">{t("settings.general.title")}</h3>
<p className="text-sm text-muted-foreground leading-relaxed">{t("settings.general.subtitle")}</p>
<h3 className="text-xl font-medium mb-2">
<Trans>General</Trans>
</h3>
<p className="text-sm text-muted-foreground leading-relaxed">
<Trans>Change general application options.</Trans>
</p>
</div>
<Separator className="my-4" />
<form onSubmit={handleSubmit} className="space-y-5">
@@ -42,25 +42,22 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
<div className="mb-4">
<h3 className="mb-1 text-lg font-medium flex items-center gap-2">
<LanguagesIcon className="h-4 w-4" />
{t("settings.general.language.title")}
<Trans>Language</Trans>
</h3>
<p className="text-sm text-muted-foreground leading-relaxed">
{t("settings.general.language.subtitle_1")}{" "}
<a
href="https://hosted.weblate.org/engage/beszel/"
className="link"
target="_blank"
rel="noopener noreferrer"
>
Weblate
</a>{" "}
{t("settings.general.language.subtitle_2")}
<Trans>
Want to help us make our translations even better? Check out{" "}
<a href="https://crowdin.com/project/beszel" className="link" target="_blank" rel="noopener noreferrer">
Crowdin
</a>{" "}
for more details.
</Trans>
</p>
</div>
<Label className="block" htmlFor="lang">
{t("settings.general.language.preferred_language")}
<Trans>Preferred Language</Trans>
</Label>
<Select value={i18n.language} onValueChange={(lang: string) => setLang(lang)}>
<Select value={i18n.locale} onValueChange={(lang: string) => dynamicActivate(lang)}>
<SelectTrigger id="lang">
<SelectValue />
</SelectTrigger>
@@ -77,13 +74,15 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
<Separator />
<div className="space-y-2">
<div className="mb-4">
<h3 className="mb-1 text-lg font-medium">{t("settings.general.chart_options.title")}</h3>
<h3 className="mb-1 text-lg font-medium">
<Trans>Chart options</Trans>
</h3>
<p className="text-sm text-muted-foreground leading-relaxed">
{t("settings.general.chart_options.subtitle")}
<Trans>Adjust display options for charts.</Trans>
</p>
</div>
<Label className="block" htmlFor="chartTime">
{t("settings.general.chart_options.default_time_period")}
<Trans>Default time period</Trans>
</Label>
<Select name="chartTime" key={userSettings.chartTime} defaultValue={userSettings.chartTime}>
<SelectTrigger id="chartTime">
@@ -98,13 +97,13 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
</SelectContent>
</Select>
<p className="text-[0.8rem] text-muted-foreground">
{t("settings.general.chart_options.default_time_period_des")}
<Trans>Sets the default time range for charts when a system is viewed.</Trans>
</p>
</div>
<Separator />
<Button type="submit" className="flex items-center gap-1.5 disabled:opacity-100" disabled={isLoading}>
{isLoading ? <LoaderCircleIcon className="h-4 w-4 animate-spin" /> : <SaveIcon className="h-4 w-4" />}
{t("settings.save_settings")}
<Trans>Save Settings</Trans>
</Button>
</form>
</div>

View File

@@ -12,9 +12,8 @@ import { UserSettings } from "@/types.js"
import General from "./general.tsx"
import Notifications from "./notifications.tsx"
import ConfigYaml from "./config-yaml.tsx"
import { isAdmin } from "@/lib/utils.ts"
import { useTranslation } from "react-i18next"
import { t } from "i18next"
import { Trans, t } from "@lingui/macro"
import { useLingui } from "@lingui/react"
export async function saveSettings(newSettings: Partial<UserSettings>) {
try {
@@ -31,47 +30,45 @@ export async function saveSettings(newSettings: Partial<UserSettings>) {
})
$userSettings.set(updatedSettings.settings)
toast({
title: t("settings.saved"),
description: t("settings.saved_des"),
title: t`Settings saved`,
description: t`Your user settings have been updated.`,
})
} catch (e) {
// console.error('update settings', e)
toast({
title: t("settings.failed_to_save"),
description: t("settings.check_logs"),
title: t`Failed to save settings`,
description: t`Check logs for more details.`,
variant: "destructive",
})
}
}
export default function SettingsLayout() {
const { t } = useTranslation()
const { _ } = useLingui()
const sidebarNavItems = [
{
title: t("settings.general.title"),
title: _(t({ message: `General`, comment: "Context: General settings" })),
href: "/settings/general",
icon: SettingsIcon,
},
{
title: t("settings.notifications.title"),
title: t`Notifications`,
href: "/settings/notifications",
icon: BellIcon,
},
]
if (isAdmin()) {
sidebarNavItems.push({
title: t("settings.yaml_config.short_title"),
{
title: t`YAML Config`,
href: "/settings/config",
icon: FileSlidersIcon,
})
}
admin: true,
},
]
const page = useStore($router)
useEffect(() => {
document.title = "Settings / Beszel"
document.title = t`Settings` + " / Beszel"
// redirect to account page if no page is specified
if (page?.path === "/settings") {
redirectPage($router, "settings", { name: "general" })
@@ -81,8 +78,12 @@ 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">{t("settings.settings")}</CardTitle>
<CardDescription>{t("settings.subtitle")}</CardDescription>
<CardTitle className="mb-1">
<Trans>Settings</Trans>
</CardTitle>
<CardDescription>
<Trans>Manage display and notification preferences.</Trans>
</CardDescription>
</CardHeader>
<CardContent className="p-0">
<Separator className="hidden md:block my-5" />

View File

@@ -12,8 +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"
import { t } from "i18next"
import { Trans, t } from "@lingui/macro"
interface ShoutrrrUrlCardProps {
url: string
@@ -27,8 +26,6 @@ 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)
@@ -62,7 +59,7 @@ const SettingsNotificationsPage = ({ userSettings }: { userSettings: UserSetting
await saveSettings(parsedData)
} catch (e: any) {
toast({
title: t("settings.failed_to_save"),
title: t`Failed to save settings`,
description: e.message,
variant: "destructive",
})
@@ -73,51 +70,67 @@ const SettingsNotificationsPage = ({ userSettings }: { userSettings: UserSetting
return (
<div>
<div>
<h3 className="text-xl font-medium mb-2">{t("settings.notifications.title")}</h3>
<p className="text-sm text-muted-foreground leading-relaxed">{t("settings.notifications.subtitle_1")}</p>
<h3 className="text-xl font-medium mb-2">
<Trans>Notifications</Trans>
</h3>
<p className="text-sm text-muted-foreground leading-relaxed">
<Trans>Configure how you receive alert notifications.</Trans>
</p>
<p className="text-sm text-muted-foreground mt-1.5 leading-relaxed">
{t("settings.notifications.subtitle_2")} <BellIcon className="inline h-4 w-4" />{" "}
{t("settings.notifications.subtitle_3")}
<Trans>
Looking instead for where to create alerts? Click the bell <BellIcon className="inline h-4 w-4" /> icons in
the systems table.
</Trans>
</p>
</div>
<Separator className="my-4" />
<div className="space-y-5">
<div className="space-y-2">
<div className="mb-4">
<h3 className="mb-1 text-lg font-medium">{t("settings.notifications.email.title")}</h3>
<h3 className="mb-1 text-lg font-medium">
<Trans>Email notifications</Trans>
</h3>
{isAdmin() && (
<p className="text-sm text-muted-foreground leading-relaxed">
{t("settings.notifications.email.please")}{" "}
<a href="/_/#/settings/mail" className="link" target="_blank">
{t("settings.notifications.email.configure_an_SMTP_server")}
</a>{" "}
{t("settings.notifications.email.to_ensure_alerts_are_delivered")}{" "}
<Trans>
Please{" "}
<a href="/_/#/settings/mail" className="link" target="_blank">
configure an SMTP server
</a>{" "}
to ensure alerts are delivered.
</Trans>
</p>
)}
</div>
<Label className="block" htmlFor="email">
{t("settings.notifications.email.to_emails")}
<Trans>To email(s)</Trans>
</Label>
<InputTags
value={emails}
onChange={setEmails}
placeholder={t("settings.notifications.email.enter_email_address")}
placeholder={t`Enter email address...`}
className="w-full"
type="email"
id="email"
/>
<p className="text-[0.8rem] text-muted-foreground">{t("settings.notifications.email.des")}</p>
<p className="text-[0.8rem] text-muted-foreground">
<Trans>Save address using enter key or comma. Leave blank to disable email notifications.</Trans>
</p>
</div>
<Separator />
<div className="space-y-3">
<div>
<h3 className="mb-1 text-lg font-medium">{t("settings.notifications.webhook.title")}</h3>
<h3 className="mb-1 text-lg font-medium">
<Trans>Webhook / Push notifications</Trans>
</h3>
<p className="text-sm text-muted-foreground leading-relaxed">
{t("settings.notifications.webhook.des_1")}{" "}
<a href="https://containrrr.dev/shoutrrr/services/overview/" target="_blank" className="link">
Shoutrrr
</a>{" "}
{t("settings.notifications.webhook.des_2")}
<Trans>
Beszel uses{" "}
<a href="https://containrrr.dev/shoutrrr/services/overview/" target="_blank" className="link">
Shoutrrr
</a>{" "}
to integrate with popular notification services.
</Trans>
</p>
</div>
{webhooks.length > 0 && (
@@ -140,7 +153,7 @@ const SettingsNotificationsPage = ({ userSettings }: { userSettings: UserSetting
onClick={addWebhook}
>
<PlusIcon className="h-4 w-4 -ms-0.5" />
{t("settings.notifications.webhook.add")} URL
<Trans>Add URL</Trans>
</Button>
</div>
<Separator />
@@ -151,7 +164,7 @@ const SettingsNotificationsPage = ({ userSettings }: { userSettings: UserSetting
disabled={isLoading}
>
{isLoading ? <LoaderCircleIcon className="h-4 w-4 animate-spin" /> : <SaveIcon className="h-4 w-4" />}
{t("settings.save_settings")}
<Trans>Save Settings</Trans>
</Button>
</div>
</div>
@@ -166,13 +179,13 @@ const ShoutrrrUrlCard = ({ url, onUrlChange, onRemove }: ShoutrrrUrlCardProps) =
const res = await pb.send("/api/beszel/send-test-notification", { url })
if ("err" in res && !res.err) {
toast({
title: t("settings.notifications.webhook.test_sent"),
description: t("settings.notifications.webhook.test_sent_des"),
title: t`Test notification sent`,
description: t`Check your notification service`,
})
} else {
toast({
title: t("error"),
description: res.err ?? "Failed to send test notification",
title: t`Error`,
description: res.err ?? t`Failed to send test notification`,
variant: "destructive",
})
}
@@ -195,7 +208,9 @@ const ShoutrrrUrlCard = ({ url, onUrlChange, onRemove }: ShoutrrrUrlCardProps) =
<LoaderCircleIcon className="h-4 w-4 animate-spin" />
) : (
<span>
{t("settings.notifications.webhook.test")} <span className="hidden sm:inline">URL</span>
<Trans>
Test <span className="hidden sm:inline">URL</span>
</Trans>
</span>
)}
</Button>

View File

@@ -1,5 +1,5 @@
import React from "react"
import { cn } from "@/lib/utils"
import { cn, isAdmin } from "@/lib/utils"
import { buttonVariants } from "../../ui/button"
import { $router, Link, navigate } from "../../router"
import { useStore } from "@nanostores/react"
@@ -11,6 +11,7 @@ interface SidebarNavProps extends React.HTMLAttributes<HTMLElement> {
href: string
title: string
icon?: React.FC<React.SVGProps<SVGSVGElement>>
admin?: boolean
}[]
}
@@ -23,17 +24,20 @@ export function SidebarNav({ className, items, ...props }: SidebarNavProps) {
<div className="md:hidden">
<Select onValueChange={(value: string) => navigate(value)} value={page?.path}>
<SelectTrigger className="w-full my-3.5">
<SelectValue placeholder="Select a page" />
<SelectValue placeholder="Select page" />
</SelectTrigger>
<SelectContent>
{items.map((item) => (
<SelectItem key={item.href} value={item.href}>
<span className="flex items-center gap-2">
{item.icon && <item.icon className="h-4 w-4" />}
{item.title}
</span>
</SelectItem>
))}
{items.map((item) => {
if (item.admin && !isAdmin()) return null
return (
<SelectItem key={item.href} value={item.href}>
<span className="flex items-center gap-2">
{item.icon && <item.icon className="h-4 w-4" />}
{item.title}
</span>
</SelectItem>
)
})}
</SelectContent>
</Select>
<Separator />

View File

@@ -1,4 +1,4 @@
import { $systems, pb, $chartTime, $containerFilter, $userSettings } from "@/lib/stores"
import { $systems, pb, $chartTime, $containerFilter, $userSettings, $direction } from "@/lib/stores"
import { ChartData, ChartTimes, ContainerStatsRecord, SystemRecord, SystemStatsRecord } from "@/types"
import React, { lazy, useCallback, useEffect, useMemo, useRef, useState } from "react"
import { Card, CardHeader, CardTitle, CardDescription } from "../ui/card"
@@ -15,7 +15,8 @@ import { ChartAverage, ChartMax, Rows, TuxIcon } from "../ui/icons"
import { useIntersectionObserver } from "@/lib/use-intersection-observer"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"
import { timeTicks } from "d3-time"
import { useTranslation } from "react-i18next"
import { Plural, Trans, t } from "@lingui/macro"
import { useLingui } from "@lingui/react"
const AreaChartDefault = lazy(() => import("../charts/area-chart"))
const ContainerChart = lazy(() => import("../charts/container-chart"))
@@ -85,7 +86,8 @@ async function getStats<T>(collection: string, system: SystemRecord, chartTime:
}
export default function SystemDetail({ name }: { name: string }) {
const { t, i18n } = useTranslation()
const direction = useStore($direction)
const { _ } = useLingui()
const systems = useStore($systems)
const chartTime = useStore($chartTime)
/** Max CPU toggle value */
@@ -157,10 +159,10 @@ export default function SystemDetail({ name }: { name: string }) {
systemStats,
containerData,
chartTime,
orientation: i18n.dir() == "rtl" ? "right" : "left",
orientation: direction === "rtl" ? "right" : "left",
...getTimeData(chartTime, lastCreated),
}
}, [systemStats, containerData, i18n.dir()])
}, [systemStats, containerData, direction])
// get stats
useEffect(() => {
@@ -232,12 +234,12 @@ export default function SystemDetail({ name }: { name: string }) {
if (!system.info) {
return []
}
let uptime: number | string = system.info.u
let uptime: React.ReactNode
if (system.info.u < 172800) {
const hours = Math.trunc(uptime / 3600)
uptime = t("hours", { count: hours })
const hours = Math.trunc(system.info.u / 3600)
uptime = <Plural value={hours} one="# hour" other="# hours" />
} else {
uptime = t("days", { count: Math.trunc(system.info?.u / 86400) })
uptime = <Plural value={Math.trunc(system.info?.u / 86400)} one="# day" other="# days" />
}
return [
{ value: system.host, Icon: GlobeIcon },
@@ -248,8 +250,8 @@ export default function SystemDetail({ name }: { name: string }) {
// hide if hostname is same as host or name
hide: system.info.h === system.host || system.info.h === system.name,
},
{ value: uptime, Icon: ClockArrowUp, label: "Uptime" },
{ value: system.info.k, Icon: TuxIcon, label: "Kernel" },
{ value: uptime, Icon: ClockArrowUp, label: t`Uptime` },
{ value: system.info.k, Icon: TuxIcon, label: t({ comment: "Linux kernel", message: "Kernel" }) },
{
value: `${system.info.m} (${system.info.c}c${system.info.t ? `/${system.info.t}t` : ""})`,
Icon: CpuIcon,
@@ -261,7 +263,7 @@ export default function SystemDetail({ name }: { name: string }) {
Icon: any
hide?: boolean
}[]
}, [system.info, t])
}, [system.info])
/** Space for tooltip if more than 12 containers */
useEffect(() => {
@@ -345,7 +347,7 @@ export default function SystemDetail({ name }: { name: string }) {
<Tooltip>
<TooltipTrigger asChild>
<Button
aria-label={t("monitor.toggle_grid")}
aria-label={t`Toggle grid`}
variant="outline"
size="icon"
className="hidden lg:flex p-0 text-primary"
@@ -358,7 +360,7 @@ export default function SystemDetail({ name }: { name: string }) {
)}
</Button>
</TooltipTrigger>
<TooltipContent>{t("monitor.toggle_grid")}</TooltipContent>
<TooltipContent>{t`Toggle grid`}</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
@@ -370,26 +372,19 @@ export default function SystemDetail({ name }: { name: string }) {
<ChartCard
empty={dataEmpty}
grid={grid}
title={t("monitor.total_cpu_usage")}
description={`${cpuMaxStore[0] && isLongerChart ? t("monitor.max_1_min") : t("monitor.average")} ${t(
"monitor.cpu_des"
)}`}
title={_(t`CPU Usage`)}
description={t`Average system-wide CPU utilization`}
cornerEl={isLongerChart ? <SelectAvgMax store={cpuMaxStore} /> : null}
>
<AreaChartDefault
chartData={chartData}
chartName={t("alerts.info.cpu_usage")}
maxToggled={cpuMaxStore[0]}
unit="%"
/>
<AreaChartDefault chartData={chartData} chartName="CPU Usage" maxToggled={cpuMaxStore[0]} unit="%" />
</ChartCard>
{containerFilterBar && (
<ChartCard
empty={dataEmpty}
grid={grid}
title={t("monitor.docker_cpu_usage")}
description={t("monitor.docker_cpu_des")}
title={t`Docker CPU Usage`}
description={t`Average CPU utilization of containers`}
cornerEl={containerFilterBar}
>
<ContainerChart chartData={chartData} dataKey="c" chartName="cpu" />
@@ -399,8 +394,8 @@ export default function SystemDetail({ name }: { name: string }) {
<ChartCard
empty={dataEmpty}
grid={grid}
title={t("monitor.total_memory_usage")}
description={t("monitor.memory_des")}
title={t`Memory Usage`}
description={t`Triggers when memory usage exceeds a threshold.`}
>
<MemChart chartData={chartData} />
</ChartCard>
@@ -409,15 +404,15 @@ export default function SystemDetail({ name }: { name: string }) {
<ChartCard
empty={dataEmpty}
grid={grid}
title={t("monitor.docker_memory_usage")}
description={t("monitor.docker_memory_des")}
title={t`Docker Memory Usage`}
description={t`Memory usage of docker containers`}
cornerEl={containerFilterBar}
>
<ContainerChart chartData={chartData} chartName="mem" dataKey="m" unit=" MB" />
</ChartCard>
)}
<ChartCard empty={dataEmpty} grid={grid} title={t("monitor.disk_space")} description={t("monitor.disk_des")}>
<ChartCard empty={dataEmpty} grid={grid} title={t`Disk Usage`} description={t`Usage of root partition`}>
<DiskChart
chartData={chartData}
dataKey="stats.du"
@@ -428,8 +423,8 @@ export default function SystemDetail({ name }: { name: string }) {
<ChartCard
empty={dataEmpty}
grid={grid}
title={t("monitor.disk_io")}
description={t("monitor.disk_io_des")}
title={t`Disk I/O`}
description={t`Throughput of root filesystem`}
cornerEl={isLongerChart ? <SelectAvgMax store={diskIoMaxStore} /> : null}
>
<AreaChartDefault chartData={chartData} maxToggled={diskIoMaxStore[0]} chartName="dio" />
@@ -438,9 +433,9 @@ export default function SystemDetail({ name }: { name: string }) {
<ChartCard
empty={dataEmpty}
grid={grid}
title={t("monitor.bandwidth")}
title={t`Bandwidth`}
cornerEl={isLongerChart ? <SelectAvgMax store={bandwidthMaxStore} /> : null}
description={t("monitor.bandwidth_des")}
description={t`Network traffic of public interfaces`}
>
<AreaChartDefault chartData={chartData} maxToggled={bandwidthMaxStore[0]} chartName="bw" />
</ChartCard>
@@ -454,8 +449,8 @@ export default function SystemDetail({ name }: { name: string }) {
>
<ChartCard
empty={dataEmpty}
title={t("monitor.docker_network_io")}
description={t("monitor.docker_network_io_des")}
title={t`Docker Network I/O`}
description={t`Network traffic of docker containers`}
cornerEl={containerFilterBar}
>
{/* @ts-ignore */}
@@ -468,8 +463,8 @@ export default function SystemDetail({ name }: { name: string }) {
<ChartCard
empty={dataEmpty}
grid={grid}
title={t("monitor.swap_usage")}
description={t("monitor.swap_des")}
title={t`Swap Usage`}
description={t`Swap space used by the system`}
>
<SwapChart chartData={chartData} />
</ChartCard>
@@ -479,8 +474,8 @@ export default function SystemDetail({ name }: { name: string }) {
<ChartCard
empty={dataEmpty}
grid={grid}
title={t("monitor.temperature")}
description={t("monitor.temperature_des")}
title={t`Temperature`}
description={t`Temperatures of system sensors`}
>
<TemperatureChart chartData={chartData} />
</ChartCard>
@@ -496,8 +491,8 @@ export default function SystemDetail({ name }: { name: string }) {
<ChartCard
empty={dataEmpty}
grid={grid}
title={`${extraFsName} ${t("monitor.usage")}`}
description={`${t("monitor.disk_usage_of")} ${extraFsName}`}
title={`${extraFsName} ${t`Usage`}`}
description={t`Disk usage of ${extraFsName}`}
>
<DiskChart
chartData={chartData}
@@ -509,7 +504,7 @@ export default function SystemDetail({ name }: { name: string }) {
empty={dataEmpty}
grid={grid}
title={`${extraFsName} I/O`}
description={`${t("monitor.throughput_of")} ${extraFsName}`}
description={t`Throughput of ${extraFsName}`}
cornerEl={isLongerChart ? <SelectAvgMax store={diskIoMaxStore} /> : null}
>
<AreaChartDefault
@@ -532,8 +527,6 @@ export default function SystemDetail({ name }: { name: string }) {
}
function ContainerFilterBar() {
const { t } = useTranslation()
const containerFilter = useStore($containerFilter)
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
@@ -542,7 +535,7 @@ function ContainerFilterBar() {
return (
<>
<Input placeholder={t("filter")} className="ps-4 pe-8" value={containerFilter} onChange={handleChange} />
<Input placeholder={t`Filter...`} className="ps-4 pe-8" value={containerFilter} onChange={handleChange} />
{containerFilter && (
<Button
type="button"
@@ -560,8 +553,6 @@ function ContainerFilterBar() {
}
function SelectAvgMax({ store }: { store: [boolean, React.Dispatch<React.SetStateAction<boolean>>] }) {
const { t } = useTranslation()
const [max, setMax] = store
const Icon = max ? ChartMax : ChartAverage
@@ -573,10 +564,10 @@ function SelectAvgMax({ store }: { store: [boolean, React.Dispatch<React.SetStat
</SelectTrigger>
<SelectContent>
<SelectItem key="avg" value="avg">
{t("monitor.average")}
<Trans>Average</Trans>
</SelectItem>
<SelectItem key="max" value="max">
{t("monitor.max_1_min")}
<Trans comment="Chart select field. Please try to keep this short.">Max 1 min</Trans>
</SelectItem>
</SelectContent>
</Select>
@@ -599,7 +590,6 @@ function ChartCard({
cornerEl?: JSX.Element | null
}) {
const { isIntersecting, ref } = useIntersectionObserver()
const { t } = useTranslation()
return (
<Card className={cn("pb-2 sm:pb-4 odd:last-of-type:col-span-full", { "col-span-full": !grid })} ref={ref}>
@@ -609,7 +599,7 @@ function ChartCard({
{cornerEl && <div className="relative py-1 block sm:w-44 sm:absolute sm:top-2.5 sm:end-3.5">{cornerEl}</div>}
</CardHeader>
<div className="ps-0 w-[calc(100%-1.6em)] h-52 relative">
{<Spinner msg={empty ? t("monitor.waiting_for") : undefined} />}
{<Spinner msg={empty ? t`Waiting for enough records to display` : undefined} />}
{isIntersecting && children}
</div>
</Card>