Polish forms and action flows #35

Merged
parkel merged 4 commits from issue-25-forms-action-flows into master 2026-05-21 14:28:21 +02:00
7 changed files with 58 additions and 6 deletions
Showing only changes of commit af548f11c4 - Show all commits

View File

@@ -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; }

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 %}

View File

@@ -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>

View File

@@ -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)