Update AppImage

This commit is contained in:
MacRimi
2025-11-18 22:05:54 +01:00
parent e1409a8045
commit d05dab6633
7 changed files with 202 additions and 110 deletions

View File

@@ -2,9 +2,10 @@
import { Card, CardContent } from "./ui/card" import { Card, CardContent } from "./ui/card"
import { Badge } from "./ui/badge" import { Badge } from "./ui/badge"
import { Wifi, Zap } from "lucide-react" import { Wifi, Zap } from 'lucide-react'
import { useState, useEffect } from "react" import { useState, useEffect } from "react"
import { fetchApi } from "../lib/api-config" import { fetchApi } from "../lib/api-config"
import { formatNetworkTraffic, getNetworkUnit } from "../lib/format-network"
interface NetworkCardProps { interface NetworkCardProps {
interface_: { 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" } 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 => { const formatSpeed = (speed: number): string => {
if (speed === 0) return "N/A" if (speed === 0) return "N/A"
if (speed >= 1000) return `${(speed / 1000).toFixed(1)} Gbps` if (speed >= 1000) return `${(speed / 1000).toFixed(1)} Gbps`
return `${speed} Mbps` 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) { export function NetworkCard({ interface_, timeframe, onClick }: NetworkCardProps) {
const typeBadge = getInterfaceTypeBadge(interface_.type) const typeBadge = getInterfaceTypeBadge(interface_.type)
const vmTypeBadge = interface_.vm_type ? getVMTypeBadge(interface_.vm_type) : null 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 }>({ const [trafficData, setTrafficData] = useState<{ received: number; sent: number }>({
received: 0, received: 0,
sent: 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(() => { useEffect(() => {
const fetchTrafficData = async () => { const fetchTrafficData = async () => {
try { try {
@@ -207,15 +206,15 @@ export function NetworkCard({ interface_, timeframe, onClick }: NetworkCardProps
<div className="font-medium text-foreground text-xs"> <div className="font-medium text-foreground text-xs">
{interface_.status.toLowerCase() === "up" && interface_.vm_type !== "vm" ? ( {interface_.status.toLowerCase() === "up" && interface_.vm_type !== "vm" ? (
<> <>
<span className="text-green-500"> {formatStorage(trafficData.received * 1024 * 1024 * 1024)}</span> <span className="text-green-500"> {formatNetworkTraffic(trafficData.received * 1024 * 1024 * 1024, networkUnit)}</span>
{" / "} {" / "}
<span className="text-blue-500"> {formatStorage(trafficData.sent * 1024 * 1024 * 1024)}</span> <span className="text-blue-500"> {formatNetworkTraffic(trafficData.sent * 1024 * 1024 * 1024, networkUnit)}</span>
</> </>
) : ( ) : (
<> <>
<span className="text-green-500"> {formatBytes(interface_.bytes_recv)}</span> <span className="text-green-500"> {formatNetworkTraffic(interface_.bytes_recv || 0, networkUnit)}</span>
{" / "} {" / "}
<span className="text-blue-500"> {formatBytes(interface_.bytes_sent)}</span> <span className="text-blue-500"> {formatNetworkTraffic(interface_.bytes_sent || 0, networkUnit)}</span>
</> </>
)} )}
</div> </div>

View File

@@ -9,6 +9,7 @@ import useSWR from "swr"
import { NetworkTrafficChart } from "./network-traffic-chart" import { NetworkTrafficChart } from "./network-traffic-chart"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
import { fetchApi } from "../lib/api-config" import { fetchApi } from "../lib/api-config"
import { formatNetworkTraffic, getNetworkUnit } from "../lib/format-network"
interface NetworkData { interface NetworkData {
interfaces: NetworkInterface[] interfaces: NetworkInterface[]
@@ -132,11 +133,6 @@ const fetcher = async (url: string): Promise<NetworkData> => {
return fetchApi<NetworkData>(url) return fetchApi<NetworkData>(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() { export function NetworkMetrics() {
const { const {
@@ -155,10 +151,10 @@ export function NetworkMetrics() {
const [networkTotals, setNetworkTotals] = useState<{ received: number; sent: number }>({ received: 0, sent: 0 }) 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 [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(() => { useEffect(() => {
setNetworkUnit(getUnitsSettings()) setNetworkUnit(getNetworkUnit())
const handleUnitChange = (e: CustomEvent) => { const handleUnitChange = (e: CustomEvent) => {
setNetworkUnit(e.detail === "Bits" ? "Bits" : "Bytes") 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 trafficInFormatted = formatNetworkTraffic(networkTotals.received * 1024 * 1024 * 1024, networkUnit)
const trafficOutFormatted = formatStorage(networkTotals.sent * 1024 * 1024 * 1024) const trafficOutFormatted = formatNetworkTraffic(networkTotals.sent * 1024 * 1024 * 1024, networkUnit)
const packetsRecvK = networkData.traffic.packets_recv ? (networkData.traffic.packets_recv / 1000).toFixed(0) : "0" const packetsRecvK = networkData.traffic.packets_recv ? (networkData.traffic.packets_recv / 1000).toFixed(0) : "0"
const totalErrors = (networkData.traffic.errin || 0) + (networkData.traffic.errout || 0) const totalErrors = (networkData.traffic.errin || 0) + (networkData.traffic.errout || 0)
@@ -731,13 +727,6 @@ export function NetworkMetrics() {
const displayInterface = currentInterfaceData || selectedInterface 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 ( return (
<> <>
{/* Basic Information */} {/* Basic Information */}
@@ -888,29 +877,32 @@ export function NetworkMetrics() {
) )
</h3> </h3>
<div className="space-y-4"> <div className="space-y-4">
{/* Traffic Data - Top Row */}
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<div className="text-sm text-muted-foreground">Bytes Received</div> <div className="text-sm text-muted-foreground">
{networkUnit === "Bits" ? "Bits Received" : "Bytes Received"}
</div>
<div className="font-medium text-green-500 text-lg"> <div className="font-medium text-green-500 text-lg">
{formatStorage(interfaceTotals.received * 1024 * 1024 * 1024)} {formatNetworkTraffic(interfaceTotals.received * 1024 * 1024 * 1024, networkUnit)}
</div> </div>
</div> </div>
<div> <div>
<div className="text-sm text-muted-foreground">Bytes Sent</div> <div className="text-sm text-muted-foreground">
{networkUnit === "Bits" ? "Bits Sent" : "Bytes Sent"}
</div>
<div className="font-medium text-blue-500 text-lg"> <div className="font-medium text-blue-500 text-lg">
{formatStorage(interfaceTotals.sent * 1024 * 1024 * 1024)} {formatNetworkTraffic(interfaceTotals.sent * 1024 * 1024 * 1024, networkUnit)}
</div> </div>
</div> </div>
</div> </div>
{/* Network Traffic Chart - Full Width Below */}
<div className="bg-muted/30 rounded-lg p-4"> <div className="bg-muted/30 rounded-lg p-4">
<NetworkTrafficChart <NetworkTrafficChart
timeframe={modalTimeframe} timeframe={modalTimeframe}
interfaceName={displayInterface.name} interfaceName={displayInterface.name}
onTotalsCalculated={setInterfaceTotals} onTotalsCalculated={setInterfaceTotals}
refreshInterval={60000} refreshInterval={60000}
networkUnit={networkUnit}
/> />
</div> </div>
@@ -951,15 +943,19 @@ export function NetworkMetrics() {
<h3 className="text-sm font-semibold text-muted-foreground mb-4">Traffic since last boot</h3> <h3 className="text-sm font-semibold text-muted-foreground mb-4">Traffic since last boot</h3>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<div className="text-sm text-muted-foreground">Bytes Received</div> <div className="text-sm text-muted-foreground">
{networkUnit === "Bits" ? "Bits Received" : "Bytes Received"}
</div>
<div className="font-medium text-green-500 text-lg"> <div className="font-medium text-green-500 text-lg">
{formatBytes(displayInterface.bytes_recv)} {formatNetworkTraffic(displayInterface.bytes_recv || 0, networkUnit)}
</div> </div>
</div> </div>
<div> <div>
<div className="text-sm text-muted-foreground">Bytes Sent</div> <div className="text-sm text-muted-foreground">
{networkUnit === "Bits" ? "Bits Sent" : "Bytes Sent"}
</div>
<div className="font-medium text-blue-500 text-lg"> <div className="font-medium text-blue-500 text-lg">
{formatBytes(displayInterface.bytes_sent)} {formatNetworkTraffic(displayInterface.bytes_sent || 0, networkUnit)}
</div> </div>
</div> </div>
<div> <div>

View File

@@ -4,6 +4,7 @@ import { useState, useEffect } from "react"
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from "recharts" import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from "recharts"
import { Loader2 } from 'lucide-react' import { Loader2 } from 'lucide-react'
import { fetchApi } from "@/lib/api-config" import { fetchApi } from "@/lib/api-config"
import { getNetworkUnit } from "@/lib/format-network"
interface NetworkMetricsData { interface NetworkMetricsData {
time: string time: string
@@ -47,7 +48,7 @@ export function NetworkTrafficChart({
interfaceName, interfaceName,
onTotalsCalculated, onTotalsCalculated,
refreshInterval = 60000, refreshInterval = 60000,
networkUnit = "Bytes", // Default to Bytes networkUnit: networkUnitProp, // Rename prop to avoid conflict
}: NetworkTrafficChartProps) { }: NetworkTrafficChartProps) {
const [data, setData] = useState<NetworkMetricsData[]>([]) const [data, setData] = useState<NetworkMetricsData[]>([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
@@ -57,11 +58,36 @@ export function NetworkTrafficChart({
netIn: true, netIn: true,
netOut: 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(() => { useEffect(() => {
setIsInitialLoad(true) setIsInitialLoad(true)
fetchMetrics() fetchMetrics()
}, [timeframe, interfaceName, networkUnit]) // Added networkUnit to dependencies }, [timeframe, interfaceName, networkUnit])
useEffect(() => { useEffect(() => {
if (refreshInterval > 0) { if (refreshInterval > 0) {

View File

@@ -10,6 +10,7 @@ import { APP_VERSION } from "./release-notes-modal"
import { getApiUrl, fetchApi } from "../lib/api-config" import { getApiUrl, fetchApi } from "../lib/api-config"
import { TwoFactorSetup } from "./two-factor-setup" import { TwoFactorSetup } from "./two-factor-setup"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
import { getNetworkUnit } from "../lib/format-network"
interface ProxMenuxTool { interface ProxMenuxTool {
key: string key: string
@@ -55,8 +56,7 @@ export function Settings() {
const [generatingToken, setGeneratingToken] = useState(false) const [generatingToken, setGeneratingToken] = useState(false)
const [tokenCopied, setTokenCopied] = useState(false) const [tokenCopied, setTokenCopied] = useState(false)
// Network unit settings state const [networkUnitSettings, setNetworkUnitSettings] = useState<"Bytes" | "Bits">("Bytes")
const [networkUnitSettings, setNetworkUnitSettings] = useState("Bytes")
const [loadingUnitSettings, setLoadingUnitSettings] = useState(true) const [loadingUnitSettings, setLoadingUnitSettings] = useState(true)
useEffect(() => { useEffect(() => {
@@ -354,14 +354,23 @@ export function Settings() {
} }
const changeNetworkUnit = (unit: string) => { const changeNetworkUnit = (unit: string) => {
localStorage.setItem("proxmenux-network-unit", unit) const networkUnit = unit as "Bytes" | "Bits"
setNetworkUnitSettings(unit) localStorage.setItem("proxmenux-network-unit", networkUnit)
setNetworkUnitSettings(networkUnit)
// Dispatch custom event to notify other components // 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 getUnitsSettings = () => {
const networkUnit = localStorage.getItem("proxmenux-network-unit") || "Bytes" const networkUnit = getNetworkUnit()
setNetworkUnitSettings(networkUnit) setNetworkUnitSettings(networkUnit)
setLoadingUnitSettings(false) setLoadingUnitSettings(false)
} }

View File

@@ -9,6 +9,7 @@ import { NodeMetricsCharts } from "./node-metrics-charts"
import { NetworkTrafficChart } from "./network-traffic-chart" import { NetworkTrafficChart } from "./network-traffic-chart"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
import { fetchApi } from "../lib/api-config" import { fetchApi } from "../lib/api-config"
import { formatNetworkTraffic, getNetworkUnit } from "../lib/format-network"
interface SystemData { interface SystemData {
cpu_usage: number cpu_usage: number
@@ -222,7 +223,7 @@ export function SystemOverview() {
if (data) setNetworkData(data) if (data) setNetworkData(data)
}, 59000) }, 59000)
setNetworkUnit(getUnitsSettings()) // Load initial setting setNetworkUnit(getNetworkUnit()) // Load initial setting
const handleUnitChange = (e: CustomEvent) => { const handleUnitChange = (e: CustomEvent) => {
setNetworkUnit(e.detail === "Bits" ? "Bits" : "Bytes") setNetworkUnit(e.detail === "Bits" ? "Bits" : "Bytes")
@@ -314,24 +315,6 @@ export function SystemOverview() {
return (bytes / 1024 ** 3).toFixed(2) 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 tempStatus = getTemperatureStatus(systemData.temperature)
const localStorage = proxmoxStorageData?.storage.find((s) => s.name === "local") const localStorage = proxmoxStorageData?.storage.find((s) => s.name === "local")
@@ -520,7 +503,7 @@ export function SystemOverview() {
<div className="space-y-2 pb-4 border-b-2 border-border"> <div className="space-y-2 pb-4 border-b-2 border-border">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-sm font-medium text-foreground">Total Node Capacity:</span> <span className="text-sm font-medium text-foreground">Total Node Capacity:</span>
<span className="text-lg font-bold text-foreground">{formatStorage(totalCapacity)}</span> <span className="text-lg font-bold text-foreground">{formatNetworkTraffic(totalCapacity, "Bytes")}</span>
</div> </div>
<Progress <Progress
value={totalPercent} value={totalPercent}
@@ -529,10 +512,10 @@ export function SystemOverview() {
<div className="flex justify-between items-center mt-1"> <div className="flex justify-between items-center mt-1">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">
Used: <span className="font-semibold text-foreground">{formatStorage(totalUsed)}</span> Used: <span className="font-semibold text-foreground">{formatNetworkTraffic(totalUsed, "Bytes")}</span>
</span> </span>
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">
Free: <span className="font-semibold text-green-500">{formatStorage(totalAvailable)}</span> Free: <span className="font-semibold text-green-500">{formatNetworkTraffic(totalAvailable, "Bytes")}</span>
</span> </span>
</div> </div>
<span className="text-xs font-semibold text-muted-foreground">{totalPercent.toFixed(1)}%</span> <span className="text-xs font-semibold text-muted-foreground">{totalPercent.toFixed(1)}%</span>
@@ -559,18 +542,18 @@ export function SystemOverview() {
<div className="text-xs font-medium text-muted-foreground mb-2">VM/LXC Storage</div> <div className="text-xs font-medium text-muted-foreground mb-2">VM/LXC Storage</div>
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-xs text-muted-foreground">Used:</span> <span className="text-xs text-muted-foreground">Used:</span>
<span className="text-sm font-semibold text-foreground">{formatStorage(vmLxcStorageUsed)}</span> <span className="text-sm font-semibold text-foreground">{formatNetworkTraffic(vmLxcStorageUsed, "Bytes")}</span>
</div> </div>
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-xs text-muted-foreground">Available:</span> <span className="text-xs text-muted-foreground">Available:</span>
<span className="text-sm font-semibold text-green-500"> <span className="text-sm font-semibold text-green-500">
{formatStorage(vmLxcStorageAvailable)} {formatNetworkTraffic(vmLxcStorageAvailable, "Bytes")}
</span> </span>
</div> </div>
<Progress value={vmLxcStoragePercent} className="mt-2 [&>div]:bg-blue-500" /> <Progress value={vmLxcStoragePercent} className="mt-2 [&>div]:bg-blue-500" />
<div className="flex justify-between items-center mt-1"> <div className="flex justify-between items-center mt-1">
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">
{formatStorage(vmLxcStorageUsed)} / {formatStorage(vmLxcStorageTotal)} {formatNetworkTraffic(vmLxcStorageUsed, "Bytes")} / {formatNetworkTraffic(vmLxcStorageTotal, "Bytes")}
</span> </span>
<span className="text-xs text-muted-foreground">{vmLxcStoragePercent.toFixed(1)}%</span> <span className="text-xs text-muted-foreground">{vmLxcStoragePercent.toFixed(1)}%</span>
</div> </div>
@@ -592,18 +575,18 @@ export function SystemOverview() {
<div className="text-xs font-medium text-muted-foreground mb-2">Local Storage (System)</div> <div className="text-xs font-medium text-muted-foreground mb-2">Local Storage (System)</div>
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-xs text-muted-foreground">Used:</span> <span className="text-xs text-muted-foreground">Used:</span>
<span className="text-sm font-semibold text-foreground">{formatStorage(localStorage.used)}</span> <span className="text-sm font-semibold text-foreground">{formatNetworkTraffic(localStorage.used, "Bytes")}</span>
</div> </div>
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-xs text-muted-foreground">Available:</span> <span className="text-xs text-muted-foreground">Available:</span>
<span className="text-sm font-semibold text-green-500"> <span className="text-sm font-semibold text-green-500">
{formatStorage(localStorage.available)} {formatNetworkTraffic(localStorage.available, "Bytes")}
</span> </span>
</div> </div>
<Progress value={localStorage.percent} className="mt-2 [&>div]:bg-purple-500" /> <Progress value={localStorage.percent} className="mt-2 [&>div]:bg-purple-500" />
<div className="flex justify-between items-center mt-1"> <div className="flex justify-between items-center mt-1">
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">
{formatStorage(localStorage.used)} / {formatStorage(localStorage.total)} {formatNetworkTraffic(localStorage.used, "Bytes")} / {formatNetworkTraffic(localStorage.total, "Bytes")}
</span> </span>
<span className="text-xs text-muted-foreground">{localStorage.percent.toFixed(1)}%</span> <span className="text-xs text-muted-foreground">{localStorage.percent.toFixed(1)}%</span>
</div> </div>
@@ -691,14 +674,14 @@ export function SystemOverview() {
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Received:</span> <span className="text-sm text-muted-foreground">Received:</span>
<span className="text-lg font-semibold text-green-500 flex items-center gap-1"> <span className="text-lg font-semibold text-green-500 flex items-center gap-1">
{formatStorage(networkTotals.received, networkUnit)} {formatNetworkTraffic(networkTotals.received, networkUnit)}
<span className="text-xs text-muted-foreground">({getTimeframeLabel(networkTimeframe)})</span> <span className="text-xs text-muted-foreground">({getTimeframeLabel(networkTimeframe)})</span>
</span> </span>
</div> </div>
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Sent:</span> <span className="text-sm text-muted-foreground">Sent:</span>
<span className="text-lg font-semibold text-blue-500 flex items-center gap-1"> <span className="text-lg font-semibold text-blue-500 flex items-center gap-1">
{formatStorage(networkTotals.sent, networkUnit)} {formatNetworkTraffic(networkTotals.sent, networkUnit)}
<span className="text-xs text-muted-foreground">({getTimeframeLabel(networkTimeframe)})</span> <span className="text-xs text-muted-foreground">({getTimeframeLabel(networkTimeframe)})</span>
</span> </span>
</div> </div>

View File

@@ -8,24 +8,11 @@ import { Badge } from "./ui/badge"
import { Progress } from "./ui/progress" import { Progress } from "./ui/progress"
import { Button } from "./ui/button" import { Button } from "./ui/button"
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "./ui/dialog" import { Dialog, DialogContent, DialogHeader, DialogTitle } from "./ui/dialog"
import { import { Server, Play, Square, Cpu, MemoryStick, HardDrive, Network, Power, RotateCcw, StopCircle, Container, ChevronDown, ChevronUp } from 'lucide-react'
Server,
Play,
Square,
Cpu,
MemoryStick,
HardDrive,
Network,
Power,
RotateCcw,
StopCircle,
Container,
ChevronDown,
ChevronUp,
} from "lucide-react"
import useSWR from "swr" import useSWR from "swr"
import { MetricsView } from "./metrics-dialog" import { MetricsView } from "./metrics-dialog"
import { formatStorage } from "@/lib/utils" // Import formatStorage utility import { formatStorage } from "@/lib/utils" // Import formatStorage utility
import { formatNetworkTraffic, getNetworkUnit } from "@/lib/format-network"
import { fetchApi } from "../lib/api-config" import { fetchApi } from "../lib/api-config"
interface VMData { interface VMData {
@@ -137,8 +124,15 @@ const fetcher = async (url: string) => {
return fetchApi(url) return fetchApi(url)
} }
const formatBytes = (bytes: number | undefined): string => { const formatBytes = (bytes: number | undefined, isNetwork: boolean = false): string => {
if (!bytes || bytes === 0) return "0 B" 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 k = 1024
const sizes = ["B", "KB", "MB", "GB", "TB"] const sizes = ["B", "KB", "MB", "GB", "TB"]
const i = Math.floor(Math.log(bytes) / Math.log(k)) const i = Math.floor(Math.log(bytes) / Math.log(k))
@@ -272,6 +266,7 @@ export function VirtualMachines() {
const [selectedMetric, setSelectedMetric] = useState<string | null>(null) const [selectedMetric, setSelectedMetric] = useState<string | null>(null)
const [ipsLoaded, setIpsLoaded] = useState(false) const [ipsLoaded, setIpsLoaded] = useState(false)
const [loadingIPs, setLoadingIPs] = useState(false) const [loadingIPs, setLoadingIPs] = useState(false)
const [networkUnit, setNetworkUnit] = useState<"Bytes" | "Bits">("Bytes")
useEffect(() => { useEffect(() => {
const fetchLXCIPs = async () => { const fetchLXCIPs = async () => {
@@ -324,6 +319,23 @@ export function VirtualMachines() {
fetchLXCIPs() fetchLXCIPs()
}, [vmData, ipsLoaded, loadingIPs]) }, [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) => { const handleVMClick = async (vm: VMData) => {
setSelectedVM(vm) setSelectedVM(vm)
setCurrentView("main") setCurrentView("main")
@@ -924,11 +936,11 @@ export function VirtualMachines() {
<div className="text-sm font-semibold space-y-0.5"> <div className="text-sm font-semibold space-y-0.5">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<HardDrive className="h-3 w-3 text-green-500" /> <HardDrive className="h-3 w-3 text-green-500" />
<span className="text-green-500"> {formatBytes(vm.diskread)}</span> <span className="text-green-500"> {formatBytes(vm.diskread, false)}</span>
</div> </div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<HardDrive className="h-3 w-3 text-blue-500" /> <HardDrive className="h-3 w-3 text-blue-500" />
<span className="text-blue-500"> {formatBytes(vm.diskwrite)}</span> <span className="text-blue-500"> {formatBytes(vm.diskwrite, false)}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -938,11 +950,11 @@ export function VirtualMachines() {
<div className="text-sm font-semibold space-y-0.5"> <div className="text-sm font-semibold space-y-0.5">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Network className="h-3 w-3 text-green-500" /> <Network className="h-3 w-3 text-green-500" />
<span className="text-green-500"> {formatBytes(vm.netin)}</span> <span className="text-green-500"> {formatBytes(vm.netin, true)}</span>
</div> </div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Network className="h-3 w-3 text-blue-500" /> <Network className="h-3 w-3 text-blue-500" />
<span className="text-blue-500"> {formatBytes(vm.netout)}</span> <span className="text-blue-500"> {formatBytes(vm.netout, true)}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -1167,11 +1179,11 @@ export function VirtualMachines() {
<div className="space-y-1"> <div className="space-y-1">
<div className="text-sm text-green-500 flex items-center gap-1"> <div className="text-sm text-green-500 flex items-center gap-1">
<span></span> <span></span>
<span>{((selectedVM.netin || 0) / 1024 ** 2).toFixed(2)} MB</span> <span>{formatNetworkTraffic(selectedVM.netin || 0, networkUnit)}</span>
</div> </div>
<div className="text-sm text-blue-500 flex items-center gap-1"> <div className="text-sm text-blue-500 flex items-center gap-1">
<span></span> <span></span>
<span>{((selectedVM.netout || 0) / 1024 ** 2).toFixed(2)} MB</span> <span>{formatNetworkTraffic(selectedVM.netout || 0, networkUnit)}</span>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -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';
}