Custom form configuration #268
6 changed files with 80 additions and 44 deletions
|
|
@ -1,3 +1,6 @@
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from django.contrib import admin, messages
|
from django.contrib import admin, messages
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django_jsonform.widgets import JSONFormWidget
|
from django_jsonform.widgets import JSONFormWidget
|
||||||
|
|
@ -313,9 +316,9 @@ class ServiceDefinitionAdmin(admin.ModelAdmin):
|
||||||
(
|
(
|
||||||
_("Form Configuration"),
|
_("Form Configuration"),
|
||||||
{
|
{
|
||||||
"fields": ("advanced_fields",),
|
"fields": ("form_config",),
|
||||||
"description": _(
|
"description": _(
|
||||||
"Configure which fields should be hidden behind an 'Advanced' toggle in the form"
|
"Optional custom form configuration. When provided, this will be used instead of auto-generating the form from the OpenAPI spec."
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
@ -323,19 +326,13 @@ class ServiceDefinitionAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
def get_form(self, request, obj=None, **kwargs):
|
def get_form(self, request, obj=None, **kwargs):
|
||||||
form = super().get_form(request, obj, **kwargs)
|
form = super().get_form(request, obj, **kwargs)
|
||||||
# JSON schema for advanced_fields field
|
schema_path = Path(__file__).parent / "schemas" / "form_config_schema.json"
|
||||||
advanced_fields_schema = {
|
with open(schema_path) as f:
|
||||||
"type": "array",
|
form_config_schema = json.load(f)
|
||||||
"title": "Advanced Fields",
|
|
||||||
"items": {
|
if "form_config" in form.base_fields:
|
||||||
"type": "string",
|
form.base_fields["form_config"].widget = JSONFormWidget(
|
||||||
"title": "Field Name",
|
schema=form_config_schema
|
||||||
"description": "Field name in dot notation (e.g., spec.parameters.monitoring.enabled)",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if "advanced_fields" in form.base_fields:
|
|
||||||
form.base_fields["advanced_fields"].widget = JSONFormWidget(
|
|
||||||
schema=advanced_fields_schema
|
|
||||||
)
|
)
|
||||||
return form
|
return form
|
||||||
|
|
||||||
|
|
|
||||||
32
src/servala/core/migrations/0012_remove_advanced_fields.py
Normal file
32
src/servala/core/migrations/0012_remove_advanced_fields.py
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
# Generated by Django 5.2.7 on 2025-10-31 10:40
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("core", "0011_alter_organizationorigin_billing_entity"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="servicedefinition",
|
||||||
|
name="advanced_fields",
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="organization",
|
||||||
|
name="name",
|
||||||
|
field=models.CharField(
|
||||||
|
max_length=32,
|
||||||
|
validators=[
|
||||||
|
django.core.validators.RegexValidator(
|
||||||
|
message="Organization name can only contain letters, numbers, and spaces.",
|
||||||
|
regex="^[A-Za-z0-9\\s]+$",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
verbose_name="Name",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
23
src/servala/core/migrations/0013_add_form_config.py
Normal file
23
src/servala/core/migrations/0013_add_form_config.py
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Generated by Django 5.2.7 on 2025-10-31 10:47
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("core", "0012_remove_advanced_fields"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="servicedefinition",
|
||||||
|
name="form_config",
|
||||||
|
field=models.JSONField(
|
||||||
|
blank=True,
|
||||||
|
help_text='Optional custom form configuration. When provided, this configuration will be used to render the service form instead of auto-generating it from the OpenAPI spec. Format: {"fieldsets": [{"title": "Section", "fields": [{...}]}]}',
|
||||||
|
null=True,
|
||||||
|
verbose_name="Form Configuration",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -365,7 +365,7 @@ class ServiceDefinition(ServalaModelMixin, models.Model):
|
||||||
help_text=_(
|
help_text=_(
|
||||||
"Optional custom form configuration. When provided, this configuration will be used "
|
"Optional custom form configuration. When provided, this configuration will be used "
|
||||||
"to render the service form instead of auto-generating it from the OpenAPI spec. "
|
"to render the service form instead of auto-generating it from the OpenAPI spec. "
|
||||||
"Format: {\"fieldsets\": [{\"title\": \"Section\", \"fields\": [{...}]}]}"
|
'Format: {"fieldsets": [{"title": "Section", "fields": [{...}]}]}'
|
||||||
),
|
),
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
|
|
|
||||||
|
|
@ -48,9 +48,6 @@
|
||||||
"description": "Whether the field is required",
|
"description": "Whether the field is required",
|
||||||
"default": false
|
"default": false
|
||||||
},
|
},
|
||||||
"default": {
|
|
||||||
"description": "Default value for the field"
|
|
||||||
},
|
|
||||||
"controlplane_field_mapping": {
|
"controlplane_field_mapping": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Dot-notation path mapping to Kubernetes spec field (e.g., 'spec.parameters.service.fqdn')"
|
"description": "Dot-notation path mapping to Kubernetes spec field (e.g., 'spec.parameters.service.fqdn')"
|
||||||
|
|
@ -78,12 +75,9 @@
|
||||||
"description": "Array of [value, label] pairs for choice fields",
|
"description": "Array of [value, label] pairs for choice fields",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"minItems": 2,
|
"items": {
|
||||||
"maxItems": 2,
|
"type": "string"
|
||||||
"items": [
|
}
|
||||||
{"type": "string"},
|
|
||||||
{"type": "string"}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"min_values": {
|
"min_values": {
|
||||||
|
|
@ -112,18 +106,8 @@
|
||||||
"enum": ["suggest_fqdn_from_name"]
|
"enum": ["suggest_fqdn_from_name"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"allOf": [
|
|
||||||
{
|
|
||||||
"if": {
|
|
||||||
"properties": {"type": {"const": "choice"}}
|
|
||||||
},
|
|
||||||
"then": {
|
|
||||||
"required": ["choices"]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -237,42 +237,42 @@ a.btn-keycloak {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* CRD Form mandatory field styling */
|
/* Expert CRD Form mandatory field styling */
|
||||||
.crd-form .form-group.mandatory .form-label {
|
.expert-crd-form .form-group.mandatory .form-label {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.crd-form .form-group.mandatory .form-label::after {
|
.expert-crd-form .form-group.mandatory .form-label::after {
|
||||||
content: " *";
|
content: " *";
|
||||||
color: #dc3545;
|
color: #dc3545;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.crd-form .form-group.mandatory {
|
.expert-crd-form .form-group.mandatory {
|
||||||
border-left: 3px solid #dc3545;
|
border-left: 3px solid #dc3545;
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
background-color: rgba(220, 53, 69, 0.05);
|
background-color: rgba(220, 53, 69, 0.05);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.crd-form .nav-tabs .nav-link .mandatory-indicator {
|
.expert-crd-form .nav-tabs .nav-link .mandatory-indicator {
|
||||||
color: #dc3545;
|
color: #dc3545;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
html[data-bs-theme="dark"] .crd-form .form-group.mandatory {
|
html[data-bs-theme="dark"] .expert-crd-form .form-group.mandatory {
|
||||||
background-color: rgba(220, 53, 69, 0.1);
|
background-color: rgba(220, 53, 69, 0.1);
|
||||||
border-left-color: #ff6b6b;
|
border-left-color: #ff6b6b;
|
||||||
}
|
}
|
||||||
|
|
||||||
html[data-bs-theme="dark"] .crd-form .form-group.mandatory .form-label::after {
|
html[data-bs-theme="dark"] .expert-crd-form .form-group.mandatory .form-label::after {
|
||||||
color: #ff6b6b;
|
color: #ff6b6b;
|
||||||
}
|
}
|
||||||
|
|
||||||
html[data-bs-theme="dark"] .crd-form .nav-tabs .nav-link .mandatory-indicator {
|
html[data-bs-theme="dark"] .expert-crd-form .nav-tabs .nav-link .mandatory-indicator {
|
||||||
color: #ff6b6b;
|
color: #ff6b6b;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue