diff --git a/beszel/site/src/components/add-system.tsx b/beszel/site/src/components/add-system.tsx
index b836442..4fba273 100644
--- a/beszel/site/src/components/add-system.tsx
+++ b/beszel/site/src/components/add-system.tsx
@@ -13,8 +13,9 @@ import {
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
-import { $publicKey, pb } from "@/lib/stores"
-import { cn, generateToken, isReadOnlyUser, tokenMap, useLocalStorage } from "@/lib/utils"
+import { $publicKey } from "@/lib/stores"
+import { cn, generateToken, tokenMap, useLocalStorage } from "@/lib/utils"
+import { pb, isReadOnlyUser } from "@/lib/api"
import { useStore } from "@nanostores/react"
import { ChevronDownIcon, ExternalLinkIcon, PlusIcon } from "lucide-react"
import { memo, useEffect, useRef, useState } from "react"
diff --git a/beszel/site/src/components/alerts/alerts-sheet.tsx b/beszel/site/src/components/alerts/alerts-sheet.tsx
index 3f2d3dc..24539a7 100644
--- a/beszel/site/src/components/alerts/alerts-sheet.tsx
+++ b/beszel/site/src/components/alerts/alerts-sheet.tsx
@@ -1,6 +1,6 @@
import { t } from "@lingui/core/macro"
import { Trans, Plural } from "@lingui/react/macro"
-import { $alerts, $systems, pb } from "@/lib/stores"
+import { $alerts, $systems } from "@/lib/stores"
import { cn, debounce } from "@/lib/utils"
import { alertInfo } from "@/lib/alerts"
import { Switch } from "@/components/ui/switch"
@@ -15,6 +15,7 @@ import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"
import { ServerIcon, GlobeIcon } from "lucide-react"
import { $router, Link } from "@/components/router"
import { DialogHeader } from "@/components/ui/dialog"
+import { pb } from "@/lib/api"
const Slider = lazy(() => import("@/components/ui/slider"))
diff --git a/beszel/site/src/components/command-palette.tsx b/beszel/site/src/components/command-palette.tsx
index 958398b..eca1dd6 100644
--- a/beszel/site/src/components/command-palette.tsx
+++ b/beszel/site/src/components/command-palette.tsx
@@ -23,11 +23,13 @@ import {
} from "@/components/ui/command"
import { memo, useEffect, useMemo } from "react"
import { $systems } from "@/lib/stores"
-import { getHostDisplayValue, isAdmin, listen } from "@/lib/utils"
+import { getHostDisplayValue, listen } from "@/lib/utils"
import { $router, basePath, navigate, prependBasePath } from "./router"
import { Trans } from "@lingui/react/macro"
import { t } from "@lingui/core/macro"
import { getPagePath } from "@nanostores/router"
+import { DialogDescription } from "@radix-ui/react-dialog"
+import { isAdmin } from "@/lib/api"
export default memo(function CommandPalette({ open, setOpen }: { open: boolean; setOpen: (open: boolean) => void }) {
useEffect(() => {
@@ -54,11 +56,9 @@ export default memo(function CommandPalette({ open, setOpen }: { open: boolean;
)
return (
+ Command palette
-
- No results found.
-
{systems.length > 0 && (
<>
@@ -214,6 +214,9 @@ export default memo(function CommandPalette({ open, setOpen }: { open: boolean;
>
)}
+
+ No results found.
+
)
diff --git a/beszel/site/src/components/login/auth-form.tsx b/beszel/site/src/components/login/auth-form.tsx
index 2859af6..c17b79f 100644
--- a/beszel/site/src/components/login/auth-form.tsx
+++ b/beszel/site/src/components/login/auth-form.tsx
@@ -5,7 +5,7 @@ import { buttonVariants } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { LoaderCircle, LockIcon, LogInIcon, MailIcon } from "lucide-react"
-import { $authenticated, pb } from "@/lib/stores"
+import { $authenticated } from "@/lib/stores"
import * as v from "valibot"
import { toast } from "../ui/use-toast"
import { Dialog, DialogContent, DialogTrigger, DialogHeader, DialogTitle } from "@/components/ui/dialog"
@@ -13,6 +13,7 @@ import { useCallback, useEffect, useState } from "react"
import { AuthMethodsList, AuthProviderInfo, OAuth2AuthConfig } from "pocketbase"
import { $router, Link, prependBasePath } from "../router"
import { getPagePath } from "@nanostores/router"
+import { pb } from "@/lib/api"
const honeypot = v.literal("")
const emailSchema = v.pipe(v.string(), v.email(t`Invalid email address.`))
diff --git a/beszel/site/src/components/login/forgot-pass-form.tsx b/beszel/site/src/components/login/forgot-pass-form.tsx
index a4ea52d..8f928d7 100644
--- a/beszel/site/src/components/login/forgot-pass-form.tsx
+++ b/beszel/site/src/components/login/forgot-pass-form.tsx
@@ -1,5 +1,5 @@
-import { Trans } from "@lingui/react/macro";
-import { t } from "@lingui/core/macro";
+import { Trans } from "@lingui/react/macro"
+import { t } from "@lingui/core/macro"
import { LoaderCircle, MailIcon, SendHorizonalIcon } from "lucide-react"
import { Input } from "../ui/input"
import { Label } from "../ui/label"
@@ -7,9 +7,9 @@ import { useCallback, useState } from "react"
import { toast } from "../ui/use-toast"
import { buttonVariants } from "../ui/button"
import { cn } from "@/lib/utils"
-import { pb } from "@/lib/stores"
import { Dialog, DialogHeader } from "../ui/dialog"
import { DialogContent, DialogTrigger, DialogTitle } from "../ui/dialog"
+import { pb } from "@/lib/api"
const showLoginFaliedToast = () => {
toast({
diff --git a/beszel/site/src/components/login/login.tsx b/beszel/site/src/components/login/login.tsx
index d4c25fb..6103efd 100644
--- a/beszel/site/src/components/login/login.tsx
+++ b/beszel/site/src/components/login/login.tsx
@@ -1,13 +1,13 @@
-import { t } from "@lingui/core/macro";
+import { t } from "@lingui/core/macro"
import { UserAuthForm } from "@/components/login/auth-form"
import { Logo } from "../logo"
import { useEffect, useMemo, useState } from "react"
-import { pb } from "@/lib/stores"
import { useStore } from "@nanostores/react"
import ForgotPassword from "./forgot-pass-form"
import { $router } from "../router"
import { AuthMethodsList } from "pocketbase"
import { useTheme } from "../theme-provider"
+import { pb } from "@/lib/api"
export default function () {
const page = useStore($router)
diff --git a/beszel/site/src/components/navbar.tsx b/beszel/site/src/components/navbar.tsx
index 156c25b..0b2e81a 100644
--- a/beszel/site/src/components/navbar.tsx
+++ b/beszel/site/src/components/navbar.tsx
@@ -15,8 +15,8 @@ import { $router, basePath, Link, prependBasePath } from "./router"
import { LangToggle } from "./lang-toggle"
import { ModeToggle } from "./mode-toggle"
import { Logo } from "./logo"
-import { pb } from "@/lib/stores"
-import { cn, isReadOnlyUser, isAdmin, logOut } from "@/lib/utils"
+import { cn, runOnce } from "@/lib/utils"
+import { isReadOnlyUser, isAdmin, logOut, pb } from "@/lib/api"
import {
DropdownMenu,
DropdownMenuTrigger,
@@ -36,12 +36,17 @@ const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0
export default function Navbar() {
return (
-
+
import("@/components/routes/home"))}
+ >
-
+
import("@/components/routes/settings/general")}>
import("../systems-table/systems-table"))
+// const SystemsTable = lazy(() => import("../systems-table/systems-table"))
export default memo(function () {
const { t } = useLingui()
diff --git a/beszel/site/src/components/routes/settings/alerts-history-data-table.tsx b/beszel/site/src/components/routes/settings/alerts-history-data-table.tsx
index 30bca2b..1b85e46 100644
--- a/beszel/site/src/components/routes/settings/alerts-history-data-table.tsx
+++ b/beszel/site/src/components/routes/settings/alerts-history-data-table.tsx
@@ -1,4 +1,4 @@
-import { pb } from "@/lib/stores"
+import { pb } from "@/lib/api"
import { cn, formatDuration, formatShortDate } from "@/lib/utils"
import { alertInfo } from "@/lib/alerts"
import { AlertsHistoryRecord } from "@/types"
diff --git a/beszel/site/src/components/routes/settings/config-yaml.tsx b/beszel/site/src/components/routes/settings/config-yaml.tsx
index e115d79..2a3b8d8 100644
--- a/beszel/site/src/components/routes/settings/config-yaml.tsx
+++ b/beszel/site/src/components/routes/settings/config-yaml.tsx
@@ -1,17 +1,16 @@
import { t } from "@lingui/core/macro"
import { Trans } from "@lingui/react/macro"
-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 { isAdmin, pb } from "@/lib/api"
export default function ConfigYaml() {
const [configContent, setConfigContent] = useState
("")
diff --git a/beszel/site/src/components/routes/settings/layout.tsx b/beszel/site/src/components/routes/settings/layout.tsx
index d264cc8..e7e757e 100644
--- a/beszel/site/src/components/routes/settings/layout.tsx
+++ b/beszel/site/src/components/routes/settings/layout.tsx
@@ -1,6 +1,6 @@
import { t } from "@lingui/core/macro"
import { Trans } from "@lingui/react/macro"
-import { useEffect } from "react"
+import { lazy, 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"
@@ -8,15 +8,23 @@ import { useStore } from "@nanostores/react"
import { $router } from "@/components/router.tsx"
import { getPagePath, redirectPage } from "@nanostores/router"
import { BellIcon, FileSlidersIcon, FingerprintIcon, SettingsIcon, AlertOctagonIcon } from "lucide-react"
-import { $userSettings, pb } from "@/lib/stores.ts"
+import { $userSettings } from "@/lib/stores.ts"
import { toast } from "@/components/ui/use-toast.ts"
import { UserSettings } from "@/types"
-import General from "./general.tsx"
-import Notifications from "./notifications.tsx"
-import ConfigYaml from "./config-yaml.tsx"
import { useLingui } from "@lingui/react/macro"
-import Fingerprints from "./tokens-fingerprints.tsx"
-import AlertsHistoryDataTable from "./alerts-history-data-table"
+import { pb } from "@/lib/api"
+
+const generalSettingsImport = () => import("./general.tsx")
+const notificationsSettingsImport = () => import("./notifications.tsx")
+const configYamlSettingsImport = () => import("./config-yaml.tsx")
+const fingerprintsSettingsImport = () => import("./tokens-fingerprints.tsx")
+const alertsHistoryDataTableSettingsImport = () => import("./alerts-history-data-table.tsx")
+
+const GeneralSettings = lazy(generalSettingsImport)
+const NotificationsSettings = lazy(notificationsSettingsImport)
+const ConfigYamlSettings = lazy(configYamlSettingsImport)
+const FingerprintsSettings = lazy(fingerprintsSettingsImport)
+const AlertsHistoryDataTableSettings = lazy(alertsHistoryDataTableSettingsImport)
export async function saveSettings(newSettings: Partial) {
try {
@@ -59,23 +67,27 @@ export default function SettingsLayout() {
title: t`Notifications`,
href: getPagePath($router, "settings", { name: "notifications" }),
icon: BellIcon,
+ preload: notificationsSettingsImport,
},
{
title: t`Tokens & Fingerprints`,
href: getPagePath($router, "settings", { name: "tokens" }),
icon: FingerprintIcon,
noReadOnly: true,
+ preload: fingerprintsSettingsImport,
},
{
title: t`Alert History`,
href: getPagePath($router, "settings", { name: "alert-history" }),
icon: AlertOctagonIcon,
+ preload: alertsHistoryDataTableSettingsImport,
},
{
title: t`YAML Config`,
href: getPagePath($router, "settings", { name: "config" }),
icon: FileSlidersIcon,
admin: true,
+ preload: configYamlSettingsImport,
},
]
@@ -120,14 +132,14 @@ function SettingsContent({ name }: { name: string }) {
switch (name) {
case "general":
- return
+ return
case "notifications":
- return
+ return
case "config":
- return
+ return
case "tokens":
- return
+ return
case "alert-history":
- return
+ return
}
}
diff --git a/beszel/site/src/components/routes/settings/notifications.tsx b/beszel/site/src/components/routes/settings/notifications.tsx
index d90e7f4..9bc3082 100644
--- a/beszel/site/src/components/routes/settings/notifications.tsx
+++ b/beszel/site/src/components/routes/settings/notifications.tsx
@@ -3,7 +3,6 @@ import { Trans } from "@lingui/react/macro"
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"
@@ -13,8 +12,8 @@ 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 { prependBasePath } from "@/components/router"
+import { isAdmin, pb } from "@/lib/api"
interface ShoutrrrUrlCardProps {
url: string
diff --git a/beszel/site/src/components/routes/settings/sidebar-nav.tsx b/beszel/site/src/components/routes/settings/sidebar-nav.tsx
index ed0663e..f77f5b9 100644
--- a/beszel/site/src/components/routes/settings/sidebar-nav.tsx
+++ b/beszel/site/src/components/routes/settings/sidebar-nav.tsx
@@ -1,5 +1,6 @@
import React from "react"
-import { cn, isAdmin, isReadOnlyUser } from "@/lib/utils"
+import { cn } from "@/lib/utils"
+import { isAdmin, isReadOnlyUser } from "@/lib/api"
import { buttonVariants } from "../../ui/button"
import { $router, Link, navigate } from "../../router"
import { useStore } from "@nanostores/react"
@@ -13,6 +14,7 @@ interface SidebarNavProps extends React.HTMLAttributes {
icon?: React.FC>
admin?: boolean
noReadOnly?: boolean
+ preload?: () => Promise<{ default: React.ComponentType }>
}[]
}
@@ -52,6 +54,7 @@ export function SidebarNav({ className, items, ...props }: SidebarNavProps) {
}
return (
item.preload?.()}
key={item.href}
href={item.href}
className={cn(
diff --git a/beszel/site/src/components/routes/settings/tokens-fingerprints.tsx b/beszel/site/src/components/routes/settings/tokens-fingerprints.tsx
index 678a02f..d5d88b9 100644
--- a/beszel/site/src/components/routes/settings/tokens-fingerprints.tsx
+++ b/beszel/site/src/components/routes/settings/tokens-fingerprints.tsx
@@ -1,6 +1,6 @@
import { Trans, useLingui } from "@lingui/react/macro"
import { t } from "@lingui/core/macro"
-import { $publicKey, pb } from "@/lib/stores"
+import { $publicKey } from "@/lib/stores"
import { memo, useEffect, useMemo, useState } from "react"
import { Table, TableCell, TableHead, TableBody, TableRow, TableHeader } from "@/components/ui/table"
import { FingerprintRecord } from "@/types"
@@ -14,7 +14,8 @@ import {
Trash2Icon,
} from "lucide-react"
import { toast } from "@/components/ui/use-toast"
-import { cn, copyToClipboard, generateToken, getHubURL, isReadOnlyUser, tokenMap } from "@/lib/utils"
+import { cn, copyToClipboard, generateToken, getHubURL, tokenMap } from "@/lib/utils"
+import { isReadOnlyUser, pb } from "@/lib/api"
import {
DropdownMenu,
DropdownMenuContent,
diff --git a/beszel/site/src/components/routes/system.tsx b/beszel/site/src/components/routes/system.tsx
index cc07c65..1c8378d 100644
--- a/beszel/site/src/components/routes/system.tsx
+++ b/beszel/site/src/components/routes/system.tsx
@@ -1,8 +1,7 @@
import { t } from "@lingui/core/macro"
-import { Trans } from "@lingui/react/macro"
+import { Plural, Trans } from "@lingui/react/macro"
import {
$systems,
- pb,
$chartTime,
$containerFilter,
$userSettings,
@@ -12,7 +11,7 @@ import {
} from "@/lib/stores"
import { ChartData, ChartTimes, ContainerStatsRecord, GPUData, SystemRecord, SystemStatsRecord } from "@/types"
import { ChartType, Unit, Os, SystemStatus } from "@/lib/enums"
-import React, { lazy, memo, useCallback, useEffect, useMemo, useRef, useState, type JSX } from "react"
+import React, { memo, useCallback, useEffect, useMemo, useRef, useState, type JSX } from "react"
import { Card, CardHeader, CardTitle, CardDescription } from "../ui/card"
import { useStore } from "@nanostores/react"
import Spinner from "../spinner"
@@ -24,13 +23,12 @@ import {
decimalString,
formatBytes,
getHostDisplayValue,
- getPbTimestamp,
listen,
parseSemVer,
toFixedFloat,
useLocalStorage,
- formatUptimeString,
} from "@/lib/utils"
+import { getPbTimestamp, pb } from "@/lib/api"
import { Separator } from "../ui/separator"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip"
import { Button } from "../ui/button"
@@ -43,15 +41,14 @@ import { useLingui } from "@lingui/react/macro"
import { $router, navigate } from "../router"
import { getPagePath } from "@nanostores/router"
import { batteryStateTranslations } from "@/lib/i18n"
-
-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 GpuPowerChart = lazy(() => import("../charts/gpu-power-chart"))
-const LoadAverageChart = lazy(() => import("../charts/load-average-chart"))
+import AreaChartDefault from "@/components/charts/area-chart"
+import ContainerChart from "@/components/charts/container-chart"
+import MemChart from "@/components/charts/mem-chart"
+import DiskChart from "@/components/charts/disk-chart"
+import SwapChart from "@/components/charts/swap-chart"
+import TemperatureChart from "@/components/charts/temperature-chart"
+import GpuPowerChart from "@/components/charts/gpu-power-chart"
+import LoadAverageChart from "@/components/charts/load-average-chart"
const cache = new Map()
@@ -288,8 +285,16 @@ export default function SystemDetail({ name }: { name: string }) {
value: system.info.k,
},
}
-
- let uptime: React.ReactNode = formatUptimeString(system.info.u)
+ let uptime: React.ReactNode
+ if (system.info.u < 3600) {
+ const mins = Math.trunc(system.info.u / 60)
+ uptime =
+ } else if (system.info.u < 172800) {
+ const hours = Math.trunc(system.info.u / 3600)
+ uptime =
+ } else {
+ uptime =
+ }
return [
{ value: getHostDisplayValue(system), Icon: GlobeIcon },
{
@@ -312,7 +317,7 @@ export default function SystemDetail({ name }: { name: string }) {
Icon: any
hide?: boolean
}[]
- }, [system.info])
+ }, [system.info, t])
/** Space for tooltip if more than 12 containers */
useEffect(() => {
diff --git a/beszel/site/src/components/systems-table/systems-table-columns.tsx b/beszel/site/src/components/systems-table/systems-table-columns.tsx
index 03e5d52..052ef5f 100644
--- a/beszel/site/src/components/systems-table/systems-table-columns.tsx
+++ b/beszel/site/src/components/systems-table/systems-table-columns.tsx
@@ -23,12 +23,11 @@ import {
formatBytes,
formatTemperature,
getMeterState,
- isReadOnlyUser,
parseSemVer,
} from "@/lib/utils"
import { EthernetIcon, GpuIcon, HourglassIcon, ThermometerIcon } from "../ui/icons"
import { useStore } from "@nanostores/react"
-import { $userSettings, pb } from "@/lib/stores"
+import { $userSettings } from "@/lib/stores"
import { Trans, useLingui } from "@lingui/react/macro"
import { useMemo, useRef, useState } from "react"
import { memo } from "react"
@@ -57,6 +56,7 @@ import { t } from "@lingui/core/macro"
import { MeterState, SystemStatus } from "@/lib/enums"
import { $router, Link } from "../router"
import { getPagePath } from "@nanostores/router"
+import { isReadOnlyUser, pb } from "@/lib/api"
const STATUS_COLORS = {
[SystemStatus.Up]: "bg-green-500",
diff --git a/beszel/site/src/components/systems-table/systems-table.tsx b/beszel/site/src/components/systems-table/systems-table.tsx
index ebe1596..77fb5a3 100644
--- a/beszel/site/src/components/systems-table/systems-table.tsx
+++ b/beszel/site/src/components/systems-table/systems-table.tsx
@@ -11,11 +11,8 @@ import {
Row,
Table as TableType,
} from "@tanstack/react-table"
-
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
-
import { Button } from "@/components/ui/button"
-
import {
DropdownMenu,
DropdownMenuCheckboxItem,
@@ -41,7 +38,7 @@ import {
import { memo, useEffect, useMemo, useState } from "react"
import { $systems } from "@/lib/stores"
import { useStore } from "@nanostores/react"
-import { cn, useLocalStorage } from "@/lib/utils"
+import { cn, runOnce, useLocalStorage } from "@/lib/utils"
import { $router, Link } from "../router"
import { useLingui, Trans } from "@lingui/react/macro"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"
@@ -54,6 +51,8 @@ import { SystemStatus } from "@/lib/enums"
type ViewMode = "table" | "grid"
type StatusFilter = "all" | "up" | "down" | "paused"
+const preloadSystemDetail = runOnce(() => import("@/components/routes/system.tsx"))
+
export default function SystemsTable() {
const data = useStore($systems)
const { i18n, t } = useLingui()
@@ -283,7 +282,7 @@ const AllSystemsTable = memo(
return (
-
+
{rows.length ? (
rows.map((row) => (
@@ -359,6 +358,7 @@ const SystemCard = memo(
return useMemo(() => {
return (
= {
diff --git a/beszel/site/src/lib/api.ts b/beszel/site/src/lib/api.ts
new file mode 100644
index 0000000..1d16ee7
--- /dev/null
+++ b/beszel/site/src/lib/api.ts
@@ -0,0 +1,116 @@
+import { ChartTimes, SystemRecord, UserSettings } from "@/types"
+import { $alerts, $systems, $userSettings } from "./stores"
+import { toast } from "@/components/ui/use-toast"
+import { t } from "@lingui/core/macro"
+import { chartTimeData } from "./utils"
+import { WritableAtom } from "nanostores"
+import { RecordModel, RecordSubscription } from "pocketbase"
+import PocketBase from "pocketbase"
+import { basePath } from "@/components/router"
+
+/** PocketBase JS Client */
+export const pb = new PocketBase(basePath)
+
+export const isAdmin = () => pb.authStore.record?.role === "admin"
+export const isReadOnlyUser = () => pb.authStore.record?.role === "readonly"
+
+const verifyAuth = () => {
+ pb.collection("users")
+ .authRefresh()
+ .catch(() => {
+ logOut()
+ toast({
+ title: t`Failed to authenticate`,
+ description: t`Please log in again`,
+ variant: "destructive",
+ })
+ })
+}
+
+/** Logs the user out by clearing the auth store and unsubscribing from realtime updates. */
+export async function logOut() {
+ $systems.set([])
+ $alerts.set({})
+ $userSettings.set({} as UserSettings)
+ sessionStorage.setItem("lo", "t") // prevent auto login on logout
+ pb.authStore.clear()
+ pb.realtime.unsubscribe()
+}
+
+/** Fetch or create user settings in database */
+export async function updateUserSettings() {
+ try {
+ const req = await pb.collection("user_settings").getFirstListItem("", { fields: "settings" })
+ $userSettings.set(req.settings)
+ return
+ } catch (e) {
+ console.error("get settings", e)
+ }
+ // create user settings if error fetching existing
+ try {
+ const createdSettings = await pb.collection("user_settings").create({ user: pb.authStore.record!.id })
+ $userSettings.set(createdSettings.settings)
+ } catch (e) {
+ console.error("create settings", e)
+ }
+}
+/** Update systems / alerts list when records change */
+export function updateRecordList(e: RecordSubscription, $store: WritableAtom) {
+ const curRecords = $store.get()
+ const newRecords = []
+ if (e.action === "delete") {
+ for (const server of curRecords) {
+ if (server.id !== e.record.id) {
+ newRecords.push(server)
+ }
+ }
+ } else {
+ let found = 0
+ for (const server of curRecords) {
+ if (server.id === e.record.id) {
+ found = newRecords.push(e.record)
+ } else {
+ newRecords.push(server)
+ }
+ }
+ if (!found) {
+ newRecords.push(e.record)
+ }
+ }
+ $store.set(newRecords)
+}
+/** Fetches updated system list from database */
+export const updateSystemList = (() => {
+ let isFetchingSystems = false
+ return async () => {
+ if (isFetchingSystems) {
+ return
+ }
+ isFetchingSystems = true
+ try {
+ const records = await pb
+ .collection("systems")
+ .getFullList({ sort: "+name", fields: "id,name,host,port,info,status" })
+
+ if (records.length) {
+ $systems.set(records)
+ } else {
+ verifyAuth()
+ }
+ } finally {
+ isFetchingSystems = false
+ }
+ }
+})()
+
+export function getPbTimestamp(timeString: ChartTimes, d?: Date) {
+ d ||= chartTimeData[timeString].getOffset(new Date())
+ const year = d.getUTCFullYear()
+ const month = String(d.getUTCMonth() + 1).padStart(2, "0")
+ const day = String(d.getUTCDate()).padStart(2, "0")
+ const hours = String(d.getUTCHours()).padStart(2, "0")
+ const minutes = String(d.getUTCMinutes()).padStart(2, "0")
+ const seconds = String(d.getUTCSeconds()).padStart(2, "0")
+
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
+}
diff --git a/beszel/site/src/lib/stores.ts b/beszel/site/src/lib/stores.ts
index 97da3b8..9a0ed48 100644
--- a/beszel/site/src/lib/stores.ts
+++ b/beszel/site/src/lib/stores.ts
@@ -1,11 +1,7 @@
-import PocketBase from "pocketbase"
import { atom, map } from "nanostores"
import { AlertMap, ChartTimes, SystemRecord, UserSettings } from "@/types"
-import { basePath } from "@/components/router"
import { Unit } from "./enums"
-
-/** PocketBase JS Client */
-export const pb = new PocketBase(basePath)
+import { pb } from "./api"
/** Store if user is authenticated */
export const $authenticated = atom(pb.authStore.isValid)
diff --git a/beszel/site/src/lib/utils.ts b/beszel/site/src/lib/utils.ts
index 3437708..03113fe 100644
--- a/beszel/site/src/lib/utils.ts
+++ b/beszel/site/src/lib/utils.ts
@@ -1,15 +1,13 @@
-import { t } from "@lingui/core/macro"
+import { plural, t } from "@lingui/core/macro"
import { toast } from "@/components/ui/use-toast"
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
-import { $alerts, $copyContent, $systems, $userSettings, pb } from "./stores"
-import type { ChartTimeData, ChartTimes, FingerprintRecord, SemVer, SystemRecord, UserSettings } from "@/types"
-import { RecordModel, RecordSubscription } from "pocketbase"
-import { WritableAtom } from "nanostores"
+import { $copyContent, $systems, $userSettings } from "./stores"
+import type { ChartTimeData, FingerprintRecord, SemVer, SystemRecord } from "@/types"
import { timeDay, timeHour } from "d3-time"
import { useEffect, useState } from "react"
-import { prependBasePath } from "@/components/router"
import { MeterState, Unit } from "./enums"
+import { prependBasePath } from "@/components/router"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
@@ -34,52 +32,6 @@ export async function copyToClipboard(content: string) {
}
}
-const verifyAuth = () => {
- pb.collection("users")
- .authRefresh()
- .catch(() => {
- logOut()
- toast({
- title: t`Failed to authenticate`,
- description: t`Please log in again`,
- variant: "destructive",
- })
- })
-}
-
-export const updateSystemList = (() => {
- let isFetchingSystems = false
- return async () => {
- if (isFetchingSystems) {
- return
- }
- isFetchingSystems = true
- try {
- const records = await pb
- .collection("systems")
- .getFullList({ sort: "+name", fields: "id,name,host,port,info,status,updated" })
-
- if (records.length) {
- $systems.set(records)
- } else {
- verifyAuth()
- }
- } finally {
- isFetchingSystems = false
- }
- }
-})()
-
-/** Logs the user out by clearing the auth store and unsubscribing from realtime updates. */
-export async function logOut() {
- $systems.set([])
- $alerts.set({})
- $userSettings.set({} as UserSettings)
- sessionStorage.setItem("lo", "t") // prevent auto login on logout
- pb.authStore.clear()
- pb.realtime.unsubscribe()
-}
-
const hourWithMinutesFormatter = new Intl.DateTimeFormat(undefined, {
hour: "numeric",
minute: "numeric",
@@ -110,47 +62,6 @@ export const updateFavicon = (newIcon: string) => {
;(document.querySelector("link[rel='icon']") as HTMLLinkElement).href = prependBasePath(`/static/${newIcon}`)
}
-export const isAdmin = () => pb.authStore.record?.role === "admin"
-export const isReadOnlyUser = () => pb.authStore.record?.role === "readonly"
-
-/** Update systems / alerts list when records change */
-export function updateRecordList(e: RecordSubscription, $store: WritableAtom) {
- const curRecords = $store.get()
- const newRecords = []
- if (e.action === "delete") {
- for (const server of curRecords) {
- if (server.id !== e.record.id) {
- newRecords.push(server)
- }
- }
- } else {
- let found = 0
- for (const server of curRecords) {
- if (server.id === e.record.id) {
- found = newRecords.push(e.record)
- } else {
- newRecords.push(server)
- }
- }
- if (!found) {
- newRecords.push(e.record)
- }
- }
- $store.set(newRecords)
-}
-
-export function getPbTimestamp(timeString: ChartTimes, d?: Date) {
- d ||= chartTimeData[timeString].getOffset(new Date())
- const year = d.getUTCFullYear()
- const month = String(d.getUTCMonth() + 1).padStart(2, "0")
- const day = String(d.getUTCDate()).padStart(2, "0")
- const hours = String(d.getUTCHours()).padStart(2, "0")
- const minutes = String(d.getUTCMinutes()).padStart(2, "0")
- const seconds = String(d.getUTCSeconds()).padStart(2, "0")
-
- return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
-}
-
export const chartTimeData: ChartTimeData = {
"1h": {
type: "1m",
@@ -329,24 +240,6 @@ export function formatBytes(
}
}
-/** Fetch or create user settings in database */
-export async function updateUserSettings() {
- try {
- const req = await pb.collection("user_settings").getFirstListItem("", { fields: "settings" })
- $userSettings.set(req.settings)
- return
- } catch (e) {
- console.error("get settings", e)
- }
- // create user settings if error fetching existing
- try {
- const createdSettings = await pb.collection("user_settings").create({ user: pb.authStore.record!.id })
- $userSettings.set(createdSettings.settings)
- } catch (e) {
- console.error("create settings", e)
- }
-}
-
export const chartMargin = { top: 12 }
/**
@@ -357,19 +250,20 @@ export const chartMargin = { top: 12 }
*/
export const getHostDisplayValue = (system: SystemRecord): string => system.host.slice(system.host.lastIndexOf("/") + 1)
-export function formatUptimeString(uptimeSeconds: number): string {
- if (!uptimeSeconds || isNaN(uptimeSeconds)) return "";
- if (uptimeSeconds < 3600) {
- const mins = Math.trunc(uptimeSeconds / 60);
- return mins === 1 ? "1 minute" : `${mins} minutes`;
- } else if (uptimeSeconds < 172800) {
- const hours = Math.trunc(uptimeSeconds / 3600);
- return hours === 1 ? "1 hour" : `${hours} hours`;
- } else {
- const days = Math.trunc(uptimeSeconds / 86400);
- return days === 1 ? "1 day" : `${days} days`;
- }
-}
+// export function formatUptimeString(uptimeSeconds: number): string {
+// if (!uptimeSeconds || isNaN(uptimeSeconds)) return ""
+// if (uptimeSeconds < 3600) {
+// const minutes = Math.trunc(uptimeSeconds / 60)
+// return plural({ minutes }, { one: "# minute", other: "# minutes" })
+// } else if (uptimeSeconds < 172800) {
+// const hours = Math.trunc(uptimeSeconds / 3600)
+// console.log(hours)
+// return plural({ hours }, { one: "# hour", other: "# hours" })
+// } else {
+// const days = Math.trunc(uptimeSeconds / 86400)
+// return plural({ days }, { one: "# day", other: "# days" })
+// }
+// }
/** Generate a random token for the agent */
export const generateToken = () => {
@@ -462,3 +356,16 @@ export const getSystemNameFromId = (() => {
return sysName
}
})()
+
+/** Run a function only once */
+export function runOnce any>(fn: T): (...args: Parameters) => ReturnType {
+ let done = false
+ let result: any
+ return (...args: any) => {
+ if (!done) {
+ result = fn(...args)
+ done = true
+ }
+ return result
+ }
+}
diff --git a/beszel/site/src/main.tsx b/beszel/site/src/main.tsx
index af3def2..56a0c10 100644
--- a/beszel/site/src/main.tsx
+++ b/beszel/site/src/main.tsx
@@ -2,26 +2,26 @@ import "./index.css"
// import { Suspense, lazy, useEffect, StrictMode } from "react"
import { Suspense, lazy, memo, useEffect } from "react"
import ReactDOM from "react-dom/client"
-import Home from "./components/routes/home.tsx"
import { ThemeProvider } from "./components/theme-provider.tsx"
import { DirectionProvider } from "@radix-ui/react-direction"
-import { $authenticated, $systems, pb, $publicKey, $copyContent, $direction } from "./lib/stores.ts"
-import { updateUserSettings, updateFavicon, updateSystemList } from "./lib/utils.ts"
+import { $authenticated, $systems, $publicKey, $copyContent, $direction } from "./lib/stores.ts"
+import { pb, updateSystemList, updateUserSettings } from "./lib/api.ts"
import { useStore } from "@nanostores/react"
import { Toaster } from "./components/ui/toaster.tsx"
import { $router } from "./components/router.tsx"
-import SystemDetail from "./components/routes/system.tsx"
+import { updateFavicon } from "@/lib/utils"
import Navbar from "./components/navbar.tsx"
import { I18nProvider } from "@lingui/react"
import { i18n } from "@lingui/core"
import { getLocale, dynamicActivate } from "./lib/i18n"
import { SystemStatus } from "./lib/enums"
import { alertManager } from "./lib/alerts"
+import Settings from "./components/routes/settings/layout.tsx"
-// const ServerDetail = lazy(() => import('./components/routes/system.tsx'))
-const LoginPage = lazy(() => import("./components/login/login.tsx"))
-const CopyToClipboardDialog = lazy(() => import("./components/copy-to-clipboard.tsx"))
-const Settings = lazy(() => import("./components/routes/settings/layout.tsx"))
+const LoginPage = lazy(() => import("@/components/login/login.tsx"))
+const Home = lazy(() => import("@/components/routes/home.tsx"))
+const SystemDetail = lazy(() => import("@/components/routes/system.tsx"))
+const CopyToClipboardDialog = lazy(() => import("@/components/copy-to-clipboard.tsx"))
const App = memo(() => {
const page = useStore($router)
@@ -78,11 +78,7 @@ const App = memo(() => {
} else if (page.route === "system") {
return
} else if (page.route === "settings") {
- return (
-
-
-
- )
+ return
}
})