Files
netbox-librenms-plugin/netbox_librenms_plugin/tables/vlans.py
Vlastislav Svatek 673e67106e
Some checks failed
ci / deploy (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
first commit
2026-06-05 10:39:05 +02:00

184 lines
6.2 KiB
Python

import django_tables2 as tables
from django.utils.html import format_html, format_html_join
from django.utils.safestring import mark_safe
from netbox.tables.columns import ToggleColumn
from utilities.paginator import EnhancedPaginator
from netbox_librenms_plugin.constants import LIBRENMS_VLAN_STATE_ACTIVE
from netbox_librenms_plugin.utils import get_table_paginate_count, get_vlan_sync_css_class
class LibreNMSVLANTable(tables.Table):
"""
Table for displaying LibreNMS VLAN data for a device.
Shows VLANs configured on the device and their sync status with NetBox.
Includes per-row VLAN group selection dropdown.
"""
class Meta:
sequence = [
"selection",
"vlan_id",
"name",
"vlan_group_selection",
"type",
"state",
]
attrs = {
"class": "table table-hover object-list",
"id": "librenms-vlan-table",
}
row_attrs = {
"data-vlan-id": lambda record: record.get("vlan_id"),
}
def __init__(self, *args, vlan_groups=None, **kwargs):
super().__init__(*args, **kwargs)
self.prefix = "vlans_"
self.vlan_groups = vlan_groups or []
selection = ToggleColumn(
orderable=False,
visible=True,
attrs={"td": {"data-col": "selection"}, "input": {"name": "select"}},
accessor="vlan_id",
)
vlan_id = tables.Column(
accessor="vlan_id",
verbose_name="VLAN ID",
attrs={"td": {"data-col": "vlan_id"}},
)
name = tables.Column(
accessor="name",
verbose_name="Name",
attrs={"td": {"data-col": "name"}},
)
vlan_group_selection = tables.Column(
verbose_name="VLAN Group",
empty_values=(),
orderable=False,
attrs={"td": {"data-col": "vlan_group_selection"}},
)
type = tables.Column(
accessor="type",
verbose_name="Type",
attrs={"td": {"data-col": "type"}},
)
state = tables.Column(
accessor="state",
verbose_name="State",
attrs={"td": {"data-col": "state"}},
)
def render_vlan_id(self, value, record):
"""Render VLAN ID with color based on sync status."""
css_class = get_vlan_sync_css_class(
record.get("exists_in_netbox", False),
record.get("name_matches", True),
)
return format_html('<span class="{}">{}</span>', css_class, value)
def render_name(self, value, record):
"""Render VLAN name with color based on sync status."""
css_class = get_vlan_sync_css_class(
record.get("exists_in_netbox", False),
record.get("name_matches", True),
)
# Add tooltip on name mismatch
if record.get("exists_in_netbox") and not record.get("name_matches", True):
netbox_name = record.get("netbox_vlan_name", "")
tooltip = f"NetBox: {netbox_name} | LibreNMS: {value}"
return format_html(
'<span class="{}" title="{}">{}</span>',
css_class,
tooltip,
value or "",
)
return format_html('<span class="{}">{}</span>', css_class, value or "")
def render_vlan_group_selection(self, value, record):
"""
Render per-row VLAN group dropdown.
Auto-selects based on matching priority:
1. Existing NetBox VLAN's group (if exists_in_netbox)
2. Unique VID match (if VID exists in exactly one group)
3. No selection (with warning icon if ambiguous)
"""
vlan_id = record.get("vlan_id")
# Determine which group to auto-select
selected_group_id = None
# Priority 1: Existing NetBox VLAN group
if record.get("exists_in_netbox") and record.get("netbox_vlan_group_id"):
selected_group_id = record["netbox_vlan_group_id"]
elif record.get("auto_selected_group_id"):
# Priority 2: unique VID match
selected_group_id = record["auto_selected_group_id"]
# Build the select element using format_html_join to prevent XSS
options_html = format_html_join(
"",
'<option value="{}" data-scope="{}"{}>{}{}</option>',
[
(
"",
"",
"",
"-- No Group (Global) --",
"",
),
]
+ [
(
group.pk,
group.scope_id if group.scope_id else "",
" selected" if group.pk == selected_group_id else "",
group.name,
f" ({group.scope})" if group.scope else "",
)
for group in self.vlan_groups
],
)
select_html = format_html(
'<select name="vlan_group_{}" class="form-select form-select-sm vlan-sync-group-select"'
' data-vlan-id="{}" data-vlan-name="{}" style="min-width: 180px;">{}</select>',
vlan_id,
vlan_id,
record.get("name", ""),
options_html,
)
# Add warning icon if ambiguous (VID exists in multiple groups at same priority level)
if record.get("is_ambiguous") and not record.get("exists_in_netbox"):
warning_html = mark_safe(
'<i class="mdi mdi-alert text-warning ms-1" '
'title="VID exists in multiple groups at the same scope level. Please select the target group."></i>'
)
return format_html("{}{}", select_html, warning_html)
return select_html
def render_state(self, value, record):
"""Render VLAN state (active/inactive)."""
if value == LIBRENMS_VLAN_STATE_ACTIVE or value == "active":
return mark_safe('<span class="text-success">Active</span>')
return mark_safe('<span class="text-muted">Inactive</span>')
def configure(self, request):
"""Configure the table with pagination."""
paginate = {
"paginator_class": EnhancedPaginator,
"per_page": get_table_paginate_count(request, self.prefix),
}
tables.RequestConfig(request, paginate).configure(self)