diff --git a/AppImage/components/network-card.tsx b/AppImage/components/network-card.tsx index 06f8f95..bb658a2 100644 --- a/AppImage/components/network-card.tsx +++ b/AppImage/components/network-card.tsx @@ -2,9 +2,10 @@ import { Card, CardContent } from "./ui/card" import { Badge } from "./ui/badge" -import { Wifi, Zap } from "lucide-react" +import { Wifi, Zap } from 'lucide-react' import { useState, useEffect } from "react" import { fetchApi } from "../lib/api-config" +import { formatNetworkTraffic, getNetworkUnit } from "../lib/format-network" interface NetworkCardProps { interface_: { @@ -59,39 +60,37 @@ const getVMTypeBadge = (vmType: string | undefined) => { return { color: "bg-gray-500/10 text-gray-500 border-gray-500/20", label: "Unknown" } } -const formatBytes = (bytes: number | undefined): string => { - if (!bytes || 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]}` -} - const formatSpeed = (speed: number): string => { if (speed === 0) return "N/A" if (speed >= 1000) return `${(speed / 1000).toFixed(1)} Gbps` return `${speed} Mbps` } -const formatStorage = (bytes: number): string => { - if (bytes === 0) return "0 B" - const k = 1024 - const sizes = ["B", "KB", "MB", "GB", "TB", "PB"] - const i = Math.floor(Math.log(bytes) / Math.log(k)) - const value = bytes / Math.pow(k, i) - const decimals = value >= 10 ? 1 : 2 - return `${value.toFixed(decimals)} ${sizes[i]}` -} - export function NetworkCard({ interface_, timeframe, onClick }: NetworkCardProps) { const typeBadge = getInterfaceTypeBadge(interface_.type) const vmTypeBadge = interface_.vm_type ? getVMTypeBadge(interface_.vm_type) : null + const [networkUnit, setNetworkUnit] = useState<"Bytes" | "Bits">(getNetworkUnit()) + const [trafficData, setTrafficData] = useState<{ received: number; sent: number }>({ received: 0, sent: 0, }) + useEffect(() => { + const handleUnitChange = () => { + setNetworkUnit(getNetworkUnit()) + } + + window.addEventListener("networkUnitChanged", handleUnitChange) + window.addEventListener("storage", handleUnitChange) + + return () => { + window.removeEventListener("networkUnitChanged", handleUnitChange) + window.removeEventListener("storage", handleUnitChange) + } + }, []) + useEffect(() => { const fetchTrafficData = async () => { try { @@ -207,15 +206,15 @@ export function NetworkCard({ interface_, timeframe, onClick }: NetworkCardProps
{interface_.status.toLowerCase() === "up" && interface_.vm_type !== "vm" ? ( <> - ↓ {formatStorage(trafficData.received * 1024 * 1024 * 1024)} + ↓ {formatNetworkTraffic(trafficData.received * 1024 * 1024 * 1024, networkUnit)} {" / "} - ↑ {formatStorage(trafficData.sent * 1024 * 1024 * 1024)} + ↑ {formatNetworkTraffic(trafficData.sent * 1024 * 1024 * 1024, networkUnit)} ) : ( <> - ↓ {formatBytes(interface_.bytes_recv)} + ↓ {formatNetworkTraffic(interface_.bytes_recv || 0, networkUnit)} {" / "} - ↑ {formatBytes(interface_.bytes_sent)} + ↑ {formatNetworkTraffic(interface_.bytes_sent || 0, networkUnit)} )}
diff --git a/AppImage/components/network-metrics.tsx b/AppImage/components/network-metrics.tsx index ff67ef7..c6737b9 100644 --- a/AppImage/components/network-metrics.tsx +++ b/AppImage/components/network-metrics.tsx @@ -9,6 +9,7 @@ import useSWR from "swr" import { NetworkTrafficChart } from "./network-traffic-chart" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select" import { fetchApi } from "../lib/api-config" +import { formatNetworkTraffic, getNetworkUnit } from "../lib/format-network" interface NetworkData { interfaces: NetworkInterface[] @@ -132,11 +133,6 @@ const fetcher = async (url: string): Promise => { return fetchApi(url) } -const getUnitsSettings = (): "Bytes" | "Bits" => { - if (typeof window === "undefined") return "Bytes" - const raw = localStorage.getItem("proxmenux-network-unit") - return raw && raw.toLowerCase() === "bits" ? "Bits" : "Bytes" -} export function NetworkMetrics() { const { @@ -155,10 +151,10 @@ export function NetworkMetrics() { const [networkTotals, setNetworkTotals] = useState<{ received: number; sent: number }>({ received: 0, sent: 0 }) const [interfaceTotals, setInterfaceTotals] = useState<{ received: number; sent: number }>({ received: 0, sent: 0 }) - const [networkUnit, setNetworkUnit] = useState<"Bytes" | "Bits">("Bytes") + const [networkUnit, setNetworkUnit] = useState<"Bytes" | "Bits">(() => getNetworkUnit()) useEffect(() => { - setNetworkUnit(getUnitsSettings()) + setNetworkUnit(getNetworkUnit()) const handleUnitChange = (e: CustomEvent) => { setNetworkUnit(e.detail === "Bits" ? "Bits" : "Bytes") @@ -210,8 +206,8 @@ export function NetworkMetrics() { ) } - const trafficInFormatted = formatStorage(networkTotals.received * 1024 * 1024 * 1024) // Convert GB to bytes - const trafficOutFormatted = formatStorage(networkTotals.sent * 1024 * 1024 * 1024) + const trafficInFormatted = formatNetworkTraffic(networkTotals.received * 1024 * 1024 * 1024, networkUnit) + const trafficOutFormatted = formatNetworkTraffic(networkTotals.sent * 1024 * 1024 * 1024, networkUnit) const packetsRecvK = networkData.traffic.packets_recv ? (networkData.traffic.packets_recv / 1000).toFixed(0) : "0" const totalErrors = (networkData.traffic.errin || 0) + (networkData.traffic.errout || 0) @@ -731,13 +727,6 @@ export function NetworkMetrics() { const displayInterface = currentInterfaceData || selectedInterface - console.log("[v0] Selected Interface:", selectedInterface.name) - console.log("[v0] Selected Interface bytes_recv:", selectedInterface.bytes_recv) - console.log("[v0] Selected Interface bytes_sent:", selectedInterface.bytes_sent) - console.log("[v0] Display Interface bytes_recv:", displayInterface.bytes_recv) - console.log("[v0] Display Interface bytes_sent:", displayInterface.bytes_sent) - console.log("[v0] Modal Network Data available:", !!modalNetworkData) - return ( <> {/* Basic Information */} @@ -888,29 +877,32 @@ export function NetworkMetrics() { )
- {/* Traffic Data - Top Row */}
-
Bytes Received
+
+ {networkUnit === "Bits" ? "Bits Received" : "Bytes Received"} +
- {formatStorage(interfaceTotals.received * 1024 * 1024 * 1024)} + {formatNetworkTraffic(interfaceTotals.received * 1024 * 1024 * 1024, networkUnit)}
-
Bytes Sent
+
+ {networkUnit === "Bits" ? "Bits Sent" : "Bytes Sent"} +
- {formatStorage(interfaceTotals.sent * 1024 * 1024 * 1024)} + {formatNetworkTraffic(interfaceTotals.sent * 1024 * 1024 * 1024, networkUnit)}
- {/* Network Traffic Chart - Full Width Below */}
@@ -951,15 +943,19 @@ export function NetworkMetrics() {

Traffic since last boot

-
Bytes Received
+
+ {networkUnit === "Bits" ? "Bits Received" : "Bytes Received"} +
- {formatBytes(displayInterface.bytes_recv)} + {formatNetworkTraffic(displayInterface.bytes_recv || 0, networkUnit)}
-
Bytes Sent
+
+ {networkUnit === "Bits" ? "Bits Sent" : "Bytes Sent"} +
- {formatBytes(displayInterface.bytes_sent)} + {formatNetworkTraffic(displayInterface.bytes_sent || 0, networkUnit)}
diff --git a/AppImage/components/network-traffic-chart.tsx b/AppImage/components/network-traffic-chart.tsx index e3a6c8e..8515db8 100644 --- a/AppImage/components/network-traffic-chart.tsx +++ b/AppImage/components/network-traffic-chart.tsx @@ -4,6 +4,7 @@ import { useState, useEffect } from "react" import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from "recharts" import { Loader2 } from 'lucide-react' import { fetchApi } from "@/lib/api-config" +import { getNetworkUnit } from "@/lib/format-network" interface NetworkMetricsData { time: string @@ -47,7 +48,7 @@ export function NetworkTrafficChart({ interfaceName, onTotalsCalculated, refreshInterval = 60000, - networkUnit = "Bytes", // Default to Bytes + networkUnit: networkUnitProp, // Rename prop to avoid conflict }: NetworkTrafficChartProps) { const [data, setData] = useState([]) const [loading, setLoading] = useState(true) @@ -57,11 +58,36 @@ export function NetworkTrafficChart({ netIn: true, netOut: true, }) + + const [networkUnit, setNetworkUnit] = useState<"Bytes" | "Bits">( + networkUnitProp || getNetworkUnit() + ) + + useEffect(() => { + const handleUnitChange = () => { + const newUnit = getNetworkUnit() + setNetworkUnit(newUnit) + } + + window.addEventListener("networkUnitChanged", handleUnitChange) + window.addEventListener("storage", handleUnitChange) + + return () => { + window.removeEventListener("networkUnitChanged", handleUnitChange) + window.removeEventListener("storage", handleUnitChange) + } + }, []) + + useEffect(() => { + if (networkUnitProp) { + setNetworkUnit(networkUnitProp) + } + }, [networkUnitProp]) useEffect(() => { setIsInitialLoad(true) fetchMetrics() - }, [timeframe, interfaceName, networkUnit]) // Added networkUnit to dependencies + }, [timeframe, interfaceName, networkUnit]) useEffect(() => { if (refreshInterval > 0) { diff --git a/AppImage/components/settings.tsx b/AppImage/components/settings.tsx index 536f747..f5cc39b 100644 --- a/AppImage/components/settings.tsx +++ b/AppImage/components/settings.tsx @@ -10,6 +10,7 @@ import { APP_VERSION } from "./release-notes-modal" import { getApiUrl, fetchApi } from "../lib/api-config" import { TwoFactorSetup } from "./two-factor-setup" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select" +import { getNetworkUnit } from "../lib/format-network" interface ProxMenuxTool { key: string @@ -55,8 +56,7 @@ export function Settings() { const [generatingToken, setGeneratingToken] = useState(false) const [tokenCopied, setTokenCopied] = useState(false) - // Network unit settings state - const [networkUnitSettings, setNetworkUnitSettings] = useState("Bytes") + const [networkUnitSettings, setNetworkUnitSettings] = useState<"Bytes" | "Bits">("Bytes") const [loadingUnitSettings, setLoadingUnitSettings] = useState(true) useEffect(() => { @@ -354,14 +354,23 @@ export function Settings() { } const changeNetworkUnit = (unit: string) => { - localStorage.setItem("proxmenux-network-unit", unit) - setNetworkUnitSettings(unit) + const networkUnit = unit as "Bytes" | "Bits" + localStorage.setItem("proxmenux-network-unit", networkUnit) + setNetworkUnitSettings(networkUnit) + // Dispatch custom event to notify other components - window.dispatchEvent(new CustomEvent("networkUnitChanged", { detail: unit })) + window.dispatchEvent(new CustomEvent("networkUnitChanged", { detail: networkUnit })) + + // Also dispatch storage event for backward compatibility + window.dispatchEvent(new StorageEvent("storage", { + key: "proxmenux-network-unit", + newValue: networkUnit, + url: window.location.href + })) } const getUnitsSettings = () => { - const networkUnit = localStorage.getItem("proxmenux-network-unit") || "Bytes" + const networkUnit = getNetworkUnit() setNetworkUnitSettings(networkUnit) setLoadingUnitSettings(false) } diff --git a/AppImage/components/system-overview.tsx b/AppImage/components/system-overview.tsx index 1cf67d5..8609355 100644 --- a/AppImage/components/system-overview.tsx +++ b/AppImage/components/system-overview.tsx @@ -9,6 +9,7 @@ import { NodeMetricsCharts } from "./node-metrics-charts" import { NetworkTrafficChart } from "./network-traffic-chart" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select" import { fetchApi } from "../lib/api-config" +import { formatNetworkTraffic, getNetworkUnit } from "../lib/format-network" interface SystemData { cpu_usage: number @@ -222,7 +223,7 @@ export function SystemOverview() { if (data) setNetworkData(data) }, 59000) - setNetworkUnit(getUnitsSettings()) // Load initial setting + setNetworkUnit(getNetworkUnit()) // Load initial setting const handleUnitChange = (e: CustomEvent) => { setNetworkUnit(e.detail === "Bits" ? "Bits" : "Bytes") @@ -314,24 +315,6 @@ export function SystemOverview() { return (bytes / 1024 ** 3).toFixed(2) } - const formatStorage = (sizeInGB: number, unit: "Bytes" | "Bits" = "Bytes"): string => { - let size = sizeInGB - let suffix = "B" - - if (unit === "Bits") { - size = size * 8 - suffix = "b" - } - - if (size < 1) { - return `${(size * 1024).toFixed(1)} M${suffix}` - } else if (size > 999) { - return `${(size / 1024).toFixed(2)} T${suffix}` - } else { - return `${size.toFixed(2)} G${suffix}` - } - } - const tempStatus = getTemperatureStatus(systemData.temperature) const localStorage = proxmoxStorageData?.storage.find((s) => s.name === "local") @@ -520,7 +503,7 @@ export function SystemOverview() {
Total Node Capacity: - {formatStorage(totalCapacity)} + {formatNetworkTraffic(totalCapacity, "Bytes")}
- Used: {formatStorage(totalUsed)} + Used: {formatNetworkTraffic(totalUsed, "Bytes")} - Free: {formatStorage(totalAvailable)} + Free: {formatNetworkTraffic(totalAvailable, "Bytes")}
{totalPercent.toFixed(1)}% @@ -559,18 +542,18 @@ export function SystemOverview() {
VM/LXC Storage
Used: - {formatStorage(vmLxcStorageUsed)} + {formatNetworkTraffic(vmLxcStorageUsed, "Bytes")}
Available: - {formatStorage(vmLxcStorageAvailable)} + {formatNetworkTraffic(vmLxcStorageAvailable, "Bytes")}
- {formatStorage(vmLxcStorageUsed)} / {formatStorage(vmLxcStorageTotal)} + {formatNetworkTraffic(vmLxcStorageUsed, "Bytes")} / {formatNetworkTraffic(vmLxcStorageTotal, "Bytes")} {vmLxcStoragePercent.toFixed(1)}%
@@ -592,18 +575,18 @@ export function SystemOverview() {
Local Storage (System)
Used: - {formatStorage(localStorage.used)} + {formatNetworkTraffic(localStorage.used, "Bytes")}
Available: - {formatStorage(localStorage.available)} + {formatNetworkTraffic(localStorage.available, "Bytes")}
- {formatStorage(localStorage.used)} / {formatStorage(localStorage.total)} + {formatNetworkTraffic(localStorage.used, "Bytes")} / {formatNetworkTraffic(localStorage.total, "Bytes")} {localStorage.percent.toFixed(1)}%
@@ -691,14 +674,14 @@ export function SystemOverview() {
Received: - ↓ {formatStorage(networkTotals.received, networkUnit)} + ↓ {formatNetworkTraffic(networkTotals.received, networkUnit)} ({getTimeframeLabel(networkTimeframe)})
Sent: - ↑ {formatStorage(networkTotals.sent, networkUnit)} + ↑ {formatNetworkTraffic(networkTotals.sent, networkUnit)} ({getTimeframeLabel(networkTimeframe)})
diff --git a/AppImage/components/virtual-machines.tsx b/AppImage/components/virtual-machines.tsx index 6687b2a..593b9e7 100644 --- a/AppImage/components/virtual-machines.tsx +++ b/AppImage/components/virtual-machines.tsx @@ -8,24 +8,11 @@ import { Badge } from "./ui/badge" import { Progress } from "./ui/progress" import { Button } from "./ui/button" import { Dialog, DialogContent, DialogHeader, DialogTitle } from "./ui/dialog" -import { - Server, - Play, - Square, - Cpu, - MemoryStick, - HardDrive, - Network, - Power, - RotateCcw, - StopCircle, - Container, - ChevronDown, - ChevronUp, -} from "lucide-react" +import { Server, Play, Square, Cpu, MemoryStick, HardDrive, Network, Power, RotateCcw, StopCircle, Container, ChevronDown, ChevronUp } from 'lucide-react' import useSWR from "swr" import { MetricsView } from "./metrics-dialog" import { formatStorage } from "@/lib/utils" // Import formatStorage utility +import { formatNetworkTraffic, getNetworkUnit } from "@/lib/format-network" import { fetchApi } from "../lib/api-config" interface VMData { @@ -137,8 +124,15 @@ const fetcher = async (url: string) => { return fetchApi(url) } -const formatBytes = (bytes: number | undefined): string => { - if (!bytes || bytes === 0) return "0 B" +const formatBytes = (bytes: number | undefined, isNetwork: boolean = false): string => { + if (!bytes || bytes === 0) return isNetwork ? "0 B/s" : "0 B" + + if (isNetwork) { + const networkUnit = getNetworkUnit() + return formatNetworkTraffic(bytes, networkUnit) + } + + // For non-network (disk), use standard bytes const k = 1024 const sizes = ["B", "KB", "MB", "GB", "TB"] const i = Math.floor(Math.log(bytes) / Math.log(k)) @@ -272,6 +266,7 @@ export function VirtualMachines() { const [selectedMetric, setSelectedMetric] = useState(null) const [ipsLoaded, setIpsLoaded] = useState(false) const [loadingIPs, setLoadingIPs] = useState(false) + const [networkUnit, setNetworkUnit] = useState<"Bytes" | "Bits">("Bytes") useEffect(() => { const fetchLXCIPs = async () => { @@ -324,6 +319,23 @@ export function VirtualMachines() { fetchLXCIPs() }, [vmData, ipsLoaded, loadingIPs]) + // Load initial network unit and listen for changes + useEffect(() => { + setNetworkUnit(getNetworkUnit()) + + const handleNetworkUnitChange = () => { + setNetworkUnit(getNetworkUnit()) + } + + window.addEventListener("networkUnitChanged", handleNetworkUnitChange) + window.addEventListener("storage", handleNetworkUnitChange) + + return () => { + window.removeEventListener("networkUnitChanged", handleNetworkUnitChange) + window.removeEventListener("storage", handleNetworkUnitChange) + } + }, []) + const handleVMClick = async (vm: VMData) => { setSelectedVM(vm) setCurrentView("main") @@ -924,11 +936,11 @@ export function VirtualMachines() {
- ↓ {formatBytes(vm.diskread)} + ↓ {formatBytes(vm.diskread, false)}
- ↑ {formatBytes(vm.diskwrite)} + ↑ {formatBytes(vm.diskwrite, false)}
@@ -938,11 +950,11 @@ export function VirtualMachines() {
- ↓ {formatBytes(vm.netin)} + ↓ {formatBytes(vm.netin, true)}
- ↑ {formatBytes(vm.netout)} + ↑ {formatBytes(vm.netout, true)}
@@ -1167,11 +1179,11 @@ export function VirtualMachines() {
- {((selectedVM.netin || 0) / 1024 ** 2).toFixed(2)} MB + {formatNetworkTraffic(selectedVM.netin || 0, networkUnit)}
- {((selectedVM.netout || 0) / 1024 ** 2).toFixed(2)} MB + {formatNetworkTraffic(selectedVM.netout || 0, networkUnit)}
diff --git a/AppImage/lib/format-network.ts b/AppImage/lib/format-network.ts new file mode 100644 index 0000000..39e645b --- /dev/null +++ b/AppImage/lib/format-network.ts @@ -0,0 +1,67 @@ +/** + * Utility functions for formatting network traffic data + * Supports conversion between Bytes and Bits based on user preferences + */ + +export type NetworkUnit = 'Bytes' | 'Bits'; + +/** + * Format network traffic value with appropriate unit + * @param bytes - Value in bytes + * @param unit - Target unit ('Bytes' or 'Bits') + * @param decimals - Number of decimal places (default: 2) + * @returns Formatted string with value and unit + */ +export function formatNetworkTraffic( + bytes: number, + unit: NetworkUnit = 'Bytes', + decimals: number = 2 +): string { + if (bytes === 0) return unit === 'Bits' ? '0 b' : '0 B'; + + const k = unit === 'Bits' ? 1000 : 1024; + const dm = decimals < 0 ? 0 : decimals; + + // For Bits: convert bytes to bits first (multiply by 8) + const value = unit === 'Bits' ? bytes * 8 : bytes; + + const sizes = unit === 'Bits' + ? ['b', 'Kb', 'Mb', 'Gb', 'Tb', 'Pb'] + : ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; + + const i = Math.floor(Math.log(value) / Math.log(k)); + const formattedValue = parseFloat((value / Math.pow(k, i)).toFixed(dm)); + + return `${formattedValue} ${sizes[i]}`; +} + +/** + * Get the current network unit preference from localStorage + * @returns 'Bytes' or 'Bits' + */ +export function getNetworkUnit(): NetworkUnit { + if (typeof window === 'undefined') return 'Bytes'; + + const stored = localStorage.getItem('networkUnit'); + return stored === 'Bits' ? 'Bits' : 'Bytes'; +} + +/** + * Get the label for network traffic based on current unit + * @param direction - 'received' or 'sent' + * @returns Label string + */ +export function getNetworkLabel(direction: 'received' | 'sent'): string { + const unit = getNetworkUnit(); + const prefix = direction === 'received' ? 'Received' : 'Sent'; + return unit === 'Bits' ? `${prefix}` : `${prefix}`; +} + +/** + * Get the unit suffix for displaying in charts + * @returns Unit suffix string (e.g., 'GB' or 'Gb') + */ +export function getNetworkUnitSuffix(): string { + const unit = getNetworkUnit(); + return unit === 'Bits' ? 'b' : 'B'; +}