2026-06-04 15:16:53 -04:00
|
|
|
import { Cable, Container, Network, Wifi, WifiOff } from "lucide-react";
|
2026-05-11 01:23:11 -05:00
|
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
|
import type { ServerMetrics } from "@/main-axios.ts";
|
|
|
|
|
import { SectionCard } from "@/components/section-card";
|
|
|
|
|
|
|
|
|
|
interface NetworkWidgetProps {
|
|
|
|
|
metrics: ServerMetrics | null;
|
|
|
|
|
metricsHistory: ServerMetrics[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function NetworkWidget({ metrics }: NetworkWidgetProps) {
|
|
|
|
|
const { t } = useTranslation();
|
|
|
|
|
|
|
|
|
|
const metricsWithNetwork = metrics as ServerMetrics & {
|
|
|
|
|
network?: {
|
|
|
|
|
interfaces?: Array<{
|
|
|
|
|
name: string;
|
|
|
|
|
state: string;
|
|
|
|
|
ip: string;
|
|
|
|
|
rx?: string;
|
|
|
|
|
tx?: string;
|
|
|
|
|
}>;
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
const interfaces = metricsWithNetwork?.network?.interfaces ?? [];
|
|
|
|
|
|
2026-06-04 15:16:53 -04:00
|
|
|
const ifaceIcon = (name: string) => {
|
|
|
|
|
if (/^(wl|wlan|wifi|ath)/.test(name))
|
|
|
|
|
return <Wifi className="size-3 text-muted-foreground/50" />;
|
|
|
|
|
if (/^(eth|en|enp|eno|ens)/.test(name))
|
|
|
|
|
return <Cable className="size-3 text-muted-foreground/50" />;
|
|
|
|
|
if (/^(docker|br-|veth|virbr|vlan|bond|tun|tap|wg|lo)/.test(name))
|
|
|
|
|
return <Container className="size-3 text-muted-foreground/50" />;
|
|
|
|
|
return <Network className="size-3 text-muted-foreground/50" />;
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-11 01:23:11 -05:00
|
|
|
return (
|
|
|
|
|
<SectionCard
|
|
|
|
|
title={t("serverStats.networkInterfaces")}
|
|
|
|
|
icon={<Network className="size-3.5" />}
|
|
|
|
|
>
|
|
|
|
|
<div className="flex flex-col gap-2 py-1">
|
|
|
|
|
{interfaces.length === 0 ? (
|
|
|
|
|
<div className="flex flex-col items-center justify-center py-6 text-muted-foreground gap-2">
|
|
|
|
|
<WifiOff className="size-6 opacity-40" />
|
|
|
|
|
<span className="text-xs">
|
|
|
|
|
{t("serverStats.noInterfacesFound")}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
2026-05-28 22:29:20 -05:00
|
|
|
<div className="flex flex-col gap-2 overflow-y-auto max-h-[260px]">
|
|
|
|
|
{interfaces.map((iface, i) => (
|
|
|
|
|
<div
|
|
|
|
|
key={i}
|
|
|
|
|
className="flex flex-col p-2 border border-border bg-muted/30 gap-1"
|
|
|
|
|
>
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div className="flex items-center gap-2">
|
2026-06-04 15:16:53 -04:00
|
|
|
{ifaceIcon(iface.name)}
|
2026-05-28 22:29:20 -05:00
|
|
|
<div
|
|
|
|
|
className={`size-1.5 rounded-full ${iface.state === "UP" ? "bg-accent-brand" : "bg-muted-foreground"}`}
|
|
|
|
|
/>
|
|
|
|
|
<span className="text-sm font-bold font-mono">
|
|
|
|
|
{iface.name}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<span className="text-[10px] font-semibold px-1.5 py-px border border-border text-muted-foreground uppercase">
|
|
|
|
|
{iface.state}
|
2026-05-11 01:23:11 -05:00
|
|
|
</span>
|
|
|
|
|
</div>
|
2026-05-28 22:29:20 -05:00
|
|
|
<div className="flex justify-between text-[10px] font-mono text-muted-foreground">
|
|
|
|
|
<span>{iface.ip}</span>
|
|
|
|
|
{(iface.rx || iface.tx) && (
|
|
|
|
|
<span>
|
|
|
|
|
↓ {iface.rx ?? "—"} / ↑ {iface.tx ?? "—"}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2026-05-11 01:23:11 -05:00
|
|
|
</div>
|
2026-05-28 22:29:20 -05:00
|
|
|
))}
|
|
|
|
|
</div>
|
2026-05-11 01:23:11 -05:00
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</SectionCard>
|
|
|
|
|
);
|
|
|
|
|
}
|