Ver Fonte

#20923: Migrate IPAM views to declarative layouts (#21695)

* #20923: Migrate IPAM views to declarative layouts

* #20923: Migrate IPAM views to declarative layouts

* fix VRF view

* fix Route Target view

* fix addressing details modal

* fix add prefix button

* fix add aggregate button

* fix add VLAN button

* fix breadcrumb on Application Service

* fix breadcrumb on ANS

* move attrs to separate file

* review feedback

* review feedback

* review feedback

* review feedback
Arthur Hanson há 1 dia atrás
pai
commit
99e9d96787
32 ficheiros alterados com 683 adições e 1316 exclusões
  1. 10 0
      netbox/ipam/models/ip.py
  2. 24 0
      netbox/ipam/ui/attrs.py
  3. 220 2
      netbox/ipam/ui/panels.py
  4. 306 44
      netbox/ipam/views.py
  5. 0 61
      netbox/templates/ipam/aggregate.html
  6. 2 0
      netbox/templates/ipam/aggregate/attrs/utilization.html
  7. 0 52
      netbox/templates/ipam/asn.html
  8. 0 56
      netbox/templates/ipam/asnrange.html
  9. 2 0
      netbox/templates/ipam/attrs/vrf.html
  10. 0 75
      netbox/templates/ipam/fhrpgroup.html
  11. 1 129
      netbox/templates/ipam/ipaddress.html
  12. 2 0
      netbox/templates/ipam/ipaddress/attrs/assigned_object.html
  13. 2 0
      netbox/templates/ipam/ipaddress/attrs/nat_inside.html
  14. 2 0
      netbox/templates/ipam/ipaddress/attrs/nat_outside.html
  15. 0 97
      netbox/templates/ipam/iprange.html
  16. 6 0
      netbox/templates/ipam/iprange/attrs/utilization.html
  17. 62 0
      netbox/templates/ipam/panels/prefix_addressing.html
  18. 0 201
      netbox/templates/ipam/prefix.html
  19. 2 0
      netbox/templates/ipam/prefix/attrs/aggregate.html
  20. 37 0
      netbox/templates/ipam/prefix/base.html
  21. 0 40
      netbox/templates/ipam/rir.html
  22. 0 40
      netbox/templates/ipam/role.html
  23. 0 67
      netbox/templates/ipam/routetarget.html
  24. 1 59
      netbox/templates/ipam/service.html
  25. 2 0
      netbox/templates/ipam/service/attrs/ip_addresses.html
  26. 0 45
      netbox/templates/ipam/servicetemplate.html
  27. 0 128
      netbox/templates/ipam/vlan.html
  28. 1 52
      netbox/templates/ipam/vlangroup.html
  29. 1 0
      netbox/templates/ipam/vlangroup/attrs/vid_ranges.html
  30. 0 64
      netbox/templates/ipam/vlantranslationpolicy.html
  31. 0 44
      netbox/templates/ipam/vlantranslationrule.html
  32. 0 60
      netbox/templates/ipam/vrf.html

+ 10 - 0
netbox/ipam/models/ip.py

@@ -367,6 +367,16 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, CachedScopeMixin, Primary
     def get_status_color(self):
         return PrefixStatusChoices.colors.get(self.status)
 
+    @cached_property
+    def aggregate(self):
+        """
+        Return the containing Aggregate for this Prefix, if any.
+        """
+        try:
+            return Aggregate.objects.get(prefix__net_contains_or_equals=str(self.prefix))
+        except Aggregate.DoesNotExist:
+            return None
+
     def get_parents(self, include_self=False):
         """
         Return all containing Prefixes in the hierarchy.

+ 24 - 0
netbox/ipam/ui/attrs.py

@@ -0,0 +1,24 @@
+from django.template.loader import render_to_string
+
+from netbox.ui import attrs
+
+
+class VRFDisplayAttr(attrs.ObjectAttribute):
+    """
+    Renders a VRF reference, displaying 'Global' when no VRF is assigned. Optionally includes
+    the route distinguisher (RD).
+    """
+    template_name = 'ipam/attrs/vrf.html'
+
+    def __init__(self, *args, show_rd=False, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.show_rd = show_rd
+
+    def render(self, obj, context):
+        value = self.get_value(obj)
+        return render_to_string(self.template_name, {
+            **self.get_context(obj, context),
+            'name': context['name'],
+            'value': value,
+            'show_rd': self.show_rd,
+        })

+ 220 - 2
netbox/ipam/ui/panels.py

@@ -2,14 +2,15 @@ from django.contrib.contenttypes.models import ContentType
 from django.urls import reverse
 from django.utils.translation import gettext_lazy as _
 
-from netbox.ui import actions, panels
+from netbox.ui import actions, attrs, panels
+
+from .attrs import VRFDisplayAttr
 
 
 class FHRPGroupAssignmentsPanel(panels.ObjectPanel):
     """
     A panel which lists all FHRP group assignments for a given object.
     """
-
     template_name = 'ipam/panels/fhrp_groups.html'
     title = _('FHRP Groups')
     actions = [
@@ -35,3 +36,220 @@ class FHRPGroupAssignmentsPanel(panels.ObjectPanel):
             label=_('Assign Group'),
         ),
     ]
+
+
+class VRFPanel(panels.ObjectAttributesPanel):
+    rd = attrs.TextAttr('rd', label=_('Route Distinguisher'), style='font-monospace')
+    tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
+    enforce_unique = attrs.BooleanAttr('enforce_unique', label=_('Unique IP Space'))
+    description = attrs.TextAttr('description')
+
+
+class RouteTargetPanel(panels.ObjectAttributesPanel):
+    name = attrs.TextAttr('name', style='font-monospace')
+    tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
+    description = attrs.TextAttr('description')
+
+
+class RIRPanel(panels.OrganizationalObjectPanel):
+    is_private = attrs.BooleanAttr('is_private', label=_('Private'))
+
+
+class ASNRangePanel(panels.ObjectAttributesPanel):
+    name = attrs.TextAttr('name')
+    rir = attrs.RelatedObjectAttr('rir', linkify=True, label=_('RIR'))
+    range = attrs.TextAttr('range_as_string_with_asdot', label=_('Range'))
+    tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
+    description = attrs.TextAttr('description')
+
+
+class ASNPanel(panels.ObjectAttributesPanel):
+    asn = attrs.TextAttr('asn_with_asdot', label=_('AS Number'))
+    rir = attrs.RelatedObjectAttr('rir', linkify=True, label=_('RIR'))
+    tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
+    description = attrs.TextAttr('description')
+
+
+class AggregatePanel(panels.ObjectAttributesPanel):
+    family = attrs.TextAttr('family', format_string='IPv{}', label=_('Family'))
+    rir = attrs.RelatedObjectAttr('rir', linkify=True, label=_('RIR'))
+    utilization = attrs.TemplatedAttr(
+        'prefix',
+        template_name='ipam/aggregate/attrs/utilization.html',
+        label=_('Utilization'),
+    )
+    tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
+    date_added = attrs.DateTimeAttr('date_added', spec='date', label=_('Date Added'))
+    description = attrs.TextAttr('description')
+
+
+class RolePanel(panels.OrganizationalObjectPanel):
+    weight = attrs.NumericAttr('weight')
+
+
+class IPRangePanel(panels.ObjectAttributesPanel):
+    family = attrs.TextAttr('family', format_string='IPv{}', label=_('Family'))
+    start_address = attrs.TextAttr('start_address', label=_('Starting Address'))
+    end_address = attrs.TextAttr('end_address', label=_('Ending Address'))
+    size = attrs.NumericAttr('size')
+    mark_populated = attrs.BooleanAttr('mark_populated', label=_('Marked Populated'))
+    mark_utilized = attrs.BooleanAttr('mark_utilized', label=_('Marked Utilized'))
+    utilization = attrs.TemplatedAttr(
+        'utilization',
+        template_name='ipam/iprange/attrs/utilization.html',
+        label=_('Utilization'),
+    )
+    vrf = VRFDisplayAttr('vrf', label=_('VRF'), show_rd=True)
+    role = attrs.RelatedObjectAttr('role', linkify=True)
+    status = attrs.ChoiceAttr('status')
+    tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
+    description = attrs.TextAttr('description')
+
+
+class IPAddressPanel(panels.ObjectAttributesPanel):
+    family = attrs.TextAttr('family', format_string='IPv{}', label=_('Family'))
+    vrf = VRFDisplayAttr('vrf', label=_('VRF'))
+    tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
+    status = attrs.ChoiceAttr('status')
+    role = attrs.ChoiceAttr('role')
+    dns_name = attrs.TextAttr('dns_name', label=_('DNS Name'))
+    description = attrs.TextAttr('description')
+    assigned_object = attrs.RelatedObjectAttr(
+        'assigned_object',
+        linkify=True,
+        grouped_by='parent_object',
+        label=_('Assignment'),
+    )
+    nat_inside = attrs.TemplatedAttr(
+        'nat_inside',
+        template_name='ipam/ipaddress/attrs/nat_inside.html',
+        label=_('NAT (inside)'),
+    )
+    nat_outside = attrs.TemplatedAttr(
+        'nat_outside',
+        template_name='ipam/ipaddress/attrs/nat_outside.html',
+        label=_('NAT (outside)'),
+    )
+    is_primary_ip = attrs.BooleanAttr('is_primary_ip', label=_('Primary IP'))
+    is_oob_ip = attrs.BooleanAttr('is_oob_ip', label=_('OOB IP'))
+
+
+class PrefixPanel(panels.ObjectAttributesPanel):
+    family = attrs.TextAttr('family', format_string='IPv{}', label=_('Family'))
+    vrf = VRFDisplayAttr('vrf', label=_('VRF'))
+    tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
+    aggregate = attrs.TemplatedAttr(
+        'aggregate',
+        template_name='ipam/prefix/attrs/aggregate.html',
+        label=_('Aggregate'),
+    )
+    scope = attrs.GenericForeignKeyAttr('scope', linkify=True)
+    vlan = attrs.RelatedObjectAttr('vlan', linkify=True, label=_('VLAN'), grouped_by='group')
+    status = attrs.ChoiceAttr('status')
+    role = attrs.RelatedObjectAttr('role', linkify=True)
+    description = attrs.TextAttr('description')
+    is_pool = attrs.BooleanAttr('is_pool', label=_('Is a pool'))
+
+
+class VLANGroupPanel(panels.ObjectAttributesPanel):
+    name = attrs.TextAttr('name')
+    description = attrs.TextAttr('description')
+    scope = attrs.GenericForeignKeyAttr('scope', linkify=True)
+    vid_ranges = attrs.TemplatedAttr(
+        'vid_ranges_items',
+        template_name='ipam/vlangroup/attrs/vid_ranges.html',
+        label=_('VLAN IDs'),
+    )
+    utilization = attrs.UtilizationAttr('utilization')
+    tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
+
+
+class VLANTranslationPolicyPanel(panels.ObjectAttributesPanel):
+    name = attrs.TextAttr('name')
+    description = attrs.TextAttr('description')
+
+
+class VLANTranslationRulePanel(panels.ObjectAttributesPanel):
+    policy = attrs.RelatedObjectAttr('policy', linkify=True)
+    local_vid = attrs.NumericAttr('local_vid', label=_('Local VID'))
+    remote_vid = attrs.NumericAttr('remote_vid', label=_('Remote VID'))
+    description = attrs.TextAttr('description')
+
+
+class FHRPGroupPanel(panels.ObjectAttributesPanel):
+    protocol = attrs.ChoiceAttr('protocol')
+    group_id = attrs.NumericAttr('group_id', label=_('Group ID'))
+    name = attrs.TextAttr('name')
+    description = attrs.TextAttr('description')
+    member_count = attrs.NumericAttr('member_count', label=_('Members'))
+
+
+class FHRPGroupAuthPanel(panels.ObjectAttributesPanel):
+    title = _('Authentication')
+
+    auth_type = attrs.ChoiceAttr('auth_type', label=_('Authentication Type'))
+    auth_key = attrs.TextAttr('auth_key', label=_('Authentication Key'))
+
+
+class VLANPanel(panels.ObjectAttributesPanel):
+    region = attrs.NestedObjectAttr('site.region', linkify=True, label=_('Region'))
+    site = attrs.RelatedObjectAttr('site', linkify=True)
+    group = attrs.RelatedObjectAttr('group', linkify=True)
+    vid = attrs.NumericAttr('vid', label=_('VLAN ID'))
+    name = attrs.TextAttr('name')
+    tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
+    status = attrs.ChoiceAttr('status')
+    role = attrs.RelatedObjectAttr('role', linkify=True)
+    description = attrs.TextAttr('description')
+    qinq_role = attrs.ChoiceAttr('qinq_role', label=_('Q-in-Q Role'))
+    qinq_svlan = attrs.RelatedObjectAttr('qinq_svlan', linkify=True, label=_('Q-in-Q SVLAN'))
+    l2vpn = attrs.RelatedObjectAttr('l2vpn_termination.l2vpn', linkify=True, label=_('L2VPN'))
+
+
+class VLANCustomerVLANsPanel(panels.ObjectsTablePanel):
+    """
+    A panel listing customer VLANs (C-VLANs) for an S-VLAN. Only renders when the VLAN has Q-in-Q
+    role 'svlan'.
+    """
+    def __init__(self):
+        super().__init__(
+            'ipam.vlan',
+            filters={'qinq_svlan_id': lambda ctx: ctx['object'].pk},
+            title=_('Customer VLANs'),
+            actions=[
+                actions.AddObject(
+                    'ipam.vlan',
+                    url_params={
+                        'qinq_role': 'cvlan',
+                        'qinq_svlan': lambda ctx: ctx['object'].pk,
+                    },
+                    label=_('Add a VLAN'),
+                ),
+            ],
+        )
+
+    def render(self, context):
+        obj = context.get('object')
+        if not obj or obj.qinq_role != 'svlan':
+            return ''
+        return super().render(context)
+
+
+class ServiceTemplatePanel(panels.ObjectAttributesPanel):
+    name = attrs.TextAttr('name')
+    protocol = attrs.ChoiceAttr('protocol')
+    ports = attrs.TextAttr('port_list', label=_('Ports'))
+    description = attrs.TextAttr('description')
+
+
+class ServicePanel(panels.ObjectAttributesPanel):
+    name = attrs.TextAttr('name')
+    parent = attrs.RelatedObjectAttr('parent', linkify=True)
+    protocol = attrs.ChoiceAttr('protocol')
+    ports = attrs.TextAttr('port_list', label=_('Ports'))
+    ip_addresses = attrs.TemplatedAttr(
+        'ipaddresses',
+        template_name='ipam/service/attrs/ip_addresses.html',
+        label=_('IP Addresses'),
+    )
+    description = attrs.TextAttr('description')

+ 306 - 44
netbox/ipam/views.py

@@ -9,8 +9,16 @@ from circuits.models import Provider
 from dcim.filtersets import InterfaceFilterSet
 from dcim.forms import InterfaceFilterForm
 from dcim.models import Device, Interface, Site
-from ipam.tables import VLANTranslationRuleTable
+from extras.ui.panels import CustomFieldsPanel, TagsPanel
 from netbox.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport
+from netbox.ui import actions, layout
+from netbox.ui.panels import (
+    CommentsPanel,
+    ContextTablePanel,
+    ObjectsTablePanel,
+    RelatedObjectsPanel,
+    TemplatePanel,
+)
 from netbox.views import generic
 from utilities.query import count_related
 from utilities.tables import get_table_ordering
@@ -23,6 +31,7 @@ from . import filtersets, forms, tables
 from .choices import PrefixStatusChoices
 from .constants import *
 from .models import *
+from .ui import panels
 from .utils import add_available_vlans, add_requested_prefixes, annotate_ip_space
 
 #
@@ -41,6 +50,27 @@ class VRFListView(generic.ObjectListView):
 @register_model_view(VRF)
 class VRFView(GetRelatedModelsMixin, generic.ObjectView):
     queryset = VRF.objects.all()
+    layout = layout.Layout(
+        layout.Row(
+            layout.Column(
+                panels.VRFPanel(),
+                TagsPanel(),
+            ),
+            layout.Column(
+                RelatedObjectsPanel(),
+                CustomFieldsPanel(),
+                CommentsPanel(),
+            ),
+        ),
+        layout.Row(
+            layout.Column(
+                ContextTablePanel('import_targets_table', title=_('Import route targets')),
+            ),
+            layout.Column(
+                ContextTablePanel('export_targets_table', title=_('Export route targets')),
+            ),
+        ),
+    )
 
     def get_extra_context(self, request, instance):
         import_targets_table = tables.RouteTargetTable(
@@ -134,6 +164,50 @@ class RouteTargetListView(generic.ObjectListView):
 @register_model_view(RouteTarget)
 class RouteTargetView(generic.ObjectView):
     queryset = RouteTarget.objects.all()
+    layout = layout.Layout(
+        layout.Row(
+            layout.Column(
+                panels.RouteTargetPanel(),
+                TagsPanel(),
+            ),
+            layout.Column(
+                CustomFieldsPanel(),
+                CommentsPanel(),
+            ),
+        ),
+        layout.Row(
+            layout.Column(
+                ObjectsTablePanel(
+                    'ipam.vrf',
+                    filters={'import_target_id': lambda ctx: ctx['object'].pk},
+                    title=_('Importing VRFs'),
+                ),
+            ),
+            layout.Column(
+                ObjectsTablePanel(
+                    'ipam.vrf',
+                    filters={'export_target_id': lambda ctx: ctx['object'].pk},
+                    title=_('Exporting VRFs'),
+                ),
+            ),
+        ),
+        layout.Row(
+            layout.Column(
+                ObjectsTablePanel(
+                    'vpn.l2vpn',
+                    filters={'import_target_id': lambda ctx: ctx['object'].pk},
+                    title=_('Importing L2VPNs'),
+                ),
+            ),
+            layout.Column(
+                ObjectsTablePanel(
+                    'vpn.l2vpn',
+                    filters={'export_target_id': lambda ctx: ctx['object'].pk},
+                    title=_('Exporting L2VPNs'),
+                ),
+            ),
+        ),
+    )
 
 
 @register_model_view(RouteTarget, 'add', detail=False)
@@ -192,6 +266,17 @@ class RIRListView(generic.ObjectListView):
 @register_model_view(RIR)
 class RIRView(GetRelatedModelsMixin, generic.ObjectView):
     queryset = RIR.objects.all()
+    layout = layout.SimpleLayout(
+        left_panels=[
+            panels.RIRPanel(),
+            TagsPanel(),
+        ],
+        right_panels=[
+            RelatedObjectsPanel(),
+            CommentsPanel(),
+            CustomFieldsPanel(),
+        ],
+    )
 
     def get_extra_context(self, request, instance):
         return {
@@ -257,6 +342,16 @@ class ASNRangeListView(generic.ObjectListView):
 @register_model_view(ASNRange)
 class ASNRangeView(generic.ObjectView):
     queryset = ASNRange.objects.all()
+    layout = layout.SimpleLayout(
+        left_panels=[
+            panels.ASNRangePanel(),
+            TagsPanel(),
+        ],
+        right_panels=[
+            CommentsPanel(),
+            CustomFieldsPanel(),
+        ],
+    )
 
 
 @register_model_view(ASNRange, 'asns')
@@ -337,6 +432,17 @@ class ASNListView(generic.ObjectListView):
 @register_model_view(ASN)
 class ASNView(GetRelatedModelsMixin, generic.ObjectView):
     queryset = ASN.objects.all()
+    layout = layout.SimpleLayout(
+        left_panels=[
+            panels.ASNPanel(),
+            TagsPanel(),
+        ],
+        right_panels=[
+            RelatedObjectsPanel(),
+            CustomFieldsPanel(),
+            CommentsPanel(),
+        ],
+    )
 
     def get_extra_context(self, request, instance):
         return {
@@ -412,6 +518,16 @@ class AggregateListView(generic.ObjectListView):
 @register_model_view(Aggregate)
 class AggregateView(generic.ObjectView):
     queryset = Aggregate.objects.all()
+    layout = layout.SimpleLayout(
+        left_panels=[
+            panels.AggregatePanel(),
+        ],
+        right_panels=[
+            CustomFieldsPanel(),
+            TagsPanel(),
+            CommentsPanel(),
+        ],
+    )
 
 
 @register_model_view(Aggregate, 'prefixes')
@@ -506,6 +622,17 @@ class RoleListView(generic.ObjectListView):
 @register_model_view(Role)
 class RoleView(GetRelatedModelsMixin, generic.ObjectView):
     queryset = Role.objects.all()
+    layout = layout.SimpleLayout(
+        left_panels=[
+            panels.RolePanel(),
+            TagsPanel(),
+        ],
+        right_panels=[
+            RelatedObjectsPanel(),
+            CommentsPanel(),
+            CustomFieldsPanel(),
+        ],
+    )
 
     def get_extra_context(self, request, instance):
         return {
@@ -569,15 +696,23 @@ class PrefixListView(generic.ObjectListView):
 @register_model_view(Prefix)
 class PrefixView(generic.ObjectView):
     queryset = Prefix.objects.all()
+    layout = layout.SimpleLayout(
+        left_panels=[
+            panels.PrefixPanel(),
+        ],
+        right_panels=[
+            TemplatePanel('ipam/panels/prefix_addressing.html'),
+            CustomFieldsPanel(),
+            TagsPanel(),
+            CommentsPanel(),
+        ],
+        bottom_panels=[
+            ContextTablePanel('duplicate_prefix_table', title=_('Duplicate prefixes')),
+            ContextTablePanel('parent_prefix_table', title=_('Parent prefixes')),
+        ],
+    )
 
     def get_extra_context(self, request, instance):
-        try:
-            aggregate = Aggregate.objects.restrict(request.user, 'view').get(
-                prefix__net_contains_or_equals=str(instance.prefix)
-            )
-        except Aggregate.DoesNotExist:
-            aggregate = None
-
         # Parent prefixes table
         parent_prefixes = Prefix.objects.restrict(request.user, 'view').filter(
             Q(vrf=instance.vrf) | Q(vrf__isnull=True, status=PrefixStatusChoices.STATUS_CONTAINER)
@@ -608,11 +743,12 @@ class PrefixView(generic.ObjectView):
         )
         duplicate_prefix_table.configure(request)
 
-        return {
-            'aggregate': aggregate,
+        context = {
             'parent_prefix_table': parent_prefix_table,
-            'duplicate_prefix_table': duplicate_prefix_table,
         }
+        if duplicate_prefixes.exists():
+            context['duplicate_prefix_table'] = duplicate_prefix_table
+        return context
 
 
 @register_model_view(Prefix, 'prefixes')
@@ -756,6 +892,19 @@ class IPRangeListView(generic.ObjectListView):
 @register_model_view(IPRange)
 class IPRangeView(generic.ObjectView):
     queryset = IPRange.objects.all()
+    layout = layout.SimpleLayout(
+        left_panels=[
+            panels.IPRangePanel(),
+        ],
+        right_panels=[
+            TagsPanel(),
+            CustomFieldsPanel(),
+            CommentsPanel(),
+        ],
+        bottom_panels=[
+            ContextTablePanel('parent_prefixes_table', title=_('Parent prefixes')),
+        ],
+    )
 
     def get_extra_context(self, request, instance):
 
@@ -853,6 +1002,23 @@ class IPAddressListView(generic.ObjectListView):
 @register_model_view(IPAddress)
 class IPAddressView(generic.ObjectView):
     queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant')
+    layout = layout.SimpleLayout(
+        left_panels=[
+            panels.IPAddressPanel(),
+            TagsPanel(),
+            CustomFieldsPanel(),
+            CommentsPanel(),
+        ],
+        right_panels=[
+            ContextTablePanel('parent_prefixes_table', title=_('Parent prefixes')),
+            ContextTablePanel('duplicate_ips_table', title=_('Duplicate IPs')),
+            ObjectsTablePanel(
+                'ipam.service',
+                filters={'ip_address_id': lambda ctx: ctx['object'].pk},
+                title=_('Application services'),
+            ),
+        ],
+    )
 
     def get_extra_context(self, request, instance):
         # Parent prefixes table
@@ -885,10 +1051,12 @@ class IPAddressView(generic.ObjectView):
         duplicate_ips_table = tables.IPAddressTable(duplicate_ips[:10], orderable=False)
         duplicate_ips_table.configure(request)
 
-        return {
+        context = {
             'parent_prefixes_table': parent_prefixes_table,
-            'duplicate_ips_table': duplicate_ips_table,
         }
+        if duplicate_ips.exists():
+            context['duplicate_ips_table'] = duplicate_ips_table
+        return context
 
 
 @register_model_view(IPAddress, 'add', detail=False)
@@ -1038,6 +1206,17 @@ class VLANGroupListView(generic.ObjectListView):
 @register_model_view(VLANGroup)
 class VLANGroupView(GetRelatedModelsMixin, generic.ObjectView):
     queryset = VLANGroup.objects.annotate_utilization()
+    layout = layout.SimpleLayout(
+        left_panels=[
+            panels.VLANGroupPanel(),
+            TagsPanel(),
+        ],
+        right_panels=[
+            RelatedObjectsPanel(),
+            CommentsPanel(),
+            CustomFieldsPanel(),
+        ],
+    )
 
     def get_extra_context(self, request, instance):
         return {
@@ -1125,19 +1304,32 @@ class VLANTranslationPolicyListView(generic.ObjectListView):
 
 
 @register_model_view(VLANTranslationPolicy)
-class VLANTranslationPolicyView(GetRelatedModelsMixin, generic.ObjectView):
+class VLANTranslationPolicyView(generic.ObjectView):
     queryset = VLANTranslationPolicy.objects.all()
-
-    def get_extra_context(self, request, instance):
-        vlan_translation_table = VLANTranslationRuleTable(
-            data=instance.rules.all(),
-            orderable=False
-        )
-        vlan_translation_table.configure(request)
-
-        return {
-            'vlan_translation_table': vlan_translation_table,
-        }
+    layout = layout.SimpleLayout(
+        left_panels=[
+            panels.VLANTranslationPolicyPanel(),
+        ],
+        right_panels=[
+            TagsPanel(),
+            CustomFieldsPanel(),
+            CommentsPanel(),
+        ],
+        bottom_panels=[
+            ObjectsTablePanel(
+                'ipam.vlantranslationrule',
+                filters={'policy_id': lambda ctx: ctx['object'].pk},
+                title=_('VLAN translation rules'),
+                actions=[
+                    actions.AddObject(
+                        'ipam.vlantranslationrule',
+                        url_params={'policy': lambda ctx: ctx['object'].pk},
+                        label=_('Add Rule'),
+                    ),
+                ],
+            ),
+        ],
+    )
 
 
 @register_model_view(VLANTranslationPolicy, 'add', detail=False)
@@ -1193,13 +1385,17 @@ class VLANTranslationRuleListView(generic.ObjectListView):
 
 
 @register_model_view(VLANTranslationRule)
-class VLANTranslationRuleView(GetRelatedModelsMixin, generic.ObjectView):
+class VLANTranslationRuleView(generic.ObjectView):
     queryset = VLANTranslationRule.objects.all()
-
-    def get_extra_context(self, request, instance):
-        return {
-            'related_models': self.get_related_models(request, instance),
-        }
+    layout = layout.SimpleLayout(
+        left_panels=[
+            panels.VLANTranslationRulePanel(),
+        ],
+        right_panels=[
+            TagsPanel(),
+            CustomFieldsPanel(),
+        ],
+    )
 
 
 @register_model_view(VLANTranslationRule, 'add', detail=False)
@@ -1251,7 +1447,36 @@ class FHRPGroupListView(generic.ObjectListView):
 
 @register_model_view(FHRPGroup)
 class FHRPGroupView(GetRelatedModelsMixin, generic.ObjectView):
-    queryset = FHRPGroup.objects.all()
+    queryset = FHRPGroup.objects.annotate(
+        member_count=count_related(FHRPGroupAssignment, 'group')
+    )
+    layout = layout.SimpleLayout(
+        left_panels=[
+            panels.FHRPGroupPanel(),
+            TagsPanel(),
+            CommentsPanel(),
+        ],
+        right_panels=[
+            panels.FHRPGroupAuthPanel(),
+            RelatedObjectsPanel(),
+            CustomFieldsPanel(),
+        ],
+        bottom_panels=[
+            ObjectsTablePanel(
+                'ipam.ipaddress',
+                filters={'fhrpgroup_id': lambda ctx: ctx['object'].pk},
+                title=_('Virtual IP addresses'),
+                actions=[
+                    actions.AddObject(
+                        'ipam.ipaddress',
+                        url_params={'fhrpgroup': lambda ctx: ctx['object'].pk},
+                        label=_('Add IP Address'),
+                    ),
+                ],
+            ),
+            ContextTablePanel('members_table', title=_('Members')),
+        ],
+    )
 
     def get_extra_context(self, request, instance):
         # Get assigned interfaces
@@ -1276,7 +1501,6 @@ class FHRPGroupView(GetRelatedModelsMixin, generic.ObjectView):
                 ),
             ),
             'members_table': members_table,
-            'member_count': FHRPGroupAssignment.objects.filter(group=instance).count(),
         }
 
 
@@ -1379,17 +1603,35 @@ class VLANListView(generic.ObjectListView):
 @register_model_view(VLAN)
 class VLANView(generic.ObjectView):
     queryset = VLAN.objects.all()
-
-    def get_extra_context(self, request, instance):
-        prefixes = Prefix.objects.restrict(request.user, 'view').filter(vlan=instance).prefetch_related(
-            'vrf', 'scope', 'role', 'tenant'
-        )
-        prefix_table = tables.PrefixTable(list(prefixes), exclude=('vlan', 'utilization'), orderable=False)
-        prefix_table.configure(request)
-
-        return {
-            'prefix_table': prefix_table,
-        }
+    layout = layout.SimpleLayout(
+        left_panels=[
+            panels.VLANPanel(),
+        ],
+        right_panels=[
+            CustomFieldsPanel(),
+            TagsPanel(),
+            CommentsPanel(),
+        ],
+        bottom_panels=[
+            ObjectsTablePanel(
+                'ipam.prefix',
+                filters={'vlan_id': lambda ctx: ctx['object'].pk},
+                title=_('Prefixes'),
+                actions=[
+                    actions.AddObject(
+                        'ipam.prefix',
+                        url_params={
+                            'tenant': lambda ctx: ctx['object'].tenant.pk if ctx['object'].tenant else None,
+                            'site': lambda ctx: ctx['object'].site.pk if ctx['object'].site else None,
+                            'vlan': lambda ctx: ctx['object'].pk,
+                        },
+                        label=_('Add a Prefix'),
+                    ),
+                ],
+            ),
+            panels.VLANCustomerVLANsPanel(),
+        ],
+    )
 
 
 @register_model_view(VLAN, 'interfaces')
@@ -1483,6 +1725,16 @@ class ServiceTemplateListView(generic.ObjectListView):
 @register_model_view(ServiceTemplate)
 class ServiceTemplateView(generic.ObjectView):
     queryset = ServiceTemplate.objects.all()
+    layout = layout.SimpleLayout(
+        left_panels=[
+            panels.ServiceTemplatePanel(),
+        ],
+        right_panels=[
+            CustomFieldsPanel(),
+            TagsPanel(),
+            CommentsPanel(),
+        ],
+    )
 
 
 @register_model_view(ServiceTemplate, 'add', detail=False)
@@ -1539,6 +1791,16 @@ class ServiceListView(generic.ObjectListView):
 @register_model_view(Service)
 class ServiceView(generic.ObjectView):
     queryset = Service.objects.all()
+    layout = layout.SimpleLayout(
+        left_panels=[
+            panels.ServicePanel(),
+        ],
+        right_panels=[
+            CustomFieldsPanel(),
+            TagsPanel(),
+            CommentsPanel(),
+        ],
+    )
 
     def get_extra_context(self, request, instance):
         context = {}

+ 0 - 61
netbox/templates/ipam/aggregate.html

@@ -1,62 +1 @@
 {% extends 'ipam/aggregate/base.html' %}
-{% load buttons %}
-{% load helpers %}
-{% load plugins %}
-{% load i18n %}
-
-{% block content %}
-  <div class="row">
-    <div class="col col-12 col-md-6">
-      <div class="card">
-        <h2 class="card-header">{% trans "Aggregate" %}</h2>
-        <table class="table table-hover attr-table">
-          <tr>
-            <th scope="row">{% trans "Family" %}</th>
-            <td>IPv{{ object.family }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "RIR" %}</th>
-            <td>
-              <a href="{% url 'ipam:aggregate_list' %}?rir={{ object.rir.slug }}">{{ object.rir }}</a>
-            </td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Utilization" %}</th>
-            <td>
-              {% utilization_graph object.get_utilization %}
-            </td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Tenant" %}</th>
-            <td>
-              {% if object.tenant.group %}
-                {{ object.tenant.group|linkify }} /
-              {% endif %}
-              {{ object.tenant|linkify|placeholder }}
-            </td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Date Added" %}</th>
-            <td>{{ object.date_added|isodate|placeholder }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Description" %}</th>
-            <td>{{ object.description|placeholder }}</td>
-          </tr>
-        </table>
-      </div>
-      {% plugin_left_page object %}
-    </div>
-    <div class="col col-12 col-md-6">
-      {% include 'inc/panels/custom_fields.html' %}
-      {% include 'inc/panels/tags.html' %}
-      {% include 'inc/panels/comments.html' %}
-      {% plugin_right_page object %}
-    </div>
-  </div>
-  <div class="row mb-3">
-    <div class="col col-md-12">
-      {% plugin_full_width_page object %}
-    </div>
-  </div>
-{% endblock %}

+ 2 - 0
netbox/templates/ipam/aggregate/attrs/utilization.html

@@ -0,0 +1,2 @@
+{% load helpers %}
+{% utilization_graph object.get_utilization %}

+ 0 - 52
netbox/templates/ipam/asn.html

@@ -1,8 +1,4 @@
 {% extends 'generic/object.html' %}
-{% load buttons %}
-{% load helpers %}
-{% load plugins %}
-{% load render_table from django_tables2 %}
 {% load i18n %}
 
 {% block breadcrumbs %}
@@ -12,51 +8,3 @@
     <li class="breadcrumb-item"><a href="{% url 'ipam:asn_list' %}?range_id={{ object.range.pk }}">{{ object.range }}</a></li>
   {% endif %}
 {% endblock breadcrumbs %}
-
-{% block content %}
-  <div class="row">
-    <div class="col col-12 col-md-6">
-      <div class="card">
-        <h2 class="card-header">{% trans "ASN" %}</h2>
-        <table class="table table-hover attr-table">
-          <tr>
-            <th scope="row">{% trans "AS Number" %}</th>
-            <td>{{ object.asn_with_asdot }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "RIR" %}</th>
-            <td>
-              <a href="{% url 'ipam:asn_list' %}?rir={{ object.rir.slug }}">{{ object.rir }}</a>
-            </td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Tenant" %}</th>
-            <td>
-              {% if object.tenant.group %}
-                {{ object.tenant.group|linkify }} /
-              {% endif %}
-              {{ object.tenant|linkify|placeholder }}
-            </td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Description" %}</th>
-            <td>{{ object.description|placeholder }}</td>
-          </tr>
-        </table>
-      </div>
-      {% plugin_left_page object %}
-      {% include 'inc/panels/tags.html' %}
-    </div>
-    <div class="col col-12 col-md-6">
-      {% include 'inc/panels/related_objects.html' %}
-      {% include 'inc/panels/custom_fields.html' %}
-      {% include 'inc/panels/comments.html' %}
-      {% plugin_right_page object %}
-    </div>
-  </div>
-  <div class="row">
-    <div class="col col-md-12">
-      {% plugin_full_width_page object %}
-    </div>
-  </div>
-{% endblock content %}

+ 0 - 56
netbox/templates/ipam/asnrange.html

@@ -1,57 +1 @@
 {% extends 'ipam/asnrange/base.html' %}
-{% load buttons %}
-{% load helpers %}
-{% load plugins %}
-{% load render_table from django_tables2 %}
-{% load i18n %}
-
-{% block content %}
-  <div class="row">
-    <div class="col col-12 col-md-6">
-      <div class="card">
-        <h2 class="card-header">{% trans "ASN Range" %}</h2>
-        <table class="table table-hover attr-table">
-          <tr>
-            <th scope="row">{% trans "Name" %}</th>
-            <td>{{ object.name }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "RIR" %}</th>
-            <td>
-              <a href="{% url 'ipam:asnrange_list' %}?rir={{ object.rir.slug }}">{{ object.rir }}</a>
-            </td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Range" %}</th>
-            <td>{{ object.range_as_string_with_asdot }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Tenant" %}</th>
-            <td>
-              {% if object.tenant.group %}
-                {{ object.tenant.group|linkify }} /
-              {% endif %}
-              {{ object.tenant|linkify|placeholder }}
-            </td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Description" %}</th>
-            <td>{{ object.description|placeholder }}</td>
-          </tr>
-        </table>
-      </div>
-      {% plugin_left_page object %}
-      {% include 'inc/panels/tags.html' %}
-    </div>
-    <div class="col col-12 col-md-6">
-      {% include 'inc/panels/comments.html' %}
-      {% include 'inc/panels/custom_fields.html' %}
-      {% plugin_right_page object %}
-    </div>
-  </div>
-  <div class="row">
-    <div class="col col-md-12">
-      {% plugin_full_width_page object %}
-    </div>
-  </div>
-{% endblock content %}

+ 2 - 0
netbox/templates/ipam/attrs/vrf.html

@@ -0,0 +1,2 @@
+{% load helpers i18n %}
+{% if value %}{{ value|linkify }}{% if show_rd %} ({{ value.rd }}){% endif %}{% else %}<span>{% trans "Global" %}</span>{% endif %}

+ 0 - 75
netbox/templates/ipam/fhrpgroup.html

@@ -1,7 +1,4 @@
 {% extends 'generic/object.html' %}
-{% load helpers %}
-{% load plugins %}
-{% load render_table from django_tables2 %}
 {% load i18n %}
 
 {# Omit assigned IP addresses from object representation #}
@@ -11,75 +8,3 @@
   {{ block.super }}
   <li class="breadcrumb-item"><a href="{% url 'ipam:fhrpgroup_list' %}?protocol={{ object.protocol }}">{{ object.get_protocol_display }}</a></li>
 {% endblock breadcrumbs %}
-
-{% block content %}
-  <div class="row">
-    <div class="col col-12 col-md-6">
-      <div class="card">
-        <h2 class="card-header">{% trans "FHRP Group" %}</h2>
-        <table class="table table-hover attr-table">
-          <tr>
-            <th scope="row">{% trans "Protocol" %}</th>
-            <td>{{ object.get_protocol_display }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Group ID" %}</th>
-            <td>{{ object.group_id }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Name" %}</th>
-            <td>{{ object.name|placeholder }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Description" %}</th>
-            <td>{{ object.description|placeholder }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Members" %}</th>
-            <td>{{ member_count }}</td>
-          </tr>
-        </table>
-      </div>
-      {% include 'inc/panels/tags.html' %}
-      {% include 'inc/panels/comments.html' %}
-      {% plugin_left_page object %}
-    </div>
-    <div class="col col-12 col-md-6">
-      <div class="card">
-        <h2 class="card-header">{% trans "Authentication" %}</h2>
-        <table class="table table-hover attr-table">
-          <tr>
-            <th scope="row">{% trans "Authentication Type" %}</th>
-            <td>{{ object.get_auth_type_display|placeholder }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Authentication Key" %}</th>
-            <td>{{ object.auth_key|placeholder }}</td>
-          </tr>
-        </table>
-      </div>
-      {% include 'inc/panels/related_objects.html' %}
-      {% include 'inc/panels/custom_fields.html' %}
-      {% plugin_right_page object %}
-    </div>
-  </div>
-  <div class="row">
-    <div class="col col-md-12">
-      <div class="card">
-        <h2 class="card-header">
-          {% trans "Virtual IP Addresses" %}
-          {% if perms.ipam.add_ipaddress %}
-            <div class="card-actions">
-              <a href="{% url 'ipam:ipaddress_add' %}?fhrpgroup={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-ghost-primary btn-sm">
-                <i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add IP Address" %}
-              </a>
-            </div>
-          {% endif %}
-        </h2>
-        {% htmx_table 'ipam:ipaddress_list' fhrpgroup_id=object.pk %}
-      </div>
-      {% include 'inc/panel_table.html' with table=members_table heading='Members' %}
-      {% plugin_full_width_page object %}
-    </div>
-  </div>
-{% endblock %}

+ 1 - 129
netbox/templates/ipam/ipaddress.html

@@ -1,129 +1 @@
-{% extends 'generic/object.html' %}
-{% load helpers %}
-{% load plugins %}
-{% load render_table from django_tables2 %}
-{% load i18n %}
-
-{% block content %}
-<div class="row">
-	<div class="col col-12 col-md-4">
-      <div class="card">
-          <h2 class="card-header">{% trans "IP Address" %}</h2>
-          <table class="table table-hover attr-table">
-              <tr>
-                  <th scope="row">{% trans "Family" %}</th>
-                  <td>IPv{{ object.family }}</td>
-              </tr>
-              <tr>
-                  <th scope="row">{% trans "VRF" %}</th>
-                  <td>
-                      {% if object.vrf %}
-                          <a href="{% url 'ipam:vrf' pk=object.vrf.pk %}">{{ object.vrf }}</a>
-                      {% else %}
-                          <span>{% trans "Global" %}</span>
-                      {% endif %}
-                  </td>
-              </tr>
-              <tr>
-                  <th scope="row">{% trans "Tenant" %}</th>
-                  <td>
-                      {% if object.tenant.group %}
-                          {{ object.tenant.group|linkify }} /
-                      {% endif %}
-                      {{ object.tenant|linkify|placeholder }}
-                  </td>
-              </tr>
-              <tr>
-                  <th scope="row">{% trans "Status" %}</th>
-                  <td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
-              </tr>
-              <tr>
-                  <th scope="row">{% trans "Role" %}</th>
-                  <td>
-                      {% if object.role %}
-                          <a href="{% url 'ipam:ipaddress_list' %}?role={{ object.role }}">{{ object.get_role_display }}</a>
-                      {% else %}
-                          {{ ''|placeholder }}
-                      {% endif %}
-                  </td>
-              </tr>
-              <tr>
-                  <th scope="row">{% trans "DNS Name" %}</th>
-                  <td>{{ object.dns_name|placeholder }}</td>
-              </tr>
-              <tr>
-                  <th scope="row">{% trans "Description" %}</th>
-                  <td>{{ object.description|placeholder }}</td>
-              </tr>
-              <tr>
-                  <th scope="row">{% trans "Assignment" %}</th>
-                  <td>
-                    {% if object.assigned_object %}
-                      {% if object.assigned_object.parent_object %}
-                        {{ object.assigned_object.parent_object|linkify }} /
-                      {% endif %}
-                      {{ object.assigned_object|linkify }}
-                    {% else %}
-                      {{ ''|placeholder }}
-                    {% endif %}
-                  </td>
-              </tr>
-              <tr>
-                  <th scope="row">{% trans "NAT (inside)" %}</th>
-                  <td>
-                      {% if object.nat_inside %}
-                          {{ object.nat_inside|linkify }}
-                          {% if object.nat_inside.assigned_object %}
-                              ({{ object.nat_inside.assigned_object.parent_object|linkify }})
-                          {% endif %}
-                      {% else %}
-                          {{ ''|placeholder }}
-                      {% endif %}
-                  </td>
-              </tr>
-              <tr>
-                  <th scope="row">{% trans "NAT (outside)" %}</th>
-                  <td>
-                    {% for ip in object.nat_outside.all %}
-                      {{ ip|linkify }}
-                      {% if ip.assigned_object %}
-                        ({{ ip.assigned_object.parent_object|linkify }})
-                      {% endif %}<br/>
-                    {% empty %}
-                      {{ ''|placeholder }}
-                    {% endfor %}
-                  </td>
-              </tr>
-              <tr>
-                <th scope="row">Primary IP</th>
-                <td>{% checkmark object.is_primary_ip %}</td>
-              </tr>
-              <tr>
-                <th scope="row">OOB IP</th>
-                <td>{% checkmark object.is_oob_ip %}</td>
-              </tr>
-          </table>
-      </div>
-      {% include 'inc/panels/tags.html' %}
-      {% include 'inc/panels/custom_fields.html' %}
-      {% include 'inc/panels/comments.html' %}
-      {% plugin_left_page object %}
-	</div>
-	<div class="col col-md-8">
-    {% include 'inc/panel_table.html' with table=parent_prefixes_table heading='Parent Prefixes' %}
-    {% if duplicate_ips_table.rows %}
-      {% include 'inc/panel_table.html' with table=duplicate_ips_table heading='Duplicate IPs' panel_class='danger' %}
-    {% endif %}
-    <div class="card">
-      <h2 class="card-header">{% trans "Application Services" %}</h2>
-      {% htmx_table 'ipam:service_list' ip_address_id=object.pk %}
-    </div>
-    {% plugin_right_page object %}
-	</div>
-</div>
-<div class="row">
-  <div class="col col-md-12">
-    {% plugin_full_width_page object %}
-  </div>
-</div>
-{% endblock %}
+{% extends 'ipam/ipaddress/base.html' %}

+ 2 - 0
netbox/templates/ipam/ipaddress/attrs/assigned_object.html

@@ -0,0 +1,2 @@
+{% load helpers %}
+{% if value.parent_object %}{{ value.parent_object|linkify }} / {% endif %}{{ value|linkify }}

+ 2 - 0
netbox/templates/ipam/ipaddress/attrs/nat_inside.html

@@ -0,0 +1,2 @@
+{% load helpers %}
+{{ value|linkify }}{% if value.assigned_object %} ({{ value.assigned_object.parent_object|linkify }}){% endif %}

+ 2 - 0
netbox/templates/ipam/ipaddress/attrs/nat_outside.html

@@ -0,0 +1,2 @@
+{% load helpers %}
+{% for ip in value.all %}{{ ip|linkify }}{% if ip.assigned_object %} ({{ ip.assigned_object.parent_object|linkify }}){% endif %}<br/>{% empty %}<span class="text-muted">&mdash;</span>{% endfor %}

+ 0 - 97
netbox/templates/ipam/iprange.html

@@ -1,98 +1 @@
 {% extends 'ipam/iprange/base.html' %}
-{% load helpers %}
-{% load plugins %}
-{% load i18n %}
-
-{% block content %}
-<div class="row">
-  <div class="col col-12 col-md-6">
-    <div class="card">
-      <h2 class="card-header">{% trans "IP Range" %}</h2>
-      <table class="table table-hover attr-table">
-        <tr>
-            <th scope="row">{% trans "Family" %}</th>
-            <td>IPv{{ object.family }}</td>
-        </tr>
-        <tr>
-            <th scope="row">{% trans "Starting Address" %}</th>
-            <td>{{ object.start_address }}</td>
-        </tr>
-        <tr>
-            <th scope="row">{% trans "Ending Address" %}</th>
-            <td>{{ object.end_address }}</td>
-        </tr>
-        <tr>
-            <th scope="row">{% trans "Size" %}</th>
-            <td>{{ object.size }}</td>
-        </tr>
-        <tr>
-            <th scope="row">{% trans "Marked Populated" %}</th>
-            <td>{% checkmark object.mark_populated %}</td>
-        </tr>
-        <tr>
-            <th scope="row">{% trans "Marked Utilized" %}</th>
-            <td>{% checkmark object.mark_utilized %}</td>
-        </tr>
-        <tr>
-            <th scope="row">{% trans "Utilization" %}</th>
-            <td>
-              {% if object.mark_utilized %}
-                {% utilization_graph 100 warning_threshold=0 danger_threshold=0 %}
-              {% else %}
-                {% utilization_graph object.utilization %}
-              {% endif %}
-            </td>
-        </tr>
-        <tr>
-            <th scope="row">{% trans "VRF" %}</th>
-            <td>
-                {% if object.vrf %}
-                    {{ object.vrf|linkify }} ({{ object.vrf.rd }})
-                {% else %}
-                    <span>{% trans "Global" %}</span>
-                {% endif %}
-            </td>
-        </tr>
-        <tr>
-            <th scope="row">{% trans "Role" %}</th>
-            <td>{{ object.role|linkify|placeholder }}</td>
-        </tr>
-        <tr>
-            <th scope="row">{% trans "Status" %}</th>
-            <td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
-        </tr>
-        <tr>
-            <th scope="row">{% trans "Tenant" %}</th>
-            <td>
-                {% if object.tenant.group %}
-                    {{ object.tenant.group|linkify }} /
-                {% endif %}
-                {{ object.tenant|linkify|placeholder }}
-            </td>
-        </tr>
-        <tr>
-            <th scope="row">{% trans "Description" %}</th>
-            <td>{{ object.description|placeholder }}</td>
-        </tr>
-      </table>
-    </div>
-    {% plugin_left_page object %}
-  </div>
-  <div class="col col-12 col-md-6">
-    {% include 'inc/panels/tags.html' %}
-    {% include 'inc/panels/custom_fields.html' %}
-    {% include 'inc/panels/comments.html' %}
-    {% plugin_right_page object %}
-  </div>
-</div>
-<div class="row">
-  <div class="col col-md-12">
-    {% include 'inc/panel_table.html' with table=parent_prefixes_table heading='Parent Prefixes' %}
-  </div>
-</div>
-<div class="row">
-  <div class="col col-md-12">
-    {% plugin_full_width_page object %}
-  </div>
-</div>
-{% endblock %}

+ 6 - 0
netbox/templates/ipam/iprange/attrs/utilization.html

@@ -0,0 +1,6 @@
+{% load helpers %}
+{% if object.mark_utilized %}
+  {% utilization_graph 100 warning_threshold=0 danger_threshold=0 %}
+{% else %}
+  {% utilization_graph value %}
+{% endif %}

+ 62 - 0
netbox/templates/ipam/panels/prefix_addressing.html

@@ -0,0 +1,62 @@
+{% load humanize helpers i18n %}
+<div class="card">
+  <h2 class="card-header">
+    {% trans "Addressing" %}
+    {% if object.prefix.version == 4 %}
+      <div class="card-actions">
+        <a class="btn btn-ghost-primary btn-sm" data-bs-toggle="modal" data-bs-target="#prefix-modal">
+          <i class="mdi mdi-information-outline" aria-hidden="true"></i> {% trans "Addressing Details" %}
+        </a>
+      </div>
+    {% endif %}
+  </h2>
+  <table class="table table-hover attr-table">
+    <tr>
+      <th scope="row">{% trans "Utilization" %}</th>
+      <td>
+        {% if object.mark_utilized %}
+          {% utilization_graph 100 warning_threshold=0 danger_threshold=0 %}
+          <small>({% trans "Marked fully utilized" %})</small>
+        {% else %}
+          {% utilization_graph object.get_utilization %}
+        {% endif %}
+      </td>
+    </tr>
+    {% with child_ip_count=object.get_child_ips.count %}
+      <tr>
+        <th scope="row">{% trans "Child IPs" %}</th>
+        <td>
+          <a href="{% url 'ipam:prefix_ipaddresses' pk=object.pk %}">{{ child_ip_count }}</a>
+        </td>
+      </tr>
+    {% endwith %}
+    {% with available_count=object.get_available_ips.size %}
+      <tr>
+        <th scope="row">{% trans "Available IPs" %}</th>
+        <td>
+          {% if available_count > 1000000 %}
+            {{ available_count|intword }}
+          {% else %}
+            {{ available_count|intcomma }}
+          {% endif %}
+        </td>
+      </tr>
+    {% endwith %}
+    <tr>
+      <th scope="row">{% trans "First available IP" %}</th>
+      <td>
+        {% with first_available_ip=object.get_first_available_ip %}
+          {% if first_available_ip %}
+            {% if perms.ipam.add_ipaddress %}
+              <a href="{% url 'ipam:ipaddress_add' %}?address={{ first_available_ip }}{% if object.vrf %}&vrf={{ object.vrf_id }}{% endif %}{% if object.tenant %}&tenant={{ object.tenant.pk }}{% endif %}">{{ first_available_ip }}</a>
+            {% else %}
+              {{ first_available_ip }}
+            {% endif %}
+          {% else %}
+            {{ ''|placeholder }}
+          {% endif %}
+        {% endwith %}
+      </td>
+    </tr>
+  </table>
+</div>

+ 0 - 201
netbox/templates/ipam/prefix.html

@@ -1,202 +1 @@
 {% extends 'ipam/prefix/base.html' %}
-{% load humanize %}
-{% load helpers %}
-{% load plugins %}
-{% load i18n %}
-{% load mptt %}
-
-{% block content %}
-<div class="row">
-  <div class="col col-12 col-md-6">
-    <div class="card">
-      <h2 class="card-header">{% trans "Prefix" %}</h2>
-      <table class="table table-hover attr-table">
-        <tr>
-          <th scope="row">{% trans "Family" %}</th>
-          <td>IPv{{ object.family }}</td>
-        </tr>
-        <tr>
-          <th scope="row">{% trans "VRF" %}</th>
-          <td>
-            {% if object.vrf %}
-              <a href="{% url 'ipam:vrf' pk=object.vrf.pk %}">{{ object.vrf }}</a>
-            {% else %}
-              <span>{% trans "Global" %}</span>
-            {% endif %}
-          </td>
-        </tr>
-        <tr>
-          <th scope="row">{% trans "Tenant" %}</th>
-            <td>
-              {% if object.tenant.group %}
-                {{ object.tenant.group|linkify }} /
-              {% endif %}
-              {{ object.tenant|linkify|placeholder }}
-            </td>
-        </tr>
-        <tr>
-          <th scope="row">{% trans "Aggregate" %}</th>
-          <td>
-            {% if aggregate %}
-              <a href="{% url 'ipam:aggregate' pk=aggregate.pk %}">{{ aggregate.prefix }}</a> ({{ aggregate.rir }})
-            {% else %}
-              {{ ''|placeholder }}
-            {% endif %}
-          </td>
-        </tr>
-        <tr>
-          <th scope="row">{% trans "Scope" %}</th>
-          {% if object.scope %}
-            <td>{{ object.scope|linkify }} ({% trans object.scope_type.name %})</td>
-          {% else %}
-            <td>{{ ''|placeholder }}</td>
-          {% endif %}
-        </tr>
-        <tr>
-          <th scope="row">{% trans "VLAN" %}</th>
-          <td>
-            {% if object.vlan %}
-              {% if object.vlan.group %}
-                {{ object.vlan.group|linkify }} /
-              {% endif %}
-              {{ object.vlan|linkify }}
-            {% else %}
-              {{ ''|placeholder }}
-            {% endif %}
-          </td>
-        </tr>
-        <tr>
-          <th scope="row">{% trans "Status" %}</th>
-          <td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
-        </tr>
-        <tr>
-          <th scope="row">{% trans "Role" %}</th>
-          <td>{{ object.role|linkify|placeholder }}</td>
-        </tr>
-        <tr>
-          <th scope="row">{% trans "Description" %}</th>
-          <td>{{ object.description|placeholder }}</td>
-        </tr>
-        <tr>
-          <th scope="row">{% trans "Is a pool" %}</th>
-          <td>{% checkmark object.is_pool %}</td>
-        </tr>
-      </table>
-    </div>
-    {% plugin_left_page object %}
-  </div>
-  <div class="col col-12 col-md-6">
-    <div class="card">
-      <h2 class="card-header">
-        {% trans "Addressing" %}
-        {% if object.prefix.version == 4 %}
-          <div class="card-actions">
-            <a class="btn btn-ghost-primary btn-sm" data-bs-toggle="modal" data-bs-target="#prefix-modal">
-              <i class="mdi mdi-information-outline" aria-hidden="true"></i> {% trans "Addressing Details" %}
-            </a>
-          </div>
-        {% endif %}
-      </h2>
-      <table class="table table-hover attr-table">
-        <tr>
-          <th scope="row">{% trans "Utilization" %}</th>
-          <td>
-            {% if object.mark_utilized %}
-              {% utilization_graph 100 warning_threshold=0 danger_threshold=0 %}
-              <small>({% trans "Marked fully utilized" %})</small>
-            {% else %}
-              {% utilization_graph object.get_utilization %}
-            {% endif %}
-          </td>
-        </tr>
-        {% with child_ip_count=object.get_child_ips.count %}
-          <tr>
-            <th scope="row">{% trans "Child IPs" %}</th>
-            <td>
-              <a href="{% url 'ipam:prefix_ipaddresses' pk=object.pk %}">{{ child_ip_count }}</a>
-            </td>
-          </tr>
-        {% endwith %}
-        {% with available_count=object.get_available_ips.size %}
-          <tr>
-            <th scope="row">{% trans "Available IPs" %}</th>
-            <td>
-              {# Use human-friendly words for counts greater than one million #}
-              {% if available_count > 1000000 %}
-                {{ available_count|intword }}
-              {% else %}
-                {{ available_count|intcomma }}
-              {% endif %}
-            </td>
-          </tr>
-        {% endwith %}
-        <tr>
-            <th scope="row">{% trans "First available IP" %}</th>
-          <td>
-            {% with first_available_ip=object.get_first_available_ip %}
-              {% if first_available_ip %}
-                {% if perms.ipam.add_ipaddress %}
-                  <a href="{% url 'ipam:ipaddress_add' %}?address={{ first_available_ip }}{% if object.vrf %}&vrf={{ object.vrf_id }}{% endif %}{% if object.tenant %}&tenant={{ object.tenant.pk }}{% endif %}">{{ first_available_ip }}</a>
-                {% else %}
-                  {{ first_available_ip }}
-                {% endif %}
-              {% else %}
-                {{ ''|placeholder }}
-              {% endif %}
-            {% endwith %}
-          </td>
-        </tr>
-      </table>
-    </div>
-    {% include 'inc/panels/custom_fields.html' %}
-    {% include 'inc/panels/tags.html' %}
-    {% include 'inc/panels/comments.html' %}
-    {% plugin_right_page object %}
-  </div>
-</div>
-<div class="row">
-    <div class="col col-md-12">
-        {% if duplicate_prefix_table.rows %}
-            {% include 'inc/panel_table.html' with table=duplicate_prefix_table heading='Duplicate Prefixes' %}
-        {% endif %}
-        {% include 'inc/panel_table.html' with table=parent_prefix_table heading='Parent Prefixes' %}
-        {% plugin_full_width_page object %}
-    </div>
-</div>
-{% endblock %}
-
-{% block modals %}
-  {{ block.super }}
-  {% if object.prefix.version == 4 %}
-    <div class="modal fade" id="prefix-modal" tabindex="-1" aria-hidden="true">
-      <div class="modal-dialog modal-dialog-centered">
-        <div class="modal-content">
-          <div class="modal-header">
-            <h5 class="modal-title">{% trans "Prefix Details" %}</h5>
-            <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
-          </div>
-          <div class="modal-body p-0">
-            <table class="table table-hover attr-table m-0">
-              <tr>
-                <th scope="row">{% trans "Network Address" %}</th>
-                <td>{{ object.prefix.network }}</td>
-              </tr>
-              <tr>
-                <th scope="row">{% trans "Network Mask" %}</th>
-                <td>{{ object.prefix.netmask }}</td>
-              </tr>
-              <tr>
-                <th scope="row">{% trans "Wildcard Mask" %}</th>
-                <td>{{ object.prefix.hostmask }}</td>
-              </tr>
-              <tr>
-                <th scope="row">{% trans "Broadcast Address" %}</th>
-                <td>{{ object.prefix.broadcast }}</td>
-              </tr>
-            </table>
-          </div>
-        </div>
-      </div>
-    </div>
-  {% endif %}
-{% endblock %}

+ 2 - 0
netbox/templates/ipam/prefix/attrs/aggregate.html

@@ -0,0 +1,2 @@
+{% load helpers %}
+<span>{{ value|linkify }} ({{ value.rir }})</span>

+ 37 - 0
netbox/templates/ipam/prefix/base.html

@@ -1,6 +1,7 @@
 {% extends 'generic/object.html' %}
 {% load buttons %}
 {% load helpers %}
+{% load i18n %}
 
 {% block breadcrumbs %}
   {{ block.super }}
@@ -8,3 +9,39 @@
     <li class="breadcrumb-item"><a href="{% url 'ipam:prefix_list' %}?vrf_id={{ object.vrf.pk }}">{{ object.vrf }}</a></li>
   {% endif %}
 {% endblock %}
+
+{% block modals %}
+  {{ block.super }}
+  {% if object.prefix.version == 4 %}
+    <div class="modal fade" id="prefix-modal" tabindex="-1" aria-hidden="true">
+      <div class="modal-dialog modal-dialog-centered">
+        <div class="modal-content">
+          <div class="modal-header">
+            <h5 class="modal-title">{% trans "Prefix Details" %}</h5>
+            <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
+          </div>
+          <div class="modal-body p-0">
+            <table class="table table-hover attr-table m-0">
+              <tr>
+                <th scope="row">{% trans "Network Address" %}</th>
+                <td>{{ object.prefix.network }}</td>
+              </tr>
+              <tr>
+                <th scope="row">{% trans "Network Mask" %}</th>
+                <td>{{ object.prefix.netmask }}</td>
+              </tr>
+              <tr>
+                <th scope="row">{% trans "Wildcard Mask" %}</th>
+                <td>{{ object.prefix.hostmask }}</td>
+              </tr>
+              <tr>
+                <th scope="row">{% trans "Broadcast Address" %}</th>
+                <td>{{ object.prefix.broadcast }}</td>
+              </tr>
+            </table>
+          </div>
+        </div>
+      </div>
+    </div>
+  {% endif %}
+{% endblock modals %}

+ 0 - 40
netbox/templates/ipam/rir.html

@@ -1,7 +1,4 @@
 {% extends 'generic/object.html' %}
-{% load helpers %}
-{% load plugins %}
-{% load render_table from django_tables2 %}
 {% load i18n %}
 
 {% block extra_controls %}
@@ -11,40 +8,3 @@
     </a>
   {% endif %}
 {% endblock extra_controls %}
-
-{% block content %}
-<div class="row mb-3">
-	<div class="col col-12 col-md-6">
-    <div class="card">
-      <h2 class="card-header">{% trans "RIR" %}</h2>
-        <table class="table table-hover attr-table">
-        <tr>
-          <th scope="row">{% trans "Name" %}</th>
-          <td>{{ object.name }}</td>
-        </tr>
-        <tr>
-          <th scope="row">{% trans "Description" %}</th>
-          <td>{{ object.description|placeholder }}</td>
-        </tr>
-        <tr>
-          <th scope="row">{% trans "Private" %}</th>
-          <td>{% checkmark object.is_private %}</td>
-        </tr>
-      </table>
-    </div>
-    {% include 'inc/panels/tags.html' %}
-    {% plugin_left_page object %}
-	</div>
-	<div class="col col-12 col-md-6">
-    {% include 'inc/panels/related_objects.html' %}
-    {% include 'inc/panels/comments.html' %}
-    {% include 'inc/panels/custom_fields.html' %}
-    {% plugin_right_page object %}
-  </div>
-</div>
-<div class="row mb-3">
-	<div class="col col-md-12">
-    {% plugin_full_width_page object %}
-  </div>
-</div>
-{% endblock %}

+ 0 - 40
netbox/templates/ipam/role.html

@@ -1,7 +1,4 @@
 {% extends 'generic/object.html' %}
-{% load helpers %}
-{% load plugins %}
-{% load render_table from django_tables2 %}
 {% load i18n %}
 
 {% block extra_controls %}
@@ -11,40 +8,3 @@
     </a>
   {% endif %}
 {% endblock extra_controls %}
-
-{% block content %}
-<div class="row mb-3">
-	<div class="col col-12 col-md-6">
-    <div class="card">
-      <h2 class="card-header">{% trans "Role" %}</h2>
-      <table class="table table-hover attr-table">
-        <tr>
-          <th scope="row">{% trans "Name" %}</th>
-          <td>{{ object.name }}</td>
-        </tr>
-        <tr>
-          <th scope="row">{% trans "Description" %}</th>
-          <td>{{ object.description|placeholder }}</td>
-        </tr>
-        <tr>
-          <th scope="row">{% trans "Weight" %}</th>
-          <td>{{ object.weight }}</td>
-        </tr>
-      </table>
-    </div>
-    {% include 'inc/panels/tags.html' %}
-    {% plugin_left_page object %}
-	</div>
-	<div class="col col-12 col-md-6">
-    {% include 'inc/panels/related_objects.html' %}
-    {% include 'inc/panels/comments.html' %}
-    {% include 'inc/panels/custom_fields.html' %}
-    {% plugin_right_page object %}
-  </div>
-</div>
-<div class="row mb-3">
-	<div class="col col-md-12">
-    {% plugin_full_width_page object %}
-  </div>
-</div>
-{% endblock %}

+ 0 - 67
netbox/templates/ipam/routetarget.html

@@ -1,68 +1 @@
 {% extends 'generic/object.html' %}
-{% load helpers %}
-{% load plugins %}
-{% load i18n %}
-
-{% block content %}
-  <div class="row">
-    <div class="col col-12 col-md-6">
-      <div class="card">
-        <h2 class="card-header">{% trans "Route Target" %}</h2>
-        <table class="table table-hover attr-table">
-          <tr>
-            <th scope="row">{% trans "Name" %}</th>
-            <td class="font-monospace">{{ object.name }}</td>
-          </tr>
-          <tr>
-            <th  scope="row">{% trans "Tenant" %}</th>
-            <td>{{ object.tenant|linkify|placeholder }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Description" %}</th>
-            <td>{{ object.description|placeholder }}</td>
-          </tr>
-        </table>
-      </div>
-      {% include 'inc/panels/tags.html' %}
-      {% plugin_left_page object %}
-    </div>
-    <div class="col col-12 col-md-6">
-      {% include 'inc/panels/custom_fields.html' %}
-      {% include 'inc/panels/comments.html' %}
-      {% plugin_right_page object %}
-    </div>
-  </div>
-  <div class="row mb-3">
-    <div class="col col-12 col-md-6">
-      <div class="card">
-        <h2 class="card-header">{% trans "Importing VRFs" %}</h2>
-        {% htmx_table 'ipam:vrf_list' import_target_id=object.pk %}
-      </div>
-    </div>
-    <div class="col col-12 col-md-6">
-      <div class="card">
-        <h2 class="card-header">{% trans "Exporting VRFs" %}</h2>
-        {% htmx_table 'ipam:vrf_list' export_target_id=object.pk %}
-      </div>
-    </div>
-  </div>
-  <div class="row mb-3">
-    <div class="col col-12 col-md-6">
-      <div class="card">
-        <h2 class="card-header">{% trans "Importing L2VPNs" %}</h2>
-        {% htmx_table 'vpn:l2vpn_list' import_target_id=object.pk %}
-      </div>
-    </div>
-    <div class="col col-12 col-md-6">
-      <div class="card">
-        <h2 class="card-header">{% trans "Exporting L2VPNs" %}</h2>
-        {% htmx_table 'vpn:l2vpn_list' export_target_id=object.pk %}
-      </div>
-    </div>
-  </div>
-  <div class="row">
-    <div class="col col-md-12">
-      {% plugin_full_width_page object %}
-    </div>
-  </div>
-{% endblock %}

+ 1 - 59
netbox/templates/ipam/service.html

@@ -1,8 +1,4 @@
 {% extends 'generic/object.html' %}
-{% load buttons %}
-{% load helpers %}
-{% load perms %}
-{% load plugins %}
 {% load i18n %}
 
 {% block breadcrumbs %}
@@ -14,58 +10,4 @@
       </a>
     </li>
   {% endif %}
-{% endblock %}
-
-{% block content %}
-<div class="row mb-3">
-	<div class="col col-12 col-md-6">
-        <div class="card">
-            <h2 class="card-header">{% trans "Service" %}</h2>
-            <table class="table table-hover attr-table">
-                <tr>
-                    <th scope="row">{% trans "Name" %}</th>
-                    <td>{{ object.name }}</td>
-                </tr>
-                <tr>
-                    <th scope="row">{% trans "Parent" %}</th>
-                    <td>{{ object.parent|linkify }}</td>
-                </tr>
-                <tr>
-                    <th scope="row">{% trans "Protocol" %}</th>
-                    <td>{{ object.get_protocol_display }}</td>
-                </tr>
-                <tr>
-                    <th scope="row">{% trans "Ports" %}</th>
-                    <td>{{ object.port_list }}</td>
-                </tr>
-                <tr>
-                    <th scope="row">{% trans "IP Addresses" %}</th>
-                    <td>
-                        {% for ipaddress in object.ipaddresses.all %}
-                            {{ ipaddress|linkify }}<br />
-                        {% empty %}
-                            {{ ''|placeholder }}
-                        {% endfor %}
-                    </td>
-                </tr>
-                <tr>
-                    <th scope="row">{% trans "Description" %}</th>
-                    <td>{{ object.description|placeholder }}</td>
-                </tr>
-            </table>
-        </div>
-        {% plugin_left_page object %}
-    </div>
-    <div class="col col-12 col-md-6">
-      {% include 'inc/panels/custom_fields.html' %}
-      {% include 'inc/panels/tags.html' %}
-      {% include 'inc/panels/comments.html' %}
-      {% plugin_right_page object %}
-    </div>
-</div>
-<div class="row mb-3">
-    <div class="col col-md-12">
-        {% plugin_full_width_page object %}
-    </div>
-</div>
-{% endblock %}
+{% endblock breadcrumbs %}

+ 2 - 0
netbox/templates/ipam/service/attrs/ip_addresses.html

@@ -0,0 +1,2 @@
+{% load helpers %}
+{% for ipaddress in value.all %}{{ ipaddress|linkify }}<br />{% empty %}<span class="text-muted">&mdash;</span>{% endfor %}

+ 0 - 45
netbox/templates/ipam/servicetemplate.html

@@ -1,46 +1 @@
 {% extends 'generic/object.html' %}
-{% load buttons %}
-{% load helpers %}
-{% load perms %}
-{% load plugins %}
-{% load i18n %}
-
-{% block content %}
-  <div class="row mb-3">
-    <div class="col col-12 col-md-6">
-      <div class="card">
-        <h2 class="card-header">{% trans "Application Service Template" %}</h2>
-        <table class="table table-hover attr-table">
-          <tr>
-            <th scope="row">{% trans "Name" %}</th>
-            <td>{{ object.name }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Protocol" %}</th>
-            <td>{{ object.get_protocol_display }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Ports" %}</th>
-            <td>{{ object.port_list }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Description" %}</th>
-            <td>{{ object.description|placeholder }}</td>
-          </tr>
-        </table>
-      </div>
-      {% plugin_left_page object %}
-    </div>
-    <div class="col col-12 col-md-6">
-      {% include 'inc/panels/custom_fields.html' %}
-      {% include 'inc/panels/tags.html' %}
-      {% include 'inc/panels/comments.html' %}
-      {% plugin_right_page object %}
-    </div>
-  </div>
-  <div class="row mb-3">
-    <div class="col col-md-12">
-      {% plugin_full_width_page object %}
-    </div>
-  </div>
-{% endblock %}

+ 0 - 128
netbox/templates/ipam/vlan.html

@@ -1,129 +1 @@
 {% extends 'ipam/vlan/base.html' %}
-{% load helpers %}
-{% load render_table from django_tables2 %}
-{% load plugins %}
-{% load i18n %}
-{% load mptt %}
-
-{% block content %}
-    <div class="row">
-        <div class="col col-12 col-md-6">
-            <div class="card">
-                <h2 class="card-header">{% trans "VLAN" %}</h2>
-                <table class="table table-hover attr-table">
-                    {% if object.site.region %}
-                        <tr>
-                            <th scope="row">{% trans "Region" %}</th>
-                            <td>
-                                {% nested_tree object.site.region %}
-                            </td>
-                        </tr>
-                    {% endif %}
-                    <tr>
-                        <th scope="row">{% trans "Site" %}</th>
-                        <td>{{ object.site|linkify|placeholder }}</td>
-                    </tr>
-                    <tr>
-                        <th scope="row">{% trans "Group" %}</th>
-                        <td>{{ object.group|linkify|placeholder }}</td>
-                    </tr>
-                    <tr>
-                        <th scope="row">{% trans "VLAN ID" %}</th>
-                        <td>{{ object.vid }}</td>
-                    </tr>
-                    <tr>
-                        <th scope="row">{% trans "Name" %}</th>
-                        <td>{{ object.name }}</td>
-                    </tr>
-                    <tr>
-                        <th scope="row">{% trans "Tenant" %}</th>
-                        <td>
-                            {% if object.tenant.group %}
-                                {{ object.tenant.group|linkify }} /
-                            {% endif %}
-                            {{ object.tenant|linkify|placeholder }}
-                        </td>
-                    </tr>
-                    <tr>
-                        <th scope="row">{% trans "Status" %}</th>
-                        <td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
-                    </tr>
-                    <tr>
-                        <th scope="row">{% trans "Role" %}</th>
-                        <td>
-                            {% if object.role %}
-                                <a href="{% url 'ipam:vlan_list' %}?role={{ object.role.slug }}">{{ object.role }}</a>
-                            {% else %}
-                                {{ ''|placeholder }}
-                            {% endif %}
-                        </td>
-                    </tr>
-                    <tr>
-                        <th scope="row">{% trans "Description" %}</th>
-                        <td>{{ object.description|placeholder }}</td>
-                    </tr>
-                    <tr>
-                      <th scope="row">{% trans "Q-in-Q Role" %}</th>
-                      <td>
-                        {% if object.qinq_role %}
-                          {% badge object.get_qinq_role_display bg_color=object.get_qinq_role_color %}
-                        {% else %}
-                          {{ ''|placeholder }}
-                        {% endif %}
-                      </td>
-                    </tr>
-                    {% if object.qinq_role == 'cvlan' %}
-                      <tr>
-                        <th scope="row">{% trans "Q-in-Q SVLAN" %}</th>
-                        <td>{{ object.qinq_svlan|linkify|placeholder }}</td>
-                      </tr>
-                    {% endif %}
-                    <tr>
-                      <th scope="row">{% trans "L2VPN" %}</th>
-                      <td>{{ object.l2vpn_termination.l2vpn|linkify|placeholder }}</td>
-                    </tr>
-                </table>
-            </div>
-            {% plugin_left_page object %}
-        </div>
-        <div class="col col-12 col-md-6">
-          {% include 'inc/panels/custom_fields.html' %}
-          {% include 'inc/panels/tags.html' %}
-          {% include 'inc/panels/comments.html' %}
-          {% plugin_right_page object %}
-        </div>
-    </div>
-    <div class="row">
-      <div class="col col-md-12">
-        <div class="card">
-          <h2 class="card-header">
-            {% trans "Prefixes" %}
-            {% if perms.ipam.add_prefix %}
-              <div class="card-actions">
-                <a href="{% url 'ipam:prefix_add' %}?{% if object.tenant %}tenant={{ object.tenant.pk }}&{% endif %}site={{ object.site.pk }}&vlan={{ object.pk }}" class="btn btn-ghost-primary btn-sm">
-                  <i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add a Prefix" %}
-                </a>
-              </div>
-            {% endif %}
-          </h2>
-          {% htmx_table 'ipam:prefix_list' vlan_id=object.pk %}
-        </div>
-        {% if object.qinq_role == 'svlan' %}
-          <div class="card">
-            <h2 class="card-header">
-              {% trans "Customer VLANs" %}
-              {% if perms.ipam.add_vlan %}
-                <div class="card-actions">
-                  <a href="{% url 'ipam:vlan_add' %}?qinq_role=cvlan&qinq_svlan={{ object.pk }}" class="btn btn-ghost-primary btn-sm">
-                    <i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add a VLAN" %}
-                  </a>
-                </div>
-              {% endif %}
-            </h2>
-            {% htmx_table 'ipam:vlan_list' qinq_svlan_id=object.pk %}
-          </div>
-        {% endif %}
-        {% plugin_full_width_page object %}
-      </div>
-    </div>
-{% endblock %}

+ 1 - 52
netbox/templates/ipam/vlangroup.html

@@ -1,7 +1,5 @@
 {% extends 'generic/object.html' %}
 {% load helpers %}
-{% load plugins %}
-{% load render_table from django_tables2 %}
 {% load i18n %}
 
 {% block breadcrumbs %}
@@ -18,53 +16,4 @@
       <span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add VLAN" %}
     </a>
   {% endif %}
-{% endblock %}
-
-{% block content %}
-<div class="row mb-3">
-	<div class="col col-12 col-md-6">
-    <div class="card">
-      <h2 class="card-header">{% trans "VLAN Group" %}</h2>
-      <table class="table table-hover attr-table">
-        <tr>
-          <th scope="row">{% trans "Name" %}</th>
-          <td>{{ object.name }}</td>
-        </tr>
-        <tr>
-          <th scope="row">{% trans "Description" %}</th>
-          <td>{{ object.description|placeholder }}</td>
-        </tr>
-        <tr>
-          <th scope="row">{% trans "Scope" %}</th>
-          <td>{{ object.scope|linkify|placeholder }}</td>
-        </tr>
-        <tr>
-          <th scope="row">{% trans "VLAN IDs" %}</th>
-          <td>{{ object.vid_ranges_items|join:", " }}</td>
-        </tr>
-        <tr>
-          <th scope="row">Utilization</th>
-          <td>{% utilization_graph object.utilization %}</td>
-        </tr>
-        <tr>
-          <th scope="row">{% trans "Tenant" %}</th>
-          <td>
-            {% if object.tenant.group %}
-              {{ object.tenant.group|linkify }} /
-            {% endif %}
-            {{ object.tenant|linkify|placeholder }}
-          </td>
-        </tr>
-      </table>
-    </div>
-    {% include 'inc/panels/tags.html' %}
-    {% plugin_left_page object %}
-	</div>
-	<div class="col col-12 col-md-6">
-    {% include 'inc/panels/related_objects.html' %}
-    {% include 'inc/panels/comments.html' %}
-    {% include 'inc/panels/custom_fields.html' %}
-    {% plugin_right_page object %}
-  </div>
-</div>
-{% endblock %}
+{% endblock extra_controls %}

+ 1 - 0
netbox/templates/ipam/vlangroup/attrs/vid_ranges.html

@@ -0,0 +1 @@
+{{ value|join:", " }}

+ 0 - 64
netbox/templates/ipam/vlantranslationpolicy.html

@@ -1,65 +1 @@
 {% extends 'generic/object.html' %}
-{% load helpers %}
-{% load plugins %}
-{% load render_table from django_tables2 %}
-{% load i18n %}
-
-{% block content %}
-  <div class="row">
-    <div class="col col-12 col-md-4">
-      <div class="card">
-        <h2 class="card-header">{% trans "VLAN Translation Policy" %}</h2>
-        <table class="table table-hover attr-table">
-          <tr>
-            <th scope="row">{% trans "Name" %}</th>
-            <td>{{ object.name|placeholder }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Description" %}</th>
-            <td>{{ object.description|placeholder }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Rules" %}</th>
-            <td>
-              {% if object.rules.count %}
-                <a href="{% url 'ipam:vlantranslationrule_list' %}?policy_id={{ object.pk }}">{{ object.rules.count }}</a>
-              {% else %}
-                0
-              {% endif %}
-            </td>
-          </tr>
-        </table>
-      </div>
-      {% plugin_left_page object %}
-    </div>
-    <div class="col col-md-8">
-      {% include 'inc/panels/tags.html' %}
-      {% include 'inc/panels/custom_fields.html' %}
-      {% include 'inc/panels/comments.html' %}
-      {% plugin_right_page object %}
-    </div>
-  </div>
-  <div class="row mb-3">
-    <div class="col col-md-12">
-      <div class="card">
-        <h2 class="card-header">
-          {% trans "VLAN Translation Rules" %}
-          {% if perms.ipam.add_vlantranslationrule %}
-            <div class="card-actions">
-              <a href="{% url 'ipam:vlantranslationrule_add' %}?device={{ object.device.pk }}&policy={{ object.pk }}&return_url={{ object.get_absolute_url }}"
-                 class="btn btn-ghost-primary btn-sm">
-                <span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add Rule" %}
-              </a>
-            </div>
-          {% endif %}
-        </h2>
-        {% htmx_table 'ipam:vlantranslationrule_list' policy_id=object.pk %}
-      </div>
-    </div>
-  </div>
-  <div class="row">
-    <div class="col col-md-12">
-      {% plugin_full_width_page object %}
-    </div>
-  </div>
-{% endblock %}

+ 0 - 44
netbox/templates/ipam/vlantranslationrule.html

@@ -1,45 +1 @@
 {% extends 'generic/object.html' %}
-{% load helpers %}
-{% load plugins %}
-{% load render_table from django_tables2 %}
-{% load i18n %}
-
-{% block content %}
-  <div class="row">
-    <div class="col col-12 col-md-4">
-      <div class="card">
-        <h2 class="card-header">{% trans "VLAN Translation Rule" %}</h2>
-        <table class="table table-hover attr-table">
-          <tr>
-            <th scope="row">{% trans "Policy" %}</th>
-            <td>{{ object.policy|linkify }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Local VID" %}</th>
-            <td>{{ object.local_vid }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Remote VID" %}</th>
-            <td>{{ object.remote_vid }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Description" %}</th>
-            <td>{{ object.description }}</td>
-          </tr>
-        </table>
-      </div>
-      {% plugin_left_page object %}
-    </div>
-    <div class="col col-12 col-md-8">
-      {% include 'inc/panels/tags.html' %}
-      {% include 'inc/panels/custom_fields.html' %}
-      {% include 'inc/panels/comments.html' %}
-      {% plugin_right_page object %}
-    </div>
-  </div>
-  <div class="row">
-    <div class="col col-md-12">
-      {% plugin_full_width_page object %}
-    </div>
-  </div>
-{% endblock %}

+ 0 - 60
netbox/templates/ipam/vrf.html

@@ -1,61 +1 @@
 {% extends 'generic/object.html' %}
-{% load buttons %}
-{% load helpers %}
-{% load plugins %}
-{% load i18n %}
-
-{% block title %}{% trans "VRF" %} {{ object }}{% endblock %}
-
-{% block content %}
-<div class="row">
-	<div class="col col-12 col-md-6">
-      <div class="card">
-          <h2 class="card-header">{% trans "VRF" %}</h2>
-          <table class="table table-hover attr-table">
-              <tr>
-                  <th scope="row">{% trans "Route Distinguisher" %}</th>
-                  <td>{{ object.rd|placeholder }}</td>
-              </tr>
-              <tr>
-                  <th scope="row">{% trans "Tenant" %}</th>
-                  <td>
-                      {% if object.tenant.group %}
-                          {{ object.tenant.group|linkify }} /
-                      {% endif %}
-                      {{ object.tenant|linkify|placeholder }}
-                  </td>
-              </tr>
-              <tr>
-                  <th scope="row">{% trans "Unique IP Space" %}</th>
-                  <td>{% checkmark object.enforce_unique %}</td>
-              </tr>
-              <tr>
-                  <th scope="row">{% trans "Description" %}</th>
-                  <td>{{ object.description|placeholder }}</td>
-              </tr>
-          </table>
-      </div>
-      {% include 'inc/panels/tags.html' %}
-      {% plugin_left_page object %}
-  </div>
-  <div class="col col-12 col-md-6">
-    {% include 'inc/panels/related_objects.html' %}
-    {% include 'inc/panels/custom_fields.html' %}
-    {% include 'inc/panels/comments.html' %}
-    {% plugin_right_page object %}
-	</div>
-</div>
-<div class="row">
-	<div class="col col-12 col-md-6">
-    {% include 'inc/panel_table.html' with table=import_targets_table heading="Import Route Targets" %}
-  </div>
-	<div class="col col-12 col-md-6">
-    {% include 'inc/panel_table.html' with table=export_targets_table heading="Export Route Targets" %}
-  </div>
-</div>
-<div class="row">
-  <div class="col col-md-12">
-    {% plugin_full_width_page object %}
-  </div>
-</div>
-{% endblock %}