From 3fe9a49ebc2ef7b193a1abc3658928946172df0b Mon Sep 17 00:00:00 2001 From: MacRimi Date: Wed, 12 Aug 2026 17:28:12 +0200 Subject: [PATCH] Update network-flow.tsx --- AppImage/components/network-flow.tsx | 50 +++++++++++++++++++--------- 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/AppImage/components/network-flow.tsx b/AppImage/components/network-flow.tsx index 0528ad00..3f0cf9e3 100644 --- a/AppImage/components/network-flow.tsx +++ b/AppImage/components/network-flow.tsx @@ -8,6 +8,7 @@ import { useEffect, useMemo, useRef, useState } from "react" import { Card, CardContent, CardHeader, CardTitle } from "./ui/card" import { Activity } from "lucide-react" import { useT } from "../lib/i18n/provider" +import { getNetworkUnit, type NetworkUnit } from "../lib/format-network" // One animated comet-trail pulse. Returned as DATA from the layout // renderers instead of an SVG string so the parent component can @@ -144,7 +145,15 @@ function nicSubLabel(n: NIC, labels: FlowLabels): string { return `${n.link} · ${roleLabel}` } -function fmt(v: number): string { +function fmt(v: number, unit: NetworkUnit = "Bytes"): string { + if (unit === "Bits") { + // MB/s (base 2) → bits/s, then decimal SI prefixes (Kb/Mb use ×1000, networking convention). + const bps = (v || 0) * 1024 * 1024 * 8 + if (!bps) return "0 b/s" + if (bps < 1000) return `${Math.round(bps)} b/s` + if (bps < 1_000_000) return `${(bps / 1000).toFixed(0)} Kb/s` + return `${(bps / 1_000_000).toFixed(1)} Mb/s` + } if (!v) return "0 B/s" // Below 1 KB/s show B/s — the previous "—" hid real-but-low traffic. if (v < 0.001) return `${Math.round(v * 1024 * 1024)} B/s` @@ -222,7 +231,7 @@ function curvedTap(cx: number, busY: number, targetY: number, r = 14): string { } // ─── Renderer: returns full SVG markup string for a given width ── -function renderHorizontal(data: NetworkFlowData, W: number, labels: FlowLabels): { svg: string; pulses: PulseData[]; height: number } { +function renderHorizontal(data: NetworkFlowData, W: number, labels: FlowLabels, unit: NetworkUnit): { svg: string; pulses: PulseData[]; height: number } { const top = activeConsumers(data.consumers) const bridges = visibleBridges(data.bridges, top) const host = data.consumers.find((c) => c.kind === "host") @@ -384,7 +393,7 @@ function renderHorizontal(data: NetworkFlowData, W: number, labels: FlowLabels): ${svgIcon("bond", bondX, y, 16, stroke)} ${b.id} ${b.mode || "bond"} - ${fmt(b.rx + b.tx)} + ${fmt(b.rx + b.tx, unit)} `) }) @@ -392,7 +401,7 @@ function renderHorizontal(data: NetworkFlowData, W: number, labels: FlowLabels): ${svgIcon("host", hostX, hostY, 24, COLORS.host)} PROXMOX - ${fmt((host?.rx || 0) + (host?.tx || 0))} + ${fmt((host?.rx || 0) + (host?.tx || 0), unit)} `) // Logarithmic mapping rate (MB/s) → pulse animation duration (s), @@ -440,7 +449,7 @@ function renderHorizontal(data: NetworkFlowData, W: number, labels: FlowLabels): ${svgIcon("bridge", bridgesX, sec.bridgeY, 16, COLORS.bridge)} ${sec.b.id} - ${fmt(bridgeRate)} + ${fmt(bridgeRate, unit)} `) // Trunk lines are STATIC only — every per-guest path will travel @@ -475,7 +484,7 @@ function renderHorizontal(data: NetworkFlowData, W: number, labels: FlowLabels): ${svgIcon(g.kind, cx, circleY, 14, stroke)} ${g.label} - ${fmt(g.rx + g.tx)} + ${fmt(g.rx + g.tx, unit)} `) } @@ -599,7 +608,7 @@ function renderHorizontal(data: NetworkFlowData, W: number, labels: FlowLabels): // own sub-trunk lives at SUB_TRUNK_X and fans out to its guests in // an arc (some above, some below the bridge.cy). All elbows use Q // curves; no sharp 90° corners anywhere. -function renderVertical(data: NetworkFlowData, labels: FlowLabels): { svg: string; pulses: PulseData[]; height: number; viewBox: string } { +function renderVertical(data: NetworkFlowData, labels: FlowLabels, unit: NetworkUnit): { svg: string; pulses: PulseData[]; height: number; viewBox: string } { // Smaller W → SVG scales up on the mobile screen, nodes look bigger. // All four x-columns evenly spaced so curve→target distances are // homogeneous (host→bridge, bridge→spine, spine→guest all ~60 px). @@ -794,7 +803,7 @@ function renderVertical(data: NetworkFlowData, labels: FlowLabels): { svg: strin ${svgIcon("bond", cx, cy, 13, color)} ${b.id} ${b.mode || "bond"} - ${fmt(rate)} + ${fmt(rate, unit)} `) }) @@ -803,7 +812,7 @@ function renderVertical(data: NetworkFlowData, labels: FlowLabels): { svg: strin ${svgIcon("host", HOST_X, hostY, 20, COLORS.host)} PROXMOX - ${fmt((host?.rx || 0) + (host?.tx || 0))} + ${fmt((host?.rx || 0) + (host?.tx || 0), unit)} `) // ─── 3. Bridges + guests, ARC layout around each bridge ── @@ -870,7 +879,7 @@ function renderVertical(data: NetworkFlowData, labels: FlowLabels): { svg: strin ${svgIcon("bridge", cx, cy, 13, COLORS.bridge)} ${b.id} - ${fmt(rate)} + ${fmt(rate, unit)} `) if (guests.length === 0) return @@ -913,7 +922,7 @@ function renderVertical(data: NetworkFlowData, labels: FlowLabels): { svg: strin ${svgIcon(g.kind, gCx, gCy, 13, stroke)} ${g.label} - ${fmt(gRate)} + ${fmt(gRate, unit)} `) }) }) @@ -956,6 +965,7 @@ export function NetworkFlow({ const ref = useRef(null) const [width, setWidth] = useState(1320) const [mode, setMode] = useState<"desktop" | "tablet" | "mobile">("desktop") + const [networkUnit, setNetworkUnit] = useState("Bytes") useEffect(() => { const update = () => { @@ -970,6 +980,16 @@ export function NetworkFlow({ return () => window.removeEventListener("resize", update) }, []) + useEffect(() => { + setNetworkUnit(getNetworkUnit()) + const onChange = (e: Event) => { + const detail = (e as CustomEvent).detail as NetworkUnit | undefined + setNetworkUnit(detail ?? getNetworkUnit()) + } + window.addEventListener("networkUnitChanged", onChange) + return () => window.removeEventListener("networkUnitChanged", onChange) + }, []) + // Stable memo key: the SVG is regenerated ONLY when something // structurally relevant changes (mode, width, who's online, // who's linked where, or a rate crossed into a different speed @@ -997,16 +1017,16 @@ export function NetworkFlow({ `${c.id}:${c.bridge}:${c.kind}:${c.offline ? 1 : 0}:${bucket(c.rx)}:${bucket(c.tx)}:${sig(c.rx)}:${sig(c.tx)}` ).join("|") const bridges = data.bridges.map((b) => `${b.id}:${b.parent || ""}`).join("|") - return `${mode}|${width}|${nics}||${bonds}||${guests}||${bridges}` - }, [data, mode, width]) + return `${mode}|${width}|${networkUnit}|${nics}||${bonds}||${guests}||${bridges}` + }, [data, mode, width, networkUnit]) const { svgContent, pulses, viewBox, height } = useMemo(() => { if (mode === "mobile") { - const out = renderVertical(data, labels) + const out = renderVertical(data, labels, networkUnit) return { svgContent: out.svg, pulses: out.pulses, viewBox: out.viewBox, height: out.height } } const W = mode === "tablet" ? 1100 : 1320 - const out = renderHorizontal(data, W, labels) + const out = renderHorizontal(data, W, labels, networkUnit) return { svgContent: out.svg, pulses: out.pulses, viewBox: `0 0 ${W} ${out.height}`, height: out.height } // eslint-disable-next-line react-hooks/exhaustive-deps }, [memoKey, labels])