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 %}
+
+
+
+ | {% trans "Group" %} |
+ {% trans "Protocol" %} |
+ {% trans "Virtual IPs" %} |
+ {% trans "Priority" %} |
+ |
+
+
+
+ {% for assignment in object.fhrp_group_assignments.all %}
+
+ | {{ 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 %}
+ |
+
+ {% empty %}
+
+ | {% trans "None" %} |
+
+ {% endfor %}
+
+
+{% 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 "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" %} |
- {% if object.scope %}
- {{ object.scope|linkify }} ({% trans object.scope_type.name %}) |
- {% else %}
- {{ ''|placeholder }} |
- {% endif %}
-
-
-
- {% include 'inc/panels/comments.html' %}
- {% plugin_left_page object %}
-
-
-
-
-
-
- | {% 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 "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 "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 "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 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 "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 }} |
-
- {% if object.mode == 'q-in-q' %}
-
- | {% trans "Q-in-Q SVLAN" %} |
- {{ object.qinq_svlan|linkify|placeholder }} |
-
- {% endif %}
-
- | {% trans "Tunnel" %} |
- {{ object.tunnel_termination.tunnel|linkify|placeholder }} |
-
-
-
- {% include 'inc/panels/tags.html' %}
- {% plugin_left_page object %}
-
-
- {% include 'inc/panels/custom_fields.html' %}
-
-
-
-
- | {% 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 %}
-
-
-
-
-
-
- {% htmx_table 'ipam:ipaddress_list' vminterface_id=object.pk %}
-
-
-
-
-
-
-
- {% 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)