diff --git a/src/servala/core/crd.py b/src/servala/core/crd.py index f356c7b..173b26c 100644 --- a/src/servala/core/crd.py +++ b/src/servala/core/crd.py @@ -173,11 +173,6 @@ class CrdModelFormMixin: for field in ("organization", "context"): self.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.") - self.fields["name"].widget = forms.HiddenInput() - def strip_title(self, field_name, label): field = self.fields[field_name] if field and field.label.startswith(label): @@ -188,10 +183,7 @@ class CrdModelFormMixin: # General fieldset for non-spec fields general_fields = [ - field_name - for field_name, field in self.fields.items() - if not field_name.startswith("spec.") - and not isinstance(field.widget, forms.HiddenInput) + field for field in self.fields if not field.startswith("spec.") ] if general_fields: fieldsets.append( diff --git a/src/servala/core/models/service.py b/src/servala/core/models/service.py index 901a822..c38cac0 100644 --- a/src/servala/core/models/service.py +++ b/src/servala/core/models/service.py @@ -188,10 +188,6 @@ class ControlPlane(ServalaModelMixin, models.Model): def get_kubernetes_client(self): return kubernetes.client.ApiClient(self.kubernetes_config) - @cached_property - def custom_objects_api(self): - return client.CustomObjectsApi(self.get_kubernetes_client()) - def test_connection(self): if not self.api_credentials: return False, _("No API credentials provided") @@ -513,19 +509,6 @@ class ServiceInstance(ServalaModelMixin, models.Model): class urls(urlman.Urls): base = "{self.organization.urls.instances}{self.name}/" - update = "{base}update/" - - def _clear_kubernetes_caches(self): - """Clears cached properties that depend on Kubernetes state.""" - attrs = self.__dict__.keys() - for key in ( - "kubernetes_object", - "spec", - "status_conditions", - "connection_credentials", - ): - if key in attrs: - delattr(self, key) @classmethod def create_instance(cls, name, organization, context, created_by, spec_data): @@ -557,7 +540,9 @@ class ServiceInstance(ServalaModelMixin, models.Model): } if label := context.control_plane.required_label: create_data["metadata"]["labels"] = {settings.DEFAULT_LABEL_KEY: label} - api_instance = context.control_plane.custom_objects_api + api_instance = client.CustomObjectsApi( + context.control_plane.get_kubernetes_client() + ) api_instance.create_namespaced_custom_object( group=context.group, version=context.version, @@ -577,47 +562,6 @@ class ServiceInstance(ServalaModelMixin, models.Model): raise ValidationError(_("Error creating instance: {}").format(str(e))) return instance - def update_spec(self, spec_data, updated_by): - try: - api_instance = self.context.control_plane.custom_objects_api - patch_body = {"spec": spec_data} - - api_instance.patch_namespaced_custom_object( - group=self.context.group, - version=self.context.version, - namespace=self.organization.namespace, - plural=self.context.kind_plural, - name=self.name, - body=patch_body, - ) - self._clear_kubernetes_caches() - self.save() # Updates updated_at timestamp - except ApiException as e: - if e.status == 404: - raise ValidationError( - _( - "Service instance not found in Kubernetes. It may have been deleted externally." - ) - ) - try: - error_body = json.loads(e.body) - reason = error_body.get("message", str(e)) - raise ValidationError( - _("Kubernetes API error updating instance: {error}").format( - error=reason - ) - ) - except (ValueError, TypeError): - raise ValidationError( - _("Kubernetes API error updating instance: {error}").format( - error=str(e) - ) - ) - except Exception as e: - raise ValidationError( - _("Error updating instance: {error}").format(error=str(e)) - ) - @cached_property def kubernetes_object(self): """Fetch the Kubernetes custom resource object""" @@ -651,20 +595,6 @@ class ServiceInstance(ServalaModelMixin, models.Model): spec.pop("writeConnectionSecretToRef", None) return spec - @cached_property - def spec_object(self): - """Dynamically generated CRD object.""" - return self.context.django_model( - name=self.name, - organization=self.organization, - context=self.context, - spec=self.spec, - # We pass -1 as ID in order to make it clear that a) this object exists (remotely), - # and b) it’s not a normal database object. This allows us to treat e.g. update - # forms differently from create forms. - pk=-1, - ) - @cached_property def status_conditions(self): if not self.kubernetes_object: diff --git a/src/servala/frontend/templates/frontend/base.html b/src/servala/frontend/templates/frontend/base.html index 89d363f..f5ce6dd 100644 --- a/src/servala/frontend/templates/frontend/base.html +++ b/src/servala/frontend/templates/frontend/base.html @@ -25,13 +25,9 @@

- - {% block page_title %} - Dashboard - {% endblock page_title %} - - {% block page_title_extra %} - {% endblock page_title_extra %} + {% block page_title %} + Dashboard + {% endblock page_title %}

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 973d2f1..b9cf4dc 100644 --- a/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html +++ b/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html @@ -5,11 +5,6 @@ {{ instance.name }} {% endblock page_title %} {% endblock html_title %} -{% block page_title_extra %} - {% if has_change_permission %} - {% translate "Edit" %} - {% endif %} -{% endblock page_title_extra %} {% block content %}
diff --git a/src/servala/frontend/templates/frontend/organizations/service_instance_update.html b/src/servala/frontend/templates/frontend/organizations/service_instance_update.html deleted file mode 100644 index fa7f502..0000000 --- a/src/servala/frontend/templates/frontend/organizations/service_instance_update.html +++ /dev/null @@ -1,42 +0,0 @@ -{% extends "frontend/base.html" %} -{% load i18n %} -{% load static %} -{% load partials %} -{% block html_title %} - {% block page_title %} - {% block title %} - {% blocktranslate with instance_name=instance.name organization_name=request.organization.name %}Update {{ instance_name }} in {{ organization_name }}{% endblocktranslate %} - {% endblock %} - {% endblock page_title %} -{% endblock html_title %} -{% block page_title_extra %} - {% translate "Back" %} -{% endblock page_title_extra %} -{% partialdef service-form %} -{% if form %} -
-
-
- {% if form_error %} -
- {% translate "Oops! Something went wrong with the service form generation. Please try again later." %} -
- {% else %} - {% include "includes/tabbed_fieldset_form.html" with form=form %} - {% endif %} -
-
-{% endif %} -{% endpartialdef %} -{% block content %} -
-
- {% if not form %} - - {% else %} -
{% partial service-form %}
- {% endif %} -
- {% endblock %} diff --git a/src/servala/frontend/urls.py b/src/servala/frontend/urls.py index 64c84bf..2838dae 100644 --- a/src/servala/frontend/urls.py +++ b/src/servala/frontend/urls.py @@ -50,11 +50,6 @@ urlpatterns = [ views.ServiceInstanceDetailView.as_view(), name="organization.instance", ), - path( - "instances//update/", - views.ServiceInstanceUpdateView.as_view(), - name="organization.instance.update", - ), ] ), ), diff --git a/src/servala/frontend/views/__init__.py b/src/servala/frontend/views/__init__.py index d13d4d5..5de38ee 100644 --- a/src/servala/frontend/views/__init__.py +++ b/src/servala/frontend/views/__init__.py @@ -9,7 +9,6 @@ from .service import ( ServiceDetailView, ServiceInstanceDetailView, ServiceInstanceListView, - ServiceInstanceUpdateView, ServiceListView, ServiceOfferingDetailView, ) @@ -23,7 +22,6 @@ __all__ = [ "ServiceDetailView", "ServiceInstanceDetailView", "ServiceInstanceListView", - "ServiceInstanceUpdateView", "ServiceListView", "ServiceOfferingDetailView", "ProfileView", diff --git a/src/servala/frontend/views/service.py b/src/servala/frontend/views/service.py index 298d5bc..b6ecc42 100644 --- a/src/servala/frontend/views/service.py +++ b/src/servala/frontend/views/service.py @@ -2,7 +2,6 @@ from django.contrib import messages from django.core.exceptions import ValidationError from django.shortcuts import redirect from django.utils.functional import cached_property -from django.utils.translation import gettext_lazy as _ from django.views.generic import DetailView, ListView from servala.core.crd import deslugify @@ -17,11 +16,7 @@ from servala.frontend.forms.service import ( ServiceFilterForm, ServiceInstanceFilterForm, ) -from servala.frontend.views.mixins import ( - HtmxUpdateView, - HtmxViewMixin, - OrganizationViewMixin, -) +from servala.frontend.views.mixins import HtmxViewMixin, OrganizationViewMixin class ServiceListView(OrganizationViewMixin, ListView): @@ -110,8 +105,6 @@ class ServiceOfferingDetailView(OrganizationViewMixin, HtmxViewMixin, DetailView ).first() def get_instance_form(self): - if not self.context_object: - return None return self.context_object.model_form_class( data=self.request.POST if self.request.method == "POST" else None, initial={ @@ -137,10 +130,6 @@ class ServiceOfferingDetailView(OrganizationViewMixin, HtmxViewMixin, DetailView return self.render_to_response(context) form = self.get_instance_form() - if not form: # Should not happen if context_object is valid, but as a safeguard - messages.error(self.request, _("Could not initialize service form.")) - return self.render_to_response(context) - if form.is_valid(): try: service_instance = ServiceInstance.create_instance( @@ -154,23 +143,21 @@ class ServiceOfferingDetailView(OrganizationViewMixin, HtmxViewMixin, DetailView except ValidationError as e: messages.error(self.request, e.message or str(e)) except Exception as e: - messages.error( - self.request, _("Error creating instance: {}").format(str(e)) - ) + messages.error(self.request, str(e)) # If the form is not valid or if the service creation failed, we render it again context["service_form"] = form return self.render_to_response(context) -class ServiceInstanceMixin: - model = ServiceInstance - context_object_name = "instance" - slug_field = "name" +class ServiceInstanceDetailView(OrganizationViewMixin, DetailView): + """View to display details of a specific service instance.""" - def dispatch(self, *args, **kwargs): - self._has_warned = False - return super().dispatch(*args, **kwargs) + template_name = "frontend/organizations/service_instance_detail.html" + context_object_name = "instance" + model = ServiceInstance + permission_type = "view" + slug_field = "name" def get_queryset(self): """Return service instance for the current organization.""" @@ -182,39 +169,6 @@ class ServiceInstanceMixin: "context__service_definition__service", ) - def get_object(self, **kwargs): - instance = super().get_object(**kwargs) - if ( - not instance.kubernetes_object - and not instance.is_deleted - and not self._has_warned - ): - messages.warning( - self.request, - _( - "Could not retrieve instance details from Kubernetes. It might have been deleted externally." - ), - ) - self._has_warned = True - return instance - - -class ServiceInstanceDetailView( - ServiceInstanceMixin, OrganizationViewMixin, DetailView -): - template_name = "frontend/organizations/service_instance_detail.html" - permission_type = "view" - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - if self.object.kubernetes_object and self.object.spec: - context["spec_fieldsets"] = self.get_nested_spec() - permission_required = ServiceInstance.get_perm("change") - context["has_change_permission"] = self.request.user.has_perm( - permission_required, self.object - ) - return context - def get_nested_spec(self): """ Organize spec data into fieldsets similar to how the form does it. @@ -318,43 +272,12 @@ class ServiceInstanceDetailView( return fieldsets - -class ServiceInstanceUpdateView( - ServiceInstanceMixin, OrganizationViewMixin, HtmxUpdateView -): - template_name = "frontend/organizations/service_instance_update.html" - permission_type = "change" - - def get_form_class(self): - return self.object.context.model_form_class - - def get_form_kwargs(self): - kwargs = super().get_form_kwargs() - kwargs["instance"] = self.object.spec_object - return kwargs - - def form_valid(self, form): - try: - spec_data = form.get_nested_data().get("spec") - self.object.update_spec(spec_data=spec_data, updated_by=self.request.user) - messages.success( - self.request, - _("Service instance '{name}' updated successfully.").format( - name=self.object.name - ), - ) - return redirect(self.object.urls.base) - except ValidationError as e: - messages.error(self.request, e.message or str(e)) - return self.form_invalid(form) - except Exception as e: - messages.error( - self.request, _("Error updating instance: {error}").format(error=str(e)) - ) - return self.form_invalid(form) - - def get_success_url(self): - return self.object.urls.base + def get_context_data(self, **kwargs): + """Return service instance for the current organization.""" + context = super().get_context_data(**kwargs) + if self.object.spec: + context["spec_fieldsets"] = self.get_nested_spec() + return context class ServiceInstanceListView(OrganizationViewMixin, ListView): @@ -370,12 +293,7 @@ class ServiceInstanceListView(OrganizationViewMixin, ListView): def get_queryset(self): """Return all service instances for the current organization with filtering.""" queryset = ServiceInstance.objects.filter( - organization=self.request.organization, - is_deleted=False, # Exclude soft-deleted - ).select_related( - "context__service_offering__provider", - "context__control_plane", - "context__service_definition__service", + organization=self.request.organization ) if self.filter_form.is_valid(): queryset = self.filter_form.filter_queryset(queryset) diff --git a/src/servala/static/css/servala.css b/src/servala/static/css/servala.css index e607205..e2eb044 100644 --- a/src/servala/static/css/servala.css +++ b/src/servala/static/css/servala.css @@ -85,8 +85,3 @@ html[data-bs-theme="dark"] .btn-outline-primary, .btn-outline-primary { a.btn-keycloak { display: inline-flex; } -.page-heading h3 { - display: flex; - flex-wrap: wrap; - justify-content: space-between; -}