with the help of Claude Code
This commit is contained in:
parent
5e543b1e4c
commit
5c39d26b62
6 changed files with 506 additions and 260 deletions
|
|
@ -35,6 +35,8 @@
|
||||||
{% block page_title_extra %}
|
{% block page_title_extra %}
|
||||||
{% endblock page_title_extra %}
|
{% endblock page_title_extra %}
|
||||||
</h3>
|
</h3>
|
||||||
|
{% block page_subtitle %}
|
||||||
|
{% endblock page_subtitle %}
|
||||||
</div>
|
</div>
|
||||||
<div class="page-content">
|
<div class="page-content">
|
||||||
{% for message in messages %}
|
{% for message in messages %}
|
||||||
|
|
|
||||||
|
|
@ -29,82 +29,314 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock page_title_extra %}
|
{% endblock page_title_extra %}
|
||||||
|
{% block page_subtitle %}
|
||||||
|
<div class="d-flex flex-wrap align-items-center gap-3 mt-2">
|
||||||
|
{% if instance.context.service_definition.service.logo %}
|
||||||
|
<img src="{{ instance.context.service_definition.service.logo.url }}"
|
||||||
|
alt="{{ instance.context.service_definition.service.name }}"
|
||||||
|
style="height: 24px; width: auto;">
|
||||||
|
{% endif %}
|
||||||
|
<span class="text-muted">|</span>
|
||||||
|
{% if instance.context.service_offering.provider.logo %}
|
||||||
|
<img src="{{ instance.context.service_offering.provider.logo.url }}"
|
||||||
|
alt="{{ instance.context.service_offering.provider.name }}"
|
||||||
|
style="height: 24px; width: auto;">
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock page_subtitle %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<div class="row match-height mb-5">
|
<!-- Row 1: Product Card and Connection Credentials -->
|
||||||
|
<div class="row match-height mb-4">
|
||||||
<div class="col-12 col-md-5">
|
<div class="col-12 col-md-5">
|
||||||
<div class="card">
|
{% if compute_plan_assignment or storage_plan %}
|
||||||
|
<div class="card h-100">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h4>{% translate "Details" %}</h4>
|
<h4 class="mb-1">{% translate "Product" %}</h4>
|
||||||
|
<p class="text-muted mb-0 small">
|
||||||
|
{{ instance.context.service_definition.service.name }}
|
||||||
|
{% translate "at" %}
|
||||||
|
{{ instance.context.service_offering.provider.name }}
|
||||||
|
({{ instance.context.control_plane.name }})
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<dl class="row">
|
|
||||||
<dt class="col-sm-4">{% translate "Service" %}</dt>
|
|
||||||
<dd class="col-sm-8">
|
|
||||||
{{ instance.context.service_definition.service.name }}
|
|
||||||
</dd>
|
|
||||||
<dt class="col-sm-4">{% translate "Service Provider" %}</dt>
|
|
||||||
<dd class="col-sm-8">
|
|
||||||
{{ instance.context.service_offering.provider.name }}
|
|
||||||
</dd>
|
|
||||||
<dt class="col-sm-4">{% translate "Control Plane" %}</dt>
|
|
||||||
<dd class="col-sm-8">
|
|
||||||
{{ instance.context.control_plane.name }}
|
|
||||||
</dd>
|
|
||||||
{% if compute_plan_assignment %}
|
{% if compute_plan_assignment %}
|
||||||
<dt class="col-sm-4">{% translate "Compute Plan" %}</dt>
|
<div class="mb-3">
|
||||||
<dd class="col-sm-8">
|
<h6 class="text-muted mb-2">{% translate "Compute" %}</h6>
|
||||||
{{ compute_plan_assignment.compute_plan.name }}
|
<div class="mb-1">
|
||||||
<span class="badge bg-{% if compute_plan_assignment.sla == 'guaranteed' %}success{% else %}secondary{% endif %} ms-1">
|
{% translate "Plan:" %} <strong>{{ compute_plan_assignment.compute_plan.name }}</strong>
|
||||||
{{ compute_plan_assignment.get_sla_display }}
|
</div>
|
||||||
</span>
|
<div class="text-muted small">
|
||||||
<div class="text-muted small mt-1">
|
|
||||||
<i class="bi bi-cpu"></i> {{ compute_plan_assignment.compute_plan.cpu_limits }} vCPU
|
<i class="bi bi-cpu"></i> {{ compute_plan_assignment.compute_plan.cpu_limits }} vCPU
|
||||||
<span class="mx-2">•</span>
|
<span class="mx-2">•</span>
|
||||||
<i class="bi bi-memory"></i> {{ compute_plan_assignment.compute_plan.memory_limits }} RAM
|
<i class="bi bi-memory"></i> {{ compute_plan_assignment.compute_plan.memory_limits }} RAM
|
||||||
<span class="mx-2">•</span>
|
<span class="mx-2">•</span>
|
||||||
<strong>CHF {{ compute_plan_assignment.price }}</strong>/{{ compute_plan_assignment.get_unit_display }}
|
SLA: {{ compute_plan_assignment.get_sla_display }}
|
||||||
|
</div>
|
||||||
|
<div class="mt-1 small">
|
||||||
|
CHF {{ compute_plan_assignment.price }} / {{ compute_plan_assignment.get_unit_display }}
|
||||||
|
{% if pricing.compute_monthly %}
|
||||||
|
<span class="text-muted">(~CHF {{ pricing.compute_monthly }} / {% translate "month" %})</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</dd>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if storage_plan %}
|
{% if storage_plan %}
|
||||||
<dt class="col-sm-4">{% translate "Storage Plan" %}</dt>
|
<div class="mb-3">
|
||||||
<dd class="col-sm-8">
|
<h6 class="text-muted mb-2">{% translate "Storage" %}</h6>
|
||||||
<strong>CHF {{ storage_plan.price_per_gib }}</strong> per GiB
|
<div class="text-muted small">
|
||||||
<div class="text-muted small">{% translate "Billed separately based on disk usage" %}</div>
|
CHF {{ storage_plan.price_per_gib }} / GiB / {% translate "hour" %}
|
||||||
</dd>
|
{% if pricing.disk_size_gib %}
|
||||||
|
<span class="mx-2">•</span>
|
||||||
|
{{ pricing.disk_size_gib }} GiB {% translate "configured" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<dt class="col-sm-4">{% translate "Created By" %}</dt>
|
</div>
|
||||||
<dd class="col-sm-8">
|
{% if pricing.storage_monthly %}
|
||||||
{{ instance.created_by|default:"-" }}
|
<div class="mt-1 small">
|
||||||
</dd>
|
CHF {{ pricing.storage_hourly }} / {% translate "hour" %}
|
||||||
<dt class="col-sm-4">{% translate "Created At" %}</dt>
|
<span class="text-muted">(~CHF {{ pricing.storage_monthly }} / {% translate "month" %})</span>
|
||||||
<dd class="col-sm-8">
|
</div>
|
||||||
{{ instance.created_at|localtime_tag }}
|
{% endif %}
|
||||||
</dd>
|
</div>
|
||||||
<dt class="col-sm-4">{% translate "Updated At" %}</dt>
|
{% endif %}
|
||||||
<dd class="col-sm-8">
|
{% if pricing.total_monthly %}
|
||||||
{{ instance.updated_at|localtime_tag }}
|
<hr class="my-2">
|
||||||
</dd>
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
</dl>
|
<span class="text-muted">{% translate "Estimated Total" %}</span>
|
||||||
|
<div class="text-end">
|
||||||
|
<div><strong>~CHF {{ pricing.total_monthly }}</strong> <span class="text-muted small">/ {% translate "month" %}</span></div>
|
||||||
|
<div class="text-muted small">CHF {{ pricing.total_hourly }} / {% translate "hour" %}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-7">
|
<div class="col-12 col-md-7">
|
||||||
{% if instance.status_conditions %}
|
{% if instance.connection_credentials %}
|
||||||
<div class="card">
|
<div class="card h-100">
|
||||||
<div class="card-header">
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
<h4>{% translate "Status" %}</h4>
|
<h4 class="mb-0">{% translate "Connection Credentials" %}</h4>
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-sm btn-outline-secondary"
|
||||||
|
id="toggle-credentials-btn"
|
||||||
|
data-show-text="{% translate "Show All" %}"
|
||||||
|
data-hide-text="{% translate "Hide All" %}"
|
||||||
|
onclick="toggleAllCredentials()">
|
||||||
|
<i class="bi bi-eye me-1"></i>
|
||||||
|
<span id="toggle-credentials-text">{% translate "Show All" %}</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row">
|
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-bordered">
|
<table class="table table-bordered table-fixed mb-0">
|
||||||
|
<colgroup>
|
||||||
|
<col style="width: 35%;">
|
||||||
|
<col style="width: 55%;">
|
||||||
|
<col style="width: 10%;">
|
||||||
|
</colgroup>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{% translate "Name" %}</th>
|
||||||
|
<th>{% translate "Value" %}</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for key, value in instance.connection_credentials.items %}
|
||||||
|
{% if "_HOST" not in key and "_URL" not in key %}
|
||||||
|
<tr>
|
||||||
|
<td class="text-truncate">{{ key }}</td>
|
||||||
|
<td class="text-truncate">
|
||||||
|
{% if key == "error" %}
|
||||||
|
<span class="text-danger">{{ value }}</span>
|
||||||
|
{% else %}
|
||||||
|
<code class="credential-value" data-value="{{ value }}">••••••••••••</code>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
{% if key != "error" %}
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-sm btn-link p-0 credential-toggle"
|
||||||
|
onclick="toggleCredential(this)"
|
||||||
|
title="{% translate 'Show/Hide' %}">
|
||||||
|
<i class="bi bi-eye"></i>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="text-muted small mt-2">
|
||||||
|
<i class="bi bi-info-circle me-1"></i>
|
||||||
|
{% translate "Click the eye icon to reveal individual credentials, or use 'Show All' to reveal all at once." %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Row 2: Zone Information (left) and Service Configuration (right) -->
|
||||||
|
{% if instance.spec and spec_fieldsets or instance.context.control_plane.user_info %}
|
||||||
|
<div class="row match-height mb-4">
|
||||||
|
<!-- Zone Information (collapsible) -->
|
||||||
|
{% if instance.context.control_plane.user_info %}
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4 class="mb-0">
|
||||||
|
<a class="text-decoration-none text-dark d-flex align-items-center justify-content-between"
|
||||||
|
data-bs-toggle="collapse"
|
||||||
|
href="#zoneInfoCollapse"
|
||||||
|
role="button"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-controls="zoneInfoCollapse">
|
||||||
|
{% translate "Zone Documentation" %}
|
||||||
|
<i class="bi bi-chevron-down ms-2 small"></i>
|
||||||
|
</a>
|
||||||
|
</h4>
|
||||||
|
<p class="text-muted small mb-0 mt-1">{% translate "Technical details for connecting to this zone" %}</p>
|
||||||
|
</div>
|
||||||
|
<div class="collapse" id="zoneInfoCollapse">
|
||||||
|
<div class="card-body pt-0">
|
||||||
|
{% include "includes/control_plane_user_info.html" with control_plane=instance.context.control_plane %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<!-- Service Configuration -->
|
||||||
|
{% if instance.spec and spec_fieldsets %}
|
||||||
|
<div class="col-12 {% if instance.context.control_plane.user_info %}col-md-8{% endif %}">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4>{% translate "Service Configuration" %}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if spec_fieldsets|length > 1 %}
|
||||||
|
<!-- Multiple fieldsets: use tabs -->
|
||||||
|
<ul class="nav nav-tabs" role="tablist">
|
||||||
|
{% for fieldset in spec_fieldsets %}
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<a href="#spec-tab-{{ forloop.counter }}"
|
||||||
|
class="nav-link {% if forloop.first %}active{% endif %}"
|
||||||
|
data-bs-toggle="tab"
|
||||||
|
role="tab">{{ fieldset.title }}</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
<div class="tab-content pt-3">
|
||||||
|
{% for fieldset in spec_fieldsets %}
|
||||||
|
<div class="tab-pane fade {% if forloop.first %}show active{% endif %}"
|
||||||
|
id="spec-tab-{{ forloop.counter }}"
|
||||||
|
role="tabpanel">
|
||||||
|
<dl class="row mb-0">
|
||||||
|
{% for field in fieldset.fields %}
|
||||||
|
<dt class="col-sm-4">
|
||||||
|
{{ field.label }}
|
||||||
|
{% if field.help_text %}
|
||||||
|
<i class="bi bi-question-circle text-muted ms-1"
|
||||||
|
data-bs-toggle="popover"
|
||||||
|
data-bs-trigger="hover"
|
||||||
|
data-bs-content="{{ field.help_text }}"></i>
|
||||||
|
{% endif %}
|
||||||
|
</dt>
|
||||||
|
<dd class="col-sm-8">
|
||||||
|
{{ field.value|render_tree }}
|
||||||
|
</dd>
|
||||||
|
{% endfor %}
|
||||||
|
</dl>
|
||||||
|
{% for sub_key, sub_fieldset in fieldset.fieldsets.items %}
|
||||||
|
<h5 class="mt-3">{{ sub_fieldset.title }}</h5>
|
||||||
|
<dl class="row mb-0">
|
||||||
|
{% for field in sub_fieldset.fields %}
|
||||||
|
<dt class="col-sm-4">{{ field.label }}</dt>
|
||||||
|
<dd class="col-sm-8">
|
||||||
|
{{ field.value|render_tree }}
|
||||||
|
</dd>
|
||||||
|
{% endfor %}
|
||||||
|
</dl>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<!-- Single fieldset: no tabs needed -->
|
||||||
|
{% for fieldset in spec_fieldsets %}
|
||||||
|
<dl class="row mb-0">
|
||||||
|
{% for field in fieldset.fields %}
|
||||||
|
<dt class="col-sm-4">
|
||||||
|
{{ field.label }}
|
||||||
|
{% if field.help_text %}
|
||||||
|
<i class="bi bi-question-circle text-muted ms-1"
|
||||||
|
data-bs-toggle="popover"
|
||||||
|
data-bs-trigger="hover"
|
||||||
|
data-bs-content="{{ field.help_text }}"></i>
|
||||||
|
{% endif %}
|
||||||
|
</dt>
|
||||||
|
<dd class="col-sm-8">
|
||||||
|
{{ field.value|render_tree }}
|
||||||
|
</dd>
|
||||||
|
{% endfor %}
|
||||||
|
</dl>
|
||||||
|
{% for sub_key, sub_fieldset in fieldset.fieldsets.items %}
|
||||||
|
<h5 class="mt-3">{{ sub_fieldset.title }}</h5>
|
||||||
|
<dl class="row mb-0">
|
||||||
|
{% for field in sub_fieldset.fields %}
|
||||||
|
<dt class="col-sm-4">{{ field.label }}</dt>
|
||||||
|
<dd class="col-sm-8">
|
||||||
|
{{ field.value|render_tree }}
|
||||||
|
</dd>
|
||||||
|
{% endfor %}
|
||||||
|
</dl>
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<!-- Row 3: Expert & Metadata (collapsed) -->
|
||||||
|
{% if instance.status_conditions or instance.created_at %}
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="accordion" id="expertAccordion">
|
||||||
|
<div class="accordion-item">
|
||||||
|
<h2 class="accordion-header" id="expertHeading">
|
||||||
|
<button class="accordion-button collapsed"
|
||||||
|
type="button"
|
||||||
|
data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#expertCollapse"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-controls="expertCollapse">
|
||||||
|
<i class="bi bi-gear me-2"></i>
|
||||||
|
{% translate "Technical Details" %}
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="expertCollapse"
|
||||||
|
class="accordion-collapse collapse"
|
||||||
|
aria-labelledby="expertHeading"
|
||||||
|
data-bs-parent="#expertAccordion">
|
||||||
|
<div class="accordion-body">
|
||||||
|
<div class="row">
|
||||||
|
<!-- Left column: Status Conditions -->
|
||||||
|
<div class="col-12 col-md-8">
|
||||||
|
{% if instance.status_conditions %}
|
||||||
|
<h5 class="mb-3">{% translate "Status Conditions" %}</h5>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-bordered table-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{% translate "Type" %}</th>
|
<th>{% translate "Type" %}</th>
|
||||||
<th>{% translate "Status" %}</th>
|
<th>{% translate "Status" %}</th>
|
||||||
<th>{% translate "Last Transition Time" %}</th>
|
<th>{% translate "Last Transition" %}</th>
|
||||||
<th>{% translate "Reason" %}</th>
|
<th>{% translate "Reason" %}</th>
|
||||||
<th>{% translate "Message" %}</th>
|
<th>{% translate "Message" %}</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -130,74 +362,21 @@
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% if control_plane.user_info %}
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<h4 class="card-title">{% translate "Service Provider Zone Information" %}</h4>
|
|
||||||
</div>
|
|
||||||
<div class="card-content">
|
|
||||||
{% include "includes/control_plane_user_info.html" with control_plane=instance.context.control_plane %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% if instance.spec and spec_fieldsets %}
|
<!-- Right column: Metadata -->
|
||||||
<div class="col-12">
|
<div class="col-12 col-md-4">
|
||||||
<div class="card">
|
<h5 class="mb-3">{% translate "Metadata" %}</h5>
|
||||||
<div class="card-header">
|
|
||||||
<h4>{% translate "Specification" %}</h4>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12">
|
|
||||||
<!-- Tabs -->
|
|
||||||
<ul class="nav nav-tabs" role="tablist">
|
|
||||||
{% for fieldset in spec_fieldsets %}
|
|
||||||
<li class="nav-item" role="presentation">
|
|
||||||
<a href="#spec-tab-{{ forloop.counter }}"
|
|
||||||
class="nav-link {% if forloop.first %}active{% endif %}"
|
|
||||||
data-bs-toggle="tab"
|
|
||||||
role="tab">{{ fieldset.title }}</a>
|
|
||||||
</li>
|
|
||||||
{% empty %}
|
|
||||||
<li class="nav-item ms-2">{% translate "No specification details available." %}</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
<!-- Tab Content -->
|
|
||||||
<div class="tab-content pt-3">
|
|
||||||
{% for fieldset in spec_fieldsets %}
|
|
||||||
<div class="tab-pane fade {% if forloop.first %}show active{% endif %}"
|
|
||||||
id="spec-tab-{{ forloop.counter }}"
|
|
||||||
role="tabpanel">
|
|
||||||
<!-- Main Fields -->
|
|
||||||
<dl class="row">
|
<dl class="row">
|
||||||
{% for field in fieldset.fields %}
|
<dt class="col-sm-5">{% translate "Created By" %}</dt>
|
||||||
<dt class="col-sm-3">{{ field.label }}</dt>
|
<dd class="col-sm-7">{{ instance.created_by|default:"-" }}</dd>
|
||||||
<dd class="col-sm-9">
|
<dt class="col-sm-5">{% translate "Created At" %}</dt>
|
||||||
{{ field.value|render_tree }}
|
<dd class="col-sm-7">{{ instance.created_at|localtime_tag }}</dd>
|
||||||
</dd>
|
<dt class="col-sm-5">{% translate "Updated At" %}</dt>
|
||||||
{% endfor %}
|
<dd class="col-sm-7">{{ instance.updated_at|localtime_tag }}</dd>
|
||||||
</dl>
|
</dl>
|
||||||
<!-- Nested Fieldsets -->
|
|
||||||
{% for sub_key, sub_fieldset in fieldset.fieldsets.items %}
|
|
||||||
<h5>{{ sub_fieldset.title }}</h5>
|
|
||||||
<dl class="row">
|
|
||||||
{% for field in sub_fieldset.fields %}
|
|
||||||
<dt class="col-sm-3">{{ field.label }}</dt>
|
|
||||||
<dd class="col-sm-9">
|
|
||||||
{{ field.value|render_tree }}
|
|
||||||
</dd>
|
|
||||||
{% endfor %}
|
|
||||||
</dl>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
{% empty %}
|
</div>
|
||||||
<p>{% translate "No specification details to display." %}</p>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -205,113 +384,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if instance.connection_credentials %}
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
|
||||||
<h4 class="mb-0">{% translate "Connection Credentials" %}</h4>
|
|
||||||
<button type="button"
|
|
||||||
class="btn btn-sm btn-outline-secondary"
|
|
||||||
id="toggle-credentials-btn"
|
|
||||||
data-show-text="{% translate "Show All" %}"
|
|
||||||
data-hide-text="{% translate "Hide All" %}"
|
|
||||||
onclick="toggleAllCredentials()">
|
|
||||||
<i class="bi bi-eye me-1"></i>
|
|
||||||
<span id="toggle-credentials-text">{% translate "Show All" %}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-bordered">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>{% translate "Name" %}</th>
|
|
||||||
<th>{% translate "Value" %}</th>
|
|
||||||
<th style="width: 50px;"></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for key, value in instance.connection_credentials.items %}
|
|
||||||
<tr>
|
|
||||||
<td>{{ key }}</td>
|
|
||||||
<td>
|
|
||||||
{% if key == "error" %}
|
|
||||||
<span class="text-danger">{{ value }}</span>
|
|
||||||
{% else %}
|
|
||||||
<code class="credential-value" data-value="{{ value }}">••••••••••••</code>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td class="text-center">
|
|
||||||
{% if key != "error" %}
|
|
||||||
<button type="button"
|
|
||||||
class="btn btn-sm btn-link p-0 credential-toggle"
|
|
||||||
onclick="toggleCredential(this)"
|
|
||||||
title="{% translate 'Show/Hide' %}">
|
|
||||||
<i class="bi bi-eye"></i>
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div class="text-muted small mt-2">
|
|
||||||
<i class="bi bi-info-circle me-1"></i>
|
|
||||||
{% translate "Click the eye icon to reveal individual credentials, or use 'Show All' to reveal all at once." %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% if instance.kubernetes_events %}
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<h4>{% translate "Events" %}</h4>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="row">
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-bordered table-sm">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th style="width: 80px;">{% translate "Type" %}</th>
|
|
||||||
<th style="width: 150px;">{% translate "Reason" %}</th>
|
|
||||||
<th>{% translate "Message" %}</th>
|
|
||||||
<th style="width: 60px;">{% translate "Count" %}</th>
|
|
||||||
<th style="width: 180px;">{% translate "Last Seen" %}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for event in instance.kubernetes_events %}
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
{% if event.type == "Warning" %}
|
|
||||||
<span class="badge text-bg-warning">{{ event.type }}</span>
|
|
||||||
{% elif event.type == "Normal" %}
|
|
||||||
<span class="badge text-bg-success">{{ event.type }}</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="badge text-bg-secondary">{{ event.type }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>{{ event.reason|default:"-" }}</td>
|
|
||||||
<td>
|
|
||||||
<span class="text-break">{{ event.message|default:"-"|truncatewords:50 }}</span>
|
|
||||||
</td>
|
|
||||||
<td class="text-center">{{ event.count }}</td>
|
|
||||||
<td>{{ event.last_timestamp|localtime_tag }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
<!-- Delete Confirmation Modal -->
|
<!-- Delete Confirmation Modal -->
|
||||||
<div class="modal fade"
|
<div class="modal fade"
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="plan-header">
|
<div class="plan-header">
|
||||||
<h6>{% trans "Storage" %}</h6>
|
<h6>{% trans "Storage" %}</h6>
|
||||||
<span class="text-muted small">{% trans "Billed separately based on disk usage" %}</span>
|
<span class="text-muted small">{% trans "Billed based on disk usage" %}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% if storage_plan %}
|
{% if storage_plan %}
|
||||||
|
|
|
||||||
|
|
@ -77,10 +77,18 @@ def render_tree(value, key=""):
|
||||||
if isinstance(value, list):
|
if isinstance(value, list):
|
||||||
if not value:
|
if not value:
|
||||||
return mark_safe('<span class="text-muted">[]</span>')
|
return mark_safe('<span class="text-muted">[]</span>')
|
||||||
|
# For simple string/number arrays, render as comma-separated or line-separated
|
||||||
|
if all(isinstance(item, (str, int, float)) for item in value):
|
||||||
|
if len(value) == 1:
|
||||||
|
return format_html("<span>{}</span>", value[0])
|
||||||
|
# Multiple items: render each on its own line
|
||||||
|
items = [format_html("<div>{}</div>", item) for item in value]
|
||||||
|
return mark_safe("".join(items))
|
||||||
|
# For complex arrays (dicts, nested lists), use list structure
|
||||||
items = []
|
items = []
|
||||||
for item in value:
|
for item in value:
|
||||||
items.append(f"<li>{render_tree(item)}</li>")
|
items.append(f"<li>{render_tree(item)}</li>")
|
||||||
return mark_safe(f'<ul class="list-unstyled mb-0 ps-3">{"".join(items)}</ul>')
|
return mark_safe(f'<ul class="list-unstyled mb-0">{"".join(items)}</ul>')
|
||||||
|
|
||||||
if isinstance(value, dict):
|
if isinstance(value, dict):
|
||||||
if not value:
|
if not value:
|
||||||
|
|
|
||||||
|
|
@ -377,17 +377,182 @@ class ServiceInstanceDetailView(
|
||||||
"price_per_gib": self.object.context.control_plane.storage_plan_price_per_gib,
|
"price_per_gib": self.object.context.control_plane.storage_plan_price_per_gib,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Calculate pricing summary
|
||||||
|
context["pricing"] = self._calculate_pricing()
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def get_nested_spec(self):
|
def _parse_disk_size_gib(self, disk_value):
|
||||||
|
"""Parse disk size string (e.g., '10Gi', '100G') to GiB as integer."""
|
||||||
|
if not disk_value:
|
||||||
|
return None
|
||||||
|
disk_str = str(disk_value)
|
||||||
|
# Handle Gi suffix (GiB)
|
||||||
|
if disk_str.endswith("Gi"):
|
||||||
|
try:
|
||||||
|
return int(disk_str[:-2])
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
# Handle G suffix (assume GB, convert to GiB approximately)
|
||||||
|
if disk_str.endswith("G"):
|
||||||
|
try:
|
||||||
|
return int(float(disk_str[:-1]) * 0.931) # GB to GiB
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
# Handle plain number (assume GiB)
|
||||||
|
try:
|
||||||
|
return int(disk_str)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _calculate_pricing(self):
|
||||||
|
"""Calculate hourly and monthly pricing for the instance."""
|
||||||
|
from decimal import Decimal, ROUND_HALF_UP
|
||||||
|
|
||||||
|
pricing = {
|
||||||
|
"compute_hourly": None,
|
||||||
|
"compute_monthly": None,
|
||||||
|
"storage_hourly": None,
|
||||||
|
"storage_monthly": None,
|
||||||
|
"total_hourly": None,
|
||||||
|
"total_monthly": None,
|
||||||
|
"disk_size_gib": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
hours_per_month = 720
|
||||||
|
|
||||||
|
# Compute plan pricing
|
||||||
|
compute_assignment = self.object.compute_plan_assignment
|
||||||
|
if compute_assignment:
|
||||||
|
price = compute_assignment.price
|
||||||
|
unit = compute_assignment.unit
|
||||||
|
|
||||||
|
# Convert to hourly
|
||||||
|
if unit == "hour":
|
||||||
|
hourly = price
|
||||||
|
elif unit == "day":
|
||||||
|
hourly = price / Decimal("24")
|
||||||
|
elif unit == "month":
|
||||||
|
hourly = price / Decimal(hours_per_month)
|
||||||
|
elif unit == "year":
|
||||||
|
hourly = price / Decimal(hours_per_month * 12)
|
||||||
|
else:
|
||||||
|
hourly = price # fallback
|
||||||
|
|
||||||
|
pricing["compute_hourly"] = hourly.quantize(
|
||||||
|
Decimal("0.01"), rounding=ROUND_HALF_UP
|
||||||
|
)
|
||||||
|
pricing["compute_monthly"] = (hourly * hours_per_month).quantize(
|
||||||
|
Decimal("0.01"), rounding=ROUND_HALF_UP
|
||||||
|
)
|
||||||
|
|
||||||
|
# Storage pricing
|
||||||
|
storage_price_per_gib = None
|
||||||
|
if (
|
||||||
|
self.object.context
|
||||||
|
and self.object.context.control_plane.storage_plan_price_per_gib
|
||||||
|
):
|
||||||
|
storage_price_per_gib = (
|
||||||
|
self.object.context.control_plane.storage_plan_price_per_gib
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get disk size from spec
|
||||||
|
disk_size_gib = None
|
||||||
|
if self.object.spec:
|
||||||
|
disk_value = self._get_value_from_path(
|
||||||
|
{"spec": self.object.spec}, "spec.parameters.size.disk"
|
||||||
|
)
|
||||||
|
disk_size_gib = self._parse_disk_size_gib(disk_value)
|
||||||
|
pricing["disk_size_gib"] = disk_size_gib
|
||||||
|
|
||||||
|
if storage_price_per_gib and disk_size_gib:
|
||||||
|
storage_hourly = storage_price_per_gib * disk_size_gib
|
||||||
|
pricing["storage_hourly"] = storage_hourly.quantize(
|
||||||
|
Decimal("0.01"), rounding=ROUND_HALF_UP
|
||||||
|
)
|
||||||
|
pricing["storage_monthly"] = (storage_hourly * hours_per_month).quantize(
|
||||||
|
Decimal("0.01"), rounding=ROUND_HALF_UP
|
||||||
|
)
|
||||||
|
|
||||||
|
# Total pricing
|
||||||
|
if pricing["compute_hourly"] is not None or pricing["storage_hourly"] is not None:
|
||||||
|
compute_h = pricing["compute_hourly"] or Decimal("0")
|
||||||
|
storage_h = pricing["storage_hourly"] or Decimal("0")
|
||||||
|
pricing["total_hourly"] = (compute_h + storage_h).quantize(
|
||||||
|
Decimal("0.01"), rounding=ROUND_HALF_UP
|
||||||
|
)
|
||||||
|
pricing["total_monthly"] = (pricing["total_hourly"] * hours_per_month).quantize(
|
||||||
|
Decimal("0.01"), rounding=ROUND_HALF_UP
|
||||||
|
)
|
||||||
|
|
||||||
|
return pricing
|
||||||
|
|
||||||
|
def _get_value_from_path(self, data, path):
|
||||||
"""
|
"""
|
||||||
Organize spec data into fieldsets similar to how the form does it.
|
Get a value from nested dict using dot notation path.
|
||||||
|
e.g., 'spec.parameters.size.disk' from {'spec': {'parameters': {'size': {'disk': '10Gi'}}}}
|
||||||
|
"""
|
||||||
|
parts = path.split(".")
|
||||||
|
current = data
|
||||||
|
for part in parts:
|
||||||
|
if isinstance(current, dict) and part in current:
|
||||||
|
current = current[part]
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
return current
|
||||||
|
|
||||||
|
def _get_form_config_fieldsets(self):
|
||||||
|
"""
|
||||||
|
Generate fieldsets from form_config, extracting values from spec.
|
||||||
|
"""
|
||||||
|
form_config = self.object.context.service_definition.form_config
|
||||||
|
spec = self.object.spec or {}
|
||||||
|
full_data = {"spec": spec, "name": self.object.name}
|
||||||
|
|
||||||
|
fieldsets = []
|
||||||
|
for fieldset_config in form_config.get("fieldsets", []):
|
||||||
|
fields = []
|
||||||
|
for field_config in fieldset_config.get("fields", []):
|
||||||
|
mapping = field_config.get("controlplane_field_mapping", "")
|
||||||
|
# Skip the name field as it's shown in the header
|
||||||
|
if mapping == "name":
|
||||||
|
continue
|
||||||
|
|
||||||
|
value = self._get_value_from_path(full_data, mapping)
|
||||||
|
if value is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
label = field_config.get("label") or deslugify(mapping.split(".")[-1])
|
||||||
|
|
||||||
|
fields.append(
|
||||||
|
{
|
||||||
|
"key": mapping,
|
||||||
|
"label": label,
|
||||||
|
"value": value,
|
||||||
|
"help_text": field_config.get("help_text", ""),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if fields:
|
||||||
|
fieldsets.append(
|
||||||
|
{
|
||||||
|
"title": fieldset_config.get("title", "Configuration"),
|
||||||
|
"fields": fields,
|
||||||
|
"fieldsets": {},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return fieldsets
|
||||||
|
|
||||||
|
def _get_auto_generated_fieldsets(self):
|
||||||
|
"""
|
||||||
|
Auto-generate fieldsets from spec structure (fallback when no form_config).
|
||||||
|
Excludes "General" tab - only returns nested structures.
|
||||||
"""
|
"""
|
||||||
spec = self.object.spec or {}
|
spec = self.object.spec or {}
|
||||||
if not spec:
|
if not spec:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
others = []
|
|
||||||
nested_fieldsets = {}
|
nested_fieldsets = {}
|
||||||
|
|
||||||
# First pass: organize fields into nested structures
|
# First pass: organize fields into nested structures
|
||||||
|
|
@ -431,15 +596,6 @@ class ServiceInstanceDetailView(
|
||||||
"value": sub_value,
|
"value": sub_value,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
# This is a top-level field
|
|
||||||
others.append(
|
|
||||||
{
|
|
||||||
"key": key,
|
|
||||||
"label": deslugify(key),
|
|
||||||
"value": value,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Second pass: Promote fields based on count
|
# Second pass: Promote fields based on count
|
||||||
for group_key, group in list(nested_fieldsets.items()):
|
for group_key, group in list(nested_fieldsets.items()):
|
||||||
|
|
@ -451,38 +607,40 @@ class ServiceInstanceDetailView(
|
||||||
group["fields"].append(field)
|
group["fields"].append(field)
|
||||||
del group["fieldsets"][sub_key]
|
del group["fieldsets"][sub_key]
|
||||||
|
|
||||||
# Move single-field groups to others
|
# Remove empty groups
|
||||||
total_fields = len(group["fields"])
|
total_fields = len(group["fields"])
|
||||||
for sub_fieldset in group["fieldsets"].values():
|
for sub_fieldset in group["fieldsets"].values():
|
||||||
total_fields += len(sub_fieldset["fields"])
|
total_fields += len(sub_fieldset["fields"])
|
||||||
|
|
||||||
if (
|
if total_fields == 0:
|
||||||
total_fields == 1
|
|
||||||
and len(group["fields"]) == 1
|
|
||||||
and not group["fieldsets"]
|
|
||||||
):
|
|
||||||
field = group["fields"][0]
|
|
||||||
field["label"] = f"{group['title']}: {field['label']}"
|
|
||||||
others.append(field)
|
|
||||||
del nested_fieldsets[group_key]
|
|
||||||
elif total_fields == 0:
|
|
||||||
del nested_fieldsets[group_key]
|
del nested_fieldsets[group_key]
|
||||||
|
|
||||||
|
# Create fieldsets from the organized data (no "General" tab)
|
||||||
fieldsets = []
|
fieldsets = []
|
||||||
if others:
|
|
||||||
fieldsets.append(
|
|
||||||
{
|
|
||||||
"title": "General",
|
|
||||||
"fields": others,
|
|
||||||
"fieldsets": {},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
# Create fieldsets from the organized data
|
|
||||||
for group in nested_fieldsets.values():
|
for group in nested_fieldsets.values():
|
||||||
fieldsets.append(group)
|
fieldsets.append(group)
|
||||||
|
|
||||||
return fieldsets
|
return fieldsets
|
||||||
|
|
||||||
|
def get_nested_spec(self):
|
||||||
|
"""
|
||||||
|
Organize spec data into fieldsets.
|
||||||
|
Uses form_config when available, otherwise auto-generates from spec structure.
|
||||||
|
"""
|
||||||
|
spec = self.object.spec or {}
|
||||||
|
if not spec:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Check if form_config exists
|
||||||
|
if (
|
||||||
|
self.object.context
|
||||||
|
and self.object.context.service_definition
|
||||||
|
and self.object.context.service_definition.form_config
|
||||||
|
):
|
||||||
|
return self._get_form_config_fieldsets()
|
||||||
|
|
||||||
|
return self._get_auto_generated_fieldsets()
|
||||||
|
|
||||||
|
|
||||||
class ServiceInstanceUpdateView(
|
class ServiceInstanceUpdateView(
|
||||||
ServiceInstanceMixin, OrganizationViewMixin, HtmxUpdateView
|
ServiceInstanceMixin, OrganizationViewMixin, HtmxUpdateView
|
||||||
|
|
|
||||||
|
|
@ -361,3 +361,9 @@ html[data-bs-theme="dark"] .beta-banner-button:hover {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
color: var(--bs-primary);
|
color: var(--bs-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Fixed table layout for credentials */
|
||||||
|
.table-fixed {
|
||||||
|
table-layout: fixed;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue