mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-23 04:22:01 -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.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'),
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
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