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 { 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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
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