mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 06:40:45 +08:00
Industrialize topology editor and add customizable edge styles.
Add tool modes, layouts, and per-edge/default style overrides with a cleaner toolbar and context menus. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
e4a365fa16
commit
99fce837f0
17 changed files with 1902 additions and 269 deletions
|
|
@ -1052,6 +1052,15 @@ def on_startup() -> None:
|
|||
conn.exec_driver_sql("COMMENT ON COLUMN ume_inventory_ne.loopback IS '业务环回IP(IPv4)'")
|
||||
conn.exec_driver_sql("COMMENT ON COLUMN ume_inventory_ne.device_level IS '网元层次'")
|
||||
conn.exec_driver_sql("COMMENT ON COLUMN ume_inventory_ne.host_name IS '主机名称'")
|
||||
conn.exec_driver_sql(
|
||||
"ALTER TABLE topology_edge ADD COLUMN IF NOT EXISTS stroke_color VARCHAR(32) DEFAULT ''"
|
||||
)
|
||||
conn.exec_driver_sql(
|
||||
"ALTER TABLE topology_edge ADD COLUMN IF NOT EXISTS stroke_width INTEGER DEFAULT 0"
|
||||
)
|
||||
conn.exec_driver_sql(
|
||||
"ALTER TABLE topology_edge ADD COLUMN IF NOT EXISTS line_style VARCHAR(16) DEFAULT ''"
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -432,6 +432,11 @@ class TopologyEdge(Base):
|
|||
target_port: Mapped[str] = mapped_column(String(128), default="")
|
||||
# manual | lldp | cdp | stale
|
||||
source: Mapped[str] = mapped_column(String(32), default="manual", index=True)
|
||||
# Optional visual overrides; empty / 0 = use provenance defaults.
|
||||
stroke_color: Mapped[str] = mapped_column(String(32), default="")
|
||||
stroke_width: Mapped[int] = mapped_column(Integer, default=0)
|
||||
# "" | solid | dashed | dotted
|
||||
line_style: Mapped[str] = mapped_column(String(16), default="")
|
||||
discovered_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||
|
|
|
|||
|
|
@ -43,6 +43,9 @@ class TopologyEdgeIn(BaseModel):
|
|||
source_port: str = ""
|
||||
target_port: str = ""
|
||||
source: str = "manual"
|
||||
stroke_color: str = Field(default="", max_length=32)
|
||||
stroke_width: int = Field(default=0, ge=0, le=12)
|
||||
line_style: str = Field(default="", max_length=16)
|
||||
|
||||
|
||||
class TopologyNodeOut(BaseModel):
|
||||
|
|
@ -68,6 +71,9 @@ class TopologyEdgeOut(BaseModel):
|
|||
source_port: str = ""
|
||||
target_port: str = ""
|
||||
source: str = "manual"
|
||||
stroke_color: str = ""
|
||||
stroke_width: int = 0
|
||||
line_style: str = ""
|
||||
discovered_at: datetime | None = None
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ from __future__ import annotations
|
|||
from datetime import datetime
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
import re
|
||||
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
|
@ -165,6 +166,31 @@ def _node_out(
|
|||
)
|
||||
|
||||
|
||||
_HEX_COLOR_RE = re.compile(r"^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$")
|
||||
_LINE_STYLES = {"", "solid", "dashed", "dotted"}
|
||||
|
||||
|
||||
def _normalize_edge_style(
|
||||
*,
|
||||
stroke_color: str = "",
|
||||
stroke_width: int = 0,
|
||||
line_style: str = "",
|
||||
) -> tuple[str, int, str]:
|
||||
color = str(stroke_color or "").strip()
|
||||
if color and not _HEX_COLOR_RE.match(color):
|
||||
raise HTTPException(status_code=400, detail="invalid_stroke_color")
|
||||
try:
|
||||
width = int(stroke_width or 0)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise HTTPException(status_code=400, detail="invalid_stroke_width") from exc
|
||||
if width < 0 or width > 12:
|
||||
raise HTTPException(status_code=400, detail="invalid_stroke_width")
|
||||
style = str(line_style or "").strip().lower()
|
||||
if style not in _LINE_STYLES:
|
||||
raise HTTPException(status_code=400, detail="invalid_line_style")
|
||||
return color, width, style
|
||||
|
||||
|
||||
def _edge_out(e: TopologyEdge) -> TopologyEdgeOut:
|
||||
return TopologyEdgeOut(
|
||||
id=e.id,
|
||||
|
|
@ -174,6 +200,9 @@ def _edge_out(e: TopologyEdge) -> TopologyEdgeOut:
|
|||
source_port=e.source_port or "",
|
||||
target_port=e.target_port or "",
|
||||
source=e.source or "manual",
|
||||
stroke_color=getattr(e, "stroke_color", None) or "",
|
||||
stroke_width=int(getattr(e, "stroke_width", 0) or 0),
|
||||
line_style=getattr(e, "line_style", None) or "",
|
||||
discovered_at=e.discovered_at,
|
||||
)
|
||||
|
||||
|
|
@ -216,6 +245,7 @@ def put_graph(db: Session, map_id: str, body: TopologyGraphPut) -> TopologyGraph
|
|||
raise HTTPException(status_code=400, detail=f"duplicate_node_id:{nid}")
|
||||
node_ids.add(nid)
|
||||
|
||||
normalized_edges: list[tuple[TopologyEdgeIn, str, str, int, str]] = []
|
||||
for e in edges_in:
|
||||
sid = str(e.source_node_id or "").strip()
|
||||
tid = str(e.target_node_id or "").strip()
|
||||
|
|
@ -224,6 +254,12 @@ def put_graph(db: Session, map_id: str, body: TopologyGraphPut) -> TopologyGraph
|
|||
src = str(e.source or "manual").strip().lower() or "manual"
|
||||
if src not in {"manual", "lldp", "cdp", "stale"}:
|
||||
raise HTTPException(status_code=400, detail="invalid_edge_source")
|
||||
color, width, line = _normalize_edge_style(
|
||||
stroke_color=getattr(e, "stroke_color", "") or "",
|
||||
stroke_width=int(getattr(e, "stroke_width", 0) or 0),
|
||||
line_style=getattr(e, "line_style", "") or "",
|
||||
)
|
||||
normalized_edges.append((e, src, color, width, line))
|
||||
|
||||
now = _utcnow()
|
||||
db.query(TopologyEdge).filter(TopologyEdge.map_id == row.id).delete(synchronize_session=False)
|
||||
|
|
@ -243,8 +279,7 @@ def put_graph(db: Session, map_id: str, body: TopologyGraphPut) -> TopologyGraph
|
|||
updated_at=now,
|
||||
)
|
||||
)
|
||||
for e in edges_in:
|
||||
src = str(e.source or "manual").strip().lower() or "manual"
|
||||
for e, src, color, width, line in normalized_edges:
|
||||
db.add(
|
||||
TopologyEdge(
|
||||
id=str(e.id).strip() or uuid4().hex,
|
||||
|
|
@ -254,6 +289,9 @@ def put_graph(db: Session, map_id: str, body: TopologyGraphPut) -> TopologyGraph
|
|||
source_port=str(e.source_port or "").strip()[:128],
|
||||
target_port=str(e.target_port or "").strip()[:128],
|
||||
source=src,
|
||||
stroke_color=color,
|
||||
stroke_width=width,
|
||||
line_style=line,
|
||||
discovered_at=now if src in {"lldp", "cdp", "stale"} else None,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue