server management
This commit is contained in:
@@ -6,6 +6,8 @@
|
||||
@inject TenantService TenantService
|
||||
@inject StorageService StorageService
|
||||
@inject CnpgService CnpgService
|
||||
@inject MongoService MongoService
|
||||
@inject RegisteredPostgresService RegisteredPostgresService
|
||||
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<i class="bi bi-shield-lock fs-4 me-2 text-primary"></i>
|
||||
@@ -42,7 +44,13 @@ else
|
||||
<i class="bi bi-hdd me-1"></i>Storage Secrets
|
||||
</button>
|
||||
<button class="btn btn-sm @(scope == "cnpg" ? "btn-primary" : "btn-outline-primary")" @onclick='() => SwitchScope("cnpg")'>
|
||||
<i class="bi bi-database me-1"></i>Database Secrets
|
||||
<i class="bi bi-database me-1"></i>PostgreSQL
|
||||
</button>
|
||||
<button class="btn btn-sm @(scope == "mongodb" ? "btn-primary" : "btn-outline-primary")" @onclick='() => SwitchScope("mongodb")'>
|
||||
<i class="bi bi-database me-1"></i>MongoDB
|
||||
</button>
|
||||
<button class="btn btn-sm @(scope == "regpostgres" ? "btn-primary" : "btn-outline-primary")" @onclick='() => SwitchScope("regpostgres")'>
|
||||
<i class="bi bi-database me-1"></i>Registered DB
|
||||
</button>
|
||||
<button class="btn btn-sm @(scope == "docker" ? "btn-primary" : "btn-outline-primary")" @onclick='() => SwitchScope("docker")'>
|
||||
<i class="bi bi-box-seam me-1"></i>Docker Registries
|
||||
@@ -153,6 +161,58 @@ else
|
||||
}
|
||||
}
|
||||
|
||||
else if (scope == "mongodb")
|
||||
{
|
||||
@if (mongoClusterOptions is not null && mongoClusterOptions.Count > 0)
|
||||
{
|
||||
<div class="card border-0 shadow-sm mb-3">
|
||||
<div class="card-body py-2">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-transparent"><i class="bi bi-database"></i></span>
|
||||
<select class="form-select" @bind="selectedMongoClusterId" @bind:after="LoadSecrets">
|
||||
<option value="@Guid.Empty">Select a MongoDB cluster...</option>
|
||||
@foreach ((string label, Guid id) in mongoClusterOptions)
|
||||
{
|
||||
<option value="@id">@label</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<EmptyState Icon="bi-database"
|
||||
Message="No MongoDB clusters found for this tenant." />
|
||||
}
|
||||
}
|
||||
|
||||
else if (scope == "regpostgres")
|
||||
{
|
||||
@if (regPostgresOptions is not null && regPostgresOptions.Count > 0)
|
||||
{
|
||||
<div class="card border-0 shadow-sm mb-3">
|
||||
<div class="card-body py-2">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-transparent"><i class="bi bi-database"></i></span>
|
||||
<select class="form-select" @bind="selectedRegPostgresInstanceId" @bind:after="LoadSecrets">
|
||||
<option value="@Guid.Empty">Select a registered Postgres instance...</option>
|
||||
@foreach ((string label, Guid id) in regPostgresOptions)
|
||||
{
|
||||
<option value="@id">@label</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<EmptyState Icon="bi-database"
|
||||
Message="No registered Postgres instances found for this tenant." />
|
||||
}
|
||||
}
|
||||
|
||||
@* --- Docker Registries Panel --- *@
|
||||
@if (scope == "docker")
|
||||
{
|
||||
@@ -179,7 +239,9 @@ else
|
||||
}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@* Add secret form *@
|
||||
@* Add secret form — hidden for read-only DB scopes *@
|
||||
@if (scope is not "mongodb" and not "regpostgres")
|
||||
{
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-md-4">
|
||||
<div class="input-group input-group-sm">
|
||||
@@ -202,6 +264,7 @@ else
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrEmpty(errorMessage))
|
||||
{
|
||||
@@ -231,9 +294,9 @@ else
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
@if (scope == "cnpg")
|
||||
@if (scope is "cnpg" or "mongodb" or "regpostgres")
|
||||
{
|
||||
<th>Scope</th>
|
||||
<th>Database</th>
|
||||
}
|
||||
<th>K8s Sync</th>
|
||||
<th>K8s Secret</th>
|
||||
@@ -247,13 +310,21 @@ else
|
||||
{
|
||||
<tr>
|
||||
<td><code>@secret.Name</code></td>
|
||||
@if (scope == "cnpg")
|
||||
@if (scope is "cnpg" or "mongodb" or "regpostgres")
|
||||
{
|
||||
<td>
|
||||
@if (secret.CnpgDatabase is not null)
|
||||
{
|
||||
<span class="badge bg-info-subtle text-info border border-info-subtle">@secret.CnpgDatabase.Name</span>
|
||||
}
|
||||
else if (secret.MongoDatabase is not null)
|
||||
{
|
||||
<span class="badge bg-success-subtle text-success border border-success-subtle">@secret.MongoDatabase.Name</span>
|
||||
}
|
||||
else if (secret.RegisteredPostgresDatabase is not null)
|
||||
{
|
||||
<span class="badge bg-primary-subtle text-primary border border-primary-subtle">@secret.RegisteredPostgresDatabase.Name</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-secondary-subtle text-secondary border border-secondary-subtle">cluster</span>
|
||||
@@ -281,6 +352,9 @@ else
|
||||
<button class="btn btn-sm btn-outline-warning me-1" @onclick="() => StartEdit(secret)" title="Edit value">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-secondary me-1" @onclick="() => ToggleHistory(secret.Id)" title="Version history">
|
||||
<i class="bi bi-clock-history"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-info me-1" @onclick="() => ToggleSync(secret)"
|
||||
title="@(secret.SyncToKubernetes ? "Disable K8s sync" : "Enable K8s sync")">
|
||||
<i class="bi @(secret.SyncToKubernetes ? "bi-cloud-slash" : "bi-cloud-arrow-up")"></i>
|
||||
@@ -328,6 +402,81 @@ else
|
||||
</tr>
|
||||
}
|
||||
|
||||
@* Version history row *@
|
||||
@if (historySecretId == secret.Id)
|
||||
{
|
||||
<tr>
|
||||
<td colspan="7" class="bg-light p-0">
|
||||
<div class="p-3">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<i class="bi bi-clock-history me-2 text-secondary"></i>
|
||||
<strong class="small">Version History</strong>
|
||||
<span class="badge bg-secondary ms-2">@(secretVersions?.Count ?? 0)</span>
|
||||
</div>
|
||||
@if (secretVersions is null)
|
||||
{
|
||||
<div class="spinner-border spinner-border-sm text-secondary" role="status"></div>
|
||||
}
|
||||
else if (secretVersions.Count == 0)
|
||||
{
|
||||
<p class="text-muted small mb-0">No previous versions recorded.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<table class="table table-sm table-bordered mb-0">
|
||||
<thead class="table-secondary">
|
||||
<tr>
|
||||
<th style="width: 60px;">#</th>
|
||||
<th>Set by</th>
|
||||
<th>Date</th>
|
||||
<th class="text-end" style="width: 160px;">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (VaultSecretVersion ver in secretVersions)
|
||||
{
|
||||
<tr>
|
||||
<td><span class="badge bg-secondary">v@ver.VersionNumber</span></td>
|
||||
<td><small class="text-muted">@(ver.CreatedBy ?? "—")</small></td>
|
||||
<td><small class="text-muted">@ver.CreatedAt.ToString("MMM d, HH:mm")</small></td>
|
||||
<td class="text-end">
|
||||
<button class="btn btn-xs btn-outline-secondary me-1"
|
||||
@onclick="() => RevealVersion(secret.Id, ver)"
|
||||
title="@(revealedVersionId == ver.Id ? "Hide" : "Reveal value")">
|
||||
<i class="bi @(revealedVersionId == ver.Id ? "bi-eye-slash" : "bi-eye")"></i>
|
||||
</button>
|
||||
<button class="btn btn-xs btn-outline-warning"
|
||||
@onclick="() => RollbackToVersion(secret.Id, ver.Id)"
|
||||
title="Restore this value">
|
||||
<i class="bi bi-arrow-counterclockwise me-1"></i>Restore
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@if (revealedVersionId == ver.Id)
|
||||
{
|
||||
<tr class="table-light">
|
||||
<td colspan="4" class="px-2 py-1">
|
||||
<span class="text-muted small me-2">Value:</span>
|
||||
@if (versionRevealLoading)
|
||||
{
|
||||
<span class="spinner-border spinner-border-sm"></span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<code class="user-select-all">@revealedVersionValue</code>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
|
||||
@if (syncConfigSecretId == secret.Id)
|
||||
{
|
||||
<tr>
|
||||
@@ -402,6 +551,14 @@ else
|
||||
private List<(string Label, Guid Id)>? cnpgClusterOptions;
|
||||
private Guid selectedCnpgClusterId;
|
||||
|
||||
// MongoDB cluster scope
|
||||
private List<(string Label, Guid Id)>? mongoClusterOptions;
|
||||
private Guid selectedMongoClusterId;
|
||||
|
||||
// Registered Postgres scope
|
||||
private List<(string Label, Guid Id)>? regPostgresOptions;
|
||||
private Guid selectedRegPostgresInstanceId;
|
||||
|
||||
// Cluster options (used in app-scope sync config)
|
||||
private List<(string Label, Guid Id)>? clusterOptions;
|
||||
|
||||
@@ -426,6 +583,13 @@ else
|
||||
private Guid? editSecretId;
|
||||
private string editSecretValue = "";
|
||||
|
||||
// Version history state
|
||||
private Guid? historySecretId;
|
||||
private List<VaultSecretVersion>? secretVersions;
|
||||
private Guid? revealedVersionId;
|
||||
private string? revealedVersionValue;
|
||||
private bool versionRevealLoading;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
SecretVault? vault = await VaultService.GetVaultAsync(TenantId);
|
||||
@@ -486,6 +650,18 @@ else
|
||||
cnpgClusterOptions = cnpgClusters
|
||||
.Select(c => ($"{c.KubernetesCluster.Name} / {c.Name}", c.Id))
|
||||
.ToList();
|
||||
|
||||
// MongoDB clusters
|
||||
List<MongoCluster> mongoClusters = await MongoService.GetClustersAsync(TenantId);
|
||||
mongoClusterOptions = mongoClusters
|
||||
.Select(c => ($"{c.KubernetesCluster.Name} / {c.Name}", c.Id))
|
||||
.ToList();
|
||||
|
||||
// Registered Postgres instances
|
||||
List<RegisteredPostgresInstance> regInstances = await RegisteredPostgresService.GetInstancesAsync(TenantId);
|
||||
regPostgresOptions = regInstances
|
||||
.Select(i => (i.Name, i.Id))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private void SwitchScope(string newScope)
|
||||
@@ -496,6 +672,8 @@ else
|
||||
selectedComponentId = Guid.Empty;
|
||||
selectedStorageLinkId = Guid.Empty;
|
||||
selectedCnpgClusterId = Guid.Empty;
|
||||
selectedMongoClusterId = Guid.Empty;
|
||||
selectedRegPostgresInstanceId = Guid.Empty;
|
||||
errorMessage = null;
|
||||
successMessage = null;
|
||||
syncClusterId = Guid.Empty;
|
||||
@@ -508,7 +686,9 @@ else
|
||||
return (scope == "app" && selectedAppId != Guid.Empty)
|
||||
|| (scope == "component" && selectedComponentId != Guid.Empty)
|
||||
|| (scope == "storage" && selectedStorageLinkId != Guid.Empty)
|
||||
|| (scope == "cnpg" && selectedCnpgClusterId != Guid.Empty);
|
||||
|| (scope == "cnpg" && selectedCnpgClusterId != Guid.Empty)
|
||||
|| (scope == "mongodb" && selectedMongoClusterId != Guid.Empty)
|
||||
|| (scope == "regpostgres" && selectedRegPostgresInstanceId != Guid.Empty);
|
||||
}
|
||||
|
||||
private async Task LoadSecrets()
|
||||
@@ -532,6 +712,14 @@ else
|
||||
{
|
||||
secrets = await VaultService.GetAllCnpgSecretsForClusterAsync(TenantId, selectedCnpgClusterId);
|
||||
}
|
||||
else if (scope == "mongodb" && selectedMongoClusterId != Guid.Empty)
|
||||
{
|
||||
secrets = await VaultService.GetAllMongoSecretsForClusterAsync(TenantId, selectedMongoClusterId);
|
||||
}
|
||||
else if (scope == "regpostgres" && selectedRegPostgresInstanceId != Guid.Empty)
|
||||
{
|
||||
secrets = await VaultService.GetAllRegisteredPostgresSecretsForInstanceAsync(TenantId, selectedRegPostgresInstanceId);
|
||||
}
|
||||
else
|
||||
{
|
||||
secrets = null;
|
||||
@@ -707,4 +895,58 @@ else
|
||||
syncingSecrets = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ToggleHistory(Guid secretId)
|
||||
{
|
||||
if (historySecretId == secretId)
|
||||
{
|
||||
historySecretId = null;
|
||||
secretVersions = null;
|
||||
revealedVersionId = null;
|
||||
revealedVersionValue = null;
|
||||
return;
|
||||
}
|
||||
|
||||
historySecretId = secretId;
|
||||
revealedVersionId = null;
|
||||
revealedVersionValue = null;
|
||||
secretVersions = null;
|
||||
secretVersions = await VaultService.GetSecretVersionsAsync(secretId);
|
||||
}
|
||||
|
||||
private async Task RevealVersion(Guid secretId, VaultSecretVersion version)
|
||||
{
|
||||
if (revealedVersionId == version.Id)
|
||||
{
|
||||
revealedVersionId = null;
|
||||
revealedVersionValue = null;
|
||||
return;
|
||||
}
|
||||
|
||||
versionRevealLoading = true;
|
||||
revealedVersionId = version.Id;
|
||||
revealedVersionValue = null;
|
||||
revealedVersionValue = await VaultService.GetSecretVersionValueAsync(secretId, version.Id);
|
||||
versionRevealLoading = false;
|
||||
}
|
||||
|
||||
private async Task RollbackToVersion(Guid secretId, Guid versionId)
|
||||
{
|
||||
errorMessage = null;
|
||||
bool ok = await VaultService.RollbackToVersionAsync(secretId, versionId);
|
||||
|
||||
if (ok)
|
||||
{
|
||||
successMessage = "Secret restored to selected version.";
|
||||
historySecretId = null;
|
||||
secretVersions = null;
|
||||
revealedVersionId = null;
|
||||
revealedVersionValue = null;
|
||||
await LoadSecrets();
|
||||
}
|
||||
else
|
||||
{
|
||||
errorMessage = "Failed to restore version.";
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user