mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 11:16:47 +00:00
refactor(instances): the VM lists' scripts load from static files
The VM lists lose their inline scripts and handlers: host_groups.js collapses a host's VMs in the grouped list, new_vm_dialog.js reads the new-VM dialog's CPU bars and capacity lines, and console_window.js (loaded by every page) opens a console button's window, on the lists and on the VM page alike. A list row's Start no longer calls the please-wait dialog, which the list pages do not have (the call only raised an error). The VM page's URL placeholders take the __name__ form the other pages use.
This commit is contained in:
parent
e143fe353a
commit
946629b035
11 changed files with 98 additions and 91 deletions
|
|
@ -69,7 +69,7 @@
|
|||
<div class="col-sm-6">
|
||||
<label class="form-label" for="selected_storage">{% trans "Storage pool" %}</label>
|
||||
<select id="selected_storage" name="selected_storage" class="form-select" required
|
||||
data-volumes-url="{% url 'volumes' instance.compute.id 'POOL' %}">
|
||||
data-volumes-url="{% url 'volumes' instance.compute.id '__pool__' %}">
|
||||
<option value="" selected disabled>{% trans "Choose a pool" %}</option>
|
||||
{% for storage in storages_host %}
|
||||
<option value="{{ storage }}">{{ storage }}</option>
|
||||
|
|
|
|||
|
|
@ -120,58 +120,10 @@
|
|||
{% include "instances/vm_states_poll.html" %}
|
||||
<script src="{% static 'js/table_filter.js' %}"></script>
|
||||
<script src="{% static 'js/find_address.js' %}"></script>
|
||||
{% if app_settings.VIEW_INSTANCES_LIST_STYLE == 'grouped' and request.user.is_superuser %}
|
||||
<script src="{% static 'js/host_groups.js' %}"></script>
|
||||
{% endif %}
|
||||
{% if request.user.is_superuser %}
|
||||
<script>
|
||||
// The new-VM dialog's CPU bars and capacity lines, read when the
|
||||
// dialog opens, two reads at a time: a CPU read holds a worker for
|
||||
// a second, a capacity read while it reads the host's VMs. Answers
|
||||
// of an earlier opening are ignored.
|
||||
const addInstance = document.getElementById("AddInstance");
|
||||
let opening = 0, queue = [], reading = 0;
|
||||
|
||||
function setCpuUsage(cell, percent, text) {
|
||||
const meter = cell.querySelector(".progress");
|
||||
cell.querySelector(".progress-bar").style.width = (percent || 0) + "%";
|
||||
if (percent === null) {
|
||||
meter.removeAttribute("aria-valuenow");
|
||||
} else {
|
||||
meter.setAttribute("aria-valuenow", percent);
|
||||
}
|
||||
cell.querySelector(".cpu-usage-value").textContent = text;
|
||||
}
|
||||
|
||||
// data: the answer, or null when the read failed
|
||||
function show(element, data) {
|
||||
if (element.classList.contains("compute-capacity")) {
|
||||
element.textContent = data === null ? "" : data.trim();
|
||||
return;
|
||||
}
|
||||
const usage = data !== null && typeof data.cpudata === "number" ? Math.round(data.cpudata) : null;
|
||||
setCpuUsage(element, usage, usage === null ? "—" : usage + "%");
|
||||
}
|
||||
|
||||
function readNext() {
|
||||
while (reading < 2 && queue.length) {
|
||||
const element = queue.shift(), current = opening;
|
||||
reading++;
|
||||
$.get(element.dataset.url)
|
||||
.done((data) => { if (current === opening) show(element, data); })
|
||||
.fail(() => { if (current === opening) show(element, null); })
|
||||
.always(() => { reading--; readNext(); });
|
||||
}
|
||||
}
|
||||
|
||||
addInstance.addEventListener("show.bs.modal", function () {
|
||||
opening++;
|
||||
addInstance.querySelectorAll(".cpu-usage").forEach((cell) => setCpuUsage(cell, null, "…"));
|
||||
// host by host, as the list shows them
|
||||
queue = Array.from(addInstance.querySelectorAll(".cpu-usage, .compute-capacity"));
|
||||
readNext();
|
||||
});
|
||||
addInstance.addEventListener("hide.bs.modal", function () {
|
||||
opening++;
|
||||
queue = [];
|
||||
});
|
||||
</script>
|
||||
<script src="{% static 'js/new_vm_dialog.js' %}"></script>
|
||||
{% endif %}
|
||||
{% endblock script %}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
<thead>
|
||||
<tr>
|
||||
<th scope="col" data-sort-method="none">
|
||||
<button type="button" class="btn btn-sm btn-link p-0" id="hide-all-instances" onclick="hide_all_host_instances()" aria-expanded="true"
|
||||
<button type="button" class="btn btn-sm btn-link p-0" id="hide-all-instances" aria-expanded="true"
|
||||
aria-label="{% trans "Collapse or expand every host" %}">{% bs_icon 'arrows-collapse' %}</button>
|
||||
</th>
|
||||
<th scope="col">{% trans "Name" %}</th>
|
||||
|
|
@ -23,10 +23,10 @@
|
|||
{% if compute.instance_set.count > 0 %}
|
||||
<tr class="{% if compute.status is True %}table-secondary{% else %}table-warning{% endif %}" data-sort-method="none" data-group-header>
|
||||
<td>
|
||||
<button type="button" class="btn btn-sm btn-link p-0" onclick="hide_host_instances('{{ compute.id }}')"
|
||||
<button type="button" class="btn btn-sm btn-link p-0 host-toggle"
|
||||
aria-expanded="true" aria-controls="host-vms-{{ compute.id }}"
|
||||
aria-label="{% blocktrans with name=compute.name %}Collapse or expand {{ name }}{% endblocktrans %}">
|
||||
<span id="collapse_host_instances_{{ compute.id }}" class="bi bi-chevron-up" aria-hidden="true"></span>
|
||||
<span class="host-chevron bi bi-chevron-up" aria-hidden="true"></span>
|
||||
</button>
|
||||
</td>
|
||||
<td colspan="{% if app_settings.VM_DRBD_STATUS == 'True' %}3{% else %}2{% endif %}">
|
||||
|
|
@ -38,7 +38,7 @@
|
|||
<span class="badge bg-danger-subtle text-danger-emphasis">{% trans "Not connected" %}</span>
|
||||
{% endif %}
|
||||
<span class="small text-body-secondary">{% blocktrans count counter=compute.instance_set.count %}{{ counter }} VM{% plural %}{{ counter }} VMs{% endblocktrans %}</span>
|
||||
<span id="inst_count_badge_{{ compute.id }}" class="badge text-bg-secondary d-none">{% trans "collapsed" %}</span>
|
||||
<span class="host-collapsed-badge badge text-bg-secondary d-none">{% trans "collapsed" %}</span>
|
||||
</div>
|
||||
</td>
|
||||
{% if compute.status is True %}
|
||||
|
|
@ -83,27 +83,3 @@
|
|||
</table>
|
||||
</div>
|
||||
|
||||
{% block script %}
|
||||
<script>
|
||||
// a collapsed host's VM rows are hidden by its table body's class, so the
|
||||
// search (table_filter.js, the hidden attribute) can still hide them too
|
||||
function set_host_collapsed(tbody, collapsed) {
|
||||
$(tbody).toggleClass('host-collapsed', collapsed);
|
||||
$(tbody).find('[aria-controls]').attr('aria-expanded', !collapsed);
|
||||
$(tbody).find('span[id^=collapse_host_instances_]').toggleClass('bi-chevron-down', collapsed).toggleClass('bi-chevron-up', !collapsed);
|
||||
$(tbody).find('span[id^=inst_count_badge_]').toggleClass('d-none', !collapsed);
|
||||
// expanded while any host is, as the next click collapses them all
|
||||
$('#hide-all-instances').attr('aria-expanded', $('tbody.searchable').not('.host-collapsed').length > 0);
|
||||
}
|
||||
// every host the same way: collapsed, unless all already are
|
||||
function hide_all_host_instances() {
|
||||
var bodies = $('tbody.searchable');
|
||||
var collapse = bodies.not('.host-collapsed').length > 0;
|
||||
bodies.each(function () { set_host_collapsed(this, collapse); });
|
||||
}
|
||||
function hide_host_instances(host) {
|
||||
var tbody = $('#collapse_host_instances_' + host).closest('tbody');
|
||||
set_host_collapsed(tbody, !tbody.hasClass('host-collapsed'));
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -8,11 +8,11 @@
|
|||
<div class="d-flex justify-content-end">
|
||||
<div class="btn-group" role="group" aria-label="{% blocktrans with name=instance.name %}Actions for {{ name }}{% endblocktrans %}">
|
||||
<button class="btn btn-sm btn-outline-secondary" type="button" data-url="{% url 'console' instance.compute_id instance.uuid %}" data-window="console-{{ instance.uuid }}"
|
||||
onclick="window.open(this.dataset.url, this.dataset.window, 'width=850,height=650')" aria-label="{% trans "Console" %}"{% if "console" not in actions %} disabled{% endif %}>{% bs_icon 'display' %} <span class="d-none d-sm-inline">{% trans "Console" %}</span></button>
|
||||
aria-label="{% trans "Console" %}"{% if "console" not in actions %} disabled{% endif %}>{% bs_icon 'display' %} <span class="d-none d-sm-inline">{% trans "Console" %}</span></button>
|
||||
{% if "clone" in actions %}
|
||||
<a href="{% url 'instances:instance' instance.id %}#clone" class="btn btn-sm btn-outline-secondary" title="{% trans "Clone" %}" aria-label="{% trans "Clone" %}">{% bs_icon 'copy' %}</a>
|
||||
{% elif "poweron" in actions %}
|
||||
<form action="{% url 'instances:poweron' instance.id %}" method="post" class="btn-group" onsubmit="showPleaseWaitDialog()">
|
||||
<form action="{% url 'instances:poweron' instance.id %}" method="post" class="btn-group">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-sm btn-outline-secondary" title="{% trans "Start" %}" aria-label="{% trans "Start" %}">{% bs_icon 'play-fill' %}</button>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -770,7 +770,7 @@
|
|||
<div class="col-12 col-sm">
|
||||
<input class="form-control" id="clone_name" name="name" type="text" value="{{ instance.name }}-clone" aria-describedby="clone-name-help"
|
||||
data-source="{{ instance.name }}" data-guess-url="{% url 'instances:guess_clone_name' %}"
|
||||
data-guess-mac-url="{% url 'instances:guess_mac_address' 'NAME' %}">
|
||||
data-guess-mac-url="{% url 'instances:guess_mac_address' '__name__' %}">
|
||||
</div>
|
||||
<div class="col-12 col-sm-auto">
|
||||
<button class="btn btn-secondary" type="button" id="guess-clone-name">{% trans "Guess" %}</button>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import unittest
|
|||
from unittest.mock import PropertyMock, patch
|
||||
|
||||
from computes.models import Compute
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
|
@ -95,7 +96,8 @@ class CreateDialogCpuTestCase(TestCase):
|
|||
@unittest.skipUnless(shutil.which("node"), "node is not installed")
|
||||
def test_the_dialog_reads_host_by_host_two_at_a_time(self):
|
||||
page = self.page()
|
||||
script = next(s for s in html.fromstring(page).xpath("//script[not(@src)]/text()") if "show.bs.modal" in s)
|
||||
self.assertIn("/static/js/new_vm_dialog.js", html.fromstring(page).xpath("//script/@src"))
|
||||
script = (settings.BASE_DIR / "static/js/new_vm_dialog.js").read_text()
|
||||
cpu = [reverse("compute_graph", args=[c.id]) for c in self.computes]
|
||||
line = [reverse("compute_capacity", args=[c.id]) for c in self.computes]
|
||||
js = RUN.replace("SCRIPT", json.dumps(script)).replace("CPU_URLS", json.dumps(cpu)).replace("LINE_URLS", json.dumps(line))
|
||||
|
|
|
|||
6
static/js/console_window.js
Normal file
6
static/js/console_window.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
// A console button (data-url, data-window) opens the console in one window
|
||||
// per VM: opening it again reloads that window.
|
||||
document.addEventListener("click", function (event) {
|
||||
var button = event.target.closest("button[data-window]");
|
||||
if (button) window.open(button.dataset.url, button.dataset.window, "width=850,height=650");
|
||||
});
|
||||
24
static/js/host_groups.js
Normal file
24
static/js/host_groups.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
// The VM list grouped by host (allinstances_index_grouped.html): a host's
|
||||
// VMs collapse under its row. A collapsed host's VM rows are hidden by its
|
||||
// table body's class, so the search (table_filter.js, the hidden attribute)
|
||||
// can still hide them too.
|
||||
function set_host_collapsed(tbody, collapsed) {
|
||||
$(tbody).toggleClass('host-collapsed', collapsed);
|
||||
$(tbody).find('[aria-controls]').attr('aria-expanded', !collapsed);
|
||||
$(tbody).find('.host-chevron').toggleClass('bi-chevron-down', collapsed).toggleClass('bi-chevron-up', !collapsed);
|
||||
$(tbody).find('.host-collapsed-badge').toggleClass('d-none', !collapsed);
|
||||
// expanded while any host is, as the next click collapses them all
|
||||
$('#hide-all-instances').attr('aria-expanded', $('tbody.searchable').not('.host-collapsed').length > 0);
|
||||
}
|
||||
|
||||
// every host the same way: collapsed, unless all already are
|
||||
$('#hide-all-instances').on('click', function () {
|
||||
var bodies = $('tbody.searchable');
|
||||
var collapse = bodies.not('.host-collapsed').length > 0;
|
||||
bodies.each(function () { set_host_collapsed(this, collapse); });
|
||||
});
|
||||
|
||||
$('.host-toggle').on('click', function () {
|
||||
var tbody = $(this).closest('tbody');
|
||||
set_host_collapsed(tbody, !tbody.hasClass('host-collapsed'));
|
||||
});
|
||||
|
|
@ -50,10 +50,6 @@
|
|||
show_tab('#clone');
|
||||
});
|
||||
|
||||
// one window per VM: opening it again reloads it
|
||||
on("console_button", "click", function () {
|
||||
window.open(this.dataset.url, this.dataset.window, 'width=850,height=650');
|
||||
});
|
||||
if (summary.dataset.vdiUrl) {
|
||||
$.get(summary.dataset.vdiUrl, function (data) {
|
||||
$("#vdi_url_input").attr("value", data);
|
||||
|
|
@ -88,7 +84,7 @@
|
|||
var pool = document.getElementById("selected_storage");
|
||||
if (pool) {
|
||||
pool.addEventListener("change", function () {
|
||||
$.getJSON(pool.dataset.volumesUrl.replace("POOL", encodeURIComponent(pool.value)), function (data) {
|
||||
$.getJSON(pool.dataset.volumesUrl.replace("__pool__", encodeURIComponent(pool.value)), function (data) {
|
||||
var vols = $("#vols").empty();
|
||||
$.each(data['vols'], function (i, item) {
|
||||
vols.append($("<option>").val(item).text(item));
|
||||
|
|
@ -109,7 +105,7 @@
|
|||
var cloneName = document.getElementById("clone_name");
|
||||
// the DHCP host entry of the clone's name holds one address: the first NIC's
|
||||
function guess_mac_address() {
|
||||
$.getJSON(cloneName.dataset.guessMacUrl.replace("NAME", encodeURIComponent(cloneName.value)), function (data) {
|
||||
$.getJSON(cloneName.dataset.guessMacUrl.replace("__name__", encodeURIComponent(cloneName.value)), function (data) {
|
||||
$('input[name="clone-net-mac-0"]').val(data['mac']);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
50
static/js/new_vm_dialog.js
Normal file
50
static/js/new_vm_dialog.js
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
// The new-VM dialog's CPU bars and capacity lines, read when the
|
||||
// dialog opens, two reads at a time: a CPU read holds a worker for
|
||||
// a second, a capacity read while it reads the host's VMs. Answers
|
||||
// of an earlier opening are ignored.
|
||||
const addInstance = document.getElementById("AddInstance");
|
||||
let opening = 0, queue = [], reading = 0;
|
||||
|
||||
function setCpuUsage(cell, percent, text) {
|
||||
const meter = cell.querySelector(".progress");
|
||||
cell.querySelector(".progress-bar").style.width = (percent || 0) + "%";
|
||||
if (percent === null) {
|
||||
meter.removeAttribute("aria-valuenow");
|
||||
} else {
|
||||
meter.setAttribute("aria-valuenow", percent);
|
||||
}
|
||||
cell.querySelector(".cpu-usage-value").textContent = text;
|
||||
}
|
||||
|
||||
// data: the answer, or null when the read failed
|
||||
function show(element, data) {
|
||||
if (element.classList.contains("compute-capacity")) {
|
||||
element.textContent = data === null ? "" : data.trim();
|
||||
return;
|
||||
}
|
||||
const usage = data !== null && typeof data.cpudata === "number" ? Math.round(data.cpudata) : null;
|
||||
setCpuUsage(element, usage, usage === null ? "—" : usage + "%");
|
||||
}
|
||||
|
||||
function readNext() {
|
||||
while (reading < 2 && queue.length) {
|
||||
const element = queue.shift(), current = opening;
|
||||
reading++;
|
||||
$.get(element.dataset.url)
|
||||
.done((data) => { if (current === opening) show(element, data); })
|
||||
.fail(() => { if (current === opening) show(element, null); })
|
||||
.always(() => { reading--; readNext(); });
|
||||
}
|
||||
}
|
||||
|
||||
addInstance.addEventListener("show.bs.modal", function () {
|
||||
opening++;
|
||||
addInstance.querySelectorAll(".cpu-usage").forEach((cell) => setCpuUsage(cell, null, "…"));
|
||||
// host by host, as the list shows them
|
||||
queue = Array.from(addInstance.querySelectorAll(".cpu-usage, .compute-capacity"));
|
||||
readNext();
|
||||
});
|
||||
addInstance.addEventListener("hide.bs.modal", function () {
|
||||
opening++;
|
||||
queue = [];
|
||||
});
|
||||
|
|
@ -85,6 +85,7 @@
|
|||
<script src="{% static 'js/sort_tables.js' %}"></script>
|
||||
<script src="{% static 'js/error_focus.js' %}"></script>
|
||||
<script src="{% static 'js/back_links.js' %}"></script>
|
||||
<script src="{% static 'js/console_window.js' %}"></script>
|
||||
{% block script %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue