gateway api fixes
This commit is contained in:
@@ -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 & 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
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1703
src/EntKube.Web/Components/Pages/Tenants/IdentityTab.razor
Normal file
1703
src/EntKube.Web/Components/Pages/Tenants/IdentityTab.razor
Normal file
File diff suppressed because it is too large
Load Diff
@@ -172,6 +172,19 @@ else
|
||||
title="Manage bucket (CORS, Policy)">
|
||||
<i class="bi bi-gear"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-info border-0 py-0"
|
||||
@onclick="() => BrowseBuckets(link)"
|
||||
disabled="@(browsingLinkId.HasValue)"
|
||||
title="Browse all buckets accessible with these credentials">
|
||||
@if (browsingLinkId == link.Id)
|
||||
{
|
||||
<span class="spinner-border spinner-border-sm"></span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<i class="bi bi-list-ul"></i>
|
||||
}
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger border-0 py-0"
|
||||
@onclick="() => DeleteCleuraS3Bucket(link.Id)"
|
||||
disabled="@deletingLinkId.HasValue"
|
||||
@@ -327,6 +340,82 @@ else
|
||||
</div>
|
||||
}
|
||||
|
||||
@* ── Browse Buckets Panel ── *@
|
||||
@if (browsedLink is not null)
|
||||
{
|
||||
<div class="card shadow-sm mb-4 border-secondary">
|
||||
<div class="card-header bg-secondary bg-opacity-10 d-flex align-items-center justify-content-between">
|
||||
<h6 class="mb-0">
|
||||
<i class="bi bi-list-ul me-2"></i>Buckets accessible via <strong>@browsedLink.Name</strong>
|
||||
</h6>
|
||||
<button class="btn btn-sm btn-outline-secondary border-0" @onclick="CloseBrowseBuckets">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if (!string.IsNullOrEmpty(browseError))
|
||||
{
|
||||
<div class="alert alert-danger small py-2">
|
||||
<i class="bi bi-exclamation-triangle me-1"></i>@browseError
|
||||
</div>
|
||||
}
|
||||
else if (browsedBuckets is null)
|
||||
{
|
||||
<div class="text-center py-3">
|
||||
<div class="spinner-border spinner-border-sm text-secondary"></div>
|
||||
<span class="ms-2 text-muted small">Loading buckets...</span>
|
||||
</div>
|
||||
}
|
||||
else if (browsedBuckets.Count == 0)
|
||||
{
|
||||
<p class="text-muted small mb-0">No buckets found under this project.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th class="small">Bucket Name</th>
|
||||
<th class="small">Created</th>
|
||||
<th class="small">Registered in EntKube</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (S3BucketInfo bucket in browsedBuckets)
|
||||
{
|
||||
bool registered = storageLinks.Any(l =>
|
||||
l.Provider == StorageProvider.CleuraS3 &&
|
||||
l.BucketName == bucket.Name);
|
||||
<tr>
|
||||
<td class="small font-monospace">@bucket.Name</td>
|
||||
<td class="small text-muted">
|
||||
@(bucket.CreatedAt == default ? "—" : bucket.CreatedAt.ToString("yyyy-MM-dd"))
|
||||
</td>
|
||||
<td class="small">
|
||||
@if (registered)
|
||||
{
|
||||
<span class="badge bg-success-subtle text-success border border-success-subtle">Yes</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-secondary-subtle text-secondary border">No</span>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="text-muted small mt-2 mb-0">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
@browsedBuckets.Count bucket(s) accessible. Buckets not registered in EntKube were created outside the platform.
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@* ── Add Storage Link Form ── *@
|
||||
@if (showForm)
|
||||
{
|
||||
@@ -652,6 +741,12 @@ else
|
||||
private int corsMaxAge = 3600;
|
||||
private string bucketPolicyJson = "";
|
||||
|
||||
// Browse buckets state
|
||||
private StorageLink? browsedLink;
|
||||
private Guid? browsingLinkId;
|
||||
private List<S3BucketInfo>? browsedBuckets;
|
||||
private string? browseError;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadData();
|
||||
@@ -846,6 +941,35 @@ else
|
||||
manageSuccess = null;
|
||||
}
|
||||
|
||||
private async Task BrowseBuckets(StorageLink link)
|
||||
{
|
||||
browsingLinkId = link.Id;
|
||||
browsedLink = link;
|
||||
browsedBuckets = null;
|
||||
browseError = null;
|
||||
StateHasChanged();
|
||||
|
||||
try
|
||||
{
|
||||
browsedBuckets = await StorageService.ListCleuraS3BucketsAsync(TenantId, link.Id);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
browseError = $"Failed to list buckets: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
browsingLinkId = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void CloseBrowseBuckets()
|
||||
{
|
||||
browsedLink = null;
|
||||
browsedBuckets = null;
|
||||
browseError = null;
|
||||
}
|
||||
|
||||
private async Task LoadCors()
|
||||
{
|
||||
if (managedLink is null)
|
||||
|
||||
@@ -63,6 +63,11 @@ else
|
||||
<i class="bi bi-bucket me-1"></i>Storage
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link @(activeTab == "identity" ? "active" : "")" @onclick='() => activeTab = "identity"'>
|
||||
<i class="bi bi-person-badge me-1"></i>Identity
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
@@ -86,6 +91,9 @@ else
|
||||
case "storage":
|
||||
<StorageTab TenantId="tenant.Id" />
|
||||
break;
|
||||
case "identity":
|
||||
<IdentityTab TenantId="tenant.Id" />
|
||||
break;
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user