mirror of
https://github.com/henrygd/beszel.git
synced 2025-12-18 19:26:16 +01:00
add prettier config and format files site files
This commit is contained in:
@@ -1,17 +1,17 @@
|
||||
import { Suspense, lazy, useEffect, useMemo, useState } from 'react'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card'
|
||||
import { $alerts, $hubVersion, $systems, pb } from '@/lib/stores'
|
||||
import { useStore } from '@nanostores/react'
|
||||
import { GithubIcon } from 'lucide-react'
|
||||
import { Separator } from '../ui/separator'
|
||||
import { alertInfo, updateRecordList, updateSystemList } from '@/lib/utils'
|
||||
import { AlertRecord, SystemRecord } from '@/types'
|
||||
import { Input } from '../ui/input'
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||
import { Link } from '../router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Suspense, lazy, useEffect, useMemo, useState } from "react"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"
|
||||
import { $alerts, $hubVersion, $systems, pb } from "@/lib/stores"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { GithubIcon } from "lucide-react"
|
||||
import { Separator } from "../ui/separator"
|
||||
import { alertInfo, updateRecordList, updateSystemList } from "@/lib/utils"
|
||||
import { AlertRecord, SystemRecord } from "@/types"
|
||||
import { Input } from "../ui/input"
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
|
||||
import { Link } from "../router"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
const SystemsTable = lazy(() => import('../systems-table/systems-table'))
|
||||
const SystemsTable = lazy(() => import("../systems-table/systems-table"))
|
||||
|
||||
export default function () {
|
||||
const { t } = useTranslation()
|
||||
@@ -35,21 +35,21 @@ export default function () {
|
||||
}, [alerts])
|
||||
|
||||
useEffect(() => {
|
||||
document.title = 'Dashboard / Beszel'
|
||||
document.title = "Dashboard / Beszel"
|
||||
|
||||
// make sure we have the latest list of systems
|
||||
updateSystemList()
|
||||
|
||||
// subscribe to real time updates for systems / alerts
|
||||
pb.collection<SystemRecord>('systems').subscribe('*', (e) => {
|
||||
pb.collection<SystemRecord>("systems").subscribe("*", (e) => {
|
||||
updateRecordList(e, $systems)
|
||||
})
|
||||
// todo: add toast if new triggered alert comes in
|
||||
pb.collection<AlertRecord>('alerts').subscribe('*', (e) => {
|
||||
pb.collection<AlertRecord>("alerts").subscribe("*", (e) => {
|
||||
updateRecordList(e, $alerts)
|
||||
})
|
||||
return () => {
|
||||
pb.collection('systems').unsubscribe('*')
|
||||
pb.collection("systems").unsubscribe("*")
|
||||
// pb.collection('alerts').unsubscribe('*')
|
||||
}
|
||||
}, [])
|
||||
@@ -61,7 +61,7 @@ export default function () {
|
||||
<Card className="mb-4">
|
||||
<CardHeader className="pb-4 px-2 sm:px-6 max-sm:pt-5 max-sm:pb-1">
|
||||
<div className="px-2 sm:px-1">
|
||||
<CardTitle>{t('home.active_alerts')}</CardTitle>
|
||||
<CardTitle>{t("home.active_alerts")}</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="max-sm:p-2">
|
||||
@@ -79,7 +79,7 @@ export default function () {
|
||||
{alert.sysname} {t(info.name)}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t('home.active_des', {
|
||||
{t("home.active_des", {
|
||||
value: alert.value,
|
||||
unit: info.unit,
|
||||
minutes: alert.min,
|
||||
@@ -102,11 +102,11 @@ export default function () {
|
||||
<CardHeader className="pb-5 px-2 sm:px-6 max-sm:pt-5 max-sm:pb-1">
|
||||
<div className="grid md:flex gap-3 w-full items-end">
|
||||
<div className="px-2 sm:px-1">
|
||||
<CardTitle className="mb-2.5">{t('all_systems')}</CardTitle>
|
||||
<CardDescription>{t('home.subtitle_1')}</CardDescription>
|
||||
<CardTitle className="mb-2.5">{t("all_systems")}</CardTitle>
|
||||
<CardDescription>{t("home.subtitle_1")}</CardDescription>
|
||||
</div>
|
||||
<Input
|
||||
placeholder={t('filter')}
|
||||
placeholder={t("filter")}
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
className="w-full md:w-56 lg:w-72 ml-auto px-4"
|
||||
/>
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import { isAdmin } from '@/lib/utils'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { redirectPage } from '@nanostores/router'
|
||||
import { $router } from '@/components/router'
|
||||
import { AlertCircleIcon, FileSlidersIcon, LoaderCircleIcon } from 'lucide-react'
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||
import { pb } from '@/lib/stores'
|
||||
import { useState } from 'react'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { toast } from '@/components/ui/use-toast'
|
||||
import clsx from 'clsx'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { isAdmin } from "@/lib/utils"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { redirectPage } from "@nanostores/router"
|
||||
import { $router } from "@/components/router"
|
||||
import { AlertCircleIcon, FileSlidersIcon, LoaderCircleIcon } from "lucide-react"
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
|
||||
import { pb } from "@/lib/stores"
|
||||
import { useState } from "react"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { toast } from "@/components/ui/use-toast"
|
||||
import clsx from "clsx"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
export default function ConfigYaml() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [configContent, setConfigContent] = useState<string>('')
|
||||
const [configContent, setConfigContent] = useState<string>("")
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const ButtonIcon = isLoading ? LoaderCircleIcon : FileSlidersIcon
|
||||
@@ -23,13 +23,13 @@ export default function ConfigYaml() {
|
||||
async function fetchConfig() {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
const { config } = await pb.send<{ config: string }>('/api/beszel/config-yaml', {})
|
||||
const { config } = await pb.send<{ config: string }>("/api/beszel/config-yaml", {})
|
||||
setConfigContent(config)
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
title: "Error",
|
||||
description: error.message,
|
||||
variant: 'destructive',
|
||||
variant: "destructive",
|
||||
})
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
@@ -37,33 +37,29 @@ export default function ConfigYaml() {
|
||||
}
|
||||
|
||||
if (!isAdmin()) {
|
||||
redirectPage($router, 'settings', { name: 'general' })
|
||||
redirectPage($router, "settings", { name: "general" })
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<h3 className="text-xl font-medium mb-2">{t('settings.yaml_config.title')}</h3>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
{t('settings.yaml_config.subtitle')}
|
||||
</p>
|
||||
<h3 className="text-xl font-medium mb-2">{t("settings.yaml_config.title")}</h3>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">{t("settings.yaml_config.subtitle")}</p>
|
||||
</div>
|
||||
<Separator className="my-4" />
|
||||
<div className="space-y-2">
|
||||
<div className="mb-4">
|
||||
<p className="text-sm text-muted-foreground leading-relaxed my-1">
|
||||
{t('settings.yaml_config.des_1')}{' '}
|
||||
<code className="bg-muted rounded-sm px-1 text-primary">config.yml</code> {t('settings.yaml_config.des_2')}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
{t('settings.yaml_config.des_3')}
|
||||
{t("settings.yaml_config.des_1")} <code className="bg-muted rounded-sm px-1 text-primary">config.yml</code>{" "}
|
||||
{t("settings.yaml_config.des_2")}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">{t("settings.yaml_config.des_3")}</p>
|
||||
<Alert className="my-4 border-destructive text-destructive w-auto table md:pr-6">
|
||||
<AlertCircleIcon className="h-4 w-4 stroke-destructive" />
|
||||
<AlertTitle>{t('settings.yaml_config.alert.title')}</AlertTitle>
|
||||
<AlertTitle>{t("settings.yaml_config.alert.title")}</AlertTitle>
|
||||
<AlertDescription>
|
||||
<p>
|
||||
{t('settings.yaml_config.alert.des_1')} <code>config.yml</code> {t('settings.yaml_config.alert.des_2')}
|
||||
{t("settings.yaml_config.alert.des_1")} <code>config.yml</code> {t("settings.yaml_config.alert.des_2")}
|
||||
</p>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
@@ -73,20 +69,15 @@ export default function ConfigYaml() {
|
||||
autoFocus
|
||||
defaultValue={configContent}
|
||||
spellCheck="false"
|
||||
rows={Math.min(25, configContent.split('\n').length)}
|
||||
rows={Math.min(25, configContent.split("\n").length)}
|
||||
className="font-mono whitespace-pre"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Separator className="my-5" />
|
||||
<Button
|
||||
type="button"
|
||||
className="mt-2 flex items-center gap-1"
|
||||
onClick={fetchConfig}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<ButtonIcon className={clsx('h-4 w-4 mr-0.5', isLoading && 'animate-spin')} />
|
||||
{t('settings.export_configuration')}
|
||||
<Button type="button" className="mt-2 flex items-center gap-1" onClick={fetchConfig} disabled={isLoading}>
|
||||
<ButtonIcon className={clsx("h-4 w-4 mr-0.5", isLoading && "animate-spin")} />
|
||||
{t("settings.export_configuration")}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,21 +1,15 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { chartTimeData } from '@/lib/utils'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { LanguagesIcon, LoaderCircleIcon, SaveIcon } from 'lucide-react'
|
||||
import { UserSettings } from '@/types'
|
||||
import { saveSettings } from './layout'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { chartTimeData } from "@/lib/utils"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { LanguagesIcon, LoaderCircleIcon, SaveIcon } from "lucide-react"
|
||||
import { UserSettings } from "@/types"
|
||||
import { saveSettings } from "./layout"
|
||||
import { useState, useEffect } from "react"
|
||||
// import { Input } from '@/components/ui/input'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import languages from '../../../lib/languages.json'
|
||||
import { useTranslation } from "react-i18next"
|
||||
import languages from "../../../lib/languages.json"
|
||||
|
||||
export default function SettingsProfilePage({ userSettings }: { userSettings: UserSettings }) {
|
||||
const { t, i18n } = useTranslation()
|
||||
@@ -38,10 +32,8 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<h3 className="text-xl font-medium mb-2">{t('settings.general.title')}</h3>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
{t('settings.general.subtitle')}
|
||||
</p>
|
||||
<h3 className="text-xl font-medium mb-2">{t("settings.general.title")}</h3>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">{t("settings.general.subtitle")}</p>
|
||||
</div>
|
||||
<Separator className="my-4" />
|
||||
<form onSubmit={handleSubmit} className="space-y-5">
|
||||
@@ -49,18 +41,18 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
|
||||
<div className="mb-4">
|
||||
<h3 className="mb-1 text-lg font-medium flex items-center gap-2">
|
||||
<LanguagesIcon className="h-4 w-4" />
|
||||
{t('settings.general.language.title')}
|
||||
{t("settings.general.language.title")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
{t('settings.general.language.subtitle_1')}{' '}
|
||||
{t("settings.general.language.subtitle_1")}{" "}
|
||||
<a href="https://crowdin.com/project/beszel" className="link" target="_blank">
|
||||
Crowdin
|
||||
</a>{' '}
|
||||
{t('settings.general.language.subtitle_2')}
|
||||
</a>{" "}
|
||||
{t("settings.general.language.subtitle_2")}
|
||||
</p>
|
||||
</div>
|
||||
<Label className="block" htmlFor="lang">
|
||||
{t('settings.general.language.preferred_language')}
|
||||
{t("settings.general.language.preferred_language")}
|
||||
</Label>
|
||||
<Select value={i18n.language} onValueChange={(lang: string) => i18n.changeLanguage(lang)}>
|
||||
<SelectTrigger id="lang">
|
||||
@@ -78,21 +70,15 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
|
||||
<Separator />
|
||||
<div className="space-y-2">
|
||||
<div className="mb-4">
|
||||
<h3 className="mb-1 text-lg font-medium">
|
||||
{t('settings.general.chart_options.title')}
|
||||
</h3>
|
||||
<h3 className="mb-1 text-lg font-medium">{t("settings.general.chart_options.title")}</h3>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
{t('settings.general.chart_options.subtitle')}
|
||||
{t("settings.general.chart_options.subtitle")}
|
||||
</p>
|
||||
</div>
|
||||
<Label className="block" htmlFor="chartTime">
|
||||
{t('settings.general.chart_options.default_time_period')}
|
||||
{t("settings.general.chart_options.default_time_period")}
|
||||
</Label>
|
||||
<Select
|
||||
name="chartTime"
|
||||
key={userSettings.chartTime}
|
||||
defaultValue={userSettings.chartTime}
|
||||
>
|
||||
<Select name="chartTime" key={userSettings.chartTime} defaultValue={userSettings.chartTime}>
|
||||
<SelectTrigger id="chartTime">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
@@ -105,21 +91,13 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-[0.8rem] text-muted-foreground">
|
||||
{t('settings.general.chart_options.default_time_period_des')}
|
||||
{t("settings.general.chart_options.default_time_period_des")}
|
||||
</p>
|
||||
</div>
|
||||
<Separator />
|
||||
<Button
|
||||
type="submit"
|
||||
className="flex items-center gap-1.5 disabled:opacity-100"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<LoaderCircleIcon className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<SaveIcon className="h-4 w-4" />
|
||||
)}
|
||||
{t('settings.save_settings')}
|
||||
<Button type="submit" className="flex items-center gap-1.5 disabled:opacity-100" disabled={isLoading}>
|
||||
{isLoading ? <LoaderCircleIcon className="h-4 w-4 animate-spin" /> : <SaveIcon className="h-4 w-4" />}
|
||||
{t("settings.save_settings")}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import { useEffect } from 'react'
|
||||
import { Separator } from '../../ui/separator'
|
||||
import { SidebarNav } from './sidebar-nav.tsx'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card.tsx'
|
||||
import { useStore } from '@nanostores/react'
|
||||
import { $router } from '@/components/router.tsx'
|
||||
import { redirectPage } from '@nanostores/router'
|
||||
import { BellIcon, FileSlidersIcon, SettingsIcon } from 'lucide-react'
|
||||
import { $userSettings, pb } from '@/lib/stores.ts'
|
||||
import { toast } from '@/components/ui/use-toast.ts'
|
||||
import { UserSettings } from '@/types.js'
|
||||
import General from './general.tsx'
|
||||
import Notifications from './notifications.tsx'
|
||||
import ConfigYaml from './config-yaml.tsx'
|
||||
import { isAdmin } from '@/lib/utils.ts'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useEffect } from "react"
|
||||
import { Separator } from "../../ui/separator"
|
||||
import { SidebarNav } from "./sidebar-nav.tsx"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card.tsx"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { $router } from "@/components/router.tsx"
|
||||
import { redirectPage } from "@nanostores/router"
|
||||
import { BellIcon, FileSlidersIcon, SettingsIcon } from "lucide-react"
|
||||
import { $userSettings, pb } from "@/lib/stores.ts"
|
||||
import { toast } from "@/components/ui/use-toast.ts"
|
||||
import { UserSettings } from "@/types.js"
|
||||
import General from "./general.tsx"
|
||||
import Notifications from "./notifications.tsx"
|
||||
import ConfigYaml from "./config-yaml.tsx"
|
||||
import { isAdmin } from "@/lib/utils.ts"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
export async function saveSettings(newSettings: Partial<UserSettings>) {
|
||||
try {
|
||||
// get fresh copy of settings
|
||||
const req = await pb.collection('user_settings').getFirstListItem('', {
|
||||
fields: 'id,settings',
|
||||
const req = await pb.collection("user_settings").getFirstListItem("", {
|
||||
fields: "id,settings",
|
||||
})
|
||||
// update user settings
|
||||
const updatedSettings = await pb.collection('user_settings').update(req.id, {
|
||||
const updatedSettings = await pb.collection("user_settings").update(req.id, {
|
||||
settings: {
|
||||
...req.settings,
|
||||
...newSettings,
|
||||
@@ -30,15 +30,15 @@ export async function saveSettings(newSettings: Partial<UserSettings>) {
|
||||
})
|
||||
$userSettings.set(updatedSettings.settings)
|
||||
toast({
|
||||
title: 'Settings saved',
|
||||
description: 'Your user settings have been updated.',
|
||||
title: "Settings saved",
|
||||
description: "Your user settings have been updated.",
|
||||
})
|
||||
} catch (e) {
|
||||
// console.error('update settings', e)
|
||||
toast({
|
||||
title: 'Failed to save settings',
|
||||
description: 'Check logs for more details.',
|
||||
variant: 'destructive',
|
||||
title: "Failed to save settings",
|
||||
description: "Check logs for more details.",
|
||||
variant: "destructive",
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -48,21 +48,21 @@ export default function SettingsLayout() {
|
||||
|
||||
const sidebarNavItems = [
|
||||
{
|
||||
title: t('settings.general.title'),
|
||||
href: '/settings/general',
|
||||
title: t("settings.general.title"),
|
||||
href: "/settings/general",
|
||||
icon: SettingsIcon,
|
||||
},
|
||||
{
|
||||
title: t('settings.notifications.title'),
|
||||
href: '/settings/notifications',
|
||||
title: t("settings.notifications.title"),
|
||||
href: "/settings/notifications",
|
||||
icon: BellIcon,
|
||||
},
|
||||
]
|
||||
|
||||
if (isAdmin()) {
|
||||
sidebarNavItems.push({
|
||||
title: t('settings.yaml_config.short_title'),
|
||||
href: '/settings/config',
|
||||
title: t("settings.yaml_config.short_title"),
|
||||
href: "/settings/config",
|
||||
icon: FileSlidersIcon,
|
||||
})
|
||||
}
|
||||
@@ -70,18 +70,18 @@ export default function SettingsLayout() {
|
||||
const page = useStore($router)
|
||||
|
||||
useEffect(() => {
|
||||
document.title = 'Settings / Beszel'
|
||||
document.title = "Settings / Beszel"
|
||||
// redirect to account page if no page is specified
|
||||
if (page?.path === '/settings') {
|
||||
redirectPage($router, 'settings', { name: 'general' })
|
||||
if (page?.path === "/settings") {
|
||||
redirectPage($router, "settings", { name: "general" })
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Card className="pt-5 px-4 pb-8 sm:pt-6 sm:px-7">
|
||||
<CardHeader className="p-0">
|
||||
<CardTitle className="mb-1">{t('settings.settings')}</CardTitle>
|
||||
<CardDescription>{t('settings.subtitle')}</CardDescription>
|
||||
<CardTitle className="mb-1">{t("settings.settings")}</CardTitle>
|
||||
<CardDescription>{t("settings.subtitle")}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<Separator className="hidden md:block my-5" />
|
||||
@@ -91,7 +91,7 @@ export default function SettingsLayout() {
|
||||
</aside>
|
||||
<div className="flex-1">
|
||||
{/* @ts-ignore */}
|
||||
<SettingsContent name={page?.params?.name ?? 'general'} />
|
||||
<SettingsContent name={page?.params?.name ?? "general"} />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -103,11 +103,11 @@ function SettingsContent({ name }: { name: string }) {
|
||||
const userSettings = useStore($userSettings)
|
||||
|
||||
switch (name) {
|
||||
case 'general':
|
||||
case "general":
|
||||
return <General userSettings={userSettings} />
|
||||
case 'notifications':
|
||||
case "notifications":
|
||||
return <Notifications userSettings={userSettings} />
|
||||
case 'config':
|
||||
case "config":
|
||||
return <ConfigYaml />
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { pb } from '@/lib/stores'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { BellIcon, LoaderCircleIcon, PlusIcon, SaveIcon, Trash2Icon } from 'lucide-react'
|
||||
import { ChangeEventHandler, useEffect, useState } from 'react'
|
||||
import { toast } from '@/components/ui/use-toast'
|
||||
import { InputTags } from '@/components/ui/input-tags'
|
||||
import { UserSettings } from '@/types'
|
||||
import { saveSettings } from './layout'
|
||||
import * as v from 'valibot'
|
||||
import { isAdmin } from '@/lib/utils'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { pb } from "@/lib/stores"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { Card } from "@/components/ui/card"
|
||||
import { BellIcon, LoaderCircleIcon, PlusIcon, SaveIcon, Trash2Icon } from "lucide-react"
|
||||
import { ChangeEventHandler, useEffect, useState } from "react"
|
||||
import { toast } from "@/components/ui/use-toast"
|
||||
import { InputTags } from "@/components/ui/input-tags"
|
||||
import { UserSettings } from "@/types"
|
||||
import { saveSettings } from "./layout"
|
||||
import * as v from "valibot"
|
||||
import { isAdmin } from "@/lib/utils"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
interface ShoutrrrUrlCardProps {
|
||||
url: string
|
||||
@@ -39,10 +39,10 @@ const SettingsNotificationsPage = ({ userSettings }: { userSettings: UserSetting
|
||||
}, [userSettings])
|
||||
|
||||
function addWebhook() {
|
||||
setWebhooks([...webhooks, ''])
|
||||
setWebhooks([...webhooks, ""])
|
||||
// focus on the new input
|
||||
queueMicrotask(() => {
|
||||
const inputs = document.querySelectorAll('#webhooks input') as NodeListOf<HTMLInputElement>
|
||||
const inputs = document.querySelectorAll("#webhooks input") as NodeListOf<HTMLInputElement>
|
||||
inputs[inputs.length - 1]?.focus()
|
||||
})
|
||||
}
|
||||
@@ -61,9 +61,9 @@ const SettingsNotificationsPage = ({ userSettings }: { userSettings: UserSetting
|
||||
await saveSettings(parsedData)
|
||||
} catch (e: any) {
|
||||
toast({
|
||||
title: 'Failed to save settings',
|
||||
title: "Failed to save settings",
|
||||
description: e.message,
|
||||
variant: 'destructive',
|
||||
variant: "destructive",
|
||||
})
|
||||
}
|
||||
setIsLoading(false)
|
||||
@@ -72,63 +72,51 @@ const SettingsNotificationsPage = ({ userSettings }: { userSettings: UserSetting
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<h3 className="text-xl font-medium mb-2">{t('settings.notifications.title')}</h3>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
{t('settings.notifications.subtitle_1')}
|
||||
</p>
|
||||
<h3 className="text-xl font-medium mb-2">{t("settings.notifications.title")}</h3>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">{t("settings.notifications.subtitle_1")}</p>
|
||||
<p className="text-sm text-muted-foreground mt-1.5 leading-relaxed">
|
||||
{t('settings.notifications.subtitle_2')}{' '}
|
||||
<BellIcon className="inline h-4 w-4" /> {t('settings.notifications.subtitle_3')}
|
||||
{t("settings.notifications.subtitle_2")} <BellIcon className="inline h-4 w-4" />{" "}
|
||||
{t("settings.notifications.subtitle_3")}
|
||||
</p>
|
||||
</div>
|
||||
<Separator className="my-4" />
|
||||
<div className="space-y-5">
|
||||
<div className="space-y-2">
|
||||
<div className="mb-4">
|
||||
<h3 className="mb-1 text-lg font-medium">
|
||||
{t('settings.notifications.email.title')}
|
||||
</h3>
|
||||
<h3 className="mb-1 text-lg font-medium">{t("settings.notifications.email.title")}</h3>
|
||||
{isAdmin() && (
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
{t('settings.notifications.email.please')}{' '}
|
||||
{t("settings.notifications.email.please")}{" "}
|
||||
<a href="/_/#/settings/mail" className="link" target="_blank">
|
||||
{t('settings.notifications.email.configure_an_SMTP_server')}
|
||||
</a>{' '}
|
||||
{t('settings.notifications.email.to_ensure_alerts_are_delivered')}{' '}
|
||||
{t("settings.notifications.email.configure_an_SMTP_server")}
|
||||
</a>{" "}
|
||||
{t("settings.notifications.email.to_ensure_alerts_are_delivered")}{" "}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Label className="block" htmlFor="email">
|
||||
{t('settings.notifications.email.to_email_s')}
|
||||
{t("settings.notifications.email.to_email_s")}
|
||||
</Label>
|
||||
<InputTags
|
||||
value={emails}
|
||||
onChange={setEmails}
|
||||
placeholder={t('settings.notifications.email.enter_email_address')}
|
||||
placeholder={t("settings.notifications.email.enter_email_address")}
|
||||
className="w-full"
|
||||
type="email"
|
||||
id="email"
|
||||
/>
|
||||
<p className="text-[0.8rem] text-muted-foreground">
|
||||
{t('settings.notifications.email.des')}
|
||||
</p>
|
||||
<p className="text-[0.8rem] text-muted-foreground">{t("settings.notifications.email.des")}</p>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<h3 className="mb-1 text-lg font-medium">
|
||||
{t('settings.notifications.webhook_push.title')}
|
||||
</h3>
|
||||
<h3 className="mb-1 text-lg font-medium">{t("settings.notifications.webhook_push.title")}</h3>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
{t('settings.notifications.webhook_push.des_1')}{' '}
|
||||
<a
|
||||
href="https://containrrr.dev/shoutrrr/services/overview/"
|
||||
target="_blank"
|
||||
className="link"
|
||||
>
|
||||
{t("settings.notifications.webhook_push.des_1")}{" "}
|
||||
<a href="https://containrrr.dev/shoutrrr/services/overview/" target="_blank" className="link">
|
||||
Shoutrrr
|
||||
</a>{' '}
|
||||
{t('settings.notifications.webhook_push.des_2')}
|
||||
</a>{" "}
|
||||
{t("settings.notifications.webhook_push.des_2")}
|
||||
</p>
|
||||
</div>
|
||||
{webhooks.length > 0 && (
|
||||
@@ -137,9 +125,7 @@ const SettingsNotificationsPage = ({ userSettings }: { userSettings: UserSetting
|
||||
<ShoutrrrUrlCard
|
||||
key={index}
|
||||
url={webhook}
|
||||
onUrlChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
updateWebhook(index, e.target.value)
|
||||
}
|
||||
onUrlChange={(e: React.ChangeEvent<HTMLInputElement>) => updateWebhook(index, e.target.value)}
|
||||
onRemove={() => removeWebhook(index)}
|
||||
/>
|
||||
))}
|
||||
@@ -153,7 +139,7 @@ const SettingsNotificationsPage = ({ userSettings }: { userSettings: UserSetting
|
||||
onClick={addWebhook}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4 -ml-0.5" />
|
||||
{t('settings.notifications.webhook_push.add_url')}
|
||||
{t("settings.notifications.webhook_push.add_url")}
|
||||
</Button>
|
||||
</div>
|
||||
<Separator />
|
||||
@@ -163,12 +149,8 @@ const SettingsNotificationsPage = ({ userSettings }: { userSettings: UserSetting
|
||||
onClick={updateSettings}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<LoaderCircleIcon className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<SaveIcon className="h-4 w-4" />
|
||||
)}
|
||||
{t('settings.save_settings')}
|
||||
{isLoading ? <LoaderCircleIcon className="h-4 w-4 animate-spin" /> : <SaveIcon className="h-4 w-4" />}
|
||||
{t("settings.save_settings")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -180,17 +162,17 @@ const ShoutrrrUrlCard = ({ url, onUrlChange, onRemove }: ShoutrrrUrlCardProps) =
|
||||
|
||||
const sendTestNotification = async () => {
|
||||
setIsLoading(true)
|
||||
const res = await pb.send('/api/beszel/send-test-notification', { url })
|
||||
if ('err' in res && !res.err) {
|
||||
const res = await pb.send("/api/beszel/send-test-notification", { url })
|
||||
if ("err" in res && !res.err) {
|
||||
toast({
|
||||
title: 'Test notification sent',
|
||||
description: 'Check your notification service',
|
||||
title: "Test notification sent",
|
||||
description: "Check your notification service",
|
||||
})
|
||||
} else {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: res.err ?? 'Failed to send test notification',
|
||||
variant: 'destructive',
|
||||
title: "Error",
|
||||
description: res.err ?? "Failed to send test notification",
|
||||
variant: "destructive",
|
||||
})
|
||||
}
|
||||
setIsLoading(false)
|
||||
@@ -211,7 +193,7 @@ const ShoutrrrUrlCard = ({ url, onUrlChange, onRemove }: ShoutrrrUrlCardProps) =
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="w-20 md:w-28"
|
||||
disabled={isLoading || url === ''}
|
||||
disabled={isLoading || url === ""}
|
||||
onClick={sendTestNotification}
|
||||
>
|
||||
{isLoading ? (
|
||||
@@ -222,14 +204,7 @@ const ShoutrrrUrlCard = ({ url, onUrlChange, onRemove }: ShoutrrrUrlCardProps) =
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="shrink-0"
|
||||
aria-label="Delete"
|
||||
onClick={onRemove}
|
||||
>
|
||||
<Button type="button" variant="outline" size="icon" className="shrink-0" aria-label="Delete" onClick={onRemove}>
|
||||
<Trash2Icon className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
import React from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { buttonVariants } from '../../ui/button'
|
||||
import { $router, Link, navigate } from '../../router'
|
||||
import { useStore } from '@nanostores/react'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "../../ui/button"
|
||||
import { $router, Link, navigate } from "../../router"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
|
||||
interface SidebarNavProps extends React.HTMLAttributes<HTMLElement> {
|
||||
items: {
|
||||
@@ -46,16 +40,16 @@ export function SidebarNav({ className, items, ...props }: SidebarNavProps) {
|
||||
</div>
|
||||
|
||||
{/* Desktop View */}
|
||||
<nav className={cn('hidden md:grid gap-1', className)} {...props}>
|
||||
<nav className={cn("hidden md:grid gap-1", className)} {...props}>
|
||||
{items.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={cn(
|
||||
buttonVariants({ variant: 'ghost' }),
|
||||
'flex items-center gap-3',
|
||||
page?.path === item.href ? 'bg-muted hover:bg-muted' : 'hover:bg-muted/50',
|
||||
'justify-start'
|
||||
buttonVariants({ variant: "ghost" }),
|
||||
"flex items-center gap-3",
|
||||
page?.path === item.href ? "bg-muted hover:bg-muted" : "hover:bg-muted/50",
|
||||
"justify-start"
|
||||
)}
|
||||
>
|
||||
{item.icon && <item.icon className="h-4 w-4" />}
|
||||
|
||||
@@ -1,40 +1,34 @@
|
||||
import { $systems, pb, $chartTime, $containerFilter, $userSettings } from '@/lib/stores'
|
||||
import {
|
||||
ChartData,
|
||||
ChartTimes,
|
||||
ContainerStatsRecord,
|
||||
SystemRecord,
|
||||
SystemStatsRecord,
|
||||
} from '@/types'
|
||||
import React, { lazy, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Card, CardHeader, CardTitle, CardDescription } from '../ui/card'
|
||||
import { useStore } from '@nanostores/react'
|
||||
import Spinner from '../spinner'
|
||||
import { ClockArrowUp, CpuIcon, GlobeIcon, LayoutGridIcon, MonitorIcon, XIcon } from 'lucide-react'
|
||||
import ChartTimeSelect from '../charts/chart-time-select'
|
||||
import { chartTimeData, cn, getPbTimestamp, useLocalStorage } from '@/lib/utils'
|
||||
import { Separator } from '../ui/separator'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip'
|
||||
import { Button } from '../ui/button'
|
||||
import { Input } from '../ui/input'
|
||||
import { ChartAverage, ChartMax, Rows, TuxIcon } from '../ui/icons'
|
||||
import { useIntersectionObserver } from '@/lib/use-intersection-observer'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'
|
||||
import { timeTicks } from 'd3-time'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { $systems, pb, $chartTime, $containerFilter, $userSettings } from "@/lib/stores"
|
||||
import { ChartData, ChartTimes, ContainerStatsRecord, SystemRecord, SystemStatsRecord } from "@/types"
|
||||
import React, { lazy, useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { Card, CardHeader, CardTitle, CardDescription } from "../ui/card"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import Spinner from "../spinner"
|
||||
import { ClockArrowUp, CpuIcon, GlobeIcon, LayoutGridIcon, MonitorIcon, XIcon } from "lucide-react"
|
||||
import ChartTimeSelect from "../charts/chart-time-select"
|
||||
import { chartTimeData, cn, getPbTimestamp, useLocalStorage } from "@/lib/utils"
|
||||
import { Separator } from "../ui/separator"
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip"
|
||||
import { Button } from "../ui/button"
|
||||
import { Input } from "../ui/input"
|
||||
import { ChartAverage, ChartMax, Rows, TuxIcon } from "../ui/icons"
|
||||
import { useIntersectionObserver } from "@/lib/use-intersection-observer"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"
|
||||
import { timeTicks } from "d3-time"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
const AreaChartDefault = lazy(() => import('../charts/area-chart'))
|
||||
const ContainerChart = lazy(() => import('../charts/container-chart'))
|
||||
const MemChart = lazy(() => import('../charts/mem-chart'))
|
||||
const DiskChart = lazy(() => import('../charts/disk-chart'))
|
||||
const SwapChart = lazy(() => import('../charts/swap-chart'))
|
||||
const TemperatureChart = lazy(() => import('../charts/temperature-chart'))
|
||||
const AreaChartDefault = lazy(() => import("../charts/area-chart"))
|
||||
const ContainerChart = lazy(() => import("../charts/container-chart"))
|
||||
const MemChart = lazy(() => import("../charts/mem-chart"))
|
||||
const DiskChart = lazy(() => import("../charts/disk-chart"))
|
||||
const SwapChart = lazy(() => import("../charts/swap-chart"))
|
||||
const TemperatureChart = lazy(() => import("../charts/temperature-chart"))
|
||||
|
||||
const cache = new Map<string, any>()
|
||||
|
||||
// create ticks and domain for charts
|
||||
function getTimeData(chartTime: ChartTimes, lastCreated: number) {
|
||||
const cached = cache.get('td')
|
||||
const cached = cache.get("td")
|
||||
if (cached && cached.chartTime === chartTime) {
|
||||
if (!lastCreated || cached.time >= lastCreated) {
|
||||
return cached.data
|
||||
@@ -43,14 +37,12 @@ function getTimeData(chartTime: ChartTimes, lastCreated: number) {
|
||||
|
||||
const now = new Date()
|
||||
const startTime = chartTimeData[chartTime].getOffset(now)
|
||||
const ticks = timeTicks(startTime, now, chartTimeData[chartTime].ticks ?? 12).map((date) =>
|
||||
date.getTime()
|
||||
)
|
||||
const ticks = timeTicks(startTime, now, chartTimeData[chartTime].ticks ?? 12).map((date) => date.getTime())
|
||||
const data = {
|
||||
ticks,
|
||||
domain: [chartTimeData[chartTime].getOffset(now).getTime(), now.getTime()],
|
||||
}
|
||||
cache.set('td', { time: now.getTime(), data, chartTime })
|
||||
cache.set("td", { time: now.getTime(), data, chartTime })
|
||||
return data
|
||||
}
|
||||
|
||||
@@ -79,20 +71,16 @@ function addEmptyValues<T extends SystemStatsRecord | ContainerStatsRecord>(
|
||||
return modifiedRecords
|
||||
}
|
||||
|
||||
async function getStats<T>(
|
||||
collection: string,
|
||||
system: SystemRecord,
|
||||
chartTime: ChartTimes
|
||||
): Promise<T[]> {
|
||||
async function getStats<T>(collection: string, system: SystemRecord, chartTime: ChartTimes): Promise<T[]> {
|
||||
const lastCached = cache.get(`${system.id}_${chartTime}_${collection}`)?.at(-1)?.created as number
|
||||
return await pb.collection<T>(collection).getFullList({
|
||||
filter: pb.filter('system={:id} && created > {:created} && type={:type}', {
|
||||
filter: pb.filter("system={:id} && created > {:created} && type={:type}", {
|
||||
id: system.id,
|
||||
created: getPbTimestamp(chartTime, lastCached ? new Date(lastCached + 1000) : undefined),
|
||||
type: chartTimeData[chartTime].type,
|
||||
}),
|
||||
fields: 'created,stats',
|
||||
sort: 'created',
|
||||
fields: "created,stats",
|
||||
sort: "created",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -105,15 +93,15 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
const cpuMaxStore = useState(false)
|
||||
const bandwidthMaxStore = useState(false)
|
||||
const diskIoMaxStore = useState(false)
|
||||
const [grid, setGrid] = useLocalStorage('grid', true)
|
||||
const [grid, setGrid] = useLocalStorage("grid", true)
|
||||
const [system, setSystem] = useState({} as SystemRecord)
|
||||
const [systemStats, setSystemStats] = useState([] as SystemStatsRecord[])
|
||||
const [containerData, setContainerData] = useState([] as ChartData['containerData'])
|
||||
const [containerData, setContainerData] = useState([] as ChartData["containerData"])
|
||||
const netCardRef = useRef<HTMLDivElement>(null)
|
||||
const [containerFilterBar, setContainerFilterBar] = useState(null as null | JSX.Element)
|
||||
const [bottomSpacing, setBottomSpacing] = useState(0)
|
||||
const [chartLoading, setChartLoading] = useState(false)
|
||||
const isLongerChart = chartTime !== '1h'
|
||||
const isLongerChart = chartTime !== "1h"
|
||||
|
||||
useEffect(() => {
|
||||
document.title = `${name} / Beszel`
|
||||
@@ -123,7 +111,7 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
setSystemStats([])
|
||||
setContainerData([])
|
||||
setContainerFilterBar(null)
|
||||
$containerFilter.set('')
|
||||
$containerFilter.set("")
|
||||
cpuMaxStore[1](false)
|
||||
bandwidthMaxStore[1](false)
|
||||
diskIoMaxStore[1](false)
|
||||
@@ -153,11 +141,11 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
if (!system.id) {
|
||||
return
|
||||
}
|
||||
pb.collection<SystemRecord>('systems').subscribe(system.id, (e) => {
|
||||
pb.collection<SystemRecord>("systems").subscribe(system.id, (e) => {
|
||||
setSystem(e.record)
|
||||
})
|
||||
return () => {
|
||||
pb.collection('systems').unsubscribe(system.id)
|
||||
pb.collection("systems").unsubscribe(system.id)
|
||||
}
|
||||
}, [system.id])
|
||||
|
||||
@@ -182,8 +170,8 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
// loading: true
|
||||
setChartLoading(true)
|
||||
Promise.allSettled([
|
||||
getStats<SystemStatsRecord>('system_stats', system, chartTime),
|
||||
getStats<ContainerStatsRecord>('container_stats', system, chartTime),
|
||||
getStats<SystemStatsRecord>("system_stats", system, chartTime),
|
||||
getStats<ContainerStatsRecord>("container_stats", system, chartTime),
|
||||
]).then(([systemStats, containerStats]) => {
|
||||
// loading: false
|
||||
setChartLoading(false)
|
||||
@@ -192,10 +180,8 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
// make new system stats
|
||||
const ss_cache_key = `${system.id}_${chartTime}_system_stats`
|
||||
let systemData = (cache.get(ss_cache_key) || []) as SystemStatsRecord[]
|
||||
if (systemStats.status === 'fulfilled' && systemStats.value.length) {
|
||||
systemData = systemData.concat(
|
||||
addEmptyValues(systemData, systemStats.value, expectedInterval)
|
||||
)
|
||||
if (systemStats.status === "fulfilled" && systemStats.value.length) {
|
||||
systemData = systemData.concat(addEmptyValues(systemData, systemStats.value, expectedInterval))
|
||||
if (systemData.length > 120) {
|
||||
systemData = systemData.slice(-100)
|
||||
}
|
||||
@@ -205,10 +191,8 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
// make new container stats
|
||||
const cs_cache_key = `${system.id}_${chartTime}_container_stats`
|
||||
let containerData = (cache.get(cs_cache_key) || []) as ContainerStatsRecord[]
|
||||
if (containerStats.status === 'fulfilled' && containerStats.value.length) {
|
||||
containerData = containerData.concat(
|
||||
addEmptyValues(containerData, containerStats.value, expectedInterval)
|
||||
)
|
||||
if (containerStats.status === "fulfilled" && containerStats.value.length) {
|
||||
containerData = containerData.concat(addEmptyValues(containerData, containerStats.value, expectedInterval))
|
||||
if (containerData.length > 120) {
|
||||
containerData = containerData.slice(-100)
|
||||
}
|
||||
@@ -225,7 +209,7 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
|
||||
// make container stats for charts
|
||||
const makeContainerData = useCallback((containers: ContainerStatsRecord[]) => {
|
||||
const containerData = [] as ChartData['containerData']
|
||||
const containerData = [] as ChartData["containerData"]
|
||||
for (let { created, stats } of containers) {
|
||||
if (!created) {
|
||||
// @ts-ignore add null value for gaps
|
||||
@@ -234,7 +218,7 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
}
|
||||
created = new Date(created).getTime()
|
||||
// @ts-ignore not dealing with this rn
|
||||
let containerStats: ChartData['containerData'][0] = { created }
|
||||
let containerStats: ChartData["containerData"][0] = { created }
|
||||
for (let container of stats) {
|
||||
containerStats[container.n] = container
|
||||
}
|
||||
@@ -251,7 +235,7 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
let uptime: number | string = system.info.u
|
||||
if (system.info.u < 172800) {
|
||||
const hours = Math.trunc(uptime / 3600)
|
||||
uptime = `${hours} hour${hours == 1 ? '' : 's'}`
|
||||
uptime = `${hours} hour${hours == 1 ? "" : "s"}`
|
||||
} else {
|
||||
uptime = `${Math.trunc(system.info?.u / 86400)} days`
|
||||
}
|
||||
@@ -260,14 +244,14 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
{
|
||||
value: system.info.h,
|
||||
Icon: MonitorIcon,
|
||||
label: 'Hostname',
|
||||
label: "Hostname",
|
||||
// hide if hostname is same as host or name
|
||||
hide: system.info.h === system.host || system.info.h === system.name,
|
||||
},
|
||||
{ value: uptime, Icon: ClockArrowUp, label: 'Uptime' },
|
||||
{ value: system.info.k, Icon: TuxIcon, label: 'Kernel' },
|
||||
{ value: uptime, Icon: ClockArrowUp, label: "Uptime" },
|
||||
{ value: system.info.k, Icon: TuxIcon, label: "Kernel" },
|
||||
{
|
||||
value: `${system.info.m} (${system.info.c}c${system.info.t ? `/${system.info.t}t` : ''})`,
|
||||
value: `${system.info.m} (${system.info.c}c${system.info.t ? `/${system.info.t}t` : ""})`,
|
||||
Icon: CpuIcon,
|
||||
hide: !system.info.m,
|
||||
},
|
||||
@@ -286,7 +270,7 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
return
|
||||
}
|
||||
const tooltipHeight = (Object.keys(containerData[0]).length - 11) * 17.8 - 40
|
||||
const wrapperEl = document.getElementById('chartwrap') as HTMLDivElement
|
||||
const wrapperEl = document.getElementById("chartwrap") as HTMLDivElement
|
||||
const wrapperRect = wrapperEl.getBoundingClientRect()
|
||||
const chartRect = netCardRef.current.getBoundingClientRect()
|
||||
const distanceToBottom = wrapperRect.bottom - chartRect.bottom
|
||||
@@ -298,7 +282,7 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
}
|
||||
|
||||
// if no data, show empty state
|
||||
const dataEmpty = !chartLoading && chartData.systemStats.length === 0;
|
||||
const dataEmpty = !chartLoading && chartData.systemStats.length === 0
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -310,19 +294,19 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
<h1 className="text-[1.6rem] font-semibold mb-1.5">{system.name}</h1>
|
||||
<div className="flex flex-wrap items-center gap-3 gap-y-2 text-sm opacity-90">
|
||||
<div className="capitalize flex gap-2 items-center">
|
||||
<span className={cn('relative flex h-3 w-3')}>
|
||||
{system.status === 'up' && (
|
||||
<span className={cn("relative flex h-3 w-3")}>
|
||||
{system.status === "up" && (
|
||||
<span
|
||||
className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"
|
||||
style={{ animationDuration: '1.5s' }}
|
||||
style={{ animationDuration: "1.5s" }}
|
||||
></span>
|
||||
)}
|
||||
<span
|
||||
className={cn('relative inline-flex rounded-full h-3 w-3', {
|
||||
'bg-green-500': system.status === 'up',
|
||||
'bg-red-500': system.status === 'down',
|
||||
'bg-primary/40': system.status === 'paused',
|
||||
'bg-yellow-500': system.status === 'pending',
|
||||
className={cn("relative inline-flex rounded-full h-3 w-3", {
|
||||
"bg-green-500": system.status === "up",
|
||||
"bg-red-500": system.status === "down",
|
||||
"bg-primary/40": system.status === "paused",
|
||||
"bg-yellow-500": system.status === "pending",
|
||||
})}
|
||||
></span>
|
||||
</span>
|
||||
@@ -361,7 +345,7 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
aria-label={t('monitor.toggle_grid')}
|
||||
aria-label={t("monitor.toggle_grid")}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="hidden lg:flex p-0 text-primary"
|
||||
@@ -374,7 +358,7 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t('monitor.toggle_grid')}</TooltipContent>
|
||||
<TooltipContent>{t("monitor.toggle_grid")}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
@@ -386,24 +370,21 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
<ChartCard
|
||||
empty={dataEmpty}
|
||||
grid={grid}
|
||||
title={t('monitor.total_cpu_usage')}
|
||||
description={`${cpuMaxStore[0] && isLongerChart ? t('monitor.max_1_min') : t('monitor.average') } ${t('monitor.cpu_des')}`}
|
||||
title={t("monitor.total_cpu_usage")}
|
||||
description={`${cpuMaxStore[0] && isLongerChart ? t("monitor.max_1_min") : t("monitor.average")} ${t(
|
||||
"monitor.cpu_des"
|
||||
)}`}
|
||||
cornerEl={isLongerChart ? <SelectAvgMax store={cpuMaxStore} /> : null}
|
||||
>
|
||||
<AreaChartDefault
|
||||
chartData={chartData}
|
||||
chartName="CPU Usage"
|
||||
maxToggled={cpuMaxStore[0]}
|
||||
unit="%"
|
||||
/>
|
||||
<AreaChartDefault chartData={chartData} chartName="CPU Usage" maxToggled={cpuMaxStore[0]} unit="%" />
|
||||
</ChartCard>
|
||||
|
||||
{containerFilterBar && (
|
||||
<ChartCard
|
||||
empty={dataEmpty}
|
||||
grid={grid}
|
||||
title={t('monitor.docker_cpu_usage')}
|
||||
description={t('monitor.docker_cpu_des')}
|
||||
title={t("monitor.docker_cpu_usage")}
|
||||
description={t("monitor.docker_cpu_des")}
|
||||
cornerEl={containerFilterBar}
|
||||
>
|
||||
<ContainerChart chartData={chartData} dataKey="c" chartName="cpu" />
|
||||
@@ -413,8 +394,8 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
<ChartCard
|
||||
empty={dataEmpty}
|
||||
grid={grid}
|
||||
title={t('monitor.total_memory_usage')}
|
||||
description={t('monitor.memory_des')}
|
||||
title={t("monitor.total_memory_usage")}
|
||||
description={t("monitor.memory_des")}
|
||||
>
|
||||
<MemChart chartData={chartData} />
|
||||
</ChartCard>
|
||||
@@ -423,15 +404,15 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
<ChartCard
|
||||
empty={dataEmpty}
|
||||
grid={grid}
|
||||
title={t('monitor.docker_memory_usage')}
|
||||
description={t('monitor.docker_memory_des')}
|
||||
title={t("monitor.docker_memory_usage")}
|
||||
description={t("monitor.docker_memory_des")}
|
||||
cornerEl={containerFilterBar}
|
||||
>
|
||||
<ContainerChart chartData={chartData} chartName="mem" dataKey="m" unit=" MB" />
|
||||
</ChartCard>
|
||||
)}
|
||||
|
||||
<ChartCard empty={dataEmpty} grid={grid} title={t('monitor.disk_space')} description={t('monitor.disk_des')}>
|
||||
<ChartCard empty={dataEmpty} grid={grid} title={t("monitor.disk_space")} description={t("monitor.disk_des")}>
|
||||
<DiskChart
|
||||
chartData={chartData}
|
||||
dataKey="stats.du"
|
||||
@@ -442,42 +423,34 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
<ChartCard
|
||||
empty={dataEmpty}
|
||||
grid={grid}
|
||||
title={t('monitor.disk_io')}
|
||||
description={t('monitor.disk_io_des')}
|
||||
title={t("monitor.disk_io")}
|
||||
description={t("monitor.disk_io_des")}
|
||||
cornerEl={isLongerChart ? <SelectAvgMax store={diskIoMaxStore} /> : null}
|
||||
>
|
||||
<AreaChartDefault
|
||||
chartData={chartData}
|
||||
maxToggled={diskIoMaxStore[0]}
|
||||
chartName="dio"
|
||||
/>
|
||||
<AreaChartDefault chartData={chartData} maxToggled={diskIoMaxStore[0]} chartName="dio" />
|
||||
</ChartCard>
|
||||
|
||||
<ChartCard
|
||||
empty={dataEmpty}
|
||||
grid={grid}
|
||||
title={t('monitor.bandwidth')}
|
||||
title={t("monitor.bandwidth")}
|
||||
cornerEl={isLongerChart ? <SelectAvgMax store={bandwidthMaxStore} /> : null}
|
||||
description={t('monitor.bandwidth_des')}
|
||||
description={t("monitor.bandwidth_des")}
|
||||
>
|
||||
<AreaChartDefault
|
||||
chartData={chartData}
|
||||
maxToggled={bandwidthMaxStore[0]}
|
||||
chartName="bw"
|
||||
/>
|
||||
<AreaChartDefault chartData={chartData} maxToggled={bandwidthMaxStore[0]} chartName="bw" />
|
||||
</ChartCard>
|
||||
|
||||
{containerFilterBar && containerData.length > 0 && (
|
||||
<div
|
||||
ref={netCardRef}
|
||||
className={cn({
|
||||
'col-span-full': !grid,
|
||||
"col-span-full": !grid,
|
||||
})}
|
||||
>
|
||||
<ChartCard
|
||||
empty={dataEmpty}
|
||||
title={t('monitor.docker_network_io')}
|
||||
description={t('monitor.docker_network_io_des')}
|
||||
title={t("monitor.docker_network_io")}
|
||||
description={t("monitor.docker_network_io_des")}
|
||||
cornerEl={containerFilterBar}
|
||||
>
|
||||
{/* @ts-ignore */}
|
||||
@@ -487,13 +460,23 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
)}
|
||||
|
||||
{(systemStats.at(-1)?.stats.su ?? 0) > 0 && (
|
||||
<ChartCard empty={dataEmpty} grid={grid} title={t('monitor.swap_usage')} description={t('monitor.swap_des')}>
|
||||
<ChartCard
|
||||
empty={dataEmpty}
|
||||
grid={grid}
|
||||
title={t("monitor.swap_usage")}
|
||||
description={t("monitor.swap_des")}
|
||||
>
|
||||
<SwapChart chartData={chartData} />
|
||||
</ChartCard>
|
||||
)}
|
||||
|
||||
{systemStats.at(-1)?.stats.t && (
|
||||
<ChartCard empty={dataEmpty} grid={grid} title={t('monitor.temperature')} description={t('monitor.temperature_des')}>
|
||||
<ChartCard
|
||||
empty={dataEmpty}
|
||||
grid={grid}
|
||||
title={t("monitor.temperature")}
|
||||
description={t("monitor.temperature_des")}
|
||||
>
|
||||
<TemperatureChart chartData={chartData} />
|
||||
</ChartCard>
|
||||
)}
|
||||
@@ -508,8 +491,8 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
<ChartCard
|
||||
empty={dataEmpty}
|
||||
grid={grid}
|
||||
title={`${extraFsName} ${t('monitor.usage')}`}
|
||||
description={`${t('monitor.disk_usage_of')} ${extraFsName}`}
|
||||
title={`${extraFsName} ${t("monitor.usage")}`}
|
||||
description={`${t("monitor.disk_usage_of")} ${extraFsName}`}
|
||||
>
|
||||
<DiskChart
|
||||
chartData={chartData}
|
||||
@@ -521,7 +504,7 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
empty={dataEmpty}
|
||||
grid={grid}
|
||||
title={`${extraFsName} I/O`}
|
||||
description={`${t('monitor.throughput_of')} ${extraFsName}`}
|
||||
description={`${t("monitor.throughput_of")} ${extraFsName}`}
|
||||
cornerEl={isLongerChart ? <SelectAvgMax store={diskIoMaxStore} /> : null}
|
||||
>
|
||||
<AreaChartDefault
|
||||
@@ -554,12 +537,7 @@ function ContainerFilterBar() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Input
|
||||
placeholder={t('filter')}
|
||||
className="pl-4 pr-8"
|
||||
value={containerFilter}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Input placeholder={t("filter")} className="pl-4 pr-8" value={containerFilter} onChange={handleChange} />
|
||||
{containerFilter && (
|
||||
<Button
|
||||
type="button"
|
||||
@@ -567,7 +545,7 @@ function ContainerFilterBar() {
|
||||
size="icon"
|
||||
aria-label="Clear"
|
||||
className="absolute right-1 top-1/2 -translate-y-1/2 h-7 w-7 text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100"
|
||||
onClick={() => $containerFilter.set('')}
|
||||
onClick={() => $containerFilter.set("")}
|
||||
>
|
||||
<XIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -576,28 +554,24 @@ function ContainerFilterBar() {
|
||||
)
|
||||
}
|
||||
|
||||
function SelectAvgMax({
|
||||
store,
|
||||
}: {
|
||||
store: [boolean, React.Dispatch<React.SetStateAction<boolean>>]
|
||||
}) {
|
||||
function SelectAvgMax({ store }: { store: [boolean, React.Dispatch<React.SetStateAction<boolean>>] }) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [max, setMax] = store
|
||||
const Icon = max ? ChartMax : ChartAverage
|
||||
|
||||
return (
|
||||
<Select value={max ? 'max' : 'avg'} onValueChange={(e) => setMax(e === 'max')}>
|
||||
<Select value={max ? "max" : "avg"} onValueChange={(e) => setMax(e === "max")}>
|
||||
<SelectTrigger className="relative pl-10 pr-5">
|
||||
<Icon className="h-4 w-4 absolute left-4 top-1/2 -translate-y-1/2 opacity-85" />
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem key="avg" value="avg">
|
||||
{t('monitor.average')}
|
||||
{t("monitor.average")}
|
||||
</SelectItem>
|
||||
<SelectItem key="max" value="max">
|
||||
{t('monitor.max_1_min')}
|
||||
{t("monitor.max_1_min")}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
@@ -616,24 +590,17 @@ function ChartCard({
|
||||
description: string
|
||||
children: React.ReactNode
|
||||
grid?: boolean
|
||||
empty?: boolean,
|
||||
empty?: boolean
|
||||
cornerEl?: JSX.Element | null
|
||||
}) {
|
||||
const { isIntersecting, ref } = useIntersectionObserver()
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={cn('pb-2 sm:pb-4 odd:last-of-type:col-span-full', { 'col-span-full': !grid })}
|
||||
ref={ref}
|
||||
>
|
||||
<Card className={cn("pb-2 sm:pb-4 odd:last-of-type:col-span-full", { "col-span-full": !grid })} ref={ref}>
|
||||
<CardHeader className="pb-5 pt-4 relative space-y-1 max-sm:py-3 max-sm:px-4">
|
||||
<CardTitle className="text-xl sm:text-2xl">{title}</CardTitle>
|
||||
<CardDescription>{description}</CardDescription>
|
||||
{cornerEl && (
|
||||
<div className="relative py-1 block sm:w-44 sm:absolute sm:top-2.5 sm:right-3.5">
|
||||
{cornerEl}
|
||||
</div>
|
||||
)}
|
||||
{cornerEl && <div className="relative py-1 block sm:w-44 sm:absolute sm:top-2.5 sm:right-3.5">{cornerEl}</div>}
|
||||
</CardHeader>
|
||||
<div className="pl-0 w-[calc(100%-1.6em)] h-52 relative">
|
||||
{<Spinner empty={empty} />}
|
||||
|
||||
Reference in New Issue
Block a user