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:
parent
084f2e8b60
commit
b45f673da7
7 changed files with 62 additions and 14 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
48
webvirtcloud/tests/test_status_contrast.py
Normal file
48
webvirtcloud/tests/test_status_contrast.py
Normal 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"))
|
||||
Loading…
Add table
Add a link
Reference in a new issue