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('{}', 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( '{}', css_class, tooltip, value or "", ) return format_html('{}', 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( "", '', [ ( "", "", "", "-- 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( '', 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( '' ) 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('Active') return mark_safe('Inactive') 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)