(ui) Add consistent page headers to key views
Introduce a shared page-header pattern with kicker, title, subtitle, and actions, then apply it to the dashboard, host detail, run detail, snapshot detail, and retention plan pages. Scope the global app header styles to avoid leaking sticky navigation styles onto page-level headers, and add view assertions for the new page context. Refs #28
This commit is contained in:
@@ -43,7 +43,7 @@
|
|||||||
outline: 3px solid rgba(7, 95, 174, 0.24);
|
outline: 3px solid rgba(7, 95, 174, 0.24);
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
header {
|
body > header {
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
box-shadow: var(--shadow-sm);
|
box-shadow: var(--shadow-sm);
|
||||||
@@ -105,6 +105,35 @@
|
|||||||
}
|
}
|
||||||
p { margin: 0 0 12px; }
|
p { margin: 0 0 12px; }
|
||||||
p:last-child { margin-bottom: 0; }
|
p:last-child { margin-bottom: 0; }
|
||||||
|
.page-header {
|
||||||
|
align-items: end;
|
||||||
|
display: flex;
|
||||||
|
gap: 18px;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.page-title {
|
||||||
|
display: grid;
|
||||||
|
gap: 5px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.page-title h1 { margin-bottom: 0; overflow-wrap: anywhere; }
|
||||||
|
.page-kicker {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 750;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.page-subtitle {
|
||||||
|
color: var(--muted);
|
||||||
|
max-width: 760px;
|
||||||
|
}
|
||||||
|
.page-header .actions {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
.grid {
|
.grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
|
||||||
@@ -483,7 +512,7 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
@media (max-width: 800px) {
|
@media (max-width: 800px) {
|
||||||
header { padding: 0 14px; position: static; }
|
body > header { padding: 0 14px; position: static; }
|
||||||
main { padding: 18px 14px 32px; }
|
main { padding: 18px 14px 32px; }
|
||||||
nav {
|
nav {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
@@ -493,6 +522,11 @@
|
|||||||
}
|
}
|
||||||
nav strong { flex-basis: 100%; margin-right: 0; }
|
nav strong { flex-basis: 100%; margin-right: 0; }
|
||||||
nav .spacer { display: none; }
|
nav .spacer { display: none; }
|
||||||
|
.page-header {
|
||||||
|
align-items: stretch;
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
.page-header .actions { justify-content: flex-start; }
|
||||||
.two-col { grid-template-columns: 1fr; }
|
.two-col { grid-template-columns: 1fr; }
|
||||||
.host-card-header { display: grid; }
|
.host-card-header { display: grid; }
|
||||||
.host-card-status { justify-content: flex-start; max-width: none; }
|
.host-card-status { justify-content: flex-start; max-width: none; }
|
||||||
|
|||||||
@@ -3,12 +3,17 @@
|
|||||||
{% block title %}pobsync dashboard{% endblock %}
|
{% block title %}pobsync dashboard{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<header class="page-header">
|
||||||
|
<div class="page-title">
|
||||||
|
<div class="page-kicker">Control panel</div>
|
||||||
<h1>Dashboard</h1>
|
<h1>Dashboard</h1>
|
||||||
|
<div class="page-subtitle">Backup health, required action, storage pressure, and recent activity in one place.</div>
|
||||||
|
</div>
|
||||||
<section class="actions" aria-label="Dashboard actions">
|
<section class="actions" aria-label="Dashboard actions">
|
||||||
<a class="button-link" href="{% url 'create_host_config' %}">New host</a>
|
<a class="button-link" href="{% url 'create_host_config' %}">New host</a>
|
||||||
<a class="button-link secondary" href="{% url 'edit_global_config' %}">{% if global_config %}Edit global config{% else %}Create global config{% endif %}</a>
|
<a class="button-link secondary" href="{% url 'edit_global_config' %}">{% if global_config %}Edit global config{% else %}Create global config{% endif %}</a>
|
||||||
</section>
|
</section>
|
||||||
|
</header>
|
||||||
|
|
||||||
{% if not global_config or not counts.hosts %}
|
{% if not global_config or not counts.hosts %}
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
|
|||||||
@@ -3,8 +3,12 @@
|
|||||||
{% block title %}{{ host.host }} | pobsync{% endblock %}
|
{% block title %}{{ host.host }} | pobsync{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<header class="page-header">
|
||||||
|
<div class="page-title">
|
||||||
|
<div class="page-kicker">Host</div>
|
||||||
<h1>{{ host.host }}</h1>
|
<h1>{{ host.host }}</h1>
|
||||||
|
<div class="page-subtitle">{{ host.address }} · {{ host.enabled|yesno:"enabled,disabled" }}</div>
|
||||||
|
</div>
|
||||||
<section class="actions" aria-label="Host actions">
|
<section class="actions" aria-label="Host actions">
|
||||||
<a class="button-link" href="{% url 'edit_host_config' host.host %}">Edit config</a>
|
<a class="button-link" href="{% url 'edit_host_config' host.host %}">Edit config</a>
|
||||||
<form method="post" action="{% url 'discover_host_snapshots' host.host %}">
|
<form method="post" action="{% url 'discover_host_snapshots' host.host %}">
|
||||||
@@ -26,6 +30,7 @@
|
|||||||
<button type="submit" class="secondary">Run connection preflight</button>
|
<button type="submit" class="secondary">Run connection preflight</button>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
</header>
|
||||||
|
|
||||||
<section class="grid" aria-label="Host summary">
|
<section class="grid" aria-label="Host summary">
|
||||||
<div class="metric"><div class="label">Snapshots</div><div class="value">{{ counts.snapshots }}</div></div>
|
<div class="metric"><div class="label">Snapshots</div><div class="value">{{ counts.snapshots }}</div></div>
|
||||||
|
|||||||
@@ -3,8 +3,12 @@
|
|||||||
{% block title %}Retention plan | {{ host.host }}{% endblock %}
|
{% block title %}Retention plan | {{ host.host }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>Retention Plan: {{ host.host }}</h1>
|
<header class="page-header">
|
||||||
|
<div class="page-title">
|
||||||
|
<div class="page-kicker">Retention</div>
|
||||||
|
<h1>{{ host.host }}</h1>
|
||||||
|
<div class="page-subtitle">Preview which snapshots stay, which would be deleted, and whether incomplete cleanup is needed.</div>
|
||||||
|
</div>
|
||||||
<section class="actions" aria-label="Retention filters">
|
<section class="actions" aria-label="Retention filters">
|
||||||
<a class="button-link" href="{% url 'host_detail' host.host %}">Back to host</a>
|
<a class="button-link" href="{% url 'host_detail' host.host %}">Back to host</a>
|
||||||
<a class="button-link" href="{% url 'host_retention_plan' host.host %}?kind=scheduled">Scheduled</a>
|
<a class="button-link" href="{% url 'host_retention_plan' host.host %}?kind=scheduled">Scheduled</a>
|
||||||
@@ -12,6 +16,7 @@
|
|||||||
<a class="button-link" href="{% url 'host_retention_plan' host.host %}?kind=all">All</a>
|
<a class="button-link" href="{% url 'host_retention_plan' host.host %}?kind=all">All</a>
|
||||||
<a class="button-link" href="{% url 'host_retention_plan' host.host %}?kind={{ kind }}&protect_bases=1">Protect bases</a>
|
<a class="button-link" href="{% url 'host_retention_plan' host.host %}?kind={{ kind }}&protect_bases=1">Protect bases</a>
|
||||||
</section>
|
</section>
|
||||||
|
</header>
|
||||||
|
|
||||||
<section class="grid" aria-label="Retention plan summary">
|
<section class="grid" aria-label="Retention plan summary">
|
||||||
<div class="metric"><div class="label">Kind</div><div class="value">{{ plan.kind }}</div></div>
|
<div class="metric"><div class="label">Kind</div><div class="value">{{ plan.kind }}</div></div>
|
||||||
|
|||||||
@@ -3,8 +3,12 @@
|
|||||||
{% block title %}Run {{ run.id }} | {{ run.host.host }}{% endblock %}
|
{% block title %}Run {{ run.id }} | {{ run.host.host }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<header class="page-header">
|
||||||
|
<div class="page-title">
|
||||||
|
<div class="page-kicker">Backup run</div>
|
||||||
<h1>Run {{ run.id }}</h1>
|
<h1>Run {{ run.id }}</h1>
|
||||||
|
<div class="page-subtitle">{{ run.host.host }} · {{ run.run_type }} · {{ run.status }}</div>
|
||||||
|
</div>
|
||||||
<section class="actions" aria-label="Run actions">
|
<section class="actions" aria-label="Run actions">
|
||||||
<a class="button-link" href="{% url 'host_detail' run.host.host %}">Back to host</a>
|
<a class="button-link" href="{% url 'host_detail' run.host.host %}">Back to host</a>
|
||||||
{% if can_cancel %}
|
{% if can_cancel %}
|
||||||
@@ -22,6 +26,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</section>
|
</section>
|
||||||
|
</header>
|
||||||
|
|
||||||
<section class="grid" aria-label="Run summary">
|
<section class="grid" aria-label="Run summary">
|
||||||
<div class="metric"><div class="label">Host</div><div class="value">{{ run.host.host }}</div></div>
|
<div class="metric"><div class="label">Host</div><div class="value">{{ run.host.host }}</div></div>
|
||||||
|
|||||||
@@ -3,11 +3,16 @@
|
|||||||
{% block title %}Snapshot {{ snapshot.dirname }} | {{ snapshot.host.host }}{% endblock %}
|
{% block title %}Snapshot {{ snapshot.dirname }} | {{ snapshot.host.host }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<header class="page-header">
|
||||||
|
<div class="page-title">
|
||||||
|
<div class="page-kicker">Snapshot</div>
|
||||||
<h1>{{ snapshot.dirname }}</h1>
|
<h1>{{ snapshot.dirname }}</h1>
|
||||||
|
<div class="page-subtitle">{{ snapshot.host.host }} · {{ snapshot.kind }} · {{ snapshot.status }}</div>
|
||||||
|
</div>
|
||||||
<section class="actions" aria-label="Snapshot actions">
|
<section class="actions" aria-label="Snapshot actions">
|
||||||
<a class="button-link" href="{% url 'host_detail' snapshot.host.host %}">Back to host</a>
|
<a class="button-link" href="{% url 'host_detail' snapshot.host.host %}">Back to host</a>
|
||||||
</section>
|
</section>
|
||||||
|
</header>
|
||||||
|
|
||||||
<section class="grid" aria-label="Snapshot summary">
|
<section class="grid" aria-label="Snapshot summary">
|
||||||
<div class="metric"><div class="label">Host</div><div class="value">{{ snapshot.host.host }}</div></div>
|
<div class="metric"><div class="label">Host</div><div class="value">{{ snapshot.host.host }}</div></div>
|
||||||
|
|||||||
@@ -101,6 +101,8 @@ class ViewTests(TestCase):
|
|||||||
response = self.client.get(reverse("dashboard"))
|
response = self.client.get(reverse("dashboard"))
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, "Control panel")
|
||||||
|
self.assertContains(response, "Backup health, required action, storage pressure, and recent activity in one place.")
|
||||||
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")
|
||||||
@@ -869,6 +871,8 @@ class ViewTests(TestCase):
|
|||||||
response = self.client.get(reverse("host_detail", args=[host.host]))
|
response = self.client.get(reverse("host_detail", args=[host.host]))
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, "Host")
|
||||||
|
self.assertContains(response, "web-01.example.test")
|
||||||
self.assertContains(response, "Effective Config")
|
self.assertContains(response, "Effective Config")
|
||||||
self.assertContains(response, "Backup source:")
|
self.assertContains(response, "Backup source:")
|
||||||
self.assertNotContains(response, "Source root:")
|
self.assertNotContains(response, "Source root:")
|
||||||
@@ -1425,6 +1429,8 @@ class ViewTests(TestCase):
|
|||||||
response = self.client.get(reverse("run_detail", args=[run.id]))
|
response = self.client.get(reverse("run_detail", args=[run.id]))
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, "Backup run")
|
||||||
|
self.assertContains(response, "web-01")
|
||||||
self.assertContains(response, "Failure")
|
self.assertContains(response, "Failure")
|
||||||
self.assertContains(response, "transport")
|
self.assertContains(response, "transport")
|
||||||
self.assertContains(response, "Check network connectivity.")
|
self.assertContains(response, "Check network connectivity.")
|
||||||
@@ -1615,6 +1621,7 @@ class ViewTests(TestCase):
|
|||||||
response = self.client.get(reverse("snapshot_detail", args=[base.id]))
|
response = self.client.get(reverse("snapshot_detail", args=[base.id]))
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, "Snapshot")
|
||||||
self.assertContains(response, base.dirname)
|
self.assertContains(response, base.dirname)
|
||||||
self.assertContains(response, "BASESNAP")
|
self.assertContains(response, "BASESNAP")
|
||||||
self.assertContains(response, "Stats")
|
self.assertContains(response, "Stats")
|
||||||
@@ -1697,7 +1704,9 @@ class ViewTests(TestCase):
|
|||||||
response = self.client.get(reverse("host_retention_plan", args=[host.host]))
|
response = self.client.get(reverse("host_retention_plan", args=[host.host]))
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertContains(response, "Retention Plan: web-01")
|
self.assertContains(response, "Retention")
|
||||||
|
self.assertContains(response, "Preview which snapshots stay")
|
||||||
|
self.assertContains(response, "web-01")
|
||||||
self.assertContains(response, old_snapshot.dirname)
|
self.assertContains(response, old_snapshot.dirname)
|
||||||
self.assertContains(response, new_snapshot.dirname)
|
self.assertContains(response, new_snapshot.dirname)
|
||||||
self.assertContains(response, "newest")
|
self.assertContains(response, "newest")
|
||||||
|
|||||||
Reference in New Issue
Block a user