mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-11 12:46:49 +00:00
feat(networks): the network pages on Bootstrap's cards, tables and dialogs
As the storage pages; reviewed with Codex. - The list is a card per network: its name with Active or Stopped and Autostart badges, its type (network_mode: Isolated, NAT, Routed, Open, Bridge, Macvtap, SR-IOV), its device, and each address with its DHCP range; a bridge or macvtap network's addresses come from the outside network. - A network's page: its heading starts or stops it, the menu sets autostart and deletes a stopped one. A Network card tells its state, type, bridge or interface, MAC and what the type does; a card per address (every <ip>, each with its DHCP range dialog, which libvirt takes on a stopped network); its fixed addresses of every subnet in one table, searched, each with Edit and Delete, one dialog adding and editing them; the VMs whose definition names it, connected or configured, linked (wvmNetwork.get_vms, one domain listing, a VM undefined meanwhile left out); its bandwidth limits where libvirt sets them (not on macvtap or SR-IOV networks), in KiB/s, from the saved definition with a note that a running network takes them when restarted; its XML in an accordion. The page reads the running and the saved definition once each, and only to render itself: its actions, which all redirect, read nothing of it first. - The New network dialog is one form, as the New pool one: the type as radio cards (the radio_card.html include, now shared by both), the chosen type's fields only, IPv6 with its box, a bridge or interface suggested from the host's (get_host_interfaces, not the bridges libvirt made for its own networks); a refused network or one libvirt refuses opens the dialog again with what was entered, the reasons beside the fields and focus on the first. - What the New pool and New network dialogs share is one script, static/js/choice_dialog.js: the radio cards show their fieldsets (data-kinds) and a refused form opens again focused on its first refused field; each dialog keeps only its own changes. - The fixed-address dialogs per family go to the page's own dialog.
This commit is contained in:
parent
0c9a002ef4
commit
0ee03c0971
15 changed files with 947 additions and 757 deletions
|
|
@ -1,4 +1,5 @@
|
|||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load bootstrap_icons %}
|
||||
{% load form_fields %}
|
||||
{% if request.user.is_superuser %}
|
||||
|
|
@ -26,13 +27,13 @@
|
|||
<legend class="form-label fs-6">{% trans "Storage type" %}</legend>
|
||||
<div class="row row-cols-1 row-cols-sm-2 g-2">
|
||||
{% trans "Directory" as title %}{% trans "Image files in a directory of the host." as help %}
|
||||
{% include "storages/pool_kind.html" with kind="dir" %}
|
||||
{% include "radio_card.html" with name="pool_kind" value="dir" chosen=pool_kind %}
|
||||
{% trans "LVM" as title %}{% trans "Logical volumes of a volume group made on a device." as help %}
|
||||
{% include "storages/pool_kind.html" with kind="logical" %}
|
||||
{% include "radio_card.html" with name="pool_kind" value="logical" chosen=pool_kind %}
|
||||
{% trans "Ceph RBD" as title %}{% trans "Images of a pool of an existing Ceph cluster." as help %}
|
||||
{% include "storages/pool_kind.html" with kind="rbd" %}
|
||||
{% include "radio_card.html" with name="pool_kind" value="rbd" chosen=pool_kind %}
|
||||
{% trans "Network filesystem" as title %}{% trans "Image files on a server's share, mounted on the host." as help %}
|
||||
{% include "storages/pool_kind.html" with kind="netfs" %}
|
||||
{% include "radio_card.html" with name="pool_kind" value="netfs" chosen=pool_kind %}
|
||||
{% if iso_pool %}
|
||||
{# the upload button is on the page of the pool named iso only #}
|
||||
<div class="col">
|
||||
|
|
@ -47,7 +48,7 @@
|
|||
</div>
|
||||
{% else %}
|
||||
{% trans "ISO images" as title %}{% trans "The directory pool named iso, for uploaded ISO images." as help %}
|
||||
{% include "storages/pool_kind.html" with kind="iso" %}
|
||||
{% include "radio_card.html" with name="pool_kind" value="iso" chosen=pool_kind %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
|
@ -60,7 +61,7 @@
|
|||
{% field_error form.name "pool_name" %}
|
||||
</div>
|
||||
|
||||
<fieldset data-kind="dir">
|
||||
<fieldset data-kinds="dir">
|
||||
<legend class="visually-hidden">{% trans "Directory" %}</legend>
|
||||
<div class="mb-3">
|
||||
<label for="pool_dir_target" class="form-label">{% trans "Directory" %}</label>
|
||||
|
|
@ -71,7 +72,7 @@
|
|||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset data-kind="logical">
|
||||
<fieldset data-kinds="logical">
|
||||
<legend class="visually-hidden">{% trans "LVM" %}</legend>
|
||||
<div class="mb-3">
|
||||
<label for="pool_lvm_source" class="form-label">{% trans "Device" %}</label>
|
||||
|
|
@ -89,7 +90,7 @@
|
|||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset data-kind="rbd">
|
||||
<fieldset data-kinds="rbd">
|
||||
<legend class="visually-hidden">{% trans "Ceph RBD" %}</legend>
|
||||
{% if ceph_secrets %}
|
||||
<div class="row g-3 mb-3">
|
||||
|
|
@ -131,7 +132,7 @@
|
|||
{% endif %}
|
||||
</fieldset>
|
||||
|
||||
<fieldset data-kind="netfs">
|
||||
<fieldset data-kinds="netfs">
|
||||
<legend class="visually-hidden">{% trans "Network filesystem" %}</legend>
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-sm-6">
|
||||
|
|
@ -165,7 +166,7 @@
|
|||
</fieldset>
|
||||
|
||||
{% if not iso_pool %}
|
||||
<fieldset data-kind="iso">
|
||||
<fieldset data-kinds="iso">
|
||||
<legend class="visually-hidden">{% trans "ISO images" %}</legend>
|
||||
<div class="mb-3">
|
||||
<label for="pool_iso_target" class="form-label">{% trans "Directory" %}</label>
|
||||
|
|
@ -185,64 +186,37 @@
|
|||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script src="{% static 'js/choice_dialog.js' %}"></script>
|
||||
<script>
|
||||
// the chosen type's fields show; the ISO pool is a directory named iso;
|
||||
// a path follows the pool's name until it is edited (after the page's
|
||||
// scripts, Bootstrap's among them, have loaded)
|
||||
// the ISO pool is a directory named iso; a path follows the pool's name
|
||||
// until it is edited (after the page's scripts, Bootstrap's among them)
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var form = document.getElementById('pool-form');
|
||||
var name = form.elements.namedItem('name');
|
||||
var otherName = name.value;
|
||||
var hasCephSecrets = {{ ceph_secrets|yesno:"true,false" }};
|
||||
function choose(kind) {
|
||||
var iso = kind === 'iso';
|
||||
if (!name.readOnly) otherName = name.value;
|
||||
name.value = iso ? 'iso' : otherName;
|
||||
name.readOnly = iso;
|
||||
form.elements.namedItem('stg_type').value = iso ? 'dir' : kind;
|
||||
form.querySelectorAll('fieldset[data-kind]').forEach(function (fields) {
|
||||
fields.hidden = fields.disabled = fields.dataset.kind !== kind;
|
||||
});
|
||||
form.querySelector('button[name=create]').disabled = kind === 'rbd' && !hasCephSecrets;
|
||||
}
|
||||
function follow() {
|
||||
form.querySelectorAll('[data-follow-name]').forEach(function (path) {
|
||||
path.dataset.derived = name.value ? path.dataset.followName + name.value : '';
|
||||
});
|
||||
name.addEventListener('input', function () {
|
||||
form.querySelectorAll('[data-follow-name]').forEach(function (path) {
|
||||
var derived = name.value ? path.dataset.followName + name.value : '';
|
||||
if (!path.value || path.value === path.dataset.derived) path.value = derived;
|
||||
path.dataset.derived = derived;
|
||||
});
|
||||
}
|
||||
form.querySelectorAll('input[name=pool_kind]').forEach(function (radio) {
|
||||
radio.addEventListener('change', function () { choose(radio.value); });
|
||||
});
|
||||
name.addEventListener('input', follow);
|
||||
// a new device or name: the erasing is agreed to again
|
||||
form.querySelectorAll('#pool_lvm_source, #pool_name').forEach(function (field) {
|
||||
field.addEventListener('input', function () { document.getElementById('pool_lvm_erase').checked = false; });
|
||||
});
|
||||
form.querySelectorAll('[data-follow-name]').forEach(function (path) {
|
||||
path.dataset.derived = name.value ? path.dataset.followName + name.value : '';
|
||||
choiceDialog(document.getElementById('AddStgPool'), 'pool_kind', function (kind) {
|
||||
var iso = kind === 'iso';
|
||||
if (!name.readOnly) otherName = name.value;
|
||||
name.value = iso ? 'iso' : otherName;
|
||||
name.readOnly = iso;
|
||||
form.elements.namedItem('stg_type').value = iso ? 'dir' : kind;
|
||||
form.querySelector('button[name=create]').disabled = kind === 'rbd' && !hasCephSecrets;
|
||||
});
|
||||
// the type sent back, or the first when it is offered no more (an iso pool made meanwhile)
|
||||
var chosen = form.querySelector('input[name=pool_kind]:checked') || form.querySelector('input[name=pool_kind]');
|
||||
chosen.checked = true;
|
||||
choose(chosen.value);
|
||||
// a refused pool: the dialog opens again with what was entered and why,
|
||||
// the first refused field focused, or the reasons when none is shown
|
||||
var dialog = document.getElementById('AddStgPool');
|
||||
if (dialog.hasAttribute('data-open')) {
|
||||
dialog.addEventListener('shown.bs.modal', function () {
|
||||
var refused = form.querySelector('fieldset:not([hidden]) [aria-invalid=true], #pool_name[aria-invalid=true]');
|
||||
var reasons = form.querySelector('.form-errors');
|
||||
if (refused) {
|
||||
refused.focus();
|
||||
} else if (reasons) {
|
||||
reasons.tabIndex = -1;
|
||||
reasons.focus();
|
||||
}
|
||||
}, {once: true});
|
||||
bootstrap.Modal.getOrCreateInstance(dialog).show();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
{# a new pool's type as a radio card: kind, title, help; pool_kind is the chosen one #}
|
||||
<div class="col">
|
||||
<label class="source-option border rounded p-2 h-100 d-flex gap-2">
|
||||
<input class="form-check-input flex-shrink-0 mt-1" type="radio" name="pool_kind" value="{{ kind }}"{% if kind == pool_kind %} checked{% endif %}>
|
||||
<span><span class="d-block fw-semibold">{{ title }}</span><span class="d-block small text-body-secondary">{{ help }}</span></span>
|
||||
</label>
|
||||
</div>
|
||||
Loading…
Add table
Add a link
Reference in a new issue