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