mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-19 09:53:34 -06:00
Closes #5425: Create separate tabs for VMs and devices under the cluster view
This commit is contained in:
parent
8fa37d3ec8
commit
b070be1c41
@ -87,6 +87,7 @@ A new Cloud model has been introduced to represent the boundary of a network tha
|
|||||||
* [#5370](https://github.com/netbox-community/netbox/issues/5370) - Extend custom field support to organizational models
|
* [#5370](https://github.com/netbox-community/netbox/issues/5370) - Extend custom field support to organizational models
|
||||||
* [#5375](https://github.com/netbox-community/netbox/issues/5375) - Add `speed` attribute to console port models
|
* [#5375](https://github.com/netbox-community/netbox/issues/5375) - Add `speed` attribute to console port models
|
||||||
* [#5401](https://github.com/netbox-community/netbox/issues/5401) - Extend custom field support to device component models
|
* [#5401](https://github.com/netbox-community/netbox/issues/5401) - Extend custom field support to device component models
|
||||||
|
* [#5425](https://github.com/netbox-community/netbox/issues/5425) - Create separate tabs for VMs and devices under the cluster view
|
||||||
* [#5451](https://github.com/netbox-community/netbox/issues/5451) - Add support for multiple-selection custom fields
|
* [#5451](https://github.com/netbox-community/netbox/issues/5451) - Add support for multiple-selection custom fields
|
||||||
* [#5608](https://github.com/netbox-community/netbox/issues/5608) - Add REST API endpoint for custom links
|
* [#5608](https://github.com/netbox-community/netbox/issues/5608) - Add REST API endpoint for custom links
|
||||||
* [#5610](https://github.com/netbox-community/netbox/issues/5610) - Add REST API endpoint for webhooks
|
* [#5610](https://github.com/netbox-community/netbox/issues/5610) - Add REST API endpoint for webhooks
|
||||||
|
@ -1,117 +1,81 @@
|
|||||||
{% extends 'generic/object.html' %}
|
{% extends 'virtualization/cluster/base.html' %}
|
||||||
{% load buttons %}
|
|
||||||
{% load custom_links %}
|
|
||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
{% load plugins %}
|
{% load plugins %}
|
||||||
|
|
||||||
{% block breadcrumbs %}
|
|
||||||
<li><a href="{{ object.type.get_absolute_url }}">{{ object.type }}</a></li>
|
|
||||||
{% if object.group %}
|
|
||||||
<li><a href="{{ object.group.get_absolute_url }}">{{ object.group }}</a></li>
|
|
||||||
{% endif %}
|
|
||||||
<li>{{ object }}</li>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-5">
|
<div class="col-md-6">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Cluster</strong>
|
<strong>Cluster</strong>
|
||||||
</div>
|
</div>
|
||||||
<table class="table table-hover panel-body attr-table">
|
<table class="table table-hover panel-body attr-table">
|
||||||
<tr>
|
<tr>
|
||||||
<td>Name</td>
|
<td>Name</td>
|
||||||
<td>{{ object.name }}</td>
|
<td>{{ object.name }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Type</td>
|
<td>Type</td>
|
||||||
<td><a href="{{ object.type.get_absolute_url }}">{{ object.type }}</a></td>
|
<td><a href="{{ object.type.get_absolute_url }}">{{ object.type }}</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Group</td>
|
<td>Group</td>
|
||||||
<td>
|
<td>
|
||||||
{% if object.group %}
|
{% if object.group %}
|
||||||
<a href="{{ object.group.get_absolute_url }}">{{ object.group }}</a>
|
<a href="{{ object.group.get_absolute_url }}">{{ object.group }}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-muted">None</span>
|
<span class="text-muted">None</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Tenant</td>
|
<td>Tenant</td>
|
||||||
<td>
|
<td>
|
||||||
{% if object.tenant %}
|
{% if object.tenant %}
|
||||||
<a href="{{ object.tenant.get_absolute_url }}">{{ object.tenant }}</a>
|
<a href="{{ object.tenant.get_absolute_url }}">{{ object.tenant }}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-muted">None</span>
|
<span class="text-muted">None</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Site</td>
|
<td>Site</td>
|
||||||
<td>
|
<td>
|
||||||
{% if object.site %}
|
{% if object.site %}
|
||||||
<a href="{{ object.site.get_absolute_url }}">{{ object.site }}</a>
|
<a href="{{ object.site.get_absolute_url }}">{{ object.site }}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-muted">None</span>
|
<span class="text-muted">None</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Virtual Machines</td>
|
<td>Virtual Machines</td>
|
||||||
<td><a href="{% url 'virtualization:virtualmachine_list' %}?cluster_id={{ object.pk }}">{{ object.virtual_machines.count }}</a></td>
|
<td><a href="{% url 'virtualization:virtualmachine_list' %}?cluster_id={{ object.pk }}">{{ object.virtual_machines.count }}</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
|
||||||
{% include 'inc/custom_fields_panel.html' %}
|
|
||||||
{% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='virtualization:cluster_list' %}
|
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel-heading">
|
|
||||||
<strong>Comments</strong>
|
|
||||||
</div>
|
|
||||||
<div class="panel-body rendered-markdown">
|
|
||||||
{% if object.comments %}
|
|
||||||
{{ object.comments|render_markdown }}
|
|
||||||
{% else %}
|
|
||||||
<span class="text-muted">None</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% plugin_left_page object %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-7">
|
{% include 'inc/custom_fields_panel.html' %}
|
||||||
<div class="panel panel-default">
|
</div>
|
||||||
<div class="panel-heading">
|
<div class="col-md-6">
|
||||||
<strong>Host Devices</strong>
|
{% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='virtualization:cluster_list' %}
|
||||||
</div>
|
<div class="panel panel-default">
|
||||||
{% if perms.virtualization.change_cluster %}
|
<div class="panel-heading">
|
||||||
<form action="{% url 'virtualization:cluster_remove_devices' pk=object.pk %}" method="post">
|
<strong>Comments</strong>
|
||||||
{% csrf_token %}
|
</div>
|
||||||
{% endif %}
|
<div class="panel-body rendered-markdown">
|
||||||
{% include 'responsive_table.html' with table=device_table %}
|
{% if object.comments %}
|
||||||
{% if perms.virtualization.change_cluster %}
|
{{ object.comments|render_markdown }}
|
||||||
<div class="panel-footer noprint">
|
{% else %}
|
||||||
<div class="pull-right">
|
<span class="text-muted">None</span>
|
||||||
<a href="{% url 'virtualization:cluster_add_devices' pk=object.pk %}?site={{ object.site.pk }}" class="btn btn-primary btn-xs">
|
{% endif %}
|
||||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span>
|
</div>
|
||||||
Add devices
|
</div>
|
||||||
</a>
|
{% plugin_left_page object %}
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" name="_remove" class="btn btn-danger primary btn-xs">
|
|
||||||
<span class="mdi mdi-trash-can-outline" aria-hidden="true"></span>
|
|
||||||
Remove devices
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% plugin_right_page object %}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
{% plugin_full_width_page object %}
|
{% plugin_full_width_page object %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
55
netbox/templates/virtualization/cluster/base.html
Normal file
55
netbox/templates/virtualization/cluster/base.html
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
{% extends 'generic/object.html' %}
|
||||||
|
{% load buttons %}
|
||||||
|
{% load helpers %}
|
||||||
|
{% load custom_links %}
|
||||||
|
{% load plugins %}
|
||||||
|
|
||||||
|
{% block breadcrumbs %}
|
||||||
|
<li><a href="{{ object.type.get_absolute_url }}">{{ object.type }}</a></li>
|
||||||
|
{% if object.group %}
|
||||||
|
<li><a href="{{ object.group.get_absolute_url }}">{{ object.group }}</a></li>
|
||||||
|
{% endif %}
|
||||||
|
<li>{{ object }}</li>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block buttons %}
|
||||||
|
{% if perms.virtualization.change_cluster and perms.virtualization.add_virtualmachine %}
|
||||||
|
<a href="{% url 'virtualization:virtualmachine_add' %}?cluster={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-primary">
|
||||||
|
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Virtual Machine
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if perms.virtualization.change_cluster %}
|
||||||
|
<a href="{% url 'virtualization:cluster_add_devices' pk=object.pk %}?site={{ object.site.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-primary">
|
||||||
|
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Assign Device
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{{ block.super }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block tabs %}
|
||||||
|
<ul class="nav nav-tabs">
|
||||||
|
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
|
||||||
|
<a href="{{ object.get_absolute_url }}">Cluster</a>
|
||||||
|
</li>
|
||||||
|
{% with virtualmachine_count=object.virtual_machines.count %}
|
||||||
|
<li role="presentation" {% if active_tab == 'virtual-machines' %} class="active"{% endif %}>
|
||||||
|
<a href="{% url 'virtualization:cluster_virtualmachines' pk=object.pk %}">Virtual Machines {% badge virtualmachine_count %}</a>
|
||||||
|
</li>
|
||||||
|
{% endwith %}
|
||||||
|
{% with device_count=object.devices.count %}
|
||||||
|
<li role="presentation" {% if active_tab == 'devices' %} class="active"{% endif %}>
|
||||||
|
<a href="{% url 'virtualization:cluster_devices' pk=object.pk %}">Devices {% badge device_count %}</a>
|
||||||
|
</li>
|
||||||
|
{% endwith %}
|
||||||
|
{% if perms.extras.view_journalentry %}
|
||||||
|
<li role="presentation"{% if active_tab == 'journal' %} class="active"{% endif %}>
|
||||||
|
<a href="{% url 'virtualization:cluster_journal' pk=object.pk %}">Journal</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% if perms.extras.view_objectchange %}
|
||||||
|
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
|
||||||
|
<a href="{% url 'virtualization:cluster_changelog' pk=object.pk %}">Change Log</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
{% endblock %}
|
25
netbox/templates/virtualization/cluster/devices.html
Normal file
25
netbox/templates/virtualization/cluster/devices.html
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{% extends 'virtualization/cluster/base.html' %}
|
||||||
|
{% load helpers %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<strong>Host Devices</strong>
|
||||||
|
</div>
|
||||||
|
<form action="{% url 'virtualization:cluster_remove_devices' pk=object.pk %}" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% include 'responsive_table.html' with table=devices_table %}
|
||||||
|
{% if perms.virtualization.change_cluster %}
|
||||||
|
<div class="panel-footer noprint">
|
||||||
|
<button type="submit" name="_remove" class="btn btn-danger primary btn-xs">
|
||||||
|
<span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> Remove devices
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -0,0 +1,15 @@
|
|||||||
|
{% extends 'virtualization/cluster/base.html' %}
|
||||||
|
{% load helpers %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<strong>Virtual Machines</strong>
|
||||||
|
</div>
|
||||||
|
{% include 'responsive_table.html' with table=virtualmachines_table %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -36,6 +36,11 @@
|
|||||||
<a href="{% url 'virtualization:virtualmachine_configcontext' pk=object.pk %}">Config Context</a>
|
<a href="{% url 'virtualization:virtualmachine_configcontext' pk=object.pk %}">Config Context</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if perms.extras.view_journalentry %}
|
||||||
|
<li role="presentation"{% if active_tab == 'journal' %} class="active"{% endif %}>
|
||||||
|
<a href="{% url 'virtualization:virtualmachine_journal' pk=object.pk %}">Journal</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
{% if perms.extras.view_objectchange %}
|
{% if perms.extras.view_objectchange %}
|
||||||
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
|
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
|
||||||
<a href="{% url 'virtualization:virtualmachine_changelog' pk=object.pk %}">Change Log</a>
|
<a href="{% url 'virtualization:virtualmachine_changelog' pk=object.pk %}">Change Log</a>
|
||||||
|
@ -127,6 +127,20 @@ class ClusterTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
'comments': 'New comments',
|
'comments': 'New comments',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
||||||
|
def test_cluster_virtualmachines(self):
|
||||||
|
cluster = Cluster.objects.first()
|
||||||
|
|
||||||
|
url = reverse('virtualization:cluster_virtualmachines', kwargs={'pk': cluster.pk})
|
||||||
|
self.assertHttpStatus(self.client.get(url), 200)
|
||||||
|
|
||||||
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
||||||
|
def test_cluster_devices(self):
|
||||||
|
cluster = Cluster.objects.first()
|
||||||
|
|
||||||
|
url = reverse('virtualization:cluster_devices', kwargs={'pk': cluster.pk})
|
||||||
|
self.assertHttpStatus(self.client.get(url), 200)
|
||||||
|
|
||||||
|
|
||||||
class VirtualMachineTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
class VirtualMachineTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||||
model = VirtualMachine
|
model = VirtualMachine
|
||||||
@ -199,7 +213,7 @@ class VirtualMachineTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
||||||
def test_device_interfaces(self):
|
def test_virtualmachine_interfaces(self):
|
||||||
virtualmachine = VirtualMachine.objects.first()
|
virtualmachine = VirtualMachine.objects.first()
|
||||||
vminterfaces = (
|
vminterfaces = (
|
||||||
VMInterface(virtual_machine=virtualmachine, name='Interface 1'),
|
VMInterface(virtual_machine=virtualmachine, name='Interface 1'),
|
||||||
|
@ -37,6 +37,8 @@ urlpatterns = [
|
|||||||
path('clusters/edit/', views.ClusterBulkEditView.as_view(), name='cluster_bulk_edit'),
|
path('clusters/edit/', views.ClusterBulkEditView.as_view(), name='cluster_bulk_edit'),
|
||||||
path('clusters/delete/', views.ClusterBulkDeleteView.as_view(), name='cluster_bulk_delete'),
|
path('clusters/delete/', views.ClusterBulkDeleteView.as_view(), name='cluster_bulk_delete'),
|
||||||
path('clusters/<int:pk>/', views.ClusterView.as_view(), name='cluster'),
|
path('clusters/<int:pk>/', views.ClusterView.as_view(), name='cluster'),
|
||||||
|
path('clusters/<int:pk>/devices/', views.ClusterDevicesView.as_view(), name='cluster_devices'),
|
||||||
|
path('clusters/<int:pk>/virtual-machines/', views.ClusterVirtualMachinesView.as_view(), name='cluster_virtualmachines'),
|
||||||
path('clusters/<int:pk>/edit/', views.ClusterEditView.as_view(), name='cluster_edit'),
|
path('clusters/<int:pk>/edit/', views.ClusterEditView.as_view(), name='cluster_edit'),
|
||||||
path('clusters/<int:pk>/delete/', views.ClusterDeleteView.as_view(), name='cluster_delete'),
|
path('clusters/<int:pk>/delete/', views.ClusterDeleteView.as_view(), name='cluster_delete'),
|
||||||
path('clusters/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='cluster_changelog', kwargs={'model': Cluster}),
|
path('clusters/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='cluster_changelog', kwargs={'model': Cluster}),
|
||||||
|
@ -155,16 +155,38 @@ class ClusterListView(generic.ObjectListView):
|
|||||||
class ClusterView(generic.ObjectView):
|
class ClusterView(generic.ObjectView):
|
||||||
queryset = Cluster.objects.all()
|
queryset = Cluster.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
class ClusterVirtualMachinesView(generic.ObjectView):
|
||||||
|
queryset = Cluster.objects.all()
|
||||||
|
template_name = 'virtualization/cluster/virtual_machines.html'
|
||||||
|
|
||||||
|
def get_extra_context(self, request, instance):
|
||||||
|
virtualmachines = VirtualMachine.objects.restrict(request.user, 'view').filter(cluster=instance)
|
||||||
|
virtualmachines_table = tables.VirtualMachineTable(virtualmachines, orderable=False)
|
||||||
|
if request.user.has_perm('virtualization.change_cluster'):
|
||||||
|
virtualmachines_table.columns.show('pk')
|
||||||
|
|
||||||
|
return {
|
||||||
|
'virtualmachines_table': virtualmachines_table,
|
||||||
|
'active_tab': 'virtual-machines',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ClusterDevicesView(generic.ObjectView):
|
||||||
|
queryset = Cluster.objects.all()
|
||||||
|
template_name = 'virtualization/cluster/devices.html'
|
||||||
|
|
||||||
def get_extra_context(self, request, instance):
|
def get_extra_context(self, request, instance):
|
||||||
devices = Device.objects.restrict(request.user, 'view').filter(cluster=instance).prefetch_related(
|
devices = Device.objects.restrict(request.user, 'view').filter(cluster=instance).prefetch_related(
|
||||||
'site', 'rack', 'tenant', 'device_type__manufacturer'
|
'site', 'rack', 'tenant', 'device_type__manufacturer'
|
||||||
)
|
)
|
||||||
device_table = DeviceTable(list(devices), orderable=False)
|
devices_table = DeviceTable(list(devices), orderable=False)
|
||||||
if request.user.has_perm('virtualization.change_cluster'):
|
if request.user.has_perm('virtualization.change_cluster'):
|
||||||
device_table.columns.show('pk')
|
devices_table.columns.show('pk')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'device_table': device_table,
|
'devices_table': devices_table,
|
||||||
|
'active_tab': 'devices',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user