(ui) Improve dashboard host card scanability
Add per-host status chips for queued, running, warning, and failed runs so the dashboard shows operational pressure without needing to open each host. Restructure host cards into clearer backup activity and snapshot health sections, with less visual clutter and better mobile wrapping.
This commit is contained in:
@@ -176,23 +176,42 @@
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.host-card-status {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
flex: 0 1 auto;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
justify-content: flex-end;
|
||||
max-width: 50%;
|
||||
}
|
||||
.host-card-layout {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(240px, 320px);
|
||||
gap: 24px;
|
||||
grid-template-columns: minmax(0, 2fr) minmax(260px, 1fr);
|
||||
}
|
||||
.host-card-section {
|
||||
align-content: start;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
.host-card-section-title {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
font-weight: 650;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.host-card-timeline {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
gap: 16px 22px;
|
||||
grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
|
||||
}
|
||||
.host-card-stats {
|
||||
align-content: start;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
border-top: 1px solid #e6edf4;
|
||||
gap: 12px 18px;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
padding-top: 12px;
|
||||
}
|
||||
.host-card-item {
|
||||
display: grid;
|
||||
@@ -209,13 +228,9 @@
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.host-card-stat {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e6edf4;
|
||||
border-radius: 6px;
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
min-width: 0;
|
||||
padding: 10px;
|
||||
}
|
||||
.host-card-stat .label {
|
||||
color: var(--muted);
|
||||
@@ -286,7 +301,10 @@
|
||||
main { padding: 16px; }
|
||||
nav { padding: 0; }
|
||||
.two-col { grid-template-columns: 1fr; }
|
||||
.host-card-header { display: grid; }
|
||||
.host-card-status { justify-content: flex-start; max-width: none; }
|
||||
.host-card-layout { grid-template-columns: 1fr; }
|
||||
.host-card-stats { grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
@@ -63,9 +63,23 @@
|
||||
</div>
|
||||
<div class="host-card-status">
|
||||
<span class="status {% if host.enabled %}ok{% else %}skipped{% endif %}">{{ host.enabled|yesno:"enabled,disabled" }}</span>
|
||||
{% if host.queued_run_count %}
|
||||
<span class="status queued">queued {{ host.queued_run_count }}</span>
|
||||
{% endif %}
|
||||
{% if host.running_run_count %}
|
||||
<span class="status running">running {{ host.running_run_count }}</span>
|
||||
{% endif %}
|
||||
{% if host.warning_run_count %}
|
||||
<span class="status warning">warning {{ host.warning_run_count }}</span>
|
||||
{% endif %}
|
||||
{% if host.failed_run_count %}
|
||||
<span class="status failed">failed {{ host.failed_run_count }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="host-card-layout">
|
||||
<div class="host-card-section">
|
||||
<div class="host-card-section-title">Backup activity</div>
|
||||
<div class="host-card-timeline">
|
||||
<div class="host-card-item">
|
||||
<div class="label">Latest Snapshot</div>
|
||||
@@ -113,6 +127,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="host-card-section">
|
||||
<div class="host-card-section-title">Snapshot health</div>
|
||||
<div class="host-card-stats">
|
||||
<div class="host-card-stat">
|
||||
<div class="label">Snapshots</div>
|
||||
@@ -132,6 +149,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if host.retention_warning.has_warning %}
|
||||
<div class="host-card-warning">
|
||||
<span class="status warning">retention</span>
|
||||
|
||||
@@ -55,6 +55,14 @@ class ViewTests(TestCase):
|
||||
},
|
||||
},
|
||||
)
|
||||
BackupRun.objects.create(host=host, status=BackupRun.Status.QUEUED)
|
||||
BackupRun.objects.create(host=host, status=BackupRun.Status.RUNNING)
|
||||
BackupRun.objects.create(
|
||||
host=host,
|
||||
run_type=BackupRun.RunType.MANUAL,
|
||||
status=BackupRun.Status.FAILED,
|
||||
started_at=datetime(2026, 5, 19, 1, 15, tzinfo=timezone.utc),
|
||||
)
|
||||
|
||||
response = self.client.get(reverse("dashboard"))
|
||||
|
||||
@@ -70,6 +78,12 @@ class ViewTests(TestCase):
|
||||
self.assertContains(response, "warning")
|
||||
self.assertContains(response, "manual")
|
||||
self.assertContains(response, "scheduled")
|
||||
self.assertContains(response, "Backup activity")
|
||||
self.assertContains(response, "Snapshot health")
|
||||
self.assertContains(response, "queued 1")
|
||||
self.assertContains(response, "running 1")
|
||||
self.assertContains(response, "warning 1")
|
||||
self.assertContains(response, "failed 1")
|
||||
|
||||
def test_dashboard_renders_backup_trend_summary(self) -> None:
|
||||
self.client.force_login(self.staff_user)
|
||||
|
||||
@@ -9,7 +9,7 @@ from django.contrib import messages
|
||||
from django.contrib.admin.views.decorators import staff_member_required
|
||||
from django.conf import settings
|
||||
from django.http import FileResponse, Http404
|
||||
from django.db.models import Count
|
||||
from django.db.models import Count, Q
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.utils import timezone
|
||||
from django.views.decorators.http import require_POST
|
||||
@@ -45,7 +45,14 @@ def dashboard(request):
|
||||
global_config = GlobalConfig.objects.filter(name="default").first()
|
||||
hosts = list(
|
||||
HostConfig.objects.select_related("schedule")
|
||||
.annotate(snapshot_count=Count("snapshots", distinct=True), run_count=Count("runs", distinct=True))
|
||||
.annotate(
|
||||
snapshot_count=Count("snapshots", distinct=True),
|
||||
run_count=Count("runs", distinct=True),
|
||||
queued_run_count=Count("runs", filter=Q(runs__status=BackupRun.Status.QUEUED), distinct=True),
|
||||
running_run_count=Count("runs", filter=Q(runs__status=BackupRun.Status.RUNNING), distinct=True),
|
||||
warning_run_count=Count("runs", filter=Q(runs__status=BackupRun.Status.WARNING), distinct=True),
|
||||
failed_run_count=Count("runs", filter=Q(runs__status=BackupRun.Status.FAILED), distinct=True),
|
||||
)
|
||||
.order_by("host")
|
||||
)
|
||||
for host_config in hosts:
|
||||
|
||||
Reference in New Issue
Block a user