1
0
Fork 0
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:
catborise 2026-10-28 20:25:00 +03:00
parent 0c9a002ef4
commit 0ee03c0971
15 changed files with 947 additions and 757 deletions

View file

@ -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 %}

View file

@ -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>