From 3f15ac38f6e1d5df2fc40d826981a28893330839 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Sun, 12 Jul 2026 21:47:46 +0800 Subject: [PATCH] feat(marker): add optional color field to marker data model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a user-chosen hex color (e.g. '#ff5722') to each marker, stored in Link._markers alongside bpf/tag/enabled. The color: - Is serialized with the link (asdict) and persisted in the topology, so it survives project reload and is consistent across devices. - Never reaches uBridge (compute ignores it) — it is purely a Web UI concern. - Can be updated independently of the BPF (color-only changes skip the ubridge round-trip). MarkerCreate schema gains color: Optional[str]; start_marker and update_marker carry it through; REST create/update endpoints pass it. The marker.match WebSocket event stays lean (no color) — the Web UI maps filter -> color from the link state it already holds. --- gns3server/api/routes/controller/links.py | 7 +++++-- gns3server/controller/udp_link.py | 23 ++++++++++++++++++----- gns3server/schemas/controller/links.py | 4 ++++ 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/gns3server/api/routes/controller/links.py b/gns3server/api/routes/controller/links.py index bb723927a..ec5e4f58a 100644 --- a/gns3server/api/routes/controller/links.py +++ b/gns3server/api/routes/controller/links.py @@ -454,12 +454,14 @@ async def create_marker( Required privilege: Link.Modify """ + name = marker_data.name or f"marker-{link.id[:8]}" await link.start_marker( - name=marker_data.name or f"marker-{link.id[:8]}", + name=name, bpf=marker_data.bpf, tag=marker_data.tag, + color=marker_data.color, ) - return link.markers.get(marker_data.name, {}) + return link.markers.get(name, {}) @router.delete( @@ -499,6 +501,7 @@ async def update_marker( name=marker_name, bpf=marker_data.bpf if marker_data.bpf else None, tag=marker_data.tag, + color=marker_data.color, ) return link.markers.get(marker_name, {}) diff --git a/gns3server/controller/udp_link.py b/gns3server/controller/udp_link.py index 73f006cea..0ef0ea9ac 100644 --- a/gns3server/controller/udp_link.py +++ b/gns3server/controller/udp_link.py @@ -335,13 +335,15 @@ class UDPLink(Link): } self._marker_capture_nodes[name] = capture_side - async def start_marker(self, name, bpf, tag=None): + async def start_marker(self, name, bpf, tag=None, color=None): """ Attach a traffic-insight marker to this link. :param name: stable filter name — echoed in MARK signals + pcap identity :param bpf: libpcap BPF expression :param tag: optional correlation id + :param color: optional hex color for the Web UI (e.g. '#ff5722'); stored + with the link and persisted in the topology, never sent to uBridge """ if name in self._markers: @@ -361,7 +363,7 @@ class UDPLink(Link): data=data, ) self._store_capture_node_for_marker(name, marker_side) - self._markers[name].update({"bpf": bpf, "tag": tag, "enabled": True}) + self._markers[name].update({"bpf": bpf, "tag": tag, "enabled": True, "color": color}) self._project.emit_notification("link.updated", self.asdict()) self._project.dump() @@ -389,7 +391,7 @@ class UDPLink(Link): self._project.emit_notification("link.updated", self.asdict()) self._project.dump() - async def update_marker(self, name, bpf=None, tag=None, enabled=None): + async def update_marker(self, name, bpf=None, tag=None, enabled=None, color=None): """ Update an existing marker. A BPF change requires delete+re-add so the ubridge side flushes the pcap and the new filter takes effect. @@ -398,6 +400,7 @@ class UDPLink(Link): :param bpf: new BPF expression (None = keep existing) :param tag: new tag id (None = keep existing) :param enabled: toggle (None = keep existing) + :param color: new hex color (None = keep existing) """ marker_info = self._markers.get(name) @@ -407,11 +410,15 @@ class UDPLink(Link): new_bpf = bpf if bpf is not None else marker_info["bpf"] new_tag = tag if tag is not None else marker_info.get("tag") new_enabled = enabled if enabled is not None else marker_info.get("enabled", True) + new_color = color if color is not None else marker_info.get("color") if not new_enabled and marker_info.get("enabled", True): # Toggle off: remove from ubridge but keep state. await self.stop_marker(name) - self._markers[name] = {**marker_info, "bpf": new_bpf, "tag": new_tag, "enabled": False} + self._markers[name] = { + **marker_info, "bpf": new_bpf, "tag": new_tag, + "enabled": False, "color": new_color, + } self._project.emit_notification("link.updated", self.asdict()) self._project.dump() return @@ -441,7 +448,13 @@ class UDPLink(Link): ), data=data, ) - self._markers[name] = {**marker_info, "bpf": new_bpf, "tag": new_tag, "enabled": True} + self._markers[name] = { + **marker_info, "bpf": new_bpf, "tag": new_tag, + "enabled": True, "color": new_color, + } + elif new_color != marker_info.get("color"): + # Color-only change: no ubridge round-trip, just update state. + self._markers[name] = {**marker_info, "color": new_color} self._project.emit_notification("link.updated", self.asdict()) self._project.dump() diff --git a/gns3server/schemas/controller/links.py b/gns3server/schemas/controller/links.py index 7a0676e04..b21afae04 100644 --- a/gns3server/schemas/controller/links.py +++ b/gns3server/schemas/controller/links.py @@ -153,6 +153,10 @@ class MarkerCreate(BaseModel): bpf: str tag: Optional[int] = None link_id: Optional[str] = None + color: Optional[str] = Field( + None, + description="User-chosen hex color for this marker in the Web UI, e.g. '#ff5722'" + ) class MarkerDelete(BaseModel):