1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 11:16:47 +00:00

fix(templates): status colours keep their contrast on every theme

The pool, network and interface cards had a solid green or red header
with light text, and status words (Connected, ON, OFF, the DRBD state)
were in the theme's green or red text colour. axe found them below the
WCAG AA 4.5:1 contrast on the live theme (4.29:1, 3.52:1), on the
default zephyr theme, whose green is light (2.5:1), and on dark themes
(darkly 1.18:1).

The card headers, and the status words as badges, are now the theme's
pale status background with its dark status text (bg-*-subtle with
text-*-emphasis). Bootstrap derives the pair from each theme's palette,
and it does not depend on the page's background: the dark Bootswatch
themes keep light-mode emphasis colours, which on their own would be
dark text on a dark page. The card border keeps the status colour.
This commit is contained in:
catborise 2026-10-21 07:58:08 +03:00
parent 084f2e8b60
commit b45f673da7
7 changed files with 62 additions and 14 deletions

View file

@ -29,9 +29,9 @@
<td class="d-none d-sm-table-cell"></td>
<td>
{% if compute.status is True %}
<span class="text-success">{% trans "Connected" %}</span>
<span class="badge bg-success-subtle text-success-emphasis">{% trans "Connected" %}</span>
{% else %}
<span class="text-danger">{% trans "Not Connected" %}</span>
<span class="badge bg-danger-subtle text-danger-emphasis">{% trans "Not Connected" %}</span>
{% endif %}
</td>
{% if app_settings.VM_DRBD_STATUS == 'True' %}
@ -76,7 +76,7 @@
</td>
{% if app_settings.VM_DRBD_STATUS == 'True' %}
<td>
{% if instance.drbd == "Primary/OK" or instance.drbd == "Secondary/OK" %}<span class="text-success">{% else %}<span class="text-danger">{% endif %}{{ instance.drbd }}</span>
{% if instance.drbd == "Primary/OK" or instance.drbd == "Secondary/OK" %}<span class="badge bg-success-subtle text-success-emphasis">{% else %}<span class="badge bg-danger-subtle text-danger-emphasis">{% endif %}{{ instance.drbd }}</span>
</td>
{% endif %}
<td>{{ instance.info.3 }}</td>

View file

@ -43,7 +43,7 @@
</td>
{% if app_settings.VM_DRBD_STATUS == 'True' %}
<td>
{% if instance.drbd == "Primary/OK" or instance.drbd == "Secondary/OK" %}<span class="text-success">{% else %}<span class="text-danger">{% endif %}{{ instance.drbd }}</span>
{% if instance.drbd == "Primary/OK" or instance.drbd == "Secondary/OK" %}<span class="badge bg-success-subtle text-success-emphasis">{% else %}<span class="badge bg-danger-subtle text-danger-emphasis">{% endif %}{{ instance.drbd }}</span>
</td>
{% endif %}
<td>{{ instance.info.3 }}</td>

View file

@ -54,9 +54,9 @@
{% for iface in ifaces_all %}
<div class="col-12 col-sm-4">
<div class="mb-3 card {% if iface.state == 1 %}border-success{% else %}border-danger{% endif %} shadow-sm">
<div class="card-header {% if iface.state == 1 %}bg-success{% else %}bg-danger{% endif %}">
<div class="card-header {% if iface.state == 1 %}bg-success-subtle{% else %}bg-danger-subtle{% endif %}">
<h6 class="my-0 card-title">
<a class="card-link text-light" href="{% url 'interface' compute.id iface.name %}">{{ iface.name }}</a>
<a class="card-link {% if iface.state == 1 %}text-success-emphasis{% else %}text-danger-emphasis{% endif %}" href="{% url 'interface' compute.id iface.name %}">{{ iface.name }}</a>
</h6>
</div>
<div class="ps-0 pe-0 card-body">

View file

@ -110,9 +110,9 @@
<dt class="col-6">{% trans "DHCP" %}:</dt>
<dd class="col-6">
{% if ipv4_dhcp_range_start and ipv4_dhcp_range_end %}
<span class="text-success">{% trans "ON" %}</span>
<span class="badge bg-success-subtle text-success-emphasis">{% trans "ON" %}</span>
{% else %}
<span class="text-danger">{% trans "OFF" %}</span>
<span class="badge bg-danger-subtle text-danger-emphasis">{% trans "OFF" %}</span>
{% endif %}
</dd>
{% if ipv4_dhcp_range_start and ipv4_dhcp_range_end %}
@ -222,9 +222,9 @@
<dd class="col-6">
{% if ipv6_dhcp_range_start and ipv6_dhcp_range_end %}
<span class="text-success">{% trans "ON" %}</span>
<span class="badge bg-success-subtle text-success-emphasis">{% trans "ON" %}</span>
{% else %}
<span class="text-danger">{% trans "OFF" %}</span>
<span class="badge bg-danger-subtle text-danger-emphasis">{% trans "OFF" %}</span>
{% endif %}
</dd>
{% if ipv6_dhcp_range_start and ipv6_dhcp_range_end %}

View file

@ -50,9 +50,9 @@
{% for pool in networks %}
<div class="col-6 col-md-4">
<div class="mb-3 card {% if pool.status %}border-success{% else %}border-danger{% endif %} shadow-sm">
<div class="card-header {% if pool.status %}bg-success{% else %}bg-danger{% endif %}">
<div class="card-header {% if pool.status %}bg-success-subtle{% else %}bg-danger-subtle{% endif %}">
<h6 class="my-0 card-title">
<a class="card-link text-light" href="{% url 'network' compute.id pool.name %}">{{ pool.name }}</a></h6>
<a class="card-link {% if pool.status %}text-success-emphasis{% else %}text-danger-emphasis{% endif %}" href="{% url 'network' compute.id pool.name %}">{{ pool.name }}</a></h6>
</div>
<div class="px-1 card-body">
<dl class="row">

View file

@ -50,9 +50,9 @@
{% for pool in storages %}
<div class="col-12 col-md-4">
<div class="mb-3 card {% if pool.status %}border-success{% else %}border-danger{% endif %} shadow-sm">
<div class="card-header {% if pool.status %}bg-success{% else %}bg-danger{% endif %}">
<div class="card-header {% if pool.status %}bg-success-subtle{% else %}bg-danger-subtle{% endif %}">
<h6 class="my-0 card-title">
<a class="card-link text-light" href="{% url 'storage' compute.id pool.name %}">{{ pool.name }}</a>
<a class="card-link {% if pool.status %}text-success-emphasis{% else %}text-danger-emphasis{% endif %}" href="{% url 'storage' compute.id pool.name %}">{{ pool.name }}</a>
</h6>
</div>
<div class="card-body">

View file

@ -0,0 +1,48 @@
"""Status colours keep their contrast on every theme: a pool, network or
interface card's header, and a status word, is the theme's pale status
background with its dark status text, a pair that does not depend on the
page's background (dark themes keep light-mode emphasis colours)."""
from unittest.mock import PropertyMock, patch
import libvirt
from computes.models import Compute
from django.contrib.auth import get_user_model
from django.test import TestCase
from django.urls import reverse
from lxml import html
class StatusContrastTests(TestCase):
def setUp(self):
self.conn = libvirt.open("test:///default")
self.addCleanup(self.conn.close)
self.compute = Compute.objects.create(name="contrast", hostname="localhost", login="", password="", type=4)
self.client.force_login(get_user_model().objects.create_superuser("contrast", "c@example.com", "pw"))
def page(self, name, *args, compute=True):
with patch("vrtManager.connection.connection_manager.get_connection", return_value=self.conn):
response = self.client.get(reverse(name, args=[self.compute.id, *args] if compute else args))
self.assertEqual(response.status_code, 200)
return html.fromstring(response.content)
def test_card_headers_are_pale_with_dark_text(self):
for name in ("storages", "networks", "interfaces"):
with self.subTest(page=name):
headers = self.page(name).xpath("//div[contains(@class, 'card-header')]")
self.assertTrue(headers)
for header in headers:
classes = header.get("class").split()
self.assertTrue({"bg-success-subtle", "bg-danger-subtle"} & set(classes), classes)
link = header.xpath(".//a")[0].get("class").split()
self.assertTrue({"text-success-emphasis", "text-danger-emphasis"} & set(link), link)
def test_status_words_are_pale_badges_with_dark_text(self):
status = self.page("network", "default").xpath("//span[normalize-space()='ON' or normalize-space()='OFF']")
# the test driver has no node memory statistics for the host row
with patch.object(Compute, "ram_usage", new_callable=PropertyMock, return_value=1):
status += self.page("instances:index", compute=False).xpath("//span[normalize-space()='Connected']")
self.assertEqual(len(status), 3)
for span in status:
self.assertIn(span.get("class"), ("badge bg-success-subtle text-success-emphasis",
"badge bg-danger-subtle text-danger-emphasis"))