diff --git a/src/servala/core/crd.py b/src/servala/core/crd.py
index 6e17949..719b79a 100644
--- a/src/servala/core/crd.py
+++ b/src/servala/core/crd.py
@@ -142,30 +142,11 @@ def get_django_field(
parent_required=is_required,
)
elif field_type == "array":
- kwargs["help_text"] = field_schema.get("description") or _("List of values")
- from servala.frontend.forms.widgets import DynamicArrayField
-
- field = models.JSONField(**kwargs)
- formfield_kwargs = {
- "label": field.verbose_name,
- "required": not field.blank,
- }
-
- array_validation = {}
- if min_items := field_schema.get("min_items"):
- array_validation["min_items"] = min_items
- if max_items := field_schema.get("max_items"):
- array_validation["max_items"] = max_items
- if unique_items := field_schema.get("unique_items"):
- array_validation["unique_items"] = unique_items
- if items_schema := field_schema.get("items"):
- array_validation["items_schema"] = items_schema
- if array_validation:
- formfield_kwargs["array_validation"] = array_validation
-
- field.formfield = lambda: DynamicArrayField(**formfield_kwargs)
-
- return field
+ # TODO: handle items / validate items, build multi-select input
+ # if field_schema.get("items") and (choices := field_schema["items"].get("enum")):
+ # choices = [c, c for c in choices]
+ kwargs["help_text"] = _("JSON field (array)")
+ return models.JSONField(**kwargs)
return models.CharField(max_length=255, **kwargs)
@@ -185,25 +166,6 @@ def unnest_data(data):
class CrdModelFormMixin:
- HIDDEN_FIELDS = [
- "spec.compositeDeletePolicy",
- "spec.compositionRef",
- "spec.compositionRevisionRef",
- "spec.compositionRevisionSelector",
- "spec.compositionSelector",
- "spec.compositionUpdatePolicy",
- "spec.parameters.network.serviceType",
- "spec.parameters.scheduling",
- "spec.parameters.security",
- "spec.parameters.size.cpu",
- "spec.parameters.size.memory",
- "spec.parameters.size.requests.cpu",
- "spec.parameters.size.requests.memory",
- "spec.publishConnectionDetailsTo",
- "spec.resourceRef",
- "spec.writeConnectionSecretToRef",
- ]
-
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.schema = self._meta.model.SCHEMA
@@ -211,12 +173,6 @@ class CrdModelFormMixin:
for field in ("organization", "context"):
self.fields[field].widget = forms.HiddenInput()
- for name, field in self.fields.items():
- if name in self.HIDDEN_FIELDS or any(
- name.startswith(f) for f in self.HIDDEN_FIELDS
- ):
- field.widget = forms.HiddenInput()
-
if self.instance and self.instance.pk:
self.fields["name"].disabled = True
self.fields["name"].help_text = _("Name cannot be changed after creation.")
@@ -224,8 +180,8 @@ class CrdModelFormMixin:
def strip_title(self, field_name, label):
field = self.fields[field_name]
- if field and field.label and (position := field.label.find(label)) != -1:
- field.label = field.label[position + len(label) :]
+ if field and field.label.startswith(label):
+ field.label = field.label[len(label) :]
def get_fieldsets(self):
fieldsets = []
@@ -249,88 +205,57 @@ class CrdModelFormMixin:
# Process spec fields
others = []
- top_level_fieldsets = {}
- hidden_spec_fields = []
+ nested_fieldsets = {}
for field_name in self.fields:
if field_name.startswith("spec."):
- if isinstance(self.fields[field_name].widget, forms.HiddenInput):
- hidden_spec_fields.append(field_name)
- continue
-
parts = field_name.split(".")
- if len(parts) == 2:
- # Top-level spec field
+ if len(parts) == 2: # Top-level spec field
others.append(field_name)
- elif len(parts) == 3:
- # Second-level field - promote to top-level fieldset
- fieldset_key = f"{parts[1]}.{parts[2]}"
- if not top_level_fieldsets.get(fieldset_key):
- top_level_fieldsets[fieldset_key] = {
- "fields": [],
- "fieldsets": [],
- "title": f"{deslugify(parts[2])}",
- }
- top_level_fieldsets[fieldset_key]["fields"].append(field_name)
else:
- # Third-level and deeper - create nested fieldsets
- fieldset_key = f"{parts[1]}.{parts[2]}"
- if not top_level_fieldsets.get(fieldset_key):
- top_level_fieldsets[fieldset_key] = {
+ parent_key = parts[1]
+ if not nested_fieldsets.get(parent_key):
+ nested_fieldsets[parent_key] = {
"fields": [],
"fieldsets": {},
- "title": f"{deslugify(parts[2])}",
+ "title": deslugify(parent_key),
}
+ parent = nested_fieldsets[parent_key]
+ if len(parts) == 3: # Top-level within fieldset
+ parent["fields"].append(field_name)
+ else:
+ sub_key = parts[2]
+ if not parent["fieldsets"].get(sub_key):
+ parent["fieldsets"][sub_key] = {
+ "title": deslugify(sub_key),
+ "fields": [],
+ }
+ parent["fieldsets"][sub_key]["fields"].append(field_name)
- sub_key = parts[3]
- if not top_level_fieldsets[fieldset_key]["fieldsets"].get(sub_key):
- top_level_fieldsets[fieldset_key]["fieldsets"][sub_key] = {
- "title": deslugify(sub_key),
- "fields": [],
- }
- top_level_fieldsets[fieldset_key]["fieldsets"][sub_key][
- "fields"
- ].append(field_name)
-
- for fieldset in top_level_fieldsets.values():
- nested_fieldsets_list = []
- for sub_fieldset in fieldset["fieldsets"].values():
- if len(sub_fieldset["fields"]) == 1:
- # If nested fieldset has only one field, move it to parent
- fieldset["fields"].append(sub_fieldset["fields"][0])
+ # Add nested fieldsets to fieldsets
+ for group in nested_fieldsets.values():
+ total_fields = 0
+ for fieldset in group["fieldsets"].values():
+ if (field_count := len(fieldset["fields"])) == 1:
+ group["fields"].append(fieldset["fields"][0])
+ fieldset["fields"] = []
else:
- # Keep as nested fieldset with proper title stripping
- title = f"{fieldset['title']}: {sub_fieldset['title']}: "
- for field in sub_fieldset["fields"]:
+ title = f"{group['title']}: {fieldset['title']}: "
+ for field in fieldset["fields"]:
self.strip_title(field, title)
- nested_fieldsets_list.append(sub_fieldset)
-
- fieldset["fieldsets"] = nested_fieldsets_list
- total_fields = len(fieldset["fields"]) + len(nested_fieldsets_list)
- if total_fields == 1 and len(fieldset["fields"]) == 1:
- others.append(fieldset["fields"][0])
+ total_fields += field_count
+ if (total_fields + len(group["fields"])) == 1:
+ others.append(group["fields"][0])
else:
- title = f"{fieldset['title']}: "
- for field in fieldset["fields"]:
+ title = f"{group['title']}: "
+ for field in group["fields"]:
self.strip_title(field, title)
- fieldsets.append(fieldset)
+ fieldsets.append(group)
# Add 'others' tab if there are any fields
if others:
fieldsets.append({"title": "Others", "fields": others, "fieldsets": []})
- if hidden_spec_fields:
- fieldsets.append(
- {
- "title": "Advanced",
- "fields": hidden_spec_fields,
- "fieldsets": [],
- "hidden": True,
- }
- )
-
- fieldsets.sort(key=lambda f: f.get("hidden", False))
-
return fieldsets
def get_nested_data(self):
diff --git a/src/servala/core/middleware.py b/src/servala/core/middleware.py
index 0cb1be7..dd8f54c 100644
--- a/src/servala/core/middleware.py
+++ b/src/servala/core/middleware.py
@@ -20,10 +20,6 @@ class OrganizationMiddleware:
organization_slug = url.kwargs.get("organization")
if organization_slug:
pk = organization_slug.rsplit("-", maxsplit=1)[-1]
- try:
- pk = int(pk)
- except ValueError:
- pk = -1
request.organization = get_object_or_404(Organization, pk=pk)
with scope(organization=request.organization):
return self.get_response(request)
diff --git a/src/servala/core/models/service.py b/src/servala/core/models/service.py
index e87a972..6eb912e 100644
--- a/src/servala/core/models/service.py
+++ b/src/servala/core/models/service.py
@@ -587,15 +587,6 @@ class ServiceInstance(ServalaModelMixin, models.Model):
)
try:
- spec_data = spec_data or {}
- if "writeConnectionSecretToRef" not in spec_data:
- spec_data["writeConnectionSecretToRef"] = {}
-
- if not spec_data["writeConnectionSecretToRef"].get("name"):
- service_slug = context.service_offering.service.slug
- secret_name = f"{organization.slug}-{instance.pk}-{service_slug}"
- spec_data["writeConnectionSecretToRef"]["name"] = secret_name
-
create_data = {
"apiVersion": f"{context.group}/{context.version}",
"kind": context.kind,
@@ -603,7 +594,7 @@ class ServiceInstance(ServalaModelMixin, models.Model):
"name": name,
"namespace": organization.namespace,
},
- "spec": spec_data,
+ "spec": spec_data or {},
}
if label := context.control_plane.required_label:
create_data["metadata"]["labels"] = {settings.DEFAULT_LABEL_KEY: label}
diff --git a/src/servala/core/rules.py b/src/servala/core/rules.py
index 5ead2c3..2d51145 100644
--- a/src/servala/core/rules.py
+++ b/src/servala/core/rules.py
@@ -2,10 +2,6 @@ import rules
def has_organization_role(user, org, roles):
- from servala.core.models import Organization
-
- if not isinstance(org, Organization):
- return False
memberships = org.memberships.all().filter(user=user)
if roles:
memberships = memberships.filter(role__in=roles)
diff --git a/src/servala/frontend/forms/widgets.py b/src/servala/frontend/forms/widgets.py
deleted file mode 100644
index 16e19bf..0000000
--- a/src/servala/frontend/forms/widgets.py
+++ /dev/null
@@ -1,191 +0,0 @@
-import json
-
-from django import forms
-from django.core.exceptions import ValidationError
-
-
-class DynamicArrayWidget(forms.Widget):
- """
- Widget for dynamic array input fields with add/remove functionality.
- Renders as a list of input fields with + and - buttons.
- """
-
- template_name = "frontend/forms/dynamic_array.html"
-
- def __init__(self, attrs=None, base_widget=None):
- super().__init__(attrs)
- self.base_widget = base_widget or forms.TextInput()
-
- def format_value(self, value):
- if value is None or value == "null":
- return []
- if isinstance(value, str):
- if value.strip() == "" or value.strip().lower() == "null":
- return []
- try:
- parsed = json.loads(value)
- if parsed is None:
- return []
- if isinstance(parsed, list):
- return [item for item in parsed if item is not None]
- return [parsed]
- except (json.JSONDecodeError, TypeError):
- return [value] if value else []
- if isinstance(value, list):
- return [item for item in value if item is not None]
- if value == "":
- return []
- return [str(value)]
-
- def get_context(self, name, value, attrs):
- context = super().get_context(name, value, attrs)
- value_list = self.format_value(value)
- if not isinstance(value_list, list):
- value_list = []
- context["value_list"] = value_list
- context["value_list_json"] = json.dumps(value_list)
- return context
-
- def value_from_datadict(self, data, files, name):
- value = data.get(name)
- if value:
- try:
- parsed = json.loads(value)
- if parsed:
- return [
- item
- for item in parsed
- if item is not None and str(item).strip()
- ]
- return []
- except (json.JSONDecodeError, TypeError):
- pass
- return []
-
-
-class DynamicArrayField(forms.JSONField):
- """
- Custom field that works with DynamicArrayWidget.
- Handles the conversion between list data and JSON for k8s generated fields.
- """
-
- widget = DynamicArrayWidget
-
- def __init__(self, **kwargs):
- self.array_validation = kwargs.pop("array_validation", {})
- kwargs.setdefault("widget", DynamicArrayWidget())
- super().__init__(**kwargs)
-
- def to_python(self, value):
- """Convert the value to a Python list"""
- if not value:
- return []
-
- if isinstance(value, list):
- return [item for item in value if item is not None and str(item).strip()]
-
- if isinstance(value, str):
- if value.strip() == "" or value.strip().lower() == "null":
- return []
- try:
- parsed = json.loads(value)
- if not parsed:
- return []
- if isinstance(parsed, list):
- return [item for item in parsed if item is not None]
- return [parsed]
- except (json.JSONDecodeError, TypeError, ValueError):
- raise ValidationError(f"Invalid JSON: {value}")
-
- return []
-
- def prepare_value(self, value):
- """Prepare value for widget rendering"""
- if value is None:
- return []
- if isinstance(value, list):
- return value
- if isinstance(value, str):
- try:
- parsed = json.loads(value)
- return parsed if isinstance(parsed, list) else [parsed]
- except (json.JSONDecodeError, TypeError):
- return [value] if value else []
- return [str(value)]
-
- def bound_data(self, data, initial):
- """Handle bound data properly to avoid JSON parsing issues"""
- if data is None:
- return initial
- # If data is already a list (from our widget), return it as-is
- if isinstance(data, list):
- return data
- # If data is a string, try to parse it as JSON
- if isinstance(data, str):
- try:
- parsed = json.loads(data)
- return parsed if isinstance(parsed, list) else [parsed]
- except (json.JSONDecodeError, TypeError):
- return [data] if data else []
- return data
-
- def validate(self, value):
- if not self.required and (not value or value == []):
- return
- super().validate(value)
- if value:
- self._validate_array_constraints(value)
-
- def _validate_array_constraints(self, value):
- if not isinstance(value, list):
- return
-
- if min_items := self.array_validation.get("min_items"):
- if len(value) < min_items:
- raise ValidationError(
- f"Array must contain at least {min_items} item(s). Currently has {len(value)}."
- )
-
- if max_items := self.array_validation.get("max_items"):
- if len(value) > max_items:
- raise ValidationError(
- f"Array must contain at most {max_items} item(s). Currently has {len(value)}."
- )
-
- if self.array_validation.get("unique_items"):
- if len(value) != len(set(value)):
- raise ValidationError("All array items must be unique.")
-
- if items_schema := self.array_validation.get("items_schema"):
- item_type = items_schema.get("type")
-
- if item_type == "string":
- for i, item in enumerate(value):
- if not isinstance(item, str):
- raise ValidationError(f"Item {i + 1} must be a string.")
-
- if min_length := items_schema.get("minLength"):
- if len(item) < min_length:
- raise ValidationError(
- f"Item {i + 1} must be at least {min_length} characters long."
- )
-
- if max_length := items_schema.get("maxLength"):
- if len(item) > max_length:
- raise ValidationError(
- f"Item {i + 1} must be at most {max_length} characters long."
- )
-
- if pattern := items_schema.get("pattern"):
- import re
-
- if not re.match(pattern, item):
- raise ValidationError(
- f"Item {i + 1} does not match the required pattern."
- )
-
- if enum_values := items_schema.get("enum"):
- if item not in enum_values:
- raise ValidationError(
- f"Item {i + 1} must be one of: {', '.join(enum_values)}"
- )
diff --git a/src/servala/frontend/templates/frontend/base.html b/src/servala/frontend/templates/frontend/base.html
index 28ecee9..89d363f 100644
--- a/src/servala/frontend/templates/frontend/base.html
+++ b/src/servala/frontend/templates/frontend/base.html
@@ -74,6 +74,5 @@
-