mirror of
https://github.com/hansjone/netx.git
synced 2026-10-10 02:00:45 +08:00
Improve topology UX, traffic discover logs, and CLI wrap parsing.
Paginate LLDP job items, surface port-traffic raw logs with a 300s default, harden topology discover/edit/zoom/fullscreen, and join mid-token CLI wraps before TextFSM. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
66f26b738d
commit
c13ab48ba7
22 changed files with 693 additions and 75 deletions
|
|
@ -27,7 +27,9 @@ export function LldpLinksPage() {
|
|||
|
||||
const [jobPage, setJobPage] = useState(1);
|
||||
const [expandedJobId, setExpandedJobId] = useState("");
|
||||
const [itemPage, setItemPage] = useState(1);
|
||||
const [itemDetail, setItemDetail] = useState<TopologyDiscoverJobItem | null>(null);
|
||||
const ITEM_PAGE_SIZE = 20;
|
||||
|
||||
const [edgeStatus, setEdgeStatus] = useState<"all" | "active" | "missing">("all");
|
||||
const [edgeKeyword, setEdgeKeyword] = useState("");
|
||||
|
|
@ -90,8 +92,8 @@ export function LldpLinksPage() {
|
|||
});
|
||||
|
||||
const jobDetailQuery = useQuery({
|
||||
queryKey: queryKeys.lldpCollectJob(expandedJobId),
|
||||
queryFn: () => fetchLldpCollectJob(expandedJobId),
|
||||
queryKey: queryKeys.lldpCollectJob(expandedJobId, itemPage),
|
||||
queryFn: () => fetchLldpCollectJob(expandedJobId, { page: itemPage, pageSize: ITEM_PAGE_SIZE }),
|
||||
enabled: Boolean(expandedJobId),
|
||||
staleTime: 800,
|
||||
refetchInterval: () => (dashQuery.data?.running_job?.id === expandedJobId ? POLL_MS : false),
|
||||
|
|
@ -193,6 +195,8 @@ export function LldpLinksPage() {
|
|||
const jobPages = pageCount(jobTotal, 10);
|
||||
const selectedCount = useMemo(() => Object.keys(selectedMap).length, [selectedMap]);
|
||||
const detailItems = jobDetailQuery.data?.items ?? [];
|
||||
const itemTotal = Number(jobDetailQuery.data?.items_total ?? detailItems.length);
|
||||
const itemPages = pageCount(itemTotal, ITEM_PAGE_SIZE);
|
||||
const edgeItems = edgesQuery.data?.items ?? [];
|
||||
const edgeTotal = Number(edgesQuery.data?.total || 0);
|
||||
const edgePages = pageCount(edgeTotal, EDGE_PAGE_SIZE);
|
||||
|
|
@ -546,6 +550,7 @@ export function LldpLinksPage() {
|
|||
onClick={() => {
|
||||
setItemDetail(null);
|
||||
setExpandedJobId(open ? "" : job.id);
|
||||
setItemPage(1);
|
||||
}}
|
||||
>
|
||||
{open ? "−" : "+"}
|
||||
|
|
@ -653,6 +658,25 @@ export function LldpLinksPage() {
|
|||
) : null}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="pager">
|
||||
<button type="button" disabled={itemPage <= 1} onClick={() => setItemPage((p) => p - 1)}>
|
||||
{t("common.prevPage")}
|
||||
</button>
|
||||
<span className="muted">
|
||||
{t("common.pagerMeta", {
|
||||
total: String(itemTotal),
|
||||
page: String(itemPage),
|
||||
pages: String(itemPages),
|
||||
})}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
disabled={itemPage >= itemPages}
|
||||
onClick={() => setItemPage((p) => p + 1)}
|
||||
>
|
||||
{t("common.nextPage")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ export function PortTrafficPage() {
|
|||
// Wizard / edit
|
||||
const [editDeviceId, setEditDeviceId] = useState("");
|
||||
const [editDeviceSnap, setEditDeviceSnap] = useState<PortTrafficDevice | null>(null);
|
||||
const [intervalSec, setIntervalSec] = useState(60);
|
||||
const [intervalSec, setIntervalSec] = useState(300);
|
||||
const [retentionDays, setRetentionDays] = useState(7);
|
||||
const [concurrency, setConcurrency] = useState(1);
|
||||
const [note, setNote] = useState("");
|
||||
|
|
@ -137,6 +137,12 @@ export function PortTrafficPage() {
|
|||
const [ports, setPorts] = useState<PortTrafficDiscoverPort[]>([]);
|
||||
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<Record<string, PortTrafficIfaceIn>>({});
|
||||
const [deepLinkHint, setDeepLinkHint] = useState("");
|
||||
const [deepLinkSource, setDeepLinkSource] = useState<"managed" | "ume" | "">("");
|
||||
|
|
@ -366,7 +372,7 @@ export function PortTrafficPage() {
|
|||
|
||||
const resetWizard = () => {
|
||||
setWizardStep(1);
|
||||
setIntervalSec(60);
|
||||
setIntervalSec(300);
|
||||
setRetentionDays(7);
|
||||
setConcurrency(1);
|
||||
setNote("");
|
||||
|
|
@ -374,6 +380,7 @@ export function PortTrafficPage() {
|
|||
setSelectedNe(null);
|
||||
setPorts([]);
|
||||
setPortsError("");
|
||||
setDiscoverMeta(null);
|
||||
setPickedIfnames({});
|
||||
setNeKeyword("");
|
||||
setNePage(1);
|
||||
|
|
@ -404,6 +411,7 @@ export function PortTrafficPage() {
|
|||
setPickedIfnames({});
|
||||
setPorts([]);
|
||||
setPortsError("");
|
||||
setDiscoverMeta(null);
|
||||
setView("edit");
|
||||
};
|
||||
|
||||
|
|
@ -434,11 +442,23 @@ export function PortTrafficPage() {
|
|||
if (!id) return;
|
||||
setPortsLoading(true);
|
||||
setPortsError("");
|
||||
setDiscoverMeta(null);
|
||||
try {
|
||||
const res = await discoverPortTrafficPorts({ source, id });
|
||||
setPorts(res.ports);
|
||||
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);
|
||||
const hit = (res.ports || []).find((p) => p.ifname === deepLinkIfname);
|
||||
if (hit) {
|
||||
setPickedIfnames((prev) => ({
|
||||
...prev,
|
||||
|
|
@ -455,6 +475,12 @@ export function PortTrafficPage() {
|
|||
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);
|
||||
|
|
@ -841,6 +867,29 @@ export function PortTrafficPage() {
|
|||
</button>
|
||||
{portsError ? <span className="muted">{portsError}</span> : null}
|
||||
</div>
|
||||
{discoverMeta ? (
|
||||
<details className="panel" open style={{ marginBottom: 12 }}>
|
||||
<summary>
|
||||
{t("portTraffic.discoverDetail")}
|
||||
{" · "}
|
||||
{discoverMeta.ok ? t("portTraffic.discoverOk") : t("portTraffic.discoverFail")}
|
||||
</summary>
|
||||
{discoverMeta.command ? (
|
||||
<p className="muted">
|
||||
{t("portTraffic.discoverCommand")}: <code>{discoverMeta.command}</code>
|
||||
</p>
|
||||
) : null}
|
||||
{discoverMeta.error ? (
|
||||
<p className="muted" style={{ color: "var(--danger, #b00)" }}>
|
||||
{discoverMeta.error}
|
||||
</p>
|
||||
) : null}
|
||||
<p className="muted">{t("portTraffic.discoverRaw")}</p>
|
||||
<pre className="topo-discover-modal__raw" style={{ maxHeight: 240, overflow: "auto" }}>
|
||||
{discoverMeta.raw_preview || "—"}
|
||||
</pre>
|
||||
</details>
|
||||
) : null}
|
||||
{ports.length ? (
|
||||
<table className="data-table">
|
||||
<thead>
|
||||
|
|
@ -894,7 +943,7 @@ export function PortTrafficPage() {
|
|||
min={15}
|
||||
max={3600}
|
||||
value={intervalSec}
|
||||
onChange={(e) => setIntervalSec(Number(e.target.value) || 60)}
|
||||
onChange={(e) => setIntervalSec(Number(e.target.value) || 300)}
|
||||
/>
|
||||
</label>
|
||||
<label className="config-sync-policy-field">
|
||||
|
|
@ -1005,7 +1054,7 @@ export function PortTrafficPage() {
|
|||
min={15}
|
||||
max={3600}
|
||||
value={intervalSec}
|
||||
onChange={(e) => setIntervalSec(Number(e.target.value) || 60)}
|
||||
onChange={(e) => setIntervalSec(Number(e.target.value) || 300)}
|
||||
/>
|
||||
</label>
|
||||
<label className="config-sync-policy-field">
|
||||
|
|
@ -1040,6 +1089,29 @@ export function PortTrafficPage() {
|
|||
<span className="muted">{t("portTraffic.selectedPorts", { count: String(pickedList.length) })}</span>
|
||||
</div>
|
||||
{portsError ? <p className="muted">{portsError}</p> : null}
|
||||
{discoverMeta ? (
|
||||
<details className="panel" open style={{ marginBottom: 12 }}>
|
||||
<summary>
|
||||
{t("portTraffic.discoverDetail")}
|
||||
{" · "}
|
||||
{discoverMeta.ok ? t("portTraffic.discoverOk") : t("portTraffic.discoverFail")}
|
||||
</summary>
|
||||
{discoverMeta.command ? (
|
||||
<p className="muted">
|
||||
{t("portTraffic.discoverCommand")}: <code>{discoverMeta.command}</code>
|
||||
</p>
|
||||
) : null}
|
||||
{discoverMeta.error ? (
|
||||
<p className="muted" style={{ color: "var(--danger, #b00)" }}>
|
||||
{discoverMeta.error}
|
||||
</p>
|
||||
) : null}
|
||||
<p className="muted">{t("portTraffic.discoverRaw")}</p>
|
||||
<pre className="topo-discover-modal__raw" style={{ maxHeight: 240, overflow: "auto" }}>
|
||||
{discoverMeta.raw_preview || "—"}
|
||||
</pre>
|
||||
</details>
|
||||
) : null}
|
||||
{ports.length ? (
|
||||
<table className="data-table">
|
||||
<thead>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue