mirror of
https://github.com/netbox-community/netbox.git
synced 2026-04-06 00:32:08 +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 circuits.models import Circuit, CircuitTermination
|
||||
from dcim.ui import panels
|
||||
from extras.ui.panels import CustomFieldsPanel, ImageAttachmentsPanel, TagsPanel
|
||||
from extras.views import ObjectConfigContextView, ObjectRenderConfigView
|
||||
from ipam.models import ASN, IPAddress, Prefix, VLAN, VLANGroup
|
||||
@@ -44,6 +43,7 @@ from .choices import DeviceFaceChoices, InterfaceModeChoices
|
||||
from .models import *
|
||||
from .models.device_components import PortMapping
|
||||
from .object_actions import BulkAddComponents, BulkDisconnect
|
||||
from .ui import panels
|
||||
|
||||
CABLE_TERMINATION_TYPES = {
|
||||
'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' %}
|
||||
{% 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.models import Device
|
||||
from dcim.tables import DeviceTable
|
||||
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 netbox.object_actions import (
|
||||
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 utilities.query import count_related
|
||||
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 .models import *
|
||||
from .object_actions import BulkAddComponents
|
||||
from .ui import panels
|
||||
|
||||
|
||||
#
|
||||
@@ -336,6 +340,7 @@ class ClusterAddDevicesView(generic.ObjectEditView):
|
||||
# Virtual machines
|
||||
#
|
||||
|
||||
|
||||
@register_model_view(VirtualMachine, 'list', path='', detail=False)
|
||||
class VirtualMachineListView(generic.ObjectListView):
|
||||
queryset = VirtualMachine.objects.prefetch_related('primary_ip4', 'primary_ip6')
|
||||
@@ -348,6 +353,44 @@ class VirtualMachineListView(generic.ObjectListView):
|
||||
@register_model_view(VirtualMachine)
|
||||
class VirtualMachineView(generic.ObjectView):
|
||||
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')
|
||||
|
||||
Reference in New Issue
Block a user