Add a real VLAN legend, matching the reference diagram's style
Previously the only VLAN visual was a small 14px swatch inside the detail table -- easy to miss, not what the user meant by 'legend'. Added a dedicated colored block per VLAN (VLAN <id> on a color fill, name in a matching-outline box next to it), stacked left of the existing detail table, same layout idea as the reference diagram this was modeled on. Also fixed a real bug this surfaced: vlan_color() picked a palette color by hash(vid), which let two different VLANs collide on the same color (209 and 217 both landed on green). Colors are now assigned by position among the VLANs actually present, which can't collide as long as there are <= 8 of them (the palette size) -- true here with room to grow. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NM3nivbufxEWiKKmatEtEe
This commit is contained in:
@@ -29,13 +29,16 @@ switches/patch panels look the part: switches get draw.io's built-in network
|
|||||||
switch icon, patch panels get a small rack-unit look (header strip + port
|
switch icon, patch panels get a small rack-unit look (header strip + port
|
||||||
ticks) rather than a plain box.
|
ticks) rather than a plain box.
|
||||||
|
|
||||||
A **VLAN table** sits at the top of the diagram (VLAN / Name / Description /
|
A **VLAN legend + table** sits at the top of the diagram: a colored
|
||||||
Subnet / Site). As of this writing NetBox has no VLANs defined here, so it
|
"VLAN <id> / name" block per VLAN on the left (each color also used for
|
||||||
renders as an explicit "not configured yet" placeholder rather than being
|
that VLAN's connector tags and table swatch), and the detail table
|
||||||
silently omitted or -- worse -- filled in with invented VLAN numbers. Add
|
(VLAN / Name / Description / Subnet / Site) next to it. If NetBox has no
|
||||||
VLANs and interface tagged/untagged assignments in NetBox and regenerate
|
VLANs defined, both render as an explicit "not configured yet" placeholder
|
||||||
(no code changes needed): the table populates for real, and every connector
|
rather than being silently omitted or -- worse -- filled in with invented
|
||||||
whose port has VLAN config picks up a short suffix like `9·V10 ↔ 1·V10`.
|
VLAN numbers. Add VLANs and interface tagged/untagged assignments in
|
||||||
|
NetBox and regenerate (no code changes needed): both populate for real,
|
||||||
|
and every connector whose port has VLAN config picks up a short suffix
|
||||||
|
like `9·V214+215T ↔ 1`.
|
||||||
Deliberately left out: literal switch CLI config dumps -- those belong in
|
Deliberately left out: literal switch CLI config dumps -- those belong in
|
||||||
the switches' NetBox config-context/notes, not pasted into a topology
|
the switches' NetBox config-context/notes, not pasted into a topology
|
||||||
diagram where they'd immediately go stale.
|
diagram where they'd immediately go stale.
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 1.9 MiB |
@@ -23,9 +23,14 @@ devs = {d["id"]: d for d in data["devices"]}
|
|||||||
edges = data["edges"] # [{a, a_port, a_vlan, b, b_port, b_vlan}, ...]
|
edges = data["edges"] # [{a, a_port, a_vlan, b, b_port, b_vlan}, ...]
|
||||||
vlans = data.get("vlans", []) # [{vid, name, description, prefixes, site}, ...]
|
vlans = data.get("vlans", []) # [{vid, name, description, prefixes, site}, ...]
|
||||||
|
|
||||||
VLAN_PALETTE = ["#D6603F", "#3C9D6B", "#E8A33D", "#5B7FD9", "#9A6BC9", "#3FAAB0", "#C2547E", "#7A8C3F"]
|
VLAN_PALETTE = ["#D6603F", "#3C9D6B", "#C2547E", "#5B7FD9", "#E8A33D", "#3FAAB0", "#9A6BC9", "#7A8C3F"]
|
||||||
|
# assigned by position among the VLANs actually present (sorted by vid, same
|
||||||
|
# order they're listed/rendered in), not a hash -- guarantees no two VLANs
|
||||||
|
# share a color as long as there are <= len(VLAN_PALETTE) of them, which a
|
||||||
|
# hash-based assignment can't promise (two vids can hash to the same slot)
|
||||||
|
_vlan_color_by_vid = {v["vid"]: VLAN_PALETTE[i % len(VLAN_PALETTE)] for i, v in enumerate(vlans)}
|
||||||
def vlan_color(vid):
|
def vlan_color(vid):
|
||||||
return VLAN_PALETTE[hash(vid) % len(VLAN_PALETTE)]
|
return _vlan_color_by_vid.get(vid, "#9AA0AA")
|
||||||
|
|
||||||
def vlan_tag(pv):
|
def vlan_tag(pv):
|
||||||
"""Short connector-label suffix for a port's VLAN config, e.g. 'V10' or
|
"""Short connector-label suffix for a port's VLAN config, e.g. 'V10' or
|
||||||
@@ -137,7 +142,17 @@ VLAN_HEADER_H = 30
|
|||||||
VLAN_TABLE_W = 900
|
VLAN_TABLE_W = 900
|
||||||
vlan_rows = max(1, len(vlans)) # the placeholder counts as one row
|
vlan_rows = max(1, len(vlans)) # the placeholder counts as one row
|
||||||
VLAN_TABLE_H = VLAN_HEADER_H + vlan_rows * VLAN_ROW_H + 16
|
VLAN_TABLE_H = VLAN_HEADER_H + vlan_rows * VLAN_ROW_H + 16
|
||||||
cursor_y = MARGIN + TITLE_H + LEGEND_ROW_H + VLAN_TABLE_H + 20
|
|
||||||
|
# a dedicated colour-coded legend (VLAN id block + name block per row),
|
||||||
|
# stacked to the left of the detail table -- this is the part that reads
|
||||||
|
# as "legend" at a glance; the table next to it carries the detail
|
||||||
|
VLAN_LEGEND_W = 260
|
||||||
|
VLAN_LEGEND_ROW_H = 42
|
||||||
|
VLAN_LEGEND_GAP = 6
|
||||||
|
VLAN_LEGEND_H = vlan_rows * (VLAN_LEGEND_ROW_H + VLAN_LEGEND_GAP) - VLAN_LEGEND_GAP
|
||||||
|
|
||||||
|
VLAN_SECTION_H = max(VLAN_TABLE_H, VLAN_LEGEND_H)
|
||||||
|
cursor_y = MARGIN + TITLE_H + LEGEND_ROW_H + VLAN_SECTION_H + 20
|
||||||
|
|
||||||
w_main, h_main = place_section(depth_main, yslot_main, cursor_y)
|
w_main, h_main = place_section(depth_main, yslot_main, cursor_y)
|
||||||
main_top = cursor_y
|
main_top = cursor_y
|
||||||
@@ -160,7 +175,7 @@ iso_rows = (len(iso_sorted) + ISO_COLS - 1) // ISO_COLS
|
|||||||
w_iso = MARGIN * 2 + PAD * 2 + (ISO_COLS - 1) * COL_W + SWITCH_ICON
|
w_iso = MARGIN * 2 + PAD * 2 + (ISO_COLS - 1) * COL_W + SWITCH_ICON
|
||||||
h_iso = PAD * 2 + LABEL_STRIP + (iso_rows - 1) * ROW_H + SWITCH_ICON + SWITCH_LABEL_H
|
h_iso = PAD * 2 + LABEL_STRIP + (iso_rows - 1) * ROW_H + SWITCH_ICON + SWITCH_LABEL_H
|
||||||
|
|
||||||
PAGE_W = max(w_main, w_dmo, w_iso, VLAN_TABLE_W + MARGIN * 2)
|
PAGE_W = max(w_main, w_dmo, w_iso, VLAN_LEGEND_W + 30 + VLAN_TABLE_W + MARGIN * 2)
|
||||||
PAGE_H = cursor_y + h_iso + MARGIN
|
PAGE_H = cursor_y + h_iso + MARGIN
|
||||||
|
|
||||||
# ---------------- style palette ----------------
|
# ---------------- style palette ----------------
|
||||||
@@ -303,12 +318,40 @@ for i, (fill, stroke, label) in enumerate(legend_items):
|
|||||||
)
|
)
|
||||||
lx += 260
|
lx += 260
|
||||||
|
|
||||||
# ---------------- VLAN legend / mapping table ----------------
|
# ---------------- VLAN legend (colour blocks) + mapping table ----------------
|
||||||
# Structure is always rendered; content depends on whether NetBox actually
|
# Structure is always rendered; content depends on whether NetBox actually
|
||||||
# has VLANs defined yet (see export_topology.py). Nothing here is invented --
|
# has VLANs defined yet (see export_topology.py). Nothing here is invented --
|
||||||
# an empty table renders as an explicit "not configured" placeholder, not as
|
# an empty legend/table renders as an explicit "not configured" placeholder,
|
||||||
# blank space or made-up VLAN numbers.
|
# not as blank space or made-up VLAN numbers.
|
||||||
vt_x, vt_y = MARGIN, MARGIN + TITLE_H + LEGEND_ROW_H
|
vlg_x, vlg_y = MARGIN, MARGIN + TITLE_H + LEGEND_ROW_H
|
||||||
|
vlg_id_w, vlg_name_w = 70, VLAN_LEGEND_W - 70
|
||||||
|
if not vlans:
|
||||||
|
add_cell(
|
||||||
|
f'<mxCell id="vlg-empty" value="No VLANs" style="rounded=1;arcSize=12;whiteSpace=wrap;html=1;'
|
||||||
|
f'fillColor=#F1F1F2;strokeColor=#9AA0AA;dashed=1;dashPattern=4 3;fontColor=#7C8AA6;fontSize=11;" '
|
||||||
|
f'vertex="1" parent="1"><mxGeometry x="{vlg_x}" y="{vlg_y}" width="{VLAN_LEGEND_W}" '
|
||||||
|
f'height="{VLAN_LEGEND_ROW_H}" as="geometry"/></mxCell>'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
ly2 = vlg_y
|
||||||
|
for v in vlans:
|
||||||
|
color = vlan_color(v["vid"])
|
||||||
|
add_cell(
|
||||||
|
f'<mxCell id="vlg-id-{v["vid"]}" value="VLAN {v["vid"]}" style="rounded=0;whiteSpace=wrap;'
|
||||||
|
f'html=1;fillColor={color};strokeColor=none;fontColor=#FFFFFF;fontStyle=1;fontSize=13;'
|
||||||
|
f'align=center;verticalAlign=middle;lineHeight=1.1;" vertex="1" parent="1">'
|
||||||
|
f'<mxGeometry x="{vlg_x}" y="{ly2}" width="{vlg_id_w}" height="{VLAN_LEGEND_ROW_H}" as="geometry"/></mxCell>'
|
||||||
|
)
|
||||||
|
add_cell(
|
||||||
|
f'<mxCell id="vlg-name-{v["vid"]}" value="{escape(v["name"])}" style="rounded=0;whiteSpace=wrap;'
|
||||||
|
f'html=1;fillColor=#FFFFFF;strokeColor={color};strokeWidth=2;fontColor=#1A2233;fontStyle=1;'
|
||||||
|
f'fontSize=12;align=center;verticalAlign=middle;" vertex="1" parent="1">'
|
||||||
|
f'<mxGeometry x="{vlg_x+vlg_id_w}" y="{ly2}" width="{vlg_name_w}" height="{VLAN_LEGEND_ROW_H}" '
|
||||||
|
f'as="geometry"/></mxCell>'
|
||||||
|
)
|
||||||
|
ly2 += VLAN_LEGEND_ROW_H + VLAN_LEGEND_GAP
|
||||||
|
|
||||||
|
vt_x, vt_y = MARGIN + VLAN_LEGEND_W + 30, MARGIN + TITLE_H + LEGEND_ROW_H
|
||||||
vt_cols = [("VLAN", 60), ("Name", 130), ("Description", 300), ("Subnet", 160), ("Site", 170)]
|
vt_cols = [("VLAN", 60), ("Name", 130), ("Description", 300), ("Subnet", 160), ("Site", 170)]
|
||||||
add_cell(
|
add_cell(
|
||||||
f'<mxCell id="vt-bg" value="" style="rounded=0;fillColor=#FFFFFF;strokeColor=#DDE1EA;" '
|
f'<mxCell id="vt-bg" value="" style="rounded=0;fillColor=#FFFFFF;strokeColor=#DDE1EA;" '
|
||||||
|
|||||||
Reference in New Issue
Block a user