mirror of
https://github.com/hansjone/netx.git
synced 2026-10-10 02:00:45 +08:00
Add period and cross-task interface compare for port traffic wall.
Compare by interface target_id with optional mapped baseline (same window or time-shifted), keep chart samples on target_row_id, and unify UI wording to interface. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
0d85b87be7
commit
653baa1b9f
16 changed files with 1185 additions and 129 deletions
|
|
@ -6,8 +6,8 @@ import {
|
|||
deletePortTrafficTask,
|
||||
discoverPortTrafficPorts,
|
||||
fetchCliTargets,
|
||||
fetchPortTrafficCompare,
|
||||
fetchPortTrafficDashboard,
|
||||
fetchPortTrafficSamples,
|
||||
fetchPortTrafficTargets,
|
||||
fetchPortTrafficTasks,
|
||||
pausePortTrafficTask,
|
||||
|
|
@ -26,6 +26,7 @@ const POLL_MS = 5000;
|
|||
const TARGET_PAGE_SIZE = 20;
|
||||
|
||||
type PortPick = PortTrafficTargetIn & { key: string };
|
||||
type BaselineMode = "off" | "shift" | "day" | "week" | "custom";
|
||||
|
||||
function neKey(source: string, id: string) {
|
||||
return `${source}:${id}`;
|
||||
|
|
@ -51,7 +52,11 @@ export function PortTrafficPage() {
|
|||
const [taskPage, setTaskPage] = useState(1);
|
||||
const [wallTaskId, setWallTaskId] = useState("");
|
||||
const [wallTargetId, setWallTargetId] = useState("");
|
||||
const [rangeHours, setRangeHours] = useState(1);
|
||||
const [mapBaselineTaskId, setMapBaselineTaskId] = useState("");
|
||||
const [mapBaselineTargetId, setMapBaselineTargetId] = useState("");
|
||||
const [rangeHours, setRangeHours] = useState(24);
|
||||
const [baseline, setBaseline] = useState<BaselineMode>("off");
|
||||
const [customOffsetHours, setCustomOffsetHours] = useState(48);
|
||||
|
||||
// Wizard state
|
||||
const [step, setStep] = useState(1);
|
||||
|
|
@ -105,22 +110,34 @@ export function PortTrafficPage() {
|
|||
refetchInterval: POLL_MS,
|
||||
});
|
||||
|
||||
const samplesQuery = useQuery({
|
||||
queryKey: queryKeys.portTrafficSamples(wallTargetId, rangeHours),
|
||||
queryFn: () => {
|
||||
const to = new Date();
|
||||
const from = new Date(to.getTime() - rangeHours * 3600 * 1000);
|
||||
return fetchPortTrafficSamples({
|
||||
const mapBaselineTargetsQuery = useQuery({
|
||||
queryKey: queryKeys.portTrafficTargets(mapBaselineTaskId),
|
||||
queryFn: () => fetchPortTrafficTargets(mapBaselineTaskId),
|
||||
enabled: view === "wall" && Boolean(mapBaselineTaskId),
|
||||
staleTime: 2000,
|
||||
refetchInterval: POLL_MS,
|
||||
});
|
||||
|
||||
const compareQuery = useQuery({
|
||||
queryKey: queryKeys.portTrafficCompare(
|
||||
wallTargetId,
|
||||
rangeHours,
|
||||
baseline,
|
||||
baseline === "custom" ? customOffsetHours : 0,
|
||||
mapBaselineTargetId,
|
||||
),
|
||||
queryFn: () =>
|
||||
fetchPortTrafficCompare({
|
||||
targetId: wallTargetId,
|
||||
from: from.toISOString(),
|
||||
to: to.toISOString(),
|
||||
});
|
||||
},
|
||||
rangeHours,
|
||||
baseline,
|
||||
offsetHours: baseline === "custom" ? customOffsetHours : undefined,
|
||||
baselineTargetId: mapBaselineTargetId || undefined,
|
||||
}),
|
||||
enabled: view === "wall" && Boolean(wallTargetId),
|
||||
staleTime: 1000,
|
||||
refetchInterval: (q) => {
|
||||
const n = q.state.data?.points?.length ?? 0;
|
||||
// Faster while waiting for the first sample; then every 5s for live wall.
|
||||
const n = q.state.data?.current?.length ?? 0;
|
||||
return n === 0 ? 2500 : POLL_MS;
|
||||
},
|
||||
});
|
||||
|
|
@ -171,7 +188,7 @@ export function PortTrafficPage() {
|
|||
showOk(res.started ? t("portTraffic.collectStarted") : t("portTraffic.collectBusy"));
|
||||
invalidateAll();
|
||||
void queryClient.invalidateQueries({ queryKey: queryKeys.portTrafficTargets(wallTaskId) });
|
||||
void queryClient.invalidateQueries({ queryKey: ["portTrafficSamples"] });
|
||||
void queryClient.invalidateQueries({ queryKey: ["portTrafficCompare"] });
|
||||
},
|
||||
onError: (e: Error) => showError(e.message),
|
||||
});
|
||||
|
|
@ -206,6 +223,9 @@ export function PortTrafficPage() {
|
|||
const openWall = (taskId: string) => {
|
||||
setWallTaskId(taskId);
|
||||
setWallTargetId("");
|
||||
setMapBaselineTaskId("");
|
||||
setMapBaselineTargetId("");
|
||||
setBaseline("off");
|
||||
setView("wall");
|
||||
};
|
||||
|
||||
|
|
@ -265,14 +285,46 @@ export function PortTrafficPage() {
|
|||
|
||||
const selectedNeList = useMemo(() => Object.values(selectedNes), [selectedNes]);
|
||||
const pickedList = useMemo(() => Object.values(pickedPorts), [pickedPorts]);
|
||||
const wallTargets = wallTargetsQuery.data?.items || [];
|
||||
const wallTaskOptions = wallTasksQuery.data?.items || tasksQuery.data?.items || [];
|
||||
const wallTargets = useMemo(
|
||||
() => (wallTargetsQuery.data?.items || []).filter((x) => x.status === "active"),
|
||||
[wallTargetsQuery.data?.items],
|
||||
);
|
||||
const selectedWallTarget = wallTargets.find((x) => x.id === wallTargetId) || null;
|
||||
const mapBaselineOptions = useMemo(
|
||||
() =>
|
||||
(mapBaselineTargetsQuery.data?.items || []).filter(
|
||||
(x) => x.status === "active" && x.id !== wallTargetId,
|
||||
),
|
||||
[mapBaselineTargetsQuery.data?.items, wallTargetId],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (view !== "wall" || wallTargetId || !wallTargets.length) return;
|
||||
setWallTargetId(wallTargets[0].id);
|
||||
}, [view, wallTargetId, wallTargets]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mapBaselineTargetId) return;
|
||||
if (mapBaselineTargetId === wallTargetId) {
|
||||
setMapBaselineTargetId("");
|
||||
return;
|
||||
}
|
||||
if (
|
||||
mapBaselineTaskId &&
|
||||
mapBaselineTargetsQuery.isFetched &&
|
||||
!mapBaselineOptions.some((x) => x.id === mapBaselineTargetId)
|
||||
) {
|
||||
setMapBaselineTargetId("");
|
||||
}
|
||||
}, [
|
||||
mapBaselineTargetId,
|
||||
wallTargetId,
|
||||
mapBaselineTaskId,
|
||||
mapBaselineOptions,
|
||||
mapBaselineTargetsQuery.isFetched,
|
||||
]);
|
||||
|
||||
const submitWizard = () => {
|
||||
if (!title.trim()) {
|
||||
showError(t("portTraffic.titleRequired"));
|
||||
|
|
@ -459,6 +511,9 @@ export function PortTrafficPage() {
|
|||
value={retentionDays}
|
||||
onChange={(e) => setRetentionDays(Number(e.target.value) || 7)}
|
||||
/>
|
||||
<span className="muted" style={{ fontSize: 12 }}>
|
||||
{t("portTraffic.retentionHintWizard")}
|
||||
</span>
|
||||
</label>
|
||||
<label className="config-sync-policy-field">
|
||||
{t("portTraffic.concurrency")}
|
||||
|
|
@ -622,7 +677,7 @@ export function PortTrafficPage() {
|
|||
setWallTargetId("");
|
||||
}}
|
||||
>
|
||||
{(wallTasksQuery.data?.items || tasksQuery.data?.items || []).map((task) => (
|
||||
{wallTaskOptions.map((task) => (
|
||||
<option key={task.id} value={task.id}>
|
||||
{task.title}
|
||||
</option>
|
||||
|
|
@ -631,11 +686,15 @@ export function PortTrafficPage() {
|
|||
</label>
|
||||
<label>
|
||||
{t("portTraffic.wallPort")}
|
||||
<select value={wallTargetId} onChange={(e) => setWallTargetId(e.target.value)}>
|
||||
<select
|
||||
value={wallTargetId}
|
||||
onChange={(e) => setWallTargetId(e.target.value)}
|
||||
>
|
||||
<option value="">{t("portTraffic.pickPort")}</option>
|
||||
{wallTargets.map((tg) => (
|
||||
<option key={tg.id} value={tg.id}>
|
||||
{tg.ne_name} / {tg.ifname}
|
||||
{wallTargets.map((tgt) => (
|
||||
<option key={tgt.id} value={tgt.id}>
|
||||
{tgt.ne_name || tgt.ne_ip || "—"} / {tgt.ifname}
|
||||
{tgt.ne_ip ? ` (${tgt.ne_ip})` : ""}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
|
@ -648,6 +707,66 @@ export function PortTrafficPage() {
|
|||
<option value={24}>24h</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
{t("portTraffic.compare")}
|
||||
<select
|
||||
value={baseline}
|
||||
onChange={(e) => setBaseline(e.target.value as BaselineMode)}
|
||||
>
|
||||
<option value="off">{t("portTraffic.compareOff")}</option>
|
||||
<option value="day">{t("portTraffic.compareDay")}</option>
|
||||
<option value="week">{t("portTraffic.compareWeek")}</option>
|
||||
<option value="shift">{t("portTraffic.compareShift")}</option>
|
||||
<option value="custom">{t("portTraffic.compareCustom")}</option>
|
||||
</select>
|
||||
</label>
|
||||
{baseline === "custom" ? (
|
||||
<label>
|
||||
{t("portTraffic.offsetHours")}
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={24 * 90}
|
||||
value={customOffsetHours}
|
||||
onChange={(e) => setCustomOffsetHours(Number(e.target.value) || 24)}
|
||||
style={{ width: 88 }}
|
||||
/>
|
||||
</label>
|
||||
) : null}
|
||||
<label>
|
||||
{t("portTraffic.mapBaselineTask")}
|
||||
<select
|
||||
value={mapBaselineTaskId}
|
||||
onChange={(e) => {
|
||||
setMapBaselineTaskId(e.target.value);
|
||||
setMapBaselineTargetId("");
|
||||
}}
|
||||
disabled={!wallTargetId}
|
||||
>
|
||||
<option value="">{t("portTraffic.mapBaselineNone")}</option>
|
||||
{wallTaskOptions.map((task) => (
|
||||
<option key={task.id} value={task.id}>
|
||||
{task.title}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
{t("portTraffic.mapBaselinePort")}
|
||||
<select
|
||||
value={mapBaselineTargetId}
|
||||
onChange={(e) => setMapBaselineTargetId(e.target.value)}
|
||||
disabled={!mapBaselineTaskId}
|
||||
>
|
||||
<option value="">{t("portTraffic.pickPort")}</option>
|
||||
{mapBaselineOptions.map((tgt) => (
|
||||
<option key={tgt.id} value={tgt.id}>
|
||||
{tgt.ne_name || tgt.ne_ip || "—"} / {tgt.ifname}
|
||||
{tgt.ne_ip ? ` (${tgt.ne_ip})` : ""}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-primary"
|
||||
|
|
@ -657,6 +776,11 @@ export function PortTrafficPage() {
|
|||
{t("portTraffic.collectNow")}
|
||||
</button>
|
||||
</div>
|
||||
{baseline === "week" ? (
|
||||
<p className="muted" style={{ marginBottom: 8 }}>
|
||||
{t("portTraffic.retentionHint")}
|
||||
</p>
|
||||
) : null}
|
||||
{selectedWallTarget?.last_error ? (
|
||||
<p className="muted" style={{ marginBottom: 8 }}>
|
||||
{t("portTraffic.targetError")}: {selectedWallTarget.last_error}
|
||||
|
|
@ -664,9 +788,24 @@ export function PortTrafficPage() {
|
|||
) : null}
|
||||
<PortTrafficWall
|
||||
target={selectedWallTarget}
|
||||
points={samplesQuery.data?.points || []}
|
||||
rangeLabel={`${rangeHours}h`}
|
||||
loading={samplesQuery.isLoading || samplesQuery.isFetching}
|
||||
points={compareQuery.data?.current || []}
|
||||
baselinePoints={
|
||||
baseline === "off" && !mapBaselineTargetId
|
||||
? []
|
||||
: compareQuery.data?.baseline || []
|
||||
}
|
||||
rangeLabel={`${rangeHours}h${
|
||||
baseline === "off"
|
||||
? ""
|
||||
: baseline === "day"
|
||||
? ` · ${t("portTraffic.compareDay")}`
|
||||
: baseline === "week"
|
||||
? ` · ${t("portTraffic.compareWeek")}`
|
||||
: baseline === "shift"
|
||||
? ` · ${t("portTraffic.compareShift")}`
|
||||
: ` · ${t("portTraffic.compareCustom")}`
|
||||
}${mapBaselineTargetId ? ` · ${t("portTraffic.mapBaselinePort")}` : ""}`}
|
||||
loading={compareQuery.isLoading || compareQuery.isFetching}
|
||||
hint={
|
||||
!wallTargetId
|
||||
? t("portTraffic.pickPort")
|
||||
|
|
|
|||
|
|
@ -22,23 +22,41 @@ function formatHoverTime(sec: number): string {
|
|||
return formatSystemTime(new Date(sec * 1000));
|
||||
}
|
||||
|
||||
function toSeries(points: PortTrafficSamplePoint[]): [number[], number[], number[]] {
|
||||
const xs: number[] = [];
|
||||
const ins: number[] = [];
|
||||
const outs: number[] = [];
|
||||
for (const p of points) {
|
||||
function toAlignedSeries(
|
||||
current: PortTrafficSamplePoint[],
|
||||
baseline: PortTrafficSamplePoint[],
|
||||
): [number[], number[], number[], number[], number[]] {
|
||||
type Row = { x: number; inC?: number; outC?: number; inB?: number; outB?: number };
|
||||
const map = new Map<number, Row>();
|
||||
for (const p of current) {
|
||||
const d = parseApiTime(p.ts);
|
||||
if (!d) continue;
|
||||
xs.push(Math.floor(d.getTime() / 1000));
|
||||
ins.push(Number(p.in_bps) || 0);
|
||||
outs.push(Number(p.out_bps) || 0);
|
||||
const x = Math.floor(d.getTime() / 1000);
|
||||
const row = map.get(x) || { x };
|
||||
row.inC = Number(p.in_bps) || 0;
|
||||
row.outC = Number(p.out_bps) || 0;
|
||||
map.set(x, row);
|
||||
}
|
||||
if (xs.length === 1) {
|
||||
xs.push(xs[0] + 60);
|
||||
ins.push(ins[0]);
|
||||
outs.push(outs[0]);
|
||||
for (const p of baseline) {
|
||||
const d = parseApiTime(p.ts);
|
||||
if (!d) continue;
|
||||
const x = Math.floor(d.getTime() / 1000);
|
||||
const row = map.get(x) || { x };
|
||||
row.inB = Number(p.in_bps) || 0;
|
||||
row.outB = Number(p.out_bps) || 0;
|
||||
map.set(x, row);
|
||||
}
|
||||
return [xs, ins, outs];
|
||||
const rows = [...map.values()].sort((a, b) => a.x - b.x);
|
||||
if (rows.length === 1) {
|
||||
rows.push({ ...rows[0], x: rows[0].x + 60 });
|
||||
}
|
||||
return [
|
||||
rows.map((r) => r.x),
|
||||
rows.map((r) => r.inC ?? NaN),
|
||||
rows.map((r) => r.outC ?? NaN),
|
||||
rows.map((r) => r.inB ?? NaN),
|
||||
rows.map((r) => r.outB ?? NaN),
|
||||
];
|
||||
}
|
||||
|
||||
function chartSize(el: HTMLElement): { width: number; height: number } {
|
||||
|
|
@ -50,18 +68,28 @@ function chartSize(el: HTMLElement): { width: number; height: number } {
|
|||
type Props = {
|
||||
target: PortTrafficTarget | null;
|
||||
points: PortTrafficSamplePoint[];
|
||||
baselinePoints?: PortTrafficSamplePoint[];
|
||||
rangeLabel: string;
|
||||
loading?: boolean;
|
||||
hint?: string;
|
||||
};
|
||||
|
||||
export function PortTrafficWall({ target, points, rangeLabel, loading, hint }: Props) {
|
||||
export function PortTrafficWall({
|
||||
target,
|
||||
points,
|
||||
baselinePoints = [],
|
||||
rangeLabel,
|
||||
loading,
|
||||
hint,
|
||||
}: Props) {
|
||||
const { t } = useI18n();
|
||||
const mountRef = useRef<HTMLDivElement | null>(null);
|
||||
const plotRef = useRef<uPlot | null>(null);
|
||||
const tipRef = useRef<HTMLDivElement | null>(null);
|
||||
const pointsRef = useRef(points);
|
||||
const baselineRef = useRef(baselinePoints);
|
||||
pointsRef.current = points;
|
||||
baselineRef.current = baselinePoints;
|
||||
|
||||
const latest = points.length ? points[points.length - 1] : null;
|
||||
const kpi = useMemo(() => {
|
||||
|
|
@ -104,18 +132,38 @@ export function PortTrafficWall({ target, points, rangeLabel, loading, hint }: P
|
|||
series: [
|
||||
{},
|
||||
{
|
||||
label: "In",
|
||||
label: t("portTraffic.seriesCurrentIn"),
|
||||
stroke: "#2dd4bf",
|
||||
width: 2,
|
||||
fill: "rgba(45, 212, 191, 0.12)",
|
||||
points: { show: true, size: 5, fill: "#2dd4bf" },
|
||||
spanGaps: false,
|
||||
},
|
||||
{
|
||||
label: "Out",
|
||||
label: t("portTraffic.seriesCurrentOut"),
|
||||
stroke: "#f59e0b",
|
||||
width: 2,
|
||||
fill: "rgba(245, 158, 11, 0.10)",
|
||||
points: { show: true, size: 5, fill: "#f59e0b" },
|
||||
spanGaps: false,
|
||||
},
|
||||
{
|
||||
label: t("portTraffic.seriesBaselineIn"),
|
||||
stroke: "rgba(45, 212, 191, 0.45)",
|
||||
width: 1.5,
|
||||
dash: [6, 4],
|
||||
points: { show: false },
|
||||
spanGaps: false,
|
||||
show: false,
|
||||
},
|
||||
{
|
||||
label: t("portTraffic.seriesBaselineOut"),
|
||||
stroke: "rgba(245, 158, 11, 0.45)",
|
||||
width: 1.5,
|
||||
dash: [6, 4],
|
||||
points: { show: false },
|
||||
spanGaps: false,
|
||||
show: false,
|
||||
},
|
||||
],
|
||||
axes: [
|
||||
|
|
@ -149,7 +197,6 @@ export function PortTrafficWall({ target, points, rangeLabel, loading, hint }: P
|
|||
},
|
||||
},
|
||||
},
|
||||
// Color keys only — no live numeric values (those are in the hover tip).
|
||||
legend: { show: true, live: false },
|
||||
cursor: {
|
||||
drag: { x: true, y: false },
|
||||
|
|
@ -170,30 +217,53 @@ export function PortTrafficWall({ target, points, rangeLabel, loading, hint }: P
|
|||
}
|
||||
|
||||
const tsSec = Number(u.data[0][idx]);
|
||||
const inBps = Number(u.data[1]?.[idx] ?? 0);
|
||||
const outBps = Number(u.data[2]?.[idx] ?? 0);
|
||||
const inBps = Number(u.data[1]?.[idx]);
|
||||
const outBps = Number(u.data[2]?.[idx]);
|
||||
const inBase = Number(u.data[3]?.[idx]);
|
||||
const outBase = Number(u.data[4]?.[idx]);
|
||||
const sample = pointsRef.current.find((p) => {
|
||||
const d = parseApiTime(p.ts);
|
||||
return d && Math.floor(d.getTime() / 1000) === tsSec;
|
||||
});
|
||||
const baseSample = baselineRef.current.find((p) => {
|
||||
const d = parseApiTime(p.ts);
|
||||
return d && Math.floor(d.getTime() / 1000) === tsSec;
|
||||
});
|
||||
|
||||
if (!tip) return;
|
||||
tip.style.display = "block";
|
||||
tip.innerHTML = [
|
||||
const rows = [
|
||||
`<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--out"><span>Out</span><strong>${formatBps(outBps)} bit/s</strong></div>`,
|
||||
Number.isFinite(inBps)
|
||||
? `<div class="pt-wall__tip-row pt-wall__tip-row--in"><span>In</span><strong>${formatBps(inBps)} bit/s</strong></div>`
|
||||
: "",
|
||||
Number.isFinite(outBps)
|
||||
? `<div class="pt-wall__tip-row pt-wall__tip-row--out"><span>Out</span><strong>${formatBps(outBps)} bit/s</strong></div>`
|
||||
: "",
|
||||
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>`
|
||||
: "",
|
||||
].join("");
|
||||
];
|
||||
if (Number.isFinite(inBase) || Number.isFinite(outBase)) {
|
||||
rows.push(
|
||||
`<div class="pt-wall__tip-row"><span>${t("portTraffic.baseline")}</span><strong>${
|
||||
Number.isFinite(inBase) ? formatBps(inBase) : "—"
|
||||
} / ${Number.isFinite(outBase) ? formatBps(outBase) : "—"} bit/s</strong></div>`,
|
||||
);
|
||||
if (baseSample?.ts_raw) {
|
||||
rows.push(
|
||||
`<div class="pt-wall__tip-row"><span>${t("portTraffic.baselineAt")}</span><strong>${formatSystemTime(baseSample.ts_raw)}</strong></div>`,
|
||||
);
|
||||
}
|
||||
}
|
||||
tip.innerHTML = rows.join("");
|
||||
placeTip(u, left, top);
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
plotRef.current = new uPlot(opts, [[], [], []], el);
|
||||
plotRef.current = new uPlot(opts, [[], [], [], [], []], el);
|
||||
|
||||
const onLeave = () => {
|
||||
if (tipRef.current) tipRef.current.style.display = "none";
|
||||
|
|
@ -211,16 +281,20 @@ export function PortTrafficWall({ target, points, rangeLabel, loading, hint }: P
|
|||
plotRef.current?.destroy();
|
||||
plotRef.current = null;
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const plot = plotRef.current;
|
||||
if (!plot) return;
|
||||
plot.setData(toSeries(points));
|
||||
const showBaseline = baselinePoints.length > 0;
|
||||
plot.setSeries(3, { show: showBaseline });
|
||||
plot.setSeries(4, { show: showBaseline });
|
||||
plot.setData(toAlignedSeries(points, baselinePoints));
|
||||
if (mountRef.current) plot.setSize(chartSize(mountRef.current));
|
||||
}, [points]);
|
||||
}, [points, baselinePoints]);
|
||||
|
||||
const empty = points.length === 0;
|
||||
const empty = points.length === 0 && baselinePoints.length === 0;
|
||||
const neLabel = target ? target.ne_name || target.ne_ip || "—" : "—";
|
||||
const ifLabel = target?.ifname || "—";
|
||||
const ipLabel = target?.ne_ip || "";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue