diff --git a/AppImage/components/network-metrics.tsx b/AppImage/components/network-metrics.tsx index e1a6465..d44760d 100644 --- a/AppImage/components/network-metrics.tsx +++ b/AppImage/components/network-metrics.tsx @@ -4,9 +4,10 @@ import { useState } from "react" import { Card, CardContent, CardHeader, CardTitle } from "./ui/card" import { Badge } from "./ui/badge" import { Dialog, DialogContent, DialogHeader, DialogTitle } from "./ui/dialog" -import { Wifi, Activity, Network, Router, AlertCircle, Zap } from "lucide-react" +import { Wifi, Activity, Network, Router, AlertCircle, Zap, Info } from "lucide-react" import useSWR from "swr" import { NetworkTrafficChart } from "./network-traffic-chart" +import { VMNetworkChart } from "./vm-lxc-network-chart" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select" interface NetworkData { @@ -343,100 +344,110 @@ export function NetworkMetrics() { - {/* Physical Interfaces section */} - - - - - Physical Interfaces - - {networkData.physical_active_count ?? 0}/{networkData.physical_total_count ?? 0} Active - - - - -
- {networkData.physical_interfaces.map((interface_, index) => { - const typeBadge = getInterfaceTypeBadge(interface_.type) - - return ( -
setSelectedInterface(interface_)} - > - {/* First row: Icon, Name, Type Badge, Status */} -
- -
-
{interface_.name}
- - {typeBadge.label} - -
- - {interface_.status.toUpperCase()} - -
- - {/* Second row: Details - Responsive layout */} -
-
-
IP Address
-
- {interface_.addresses.length > 0 ? interface_.addresses[0].ip : "N/A"} -
-
- -
-
Speed
-
- - {formatSpeed(interface_.speed)} -
-
- -
-
Traffic
-
- ↓ {formatBytes(interface_.bytes_recv)} - {" / "} - ↑ {formatBytes(interface_.bytes_sent)} -
-
- - {interface_.mac_address && ( -
-
MAC
-
- {interface_.mac_address} -
-
- )} -
-
- ) - })} -
-
-
- - {networkData.bridge_interfaces && networkData.bridge_interfaces.length > 0 && ( + {/* VM & LXC Network Interfaces section */} + {networkData.vm_lxc_interfaces && networkData.vm_lxc_interfaces.length > 0 && ( + + VM & LXC Network Interfaces + + {networkData.vm_lxc_active_count ?? 0} / {networkData.vm_lxc_total_count ?? 0} Active + + + + +
+ {networkData.vm_lxc_interfaces.map((interface_, index) => { + const vmTypeBadge = getVMTypeBadge(interface_.vm_type) + + return ( +
setSelectedInterface(interface_)} + > + {/* First row: Icon, Name, VM/LXC Badge, VM Name, Status */} +
+ +
+
{interface_.name}
+ + {vmTypeBadge.label} + + {interface_.vm_name && ( +
→ {interface_.vm_name}
+ )} +
+ + {interface_.status.toUpperCase()} + +
+ + {/* Second row: Details - Responsive layout */} +
+
+
VMID
+
{interface_.vmid ?? "N/A"}
+
+ +
+
Speed
+
+ + {formatSpeed(interface_.speed)} +
+
+ +
+
Traffic
+
+ ↓ {formatBytes(interface_.bytes_recv)} + {" / "} + ↑ {formatBytes(interface_.bytes_sent)} +
+
+ + {interface_.mac_address && ( +
+
MAC
+
+ {interface_.mac_address} +
+
+ )} +
+
+ ) + })} +
+
+
+ )} + + {networkData.bridge_interfaces && networkData.bridge_interfaces.length > 0 && ( + + + Bridge Interfaces - + {networkData.bridge_active_count ?? 0}/{networkData.bridge_total_count ?? 0} Active + + + Since Boot + @@ -542,93 +553,96 @@ export function NetworkMetrics() { )} - {/* VM & LXC Network Interfaces section */} - {networkData.vm_lxc_interfaces && networkData.vm_lxc_interfaces.length > 0 && ( - - - - - VM & LXC Network Interfaces - - {networkData.vm_lxc_active_count ?? 0} / {networkData.vm_lxc_total_count ?? 0} Active - - - - -
- {networkData.vm_lxc_interfaces.map((interface_, index) => { - const vmTypeBadge = getVMTypeBadge(interface_.vm_type) + + + + + Physical Interfaces + + {networkData.physical_active_count ?? 0}/{networkData.physical_total_count ?? 0} Active + + + + Since Boot + + + + +
+ {networkData.physical_interfaces.map((interface_, index) => { + const typeBadge = getInterfaceTypeBadge(interface_.type) - return ( -
setSelectedInterface(interface_)} - > - {/* First row: Icon, Name, VM/LXC Badge, VM Name, Status */} -
- -
-
{interface_.name}
- - {vmTypeBadge.label} - - {interface_.vm_name && ( -
→ {interface_.vm_name}
- )} -
- - {interface_.status.toUpperCase()} + return ( +
setSelectedInterface(interface_)} + > + {/* First row: Icon, Name, Type Badge, Status */} +
+ +
+
{interface_.name}
+ + {typeBadge.label}
- - {/* Second row: Details - Responsive layout */} -
-
-
VMID
-
{interface_.vmid ?? "N/A"}
-
- -
-
Speed
-
- - {formatSpeed(interface_.speed)} -
-
- -
-
Traffic
-
- ↓ {formatBytes(interface_.bytes_recv)} - {" / "} - ↑ {formatBytes(interface_.bytes_sent)} -
-
- - {interface_.mac_address && ( -
-
MAC
-
- {interface_.mac_address} -
-
- )} -
+ + {interface_.status.toUpperCase()} +
- ) - })} -
- - - )} + + {/* Second row: Details - Responsive layout */} +
+
+
IP Address
+
+ {interface_.addresses.length > 0 ? interface_.addresses[0].ip : "N/A"} +
+
+ +
+
Speed
+
+ + {formatSpeed(interface_.speed)} +
+
+ +
+
Traffic
+
+ ↓ {formatBytes(interface_.bytes_recv)} + {" / "} + ↑ {formatBytes(interface_.bytes_sent)} +
+
+ + {interface_.mac_address && ( +
+
MAC
+
+ {interface_.mac_address} +
+
+ )} +
+
+ ) + })} +
+ + {/* Interface Details Modal */} setSelectedInterface(null)}> @@ -832,6 +846,18 @@ export function NetworkMetrics() {
+ {/* Network chart for VM/LXC interfaces */} + {selectedInterface.type === "vm_lxc" && selectedInterface.vmid && selectedInterface.vm_type && ( +
+

Network Traffic History

+ +
+ )} + {/* Bond Information */} {selectedInterface.type === "bond" && selectedInterface.bond_slaves && (
diff --git a/AppImage/components/vm-lxc-network-chart.tsx b/AppImage/components/vm-lxc-network-chart.tsx new file mode 100644 index 0000000..c971a5d --- /dev/null +++ b/AppImage/components/vm-lxc-network-chart.tsx @@ -0,0 +1,195 @@ +"use client" + +import { useEffect, useState } from "react" +import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts" +import useSWR from "swr" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select" + +interface VMNetworkChartProps { + vmid: number + vmType: "qemu" | "lxc" + initialTimeframe?: "hour" | "day" | "week" | "month" | "year" +} + +const fetcher = async (url: string) => { + const response = await fetch(url, { + method: "GET", + headers: { "Content-Type": "application/json" }, + signal: AbortSignal.timeout(10000), + }) + if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`) + return response.json() +} + +const formatBytes = (bytes: number): string => { + if (bytes === 0) return "0 B" + const k = 1024 + const sizes = ["B", "KB", "MB", "GB", "TB"] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}` +} + +export function VMNetworkChart({ vmid, vmType, initialTimeframe = "day" }: VMNetworkChartProps) { + const [timeframe, setTimeframe] = useState<"hour" | "day" | "week" | "month" | "year">(initialTimeframe) + const [visibleLines, setVisibleLines] = useState({ received: true, sent: true }) + const [chartData, setChartData] = useState([]) + + const { data: rrdData } = useSWR(`/api/${vmType}/${vmid}/rrddata?timeframe=${timeframe}`, fetcher, { + refreshInterval: 30000, + revalidateOnFocus: false, + }) + + useEffect(() => { + if (!rrdData) return + + const transformedData = rrdData.map((point: any) => { + const timestamp = new Date(point.time * 1000) + const hours = timestamp.getHours().toString().padStart(2, "0") + const minutes = timestamp.getMinutes().toString().padStart(2, "0") + const month = (timestamp.getMonth() + 1).toString().padStart(2, "0") + const day = timestamp.getDate().toString().padStart(2, "0") + + let timeLabel = `${hours}:${minutes}` + if (timeframe === "week" || timeframe === "month") { + timeLabel = `${month}/${day}` + } else if (timeframe === "year") { + timeLabel = `${month}/${day}` + } + + // Calculate traffic in GB for the interval + const intervalSeconds = timeframe === "hour" ? 60 : timeframe === "day" ? 60 : timeframe === "week" ? 1800 : 3600 + const receivedGB = ((point.netin || 0) * intervalSeconds) / (1024 * 1024 * 1024) + const sentGB = ((point.netout || 0) * intervalSeconds) / (1024 * 1024 * 1024) + + return { + time: timeLabel, + received: receivedGB, + sent: sentGB, + } + }) + + setChartData(transformedData) + }, [rrdData, timeframe]) + + const toggleLine = (line: "received" | "sent") => { + setVisibleLines((prev) => ({ ...prev, [line]: !prev[line] })) + } + + const getTimeframeLabel = () => { + switch (timeframe) { + case "hour": + return "1 Hour" + case "day": + return "24 Hours" + case "week": + return "7 Days" + case "month": + return "30 Days" + case "year": + return "1 Year" + default: + return "24 Hours" + } + } + + const CustomTooltip = ({ active, payload }: any) => { + if (active && payload && payload.length) { + return ( +
+

{payload[0].payload.time}

+ {payload.map((entry: any, index: number) => ( +
+ + + {entry.name}: + + {formatBytes(entry.value * 1024 * 1024 * 1024)} +
+ ))} +
+ ) + } + return null + } + + return ( +
+ {/* Timeframe Selector */} +
+ +
+ + {/* Interactive Legend */} +
+ + +
+ + {/* Chart */} + + + + + + + + + + + + + + + { + if (value === 0) return "0" + if (value < 0.01) return `${(value * 1024).toFixed(0)} MB` + return `${value.toFixed(2)} GB` + }} + label={{ value: "GB", angle: -90, position: "insideLeft", style: { fill: "hsl(var(--muted-foreground))" } }} + /> + } /> + {visibleLines.received && ( + + )} + {visibleLines.sent && ( + + )} + + +
+ ) +}