This commit is contained in:
MacRimi
2025-11-18 17:14:46 +01:00
4 changed files with 130 additions and 32 deletions

View File

@@ -1,6 +1,6 @@
"use client"
import { useState } from "react"
import { useEffect, useState } from "react"
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"
import { Badge } from "./ui/badge"
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "./ui/dialog"
@@ -132,6 +132,13 @@ const fetcher = async (url: string): Promise<NetworkData> => {
return fetchApi<NetworkData>(url)
}
const getUnitsSettings = (): "Bytes" | "Bits" => {
const raw = localStorage.getItem("proxmenux-network-unit");
const networkUnit = raw && raw.toLowerCase() === "bits" ? "Bits" : "Bytes";
console.log("[v0] Loaded network unit from localStorage:", networkUnit);
return networkUnit;
};
export function NetworkMetrics() {
const {
data: networkData,
@@ -160,6 +167,13 @@ export function NetworkMetrics() {
revalidateOnFocus: false,
})
const [networkUnit, setNetworkUnit] = useState<"Bytes" | "Bits">("Bytes");
useEffect(() => {
const networkUnitSetting = getUnitsSettings();
setNetworkUnit(networkUnitSetting);
}, []);
if (isLoading) {
return (
<div className="space-y-6">
@@ -375,7 +389,7 @@ export function NetworkMetrics() {
</CardTitle>
</CardHeader>
<CardContent>
<NetworkTrafficChart timeframe={timeframe} onTotalsCalculated={setNetworkTotals} />
<NetworkTrafficChart timeframe={timeframe} onTotalsCalculated={setNetworkTotals} networkUnit={networkUnit} />
</CardContent>
</Card>

View File

@@ -17,33 +17,16 @@ interface NetworkTrafficChartProps {
interfaceName?: string
onTotalsCalculated?: (totals: { received: number; sent: number }) => void
refreshInterval?: number // En milisegundos, por defecto 60000 (60 segundos)
networkUnit?: "Bytes" | "Bits"
}
const CustomNetworkTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
return (
<div className="bg-gray-900/95 backdrop-blur-sm border border-gray-700 rounded-lg p-3 shadow-xl">
<p className="text-sm font-semibold text-white mb-2">{label}</p>
<div className="space-y-1.5">
{payload.map((entry: any, index: number) => (
<div key={index} className="flex items-center gap-2">
<div className="w-2.5 h-2.5 rounded-full flex-shrink-0" style={{ backgroundColor: entry.color }} />
<span className="text-xs text-gray-300 min-w-[60px]">{entry.name}:</span>
<span className="text-sm font-semibold text-white">{entry.value.toFixed(3)} GB</span>
</div>
))}
</div>
</div>
)
}
return null
}
export function NetworkTrafficChart({
timeframe,
interfaceName,
onTotalsCalculated,
refreshInterval = 60000,
networkUnit = "Bits",
}: NetworkTrafficChartProps) {
const [data, setData] = useState<NetworkMetricsData[]>([])
const [loading, setLoading] = useState(true)
@@ -138,6 +121,15 @@ export function NetworkTrafficChart({
const netInBytes = (item.netin || 0) * intervalSeconds
const netOutBytes = (item.netout || 0) * intervalSeconds
if (networkUnit === "Bits") {
return {
time: timeLabel,
timestamp: item.time,
netIn: Number((netInBytes * 8 / 1024 / 1024 / 1024).toFixed(4)),
netOut: Number((netOutBytes * 8 / 1024 / 1024 / 1024).toFixed(4)),
}
}
return {
time: timeLabel,
timestamp: item.time,
@@ -222,6 +214,26 @@ export function NetworkTrafficChart({
)
}
const CustomNetworkTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
return (
<div className="bg-gray-900/95 backdrop-blur-sm border border-gray-700 rounded-lg p-3 shadow-xl">
<p className="text-sm font-semibold text-white mb-2">{label}</p>
<div className="space-y-1.5">
{payload.map((entry: any, index: number) => (
<div key={index} className="flex items-center gap-2">
<div className="w-2.5 h-2.5 rounded-full flex-shrink-0" style={{ backgroundColor: entry.color }} />
<span className="text-xs text-gray-300 min-w-[60px]">{entry.name}:</span>
<span className="text-sm font-semibold text-white">{entry.value.toFixed(3)} {networkUnit === "Bits" ? "Gb" : "GB"}</span>
</div>
))}
</div>
</div>
)
}
return null
}
return (
<ResponsiveContainer width="100%" height={300}>
<AreaChart data={data} margin={{ bottom: 80 }}>
@@ -240,7 +252,7 @@ export function NetworkTrafficChart({
stroke="currentColor"
className="text-foreground"
tick={{ fill: "currentColor", fontSize: 12 }}
label={{ value: "GB", angle: -90, position: "insideLeft", fill: "currentColor" }}
label={{ value: networkUnit === "Bits" ? "Gb" : "GB", angle: -90, position: "insideLeft", fill: "currentColor" }}
domain={[0, "auto"]}
/>
<Tooltip content={<CustomNetworkTooltip />} />

View File

@@ -19,10 +19,12 @@ import {
Copy,
Eye,
EyeOff,
Ruler,
} from "lucide-react"
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"
interface ProxMenuxTool {
key: string
@@ -59,6 +61,9 @@ export function Settings() {
[APP_VERSION]: true, // Current version expanded by default
})
const [networkUnitSettings, setNetworkUnitSettings] = useState("Bytes");
const [loadingUnitSettings, setLoadingUnitSettings] = useState(true);
// API Token state management
const [showApiTokenSection, setShowApiTokenSection] = useState(false)
const [apiToken, setApiToken] = useState("")
@@ -71,8 +76,22 @@ export function Settings() {
useEffect(() => {
checkAuthStatus()
loadProxmenuxTools()
getUnitsSettings();
}, [])
const changeNetworkUnit = (unit: string) => {
localStorage.setItem("proxmenux-network-unit", unit);
setNetworkUnitSettings(unit);
};
const getUnitsSettings = () => {
const networkUnit =
localStorage.getItem("proxmenux-network-unit") || "Bytes";
console.log("[v0] Loaded network unit from localStorage:", networkUnit);
setNetworkUnitSettings(networkUnit);
setLoadingUnitSettings(false);
};
const checkAuthStatus = async () => {
try {
const response = await fetch(getApiUrl("/api/auth/status"))
@@ -854,6 +873,42 @@ export function Settings() {
</CardContent>
</Card>
)}
{/* ProxMenux unit settings*/}
<Card>
<CardHeader>
<div className="flex items-center gap-2">
<Ruler className="h-5 w-5 text-green-500" />
<CardTitle>ProxMenux unit settings</CardTitle>
</div>
<CardDescription>Change settings related to units</CardDescription>
</CardHeader>
<CardContent>
{loadingUnitSettings ? (
<div className="flex items-center justify-center py-8">
<div className="animate-spin h-8 w-8 border-4 border-green-500 border-t-transparent rounded-full" />
</div>
) : (
<div className="text-foreground flex items-center justify-between">
<div className="flex items-center">
Network Unit Display
</div>
<Select
value={networkUnitSettings}
onValueChange={changeNetworkUnit}
>
<SelectTrigger className="w-28 h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="Bits">Bits</SelectItem>
<SelectItem value="Bytes">Bytes</SelectItem>
</SelectContent>
</Select>
</div>
)}
</CardContent>
</Card>
{/* ProxMenux Optimizations */}
<Card>
@@ -907,4 +962,4 @@ export function Settings() {
/>
</div>
)
}
}

View File

@@ -146,6 +146,13 @@ const fetchProxmoxStorageData = async (): Promise<ProxmoxStorageData | null> =>
}
}
const getUnitsSettings = (): "Bytes" | "Bits" => {
const raw = localStorage.getItem("proxmenux-network-unit");
const networkUnit = raw && raw.toLowerCase() === "bits" ? "Bits" : "Bytes";
console.log("[v0] Loaded network unit from localStorage:", networkUnit);
return networkUnit;
};
export function SystemOverview() {
const [systemData, setSystemData] = useState<SystemData | null>(null)
const [vmData, setVmData] = useState<VMData[]>([])
@@ -161,6 +168,7 @@ export function SystemOverview() {
const [error, setError] = useState<string | null>(null)
const [networkTimeframe, setNetworkTimeframe] = useState("day")
const [networkTotals, setNetworkTotals] = useState<{ received: number; sent: number }>({ received: 0, sent: 0 })
const [networkUnit, setNetworkUnit] = useState<"Bytes" | "Bits">("Bytes");
useEffect(() => {
const fetchAllData = async () => {
@@ -178,6 +186,9 @@ export function SystemOverview() {
return
}
const networkUnitSetting = getUnitsSettings();
setNetworkUnit(networkUnitSetting);
setSystemData(systemResult)
setVmData(vmResult)
setStorageData(storageResults[0])
@@ -298,13 +309,19 @@ export function SystemOverview() {
return (bytes / 1024 ** 3).toFixed(2)
}
const formatStorage = (sizeInGB: number): string => {
if (sizeInGB < 1) {
return `${(sizeInGB * 1024).toFixed(1)} MB`
} else if (sizeInGB > 999) {
return `${(sizeInGB / 1024).toFixed(2)} TB`
const formatStorage = (sizeInGB: number, unit: "Bytes" | "Bits" = "Bytes"): string => {
let size = sizeInGB;
let sufix = "B";
if (unit === "Bits") {
size = size * 8;
sufix = "b";
}
if (size < 1) {
return `${(size * 1024).toFixed(1)} M${sufix}`
} else if (size > 999) {
return `${(size / 1024).toFixed(2)} T${sufix}`
} else {
return `${sizeInGB.toFixed(2)} GB`
return `${size.toFixed(2)} G${sufix}`
}
}
@@ -667,21 +684,21 @@ 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)}
{formatStorage(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)}
{formatStorage(networkTotals.sent, networkUnit)}
<span className="text-xs text-muted-foreground">({getTimeframeLabel(networkTimeframe)})</span>
</span>
</div>
</div>
<div className="pt-3 border-t border-border">
<NetworkTrafficChart timeframe={networkTimeframe} onTotalsCalculated={setNetworkTotals} />
<NetworkTrafficChart timeframe={networkTimeframe} onTotalsCalculated={setNetworkTotals} networkUnit={networkUnit} />
</div>
</div>
) : (