Service Instance Update #61
9 changed files with 274 additions and 23 deletions
|
@ -9,6 +9,18 @@ from django.utils.translation import gettext_lazy as _
|
||||||
from servala.core.models import ServiceInstance
|
from servala.core.models import ServiceInstance
|
||||||
|
|
||||||
|
|
||||||
|
class CRDModel(models.Model):
|
||||||
|
"""Base class for all virtual CRD models"""
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
if spec := kwargs.pop("spec", None):
|
||||||
|
kwargs.update(unnest_data({"spec": spec}))
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
def duplicate_field(field_name, model):
|
def duplicate_field(field_name, model):
|
||||||
# Get the field from the model
|
# Get the field from the model
|
||||||
field = model._meta.get_field(field_name)
|
field = model._meta.get_field(field_name)
|
||||||
|
@ -47,7 +59,7 @@ def generate_django_model(schema, group, version, kind):
|
||||||
|
|
||||||
# create the model class
|
# create the model class
|
||||||
model_name = kind
|
model_name = kind
|
||||||
model_class = type(model_name, (models.Model,), model_fields)
|
model_class = type(model_name, (CRDModel,), model_fields)
|
||||||
return model_class
|
return model_class
|
||||||
|
|
||||||
|
|
||||||
|
@ -138,6 +150,21 @@ def get_django_field(
|
||||||
return models.CharField(max_length=255, **kwargs)
|
return models.CharField(max_length=255, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def unnest_data(data):
|
||||||
|
result = {}
|
||||||
|
|
||||||
|
def _flatten_dict(d, parent_key=""):
|
||||||
|
for key, value in d.items():
|
||||||
|
new_key = f"{parent_key}.{key}" if parent_key else key
|
||||||
|
if isinstance(value, dict):
|
||||||
|
_flatten_dict(value, new_key)
|
||||||
|
else:
|
||||||
|
result[new_key] = value
|
||||||
|
|
||||||
|
_flatten_dict(data)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
class CrdModelFormMixin:
|
class CrdModelFormMixin:
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
@ -146,6 +173,11 @@ class CrdModelFormMixin:
|
||||||
for field in ("organization", "context"):
|
for field in ("organization", "context"):
|
||||||
self.fields[field].widget = forms.HiddenInput()
|
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):
|
def strip_title(self, field_name, label):
|
||||||
field = self.fields[field_name]
|
field = self.fields[field_name]
|
||||||
if field and field.label.startswith(label):
|
if field and field.label.startswith(label):
|
||||||
|
@ -156,7 +188,10 @@ class CrdModelFormMixin:
|
||||||
|
|
||||||
# General fieldset for non-spec fields
|
# General fieldset for non-spec fields
|
||||||
general_fields = [
|
general_fields = [
|
||||||
field for field in self.fields if not field.startswith("spec.")
|
field_name
|
||||||
|
for field_name, field in self.fields.items()
|
||||||
|
if not field_name.startswith("spec.")
|
||||||
|
and not isinstance(field.widget, forms.HiddenInput)
|
||||||
]
|
]
|
||||||
if general_fields:
|
if general_fields:
|
||||||
fieldsets.append(
|
fieldsets.append(
|
||||||
|
|
|
@ -188,6 +188,10 @@ class ControlPlane(ServalaModelMixin, models.Model):
|
||||||
def get_kubernetes_client(self):
|
def get_kubernetes_client(self):
|
||||||
return kubernetes.client.ApiClient(self.kubernetes_config)
|
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):
|
def test_connection(self):
|
||||||
if not self.api_credentials:
|
if not self.api_credentials:
|
||||||
return False, _("No API credentials provided")
|
return False, _("No API credentials provided")
|
||||||
|
@ -509,6 +513,19 @@ class ServiceInstance(ServalaModelMixin, models.Model):
|
||||||
|
|
||||||
class urls(urlman.Urls):
|
class urls(urlman.Urls):
|
||||||
base = "{self.organization.urls.instances}{self.name}/"
|
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
|
@classmethod
|
||||||
def create_instance(cls, name, organization, context, created_by, spec_data):
|
def create_instance(cls, name, organization, context, created_by, spec_data):
|
||||||
|
@ -540,9 +557,7 @@ class ServiceInstance(ServalaModelMixin, models.Model):
|
||||||
}
|
}
|
||||||
if label := context.control_plane.required_label:
|
if label := context.control_plane.required_label:
|
||||||
create_data["metadata"]["labels"] = {settings.DEFAULT_LABEL_KEY: label}
|
create_data["metadata"]["labels"] = {settings.DEFAULT_LABEL_KEY: label}
|
||||||
api_instance = client.CustomObjectsApi(
|
api_instance = context.control_plane.custom_objects_api
|
||||||
context.control_plane.get_kubernetes_client()
|
|
||||||
)
|
|
||||||
api_instance.create_namespaced_custom_object(
|
api_instance.create_namespaced_custom_object(
|
||||||
group=context.group,
|
group=context.group,
|
||||||
version=context.version,
|
version=context.version,
|
||||||
|
@ -562,6 +577,47 @@ class ServiceInstance(ServalaModelMixin, models.Model):
|
||||||
raise ValidationError(_("Error creating instance: {}").format(str(e)))
|
raise ValidationError(_("Error creating instance: {}").format(str(e)))
|
||||||
return instance
|
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
|
@cached_property
|
||||||
def kubernetes_object(self):
|
def kubernetes_object(self):
|
||||||
"""Fetch the Kubernetes custom resource object"""
|
"""Fetch the Kubernetes custom resource object"""
|
||||||
|
@ -595,6 +651,20 @@ class ServiceInstance(ServalaModelMixin, models.Model):
|
||||||
spec.pop("writeConnectionSecretToRef", None)
|
spec.pop("writeConnectionSecretToRef", None)
|
||||||
return spec
|
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
|
@cached_property
|
||||||
def status_conditions(self):
|
def status_conditions(self):
|
||||||
if not self.kubernetes_object:
|
if not self.kubernetes_object:
|
||||||
|
|
|
@ -25,9 +25,13 @@
|
||||||
<div class="content-wrapper container">
|
<div class="content-wrapper container">
|
||||||
<div class="page-heading">
|
<div class="page-heading">
|
||||||
<h3>
|
<h3>
|
||||||
{% block page_title %}
|
<span>
|
||||||
Dashboard
|
{% block page_title %}
|
||||||
{% endblock page_title %}
|
Dashboard
|
||||||
|
{% endblock page_title %}
|
||||||
|
</span>
|
||||||
|
{% block page_title_extra %}
|
||||||
|
{% endblock page_title_extra %}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="page-content">
|
<div class="page-content">
|
||||||
|
|
|
@ -5,6 +5,11 @@
|
||||||
{{ instance.name }}
|
{{ instance.name }}
|
||||||
{% endblock page_title %}
|
{% endblock page_title %}
|
||||||
{% endblock html_title %}
|
{% endblock html_title %}
|
||||||
|
{% block page_title_extra %}
|
||||||
|
{% if has_change_permission %}
|
||||||
|
<a href="{{ instance.urls.update }}" class="btn btn-primary me-1 mb-1">{% translate "Edit" %}</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock page_title_extra %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
{% 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 %}
|
||||||
|
<a href="{{ instance.urls.base }}" class="btn btn-secondary me-1 mb-1">{% translate "Back" %}</a>
|
||||||
|
{% endblock page_title_extra %}
|
||||||
|
{% partialdef service-form %}
|
||||||
|
{% if form %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header d-flex align-items-center"></div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if form_error %}
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
{% translate "Oops! Something went wrong with the service form generation. Please try again later." %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
{% include "includes/tabbed_fieldset_form.html" with form=form %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endpartialdef %}
|
||||||
|
{% block content %}
|
||||||
|
<section class="section">
|
||||||
|
<div class="card">
|
||||||
|
{% if not form %}
|
||||||
|
<div class="alert alert-warning" role="alert">
|
||||||
|
{% translate "Cannot update this service instance because its details could not be retrieved from the underlying system. It might have been deleted externally." %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div id="service-form">{% partial service-form %}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endblock content %}
|
|
@ -50,6 +50,11 @@ urlpatterns = [
|
||||||
views.ServiceInstanceDetailView.as_view(),
|
views.ServiceInstanceDetailView.as_view(),
|
||||||
name="organization.instance",
|
name="organization.instance",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"instances/<slug:slug>/update/",
|
||||||
|
views.ServiceInstanceUpdateView.as_view(),
|
||||||
|
name="organization.instance.update",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -9,6 +9,7 @@ from .service import (
|
||||||
ServiceDetailView,
|
ServiceDetailView,
|
||||||
ServiceInstanceDetailView,
|
ServiceInstanceDetailView,
|
||||||
ServiceInstanceListView,
|
ServiceInstanceListView,
|
||||||
|
ServiceInstanceUpdateView,
|
||||||
ServiceListView,
|
ServiceListView,
|
||||||
ServiceOfferingDetailView,
|
ServiceOfferingDetailView,
|
||||||
)
|
)
|
||||||
|
@ -22,6 +23,7 @@ __all__ = [
|
||||||
"ServiceDetailView",
|
"ServiceDetailView",
|
||||||
"ServiceInstanceDetailView",
|
"ServiceInstanceDetailView",
|
||||||
"ServiceInstanceListView",
|
"ServiceInstanceListView",
|
||||||
|
"ServiceInstanceUpdateView",
|
||||||
"ServiceListView",
|
"ServiceListView",
|
||||||
"ServiceOfferingDetailView",
|
"ServiceOfferingDetailView",
|
||||||
"ProfileView",
|
"ProfileView",
|
||||||
|
|
|
@ -2,6 +2,7 @@ from django.contrib import messages
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.generic import DetailView, ListView
|
from django.views.generic import DetailView, ListView
|
||||||
|
|
||||||
from servala.core.crd import deslugify
|
from servala.core.crd import deslugify
|
||||||
|
@ -16,7 +17,11 @@ from servala.frontend.forms.service import (
|
||||||
ServiceFilterForm,
|
ServiceFilterForm,
|
||||||
ServiceInstanceFilterForm,
|
ServiceInstanceFilterForm,
|
||||||
)
|
)
|
||||||
from servala.frontend.views.mixins import HtmxViewMixin, OrganizationViewMixin
|
from servala.frontend.views.mixins import (
|
||||||
|
HtmxUpdateView,
|
||||||
|
HtmxViewMixin,
|
||||||
|
OrganizationViewMixin,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ServiceListView(OrganizationViewMixin, ListView):
|
class ServiceListView(OrganizationViewMixin, ListView):
|
||||||
|
@ -105,6 +110,8 @@ class ServiceOfferingDetailView(OrganizationViewMixin, HtmxViewMixin, DetailView
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
def get_instance_form(self):
|
def get_instance_form(self):
|
||||||
|
if not self.context_object:
|
||||||
|
return None
|
||||||
return self.context_object.model_form_class(
|
return self.context_object.model_form_class(
|
||||||
data=self.request.POST if self.request.method == "POST" else None,
|
data=self.request.POST if self.request.method == "POST" else None,
|
||||||
initial={
|
initial={
|
||||||
|
@ -130,6 +137,10 @@ class ServiceOfferingDetailView(OrganizationViewMixin, HtmxViewMixin, DetailView
|
||||||
return self.render_to_response(context)
|
return self.render_to_response(context)
|
||||||
|
|
||||||
form = self.get_instance_form()
|
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():
|
if form.is_valid():
|
||||||
try:
|
try:
|
||||||
service_instance = ServiceInstance.create_instance(
|
service_instance = ServiceInstance.create_instance(
|
||||||
|
@ -143,22 +154,24 @@ class ServiceOfferingDetailView(OrganizationViewMixin, HtmxViewMixin, DetailView
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
messages.error(self.request, e.message or str(e))
|
messages.error(self.request, e.message or str(e))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
messages.error(self.request, str(e))
|
messages.error(
|
||||||
|
self.request, _("Error creating instance: {}").format(str(e))
|
||||||
|
)
|
||||||
|
|
||||||
# If the form is not valid or if the service creation failed, we render it again
|
# If the form is not valid or if the service creation failed, we render it again
|
||||||
context["service_form"] = form
|
context["service_form"] = form
|
||||||
return self.render_to_response(context)
|
return self.render_to_response(context)
|
||||||
|
|
||||||
|
|
||||||
class ServiceInstanceDetailView(OrganizationViewMixin, DetailView):
|
class ServiceInstanceMixin:
|
||||||
"""View to display details of a specific service instance."""
|
|
||||||
|
|
||||||
template_name = "frontend/organizations/service_instance_detail.html"
|
|
||||||
context_object_name = "instance"
|
|
||||||
model = ServiceInstance
|
model = ServiceInstance
|
||||||
permission_type = "view"
|
context_object_name = "instance"
|
||||||
slug_field = "name"
|
slug_field = "name"
|
||||||
|
|
||||||
|
def dispatch(self, *args, **kwargs):
|
||||||
|
self._has_warned = False
|
||||||
|
return super().dispatch(*args, **kwargs)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
"""Return service instance for the current organization."""
|
"""Return service instance for the current organization."""
|
||||||
return ServiceInstance.objects.filter(
|
return ServiceInstance.objects.filter(
|
||||||
|
@ -169,6 +182,39 @@ class ServiceInstanceDetailView(OrganizationViewMixin, DetailView):
|
||||||
"context__service_definition__service",
|
"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):
|
def get_nested_spec(self):
|
||||||
"""
|
"""
|
||||||
Organize spec data into fieldsets similar to how the form does it.
|
Organize spec data into fieldsets similar to how the form does it.
|
||||||
|
@ -272,12 +318,43 @@ class ServiceInstanceDetailView(OrganizationViewMixin, DetailView):
|
||||||
|
|
||||||
return fieldsets
|
return fieldsets
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
"""Return service instance for the current organization."""
|
class ServiceInstanceUpdateView(
|
||||||
context = super().get_context_data(**kwargs)
|
ServiceInstanceMixin, OrganizationViewMixin, HtmxUpdateView
|
||||||
if self.object.spec:
|
):
|
||||||
context["spec_fieldsets"] = self.get_nested_spec()
|
template_name = "frontend/organizations/service_instance_update.html"
|
||||||
return context
|
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
|
||||||
|
|
||||||
|
|
||||||
class ServiceInstanceListView(OrganizationViewMixin, ListView):
|
class ServiceInstanceListView(OrganizationViewMixin, ListView):
|
||||||
|
@ -293,7 +370,12 @@ class ServiceInstanceListView(OrganizationViewMixin, ListView):
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
"""Return all service instances for the current organization with filtering."""
|
"""Return all service instances for the current organization with filtering."""
|
||||||
queryset = ServiceInstance.objects.filter(
|
queryset = ServiceInstance.objects.filter(
|
||||||
organization=self.request.organization
|
organization=self.request.organization,
|
||||||
|
is_deleted=False, # Exclude soft-deleted
|
||||||
|
).select_related(
|
||||||
|
"context__service_offering__provider",
|
||||||
|
"context__control_plane",
|
||||||
|
"context__service_definition__service",
|
||||||
)
|
)
|
||||||
if self.filter_form.is_valid():
|
if self.filter_form.is_valid():
|
||||||
queryset = self.filter_form.filter_queryset(queryset)
|
queryset = self.filter_form.filter_queryset(queryset)
|
||||||
|
|
|
@ -85,3 +85,8 @@ html[data-bs-theme="dark"] .btn-outline-primary, .btn-outline-primary {
|
||||||
a.btn-keycloak {
|
a.btn-keycloak {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
}
|
}
|
||||||
|
.page-heading h3 {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue