Skip to content

Commit 0a89d27

Browse files
Correct Statseeker module to use InventoryInterface object (#32)
(fix): Correct Statseeker module to use InventoryInterface object for tracked/priority interfaces. Remove priority_interfaces field from Inventory object. Utilize TomSelect for select fields and modify data placeholder in places that use multi select fields.
1 parent 825312d commit 0a89d27

File tree

10 files changed

+81
-29
lines changed

10 files changed

+81
-29
lines changed

src/network_ops_dashboard/inventory/forms.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class Meta:
2929
port_rest = forms.CharField(label="Port (REST):", initial='443')
3030
port_netc = forms.CharField(label="Port (NETCONF):", initial='830')
3131
port_gnmi = forms.CharField(label="Port (gNMI):", initial='9339')
32-
device_tag = forms.ModelMultipleChoiceField(label="Device Tags:", queryset=DeviceTag.objects.all(), widget=forms.SelectMultiple(attrs={'class': 'form-select'}), required=False)
32+
device_tag = forms.ModelMultipleChoiceField(label="Device Tags:", queryset=DeviceTag.objects.all(), widget=forms.SelectMultiple(attrs={'class': 'form-select', "data-placeholder": "Select DeviceTags"}), required=False)
3333
creds_ssh = forms.ModelChoiceField(label="SSH Credential:", queryset=NetworkCredential.objects.all(), required=False)
3434
creds_rest = forms.ModelChoiceField(label="REST Credential:", queryset=NetworkCredential.objects.all(), required=False)
3535

src/network_ops_dashboard/inventory/models.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -170,13 +170,6 @@ class Status(models.TextChoices):
170170
last_backup_at = models.DateTimeField(null=True, blank=True)
171171
discovery_source = models.CharField(max_length=64, blank=True, default="")
172172
sw_version = models.CharField(max_length=128, blank=True, default="")
173-
# Priority/Tracked Interfaces
174-
priority_interfaces = models.JSONField(default=list, blank=True, null=True)
175-
# stores ["Ethernet1/1", "Ethernet2/43", "mgmt0"]
176-
def get_priority_interfaces(self):
177-
return [k.strip() for k in (self.priority_interfaces or "").split(",") if k.strip()]
178-
def set_priority_interfaces(self, items):
179-
self.priority_interfaces = ",".join([str(i).strip() for i in (items or [])])
180173
# Flex Field
181174
extra = models.JSONField(blank=True, default=dict)
182175

src/network_ops_dashboard/inventory/views.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ def inventory_home(request):
3333

3434
@login_required
3535
def inventory_edit_modal(request, pk):
36+
if not pk:
37+
return redirect("inventory_home")
38+
3639
device = get_object_or_404(Inventory, pk=pk)
3740

3841
if request.method == "POST":
@@ -121,7 +124,6 @@ def inventory_edit_modal(request, pk):
121124
},
122125
)
123126

124-
125127
@login_required(login_url='/accounts/login/')
126128
def inventory_add_modal(request):
127129
if request.method == "POST":
@@ -131,7 +133,9 @@ def inventory_add_modal(request):
131133
return HttpResponse('<script>window.location.reload()</script>')
132134
else:
133135
form = InventoryForm()
134-
return render(request, "network_ops_dashboard/inventory/_inventory_form.html", {"form": form, "device": None})
136+
context = {"form": form}
137+
context["device"] = None
138+
return render(request, "network_ops_dashboard/inventory/_inventory_form.html", context)
135139

136140
@login_required(login_url='/accounts/login/')
137141
def inventory_delete_modal(request, pk):

src/network_ops_dashboard/notices/statseeker/forms.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ class Meta:
3030
"credential": forms.Select(attrs={"class": "form-select"}),
3131
"base_url": forms.TextInput(attrs={"class": "form-control", "placeholder": "https://..."}),
3232
"verify_ssl": forms.CheckboxInput(attrs={"class": "form-check-input"}),
33-
"tracked_devices": forms.SelectMultiple(attrs={"class": "form-select", "size": 8}),
33+
"tracked_devices": forms.SelectMultiple(attrs={"class": "form-select", "data-placeholder": "Select Devices", "size": 8}),
3434
"top_n": forms.NumberInput(attrs={"class": "form-control", "min": 1}),
3535
"min_interval_minutes": forms.NumberInput(attrs={"class": "form-control", "min": 1}),
3636
"error_pct_threshold": forms.NumberInput(attrs={"class": "form-control", "min": 0, "step": "0.1"}),
@@ -45,7 +45,7 @@ def __init__(self, *args, **kwargs):
4545
self.fields["credential"].queryset = NetworkCredential.objects.order_by("name")
4646
except Exception:
4747
pass
48-
# If you’d like to pre-filter available devices, do it here:
48+
4949
self.fields["tracked_devices"].queryset = Inventory.objects.all().order_by("name")
50-
self.fields["tracked_devices"].help_text = "Choose devices to watch. Their priority_interfaces field determines which interfaces to evaluate for IF_DOWN/IF_ERRORS."
50+
self.fields["tracked_devices"].help_text = "Choose devices to watch. Their tracked interfaces determines which to evaluate for IF_DOWN/IF_ERRORS."
5151

src/network_ops_dashboard/notices/statseeker/scripts/services.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,20 @@
99
logger = logging.getLogger('network_ops_dashboard.notices.statseeker')
1010

1111
def _tracked_interfaces(inv: Inventory):
12-
# Inventory.priority_interfaces is a comma-separated string.
12+
"""
13+
Return a list of interface names that are marked as priority
14+
for a given Inventory object.
15+
"""
1316
try:
14-
return inv.get_priority_interfaces()
15-
except Exception:
16-
# fallback if needed
17-
s = (inv.priority_interfaces or "").strip()
17+
# use the related_name defined in InventoryInterface.device FK
18+
return list(
19+
inv.interfaces.filter(is_priority=True)
20+
.order_by("rack", "slot", "subslot", "port", "name")
21+
.values_list("name", flat=True)
22+
)
23+
except Exception as e:
24+
# fallback for legacy data or schema mismatch
25+
s = getattr(inv, "priority_interfaces", "") or ""
1826
return [p.strip() for p in s.split(",") if p.strip()]
1927

2028
def _SScookie(base, auth, verifySSL):

src/network_ops_dashboard/reports/changes/views.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ def changes(request):
4848
groups = (CompanyChanges.objects
4949
.exclude(group__isnull=True).exclude(group__exact="")
5050
.order_by('group').values_list('group', flat=True).distinct())
51+
52+
sites = Site.objects.all().order_by('name')
5153

5254
return render(
5355
request,
@@ -59,6 +61,7 @@ def changes(request):
5961
'locations': locations,
6062
'teams': teams,
6163
'groups': groups,
64+
'sites': sites,
6265
}
6366
)
6467

@@ -70,9 +73,6 @@ def changes_update(request):
7073
@staff_member_required
7174
@require_POST
7275
def save_changes_settings(request):
73-
import json
74-
from network_ops_dashboard.inventory.models import Site
75-
7676
s, _ = CompanyChangesSettings.objects.get_or_create(pk=1)
7777

7878
s.changes_folder = request.POST.get("changes_folder", s.changes_folder or "")

src/network_ops_dashboard/sitesettings/forms.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ class Meta:
1010
fields = ('company', 'teamname', 'websites', 'publicscripts', 'companylogo')
1111
company = forms.CharField(label="Company Name:", required=True)
1212
teamname = forms.CharField(label="Team Name:", required=True)
13-
websites = forms.ModelMultipleChoiceField(label="Home Site Websites:", help_text="Websites show up on main public page.", queryset=SiteSettingsWebsite.objects.all(), widget=forms.SelectMultiple(attrs={'class': 'form-select'}), required=False)
14-
publicscripts = forms.ModelMultipleChoiceField(label="Public Script Page Websites:", help_text="Websites show up public scripts page.", queryset=SiteSettingsWebsite.objects.all(), widget=forms.SelectMultiple(attrs={'class': 'form-select'}), required=False)
13+
websites = forms.ModelMultipleChoiceField(label="Home Site Websites:", help_text="Websites show up on main public page.", queryset=SiteSettingsWebsite.objects.all(), widget=forms.SelectMultiple(attrs={'class': 'form-select', "data-placeholder": "Select Websites"}), required=False)
14+
publicscripts = forms.ModelMultipleChoiceField(label="Public Script Page Websites:", help_text="Websites show up public scripts page.", queryset=SiteSettingsWebsite.objects.all(), widget=forms.SelectMultiple(attrs={'class': 'form-select', "data-placeholder": "Select Websites"}), required=False)
1515
companylogo = forms.ImageField()
1616

1717
class SiteSettingsWebsiteForm(forms.ModelForm):

src/network_ops_dashboard/templates/network_ops_dashboard/base.html

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,11 @@
3030
{% endif %}
3131
<link rel="stylesheet" href="{% static 'css/base.css' %}">
3232
<link rel="stylesheet" href="{% static 'css/style.css' %}">
33-
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
33+
<link href="https://cdn.jsdelivr.net/npm/tom-select@2.3.1/dist/css/tom-select.bootstrap5.min.css" rel="stylesheet">
34+
<script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
3435
<script src="https://bootswatch.com/_vendor/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
3536
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
37+
<script src="https://cdn.jsdelivr.net/npm/tom-select@2.3.1/dist/js/tom-select.complete.min.js"></script>
3638
<script type="text/javascript">
3739
function getDocHeight(doc) {
3840
doc = doc || document;
@@ -222,7 +224,52 @@
222224
</div>
223225
{% endif %}
224226

227+
228+
225229
{% block content %}
226230
{% endblock %}
231+
<script>
232+
(function() {
233+
function initTomSelects(root=document) {
234+
const selects = root.querySelectorAll('select[multiple]:not([data-ts-init])');
235+
selects.forEach(function(el) {
236+
el.setAttribute('data-ts-init', 'true');
237+
if (typeof TomSelect === 'undefined') {
238+
console.error('TomSelect library not loaded!');
239+
return;
240+
}
241+
console.log('Initializing Tom Select for', el.name);
242+
new TomSelect(el, {
243+
plugins: ['remove_button'],
244+
closeAfterSelect: false,
245+
hideSelected: false,
246+
maxItems: null,
247+
persist: false,
248+
render: {
249+
option: (data, escape) => `<div>${escape(data.text)}</div>`,
250+
item: (data, escape) => `<div>${escape(data.text)}</div>`
251+
}
252+
});
253+
});
254+
}
255+
256+
// Run once when DOM is ready
257+
document.addEventListener('DOMContentLoaded', function() {
258+
initTomSelects();
259+
});
260+
261+
// Re-run after any HTMX swap (modals, partials, etc)
262+
document.body.addEventListener('htmx:afterSwap', function(e) {
263+
initTomSelects(e.target);
264+
});
265+
266+
// Safety net: retry a few times in case of late load
267+
let attempts = 0;
268+
const interval = setInterval(() => {
269+
initTomSelects();
270+
if (++attempts > 10) clearInterval(interval);
271+
}, 500);
272+
})();
273+
</script>
227274
</body>
228275
</html>

src/network_ops_dashboard/templates/network_ops_dashboard/dashboard.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -352,12 +352,13 @@ <h2 class="accordion-header" id="ssHeading">
352352
{{ statseeker_form.base_url }}
353353
</div>
354354
</div>
355-
356355
<div class="row g-3 mt-1">
357356
<div class="col-md-6">
358357
<label class="form-label">{{ statseeker_form.tracked_devices.label }}</label>
359358
{{ statseeker_form.tracked_devices }}
360-
<div class="form-text">Set device <code>priority_interfaces</code> for IF_DOWN/IF_ERRORS.</div>
359+
<div class="form-text text-muted small">
360+
Select device(s) whose tracked interfaces will be monitored for link status and errors.
361+
</div>
361362
</div>
362363
<div class="col-md-3">
363364
<label class="form-label">{{ statseeker_form.top_n.label }}</label>
@@ -368,7 +369,6 @@ <h2 class="accordion-header" id="ssHeading">
368369
{{ statseeker_form.verify_ssl }}
369370
</div>
370371
</div>
371-
372372
<div class="row g-3 mt-1">
373373
<div class="col-md-4">
374374
<label class="form-label">{{ statseeker_form.error_pct_threshold.label }}</label>

src/network_ops_dashboard/templates/network_ops_dashboard/reports/changes/home.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ <h5 class="modal-title">Change Email Parser Settings</h5>
122122

123123
<div class="col-12" id="sitesMultiWrap" {% if not settings_obj.use_sites_for_locations %}style="display:none"{% endif %}>
124124
<label class="form-label">Limit to specific Sites (optional)</label>
125-
<select name="sites_to_filter" class="form-select" multiple size="8">
125+
<select name="sites_to_filter" class="form-select" multiple size="8" data-placeholder="Select Sites">
126126
{% for site in sites %}
127127
<option value="{{ site.pk }}"
128128
{% if site in settings_obj.sites_to_filter.all %}selected{% endif %}>
@@ -131,7 +131,7 @@ <h5 class="modal-title">Change Email Parser Settings</h5>
131131
{% endfor %}
132132
</select>
133133
<div class="form-text">
134-
Hold Ctrl/Cmd to select multiple. If none selected, all Sites are considered valid.
134+
If none selected, all Sites are considered valid.
135135
</div>
136136
</div>
137137

0 commit comments

Comments
 (0)