mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-25 00:36:11 -06:00
Introduce SearchTable and enable HTMX for results
This commit is contained in:
parent
f69a13e05b
commit
7cc85a609d
@ -17,7 +17,14 @@ LOOKUP_CHOICES = (
|
|||||||
|
|
||||||
class SearchForm(BootstrapMixin, forms.Form):
|
class SearchForm(BootstrapMixin, forms.Form):
|
||||||
q = forms.CharField(
|
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(
|
obj_types = forms.MultipleChoiceField(
|
||||||
choices=[],
|
choices=[],
|
||||||
|
@ -192,7 +192,7 @@ class CachedValueSearchBackend(SearchBackend):
|
|||||||
# Wrap the base query to return only the lowest-weight result for each object
|
# 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
|
# Hat-tip to https://blog.oyam.dev/django-filter-by-window-function/ for the solution
|
||||||
sql, params = queryset.query.sql_with_params()
|
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",
|
f"SELECT * FROM ({sql}) t WHERE row_number = 1",
|
||||||
params
|
params
|
||||||
)
|
)
|
||||||
|
@ -4,6 +4,7 @@ from django.contrib.contenttypes.fields import GenericForeignKey
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import FieldDoesNotExist
|
from django.core.exceptions import FieldDoesNotExist
|
||||||
from django.db.models.fields.related import RelatedField
|
from django.db.models.fields.related import RelatedField
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
from django_tables2.data import TableQuerysetData
|
from django_tables2.data import TableQuerysetData
|
||||||
|
|
||||||
from extras.models import CustomField, CustomLink
|
from extras.models import CustomField, CustomLink
|
||||||
@ -14,6 +15,7 @@ from utilities.paginator import EnhancedPaginator, get_paginate_count
|
|||||||
__all__ = (
|
__all__ = (
|
||||||
'BaseTable',
|
'BaseTable',
|
||||||
'NetBoxTable',
|
'NetBoxTable',
|
||||||
|
'SearchTable',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -192,3 +194,18 @@ class NetBoxTable(BaseTable):
|
|||||||
])
|
])
|
||||||
|
|
||||||
super().__init__(*args, extra_columns=extra_columns, **kwargs)
|
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')
|
||||||
|
@ -8,7 +8,6 @@ from django.http import HttpResponseServerError
|
|||||||
from django.shortcuts import redirect, render
|
from django.shortcuts import redirect, render
|
||||||
from django.template import loader
|
from django.template import loader
|
||||||
from django.template.exceptions import TemplateDoesNotExist
|
from django.template.exceptions import TemplateDoesNotExist
|
||||||
from django.urls import reverse
|
|
||||||
from django.views.decorators.csrf import requires_csrf_token
|
from django.views.decorators.csrf import requires_csrf_token
|
||||||
from django.views.defaults import ERROR_500_TEMPLATE_NAME, page_not_found
|
from django.views.defaults import ERROR_500_TEMPLATE_NAME, page_not_found
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
@ -23,9 +22,10 @@ from extras.models import ObjectChange
|
|||||||
from extras.tables import ObjectChangeTable
|
from extras.tables import ObjectChangeTable
|
||||||
from ipam.models import Aggregate, IPAddress, IPRange, Prefix, VLAN, VRF
|
from ipam.models import Aggregate, IPAddress, IPRange, Prefix, VLAN, VRF
|
||||||
from netbox.forms import SearchForm
|
from netbox.forms import SearchForm
|
||||||
from netbox.search import get_registry
|
|
||||||
from netbox.search.backends import search_backend
|
from netbox.search.backends import search_backend
|
||||||
|
from netbox.tables import SearchTable
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
|
from utilities.htmx import is_htmx
|
||||||
from virtualization.models import Cluster, VirtualMachine
|
from virtualization.models import Cluster, VirtualMachine
|
||||||
from wireless.models import WirelessLAN, WirelessLink
|
from wireless.models import WirelessLAN, WirelessLink
|
||||||
|
|
||||||
@ -170,9 +170,17 @@ class SearchView(View):
|
|||||||
lookup=form.cleaned_data['lookup']
|
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', {
|
return render(request, 'search.html', {
|
||||||
'form': form,
|
'form': form,
|
||||||
'results': results,
|
'table': table,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@ -15,49 +15,24 @@
|
|||||||
</ul>
|
</ul>
|
||||||
{% endblock tabs %}
|
{% endblock tabs %}
|
||||||
|
|
||||||
{% block content-wrapper %}
|
{% block content %}
|
||||||
<div class="row px-3">
|
<div class="row px-3">
|
||||||
<div class="col col-6 offset-3 py-3">
|
<div class="col col-6 offset-3 py-3">
|
||||||
<form action="{% url 'search' %}" method="get" class="form form-horizontal">
|
<form action="{% url 'search' %}" method="get" class="form form-horizontal">
|
||||||
{% render_form form %}
|
{% render_form form %}
|
||||||
<div class="text-end">
|
<div class="text-end">
|
||||||
<button type="submit" class="btn btn-primary">
|
<button type="submit" class="btn btn-primary">
|
||||||
<span class="mdi mdi-magnify" aria-hidden="true"></span> Search
|
<span class="mdi mdi-magnify" aria-hidden="true"></span> Search
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row px-3">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body" id="search_results">
|
||||||
|
{% include 'htmx/table.html' %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% if request.GET.q %}
|
</div>
|
||||||
{% if results %}
|
{% endblock content %}
|
||||||
<div class="row px-3">
|
|
||||||
<div class="col">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-body table-responsive">
|
|
||||||
<table class="table table-hover">
|
|
||||||
<tr>
|
|
||||||
<th>Type</th>
|
|
||||||
<th>Object</th>
|
|
||||||
<th>Field</th>
|
|
||||||
<th>Value</th>
|
|
||||||
</tr>
|
|
||||||
{% for result in results %}
|
|
||||||
<tr>
|
|
||||||
<td>{{ result.object|meta:"verbose_name"|bettertitle }}</td>
|
|
||||||
<td>
|
|
||||||
<a href="{{ result.object.get_absolute_url }}">{{ result.object }}</a>
|
|
||||||
</td>
|
|
||||||
<td>{{ result.field|placeholder }}</td>
|
|
||||||
<td>{{ result.value|placeholder }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<h3 class="text-muted text-center">No results found</h3>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
{% endblock content-wrapper %}
|
|
||||||
|
Loading…
Reference in New Issue
Block a user