import { useState } from "react"; import { ChevronDown, ChevronRight, Network, ArrowUp, ArrowDown, } from "lucide-react"; import { useTranslation } from "react-i18next"; import { useManagerData, useManagerAction } from "./useManagerData"; import { ManagerCardShell } from "./ManagerCardShell"; interface WireGuardPeer { publicKey: string; endpoint: string | null; allowedIPs: string[]; latestHandshake: number | null; rxBytes: number; txBytes: number; } interface WireGuardInterface { name: string; publicKey: string | null; listenPort: number | null; up: boolean; peers: WireGuardPeer[]; } interface WireGuardData { installed: boolean; interfaces: WireGuardInterface[]; } function fmtBytes(n: number): string { if (n >= 1024 * 1024 * 1024) return `${(n / 1024 / 1024 / 1024).toFixed(1)}GB`; if (n >= 1024 * 1024) return `${(n / 1024 / 1024).toFixed(1)}MB`; if (n >= 1024) return `${(n / 1024).toFixed(1)}KB`; return `${n}B`; } function fmtHandshake(ts: number | null): string { if (!ts) return ""; const diff = Math.floor(Date.now() / 1000) - ts; if (diff < 60) return `${diff}s ago`; if (diff < 3600) return `${Math.floor(diff / 60)}m ago`; if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`; return `${Math.floor(diff / 86400)}d ago`; } function truncKey(key: string): string { return key.length > 12 ? `${key.slice(0, 12)}...` : key; } export function WireGuardManagerCard({ hostId }: { hostId: number | null }) { const { t } = useTranslation(); const { data, loading, error, refresh } = useManagerData( hostId, "wireguard", ); const { busy, run } = useManagerAction(hostId); const [expanded, setExpanded] = useState>(new Set()); const toggleExpand = (name: string) => { setExpanded((prev) => { const next = new Set(prev); if (next.has(name)) next.delete(name); else next.add(name); return next; }); }; const handleAction = async (iface: WireGuardInterface) => { const action = iface.up ? "down" : "up"; await run( "wireguard", { interface: iface.name, action }, { action: "action", loadingMsg: action === "up" ? t("hostMetrics.managers.wgBringingUp", { name: iface.name }) : t("hostMetrics.managers.wgBringingDown", { name: iface.name }), successMsg: action === "up" ? t("hostMetrics.managers.wgInterfaceUpDone", { name: iface.name }) : t("hostMetrics.managers.wgInterfaceDownDone", { name: iface.name, }), onDone: refresh, }, ); }; const isNotInstalled = data && !data.installed; const isEmpty = data?.installed && data.interfaces.length === 0; return ( } loading={loading} error={error} onRefresh={refresh} empty={isNotInstalled || isEmpty} emptyMessage={ isNotInstalled ? t("hostMetrics.managers.wgNotInstalled") : t("hostMetrics.managers.wgNoInterfaces") } >
{data?.interfaces.map((iface) => (
{expanded.has(iface.name) && (
{iface.peers.length === 0 ? ( No peers ) : ( iface.peers.map((peer) => (
{truncKey(peer.publicKey)} {peer.endpoint && ( {peer.endpoint} )}
{peer.allowedIPs.length > 0 && ( {peer.allowedIPs.join(", ")} )}
{t("hostMetrics.managers.wgLastHandshake")}:{" "} {peer.latestHandshake ? fmtHandshake(peer.latestHandshake) : t("hostMetrics.managers.wgHandshakeNever")} {fmtBytes(peer.rxBytes)} / {fmtBytes(peer.txBytes)}
)) )}
)}
))}
); }