mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 03:10:46 +08:00
Simplify port traffic wall: latest KPIs only, hover tip for point values.
Move device/port to the footer and drop live legend numbers to cut duplication. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
c105cd8fd6
commit
235e1a59ec
4 changed files with 92 additions and 77 deletions
|
|
@ -113,6 +113,8 @@ const en = {
|
||||||
kpiBw: "Bandwidth",
|
kpiBw: "Bandwidth",
|
||||||
latestAt: "Latest sample",
|
latestAt: "Latest sample",
|
||||||
hoverPoint: "Cursor",
|
hoverPoint: "Cursor",
|
||||||
|
wallLiveTitle: "Live traffic",
|
||||||
|
wallDevice: "Device",
|
||||||
kpi: {
|
kpi: {
|
||||||
tasks: "Tasks",
|
tasks: "Tasks",
|
||||||
running: "Running",
|
running: "Running",
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,8 @@ const zh = {
|
||||||
kpiBw: "带宽",
|
kpiBw: "带宽",
|
||||||
latestAt: "最新样点",
|
latestAt: "最新样点",
|
||||||
hoverPoint: "光标位置",
|
hoverPoint: "光标位置",
|
||||||
|
wallLiveTitle: "实时流量",
|
||||||
|
wallDevice: "设备",
|
||||||
kpi: {
|
kpi: {
|
||||||
tasks: "任务数",
|
tasks: "任务数",
|
||||||
running: "运行中",
|
running: "运行中",
|
||||||
|
|
|
||||||
|
|
@ -3011,6 +3011,13 @@ pre {
|
||||||
margin-bottom: 14px;
|
margin-bottom: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pt-wall__head-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
.pt-wall__title {
|
.pt-wall__title {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -3141,10 +3148,52 @@ pre {
|
||||||
}
|
}
|
||||||
|
|
||||||
.pt-wall__chart .u-legend .u-value {
|
.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;
|
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 {
|
.pt-wall__empty {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef } from "react";
|
||||||
import uPlot from "uplot";
|
import uPlot from "uplot";
|
||||||
import "uplot/dist/uPlot.min.css";
|
import "uplot/dist/uPlot.min.css";
|
||||||
import type { PortTrafficSamplePoint, PortTrafficTarget } from "../../types";
|
import type { PortTrafficSamplePoint, PortTrafficTarget } from "../../types";
|
||||||
|
|
@ -47,14 +47,6 @@ function chartSize(el: HTMLElement): { width: number; height: number } {
|
||||||
return { width, height };
|
return { width, height };
|
||||||
}
|
}
|
||||||
|
|
||||||
type HoverKpi = {
|
|
||||||
tsSec: number;
|
|
||||||
inBps: number;
|
|
||||||
outBps: number;
|
|
||||||
inUtil: number;
|
|
||||||
outUtil: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
target: PortTrafficTarget | null;
|
target: PortTrafficTarget | null;
|
||||||
points: PortTrafficSamplePoint[];
|
points: PortTrafficSamplePoint[];
|
||||||
|
|
@ -69,13 +61,10 @@ export function PortTrafficWall({ target, points, rangeLabel, loading, hint }: P
|
||||||
const plotRef = useRef<uPlot | null>(null);
|
const plotRef = useRef<uPlot | null>(null);
|
||||||
const tipRef = useRef<HTMLDivElement | null>(null);
|
const tipRef = useRef<HTMLDivElement | null>(null);
|
||||||
const pointsRef = useRef(points);
|
const pointsRef = useRef(points);
|
||||||
const hoverIdxRef = useRef<number | null>(null);
|
|
||||||
const [hover, setHover] = useState<HoverKpi | null>(null);
|
|
||||||
|
|
||||||
pointsRef.current = points;
|
pointsRef.current = points;
|
||||||
|
|
||||||
const latest = points.length ? points[points.length - 1] : null;
|
const latest = points.length ? points[points.length - 1] : null;
|
||||||
const latestKpi = useMemo(() => {
|
const kpi = useMemo(() => {
|
||||||
const bw = latest?.bw_bps || target?.bw_bps || 0;
|
const bw = latest?.bw_bps || target?.bw_bps || 0;
|
||||||
return {
|
return {
|
||||||
bw,
|
bw,
|
||||||
|
|
@ -87,27 +76,11 @@ export function PortTrafficWall({ target, points, rangeLabel, loading, hint }: P
|
||||||
};
|
};
|
||||||
}, [latest, target]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
const el = mountRef.current;
|
const el = mountRef.current;
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
|
|
||||||
const { width, height } = chartSize(el);
|
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 placeTip = (u: uPlot, left: number, top: number) => {
|
||||||
const tip = tipRef.current;
|
const tip = tipRef.current;
|
||||||
|
|
@ -129,16 +102,13 @@ export function PortTrafficWall({ target, points, rangeLabel, loading, hint }: P
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
series: [
|
series: [
|
||||||
{
|
{},
|
||||||
value: (_u, raw) => (raw == null ? "—" : formatHoverTime(Number(raw))),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: "In",
|
label: "In",
|
||||||
stroke: "#2dd4bf",
|
stroke: "#2dd4bf",
|
||||||
width: 2,
|
width: 2,
|
||||||
fill: "rgba(45, 212, 191, 0.12)",
|
fill: "rgba(45, 212, 191, 0.12)",
|
||||||
points: { show: true, size: 5, fill: "#2dd4bf" },
|
points: { show: true, size: 5, fill: "#2dd4bf" },
|
||||||
value: valueFmt,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Out",
|
label: "Out",
|
||||||
|
|
@ -146,7 +116,6 @@ export function PortTrafficWall({ target, points, rangeLabel, loading, hint }: P
|
||||||
width: 2,
|
width: 2,
|
||||||
fill: "rgba(245, 158, 11, 0.10)",
|
fill: "rgba(245, 158, 11, 0.10)",
|
||||||
points: { show: true, size: 5, fill: "#f59e0b" },
|
points: { show: true, size: 5, fill: "#f59e0b" },
|
||||||
value: valueFmt,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
axes: [
|
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: {
|
cursor: {
|
||||||
drag: { x: true, y: false },
|
drag: { x: true, y: false },
|
||||||
focus: { prox: 32 },
|
focus: { prox: 32 },
|
||||||
|
|
@ -195,10 +165,6 @@ export function PortTrafficWall({ target, points, rangeLabel, loading, hint }: P
|
||||||
const top = u.cursor.top ?? 0;
|
const top = u.cursor.top ?? 0;
|
||||||
|
|
||||||
if (idx == null || idx < 0 || !u.data[0]?.length) {
|
if (idx == null || idx < 0 || !u.data[0]?.length) {
|
||||||
if (hoverIdxRef.current !== null) {
|
|
||||||
hoverIdxRef.current = null;
|
|
||||||
setHover(null);
|
|
||||||
}
|
|
||||||
if (tip) tip.style.display = "none";
|
if (tip) tip.style.display = "none";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -211,28 +177,17 @@ export function PortTrafficWall({ target, points, rangeLabel, loading, hint }: P
|
||||||
return d && Math.floor(d.getTime() / 1000) === tsSec;
|
return d && Math.floor(d.getTime() / 1000) === tsSec;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (tip) {
|
if (!tip) return;
|
||||||
tip.style.display = "block";
|
tip.style.display = "block";
|
||||||
tip.innerHTML = [
|
tip.innerHTML = [
|
||||||
`<div class="pt-wall__tip-time">${formatHoverTime(tsSec)}</div>`,
|
`<div class="pt-wall__tip-time">${formatHoverTime(tsSec)}</div>`,
|
||||||
`<div class="pt-wall__tip-row pt-wall__tip-row--in"><span>In</span><strong>${formatBps(inBps)} bit/s</strong></div>`,
|
`<div class="pt-wall__tip-row pt-wall__tip-row--in"><span>In</span><strong>${formatBps(inBps)} bit/s</strong></div>`,
|
||||||
`<div class="pt-wall__tip-row pt-wall__tip-row--out"><span>Out</span><strong>${formatBps(outBps)} bit/s</strong></div>`,
|
`<div class="pt-wall__tip-row pt-wall__tip-row--out"><span>Out</span><strong>${formatBps(outBps)} bit/s</strong></div>`,
|
||||||
sample
|
sample
|
||||||
? `<div class="pt-wall__tip-row"><span>Util</span><strong>${Number(sample.in_util_pct || 0).toFixed(1)}% / ${Number(sample.out_util_pct || 0).toFixed(1)}%</strong></div>`
|
? `<div class="pt-wall__tip-row"><span>Util</span><strong>${Number(sample.in_util_pct || 0).toFixed(1)}% / ${Number(sample.out_util_pct || 0).toFixed(1)}%</strong></div>`
|
||||||
: "",
|
: "",
|
||||||
].join("");
|
].join("");
|
||||||
placeTip(u, left, top);
|
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),
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
@ -241,8 +196,6 @@ export function PortTrafficWall({ target, points, rangeLabel, loading, hint }: P
|
||||||
plotRef.current = new uPlot(opts, [[], [], []], el);
|
plotRef.current = new uPlot(opts, [[], [], []], el);
|
||||||
|
|
||||||
const onLeave = () => {
|
const onLeave = () => {
|
||||||
hoverIdxRef.current = null;
|
|
||||||
setHover(null);
|
|
||||||
if (tipRef.current) tipRef.current.style.display = "none";
|
if (tipRef.current) tipRef.current.style.display = "none";
|
||||||
};
|
};
|
||||||
plotRef.current.over.addEventListener("mouseleave", onLeave);
|
plotRef.current.over.addEventListener("mouseleave", onLeave);
|
||||||
|
|
@ -268,31 +221,29 @@ export function PortTrafficWall({ target, points, rangeLabel, loading, hint }: P
|
||||||
}, [points]);
|
}, [points]);
|
||||||
|
|
||||||
const empty = points.length === 0;
|
const empty = points.length === 0;
|
||||||
|
const neLabel = target ? target.ne_name || target.ne_ip || "—" : "—";
|
||||||
|
const ifLabel = target?.ifname || "—";
|
||||||
|
const ipLabel = target?.ne_ip || "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pt-wall">
|
<div className="pt-wall">
|
||||||
<div className="pt-wall__head">
|
<div className="pt-wall__head">
|
||||||
<div className="pt-wall__title">
|
<div className="pt-wall__head-title">{t("portTraffic.wallLiveTitle")}</div>
|
||||||
<span className="pt-wall__ne">{target ? target.ne_name || target.ne_ip || "—" : "—"}</span>
|
|
||||||
<span className="pt-wall__if">{target?.ifname || "Select interface"}</span>
|
|
||||||
</div>
|
|
||||||
<div className="pt-wall__range">
|
<div className="pt-wall__range">
|
||||||
{rangeLabel}
|
{rangeLabel}
|
||||||
{kpi.hovering
|
{kpi.ts ? ` · ${t("portTraffic.latestAt")} ${formatSystemTime(kpi.ts)}` : ""}
|
||||||
? ` · ${t("portTraffic.hoverPoint")}`
|
|
||||||
: latestKpi.ts
|
|
||||||
? ` · ${t("portTraffic.latestAt")} ${formatSystemTime(latestKpi.ts)}`
|
|
||||||
: ""}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-wall__kpis">
|
<div className="pt-wall__kpis">
|
||||||
<div className="pt-wall__kpi">
|
<div className="pt-wall__kpi">
|
||||||
<div className="pt-wall__kpi-label">{t("portTraffic.kpiBw")}</div>
|
<div className="pt-wall__kpi-label">
|
||||||
|
{t("portTraffic.kpiBw")} · {t("portTraffic.kpiLatest")}
|
||||||
|
</div>
|
||||||
<div className="pt-wall__kpi-value">{formatBwLabel(kpi.bw)}</div>
|
<div className="pt-wall__kpi-value">{formatBwLabel(kpi.bw)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-wall__kpi pt-wall__kpi--in">
|
<div className="pt-wall__kpi pt-wall__kpi--in">
|
||||||
<div className="pt-wall__kpi-label">
|
<div className="pt-wall__kpi-label">
|
||||||
In · {kpi.hovering ? t("portTraffic.hoverPoint") : t("portTraffic.kpiLatest")}
|
In · {t("portTraffic.kpiLatest")}
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-wall__kpi-value">
|
<div className="pt-wall__kpi-value">
|
||||||
{formatBps(kpi.inBps)}
|
{formatBps(kpi.inBps)}
|
||||||
|
|
@ -301,7 +252,7 @@ export function PortTrafficWall({ target, points, rangeLabel, loading, hint }: P
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-wall__kpi pt-wall__kpi--out">
|
<div className="pt-wall__kpi pt-wall__kpi--out">
|
||||||
<div className="pt-wall__kpi-label">
|
<div className="pt-wall__kpi-label">
|
||||||
Out · {kpi.hovering ? t("portTraffic.hoverPoint") : t("portTraffic.kpiLatest")}
|
Out · {t("portTraffic.kpiLatest")}
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-wall__kpi-value">
|
<div className="pt-wall__kpi-value">
|
||||||
{formatBps(kpi.outBps)}
|
{formatBps(kpi.outBps)}
|
||||||
|
|
@ -310,7 +261,7 @@ export function PortTrafficWall({ target, points, rangeLabel, loading, hint }: P
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-wall__kpi">
|
<div className="pt-wall__kpi">
|
||||||
<div className="pt-wall__kpi-label">
|
<div className="pt-wall__kpi-label">
|
||||||
Util · {kpi.hovering ? t("portTraffic.hoverPoint") : t("portTraffic.kpiLatest")}
|
Util · {t("portTraffic.kpiLatest")}
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-wall__kpi-value">
|
<div className="pt-wall__kpi-value">
|
||||||
{kpi.inUtil.toFixed(1)}%
|
{kpi.inUtil.toFixed(1)}%
|
||||||
|
|
@ -328,6 +279,17 @@ export function PortTrafficWall({ target, points, rangeLabel, loading, hint }: P
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="pt-wall__foot">
|
||||||
|
<div className="pt-wall__foot-ne" title={neLabel}>
|
||||||
|
<span className="pt-wall__foot-label">{t("portTraffic.wallDevice")}</span>
|
||||||
|
<span className="pt-wall__foot-value">{neLabel}</span>
|
||||||
|
{ipLabel ? <span className="pt-wall__foot-ip">{ipLabel}</span> : null}
|
||||||
|
</div>
|
||||||
|
<div className="pt-wall__foot-if" title={ifLabel}>
|
||||||
|
<span className="pt-wall__foot-label">{t("portTraffic.wallPort")}</span>
|
||||||
|
<span className="pt-wall__foot-value pt-wall__foot-value--mono">{ifLabel}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue