diff --git a/hub/services/migrations/0030_serviceoffering_msp.py b/hub/services/migrations/0030_serviceoffering_msp.py new file mode 100644 index 0000000..7c6c2d4 --- /dev/null +++ b/hub/services/migrations/0030_serviceoffering_msp.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2 on 2025-05-23 15:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("services", "0029_alter_computeplangroup_options"), + ] + + operations = [ + migrations.AddField( + model_name="serviceoffering", + name="msp", + field=models.CharField( + choices=[("VS", "VSHN")], + default="VS", + max_length=2, + verbose_name="Managed Service Provider", + ), + ), + ] diff --git a/hub/services/models/base.py b/hub/services/models/base.py index 04d9b26..9a7abce 100644 --- a/hub/services/models/base.py +++ b/hub/services/models/base.py @@ -29,6 +29,11 @@ class Unit(models.TextChoices): CPU = "CPU", "vCPU" +# This should be a relation, but for now this is good enough :TM: +class ManagedServiceProvider(models.TextChoices): + VS = "VS", "VSHN" + + class ReusableText(models.Model): name = models.CharField(max_length=100) textsnippet = models.ForeignKey( diff --git a/hub/services/models/services.py b/hub/services/models/services.py index 4f4b873..5c155b8 100644 --- a/hub/services/models/services.py +++ b/hub/services/models/services.py @@ -5,7 +5,7 @@ from django.urls import reverse from django.utils.text import slugify from django_prose_editor.fields import ProseEditorField -from .base import Category, ReusableText, validate_image_size +from .base import Category, ReusableText, ManagedServiceProvider, validate_image_size from .providers import CloudProvider @@ -57,6 +57,12 @@ class ServiceOffering(models.Model): service = models.ForeignKey( Service, on_delete=models.CASCADE, related_name="offerings" ) + msp = models.CharField( + "Managed Service Provider", + max_length=2, + default=ManagedServiceProvider.VS, + choices=ManagedServiceProvider.choices, + ) cloud_provider = models.ForeignKey( CloudProvider, on_delete=models.CASCADE, related_name="offerings" ) diff --git a/hub/services/static/css/servala-main.css b/hub/services/static/css/servala-main.css index dc240b5..784acab 100644 --- a/hub/services/static/css/servala-main.css +++ b/hub/services/static/css/servala-main.css @@ -12338,4 +12338,175 @@ a.btn:focus { .clickable-button { position: relative; z-index: 2; +} + +/* Accordion styles */ +.accordion { + --bs-accordion-color: var(--bs-body-color); + --bs-accordion-bg: var(--bs-body-bg); + --bs-accordion-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, border-radius 0.15s ease; + --bs-accordion-border-color: var(--bs-border-color); + --bs-accordion-border-width: var(--bs-border-width); + --bs-accordion-border-radius: var(--bs-border-radius); + --bs-accordion-inner-border-radius: calc(var(--bs-border-radius) - var(--bs-border-width)); + --bs-accordion-btn-padding-x: 1.25rem; + --bs-accordion-btn-padding-y: 1rem; + --bs-accordion-btn-color: var(--bs-body-color); + --bs-accordion-btn-bg: var(--bs-accordion-bg); + --bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e"); + --bs-accordion-btn-icon-width: 1.25rem; + --bs-accordion-btn-icon-transform: rotate(-180deg); + --bs-accordion-btn-icon-transition: transform 0.2s ease-in-out; + --bs-accordion-btn-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23052c65'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e"); + --bs-accordion-btn-focus-border-color: #86b7fe; + --bs-accordion-btn-focus-box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); + --bs-accordion-body-padding-x: 1.25rem; + --bs-accordion-body-padding-y: 1rem; + --bs-accordion-active-color: var(--bs-primary-text-emphasis); + --bs-accordion-active-bg: var(--bs-primary-bg-subtle); +} + +.accordion-button { + position: relative; + display: flex; + align-items: center; + width: 100%; + padding: var(--bs-accordion-btn-padding-y) var(--bs-accordion-btn-padding-x); + font-size: 1rem; + color: var(--bs-accordion-btn-color); + text-align: left; + background-color: var(--bs-accordion-btn-bg); + border: 0; + border-radius: 0; + overflow-anchor: none; + transition: var(--bs-accordion-transition); +} + +@media (prefers-reduced-motion: reduce) { + .accordion-button { + transition: none; + } +} + +.accordion-button:not(.collapsed) { + color: var(--bs-accordion-active-color); + background-color: var(--bs-accordion-active-bg); + box-shadow: inset 0 calc(-1 * var(--bs-accordion-border-width)) 0 var(--bs-accordion-border-color); +} + +.accordion-button:not(.collapsed):after { + background-image: var(--bs-accordion-btn-active-icon); + transform: var(--bs-accordion-btn-icon-transform); +} + +.accordion-button:after { + flex-shrink: 0; + width: var(--bs-accordion-btn-icon-width); + height: var(--bs-accordion-btn-icon-width); + margin-left: auto; + content: ""; + background-image: var(--bs-accordion-btn-icon); + background-repeat: no-repeat; + background-size: var(--bs-accordion-btn-icon-width); + transition: var(--bs-accordion-btn-icon-transition); +} + +@media (prefers-reduced-motion: reduce) { + .accordion-button:after { + transition: none; + } +} + +.accordion-button:hover { + z-index: 2; +} + +.accordion-button:focus { + z-index: 3; + border-color: var(--bs-accordion-btn-focus-border-color); + outline: 0; + box-shadow: var(--bs-accordion-btn-focus-box-shadow); +} + +.accordion-header { + margin-bottom: 0; +} + +.accordion-item { + color: var(--bs-accordion-color); + background-color: var(--bs-accordion-bg); + border: var(--bs-accordion-border-width) solid var(--bs-accordion-border-color); +} + +.accordion-item:first-of-type { + border-top-left-radius: var(--bs-accordion-border-radius); + border-top-right-radius: var(--bs-accordion-border-radius); +} + +.accordion-item:first-of-type .accordion-button { + border-top-left-radius: var(--bs-accordion-inner-border-radius); + border-top-right-radius: var(--bs-accordion-inner-border-radius); +} + +.accordion-item:not(:first-of-type) { + border-top: 0; +} + +.accordion-item:last-of-type { + border-bottom-right-radius: var(--bs-accordion-border-radius); + border-bottom-left-radius: var(--bs-accordion-border-radius); +} + +.accordion-item:last-of-type .accordion-button.collapsed { + border-bottom-right-radius: var(--bs-accordion-inner-border-radius); + border-bottom-left-radius: var(--bs-accordion-inner-border-radius); +} + +.accordion-item:last-of-type .accordion-collapse { + border-bottom-right-radius: var(--bs-accordion-border-radius); + border-bottom-left-radius: var(--bs-accordion-border-radius); +} + +.accordion-body { + padding: var(--bs-accordion-body-padding-y) var(--bs-accordion-body-padding-x); +} + +.accordion-flush .accordion-collapse { + border-width: 0; +} + +.accordion-flush .accordion-item { + border-right: 0; + border-left: 0; + border-radius: 0; +} + +.accordion-flush .accordion-item:first-child { + border-top: 0; +} + +.accordion-flush .accordion-item:last-child { + border-bottom: 0; +} + +.accordion-flush .accordion-item .accordion-button, +.accordion-flush .accordion-item .accordion-button.collapsed { + border-radius: 0; +} + +[data-bs-theme=dark] .accordion-button:not(.collapsed) { + box-shadow: none; +} + +[data-bs-theme=dark] .accordion-button::after { + --bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e"); + --bs-accordion-btn-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e"); +} + +.accordion-button:not(.collapsed) { + color: white !important; +} + +.accordion-button:not(.collapsed)::after { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23ffffff'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e"); } \ No newline at end of file diff --git a/hub/services/templates/services/offering_detail.html b/hub/services/templates/services/offering_detail.html index 5d71aa8..77b4413 100644 --- a/hub/services/templates/services/offering_detail.html +++ b/hub/services/templates/services/offering_detail.html @@ -152,50 +152,121 @@ {% endif %} - - {% if offering.plans.all %} +
{{ representative_plan.compute_plan_group_description }}
+ {% endif %} + {% endwith %} + {% endif %} + {% if forloop.first %} + {% comment %} Only show description for first service level {% endcomment %} + {% endif %} + {% endfor %} + + {% for service_level, pricing_data in service_levels.items %} +Compute Plan | +vCPUs | +RAM (GB) | +Currency | +Compute Price | +Service Price | +Total Price | +
---|---|---|---|---|---|---|
{{ row.compute_plan }} | +{{ row.vcpus }} | +{{ row.ram }} | +{{ row.currency }} | +{{ row.compute_plan_price|floatformat:2 }} | +{{ row.sla_price|floatformat:2 }} | +{{ row.final_price|floatformat:2 }} | +
No pricing data available for {{ service_level }}.
+ {% endif %} +No plans available yet.
-No plans available yet.
+