Просмотр исходного кода

#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 1 день назад
Родитель
Сommit
99e9d96787
32 измененных файлов с 683 добавлено и 1316 удалено
  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):
     def get_status_color(self):
         return PrefixStatusChoices.colors.get(self.status)
         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):
     def get_parents(self, include_self=False):
         """
         """
         Return all containing Prefixes in the hierarchy.
         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.urls import reverse
 from django.utils.translation import gettext_lazy as _
 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):
 class FHRPGroupAssignmentsPanel(panels.ObjectPanel):
     """
     """
     A panel which lists all FHRP group assignments for a given object.
     A panel which lists all FHRP group assignments for a given object.
     """
     """
-
     template_name = 'ipam/panels/fhrp_groups.html'
     template_name = 'ipam/panels/fhrp_groups.html'
     title = _('FHRP Groups')
     title = _('FHRP Groups')
     actions = [
     actions = [
@@ -35,3 +36,220 @@ class FHRPGroupAssignmentsPanel(panels.ObjectPanel):
             label=_('Assign Group'),
             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.filtersets import InterfaceFilterSet
 from dcim.forms import InterfaceFilterForm
 from dcim.forms import InterfaceFilterForm
 from dcim.models import Device, Interface, Site
 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.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 netbox.views import generic
 from utilities.query import count_related
 from utilities.query import count_related
 from utilities.tables import get_table_ordering
 from utilities.tables import get_table_ordering
@@ -23,6 +31,7 @@ from . import filtersets, forms, tables
 from .choices import PrefixStatusChoices
 from .choices import PrefixStatusChoices
 from .constants import *
 from .constants import *
 from .models import *
 from .models import *
+from .ui import panels
 from .utils import add_available_vlans, add_requested_prefixes, annotate_ip_space
 from .utils import add_available_vlans, add_requested_prefixes, annotate_ip_space
 
 
 #
 #
@@ -41,6 +50,27 @@ class VRFListView(generic.ObjectListView):
 @register_model_view(VRF)
 @register_model_view(VRF)
 class VRFView(GetRelatedModelsMixin, generic.ObjectView):
 class VRFView(GetRelatedModelsMixin, generic.ObjectView):
     queryset = VRF.objects.all()
     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):
     def get_extra_context(self, request, instance):
         import_targets_table = tables.RouteTargetTable(
         import_targets_table = tables.RouteTargetTable(
@@ -134,6 +164,50 @@ class RouteTargetListView(generic.ObjectListView):
 @register_model_view(RouteTarget)
 @register_model_view(RouteTarget)
 class RouteTargetView(generic.ObjectView):
 class RouteTargetView(generic.ObjectView):
     queryset = RouteTarget.objects.all()
     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)
 @register_model_view(RouteTarget, 'add', detail=False)
@@ -192,6 +266,17 @@ class RIRListView(generic.ObjectListView):
 @register_model_view(RIR)
 @register_model_view(RIR)
 class RIRView(GetRelatedModelsMixin, generic.ObjectView):
 class RIRView(GetRelatedModelsMixin, generic.ObjectView):
     queryset = RIR.objects.all()
     queryset = RIR.objects.all()
+    layout = layout.SimpleLayout(
+        left_panels=[
+            panels.RIRPanel(),
+            TagsPanel(),
+        ],
+        right_panels=[
+            RelatedObjectsPanel(),
+            CommentsPanel(),
+            CustomFieldsPanel(),
+        ],
+    )
 
 
     def get_extra_context(self, request, instance):
     def get_extra_context(self, request, instance):
         return {
         return {
@@ -257,6 +342,16 @@ class ASNRangeListView(generic.ObjectListView):
 @register_model_view(ASNRange)
 @register_model_view(ASNRange)
 class ASNRangeView(generic.ObjectView):
 class ASNRangeView(generic.ObjectView):
     queryset = ASNRange.objects.all()
     queryset = ASNRange.objects.all()
+    layout = layout.SimpleLayout(
+        left_panels=[
+            panels.ASNRangePanel(),
+            TagsPanel(),
+        ],
+        right_panels=[
+            CommentsPanel(),
+            CustomFieldsPanel(),
+        ],
+    )
 
 
 
 
 @register_model_view(ASNRange, 'asns')
 @register_model_view(ASNRange, 'asns')
@@ -337,6 +432,17 @@ class ASNListView(generic.ObjectListView):
 @register_model_view(ASN)
 @register_model_view(ASN)
 class ASNView(GetRelatedModelsMixin, generic.ObjectView):
 class ASNView(GetRelatedModelsMixin, generic.ObjectView):
     queryset = ASN.objects.all()
     queryset = ASN.objects.all()
+    layout = layout.SimpleLayout(
+        left_panels=[
+            panels.ASNPanel(),
+            TagsPanel(),
+        ],
+        right_panels=[
+            RelatedObjectsPanel(),
+            CustomFieldsPanel(),
+            CommentsPanel(),
+        ],
+    )
 
 
     def get_extra_context(self, request, instance):
     def get_extra_context(self, request, instance):
         return {
         return {
@@ -412,6 +518,16 @@ class AggregateListView(generic.ObjectListView):
 @register_model_view(Aggregate)
 @register_model_view(Aggregate)
 class AggregateView(generic.ObjectView):
 class AggregateView(generic.ObjectView):
     queryset = Aggregate.objects.all()
     queryset = Aggregate.objects.all()
+    layout = layout.SimpleLayout(
+        left_panels=[
+            panels.AggregatePanel(),
+        ],
+        right_panels=[
+            CustomFieldsPanel(),
+            TagsPanel(),
+            CommentsPanel(),
+        ],
+    )
 
 
 
 
 @register_model_view(Aggregate, 'prefixes')
 @register_model_view(Aggregate, 'prefixes')
@@ -506,6 +622,17 @@ class RoleListView(generic.ObjectListView):
 @register_model_view(Role)
 @register_model_view(Role)
 class RoleView(GetRelatedModelsMixin, generic.ObjectView):
 class RoleView(GetRelatedModelsMixin, generic.ObjectView):
     queryset = Role.objects.all()
     queryset = Role.objects.all()
+    layout = layout.SimpleLayout(
+        left_panels=[
+            panels.RolePanel(),
+            TagsPanel(),
+        ],
+        right_panels=[
+            RelatedObjectsPanel(),
+            CommentsPanel(),
+            CustomFieldsPanel(),
+        ],
+    )
 
 
     def get_extra_context(self, request, instance):
     def get_extra_context(self, request, instance):
         return {
         return {
@@ -569,15 +696,23 @@ class PrefixListView(generic.ObjectListView):
 @register_model_view(Prefix)
 @register_model_view(Prefix)
 class PrefixView(generic.ObjectView):
 class PrefixView(generic.ObjectView):
     queryset = Prefix.objects.all()
     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):
     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 table
         parent_prefixes = Prefix.objects.restrict(request.user, 'view').filter(
         parent_prefixes = Prefix.objects.restrict(request.user, 'view').filter(
             Q(vrf=instance.vrf) | Q(vrf__isnull=True, status=PrefixStatusChoices.STATUS_CONTAINER)
             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)
         duplicate_prefix_table.configure(request)
 
 
-        return {
-            'aggregate': aggregate,
+        context = {
             'parent_prefix_table': parent_prefix_table,
             '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')
 @register_model_view(Prefix, 'prefixes')
@@ -756,6 +892,19 @@ class IPRangeListView(generic.ObjectListView):
 @register_model_view(IPRange)
 @register_model_view(IPRange)
 class IPRangeView(generic.ObjectView):
 class IPRangeView(generic.ObjectView):
     queryset = IPRange.objects.all()
     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):
     def get_extra_context(self, request, instance):
 
 
@@ -853,6 +1002,23 @@ class IPAddressListView(generic.ObjectListView):
 @register_model_view(IPAddress)
 @register_model_view(IPAddress)
 class IPAddressView(generic.ObjectView):
 class IPAddressView(generic.ObjectView):
     queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant')
     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):
     def get_extra_context(self, request, instance):
         # Parent prefixes table
         # Parent prefixes table
@@ -885,10 +1051,12 @@ class IPAddressView(generic.ObjectView):
         duplicate_ips_table = tables.IPAddressTable(duplicate_ips[:10], orderable=False)
         duplicate_ips_table = tables.IPAddressTable(duplicate_ips[:10], orderable=False)
         duplicate_ips_table.configure(request)
         duplicate_ips_table.configure(request)
 
 
-        return {
+        context = {
             'parent_prefixes_table': parent_prefixes_table,
             '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)
 @register_model_view(IPAddress, 'add', detail=False)
@@ -1038,6 +1206,17 @@ class VLANGroupListView(generic.ObjectListView):
 @register_model_view(VLANGroup)
 @register_model_view(VLANGroup)
 class VLANGroupView(GetRelatedModelsMixin, generic.ObjectView):
 class VLANGroupView(GetRelatedModelsMixin, generic.ObjectView):
     queryset = VLANGroup.objects.annotate_utilization()
     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):
     def get_extra_context(self, request, instance):
         return {
         return {
@@ -1125,19 +1304,32 @@ class VLANTranslationPolicyListView(generic.ObjectListView):
 
 
 
 
 @register_model_view(VLANTranslationPolicy)
 @register_model_view(VLANTranslationPolicy)
-class VLANTranslationPolicyView(GetRelatedModelsMixin, generic.ObjectView):
+class VLANTranslationPolicyView(generic.ObjectView):
     queryset = VLANTranslationPolicy.objects.all()
     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)
 @register_model_view(VLANTranslationPolicy, 'add', detail=False)
@@ -1193,13 +1385,17 @@ class VLANTranslationRuleListView(generic.ObjectListView):
 
 
 
 
 @register_model_view(VLANTranslationRule)
 @register_model_view(VLANTranslationRule)
-class VLANTranslationRuleView(GetRelatedModelsMixin, generic.ObjectView):
+class VLANTranslationRuleView(generic.ObjectView):
     queryset = VLANTranslationRule.objects.all()
     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)
 @register_model_view(VLANTranslationRule, 'add', detail=False)
@@ -1251,7 +1447,36 @@ class FHRPGroupListView(generic.ObjectListView):
 
 
 @register_model_view(FHRPGroup)
 @register_model_view(FHRPGroup)
 class FHRPGroupView(GetRelatedModelsMixin, generic.ObjectView):
 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):
     def get_extra_context(self, request, instance):
         # Get assigned interfaces
         # Get assigned interfaces
@@ -1276,7 +1501,6 @@ class FHRPGroupView(GetRelatedModelsMixin, generic.ObjectView):
                 ),
                 ),
             ),
             ),
             'members_table': members_table,
             'members_table': members_table,
-            'member_count': FHRPGroupAssignment.objects.filter(group=instance).count(),
         }
         }
 
 
 
 
@@ -1379,17 +1603,35 @@ class VLANListView(generic.ObjectListView):
 @register_model_view(VLAN)
 @register_model_view(VLAN)
 class VLANView(generic.ObjectView):
 class VLANView(generic.ObjectView):
     queryset = VLAN.objects.all()
     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')
 @register_model_view(VLAN, 'interfaces')
@@ -1483,6 +1725,16 @@ class ServiceTemplateListView(generic.ObjectListView):
 @register_model_view(ServiceTemplate)
 @register_model_view(ServiceTemplate)
 class ServiceTemplateView(generic.ObjectView):
 class ServiceTemplateView(generic.ObjectView):
     queryset = ServiceTemplate.objects.all()
     queryset = ServiceTemplate.objects.all()
+    layout = layout.SimpleLayout(
+        left_panels=[
+            panels.ServiceTemplatePanel(),
+        ],
+        right_panels=[
+            CustomFieldsPanel(),
+            TagsPanel(),
+            CommentsPanel(),
+        ],
+    )
 
 
 
 
 @register_model_view(ServiceTemplate, 'add', detail=False)
 @register_model_view(ServiceTemplate, 'add', detail=False)
@@ -1539,6 +1791,16 @@ class ServiceListView(generic.ObjectListView):
 @register_model_view(Service)
 @register_model_view(Service)
 class ServiceView(generic.ObjectView):
 class ServiceView(generic.ObjectView):
     queryset = Service.objects.all()
     queryset = Service.objects.all()
+    layout = layout.SimpleLayout(
+        left_panels=[
+            panels.ServicePanel(),
+        ],
+        right_panels=[
+            CustomFieldsPanel(),
+            TagsPanel(),
+            CommentsPanel(),
+        ],
+    )
 
 
     def get_extra_context(self, request, instance):
     def get_extra_context(self, request, instance):
         context = {}
         context = {}

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

@@ -1,62 +1 @@
 {% extends 'ipam/aggregate/base.html' %}
 {% 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' %}
 {% extends 'generic/object.html' %}
-{% load buttons %}
-{% load helpers %}
-{% load plugins %}
-{% load render_table from django_tables2 %}
 {% load i18n %}
 {% load i18n %}
 
 
 {% block breadcrumbs %}
 {% 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>
     <li class="breadcrumb-item"><a href="{% url 'ipam:asn_list' %}?range_id={{ object.range.pk }}">{{ object.range }}</a></li>
   {% endif %}
   {% endif %}
 {% endblock breadcrumbs %}
 {% 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' %}
 {% 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' %}
 {% extends 'generic/object.html' %}
-{% load helpers %}
-{% load plugins %}
-{% load render_table from django_tables2 %}
 {% load i18n %}
 {% load i18n %}
 
 
 {# Omit assigned IP addresses from object representation #}
 {# Omit assigned IP addresses from object representation #}
@@ -11,75 +8,3 @@
   {{ block.super }}
   {{ block.super }}
   <li class="breadcrumb-item"><a href="{% url 'ipam:fhrpgroup_list' %}?protocol={{ object.protocol }}">{{ object.get_protocol_display }}</a></li>
   <li class="breadcrumb-item"><a href="{% url 'ipam:fhrpgroup_list' %}?protocol={{ object.protocol }}">{{ object.get_protocol_display }}</a></li>
 {% endblock breadcrumbs %}
 {% 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' %}
 {% 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' %}
 {% 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' %}
 {% extends 'generic/object.html' %}
 {% load buttons %}
 {% load buttons %}
 {% load helpers %}
 {% load helpers %}
+{% load i18n %}
 
 
 {% block breadcrumbs %}
 {% block breadcrumbs %}
   {{ block.super }}
   {{ 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>
     <li class="breadcrumb-item"><a href="{% url 'ipam:prefix_list' %}?vrf_id={{ object.vrf.pk }}">{{ object.vrf }}</a></li>
   {% endif %}
   {% endif %}
 {% endblock %}
 {% 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' %}
 {% extends 'generic/object.html' %}
-{% load helpers %}
-{% load plugins %}
-{% load render_table from django_tables2 %}
 {% load i18n %}
 {% load i18n %}
 
 
 {% block extra_controls %}
 {% block extra_controls %}
@@ -11,40 +8,3 @@
     </a>
     </a>
   {% endif %}
   {% endif %}
 {% endblock extra_controls %}
 {% 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' %}
 {% extends 'generic/object.html' %}
-{% load helpers %}
-{% load plugins %}
-{% load render_table from django_tables2 %}
 {% load i18n %}
 {% load i18n %}
 
 
 {% block extra_controls %}
 {% block extra_controls %}
@@ -11,40 +8,3 @@
     </a>
     </a>
   {% endif %}
   {% endif %}
 {% endblock extra_controls %}
 {% 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' %}
 {% 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' %}
 {% extends 'generic/object.html' %}
-{% load buttons %}
-{% load helpers %}
-{% load perms %}
-{% load plugins %}
 {% load i18n %}
 {% load i18n %}
 
 
 {% block breadcrumbs %}
 {% block breadcrumbs %}
@@ -14,58 +10,4 @@
       </a>
       </a>
     </li>
     </li>
   {% endif %}
   {% 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' %}
 {% 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' %}
 {% 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' %}
 {% extends 'generic/object.html' %}
 {% load helpers %}
 {% load helpers %}
-{% load plugins %}
-{% load render_table from django_tables2 %}
 {% load i18n %}
 {% load i18n %}
 
 
 {% block breadcrumbs %}
 {% block breadcrumbs %}
@@ -18,53 +16,4 @@
       <span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add VLAN" %}
       <span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add VLAN" %}
     </a>
     </a>
   {% endif %}
   {% 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' %}
 {% 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' %}
 {% 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' %}
 {% 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 %}