gateway api fixes

This commit is contained in:
Nils Blomgren
2026-05-21 16:57:50 +02:00
parent 8b94fb8826
commit e0f967482e
68 changed files with 44082 additions and 437 deletions

View File

@@ -3,6 +3,7 @@
@using Microsoft.EntityFrameworkCore
@inject TenantService TenantService
@inject DeploymentService DeploymentService
@inject CnpgService CnpgService
@* ═══════════════════════════════════════════════════════════════════
App Detail — a full-page view for a single application.
@@ -226,6 +227,16 @@
<i class="bi bi-diagram-3 me-1"></i>Resources
</button>
</li>
<li class="nav-item">
<button class="nav-link @(deploymentSection == "databases" ? "active" : "")"
@onclick="OpenDatabasesTab">
<i class="bi bi-database me-1"></i>Databases
@if (databaseBindings is not null && databaseBindings.Count > 0)
{
<span class="badge bg-primary ms-1">@databaseBindings.Count</span>
}
</button>
</li>
</ul>
@* ── Manifests sub-tab ── *@
@@ -393,6 +404,123 @@
</div>
</div>
}
@* ── Databases sub-tab ── *@
@if (deploymentSection == "databases")
{
<div class="card shadow-sm">
<div class="card-header bg-white py-2 d-flex align-items-center justify-content-between">
<div>
<i class="bi bi-database me-2 text-primary"></i>
<strong>Database Bindings</strong>
</div>
<button class="btn btn-sm btn-outline-primary" @onclick="() => showAddBinding = !showAddBinding">
<i class="bi bi-plus me-1"></i>Attach Database
</button>
</div>
<div class="card-body">
@if (!string.IsNullOrEmpty(bindingError))
{
<div class="alert alert-danger py-1 small mb-2">@bindingError</div>
}
@* --- Add binding form --- *@
@if (showAddBinding)
{
<div class="card border-primary mb-3">
<div class="card-body">
<h6 class="mb-3"><i class="bi bi-plus-circle me-1"></i>Attach a PostgreSQL Database</h6>
<div class="row g-2 mb-2">
<div class="col-md-6">
<label class="form-label small">Database</label>
<select class="form-select form-select-sm" @bind="newBindingDatabaseId">
<option value="">Select database...</option>
@if (availableCnpgDatabases is not null)
{
@foreach ((CnpgCluster cluster, CnpgDatabase database) in availableCnpgDatabases)
{
<option value="@database.Id">@cluster.Name / @database.Name</option>
}
}
</select>
</div>
<div class="col-md-6">
<label class="form-label small">Secret name in app namespace</label>
<input class="form-control form-control-sm" @bind="newBindingSecretName"
placeholder="e.g. my-app-db" />
<div class="form-text">The K8s Secret created in <code>@selectedDeployment?.Namespace</code>.</div>
</div>
</div>
<div class="d-flex gap-1">
<button class="btn btn-sm btn-primary" @onclick="AddDatabaseBinding"
disabled="@(newBindingDatabaseId == Guid.Empty || string.IsNullOrWhiteSpace(newBindingSecretName))">
Attach &amp; Sync
</button>
<button class="btn btn-sm btn-outline-secondary" @onclick="() => showAddBinding = false">Cancel</button>
</div>
</div>
</div>
}
@* --- Binding list --- *@
@if (databaseBindings is null)
{
<div class="text-center py-3">
<div class="spinner-border spinner-border-sm text-primary" role="status"></div>
</div>
}
else if (databaseBindings.Count == 0)
{
<div class="text-center py-4">
<i class="bi bi-database text-muted" style="font-size: 2rem;"></i>
<p class="text-muted small mt-2 mb-0">No databases attached. Attach a database to have its credentials automatically synced to this deployment's namespace.</p>
</div>
}
else
{
<table class="table table-sm mb-0">
<thead class="table-light">
<tr>
<th class="small">Database</th>
<th class="small">Secret name</th>
<th class="small">Last synced</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (DatabaseBinding binding in databaseBindings)
{
<tr>
<td class="small">
@if (binding.CnpgDatabase is not null)
{
<i class="bi bi-database me-1 text-primary"></i>
@binding.CnpgDatabase.CnpgCluster.Name<span class="text-muted"> / </span>@binding.CnpgDatabase.Name
}
else if (binding.MongoDatabase is not null)
{
<i class="bi bi-database me-1 text-success"></i>
@binding.MongoDatabase.MongoCluster.Name<span class="text-muted"> / </span>@binding.MongoDatabase.Name
}
</td>
<td><code class="small">@binding.KubernetesSecretName</code></td>
<td class="small text-muted">
@(binding.LastSyncedAt.HasValue ? binding.LastSyncedAt.Value.ToString("yyyy-MM-dd HH:mm") : "Never")
</td>
<td>
<button class="btn btn-link btn-sm text-danger p-0" title="Detach"
@onclick="() => RemoveDatabaseBinding(binding.Id)">
<i class="bi bi-x-circle" style="font-size:0.85rem;"></i>
</button>
</td>
</tr>
}
</tbody>
</table>
}
</div>
</div>
}
}
else
{
@@ -602,6 +730,14 @@
// Resource tree
private List<DeploymentResource>? resourceTree;
// Database bindings
private List<DatabaseBinding>? databaseBindings;
private List<(CnpgCluster Cluster, CnpgDatabase Database)>? availableCnpgDatabases;
private bool showAddBinding;
private Guid newBindingDatabaseId;
private string newBindingSecretName = "";
private string? bindingError;
protected override async Task OnInitializedAsync()
{
await LoadEnvironments();
@@ -790,6 +926,80 @@
}
}
// ──────── Database bindings ────────
private async Task OpenDatabasesTab()
{
deploymentSection = "databases";
if (selectedDeployment is not null)
{
databaseBindings = await CnpgService.GetDatabaseBindingsAsync(selectedDeployment.Id);
if (availableCnpgDatabases is null)
{
List<CnpgCluster> clusters = await CnpgService.GetClustersAsync(TenantId);
availableCnpgDatabases = clusters
.SelectMany(c => c.Databases.Select(d => (c, d)))
.ToList();
}
}
}
private async Task AddDatabaseBinding()
{
if (selectedDeployment is null || newBindingDatabaseId == Guid.Empty)
{
return;
}
bindingError = null;
try
{
DatabaseBinding binding = await CnpgService.AddCnpgDatabaseBindingAsync(
selectedDeployment.Id, newBindingDatabaseId, newBindingSecretName);
// Find the cluster that owns this database so we can call sync.
(CnpgCluster cluster, CnpgDatabase _)? match = availableCnpgDatabases?
.FirstOrDefault(x => x.Database.Id == newBindingDatabaseId);
if (match.HasValue)
{
await CnpgService.SyncDatabaseCredentialsToK8sAsync(
TenantId, match.Value.cluster.Id, newBindingDatabaseId);
}
showAddBinding = false;
newBindingDatabaseId = Guid.Empty;
newBindingSecretName = "";
databaseBindings = await CnpgService.GetDatabaseBindingsAsync(selectedDeployment.Id);
}
catch (Exception ex)
{
bindingError = $"Failed to attach database: {ex.Message}";
}
}
private async Task RemoveDatabaseBinding(Guid bindingId)
{
bindingError = null;
try
{
await CnpgService.RemoveDatabaseBindingAsync(bindingId);
if (selectedDeployment is not null)
{
databaseBindings = await CnpgService.GetDatabaseBindingsAsync(selectedDeployment.Id);
}
}
catch (Exception ex)
{
bindingError = $"Failed to detach database: {ex.Message}";
}
}
// ──────── Rendering helpers ────────
private RenderFragment GetTypeBadge(DeploymentType type) => type switch