mirror of
https://github.com/hansjone/netx.git
synced 2026-10-18 11:30:44 +08:00
Polish topology toolbar search and restore sidebar icons.
Group tools with display, drop the redundant locate button, clear find query after Enter, and restore corrupted edit/delete/collapse glyphs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
8cf1696d8c
commit
728ec815a3
2 changed files with 233 additions and 210 deletions
|
|
@ -2257,26 +2257,47 @@ pre {
|
||||||
}
|
}
|
||||||
|
|
||||||
.topo-toolbar__group + .topo-toolbar__group,
|
.topo-toolbar__group + .topo-toolbar__group,
|
||||||
.topo-tools + .topo-toolbar__group,
|
.topo-toolbar__panel + .topo-toolbar__group,
|
||||||
.topo-toolbar__group + .topo-toolbar__display,
|
.topo-toolbar__group + .topo-toolbar__group--find,
|
||||||
.topo-tools + .topo-toolbar__display {
|
.topo-toolbar__panel + .topo-toolbar__group--find {
|
||||||
margin-left: 2px;
|
margin-left: 2px;
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
border-left: 1px solid #e2e8f0;
|
border-left: 1px solid #e2e8f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.topo-toolbar__panel {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: #f1f5f9;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topo-toolbar__panel .topo-tools {
|
||||||
|
background: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topo-toolbar__panel .topo-toolbar__display > summary {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
.topo-toolbar__group--find {
|
.topo-toolbar__group--find {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.topo-find-suggest {
|
.topo-find-suggest {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: calc(100% + 4px);
|
top: calc(100% + 4px);
|
||||||
left: 0;
|
right: 0;
|
||||||
|
left: auto;
|
||||||
z-index: 30;
|
z-index: 30;
|
||||||
width: min(320px, 70vw);
|
width: min(320px, 70vw);
|
||||||
max-height: 280px;
|
max-height: 280px;
|
||||||
|
|
|
||||||
|
|
@ -461,6 +461,7 @@ export function TopologyPage() {
|
||||||
const discoverAbortRef = useRef<AbortController | null>(null);
|
const discoverAbortRef = useRef<AbortController | null>(null);
|
||||||
const searchHitTimerRef = useRef<number | null>(null);
|
const searchHitTimerRef = useRef<number | null>(null);
|
||||||
const findBoxRef = useRef<HTMLDivElement | null>(null);
|
const findBoxRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
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>([]);
|
||||||
const rfRef = useRef<ReactFlowInstance<Node<NeNodeData>, Edge> | null>(null);
|
const rfRef = useRef<ReactFlowInstance<Node<NeNodeData>, Edge> | null>(null);
|
||||||
|
|
@ -1430,9 +1431,13 @@ export function TopologyPage() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const q = canvasQuery.trim();
|
const q = canvasQuery.trim();
|
||||||
if (!q) {
|
if (!q) {
|
||||||
setSearchHitIds([]);
|
|
||||||
setFindActiveIdx(0);
|
setFindActiveIdx(0);
|
||||||
setFindOpen(false);
|
setFindOpen(false);
|
||||||
|
if (findJustLocatedRef.current) {
|
||||||
|
findJustLocatedRef.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSearchHitIds([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setFindOpen(true);
|
setFindOpen(true);
|
||||||
|
|
@ -1452,7 +1457,10 @@ export function TopologyPage() {
|
||||||
nodeId ||
|
nodeId ||
|
||||||
canvasHits[Math.max(0, Math.min(findActiveIdx, canvasHits.length - 1))]?.id ||
|
canvasHits[Math.max(0, Math.min(findActiveIdx, canvasHits.length - 1))]?.id ||
|
||||||
canvasHits[0].id;
|
canvasHits[0].id;
|
||||||
|
findJustLocatedRef.current = true;
|
||||||
setFindOpen(false);
|
setFindOpen(false);
|
||||||
|
setCanvasQuery("");
|
||||||
|
setFindActiveIdx(0);
|
||||||
locateNode(id);
|
locateNode(id);
|
||||||
},
|
},
|
||||||
[canvasQuery, canvasHits, findActiveIdx, locateNode, showError, t],
|
[canvasQuery, canvasHits, findActiveIdx, locateNode, showError, t],
|
||||||
|
|
@ -1533,7 +1541,7 @@ export function TopologyPage() {
|
||||||
name,
|
name,
|
||||||
ip: ne.ip_address || "",
|
ip: ne.ip_address || "",
|
||||||
vendor: "ZTE",
|
vendor: "ZTE",
|
||||||
meta: `${ne.ip_address || "-"} · ${ne.ne_type || "UME"}`,
|
meta: `${ne.ip_address || "-"} ? ${ne.ne_type || "UME"}`,
|
||||||
connect_status: ne.connection_status || "",
|
connect_status: ne.connection_status || "",
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
@ -1839,6 +1847,7 @@ export function TopologyPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="topo-toolbar__row topo-toolbar__row--tools">
|
<div className="topo-toolbar__row topo-toolbar__row--tools">
|
||||||
|
<div className="topo-toolbar__panel" aria-label={t("topology.toolModes")}>
|
||||||
<div className="topo-tools" role="toolbar" aria-label={t("topology.toolModes")}>
|
<div className="topo-tools" role="toolbar" aria-label={t("topology.toolModes")}>
|
||||||
{(
|
{(
|
||||||
[
|
[
|
||||||
|
|
@ -1863,134 +1872,6 @@ export function TopologyPage() {
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
className="topo-toolbar__group topo-toolbar__group--find"
|
|
||||||
aria-label={t("topology.findNode")}
|
|
||||||
ref={findBoxRef}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
className="topo-toolbar__find"
|
|
||||||
value={canvasQuery}
|
|
||||||
onChange={(e) => setCanvasQuery(e.target.value)}
|
|
||||||
onFocus={() => {
|
|
||||||
if (canvasQuery.trim()) setFindOpen(true);
|
|
||||||
}}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === "ArrowDown") {
|
|
||||||
e.preventDefault();
|
|
||||||
if (!canvasHits.length) return;
|
|
||||||
setFindOpen(true);
|
|
||||||
setFindActiveIdx((i) => (i + 1) % canvasHits.length);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (e.key === "ArrowUp") {
|
|
||||||
e.preventDefault();
|
|
||||||
if (!canvasHits.length) return;
|
|
||||||
setFindOpen(true);
|
|
||||||
setFindActiveIdx((i) => (i - 1 + canvasHits.length) % canvasHits.length);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (e.key === "Escape") {
|
|
||||||
setFindOpen(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
e.preventDefault();
|
|
||||||
findOnCanvas();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
placeholder={t("topology.findNodePh")}
|
|
||||||
disabled={!mapId || nodes.length === 0}
|
|
||||||
aria-label={t("topology.findNode")}
|
|
||||||
aria-autocomplete="list"
|
|
||||||
aria-expanded={findOpen}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn--sm btn--ghost"
|
|
||||||
disabled={!mapId || nodes.length === 0 || !canvasQuery.trim()}
|
|
||||||
onClick={() => findOnCanvas()}
|
|
||||||
>
|
|
||||||
{t("topology.findNode")}
|
|
||||||
</button>
|
|
||||||
{findOpen && canvasQuery.trim() ? (
|
|
||||||
<div className="topo-find-suggest" role="listbox">
|
|
||||||
{canvasHits.length === 0 ? (
|
|
||||||
<div className="topo-find-suggest__empty">{t("topology.findNoMatch")}</div>
|
|
||||||
) : (
|
|
||||||
canvasHits.slice(0, 12).map((n, idx) => (
|
|
||||||
<button
|
|
||||||
key={n.id}
|
|
||||||
type="button"
|
|
||||||
role="option"
|
|
||||||
aria-selected={idx === findActiveIdx}
|
|
||||||
className={`topo-find-suggest__item${idx === findActiveIdx ? " is-active" : ""}`}
|
|
||||||
onMouseEnter={() => setFindActiveIdx(idx)}
|
|
||||||
onClick={() => findOnCanvas(n.id)}
|
|
||||||
>
|
|
||||||
<span className="topo-find-suggest__name">{n.data.label || n.id}</span>
|
|
||||||
<span className="topo-find-suggest__meta">
|
|
||||||
{[n.data.ne_ip, n.data.vendor].filter(Boolean).join(" ? ")}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
{canvasHits.length > 12 ? (
|
|
||||||
<div className="topo-find-suggest__more">
|
|
||||||
{t("topology.findMore").replace("{{count}}", String(canvasHits.length - 12))}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
<div className="topo-toolbar__group" aria-label={t("topology.layout")}>
|
|
||||||
<select
|
|
||||||
className="topo-toolbar__select"
|
|
||||||
aria-label={t("topology.layout")}
|
|
||||||
disabled={!mapId || nodes.length === 0}
|
|
||||||
defaultValue=""
|
|
||||||
onChange={(e) => {
|
|
||||||
const v = e.target.value as LayoutKind | "selected-tb" | "";
|
|
||||||
e.target.value = "";
|
|
||||||
if (!v) return;
|
|
||||||
if (v === "selected-tb") void applyLayout("hierarchical-tb", { onlySelected: true });
|
|
||||||
else void applyLayout(v);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<option value="" disabled>
|
|
||||||
{t("topology.layout")}
|
|
||||||
</option>
|
|
||||||
<option value="hierarchical-tb">{t("topology.layoutHierarchicalTb")}</option>
|
|
||||||
<option value="hierarchical-lr">{t("topology.layoutHierarchicalLr")}</option>
|
|
||||||
<option value="force">{t("topology.layoutForce")}</option>
|
|
||||||
<option value="grid">{t("topology.layoutGrid")}</option>
|
|
||||||
<option value="radial">{t("topology.layoutRadial")}</option>
|
|
||||||
<option value="selected-tb">{t("topology.layoutSelected")}</option>
|
|
||||||
</select>
|
|
||||||
<select
|
|
||||||
className="topo-toolbar__select"
|
|
||||||
aria-label={t("topology.align")}
|
|
||||||
disabled={selectedNodes.length < 2}
|
|
||||||
defaultValue=""
|
|
||||||
onChange={(e) => {
|
|
||||||
const v = e.target.value as Parameters<typeof alignNodes>[2] | "";
|
|
||||||
e.target.value = "";
|
|
||||||
if (v) applyAlign(v);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<option value="" disabled>
|
|
||||||
{t("topology.align")}
|
|
||||||
</option>
|
|
||||||
<option value="left">{t("topology.alignLeft")}</option>
|
|
||||||
<option value="right">{t("topology.alignRight")}</option>
|
|
||||||
<option value="top">{t("topology.alignTop")}</option>
|
|
||||||
<option value="bottom">{t("topology.alignBottom")}</option>
|
|
||||||
<option value="h-center">{t("topology.alignHCenter")}</option>
|
|
||||||
<option value="v-center">{t("topology.alignVCenter")}</option>
|
|
||||||
<option value="h-distribute">{t("topology.alignHDistribute")}</option>
|
|
||||||
<option value="v-distribute">{t("topology.alignVDistribute")}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<details className="topo-toolbar__display">
|
<details className="topo-toolbar__display">
|
||||||
<summary>{t("topology.display")}</summary>
|
<summary>{t("topology.display")}</summary>
|
||||||
<div className="topo-display-toggles" role="group" aria-label={t("topology.display")}>
|
<div className="topo-display-toggles" role="group" aria-label={t("topology.display")}>
|
||||||
|
|
@ -2097,6 +1978,127 @@ export function TopologyPage() {
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="topo-toolbar__group" aria-label={t("topology.layout")}>
|
||||||
|
<select
|
||||||
|
className="topo-toolbar__select"
|
||||||
|
aria-label={t("topology.layout")}
|
||||||
|
disabled={!mapId || nodes.length === 0}
|
||||||
|
defaultValue=""
|
||||||
|
onChange={(e) => {
|
||||||
|
const v = e.target.value as LayoutKind | "selected-tb" | "";
|
||||||
|
e.target.value = "";
|
||||||
|
if (!v) return;
|
||||||
|
if (v === "selected-tb") void applyLayout("hierarchical-tb", { onlySelected: true });
|
||||||
|
else void applyLayout(v);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="" disabled>
|
||||||
|
{t("topology.layout")}
|
||||||
|
</option>
|
||||||
|
<option value="hierarchical-tb">{t("topology.layoutHierarchicalTb")}</option>
|
||||||
|
<option value="hierarchical-lr">{t("topology.layoutHierarchicalLr")}</option>
|
||||||
|
<option value="force">{t("topology.layoutForce")}</option>
|
||||||
|
<option value="grid">{t("topology.layoutGrid")}</option>
|
||||||
|
<option value="radial">{t("topology.layoutRadial")}</option>
|
||||||
|
<option value="selected-tb">{t("topology.layoutSelected")}</option>
|
||||||
|
</select>
|
||||||
|
<select
|
||||||
|
className="topo-toolbar__select"
|
||||||
|
aria-label={t("topology.align")}
|
||||||
|
disabled={selectedNodes.length < 2}
|
||||||
|
defaultValue=""
|
||||||
|
onChange={(e) => {
|
||||||
|
const v = e.target.value as Parameters<typeof alignNodes>[2] | "";
|
||||||
|
e.target.value = "";
|
||||||
|
if (v) applyAlign(v);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="" disabled>
|
||||||
|
{t("topology.align")}
|
||||||
|
</option>
|
||||||
|
<option value="left">{t("topology.alignLeft")}</option>
|
||||||
|
<option value="right">{t("topology.alignRight")}</option>
|
||||||
|
<option value="top">{t("topology.alignTop")}</option>
|
||||||
|
<option value="bottom">{t("topology.alignBottom")}</option>
|
||||||
|
<option value="h-center">{t("topology.alignHCenter")}</option>
|
||||||
|
<option value="v-center">{t("topology.alignVCenter")}</option>
|
||||||
|
<option value="h-distribute">{t("topology.alignHDistribute")}</option>
|
||||||
|
<option value="v-distribute">{t("topology.alignVDistribute")}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="topo-toolbar__group topo-toolbar__group--find"
|
||||||
|
aria-label={t("topology.findNode")}
|
||||||
|
ref={findBoxRef}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
className="topo-toolbar__find"
|
||||||
|
value={canvasQuery}
|
||||||
|
onChange={(e) => setCanvasQuery(e.target.value)}
|
||||||
|
onFocus={() => {
|
||||||
|
if (canvasQuery.trim()) setFindOpen(true);
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "ArrowDown") {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!canvasHits.length) return;
|
||||||
|
setFindOpen(true);
|
||||||
|
setFindActiveIdx((i) => (i + 1) % canvasHits.length);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.key === "ArrowUp") {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!canvasHits.length) return;
|
||||||
|
setFindOpen(true);
|
||||||
|
setFindActiveIdx((i) => (i - 1 + canvasHits.length) % canvasHits.length);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
setFindOpen(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
findOnCanvas();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder={t("topology.findNodePh")}
|
||||||
|
disabled={!mapId || nodes.length === 0}
|
||||||
|
aria-label={t("topology.findNode")}
|
||||||
|
aria-autocomplete="list"
|
||||||
|
aria-expanded={findOpen}
|
||||||
|
/>
|
||||||
|
{findOpen && canvasQuery.trim() ? (
|
||||||
|
<div className="topo-find-suggest" role="listbox">
|
||||||
|
{canvasHits.length === 0 ? (
|
||||||
|
<div className="topo-find-suggest__empty">{t("topology.findNoMatch")}</div>
|
||||||
|
) : (
|
||||||
|
canvasHits.slice(0, 12).map((n, idx) => (
|
||||||
|
<button
|
||||||
|
key={n.id}
|
||||||
|
type="button"
|
||||||
|
role="option"
|
||||||
|
aria-selected={idx === findActiveIdx}
|
||||||
|
className={`topo-find-suggest__item${idx === findActiveIdx ? " is-active" : ""}`}
|
||||||
|
onMouseEnter={() => setFindActiveIdx(idx)}
|
||||||
|
onClick={() => findOnCanvas(n.id)}
|
||||||
|
>
|
||||||
|
<span className="topo-find-suggest__name">{n.data.label || n.id}</span>
|
||||||
|
<span className="topo-find-suggest__meta">
|
||||||
|
{[n.data.ne_ip, n.data.vendor].filter(Boolean).join(" ? ")}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
{canvasHits.length > 12 ? (
|
||||||
|
<div className="topo-find-suggest__more">
|
||||||
|
{t("topology.findMore").replace("{{count}}", String(canvasHits.length - 12))}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{discoverOpen ? (
|
{discoverOpen ? (
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue