diff --git a/netbox/netbox/forms/__init__.py b/netbox/netbox/forms/__init__.py index daa5cf904..838a29233 100644 --- a/netbox/netbox/forms/__init__.py +++ b/netbox/netbox/forms/__init__.py @@ -17,7 +17,14 @@ LOOKUP_CHOICES = ( class SearchForm(BootstrapMixin, forms.Form): q = forms.CharField( - label='Search' + label='Search', + widget=forms.TextInput( + attrs={ + 'hx-get': '', + 'hx-target': '#search_results', + 'hx-trigger': 'keyup[target.value.length >= 3] changed delay:500ms', + } + ) ) obj_types = forms.MultipleChoiceField( choices=[], diff --git a/netbox/netbox/search/backends.py b/netbox/netbox/search/backends.py index 3dc5aa083..d48ee4356 100644 --- a/netbox/netbox/search/backends.py +++ b/netbox/netbox/search/backends.py @@ -192,7 +192,7 @@ class CachedValueSearchBackend(SearchBackend): # Wrap the base query to return only the lowest-weight result for each object # Hat-tip to https://blog.oyam.dev/django-filter-by-window-function/ for the solution sql, params = queryset.query.sql_with_params() - results = CachedValue.objects.prefetch_related(prefetch).raw( + results = CachedValue.objects.prefetch_related(prefetch, 'object_type').raw( f"SELECT * FROM ({sql}) t WHERE row_number = 1", params ) diff --git a/netbox/netbox/tables/tables.py b/netbox/netbox/tables/tables.py index 38399b5fe..f1d51d63f 100644 --- a/netbox/netbox/tables/tables.py +++ b/netbox/netbox/tables/tables.py @@ -4,6 +4,7 @@ from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.core.exceptions import FieldDoesNotExist from django.db.models.fields.related import RelatedField +from django.utils.translation import gettext as _ from django_tables2.data import TableQuerysetData from extras.models import CustomField, CustomLink @@ -14,6 +15,7 @@ from utilities.paginator import EnhancedPaginator, get_paginate_count __all__ = ( 'BaseTable', 'NetBoxTable', + 'SearchTable', ) @@ -192,3 +194,18 @@ class NetBoxTable(BaseTable): ]) super().__init__(*args, extra_columns=extra_columns, **kwargs) + + +class SearchTable(tables.Table): + object_type = columns.ContentTypeColumn() + object = tables.Column( + linkify=True + ) + field = tables.Column() + value = tables.Column() + + class Meta: + attrs = { + 'class': 'table table-hover object-list', + } + empty_text = _('No results found') diff --git a/netbox/netbox/views/__init__.py b/netbox/netbox/views/__init__.py index cde6b5966..4a56f82e7 100644 --- a/netbox/netbox/views/__init__.py +++ b/netbox/netbox/views/__init__.py @@ -8,7 +8,6 @@ from django.http import HttpResponseServerError from django.shortcuts import redirect, render from django.template import loader from django.template.exceptions import TemplateDoesNotExist -from django.urls import reverse from django.views.decorators.csrf import requires_csrf_token from django.views.defaults import ERROR_500_TEMPLATE_NAME, page_not_found from django.views.generic import View @@ -23,9 +22,10 @@ from extras.models import ObjectChange from extras.tables import ObjectChangeTable from ipam.models import Aggregate, IPAddress, IPRange, Prefix, VLAN, VRF from netbox.forms import SearchForm -from netbox.search import get_registry from netbox.search.backends import search_backend +from netbox.tables import SearchTable from tenancy.models import Tenant +from utilities.htmx import is_htmx from virtualization.models import Cluster, VirtualMachine from wireless.models import WirelessLAN, WirelessLink @@ -170,9 +170,17 @@ class SearchView(View): lookup=form.cleaned_data['lookup'] ) + table = SearchTable(results) + + # If this is an HTMX request, return only the rendered table HTML + if is_htmx(request): + return render(request, 'htmx/table.html', { + 'table': table, + }) + return render(request, 'search.html', { 'form': form, - 'results': results, + 'table': table, }) diff --git a/netbox/templates/search.html b/netbox/templates/search.html index 4942908da..7059e7dc6 100644 --- a/netbox/templates/search.html +++ b/netbox/templates/search.html @@ -15,49 +15,24 @@ {% endblock tabs %} -{% block content-wrapper %} -
Type | -Object | -Field | -Value | -
---|---|---|---|
{{ result.object|meta:"verbose_name"|bettertitle }} | -- {{ result.object }} - | -{{ result.field|placeholder }} | -{{ result.value|placeholder }} | -