Przeglądaj źródła

Closes #7530: Move device type component lists to separate views

jeremystretch 4 lat temu
rodzic
commit
8c058dcd45

+ 1 - 0
docs/release-notes/version-3.1.md

@@ -24,6 +24,7 @@ When assigning a contact to an object, the user must select a predefined role (e
 * [#6715](https://github.com/netbox-community/netbox/issues/6715) - Add tenant assignment for cables
 * [#6874](https://github.com/netbox-community/netbox/issues/6874) - Add tenant assignment for locations
 * [#7354](https://github.com/netbox-community/netbox/issues/7354) - Relax uniqueness constraints on region, site group, and location names
+* [#7530](https://github.com/netbox-community/netbox/issues/7530) - Move device type component lists to separate views
 
 ### Other Changes
 

+ 110 - 0
netbox/dcim/tests/test_views.py

@@ -435,6 +435,116 @@ class DeviceTypeTestCase(
             'is_full_depth': False,
         }
 
+    @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
+    def test_devicetype_consoleports(self):
+        devicetype = DeviceType.objects.first()
+        console_ports = (
+            ConsolePortTemplate(device_type=devicetype, name='Console Port 1'),
+            ConsolePortTemplate(device_type=devicetype, name='Console Port 2'),
+            ConsolePortTemplate(device_type=devicetype, name='Console Port 3'),
+        )
+        ConsolePortTemplate.objects.bulk_create(console_ports)
+
+        url = reverse('dcim:devicetype_consoleports', kwargs={'pk': devicetype.pk})
+        self.assertHttpStatus(self.client.get(url), 200)
+
+    @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
+    def test_devicetype_consoleserverports(self):
+        devicetype = DeviceType.objects.first()
+        console_server_ports = (
+            ConsoleServerPortTemplate(device_type=devicetype, name='Console Server Port 1'),
+            ConsoleServerPortTemplate(device_type=devicetype, name='Console Server Port 2'),
+            ConsoleServerPortTemplate(device_type=devicetype, name='Console Server Port 3'),
+        )
+        ConsoleServerPortTemplate.objects.bulk_create(console_server_ports)
+
+        url = reverse('dcim:devicetype_consoleserverports', kwargs={'pk': devicetype.pk})
+        self.assertHttpStatus(self.client.get(url), 200)
+
+    @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
+    def test_devicetype_powerports(self):
+        devicetype = DeviceType.objects.first()
+        power_ports = (
+            PowerPortTemplate(device_type=devicetype, name='Power Port 1'),
+            PowerPortTemplate(device_type=devicetype, name='Power Port 2'),
+            PowerPortTemplate(device_type=devicetype, name='Power Port 3'),
+        )
+        PowerPortTemplate.objects.bulk_create(power_ports)
+
+        url = reverse('dcim:devicetype_powerports', kwargs={'pk': devicetype.pk})
+        self.assertHttpStatus(self.client.get(url), 200)
+
+    @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
+    def test_devicetype_poweroutlets(self):
+        devicetype = DeviceType.objects.first()
+        power_outlets = (
+            PowerOutletTemplate(device_type=devicetype, name='Power Outlet 1'),
+            PowerOutletTemplate(device_type=devicetype, name='Power Outlet 2'),
+            PowerOutletTemplate(device_type=devicetype, name='Power Outlet 3'),
+        )
+        PowerOutletTemplate.objects.bulk_create(power_outlets)
+
+        url = reverse('dcim:devicetype_poweroutlets', kwargs={'pk': devicetype.pk})
+        self.assertHttpStatus(self.client.get(url), 200)
+
+    @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
+    def test_devicetype_interfaces(self):
+        devicetype = DeviceType.objects.first()
+        interfaces = (
+            InterfaceTemplate(device_type=devicetype, name='Interface 1'),
+            InterfaceTemplate(device_type=devicetype, name='Interface 2'),
+            InterfaceTemplate(device_type=devicetype, name='Interface 3'),
+        )
+        InterfaceTemplate.objects.bulk_create(interfaces)
+
+        url = reverse('dcim:devicetype_interfaces', kwargs={'pk': devicetype.pk})
+        self.assertHttpStatus(self.client.get(url), 200)
+
+    @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
+    def test_devicetype_rearports(self):
+        devicetype = DeviceType.objects.first()
+        rear_ports = (
+            RearPortTemplate(device_type=devicetype, name='Rear Port 1'),
+            RearPortTemplate(device_type=devicetype, name='Rear Port 2'),
+            RearPortTemplate(device_type=devicetype, name='Rear Port 3'),
+        )
+        RearPortTemplate.objects.bulk_create(rear_ports)
+
+        url = reverse('dcim:devicetype_rearports', kwargs={'pk': devicetype.pk})
+        self.assertHttpStatus(self.client.get(url), 200)
+
+    @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
+    def test_devicetype_frontports(self):
+        devicetype = DeviceType.objects.first()
+        rear_ports = (
+            RearPortTemplate(device_type=devicetype, name='Rear Port 1'),
+            RearPortTemplate(device_type=devicetype, name='Rear Port 2'),
+            RearPortTemplate(device_type=devicetype, name='Rear Port 3'),
+        )
+        RearPortTemplate.objects.bulk_create(rear_ports)
+        front_ports = (
+            FrontPortTemplate(device_type=devicetype, name='Front Port 1', rear_port=rear_ports[0], rear_port_position=1),
+            FrontPortTemplate(device_type=devicetype, name='Front Port 2', rear_port=rear_ports[1], rear_port_position=1),
+            FrontPortTemplate(device_type=devicetype, name='Front Port 3', rear_port=rear_ports[2], rear_port_position=1),
+        )
+        FrontPortTemplate.objects.bulk_create(front_ports)
+
+        url = reverse('dcim:devicetype_frontports', kwargs={'pk': devicetype.pk})
+        self.assertHttpStatus(self.client.get(url), 200)
+
+    @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
+    def test_devicetype_devicebays(self):
+        devicetype = DeviceType.objects.first()
+        device_bays = (
+            DeviceBayTemplate(device_type=devicetype, name='Device Bay 1'),
+            DeviceBayTemplate(device_type=devicetype, name='Device Bay 2'),
+            DeviceBayTemplate(device_type=devicetype, name='Device Bay 3'),
+        )
+        DeviceBayTemplate.objects.bulk_create(device_bays)
+
+        url = reverse('dcim:devicetype_devicebays', kwargs={'pk': devicetype.pk})
+        self.assertHttpStatus(self.client.get(url), 200)
+
     @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
     def test_import_objects(self):
         """

+ 8 - 0
netbox/dcim/urls.py

@@ -109,6 +109,14 @@ urlpatterns = [
     path('device-types/edit/', views.DeviceTypeBulkEditView.as_view(), name='devicetype_bulk_edit'),
     path('device-types/delete/', views.DeviceTypeBulkDeleteView.as_view(), name='devicetype_bulk_delete'),
     path('device-types/<int:pk>/', views.DeviceTypeView.as_view(), name='devicetype'),
+    path('device-types/<int:pk>/console-ports/', views.DeviceTypeConsolePortsView.as_view(), name='devicetype_consoleports'),
+    path('device-types/<int:pk>/console-server-ports/', views.DeviceTypeConsoleServerPortsView.as_view(), name='devicetype_consoleserverports'),
+    path('device-types/<int:pk>/power-ports/', views.DeviceTypePowerPortsView.as_view(), name='devicetype_powerports'),
+    path('device-types/<int:pk>/power-outlets/', views.DeviceTypePowerOutletsView.as_view(), name='devicetype_poweroutlets'),
+    path('device-types/<int:pk>/interfaces/', views.DeviceTypeInterfacesView.as_view(), name='devicetype_interfaces'),
+    path('device-types/<int:pk>/front-ports/', views.DeviceTypeFrontPortsView.as_view(), name='devicetype_frontports'),
+    path('device-types/<int:pk>/rear-ports/', views.DeviceTypeRearPortsView.as_view(), name='devicetype_rearports'),
+    path('device-types/<int:pk>/device-bays/', views.DeviceTypeDeviceBaysView.as_view(), name='devicetype_devicebays'),
     path('device-types/<int:pk>/edit/', views.DeviceTypeEditView.as_view(), name='devicetype_edit'),
     path('device-types/<int:pk>/delete/', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'),
     path('device-types/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='devicetype_changelog', kwargs={'model': DeviceType}),

+ 50 - 52
netbox/dcim/views.py

@@ -54,11 +54,19 @@ class DeviceComponentsView(generic.ObjectView):
         paginate_table(table, request)
 
         return {
-            f'{self.model._meta.model_name}_table': table,
+            'table': table,
             'active_tab': f"{self.model._meta.verbose_name_plural.replace(' ', '-')}",
         }
 
 
+class DeviceTypeComponentsView(DeviceComponentsView):
+    queryset = DeviceType.objects.all()
+    template_name = 'dcim/devicetype/component_templates.html'
+
+    def get_components(self, request, instance):
+        return self.model.objects.restrict(request.user, 'view').filter(device_type=instance)
+
+
 class BulkDisconnectView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
     """
     An extendable view for disconnection console/power/interface components in bulk.
@@ -782,62 +790,52 @@ class DeviceTypeView(generic.ObjectView):
     def get_extra_context(self, request, instance):
         instance_count = Device.objects.restrict(request.user).filter(device_type=instance).count()
 
-        # Component tables
-        consoleport_table = tables.ConsolePortTemplateTable(
-            ConsolePortTemplate.objects.restrict(request.user, 'view').filter(device_type=instance),
-            orderable=False
-        )
-        consoleserverport_table = tables.ConsoleServerPortTemplateTable(
-            ConsoleServerPortTemplate.objects.restrict(request.user, 'view').filter(device_type=instance),
-            orderable=False
-        )
-        powerport_table = tables.PowerPortTemplateTable(
-            PowerPortTemplate.objects.restrict(request.user, 'view').filter(device_type=instance),
-            orderable=False
-        )
-        poweroutlet_table = tables.PowerOutletTemplateTable(
-            PowerOutletTemplate.objects.restrict(request.user, 'view').filter(device_type=instance),
-            orderable=False
-        )
-        interface_table = tables.InterfaceTemplateTable(
-            list(InterfaceTemplate.objects.restrict(request.user, 'view').filter(device_type=instance)),
-            orderable=False
-        )
-        front_port_table = tables.FrontPortTemplateTable(
-            FrontPortTemplate.objects.restrict(request.user, 'view').filter(device_type=instance),
-            orderable=False
-        )
-        rear_port_table = tables.RearPortTemplateTable(
-            RearPortTemplate.objects.restrict(request.user, 'view').filter(device_type=instance),
-            orderable=False
-        )
-        devicebay_table = tables.DeviceBayTemplateTable(
-            DeviceBayTemplate.objects.restrict(request.user, 'view').filter(device_type=instance),
-            orderable=False
-        )
-        if request.user.has_perm('dcim.change_devicetype'):
-            consoleport_table.columns.show('pk')
-            consoleserverport_table.columns.show('pk')
-            powerport_table.columns.show('pk')
-            poweroutlet_table.columns.show('pk')
-            interface_table.columns.show('pk')
-            front_port_table.columns.show('pk')
-            rear_port_table.columns.show('pk')
-            devicebay_table.columns.show('pk')
-
         return {
             'instance_count': instance_count,
-            'consoleport_table': consoleport_table,
-            'consoleserverport_table': consoleserverport_table,
-            'powerport_table': powerport_table,
-            'poweroutlet_table': poweroutlet_table,
-            'interface_table': interface_table,
-            'front_port_table': front_port_table,
-            'rear_port_table': rear_port_table,
-            'devicebay_table': devicebay_table,
+            'active_tab': 'devicetype',
         }
 
 
+class DeviceTypeConsolePortsView(DeviceTypeComponentsView):
+    model = ConsolePortTemplate
+    table = tables.ConsolePortTemplateTable
+
+
+class DeviceTypeConsoleServerPortsView(DeviceTypeComponentsView):
+    model = ConsoleServerPortTemplate
+    table = tables.ConsoleServerPortTemplateTable
+
+
+class DeviceTypePowerPortsView(DeviceTypeComponentsView):
+    model = PowerPortTemplate
+    table = tables.PowerPortTemplateTable
+
+
+class DeviceTypePowerOutletsView(DeviceTypeComponentsView):
+    model = PowerOutletTemplate
+    table = tables.PowerOutletTemplateTable
+
+
+class DeviceTypeInterfacesView(DeviceTypeComponentsView):
+    model = InterfaceTemplate
+    table = tables.InterfaceTemplateTable
+
+
+class DeviceTypeFrontPortsView(DeviceTypeComponentsView):
+    model = FrontPortTemplate
+    table = tables.FrontPortTemplateTable
+
+
+class DeviceTypeRearPortsView(DeviceTypeComponentsView):
+    model = RearPortTemplate
+    table = tables.RearPortTemplateTable
+
+
+class DeviceTypeDeviceBaysView(DeviceTypeComponentsView):
+    model = DeviceBayTemplate
+    table = tables.DeviceBayTemplateTable
+
+
 class DeviceTypeEditView(generic.ObjectEditView):
     queryset = DeviceType.objects.all()
     model_form = forms.DeviceTypeForm

+ 3 - 3
netbox/templates/dcim/device/consoleports.html

@@ -7,7 +7,7 @@
   <form method="post">
     {% csrf_token %}
     {% include 'inc/table_controls.html' with table_modal="DeviceConsolePortTable_config" %}
-    {% render_table consoleport_table 'inc/table.html' %}
+    {% render_table table 'inc/table.html' %}
     <div class="noprint bulk-buttons">
         <div class="bulk-button-group">
             {% if perms.dcim.change_consoleport %}
@@ -36,6 +36,6 @@
         {% endif %}
     </div>
   </form>
-  {% include 'inc/paginator.html' with paginator=consoleport_table.paginator page=consoleport_table.page %}
-  {% table_config_form consoleport_table %}
+  {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
+  {% table_config_form table %}
 {% endblock %}

+ 3 - 3
netbox/templates/dcim/device/consoleserverports.html

@@ -7,7 +7,7 @@
   <form method="post">
     {% csrf_token %}
     {% include 'inc/table_controls.html' with table_modal="DeviceConsoleServerPortTable_config" %}
-    {% render_table consoleserverport_table 'inc/table.html' %}
+    {% render_table table 'inc/table.html' %}
     <div class="noprint bulk-buttons">
         <div class="bulk-button-group">
             {% if perms.dcim.change_consoleserverport %}
@@ -36,6 +36,6 @@
         {% endif %}
     </div>
   </form>
-  {% include 'inc/paginator.html' with paginator=consoleserverport_table.paginator page=consoleserverport_table.page %}
-  {% table_config_form consoleserverport_table %}
+  {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
+  {% table_config_form table %}
 {% endblock %}

+ 3 - 3
netbox/templates/dcim/device/devicebays.html

@@ -7,7 +7,7 @@
   <form method="post">
     {% csrf_token %}
     {% include 'inc/table_controls.html' with table_modal="DeviceDeviceBayTable_config" %}
-    {% render_table devicebay_table 'inc/table.html' %}
+    {% render_table table 'inc/table.html' %}
     <div class="noprint bulk-buttons">
         <div class="bulk-button-group">
             {% if perms.dcim.change_devicebay %}
@@ -33,6 +33,6 @@
         {% endif %}
     </div>
   </form>
-  {% include 'inc/paginator.html' with paginator=devicebay_table.paginator page=devicebay_table.page %}
-  {% table_config_form devicebay_table %}
+  {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
+  {% table_config_form table %}
 {% endblock %}

+ 3 - 3
netbox/templates/dcim/device/frontports.html

@@ -7,7 +7,7 @@
   <form method="post">
     {% csrf_token %}
     {% include 'inc/table_controls.html' with table_modal="DeviceFrontPortTable_config" %}
-    {% render_table frontport_table 'inc/table.html' %}
+    {% render_table table 'inc/table.html' %}
     <div class="noprint bulk-buttons">
         <div class="bulk-button-group">
             {% if perms.dcim.change_frontport %}
@@ -36,6 +36,6 @@
         {% endif %}
     </div>
   </form>
-  {% include 'inc/paginator.html' with paginator=frontport_table.paginator page=frontport_table.page %}
-  {% table_config_form frontport_table %}
+  {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
+  {% table_config_form table %}
 {% endblock %}

+ 3 - 3
netbox/templates/dcim/device/interfaces.html

@@ -34,7 +34,7 @@
         </div>
       </div>
     </div>
-    {% render_table interface_table 'inc/table.html' %}
+    {% render_table table 'inc/table.html' %}
     <div class="noprint bulk-buttons">
         <div class="bulk-button-group">
         {% if perms.dcim.change_interface %}
@@ -63,6 +63,6 @@
         {% endif %}
     </div>
   </form>
-  {% include 'inc/paginator.html' with paginator=interface_table.paginator page=interface_table.page %}
-  {% table_config_form interface_table %}
+  {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
+  {% table_config_form table %}
 {% endblock %}

+ 3 - 3
netbox/templates/dcim/device/inventory.html

@@ -7,7 +7,7 @@
   <form method="post">
     {% csrf_token %}
     {% include 'inc/table_controls.html' with table_modal="DeviceInventoryItemTable_config" %}
-    {% render_table inventoryitem_table 'inc/table.html' %}
+    {% render_table table 'inc/table.html' %}
     <div class="noprint bulk-buttons">
         <div class="bulk-button-group">
             {% if perms.dcim.change_inventoryitem %}
@@ -33,6 +33,6 @@
         {% endif %}
     </div>
   </form>
-  {% include 'inc/paginator.html' with paginator=inventoryitem_table.paginator page=inventoryitem_table.page %}
-  {% table_config_form inventoryitem_table %}
+  {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
+  {% table_config_form table %}
 {% endblock %}

+ 3 - 3
netbox/templates/dcim/device/poweroutlets.html

@@ -7,7 +7,7 @@
   <form method="post">
     {% csrf_token %}
     {% include 'inc/table_controls.html' with table_modal="DevicePowerOutletTable_config" %}
-    {% render_table poweroutlet_table 'inc/table.html' %}
+    {% render_table table 'inc/table.html' %}
     <div class="noprint bulk-buttons">
         <div class="bulk-button-group">
             {% if perms.dcim.change_powerport %}
@@ -36,6 +36,6 @@
         {% endif %}
     </div>
   </form>
-  {% include 'inc/paginator.html' with paginator=poweroutlet_table.paginator page=poweroutlet_table.page %}
-  {% table_config_form poweroutlet_table %}
+  {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
+  {% table_config_form table %}
 {% endblock %}

+ 3 - 3
netbox/templates/dcim/device/powerports.html

@@ -7,7 +7,7 @@
   <form method="post">
     {% csrf_token %}
     {% include 'inc/table_controls.html' with table_modal="DevicePowerPortTable_config" %}
-    {% render_table powerport_table 'inc/table.html' %}
+    {% render_table table 'inc/table.html' %}
     <div class="noprint bulk-buttons">
         <div class="bulk-button-group">
             {% if perms.dcim.change_powerport %}
@@ -36,6 +36,6 @@
         {% endif %}
     </div>
   </form>
-  {% include 'inc/paginator.html' with paginator=powerport_table.paginator page=powerport_table.page %}
-  {% table_config_form powerport_table %}
+  {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
+  {% table_config_form table %}
 {% endblock %}

+ 3 - 3
netbox/templates/dcim/device/rearports.html

@@ -7,7 +7,7 @@
   <form method="post">
     {% csrf_token %}
     {% include 'inc/table_controls.html' with table_modal="DeviceRearPortTable_config" %}
-    {% render_table rearport_table 'inc/table.html' %}
+    {% render_table table 'inc/table.html' %}
     <div class="noprint bulk-buttons">
         <div class="bulk-button-group">
             {% if perms.dcim.change_rearport %}
@@ -36,6 +36,6 @@
         {% endif %}
     </div>
   </form>
-  {% include 'inc/paginator.html' with paginator=rearport_table.paginator page=rearport_table.page %}
-  {% table_config_form rearport_table %}
+  {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
+  {% table_config_form table %}
 {% endblock %}

+ 1 - 116
netbox/templates/dcim/devicetype.html

@@ -1,51 +1,8 @@
-{% extends 'generic/object.html' %}
+{% extends 'dcim/devicetype/base.html' %}
 {% load buttons %}
 {% load helpers %}
 {% load plugins %}
 
-{% block title %}{{ object.manufacturer }} {{ object.model }}{% endblock %}
-
-{% block breadcrumbs %}
-  {{ block.super }}
-  <li class="breadcrumb-item"><a href="{% url 'dcim:devicetype_list' %}?manufacturer_id={{ object.manufacturer.pk }}">{{ object.manufacturer }}</a></li>
-{% endblock %}
-
-{% block extra_controls %}
-  {% if perms.dcim.change_devicetype %}
-    <div class="dropdown">
-      <button type="button" class="btn btn-primary btn-sm dropdown-toggle"data-bs-toggle="dropdown" aria-expanded="false">
-        <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Components
-      </button>
-      <ul class="dropdown-menu">
-        {% if perms.dcim.add_consoleporttemplate %}
-          <li><a class="dropdown-item" href="{% url 'dcim:consoleporttemplate_add' %}?device_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_consoleports">Console Ports</a></li>
-        {% endif %}
-        {% if perms.dcim.add_consoleserverporttemplate %}
-          <li><a class="dropdown-item" href="{% url 'dcim:consoleserverporttemplate_add' %}?device_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_consoleserverports">Console Server Ports</a></li>
-        {% endif %}
-        {% if perms.dcim.add_powerporttemplate %}
-          <li><a class="dropdown-item" href="{% url 'dcim:powerporttemplate_add' %}?device_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_powerports">Power Ports</a></li>
-        {% endif %}
-        {% if perms.dcim.add_poweroutlettemplate %}
-          <li><a class="dropdown-item" href="{% url 'dcim:poweroutlettemplate_add' %}?device_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_poweroutlets">Power Outlets</a></li>
-        {% endif %}
-        {% if perms.dcim.add_interfacetemplate %}
-          <li><a class="dropdown-item" href="{% url 'dcim:interfacetemplate_add' %}?device_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_interfaces">Interfaces</a></li>
-        {% endif %}
-        {% if perms.dcim.add_frontporttemplate %}
-          <li><a class="dropdown-item" href="{% url 'dcim:frontporttemplate_add' %}?device_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_frontports">Front Ports</a></li>
-        {% endif %}
-        {% if perms.dcim.add_rearporttemplate %}
-          <li><a class="dropdown-item" href="{% url 'dcim:rearporttemplate_add' %}?device_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_rearports">Rear Ports</a></li>
-        {% endif %}
-        {% if perms.dcim.add_devicebaytemplate %}
-          <li><a class="dropdown-item" href="{% url 'dcim:devicebaytemplate_add' %}?device_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_devicebays">Device Bays</a></li>
-        {% endif %}
-      </ul>
-    </div>
-  {% endif %}
-{% endblock %}
-
 {% block content %}
     <div class="row">
         <div class="col col-md-6">
@@ -141,76 +98,4 @@
             {% plugin_full_width_page object %}
         </div>
     </div>
-    <div class="row my-3">
-        <div class="col col-md-12">
-            <ul class="nav nav-pills mb-1" role="tablist">
-                <li class="nav-item" role="presentation">
-                    <button class="nav-link active" data-bs-target="#interfaces" role="tab" data-bs-toggle="tab">
-                        Interfaces {% badge interface_table.rows|length %}
-                    </button>
-                </li>
-                <li class="nav-item" role="presentation">
-                    <button class="nav-link" data-bs-target="#frontports" role="tab" data-bs-toggle="tab">
-                        Front Ports {% badge front_port_table.rows|length %}
-                    </button>
-                </li>
-                <li class="nav-item" role="presentation">
-                    <button class="nav-link" data-bs-target="#rearports" role="tab" data-bs-toggle="tab">
-                        Rear Ports {% badge rear_port_table.rows|length %}
-                    </button>
-                </li>
-                <li class="nav-item" role="presentation">
-                    <button class="nav-link" data-bs-target="#consoleports" role="tab" data-bs-toggle="tab">
-                        Console Ports {% badge consoleport_table.rows|length %}
-                    </button>
-                </li>
-                <li class="nav-item" role="presentation">
-                    <button class="nav-link" data-bs-target="#consoleserverports" role="tab" data-bs-toggle="tab">
-                        Console Server Ports {% badge consoleserverport_table.rows|length %}
-                    </button>
-                </li>
-                <li class="nav-item" role="presentation">
-                    <button class="nav-link" data-bs-target="#powerports" role="tab" data-bs-toggle="tab">
-                        Power Ports {% badge powerport_table.rows|length %}
-                    </button>
-                </li>
-                <li class="nav-item" role="presentation">
-                    <button class="nav-link" data-bs-target="#poweroutlets" role="tab" data-bs-toggle="tab">
-                        Power Outlets {% badge poweroutlet_table.rows|length %}
-                    </button>
-                </li>
-                <li class="nav-item" role="presentation">
-                    <button class="nav-link" data-bs-target="#devicebays" role="tab" data-bs-toggle="tab">
-                        Device Bays {% badge devicebay_table.rows|length %}
-                    </button>
-                </li>
-            </ul>
-            <div class="tab-content p-0">
-                <div role="tabpanel" class="tab-pane active" id="interfaces">
-                    {% include 'dcim/inc/devicetype_component_table.html' with table=interface_table title='Interfaces' tab='interfaces' %}
-                </div>
-                <div role="tabpanel" class="tab-pane" id="frontports">
-                    {% include 'dcim/inc/devicetype_component_table.html' with table=front_port_table title='Front Ports' tab='frontports' %}
-                </div>
-                <div role="tabpanel" class="tab-pane" id="rearports">
-                    {% include 'dcim/inc/devicetype_component_table.html' with table=rear_port_table title='Rear Ports' tab='rearports' %}
-                </div>
-                <div role="tabpanel" class="tab-pane" id="consoleports">
-                    {% include 'dcim/inc/devicetype_component_table.html' with table=consoleport_table title='Console Ports' tab='consoleports' %}
-                </div>
-                <div role="tabpanel" class="tab-pane" id="consoleserverports">
-                    {% include 'dcim/inc/devicetype_component_table.html' with table=consoleserverport_table title='Console Server Ports' tab='consoleserverports' %}
-                </div>
-                <div role="tabpanel" class="tab-pane" id="powerports">
-                    {% include 'dcim/inc/devicetype_component_table.html' with table=powerport_table title='Power Ports' tab='powerports' %}
-                </div>
-                <div role="tabpanel" class="tab-pane" id="poweroutlets">
-                    {% include 'dcim/inc/devicetype_component_table.html' with table=poweroutlet_table title='Power Outlets' tab='poweroutlets' %}
-                </div>
-                <div role="tabpanel" class="tab-pane" id="devicebays">
-                    {% include 'dcim/inc/devicetype_component_table.html' with table=devicebay_table title='Device Bays' tab='devicebays' %}
-                </div>
-            </div>
-        </div>
-    </div>
 {% endblock %}

+ 119 - 0
netbox/templates/dcim/devicetype/base.html

@@ -0,0 +1,119 @@
+{% extends 'generic/object.html' %}
+{% load buttons %}
+{% load helpers %}
+{% load plugins %}
+
+{% block title %}{{ object.manufacturer }} {{ object.model }}{% endblock %}
+
+{% block breadcrumbs %}
+  {{ block.super }}
+  <li class="breadcrumb-item"><a href="{% url 'dcim:devicetype_list' %}?manufacturer_id={{ object.manufacturer.pk }}">{{ object.manufacturer }}</a></li>
+{% endblock %}
+
+{% block extra_controls %}
+  {% if perms.dcim.change_devicetype %}
+    <div class="dropdown">
+      <button type="button" class="btn btn-primary btn-sm dropdown-toggle"data-bs-toggle="dropdown" aria-expanded="false">
+        <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Components
+      </button>
+      <ul class="dropdown-menu">
+        {% if perms.dcim.add_consoleporttemplate %}
+          <li><a class="dropdown-item" href="{% url 'dcim:consoleporttemplate_add' %}?device_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_consoleports">Console Ports</a></li>
+        {% endif %}
+        {% if perms.dcim.add_consoleserverporttemplate %}
+          <li><a class="dropdown-item" href="{% url 'dcim:consoleserverporttemplate_add' %}?device_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_consoleserverports">Console Server Ports</a></li>
+        {% endif %}
+        {% if perms.dcim.add_powerporttemplate %}
+          <li><a class="dropdown-item" href="{% url 'dcim:powerporttemplate_add' %}?device_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_powerports">Power Ports</a></li>
+        {% endif %}
+        {% if perms.dcim.add_poweroutlettemplate %}
+          <li><a class="dropdown-item" href="{% url 'dcim:poweroutlettemplate_add' %}?device_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_poweroutlets">Power Outlets</a></li>
+        {% endif %}
+        {% if perms.dcim.add_interfacetemplate %}
+          <li><a class="dropdown-item" href="{% url 'dcim:interfacetemplate_add' %}?device_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_interfaces">Interfaces</a></li>
+        {% endif %}
+        {% if perms.dcim.add_frontporttemplate %}
+          <li><a class="dropdown-item" href="{% url 'dcim:frontporttemplate_add' %}?device_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_frontports">Front Ports</a></li>
+        {% endif %}
+        {% if perms.dcim.add_rearporttemplate %}
+          <li><a class="dropdown-item" href="{% url 'dcim:rearporttemplate_add' %}?device_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_rearports">Rear Ports</a></li>
+        {% endif %}
+        {% if perms.dcim.add_devicebaytemplate %}
+          <li><a class="dropdown-item" href="{% url 'dcim:devicebaytemplate_add' %}?device_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_devicebays">Device Bays</a></li>
+        {% endif %}
+      </ul>
+    </div>
+  {% endif %}
+{% endblock %}
+
+{% block tab_items %}
+    <li role="presentation" class="nav-item">
+        <a href="{% url 'dcim:devicetype' pk=object.pk %}" class="nav-link{% if active_tab == 'devicetype' %} active{% endif %}">
+            Device Type
+        </a>
+    </li>
+
+    {% with interface_count=object.interfacetemplates.count %}
+        {% if interface_count %}
+            <li role="presentation" class="nav-item">
+                <a class="nav-link {% if active_tab == 'interface-templates' %} active{% endif %}" href="{% url 'dcim:devicetype_interfaces' pk=object.pk %}">Interfaces {% badge interface_count %}</a>
+            </li>
+        {% endif %}
+    {% endwith %}
+
+    {% with frontport_count=object.frontporttemplates.count %}
+        {% if frontport_count %}
+            <li role="presentation" class="nav-item">
+                <a class="nav-link {% if active_tab == 'front-port-templates' %} active{% endif %}" href="{% url 'dcim:devicetype_frontports' pk=object.pk %}">Front Ports {% badge frontport_count %}</a>
+            </li>
+        {% endif %}
+    {% endwith %}
+
+    {% with rearport_count=object.rearporttemplates.count %}
+        {% if rearport_count %}
+            <li role="presentation" class="nav-item">
+                <a class="nav-link {% if active_tab == 'rear-port-templates' %} active{% endif %}" href="{% url 'dcim:devicetype_rearports' pk=object.pk %}">Rear Ports {% badge rearport_count %}</a>
+            </li>
+        {% endif %}
+    {% endwith %}
+
+    {% with consoleport_count=object.consoleporttemplates.count %}
+        {% if consoleport_count %}
+            <li role="presentation" class="nav-item">
+                <a class="nav-link {% if active_tab == 'console-port-templates' %} active{% endif %}" href="{% url 'dcim:devicetype_consoleports' pk=object.pk %}">Console Ports {% badge consoleport_count %}</a>
+            </li>
+        {% endif %}
+    {% endwith %}
+
+    {% with consoleserverport_count=object.consoleserverporttemplates.count %}
+        {% if consoleserverport_count %}
+            <li role="presentation" class="nav-item">
+                <a class="nav-link {% if active_tab == 'console-server-port-templates' %} active{% endif %}" href="{% url 'dcim:devicetype_consoleserverports' pk=object.pk %}">Console Server Ports {% badge consoleserverport_count %}</a>
+            </li>
+        {% endif %}
+    {% endwith %}
+
+    {% with powerport_count=object.powerporttemplates.count %}
+        {% if powerport_count %}
+            <li role="presentation" class="nav-item">
+                <a class="nav-link {% if active_tab == 'power-port-templates' %} active{% endif %}" href="{% url 'dcim:devicetype_powerports' pk=object.pk %}">Power Ports {% badge powerport_count %}</a>
+            </li>
+        {% endif %}
+    {% endwith %}
+
+    {% with poweroutlet_count=object.poweroutlettemplates.count %}
+        {% if poweroutlet_count %}
+            <li role="presentation" class="nav-item">
+                <a class="nav-link {% if active_tab == 'power-outlet-templates' %} active{% endif %}" href="{% url 'dcim:devicetype_poweroutlets' pk=object.pk %}">Power Outlets {% badge poweroutlet_count %}</a>
+            </li>
+        {% endif %}
+    {% endwith %}
+
+    {% with devicebay_count=object.devicebaytemplates.count %}
+        {% if devicebay_count %}
+            <li role="presentation" class="nav-item">
+                <a class="nav-link {% if active_tab == 'device-bay-templates' %} active{% endif %}" href="{% url 'dcim:devicetype_devicebays' pk=object.pk %}">Device Bays {% badge devicebay_count %}</a>
+            </li>
+        {% endif %}
+    {% endwith %}
+{% endblock %}

+ 7 - 4
netbox/templates/dcim/inc/devicetype_component_table.html → netbox/templates/dcim/devicetype/component_templates.html

@@ -1,7 +1,9 @@
-{% load helpers %}
+{% extends 'dcim/devicetype/base.html' %}
 {% load render_table from django_tables2 %}
+{% load helpers %}
 
-{% if perms.dcim.change_devicetype %}
+{% block content %}
+  {% if perms.dcim.change_devicetype %}
     <form method="post">
         {% csrf_token %}
         <div class="card">
@@ -33,7 +35,7 @@
             </div>
         </div>
     </form>
-{% else %}
+  {% else %}
     <div class="card">
         <h5 class="card-header">
             {{ title }}
@@ -42,4 +44,5 @@
             {% render_table table 'inc/table.html' %}
         </div>
     </div>
-{% endif %}
+  {% endif %}
+{% endblock content %}

+ 0 - 42
netbox/templates/dcim/inc/device_component_table.html

@@ -1,42 +0,0 @@
-{% load helpers %}
-{% load perms %}
-<form method="post">
-    {% csrf_token %}
-    <div class="card">
-        <h5 class="card-header">
-            {{ title }}
-        </h5>
-        <div class="card-body">
-            <table class="table table-hover component-list">
-                {% for obj in components %}
-                    {% include component_template %}
-                {% endfor %}
-            </table>
-        </div>
-        {% if components and perms.dcim.change_consoleport %}
-            <div class="card-footer noprint">
-                <button type="submit" name="_rename" formaction="{% url 'dcim:consoleport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-sm">
-                    <span class="mdi mdi-pencil" aria-hidden="true"></span> Rename
-                </button>
-                <button type="submit" name="_edit" formaction="{% url 'dcim:consoleport_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-sm">
-                    <span class="mdi mdi-pencil" aria-hidden="true"></span> Edit
-                </button>
-                <button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-sm">
-                    <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
-                </button>
-            {% endif %}
-            {% if components and perms.dcim.delete_consoleport %}
-                <button type="submit" name="_delete" formaction="{% url 'dcim:consoleport_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-sm">
-                    <span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> Delete
-                </button>
-            {% endif %}
-            {% if components and perms.dcim.add_consoleport %}
-                <div class="float-end">
-                    <a href="{% url 'dcim:consoleport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-sm btn-primary">
-                        <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add console port
-                    </a>
-                </div>
-            </div>
-        {% endif %}
-    </div>
-</form>