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:
parent
95562544eb
commit
c401479cf9
17 changed files with 114 additions and 68 deletions
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
46
static/js/confirm_dialog.js
Normal file
46
static/js/confirm_dialog.js
Normal 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);
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue