1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-09 11:46:49 +00:00

feat(templates): every confirmation asks in the page's own dialog

Deleting or detaching a disk, deleting a network card, forcing a VM off,
reverting or deleting a snapshot, stopping or deleting a pool, a
network, an interface, a filter or a secret, and the other actions that
ask first opened the browser's yes/no box (confirm()), and the ISO
upload told its mistakes in the browser's alert().

One dialog in the base page now asks for all of them: its title and
confirming button carry the button's own words, its body the button's
question, the confirming button red for a destructive action. Confirmed,
the button is clicked again, so its form sends what it did before. A
button inside another dialog (the flavors) hides that one while asking
and shows it again after; Cancel gives the focus back, and the
confirming button acts once. The ISO upload's mistakes show in its
dialog's error box, announced.
This commit is contained in:
catborise 2026-10-31 20:22:49 +03:00
parent 95562544eb
commit c401479cf9
17 changed files with 114 additions and 68 deletions

View file

@ -8,8 +8,7 @@
<span><strong>{{ flavor.label }}</strong> · {{ flavor.vcpu }} vCPU · {{ flavor.memory }} MiB · {{ flavor.disk }} GiB</span>
<form class="ms-auto" action="{% url 'instances:flavor_delete' flavor.id %}" method="post">{% csrf_token %}
<button type="submit" class="btn btn-sm btn-secondary"
data-confirm="{% blocktrans with name=flavor.label %}Delete flavor {{ name }}? Only the size template goes; VMs made from it keep their size.{% endblocktrans %}"
onclick="return confirm(this.dataset.confirm)">{% trans "Delete" %}</button>
data-confirm="{% blocktrans with name=flavor.label %}Delete flavor {{ name }}? Only the size template goes; VMs made from it keep their size.{% endblocktrans %}">{% trans "Delete" %}</button>
</form>
</li>
{% endfor %}

View file

@ -47,15 +47,13 @@
{% if "force_off" in actions %}
<li><form action="{% url 'instances:force_off' instance.id %}" method="post">{% csrf_token %}
<button type="submit" class="dropdown-item text-danger"
data-confirm="{% blocktrans with name=instance.name %}Force off {{ name }}? It stops at once, as after a power cut; what the guest has not written to disk is lost.{% endblocktrans %}"
onclick="return confirm(this.dataset.confirm)">{% bs_icon 'power' %} {% trans "Force off" %}</button>
data-confirm="{% blocktrans with name=instance.name %}Force off {{ name }}? It stops at once, as after a power cut; what the guest has not written to disk is lost.{% endblocktrans %}">{% bs_icon 'power' %} {% trans "Force off" %}</button>
</form></li>
{% endif %}
{% if "powercycle" in actions %}
<li><form action="{% url 'instances:powercycle' instance.id %}" method="post">{% csrf_token %}
<button type="submit" class="dropdown-item text-danger"
data-confirm="{% blocktrans with name=instance.name %}Power cycle {{ name }}? It stops at once, as after a power cut, and starts again; what the guest has not written to disk is lost.{% endblocktrans %}"
onclick="return confirm(this.dataset.confirm)">{% bs_icon 'repeat' %} {% trans "Power cycle" %}</button>
data-confirm="{% blocktrans with name=instance.name %}Power cycle {{ name }}? It stops at once, as after a power cut, and starts again; what the guest has not written to disk is lost.{% endblocktrans %}">{% bs_icon 'repeat' %} {% trans "Power cycle" %}</button>
</form></li>
{% endif %}
</ul>

View file

@ -21,6 +21,6 @@
<div class="d-flex flex-wrap gap-2 mt-2">
<button type="submit" class="btn btn-sm btn-primary" name="set_qos">{% trans "Save" %}</button>
<button type="submit" class="btn btn-sm btn-danger" name="unset_qos" formaction="{% url 'instances:unset_qos' instance.id %}" formnovalidate
data-confirm="{% blocktrans with mac=network.mac %}Remove the {{ direction }} limit of {{ mac }}? A running VM keeps it until it is powered off and on.{% endblocktrans %}" onclick="return confirm(this.dataset.confirm)">{% trans "Delete limit" %}</button>
data-confirm="{% blocktrans with mac=network.mac %}Remove the {{ direction }} limit of {{ mac }}? A running VM keeps it until it is powered off and on.{% endblocktrans %}">{% trans "Delete limit" %}</button>
</div>
</form>

View file

@ -32,13 +32,11 @@
<div class="btn-group mb-2" role="group" aria-label="{% trans "Power actions that cut the power" %}">
{% if "powercycle" in power_actions %}
<button type="submit" class="btn btn-danger" name="powercycle" formaction="{% url 'instances:powercycle' instance.id %}"
data-confirm="{% blocktrans with name=instance.name %}Power cycle {{ name }}? It stops at once, as after a power cut, and starts again; what the guest has not written to disk is lost.{% endblocktrans %}"
onclick="return confirm(this.dataset.confirm)">{% trans "Power Cycle" %}</button>
data-confirm="{% blocktrans with name=instance.name %}Power cycle {{ name }}? It stops at once, as after a power cut, and starts again; what the guest has not written to disk is lost.{% endblocktrans %}">{% trans "Power Cycle" %}</button>
{% endif %}
{% if "force_off" in power_actions %}
<button type="submit" class="btn btn-danger" name="powerforce" formaction="{% url 'instances:force_off' instance.id %}"
data-confirm="{% blocktrans with name=instance.name %}Force off {{ name }}? It stops at once, as after a power cut; what the guest has not written to disk is lost.{% endblocktrans %}"
onclick="return confirm(this.dataset.confirm)">{% trans "Force Off" %}</button>
data-confirm="{% blocktrans with name=instance.name %}Force off {{ name }}? It stops at once, as after a power cut; what the guest has not written to disk is lost.{% endblocktrans %}">{% trans "Force Off" %}</button>
{% endif %}
</div>
<p class="small text-body-secondary mb-0">

View file

@ -246,7 +246,7 @@
<input type="hidden" name="dev" value="{{ disk.dev }}">
<input type="hidden" name="storage" value="{{ disk.storage }}">
<input type="hidden" name="name" value="{{ disk.image }}">
<button type="submit" class="btn btn-sm btn-secondary" value="{{ disk.dev }}"{% if instance.status != 5 %} disabled{% endif %} data-confirm="{% blocktrans with dev=disk.dev %}Detach {{ dev }}? Its file is kept.{% endblocktrans %}" onclick="return confirm(this.dataset.confirm)">{% trans "Detach" %}</button>
<button type="submit" class="btn btn-sm btn-secondary" value="{{ disk.dev }}"{% if instance.status != 5 %} disabled{% endif %} data-confirm="{% blocktrans with dev=disk.dev %}Detach {{ dev }}? Its file is kept.{% endblocktrans %}">{% trans "Detach" %}</button>
</form>
<form class="d-inline" action="{% url 'instances:delete_vol' instance.id %}" method="post">
{% csrf_token %}
@ -254,7 +254,7 @@
<input type="hidden" name="dev" value="{{ disk.dev }}">
<input type="hidden" name="storage" value="{{ disk.storage }}">
<input type="hidden" name="name" value="{{ disk.image }}">
<button type="submit" class="btn btn-sm btn-danger"{% if instance.status != 5 %} disabled{% endif %} data-confirm="{% blocktrans with dev=disk.dev %}Delete {{ dev }}? It is detached and its file and data are removed.{% endblocktrans %}" onclick="return confirm(this.dataset.confirm)">{% trans "Delete" %}</button>
<button type="submit" class="btn btn-sm btn-danger"{% if instance.status != 5 %} disabled{% endif %} data-confirm="{% blocktrans with dev=disk.dev %}Delete {{ dev }}? It is detached and its file and data are removed.{% endblocktrans %}">{% trans "Delete" %}</button>
</form>
</div>
</div>
@ -328,7 +328,7 @@
{% if network.type != "hostdev" %}
<form action="{% url 'instances:delete_network' instance.id %}" method="post" aria-label="{% trans "Delete network device form" %}">{% csrf_token %}
<button class="btn btn-sm btn-danger" value="{{ network.mac }}" name="delete_network"
data-confirm="{% blocktrans with mac=network.mac %}Remove network card {{ mac }}? A running VM loses its connection through it at once.{% endblocktrans %}" onclick="return confirm(this.dataset.confirm)">{% trans "Delete" %}</button>
data-confirm="{% blocktrans with mac=network.mac %}Remove network card {{ mac }}? A running VM loses its connection through it at once.{% endblocktrans %}">{% trans "Delete" %}</button>
</form>
{% endif %}
</div>
@ -467,8 +467,7 @@
<input type="hidden" name="product" value="{{ device.product }}">
<input type="hidden" name="address" value="{% if device.address %}{{ device.address.0 }}/{{ device.address.1 }}{% endif %}">
<button type="submit" class="btn btn-sm btn-outline-danger" aria-label="{% trans "Remove the USB device" %}"
data-confirm="{% trans "Remove this USB device from the VM? A running VM loses it at once." %}"
onclick="return confirm(this.dataset.confirm)">{% bs_icon 'trash' %}</button>
data-confirm="{% trans "Remove this USB device from the VM? A running VM loses it at once." %}">{% bs_icon 'trash' %}</button>
</form>
</td>
{% endif %}
@ -501,7 +500,7 @@
<input type="hidden" name="address" value="{{ device.address }}">
<button type="submit" class="btn btn-sm btn-outline-danger" aria-label="{% trans "Remove the PCI device" %}"
{% if instance.status != 1 and instance.status != 5 or instance.has_managed_save_image %}disabled title="{% trans "A paused VM, or one with a saved state, does not change its PCI devices" %}"{% endif %}
data-confirm="{% trans "Remove this PCI device from the VM? A running VM's guest has to let it go." %}" onclick="return confirm(this.dataset.confirm)">{% bs_icon 'trash' %}</button>
data-confirm="{% trans "Remove this PCI device from the VM? A running VM's guest has to let it go." %}">{% bs_icon 'trash' %}</button>
</form>
</td>
{% endif %}

View file

@ -87,27 +87,27 @@
<input type="hidden" name="name" value="{{ snap.name }}">
<button type="submit" class="btn btn-sm btn-secondary" name="revert_external_snapshot"
data-confirm="{% blocktrans with name=snap.name %}Revert to snapshot {{ name }}? Changes made since it are lost.{% endblocktrans %}{% if not snap.memory %}{% if instance.status == 1 %} {% trans "The VM is powered off and starts again from the snapshot's disks, as after a power cut." %}{% elif instance.status == 3 %} {% trans "The VM is powered off and comes back paused on the snapshot's disks." %}{% endif %}{% endif %}{% if snap.detaches %} {% trans 'These disks are detached; their writes since the current snapshot are discarded and their files are kept:' %} {% for d in snap.detaches %}{{ d.dev }} ({{ d.path }}){% if not forloop.last %}, {% endif %}{% endfor %}.{% endif %}{% if instance.nvram %} {% trans 'Firmware variables (NVRAM) are not restored.' %}{% endif %}"
onclick="return confirm(this.dataset.confirm)"{% if busy or held %} disabled{% endif %}>{% trans "Revert" %}</button>
{% if busy or held %} disabled{% endif %}>{% trans "Revert" %}</button>
</form>
<form action="{% url 'instances:delete_external_snapshot' instance.id %}" method="post">{% csrf_token %}{% submission_key %}
<input type="hidden" name="name" value="{{ snap.name }}">
<button type="submit" class="btn btn-sm btn-danger" name="delete_external_snapshot"
data-confirm="{% blocktrans with name=snap.name %}Delete snapshot {{ name }}? Its files are merged with the disks; the VM's data is kept.{% endblocktrans %}"
onclick="return confirm(this.dataset.confirm)"{% if busy or held %} disabled{% endif %}>{% trans "Delete" %}</button>
{% if busy or held %} disabled{% endif %}>{% trans "Delete" %}</button>
</form>
{% else %}
<form action="{% url 'instances:revert_snapshot' instance.id %}" method="post">{% csrf_token %}{% submission_key %}
<input type="hidden" name="name" value="{{ snap.name }}">
<button type="submit" class="btn btn-sm btn-secondary" name="revert_snapshot"
data-confirm="{% blocktrans with name=snap.name %}Revert to snapshot {{ name }}? Disk changes made since it are lost; a snapshot of a running VM starts it again as it was then.{% endblocktrans %}"
onclick="return confirm(this.dataset.confirm)"{% if instance.status != 5 or busy or held %} disabled{% endif %}
{% if instance.status != 5 or busy or held %} disabled{% endif %}
{% if instance.status != 5 %}title="{% trans "Shut the VM down to revert to an internal snapshot." %}"{% endif %}>{% trans "Revert" %}</button>
</form>
<form action="{% url 'instances:delete_snapshot' instance.id %}" method="post">{% csrf_token %}
<input type="hidden" name="name" value="{{ snap.name }}">
<button type="submit" class="btn btn-sm btn-danger"
data-confirm="{% blocktrans with name=snap.name %}Delete snapshot {{ name }}? The VM's current disks are not changed.{% endblocktrans %}"
onclick="return confirm(this.dataset.confirm)"{% if busy or held %} disabled{% endif %}>{% trans "Delete" %}</button>
{% if busy or held %} disabled{% endif %}>{% trans "Delete" %}</button>
</form>
{% endif %}
</li>

View file

@ -98,7 +98,7 @@ class StateTests(TestCase):
for action in {"force_off", "powercycle"} & ACTIONS[state]:
button = page.xpath(f"//form[@action='{reverse(f'instances:{action}', args=[vm.id])}']//button")[0]
with self.subTest(url=url, vm=vm.name, action=action):
self.assertEqual(button.get("onclick"), "return confirm(this.dataset.confirm)")
self.assertIsNone(button.get("onclick")) # the page's dialog asks, not the browser
self.assertIn(vm.name, button.get("data-confirm"))
self.assertIn("power cut", button.get("data-confirm"))

View file

@ -13,8 +13,7 @@
<form method="post" class="btn-group">{% csrf_token %}
{% if state %}
<button type="submit" class="btn btn-outline-secondary" name="stop"
data-confirm="{% blocktrans with name=iface %}Stop interface {{ name }}? The host and its VMs lose their connections through it at once; if you reach the host through it, you lose the host.{% endblocktrans %}"
onclick="return confirm(this.dataset.confirm)">{% bs_icon 'stop-fill' %} {% trans "Stop" %}</button>
data-confirm="{% blocktrans with name=iface %}Stop interface {{ name }}? The host and its VMs lose their connections through it at once; if you reach the host through it, you lose the host.{% endblocktrans %}">{% bs_icon 'stop-fill' %} {% trans "Stop" %}</button>
{% else %}
<button type="submit" class="btn btn-outline-secondary" name="start">{% bs_icon 'play-fill' %} {% trans "Start" %}</button>
{% endif %}
@ -24,8 +23,7 @@
<ul class="dropdown-menu dropdown-menu-end shadow">
<li><form method="post">{% csrf_token %}
<button type="submit" class="dropdown-item text-danger" name="delete"{% if state %} disabled{% endif %}
data-confirm="{% blocktrans with name=iface %}Delete interface {{ name }}? Its definition is removed from the host; a bridge's members are defined again on their own, its addresses going to its only member.{% endblocktrans %}"
onclick="return confirm(this.dataset.confirm)">{% bs_icon 'trash' %} {% if state %}{% trans "Delete (stop it first)" %}{% else %}{% trans "Delete interface" %}{% endif %}</button>
data-confirm="{% blocktrans with name=iface %}Delete interface {{ name }}? Its definition is removed from the host; a bridge's members are defined again on their own, its addresses going to its only member.{% endblocktrans %}">{% bs_icon 'trash' %} {% if state %}{% trans "Delete (stop it first)" %}{% else %}{% trans "Delete interface" %}{% endif %}</button>
</form></li>
</ul>
</div>

View file

@ -13,8 +13,7 @@
<form method="post" class="btn-group">{% csrf_token %}
{% if state %}
<button type="submit" class="btn btn-outline-secondary" name="stop"
data-confirm="{% blocktrans with name=pool %}Stop network {{ name }}? Running VMs on it lose their connection through it at once.{% endblocktrans %}"
onclick="return confirm(this.dataset.confirm)">{% bs_icon 'stop-fill' %} {% trans "Stop" %}</button>
data-confirm="{% blocktrans with name=pool %}Stop network {{ name }}? Running VMs on it lose their connection through it at once.{% endblocktrans %}">{% bs_icon 'stop-fill' %} {% trans "Stop" %}</button>
{% else %}
<button type="submit" class="btn btn-outline-secondary" name="start">{% bs_icon 'play-fill' %} {% trans "Start" %}</button>
{% endif %}
@ -25,8 +24,7 @@
<li><form method="post">{% csrf_token %}
{% if autostart %}
<button type="submit" class="dropdown-item" name="unset_autostart"
data-confirm="{% blocktrans with name=pool %}Disable autostart of network {{ name }}? After the host restarts it stays stopped, and VMs that use it do not start.{% endblocktrans %}"
onclick="return confirm(this.dataset.confirm)">{% bs_icon 'toggle-off' %} {% trans "Disable autostart" %}</button>
data-confirm="{% blocktrans with name=pool %}Disable autostart of network {{ name }}? After the host restarts it stays stopped, and VMs that use it do not start.{% endblocktrans %}">{% bs_icon 'toggle-off' %} {% trans "Disable autostart" %}</button>
{% else %}
<button type="submit" class="dropdown-item" name="set_autostart">{% bs_icon 'toggle-on' %} {% trans "Enable autostart" %}</button>
{% endif %}
@ -35,8 +33,7 @@
<li><hr class="dropdown-divider"></li>
<li><form method="post">{% csrf_token %}
<button type="submit" class="dropdown-item text-danger" name="delete"
data-confirm="{% blocktrans with name=pool %}Delete network {{ name }}? VMs that use it no longer start.{% endblocktrans %}"
onclick="return confirm(this.dataset.confirm)">{% bs_icon 'trash' %} {% trans "Delete network" %}</button>
data-confirm="{% blocktrans with name=pool %}Delete network {{ name }}? VMs that use it no longer start.{% endblocktrans %}">{% bs_icon 'trash' %} {% trans "Delete network" %}</button>
</form></li>
{% endif %}
</ul>
@ -168,8 +165,7 @@
<input type="hidden" name="ip_index" value="{{ host.ip_index }}">
<input type="hidden" name="original" value="{{ host.ip }}">
<button type="submit" class="dropdown-item text-danger" name="delete_fixed_address"
data-confirm="{% blocktrans with address=host.ip %}Delete the fixed address {{ address }}? The VM may get another address when its lease ends.{% endblocktrans %}"
onclick="return confirm(this.dataset.confirm)">{% bs_icon 'trash' %} {% trans "Delete" %}</button>
data-confirm="{% blocktrans with address=host.ip %}Delete the fixed address {{ address }}? The VM may get another address when its lease ends.{% endblocktrans %}">{% bs_icon 'trash' %} {% trans "Delete" %}</button>
</form></li>
</ul>
</div>
@ -305,8 +301,7 @@
<form method="post" class="btn-group">{% csrf_token %}
<input type="hidden" name="qos_direction" value="{{ direction }}">
<button type="submit" class="btn btn-sm btn-outline-secondary" name="unset_qos" aria-label="{% trans "Remove" %}"
data-confirm="{% blocktrans with name=pool %}Remove the {{ direction }} limit of network {{ name }}? It stays in force until the network is stopped and started.{% endblocktrans %}"
onclick="return confirm(this.dataset.confirm)">{% bs_icon 'trash' %}</button>
data-confirm="{% blocktrans with name=pool %}Remove the {{ direction }} limit of network {{ name }}? It stays in force until the network is stopped and started.{% endblocktrans %}">{% bs_icon 'trash' %}</button>
</form>
</div>
</td>

View file

@ -69,8 +69,7 @@
<input type="hidden" name="index" value="{{ forloop.counter0 }}">
<input type="hidden" name="ref" value="{{ ref.text }}">
<button type="submit" class="btn btn-sm btn-outline-danger" name="del_nwfilter_ref" aria-label="{% blocktrans with ref=ref.filter %}Remove the reference to {{ ref }}{% endblocktrans %}"
data-confirm="{% blocktrans with ref=ref.filter %}Remove the reference to {{ ref }}? Its rules no longer apply through this filter.{% endblocktrans %}"
onclick="return confirm(this.dataset.confirm)">{% bs_icon 'trash' %}</button>
data-confirm="{% blocktrans with ref=ref.filter %}Remove the reference to {{ ref }}? Its rules no longer apply through this filter.{% endblocktrans %}">{% bs_icon 'trash' %}</button>
</form>
</td>
</tr>
@ -125,8 +124,7 @@
<input type="hidden" name="index" value="{{ forloop.counter0 }}">
<input type="hidden" name="rule" value="{{ rule.text }}">
<button type="submit" class="btn btn-sm btn-outline-danger" name="del_nwfilter_rule" aria-label="{% blocktrans with number=forloop.counter %}Delete rule {{ number }}{% endblocktrans %}"
data-confirm="{% trans "Delete this rule? Interfaces that use the filter no longer get it." %}"
onclick="return confirm(this.dataset.confirm)">{% bs_icon 'trash' %}</button>
data-confirm="{% trans "Delete this rule? Interfaces that use the filter no longer get it." %}">{% bs_icon 'trash' %}</button>
</form>
</td>
</tr>

View file

@ -60,8 +60,7 @@
<input type="hidden" name="nwfiltername" value="{{ nwf.name }}">
{# a filter in use is not deleted: a VM using it, or a filter referencing it, would not start #}
<button type="submit" class="dropdown-item text-danger" name="del_nwfilter"{% if nwf.vms or nwf.referenced_by %} disabled{% endif %}
data-confirm="{% blocktrans with name=nwf.name %}Delete filter {{ name }}?{% endblocktrans %}"
onclick="return confirm(this.dataset.confirm)">{% bs_icon 'trash' %} {% if nwf.vms or nwf.referenced_by %}{% trans "Delete (in use)" %}{% else %}{% trans "Delete filter" %}{% endif %}</button>
data-confirm="{% blocktrans with name=nwf.name %}Delete filter {{ name }}?{% endblocktrans %}">{% bs_icon 'trash' %} {% if nwf.vms or nwf.referenced_by %}{% trans "Delete (in use)" %}{% else %}{% trans "Delete filter" %}{% endif %}</button>
</form></li>
</ul>
</div>

View file

@ -0,0 +1,46 @@
// A button with data-confirm asks in the page's confirm dialog before it acts:
// the dialog's title and its confirming button carry the button's own words
// (or its accessible name, for an icon), its body the data-confirm question.
// Confirmed, the button is clicked again, so its form sends its name and value.
// A button in another dialog hides that one while asking (Bootstrap does not
// stack dialogs) and shows it again after; Cancel gives the focus back.
document.addEventListener("click", function (event) {
var button = event.target.closest("button[data-confirm]");
if (!button || button.dataset.confirmed) return;
event.preventDefault();
var dialog = document.getElementById("confirm-dialog");
var parent = button.closest(".modal");
var action = button.textContent.trim() || button.getAttribute("aria-label") || button.title;
var ok = dialog.querySelector("[data-confirm-ok]");
var confirmed = false;
dialog.querySelector(".modal-title").textContent = action;
dialog.querySelector(".modal-body").textContent = button.dataset.confirm;
ok.textContent = action;
ok.className = "btn " + (/\b(btn-|btn-outline-|text-)danger\b/.test(button.className) ? "btn-danger" : "btn-primary");
ok.disabled = false;
ok.onclick = function () {
ok.disabled = true; // once: the dialog is still open while it hides
confirmed = true;
bootstrap.Modal.getInstance(dialog).hide();
button.dataset.confirmed = "1";
button.click();
delete button.dataset.confirmed;
};
dialog.addEventListener("hidden.bs.modal", function () {
if (parent) {
// a form sent in place (the flavors) keeps its dialog; a page sent away does not care
bootstrap.Modal.getOrCreateInstance(parent).show();
} else if (!confirmed) {
// a menu's item is hidden with its menu: its toggle takes the focus
var menu = button.closest(".dropdown");
(menu ? menu.querySelector("[data-bs-toggle=dropdown]") : button).focus();
}
}, {once: true});
var ask = function () { bootstrap.Modal.getOrCreateInstance(dialog).show(); };
if (parent) {
parent.addEventListener("hidden.bs.modal", ask, {once: true});
bootstrap.Modal.getInstance(parent).hide();
} else {
ask();
}
}, true);

View file

@ -24,7 +24,7 @@
<span class="spinner-border spinner-border-sm" aria-hidden="true"></span>
<span id="upload-status-text">{% trans "Starting upload..." %}</span>
</div>
<div id="upload-error-message" class="alert alert-danger mt-3 d-none"></div>
<div id="upload-error-message" class="alert alert-danger mt-3 d-none" role="alert"></div>
</div>
<div class="modal-footer">
<input type="hidden" name="iso_upload" value="true">

View file

@ -15,8 +15,7 @@
{% if state %}
<form method="post" class="btn-group">{% csrf_token %}
<button type="submit" class="btn btn-outline-secondary" name="stop"
data-confirm="{% blocktrans with name=pool %}Stop pool {{ name }}? Its files stay, but until it is started again its volumes cannot be managed and VMs with disks in it may not start.{% endblocktrans %}"
onclick="return confirm(this.dataset.confirm)">{% bs_icon 'stop-fill' %} {% trans "Stop" %}</button>
data-confirm="{% blocktrans with name=pool %}Stop pool {{ name }}? Its files stay, but until it is started again its volumes cannot be managed and VMs with disks in it may not start.{% endblocktrans %}">{% bs_icon 'stop-fill' %} {% trans "Stop" %}</button>
</form>
{% else %}
<form method="post" class="btn-group">{% csrf_token %}
@ -29,8 +28,7 @@
<li><form method="post">{% csrf_token %}
{% if autostart %}
<button type="submit" class="dropdown-item" name="unset_autostart"
data-confirm="{% blocktrans with name=pool %}Disable autostart of pool {{ name }}? After the host restarts it stays stopped.{% endblocktrans %}"
onclick="return confirm(this.dataset.confirm)">{% bs_icon 'toggle-off' %} {% trans "Disable autostart" %}</button>
data-confirm="{% blocktrans with name=pool %}Disable autostart of pool {{ name }}? After the host restarts it stays stopped.{% endblocktrans %}">{% bs_icon 'toggle-off' %} {% trans "Disable autostart" %}</button>
{% else %}
<button type="submit" class="dropdown-item" name="set_autostart">{% bs_icon 'toggle-on' %} {% trans "Enable autostart" %}</button>
{% endif %}
@ -39,8 +37,7 @@
<li><hr class="dropdown-divider"></li>
<li><form method="post">{% csrf_token %}
<button type="submit" class="dropdown-item text-danger" name="delete"
data-confirm="{% blocktrans with name=pool %}Delete pool {{ name }}? Only its definition goes; its volumes and their files stay.{% endblocktrans %}"
onclick="return confirm(this.dataset.confirm)">{% bs_icon 'trash' %} {% trans "Delete pool" %}</button>
data-confirm="{% blocktrans with name=pool %}Delete pool {{ name }}? Only its definition goes; its volumes and their files stay.{% endblocktrans %}">{% bs_icon 'trash' %} {% trans "Delete pool" %}</button>
</form></li>
{% endif %}
</ul>
@ -152,8 +149,7 @@
<li><form method="post">{% csrf_token %}
<input type="hidden" name="volname" value="{{ volume.name }}">
<button type="submit" class="dropdown-item text-danger" name="del_volume"
data-confirm="{% blocktrans with name=volume.name %}Delete volume {{ name }}? Its file and data are removed; a volume a VM uses is not deleted.{% endblocktrans %}"
onclick="return confirm(this.dataset.confirm)">{% bs_icon 'trash' %} {% trans "Delete" %}</button>
data-confirm="{% blocktrans with name=volume.name %}Delete volume {{ name }}? Its file and data are removed; a volume a VM uses is not deleted.{% endblocktrans %}">{% bs_icon 'trash' %} {% trans "Delete" %}</button>
</form></li>
</ul>
</div>
@ -257,20 +253,20 @@
if (uploadInProgress) return;
var fileInput = $('#id_file')[0];
var errorMessage = $('#upload-error-message');
if (fileInput.files.length === 0) {
alert("{% trans 'Please select a file to upload.'|escapejs %}");
errorMessage.text("{% trans 'Please select a file to upload.'|escapejs %}").removeClass("d-none");
return;
}
var file = fileInput.files[0];
var fileName = file.name;
var errorMessage = $('#upload-error-message');
if (!/\.iso$/i.test(fileName) || fileName.length <= 4) {
errorMessage.text("{% trans 'Only .iso files can be uploaded.'|escapejs %}").removeClass("d-none");
return;
}
if (file.size === 0) {
alert("{% trans 'The selected file is empty.'|escapejs %}");
errorMessage.text("{% trans 'The selected file is empty.'|escapejs %}").removeClass("d-none");
return;
}
var chunkSize = 32 * 1024 * 1024; // Fewer SSH connections per upload

View file

@ -59,6 +59,23 @@
{% block content %}{% endblock %}
</div>
{# the dialog every button with data-confirm asks in (confirm_dialog.js) #}
<div class="modal fade" id="confirm-dialog" tabindex="-1" aria-labelledby="confirm-dialog-title" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title text-break" id="confirm-dialog-title"></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% trans "Close" %}"></button>
</div>
<div class="modal-body text-break"></div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
<button type="button" class="btn btn-primary" data-confirm-ok></button>
</div>
</div>
</div>
</div>
<!-- Bootstrap core JavaScript
================================================== -->
<!-- Placed at the end of the document so the pages load faster -->
@ -66,6 +83,7 @@
<script src="{% static 'js/jquery.js' %}"></script>
<!-- Bootstrap Core JavaScript -->
<script src="{% static 'js/bootstrap.bundle.min.js' %}"></script>
<script src="{% static 'js/confirm_dialog.js' %}"></script>
<!-- JavaScript Cookie -->
<script src="{% static 'js/js.cookie.js' %}"></script>
<!-- Table sorting -->

View file

@ -72,8 +72,7 @@
<input type="hidden" name="uuid" value="{{ secret.uuid }}">
{# a secret in use is not deleted: the VM or pool using it would not start #}
<button type="submit" class="dropdown-item text-danger" name="delete"{% if secret.vms or secret.pools %} disabled{% endif %}
data-confirm="{% blocktrans with usage=secret.usage|default:secret.uuid %}Delete secret {{ usage }}? Its stored value will be lost. No current or saved definition of a VM or pool names it; snapshots, LUKS volumes and qemu.conf's TLS settings are not checked.{% endblocktrans %}"
onclick="return confirm(this.dataset.confirm)">{% bs_icon 'trash' %} {% if secret.vms or secret.pools %}{% trans "Delete (in use)" %}{% else %}{% trans "Delete secret" %}{% endif %}</button>
data-confirm="{% blocktrans with usage=secret.usage|default:secret.uuid %}Delete secret {{ usage }}? Its stored value will be lost. No current or saved definition of a VM or pool names it; snapshots, LUKS volumes and qemu.conf's TLS settings are not checked.{% endblocktrans %}">{% bs_icon 'trash' %} {% if secret.vms or secret.pools %}{% trans "Delete (in use)" %}{% else %}{% trans "Delete secret" %}{% endif %}</button>
</form></li>
</ul>
</div>

View file

@ -1,29 +1,32 @@
"""A confirmation says what its action does: every prompt in a template is the
button's own data-confirm text, shown by return confirm(this.dataset.confirm)."""
"""A confirmation says what its action does, in the page's dialog: a button
asks its own data-confirm question through confirm_dialog.js, never the
browser's confirm()."""
import re
from pathlib import Path
from django.conf import settings
from django.test import SimpleTestCase
ONCLICK = 'onclick="return confirm(this.dataset.confirm)"'
class ConfirmPromptTests(SimpleTestCase):
def test_every_prompt_is_the_buttons_own_text(self):
def test_every_prompt_is_a_buttons_own_text_in_the_pages_dialog(self):
base = Path(settings.BASE_DIR)
templates = [*base.glob("*/templates/**/*.html"), *base.glob("templates/**/*.html")]
self.assertTrue(templates)
for path in templates:
text = path.read_text()
at = text.find("confirm(")
while at != -1:
with self.subTest(template=str(path.relative_to(base))):
self.assertNotIn("confirm(", text)
for match in re.finditer(r"data-confirm=", text):
# the tag around it: prompt texts and template tags hold no < or >
tag = text[text.rfind("<", 0, at):text.find(">", at) + 1]
tag = text[text.rfind("<", 0, match.start()):text.find(">", match.start()) + 1]
end = text.find("</button>", match.start())
content = re.sub(r"\{%\s*(bs_icon|if|elif|else|endif)\b[^%]*%\}", "",
text[text.find(">", match.start()) + 1:end]).strip()
with self.subTest(template=str(path.relative_to(base)), tag=tag[:60]):
self.assertRegex(tag, r"^<(button|input|a)\b")
self.assertIn(ONCLICK, tag)
self.assertIn('data-confirm="', tag)
self.assertRegex(tag, r"^<button\b")
self.assertNotIn('data-confirm=""', tag)
self.assertNotIn("Are you sure", tag)
at = text.find("confirm(", at + 1)
# the dialog's title and confirming button: the button's words, or an icon's name
self.assertTrue(content or "aria-label=" in tag or "title=" in tag)