From 935a0e9bdca04557a8dd9aa66b1fba9cc5103af9 Mon Sep 17 00:00:00 2001 From: catborise Date: Thu, 29 Oct 2026 21:58:58 +0300 Subject: [PATCH] feat(instances): the top of a VM's page in one card The top of a VM's page was one line of values between pipes, an icon for the guest agent and a hidden snapshot count, then a row of big action icons; the host links sat in a bar fixed to the bottom of the window, over the content. One card now holds it all (design chosen by the owner from four): - the name with the state badge, the title under it, and where the VM is: its host and the host's VM list for administrators, the VM list for owners; - OS details (with a connected guest agent) and Refresh; - the guest agent's state in words and the snapshot count; - CPU, memory, each disk with its device name and size, and the IPv4 addresses, labelled; - the page's sections as small labelled pills in the card's footer: one row from 992 px, wrapping below. The page has no other heading: base.html's heading area is a block it leaves out. Each section below it (Power, Access, Resize, Snapshot, Settings, Stats, Destroy) is a card of the same kind, so the page reads as one design (owner's choice of two): its sub-tabs as a small underlined nav in the card's header, its panes in the card's body without their own borders. Buttons are of normal size in the theme's primary colour, and those that cannot be undone (Power Cycle, Force Off, Destroy) in the danger colour. The Stats charts lose their coloured borders. instance_tabs.js finds the Settings tabs by their tablist role. The bottom bar and its setting (VIEW_INSTANCE_DETAIL_BOTTOM_BAR) are removed, and with it the CSS that kept the old Settings tabs on one line. --- .../migrations/0023_remove_bottom_bar.py | 18 ++ appsettings/tests/test_migrations.py | 5 +- appsettings/views.py | 3 +- .../templates/add_instance_network_block.html | 2 +- .../templates/add_instance_owner_block.html | 2 +- instances/templates/add_instance_volume.html | 6 +- instances/templates/bottom_bar.html | 28 --- instances/templates/instance.html | 179 ++++++++---------- instances/templates/instances/access_tab.html | 34 ++-- .../instances/destroy_instance_form.html | 2 +- .../templates/instances/destroy_tab.html | 16 +- .../instances/edit_instance_volume.html | 2 +- instances/templates/instances/host_link.html | 11 ++ instances/templates/instances/info_tab.html | 2 +- instances/templates/instances/power_tab.html | 46 +++-- instances/templates/instances/resize_tab.html | 34 ++-- .../templates/instances/settings_tab.html | 84 ++++---- .../templates/instances/snapshots_tab.html | 24 ++- instances/templates/instances/stats_tab.html | 22 ++- instances/tests/test_page_controls.py | 20 +- instances/tests/test_snapshot_views.py | 3 +- instances/views.py | 2 +- static/css/webvirtcloud.css | 11 -- .../arrows-angle-expand_None_None_None.svg | 8 + .../bar-chart-line_None_None_None.svg | 8 + .../check-circle_None_None_None.svg | 11 ++ .../exclamation-circle_None_None_None.svg | 11 ++ static/js/instance_tabs.js | 2 +- templates/base.html | 2 + webvirtcloud/tests/js/instance_tabs.test.js | 4 +- 30 files changed, 327 insertions(+), 275 deletions(-) create mode 100644 appsettings/migrations/0023_remove_bottom_bar.py delete mode 100644 instances/templates/bottom_bar.html create mode 100644 instances/templates/instances/host_link.html create mode 100644 static/icon_cache/arrows-angle-expand_None_None_None.svg create mode 100644 static/icon_cache/bar-chart-line_None_None_None.svg create mode 100644 static/icon_cache/check-circle_None_None_None.svg create mode 100644 static/icon_cache/exclamation-circle_None_None_None.svg diff --git a/appsettings/migrations/0023_remove_bottom_bar.py b/appsettings/migrations/0023_remove_bottom_bar.py new file mode 100644 index 00000000..aae52186 --- /dev/null +++ b/appsettings/migrations/0023_remove_bottom_bar.py @@ -0,0 +1,18 @@ +from django.db import migrations + + +def remove_bottom_bar(apps, schema_editor): + """A VM's page shows its host's links above it, always, as the host's + pages do: the setting for the bar at the bottom goes.""" + AppSettings = apps.get_model("appsettings", "AppSettings") + AppSettings.objects.using(schema_editor.connection.alias).filter(key="VIEW_INSTANCE_DETAIL_BOTTOM_BAR").delete() + + +class Migration(migrations.Migration): + dependencies = [ + ("appsettings", "0022_cpu_mode_choices"), + ] + + operations = [ + migrations.RunPython(remove_bottom_bar, migrations.RunPython.noop), + ] diff --git a/appsettings/tests/test_migrations.py b/appsettings/tests/test_migrations.py index 30f0829f..c2325d21 100644 --- a/appsettings/tests/test_migrations.py +++ b/appsettings/tests/test_migrations.py @@ -95,10 +95,13 @@ descriptions = importlib.import_module("appsettings.migrations.0021_setting_desc class DescriptionsTestCase(TestCase): def test_every_setting_says_what_it_does_within_its_column(self): limit = AppSettings._meta.get_field("description").max_length + removed = {"VIEW_INSTANCE_DETAIL_BOTTOM_BAR"} # by 0023 for key, text in descriptions.DESCRIPTIONS.items(): with self.subTest(key=key): self.assertLessEqual(len(text), limit) - self.assertEqual(AppSettings.objects.get(key=key).description, text) + if key not in removed: + self.assertEqual(AppSettings.objects.get(key=key).description, text) + self.assertFalse(AppSettings.objects.filter(key__in=removed).exists()) cpu_mode = importlib.import_module("appsettings.migrations.0022_cpu_mode_choices") diff --git a/appsettings/views.py b/appsettings/views.py index e676e2ec..fcd62f07 100644 --- a/appsettings/views.py +++ b/appsettings/views.py @@ -46,8 +46,7 @@ GROUPS = [ (_("Snapshots and jobs"), ["SNAPSHOT_FREE_SPACE_PERCENT", "SNAPSHOT_FREE_SPACE_MIN_GIB", "WORKER_CONCURRENCY", "JOB_RETENTION_DAYS"]), (_("Login"), ["LOGIN_FAILURE_LIMIT", "LOGIN_LOCKOUT_MINUTES"]), - (_("Lists and pages"), ["VIEW_INSTANCES_LIST_STYLE", "VIEW_INSTANCE_DETAIL_BOTTOM_BAR", "VM_DRBD_STATUS", - "LOGS_PER_PAGE"]), + (_("Lists and pages"), ["VIEW_INSTANCES_LIST_STYLE", "VM_DRBD_STATUS", "LOGS_PER_PAGE"]), ] diff --git a/instances/templates/add_instance_network_block.html b/instances/templates/add_instance_network_block.html index 560320cc..72bf2a8f 100644 --- a/instances/templates/add_instance_network_block.html +++ b/instances/templates/add_instance_network_block.html @@ -1,7 +1,7 @@ {% load i18n %} {% load bootstrap_icons %} {% if request.user.is_superuser %} - diff --git a/instances/templates/add_instance_owner_block.html b/instances/templates/add_instance_owner_block.html index b26f1a52..b26a4bfc 100644 --- a/instances/templates/add_instance_owner_block.html +++ b/instances/templates/add_instance_owner_block.html @@ -1,7 +1,7 @@ {% load i18n %} {% load bootstrap_icons %} {% if request.user.is_superuser %} - + diff --git a/instances/templates/add_instance_volume.html b/instances/templates/add_instance_volume.html index 762bc5b7..6b13c17e 100644 --- a/instances/templates/add_instance_volume.html +++ b/instances/templates/add_instance_volume.html @@ -1,7 +1,7 @@ {% load i18n %} {% load bootstrap_icons %} {% if request.user.is_superuser %} - + @@ -93,7 +93,7 @@ @@ -147,7 +147,7 @@ diff --git a/instances/templates/bottom_bar.html b/instances/templates/bottom_bar.html deleted file mode 100644 index c2945340..00000000 --- a/instances/templates/bottom_bar.html +++ /dev/null @@ -1,28 +0,0 @@ -{% load i18n %} -{% load bootstrap_icons %} - diff --git a/instances/templates/instance.html b/instances/templates/instance.html index 615bf795..f44ddd83 100644 --- a/instances/templates/instance.html +++ b/instances/templates/instance.html @@ -7,115 +7,92 @@ {% block title %}{% trans "Instance" %} - {{ instance.name }}{% endblock %} -{% block page_heading %} -{{ instance.name }}{% if instance.title %} ({{ instance.title }}){% endif %} -{% endblock page_heading %} +{% block page_header %}{% endblock page_header %}{# the card below has the heading #} {% block content %} {% include 'pleasewaitdialog.html' %} -
-
- {% vm_state instance.status as state %} - {{ state.0 }} - | - +
+
+
+
+

{{ instance.name }}

+ {% vm_state instance.status as state %} + {{ state.0 }} +
+ {% if instance.title %}

{{ instance.title }}

{% endif %} + {% include 'instances/host_link.html' %} +
+
{% if instance.guest_agent_ready %} - class="badge bg-success" - title="{% trans "Guest Agent Enabled & Connected" %}" - {% else %} - class="badge bg-warning" - title="{% trans "Guest Agent Enabled but not Connected" %}" + {% endif %} + {% bs_icon 'arrow-clockwise' %} {% trans "Refresh" %} +
+
+
+ {% if not instance.guest_agent %} + {% bs_icon 'dash-circle' %} {% trans "Guest agent off" %} + {% elif instance.guest_agent_ready %} + {% bs_icon 'check-circle' %} {% trans "Guest agent connected" %} {% else %} - class="badge bg-danger" - title="{% trans "Guest Agent Not Enabled & Not Connected" %}" - {% endif %}>{% bs_icon 'broadcast' size='1em' %} - - | - {% if instance.snapshots %} - {% bs_icon 'camera'%} - | - {% endif %} - {% if instance.cur_vcpu %} - {{ instance.cur_vcpu }} {% trans "VCPU" %} - {% else %} - {{ instance.vcpu }} {% trans "VCPU" %} - {% endif %} - | - {{ instance.cur_memory }} {% trans "MB" %} {% trans "RAM" %} - | - {% for disk in instance.disks %} - {{ disk.size|filesizeformat }} {% trans "Disk" %} | - {% endfor %} - {% for net in instance.networks %} - {% for ipv4 in net.ipv4 %} - {{ ipv4 }} | - {% endfor %} - {% endfor %} - {% if instance.guest_agent_ready %} - - {% bs_icon 'info-circle' %} - - {% endif %} - - {% bs_icon 'repeat'%} - + {% bs_icon 'exclamation-circle' %} {% trans "Guest agent not connected" %} + {% endif %} + {% bs_icon 'camera' %} {% blocktrans count counter=instance.snapshots|length %}{{ counter }} snapshot{% plural %}{{ counter }} snapshots{% endblocktrans %} +
+
+
+
{% trans "CPU" %}
+
{% firstof instance.cur_vcpu instance.vcpu %} {% trans "of" %} {{ instance.vcpu }} {% trans "vCPU" %}
+
+
+
{% trans "Memory" %}
+
{{ instance.cur_memory }} {% trans "MB" %}
+
+
+
{% trans "Disks" %}
+
{% for disk in instance.disks %}{{ disk.dev }} {{ disk.size|filesizeformat }}{% if not forloop.last %} · {% endif %}{% empty %}{% trans "None" %}{% endfor %}
+
+
+
{% trans "IPv4" %}
+
{{ ipv4_addresses|join:", "|default:"—" }}
+
+
-
-
+ +
- -
{% include 'instances/power_tab.html' %} @@ -128,10 +105,6 @@
- {% if app_settings.VIEW_INSTANCE_DETAIL_BOTTOM_BAR == 'True' %} - {% include 'bottom_bar.html' %} - {% endif %} - {% endblock %} {% block script %} diff --git a/instances/templates/instances/access_tab.html b/instances/templates/instances/access_tab.html index 76e7a191..1169c82e 100644 --- a/instances/templates/instances/access_tab.html +++ b/instances/templates/instances/access_tab.html @@ -1,7 +1,9 @@ {% load i18n %} -
+
-
{% endif %} - diff --git a/instances/templates/instances/destroy_tab.html b/instances/templates/instances/destroy_tab.html index f2f5d06f..cd97ea26 100644 --- a/instances/templates/instances/destroy_tab.html +++ b/instances/templates/instances/destroy_tab.html @@ -1,23 +1,27 @@ {% load i18n %} -
+
-