Przeglądaj źródła

feat(virtualization): Refactor VirtualMachine view to UI layout

Migrate the VirtualMachine detail view to SimpleLayout with standardized
panels for attributes, clusters, and resources. Modularize templates
to improve maintainability and reuse.

Fixes #21337
Martin Hauser 1 tydzień temu
rodzic
commit
5013297326

+ 1 - 1
netbox/dcim/views.py

@@ -13,7 +13,6 @@ from django.utils.translation import gettext_lazy as _
 from django.views.generic import View
 from django.views.generic import View
 
 
 from circuits.models import Circuit, CircuitTermination
 from circuits.models import Circuit, CircuitTermination
-from dcim.ui import panels
 from extras.ui.panels import CustomFieldsPanel, ImageAttachmentsPanel, TagsPanel
 from extras.ui.panels import CustomFieldsPanel, ImageAttachmentsPanel, TagsPanel
 from extras.views import ObjectConfigContextView, ObjectRenderConfigView
 from extras.views import ObjectConfigContextView, ObjectRenderConfigView
 from ipam.models import ASN, IPAddress, Prefix, VLAN, VLANGroup
 from ipam.models import ASN, IPAddress, Prefix, VLAN, VLANGroup
@@ -44,6 +43,7 @@ from .choices import DeviceFaceChoices, InterfaceModeChoices
 from .models import *
 from .models import *
 from .models.device_components import PortMapping
 from .models.device_components import PortMapping
 from .object_actions import BulkAddComponents, BulkDisconnect
 from .object_actions import BulkAddComponents, BulkDisconnect
+from .ui import panels
 
 
 CABLE_TERMINATION_TYPES = {
 CABLE_TERMINATION_TYPES = {
     'dcim.consoleport': ConsolePort,
     'dcim.consoleport': ConsolePort,

+ 34 - 0
netbox/templates/virtualization/panels/virtual_machine_resources.html

@@ -0,0 +1,34 @@
+{% load helpers %}
+{% load i18n %}
+
+<div class="card">
+  <h2 class="card-header">{% trans "Resources" %}</h2>
+  <table class="table table-hover attr-table">
+    <tr>
+      <th scope="row"><i class="mdi mdi-gauge"></i> {% trans "Virtual CPUs" %}</th>
+      <td>{{ object.vcpus|placeholder }}</td>
+    </tr>
+    <tr>
+      <th scope="row"><i class="mdi mdi-chip"></i> {% trans "Memory" %}</th>
+      <td>
+        {% if object.memory %}
+          <span title={{ object.memory }}>{{ object.memory|humanize_ram_megabytes }}</span>
+        {% else %}
+          {{ ''|placeholder }}
+        {% endif %}
+      </td>
+    </tr>
+    <tr>
+      <th scope="row">
+        <i class="mdi mdi-harddisk"></i> {% trans "Disk Space" %}
+      </th>
+      <td>
+        {% if object.disk %}
+          {{ object.disk|humanize_disk_megabytes }}
+        {% else %}
+          {{ ''|placeholder }}
+        {% endif %}
+      </td>
+    </tr>
+  </table>
+</div>

+ 0 - 198
netbox/templates/virtualization/virtualmachine.html

@@ -1,199 +1 @@
 {% extends 'virtualization/virtualmachine/base.html' %}
 {% extends 'virtualization/virtualmachine/base.html' %}
-{% load buttons %}
-{% load static %}
-{% load helpers %}
-{% load plugins %}
-{% load i18n %}
-
-{% block content %}
-<div class="row my-3">
-	<div class="col col-12 col-md-6">
-        <div class="card">
-            <h2 class="card-header">{% trans "Virtual Machine" %}</h2>
-            <table class="table table-hover attr-table">
-                <tr>
-                    <th scope="row">{% trans "Name" %}</th>
-                    <td>{{ object }}</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 "Start on boot" %}</th>
-                    <td>{% badge object.get_start_on_boot_display bg_color=object.get_start_on_boot_color %}</td>
-                </tr>
-                <tr>
-                    <th scope="row">{% trans "Role" %}</th>
-                    <td>{{ object.role|linkify|placeholder }}</td>
-                </tr>
-                <tr>
-                    <th scope="row">{% trans "Platform" %}</th>
-                    <td>{{ object.platform|linkify|placeholder }}</td>
-                </tr>
-                <tr>
-                  <th scope="row">{% trans "Description" %}</th>
-                  <td>{{ object.description|placeholder }}</td>
-                </tr>
-                <tr>
-                  <th scope="row">{% trans "Serial Number" %}</th>
-                  <td>{{ object.serial|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 "Config Template" %}</th>
-                    <td>{{ object.config_template|linkify|placeholder }}</td>
-                </tr>
-                <tr>
-                    <th scope="row">{% trans "Primary IPv4" %}</th>
-                    <td>
-                      {% if object.primary_ip4 %}
-                        <a href="{% url 'ipam:ipaddress' pk=object.primary_ip4.pk %}" id="primary_ip4">{{ object.primary_ip4.address.ip }}</a>
-                        {% if object.primary_ip4.nat_inside %}
-                          ({% trans "NAT for" %} <a href="{{ object.primary_ip4.nat_inside.get_absolute_url }}">{{ object.primary_ip4.nat_inside.address.ip }}</a>)
-                        {% elif object.primary_ip4.nat_outside.exists %}
-                          ({% trans "NAT" %}: {% for nat in object.primary_ip4.nat_outside.all %}<a href="{{ nat.get_absolute_url }}">{{ nat.address.ip }}</a>{% if not forloop.last %}, {% endif %}{% endfor %})
-                        {% endif %}
-                        {% copy_content "primary_ip4" %}
-                      {% else %}
-                        {{ ''|placeholder }}
-                      {% endif %}
-                    </td>
-                </tr>
-                <tr>
-                    <th scope="row">{% trans "Primary IPv6" %}</th>
-                    <td>
-                      {% if object.primary_ip6 %}
-                        <a href="{% url 'ipam:ipaddress' pk=object.primary_ip6.pk %}" id="primary_ip6">{{ object.primary_ip6.address.ip }}</a>
-                        {% if object.primary_ip6.nat_inside %}
-                          ({% trans "NAT for" %} <a href="{{ object.primary_ip6.nat_inside.get_absolute_url }}">{{ object.primary_ip6.nat_inside.address.ip }}</a>)
-                        {% elif object.primary_ip6.nat_outside.exists %}
-                          ({% trans "NAT" %}: {% for nat in object.primary_ip6.nat_outside.all %}<a href="{{ nat.get_absolute_url }}">{{ nat.address.ip }}</a>{% if not forloop.last %}, {% endif %}{% endfor %})
-                        {% endif %}
-                        {% copy_content "primary_ip6" %}
-                      {% else %}
-                        {{ ''|placeholder }}
-                      {% endif %}
-                    </td>
-                </tr>
-            </table>
-        </div>
-        {% include 'inc/panels/custom_fields.html' %}
-        {% 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 "Cluster" %}</h2>
-            <table class="table table-hover attr-table">
-                <tr>
-                    <th scope="row">{% trans "Site" %}</th>
-                    <td>
-                        {{ object.site|linkify|placeholder }}
-                    </td>
-                </tr>
-                <tr>
-                    <th scope="row">{% trans "Cluster" %}</th>
-                    <td>
-                        {% if object.cluster.group %}
-                            {{ object.cluster.group|linkify }} /
-                        {% endif %}
-                        {{ object.cluster|linkify|placeholder }}
-                    </td>
-                </tr>
-                <tr>
-                    <th scope="row">{% trans "Cluster Type" %}</th>
-                    <td>
-                        {{ object.cluster.type|linkify|placeholder }}
-                    </td>
-                </tr>
-                <tr>
-                    <th scope="row">{% trans "Device" %}</th>
-                    <td>
-                        {{ object.device|linkify|placeholder }}
-                    </td>
-                </tr>
-            </table>
-        </div>
-        <div class="card">
-            <h2 class="card-header">{% trans "Resources" %}</h2>
-            <table class="table table-hover attr-table">
-                <tr>
-                    <th scope="row"><i class="mdi mdi-gauge"></i> {% trans "Virtual CPUs" %}</th>
-                    <td>{{ object.vcpus|placeholder }}</td>
-                </tr>
-                <tr>
-                    <th scope="row"><i class="mdi mdi-chip"></i> {% trans "Memory" %}</th>
-                    <td>
-                        {% if object.memory %}
-                            <span title={{ object.memory }}>{{ object.memory|humanize_ram_megabytes }}</span>
-                        {% else %}
-                            {{ ''|placeholder }}
-                        {% endif %}
-                    </td>
-                </tr>
-                <tr>
-                  <th scope="row">
-                    <i class="mdi mdi-harddisk"></i> {% trans "Disk Space" %}
-                  </th>
-                  <td>
-                    {% if object.disk %}
-                      {{ object.disk|humanize_disk_megabytes }}
-                    {% else %}
-                      {{ ''|placeholder }}
-                    {% endif %}
-                  </td>
-                </tr>
-            </table>
-        </div>
-        <div class="card">
-          <h2 class="card-header">
-            {% trans "Application Services" %}
-            {% if perms.ipam.add_service %}
-              <div class="card-actions">
-                <a href="{% url 'ipam:service_add' %}?parent_object_type={{ object|content_type_id }}&parent={{ object.pk }}" class="btn btn-ghost-primary btn-sm">
-                  <span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add an application service" %}
-                </a>
-              </div>
-            {% endif %}
-          </h2>
-          {% htmx_table 'ipam:service_list' virtual_machine_id=object.pk %}
-        </div>
-        {% include 'inc/panels/image_attachments.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 Disks" %}
-        {% if perms.virtualization.add_virtualdisk %}
-          <div class="card-actions">
-            <a href="{% url 'virtualization:virtualdisk_add' %}?device={{ object.device.pk }}&virtual_machine={{ 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 Virtual Disk" %}
-            </a>
-          </div>
-        {% endif %}
-      </h2>
-      {% htmx_table 'virtualization:virtualdisk_list' virtual_machine_id=object.pk %}
-    </div>
-  </div>
-</div>
-
-<div class="row">
-    <div class="col col-md-12">
-        {% plugin_full_width_page object %}
-    </div>
-</div>
-{% endblock %}

+ 10 - 0
netbox/templates/virtualization/virtualmachine/attrs/ipaddress.html

@@ -0,0 +1,10 @@
+{% load i18n %}
+<a href="{{ value.get_absolute_url }}"{% if name %} id="attr_{{ name }}"{% endif %}>{{ value.address.ip }}</a>
+{% if value.nat_inside %}
+  ({% trans "NAT for" %} <a href="{{ value.nat_inside.get_absolute_url }}">{{ value.nat_inside.address.ip }}</a>)
+{% elif value.nat_outside.exists %}
+  ({% trans "NAT" %}: {% for nat in value.nat_outside.all %}<a href="{{ nat.get_absolute_url }}">{{ nat.address.ip }}</a>{% if not forloop.last %}, {% endif %}{% endfor %})
+{% endif %}
+<a class="btn btn-sm btn-primary copy-content" data-clipboard-target="#attr_{{ name }}" title="{% trans "Copy to clipboard" %}">
+  <i class="mdi mdi-content-copy"></i>
+</a>

+ 0 - 0
netbox/virtualization/ui/__init__.py


+ 34 - 0
netbox/virtualization/ui/panels.py

@@ -0,0 +1,34 @@
+from django.utils.translation import gettext_lazy as _
+
+from netbox.ui import attrs, panels
+
+
+class VirtualMachinePanel(panels.ObjectAttributesPanel):
+    name = attrs.TextAttr('name')
+    status = attrs.ChoiceAttr('status')
+    start_on_boot = attrs.ChoiceAttr('start_on_boot')
+    role = attrs.RelatedObjectAttr('role', linkify=True)
+    platform = attrs.NestedObjectAttr('platform', linkify=True, max_depth=3)
+    description = attrs.TextAttr('description')
+    serial = attrs.TextAttr('serial', label=_('Serial number'), style='font-monospace', copy_button=True)
+    tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
+    config_template = attrs.RelatedObjectAttr('config_template', linkify=True)
+    primary_ip4 = attrs.TemplatedAttr(
+        'primary_ip4',
+        label=_('Primary IPv4'),
+        template_name='virtualization/virtualmachine/attrs/ipaddress.html',
+    )
+    primary_ip6 = attrs.TemplatedAttr(
+        'primary_ip6',
+        label=_('Primary IPv6'),
+        template_name='virtualization/virtualmachine/attrs/ipaddress.html',
+    )
+
+
+class VirtualMachineClusterPanel(panels.ObjectAttributesPanel):
+    title = _('Cluster')
+
+    site = attrs.RelatedObjectAttr('site', linkify=True, grouped_by='group')
+    cluster = attrs.RelatedObjectAttr('cluster', linkify=True)
+    cluster_type = attrs.RelatedObjectAttr('cluster.type', linkify=True)
+    device = attrs.RelatedObjectAttr('device', linkify=True)

+ 43 - 0
netbox/virtualization/views.py

@@ -10,12 +10,15 @@ from dcim.filtersets import DeviceFilterSet
 from dcim.forms import DeviceFilterForm
 from dcim.forms import DeviceFilterForm
 from dcim.models import Device
 from dcim.models import Device
 from dcim.tables import DeviceTable
 from dcim.tables import DeviceTable
+from extras.ui.panels import CustomFieldsPanel, ImageAttachmentsPanel, TagsPanel
 from extras.views import ObjectConfigContextView, ObjectRenderConfigView
 from extras.views import ObjectConfigContextView, ObjectRenderConfigView
 from ipam.models import IPAddress, VLANGroup
 from ipam.models import IPAddress, VLANGroup
 from ipam.tables import InterfaceVLANTable, VLANTranslationRuleTable
 from ipam.tables import InterfaceVLANTable, VLANTranslationRuleTable
 from netbox.object_actions import (
 from netbox.object_actions import (
     AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport, BulkRename, DeleteObject, EditObject,
     AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport, BulkRename, DeleteObject, EditObject,
 )
 )
+from netbox.ui import actions, layout
+from netbox.ui.panels import CommentsPanel, ObjectsTablePanel, 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.query_functions import CollateAsChar
 from utilities.query_functions import CollateAsChar
@@ -23,6 +26,7 @@ from utilities.views import GetRelatedModelsMixin, ViewTab, register_model_view
 from . import filtersets, forms, tables
 from . import filtersets, forms, tables
 from .models import *
 from .models import *
 from .object_actions import BulkAddComponents
 from .object_actions import BulkAddComponents
+from .ui import panels
 
 
 
 
 #
 #
@@ -336,6 +340,7 @@ class ClusterAddDevicesView(generic.ObjectEditView):
 # Virtual machines
 # Virtual machines
 #
 #
 
 
+
 @register_model_view(VirtualMachine, 'list', path='', detail=False)
 @register_model_view(VirtualMachine, 'list', path='', detail=False)
 class VirtualMachineListView(generic.ObjectListView):
 class VirtualMachineListView(generic.ObjectListView):
     queryset = VirtualMachine.objects.prefetch_related('primary_ip4', 'primary_ip6')
     queryset = VirtualMachine.objects.prefetch_related('primary_ip4', 'primary_ip6')
@@ -348,6 +353,44 @@ class VirtualMachineListView(generic.ObjectListView):
 @register_model_view(VirtualMachine)
 @register_model_view(VirtualMachine)
 class VirtualMachineView(generic.ObjectView):
 class VirtualMachineView(generic.ObjectView):
     queryset = VirtualMachine.objects.all()
     queryset = VirtualMachine.objects.all()
+    layout = layout.SimpleLayout(
+        left_panels=[
+            panels.VirtualMachinePanel(),
+            CustomFieldsPanel(),
+            TagsPanel(),
+            CommentsPanel(),
+        ],
+        right_panels=[
+            panels.VirtualMachineClusterPanel(),
+            TemplatePanel('virtualization/panels/virtual_machine_resources.html'),
+            ObjectsTablePanel(
+                model='ipam.Service',
+                title=_('Application Services'),
+                filters={'virtual_machine_id': lambda ctx: ctx['object'].pk},
+                actions=[
+                    actions.AddObject(
+                        'ipam.Service',
+                        url_params={
+                            'parent_object_type': lambda ctx: ContentType.objects.get_for_model(ctx['object']).pk,
+                            'parent': lambda ctx: ctx['object'].pk,
+                        },
+                    ),
+                ],
+            ),
+            ImageAttachmentsPanel(),
+        ],
+        bottom_panels=[
+            ObjectsTablePanel(
+                model='virtualization.VirtualDisk',
+                filters={'virtual_machine_id': lambda ctx: ctx['object'].pk},
+                actions=[
+                    actions.AddObject(
+                        'virtualization.VirtualDisk', url_params={'virtual_machine': lambda ctx: ctx['object'].pk}
+                    ),
+                ],
+            ),
+        ],
+    )
 
 
 
 
 @register_model_view(VirtualMachine, 'interfaces')
 @register_model_view(VirtualMachine, 'interfaces')