From a0f271545a75b95efd57260df99e7331c6569bfe Mon Sep 17 00:00:00 2001 From: henrygd Date: Sat, 2 Aug 2025 17:04:38 -0400 Subject: [PATCH] refactoring (no functionality changes) --- beszel/cmd/agent/agent.go | 22 +- beszel/internal/agent/agent.go | 28 +- beszel/internal/alerts/alerts_system.go | 21 +- beszel/internal/users/users.go | 36 +- .../src/components/alerts/alert-button.tsx | 10 +- beszel/site/src/components/routes/home.tsx | 8 +- .../systems-table/systems-table-columns.tsx | 420 ++++++++++++++++++ .../systems-table/systems-table.tsx | 415 +---------------- 8 files changed, 489 insertions(+), 471 deletions(-) create mode 100644 beszel/site/src/components/systems-table/systems-table-columns.tsx diff --git a/beszel/cmd/agent/agent.go b/beszel/cmd/agent/agent.go index fcde345..86b00e2 100644 --- a/beszel/cmd/agent/agent.go +++ b/beszel/cmd/agent/agent.go @@ -8,6 +8,7 @@ import ( "fmt" "log" "os" + "strings" "golang.org/x/crypto/ssh" ) @@ -25,13 +26,16 @@ func (opts *cmdOptions) parse() bool { flag.StringVar(&opts.listen, "listen", "", "Address or port to listen on") flag.Usage = func() { - fmt.Printf("Usage: %s [command] [flags]\n", os.Args[0]) - fmt.Println("\nCommands:") - fmt.Println(" health Check if the agent is running") - fmt.Println(" help Display this help message") - fmt.Println(" update Update to the latest version") - fmt.Println(" version Display the version") - fmt.Println("\nFlags:") + builder := strings.Builder{} + builder.WriteString("Usage: ") + builder.WriteString(os.Args[0]) + builder.WriteString(" [command] [flags]\n") + builder.WriteString("\nCommands:\n") + builder.WriteString(" health Check if the agent is running\n") + builder.WriteString(" help Display this help message\n") + builder.WriteString(" update Update to the latest version\n") + builder.WriteString("\nFlags:\n") + fmt.Print(builder.String()) flag.PrintDefaults() } @@ -111,12 +115,12 @@ func main() { serverConfig.Addr = addr serverConfig.Network = agent.GetNetwork(addr) - agent, err := agent.NewAgent() + a, err := agent.NewAgent() if err != nil { log.Fatal("Failed to create agent: ", err) } - if err := agent.Start(serverConfig); err != nil { + if err := a.Start(serverConfig); err != nil { log.Fatal("Failed to start server: ", err) } } diff --git a/beszel/internal/agent/agent.go b/beszel/internal/agent/agent.go index 15e0653..2e5f639 100644 --- a/beszel/internal/agent/agent.go +++ b/beszel/internal/agent/agent.go @@ -113,37 +113,37 @@ func (a *Agent) gatherStats(sessionID string) *system.CombinedData { a.Lock() defer a.Unlock() - cachedData, ok := a.cache.Get(sessionID) - if ok { - slog.Debug("Cached stats", "session", sessionID) - return cachedData + data, isCached := a.cache.Get(sessionID) + if isCached { + slog.Debug("Cached data", "session", sessionID) + return data } - *cachedData = system.CombinedData{ + *data = system.CombinedData{ Stats: a.getSystemStats(), Info: a.systemInfo, } - slog.Debug("System stats", "data", cachedData) + slog.Debug("System data", "data", data) if a.dockerManager != nil { if containerStats, err := a.dockerManager.getDockerStats(); err == nil { - cachedData.Containers = containerStats - slog.Debug("Docker stats", "data", cachedData.Containers) + data.Containers = containerStats + slog.Debug("Containers", "data", data.Containers) } else { - slog.Debug("Docker stats", "err", err) + slog.Debug("Containers", "err", err) } } - cachedData.Stats.ExtraFs = make(map[string]*system.FsStats) + data.Stats.ExtraFs = make(map[string]*system.FsStats) for name, stats := range a.fsStats { if !stats.Root && stats.DiskTotal > 0 { - cachedData.Stats.ExtraFs[name] = stats + data.Stats.ExtraFs[name] = stats } } - slog.Debug("Extra filesystems", "data", cachedData.Stats.ExtraFs) + slog.Debug("Extra FS", "data", data.Stats.ExtraFs) - a.cache.Set(sessionID, cachedData) - return cachedData + a.cache.Set(sessionID, data) + return data } // StartAgent initializes and starts the agent with optional WebSocket connection diff --git a/beszel/internal/alerts/alerts_system.go b/beszel/internal/alerts/alerts_system.go index c261c69..efd2d73 100644 --- a/beszel/internal/alerts/alerts_system.go +++ b/beszel/internal/alerts/alerts_system.go @@ -293,18 +293,11 @@ func (am *AlertManager) sendSystemAlert(alert SystemAlertData) { // app.Logger().Error("failed to save alert record", "err", err) return } - // expand the user relation and send the alert - if errs := am.hub.ExpandRecord(alert.alertRecord, []string{"user"}, nil); len(errs) > 0 { - // app.Logger().Error("failed to expand user relation", "errs", errs) - return - } - if user := alert.alertRecord.ExpandedOne("user"); user != nil { - am.SendAlert(AlertMessageData{ - UserID: user.Id, - Title: subject, - Message: body, - Link: am.hub.MakeLink("system", systemName), - LinkText: "View " + systemName, - }) - } + am.SendAlert(AlertMessageData{ + UserID: alert.alertRecord.GetString("user"), + Title: subject, + Message: body, + Link: am.hub.MakeLink("system", systemName), + LinkText: "View " + systemName, + }) } diff --git a/beszel/internal/users/users.go b/beszel/internal/users/users.go index 687d501..ca4dfda 100644 --- a/beszel/internal/users/users.go +++ b/beszel/internal/users/users.go @@ -6,6 +6,7 @@ import ( "log" "net/http" + "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase/core" ) @@ -42,29 +43,26 @@ func (um *UserManager) InitializeUserRole(e *core.RecordEvent) error { // Initialize user settings with defaults if not set func (um *UserManager) InitializeUserSettings(e *core.RecordEvent) error { record := e.Record - // intialize settings with defaults + // intialize settings with defaults (zero values can be ignored) settings := UserSettings{ - ChartTime: "1h", - NotificationEmails: []string{}, - NotificationWebhooks: []string{}, + ChartTime: "1h", } record.UnmarshalJSONField("settings", &settings) - if len(settings.NotificationEmails) == 0 { - // get user email from auth record - if errs := um.app.ExpandRecord(record, []string{"user"}, nil); len(errs) == 0 { - // app.Logger().Error("failed to expand user relation", "errs", errs) - if user := record.ExpandedOne("user"); user != nil { - settings.NotificationEmails = []string{user.GetString("email")} - } else { - log.Println("Failed to get user email from auth record") - } - } else { - log.Println("failed to expand user relation", "errs", errs) - } + // get user email from auth record + var user struct { + Email string `db:"email"` + } + err := e.App.DB().NewQuery("SELECT email FROM users WHERE id = {:id}").Bind(dbx.Params{ + "id": record.GetString("user"), + }).One(&user) + if err != nil { + log.Println("failed to get user email", "err", err) + return err + } + settings.NotificationEmails = []string{user.Email} + if len(settings.NotificationWebhooks) == 0 { + settings.NotificationWebhooks = []string{""} } - // if len(settings.NotificationWebhooks) == 0 { - // settings.NotificationWebhooks = []string{""} - // } record.Set("settings", settings) return e.Next() } diff --git a/beszel/site/src/components/alerts/alert-button.tsx b/beszel/site/src/components/alerts/alert-button.tsx index b06eef5..1cb2e8b 100644 --- a/beszel/site/src/components/alerts/alert-button.tsx +++ b/beszel/site/src/components/alerts/alert-button.tsx @@ -72,18 +72,18 @@ function AlertDialogContent({ system }: { system: SystemRecord }) { const alerts = useStore($alerts) const [overwriteExisting, setOverwriteExisting] = useState(false) - // alertsSignature changes only when alerts for this system change - let alertsSignature = "" + /* key to prevent re-rendering */ + const alertsSignature: string[] = [] + const systemAlerts = alerts.filter((alert) => { if (alert.system === system.id) { - alertsSignature += alert.name + alert.min + alert.value + alertsSignature.push(alert.name, alert.min, alert.value) return true } return false }) as AlertRecord[] return useMemo(() => { - // console.log("render modal", system.name, alertsSignature) const data = Object.keys(alertInfo).map((name) => { const alert = alertInfo[name as keyof typeof alertInfo] return { @@ -149,5 +149,5 @@ function AlertDialogContent({ system }: { system: SystemRecord }) { ) - }, [alertsSignature, overwriteExisting]) + }, [alertsSignature.join(""), overwriteExisting]) } diff --git a/beszel/site/src/components/routes/home.tsx b/beszel/site/src/components/routes/home.tsx index d8686f1..bf4867a 100644 --- a/beszel/site/src/components/routes/home.tsx +++ b/beszel/site/src/components/routes/home.tsx @@ -18,7 +18,9 @@ export const Home = memo(() => { const systems = useStore($systems) const { t } = useLingui() - let alertsKey = "" + /* key to prevent re-rendering of active alerts */ + const alertsKey: string[] = [] + const activeAlerts = useMemo(() => { const activeAlerts = alerts.filter((alert) => { const active = alert.triggered && alert.name in alertInfo @@ -26,7 +28,7 @@ export const Home = memo(() => { return false } alert.sysname = systems.find((system) => system.id === alert.system)?.name - alertsKey += alert.id + alertsKey.push(alert.id) return true }) return activeAlerts @@ -81,7 +83,7 @@ export const Home = memo(() => { ), - [alertsKey] + [alertsKey.join("")] ) }) diff --git a/beszel/site/src/components/systems-table/systems-table-columns.tsx b/beszel/site/src/components/systems-table/systems-table-columns.tsx new file mode 100644 index 0000000..83fc321 --- /dev/null +++ b/beszel/site/src/components/systems-table/systems-table-columns.tsx @@ -0,0 +1,420 @@ +import { SystemRecord } from "@/types" +import { CellContext, ColumnDef, HeaderContext } from "@tanstack/react-table" +import { ClassValue } from "clsx" +import { + ArrowUpDownIcon, + CopyIcon, + CpuIcon, + HardDriveIcon, + MemoryStickIcon, + MoreHorizontalIcon, + PauseCircleIcon, + PenBoxIcon, + PlayCircleIcon, + ServerIcon, + Trash2Icon, + WifiIcon, +} from "lucide-react" +import { Button } from "../ui/button" +import { + cn, + copyToClipboard, + decimalString, + formatBytes, + formatTemperature, + 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 { Trans, useLingui } from "@lingui/react/macro" +import { useMemo, useRef, useState } from "react" +import { memo } from "react" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "../ui/dropdown-menu" +import AlertButton from "../alerts/alert-button" +import { Dialog } from "../ui/dialog" +import { SystemDialog } from "../add-system" +import { AlertDialog } from "../ui/alert-dialog" +import { + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "../ui/alert-dialog" +import { buttonVariants } from "../ui/button" +import { t } from "@lingui/core/macro" + +/** + * @param viewMode - "table" or "grid" + * @returns - Column definitions for the systems table + */ +export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnDef[] { + const statusTranslations = { + up: () => t`Up`.toLowerCase(), + down: () => t`Down`.toLowerCase(), + paused: () => t`Paused`.toLowerCase(), + } + return [ + { + size: 200, + minSize: 0, + accessorKey: "name", + id: "system", + name: () => t`System`, + filterFn: (row, _, filterVal) => { + 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 + } + return false + }, + enableHiding: false, + invertSorting: false, + Icon: ServerIcon, + cell: (info) => ( + + + {info.getValue() as string} + + ), + header: sortableHeader, + }, + { + accessorFn: ({ info }) => info.cpu, + id: "cpu", + name: () => t`CPU`, + cell: CellFormatter, + Icon: CpuIcon, + header: sortableHeader, + }, + { + // accessorKey: "info.mp", + accessorFn: ({ info }) => info.mp, + id: "memory", + name: () => t`Memory`, + cell: CellFormatter, + Icon: MemoryStickIcon, + header: sortableHeader, + }, + { + accessorFn: ({ info }) => info.dp, + id: "disk", + name: () => t`Disk`, + cell: CellFormatter, + Icon: HardDriveIcon, + header: sortableHeader, + }, + { + accessorFn: ({ info }) => info.g, + id: "gpu", + name: () => "GPU", + cell: CellFormatter, + Icon: GpuIcon, + header: sortableHeader, + }, + { + id: "loadAverage", + accessorFn: ({ info }) => { + const sum = info.la?.reduce((acc, curr) => acc + curr, 0) + // TODO: remove this in future release in favor of la array + if (!sum) { + return (info.l1 ?? 0) + (info.l5 ?? 0) + (info.l15 ?? 0) + } + return sum + }, + name: () => t({ message: "Load Avg", comment: "Short label for load average" }), + size: 0, + Icon: HourglassIcon, + header: sortableHeader, + cell(info: CellContext) { + const { info: sysInfo, status } = info.row.original + // agent version + const { minor, patch } = parseSemVer(sysInfo.v) + let loadAverages = sysInfo.la + + // use legacy load averages if agent version is less than 12.1.0 + if (!loadAverages || (minor === 12 && patch < 1)) { + loadAverages = [sysInfo.l1 ?? 0, sysInfo.l5 ?? 0, sysInfo.l15 ?? 0] + } + + const max = Math.max(...loadAverages) + if (max === 0 && (status === "paused" || minor < 12)) { + return null + } + + function getDotColor() { + const normalized = max / (sysInfo.t ?? 1) + if (status !== "up") return "bg-primary/30" + if (normalized < 0.7) return "bg-green-500" + if (normalized < 1) return "bg-yellow-500" + return "bg-red-600" + } + + return ( +
+ + {loadAverages?.map((la, i) => ( + {decimalString(la, la >= 10 ? 1 : 2)} + ))} +
+ ) + }, + }, + { + accessorFn: ({ info }) => info.bb || (info.b || 0) * 1024 * 1024, + id: "net", + name: () => t`Net`, + size: 0, + Icon: EthernetIcon, + header: sortableHeader, + cell(info) { + const sys = info.row.original + if (sys.status === "paused") { + return null + } + const userSettings = useStore($userSettings) + const { value, unit } = formatBytes(info.getValue() as number, true, userSettings.unitNet, false) + return ( + + {decimalString(value, value >= 100 ? 1 : 2)} {unit} + + ) + }, + }, + { + accessorFn: ({ info }) => info.dt, + id: "temp", + name: () => t({ message: "Temp", comment: "Temperature label in systems table" }), + size: 50, + hideSort: true, + Icon: ThermometerIcon, + header: sortableHeader, + cell(info) { + const val = info.getValue() as number + if (!val) { + return null + } + const userSettings = useStore($userSettings) + const { value, unit } = formatTemperature(val, userSettings.unitTemp) + return ( + + {decimalString(value, value >= 100 ? 1 : 2)} {unit} + + ) + }, + }, + { + accessorFn: ({ info }) => info.v, + id: "agent", + name: () => t`Agent`, + // invertSorting: true, + size: 50, + Icon: WifiIcon, + hideSort: true, + header: sortableHeader, + cell(info) { + const version = info.getValue() as string + if (!version) { + return null + } + const system = info.row.original + return ( + + + {info.getValue() as string} + + ) + }, + }, + { + id: "actions", + // @ts-ignore + name: () => t({ message: "Actions", comment: "Table column" }), + size: 50, + cell: ({ row }) => ( +
+ + +
+ ), + }, + ] as ColumnDef[] +} + +function sortableHeader(context: HeaderContext) { + const { column } = context + // @ts-ignore + const { Icon, hideSort, name }: { Icon: React.ElementType; name: () => string; hideSort: boolean } = column.columnDef + return ( + + ) +} +function CellFormatter(info: CellContext) { + const val = Number(info.getValue()) || 0 + return ( +
+ {decimalString(val, val >= 10 ? 1 : 2)}% + + + +
+ ) +} + +export function IndicatorDot({ system, className }: { system: SystemRecord; className?: ClassValue }) { + className ||= { + "bg-green-500": system.status === "up", + "bg-red-500": system.status === "down", + "bg-primary/40": system.status === "paused", + "bg-yellow-500": system.status === "pending", + } + return ( + + ) +} + +export const ActionsButton = memo(({ system }: { system: SystemRecord }) => { + const [deleteOpen, setDeleteOpen] = useState(false) + const [editOpen, setEditOpen] = useState(false) + let editOpened = useRef(false) + const { t } = useLingui() + const { id, status, host, name } = system + + return useMemo(() => { + return ( + <> + + + + + + {!isReadOnlyUser() && ( + { + editOpened.current = true + setEditOpen(true) + }} + > + + Edit + + )} + { + pb.collection("systems").update(id, { + status: status === "paused" ? "pending" : "paused", + }) + }} + > + {status === "paused" ? ( + <> + + Resume + + ) : ( + <> + + Pause + + )} + + copyToClipboard(name)}> + + Copy name + + 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 + + + + + + ) + }, [id, status, host, name, t, deleteOpen, editOpen]) +}) diff --git a/beszel/site/src/components/systems-table/systems-table.tsx b/beszel/site/src/components/systems-table/systems-table.tsx index 5eccf41..f81a0b1 100644 --- a/beszel/site/src/components/systems-table/systems-table.tsx +++ b/beszel/site/src/components/systems-table/systems-table.tsx @@ -1,5 +1,4 @@ import { - CellContext, ColumnDef, ColumnFiltersState, getFilteredRowModel, @@ -9,14 +8,13 @@ import { VisibilityState, getCoreRowModel, useReactTable, - HeaderContext, Row, Table as TableType, } from "@tanstack/react-table" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" -import { Button, buttonVariants } from "@/components/ui/button" +import { Button } from "@/components/ui/button" import { DropdownMenu, @@ -29,105 +27,30 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" - -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from "@/components/ui/alert-dialog" - import { SystemRecord } from "@/types" import { - MoreHorizontalIcon, ArrowUpDownIcon, - MemoryStickIcon, - CopyIcon, - PauseCircleIcon, - PlayCircleIcon, - Trash2Icon, - WifiIcon, - HardDriveIcon, - ServerIcon, - CpuIcon, LayoutGridIcon, LayoutListIcon, ArrowDownIcon, ArrowUpIcon, Settings2Icon, EyeIcon, - PenBoxIcon, } from "lucide-react" -import { memo, useEffect, useMemo, useRef, useState } from "react" -import { $systems, $userSettings, pb } from "@/lib/stores" +import { memo, useEffect, useMemo, useState } from "react" +import { $systems } from "@/lib/stores" import { useStore } from "@nanostores/react" -import { - cn, - copyToClipboard, - isReadOnlyUser, - useLocalStorage, - formatTemperature, - decimalString, - formatBytes, - parseSemVer, -} from "@/lib/utils" -import AlertsButton from "../alerts/alert-button" +import { cn, useLocalStorage } from "@/lib/utils" import { $router, Link, navigate } from "../router" -import { EthernetIcon, GpuIcon, HourglassIcon, ThermometerIcon } from "../ui/icons" 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" import { getPagePath } from "@nanostores/router" -import { SystemDialog } from "../add-system" -import { Dialog } from "../ui/dialog" +import SystemsTableColumns, { ActionsButton, IndicatorDot } from "./systems-table-columns" +import AlertButton from "../alerts/alert-button" type ViewMode = "table" | "grid" -function CellFormatter(info: CellContext) { - const val = Number(info.getValue()) || 0 - return ( -
- {decimalString(val, val >= 10 ? 1 : 2)}% - - - -
- ) -} - -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 { i18n, t } = useLingui() @@ -145,212 +68,7 @@ export default function SystemsTable() { } }, [filter]) - const columnDefs = useMemo(() => { - const statusTranslations = { - up: () => t`Up`.toLowerCase(), - down: () => t`Down`.toLowerCase(), - paused: () => t`Paused`.toLowerCase(), - } - return [ - { - size: 200, - minSize: 0, - accessorKey: "name", - id: "system", - name: () => t`System`, - filterFn: (row, _, filterVal) => { - 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 - } - return false - }, - enableHiding: false, - invertSorting: false, - Icon: ServerIcon, - cell: (info) => ( - - - - {info.getValue() as string} - - - ), - header: sortableHeader, - }, - { - accessorFn: ({ info }) => info.cpu, - id: "cpu", - name: () => t`CPU`, - cell: CellFormatter, - Icon: CpuIcon, - header: sortableHeader, - }, - { - // accessorKey: "info.mp", - accessorFn: ({ info }) => info.mp, - id: "memory", - name: () => t`Memory`, - cell: CellFormatter, - Icon: MemoryStickIcon, - header: sortableHeader, - }, - { - accessorFn: ({ info }) => info.dp, - id: "disk", - name: () => t`Disk`, - cell: CellFormatter, - Icon: HardDriveIcon, - header: sortableHeader, - }, - { - accessorFn: ({ info }) => info.g, - id: "gpu", - name: () => "GPU", - cell: CellFormatter, - Icon: GpuIcon, - header: sortableHeader, - }, - { - id: "loadAverage", - accessorFn: ({ info }) => { - const sum = info.la?.reduce((acc, curr) => acc + curr, 0) - // TODO: remove this in future release in favor of la array - if (!sum) { - return (info.l1 ?? 0) + (info.l5 ?? 0) + (info.l15 ?? 0) - } - return sum - }, - name: () => t({ message: "Load Avg", comment: "Short label for load average" }), - size: 0, - Icon: HourglassIcon, - header: sortableHeader, - cell(info: CellContext) { - const { info: sysInfo, status } = info.row.original - // agent version - const { minor, patch } = parseSemVer(sysInfo.v) - let loadAverages = sysInfo.la - - // use legacy load averages if agent version is less than 12.1.0 - if (!loadAverages || (minor === 12 && patch < 1)) { - loadAverages = [sysInfo.l1 ?? 0, sysInfo.l5 ?? 0, sysInfo.l15 ?? 0] - } - - const max = Math.max(...loadAverages) - if (max === 0 && (status === "paused" || minor < 12)) { - return null - } - - function getDotColor() { - const normalized = max / (sysInfo.t ?? 1) - if (status !== "up") return "bg-primary/30" - if (normalized < 0.7) return "bg-green-500" - if (normalized < 1) return "bg-yellow-500" - return "bg-red-600" - } - - return ( -
- - {loadAverages?.map((la, i) => ( - {decimalString(la, la >= 10 ? 1 : 2)} - ))} -
- ) - }, - }, - { - accessorFn: ({ info }) => info.bb || (info.b || 0) * 1024 * 1024, - id: "net", - name: () => t`Net`, - size: 0, - Icon: EthernetIcon, - header: sortableHeader, - cell(info) { - const sys = info.row.original - if (sys.status === "paused") { - return null - } - const userSettings = useStore($userSettings) - const { value, unit } = formatBytes(info.getValue() as number, true, userSettings.unitNet, false) - return ( - - {decimalString(value, value >= 100 ? 1 : 2)} {unit} - - ) - }, - }, - { - accessorFn: ({ info }) => info.dt, - id: "temp", - name: () => t({ message: "Temp", comment: "Temperature label in systems table" }), - size: 50, - hideSort: true, - Icon: ThermometerIcon, - header: sortableHeader, - cell(info) { - const val = info.getValue() as number - if (!val) { - return null - } - const userSettings = useStore($userSettings) - const { value, unit } = formatTemperature(val, userSettings.unitTemp) - return ( - - {decimalString(value, value >= 100 ? 1 : 2)} {unit} - - ) - }, - }, - { - accessorFn: ({ info }) => info.v, - id: "agent", - name: () => t`Agent`, - // invertSorting: true, - size: 50, - Icon: WifiIcon, - hideSort: true, - header: sortableHeader, - cell(info) { - const version = info.getValue() as string - if (!version) { - return null - } - const system = info.row.original - return ( - - - {info.getValue() as string} - - ) - }, - }, - { - id: "actions", - // @ts-ignore - name: () => t({ message: "Actions", comment: "Table column" }), - size: 50, - cell: ({ row }) => ( -
- - -
- ), - }, - ] as ColumnDef[] - }, []) + const columnDefs = useMemo(() => SystemsTableColumns(viewMode), []) const table = useReactTable({ data, @@ -628,7 +346,7 @@ const SystemCard = memo( {table.getColumn("actions")?.getIsVisible() && (
- +
)} @@ -663,120 +381,3 @@ const SystemCard = memo( }, [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() - const { id, status, host, name } = system - - return useMemo(() => { - return ( - <> - - - - - - {!isReadOnlyUser() && ( - { - editOpened.current = true - setEditOpen(true) - }} - > - - Edit - - )} - { - pb.collection("systems").update(id, { - status: status === "paused" ? "pending" : "paused", - }) - }} - > - {status === "paused" ? ( - <> - - Resume - - ) : ( - <> - - Pause - - )} - - copyToClipboard(name)}> - - Copy name - - 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 - - - - - - ) - }, [id, status, host, name, t, deleteOpen, editOpen]) -}) - -function IndicatorDot({ system, className }: { system: SystemRecord; className?: ClassValue }) { - className ||= { - "bg-green-500": system.status === "up", - "bg-red-500": system.status === "down", - "bg-primary/40": system.status === "paused", - "bg-yellow-500": system.status === "pending", - } - return ( - - ) -}