From cc47afc40184bd9c74a3ed492e84143245364367 Mon Sep 17 00:00:00 2001 From: Martin Hauser Date: Fri, 20 Feb 2026 14:58:20 +0100 Subject: [PATCH] refactor(virtualization): Port to declarative layout Add declarative layout panels for Cluster, Cluster Group, Cluster Type, Virtual Disk, and VM Interface, including addressing, VLAN assignment, and FHRP group handling. Expand the declarative layout primitives: - add GFK attribute rendering support - add panel for rendering context-provided tables - update templates to support new panels/attrs Closes #20923 --- netbox/ipam/ui/__init__.py | 0 netbox/ipam/ui/panels.py | 37 +++++ netbox/netbox/ui/attrs.py | 27 ++++ netbox/netbox/ui/panels.py | 40 +++++ netbox/templates/ipam/panels/fhrp_groups.html | 47 ++++++ netbox/templates/ui/attrs/generic_object.html | 3 + netbox/templates/ui/panels/context_table.html | 6 + netbox/templates/virtualization/cluster.html | 94 ----------- .../virtualization/clustergroup.html | 36 ----- .../templates/virtualization/clustertype.html | 42 ----- .../panels/cluster_resources.html | 34 ++++ .../templates/virtualization/virtualdisk.html | 45 ------ .../virtualdisk/attrs/size.html | 2 + .../templates/virtualization/vminterface.html | 152 ------------------ netbox/virtualization/ui/panels.py | 42 +++++ netbox/virtualization/views.py | 92 ++++++++++- 16 files changed, 329 insertions(+), 370 deletions(-) create mode 100644 netbox/ipam/ui/__init__.py create mode 100644 netbox/ipam/ui/panels.py create mode 100644 netbox/templates/ipam/panels/fhrp_groups.html create mode 100644 netbox/templates/ui/attrs/generic_object.html create mode 100644 netbox/templates/ui/panels/context_table.html create mode 100644 netbox/templates/virtualization/panels/cluster_resources.html create mode 100644 netbox/templates/virtualization/virtualdisk/attrs/size.html diff --git a/netbox/ipam/ui/__init__.py b/netbox/ipam/ui/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/ipam/ui/panels.py b/netbox/ipam/ui/panels.py new file mode 100644 index 000000000..a2148057a --- /dev/null +++ b/netbox/ipam/ui/panels.py @@ -0,0 +1,37 @@ +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 + + +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 = [ + actions.AddObject( + 'ipam.FHRPGroup', + url_params={ + 'return_url': lambda ctx: reverse( + 'ipam:fhrpgroupassignment_add', + query={ + 'interface_type': ContentType.objects.get_for_model(ctx['object']).pk, + 'interface_id': ctx['object'].pk, + }, + ), + }, + label=_('Create Group'), + ), + actions.AddObject( + 'ipam.FHRPGroupAssignment', + url_params={ + 'interface_type': lambda ctx: ContentType.objects.get_for_model(ctx['object']).pk, + 'interface_id': lambda ctx: ctx['object'].pk, + }, + label=_('Assign Group'), + ), + ] diff --git a/netbox/netbox/ui/attrs.py b/netbox/netbox/ui/attrs.py index 37cc1ba12..c20c99b83 100644 --- a/netbox/netbox/ui/attrs.py +++ b/netbox/netbox/ui/attrs.py @@ -10,6 +10,7 @@ __all__ = ( 'BooleanAttr', 'ColorAttr', 'ChoiceAttr', + 'GenericForeignKeyAttr', 'GPSCoordinatesAttr', 'ImageAttr', 'NestedObjectAttr', @@ -279,6 +280,32 @@ class NestedObjectAttr(ObjectAttribute): } +class GenericForeignKeyAttr(ObjectAttribute): + """ + An attribute representing a related generic relation object. + + This attribute is similar to `RelatedObjectAttr` but uses the + ContentType of the related object to be displayed alongside the value. + + Parameters: + linkify (bool): If True, the rendered value will be hyperlinked + to the related object's detail view + """ + template_name = 'ui/attrs/generic_object.html' + + def __init__(self, *args, linkify=None, **kwargs): + super().__init__(*args, **kwargs) + self.linkify = linkify + + def get_context(self, obj, context): + value = self.get_value(obj) + content_type = value._meta.verbose_name + return { + 'content_type': content_type, + 'linkify': self.linkify, + } + + class AddressAttr(ObjectAttribute): """ A physical or mailing address. diff --git a/netbox/netbox/ui/panels.py b/netbox/netbox/ui/panels.py index d87cd6c49..55e36b704 100644 --- a/netbox/netbox/ui/panels.py +++ b/netbox/netbox/ui/panels.py @@ -12,6 +12,7 @@ from utilities.views import get_viewname __all__ = ( 'CommentsPanel', + 'ContextTablePanel', 'JSONPanel', 'NestedGroupObjectPanel', 'ObjectAttributesPanel', @@ -339,3 +340,42 @@ class PluginContentPanel(Panel): def render(self, context): obj = context.get('object') return _get_registered_content(obj, self.method, context) + + +class ContextTablePanel(ObjectPanel): + """ + A panel which renders a django-tables2/NetBoxTable instance provided + via the view's extra context. + + This is useful when you already have a fully constructed table + (custom queryset, special columns, no list view) and just want to + render it inside a declarative layout panel. + + Parameters: + table (str | callable): Either the context key holding the table + (e.g. "vlan_table") or a callable which accepts the template + context and returns a table instance. + """ + template_name = 'ui/panels/context_table.html' + + def __init__(self, table, **kwargs): + super().__init__(**kwargs) + self.table = table + + def _resolve_table(self, context): + if callable(self.table): + return self.table(context) + return context.get(self.table) + + def get_context(self, context): + table = self._resolve_table(context) + return { + **super().get_context(context), + 'table': table, + } + + def render(self, context): + table = self._resolve_table(context) + if table is None: + return '' + return super().render(context) diff --git a/netbox/templates/ipam/panels/fhrp_groups.html b/netbox/templates/ipam/panels/fhrp_groups.html new file mode 100644 index 000000000..aa9a91dcc --- /dev/null +++ b/netbox/templates/ipam/panels/fhrp_groups.html @@ -0,0 +1,47 @@ +{% extends "ui/panels/_base.html" %} +{% load perms %} +{% load i18n %} + +{% block panel_content %} + + + + + + + + + + + + {% for assignment in object.fhrp_group_assignments.all %} + + + + + + + + {% empty %} + + + + {% endfor %} + +
{% trans "Group" %}{% trans "Protocol" %}{% trans "Virtual IPs" %}{% trans "Priority" %}
{{ assignment.group|linkify:"group_id" }}{{ assignment.group.get_protocol_display }} + {% for ipaddress in assignment.group.ip_addresses.all %} + {{ ipaddress|linkify }}{% if not forloop.last %}
{% endif %} + {% endfor %} +
{{ assignment.priority }} + {% if request.user|can_change:assignment %} + + + + {% endif %} + {% if request.user|can_delete:assignment %} + + + + {% endif %} +
{% trans "None" %}
+{% endblock panel_content %} diff --git a/netbox/templates/ui/attrs/generic_object.html b/netbox/templates/ui/attrs/generic_object.html new file mode 100644 index 000000000..6ffabb94a --- /dev/null +++ b/netbox/templates/ui/attrs/generic_object.html @@ -0,0 +1,3 @@ + + {% if linkify %}{{ value|linkify }}{% else %}{{ value }}{% endif %}{% if content_type %} ({{ content_type }}){% endif %} + diff --git a/netbox/templates/ui/panels/context_table.html b/netbox/templates/ui/panels/context_table.html new file mode 100644 index 000000000..1297defe5 --- /dev/null +++ b/netbox/templates/ui/panels/context_table.html @@ -0,0 +1,6 @@ +{% extends "ui/panels/_base.html" %} +{% load render_table from django_tables2 %} + +{% block panel_content %} + {% render_table table 'inc/table.html' %} +{% endblock panel_content %} diff --git a/netbox/templates/virtualization/cluster.html b/netbox/templates/virtualization/cluster.html index 0dc7efe60..b2b7c3996 100644 --- a/netbox/templates/virtualization/cluster.html +++ b/netbox/templates/virtualization/cluster.html @@ -1,95 +1 @@ {% extends 'virtualization/cluster/base.html' %} -{% load helpers %} -{% load plugins %} -{% load i18n %} - -{% block content %} -
-
-
-

{% trans "Cluster" %}

- - - - - - - - - - - - - - - - - - - - - - - - - - - - {% if object.scope %} - - {% else %} - - {% endif %} - -
{% trans "Name" %}{{ object.name }}
{% trans "Type" %}{{ object.type|linkify }}
{% trans "Status" %}{% badge object.get_status_display bg_color=object.get_status_color %}
{% trans "Description" %}{{ object.description|placeholder }}
{% trans "Group" %}{{ object.group|linkify|placeholder }}
{% trans "Tenant" %} - {% if object.tenant.group %} - {{ object.tenant.group|linkify }} / - {% endif %} - {{ object.tenant|linkify|placeholder }} -
{% trans "Scope" %}{{ object.scope|linkify }} ({% trans object.scope_type.name %}){{ ''|placeholder }}
-
- {% include 'inc/panels/comments.html' %} - {% plugin_left_page object %} -
-
-
-

{% trans "Allocated Resources" %}

- - - - - - - - - - - - - -
{% trans "Virtual CPUs" %}{{ vcpus_sum|placeholder }}
{% trans "Memory" %} - {% if memory_sum %} - {{ memory_sum|humanize_ram_megabytes }} - {% else %} - {{ ''|placeholder }} - {% endif %} -
{% trans "Disk Space" %} - {% if disk_sum %} - {{ disk_sum|humanize_disk_megabytes }} - {% else %} - {{ ''|placeholder }} - {% endif %} -
-
- {% include 'inc/panels/related_objects.html' %} - {% include 'inc/panels/custom_fields.html' %} - {% include 'inc/panels/tags.html' %} - {% plugin_right_page object %} -
-
-
-
- {% plugin_full_width_page object %} -
-
-{% endblock %} diff --git a/netbox/templates/virtualization/clustergroup.html b/netbox/templates/virtualization/clustergroup.html index b45ae60b4..fe5084b87 100644 --- a/netbox/templates/virtualization/clustergroup.html +++ b/netbox/templates/virtualization/clustergroup.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,36 +8,3 @@ {% endif %} {% endblock extra_controls %} - -{% block content %} -
-
-
-

{% trans "Cluster Group" %}

- - - - - - - - - -
{% trans "Name" %}{{ object.name }}
{% trans "Description" %}{{ object.description|placeholder }}
-
- {% include 'inc/panels/tags.html' %} - {% plugin_left_page object %} -
-
- {% include 'inc/panels/related_objects.html' %} - {% include 'inc/panels/comments.html' %} - {% include 'inc/panels/custom_fields.html' %} - {% plugin_right_page object %} -
-
-
-
- {% plugin_full_width_page object %} -
-
-{% endblock %} diff --git a/netbox/templates/virtualization/clustertype.html b/netbox/templates/virtualization/clustertype.html index 016320f51..039d63be7 100644 --- a/netbox/templates/virtualization/clustertype.html +++ b/netbox/templates/virtualization/clustertype.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,42 +8,3 @@ {% endif %} {% endblock extra_controls %} - -{% block content %} -
-
-
-

{% trans "Cluster Type" %}

- - - - - - - - - - - - - -
{% trans "Name" %}{{ object.name }}
{% trans "Description" %}{{ object.description|placeholder }}
{% trans "Clusters" %} - {{ object.clusters.count }} -
-
- {% include 'inc/panels/tags.html' %} - {% plugin_left_page object %} -
-
- {% include 'inc/panels/related_objects.html' %} - {% include 'inc/panels/comments.html' %} - {% include 'inc/panels/custom_fields.html' %} - {% plugin_right_page object %} -
-
-
-
- {% plugin_full_width_page object %} -
-
-{% endblock %} diff --git a/netbox/templates/virtualization/panels/cluster_resources.html b/netbox/templates/virtualization/panels/cluster_resources.html new file mode 100644 index 000000000..bdaec3d89 --- /dev/null +++ b/netbox/templates/virtualization/panels/cluster_resources.html @@ -0,0 +1,34 @@ +{% load helpers %} +{% load i18n %} + +
+

{% trans "Allocated Resources" %}

+ + + + + + + + + + + + + +
{% trans "Virtual CPUs" %}{{ vcpus_sum|placeholder }}
{% trans "Memory" %} + {% if memory_sum %} + {{ memory_sum|humanize_ram_megabytes }} + {% else %} + {{ ''|placeholder }} + {% endif %} +
+ {% trans "Disk Space" %} + + {% if disk_sum %} + {{ disk_sum|humanize_disk_megabytes }} + {% else %} + {{ ''|placeholder }} + {% endif %} +
+
diff --git a/netbox/templates/virtualization/virtualdisk.html b/netbox/templates/virtualization/virtualdisk.html index 852863c00..720e410ae 100644 --- a/netbox/templates/virtualization/virtualdisk.html +++ b/netbox/templates/virtualization/virtualdisk.html @@ -10,48 +10,3 @@ {{ object.virtual_machine }} {% endblock %} - -{% block content %} -
-
-
-

{% trans "Virtual Disk" %}

- - - - - - - - - - - - - - - - - -
{% trans "Virtual Machine" %}{{ object.virtual_machine|linkify }}
{% trans "Name" %}{{ object.name }}
{% trans "Size" %} - {% if object.size %} - {{ object.size|humanize_disk_megabytes }} - {% else %} - {{ ''|placeholder }} - {% endif %} -
{% trans "Description" %}{{ object.description|placeholder }}
-
- {% include 'inc/panels/tags.html' %} - {% plugin_left_page object %} -
-
- {% include 'inc/panels/custom_fields.html' %} - {% plugin_right_page object %} -
-
-
-
- {% plugin_full_width_page object %} -
-
-{% endblock %} diff --git a/netbox/templates/virtualization/virtualdisk/attrs/size.html b/netbox/templates/virtualization/virtualdisk/attrs/size.html new file mode 100644 index 000000000..1185dbc20 --- /dev/null +++ b/netbox/templates/virtualization/virtualdisk/attrs/size.html @@ -0,0 +1,2 @@ +{% load helpers %} +{{ value|humanize_disk_megabytes }} diff --git a/netbox/templates/virtualization/vminterface.html b/netbox/templates/virtualization/vminterface.html index b8ae28c5d..73127982d 100644 --- a/netbox/templates/virtualization/vminterface.html +++ b/netbox/templates/virtualization/vminterface.html @@ -1,8 +1,4 @@ {% extends 'generic/object.html' %} -{% load helpers %} -{% load plugins %} -{% load render_table from django_tables2 %} -{% load i18n %} {% block breadcrumbs %} {{ block.super }} @@ -10,151 +6,3 @@ {{ object.virtual_machine }} {% endblock %} - -{% block content %} -
-
-
-

{% trans "Interface" %}

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {% if object.mode == 'q-in-q' %} - - - - - {% endif %} - - - - -
{% trans "Virtual Machine" %}{{ object.virtual_machine|linkify }}
{% trans "Name" %}{{ object.name }}
{% trans "Enabled" %} - {% if object.enabled %} - - {% else %} - - {% endif %} -
{% trans "Parent" %}{{ object.parent|linkify|placeholder }}
{% trans "Bridge" %}{{ object.bridge|linkify|placeholder }}
{% trans "Description" %}{{ object.description|placeholder }}
{% trans "MTU" %}{{ object.mtu|placeholder }}
{% trans "802.1Q Mode" %}{{ object.get_mode_display|placeholder }}
{% trans "Q-in-Q SVLAN" %}{{ object.qinq_svlan|linkify|placeholder }}
{% trans "Tunnel" %}{{ object.tunnel_termination.tunnel|linkify|placeholder }}
-
- {% include 'inc/panels/tags.html' %} - {% plugin_left_page object %} -
-
- {% include 'inc/panels/custom_fields.html' %} -
-

{% trans "Addressing" %}

- - - - - - - - - - - - - -
{% trans "MAC Address" %} - {% if object.primary_mac_address %} - {{ object.primary_mac_address|linkify }} - {% trans "Primary" %} - {% else %} - {{ ''|placeholder }} - {% endif %} -
{% trans "VRF" %}{{ object.vrf|linkify|placeholder }}
{% trans "VLAN Translation" %}{{ object.vlan_translation_policy|linkify|placeholder }}
-
- {% include 'ipam/inc/panels/fhrp_groups.html' %} - {% plugin_right_page object %} -
-
-
-
-
-

- {% trans "IP Addresses" %} - {% if perms.ipam.add_ipaddress %} - - {% endif %} -

- {% htmx_table 'ipam:ipaddress_list' vminterface_id=object.pk %} -
-
-
-
-
-
-

- {% trans "MAC Addresses" %} - {% if perms.ipam.add_macaddress %} - - {% endif %} -

- {% htmx_table 'dcim:macaddress_list' vminterface_id=object.pk %} -
-
-
-
-
- {% include 'inc/panel_table.html' with table=vlan_table heading="VLANs" %} -
-
-{% if object.vlan_translation_policy %} -
-
- {% include 'inc/panel_table.html' with table=vlan_translation_table heading="VLAN Translation" %} -
-
-{% endif %} -
-
- {% include 'inc/panel_table.html' with table=child_interfaces_table heading="Child Interfaces" %} -
-
-
-
- {% plugin_full_width_page object %} -
-
-{% endblock %} diff --git a/netbox/virtualization/ui/panels.py b/netbox/virtualization/ui/panels.py index bff967cb6..fef6de3f1 100644 --- a/netbox/virtualization/ui/panels.py +++ b/netbox/virtualization/ui/panels.py @@ -3,6 +3,16 @@ from django.utils.translation import gettext_lazy as _ from netbox.ui import attrs, panels +class ClusterPanel(panels.ObjectAttributesPanel): + name = attrs.TextAttr('name') + type = attrs.RelatedObjectAttr('type', linkify=True) + status = attrs.ChoiceAttr('status') + description = attrs.TextAttr('description') + group = attrs.RelatedObjectAttr('group', linkify=True) + tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group') + scope = attrs.GenericForeignKeyAttr('scope', linkify=True) + + class VirtualMachinePanel(panels.ObjectAttributesPanel): name = attrs.TextAttr('name') status = attrs.ChoiceAttr('status') @@ -32,3 +42,35 @@ class VirtualMachineClusterPanel(panels.ObjectAttributesPanel): cluster = attrs.RelatedObjectAttr('cluster', linkify=True) cluster_type = attrs.RelatedObjectAttr('cluster.type', linkify=True) device = attrs.RelatedObjectAttr('device', linkify=True) + + +class VirtualDiskPanel(panels.ObjectAttributesPanel): + virtual_machine = attrs.RelatedObjectAttr('virtual_machine', linkify=True, label=_('Virtual Machine')) + name = attrs.TextAttr('name') + size = attrs.TemplatedAttr('size', template_name='virtualization/virtualdisk/attrs/size.html') + description = attrs.TextAttr('description') + + +class VMInterfacePanel(panels.ObjectAttributesPanel): + virtual_machine = attrs.RelatedObjectAttr('virtual_machine', linkify=True, label=_('Virtual Machine')) + name = attrs.TextAttr('name') + enabled = attrs.BooleanAttr('enabled') + parent = attrs.RelatedObjectAttr('parent_interface', linkify=True) + bridge = attrs.RelatedObjectAttr('bridge', linkify=True) + description = attrs.TextAttr('description') + mtu = attrs.TextAttr('mtu', label=_('MTU')) + mode = attrs.ChoiceAttr('mode', label=_('802.1Q Mode')) + qinq_svlan = attrs.RelatedObjectAttr('qinq_svlan', linkify=True, label=_('Q-in-Q SVLAN')) + tunnel_termination = attrs.RelatedObjectAttr('tunnel_termination.tunnel', linkify=True, label=_('Tunnel')) + + +class VMInterfaceAddressingPanel(panels.ObjectAttributesPanel): + title = _('Addressing') + + primary_mac_address = attrs.TextAttr( + 'primary_mac_address', label=_('MAC Address'), style='font-monospace', copy_button=True + ) + vrf = attrs.RelatedObjectAttr('vrf', linkify=True, label=_('VRF')) + vlan_translation_policy = attrs.RelatedObjectAttr( + 'vlan_translation_policy', linkify=True, label=_('VLAN Translation') + ) diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py index eb4d31be4..1fca15c5e 100644 --- a/netbox/virtualization/views.py +++ b/netbox/virtualization/views.py @@ -14,6 +14,7 @@ from extras.ui.panels import CustomFieldsPanel, ImageAttachmentsPanel, TagsPanel from extras.views import ObjectConfigContextView, ObjectRenderConfigView from ipam.models import IPAddress, VLANGroup from ipam.tables import InterfaceVLANTable, VLANTranslationRuleTable +from ipam.ui.panels import FHRPGroupAssignmentsPanel from netbox.object_actions import ( AddObject, BulkDelete, @@ -25,7 +26,14 @@ from netbox.object_actions import ( EditObject, ) from netbox.ui import actions, layout -from netbox.ui.panels import CommentsPanel, ObjectsTablePanel, TemplatePanel +from netbox.ui.panels import ( + CommentsPanel, + ContextTablePanel, + ObjectsTablePanel, + OrganizationalObjectPanel, + RelatedObjectsPanel, + TemplatePanel, +) from netbox.views import generic from utilities.query import count_related from utilities.query_functions import CollateAsChar @@ -54,6 +62,17 @@ class ClusterTypeListView(generic.ObjectListView): @register_model_view(ClusterType) class ClusterTypeView(GetRelatedModelsMixin, generic.ObjectView): queryset = ClusterType.objects.all() + layout = layout.SimpleLayout( + left_panels=[ + OrganizationalObjectPanel(), + TagsPanel(), + ], + right_panels=[ + RelatedObjectsPanel(), + CustomFieldsPanel(), + CommentsPanel(), + ], + ) def get_extra_context(self, request, instance): return { @@ -121,6 +140,17 @@ class ClusterGroupListView(generic.ObjectListView): @register_model_view(ClusterGroup) class ClusterGroupView(GetRelatedModelsMixin, generic.ObjectView): queryset = ClusterGroup.objects.all() + layout = layout.SimpleLayout( + left_panels=[ + OrganizationalObjectPanel(), + TagsPanel(), + ], + right_panels=[ + RelatedObjectsPanel(), + CustomFieldsPanel(), + CommentsPanel(), + ], + ) def get_extra_context(self, request, instance): return { @@ -202,6 +232,18 @@ class ClusterListView(generic.ObjectListView): @register_model_view(Cluster) class ClusterView(GetRelatedModelsMixin, generic.ObjectView): queryset = Cluster.objects.all() + layout = layout.SimpleLayout( + left_panels=[ + panels.ClusterPanel(), + CommentsPanel(), + ], + right_panels=[ + TemplatePanel('virtualization/panels/cluster_resources.html'), + RelatedObjectsPanel(), + CustomFieldsPanel(), + TagsPanel(), + ], + ) def get_extra_context(self, request, instance): return { @@ -507,6 +549,7 @@ class VirtualMachineBulkDeleteView(generic.BulkDeleteView): # VM interfaces # + @register_model_view(VMInterface, 'list', path='', detail=False) class VMInterfaceListView(generic.ObjectListView): queryset = VMInterface.objects.all() @@ -518,6 +561,44 @@ class VMInterfaceListView(generic.ObjectListView): @register_model_view(VMInterface) class VMInterfaceView(generic.ObjectView): queryset = VMInterface.objects.all() + layout = layout.SimpleLayout( + left_panels=[ + panels.VMInterfacePanel(), + TagsPanel(), + ], + right_panels=[ + CustomFieldsPanel(), + panels.VMInterfaceAddressingPanel(), + FHRPGroupAssignmentsPanel(), + ], + bottom_panels=[ + ObjectsTablePanel( + model='ipam.IPaddress', + filters={'vminterface_id': lambda ctx: ctx['object'].pk}, + actions=[ + actions.AddObject( + 'ipam.IPaddress', + url_params={ + 'virtual_machine': lambda ctx: ctx['object'].virtual_machine.pk, + 'vminterface': lambda ctx: ctx['object'].pk, + }, + ), + ], + ), + ObjectsTablePanel( + model='dcim.MACAddress', + filters={'vminterface_id': lambda ctx: ctx['object'].pk}, + actions=[ + actions.AddObject( + 'dcim.MACAddress', url_params={'vminterface': lambda ctx: ctx['object'].pk} + ), + ], + ), + ContextTablePanel('vlan_table', title=_('Assigned VLANs')), + ContextTablePanel('vlan_translation_table', title=_('VLAN Translation')), + ContextTablePanel('child_interfaces_table', title=_('Child Interfaces')), + ], + ) def get_extra_context(self, request, instance): @@ -623,6 +704,15 @@ class VirtualDiskListView(generic.ObjectListView): @register_model_view(VirtualDisk) class VirtualDiskView(generic.ObjectView): queryset = VirtualDisk.objects.all() + layout = layout.SimpleLayout( + left_panels=[ + panels.VirtualDiskPanel(), + TagsPanel(), + ], + right_panels=[ + CustomFieldsPanel(), + ], + ) @register_model_view(VirtualDisk, 'add', detail=False)