From 73cd2704eb3a8f93f0866cb7e73400ed1906c4e7 Mon Sep 17 00:00:00 2001 From: oliver Date: Wed, 5 Aug 2026 22:52:10 +0800 Subject: [PATCH] Hide TOPO/LLDP placeholder NEs on the canvas by default. Add a Display toggle to show placeholders; hide their incident edges and skip them in canvas find while hidden. Co-authored-by: Cursor --- web/src/i18n/en.ts | 1 + web/src/i18n/zh.ts | 1 + web/src/pages/TopologyPage.tsx | 75 ++++++++++++++++++++++++++++++---- 3 files changed, 69 insertions(+), 8 deletions(-) diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index 060ea6c..ef2265a 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -1566,6 +1566,7 @@ const en = { }, hideIp: "Hide IP", hideVendor: "Hide vendor", + showPlaceholders: "Show placeholders (TOPO/LLDP)", hidePorts: "Hide ports", edgeFlow: "Edge flow", collapseSidebar: "Collapse sidebar", diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index 9576341..de23851 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -1555,6 +1555,7 @@ const zh = { }, hideIp: "隐藏 IP", hideVendor: "隐藏厂商", + showPlaceholders: "显示占位网元(TOPO/LLDP)", hidePorts: "隐藏端口", edgeFlow: "链路流动", collapseSidebar: "折叠侧栏", diff --git a/web/src/pages/TopologyPage.tsx b/web/src/pages/TopologyPage.tsx index ac87dd1..ab718d6 100644 --- a/web/src/pages/TopologyPage.tsx +++ b/web/src/pages/TopologyPage.tsx @@ -484,6 +484,7 @@ const AUTO_LAYOUT_DISCOVER_KEY = "netx.topology.autoLayoutAfterDiscover"; const DISCOVER_AUTO_ADD_KEY = "netx.topology.discoverAutoAddUnmatched.v2"; const DISCOVER_PROJECT_NEIGHBORS_KEY = "netx.topology.discoverProjectNeighbors.v2"; const SCALE_BUNDLE_WIDTH_KEY = "netx.topology.scaleBundleWidth"; +const SHOW_PLACEHOLDERS_KEY = "netx.topology.showPlaceholders"; const CANVAS_BG_KEY = "netx.topology.canvasBg"; const DEFAULT_CANVAS_BG = "#0f172a"; /** Previous light default — migrate so existing sessions pick up dark canvas. */ @@ -860,6 +861,10 @@ export function TopologyPage() { const [scaleBundleWidth, setScaleBundleWidth] = useState(() => loadBoolFlag(SCALE_BUNDLE_WIDTH_KEY, false), ); + /** Topology/LLDP placeholders hidden by default; toggle in Display menu. */ + const [showPlaceholders, setShowPlaceholders] = useState(() => + loadBoolFlag(SHOW_PLACEHOLDERS_KEY, false), + ); const [edgeFlow, setEdgeFlow] = useState(false); const [edgeDefaults, setEdgeDefaults] = useState(() => loadEdgeDefaults()); const [canvasBg, setCanvasBg] = useState(loadCanvasBg); @@ -914,6 +919,21 @@ export function TopologyPage() { const findJustLocatedRef = useRef(false); const [nodes, setNodes, onNodesChange] = useNodesState>([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); + + useEffect(() => { + if (showPlaceholders) return; + setNodes((ns) => { + let changed = false; + const next = ns.map((n) => { + if (!n.selected) return n; + if (!isPlaceholderSource(n.data.managed_source, n.data.ne_ip)) return n; + changed = true; + return { ...n, selected: false }; + }); + return changed ? next : ns; + }); + }, [showPlaceholders, setNodes]); + const rfRef = useRef, Edge> | null>(null); const canvasRef = useRef(null); const dirtyRef = useRef(false); @@ -1034,6 +1054,33 @@ export function TopologyPage() { }); }, [edges, expandPhysicalLinks, hidePorts, scaleBundleWidth, edgeFlow, edgeDefaults, selectedEdgeId]); + const hiddenPlaceholderIds = useMemo(() => { + if (showPlaceholders) return new Set(); + return new Set( + nodes + .filter((n) => isPlaceholderSource(n.data.managed_source, n.data.ne_ip)) + .map((n) => n.id), + ); + }, [nodes, showPlaceholders]); + + const flowNodes = useMemo(() => { + const list = hiddenPlaceholderIds.size + ? nodes.filter((n) => !hiddenPlaceholderIds.has(n.id)) + : nodes; + return list.map((n) => + searchHitIds.includes(n.id) + ? { ...n, className: "is-search-hit" } + : { ...n, className: undefined }, + ); + }, [nodes, hiddenPlaceholderIds, searchHitIds]); + + const flowEdges = useMemo(() => { + if (!hiddenPlaceholderIds.size) return displayEdges; + return displayEdges.filter( + (e) => !hiddenPlaceholderIds.has(e.source) && !hiddenPlaceholderIds.has(e.target), + ); + }, [displayEdges, hiddenPlaceholderIds]); + const treeQuery = useQuery({ queryKey: queryKeys.topologyTree, queryFn: fetchTopologyTree, @@ -2670,8 +2717,12 @@ export function TopologyPage() { const canvasHits = useMemo(() => { const q = canvasQuery.trim(); if (!q) return []; - return nodes.filter((n) => nodeMatchesQuery(n, q)); - }, [canvasQuery, nodes]); + return nodes.filter( + (n) => + nodeMatchesQuery(n, q) && + (showPlaceholders || !isPlaceholderSource(n.data.managed_source, n.data.ne_ip)), + ); + }, [canvasQuery, nodes, showPlaceholders]); useEffect(() => { const q = canvasQuery.trim(); @@ -3362,6 +3413,18 @@ export function TopologyPage() { /> {t("topology.hideVendor")} +