mirror of
https://github.com/netbox-community/netbox.git
synced 2026-04-23 01:08:45 +02:00
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
This commit is contained in:
committed by
Jeremy Stretch
parent
584e0a9b8c
commit
5013297326
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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 %}
|
|
||||||
|
|||||||
@@ -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
netbox/virtualization/ui/__init__.py
Normal file
0
netbox/virtualization/ui/__init__.py
Normal file
34
netbox/virtualization/ui/panels.py
Normal file
34
netbox/virtualization/ui/panels.py
Normal file
@@ -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)
|
||||||
@@ -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')
|
||||||
|
|||||||
Reference in New Issue
Block a user