import { useState, useEffect } from "react"; import { Cable, Container, Network, Wifi, WifiOff } from "lucide-react"; import { useTranslation } from "react-i18next"; import type { ServerMetrics } from "@/main-axios"; import { MetricCard } from "@/components/metric-card"; import { LineChart, type LineChartSeries } from "@/components/charts/LineChart"; import { getMetricsHistory, type MetricsHistoryRow, } from "@/api/host-metrics-api"; import { CardTimeTabs, type HistoryTab } from "./CardTimeTabs"; function ifaceIcon(name: string) { if (/^(wl|wlan|wifi|ath)/.test(name)) return ; if (/^(eth|en|enp|eno|ens)/.test(name)) return ; if (/^(docker|br-|veth|virbr|vlan|bond|tun|tap|wg|lo)/.test(name)) return ; return ; } function computeNetRates(rows: MetricsHistoryRow[]): { rxRates: Array; txRates: Array; } { const rxRates: Array = []; const txRates: Array = []; for (let i = 0; i < rows.length; i++) { if (i === 0) { rxRates.push(null); txRates.push(null); continue; } const prev = rows[i - 1]; const curr = rows[i]; const dtSec = (new Date(curr.ts).getTime() - new Date(prev.ts).getTime()) / 1000; if ( dtSec < 0.5 || prev.net_rx_bytes == null || curr.net_rx_bytes == null || prev.net_tx_bytes == null || curr.net_tx_bytes == null ) { rxRates.push(null); txRates.push(null); continue; } const rxDelta = curr.net_rx_bytes - prev.net_rx_bytes; const txDelta = curr.net_tx_bytes - prev.net_tx_bytes; rxRates.push(rxDelta < 0 ? null : rxDelta / dtSec); txRates.push(txDelta < 0 ? null : txDelta / dtSec); } return { rxRates, txRates }; } function formatBytes(bps: number): string { if (bps >= 1024 * 1024) return `${(bps / 1024 / 1024).toFixed(1)} MB/s`; if (bps >= 1024) return `${(bps / 1024).toFixed(1)} KB/s`; return `${Math.round(bps)} B/s`; } export function NetworkCard({ metrics, hostId, }: { metrics: ServerMetrics | null; hostId: number | null; }) { const { t } = useTranslation(); const [activeTab, setActiveTab] = useState("live"); const [rows, setRows] = useState(null); const [loading, setLoading] = useState(false); useEffect(() => { if (activeTab === "live" || hostId == null) return; let cancelled = false; setLoading(true); setRows(null); getMetricsHistory(hostId, { range: activeTab }) .then((res) => { if (!cancelled) { setRows(res.rows); setLoading(false); } }) .catch(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [activeTab, hostId]); const interfaces = metrics?.network?.interfaces ?? []; const { rxRates, txRates } = rows ? computeNetRates(rows) : { rxRates: [], txRates: [] }; const netMax = Math.max( rxRates.reduce((m, v) => (v != null && v > m ? v : m), 0), txRates.reduce((m, v) => (v != null && v > m ? v : m), 0), 1024, ); const timestamps = rows?.map((r) => r.ts) ?? []; const hasNetData = rxRates.some((v) => v !== null); const rxSeries: LineChartSeries = { key: "rx", label: t("metricsHistory.download"), color: "var(--chart-2)", data: rxRates, }; const txSeries: LineChartSeries = { key: "tx", label: t("metricsHistory.upload"), color: "var(--chart-5)", data: txRates, }; return ( } scroll={activeTab === "live"} action={ hostId != null ? ( ) : undefined } > {activeTab === "live" && (interfaces.length === 0 ? (
{t("hostMetrics.noInterfacesFound")}
) : (
{interfaces.map((iface, i) => (
{ifaceIcon(iface.name)} {iface.name}
{iface.state}
{iface.ip} {(iface.rxRateBps != null || iface.txRateBps != null) && ( ↓{" "} {iface.rxRateBps == null ? "—" : formatBytes(iface.rxRateBps)}{" "} / ↑{" "} {iface.txRateBps == null ? "—" : formatBytes(iface.txRateBps)} )}
))}
))} {activeTab !== "live" && ( <> {loading && (
{t("common.loading")}
)} {!loading && rows !== null && !hasNetData && (
{t("metricsHistory.noData")}
)} {!loading && hasNetData && ( )} )}
); }