diff --git a/src/servala/core/forms.py b/src/servala/core/forms.py index baa85fb..034d1c9 100644 --- a/src/servala/core/forms.py +++ b/src/servala/core/forms.py @@ -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"}, } diff --git a/src/servala/core/migrations/0012_convert_user_info_to_array.py b/src/servala/core/migrations/0012_convert_user_info_to_array.py new file mode 100644 index 0000000..892949e --- /dev/null +++ b/src/servala/core/migrations/0012_convert_user_info_to_array.py @@ -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), + ] diff --git a/src/servala/core/models/service.py b/src/servala/core/models/service.py index 42fc500..3af8c89 100644 --- a/src/servala/core/models/service.py +++ b/src/servala/core/models/service.py @@ -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( diff --git a/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html b/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html index d375344..4aaef14 100644 --- a/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html +++ b/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html @@ -102,7 +102,16 @@ {% endif %} - {% include "includes/control_plane_user_info.html" with control_plane=instance.context.control_plane %} + {% if control_plane.user_info %} +
+
+

{% translate "Service Provider Zone Information" %}

+
+
+ {% include "includes/control_plane_user_info.html" with control_plane=instance.context.control_plane %} +
+
+ {% endif %} {% if instance.spec and spec_fieldsets %}
@@ -239,3 +248,12 @@
{% endblock content %} +{% block extra_js %} + +{% endblock extra_js %} diff --git a/src/servala/frontend/templates/frontend/organizations/service_offering_detail.html b/src/servala/frontend/templates/frontend/organizations/service_offering_detail.html index 842e610..3305328 100644 --- a/src/servala/frontend/templates/frontend/organizations/service_offering_detail.html +++ b/src/servala/frontend/templates/frontend/organizations/service_offering_detail.html @@ -7,9 +7,13 @@ {{ offering }} {% endblock page_title %} {% endblock html_title %} -{% partialdef control-plane-info %} -{% if selected_plane %} - {% include "includes/control_plane_user_info.html" with control_plane=selected_plane %} +{% partialdef control-plane-info inline=True %} +{% if selected_plane and selected_plane.user_info %} +
+
+ {% include "includes/control_plane_user_info.html" with control_plane=selected_plane %} +
+
{% endif %} {% endpartialdef %} {% partialdef service-form %} @@ -30,65 +34,98 @@ {% endpartialdef %} {% block content %}
-
-
-
-
- {% if service.logo %} - {{ service.name }} - {% endif %} -
-

{{ offering }}

- {{ offering.service.category }} + {% if not has_control_planes %} + +
+
+ -
- {% if offering.description %} -
-
-

{{ offering.description|urlize }}

-
-
- {% endif %} - {% if not has_control_planes %} -

{% translate "We currently cannot offer this service, sorry!" %}

- {% else %} +
+
+ {% else %} + +
+ +
+
+
+
{% translate "Service Provider Zone" %}
+
+
+ hx-swap="outerHTML" + class="control-plane-select-form"> {{ select_form }}
- {% endif %} - {% if service.external_links or offering.external_links %} -
-
-
{% translate "External Links" %}
-
- {% for link in service.external_links %} - {% include "includes/external_link.html" %} - {% endfor %} - {% for link in offering.external_links %} - {% include "includes/external_link.html" %} - {% endfor %} -
-
+ +
+ {% partial control-plane-info %}
- {% endif %} +
+
+
+ +
+
+
+
{% translate "Service Information" %}
+
+
+ {% if offering.service.logo or offering.description %} +
+ {% if offering.service.logo %} +
+ {{ offering.service.name }} +
+ {% endif %} + {% if offering.description %} +
+

{{ offering.description|urlize }}

+
+ {% endif %} +
+ {% endif %} + {% if offering.service.external_links or offering.external_links %} + {% if offering.service.logo or offering.description %}
{% endif %} +
{% translate "External Links" %}
+
+ {% for link in offering.service.external_links %} + {% include "includes/external_link.html" %} + {% endfor %} + {% for link in offering.external_links %} + {% include "includes/external_link.html" %} + {% endfor %} +
+ {% else %} + {% if not offering.service.logo and not offering.description %} +

{% translate "No additional information available." %}

+ {% endif %} + {% endif %} +
-
{% partial service-form %}
-
- {% if has_control_planes %} -
{% partial control-plane-info %}
- {% endif %} + {% endif %} + +
+
+
{% partial service-form %}
@@ -103,4 +140,19 @@ {% endif %} + {% endblock extra_js %} diff --git a/src/servala/frontend/templates/includes/control_plane_user_info.html b/src/servala/frontend/templates/includes/control_plane_user_info.html index b9ffe99..a3a27f5 100644 --- a/src/servala/frontend/templates/includes/control_plane_user_info.html +++ b/src/servala/frontend/templates/includes/control_plane_user_info.html @@ -1,26 +1,26 @@ {% load i18n %} -{% comment %} -Reusable snippet for displaying ControlPlane user_info -Usage: {% include "includes/control_plane_user_info.html" with control_plane=control_plane_object %} -{% endcomment %} {% if control_plane.user_info %} -
-
-

{% translate "Service Provider Zone Information" %}

-
-
-
- - - {% for key, value in control_plane.user_info.items %} - - - - - {% endfor %} - -
{{ key }}{{ value }}
+
+ {% for info in control_plane.user_info %} +
+
+ + {{ info.title }} + + {% if info.help_text %} + + {% endif %} +
+
+ {{ info.content }} +
-
+ {% endfor %}
{% endif %}