mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2025-11-20 04:26:18 +00:00
Update AppImage
This commit is contained in:
@@ -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
|
||||
<div className="font-medium text-foreground text-xs">
|
||||
{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>
|
||||
|
||||
@@ -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<NetworkData> => {
|
||||
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() {
|
||||
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() {
|
||||
)
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
{/* Traffic Data - Top Row */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<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">
|
||||
{formatStorage(interfaceTotals.received * 1024 * 1024 * 1024)}
|
||||
{formatNetworkTraffic(interfaceTotals.received * 1024 * 1024 * 1024, networkUnit)}
|
||||
</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">
|
||||
{formatStorage(interfaceTotals.sent * 1024 * 1024 * 1024)}
|
||||
{formatNetworkTraffic(interfaceTotals.sent * 1024 * 1024 * 1024, networkUnit)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Network Traffic Chart - Full Width Below */}
|
||||
<div className="bg-muted/30 rounded-lg p-4">
|
||||
<NetworkTrafficChart
|
||||
timeframe={modalTimeframe}
|
||||
interfaceName={displayInterface.name}
|
||||
onTotalsCalculated={setInterfaceTotals}
|
||||
refreshInterval={60000}
|
||||
networkUnit={networkUnit}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -951,15 +943,19 @@ export function NetworkMetrics() {
|
||||
<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>
|
||||
<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">
|
||||
{formatBytes(displayInterface.bytes_recv)}
|
||||
{formatNetworkTraffic(displayInterface.bytes_recv || 0, networkUnit)}
|
||||
</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">
|
||||
{formatBytes(displayInterface.bytes_sent)}
|
||||
{formatNetworkTraffic(displayInterface.bytes_sent || 0, networkUnit)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -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<NetworkMetricsData[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
@@ -58,10 +59,35 @@ export function NetworkTrafficChart({
|
||||
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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
<div className="space-y-2 pb-4 border-b-2 border-border">
|
||||
<div className="flex justify-between items-center">
|
||||
<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>
|
||||
<Progress
|
||||
value={totalPercent}
|
||||
@@ -529,10 +512,10 @@ export function SystemOverview() {
|
||||
<div className="flex justify-between items-center mt-1">
|
||||
<div className="flex items-center gap-3">
|
||||
<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 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>
|
||||
</div>
|
||||
<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="flex justify-between items-center">
|
||||
<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 className="flex justify-between items-center">
|
||||
<span className="text-xs text-muted-foreground">Available:</span>
|
||||
<span className="text-sm font-semibold text-green-500">
|
||||
{formatStorage(vmLxcStorageAvailable)}
|
||||
{formatNetworkTraffic(vmLxcStorageAvailable, "Bytes")}
|
||||
</span>
|
||||
</div>
|
||||
<Progress value={vmLxcStoragePercent} className="mt-2 [&>div]:bg-blue-500" />
|
||||
<div className="flex justify-between items-center mt-1">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{formatStorage(vmLxcStorageUsed)} / {formatStorage(vmLxcStorageTotal)}
|
||||
{formatNetworkTraffic(vmLxcStorageUsed, "Bytes")} / {formatNetworkTraffic(vmLxcStorageTotal, "Bytes")}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">{vmLxcStoragePercent.toFixed(1)}%</span>
|
||||
</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="flex justify-between items-center">
|
||||
<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 className="flex justify-between items-center">
|
||||
<span className="text-xs text-muted-foreground">Available:</span>
|
||||
<span className="text-sm font-semibold text-green-500">
|
||||
{formatStorage(localStorage.available)}
|
||||
{formatNetworkTraffic(localStorage.available, "Bytes")}
|
||||
</span>
|
||||
</div>
|
||||
<Progress value={localStorage.percent} className="mt-2 [&>div]:bg-purple-500" />
|
||||
<div className="flex justify-between items-center mt-1">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{formatStorage(localStorage.used)} / {formatStorage(localStorage.total)}
|
||||
{formatNetworkTraffic(localStorage.used, "Bytes")} / {formatNetworkTraffic(localStorage.total, "Bytes")}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">{localStorage.percent.toFixed(1)}%</span>
|
||||
</div>
|
||||
@@ -691,14 +674,14 @@ export function SystemOverview() {
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-muted-foreground">Received:</span>
|
||||
<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>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-muted-foreground">Sent:</span>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@@ -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<string | null>(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() {
|
||||
<div className="text-sm font-semibold space-y-0.5">
|
||||
<div className="flex items-center gap-1">
|
||||
<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 className="flex items-center gap-1">
|
||||
<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>
|
||||
@@ -938,11 +950,11 @@ export function VirtualMachines() {
|
||||
<div className="text-sm font-semibold space-y-0.5">
|
||||
<div className="flex items-center gap-1">
|
||||
<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 className="flex items-center gap-1">
|
||||
<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>
|
||||
@@ -1167,11 +1179,11 @@ export function VirtualMachines() {
|
||||
<div className="space-y-1">
|
||||
<div className="text-sm text-green-500 flex items-center gap-1">
|
||||
<span>↓</span>
|
||||
<span>{((selectedVM.netin || 0) / 1024 ** 2).toFixed(2)} MB</span>
|
||||
<span>{formatNetworkTraffic(selectedVM.netin || 0, networkUnit)}</span>
|
||||
</div>
|
||||
<div className="text-sm text-blue-500 flex items-center gap-1">
|
||||
<span>↑</span>
|
||||
<span>{((selectedVM.netout || 0) / 1024 ** 2).toFixed(2)} MB</span>
|
||||
<span>{formatNetworkTraffic(selectedVM.netout || 0, networkUnit)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
67
AppImage/lib/format-network.ts
Normal file
67
AppImage/lib/format-network.ts
Normal 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';
|
||||
}
|
||||
Reference in New Issue
Block a user