import { useEffect, useMemo, useRef, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useNavigate, useSearchParams } from "react-router-dom"; import { createPortTrafficDevice, deletePortTrafficDevice, discoverPortTrafficPorts, fetchCliTargets, fetchPortTrafficDashboard, fetchPortTrafficDevices, fetchPortTrafficEvents, fetchPortTrafficTargets, pausePortTrafficDevice, putPortTrafficInterfaces, rebindPortTrafficDevice, startPortTrafficDevice, stopPortTrafficDevice, updatePortTrafficDevice, } from "../../services/api"; import { queryKeys } from "../../constants/queryKeys"; import { useI18n } from "../../i18n"; import { useToast } from "../../hooks/useToast"; import type { CliTargetItem, PortTrafficDevice, PortTrafficDiscoverPort, PortTrafficIfaceIn, } from "../../types"; import { pageCount } from "../../utils/display"; import { formatSystemTime } from "../../utils/time"; const POLL_MS = 5000; const TARGET_PAGE_SIZE = 20; type ViewMode = "list" | "wizard" | "edit"; function statusTone(status: string): "running" | "paused" | "stopped" | "other" { if (status === "running") return "running"; if (status === "paused") return "paused"; if (status === "stopped") return "stopped"; return "other"; } function upDownPillClass(value: string): string { const v = String(value || "").trim().toLowerCase(); if (v === "up") return "conn-pill conn-pill--up"; if (v === "down") return "conn-pill conn-pill--down"; return "conn-pill conn-pill--unknown"; } function PortUpDownCell({ admin, phy, prot, labels, }: { admin?: string; phy?: string; prot?: string; labels: { admin: string; phy: string; prot: string }; }) { const a = String(admin || "").trim() || "—"; const p = String(phy || "").trim() || "—"; const r = String(prot || "").trim() || "—"; return ( {a} {p} {r} ); } function formatPortTrafficLogMessage( raw: string, t: (key: string, vars?: Record) => string, ): string { const s = String(raw || "").trim(); if (!s) return ""; if (s.includes("managed_ne_not_found") || s.includes("ume_ne_not_found")) { return t("portTraffic.err.managedNeNotFound"); } const m = s.match(/^(\d+)_target_errors$/); if (m) return t("portTraffic.err.targetErrors", { count: m[1] }); if (s === "unsupported_vendor") return t("portTraffic.err.unsupportedVendor"); if (s === "invalid_source") return t("portTraffic.err.invalidSource"); return s; } function needsNeRebind(row: PortTrafficDevice): boolean { const err = String(row.last_error || ""); if (err.includes("managed_ne_not_found") || err.includes("ume_ne_not_found")) return true; // Older rounds only stored "N_target_errors"; still offer rebind when IP is known. if (/^\d+_target_errors$/.test(err.trim()) && String(row.ne_ip || "").trim()) return true; return false; } function formatBw(bps: number) { if (!bps) return "—"; if (bps >= 1e9) return `${bps / 1e9}G`; if (bps >= 1e6) return `${bps / 1e6}M`; return `${bps}`; } function deviceLabel(d: PortTrafficDevice) { return d.ne_name || d.ne_ip || d.ne_id || "—"; } export function PortTrafficPage() { const { t } = useI18n(); const { showOk, showError } = useToast(); const queryClient = useQueryClient(); const navigate = useNavigate(); const [searchParams, setSearchParams] = useSearchParams(); const deepLinkAppliedRef = useRef(false); const [view, setView] = useState("list"); const [listPage, setListPage] = useState(1); // Wizard / edit const [editDeviceId, setEditDeviceId] = useState(""); const [editDeviceSnap, setEditDeviceSnap] = useState(null); const [intervalSec, setIntervalSec] = useState(300); const [retentionDays, setRetentionDays] = useState(7); const [concurrency, setConcurrency] = useState(1); const [note, setNote] = useState(""); const [startNow, setStartNow] = useState(true); const [neKeyword, setNeKeyword] = useState(""); const [nePage, setNePage] = useState(1); const [selectedNe, setSelectedNe] = useState(null); const [ports, setPorts] = useState([]); const [portsLoading, setPortsLoading] = useState(false); const [portsError, setPortsError] = useState(""); const [discoverMeta, setDiscoverMeta] = useState<{ ok: boolean; command: string; raw_preview: string; error: string; } | null>(null); const [pickedIfnames, setPickedIfnames] = useState>({}); const [deepLinkHint, setDeepLinkHint] = useState(""); const [deepLinkSource, setDeepLinkSource] = useState<"managed" | "ume" | "">(""); const [deepLinkNeId, setDeepLinkNeId] = useState(""); const [deepLinkIfname, setDeepLinkIfname] = useState(""); const [wizardStep, setWizardStep] = useState<1 | 2 | 3>(1); const [logDeviceId, setLogDeviceId] = useState(""); const [logDeviceLabel, setLogDeviceLabel] = useState(""); const [rebindDevice, setRebindDevice] = useState(null); const [rebindKeyword, setRebindKeyword] = useState(""); const [rebindPage, setRebindPage] = useState(1); const [rebindSelectedNe, setRebindSelectedNe] = useState(null); const dashQuery = useQuery({ queryKey: queryKeys.portTrafficDashboard, queryFn: fetchPortTrafficDashboard, staleTime: 2000, refetchInterval: POLL_MS, }); const devicesQuery = useQuery({ queryKey: queryKeys.portTrafficDevices(listPage), queryFn: () => fetchPortTrafficDevices({ page: listPage, pageSize: 10 }), staleTime: 2000, refetchInterval: POLL_MS, }); const nesQuery = useQuery({ queryKey: queryKeys.cliTargets(neKeyword, nePage, TARGET_PAGE_SIZE), queryFn: () => fetchCliTargets({ source: "all", keyword: neKeyword, page: nePage, pageSize: TARGET_PAGE_SIZE }), enabled: view === "wizard" && wizardStep === 1, staleTime: 5000, }); const rebindSource = rebindDevice?.source === "ume" ? "ume" : "managed"; const rebindNesQuery = useQuery({ queryKey: ["cliTargets", "rebind", rebindSource, rebindKeyword, rebindPage, TARGET_PAGE_SIZE], queryFn: () => fetchCliTargets({ source: rebindSource, keyword: rebindKeyword, page: rebindPage, pageSize: TARGET_PAGE_SIZE, }), enabled: Boolean(rebindDevice), staleTime: 5000, }); const editTargetsQuery = useQuery({ queryKey: queryKeys.portTrafficTargets(editDeviceId), queryFn: () => fetchPortTrafficTargets(editDeviceId), enabled: view === "edit" && Boolean(editDeviceId), staleTime: 2000, }); const logQuery = useQuery({ queryKey: queryKeys.portTrafficEvents(logDeviceId), queryFn: () => fetchPortTrafficEvents(logDeviceId, 200), enabled: Boolean(logDeviceId), staleTime: 2000, refetchInterval: logDeviceId ? POLL_MS : false, }); useEffect(() => { if (deepLinkAppliedRef.current) return; const neId = (searchParams.get("ne_id") || "").trim(); if (!neId) return; deepLinkAppliedRef.current = true; const source = searchParams.get("source") === "ume" ? "ume" : "managed"; const ifname = (searchParams.get("ifname") || "").trim(); setDeepLinkNeId(neId); setDeepLinkSource(source); setDeepLinkIfname(ifname); setView("wizard"); setWizardStep(1); setNeKeyword(neId); setNePage(1); setDeepLinkHint( t("portTraffic.deepLinkHint") .replace("{{ne}}", neId) .replace( "{{ifname}}", ifname ? t("portTraffic.deepLinkIfname").replace("{{ifname}}", ifname) : "", ), ); setSearchParams({}, { replace: true }); }, [searchParams, setSearchParams, t]); useEffect(() => { if (!deepLinkNeId || view !== "wizard" || wizardStep !== 1) return; const items = nesQuery.data?.items || []; const hit = items.find((row) => { const src = row.source === "ume" ? "ume" : "managed"; if (deepLinkSource && src !== deepLinkSource) return false; return row.id === deepLinkNeId; }); if (!hit) return; setSelectedNe(hit); setDeepLinkNeId(""); }, [deepLinkNeId, deepLinkSource, nesQuery.data, view, wizardStep]); const invalidateAll = () => { void queryClient.invalidateQueries({ queryKey: queryKeys.portTrafficDevicesAll }); void queryClient.invalidateQueries({ queryKey: queryKeys.portTrafficDashboard }); }; const createMut = useMutation({ mutationFn: createPortTrafficDevice, onSuccess: () => { showOk(t("portTraffic.created")); invalidateAll(); resetWizard(); setView("list"); }, onError: (e: Error) => showError(e.message || t("portTraffic.createFailed")), }); const updateMut = useMutation({ mutationFn: ({ id, body }: { id: string; body: Parameters[1] }) => updatePortTrafficDevice(id, body), onSuccess: () => { showOk(t("portTraffic.updated")); invalidateAll(); }, onError: (e: Error) => showError(e.message), }); const putIfacesMut = useMutation({ mutationFn: ({ id, ifaces }: { id: string; ifaces: PortTrafficIfaceIn[] }) => putPortTrafficInterfaces(id, ifaces), onSuccess: () => { showOk(t("portTraffic.interfacesSaved")); invalidateAll(); void queryClient.invalidateQueries({ queryKey: queryKeys.portTrafficTargets(editDeviceId) }); }, onError: (e: Error) => showError(e.message), }); const startMut = useMutation({ mutationFn: startPortTrafficDevice, onSuccess: () => { showOk(t("portTraffic.started")); invalidateAll(); }, onError: (e: Error) => showError(e.message), }); const pauseMut = useMutation({ mutationFn: pausePortTrafficDevice, onSuccess: () => { showOk(t("portTraffic.paused")); invalidateAll(); }, onError: (e: Error) => showError(e.message), }); const stopMut = useMutation({ mutationFn: stopPortTrafficDevice, onSuccess: () => { showOk(t("portTraffic.stopped")); invalidateAll(); }, onError: (e: Error) => showError(e.message), }); const deleteMut = useMutation({ mutationFn: deletePortTrafficDevice, onSuccess: () => { showOk(t("portTraffic.deleted")); invalidateAll(); }, onError: (e: Error) => showError(e.message), }); const rebindMut = useMutation({ mutationFn: ({ id, neId }: { id: string; neId: string }) => rebindPortTrafficDevice(id, { ne_id: neId }), onSuccess: () => { showOk(t("portTraffic.rebindOk")); setRebindDevice(null); setRebindSelectedNe(null); setRebindKeyword(""); setRebindPage(1); invalidateAll(); }, onError: (e: Error) => { const msg = String(e.message || ""); if (msg.includes("managed_ne_not_found") || msg.includes("ume_ne_not_found")) { showError(t("portTraffic.err.rebindNeMissing")); } else if (msg.includes("device_already_monitored")) { showError(t("portTraffic.err.alreadyMonitored")); } else if (msg.includes("ne_id_required")) { showError(t("portTraffic.err.rebindPickFirst")); } else { showError(msg || t("portTraffic.rebindFailed")); } }, }); const openRebind = (row: PortTrafficDevice) => { setRebindDevice(row); setRebindSelectedNe(null); setRebindKeyword(row.ne_ip || row.ne_name || ""); setRebindPage(1); }; const closeRebind = () => { if (rebindMut.isPending) return; setRebindDevice(null); setRebindSelectedNe(null); setRebindKeyword(""); setRebindPage(1); }; const confirmRebind = () => { if (!rebindDevice || !rebindSelectedNe) { showError(t("portTraffic.err.rebindPickFirst")); return; } const src = rebindSelectedNe.source === "ume" ? "ume" : "managed"; if (src !== rebindSource) { showError(t("portTraffic.err.rebindSourceMismatch")); return; } const label = `${rebindSelectedNe.name} (${rebindSelectedNe.ip_address})`; if (!window.confirm(t("portTraffic.rebindConfirm", { ne: label }))) return; rebindMut.mutate({ id: rebindDevice.id, neId: rebindSelectedNe.id }); }; const resetWizard = () => { setWizardStep(1); setIntervalSec(300); setRetentionDays(7); setConcurrency(1); setNote(""); setStartNow(true); setSelectedNe(null); setPorts([]); setPortsError(""); setDiscoverMeta(null); setPickedIfnames({}); setNeKeyword(""); setNePage(1); setDeepLinkHint(""); setDeepLinkSource(""); setDeepLinkNeId(""); setDeepLinkIfname(""); setEditDeviceId(""); setEditDeviceSnap(null); }; const openWizard = () => { resetWizard(); setView("wizard"); }; const openWallList = () => { navigate("/network/tasks/port-traffic/wall"); }; const openEdit = (row: PortTrafficDevice) => { setEditDeviceId(row.id); setEditDeviceSnap(row); setIntervalSec(row.interval_sec); setRetentionDays(row.retention_days); setConcurrency(row.concurrency || 1); setNote(row.note || ""); setPickedIfnames({}); setPorts([]); setPortsError(""); setDiscoverMeta(null); setView("edit"); }; useEffect(() => { if (view !== "edit" || !editTargetsQuery.data) return; const next: Record = {}; for (const tgt of editTargetsQuery.data.items) { if (tgt.status !== "active") continue; next[tgt.ifname] = { ifname: tgt.ifname, if_description: tgt.if_description, bw_bps: tgt.bw_bps, }; } setPickedIfnames(next); }, [view, editTargetsQuery.data]); const discoverSelected = async () => { const source = view === "edit" ? editDeviceSnap?.source === "ume" ? "ume" : "managed" : selectedNe?.source === "ume" ? "ume" : "managed"; const id = view === "edit" ? editDeviceSnap?.ne_id || "" : selectedNe?.id || ""; if (!id) return; setPortsLoading(true); setPortsError(""); setDiscoverMeta(null); try { const res = await discoverPortTrafficPorts({ source, id }); setPorts(res.ports || []); setDiscoverMeta({ ok: res.ok !== false, command: res.command || "", raw_preview: res.raw_preview || "", error: res.error || "", }); if (res.ok === false) { const msg = res.error || t("portTraffic.discoverFail"); setPortsError(msg); showError(msg); } if (deepLinkIfname) { const hit = (res.ports || []).find((p) => p.ifname === deepLinkIfname); if (hit) { setPickedIfnames((prev) => ({ ...prev, [hit.ifname]: { ifname: hit.ifname, if_description: hit.description, bw_bps: hit.bw_bps, }, })); } setDeepLinkIfname(""); } } catch (e) { const msg = e instanceof Error ? e.message : "discover_failed"; setPorts([]); setPortsError(msg); setDiscoverMeta({ ok: false, command: "", raw_preview: "", error: msg, }); showError(msg); } finally { setPortsLoading(false); } }; const toggleIfname = (port: PortTrafficDiscoverPort) => { setPickedIfnames((prev) => { const next = { ...prev }; if (next[port.ifname]) { delete next[port.ifname]; return next; } next[port.ifname] = { ifname: port.ifname, if_description: port.description, bw_bps: port.bw_bps, }; return next; }); }; const pickedList = useMemo(() => Object.values(pickedIfnames), [pickedIfnames]); const discoveredByIfname = useMemo( () => Object.fromEntries(ports.map((p) => [p.ifname, p])), [ports], ); const upDownLabels = { admin: t("portTraffic.col.admin"), phy: t("portTraffic.col.phy"), prot: t("portTraffic.col.prot"), }; const submitWizard = () => { if (!selectedNe) { showError(t("portTraffic.pickNeFirst")); return; } if (!pickedList.length) { showError(t("portTraffic.portsRequired")); return; } const source = selectedNe.source === "ume" ? "ume" : "managed"; createMut.mutate({ source, ne_id: selectedNe.id, ne_name: selectedNe.name, ne_ip: selectedNe.ip_address, vendor: selectedNe.vendor || "", note: note.trim(), interval_sec: intervalSec, retention_days: retentionDays, concurrency, start_now: startNow, interfaces: pickedList, }); }; const saveEdit = async () => { if (!editDeviceId) return; try { await updateMut.mutateAsync({ id: editDeviceId, body: { note: note.trim(), interval_sec: intervalSec, retention_days: retentionDays, concurrency, }, }); await putIfacesMut.mutateAsync({ id: editDeviceId, ifaces: pickedList }); setView("list"); } catch { /* toast already shown */ } }; const dash = dashQuery.data; const devices = devicesQuery.data?.items || []; const pages = pageCount(devicesQuery.data?.total || 0, 10); const editDevice = editDeviceSnap; const statusText = (status: string) => { const tone = statusTone(status); if (tone === "running") return t("portTraffic.statusRunning"); if (tone === "paused") return t("portTraffic.statusPaused"); if (tone === "stopped") return t("portTraffic.statusStopped"); return status; }; return (

{t("portTraffic.title")}

{view !== "list" ? ( ) : ( <> )}
{view === "list" ? (
{t("portTraffic.kpi.devices")}
{dash?.device_count ?? dash?.task_count ?? "—"}
{t("portTraffic.kpi.running")}
{dash?.running_device_count ?? dash?.running_task_count ?? "—"}
{t("portTraffic.kpi.ports")}
{dash?.active_target_count ?? "—"}
{t("portTraffic.kpi.samples24h")}
{dash?.sample_count_24h ?? "—"}
{!devices.length ? (

{t("portTraffic.empty")}

) : ( <>
{devices.map((row) => { const tone = statusTone(row.status); return ( ); })}
{t("portTraffic.col.device")} {t("portTraffic.col.status")} {t("portTraffic.col.ports")} {t("portTraffic.col.interval")} {t("portTraffic.col.last")} {t("portTraffic.col.actions")}
{deviceLabel(row)}
{row.ne_ip || row.ne_id} {row.vendor ? ` · ${row.vendor}` : ""} {row.note ? ` · ${row.note}` : ""}
{statusText(row.status)} {row.collect_running ? ( {t("portTraffic.collecting")} ) : null} {row.active_target_count}/{row.target_count} {row.interval_sec}s {formatSystemTime(row.last_collect_ended_at) || "—"}
{needsNeRebind(row) ? ( ) : null} {row.status !== "running" ? ( ) : ( )} {row.status !== "stopped" ? ( ) : null}
{listPage}/{Math.max(1, pages)}
)}
) : null} {view === "wizard" ? (
{[1, 2, 3].map((n) => ( ))}
{wizardStep === 1 ? ( <> {deepLinkHint ?

{deepLinkHint}

: null}
{ setNeKeyword(e.target.value); setNePage(1); }} placeholder={t("portTraffic.neKeywordPh")} /> {selectedNe ? ( {t("portTraffic.selectedOneNe")}: {selectedNe.name} ({selectedNe.ip_address}) ) : null}
{(nesQuery.data?.items || []).map((row) => { const checked = selectedNe?.id === row.id && (selectedNe.source === "ume" ? "ume" : "managed") === (row.source === "ume" ? "ume" : "managed"); return ( ); })}
{t("portTraffic.col.source")} {t("portTraffic.col.name")} IP {t("portTraffic.col.vendor")}
{ setSelectedNe(row); setPorts([]); setPickedIfnames({}); }} /> {row.source} {row.name} {row.ip_address} {row.vendor || "—"}
) : null} {wizardStep === 2 ? (
{!selectedNe ?

{t("portTraffic.pickNeFirst")}

: null} {selectedNe ? ( <>
{selectedNe.name} ({selectedNe.ip_address}) {portsError ? {portsError} : null}
{discoverMeta ? (
{t("portTraffic.discoverDetail")} {" · "} {discoverMeta.ok ? t("portTraffic.discoverOk") : t("portTraffic.discoverFail")} {discoverMeta.command ? (

{t("portTraffic.discoverCommand")}: {discoverMeta.command}

) : null} {discoverMeta.error ? (

{discoverMeta.error}

) : null}

{t("portTraffic.discoverRaw")}

                        {discoverMeta.raw_preview || "—"}
                      
) : null} {ports.length ? ( {ports.map((p) => ( ))}
{t("portTraffic.col.ifname")} {t("portTraffic.col.bw")} {t("portTraffic.col.upDown")} {t("portTraffic.col.description")}
toggleIfname(p)} /> {p.ifname} {p.bw_raw || formatBw(p.bw_bps)} {p.description || "—"}
) : null}

{t("portTraffic.selectedPorts", { count: String(pickedList.length) })}

) : null}
) : null} {wizardStep === 3 ? (

{selectedNe?.name || "—"} ({selectedNe?.ip_address || "—"})

{t("portTraffic.selectedPorts", { count: String(pickedList.length) })}

    {pickedList.slice(0, 40).map((p) => { const disc = discoveredByIfname[p.ifname]; return (
  • {p.ifname} ({formatBw(p.bw_bps || 0)}) {disc ? ( ) : null}
  • ); })} {pickedList.length > 40 ?
  • … +{pickedList.length - 40}
  • : null}
) : null}
) : null} {view === "edit" && editDevice ? (

{t("portTraffic.edit")}: {deviceLabel(editDevice)}

{t("portTraffic.selectedPorts", { count: String(pickedList.length) })}
{portsError ?

{portsError}

: null} {discoverMeta ? (
{t("portTraffic.discoverDetail")} {" · "} {discoverMeta.ok ? t("portTraffic.discoverOk") : t("portTraffic.discoverFail")} {discoverMeta.command ? (

{t("portTraffic.discoverCommand")}: {discoverMeta.command}

) : null} {discoverMeta.error ? (

{discoverMeta.error}

) : null}

{t("portTraffic.discoverRaw")}

                {discoverMeta.raw_preview || "—"}
              
) : null} {ports.length ? ( {ports.map((p) => ( ))}
{t("portTraffic.col.ifname")} {t("portTraffic.col.bw")} {t("portTraffic.col.upDown")} {t("portTraffic.col.description")}
toggleIfname(p)} /> {p.ifname} {p.bw_raw || formatBw(p.bw_bps)} {p.description || "—"}
) : (
    {pickedList.map((p) => (
  • {p.ifname}
  • ))}
)}
) : null} {logDeviceId ? (
{ setLogDeviceId(""); setLogDeviceLabel(""); }} >
e.stopPropagation()} >

{t("portTraffic.logTitle")}

{t("portTraffic.logDevice")}: {logDeviceLabel || "—"}

{logQuery.isLoading ? (

…

) : !(logQuery.data?.items || []).length ? (

{t("portTraffic.logEmpty")}

) : (
{(logQuery.data?.items || []).map((ev) => { const level = String(ev.level || "error").toLowerCase(); const tone = level === "warn" || level === "warning" ? "warn" : level === "info" ? "info" : "error"; return (
{level} {formatSystemTime(ev.created_at) || "—"} {ev.ifname ? ( {ev.ifname} ) : null}
{formatPortTrafficLogMessage(ev.message, t)}
); })}
)}
) : null} {rebindDevice ? (
e.stopPropagation()} >

{t("portTraffic.rebindTitle")}

{t("portTraffic.rebindCurrent")}: {deviceLabel(rebindDevice)} {rebindDevice.ne_ip ? ` · ${rebindDevice.ne_ip}` : ""} {` · ${rebindSource}`} {rebindDevice.ne_id ? ` · ID ${rebindDevice.ne_id.slice(0, 8)}…` : ""}

{t("portTraffic.rebindHint")}

{ setRebindKeyword(e.target.value); setRebindPage(1); }} placeholder={t("portTraffic.neKeywordPh")} /> {rebindSelectedNe ? ( {t("portTraffic.selectedOneNe")}: {rebindSelectedNe.name} ( {rebindSelectedNe.ip_address}) ) : null}
{rebindNesQuery.isLoading ? (

…

) : !(rebindNesQuery.data?.items || []).length ? (

{t("portTraffic.rebindEmpty")}

) : (
{(rebindNesQuery.data?.items || []).map((row) => { const checked = rebindSelectedNe?.id === row.id; const sameIp = Boolean(rebindDevice.ne_ip) && String(row.ip_address || "").trim() === String(rebindDevice.ne_ip || "").trim(); return ( ); })}
{t("portTraffic.col.name")} IP {t("portTraffic.col.vendor")} ID
setRebindSelectedNe(row)} /> {row.name} {row.ip_address} {sameIp ? ( {t("portTraffic.rebindSameIp")} ) : null} {row.vendor || "—"} {row.id.slice(0, 8)}…
)}
{rebindPage}/ {Math.max(1, pageCount(rebindNesQuery.data?.total || 0, TARGET_PAGE_SIZE))}
) : null}
); }