Browse Source

Initial work on global search

Jeremy Stretch 8 years ago
parent
commit
afdb24610d

+ 40 - 0
netbox/netbox/forms.py

@@ -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'
+    )

+ 3 - 2
netbox/netbox/urls.py

@@ -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'),

+ 154 - 5
netbox/netbox/views.py

@@ -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 circuits.models import Provider, Circuit
-from dcim.models import Site, Rack, Device, ConsolePort, PowerPort, InterfaceConnection
+from django.views.generic import View
+
+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

+ 1 - 1
netbox/secrets/views.py

@@ -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

+ 7 - 56
netbox/templates/home.html

@@ -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">

+ 74 - 0
netbox/templates/search.html

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