4 Commits

Author SHA1 Message Date
a3a8fea071 Merge pull request 'issue-38-dashboard-responsive-layout' (#41) from issue-38-dashboard-responsive-layout into master
Reviewed-on: #41
2026-05-21 14:51:34 +02:00
0e2f48ab65 (ui) Remove dashboard panel overflow traps
Let dashboard-specific panels size naturally instead of inheriting generic
panel overflow behavior, and tighten activity and host card sizing so long
content wraps without creating internal scrollbars.

Refs #38
2026-05-21 14:50:05 +02:00
b55950e24a (ui) Add dashboard section responsive hooks
Give dashboard summary, trends, and host sections dedicated layout hooks
and tighten their responsive behavior so metrics and host cards remain
readable on narrower screens.

Refs #38
2026-05-21 14:46:23 +02:00
025cd0336c (ui) Harden dashboard responsive layout
Rework the dashboard priority grid to avoid cramped four-column layouts,
prevent stretched empty panels, and make long host and snapshot text wrap
safely across dashboard cards.

Refs #38
2026-05-21 14:43:24 +02:00
3 changed files with 105 additions and 16 deletions

View File

@@ -179,10 +179,23 @@
box-shadow: var(--shadow); box-shadow: var(--shadow);
transform: translateY(-1px); transform: translateY(-1px);
} }
.metric-link:focus-visible { .metric-link:focus-visible {
outline: 3px solid #93c5fd; outline: 3px solid #93c5fd;
outline-offset: 2px; outline-offset: 2px;
} }
.dashboard-summary-grid {
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}
.dashboard-summary-grid .metric {
min-height: 78px;
}
.dashboard-summary-grid .metric .value {
font-size: 25px;
}
.dashboard-trends-panel,
.dashboard-hosts-panel {
overflow: visible;
}
.panel { .panel {
margin-bottom: 18px; margin-bottom: 18px;
overflow: auto; overflow: auto;
@@ -304,17 +317,23 @@
} }
.inline-form { margin: 0; } .inline-form { margin: 0; }
.dashboard-priority-grid { .dashboard-priority-grid {
align-items: start;
display: grid; display: grid;
gap: 14px; gap: 14px;
grid-template-columns: minmax(280px, 1.25fr) repeat(3, minmax(220px, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
margin-bottom: 20px; margin-bottom: 20px;
} }
.priority-panel { .priority-panel {
display: grid; display: grid;
gap: 12px; gap: 12px;
margin-bottom: 0; margin-bottom: 0;
min-width: 0;
overflow: visible;
}
.priority-panel > h2:first-child {
flex-wrap: wrap;
margin-bottom: 0;
} }
.priority-panel > h2:first-child { margin-bottom: 0; }
.action-list, .action-list,
.activity-list, .activity-list,
.schedule-list { .schedule-list {
@@ -385,6 +404,9 @@
.activity-row { .activity-row {
grid-template-columns: max-content minmax(0, 1fr); grid-template-columns: max-content minmax(0, 1fr);
} }
.activity-row .status {
justify-self: start;
}
.schedule-row { .schedule-row {
grid-template-columns: minmax(0, 1fr) max-content; grid-template-columns: minmax(0, 1fr) max-content;
} }
@@ -404,6 +426,14 @@
gap: 2px; gap: 2px;
min-width: 0; min-width: 0;
} }
.action-row strong,
.action-row .muted,
.activity-row strong,
.activity-row .muted,
.schedule-row strong,
.schedule-row .muted {
overflow-wrap: anywhere;
}
.schedule-time { .schedule-time {
justify-items: end; justify-items: end;
text-align: right; text-align: right;
@@ -436,6 +466,10 @@
justify-content: space-between; justify-content: space-between;
padding-top: 8px; padding-top: 8px;
} }
.storage-priority-facts strong {
text-align: right;
overflow-wrap: anywhere;
}
.host-control-grid { .host-control-grid {
display: grid; display: grid;
gap: 14px; gap: 14px;
@@ -572,6 +606,7 @@
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: var(--radius); border-radius: var(--radius);
box-shadow: var(--shadow-sm); box-shadow: var(--shadow-sm);
min-width: 0;
padding: 16px; padding: 16px;
} }
.host-card:hover { .host-card:hover {
@@ -605,8 +640,8 @@
} }
.host-card-layout { .host-card-layout {
display: grid; display: grid;
gap: 24px; gap: 18px;
grid-template-columns: minmax(0, 2fr) minmax(260px, 1fr); grid-template-columns: minmax(0, 1.7fr) minmax(240px, 0.9fr);
} }
.host-card-section { .host-card-section {
align-content: start; align-content: start;
@@ -623,7 +658,7 @@
.host-card-timeline { .host-card-timeline {
display: grid; display: grid;
gap: 16px 22px; gap: 16px 22px;
grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
} }
.host-card-stats { .host-card-stats {
align-content: start; align-content: start;
@@ -649,6 +684,10 @@
.host-card-item .value { .host-card-item .value {
overflow-wrap: anywhere; overflow-wrap: anywhere;
} }
.host-card-item .value a {
overflow-wrap: anywhere;
word-break: break-word;
}
.host-card-stat { .host-card-stat {
display: grid; display: grid;
gap: 3px; gap: 3px;
@@ -679,6 +718,9 @@
margin-top: 14px; margin-top: 14px;
padding: 10px; padding: 10px;
} }
.host-card-warning > * {
min-width: 0;
}
.messages { display: grid; gap: 8px; margin-bottom: 18px; } .messages { display: grid; gap: 8px; margin-bottom: 18px; }
.message { .message {
background: var(--panel); background: var(--panel);
@@ -778,6 +820,46 @@
.host-card-stats { grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); } .host-card-stats { grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); }
.insight-grid { grid-template-columns: 1fr; } .insight-grid { grid-template-columns: 1fr; }
} }
@media (max-width: 1100px) {
.dashboard-priority-grid {
grid-template-columns: 1fr;
}
.dashboard-summary-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.host-card-layout {
grid-template-columns: 1fr;
}
.host-card-status {
justify-content: flex-start;
max-width: none;
}
.schedule-row {
grid-template-columns: 1fr;
}
.schedule-time {
justify-items: start;
text-align: left;
}
}
@media (max-width: 560px) {
.dashboard-summary-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.metric {
min-height: 76px;
padding: 12px;
}
.metric .value {
font-size: 24px;
}
.host-card {
padding: 13px;
}
.host-card-stats {
grid-template-columns: 1fr;
}
}
</style> </style>
</head> </head>
<body> <body>

View File

@@ -33,7 +33,7 @@
{% endif %} {% endif %}
<section class="dashboard-priority-grid" aria-label="Operator priorities"> <section class="dashboard-priority-grid" aria-label="Operator priorities">
<article class="panel priority-panel"> <article class="panel priority-panel dashboard-panel-required">
<h2>Required Action</h2> <h2>Required Action</h2>
{% if action_items %} {% if action_items %}
<div class="action-list"> <div class="action-list">
@@ -70,7 +70,7 @@
{% endif %} {% endif %}
</article> </article>
<article class="panel priority-panel"> <article class="panel priority-panel dashboard-panel-schedules">
<h2>Next Scheduled Work <a class="button-link secondary compact" href="{% url 'schedules_list' %}">View all</a></h2> <h2>Next Scheduled Work <a class="button-link secondary compact" href="{% url 'schedules_list' %}">View all</a></h2>
{% if next_schedule_rows %} {% if next_schedule_rows %}
<div class="schedule-list"> <div class="schedule-list">
@@ -96,7 +96,7 @@
{% endif %} {% endif %}
</article> </article>
<article class="panel priority-panel"> <article class="panel priority-panel dashboard-panel-activity">
<h2>Recent Activity <a class="button-link secondary compact" href="{% url 'runs_list' %}">View all</a></h2> <h2>Recent Activity <a class="button-link secondary compact" href="{% url 'runs_list' %}">View all</a></h2>
{% if recent_runs %} {% if recent_runs %}
<div class="activity-list"> <div class="activity-list">
@@ -115,7 +115,7 @@
{% endif %} {% endif %}
</article> </article>
<article class="panel priority-panel"> <article class="panel priority-panel dashboard-panel-storage">
<h2>Storage Pressure</h2> <h2>Storage Pressure</h2>
{% if stats_summary.runs_sampled %} {% if stats_summary.runs_sampled %}
<div class="storage-priority"> <div class="storage-priority">
@@ -163,7 +163,7 @@
</article> </article>
</section> </section>
<section class="grid" aria-label="Summary"> <section class="grid dashboard-summary-grid" aria-label="Summary">
<a class="metric metric-link" href="#hosts"><div class="label">Hosts</div><div class="value">{{ counts.enabled_hosts }}/{{ counts.hosts }}</div></a> <a class="metric metric-link" href="#hosts"><div class="label">Hosts</div><div class="value">{{ counts.enabled_hosts }}/{{ counts.hosts }}</div></a>
<a class="metric metric-link" href="{% url 'schedules_list' %}"><div class="label">Schedules</div><div class="value">{{ counts.enabled_schedules }}/{{ counts.schedules }}</div></a> <a class="metric metric-link" href="{% url 'schedules_list' %}"><div class="label">Schedules</div><div class="value">{{ counts.enabled_schedules }}/{{ counts.schedules }}</div></a>
<a class="metric metric-link" href="{% url 'snapshots_list' %}"><div class="label">Snapshots</div><div class="value">{{ counts.snapshots }}</div></a> <a class="metric metric-link" href="{% url 'snapshots_list' %}"><div class="label">Snapshots</div><div class="value">{{ counts.snapshots }}</div></a>
@@ -172,7 +172,7 @@
<a class="metric metric-link {% if counts.failed_runs %}failed{% endif %}" href="{% url 'runs_list' %}?status=failed&amp;review=needed"><div class="label">Failed</div><div class="value">{{ counts.failed_runs }}</div></a> <a class="metric metric-link {% if counts.failed_runs %}failed{% endif %}" href="{% url 'runs_list' %}?status=failed&amp;review=needed"><div class="label">Failed</div><div class="value">{{ counts.failed_runs }}</div></a>
</section> </section>
<section class="panel"> <section class="panel dashboard-trends-panel">
<h2>Backup Trends</h2> <h2>Backup Trends</h2>
{% if stats_summary.runs_sampled %} {% if stats_summary.runs_sampled %}
<div class="insight-grid" aria-label="Backup trends"> <div class="insight-grid" aria-label="Backup trends">
@@ -216,7 +216,7 @@
{% endif %} {% endif %}
</section> </section>
<section class="panel" id="hosts"> <section class="panel dashboard-hosts-panel" id="hosts">
<h2>Hosts</h2> <h2>Hosts</h2>
<div class="host-list"> <div class="host-list">
{% for host in hosts %} {% for host in hosts %}

View File

@@ -103,6 +103,13 @@ class ViewTests(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, "Control panel") self.assertContains(response, "Control panel")
self.assertContains(response, "Backup health, required action, storage pressure, and recent activity in one place.") self.assertContains(response, "Backup health, required action, storage pressure, and recent activity in one place.")
self.assertContains(response, "dashboard-panel-required")
self.assertContains(response, "dashboard-panel-schedules")
self.assertContains(response, "dashboard-panel-activity")
self.assertContains(response, "dashboard-panel-storage")
self.assertContains(response, "dashboard-summary-grid")
self.assertContains(response, "dashboard-trends-panel")
self.assertContains(response, "dashboard-hosts-panel")
self.assertContains(response, "Dashboard") self.assertContains(response, "Dashboard")
self.assertContains(response, "web-01") self.assertContains(response, "web-01")
self.assertContains(response, "20260519-021500Z__ABCDEFGH") self.assertContains(response, "20260519-021500Z__ABCDEFGH")