(ui) Standardize primary form actions
Add shared form action styling and consistent Cancel links across config, schedule, and SSH key forms so create/edit flows behave predictably. Refs #25
This commit is contained in:
@@ -280,6 +280,17 @@
|
|||||||
}
|
}
|
||||||
button.secondary:hover,
|
button.secondary:hover,
|
||||||
.button-link.secondary:hover { background: #eef3f8; }
|
.button-link.secondary:hover { background: #eef3f8; }
|
||||||
|
button.danger,
|
||||||
|
.button-link.danger {
|
||||||
|
background: var(--failed);
|
||||||
|
border-color: var(--failed);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
button.danger:hover,
|
||||||
|
.button-link.danger:hover {
|
||||||
|
background: #842828;
|
||||||
|
border-color: #842828;
|
||||||
|
}
|
||||||
button.compact,
|
button.compact,
|
||||||
.button-link.compact {
|
.button-link.compact {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@@ -679,6 +690,16 @@
|
|||||||
.message.error { border-color: #e8b4b4; background: #fff0f0; color: var(--failed); }
|
.message.error { border-color: #e8b4b4; background: #fff0f0; color: var(--failed); }
|
||||||
.message.warning { border-color: #e7cf8a; background: #fff8df; color: var(--running); }
|
.message.warning { border-color: #e7cf8a; background: #fff8df; color: var(--running); }
|
||||||
.form-grid { display: grid; gap: 15px; max-width: 720px; }
|
.form-grid { display: grid; gap: 15px; max-width: 720px; }
|
||||||
|
.form-actions {
|
||||||
|
align-items: center;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 4px;
|
||||||
|
padding-top: 15px;
|
||||||
|
}
|
||||||
|
.form-actions .button-link.secondary { margin-left: auto; }
|
||||||
.field { display: grid; gap: 6px; }
|
.field { display: grid; gap: 6px; }
|
||||||
.field label { font-weight: 700; }
|
.field label { font-weight: 700; }
|
||||||
.field input[type="text"], .field input[type="number"], .field select, .field textarea {
|
.field input[type="text"], .field input[type="number"], .field select, .field textarea {
|
||||||
@@ -736,6 +757,7 @@
|
|||||||
.host-control-grid { grid-template-columns: 1fr; }
|
.host-control-grid { grid-template-columns: 1fr; }
|
||||||
.schedule-row { grid-template-columns: 1fr; }
|
.schedule-row { grid-template-columns: 1fr; }
|
||||||
.schedule-time { justify-items: start; text-align: left; }
|
.schedule-time { justify-items: start; text-align: left; }
|
||||||
|
.form-actions .button-link.secondary { margin-left: 0; }
|
||||||
.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; }
|
||||||
.host-card-layout { grid-template-columns: 1fr; }
|
.host-card-layout { grid-template-columns: 1fr; }
|
||||||
|
|||||||
@@ -33,8 +33,9 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<div class="actions">
|
<div class="form-actions">
|
||||||
<button type="submit">Save global config</button>
|
<button type="submit">Save global config</button>
|
||||||
|
<a class="button-link secondary" href="{% url 'dashboard' %}">Cancel</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -33,8 +33,13 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<div class="actions">
|
<div class="form-actions">
|
||||||
<button type="submit">{% if host %}Save config{% else %}Create host{% endif %}</button>
|
<button type="submit">{% if host %}Save config{% else %}Create host{% endif %}</button>
|
||||||
|
{% if host %}
|
||||||
|
<a class="button-link secondary" href="{% url 'host_detail' host.host %}">Cancel</a>
|
||||||
|
{% else %}
|
||||||
|
<a class="button-link secondary" href="{% url 'dashboard' %}">Cancel</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -30,8 +30,9 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<div class="actions">
|
<div class="form-actions">
|
||||||
<button type="submit">Save schedule</button>
|
<button type="submit">Save schedule</button>
|
||||||
|
<a class="button-link secondary" href="{% url 'host_detail' host.host %}">Cancel</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -41,8 +41,9 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<div class="actions">
|
<div class="form-actions">
|
||||||
<button type="submit">{% if credential %}Save SSH key{% else %}Create SSH key{% endif %}</button>
|
<button type="submit">{% if credential %}Save SSH key{% else %}Create SSH key{% endif %}</button>
|
||||||
|
<a class="button-link secondary" href="{% url 'ssh_credentials' %}">Cancel</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
@@ -64,7 +65,9 @@
|
|||||||
<label for="confirm_name">Confirm key name</label>
|
<label for="confirm_name">Confirm key name</label>
|
||||||
<input id="confirm_name" name="confirm_name" type="text" {% if credential.hosts.exists or credential.global_configs.exists %}disabled{% endif %}>
|
<input id="confirm_name" name="confirm_name" type="text" {% if credential.hosts.exists or credential.global_configs.exists %}disabled{% endif %}>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-actions">
|
||||||
<button type="submit" class="danger" {% if credential.hosts.exists or credential.global_configs.exists %}disabled{% endif %}>Delete SSH key</button>
|
<button type="submit" class="danger" {% if credential.hosts.exists or credential.global_configs.exists %}disabled{% endif %}>Delete SSH key</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -29,8 +29,9 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<div class="actions">
|
<div class="form-actions">
|
||||||
<button type="submit">Generate SSH key</button>
|
<button type="submit">Generate SSH key</button>
|
||||||
|
<a class="button-link secondary" href="{% url 'ssh_credentials' %}">Cancel</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -542,6 +542,19 @@ class ViewTests(TestCase):
|
|||||||
self.assertEqual(credential.private_key, "UPLOADED PRIVATE KEY\n")
|
self.assertEqual(credential.private_key, "UPLOADED PRIVATE KEY\n")
|
||||||
self.assertEqual(credential.public_key, "DERIVED PUBLIC KEY")
|
self.assertEqual(credential.public_key, "DERIVED PUBLIC KEY")
|
||||||
|
|
||||||
|
def test_ssh_credential_forms_render_cancel_actions(self) -> None:
|
||||||
|
self.client.force_login(self.staff_user)
|
||||||
|
credential = SshCredential.objects.create(name="backup-key")
|
||||||
|
|
||||||
|
create_response = self.client.get(reverse("create_ssh_credential"))
|
||||||
|
edit_response = self.client.get(reverse("edit_ssh_credential", args=[credential.id]))
|
||||||
|
generate_response = self.client.get(reverse("generate_ssh_credential"))
|
||||||
|
|
||||||
|
for response in (create_response, edit_response, generate_response):
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, "Cancel")
|
||||||
|
self.assertContains(response, reverse("ssh_credentials"))
|
||||||
|
|
||||||
def test_ssh_credentials_view_generates_filesystem_key(self) -> None:
|
def test_ssh_credentials_view_generates_filesystem_key(self) -> None:
|
||||||
self.client.force_login(self.staff_user)
|
self.client.force_login(self.staff_user)
|
||||||
with TemporaryDirectory() as tmp, override_settings(POBSYNC_HOME=str(Path(tmp) / "home")):
|
with TemporaryDirectory() as tmp, override_settings(POBSYNC_HOME=str(Path(tmp) / "home")):
|
||||||
@@ -746,6 +759,8 @@ class ViewTests(TestCase):
|
|||||||
self.assertContains(response, f'value="{credential.id}" selected')
|
self.assertContains(response, f'value="{credential.id}" selected')
|
||||||
self.assertContains(response, "--archive")
|
self.assertContains(response, "--archive")
|
||||||
self.assertContains(response, "/proc/***")
|
self.assertContains(response, "/proc/***")
|
||||||
|
self.assertContains(response, "Cancel")
|
||||||
|
self.assertContains(response, reverse("dashboard"))
|
||||||
|
|
||||||
def test_global_config_form_renders_static_container_backup_root_on_edit(self) -> None:
|
def test_global_config_form_renders_static_container_backup_root_on_edit(self) -> None:
|
||||||
self.client.force_login(self.staff_user)
|
self.client.force_login(self.staff_user)
|
||||||
@@ -2142,6 +2157,8 @@ class ViewTests(TestCase):
|
|||||||
self.assertContains(response, "evaluated by the pobsync scheduler service")
|
self.assertContains(response, "evaluated by the pobsync scheduler service")
|
||||||
self.assertContains(response, "15 2 * * *")
|
self.assertContains(response, "15 2 * * *")
|
||||||
self.assertContains(response, "Save schedule")
|
self.assertContains(response, "Save schedule")
|
||||||
|
self.assertContains(response, "Cancel")
|
||||||
|
self.assertContains(response, reverse("host_detail", args=[host.host]))
|
||||||
|
|
||||||
def test_schedule_form_creates_schedule(self) -> None:
|
def test_schedule_form_creates_schedule(self) -> None:
|
||||||
self.client.force_login(self.staff_user)
|
self.client.force_login(self.staff_user)
|
||||||
@@ -2243,6 +2260,8 @@ class ViewTests(TestCase):
|
|||||||
self.assertContains(response, "/srv")
|
self.assertContains(response, "/srv")
|
||||||
self.assertContains(response, "*.tmp")
|
self.assertContains(response, "*.tmp")
|
||||||
self.assertContains(response, "--numeric-ids")
|
self.assertContains(response, "--numeric-ids")
|
||||||
|
self.assertContains(response, "Cancel")
|
||||||
|
self.assertContains(response, reverse("host_detail", args=[host.host]))
|
||||||
|
|
||||||
def test_host_config_form_renders_effective_config_check(self) -> None:
|
def test_host_config_form_renders_effective_config_check(self) -> None:
|
||||||
self.client.force_login(self.staff_user)
|
self.client.force_login(self.staff_user)
|
||||||
|
|||||||
Reference in New Issue
Block a user