mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-25 01:48:38 -06:00
Initial work on global search
This commit is contained in:
parent
58e4bf1cc3
commit
afdb24610d
40
netbox/netbox/forms.py
Normal file
40
netbox/netbox/forms.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
from django import forms
|
||||||
|
|
||||||
|
from utilities.forms import BootstrapMixin
|
||||||
|
|
||||||
|
|
||||||
|
OBJ_TYPE_CHOICES = (
|
||||||
|
('', 'All Objects'),
|
||||||
|
('Circuits', (
|
||||||
|
('provider', 'Providers'),
|
||||||
|
('circuit', 'Circuits'),
|
||||||
|
)),
|
||||||
|
('DCIM', (
|
||||||
|
('site', 'Sites'),
|
||||||
|
('rack', 'Racks'),
|
||||||
|
('devicetype', 'Device types'),
|
||||||
|
('device', 'Devices'),
|
||||||
|
)),
|
||||||
|
('IPAM', (
|
||||||
|
('vrf', 'VRFs'),
|
||||||
|
('aggregate', 'Aggregates'),
|
||||||
|
('prefix', 'Prefixes'),
|
||||||
|
('ipaddress', 'IP addresses'),
|
||||||
|
('vlan', 'VLANs'),
|
||||||
|
)),
|
||||||
|
('Secrets', (
|
||||||
|
('secret', 'Secrets'),
|
||||||
|
)),
|
||||||
|
('Tenancy', (
|
||||||
|
('tenant', 'Tenants'),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SearchForm(BootstrapMixin, forms.Form):
|
||||||
|
q = forms.CharField(
|
||||||
|
label='Query'
|
||||||
|
)
|
||||||
|
obj_type = forms.ChoiceField(
|
||||||
|
choices=OBJ_TYPE_CHOICES, required=False, label='Type'
|
||||||
|
)
|
@ -2,7 +2,7 @@ from django.conf import settings
|
|||||||
from django.conf.urls import include, url
|
from django.conf.urls import include, url
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from netbox.views import APIRootView, home, handle_500, trigger_500
|
from netbox.views import APIRootView, home, handle_500, SearchView, trigger_500
|
||||||
from users.views import login, logout
|
from users.views import login, logout
|
||||||
|
|
||||||
|
|
||||||
@ -10,8 +10,9 @@ handler500 = handle_500
|
|||||||
|
|
||||||
_patterns = [
|
_patterns = [
|
||||||
|
|
||||||
# Default page
|
# Base views
|
||||||
url(r'^$', home, name='home'),
|
url(r'^$', home, name='home'),
|
||||||
|
url(r'^search/$', SearchView.as_view(), name='search'),
|
||||||
|
|
||||||
# Login/logout
|
# Login/logout
|
||||||
url(r'^login/$', login, name='login'),
|
url(r'^login/$', login, name='login'),
|
||||||
|
@ -1,18 +1,122 @@
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
from rest_framework.permissions import IsAuthenticated
|
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.reverse import reverse
|
from rest_framework.reverse import reverse
|
||||||
|
|
||||||
|
from django.db.models import Count
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
|
from django.views.generic import View
|
||||||
|
|
||||||
from circuits.models import Provider, Circuit
|
from circuits.filters import CircuitFilter, ProviderFilter
|
||||||
from dcim.models import Site, Rack, Device, ConsolePort, PowerPort, InterfaceConnection
|
from circuits.models import Circuit, Provider
|
||||||
|
from circuits.tables import CircuitTable, ProviderTable
|
||||||
|
from dcim.filters import DeviceFilter, DeviceTypeFilter, RackFilter, SiteFilter
|
||||||
|
from dcim.models import ConsolePort, Device, DeviceType, InterfaceConnection, PowerPort, Rack, Site
|
||||||
|
from dcim.tables import DeviceTable, DeviceTypeTable, RackTable, SiteTable
|
||||||
from extras.models import UserAction
|
from extras.models import UserAction
|
||||||
from ipam.models import Aggregate, Prefix, IPAddress, VLAN, VRF
|
from ipam.filters import AggregateFilter, IPAddressFilter, PrefixFilter, VLANFilter, VRFFilter
|
||||||
|
from ipam.models import Aggregate, IPAddress, Prefix, VLAN, VRF
|
||||||
|
from ipam.tables import AggregateTable, IPAddressTable, PrefixTable, VLANTable, VRFTable
|
||||||
|
from secrets.filters import SecretFilter
|
||||||
from secrets.models import Secret
|
from secrets.models import Secret
|
||||||
|
from secrets.tables import SecretTable
|
||||||
|
from tenancy.filters import TenantFilter
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
|
from tenancy.tables import TenantTable
|
||||||
|
from .forms import SearchForm
|
||||||
|
|
||||||
|
|
||||||
|
SEARCH_MAX_RESULTS = 15
|
||||||
|
SEARCH_TYPES = {
|
||||||
|
# Circuits
|
||||||
|
'provider': {
|
||||||
|
'queryset': Provider.objects.annotate(count_circuits=Count('circuits')),
|
||||||
|
'filter': ProviderFilter,
|
||||||
|
'table': ProviderTable,
|
||||||
|
'url': 'circuits:provider_list',
|
||||||
|
},
|
||||||
|
'circuit': {
|
||||||
|
'queryset': Circuit.objects.select_related('provider', 'type', 'tenant').prefetch_related(
|
||||||
|
'terminations__site'
|
||||||
|
),
|
||||||
|
'filter': CircuitFilter,
|
||||||
|
'table': CircuitTable,
|
||||||
|
'url': 'circuits:circuit_list',
|
||||||
|
},
|
||||||
|
# DCIM
|
||||||
|
'site': {
|
||||||
|
'queryset': Site.objects.select_related('region', 'tenant'),
|
||||||
|
'filter': SiteFilter,
|
||||||
|
'table': SiteTable,
|
||||||
|
'url': 'dcim:site_list',
|
||||||
|
},
|
||||||
|
'rack': {
|
||||||
|
'queryset': Rack.objects.select_related('site', 'group', 'tenant', 'role').prefetch_related('devices__device_type').annotate(device_count=Count('devices', distinct=True)),
|
||||||
|
'filter': RackFilter,
|
||||||
|
'table': RackTable,
|
||||||
|
'url': 'dcim:rack_list',
|
||||||
|
},
|
||||||
|
'devicetype': {
|
||||||
|
'queryset': DeviceType.objects.select_related('manufacturer').annotate(instance_count=Count('instances')),
|
||||||
|
'filter': DeviceTypeFilter,
|
||||||
|
'table': DeviceTypeTable,
|
||||||
|
'url': 'dcim:devicetype_list',
|
||||||
|
},
|
||||||
|
'device': {
|
||||||
|
'queryset': Device.objects.select_related(
|
||||||
|
'device_type__manufacturer', 'device_role', 'tenant', 'site', 'rack', 'primary_ip4', 'primary_ip6'
|
||||||
|
),
|
||||||
|
'filter': DeviceFilter,
|
||||||
|
'table': DeviceTable,
|
||||||
|
'url': 'dcim:device_list',
|
||||||
|
},
|
||||||
|
# IPAM
|
||||||
|
'vrf': {
|
||||||
|
'queryset': VRF.objects.select_related('tenant'),
|
||||||
|
'filter': VRFFilter,
|
||||||
|
'table': VRFTable,
|
||||||
|
'url': 'ipam:vrf_list',
|
||||||
|
},
|
||||||
|
'aggregate': {
|
||||||
|
'queryset': Aggregate.objects.select_related('rir'),
|
||||||
|
'filter': AggregateFilter,
|
||||||
|
'table': AggregateTable,
|
||||||
|
'url': 'ipam:aggregate_list',
|
||||||
|
},
|
||||||
|
'prefix': {
|
||||||
|
'queryset': Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role'),
|
||||||
|
'filter': PrefixFilter,
|
||||||
|
'table': PrefixTable,
|
||||||
|
'url': 'ipam:prefix_list',
|
||||||
|
},
|
||||||
|
'ipaddress': {
|
||||||
|
'queryset': IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device'),
|
||||||
|
'filter': IPAddressFilter,
|
||||||
|
'table': IPAddressTable,
|
||||||
|
'url': 'ipam:ipaddress_list',
|
||||||
|
},
|
||||||
|
'vlan': {
|
||||||
|
'queryset': VLAN.objects.select_related('site', 'group', 'tenant', 'role').prefetch_related('prefixes'),
|
||||||
|
'filter': VLANFilter,
|
||||||
|
'table': VLANTable,
|
||||||
|
'url': 'ipam:vlan_list',
|
||||||
|
},
|
||||||
|
# Secrets
|
||||||
|
'secret': {
|
||||||
|
'queryset': Secret.objects.select_related('role', 'device'),
|
||||||
|
'filter': SecretFilter,
|
||||||
|
'table': SecretTable,
|
||||||
|
'url': 'secrets:secret_list',
|
||||||
|
},
|
||||||
|
# Tenancy
|
||||||
|
'tenant': {
|
||||||
|
'queryset': Tenant.objects.select_related('group'),
|
||||||
|
'filter': TenantFilter,
|
||||||
|
'table': TenantTable,
|
||||||
|
'url': 'tenancy:tenant_list',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def home(request):
|
def home(request):
|
||||||
@ -47,11 +151,56 @@ def home(request):
|
|||||||
}
|
}
|
||||||
|
|
||||||
return render(request, 'home.html', {
|
return render(request, 'home.html', {
|
||||||
|
'search_form': SearchForm(),
|
||||||
'stats': stats,
|
'stats': stats,
|
||||||
'recent_activity': UserAction.objects.select_related('user')[:50]
|
'recent_activity': UserAction.objects.select_related('user')[:50]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class SearchView(View):
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
|
||||||
|
# No query
|
||||||
|
if 'q' not in request.GET:
|
||||||
|
return render(request, 'search.html', {
|
||||||
|
'form': SearchForm(),
|
||||||
|
})
|
||||||
|
|
||||||
|
form = SearchForm(request.GET)
|
||||||
|
results = []
|
||||||
|
|
||||||
|
if form.is_valid():
|
||||||
|
|
||||||
|
# Searching for a single type of object
|
||||||
|
if form.cleaned_data['obj_type']:
|
||||||
|
obj_types = [form.cleaned_data['obj_type']]
|
||||||
|
# Searching all object types
|
||||||
|
else:
|
||||||
|
obj_types = SEARCH_TYPES.keys()
|
||||||
|
|
||||||
|
for obj_type in obj_types:
|
||||||
|
queryset = SEARCH_TYPES[obj_type]['queryset']
|
||||||
|
filter = SEARCH_TYPES[obj_type]['filter']
|
||||||
|
table = SEARCH_TYPES[obj_type]['table']
|
||||||
|
url = SEARCH_TYPES[obj_type]['url']
|
||||||
|
filtered_queryset = filter({'q': form.cleaned_data['q']}, queryset=queryset).qs
|
||||||
|
total_count = filtered_queryset.count()
|
||||||
|
if total_count:
|
||||||
|
results.append({
|
||||||
|
'name': queryset.model._meta.verbose_name_plural,
|
||||||
|
'table': table(filtered_queryset[:SEARCH_MAX_RESULTS]),
|
||||||
|
'total': total_count,
|
||||||
|
'url': '{}?q={}'.format(reverse(url), form.cleaned_data['q'])
|
||||||
|
})
|
||||||
|
|
||||||
|
return render(request, 'search.html', {
|
||||||
|
'form': form,
|
||||||
|
'results': results,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class APIRootView(APIView):
|
class APIRootView(APIView):
|
||||||
_ignore_model_permissions = True
|
_ignore_model_permissions = True
|
||||||
exclude_from_schema = True
|
exclude_from_schema = True
|
||||||
|
@ -48,7 +48,7 @@ class SecretRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
|
|
||||||
@method_decorator(login_required, name='dispatch')
|
@method_decorator(login_required, name='dispatch')
|
||||||
class SecretListView(ObjectListView):
|
class SecretListView(ObjectListView):
|
||||||
queryset = Secret.objects.select_related('role').prefetch_related('device')
|
queryset = Secret.objects.select_related('role', 'device')
|
||||||
filter = filters.SecretFilter
|
filter = filters.SecretFilter
|
||||||
filter_form = forms.SecretFilterForm
|
filter_form = forms.SecretFilterForm
|
||||||
table = tables.SecretTable
|
table = tables.SecretTable
|
||||||
|
@ -3,62 +3,13 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row home-search" style="padding: 15px 0px 20px">
|
<div class="row home-search" style="padding: 15px 0px 20px">
|
||||||
<div class="col-sm-6 col-md-3">
|
<div class="col-md-12 text-right">
|
||||||
<form action="{% url 'dcim:device_list' %}" method="get">
|
<form action="{% url 'search' %}" method="get" class="form form-inline">
|
||||||
<div class="input-group">
|
{{ search_form.q }}
|
||||||
<input type="text" name="q" placeholder="Search devices" class="form-control" />
|
{{ search_form.obj_type }}
|
||||||
<span class="input-group-btn">
|
<button type="submit" class="btn btn-primary">Search</button>
|
||||||
<button type="submit" class="btn btn-primary">
|
</form>
|
||||||
<span class="fa fa-search" aria-hidden="true"></span>
|
</div>
|
||||||
Devices
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<p></p>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-6 col-md-3">
|
|
||||||
<form action="{% url 'ipam:prefix_list' %}" method="get">
|
|
||||||
<div class="input-group">
|
|
||||||
<input type="text" name="q" placeholder="Search prefixes" class="form-control" />
|
|
||||||
<span class="input-group-btn">
|
|
||||||
<button type="submit" class="btn btn-primary">
|
|
||||||
<span class="fa fa-search" aria-hidden="true"></span>
|
|
||||||
Prefixes
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<p></p>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-6 col-md-3">
|
|
||||||
<form action="{% url 'ipam:ipaddress_list' %}" method="get">
|
|
||||||
<div class="input-group">
|
|
||||||
<input type="text" name="q" placeholder="Search IPs" class="form-control" />
|
|
||||||
<span class="input-group-btn">
|
|
||||||
<button type="submit" class="btn btn-primary">
|
|
||||||
<span class="fa fa-search" aria-hidden="true"></span>
|
|
||||||
IPs
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<p></p>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-6 col-md-3">
|
|
||||||
<form action="{% url 'circuits:circuit_list' %}" method="get">
|
|
||||||
<div class="input-group">
|
|
||||||
<input type="text" name="q" placeholder="Search circuits" class="form-control" />
|
|
||||||
<span class="input-group-btn">
|
|
||||||
<button type="submit" class="btn btn-primary">
|
|
||||||
<span class="fa fa-search" aria-hidden="true"></span>
|
|
||||||
Circuits
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<p></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-6 col-md-4">
|
<div class="col-sm-6 col-md-4">
|
||||||
|
74
netbox/templates/search.html
Normal file
74
netbox/templates/search.html
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
{% extends '_base.html' %}
|
||||||
|
{% load form_helpers %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% if request.GET.q %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4 col-md-offset-4">
|
||||||
|
{# Compressed search form #}
|
||||||
|
<form action="{% url 'search' %}" method="get" class="form form-inline pull-right">
|
||||||
|
{{ form.q }}
|
||||||
|
{{ form.obj_type }}
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<span class="fa fa-search" aria-hidden="true"></span> Search
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-10">
|
||||||
|
{% for obj_type in results %}
|
||||||
|
<h3 id="{{ obj_type.name }}">{{ obj_type.name|title }}</h3>
|
||||||
|
{% include 'table.html' with table=obj_type.table %}
|
||||||
|
{% if obj_type.total > obj_type.table.rows|length %}
|
||||||
|
<a href="{{ obj_type.url }}" class="btn btn-primary pull-right">
|
||||||
|
<span class="fa fa-search" aria-hidden="true"></span>
|
||||||
|
All {{ obj_type.total }} results
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
<div class="clearfix"></div>
|
||||||
|
{% empty %}
|
||||||
|
<h3 class="text-muted">No results found</h3>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2" style="padding-top: 20px;">
|
||||||
|
{% if results %}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<strong>Search Results</strong>
|
||||||
|
</div>
|
||||||
|
<div class="list-group">
|
||||||
|
{% for obj_type in results %}
|
||||||
|
<a href="#{{ obj_type.name }}" class="list-group-item">
|
||||||
|
{{ obj_type.name|title }}
|
||||||
|
<span class="badge">{{ obj_type.total }}</span>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
{# Larger search form #}
|
||||||
|
<div class="row" style="margin-top: 150px;">
|
||||||
|
<div class="col-sm-4 col-sm-offset-4">
|
||||||
|
<form action="{% url 'search' %}" method="get" class="form form-horizontal">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<strong>Search</strong>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% render_form form %}
|
||||||
|
</div>
|
||||||
|
<div class="panel-footer text-right">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<span class="fa fa-search" aria-hidden="true"></span> Search
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
Loading…
Reference in New Issue
Block a user