1# SPDX-License-Identifier: Apache-2.0
2# Copyright (C) 2025 Marcin Zieba <marcinpsk@gmail.com>
3import django_tables2 as tables
4from django.utils.html import format_html
5from netbox.tables import NetBoxTable, columns
6
7from .models import InterfaceNameRule
8
9
10class SpecificityColumn(tables.Column):
11 """Renders the rule's specificity_score as a badge with its label."""
12
13 def __init__(self, *args, **kwargs):
14 kwargs.setdefault("verbose_name", "Priority")
15 kwargs.setdefault("orderable", False)
16 kwargs.setdefault("attrs", {"th": {"title": "Higher score = higher priority in rule lookup"}})
17 super().__init__(*args, **kwargs)
18
19 def render(self, value, record):
20 """Render the specificity score as a coloured badge."""
21 label = record.specificity_label
22 if record.applies_to_device_interfaces:
23 css = "text-bg-warning" # device-interface rule
24 elif not record.module_type_is_regex:
25 css = "text-bg-success" # exact FK — always highest priority
26 elif record.device_type_id or record.parent_module_type_id:
27 css = "text-bg-primary" # device- or parent-scoped regex
28 elif record.platform_id:
29 css = "text-bg-info" # platform-scoped regex
30 else:
31 css = "text-bg-secondary" # unscoped (global) regex
32 return format_html(
33 '<span class="badge {}" title="{}" style="font-family:monospace">{}</span>',
34 css,
35 label,
36 value,
37 )
38
39 def value(self, value, record=None, **kwargs):
40 """Return the raw numeric specificity score for CSV export."""
41 return value
42
43
44class InterfaceNameRuleTable(NetBoxTable):
45 """Table for displaying InterfaceNameRule objects."""
46
47 pk = columns.ToggleColumn()
48 enabled = tables.TemplateColumn(
49 template_code="""
50{% if record.enabled %}
51<button type="button" class="btn btn-sm btn-success toggle-btn"
52 data-toggle-url="{% url 'plugins:netbox_interface_name_rules:interfacenamerule_toggle' record.pk %}"
53 data-pk="{{ record.pk }}"
54 title="Enabled — click to disable"
55 aria-label="Toggle rule enabled">
56 <i class="mdi mdi-check"></i>
57</button>
58{% else %}
59<button type="button" class="btn btn-sm btn-secondary toggle-btn"
60 data-toggle-url="{% url 'plugins:netbox_interface_name_rules:interfacenamerule_toggle' record.pk %}"
61 data-pk="{{ record.pk }}"
62 title="Disabled — click to enable"
63 aria-label="Toggle rule enabled">
64 <i class="mdi mdi-minus"></i>
65</button>
66{% endif %}
67""",
68 verbose_name="Enabled",
69 orderable=True,
70 attrs={"td": {"class": "text-center"}, "th": {"class": "text-center"}},
71 )
72 specificity_score = SpecificityColumn()
73 module_type = tables.Column(verbose_name="Module Type", linkify=True)
74 module_type_pattern = tables.Column(verbose_name="Pattern")
75 module_type_is_regex = tables.TemplateColumn(
76 template_code="""
77{% if record.applies_to_device_interfaces %}
78<span class="badge text-bg-info" title="Pattern filters interface names (device-level rule — not a module type selector)">
79 <i class="mdi mdi-filter-outline"></i>
80</span>
81{% elif record.module_type_is_regex %}
82<span class="badge text-bg-success" title="Module type is matched by regex pattern">
83 <i class="mdi mdi-check"></i>
84</span>
85{% else %}
86<span class="text-muted" title="Exact module type match (FK)">—</span>
87{% endif %}
88""",
89 verbose_name="Regex",
90 orderable=True,
91 attrs={"td": {"class": "text-center"}, "th": {"class": "text-center"}},
92 )
93 parent_module_type = tables.Column(verbose_name="Parent Module Type", linkify=True)
94 device_type = tables.Column(verbose_name="Device Type", linkify=True)
95 platform = tables.Column(verbose_name="Platform", linkify=True)
96 applies_to_device_interfaces = columns.BooleanColumn(verbose_name="Device Ifaces")
97 name_template = tables.Column(verbose_name="Name Template")
98 channel_count = tables.Column(verbose_name="Channels")
99 channel_start = tables.Column(verbose_name="Channel Start")
100 description = tables.Column(verbose_name="Description", linkify=False)
101 actions = columns.ActionsColumn(
102 actions=("edit", "delete"),
103 extra_buttons=(
104 "<a href=\"{% url 'plugins:netbox_interface_name_rules:interfacenamerule_test' %}?rule_id={{ record.pk }}\""
105 ' class="btn btn-sm btn-outline-secondary" title="Test in Build Rule" aria-label="Test in Build Rule">'
106 '<i class="mdi mdi-flask-outline"></i></a>'
107 "<a href=\"{% url 'plugins:netbox_interface_name_rules:interfacenamerule_duplicate' record.pk %}\""
108 ' class="btn btn-sm btn-success" title="Duplicate" aria-label="Duplicate">'
109 '<i class="mdi mdi-content-copy"></i></a>'
110 ),
111 )
112
113 class Meta:
114 model = InterfaceNameRule
115 fields = (
116 "pk",
117 "id",
118 "enabled",
119 "specificity_score",
120 "module_type",
121 "module_type_pattern",
122 "module_type_is_regex",
123 "parent_module_type",
124 "device_type",
125 "platform",
126 "applies_to_device_interfaces",
127 "name_template",
128 "channel_count",
129 "channel_start",
130 "description",
131 "actions",
132 )
133 default_columns = (
134 "pk",
135 "id",
136 "enabled",
137 "specificity_score",
138 "module_type",
139 "module_type_pattern",
140 "module_type_is_regex",
141 "parent_module_type",
142 "device_type",
143 "platform",
144 "applies_to_device_interfaces",
145 "name_template",
146 "channel_count",
147 "description",
148 "actions",
149 )
150 row_attrs = {"class": lambda record: "" if record.enabled else "text-muted opacity-50"}
151 attrs = {"class": "table table-hover table-headings table-striped"}