Compare commits
No commits in common. "d093d422ced4894ea3f5728741424aca0feb7922" and "267dc56f3279ed00f2bd56cedb6fb188f5e7db61" have entirely different histories.
d093d422ce
...
267dc56f32
6 changed files with 95 additions and 207 deletions
|
@ -14,7 +14,6 @@ from servala.core.models import (
|
||||||
ServiceCategory,
|
ServiceCategory,
|
||||||
ServiceDefinition,
|
ServiceDefinition,
|
||||||
ServiceOffering,
|
ServiceOffering,
|
||||||
ServiceOfferingControlPlane,
|
|
||||||
User,
|
User,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -177,7 +176,7 @@ class PlanAdmin(admin.ModelAdmin):
|
||||||
class ServiceDefinitionAdmin(admin.ModelAdmin):
|
class ServiceDefinitionAdmin(admin.ModelAdmin):
|
||||||
form = ServiceDefinitionAdminForm
|
form = ServiceDefinitionAdminForm
|
||||||
list_display = ("name", "service")
|
list_display = ("name", "service")
|
||||||
list_filter = ("service",)
|
list_filter = ("service", "control_planes")
|
||||||
search_fields = ("name", "description")
|
search_fields = ("name", "description")
|
||||||
autocomplete_fields = ("service",)
|
autocomplete_fields = ("service",)
|
||||||
|
|
||||||
|
@ -200,27 +199,11 @@ class ServiceDefinitionAdmin(admin.ModelAdmin):
|
||||||
return ["api_definition"]
|
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)
|
@admin.register(ServiceOffering)
|
||||||
class ServiceOfferingAdmin(admin.ModelAdmin):
|
class ServiceOfferingAdmin(admin.ModelAdmin):
|
||||||
list_display = ("id", "service", "provider")
|
list_display = ("id", "service", "provider")
|
||||||
list_filter = ("service", "provider")
|
list_filter = ("service", "provider", "control_planes")
|
||||||
search_fields = ("description",)
|
search_fields = ("description",)
|
||||||
autocomplete_fields = ("service", "provider")
|
autocomplete_fields = ("service", "provider")
|
||||||
inlines = (
|
filter_horizontal = ("control_planes",)
|
||||||
ServiceOfferingControlPlaneInline,
|
inlines = (PlanInline,)
|
||||||
PlanInline,
|
|
||||||
)
|
|
||||||
|
|
|
@ -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",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
72
src/servala/core/migrations/0007_service_definitions.py
Normal file
72
src/servala/core/migrations/0007_service_definitions.py
Normal 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,
|
||||||
|
),
|
||||||
|
]
|
|
@ -13,7 +13,6 @@ from .service import (
|
||||||
ServiceCategory,
|
ServiceCategory,
|
||||||
ServiceDefinition,
|
ServiceDefinition,
|
||||||
ServiceOffering,
|
ServiceOffering,
|
||||||
ServiceOfferingControlPlane,
|
|
||||||
)
|
)
|
||||||
from .user import User
|
from .user import User
|
||||||
|
|
||||||
|
@ -30,6 +29,5 @@ __all__ = [
|
||||||
"ServiceCategory",
|
"ServiceCategory",
|
||||||
"ServiceDefinition",
|
"ServiceDefinition",
|
||||||
"ServiceOffering",
|
"ServiceOffering",
|
||||||
"ServiceOfferingControlPlane",
|
|
||||||
"User",
|
"User",
|
||||||
]
|
]
|
||||||
|
|
|
@ -103,6 +103,12 @@ class ControlPlane(models.Model):
|
||||||
related_name="control_planes",
|
related_name="control_planes",
|
||||||
verbose_name=_("Cloud provider"),
|
verbose_name=_("Cloud provider"),
|
||||||
)
|
)
|
||||||
|
service_definition = models.ForeignKey(
|
||||||
|
to="ServiceDefinition",
|
||||||
|
related_name="control_planes",
|
||||||
|
verbose_name=_("Service definition"),
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Control plane")
|
verbose_name = _("Control plane")
|
||||||
|
@ -257,18 +263,6 @@ class ServiceDefinition(models.Model):
|
||||||
verbose_name=_("Service"),
|
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:
|
class Meta:
|
||||||
verbose_name = _("Service definition")
|
verbose_name = _("Service definition")
|
||||||
verbose_name_plural = _("Service definitions")
|
verbose_name_plural = _("Service definitions")
|
||||||
|
@ -277,40 +271,6 @@ class ServiceDefinition(models.Model):
|
||||||
return self.name
|
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):
|
class ServiceOffering(models.Model):
|
||||||
"""
|
"""
|
||||||
A service offering, e.g. "PostgreSQL on AWS", "MinIO on GCP".
|
A service offering, e.g. "PostgreSQL on AWS", "MinIO on GCP".
|
||||||
|
@ -330,7 +290,6 @@ class ServiceOffering(models.Model):
|
||||||
)
|
)
|
||||||
control_planes = models.ManyToManyField(
|
control_planes = models.ManyToManyField(
|
||||||
to="ControlPlane",
|
to="ControlPlane",
|
||||||
through="ServiceOfferingControlPlane",
|
|
||||||
related_name="offerings",
|
related_name="offerings",
|
||||||
verbose_name=_("Control planes"),
|
verbose_name=_("Control planes"),
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,30 +2,21 @@
|
||||||
* This script marks the current path as active in the sidebar.
|
* 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', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const currentPath = window.location.pathname;
|
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)
|
sidebarLinks.forEach(link => {
|
||||||
if (exactMatches.length > 0) {
|
// Skip links that are inside buttons (like logout)
|
||||||
markActive(exactMatches[0])
|
if (link.tagName === 'BUTTON') return;
|
||||||
} else {
|
|
||||||
fuzzyMatches = sidebarLinks.filter(link => currentPath.startsWith(link.getAttribute('href')))
|
if (link.getAttribute('href') === currentPath) {
|
||||||
if (fuzzyMatches.length > 0) {
|
const parentItem = link.closest('.sidebar-item');
|
||||||
const longestMatch = fuzzyMatches.sort((a, b) => b.href.length - a.href.length)[0]
|
if (parentItem) {
|
||||||
markActive(longestMatch)
|
parentItem.classList.add('active');
|
||||||
|
} else {
|
||||||
|
link.classList.add('active');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
})
|
})
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue