Compare commits

..

No commits in common. "d093d422ced4894ea3f5728741424aca0feb7922" and "267dc56f3279ed00f2bd56cedb6fb188f5e7db61" have entirely different histories.

6 changed files with 95 additions and 207 deletions

View file

@ -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,)

View file

@ -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",
),
),
]

View file

@ -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,
),
]

View file

@ -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",
]

View file

@ -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"),
)

View file

@ -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');
}
}
}
})
})