diff --git a/beszel/site/src/components/command-palette.tsx b/beszel/site/src/components/command-palette.tsx index 0f76e6a..fdd76ff 100644 --- a/beszel/site/src/components/command-palette.tsx +++ b/beszel/site/src/components/command-palette.tsx @@ -19,17 +19,15 @@ import { CommandSeparator, CommandShortcut, } from "@/components/ui/command" -import { useEffect } from "react" -import { useStore } from "@nanostores/react" +import { memo, useEffect, useMemo } from "react" import { $systems } from "@/lib/stores" import { getHostDisplayValue, isAdmin, listen } from "@/lib/utils" import { $router, basePath, navigate } from "./router" -import { Trans, t } from "@lingui/macro" +import { Trans } from "@lingui/react/macro" +import { t } from "@lingui/core/macro" import { getPagePath } from "@nanostores/router" -export default function CommandPalette({ open, setOpen }: { open: boolean; setOpen: (open: boolean) => void }) { - const systems = useStore($systems) - +export default memo(function CommandPalette({ open, setOpen }: { open: boolean; setOpen: (open: boolean) => void }) { useEffect(() => { const down = (e: KeyboardEvent) => { if (e.key === "k" && (e.metaKey || e.ctrlKey)) { @@ -40,157 +38,160 @@ export default function CommandPalette({ open, setOpen }: { open: boolean; setOp return listen(document, "keydown", down) }, [open, setOpen]) - return ( - - - - - No results found. - - {systems.length > 0 && ( - <> - - {systems.map((system) => ( + return useMemo(() => { + const systems = $systems.get() + return ( + + + + + No results found. + + {systems.length > 0 && ( + <> + + {systems.map((system) => ( + { + navigate(getPagePath($router, "system", { name: system.name })) + setOpen(false) + }} + > + + {system.name} + {getHostDisplayValue(system)} + + ))} + + + + )} + + { + navigate(basePath) + setOpen(false) + }} + > + + + Dashboard + + + Page + + + { + navigate(getPagePath($router, "settings", { name: "general" })) + setOpen(false) + }} + > + + + Settings + + + Settings + + + { + navigate(getPagePath($router, "settings", { name: "notifications" })) + setOpen(false) + }} + > + + + Notifications + + + Settings + + + { + window.location.href = "https://beszel.dev/guide/what-is-beszel" + }} + > + + + Documentation + + beszel.dev + + + {isAdmin() && ( + <> + + { - navigate(getPagePath($router, "system", { name: system.name })) setOpen(false) + window.open("/_/", "_blank") }} > - - {system.name} - {getHostDisplayValue(system)} + + + Users + + + Admin + - ))} - - - - )} - - { - navigate(basePath) - setOpen(false) - }} - > - - - Dashboard - - - Page - - - { - navigate(getPagePath($router, "settings", { name: "general" })) - setOpen(false) - }} - > - - - Settings - - - Settings - - - { - navigate(getPagePath($router, "settings", { name: "notifications" })) - setOpen(false) - }} - > - - - Notifications - - - Settings - - - { - window.location.href = "https://beszel.dev/guide/what-is-beszel" - }} - > - - - Documentation - - beszel.dev - - - {isAdmin() && ( - <> - - - { - setOpen(false) - window.open("/_/", "_blank") - }} - > - - - Users - - - Admin - - - { - setOpen(false) - window.open("/_/#/logs", "_blank") - }} - > - - - Logs - - - Admin - - - { - setOpen(false) - window.open("/_/#/settings/backups", "_blank") - }} - > - - - Backups - - - Admin - - - { - setOpen(false) - window.open("/_/#/settings/mail", "_blank") - }} - > - - - SMTP settings - - - Admin - - - - - )} - - - ) -} + { + setOpen(false) + window.open("/_/#/logs", "_blank") + }} + > + + + Logs + + + Admin + + + { + setOpen(false) + window.open("/_/#/settings/backups", "_blank") + }} + > + + + Backups + + + Admin + + + { + setOpen(false) + window.open("/_/#/settings/mail", "_blank") + }} + > + + + SMTP settings + + + Admin + + + + + )} + + + ) + }, [open]) +}) diff --git a/beszel/site/src/components/routes/home.tsx b/beszel/site/src/components/routes/home.tsx index f38e1fb..a2b7776 100644 --- a/beszel/site/src/components/routes/home.tsx +++ b/beszel/site/src/components/routes/home.tsx @@ -1,6 +1,6 @@ -import { Suspense, lazy, useEffect, useMemo } from "react" +import { Suspense, lazy, memo, useEffect, useMemo } from "react" import { Card, CardContent, CardHeader, CardTitle } from "../ui/card" -import { $alerts, $hubVersion, $systems, pb } from "@/lib/stores" +import { $alerts, $systems, pb } from "@/lib/stores" import { useStore } from "@nanostores/react" import { GithubIcon } from "lucide-react" import { Separator } from "../ui/separator" @@ -8,17 +8,17 @@ import { alertInfo, updateRecordList, updateSystemList } from "@/lib/utils" import { AlertRecord, SystemRecord } from "@/types" import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" import { $router, Link } from "../router" -import { Plural, t, Trans } from "@lingui/macro" +import { Plural, Trans, useLingui } from "@lingui/react/macro" import { getPagePath } from "@nanostores/router" const SystemsTable = lazy(() => import("../systems-table/systems-table")) -export default function Home() { - const hubVersion = useStore($hubVersion) - +export const Home = memo(() => { const alerts = useStore($alerts) const systems = useStore($systems) + const { t } = useLingui() + let alertsKey = "" const activeAlerts = useMemo(() => { const activeAlerts = alerts.filter((alert) => { const active = alert.triggered && alert.name in alertInfo @@ -26,14 +26,17 @@ export default function Home() { return false } alert.sysname = systems.find((system) => system.id === alert.system)?.name + alertsKey += alert.id return true }) return activeAlerts - }, [alerts]) + }, [systems, alerts]) useEffect(() => { document.title = t`Dashboard` + " / Beszel" + }, [t]) + useEffect(() => { // make sure we have the latest list of systems updateSystemList() @@ -41,7 +44,6 @@ export default function Home() { pb.collection("systems").subscribe("*", (e) => { updateRecordList(e, $systems) }) - // todo: add toast if new triggered alert comes in pb.collection("alerts").subscribe("*", (e) => { updateRecordList(e, $alerts) }) @@ -51,56 +53,15 @@ export default function Home() { } }, []) - return ( - <> - {/* show active alerts */} - {activeAlerts.length > 0 && ( - - -
- - Active Alerts - -
-
- - {activeAlerts.length > 0 && ( -
- {activeAlerts.map((alert) => { - const info = alertInfo[alert.name as keyof typeof alertInfo] - return ( - - - - {alert.sysname} {info.name().toLowerCase().replace("cpu", "CPU")} - - - - Exceeds {alert.value} - {info.unit} in last - - - - - ) - })} -
- )} -
-
- )} - - - + return useMemo( + () => ( + <> + {/* show active alerts */} + {activeAlerts.length > 0 && } + + + - {hubVersion && (
- Beszel {hubVersion} + Beszel {globalThis.BESZEL.HUB_VERSION}
- )} - + + ), + [alertsKey] ) -} +}) + +const ActiveAlerts = memo(({ activeAlerts }: { activeAlerts: AlertRecord[] }) => { + return ( + + +
+ + Active Alerts + +
+
+ + {activeAlerts.length > 0 && ( +
+ {activeAlerts.map((alert) => { + const info = alertInfo[alert.name as keyof typeof alertInfo] + return ( + + + + {alert.sysname} {info.name().toLowerCase().replace("cpu", "CPU")} + + + + Exceeds {alert.value} + {info.unit} in last + + + + + ) + })} +
+ )} +
+
+ ) +}) diff --git a/beszel/site/src/components/systems-table/systems-table.tsx b/beszel/site/src/components/systems-table/systems-table.tsx index c7bae32..993aee6 100644 --- a/beszel/site/src/components/systems-table/systems-table.tsx +++ b/beszel/site/src/components/systems-table/systems-table.tsx @@ -10,6 +10,8 @@ import { getCoreRowModel, useReactTable, HeaderContext, + Row, + Table as TableType, } from "@tanstack/react-table" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" @@ -61,14 +63,13 @@ import { PenBoxIcon, } from "lucide-react" import { memo, useEffect, useMemo, useRef, useState } from "react" -import { $hubVersion, $systems, pb } from "@/lib/stores" +import { $systems, pb } from "@/lib/stores" import { useStore } from "@nanostores/react" import { cn, copyToClipboard, decimalString, isReadOnlyUser, useLocalStorage } from "@/lib/utils" import AlertsButton from "../alerts/alert-button" import { $router, Link, navigate } from "../router" import { EthernetIcon, GpuIcon, ThermometerIcon } from "../ui/icons" -import { Trans, t } from "@lingui/macro" -import { useLingui } from "@lingui/react" +import { useLingui, Trans } from "@lingui/react/macro" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card" import { Input } from "../ui/input" import { ClassValue } from "clsx" @@ -103,62 +104,66 @@ function CellFormatter(info: CellContext) { function sortableHeader(context: HeaderContext) { const { column } = context + // @ts-ignore + const { Icon, hideSort, name }: { Icon: React.ElementType; name: () => string; hideSort: boolean } = column.columnDef return ( ) } export default function SystemsTable() { const data = useStore($systems) - const hubVersion = useStore($hubVersion) + const { i18n, t } = useLingui() const [filter, setFilter] = useState() - const [sorting, setSorting] = useState([{ id: t`System`, desc: false }]) + const [sorting, setSorting] = useState([{ id: "system", desc: false }]) const [columnFilters, setColumnFilters] = useState([]) const [columnVisibility, setColumnVisibility] = useLocalStorage("cols", {}) const [viewMode, setViewMode] = useLocalStorage("viewMode", window.innerWidth > 1024 ? "table" : "grid") - const { i18n } = useLingui() + + const locale = i18n.locale useEffect(() => { if (filter !== undefined) { - table.getColumn(t`System`)?.setFilterValue(filter) + table.getColumn("system")?.setFilterValue(filter) } }, [filter]) - const columns = useMemo(() => { - // Create status translations for filtering + const columnDefs = useMemo(() => { const statusTranslations = { - "up": t`Up`.toLowerCase(), - "down": t`Down`.toLowerCase(), - "paused": t`Paused`.toLowerCase() - }; + up: () => t`Up`.toLowerCase(), + down: () => t`Down`.toLowerCase(), + paused: () => t`Paused`.toLowerCase(), + } return [ { // size: 200, size: 200, minSize: 0, accessorKey: "name", - id: t`System`, + id: "system", + name: () => t`System`, filterFn: (row, _, filterVal) => { - const filterLower = filterVal.toLowerCase(); - const { name, status } = row.original; + const filterLower = filterVal.toLowerCase() + const { name, status } = row.original // Check if the filter matches the name or status for this row - if (name.toLowerCase().includes(filterLower) || statusTranslations[status as keyof typeof statusTranslations]?.includes(filterLower)) { - return true; + if ( + name.toLowerCase().includes(filterLower) || + statusTranslations[status as keyof typeof statusTranslations]?.().includes(filterLower) + ) { + return true } - return false; + return false }, enableHiding: false, - icon: ServerIcon, + Icon: ServerIcon, cell: (info) => ( @@ -177,43 +182,48 @@ export default function SystemsTable() { }, { accessorKey: "info.cpu", - id: t`CPU`, + id: "cpu", + name: () => t`CPU`, invertSorting: true, cell: CellFormatter, - icon: CpuIcon, + Icon: CpuIcon, header: sortableHeader, }, { accessorKey: "info.mp", - id: t`Memory`, + id: "memory", + name: () => t`Memory`, invertSorting: true, cell: CellFormatter, - icon: MemoryStickIcon, + Icon: MemoryStickIcon, header: sortableHeader, }, { accessorKey: "info.dp", - id: t`Disk`, + id: "disk", + name: () => t`Disk`, invertSorting: true, cell: CellFormatter, - icon: HardDriveIcon, + Icon: HardDriveIcon, header: sortableHeader, }, { accessorFn: (originalRow) => originalRow.info.g, - id: "GPU", + id: "gpu", + name: () => t`GPU`, invertSorting: true, sortUndefined: -1, cell: CellFormatter, - icon: GpuIcon, + Icon: GpuIcon, header: sortableHeader, }, { accessorFn: (originalRow) => originalRow.info.b || 0, - id: t`Net`, + id: "net", + name: () => t`Net`, invertSorting: true, size: 50, - icon: EthernetIcon, + Icon: EthernetIcon, header: sortableHeader, cell(info) { const val = info.getValue() as number @@ -230,15 +240,13 @@ export default function SystemsTable() { }, { accessorFn: (originalRow) => originalRow.info.dt, - id: t({ - message: "Temp", - comment: "Temperature label in systems table", - }), + id: "temp", + name: () => t({ message: "Temp", comment: "Temperature label in systems table" }), invertSorting: true, sortUndefined: -1, size: 50, hideSort: true, - icon: ThermometerIcon, + Icon: ThermometerIcon, header: sortableHeader, cell(info) { const val = info.getValue() as number @@ -258,15 +266,16 @@ export default function SystemsTable() { }, { accessorKey: "info.v", - id: t`Agent`, + id: "agent", + name: () => t`Agent`, invertSorting: true, size: 50, - icon: WifiIcon, + Icon: WifiIcon, hideSort: true, header: sortableHeader, cell(info) { const version = info.getValue() as string - if (!version || !hubVersion) { + if (!version) { return null } const system = info.row.original @@ -280,7 +289,7 @@ export default function SystemsTable() { system={system} className={ (system.status !== "up" && "bg-primary/30") || - (version === hubVersion && "bg-green-500") || + (version === globalThis.BESZEL.HUB_VERSION && "bg-green-500") || "bg-yellow-500" } /> @@ -290,7 +299,9 @@ export default function SystemsTable() { }, }, { - id: t({ message: "Actions", comment: "Table column" }), + id: "actions", + // @ts-ignore + name: () => t({ message: "Actions", comment: "Table column" }), size: 50, cell: ({ row }) => (
@@ -300,11 +311,11 @@ export default function SystemsTable() { ), }, ] as ColumnDef[] - }, [hubVersion, i18n.locale]) + }, []) const table = useReactTable({ data, - columns, + columns: columnDefs, getCoreRowModel: getCoreRowModel(), onSortingChange: setSorting, getSortedRowModel: getSortedRowModel(), @@ -318,15 +329,17 @@ export default function SystemsTable() { }, defaultColumn: { minSize: 0, - size: Number.MAX_SAFE_INTEGER, - maxSize: Number.MAX_SAFE_INTEGER, + size: 900, + maxSize: 900, }, }) const rows = table.getRowModel().rows - - return ( - + const columns = table.getAllColumns() + const visibleColumns = table.getVisibleLeafColumns() + // TODO: hiding temp then gpu messes up table headers + const CardHead = useMemo(() => { + return (
@@ -377,8 +390,8 @@ export default function SystemsTable() {
- {table.getAllColumns().map((column) => { - if (column.id === t`Actions` || !column.getCanSort()) return null + {columns.map((column) => { + if (!column.getCanSort()) return null let Icon = // if current sort column, show sort direction if (sorting[0]?.id === column.id) { @@ -397,7 +410,8 @@ export default function SystemsTable() { key={column.id} > {Icon} - {column.id} + {/* @ts-ignore */} + {column.columnDef.name()} ) })} @@ -411,8 +425,7 @@ export default function SystemsTable() {
- {table - .getAllColumns() + {columns .filter((column) => column.getCanHide()) .map((column) => { return ( @@ -422,7 +435,8 @@ export default function SystemsTable() { checked={column.getIsVisible()} onCheckedChange={(value) => column.toggleVisibility(!!value)} > - {column.id} + {/* @ts-ignore */} + {column.columnDef.name()} ) })} @@ -434,128 +448,24 @@ export default function SystemsTable() {
+ ) + }, [visibleColumns.length, sorting, viewMode, locale]) + + return ( + + {CardHead}
{viewMode === "table" ? ( // table layout
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder - ? null - : flexRender(header.column.columnDef.header, header.getContext())} - - ) - })} - - ))} - - - {rows.length ? ( - table.getRowModel().rows.map((row) => ( - { - const target = e.target as HTMLElement - if (!target.closest("[data-nolink]") && e.currentTarget.contains(target)) { - navigate(getPagePath($router, "system", { name: row.original.name })) - } - }} - > - {row.getVisibleCells().map((cell) => ( - 10 ? "py-2" : "py-2.5")} - > - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} - - )) - ) : ( - - - No systems found. - - - )} - -
+
) : ( // grid layout
- {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => { - const system = row.original - const { status } = system - return ( - - -
- -
- - - {system.name} - -
-
- {table.getColumn(t`Actions`)?.getIsVisible() && ( -
- - -
- )} -
-
- - {table.getAllColumns().map((column) => { - if (!column.getIsVisible() || column.id === t`System` || column.id === t`Actions`) return null - const cell = row.getAllCells().find((cell) => cell.column.id === column.id) - if (!cell) return null - return ( -
- {/* @ts-ignore */} - {column.columnDef?.icon && ( - // @ts-ignore - - )} -
- {column.id}: -
{flexRender(cell.column.columnDef.cell, cell.getContext())}
-
-
- ) - })} -
- - {row.original.name} - -
- ) + {rows?.length ? ( + rows.map((row) => { + return }) ) : (
@@ -569,6 +479,247 @@ export default function SystemsTable() { ) } +const AllSystemsTable = memo( + ({ table, rows, colLength }: { table: TableType; rows: Row[]; colLength: number }) => { + return ( + + + + {rows.length ? ( + rows.map((row) => ( + + )) + ) : ( + + + No systems found. + + + )} + +
+ ) + } +) + +function SystemsTableHead({ table, colLength }: { table: TableType; colLength: number }) { + const { i18n } = useLingui() + return useMemo(() => { + return ( + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {flexRender(header.column.columnDef.header, header.getContext())} + + ) + })} + + ))} + + ) + }, [i18n.locale, colLength]) +} + +const SystemTableRow = memo( + ({ row, length, colLength }: { row: Row; length: number; colLength: number }) => { + const system = row.original + const { t } = useLingui() + return useMemo(() => { + return ( + { + const target = e.target as HTMLElement + if (!target.closest("[data-nolink]") && e.currentTarget.contains(target)) { + navigate(getPagePath($router, "system", { name: system.name })) + } + }} + > + {row.getVisibleCells().map((cell) => ( + 10 ? "py-2" : "py-2.5")} + > + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + ) + }, [system, colLength, t]) + } +) + +const SystemCard = memo( + ({ row, table, colLength }: { row: Row; table: TableType; colLength: number }) => { + const system = row.original + const { t } = useLingui() + + return useMemo(() => { + return ( + + +
+ +
+ + + {system.name} + +
+
+ {table.getColumn("actions")?.getIsVisible() && ( +
+ + +
+ )} +
+
+ + {table.getAllColumns().map((column) => { + if (!column.getIsVisible() || column.id === "system" || column.id === "actions") return null + const cell = row.getAllCells().find((cell) => cell.column.id === column.id) + if (!cell) return null + // @ts-ignore + const { Icon, name } = column.columnDef as ColumnDef + return ( +
+ {Icon && } +
+ {name()}: +
{flexRender(cell.column.columnDef.cell, cell.getContext())}
+
+
+ ) + })} +
+ + {row.original.name} + +
+ ) + }, [system, colLength, t]) + } +) + +const ActionsButton = memo(({ system }: { system: SystemRecord }) => { + const [deleteOpen, setDeleteOpen] = useState(false) + const [editOpen, setEditOpen] = useState(false) + let editOpened = useRef(false) + const { t } = useLingui() + + return useMemo(() => { + const { id, status, host, name } = system + return ( + <> + + + + + + {!isReadOnlyUser() && ( + { + editOpened.current = true + setEditOpen(true) + }} + > + + Edit + + )} + { + pb.collection("systems").update(id, { + status: status === "paused" ? "pending" : "paused", + }) + }} + > + {status === "paused" ? ( + <> + + Resume + + ) : ( + <> + + Pause + + )} + + copyToClipboard(host)}> + + Copy host + + + setDeleteOpen(true)}> + + Delete + + + + {/* edit dialog */} + + {editOpened.current && } + + {/* deletion dialog */} + setDeleteOpen(open)}> + + + + Are you sure you want to delete {name}? + + + + This action cannot be undone. This will permanently delete all current records for {name} from the + database. + + + + + + Cancel + + pb.collection("systems").delete(id)} + > + Continue + + + + + + ) + }, [system.id, t, deleteOpen, editOpen]) +}) + function IndicatorDot({ system, className }: { system: SystemRecord; className?: ClassValue }) { className ||= { "bg-green-500": system.status === "up", @@ -583,99 +734,3 @@ function IndicatorDot({ system, className }: { system: SystemRecord; className?: /> ) } - -const ActionsButton = memo(({ system }: { system: SystemRecord }) => { - const [deleteOpen, setDeleteOpen] = useState(false) - const [editOpen, setEditOpen] = useState(false) - let editOpened = useRef(false) - - const { id, status, host, name } = system - - return ( - <> - - - - - - {!isReadOnlyUser() && ( - { - editOpened.current = true - setEditOpen(true) - }} - > - - Edit - - )} - { - pb.collection("systems").update(id, { - status: status === "paused" ? "pending" : "paused", - }) - }} - > - {status === "paused" ? ( - <> - - Resume - - ) : ( - <> - - Pause - - )} - - copyToClipboard(host)}> - - Copy host - - - setDeleteOpen(true)}> - - Delete - - - - {/* edit dialog */} - - {editOpened.current && } - - {/* deletion dialog */} - setDeleteOpen(open)}> - - - - Are you sure you want to delete {name}? - - - - This action cannot be undone. This will permanently delete all current records for {name} from the - database. - - - - - - Cancel - - pb.collection("systems").delete(id)} - > - Continue - - - - - - ) -}) diff --git a/beszel/site/src/index.css b/beszel/site/src/index.css index 5b2582f..3912179 100644 --- a/beszel/site/src/index.css +++ b/beszel/site/src/index.css @@ -74,6 +74,7 @@ @layer base { * { @apply border-border; + overflow-anchor: none; } body { @apply bg-background text-foreground; diff --git a/beszel/site/src/lib/stores.ts b/beszel/site/src/lib/stores.ts index 9b24c1f..236ffa5 100644 --- a/beszel/site/src/lib/stores.ts +++ b/beszel/site/src/lib/stores.ts @@ -18,9 +18,6 @@ export const $alerts = atom([] as AlertRecord[]) /** SSH public key */ export const $publicKey = atom("") -/** Beszel hub version */ -export const $hubVersion = atom("") - /** Chart time period */ export const $chartTime = atom("1h") as PreinitializedWritableAtom diff --git a/beszel/site/src/main.tsx b/beszel/site/src/main.tsx index 84e0ae0..32a2bde 100644 --- a/beszel/site/src/main.tsx +++ b/beszel/site/src/main.tsx @@ -1,11 +1,11 @@ import "./index.css" // import { Suspense, lazy, useEffect, StrictMode } from "react" -import { Suspense, lazy, useEffect } from "react" +import { Suspense, lazy, memo, useEffect } from "react" import ReactDOM from "react-dom/client" -import Home from "./components/routes/home.tsx" +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, $hubVersion, $copyContent, $direction } from "./lib/stores.ts" +import { $authenticated, $systems, pb, $publicKey, $copyContent, $direction } from "./lib/stores.ts" import { updateUserSettings, updateAlerts, updateFavicon, updateSystemList } from "./lib/utils.ts" import { useStore } from "@nanostores/react" import { Toaster } from "./components/ui/toaster.tsx" @@ -14,13 +14,14 @@ import SystemDetail from "./components/routes/system.tsx" import Navbar from "./components/navbar.tsx" import { I18nProvider } from "@lingui/react" import { i18n } from "@lingui/core" +import "@/lib/i18n.ts" // 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 App = () => { +const App = memo(() => { const page = useStore($router) const authenticated = useStore($authenticated) const systems = useStore($systems) @@ -33,7 +34,6 @@ const App = () => { // get version / public key pb.send("/api/beszel/getkey", {}).then((data) => { $publicKey.set(data.key) - $hubVersion.set(data.v) }) // get servers / alerts / settings updateUserSettings() @@ -74,7 +74,7 @@ const App = () => { ) } -} +}) const Layout = () => { const authenticated = useStore($authenticated)