/** biome-ignore-all lint/correctness/useHookAtTopLevel: Hooks live inside memoized column definitions */ import { t } from "@lingui/core/macro" import { Trans, useLingui } from "@lingui/react/macro" import { useStore } from "@nanostores/react" import { getPagePath } from "@nanostores/router" import type { CellContext, ColumnDef, HeaderContext } from "@tanstack/react-table" import type { ClassValue } from "clsx" import { ArrowUpDownIcon, ChevronRightSquareIcon, ClockArrowUp, CopyIcon, CpuIcon, HardDriveIcon, MemoryStickIcon, MoreHorizontalIcon, PauseCircleIcon, PenBoxIcon, PlayCircleIcon, ServerIcon, TerminalSquareIcon, Trash2Icon, WifiIcon, } from "lucide-react" import { memo, useMemo, useRef, useState } from "react" import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip" import { isReadOnlyUser, pb } from "@/lib/api" import { BatteryState, ConnectionType, connectionTypeLabels, MeterState, SystemStatus } from "@/lib/enums" import { $longestSystemNameLen, $userSettings } from "@/lib/stores" import { cn, copyToClipboard, decimalString, formatBytes, formatTemperature, parseSemVer, secondsToUptimeString, } from "@/lib/utils" import { batteryStateTranslations } from "@/lib/i18n" import type { SystemRecord } from "@/types" import { SystemDialog } from "../add-system" import AlertButton from "../alerts/alert-button" import { $router, Link } from "../router" import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "../ui/alert-dialog" import { Button, buttonVariants } from "../ui/button" import { Dialog } from "../ui/dialog" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "../ui/dropdown-menu" import { BatteryMediumIcon, EthernetIcon, GpuIcon, HourglassIcon, ThermometerIcon, WebSocketIcon, BatteryHighIcon, BatteryLowIcon, PlugChargingIcon, BatteryFullIcon, } from "../ui/icons" const STATUS_COLORS = { [SystemStatus.Up]: "bg-green-500", [SystemStatus.Down]: "bg-red-500", [SystemStatus.Paused]: "bg-primary/40", [SystemStatus.Pending]: "bg-yellow-500", } as const function getMeterStateByThresholds(value: number, warn = 65, crit = 90): MeterState { return value >= crit ? MeterState.Crit : value >= warn ? MeterState.Warn : MeterState.Good } /** * @param viewMode - "table" or "grid" * @returns - Column definitions for the systems table */ export function SystemsTableColumns(viewMode: "table" | "grid"): ColumnDef[] { return [ { // size: 200, size: 100, minSize: 0, accessorKey: "name", id: "system", name: () => t`System`, sortingFn: (a, b) => a.original.name.localeCompare(b.original.name), filterFn: (() => { let filterInput = "" let filterInputLower = "" const nameCache = new Map() const statusTranslations = { [SystemStatus.Up]: t`Up`.toLowerCase(), [SystemStatus.Down]: t`Down`.toLowerCase(), [SystemStatus.Paused]: t`Paused`.toLowerCase(), } as const // match filter value against name or translated status return (row, _, newFilterInput) => { const { name, status } = row.original if (newFilterInput !== filterInput) { filterInput = newFilterInput filterInputLower = newFilterInput.toLowerCase() } let nameLower = nameCache.get(name) if (nameLower === undefined) { nameLower = name.toLowerCase() nameCache.set(name, nameLower) } if (nameLower.includes(filterInputLower)) { return true } const statusLower = statusTranslations[status as keyof typeof statusTranslations] return statusLower?.includes(filterInputLower) || false } })(), enableHiding: false, invertSorting: false, Icon: ServerIcon, cell: (info) => { const { name, id } = info.row.original const longestName = useStore($longestSystemNameLen) const linkUrl = getPagePath($router, "system", { id }) return ( <> { // set title on hover if text is truncated to show full name const a = e.currentTarget if (a.scrollWidth > a.clientWidth) { a.title = name } else { a.removeAttribute("title") } }} > {name} ) }, header: sortableHeader, }, { accessorFn: ({ info }) => info.cpu || undefined, id: "cpu", name: () => t`CPU`, cell: TableCellWithMeter, Icon: CpuIcon, header: sortableHeader, }, { // accessorKey: "info.mp", accessorFn: ({ info }) => info.mp || undefined, id: "memory", name: () => t`Memory`, cell: TableCellWithMeter, Icon: MemoryStickIcon, header: sortableHeader, }, { accessorFn: ({ info }) => info.dp || undefined, id: "disk", name: () => t`Disk`, cell: (info: CellContext) => info.row.original.info.efs ? DiskCellWithMultiple(info) : TableCellWithMeter(info), Icon: HardDriveIcon, header: sortableHeader, }, { accessorFn: ({ info }) => info.g || undefined, id: "gpu", name: () => "GPU", cell: TableCellWithMeter, Icon: GpuIcon, header: sortableHeader, }, { id: "loadAverage", accessorFn: ({ info }) => info.la?.reduce((acc, curr) => acc + curr, 0), 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 const { major, minor } = parseSemVer(sysInfo.v) const { colorWarn = 65, colorCrit = 90 } = useStore($userSettings, { keys: ["colorWarn", "colorCrit"] }) const loadAverages = sysInfo.la || [] const max = Math.max(...loadAverages) if (max === 0 && (status === SystemStatus.Paused || (major < 1 && minor < 13))) { return null } const normalizedLoad = max / (sysInfo.t ?? 1) const threshold = getMeterStateByThresholds(normalizedLoad * 100, colorWarn, colorCrit) return (
{loadAverages?.map((la, i) => ( {decimalString(la, la >= 10 ? 1 : 2)} ))}
) }, }, { accessorFn: ({ info, status }) => (status !== SystemStatus.Up ? undefined : info.bb), id: "net", name: () => t`Net`, size: 0, Icon: EthernetIcon, header: sortableHeader, sortUndefined: "last", cell(info) { const val = info.getValue() as number | undefined if (val === undefined) { return null } const userSettings = useStore($userSettings, { keys: ["unitNet"] }) const { value, unit } = formatBytes(val, 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 const userSettings = useStore($userSettings, { keys: ["unitTemp"] }) if (!val) { return null } const { value, unit } = formatTemperature(val, userSettings.unitTemp) return ( {decimalString(value, value >= 100 ? 1 : 2)} {unit} ) }, }, { accessorFn: ({ info }) => info.bat?.[0], id: "battery", name: () => t({ message: "Bat", comment: "Battery label in systems table header" }), size: 70, Icon: BatteryMediumIcon, header: sortableHeader, hideSort: true, cell(info) { const [pct, state] = info.row.original.info.bat ?? [] if (pct === undefined) { return null } let Icon = PlugChargingIcon let iconColor = "text-muted-foreground" if (state !== BatteryState.Charging) { if (pct < 25) { iconColor = pct < 11 ? "text-red-500" : "text-yellow-500" Icon = BatteryLowIcon } else if (pct < 75) { Icon = BatteryMediumIcon } else if (pct < 95) { Icon = BatteryHighIcon } else { Icon = BatteryFullIcon } } const stateLabel = state !== undefined ? (batteryStateTranslations[state as BatteryState]?.() ?? undefined) : undefined return ( {pct}% ) }, }, { accessorFn: ({ info }) => info.sv?.[0], id: "services", name: () => t`Services`, size: 50, Icon: TerminalSquareIcon, header: sortableHeader, hideSort: true, sortingFn: (a, b) => { // sort priorities: 1) failed services, 2) total services const [totalCountA, numFailedA] = a.original.info.sv ?? [0, 0] const [totalCountB, numFailedB] = b.original.info.sv ?? [0, 0] if (numFailedA !== numFailedB) { return numFailedA - numFailedB } return totalCountA - totalCountB }, cell(info) { const sys = info.row.original const [totalCount, numFailed] = sys.info.sv ?? [0, 0] if (sys.status !== SystemStatus.Up || totalCount === 0) { return null } return ( 0, [STATUS_COLORS[SystemStatus.Up]]: numFailed === 0, })} /> {totalCount}{" "} ({t`Failed`.toLowerCase()}: {numFailed}) ) }, }, { accessorFn: ({ info }) => info.u || undefined, id: "uptime", name: () => t`Uptime`, size: 50, Icon: ClockArrowUp, header: sortableHeader, hideSort: true, cell(info) { const uptime = info.getValue() as number if (!uptime) { return null } return {secondsToUptimeString(uptime)} }, }, { accessorFn: ({ info }) => info.v, id: "agent", name: () => t`Agent`, 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 const color = { "text-green-500": version === globalThis.BESZEL.HUB_VERSION, "text-yellow-500": version !== globalThis.BESZEL.HUB_VERSION, "text-red-500": system.status !== SystemStatus.Up, } return ( {system.info.ct === ConnectionType.WebSocket && ( )} {system.info.ct === ConnectionType.SSH && ( )} {!system.info.ct && } {info.getValue() as string} ) }, }, { id: "actions", // @ts-expect-error name: () => t({ message: "Actions", comment: "Table column" }), size: 50, cell: ({ row }) => (
), }, ] as ColumnDef[] } function sortableHeader(context: HeaderContext) { const { column } = context // @ts-expect-error const { Icon, hideSort, name }: { Icon: React.ElementType; name: () => string; hideSort: boolean } = column.columnDef const isSorted = column.getIsSorted() return ( ) } function TableCellWithMeter(info: CellContext) { const { colorWarn = 65, colorCrit = 90 } = useStore($userSettings, { keys: ["colorWarn", "colorCrit"] }) const val = Number(info.getValue()) || 0 const threshold = getMeterStateByThresholds(val, colorWarn, colorCrit) const meterClass = cn( "h-full", (info.row.original.status !== SystemStatus.Up && STATUS_COLORS.paused) || (threshold === MeterState.Good && STATUS_COLORS.up) || (threshold === MeterState.Warn && STATUS_COLORS.pending) || STATUS_COLORS.down ) return (
{decimalString(val, val >= 10 ? 1 : 2)}%
) } function DiskCellWithMultiple(info: CellContext) { const { colorWarn = 65, colorCrit = 90 } = useStore($userSettings, { keys: ["colorWarn", "colorCrit"] }) const { info: sysInfo, status, id } = info.row.original const extraFs = Object.entries(sysInfo.efs ?? {}) const rootDiskPct = sysInfo.dp // sort extra disks by percentage descending extraFs.sort((a, b) => b[1] - a[1]) function getIndicatorColor(pct: number) { const threshold = getMeterStateByThresholds(pct, colorWarn, colorCrit) return ( (status !== SystemStatus.Up && STATUS_COLORS.paused) || (threshold === MeterState.Good && STATUS_COLORS.up) || (threshold === MeterState.Warn && STATUS_COLORS.pending) || STATUS_COLORS.down ) } function getMeterClass(pct: number) { return cn("h-full", getIndicatorColor(pct)) } // Extra disk indicators (max 3 dots - one per state if any disk exists in range) const stateColors = [STATUS_COLORS.up, STATUS_COLORS.pending, STATUS_COLORS.down] const extraDiskIndicators = status !== SystemStatus.Up ? [] : [...new Set(extraFs.map(([, pct]) => getMeterStateByThresholds(pct, colorWarn, colorCrit)))] .sort() .map((state) => stateColors[state]) return (
{decimalString(rootDiskPct, rootDiskPct >= 10 ? 1 : 2)}% {/* Root disk */} {/* Extra disk indicators */} {extraDiskIndicators.map((color) => ( ))}
Root
{decimalString(rootDiskPct, rootDiskPct >= 10 ? 1 : 2)}%
{extraFs.map(([name, pct]) => { return (
{name}
{decimalString(pct, pct >= 10 ? 1 : 2)}%
) })}
) } export function IndicatorDot({ system, className }: { system: SystemRecord; className?: ClassValue }) { className ||= STATUS_COLORS[system.status as keyof typeof STATUS_COLORS] || "" return ( ) } export const ActionsButton = memo(({ system }: { system: SystemRecord }) => { const [deleteOpen, setDeleteOpen] = useState(false) const [editOpen, setEditOpen] = useState(false) const 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 === SystemStatus.Paused ? SystemStatus.Pending : SystemStatus.Paused, }) }} > {status === SystemStatus.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, system, t, deleteOpen, editOpen]) })