Skip to content

Commit a00f501

Browse files
authored
Merge pull request #58 from peterbaumert/feature/svg-renderer
feat: add SVG renderer, patch panel device tab support, improved list table
2 parents 5d68d77 + 6082031 commit a00f501

20 files changed

Lines changed: 2354 additions & 45 deletions

File tree

README.md

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
![CI](https://github.com/peterbaumert/netbox-device-view/actions/workflows/test.yml/badge.svg)
66
![License](https://img.shields.io/github/license/peterbaumert/netbox-device-view)
77

8-
Renders a visual CSS grid representation of a device's physical ports and interfaces directly on the NetBox device detail page.
8+
Renders a visual CSS grid representation of a device's physical ports and interfaces directly on the NetBox device detail page. An SVG renderer is also available for YAML-based layouts.
99

1010
![example](https://github.com/peterbaumert/netbox-device-view/blob/main/docs/example_view.png?raw=true)
1111

@@ -88,6 +88,17 @@ The plugin derives a `stylename` for each interface and port:
8888

8989
A DeviceView supports two layout formats. **YAML is preferred** — it is easier to write, validate, and maintain. Legacy CSS is still fully supported for backward compatibility.
9090

91+
### Render modes
92+
93+
A DeviceView also has a **Render Mode** field:
94+
95+
| Mode | Description |
96+
|------|-------------|
97+
| `css` (default) | CSS Grid renderer — works with both YAML and legacy CSS layouts |
98+
| `svg` | SVG renderer — requires a YAML layout; produces a scalable `<svg>` element |
99+
100+
SVG mode produces the same port positions as CSS mode (using `canvas.cell_size` for coordinates) and supports front/rear faces, module variants, hover tooltips, and click targets. Virtual Chassis devices always fall back to CSS mode.
101+
91102
### YAML layout (recommended)
92103

93104
Fill the **YAML Layout** field with a YAML document describing the device layout. The plugin compiles it to CSS at render time.
@@ -113,13 +124,13 @@ views:
113124
sections:
114125
- sequence:
115126
kind: interface
116-
prefix: "gigabitethernet0-"
127+
prefix: "gigabitethernet1-"
117128
start: 1
118129
count: 12
119130
pattern: top-odd
120131
- sequence:
121132
kind: interface
122-
prefix: "gigabitethernet0-"
133+
prefix: "gigabitethernet1-"
123134
start: 13
124135
count: 12
125136
pattern: top-odd
@@ -135,13 +146,13 @@ variants:
135146
sections:
136147
- sequence:
137148
kind: interface
138-
prefix: "gigabitethernet0-"
149+
prefix: "gigabitethernet1-"
139150
start: 1
140151
count: 12
141152
pattern: top-odd
142153
- sequence:
143154
kind: interface
144-
prefix: "gigabitethernet0-"
155+
prefix: "gigabitethernet1-"
145156
start: 13
146157
count: 12
147158
pattern: top-odd
@@ -153,7 +164,7 @@ variants:
153164
pattern: top-odd
154165
```
155166
156-
See [`docs/yaml-layout-schema.md`](docs/yaml-layout-schema.md) for the full schema reference and [`examples/yaml/`](examples/yaml/) for ready-made YAML files.
167+
See [`docs/yaml-layout-schema.md`](docs/yaml-layout-schema.md)
157168

158169
### CSS layout (legacy)
159170

@@ -174,6 +185,18 @@ See [`examples/`](examples/) for ready-made CSS files.
174185

175186
> **Precedence:** if a DeviceView has a YAML layout, it is used and the CSS field is ignored.
176187

188+
### Migrating from CSS to YAML
189+
190+
> [!IMPORTANT]
191+
> If you have existing DeviceViews with CSS `grid_template_area` layouts and want to switch to the SVG renderer, run the bundled management command to convert them automatically:
192+
>
193+
> ```bash
194+
> cd /opt/netbox/netbox
195+
> python manage.py css_to_yaml
196+
> ```
197+
>
198+
> This converts every DeviceView that has a CSS layout but no YAML layout, writes the result into the **YAML Layout** field, and leaves the original CSS untouched (so you can roll back by clearing the YAML field). After running it, set **Render Mode → SVG** on the DeviceViews you want to upgrade.
199+
177200
### Module variants
178201

179202
When a device has an installed module, an extra CSS class matching the module model is added to the device div. In YAML, declare a `variants:` block. In CSS, add a `.deviceview.module{ModelName}.area` rule.
@@ -195,3 +218,9 @@ Add a `.deviceview.module{ModelName}.area` rule to your CSS that includes the ne
195218

196219
**Virtual chassis ports missing**
197220
Ensure your CSS uses `.area.d{vc_position}` scoped selectors. Each VC member needs its own rule.
221+
222+
**SVG mode shows nothing / falls back to CSS**
223+
SVG mode requires a YAML layout. If the DeviceView has no YAML layout, or the device is part of a Virtual Chassis, it silently falls back to CSS rendering.
224+
225+
**Port status colours not showing in SVG mode**
226+
Status classes (`bg-success`, `bg-secondary`, `bg-danger`) are applied by JavaScript using the `data-stylename` attribute. Ensure the static file `netbox_device_view/css/device_view_svg.css` is collected (`python manage.py collectstatic`).

docs/yaml-layout-schema.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,74 @@ The YAML renderer produces CSS equivalent to the legacy hand-written format:
239239

240240
---
241241

242+
## SVG renderer
243+
244+
When a DeviceView has `render_mode` set to `svg`, the plugin uses the same `NormalizedLayout` model to emit an `<svg>` element instead of CSS. The `cell_size` canvas field controls the pixel size of each port cell (default: 20 px if set to 0).
245+
246+
### Coordinate formula
247+
248+
```
249+
x = PADDING + (col - 1) * (cell_size + GAP)
250+
y = PADDING + (row - 1) * (cell_size + GAP)
251+
```
252+
253+
Constants: `PADDING = 6 px`, `GAP = 2 px`, `CORNER_RADIUS = 3 px`, `FONT_SIZE = 7 px`.
254+
255+
### SVG output structure
256+
257+
```xml
258+
<svg class="dv-svg" viewBox="0 0 W H" xmlns="...">
259+
<!-- background -->
260+
<rect class="dv-bg" .../>
261+
262+
<!-- base layer (always present; wrapped in <g class="dv-base"> only when variants exist) -->
263+
<g class="dv-port dv-kind-interface <stylename>" data-stylename="<stylename>">
264+
<rect class="dv-port-rect" .../>
265+
<text class="dv-port-label" ...>Gi0/1</text>
266+
<title>gigabitethernet0-1</title>
267+
</g>
268+
...
269+
270+
<!-- variant layers (hidden by default; activated by JS) -->
271+
<g class="dv-variant dv-variant-C9300-NM-8X" style="display:none">
272+
<g class="dv-port dv-kind-interface tengigabitethernet1-1" data-stylename="tengigabitethernet1-1">
273+
...
274+
</g>
275+
</g>
276+
</svg>
277+
```
278+
279+
### Front/rear faces
280+
281+
Patch panels with separate front and rear views produce **two `<svg>` elements**, each with a `data-face` attribute and a `dv-face-front` / `dv-face-rear` class. The JS in the template shows/hides them based on the active tab.
282+
283+
### CSS classes on port groups
284+
285+
| Class | Meaning |
286+
|-------|---------|
287+
| `dv-port` | Any clickable port element |
288+
| `dv-kind-{kind}` | Element kind (`interface`, `port`, `console-port`, etc.) |
289+
| `{stylename}` | Derived CSS name — same as grid-area name in CSS mode |
290+
| `dv-spacer` | Spacer element |
291+
| `dv-blank` | Blank decorative element |
292+
| `dv-label` | Text label element |
293+
| `dv-base` | Wrapper `<g>` for base elements when variants are present |
294+
| `dv-variant` | Wrapper `<g>` for a module variant layer |
295+
| `dv-variant-{ModelName}` | Identifies which variant this layer belongs to |
296+
| `dv-face-front` / `dv-face-rear` | Applied to the top-level `<svg>` for patch panels |
297+
298+
### Interactivity
299+
300+
Status coloring, hover tooltips (Bootstrap), and variant switching are handled by JavaScript injected in the template — the SVG renderer itself emits only structural markup. The `data-stylename` attribute on port `<g>` elements is the hook used by JS to apply status classes (e.g. `bg-success`, `bg-secondary`, `bg-danger`).
301+
302+
### Limitations
303+
304+
- **Virtual Chassis** devices fall back to CSS rendering automatically — SVG mode for VC is not yet supported.
305+
- SVG mode requires a YAML layout; if no YAML is present the DeviceView silently falls back to CSS.
306+
- `cell_size: 0` (patch-panel auto-sizing) uses `DEFAULT_CELL = 20 px` in SVG mode since CSS `auto` sizing has no SVG equivalent.
307+
308+
---
309+
242310
## Examples
243311

244312
Ready-made YAML layouts are in [`examples/yaml/`](../examples/yaml/):

netbox_device_view/api/serializers.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class Meta:
1111
"device_type",
1212
"grid_template_area",
1313
"yaml_layout",
14+
"render_mode",
1415
"tags",
1516
"custom_fields",
1617
"created",

netbox_device_view/forms.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
class DeviceViewForm(NetBoxModelForm):
99
class Meta:
1010
model = DeviceView
11-
fields = ("device_type", "grid_template_area", "yaml_layout")
11+
fields = ("device_type", "grid_template_area", "yaml_layout", "render_mode")
1212

1313

1414
class DeviceViewImportForm(NetBoxModelImportForm):
@@ -20,4 +20,4 @@ class DeviceViewImportForm(NetBoxModelImportForm):
2020

2121
class Meta:
2222
model = DeviceView
23-
fields = ("device_type", "grid_template_area", "yaml_layout")
23+
fields = ("device_type", "grid_template_area", "yaml_layout", "render_mode")

netbox_device_view/layout/__init__.py

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,25 @@
33
44
Public API
55
----------
6-
from netbox_device_view.layout import parse_yaml, render_css, validate_yaml
6+
from netbox_device_view.layout import parse_yaml, render_css, render_svg, validate_yaml
77
88
# Parse a YAML layout string → NormalizedLayout
99
layout = parse_yaml(yaml_text)
1010
11-
# Render a NormalizedLayout → CSS string
11+
# Render a NormalizedLayout → CSS string (CSS Grid renderer)
1212
css = render_css(layout)
1313
14+
# Render a NormalizedLayout → SVG string (SVG renderer)
15+
svg = render_svg(layout)
16+
1417
# Validate YAML and return a list of error strings (empty = valid)
1518
errors = validate_yaml(yaml_text)
1619
1720
# Get CSS from a DeviceView record (handles both YAML and legacy)
1821
css = get_css_for_device_view(device_view_obj)
22+
23+
# Get SVG from a DeviceView record (requires YAML layout)
24+
svg = get_svg_for_device_view(device_view_obj)
1925
"""
2026

2127
from .legacy import wrap_legacy_css
@@ -28,7 +34,8 @@
2834
PlacedElement,
2935
)
3036
from .parser import LayoutParseError, parse, validate
31-
from .renderers.css_grid import render
37+
from .renderers.css_grid import render as _render_css
38+
from .renderers.svg import render as _render_svg
3239

3340
__all__ = [
3441
# Model
@@ -44,18 +51,20 @@
4451
"LayoutParseError",
4552
# Legacy adapter
4653
"wrap_legacy_css",
47-
# Renderer
48-
"render",
54+
# Renderers
55+
"render_css",
56+
"render_svg",
4957
# Convenience aliases
5058
"parse_yaml",
51-
"render_css",
5259
"validate_yaml",
5360
"get_css_for_device_view",
61+
"get_svg_for_device_view",
5462
]
5563

5664
# Convenience aliases
5765
parse_yaml = parse
58-
render_css = render
66+
render_css = _render_css
67+
render_svg = _render_svg
5968
validate_yaml = validate
6069

6170

@@ -78,6 +87,33 @@ def get_css_for_device_view(device_view) -> str:
7887
"""
7988
if device_view.has_yaml_layout:
8089
layout = parse(device_view.yaml_layout)
81-
return render(layout)
90+
return _render_css(layout)
8291
# Legacy path — return as-is
8392
return device_view.grid_template_area
93+
94+
95+
def get_svg_for_device_view(device_view, variant_name=None) -> str:
96+
"""
97+
Return an SVG string for a DeviceView record.
98+
99+
Only works when the device view has a YAML layout; returns an empty
100+
string for legacy CSS-only records (the caller must fall back to CSS
101+
rendering in that case).
102+
103+
Parameters
104+
----------
105+
device_view : DeviceView
106+
A DeviceView model instance with ``yaml_layout`` field.
107+
variant_name : str | None
108+
Optional module variant name to render.
109+
110+
Returns
111+
-------
112+
str
113+
SVG markup ready for ``{% autoescape off %}`` injection, or ``""``
114+
if the record has no YAML layout.
115+
"""
116+
if not device_view.has_yaml_layout:
117+
return ""
118+
layout = parse(device_view.yaml_layout)
119+
return _render_svg(layout, variant_name=variant_name)

0 commit comments

Comments
 (0)