Initial work on global search

This commit is contained in:
Jeremy Stretch 2017-03-29 12:04:57 -04:00
parent 58e4bf1cc3
commit afdb24610d
6 changed files with 278 additions and 63 deletions

40
netbox/netbox/forms.py Normal file
View 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'
)

View File

@ -2,7 +2,7 @@ from django.conf import settings
from django.conf.urls import include, url
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
@ -10,8 +10,9 @@ handler500 = handle_500
_patterns = [
# Default page
# Base views
url(r'^$', home, name='home'),
url(r'^search/$', SearchView.as_view(), name='search'),
# Login/logout
url(r'^login/$', login, name='login'),

View File

@ -1,18 +1,122 @@
import sys
from rest_framework.permissions import IsAuthenticated
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.reverse import reverse
from django.db.models import Count
from django.shortcuts import render
from django.views.generic import View
from circuits.models import Provider, Circuit
from dcim.models import Site, Rack, Device, ConsolePort, PowerPort, InterfaceConnection
from circuits.filters import CircuitFilter, ProviderFilter
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 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.tables import SecretTable
from tenancy.filters import TenantFilter
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):
@ -47,11 +151,56 @@ def home(request):
}
return render(request, 'home.html', {
'search_form': SearchForm(),
'stats': stats,
'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):
_ignore_model_permissions = True
exclude_from_schema = True

View File

@ -48,7 +48,7 @@ class SecretRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
@method_decorator(login_required, name='dispatch')
class SecretListView(ObjectListView):
queryset = Secret.objects.select_related('role').prefetch_related('device')
queryset = Secret.objects.select_related('role', 'device')
filter = filters.SecretFilter
filter_form = forms.SecretFilterForm
table = tables.SecretTable

View File

@ -3,62 +3,13 @@
{% block content %}
<div class="row home-search" style="padding: 15px 0px 20px">
<div class="col-sm-6 col-md-3">
<form action="{% url 'dcim:device_list' %}" method="get">
<div class="input-group">
<input type="text" name="q" placeholder="Search devices" class="form-control" />
<span class="input-group-btn">
<button type="submit" class="btn btn-primary">
<span class="fa fa-search" aria-hidden="true"></span>
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 class="col-md-12 text-right">
<form action="{% url 'search' %}" method="get" class="form form-inline">
{{ search_form.q }}
{{ search_form.obj_type }}
<button type="submit" class="btn btn-primary">Search</button>
</form>
</div>
</div>
<div class="row">
<div class="col-sm-6 col-md-4">

View 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 %}