diff --git a/netbox/circuits/tables.py b/netbox/circuits/tables.py index ab877a8ce..07e2c4477 100644 --- a/netbox/circuits/tables.py +++ b/netbox/circuits/tables.py @@ -1,7 +1,7 @@ import django_tables2 as tables from django_tables2.utils import Accessor -from utilities.tables import BaseTable, ToggleColumn +from utilities.tables import BaseTable, SearchTable, ToggleColumn from .models import Circuit, CircuitType, Provider @@ -19,9 +19,7 @@ CIRCUITTYPE_ACTIONS = """ class ProviderTable(BaseTable): pk = ToggleColumn() - name = tables.LinkColumn('circuits:provider', args=[Accessor('slug')], verbose_name='Name') - asn = tables.Column(verbose_name='ASN') - account = tables.Column(verbose_name='Account') + name = tables.LinkColumn() circuit_count = tables.Column(accessor=Accessor('count_circuits'), verbose_name='Circuits') class Meta(BaseTable.Meta): @@ -29,17 +27,25 @@ class ProviderTable(BaseTable): fields = ('pk', 'name', 'asn', 'account', 'circuit_count') +class ProviderSearchTable(SearchTable): + name = tables.LinkColumn() + + class Meta(SearchTable.Meta): + model = Provider + fields = ('name', 'asn', 'account') + + # # Circuit types # class CircuitTypeTable(BaseTable): pk = ToggleColumn() - name = tables.LinkColumn(verbose_name='Name') + name = tables.LinkColumn() circuit_count = tables.Column(verbose_name='Circuits') - slug = tables.Column(verbose_name='Slug') - actions = tables.TemplateColumn(template_code=CIRCUITTYPE_ACTIONS, attrs={'td': {'class': 'text-right'}}, - verbose_name='') + actions = tables.TemplateColumn( + template_code=CIRCUITTYPE_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='' + ) class Meta(BaseTable.Meta): model = CircuitType @@ -52,16 +58,28 @@ class CircuitTypeTable(BaseTable): class CircuitTable(BaseTable): pk = ToggleColumn() - cid = tables.LinkColumn('circuits:circuit', args=[Accessor('pk')], verbose_name='ID') - type = tables.Column(verbose_name='Type') - provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')], verbose_name='Provider') - tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant') - a_side = tables.LinkColumn('dcim:site', accessor=Accessor('termination_a.site'), orderable=False, - args=[Accessor('termination_a.site.slug')]) - z_side = tables.LinkColumn('dcim:site', accessor=Accessor('termination_z.site'), orderable=False, - args=[Accessor('termination_z.site.slug')]) - description = tables.Column(verbose_name='Description') + cid = tables.LinkColumn(verbose_name='ID') + provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')]) + tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')]) + a_side = tables.LinkColumn( + 'dcim:site', accessor=Accessor('termination_a.site'), orderable=False, + args=[Accessor('termination_a.site.slug')] + ) + z_side = tables.LinkColumn( + 'dcim:site', accessor=Accessor('termination_z.site'), orderable=False, + args=[Accessor('termination_z.site.slug')] + ) class Meta(BaseTable.Meta): model = Circuit fields = ('pk', 'cid', 'type', 'provider', 'tenant', 'a_side', 'z_side', 'description') + + +class CircuitSearchTable(SearchTable): + cid = tables.LinkColumn(verbose_name='ID') + provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')]) + tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')]) + + class Meta(SearchTable.Meta): + model = Circuit + fields = ('cid', 'type', 'provider', 'tenant', 'description') diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 9d9425f9c..6eab5ae34 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -8,9 +8,9 @@ from tenancy.models import Tenant from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter from .models import ( ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, - DeviceBayTemplate, DeviceRole, DeviceType, IFACE_FF_LAG, Interface, InterfaceConnection, InterfaceTemplate, - Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, - RackReservation, RackRole, Region, Site, VIRTUAL_IFACE_TYPES, + DeviceBayTemplate, DeviceRole, DeviceType, IFACE_FF_LAG, Interface, InterfaceTemplate, Manufacturer, InventoryItem, + Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, + RackRole, Region, Site, VIRTUAL_IFACE_TYPES, ) diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index c4c7c5b73..b6ebb1be2 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -1,7 +1,7 @@ import django_tables2 as tables from django_tables2.utils import Accessor -from utilities.tables import BaseTable, ToggleColumn +from utilities.tables import BaseTable, SearchTable, ToggleColumn from .models import ( ConsolePort, ConsolePortTemplate, ConsoleServerPortTemplate, Device, DeviceBayTemplate, DeviceRole, DeviceType, @@ -136,11 +136,9 @@ class RegionTable(BaseTable): class SiteTable(BaseTable): pk = ToggleColumn() - name = tables.LinkColumn('dcim:site', args=[Accessor('slug')], verbose_name='Name') - facility = tables.Column(verbose_name='Facility') - region = tables.TemplateColumn(template_code=SITE_REGION_LINK, verbose_name='Region') - tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant') - asn = tables.Column(verbose_name='ASN') + name = tables.LinkColumn() + region = tables.TemplateColumn(template_code=SITE_REGION_LINK) + tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')]) rack_count = tables.Column(accessor=Accessor('count_racks'), orderable=False, verbose_name='Racks') device_count = tables.Column(accessor=Accessor('count_devices'), orderable=False, verbose_name='Devices') prefix_count = tables.Column(accessor=Accessor('count_prefixes'), orderable=False, verbose_name='Prefixes') @@ -155,6 +153,16 @@ class SiteTable(BaseTable): ) +class SiteSearchTable(SearchTable): + name = tables.LinkColumn() + region = tables.TemplateColumn(template_code=SITE_REGION_LINK) + tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')]) + + class Meta(SearchTable.Meta): + model = Site + fields = ('name', 'facility', 'region', 'tenant', 'asn') + + # # Rack groups # @@ -197,20 +205,33 @@ class RackRoleTable(BaseTable): class RackTable(BaseTable): pk = ToggleColumn() - name = tables.LinkColumn('dcim:rack', args=[Accessor('pk')], verbose_name='Name') - site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site') + name = tables.LinkColumn() + site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group') - facility_id = tables.Column(verbose_name='Facility ID') - tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant') - role = tables.TemplateColumn(RACK_ROLE, verbose_name='Role') + tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')]) + role = tables.TemplateColumn(RACK_ROLE) u_height = tables.TemplateColumn("{{ record.u_height }}U", verbose_name='Height') - devices = tables.Column(accessor=Accessor('device_count'), verbose_name='Devices') + devices = tables.Column(accessor=Accessor('device_count')) get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization') class Meta(BaseTable.Meta): model = Rack - fields = ('pk', 'name', 'site', 'group', 'facility_id', 'tenant', 'role', 'u_height', 'devices', - 'get_utilization') + fields = ( + 'pk', 'name', 'site', 'group', 'facility_id', 'tenant', 'role', 'u_height', 'devices', 'get_utilization' + ) + + +class RackSearchTable(SearchTable): + name = tables.LinkColumn() + site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) + group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group') + tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')]) + role = tables.TemplateColumn(RACK_ROLE) + u_height = tables.TemplateColumn("{{ record.u_height }}U", verbose_name='Height') + + class Meta(SearchTable.Meta): + model = Rack + fields = ('name', 'site', 'group', 'facility_id', 'tenant', 'role', 'u_height') class RackImportTable(BaseTable): @@ -249,9 +270,7 @@ class ManufacturerTable(BaseTable): class DeviceTypeTable(BaseTable): pk = ToggleColumn() - manufacturer = tables.Column(verbose_name='Manufacturer') model = tables.LinkColumn('dcim:devicetype', args=[Accessor('pk')], verbose_name='Device Type') - part_number = tables.Column(verbose_name='Part Number') is_full_depth = tables.BooleanColumn(verbose_name='Full Depth') is_console_server = tables.BooleanColumn(verbose_name='CS') is_pdu = tables.BooleanColumn(verbose_name='PDU') @@ -267,6 +286,22 @@ class DeviceTypeTable(BaseTable): ) +class DeviceTypeSearchTable(SearchTable): + model = tables.LinkColumn('dcim:devicetype', args=[Accessor('pk')], verbose_name='Device Type') + is_full_depth = tables.BooleanColumn(verbose_name='Full Depth') + is_console_server = tables.BooleanColumn(verbose_name='CS') + is_pdu = tables.BooleanColumn(verbose_name='PDU') + is_network_device = tables.BooleanColumn(verbose_name='Net') + subdevice_role = tables.TemplateColumn(SUBDEVICE_ROLE_TEMPLATE, verbose_name='Subdevice Role') + + class Meta(SearchTable.Meta): + model = DeviceType + fields = ( + 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'is_console_server', 'is_pdu', + 'is_network_device', 'subdevice_role', + ) + + # # Device type components # @@ -373,22 +408,42 @@ class PlatformTable(BaseTable): class DeviceTable(BaseTable): pk = ToggleColumn() + name = tables.TemplateColumn(template_code=DEVICE_LINK) status = tables.TemplateColumn(template_code=STATUS_ICON, verbose_name='') - name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name') - tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant') - site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site') - rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')], verbose_name='Rack') + tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')]) + site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) + rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')]) device_role = tables.TemplateColumn(DEVICE_ROLE, verbose_name='Role') - device_type = tables.LinkColumn('dcim:devicetype', args=[Accessor('device_type.pk')], verbose_name='Type', - text=lambda record: record.device_type.full_name) - primary_ip = tables.TemplateColumn(orderable=False, verbose_name='IP Address', - template_code=DEVICE_PRIMARY_IP) + device_type = tables.LinkColumn( + 'dcim:devicetype', args=[Accessor('device_type.pk')], verbose_name='Type', + text=lambda record: record.device_type.full_name + ) + primary_ip = tables.TemplateColumn( + orderable=False, verbose_name='IP Address', template_code=DEVICE_PRIMARY_IP + ) class Meta(BaseTable.Meta): model = Device fields = ('pk', 'name', 'status', 'tenant', 'site', 'rack', 'device_role', 'device_type', 'primary_ip') +class DeviceSearchTable(SearchTable): + name = tables.TemplateColumn(template_code=DEVICE_LINK) + status = tables.TemplateColumn(template_code=STATUS_ICON, verbose_name='') + tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')]) + site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) + rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')]) + device_role = tables.TemplateColumn(DEVICE_ROLE, verbose_name='Role') + device_type = tables.LinkColumn( + 'dcim:devicetype', args=[Accessor('device_type.pk')], verbose_name='Type', + text=lambda record: record.device_type.full_name + ) + + class Meta(SearchTable.Meta): + model = Device + fields = ('name', 'status', 'tenant', 'site', 'rack', 'device_role', 'device_type') + + class DeviceImportTable(BaseTable): name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name') tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant') diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index 6c99f7d9e..49f87d716 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -1,7 +1,7 @@ import django_tables2 as tables from django_tables2.utils import Accessor -from utilities.tables import BaseTable, ToggleColumn +from utilities.tables import BaseTable, SearchTable, ToggleColumn from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF @@ -133,16 +133,25 @@ TENANT_LINK = """ class VRFTable(BaseTable): pk = ToggleColumn() - name = tables.LinkColumn('ipam:vrf', args=[Accessor('pk')], verbose_name='Name') + name = tables.LinkColumn() rd = tables.Column(verbose_name='RD') - tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant') - description = tables.Column(verbose_name='Description') + tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')]) class Meta(BaseTable.Meta): model = VRF fields = ('pk', 'name', 'rd', 'tenant', 'description') +class VRFSearchTable(SearchTable): + name = tables.LinkColumn() + rd = tables.Column(verbose_name='RD') + tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')]) + + class Meta(SearchTable.Meta): + model = VRF + fields = ('name', 'rd', 'tenant', 'description') + + # # RIRs # @@ -177,18 +186,25 @@ class RIRTable(BaseTable): class AggregateTable(BaseTable): pk = ToggleColumn() - prefix = tables.LinkColumn('ipam:aggregate', args=[Accessor('pk')], verbose_name='Aggregate') - rir = tables.Column(verbose_name='RIR') + prefix = tables.LinkColumn(verbose_name='Aggregate') child_count = tables.Column(verbose_name='Prefixes') get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization') date_added = tables.DateColumn(format="Y-m-d", verbose_name='Added') - description = tables.Column(verbose_name='Description') class Meta(BaseTable.Meta): model = Aggregate fields = ('pk', 'prefix', 'rir', 'child_count', 'get_utilization', 'date_added', 'description') +class AggregateSearchTable(SearchTable): + prefix = tables.LinkColumn(verbose_name='Aggregate') + date_added = tables.DateColumn(format="Y-m-d", verbose_name='Added') + + class Meta(SearchTable.Meta): + model = Aggregate + fields = ('prefix', 'rir', 'date_added', 'description') + + # # Roles # @@ -212,14 +228,13 @@ class RoleTable(BaseTable): class PrefixTable(BaseTable): pk = ToggleColumn() - status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status') - prefix = tables.TemplateColumn(PREFIX_LINK, verbose_name='Prefix', attrs={'th': {'style': 'padding-left: 17px'}}) + prefix = tables.TemplateColumn(PREFIX_LINK, attrs={'th': {'style': 'padding-left: 17px'}}) + status = tables.TemplateColumn(STATUS_LABEL) vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF') - tenant = tables.TemplateColumn(TENANT_LINK, verbose_name='Tenant') - site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site') + tenant = tables.TemplateColumn(TENANT_LINK) + site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) vlan = tables.LinkColumn('ipam:vlan', args=[Accessor('vlan.pk')], verbose_name='VLAN') - role = tables.TemplateColumn(PREFIX_ROLE_LINK, verbose_name='Role') - description = tables.Column(verbose_name='Description') + role = tables.TemplateColumn(PREFIX_ROLE_LINK) class Meta(BaseTable.Meta): model = Prefix @@ -230,12 +245,11 @@ class PrefixTable(BaseTable): class PrefixBriefTable(BaseTable): - prefix = tables.TemplateColumn(PREFIX_LINK_BRIEF, verbose_name='Prefix') - vrf = tables.LinkColumn('ipam:vrf', args=[Accessor('vrf.pk')], default='Global', verbose_name='VRF') - site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site') - status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status') - vlan = tables.LinkColumn('ipam:vlan', args=[Accessor('vlan.pk')], verbose_name='VLAN') - role = tables.Column(verbose_name='Role') + prefix = tables.TemplateColumn(PREFIX_LINK_BRIEF) + vrf = tables.LinkColumn('ipam:vrf', args=[Accessor('vrf.pk')], default='Global') + site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) + status = tables.TemplateColumn(STATUS_LABEL) + vlan = tables.LinkColumn('ipam:vlan', args=[Accessor('vlan.pk')]) class Meta(BaseTable.Meta): model = Prefix @@ -243,6 +257,20 @@ class PrefixBriefTable(BaseTable): orderable = False +class PrefixSearchTable(SearchTable): + prefix = tables.TemplateColumn(PREFIX_LINK, attrs={'th': {'style': 'padding-left: 17px'}}) + status = tables.TemplateColumn(STATUS_LABEL) + vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF') + tenant = tables.TemplateColumn(TENANT_LINK) + site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) + vlan = tables.LinkColumn('ipam:vlan', args=[Accessor('vlan.pk')], verbose_name='VLAN') + role = tables.TemplateColumn(PREFIX_ROLE_LINK) + + class Meta(SearchTable.Meta): + model = Prefix + fields = ('prefix', 'status', 'vrf', 'tenant', 'site', 'vlan', 'role', 'description') + + # # IPAddresses # @@ -250,13 +278,11 @@ class PrefixBriefTable(BaseTable): class IPAddressTable(BaseTable): pk = ToggleColumn() address = tables.TemplateColumn(IPADDRESS_LINK, verbose_name='IP Address') - status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status') + status = tables.TemplateColumn(STATUS_LABEL) vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF') - tenant = tables.TemplateColumn(TENANT_LINK, verbose_name='Tenant') - device = tables.LinkColumn('dcim:device', args=[Accessor('interface.device.pk')], orderable=False, - verbose_name='Device') - interface = tables.Column(orderable=False, verbose_name='Interface') - description = tables.Column(verbose_name='Description') + tenant = tables.TemplateColumn(TENANT_LINK) + device = tables.LinkColumn('dcim:device', args=[Accessor('interface.device.pk')], orderable=False) + interface = tables.Column(orderable=False) class Meta(BaseTable.Meta): model = IPAddress @@ -268,17 +294,30 @@ class IPAddressTable(BaseTable): class IPAddressBriefTable(BaseTable): address = tables.LinkColumn('ipam:ipaddress', args=[Accessor('pk')], verbose_name='IP Address') - device = tables.LinkColumn('dcim:device', args=[Accessor('interface.device.pk')], orderable=False, - verbose_name='Device') - interface = tables.Column(orderable=False, verbose_name='Interface') - nat_inside = tables.LinkColumn('ipam:ipaddress', args=[Accessor('nat_inside.pk')], orderable=False, - verbose_name='NAT (Inside)') + device = tables.LinkColumn('dcim:device', args=[Accessor('interface.device.pk')], orderable=False) + interface = tables.Column(orderable=False) + nat_inside = tables.LinkColumn( + 'ipam:ipaddress', args=[Accessor('nat_inside.pk')], orderable=False, verbose_name='NAT (Inside)' + ) class Meta(BaseTable.Meta): model = IPAddress fields = ('address', 'device', 'interface', 'nat_inside') +class IPAddressSearchTable(SearchTable): + address = tables.TemplateColumn(IPADDRESS_LINK, verbose_name='IP Address') + status = tables.TemplateColumn(STATUS_LABEL) + vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF') + tenant = tables.TemplateColumn(TENANT_LINK) + device = tables.LinkColumn('dcim:device', args=[Accessor('interface.device.pk')], orderable=False) + interface = tables.Column(orderable=False) + + class Meta(SearchTable.Meta): + model = IPAddress + fields = ('address', 'status', 'vrf', 'tenant', 'device', 'interface', 'description') + + # # VLAN groups # @@ -304,15 +343,26 @@ class VLANGroupTable(BaseTable): class VLANTable(BaseTable): pk = ToggleColumn() vid = tables.LinkColumn('ipam:vlan', args=[Accessor('pk')], verbose_name='ID') - site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site') + site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group') - name = tables.Column(verbose_name='Name') prefixes = tables.TemplateColumn(VLAN_PREFIXES, orderable=False, verbose_name='Prefixes') - tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant') - status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status') - role = tables.TemplateColumn(VLAN_ROLE_LINK, verbose_name='Role') - description = tables.Column(verbose_name='Description') + tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')]) + status = tables.TemplateColumn(STATUS_LABEL) + role = tables.TemplateColumn(VLAN_ROLE_LINK) class Meta(BaseTable.Meta): model = VLAN fields = ('pk', 'vid', 'site', 'group', 'name', 'prefixes', 'tenant', 'status', 'role', 'description') + + +class VLANSearchTable(SearchTable): + vid = tables.LinkColumn('ipam:vlan', args=[Accessor('pk')], verbose_name='ID') + site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) + group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group') + tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')]) + status = tables.TemplateColumn(STATUS_LABEL) + role = tables.TemplateColumn(VLAN_ROLE_LINK) + + class Meta(SearchTable.Meta): + model = VLAN + fields = ('vid', 'site', 'group', 'name', 'tenant', 'status', 'role', 'description') diff --git a/netbox/netbox/forms.py b/netbox/netbox/forms.py new file mode 100644 index 000000000..63af2e04b --- /dev/null +++ b/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', widget=forms.TextInput(attrs={'style': 'width: 350px'}) + ) + obj_type = forms.ChoiceField( + choices=OBJ_TYPE_CHOICES, required=False, label='Type' + ) diff --git a/netbox/netbox/urls.py b/netbox/netbox/urls.py index e7a76bed9..724ab3090 100644 --- a/netbox/netbox/urls.py +++ b/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'), diff --git a/netbox/netbox/views.py b/netbox/netbox/views.py index 17a1e19d2..0e0b9e50c 100644 --- a/netbox/netbox/views.py +++ b/netbox/netbox/views.py @@ -1,18 +1,117 @@ 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.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 CircuitSearchTable, ProviderSearchTable +from dcim.filters import DeviceFilter, DeviceTypeFilter, RackFilter, SiteFilter +from dcim.models import ConsolePort, Device, DeviceType, InterfaceConnection, PowerPort, Rack, Site +from dcim.tables import DeviceSearchTable, DeviceTypeSearchTable, RackSearchTable, SiteSearchTable 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 AggregateSearchTable, IPAddressSearchTable, PrefixSearchTable, VLANSearchTable, VRFSearchTable +from secrets.filters import SecretFilter from secrets.models import Secret +from secrets.tables import SecretSearchTable +from tenancy.filters import TenantFilter from tenancy.models import Tenant +from tenancy.tables import TenantSearchTable +from .forms import SearchForm + + +SEARCH_MAX_RESULTS = 15 +SEARCH_TYPES = { + # Circuits + 'provider': { + 'queryset': Provider.objects.all(), + 'filter': ProviderFilter, + 'table': ProviderSearchTable, + 'url': 'circuits:provider_list', + }, + 'circuit': { + 'queryset': Circuit.objects.select_related('type', 'provider', 'tenant'), + 'filter': CircuitFilter, + 'table': CircuitSearchTable, + 'url': 'circuits:circuit_list', + }, + # DCIM + 'site': { + 'queryset': Site.objects.select_related('region', 'tenant'), + 'filter': SiteFilter, + 'table': SiteSearchTable, + 'url': 'dcim:site_list', + }, + 'rack': { + 'queryset': Rack.objects.select_related('site', 'group', 'tenant', 'role'), + 'filter': RackFilter, + 'table': RackSearchTable, + 'url': 'dcim:rack_list', + }, + 'devicetype': { + 'queryset': DeviceType.objects.select_related('manufacturer'), + 'filter': DeviceTypeFilter, + 'table': DeviceTypeSearchTable, + 'url': 'dcim:devicetype_list', + }, + 'device': { + 'queryset': Device.objects.select_related('device_type__manufacturer', 'device_role', 'tenant', 'site', 'rack'), + 'filter': DeviceFilter, + 'table': DeviceSearchTable, + 'url': 'dcim:device_list', + }, + # IPAM + 'vrf': { + 'queryset': VRF.objects.select_related('tenant'), + 'filter': VRFFilter, + 'table': VRFSearchTable, + 'url': 'ipam:vrf_list', + }, + 'aggregate': { + 'queryset': Aggregate.objects.select_related('rir'), + 'filter': AggregateFilter, + 'table': AggregateSearchTable, + 'url': 'ipam:aggregate_list', + }, + 'prefix': { + 'queryset': Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role'), + 'filter': PrefixFilter, + 'table': PrefixSearchTable, + 'url': 'ipam:prefix_list', + }, + 'ipaddress': { + 'queryset': IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device'), + 'filter': IPAddressFilter, + 'table': IPAddressSearchTable, + 'url': 'ipam:ipaddress_list', + }, + 'vlan': { + 'queryset': VLAN.objects.select_related('site', 'group', 'tenant', 'role'), + 'filter': VLANFilter, + 'table': VLANSearchTable, + 'url': 'ipam:vlan_list', + }, + # Secrets + 'secret': { + 'queryset': Secret.objects.select_related('role', 'device'), + 'filter': SecretFilter, + 'table': SecretSearchTable, + 'url': 'secrets:secret_list', + }, + # Tenancy + 'tenant': { + 'queryset': Tenant.objects.select_related('group'), + 'filter': TenantFilter, + 'table': TenantSearchTable, + 'url': 'tenancy:tenant_list', + }, +} def home(request): @@ -47,11 +146,59 @@ 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_cls = SEARCH_TYPES[obj_type]['filter'] + table = SEARCH_TYPES[obj_type]['table'] + url = SEARCH_TYPES[obj_type]['url'] + + # Construct the results table for this object type + filtered_queryset = filter_cls({'q': form.cleaned_data['q']}, queryset=queryset).qs + table = table(filtered_queryset) + table.paginate(per_page=SEARCH_MAX_RESULTS) + + if table.page: + results.append({ + 'name': queryset.model._meta.verbose_name_plural, + 'table': table, + '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 diff --git a/netbox/project-static/css/base.css b/netbox/project-static/css/base.css index 11ea04b72..db37c3535 100644 --- a/netbox/project-static/css/base.css +++ b/netbox/project-static/css/base.css @@ -92,6 +92,9 @@ tfoot td { table.attr-table td:nth-child(1) { width: 25%; } +.table-headings th { + background-color: #f5f5f5; +} /* Paginator */ div.paginator { diff --git a/netbox/secrets/tables.py b/netbox/secrets/tables.py index 2fb6d9bbe..15e003d8f 100644 --- a/netbox/secrets/tables.py +++ b/netbox/secrets/tables.py @@ -1,7 +1,7 @@ import django_tables2 as tables from django_tables2.utils import Accessor -from utilities.tables import BaseTable, ToggleColumn +from utilities.tables import BaseTable, SearchTable, ToggleColumn from .models import SecretRole, Secret @@ -36,11 +36,15 @@ class SecretRoleTable(BaseTable): class SecretTable(BaseTable): pk = ToggleColumn() - device = tables.LinkColumn('secrets:secret', args=[Accessor('pk')], verbose_name='Device') - role = tables.Column(verbose_name='Role') - name = tables.Column(verbose_name='Name') - last_updated = tables.DateTimeColumn(verbose_name='Last updated') + device = tables.LinkColumn() class Meta(BaseTable.Meta): model = Secret fields = ('pk', 'device', 'role', 'name', 'last_updated') + + +class SecretSearchTable(SearchTable): + + class Meta(SearchTable.Meta): + model = Secret + fields = ('device', 'role', 'name', 'last_updated') diff --git a/netbox/secrets/views.py b/netbox/secrets/views.py index 8a1b35a6b..308e9de3d 100644 --- a/netbox/secrets/views.py +++ b/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 diff --git a/netbox/templates/home.html b/netbox/templates/home.html index 043526332..15965b13f 100644 --- a/netbox/templates/home.html +++ b/netbox/templates/home.html @@ -2,64 +2,7 @@ {% load render_table from django_tables2 %} {% block content %} - +{% include 'search_form.html' %}
diff --git a/netbox/templates/panel_table.html b/netbox/templates/panel_table.html index 0d44d7ea9..cb807eeb6 100644 --- a/netbox/templates/panel_table.html +++ b/netbox/templates/panel_table.html @@ -20,6 +20,7 @@ {% endblock %} {% block pagination %} - {% include 'paginator.html' %} + {% if not hide_paginator %} + {% include 'paginator.html' %} + {% endif %} {% endblock pagination %} - diff --git a/netbox/templates/search.html b/netbox/templates/search.html new file mode 100644 index 000000000..afd4293ca --- /dev/null +++ b/netbox/templates/search.html @@ -0,0 +1,65 @@ +{% extends '_base.html' %} +{% load helpers %} +{% load form_helpers %} + +{% block title %}Search{% endblock %} + +{% block content %} + {% if request.GET.q %} + {% include 'search_form.html' with search_form=form %} + {% if results %} +
+
+ {% for obj_type in results %} +

{{ obj_type.name|bettertitle }}

+ {% include 'panel_table.html' with table=obj_type.table hide_paginator=True %} + {% if obj_type.table.page.has_next %} + + + See all {{ obj_type.table.page.paginator.count }} results + + {% endif %} +
+ {% endfor %} +
+
+
+
+ Search Results +
+ +
+
+
+ {% else %} +

No results found

+ {% endif %} + {% else %} +
+
+
+
+
+ Search +
+
+ {% render_form form %} +
+ +
+
+
+
+ {% endif %} +{% endblock %} diff --git a/netbox/templates/search_form.html b/netbox/templates/search_form.html new file mode 100644 index 000000000..b60d879b7 --- /dev/null +++ b/netbox/templates/search_form.html @@ -0,0 +1,9 @@ +
+
+
+ {{ search_form.q }} + {{ search_form.obj_type }} + +
+
+
diff --git a/netbox/templates/table.html b/netbox/templates/table.html index 8782f0796..4792f8e68 100644 --- a/netbox/templates/table.html +++ b/netbox/templates/table.html @@ -4,5 +4,7 @@ {# Extends the stock django_tables2 template to provide custom formatting of the pagination controls #} {% block pagination %} - {% include 'paginator.html' %} + {% if not hide_paginator %} + {% include 'paginator.html' %} + {% endif %} {% endblock pagination %} diff --git a/netbox/tenancy/tables.py b/netbox/tenancy/tables.py index 0ce0d577c..bacb4c12f 100644 --- a/netbox/tenancy/tables.py +++ b/netbox/tenancy/tables.py @@ -1,7 +1,7 @@ import django_tables2 as tables from django_tables2.utils import Accessor -from utilities.tables import BaseTable, ToggleColumn +from utilities.tables import BaseTable, SearchTable, ToggleColumn from .models import Tenant, TenantGroup @@ -36,10 +36,15 @@ class TenantGroupTable(BaseTable): class TenantTable(BaseTable): pk = ToggleColumn() - name = tables.LinkColumn('tenancy:tenant', args=[Accessor('slug')], verbose_name='Name') - group = tables.Column(verbose_name='Group') - description = tables.Column(verbose_name='Description') + name = tables.LinkColumn() class Meta(BaseTable.Meta): model = Tenant fields = ('pk', 'name', 'group', 'description') + + +class TenantSearchTable(SearchTable): + + class Meta(SearchTable.Meta): + model = Tenant + fields = ('name', 'group', 'description') diff --git a/netbox/utilities/tables.py b/netbox/utilities/tables.py index a53227937..1c5eab2a6 100644 --- a/netbox/utilities/tables.py +++ b/netbox/utilities/tables.py @@ -4,7 +4,9 @@ from django.utils.safestring import mark_safe class BaseTable(tables.Table): - + """ + Default table for object lists + """ def __init__(self, *args, **kwargs): super(BaseTable, self).__init__(*args, **kwargs) @@ -18,6 +20,17 @@ class BaseTable(tables.Table): } +class SearchTable(tables.Table): + """ + Default table for search results + """ + class Meta: + attrs = { + 'class': 'table table-hover table-headings', + } + orderable = False + + class ToggleColumn(tables.CheckBoxColumn): def __init__(self, *args, **kwargs): diff --git a/netbox/utilities/templatetags/helpers.py b/netbox/utilities/templatetags/helpers.py index 164aa24b2..3c5770cb3 100644 --- a/netbox/utilities/templatetags/helpers.py +++ b/netbox/utilities/templatetags/helpers.py @@ -51,6 +51,13 @@ def startswith(value, arg): """ return str(value).startswith(arg) +@register.filter() +def bettertitle(value): + """ + Alternative to the builtin title(); uppercases words without replacing letters that are already uppercase. + """ + return ' '.join([w[0].upper() + w[1:] for w in value.split()]) + # # Tags