diff --git a/src/servala/core/models/service.py b/src/servala/core/models/service.py index c6552c4..e973235 100644 --- a/src/servala/core/models/service.py +++ b/src/servala/core/models/service.py @@ -1152,5 +1152,50 @@ class ServiceInstance(ServalaModelMixin, models.Model): except (AttributeError, KeyError, IndexError): return None + @cached_property + def kubernetes_events(self) -> dict: + """ + Returns a list of event dictionaries sorted by last timestamp (newest first). + """ + if not self.kubernetes_object: + return [] + + try: + v1 = kubernetes.client.CoreV1Api( + self.context.control_plane.get_kubernetes_client() + ) + events = v1.list_namespaced_event( + namespace=self.organization.namespace, + field_selector=f"involvedObject.name={self.name},involvedObject.kind={self.context.kind}", + ) + event_list = [] + for event in events.items: + event_dict = { + "type": event.type, # Normal or Warning + "reason": event.reason, + "message": event.message, + "count": event.count or 1, + "first_timestamp": ( + event.first_timestamp.isoformat() + if event.first_timestamp + else None + ), + "last_timestamp": ( + event.last_timestamp.isoformat() + if event.last_timestamp + else None + ), + "source": event.source.component if event.source else None, + } + event_list.append(event_dict) + + event_list.sort(key=lambda x: x.get("last_timestamp") or "", reverse=True) + + return event_list + except ApiException: + return [] + except Exception: + return [] + auditlog.register(ServiceInstance, exclude_fields=["updated_at"], serialize_data=True) diff --git a/src/servala/frontend/templates/frontend/base.html b/src/servala/frontend/templates/frontend/base.html index 620cb6d..8454e0d 100644 --- a/src/servala/frontend/templates/frontend/base.html +++ b/src/servala/frontend/templates/frontend/base.html @@ -35,6 +35,8 @@ {% block page_title_extra %} {% endblock page_title_extra %} + {% block page_subtitle %} + {% endblock page_subtitle %}
{% for message in messages %} diff --git a/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html b/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html index 5c72de6..eaebc94 100644 --- a/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html +++ b/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html @@ -29,132 +29,197 @@ {% endif %}
{% endblock page_title_extra %} +{% block page_subtitle %} +
+ {% if instance.context.service_definition.service.logo %} + {{ instance.context.service_definition.service.name }} + {% endif %} + | + {% if instance.context.service_offering.provider.logo %} + {{ instance.context.service_offering.provider.name }} + {% endif %} +
+{% endblock page_subtitle %} {% block content %}
-
+ +
-
-
-

{% translate "Details" %}

-
-
-
-
{% translate "Service" %}
-
+ {% if compute_plan_assignment or storage_plan %} +
+
+

{% translate "Product" %}

+

{{ instance.context.service_definition.service.name }} -

-
{% translate "Service Provider" %}
-
+ {% translate "at" %} {{ instance.context.service_offering.provider.name }} -
-
{% translate "Control Plane" %}
-
- {{ instance.context.control_plane.name }} -
+ ({{ instance.context.control_plane.name }}) +

+
+
{% if compute_plan_assignment %} -
{% translate "Compute Plan" %}
-
- {{ compute_plan_assignment.compute_plan.name }} - - {{ compute_plan_assignment.get_sla_display }} - -
+
+
{% translate "Compute" %}
+
+ {% translate "Plan:" %} {{ compute_plan_assignment.compute_plan.name }} +
+
{{ compute_plan_assignment.compute_plan.cpu_limits }} vCPU {{ compute_plan_assignment.compute_plan.memory_limits }} RAM - CHF {{ compute_plan_assignment.price }}/{{ compute_plan_assignment.get_unit_display }} + SLA: {{ compute_plan_assignment.get_sla_display }}
-
+
+ CHF {{ compute_plan_assignment.price }} / {{ compute_plan_assignment.get_unit_display }} + {% if pricing.compute_monthly %} + (~CHF {{ pricing.compute_monthly }} / {% translate "month" %}) + {% endif %} +
+
{% endif %} {% if storage_plan %} -
{% translate "Storage Plan" %}
-
- CHF {{ storage_plan.price_per_gib }} per GiB -
{% translate "Billed separately based on disk usage" %}
-
+
+
{% translate "Storage" %}
+
+ CHF {{ storage_plan.price_per_gib }} / GiB / {% translate "hour" %} + {% if pricing.disk_size_gib %} + + {{ pricing.disk_size_gib }} GiB {% translate "configured" %} + {% endif %} +
+ {% if pricing.storage_monthly %} +
+ CHF {{ pricing.storage_hourly }} / {% translate "hour" %} + (~CHF {{ pricing.storage_monthly }} / {% translate "month" %}) +
+ {% endif %} +
{% endif %} -
{% translate "Created By" %}
-
- {{ instance.created_by|default:"-" }} -
-
{% translate "Created At" %}
-
- {{ instance.created_at|date:"SHORT_DATETIME_FORMAT" }} -
-
{% translate "Updated At" %}
-
- {{ instance.updated_at|date:"SHORT_DATETIME_FORMAT" }} -
- + {% if pricing.total_monthly %} +
+
+ {% translate "Estimated Total" %} +
+
~CHF {{ pricing.total_monthly }} / {% translate "month" %}
+
CHF {{ pricing.total_hourly }} / {% translate "hour" %}
+
+
+ {% endif %} +
-
+ {% endif %}
- {% if instance.status_conditions %} -
-
-

{% translate "Status" %}

+ {% if instance.connection_credentials %} +
+
+

{% translate "Connection Credentials" %}

+
-
-
- - - - - - - - - - - - {% for condition in instance.status_conditions %} +
+
{% translate "Type" %}{% translate "Status" %}{% translate "Last Transition Time" %}{% translate "Reason" %}{% translate "Message" %}
+ + + + + + + + + + + + + + {% for key, value in instance.connection_credentials.items %} + {% if "_HOST" not in key and "_URL" not in key %} - - + + - - - - {% endfor %} - -
{% translate "Name" %}{% translate "Value" %}
{{ condition.type }} - {% if condition.status == "True" %} - True - {% elif condition.status == "False" %} - False + {{ key }} + {% if key == "error" %} + {{ value }} {% else %} - {{ condition.status }} + •••••••••••• + {% endif %} + + {% if key != "error" %} + {% endif %} {{ condition.lastTransitionTime|date:"SHORT_DATETIME_FORMAT" }}{{ condition.reason|default:"-" }}{{ condition.message|truncatewords:20|default:"-" }}
+ {% endif %} + {% endfor %} + + +
+
+ + {% translate "Click the eye icon to reveal individual credentials, or use 'Show All' to reveal all at once." %} +
+
+
+ {% endif %} +
+
+ + {% if instance.spec and spec_fieldsets or instance.context.control_plane.user_info %} +
+ + {% if instance.context.control_plane.user_info %} +
+
+
+

+ +

+

{% translate "Technical details for connecting to this zone" %}

+
+
+
+ {% include "includes/control_plane_user_info.html" with control_plane=instance.context.control_plane %}
{% endif %} - {% if control_plane.user_info %} -
-
-

{% translate "Service Provider Zone Information" %}

-
-
- {% include "includes/control_plane_user_info.html" with control_plane=instance.context.control_plane %} -
-
- {% endif %} -
- {% if instance.spec and spec_fieldsets %} -
-
-
-

{% translate "Specification" %}

-
-
-
-
- + + {% if instance.spec and spec_fieldsets %} +
+
+
+

{% translate "Service Configuration" %}

+
+
+ {% if spec_fieldsets|length > 1 %} + -
{% for fieldset in spec_fieldsets %}
- -
+
{% for field in fieldset.fields %} -
{{ field.label }}
-
- {% if field.value|default:""|stringformat:"s"|slice:":1" == "{" or field.value|default:""|stringformat:"s"|slice:":1" == "[" %} -
{{ field.value|pprint }}
- {% else %} - {{ field.value|default:"-" }} +
+ {{ field.label }} + {% if field.help_text %} + {% endif %} +
+
+ {{ field.value|render_tree }}
{% endfor %}
- {% for sub_key, sub_fieldset in fieldset.fieldsets.items %} -
{{ sub_fieldset.title }}
-
+
{{ sub_fieldset.title }}
+
{% for field in sub_fieldset.fields %} -
{{ field.label }}
-
- {% if field.value|default:""|stringformat:"s"|slice:":1" == "{" or field.value|default:""|stringformat:"s"|slice:":1" == "[" %} -
{{ field.value|pprint }}
- {% else %} - {{ field.value|default:"-" }} - {% endif %} +
{{ field.label }}
+
+ {{ field.value|render_tree }}
{% endfor %}
{% endfor %}
- {% empty %} -

{% translate "No specification details to display." %}

{% endfor %}
+ {% else %} + + {% for fieldset in spec_fieldsets %} +
+ {% for field in fieldset.fields %} +
+ {{ field.label }} + {% if field.help_text %} + + {% endif %} +
+
+ {{ field.value|render_tree }} +
+ {% endfor %} +
+ {% for sub_key, sub_fieldset in fieldset.fieldsets.items %} +
{{ sub_fieldset.title }}
+
+ {% for field in sub_fieldset.fields %} +
{{ field.label }}
+
+ {{ field.value|render_tree }} +
+ {% endfor %} +
+ {% endfor %} + {% endfor %} + {% endif %} +
+
+
+ {% endif %} +
+ {% endif %} + + {% if instance.status_conditions or instance.created_at %} +
+
+
+
+

+ +

+
+
+
+ +
+ {% if instance.status_conditions %} +
{% translate "Status Conditions" %}
+
+ + + + + + + + + + + + {% for condition in instance.status_conditions %} + + + + + + + + {% endfor %} + +
{% translate "Type" %}{% translate "Status" %}{% translate "Last Transition" %}{% translate "Reason" %}{% translate "Message" %}
{{ condition.type }} + {% if condition.status == "True" %} + True + {% elif condition.status == "False" %} + False + {% else %} + {{ condition.status }} + {% endif %} + {{ condition.lastTransitionTime|localtime_tag }}{{ condition.reason|default:"-" }}{{ condition.message|truncatewords:20|default:"-" }}
+
+ {% endif %} +
+ +
+
{% translate "Metadata" %}
+
+
{% translate "Created By" %}
+
{{ instance.created_by|default:"-" }}
+
{% translate "Created At" %}
+
{{ instance.created_at|localtime_tag }}
+
{% translate "Updated At" %}
+
{{ instance.updated_at|localtime_tag }}
+
+
+
- {% endif %} - {% if instance.connection_credentials %} -
-
-
-

{% translate "Connection Credentials" %}

-
-
-
- - - - - - - - - {% for key, value in instance.connection_credentials.items %} - - - - - {% endfor %} - -
{% translate "Name" %}{% translate "Value" %}
{{ key }} - {% if key == "error" %} - {{ value }} - {% else %} - {{ value }} - {% endif %} -
-
-
-
-
- {% endif %} -
+
+ {% endif %}