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 parent_name_template = tables.Column(verbose_name="Parent Name Template")
99 breakout_mode = columns.ChoiceFieldColumn(verbose_name="Breakout Mode")
100 channel_count = tables.Column(verbose_name="Channels")
101 channel_start = tables.Column(verbose_name="Channel Start")
102 description = tables.Column(verbose_name="Description", linkify=False)
103 actions = columns.ActionsColumn(
104 actions=("edit", "delete"),
105 extra_buttons=(
106 "<a href=\"{% url 'plugins:netbox_interface_name_rules:interfacenamerule_test' %}?rule_id={{ record.pk }}\""
107 ' class="btn btn-sm btn-outline-secondary" title="Test in Build Rule" aria-label="Test in Build Rule">'
108 '<i class="mdi mdi-flask-outline"></i></a>'
109 "<a href=\"{% url 'plugins:netbox_interface_name_rules:interfacenamerule_duplicate' record.pk %}\""
110 ' class="btn btn-sm btn-success" title="Duplicate" aria-label="Duplicate">'
111 '<i class="mdi mdi-content-copy"></i></a>'
112 ),
113 )
114
115 class Meta:
116 model = InterfaceNameRule
117 fields = (
118 "pk",
119 "id",
120 "enabled",
121 "specificity_score",
122 "module_type",
123 "module_type_pattern",
124 "module_type_is_regex",
125 "parent_module_type",
126 "device_type",
127 "platform",
128 "applies_to_device_interfaces",
129 "name_template",
130 "parent_name_template",
131 "breakout_mode",
132 "channel_count",
133 "channel_start",
134 "description",
135 "actions",
136 )
137 default_columns = (
138 "pk",
139 "id",
140 "enabled",
141 "specificity_score",
142 "module_type",
143 "module_type_pattern",
144 "module_type_is_regex",
145 "parent_module_type",
146 "device_type",
147 "platform",
148 "applies_to_device_interfaces",
149 "name_template",
150 "breakout_mode",
151 "channel_count",
152 "description",
153 "actions",
154 )
155 row_attrs = {"class": lambda record: "" if record.enabled else "text-muted opacity-50"}
156 attrs = {"class": "table table-hover table-headings table-striped"}