Skip to content

Commit b070be1

Browse files
committed
Closes #5425: Create separate tabs for VMs and devices under the cluster view
1 parent 8fa37d3 commit b070be1

File tree

9 files changed

+213
-110
lines changed

9 files changed

+213
-110
lines changed

docs/release-notes/version-2.11.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ A new Cloud model has been introduced to represent the boundary of a network tha
8787
* [#5370](https://github.com/netbox-community/netbox/issues/5370) - Extend custom field support to organizational models
8888
* [#5375](https://github.com/netbox-community/netbox/issues/5375) - Add `speed` attribute to console port models
8989
* [#5401](https://github.com/netbox-community/netbox/issues/5401) - Extend custom field support to device component models
90+
* [#5425](https://github.com/netbox-community/netbox/issues/5425) - Create separate tabs for VMs and devices under the cluster view
9091
* [#5451](https://github.com/netbox-community/netbox/issues/5451) - Add support for multiple-selection custom fields
9192
* [#5608](https://github.com/netbox-community/netbox/issues/5608) - Add REST API endpoint for custom links
9293
* [#5610](https://github.com/netbox-community/netbox/issues/5610) - Add REST API endpoint for webhooks
Lines changed: 70 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -1,117 +1,81 @@
1-
{% extends 'generic/object.html' %}
2-
{% load buttons %}
3-
{% load custom_links %}
1+
{% extends 'virtualization/cluster/base.html' %}
42
{% load helpers %}
53
{% load plugins %}
64

7-
{% block breadcrumbs %}
8-
<li><a href="{{ object.type.get_absolute_url }}">{{ object.type }}</a></li>
9-
{% if object.group %}
10-
<li><a href="{{ object.group.get_absolute_url }}">{{ object.group }}</a></li>
11-
{% endif %}
12-
<li>{{ object }}</li>
13-
{% endblock %}
14-
155
{% block content %}
166
<div class="row">
17-
<div class="col-md-5">
18-
<div class="panel panel-default">
19-
<div class="panel-heading">
20-
<strong>Cluster</strong>
21-
</div>
22-
<table class="table table-hover panel-body attr-table">
23-
<tr>
24-
<td>Name</td>
25-
<td>{{ object.name }}</td>
26-
</tr>
27-
<tr>
28-
<td>Type</td>
29-
<td><a href="{{ object.type.get_absolute_url }}">{{ object.type }}</a></td>
30-
</tr>
31-
<tr>
32-
<td>Group</td>
33-
<td>
34-
{% if object.group %}
35-
<a href="{{ object.group.get_absolute_url }}">{{ object.group }}</a>
36-
{% else %}
37-
<span class="text-muted">None</span>
38-
{% endif %}
39-
</td>
40-
</tr>
41-
<tr>
42-
<td>Tenant</td>
43-
<td>
44-
{% if object.tenant %}
45-
<a href="{{ object.tenant.get_absolute_url }}">{{ object.tenant }}</a>
46-
{% else %}
47-
<span class="text-muted">None</span>
48-
{% endif %}
49-
</td>
50-
</tr>
51-
<tr>
52-
<td>Site</td>
53-
<td>
54-
{% if object.site %}
55-
<a href="{{ object.site.get_absolute_url }}">{{ object.site }}</a>
56-
{% else %}
57-
<span class="text-muted">None</span>
58-
{% endif %}
59-
</td>
60-
</tr>
61-
<tr>
62-
<td>Virtual Machines</td>
63-
<td><a href="{% url 'virtualization:virtualmachine_list' %}?cluster_id={{ object.pk }}">{{ object.virtual_machines.count }}</a></td>
64-
</tr>
65-
</table>
66-
</div>
67-
{% include 'inc/custom_fields_panel.html' %}
68-
{% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='virtualization:cluster_list' %}
69-
<div class="panel panel-default">
70-
<div class="panel-heading">
71-
<strong>Comments</strong>
72-
</div>
73-
<div class="panel-body rendered-markdown">
74-
{% if object.comments %}
75-
{{ object.comments|render_markdown }}
76-
{% else %}
77-
<span class="text-muted">None</span>
78-
{% endif %}
79-
</div>
80-
</div>
81-
{% plugin_left_page object %}
82-
</div>
83-
<div class="col-md-7">
84-
<div class="panel panel-default">
85-
<div class="panel-heading">
86-
<strong>Host Devices</strong>
87-
</div>
88-
{% if perms.virtualization.change_cluster %}
89-
<form action="{% url 'virtualization:cluster_remove_devices' pk=object.pk %}" method="post">
90-
{% csrf_token %}
7+
<div class="col-md-6">
8+
<div class="panel panel-default">
9+
<div class="panel-heading">
10+
<strong>Cluster</strong>
11+
</div>
12+
<table class="table table-hover panel-body attr-table">
13+
<tr>
14+
<td>Name</td>
15+
<td>{{ object.name }}</td>
16+
</tr>
17+
<tr>
18+
<td>Type</td>
19+
<td><a href="{{ object.type.get_absolute_url }}">{{ object.type }}</a></td>
20+
</tr>
21+
<tr>
22+
<td>Group</td>
23+
<td>
24+
{% if object.group %}
25+
<a href="{{ object.group.get_absolute_url }}">{{ object.group }}</a>
26+
{% else %}
27+
<span class="text-muted">None</span>
28+
{% endif %}
29+
</td>
30+
</tr>
31+
<tr>
32+
<td>Tenant</td>
33+
<td>
34+
{% if object.tenant %}
35+
<a href="{{ object.tenant.get_absolute_url }}">{{ object.tenant }}</a>
36+
{% else %}
37+
<span class="text-muted">None</span>
9138
{% endif %}
92-
{% include 'responsive_table.html' with table=device_table %}
93-
{% if perms.virtualization.change_cluster %}
94-
<div class="panel-footer noprint">
95-
<div class="pull-right">
96-
<a href="{% url 'virtualization:cluster_add_devices' pk=object.pk %}?site={{ object.site.pk }}" class="btn btn-primary btn-xs">
97-
<span class="mdi mdi-plus-thick" aria-hidden="true"></span>
98-
Add devices
99-
</a>
100-
</div>
101-
<button type="submit" name="_remove" class="btn btn-danger primary btn-xs">
102-
<span class="mdi mdi-trash-can-outline" aria-hidden="true"></span>
103-
Remove devices
104-
</button>
105-
</div>
106-
</form>
39+
</td>
40+
</tr>
41+
<tr>
42+
<td>Site</td>
43+
<td>
44+
{% if object.site %}
45+
<a href="{{ object.site.get_absolute_url }}">{{ object.site }}</a>
46+
{% else %}
47+
<span class="text-muted">None</span>
10748
{% endif %}
108-
</div>
109-
{% plugin_right_page object %}
110-
</div>
49+
</td>
50+
</tr>
51+
<tr>
52+
<td>Virtual Machines</td>
53+
<td><a href="{% url 'virtualization:virtualmachine_list' %}?cluster_id={{ object.pk }}">{{ object.virtual_machines.count }}</a></td>
54+
</tr>
55+
</table>
56+
</div>
57+
{% include 'inc/custom_fields_panel.html' %}
58+
</div>
59+
<div class="col-md-6">
60+
{% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='virtualization:cluster_list' %}
61+
<div class="panel panel-default">
62+
<div class="panel-heading">
63+
<strong>Comments</strong>
64+
</div>
65+
<div class="panel-body rendered-markdown">
66+
{% if object.comments %}
67+
{{ object.comments|render_markdown }}
68+
{% else %}
69+
<span class="text-muted">None</span>
70+
{% endif %}
71+
</div>
72+
</div>
73+
{% plugin_left_page object %}
74+
</div>
11175
</div>
11276
<div class="row">
113-
<div class="col-md-12">
114-
{% plugin_full_width_page object %}
115-
</div>
77+
<div class="col-md-12">
78+
{% plugin_full_width_page object %}
79+
</div>
11680
</div>
11781
{% endblock %}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
{% extends 'generic/object.html' %}
2+
{% load buttons %}
3+
{% load helpers %}
4+
{% load custom_links %}
5+
{% load plugins %}
6+
7+
{% block breadcrumbs %}
8+
<li><a href="{{ object.type.get_absolute_url }}">{{ object.type }}</a></li>
9+
{% if object.group %}
10+
<li><a href="{{ object.group.get_absolute_url }}">{{ object.group }}</a></li>
11+
{% endif %}
12+
<li>{{ object }}</li>
13+
{% endblock %}
14+
15+
{% block buttons %}
16+
{% if perms.virtualization.change_cluster and perms.virtualization.add_virtualmachine %}
17+
<a href="{% url 'virtualization:virtualmachine_add' %}?cluster={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-primary">
18+
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Virtual Machine
19+
</a>
20+
{% endif %}
21+
{% if perms.virtualization.change_cluster %}
22+
<a href="{% url 'virtualization:cluster_add_devices' pk=object.pk %}?site={{ object.site.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-primary">
23+
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Assign Device
24+
</a>
25+
{% endif %}
26+
{{ block.super }}
27+
{% endblock %}
28+
29+
{% block tabs %}
30+
<ul class="nav nav-tabs">
31+
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
32+
<a href="{{ object.get_absolute_url }}">Cluster</a>
33+
</li>
34+
{% with virtualmachine_count=object.virtual_machines.count %}
35+
<li role="presentation" {% if active_tab == 'virtual-machines' %} class="active"{% endif %}>
36+
<a href="{% url 'virtualization:cluster_virtualmachines' pk=object.pk %}">Virtual Machines {% badge virtualmachine_count %}</a>
37+
</li>
38+
{% endwith %}
39+
{% with device_count=object.devices.count %}
40+
<li role="presentation" {% if active_tab == 'devices' %} class="active"{% endif %}>
41+
<a href="{% url 'virtualization:cluster_devices' pk=object.pk %}">Devices {% badge device_count %}</a>
42+
</li>
43+
{% endwith %}
44+
{% if perms.extras.view_journalentry %}
45+
<li role="presentation"{% if active_tab == 'journal' %} class="active"{% endif %}>
46+
<a href="{% url 'virtualization:cluster_journal' pk=object.pk %}">Journal</a>
47+
</li>
48+
{% endif %}
49+
{% if perms.extras.view_objectchange %}
50+
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
51+
<a href="{% url 'virtualization:cluster_changelog' pk=object.pk %}">Change Log</a>
52+
</li>
53+
{% endif %}
54+
</ul>
55+
{% endblock %}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{% extends 'virtualization/cluster/base.html' %}
2+
{% load helpers %}
3+
4+
{% block content %}
5+
<div class="row">
6+
<div class="col-md-12">
7+
<div class="panel panel-default">
8+
<div class="panel-heading">
9+
<strong>Host Devices</strong>
10+
</div>
11+
<form action="{% url 'virtualization:cluster_remove_devices' pk=object.pk %}" method="post">
12+
{% csrf_token %}
13+
{% include 'responsive_table.html' with table=devices_table %}
14+
{% if perms.virtualization.change_cluster %}
15+
<div class="panel-footer noprint">
16+
<button type="submit" name="_remove" class="btn btn-danger primary btn-xs">
17+
<span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> Remove devices
18+
</button>
19+
</div>
20+
{% endif %}
21+
</form>
22+
</div>
23+
</div>
24+
</div>
25+
{% endblock %}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{% extends 'virtualization/cluster/base.html' %}
2+
{% load helpers %}
3+
4+
{% block content %}
5+
<div class="row">
6+
<div class="col-md-12">
7+
<div class="panel panel-default">
8+
<div class="panel-heading">
9+
<strong>Virtual Machines</strong>
10+
</div>
11+
{% include 'responsive_table.html' with table=virtualmachines_table %}
12+
</div>
13+
</div>
14+
</div>
15+
{% endblock %}

netbox/templates/virtualization/virtualmachine/base.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@
3636
<a href="{% url 'virtualization:virtualmachine_configcontext' pk=object.pk %}">Config Context</a>
3737
</li>
3838
{% endif %}
39+
{% if perms.extras.view_journalentry %}
40+
<li role="presentation"{% if active_tab == 'journal' %} class="active"{% endif %}>
41+
<a href="{% url 'virtualization:virtualmachine_journal' pk=object.pk %}">Journal</a>
42+
</li>
43+
{% endif %}
3944
{% if perms.extras.view_objectchange %}
4045
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
4146
<a href="{% url 'virtualization:virtualmachine_changelog' pk=object.pk %}">Change Log</a>

netbox/virtualization/tests/test_views.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,20 @@ def setUpTestData(cls):
127127
'comments': 'New comments',
128128
}
129129

130+
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
131+
def test_cluster_virtualmachines(self):
132+
cluster = Cluster.objects.first()
133+
134+
url = reverse('virtualization:cluster_virtualmachines', kwargs={'pk': cluster.pk})
135+
self.assertHttpStatus(self.client.get(url), 200)
136+
137+
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
138+
def test_cluster_devices(self):
139+
cluster = Cluster.objects.first()
140+
141+
url = reverse('virtualization:cluster_devices', kwargs={'pk': cluster.pk})
142+
self.assertHttpStatus(self.client.get(url), 200)
143+
130144

131145
class VirtualMachineTestCase(ViewTestCases.PrimaryObjectViewTestCase):
132146
model = VirtualMachine
@@ -199,7 +213,7 @@ def setUpTestData(cls):
199213
}
200214

201215
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
202-
def test_device_interfaces(self):
216+
def test_virtualmachine_interfaces(self):
203217
virtualmachine = VirtualMachine.objects.first()
204218
vminterfaces = (
205219
VMInterface(virtual_machine=virtualmachine, name='Interface 1'),

netbox/virtualization/urls.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
path('clusters/edit/', views.ClusterBulkEditView.as_view(), name='cluster_bulk_edit'),
3838
path('clusters/delete/', views.ClusterBulkDeleteView.as_view(), name='cluster_bulk_delete'),
3939
path('clusters/<int:pk>/', views.ClusterView.as_view(), name='cluster'),
40+
path('clusters/<int:pk>/devices/', views.ClusterDevicesView.as_view(), name='cluster_devices'),
41+
path('clusters/<int:pk>/virtual-machines/', views.ClusterVirtualMachinesView.as_view(), name='cluster_virtualmachines'),
4042
path('clusters/<int:pk>/edit/', views.ClusterEditView.as_view(), name='cluster_edit'),
4143
path('clusters/<int:pk>/delete/', views.ClusterDeleteView.as_view(), name='cluster_delete'),
4244
path('clusters/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='cluster_changelog', kwargs={'model': Cluster}),

netbox/virtualization/views.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,16 +155,38 @@ class ClusterListView(generic.ObjectListView):
155155
class ClusterView(generic.ObjectView):
156156
queryset = Cluster.objects.all()
157157

158+
159+
class ClusterVirtualMachinesView(generic.ObjectView):
160+
queryset = Cluster.objects.all()
161+
template_name = 'virtualization/cluster/virtual_machines.html'
162+
163+
def get_extra_context(self, request, instance):
164+
virtualmachines = VirtualMachine.objects.restrict(request.user, 'view').filter(cluster=instance)
165+
virtualmachines_table = tables.VirtualMachineTable(virtualmachines, orderable=False)
166+
if request.user.has_perm('virtualization.change_cluster'):
167+
virtualmachines_table.columns.show('pk')
168+
169+
return {
170+
'virtualmachines_table': virtualmachines_table,
171+
'active_tab': 'virtual-machines',
172+
}
173+
174+
175+
class ClusterDevicesView(generic.ObjectView):
176+
queryset = Cluster.objects.all()
177+
template_name = 'virtualization/cluster/devices.html'
178+
158179
def get_extra_context(self, request, instance):
159180
devices = Device.objects.restrict(request.user, 'view').filter(cluster=instance).prefetch_related(
160181
'site', 'rack', 'tenant', 'device_type__manufacturer'
161182
)
162-
device_table = DeviceTable(list(devices), orderable=False)
183+
devices_table = DeviceTable(list(devices), orderable=False)
163184
if request.user.has_perm('virtualization.change_cluster'):
164-
device_table.columns.show('pk')
185+
devices_table.columns.show('pk')
165186

166187
return {
167-
'device_table': device_table,
188+
'devices_table': devices_table,
189+
'active_tab': 'devices',
168190
}
169191

170192

0 commit comments

Comments
 (0)