"use client"; import { useCallback, useEffect, useMemo, useState } from "react"; import { Center, Group, Indicator, Loader, Progress, Text } from "@mantine/core"; import type { DataTableColumn, DataTableSortStatus } from "mantine-datatable"; import { Activity, Battery, Cpu, HardDrive, MemoryStick, Monitor, Network, Server, Thermometer, Wifi, } from "lucide-react"; import { clientApi } from "@homarr/api/client"; import { useSession } from "@homarr/auth/client"; import { constructBoardPermissions } from "@homarr/auth/shared"; import { useOptionalBoard } from "@homarr/boards/context"; import { useModalAction } from "@homarr/modals"; import { showErrorNotification } from "@homarr/notifications"; import { useScopedI18n } from "@homarr/translation/client"; import type { WidgetComponentProps } from "../definition"; import { HomarrDataTable } from "../common/homarr-data-table"; import { usePersistedTableLayout, useTableLayoutPersistence } from "../common/use-persisted-table-layout"; import type { BeszelSystemRow } from "../beszel/_shared/types"; import { loadAvgColor, statusColorMap, thresholdColor } from "../beszel/_shared/colors"; import { formatByteRate, formatLoadAvg, formatPercent, formatTemp, formatUptime, getProgressTrackSize, } from "../beszel/_shared/format"; import { useBeszelFilteredSystems } from "../beszel/_shared/hooks"; import { BeszelIntegrationErrorIndicator } from "../beszel/_shared/error-indicator"; import { BeszelSystemStatsModal } from "../beszel/_shared/system-stats-modal"; import { DiskUsage } from "../beszel/_shared/disk-usage"; const directionMultiplier: Record = { asc: 1, desc: -1 }; type SystemRowWithKey = BeszelSystemRow & { _key: string }; const columnAccessors = [ "name", "cpu", "memory", "disk", "gpu", "loadAvg", "netBytes", "temp", "battery", "services", "uptime", "agentVersion", ] as const satisfies readonly (keyof BeszelSystemRow)[]; interface SizeConfig { iconSize: number; fontSize: "xs" | "sm"; progressSize: "xs" | "sm"; cellPadding: number; valueMiw: number; } const getSizeConfig = (width: number): SizeConfig => { if (width < 400) { return { iconSize: 10, fontSize: "xs", progressSize: "xs", cellPadding: 2, valueMiw: 30, }; } return { iconSize: 14, fontSize: "sm", progressSize: "sm", cellPadding: 4, valueMiw: 38, }; }; export default function BeszelSystemTableWidget({ options, integrationIds, isEditMode, width, boardId, itemId, setOptions, }: WidgetComponentProps<"beszelSystemTable">) { const t = useScopedI18n("widget.beszelSystemTable"); const { openModal } = useModalAction(BeszelSystemStatsModal); const board = useOptionalBoard(); const { data: session } = useSession(); const hasChangeAccess = board ? constructBoardPermissions(board, session).hasChangeAccess : false; const { data: results = [], error: systemsError, isPending, } = clientApi.widget.beszel.getSystems.useQuery({ integrationIds }); const size = useMemo(() => getSizeConfig(width), [width]); const { mutate: saveItemOptions } = clientApi.widget.options.saveItemOptions.useMutation({ onError: () => showErrorNotification({ title: t("error.layoutSaveTitle"), message: t("error.layoutSaveMessage"), }), }); const persistLayout = useTableLayoutPersistence({ boardId, hasChangeAccess, itemId, saveItemOptions, setOptions, }); const filteredSystems = useBeszelFilteredSystems(results, options.statusFilter); const [sortStatus, setSortStatus] = useState>({ columnAccessor: options.sortBy, direction: options.sortDirection as "asc" | "desc", }); useEffect(() => { setSortStatus({ columnAccessor: options.sortBy, direction: options.sortDirection as "asc" | "desc", }); }, [options.sortBy, options.sortDirection]); const sortedSystems = useMemo(() => { const accessor = sortStatus.columnAccessor as keyof BeszelSystemRow; const dir = directionMultiplier[sortStatus.direction] ?? 1; return [...filteredSystems].toSorted((a, b) => { const aVal = a[accessor] ?? 0; const bVal = b[accessor] ?? 0; if (typeof aVal === "string" && typeof bVal === "string") return aVal.localeCompare(bVal) * dir; if (typeof aVal === "number" && typeof bVal === "number") return (aVal - bVal) * dir; return 0; }); }, [filteredSystems, sortStatus]); const PercentCell = ({ value }: { value: number }) => ( {formatPercent(value)} ); const columns = useMemo((): DataTableColumn[] => { const cols: (DataTableColumn | false)[] = [ { accessor: "name", width: 160, ellipsis: true, title: ( {t("column.system")} ), sortable: true, render: (record) => ( {record.name} ), }, options.showCpu && { accessor: "cpu", width: 140, title: ( {t("column.cpu")} ), sortable: true, render: (record) => , }, options.showMemory && { accessor: "memory", width: 140, title: ( {t("column.memory")} ), sortable: true, render: (record) => , }, options.showDisk && { accessor: "disk", width: 160, title: ( {t("column.disk")} ), sortable: true, render: (record) => ( ), }, options.showGpu && { accessor: "gpu", width: 140, title: ( {t("column.gpu")} ), sortable: true, render: (record) => , }, options.showLoadAvg && { accessor: "loadAvg", width: 100, title: ( {t("column.loadAvg")} ), sortable: true, render: (record) => ( {formatLoadAvg(record.loadAvg)} ), }, options.showNet && { accessor: "netBytes", width: 100, title: ( {t("column.net")} ), sortable: true, render: (record) => ( {formatByteRate(record.netBytes)} ), }, options.showTemp && { accessor: "temp", width: 80, title: ( {t("column.temp")} ), sortable: true, render: (record) => {formatTemp(record.temp, false)}, }, options.showBattery && { accessor: "battery", width: 70, title: ( {t("column.battery")} ), render: (record) => {record.battery ? `${record.battery[0]}%` : "—"}, }, options.showServices && { accessor: "services", width: 80, title: ( {t("column.services")} ), sortable: true, render: (record) => {record.services}, }, options.showUptime && { accessor: "uptime", width: 90, title: ( {t("column.uptime")} ), sortable: true, render: (record) => {formatUptime(record.uptime)}, }, options.showAgent && { accessor: "agentVersion", width: 90, title: ( {t("column.agent")} ), sortable: true, render: (record) => {record.agentVersion}, }, ]; return cols.filter(Boolean) as DataTableColumn[]; }, [ options.showAgent, options.showBattery, options.showCpu, options.showDisk, options.showGpu, options.showLoadAvg, options.showMemory, options.showNet, options.showServices, options.showTemp, options.showUptime, size, t, ]); const { effectiveColumns, storeKey } = usePersistedTableLayout({ columns, columnAccessors, columnOrder: options.columnOrder, columnWidths: options.columnWidths, itemId, storeKeyPrefix: "beszel-system-table", onLayoutChange: persistLayout, }); const handleRowClick = useCallback( ({ record }: { record: SystemRowWithKey }) => { const integrationId = record._key.split(":")[0] ?? ""; openModal({ integrationId, systemId: record.id }, { title: record.name }); }, [openModal], ); if (systemsError) throw systemsError; if (isPending) { return (
); } return (
); }