diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index 34f9055..ccaff76 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -113,6 +113,8 @@ const en = { kpiBw: "Bandwidth", latestAt: "Latest sample", hoverPoint: "Cursor", + wallLiveTitle: "Live traffic", + wallDevice: "Device", kpi: { tasks: "Tasks", running: "Running", diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index 66c8530..f989db0 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -113,6 +113,8 @@ const zh = { kpiBw: "带宽", latestAt: "最新样点", hoverPoint: "光标位置", + wallLiveTitle: "实时流量", + wallDevice: "设备", kpi: { tasks: "任务数", running: "运行中", diff --git a/web/src/index.css b/web/src/index.css index 5034363..39137fc 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -3011,6 +3011,13 @@ pre { margin-bottom: 14px; } +.pt-wall__head-title { + font-size: 14px; + font-weight: 600; + letter-spacing: 0.04em; + color: #cbd5e1; +} + .pt-wall__title { display: flex; flex-direction: column; @@ -3141,10 +3148,52 @@ pre { } .pt-wall__chart .u-legend .u-value { - font-variant-numeric: tabular-nums; + display: none; +} + +.pt-wall__foot { + display: flex; + flex-wrap: wrap; + gap: 10px 28px; + margin-top: 12px; + padding-top: 12px; + border-top: 1px solid rgba(148, 163, 184, 0.18); + align-items: baseline; +} + +.pt-wall__foot-ne, +.pt-wall__foot-if { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 6px 10px; + min-width: 0; +} + +.pt-wall__foot-label { + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--pt-muted); +} + +.pt-wall__foot-value { + font-size: 15px; + font-weight: 600; color: #e2e8f0; } +.pt-wall__foot-value--mono { + font-family: "IBM Plex Mono", "Cascadia Mono", Consolas, monospace; + letter-spacing: 0.02em; +} + +.pt-wall__foot-ip { + font-size: 12px; + color: var(--pt-muted); + font-variant-numeric: tabular-nums; +} + .pt-wall__empty { position: absolute; inset: 0; diff --git a/web/src/pages/network/PortTrafficWall.tsx b/web/src/pages/network/PortTrafficWall.tsx index 88cdd51..c3a5a0f 100644 --- a/web/src/pages/network/PortTrafficWall.tsx +++ b/web/src/pages/network/PortTrafficWall.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useRef, useState } from "react"; +import { useEffect, useMemo, useRef } from "react"; import uPlot from "uplot"; import "uplot/dist/uPlot.min.css"; import type { PortTrafficSamplePoint, PortTrafficTarget } from "../../types"; @@ -47,14 +47,6 @@ function chartSize(el: HTMLElement): { width: number; height: number } { return { width, height }; } -type HoverKpi = { - tsSec: number; - inBps: number; - outBps: number; - inUtil: number; - outUtil: number; -}; - type Props = { target: PortTrafficTarget | null; points: PortTrafficSamplePoint[]; @@ -69,13 +61,10 @@ export function PortTrafficWall({ target, points, rangeLabel, loading, hint }: P const plotRef = useRef(null); const tipRef = useRef(null); const pointsRef = useRef(points); - const hoverIdxRef = useRef(null); - const [hover, setHover] = useState(null); - pointsRef.current = points; const latest = points.length ? points[points.length - 1] : null; - const latestKpi = useMemo(() => { + const kpi = useMemo(() => { const bw = latest?.bw_bps || target?.bw_bps || 0; return { bw, @@ -87,27 +76,11 @@ export function PortTrafficWall({ target, points, rangeLabel, loading, hint }: P }; }, [latest, target]); - const kpi = hover - ? { - bw: latestKpi.bw, - inBps: hover.inBps, - outBps: hover.outBps, - inUtil: hover.inUtil, - outUtil: hover.outUtil, - hovering: true as const, - } - : { - ...latestKpi, - hovering: false as const, - }; - useEffect(() => { const el = mountRef.current; if (!el) return; const { width, height } = chartSize(el); - const valueFmt = (_u: uPlot, v: number | null | undefined) => - v == null || Number.isNaN(v) ? "—" : `${formatBps(v)} bit/s`; const placeTip = (u: uPlot, left: number, top: number) => { const tip = tipRef.current; @@ -129,16 +102,13 @@ export function PortTrafficWall({ target, points, rangeLabel, loading, hint }: P width, height, series: [ - { - value: (_u, raw) => (raw == null ? "—" : formatHoverTime(Number(raw))), - }, + {}, { label: "In", stroke: "#2dd4bf", width: 2, fill: "rgba(45, 212, 191, 0.12)", points: { show: true, size: 5, fill: "#2dd4bf" }, - value: valueFmt, }, { label: "Out", @@ -146,7 +116,6 @@ export function PortTrafficWall({ target, points, rangeLabel, loading, hint }: P width: 2, fill: "rgba(245, 158, 11, 0.10)", points: { show: true, size: 5, fill: "#f59e0b" }, - value: valueFmt, }, ], axes: [ @@ -180,7 +149,8 @@ export function PortTrafficWall({ target, points, rangeLabel, loading, hint }: P }, }, }, - legend: { show: true, live: true }, + // Color keys only — no live numeric values (those are in the hover tip). + legend: { show: true, live: false }, cursor: { drag: { x: true, y: false }, focus: { prox: 32 }, @@ -195,10 +165,6 @@ export function PortTrafficWall({ target, points, rangeLabel, loading, hint }: P const top = u.cursor.top ?? 0; if (idx == null || idx < 0 || !u.data[0]?.length) { - if (hoverIdxRef.current !== null) { - hoverIdxRef.current = null; - setHover(null); - } if (tip) tip.style.display = "none"; return; } @@ -211,28 +177,17 @@ export function PortTrafficWall({ target, points, rangeLabel, loading, hint }: P return d && Math.floor(d.getTime() / 1000) === tsSec; }); - if (tip) { - tip.style.display = "block"; - tip.innerHTML = [ - `
${formatHoverTime(tsSec)}
`, - `
In${formatBps(inBps)} bit/s
`, - `
Out${formatBps(outBps)} bit/s
`, - sample - ? `
Util${Number(sample.in_util_pct || 0).toFixed(1)}% / ${Number(sample.out_util_pct || 0).toFixed(1)}%
` - : "", - ].join(""); - placeTip(u, left, top); - } - - if (hoverIdxRef.current === idx) return; - hoverIdxRef.current = idx; - setHover({ - tsSec, - inBps, - outBps, - inUtil: Number(sample?.in_util_pct ?? 0), - outUtil: Number(sample?.out_util_pct ?? 0), - }); + if (!tip) return; + tip.style.display = "block"; + tip.innerHTML = [ + `
${formatHoverTime(tsSec)}
`, + `
In${formatBps(inBps)} bit/s
`, + `
Out${formatBps(outBps)} bit/s
`, + sample + ? `
Util${Number(sample.in_util_pct || 0).toFixed(1)}% / ${Number(sample.out_util_pct || 0).toFixed(1)}%
` + : "", + ].join(""); + placeTip(u, left, top); }, ], }, @@ -241,8 +196,6 @@ export function PortTrafficWall({ target, points, rangeLabel, loading, hint }: P plotRef.current = new uPlot(opts, [[], [], []], el); const onLeave = () => { - hoverIdxRef.current = null; - setHover(null); if (tipRef.current) tipRef.current.style.display = "none"; }; plotRef.current.over.addEventListener("mouseleave", onLeave); @@ -268,31 +221,29 @@ export function PortTrafficWall({ target, points, rangeLabel, loading, hint }: P }, [points]); const empty = points.length === 0; + const neLabel = target ? target.ne_name || target.ne_ip || "—" : "—"; + const ifLabel = target?.ifname || "—"; + const ipLabel = target?.ne_ip || ""; return (
-
- {target ? target.ne_name || target.ne_ip || "—" : "—"} - {target?.ifname || "Select interface"} -
+
{t("portTraffic.wallLiveTitle")}
{rangeLabel} - {kpi.hovering - ? ` · ${t("portTraffic.hoverPoint")}` - : latestKpi.ts - ? ` · ${t("portTraffic.latestAt")} ${formatSystemTime(latestKpi.ts)}` - : ""} + {kpi.ts ? ` · ${t("portTraffic.latestAt")} ${formatSystemTime(kpi.ts)}` : ""}
-
{t("portTraffic.kpiBw")}
+
+ {t("portTraffic.kpiBw")} · {t("portTraffic.kpiLatest")} +
{formatBwLabel(kpi.bw)}
- In · {kpi.hovering ? t("portTraffic.hoverPoint") : t("portTraffic.kpiLatest")} + In · {t("portTraffic.kpiLatest")}
{formatBps(kpi.inBps)} @@ -301,7 +252,7 @@ export function PortTrafficWall({ target, points, rangeLabel, loading, hint }: P
- Out · {kpi.hovering ? t("portTraffic.hoverPoint") : t("portTraffic.kpiLatest")} + Out · {t("portTraffic.kpiLatest")}
{formatBps(kpi.outBps)} @@ -310,7 +261,7 @@ export function PortTrafficWall({ target, points, rangeLabel, loading, hint }: P
- Util · {kpi.hovering ? t("portTraffic.hoverPoint") : t("portTraffic.kpiLatest")} + Util · {t("portTraffic.kpiLatest")}
{kpi.inUtil.toFixed(1)}% @@ -328,6 +279,17 @@ export function PortTrafficWall({ target, points, rangeLabel, loading, hint }: P
) : null}
+
+
+ {t("portTraffic.wallDevice")} + {neLabel} + {ipLabel ? {ipLabel} : null} +
+
+ {t("portTraffic.wallPort")} + {ifLabel} +
+
); }