mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 11:16:47 +00:00
feat(computes): the host form names its connection and folds the advanced fields
The host form said "Add Compute" or "Edit Compute" whatever the connection, set its labels in a narrow column, and on an edit showed an empty password box as if the password were gone; the migration address and the gstfsd key sat among the basics with their long help, Cancel went back in the browser history and Save was green. Now the heading names the host and the connection type, with the line the add menu uses for it; a Connection card holds the name, the address, the login and the password in two columns (SASL login and password for TCP and TLS, SSH user for SSH) and the description the hosts list shows; the migration address and the gstfsd key fold under Advanced, whose summary says what is set, open when one has a value or an error. A stored password or key says so in its box. Cancel returns to the hosts list and Save is the primary action.
This commit is contained in:
parent
0044359202
commit
9790da9931
3 changed files with 81 additions and 22 deletions
|
|
@ -11,6 +11,12 @@ class KeepSecretsMixin:
|
|||
"""The form never shows the stored password or gstfsd key: left empty on
|
||||
an edit, they stay as they are."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
for name in ("password", "gstfsd_key"):
|
||||
if name in self.fields and getattr(self.instance, name):
|
||||
self.fields[name].widget.attrs["placeholder"] = _("Stored, leave empty to keep")
|
||||
|
||||
def clean_password(self):
|
||||
return self.cleaned_data["password"] or self.instance.password
|
||||
|
||||
|
|
@ -19,43 +25,62 @@ class KeepSecretsMixin:
|
|||
|
||||
|
||||
SECRET_WIDGETS = {"password": forms.PasswordInput(), "gstfsd_key": forms.PasswordInput()}
|
||||
DETAILS = {"details": _("Description")}
|
||||
DETAILS_HELP = {"details": _("Shown on the hosts list")}
|
||||
SASL_LABELS = {"login": _("SASL login"), "password": _("SASL password")} | DETAILS
|
||||
|
||||
|
||||
class TcpComputeForm(KeepSecretsMixin, forms.ModelForm):
|
||||
kind = "TCP"
|
||||
about = _("libvirt over TCP, with a SASL login")
|
||||
hostname = forms.CharField(validators=[validate_hostname])
|
||||
type = forms.IntegerField(widget=forms.HiddenInput, initial=CONN_TCP)
|
||||
|
||||
class Meta:
|
||||
model = Compute
|
||||
widgets = SECRET_WIDGETS
|
||||
labels = SASL_LABELS
|
||||
help_texts = DETAILS_HELP
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class SshComputeForm(KeepSecretsMixin, forms.ModelForm):
|
||||
kind = "SSH"
|
||||
about = _("libvirt over SSH, with the panel's SSH key")
|
||||
hostname = forms.CharField(validators=[validate_hostname], label=_("FQDN/IP"))
|
||||
type = forms.IntegerField(widget=forms.HiddenInput, initial=CONN_SSH)
|
||||
|
||||
class Meta:
|
||||
model = Compute
|
||||
widgets = SECRET_WIDGETS
|
||||
labels = {"login": _("SSH user")} | DETAILS
|
||||
help_texts = DETAILS_HELP
|
||||
exclude = ["password"]
|
||||
|
||||
|
||||
class TlsComputeForm(KeepSecretsMixin, forms.ModelForm):
|
||||
kind = "TLS"
|
||||
about = _("libvirt over TLS, with certificates")
|
||||
hostname = forms.CharField(validators=[validate_hostname])
|
||||
type = forms.IntegerField(widget=forms.HiddenInput, initial=CONN_TLS)
|
||||
|
||||
class Meta:
|
||||
model = Compute
|
||||
widgets = SECRET_WIDGETS
|
||||
labels = SASL_LABELS
|
||||
help_texts = DETAILS_HELP
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class SocketComputeForm(KeepSecretsMixin, forms.ModelForm):
|
||||
kind = _("Local")
|
||||
about = _("the libvirt of the host the panel runs on, by its socket")
|
||||
hostname = forms.CharField(widget=forms.HiddenInput, initial="localhost")
|
||||
type = forms.IntegerField(widget=forms.HiddenInput, initial=CONN_SOCKET)
|
||||
|
||||
class Meta:
|
||||
model = Compute
|
||||
widgets = SECRET_WIDGETS
|
||||
labels = DETAILS
|
||||
help_texts = DETAILS_HELP
|
||||
fields = ["name", "details", "hostname", "type", "migration_address", "gstfsd_key"]
|
||||
|
|
|
|||
|
|
@ -1,29 +1,49 @@
|
|||
{% extends "base.html" %}
|
||||
{% load django_bootstrap5 %}
|
||||
{% load bootstrap_icons %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% if form.instance.pk %}{% trans "Edit Compute" %}{% else %}{% trans "Add Compute" %}{% endif %}{% endblock %}
|
||||
{% block title %}{% if form.instance.pk %}{% trans "Edit host" %}{% else %}{% trans "Add host" %}{% endif %}{% endblock %}
|
||||
|
||||
{% block page_heading %}
|
||||
<span class="text-break">{% if form.instance.pk %}{% blocktrans with name=form.instance.name %}Edit host: {{ name }}{% endblocktrans %}{% else %}{% trans "Add host" %}{% endif %}</span>
|
||||
<span class="badge text-bg-light border align-middle fs-6">{{ form.kind }}</span>
|
||||
{% endblock page_heading %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<h2 class="mb-3">{% if form.instance.pk %}{% trans "Edit Compute" %}{% else %}{% trans "Add Compute" %}{% endif %}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-10 offset-sm-1">
|
||||
<form id="create-update" action="" method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form layout='horizontal' %}
|
||||
</form>
|
||||
<div class="float-end">
|
||||
<a class="btn btn-primary" href="javascript:history.back()">
|
||||
{% bs_icon 'x-square-fill' %} {% trans "Cancel" %}</a>
|
||||
<button type="submit" form="create-update" class="btn btn-success">
|
||||
{% bs_icon 'check-circle-fill' %} {% trans "Save" %}
|
||||
</button>
|
||||
<p class="text-body-secondary">{{ form.about }}</p>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form_errors form type='non_fields' %}
|
||||
{% for field in form.hidden_fields %}{{ field }}{% endfor %}
|
||||
<section class="card mb-3" aria-labelledby="connection-title">
|
||||
<div class="card-header"><h2 class="h5 mb-0" id="connection-title">{% trans "Connection" %}</h2></div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">{% bootstrap_field form.name %}</div>
|
||||
{% if not form.hostname.is_hidden %}<div class="col-md-6">{% bootstrap_field form.hostname %}</div>{% endif %}
|
||||
{% if "login" in form.fields %}<div class="col-md-6">{% bootstrap_field form.login %}</div>{% endif %}
|
||||
{% if "password" in form.fields %}<div class="col-md-6">{% bootstrap_field form.password %}</div>{% endif %}
|
||||
<div class="col-12">{% bootstrap_field form.details %}</div>
|
||||
</div>
|
||||
{# open when one of them is set or refused #}
|
||||
<details class="border-top pt-3"{% if form.migration_address.value or form.migration_address.errors or form.gstfsd_key.errors %} open{% endif %}>
|
||||
<summary>
|
||||
<span class="fw-semibold">{% trans "Advanced" %}</span>
|
||||
<span class="d-block small text-body-secondary text-break ms-3">
|
||||
{% trans "Migration address" %}: {{ form.migration_address.value|default:_("the host name") }}
|
||||
· {% trans "gstfsd key" %}: {% if form.instance.gstfsd_key %}{% trans "stored" %}{% else %}{% trans "none" %}{% endif %}
|
||||
</span>
|
||||
</summary>
|
||||
<div class="mt-3">
|
||||
{% bootstrap_field form.migration_address %}
|
||||
{% bootstrap_field form.gstfsd_key %}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</section>
|
||||
<div class="d-flex justify-content-end gap-2">
|
||||
<a class="btn btn-outline-secondary" href="{% url 'computes' %}">{% trans "Cancel" %}</a>
|
||||
<button type="submit" class="btn btn-primary">{% trans "Save" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
</form>
|
||||
{% endblock content %}
|
||||
|
|
|
|||
|
|
@ -29,9 +29,23 @@ class ComputePasswordTests(TestCase):
|
|||
|
||||
def test_a_long_password_is_kept_and_the_page_says_edit(self):
|
||||
compute = Compute.objects.create(name="cp-long", hostname="192.0.2.7", login="admin", password="", type=1)
|
||||
self.assertContains(self.client.get(reverse("compute_update", args=[compute.id])), "Edit Compute")
|
||||
self.assertContains(self.client.get(reverse("compute_update", args=[compute.id])), "Edit host")
|
||||
self.assertEqual(self.edit(compute, "p" * 40), "p" * 40)
|
||||
|
||||
def test_the_form_says_a_secret_is_stored_and_names_the_connection(self):
|
||||
compute = Compute.objects.create(name="cp-stored", hostname="192.0.2.9", login="admin", password="secret",
|
||||
type=1)
|
||||
response = self.client.get(reverse("compute_update", args=[compute.id]))
|
||||
self.assertContains(response, 'placeholder="Stored, leave empty to keep"', count=1) # the password only
|
||||
self.assertContains(response, "libvirt over TCP, with a SASL login")
|
||||
self.assertContains(response, "SASL password")
|
||||
self.assertNotContains(self.client.get(reverse("add_tcp_host")), "Stored, leave empty to keep")
|
||||
# the advanced fields fold away unless one is set
|
||||
self.assertNotContains(response, "<details class=\"border-top pt-3\" open>")
|
||||
Compute.objects.filter(pk=compute.pk).update(migration_address="192.0.2.19")
|
||||
self.assertContains(self.client.get(reverse("compute_update", args=[compute.id])),
|
||||
"<details class=\"border-top pt-3\" open>")
|
||||
|
||||
def test_the_api_keeps_the_password_when_it_is_left_empty(self):
|
||||
compute = Compute.objects.create(name="cp-api", hostname="192.0.2.8", login="admin", password="secret", type=1)
|
||||
for password in ("", None):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue