From 3aac8f51f2f43b7f425d61b3a2288ad107b788ab Mon Sep 17 00:00:00 2001 From: catborise Date: Tue, 27 Oct 2026 23:12:02 +0300 Subject: [PATCH] feat(instances): the VM list on Bootstrap's pills, badges and menus, its actions by the user's rights - The state filter is a row of pills, All and each state, counting the VMs the search finds (the chosen state aside); a state without any is hidden unless chosen (All never), and a status line tells screen readers how many VMs match. Every state keeps its pill. - A labelled "New VM" button replaces the bare green plus; "Find IP" says "Find IP / MAC", which it finds. - States are badges; a shut-off VM is grey, not red: it is not a fault. - A VM's cell holds its name, its title, and for those who see owners its first owner as a badge, "+N" for the others (named for screen readers and on hover); the separate user column goes. - Each row has the same button group for a superuser or the VM's owners, from the actions the user may use (states.allowed_actions), all of it disabled on a VM an administrator paused; view-only users get none. It holds Console, the power action the state calls for (Start, Resume, Shut down, or Clone for a template) and a menu of the others (Suspend, Force off, Power cycle), what does not apply now disabled. The power button is an icon, named on hover and for screen readers, so every group is as wide and they line up from row to row; the power cut confirmations stay. On a phone Console shows its icon only too. - A grouped host's row has its name, a Connected or Not connected badge, its VM count and a labelled host memory bar; an unreachable host's row is table-warning. Its collapse control and the one for all hosts are buttons, with aria-expanded kept. - vCPUs and memory hide on a phone in both layouts, and table cells keep Bootstrap's own padding there: Zephyr doubles it and a list's last column no longer fit the screen. --- instances/states.py | 11 +- instances/templates/allinstances.html | 37 ++++--- .../templates/allinstances_index_grouped.html | 80 ++++++-------- .../allinstances_index_nongrouped.html | 34 ++---- instances/templates/create_inst_block.html | 6 +- instances/templates/instance.html | 2 +- instances/templates/instance_actions.html | 104 +++++++++--------- instances/templates/instance_name_cell.html | 17 +++ instances/templates/instance_state.html | 2 +- instances/templatetags/vm_states.py | 8 +- instances/tests/js/table_filter.test.js | 21 +++- instances/tests/test_list_unreachable.py | 2 +- instances/tests/test_states.py | 42 ++++++- static/css/webvirtcloud.css | 9 ++ .../arrows-collapse_None_None_None.svg | 8 ++ static/icon_cache/display_None_None_None.svg | 8 ++ static/icon_cache/person_None_None_None.svg | 8 ++ static/js/table_filter.js | 49 ++++++++- 18 files changed, 295 insertions(+), 153 deletions(-) create mode 100644 instances/templates/instance_name_cell.html create mode 100644 static/icon_cache/arrows-collapse_None_None_None.svg create mode 100644 static/icon_cache/display_None_None_None.svg create mode 100644 static/icon_cache/person_None_None_None.svg diff --git a/instances/states.py b/instances/states.py index ed8c972e..ea74d7d2 100644 --- a/instances/states.py +++ b/instances/states.py @@ -12,7 +12,7 @@ STATES = { 2: (_("Blocked"), "success"), # running, waiting for a resource 3: (_("Suspended"), "warning"), 4: (_("Shutting down"), "warning"), - 5: (_("Off"), "danger"), + 5: (_("Off"), "secondary"), # shut off is not a fault 6: (_("Crashed"), "danger"), 7: (_("PM suspended"), "warning"), # suspended by the guest } @@ -38,6 +38,12 @@ def state_label(state): return UNKNOWN if state is None else STATES.get(state, STATES[0]) +def manages(user, instance): + """Superusers and a VM's owners: the users its power actions are for.""" + # the lists prefetch userinstance_set + return user.is_superuser or any(ui.user_id == user.id for ui in instance.userinstance_set.all()) + + def allowed_actions(user, instance, state): """ The power actions the state allows and the user may use: superusers all, @@ -50,8 +56,7 @@ def allowed_actions(user, instance, state): return {"clone"} if user.has_perm("instances.clone_instances") else set() if user.is_superuser: return STATE_ACTIONS.get(state, set()) - # the lists prefetch userinstance_set - if not any(ui.user_id == user.id for ui in instance.userinstance_set.all()): + if not manages(user, instance): return set() if state == 3: return set() diff --git a/instances/templates/allinstances.html b/instances/templates/allinstances.html index e3d5e000..4841db37 100644 --- a/instances/templates/allinstances.html +++ b/instances/templates/allinstances.html @@ -5,22 +5,16 @@ {% block page_heading %}{% trans "Instances" %}{% endblock page_heading %} {% block page_heading_extra %} -{% if request.user.is_superuser %} - {% include 'create_inst_block.html' %} -{% endif %} - {# the list's filters (state, text) and the address search, as one control #} -
-
- +
+ {# the list's search and the address search, as one control #} +
+ data-bs-toggle="modal" data-bs-target="#find-address">{% trans "Find IP / MAC" %}
+ {% if request.user.is_superuser %} + {% include 'create_inst_block.html' %} + {% endif %}
{% endblock page_heading_extra %} @@ -45,6 +39,23 @@
{% endif %}
+ {% if instances.exists %} + {# the state filter: table_filter.js counts each state's VMs the search finds #} + + +
+ {% endif %} {% if not instances.exists %}

{% trans "There are no VMs here yet." %}

{% elif app_settings.VIEW_INSTANCES_LIST_STYLE == 'grouped' and request.user.is_superuser %} diff --git a/instances/templates/allinstances_index_grouped.html b/instances/templates/allinstances_index_grouped.html index 69a796ae..cdfd118a 100644 --- a/instances/templates/allinstances_index_grouped.html +++ b/instances/templates/allinstances_index_grouped.html @@ -4,48 +4,51 @@ - - - + + {% if app_settings.VM_DRBD_STATUS == 'True' %} {% endif %} - + - + {% for compute in computes %} - + {% if compute.instance_set.count > 0 %} - + - - - - {% if app_settings.VM_DRBD_STATUS == 'True' %} - - {% endif %} {% if compute.status is True %} - - + + {% else %} @@ -58,21 +61,7 @@ {# searched for its name, title and owners, not its buttons #} - - + @@ -83,7 +72,7 @@ {% endif %} - @@ -100,8 +89,11 @@ // search (table_filter.js, the hidden attribute) can still hide them too function set_host_collapsed(tbody, collapsed) { $(tbody).toggleClass('host-collapsed', collapsed); + $(tbody).find('[aria-controls]').attr('aria-expanded', !collapsed); $(tbody).find('span[id^=collapse_host_instances_]').toggleClass('bi-chevron-down', collapsed).toggleClass('bi-chevron-up', !collapsed); $(tbody).find('span[id^=inst_count_badge_]').toggleClass('d-none', !collapsed); + // expanded while any host is, as the next click collapses them all + $('#hide-all-instances').attr('aria-expanded', $('tbody.searchable').not('.host-collapsed').length > 0); } // every host the same way: collapsed, unless all already are function hide_all_host_instances() { diff --git a/instances/templates/allinstances_index_nongrouped.html b/instances/templates/allinstances_index_nongrouped.html index 7e57119f..97038f9c 100644 --- a/instances/templates/allinstances_index_nongrouped.html +++ b/instances/templates/allinstances_index_nongrouped.html @@ -3,42 +3,28 @@
#{% trans "Name" %}
{% trans "Description" %}
{% trans "User"%} + + {% trans "Name" %} {% trans "Status" %}{% trans "Role/Disk" %}{% trans "VCPU" %}{% trans "vCPUs" %} {% trans "Memory" %}{% trans "Actions" %} & {% trans "Mem Usage" %}{% trans "Actions" %}
- + - {{ compute.name }} - {{ compute.instance_set.count }} + +
+ {{ compute.name }} + {% if compute.status is True %} + {% trans "Connected" %} + {% else %} + {% trans "Not connected" %} + {% endif %} + {% blocktrans count counter=compute.instance_set.count %}{{ counter }} VM{% plural %}{{ counter }} VMs{% endblocktrans %} + {% trans "collapsed" %} +
- {% if compute.status is True %} - {% trans "Connected" %} - {% else %} - {% trans "Not Connected" %} - {% endif %} - {{ compute.cpu_count }}{{ compute.ram_size|filesizeformat }}{{ compute.cpu_count }}{{ compute.ram_size|filesizeformat }} -
-
{{ compute.ram_usage }}% -
+
{% blocktrans with percent=compute.ram_usage %}host memory {{ percent }}% in use{% endblocktrans %}
+
+
- {{ instance.name }} -
- {% if instance.info %}

{{ instance.title }}

{% endif %} -
- - {% if instance.userinstance_set.all.count > 0 %} - {{ instance.userinstance_set.all.0.user }} - {% if instance.userinstance_set.all.count > 1 %} - (+{{ instance.userinstance_set.all.count|add:"-1" }}) - {% endif %} - {% endif %} - - {% include 'instance_name_cell.html' with owners=True %} {% include 'instance_state.html' %} {{ instance.info.3 }} {% if instance.info %}{% widthratio instance.info.2 1024 1 %} MiB{% endif %} + {% include 'instance_actions.html' %}
- + {% if show_hosts %} - + {% endif %} {% if app_settings.VM_DRBD_STATUS == 'True' %} {% endif %} - - - + + + {% for instance in instances %} {# searched for its name, title, host and owners, not its buttons #} - + {% if show_hosts %} {% endif %} {% endif %} - - - + + diff --git a/instances/templates/create_inst_block.html b/instances/templates/create_inst_block.html index d9c72304..915ec304 100644 --- a/instances/templates/create_inst_block.html +++ b/instances/templates/create_inst_block.html @@ -1,8 +1,8 @@ {% load i18n %} {% load bootstrap_icons %} - - - +
{% trans 'Name' %}
{% trans 'Description' %}
{% trans 'Name' %}{% trans 'Host' %}
{% trans 'User' %}
{% trans 'Host' %}{% trans 'Status' %}{% trans 'Role/Disk' %}{% trans 'VCPU' %}{% trans 'Memory' %}{% trans 'Actions' %}{% trans 'vCPUs' %}{% trans 'Memory' %}{% trans 'Actions' %}
- - {{ instance.name }} - -
- {% if instance.info %}

{{ instance.title }}

{% endif %} -
{% include 'instance_name_cell.html' with owners=show_hosts %} {# the host pages are a superuser's #} - {% if request.user.is_superuser %}{{ instance.compute.name }}{% else %}{{ instance.compute.name }}{% endif %}
- - {% if instance.userinstance_set.all.count > 0 %} - {{ instance.userinstance_set.all.0.user }} - {% if instance.userinstance_set.all.count > 1 %} - (+{{ instance.userinstance_set.all.count|add:"-1" }}) - {% endif %} - {% endif %} - + {% if request.user.is_superuser %}{{ instance.compute.name }}{% else %}{{ instance.compute.name }}{% endif %}
@@ -49,9 +35,9 @@ {% if instance.drbd == "Primary/OK" or instance.drbd == "Secondary/OK" %}{% else %}{% endif %}{{ instance.drbd }} {{ instance.info.3 }}{% if instance.info %}{% widthratio instance.info.2 1024 1 %} MiB{% endif %} + {{ instance.info.3 }}{% if instance.info %}{% widthratio instance.info.2 1024 1 %} MiB{% endif %} {% include 'instance_actions.html' %}