mirror of
https://github.com/hansjone/netx.git
synced 2026-10-13 02:03:23 +08:00
Fix placeholder toggle to hide only the TOPO/LLDP badge.
Keep placeholder NEs on the canvas; Display menu controls the corner badge only (off by default). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
73cd2704eb
commit
a746a84d01
3 changed files with 34 additions and 66 deletions
|
|
@ -1566,7 +1566,7 @@ const en = {
|
||||||
},
|
},
|
||||||
hideIp: "Hide IP",
|
hideIp: "Hide IP",
|
||||||
hideVendor: "Hide vendor",
|
hideVendor: "Hide vendor",
|
||||||
showPlaceholders: "Show placeholders (TOPO/LLDP)",
|
showPlaceholderBadge: "Show placeholder badge (TOPO/LLDP)",
|
||||||
hidePorts: "Hide ports",
|
hidePorts: "Hide ports",
|
||||||
edgeFlow: "Edge flow",
|
edgeFlow: "Edge flow",
|
||||||
collapseSidebar: "Collapse sidebar",
|
collapseSidebar: "Collapse sidebar",
|
||||||
|
|
|
||||||
|
|
@ -1555,7 +1555,7 @@ const zh = {
|
||||||
},
|
},
|
||||||
hideIp: "隐藏 IP",
|
hideIp: "隐藏 IP",
|
||||||
hideVendor: "隐藏厂商",
|
hideVendor: "隐藏厂商",
|
||||||
showPlaceholders: "显示占位网元(TOPO/LLDP)",
|
showPlaceholderBadge: "显示占位角标(TOPO/LLDP)",
|
||||||
hidePorts: "隐藏端口",
|
hidePorts: "隐藏端口",
|
||||||
edgeFlow: "链路流动",
|
edgeFlow: "链路流动",
|
||||||
collapseSidebar: "折叠侧栏",
|
collapseSidebar: "折叠侧栏",
|
||||||
|
|
|
||||||
|
|
@ -304,6 +304,8 @@ type TopoDisplayOpts = {
|
||||||
hideVendor: boolean;
|
hideVendor: boolean;
|
||||||
hidePorts: boolean;
|
hidePorts: boolean;
|
||||||
connectMode: boolean;
|
connectMode: boolean;
|
||||||
|
/** Show TOPO/LLDP placeholder corner badge on nodes. */
|
||||||
|
showPlaceholderBadge: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type CtxMenu =
|
type CtxMenu =
|
||||||
|
|
@ -317,6 +319,7 @@ const TopoDisplayContext = createContext<TopoDisplayOpts>({
|
||||||
hideVendor: true,
|
hideVendor: true,
|
||||||
hidePorts: true,
|
hidePorts: true,
|
||||||
connectMode: false,
|
connectMode: false,
|
||||||
|
showPlaceholderBadge: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
function newId(): string {
|
function newId(): string {
|
||||||
|
|
@ -418,9 +421,10 @@ function isPlaceholderSource(source: string | undefined, neIp: string): boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const NeNode = memo(function NeNode({ data, selected }: NodeProps<Node<NeNodeData>>) {
|
const NeNode = memo(function NeNode({ data, selected }: NodeProps<Node<NeNodeData>>) {
|
||||||
const { hideIp, hideVendor, connectMode } = useContext(TopoDisplayContext);
|
const { hideIp, hideVendor, connectMode, showPlaceholderBadge } = useContext(TopoDisplayContext);
|
||||||
const tone = nodeIconTone(data.vendor, data.managed_ne_id, data.ume_ne_id);
|
const tone = nodeIconTone(data.vendor, data.managed_ne_id, data.ume_ne_id);
|
||||||
const placeholder = isPlaceholderSource(data.managed_source, data.ne_ip);
|
const placeholder = isPlaceholderSource(data.managed_source, data.ne_ip);
|
||||||
|
const showBadge = placeholder && showPlaceholderBadge;
|
||||||
const name = data.label || (!hideIp ? data.ne_ip : "") || "NE";
|
const name = data.label || (!hideIp ? data.ne_ip : "") || "NE";
|
||||||
const secondary = [
|
const secondary = [
|
||||||
hideIp || !data.ne_ip || data.ne_ip === name ? "" : data.ne_ip,
|
hideIp || !data.ne_ip || data.ne_ip === name ? "" : data.ne_ip,
|
||||||
|
|
@ -430,8 +434,8 @@ const NeNode = memo(function NeNode({ data, selected }: NodeProps<Node<NeNodeDat
|
||||||
<div
|
<div
|
||||||
className={`topo-node topo-node--${tone}${selected ? " is-selected" : ""}${
|
className={`topo-node topo-node--${tone}${selected ? " is-selected" : ""}${
|
||||||
connectMode ? " is-connect-mode" : ""
|
connectMode ? " is-connect-mode" : ""
|
||||||
}${placeholder ? " is-placeholder" : ""}`}
|
}${showBadge ? " is-placeholder" : ""}`}
|
||||||
title={placeholder ? data.managed_source || "placeholder" : undefined}
|
title={showBadge ? data.managed_source || "placeholder" : undefined}
|
||||||
>
|
>
|
||||||
<div className="topo-node__glyph">
|
<div className="topo-node__glyph">
|
||||||
<Handle
|
<Handle
|
||||||
|
|
@ -447,7 +451,7 @@ const NeNode = memo(function NeNode({ data, selected }: NodeProps<Node<NeNodeDat
|
||||||
isConnectable={connectMode}
|
isConnectable={connectMode}
|
||||||
/>
|
/>
|
||||||
<RouterIcon />
|
<RouterIcon />
|
||||||
{placeholder ? (
|
{showBadge ? (
|
||||||
<span className="topo-node__badge" aria-hidden>
|
<span className="topo-node__badge" aria-hidden>
|
||||||
{String(data.managed_source || "ph").slice(0, 4)}
|
{String(data.managed_source || "ph").slice(0, 4)}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -484,7 +488,7 @@ const AUTO_LAYOUT_DISCOVER_KEY = "netx.topology.autoLayoutAfterDiscover";
|
||||||
const DISCOVER_AUTO_ADD_KEY = "netx.topology.discoverAutoAddUnmatched.v2";
|
const DISCOVER_AUTO_ADD_KEY = "netx.topology.discoverAutoAddUnmatched.v2";
|
||||||
const DISCOVER_PROJECT_NEIGHBORS_KEY = "netx.topology.discoverProjectNeighbors.v2";
|
const DISCOVER_PROJECT_NEIGHBORS_KEY = "netx.topology.discoverProjectNeighbors.v2";
|
||||||
const SCALE_BUNDLE_WIDTH_KEY = "netx.topology.scaleBundleWidth";
|
const SCALE_BUNDLE_WIDTH_KEY = "netx.topology.scaleBundleWidth";
|
||||||
const SHOW_PLACEHOLDERS_KEY = "netx.topology.showPlaceholders";
|
const SHOW_PLACEHOLDER_BADGE_KEY = "netx.topology.showPlaceholderBadge";
|
||||||
const CANVAS_BG_KEY = "netx.topology.canvasBg";
|
const CANVAS_BG_KEY = "netx.topology.canvasBg";
|
||||||
const DEFAULT_CANVAS_BG = "#0f172a";
|
const DEFAULT_CANVAS_BG = "#0f172a";
|
||||||
/** Previous light default — migrate so existing sessions pick up dark canvas. */
|
/** Previous light default — migrate so existing sessions pick up dark canvas. */
|
||||||
|
|
@ -861,9 +865,9 @@ export function TopologyPage() {
|
||||||
const [scaleBundleWidth, setScaleBundleWidth] = useState(() =>
|
const [scaleBundleWidth, setScaleBundleWidth] = useState(() =>
|
||||||
loadBoolFlag(SCALE_BUNDLE_WIDTH_KEY, false),
|
loadBoolFlag(SCALE_BUNDLE_WIDTH_KEY, false),
|
||||||
);
|
);
|
||||||
/** Topology/LLDP placeholders hidden by default; toggle in Display menu. */
|
/** TOPO/LLDP corner badge hidden by default; toggle in Display menu. */
|
||||||
const [showPlaceholders, setShowPlaceholders] = useState(() =>
|
const [showPlaceholderBadge, setShowPlaceholderBadge] = useState(() =>
|
||||||
loadBoolFlag(SHOW_PLACEHOLDERS_KEY, false),
|
loadBoolFlag(SHOW_PLACEHOLDER_BADGE_KEY, false),
|
||||||
);
|
);
|
||||||
const [edgeFlow, setEdgeFlow] = useState(false);
|
const [edgeFlow, setEdgeFlow] = useState(false);
|
||||||
const [edgeDefaults, setEdgeDefaults] = useState<EdgeDefaults>(() => loadEdgeDefaults());
|
const [edgeDefaults, setEdgeDefaults] = useState<EdgeDefaults>(() => loadEdgeDefaults());
|
||||||
|
|
@ -919,21 +923,6 @@ export function TopologyPage() {
|
||||||
const findJustLocatedRef = useRef(false);
|
const findJustLocatedRef = useRef(false);
|
||||||
const [nodes, setNodes, onNodesChange] = useNodesState<Node<NeNodeData>>([]);
|
const [nodes, setNodes, onNodesChange] = useNodesState<Node<NeNodeData>>([]);
|
||||||
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);
|
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);
|
||||||
|
|
||||||
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<ReactFlowInstance<Node<NeNodeData>, Edge> | null>(null);
|
const rfRef = useRef<ReactFlowInstance<Node<NeNodeData>, Edge> | null>(null);
|
||||||
const canvasRef = useRef<HTMLDivElement | null>(null);
|
const canvasRef = useRef<HTMLDivElement | null>(null);
|
||||||
const dirtyRef = useRef(false);
|
const dirtyRef = useRef(false);
|
||||||
|
|
@ -1032,8 +1021,14 @@ export function TopologyPage() {
|
||||||
|
|
||||||
const toolBehavior = useMemo(() => behaviorForMode(toolMode), [toolMode]);
|
const toolBehavior = useMemo(() => behaviorForMode(toolMode), [toolMode]);
|
||||||
const displayOpts = useMemo(
|
const displayOpts = useMemo(
|
||||||
() => ({ hideIp, hideVendor, hidePorts, connectMode: toolMode === "connect" }),
|
() => ({
|
||||||
[hideIp, hideVendor, hidePorts, toolMode],
|
hideIp,
|
||||||
|
hideVendor,
|
||||||
|
hidePorts,
|
||||||
|
connectMode: toolMode === "connect",
|
||||||
|
showPlaceholderBadge,
|
||||||
|
}),
|
||||||
|
[hideIp, hideVendor, hidePorts, toolMode, showPlaceholderBadge],
|
||||||
);
|
);
|
||||||
const displayEdges = useMemo(() => {
|
const displayEdges = useMemo(() => {
|
||||||
const built = buildLinkDisplayEdges(edges, expandPhysicalLinks, hidePorts, scaleBundleWidth).map((e) =>
|
const built = buildLinkDisplayEdges(edges, expandPhysicalLinks, hidePorts, scaleBundleWidth).map((e) =>
|
||||||
|
|
@ -1054,33 +1049,6 @@ export function TopologyPage() {
|
||||||
});
|
});
|
||||||
}, [edges, expandPhysicalLinks, hidePorts, scaleBundleWidth, edgeFlow, edgeDefaults, selectedEdgeId]);
|
}, [edges, expandPhysicalLinks, hidePorts, scaleBundleWidth, edgeFlow, edgeDefaults, selectedEdgeId]);
|
||||||
|
|
||||||
const hiddenPlaceholderIds = useMemo(() => {
|
|
||||||
if (showPlaceholders) return new Set<string>();
|
|
||||||
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({
|
const treeQuery = useQuery({
|
||||||
queryKey: queryKeys.topologyTree,
|
queryKey: queryKeys.topologyTree,
|
||||||
queryFn: fetchTopologyTree,
|
queryFn: fetchTopologyTree,
|
||||||
|
|
@ -2717,12 +2685,8 @@ export function TopologyPage() {
|
||||||
const canvasHits = useMemo(() => {
|
const canvasHits = useMemo(() => {
|
||||||
const q = canvasQuery.trim();
|
const q = canvasQuery.trim();
|
||||||
if (!q) return [];
|
if (!q) return [];
|
||||||
return nodes.filter(
|
return nodes.filter((n) => nodeMatchesQuery(n, q));
|
||||||
(n) =>
|
}, [canvasQuery, nodes]);
|
||||||
nodeMatchesQuery(n, q) &&
|
|
||||||
(showPlaceholders || !isPlaceholderSource(n.data.managed_source, n.data.ne_ip)),
|
|
||||||
);
|
|
||||||
}, [canvasQuery, nodes, showPlaceholders]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const q = canvasQuery.trim();
|
const q = canvasQuery.trim();
|
||||||
|
|
@ -3416,14 +3380,14 @@ export function TopologyPage() {
|
||||||
<label className="topo-display-toggles__item">
|
<label className="topo-display-toggles__item">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={showPlaceholders}
|
checked={showPlaceholderBadge}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const next = e.target.checked;
|
const next = e.target.checked;
|
||||||
setShowPlaceholders(next);
|
setShowPlaceholderBadge(next);
|
||||||
persistBoolFlag(SHOW_PLACEHOLDERS_KEY, next);
|
persistBoolFlag(SHOW_PLACEHOLDER_BADGE_KEY, next);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{t("topology.showPlaceholders")}
|
{t("topology.showPlaceholderBadge")}
|
||||||
</label>
|
</label>
|
||||||
<label className="topo-display-toggles__item">
|
<label className="topo-display-toggles__item">
|
||||||
<input
|
<input
|
||||||
|
|
@ -4157,8 +4121,12 @@ export function TopologyPage() {
|
||||||
{treeRoot ? (
|
{treeRoot ? (
|
||||||
<TopoDisplayContext.Provider value={displayOpts}>
|
<TopoDisplayContext.Provider value={displayOpts}>
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
nodes={flowNodes}
|
nodes={nodes.map((n) =>
|
||||||
edges={flowEdges}
|
searchHitIds.includes(n.id)
|
||||||
|
? { ...n, className: "is-search-hit" }
|
||||||
|
: { ...n, className: undefined },
|
||||||
|
)}
|
||||||
|
edges={displayEdges}
|
||||||
nodeTypes={nodeTypes}
|
nodeTypes={nodeTypes}
|
||||||
edgeTypes={edgeTypes}
|
edgeTypes={edgeTypes}
|
||||||
onlyRenderVisibleElements
|
onlyRenderVisibleElements
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue