diff --git a/src/servala/core/admin.py b/src/servala/core/admin.py index 9310bd4..665c291 100644 --- a/src/servala/core/admin.py +++ b/src/servala/core/admin.py @@ -14,7 +14,6 @@ from servala.core.models import ( ServiceCategory, ServiceDefinition, ServiceOffering, - ServiceOfferingControlPlane, User, ) @@ -177,7 +176,7 @@ class PlanAdmin(admin.ModelAdmin): class ServiceDefinitionAdmin(admin.ModelAdmin): form = ServiceDefinitionAdminForm list_display = ("name", "service") - list_filter = ("service",) + list_filter = ("service", "control_planes") search_fields = ("name", "description") autocomplete_fields = ("service",) @@ -200,27 +199,11 @@ class ServiceDefinitionAdmin(admin.ModelAdmin): return ["api_definition"] -class ServiceOfferingControlPlaneInline(admin.TabularInline): - model = ServiceOfferingControlPlane - extra = 1 - autocomplete_fields = ("control_plane", "service_definition") - - -@admin.register(ServiceOfferingControlPlane) -class ServiceOfferingControlPlaneAdmin(admin.ModelAdmin): - list_display = ("service_offering", "control_plane", "service_definition") - list_filter = ("service_offering", "control_plane", "service_definition") - search_fields = ("service_offering__service__name", "control_plane__name") - autocomplete_fields = ("service_offering", "control_plane", "service_definition") - - @admin.register(ServiceOffering) class ServiceOfferingAdmin(admin.ModelAdmin): list_display = ("id", "service", "provider") - list_filter = ("service", "provider") + list_filter = ("service", "provider", "control_planes") search_fields = ("description",) autocomplete_fields = ("service", "provider") - inlines = ( - ServiceOfferingControlPlaneInline, - PlanInline, - ) + filter_horizontal = ("control_planes",) + inlines = (PlanInline,) diff --git a/src/servala/core/migrations/0007_service_definition.py b/src/servala/core/migrations/0007_service_definition.py deleted file mode 100644 index be0fac6..0000000 --- a/src/servala/core/migrations/0007_service_definition.py +++ /dev/null @@ -1,115 +0,0 @@ -# Generated by Django 5.2b1 on 2025-03-25 11:02 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("core", "0006_service_slug"), - ] - - operations = [ - migrations.RemoveField( - model_name="serviceoffering", - name="control_plane", - ), - migrations.CreateModel( - name="ServiceDefinition", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("name", models.CharField(max_length=100, verbose_name="Name")), - ( - "description", - models.TextField(blank=True, verbose_name="Description"), - ), - ( - "api_definition", - models.JSONField( - blank=True, - help_text="Contains group, version, and kind information", - null=True, - verbose_name="API Definition", - ), - ), - ( - "service", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="service_definitions", - to="core.service", - verbose_name="Service", - ), - ), - ], - options={ - "verbose_name": "Service definition", - "verbose_name_plural": "Service definitions", - }, - ), - migrations.CreateModel( - name="ServiceOfferingControlPlane", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "control_plane", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="offering_connections", - to="core.controlplane", - verbose_name="Control plane", - ), - ), - ( - "service_definition", - models.ForeignKey( - on_delete=django.db.models.deletion.PROTECT, - related_name="offering_control_planes", - to="core.servicedefinition", - verbose_name="Service definition", - ), - ), - ( - "service_offering", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="control_plane_connections", - to="core.serviceoffering", - verbose_name="Service offering", - ), - ), - ], - options={ - "verbose_name": "Service offering control plane connection", - "verbose_name_plural": "Service offering control planes connections", - "unique_together": {("service_offering", "control_plane")}, - }, - ), - migrations.AddField( - model_name="serviceoffering", - name="control_planes", - field=models.ManyToManyField( - related_name="offerings", - through="core.ServiceOfferingControlPlane", - to="core.controlplane", - verbose_name="Control planes", - ), - ), - ] diff --git a/src/servala/core/migrations/0007_service_definitions.py b/src/servala/core/migrations/0007_service_definitions.py new file mode 100644 index 0000000..bdc11e1 --- /dev/null +++ b/src/servala/core/migrations/0007_service_definitions.py @@ -0,0 +1,72 @@ +# Generated by Django 5.2b1 on 2025-03-24 14:41 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0006_service_slug"), + ] + + operations = [ + migrations.RenameField( + model_name="serviceoffering", + old_name="control_plane", + new_name="control_planes", + ), + migrations.CreateModel( + name="ServiceDefinition", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=100, verbose_name="Name")), + ( + "description", + models.TextField(blank=True, verbose_name="Description"), + ), + ( + "api_definition", + models.JSONField( + blank=True, + help_text="Contains group, version, and kind information", + null=True, + verbose_name="API Definition", + ), + ), + ( + "service", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="service_definitions", + to="core.service", + verbose_name="Service", + ), + ), + ], + options={ + "verbose_name": "Service definition", + "verbose_name_plural": "Service definitions", + }, + ), + migrations.AddField( + model_name="controlplane", + name="service_definition", + field=models.ForeignKey( + default=1, + on_delete=django.db.models.deletion.PROTECT, + related_name="control_planes", + to="core.servicedefinition", + verbose_name="Service definition", + ), + preserve_default=False, + ), + ] diff --git a/src/servala/core/models/__init__.py b/src/servala/core/models/__init__.py index 6d5b24d..b4ddf21 100644 --- a/src/servala/core/models/__init__.py +++ b/src/servala/core/models/__init__.py @@ -13,7 +13,6 @@ from .service import ( ServiceCategory, ServiceDefinition, ServiceOffering, - ServiceOfferingControlPlane, ) from .user import User @@ -30,6 +29,5 @@ __all__ = [ "ServiceCategory", "ServiceDefinition", "ServiceOffering", - "ServiceOfferingControlPlane", "User", ] diff --git a/src/servala/core/models/service.py b/src/servala/core/models/service.py index f1dc20b..330e077 100644 --- a/src/servala/core/models/service.py +++ b/src/servala/core/models/service.py @@ -103,6 +103,12 @@ class ControlPlane(models.Model): related_name="control_planes", verbose_name=_("Cloud provider"), ) + service_definition = models.ForeignKey( + to="ServiceDefinition", + related_name="control_planes", + verbose_name=_("Service definition"), + on_delete=models.PROTECT, + ) class Meta: verbose_name = _("Control plane") @@ -257,18 +263,6 @@ class ServiceDefinition(models.Model): verbose_name=_("Service"), ) - @property - def control_planes(self): - return ControlPlane.objects.filter( - offering_connections__service_definition=self - ).distinct() - - @property - def service_offerings(self): - return ServiceOffering.objects.filter( - control_plane_connections__service_definition=self - ).distinct() - class Meta: verbose_name = _("Service definition") verbose_name_plural = _("Service definitions") @@ -277,40 +271,6 @@ class ServiceDefinition(models.Model): return self.name -class ServiceOfferingControlPlane(models.Model): - """ - Each combination of ServiceOffering and ControlPlane can have a different - ServiceDefinition, which is here modeled as the "through" model. - """ - - service_offering = models.ForeignKey( - to="ServiceOffering", - on_delete=models.CASCADE, - related_name="control_plane_connections", - verbose_name=_("Service offering"), - ) - control_plane = models.ForeignKey( - to="ControlPlane", - on_delete=models.CASCADE, - related_name="offering_connections", - verbose_name=_("Control plane"), - ) - service_definition = models.ForeignKey( - to="ServiceDefinition", - on_delete=models.PROTECT, - related_name="offering_control_planes", - verbose_name=_("Service definition"), - ) - - class Meta: - verbose_name = _("Service offering control plane connection") - verbose_name_plural = _("Service offering control planes connections") - unique_together = [("service_offering", "control_plane")] - - def __str__(self): - return f"{self.service_offering} on {self.control_plane} with {self.service_definition}" - - class ServiceOffering(models.Model): """ A service offering, e.g. "PostgreSQL on AWS", "MinIO on GCP". @@ -330,7 +290,6 @@ class ServiceOffering(models.Model): ) control_planes = models.ManyToManyField( to="ControlPlane", - through="ServiceOfferingControlPlane", related_name="offerings", verbose_name=_("Control planes"), ) diff --git a/src/servala/static/js/sidebar.js b/src/servala/static/js/sidebar.js index f541788..1f79795 100644 --- a/src/servala/static/js/sidebar.js +++ b/src/servala/static/js/sidebar.js @@ -2,30 +2,21 @@ * This script marks the current path as active in the sidebar. */ -const markActive = (link) => { - const parentItem = link.closest('.sidebar-item'); - if (parentItem) { - parentItem.classList.add('active'); - } else { - link.classList.add('active'); - } -} - -const checkLink = (fuzzy) => { -} - document.addEventListener('DOMContentLoaded', () => { const currentPath = window.location.pathname; - const sidebarLinks = [...document.querySelectorAll('a.sidebar-link')] + const sidebarLinks = document.querySelectorAll('.sidebar-link'); - const exactMatches = sidebarLinks.filter(link => link.getAttribute('href') === currentPath) - if (exactMatches.length > 0) { - markActive(exactMatches[0]) - } else { - fuzzyMatches = sidebarLinks.filter(link => currentPath.startsWith(link.getAttribute('href'))) - if (fuzzyMatches.length > 0) { - const longestMatch = fuzzyMatches.sort((a, b) => b.href.length - a.href.length)[0] - markActive(longestMatch) + sidebarLinks.forEach(link => { + // Skip links that are inside buttons (like logout) + if (link.tagName === 'BUTTON') return; + + if (link.getAttribute('href') === currentPath) { + const parentItem = link.closest('.sidebar-item'); + if (parentItem) { + parentItem.classList.add('active'); + } else { + link.classList.add('active'); + } } - } + }) })