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, 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 { 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" import { MeterState } from "@/lib/enums" /** * @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: TableCellWithMeter, Icon: CpuIcon, header: sortableHeader, }, { // accessorKey: "info.mp", accessorFn: ({ info }) => info.mp, id: "memory", name: () => t`Memory`, cell: TableCellWithMeter, Icon: MemoryStickIcon, header: sortableHeader, }, { accessorFn: ({ info }) => info.dp, id: "disk", name: () => t`Disk`, cell: TableCellWithMeter, Icon: HardDriveIcon, header: sortableHeader, }, { accessorFn: ({ info }) => info.g, id: "gpu", name: () => "GPU", cell: TableCellWithMeter, 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 } const normalizedLoad = max / (sysInfo.t ?? 1) const threshold = getMeterState(normalizedLoad * 100) 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 TableCellWithMeter(info: CellContext) { const val = Number(info.getValue()) || 0 const threshold = getMeterState(val) 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]) })