Compare commits

...

2 commits

Author SHA1 Message Date
b8f3621b47 Migrate user info data
All checks were successful
Tests / test (push) Successful in 30s
2025-10-24 12:19:04 +02:00
75fe0799e0 Implement user info help text with popovers 2025-10-24 12:16:38 +02:00
6 changed files with 121 additions and 10 deletions

View file

@ -5,14 +5,25 @@ from django_jsonform.widgets import JSONFormWidget
from servala.core.models import ControlPlane, ServiceDefinition
CONTROL_PLANE_USER_INFO_SCHEMA = {
"type": "object",
"properties": {
"CNAME Record": {
"title": "CNAME Record",
"type": "string",
"type": "array",
"items": {
"type": "object",
"properties": {
"title": {
"type": "string",
"title": "Title",
},
"content": {
"type": "string",
"title": "Content",
},
"help_text": {
"type": "string",
"title": "Help Text (optional)",
},
},
"required": ["title", "content"],
},
"additionalProperties": {"type": "string"},
}

View file

@ -0,0 +1,65 @@
# Generated by Django 5.2.7 on 2025-10-24 10:04
from django.db import migrations
def convert_user_info_to_array(apps, schema_editor):
"""
Convert user_info from object format {"key": "value"} to array format
[{"title": "key", "content": "value"}].
"""
ControlPlane = apps.get_model("core", "ControlPlane")
for control_plane in ControlPlane.objects.all():
if not control_plane.user_info:
continue
# If it's already an array (migration already run or new format), skip
if isinstance(control_plane.user_info, list):
continue
# Convert from dict to array
if isinstance(control_plane.user_info, dict):
new_user_info = []
for key, value in control_plane.user_info.items():
new_user_info.append({"title": key, "content": value})
control_plane.user_info = new_user_info
control_plane.save(update_fields=["user_info"])
def reverse_user_info_to_object(apps, schema_editor):
"""
Reverse the migration by converting array format back to object format.
Note: help_text will be lost during reversal.
"""
ControlPlane = apps.get_model("core", "ControlPlane")
for control_plane in ControlPlane.objects.all():
if not control_plane.user_info:
continue
# If it's already an object, skip
if isinstance(control_plane.user_info, dict):
continue
# Convert from array to dict
if isinstance(control_plane.user_info, list):
new_user_info = {}
for item in control_plane.user_info:
if isinstance(item, dict) and "title" in item and "content" in item:
new_user_info[item["title"]] = item["content"]
control_plane.user_info = new_user_info
control_plane.save(update_fields=["user_info"])
class Migration(migrations.Migration):
dependencies = [
("core", "0011_alter_organizationorigin_billing_entity"),
]
operations = [
migrations.RunPython(convert_user_info_to_array, reverse_user_info_to_object),
]

View file

@ -156,7 +156,8 @@ class ControlPlane(ServalaModelMixin, models.Model):
blank=True,
verbose_name=_("User Information"),
help_text=_(
"Key-value information displayed to users when selecting this control plane"
'Array of info objects: [{"title": "", "content": "", "help_text": ""}]. '
"The help_text field is optional and will be shown as a hover popover on an info icon."
),
)
wildcard_dns = models.CharField(

View file

@ -248,3 +248,12 @@
</div>
</div>
{% endblock content %}
{% block extra_js %}
<script>
// Initialize Bootstrap popovers for help text
document.addEventListener('DOMContentLoaded', function() {
const popoverTriggerList = document.querySelectorAll('[data-bs-toggle="popover"]');
[...popoverTriggerList].map(el => new bootstrap.Popover(el));
});
</script>
{% endblock extra_js %}

View file

@ -108,4 +108,19 @@
</script>
<script defer src="{% static "js/fqdn.js" %}"></script>
{% endif %}
<script>
// Initialize Bootstrap popovers for help text
document.addEventListener('DOMContentLoaded', function() {
const popoverTriggerList = document.querySelectorAll('[data-bs-toggle="popover"]');
[...popoverTriggerList].map(el => new bootstrap.Popover(el));
});
// Re-initialize popovers after HTMX swaps
document.body.addEventListener('htmx:afterSwap', function(event) {
if (event.detail.target.id === 'control-plane-info') {
const popoverTriggerList = event.detail.target.querySelectorAll('[data-bs-toggle="popover"]');
[...popoverTriggerList].map(el => new bootstrap.Popover(el));
}
});
</script>
{% endblock extra_js %}

View file

@ -3,10 +3,20 @@
<div class="table-responsive">
<table class="table mb-0 table-lg">
<tbody>
{% for key, value in control_plane.user_info.items %}
{% for info in control_plane.user_info %}
<tr>
<th>{{ key }}</th>
<td>{{ value }}</td>
<th>
{{ info.title }}
{% if info.help_text %}
<i class="bi bi-info-circle ms-1"
data-bs-toggle="popover"
data-bs-trigger="hover focus"
data-bs-placement="top"
data-bs-content="{{ info.help_text }}"
style="cursor: help"></i>
{% endif %}
</th>
<td>{{ info.content }}</td>
</tr>
{% endfor %}
</tbody>