From 050d071ff967da5240ec70252c4e923103044ea4 Mon Sep 17 00:00:00 2001 From: catborise Date: Wed, 28 Oct 2026 22:17:19 +0300 Subject: [PATCH] feat(interfaces): the interface pages on Bootstrap's cards, tables and dialogs As the storage and network pages; plan reviewed with Codex. - The list is grouped by type (bridges, bonds, VLANs, ethernet; a type the host has none of is left out), three cards a row, with a search of their names, MACs and networks (table_filter.js, which now takes any element with data-search as a row); a card per interface: its link as the kernel reports it (up green, down grey, another state or unknown told as such, no red card for a link that is merely down), MAC, MTU, speed when known, the members it reports and the libvirt networks that use it, linked. - An interface's page: an Interface card (type, libvirt's own state where it manages interfaces, link, start mode, MAC, MTU, speed, STP, networks using it); on a host whose libvirt changes interfaces, a card per family with how it is addressed and its addresses; the members of a bridge, bond or VLAN with their link and speed, or that none is reported; for a bridge, the running VMs' cards on it, directly or through a network using it, with their tap and MAC, linked (wvmInterface.get_vms: one domain listing). Not the bridge's delay: netcf tells it in seconds, the udev backend in hundredths. Start or Stop in its heading, Delete in its menu, disabled while it runs, on hosts that can change interfaces only. - A host whose libvirt only reads interfaces says so once, with that it does not tell their addresses, instead of "None" on every page. - The New interface dialog is one form, as the New pool and New network ones (choice_dialog.js, radio_card.html): Bridge or Ethernet, a bridge's member from the devices it can take, an ethernet interface's device from the ones the host does not define yet, a static address's fields with its choice; a refused interface, or one libvirt refuses, opens it again with what was entered; a made one opens its page. - networks_by_device maps the libvirt networks to their bridges or interfaces on the page's own connection. --- instances/tests/js/table_filter.test.js | 9 +- interfaces/templates/create_iface_block.html | 280 ++++++++++-------- interfaces/templates/interface.html | 234 ++++++++++----- interfaces/templates/interfaces.html | 117 ++++---- .../templates/interfaces_unchangeable.html | 4 +- interfaces/templates/link_badge.html | 1 + interfaces/tests/test_forms.py | 48 ++- interfaces/tests/test_live_interfaces.py | 2 +- interfaces/tests/test_reads.py | 27 ++ interfaces/views.py | 46 ++- static/js/table_filter.js | 9 +- vrtManager/interface.py | 44 ++- webvirtcloud/tests/test_status_contrast.py | 19 +- 13 files changed, 532 insertions(+), 308 deletions(-) create mode 100644 interfaces/templates/link_badge.html diff --git a/instances/tests/js/table_filter.test.js b/instances/tests/js/table_filter.test.js index a5285a3c..ffd601e3 100644 --- a/instances/tests/js/table_filter.test.js +++ b/instances/tests/js/table_filter.test.js @@ -16,7 +16,7 @@ function plainRow(text) { return {textContent: text, dataset: {}, hidden: false, hasAttribute: () => false, querySelector: () => null}; } function group(rows, host) { - return {dataset: host ? {host} : {}, querySelectorAll: (s) => (s === "tr" ? rows : [])}; + return {dataset: host ? {host} : {}, querySelectorAll: (s) => (s === "tr, [data-search], [data-group-header]" ? rows : [])}; } function page(groups, text, state, noMatch, extra = {}) { // a list without a state list (state undefined) has no such element @@ -57,6 +57,13 @@ assert.deepStrictEqual(visible(a), ["host-a Connected", "db-1 Off"], "the state filterInstances(page(groups, "Connected", "")); assert.deepStrictEqual(visible(a), [], "a host row's other text is not its name"); +// the interfaces list: a type's heading shows while any of its cards does +const bridges = [hostRow("Bridges 2"), vm("x", undefined, "br-v101"), vm("x", undefined, "br-v102")]; +const vlans = [hostRow("VLANs 1"), vm("x", undefined, "bond1.101")]; +filterInstances(page([group(bridges), group(vlans)], "v102")); +assert.deepStrictEqual(bridges.map((r) => r.hidden), [false, true, false], "a card is found by its data-search"); +assert.deepStrictEqual(vlans.map((r) => r.hidden), [true, true], "a type without a match is hidden, its heading too"); + // any other list: no state list, no groups; its rows are filtered by their text const plain = [plainRow("alice Active"), plainRow("bob (vda) Blocked"), plainRow("carol Active")]; filterInstances(page([group(plain)], "ACTIVE")); diff --git a/interfaces/templates/create_iface_block.html b/interfaces/templates/create_iface_block.html index 432b8c78..ece6d9c9 100644 --- a/interfaces/templates/create_iface_block.html +++ b/interfaces/templates/create_iface_block.html @@ -1,150 +1,168 @@ {% load i18n %} +{% load static %} {% load bootstrap_icons %} +{% load form_fields %} {% if request.user.is_superuser %} - - - + - -