issue-38-dashboard-responsive-layout #41

Merged
parkel merged 3 commits from issue-38-dashboard-responsive-layout into master 2026-05-21 14:51:34 +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")