"use client"; import type { MantineSize } from "@mantine/core"; import { Badge, Box, Card, Center, Group, Loader, Progress, Text, Stack } from "@mantine/core"; import { Activity, Battery, BatteryCharging, Cpu, HardDrive, MemoryStick, Monitor, Network, Server, Thermometer, Wifi, } from "lucide-react"; import { IconServerOff } from "@tabler/icons-react"; import { clientApi } from "@homarr/api/client"; import { useRequiredBoard } from "@homarr/boards/context"; import { useModalAction } from "@homarr/modals"; import { useScopedI18n } from "@homarr/translation/client"; import classes from "./component.module.css"; import type { WidgetComponentProps } from "../definition"; import type { BeszelSystemRow } from "../beszel/_shared/types"; import { 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"; interface SizeConfig { iconSize: number; fontSize: string; progressSize: "xs" | "sm"; labelMiw: number; valueMiw: number; rowHeight: number; cardPadding: number; badgeHeight: number; badgeSize: "xs" | "sm"; gap: number; } const defaultSizeConfig: SizeConfig = { iconSize: 10, fontSize: "10px", progressSize: "xs", labelMiw: 40, valueMiw: 30, rowHeight: 18, cardPadding: 4, badgeHeight: 20, badgeSize: "xs", gap: 2, }; const sizeThresholds: [number, SizeConfig][] = [ [0, defaultSizeConfig], [ 160, { iconSize: 12, fontSize: "xs", progressSize: "xs", labelMiw: 50, valueMiw: 34, rowHeight: 22, cardPadding: 6, badgeHeight: 22, badgeSize: "xs", gap: 6, }, ], [ 280, { iconSize: 14, fontSize: "xs", progressSize: "sm", labelMiw: 55, valueMiw: 38, rowHeight: 24, cardPadding: 8, badgeHeight: 26, badgeSize: "sm", gap: 8, }, ], ]; const getSizeConfig = (cellWidth: number, cellHeight: number): SizeConfig => { const basis = Math.min(cellWidth, cellHeight); let config = defaultSizeConfig; for (const [threshold, cfg] of sizeThresholds) { if (basis >= threshold) config = cfg; } return config; }; const getMaxVisibleMetrics = (cellHeight: number, size: SizeConfig): number => { const available = cellHeight - size.cardPadding * 2 - size.badgeHeight - size.gap; return Math.max(1, Math.floor(available / size.rowHeight)); }; const MIN_CELL_WIDTH = 180; const MIN_CELL_HEIGHT = 80; const getColCount = (width: number, height: number, itemCount: number): number => { if (itemCount <= 1) return 1; const maxCols = Math.min(itemCount, Math.max(1, Math.floor(width / MIN_CELL_WIDTH))); const aspectRatio = width / Math.max(height, MIN_CELL_HEIGHT); return Math.min(maxCols, Math.max(1, Math.ceil(Math.sqrt(itemCount * aspectRatio)))); }; interface MetricRowProps { icon: React.ReactNode; label: string; value: string; progress?: { value: number; color: string }; size: SizeConfig; } const MetricValue = ({ value, progress, size }: Pick) => ( {value} {progress && ( )} ); const MetricRow = ({ icon, label, value, progress, size }: MetricRowProps) => ( {icon} {label} ); interface SystemCardProps { system: BeszelSystemRow & { _key: string }; options: WidgetComponentProps<"beszelSystemGrid">["options"]; t: ReturnType>; size: SizeConfig; maxMetrics: number; itemRadius: MantineSize; onClick?: () => void; } const metricRenderers = [ { key: "showCpu", render: (s: BeszelSystemRow, t: SystemCardProps["t"], sz: SizeConfig) => ( } label={t("metric.cpu")} value={formatPercent(s.cpu)} progress={{ value: s.cpu, color: thresholdColor(s.cpu) }} size={sz} /> ), visible: (s: BeszelSystemRow, o: SystemCardProps["options"]) => o.showCpu, }, { key: "showMemory", render: (s: BeszelSystemRow, t: SystemCardProps["t"], sz: SizeConfig) => ( } label={t("metric.memory")} value={formatPercent(s.memory)} progress={{ value: s.memory, color: thresholdColor(s.memory) }} size={sz} /> ), visible: (s: BeszelSystemRow, o: SystemCardProps["options"]) => o.showMemory, }, { key: "showDisk", render: (s: BeszelSystemRow, t: SystemCardProps["t"], sz: SizeConfig) => ( {t("metric.disk")} ), visible: (s: BeszelSystemRow, o: SystemCardProps["options"]) => o.showDisk, }, { key: "showGpu", render: (s: BeszelSystemRow, t: SystemCardProps["t"], sz: SizeConfig) => ( } label={t("metric.gpu")} value={formatPercent(s.gpu)} progress={{ value: s.gpu, color: thresholdColor(s.gpu) }} size={sz} /> ), visible: (s: BeszelSystemRow, o: SystemCardProps["options"]) => o.showGpu && s.gpu > 0, }, { key: "showLoadAvg", render: (s: BeszelSystemRow, t: SystemCardProps["t"], sz: SizeConfig) => ( } label={t("metric.loadAvg")} value={formatLoadAvg(s.loadAvg)} size={sz} /> ), visible: (s: BeszelSystemRow, o: SystemCardProps["options"]) => o.showLoadAvg && s.loadAvg !== null, }, { key: "showNet", render: (s: BeszelSystemRow, t: SystemCardProps["t"], sz: SizeConfig) => ( } label={t("metric.net")} value={formatByteRate(s.netBytes)} size={sz} /> ), visible: (s: BeszelSystemRow, o: SystemCardProps["options"]) => o.showNet, }, { key: "showTemp", render: (s: BeszelSystemRow, t: SystemCardProps["t"], sz: SizeConfig) => ( } label={t("metric.temp")} value={formatTemp(s.temp, false)} size={sz} /> ), visible: (s: BeszelSystemRow, o: SystemCardProps["options"]) => o.showTemp && s.temp !== null, }, { key: "showBattery", render: (s: BeszelSystemRow, t: SystemCardProps["t"], sz: SizeConfig) => { const batteryIcons = [Battery, BatteryCharging] as const; const Icon = batteryIcons[Math.min(1, s.battery?.[1] ?? 0)] ?? Battery; return ( } label={t("metric.battery")} value={`${s.battery?.[0] ?? 0}%`} size={sz} /> ); }, visible: (s: BeszelSystemRow, o: SystemCardProps["options"]) => o.showBattery && s.battery !== null, }, { key: "showServices", render: (s: BeszelSystemRow, t: SystemCardProps["t"], sz: SizeConfig) => ( } label={t("metric.services")} value={String(s.services)} size={sz} /> ), visible: (s: BeszelSystemRow, o: SystemCardProps["options"]) => o.showServices, }, { key: "showUptime", render: (s: BeszelSystemRow, t: SystemCardProps["t"], sz: SizeConfig) => ( } label={t("metric.uptime")} value={formatUptime(s.uptime)} size={sz} /> ), visible: (s: BeszelSystemRow, o: SystemCardProps["options"]) => o.showUptime, }, { key: "showAgent", render: (s: BeszelSystemRow, t: SystemCardProps["t"], sz: SizeConfig) => ( } label={t("metric.agent")} value={s.agentVersion} size={sz} /> ), visible: (s: BeszelSystemRow, o: SystemCardProps["options"]) => o.showAgent, }, ] as const; const SystemCard = ({ system, options, t, size, maxMetrics, itemRadius, onClick }: SystemCardProps) => { const visibleMetrics = metricRenderers.filter((m) => m.visible(system, options)).slice(0, maxMetrics); return ( {system.name} {visibleMetrics.map((m) => m.render(system, t, size))} ); }; export default function BeszelSystemGridWidget({ options, integrationIds, isEditMode, width, height, }: WidgetComponentProps<"beszelSystemGrid">) { const t = useScopedI18n("widget.beszelSystemGrid"); const board = useRequiredBoard(); const { openModal } = useModalAction(BeszelSystemStatsModal); const { data: results = [], error: systemsError, isPending, } = clientApi.widget.beszel.getSystems.useQuery({ integrationIds }); const filteredSystems = useBeszelFilteredSystems(results, options.statusFilter); if (systemsError) throw systemsError; if (isPending) { return (
); } if (filteredSystems.length === 0) { return (
{t("empty.noSystems")}
); } const cols = getColCount(width, height, filteredSystems.length); const rows = Math.ceil(filteredSystems.length / cols) || 1; const rawCellHeight = height / rows; const scrollEnabled = rawCellHeight < MIN_CELL_HEIGHT; const effectiveCellHeight = scrollEnabled ? MIN_CELL_HEIGHT : rawCellHeight; const cellWidth = width / cols; const size = getSizeConfig(cellWidth, effectiveCellHeight); const maxMetrics = getMaxVisibleMetrics(effectiveCellHeight, size); return ( {filteredSystems.map((system) => { const integrationId = system._key.split(":")[0] ?? ""; const handleClick = isEditMode ? undefined : () => openModal({ integrationId, systemId: system.id }, { title: system.name }); return ( ); })} ); }