database and gitsync issues fixed
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
@inject PrometheusService PrometheusService
|
||||
@inject AuditService AuditService
|
||||
@inject IncidentService IncidentService
|
||||
@inject CustomerGitService CustomerGitService
|
||||
@inject AuthenticationStateProvider AuthStateProvider
|
||||
@inject NavigationManager Navigation
|
||||
|
||||
@@ -37,11 +38,9 @@
|
||||
}
|
||||
else if (customers is null || customers.Count == 0)
|
||||
{
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-shield-lock text-muted" style="font-size: 3rem;"></i>
|
||||
<h4 class="mt-3">No access granted</h4>
|
||||
<p class="text-muted">You don't have access to any customers yet. Ask a tenant administrator to grant you access.</p>
|
||||
</div>
|
||||
<EmptyState Icon="bi-shield-lock"
|
||||
Title="No access granted"
|
||||
Message="You don't have access to any customers yet. Ask a tenant administrator to grant you access." />
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -49,7 +48,7 @@ else
|
||||
<nav aria-label="breadcrumb" class="mb-3">
|
||||
<ol class="breadcrumb small">
|
||||
<li class="breadcrumb-item">
|
||||
@if (selectedDeployment is not null || selectedIdentityRealm is not null || selectedSecretsApp is not null || selectedRegistryProject is not null || selectedGovernanceApp is not null)
|
||||
@if (selectedDeployment is not null || selectedIdentityRealm is not null || selectedSecretsApp is not null || selectedRegistryProject is not null || selectedGovernanceApp is not null || selectedResourcesApp is not null || showGitPolicies)
|
||||
{
|
||||
<a href="javascript:void(0)" class="text-decoration-none" @onclick="BackToApps">
|
||||
<i class="bi bi-grid me-1"></i>Portal
|
||||
@@ -70,7 +69,7 @@ else
|
||||
@if (selectedCustomer is not null)
|
||||
{
|
||||
<li class="breadcrumb-item">
|
||||
@if (selectedDeployment is not null || selectedIdentityRealm is not null || selectedSecretsApp is not null || selectedRegistryProject is not null || selectedGovernanceApp is not null)
|
||||
@if (selectedDeployment is not null || selectedIdentityRealm is not null || selectedSecretsApp is not null || selectedRegistryProject is not null || selectedGovernanceApp is not null || selectedResourcesApp is not null || showGitPolicies)
|
||||
{
|
||||
<a href="javascript:void(0)" class="text-decoration-none" @onclick="BackToApps">@selectedCustomer.Name</a>
|
||||
}
|
||||
@@ -109,6 +108,18 @@ else
|
||||
<i class="bi bi-shield-check me-1"></i>@selectedGovernanceApp.Name — Policy
|
||||
</li>
|
||||
}
|
||||
else if (selectedResourcesApp is not null)
|
||||
{
|
||||
<li class="breadcrumb-item active">
|
||||
<i class="bi bi-diagram-3 me-1"></i>@selectedResourcesApp.Name — Resources
|
||||
</li>
|
||||
}
|
||||
else if (showGitPolicies)
|
||||
{
|
||||
<li class="breadcrumb-item active">
|
||||
<i class="bi bi-git me-1"></i>Git Policies
|
||||
</li>
|
||||
}
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
@@ -144,10 +155,46 @@ else
|
||||
</div>
|
||||
}
|
||||
|
||||
@* ════════════════════════════════════════════════════════════════
|
||||
Level 3: Git URL policies (read-only)
|
||||
════════════════════════════════════════════════════════════════ *@
|
||||
else if (showGitPolicies)
|
||||
{
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-white py-2 d-flex align-items-center gap-2">
|
||||
<span class="bi bi-git text-primary"></span>
|
||||
<strong>Git URL Policies — @selectedCustomer!.Name</strong>
|
||||
<button class="btn-close btn-sm ms-auto" @onclick="() => showGitPolicies = false"></button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted small mb-3">
|
||||
These URL patterns define which git repositories you may use in deployments.
|
||||
Contact your administrator to change them.
|
||||
</p>
|
||||
@if (gitPolicies.Count == 0)
|
||||
{
|
||||
<div class="text-muted small">No git URL policies defined.</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<ul class="list-group list-group-flush">
|
||||
@foreach (CustomerGitRepoPolicy policy in gitPolicies)
|
||||
{
|
||||
<li class="list-group-item d-flex align-items-center gap-2 px-0">
|
||||
<i class="bi bi-check-circle text-success"></i>
|
||||
<code class="small">@policy.UrlPattern</code>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@* ════════════════════════════════════════════════════════════════
|
||||
Level 2: Apps and deployments for a selected customer
|
||||
════════════════════════════════════════════════════════════════ *@
|
||||
else if (selectedDeployment is null && selectedIdentityRealm is null && selectedSecretsApp is null && selectedRegistryProject is null)
|
||||
else if (selectedDeployment is null && selectedIdentityRealm is null && selectedSecretsApp is null && selectedRegistryProject is null && !showGitPolicies)
|
||||
{
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<i class="bi bi-people fs-3 me-2 text-primary"></i>
|
||||
@@ -160,6 +207,12 @@ else
|
||||
@onclick="() => showMonitoringPanel = !showMonitoringPanel">
|
||||
<span class="bi bi-heart-pulse me-1"></span>Monitoring
|
||||
</button>
|
||||
@if (gitPolicies.Count > 0)
|
||||
{
|
||||
<button class="btn btn-sm btn-outline-secondary" @onclick="() => showGitPolicies = true" title="Git URL Policies">
|
||||
<span class="bi bi-git me-1"></span>Git Policies
|
||||
</button>
|
||||
}
|
||||
<a class="btn btn-sm btn-outline-secondary" href="/portal/status">
|
||||
<span class="bi bi-activity me-1"></span>Status Overview
|
||||
</a>
|
||||
@@ -173,7 +226,7 @@ else
|
||||
@if (showMonitoringPanel)
|
||||
{
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header d-flex align-items-center gap-2 py-2">
|
||||
<div class="card-header bg-white d-flex align-items-center gap-2 py-2">
|
||||
<span class="bi bi-heart-pulse text-primary"></span>
|
||||
<strong>Monitoring — @selectedCustomer.Name</strong>
|
||||
<button class="btn-close btn-sm ms-auto" @onclick="() => showMonitoringPanel = false"></button>
|
||||
@@ -204,10 +257,8 @@ else
|
||||
|
||||
@if (selectedCustomer.Apps.Count == 0)
|
||||
{
|
||||
<div class="text-center py-4">
|
||||
<i class="bi bi-app-indicator text-muted" style="font-size: 2rem;"></i>
|
||||
<p class="text-muted mt-2">No apps configured for this customer yet.</p>
|
||||
</div>
|
||||
<EmptyState Icon="bi-app-indicator"
|
||||
Message="No apps configured for this customer yet." />
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -223,7 +274,7 @@ else
|
||||
<i class="bi bi-app-indicator me-2 text-primary"></i>
|
||||
<strong>@app.Name</strong>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<div class="d-flex gap-2 align-items-center">
|
||||
@if (currentAccessRole >= CustomerAccessRole.Admin)
|
||||
{
|
||||
<button class="btn btn-sm btn-outline-primary"
|
||||
@@ -231,28 +282,45 @@ else
|
||||
<i class="bi bi-plus me-1"></i>New Deployment
|
||||
</button>
|
||||
}
|
||||
@if (hasIdentity)
|
||||
{
|
||||
<button class="btn btn-sm btn-outline-primary"
|
||||
@onclick="() => SelectIdentityRealm(app.Id)">
|
||||
<i class="bi bi-shield-lock me-1"></i>Identity
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button"
|
||||
data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="bi bi-three-dots me-1"></i>More
|
||||
</button>
|
||||
}
|
||||
@if (hasRegistry)
|
||||
{
|
||||
<button class="btn btn-sm btn-outline-primary"
|
||||
@onclick="() => SelectRegistryProject(app.Id)">
|
||||
<i class="bi bi-archive me-1"></i>Registry
|
||||
</button>
|
||||
}
|
||||
<button class="btn btn-sm btn-outline-secondary"
|
||||
@onclick="() => SelectSecretsApp(app)">
|
||||
<i class="bi bi-key me-1"></i>Secrets
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-secondary"
|
||||
@onclick="() => SelectGovernanceApp(app)">
|
||||
<i class="bi bi-shield-check me-1"></i>Policy
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
@if (hasIdentity)
|
||||
{
|
||||
<li>
|
||||
<button class="dropdown-item" @onclick="() => SelectIdentityRealm(app.Id)">
|
||||
<i class="bi bi-shield-lock me-2 text-primary"></i>Identity
|
||||
</button>
|
||||
</li>
|
||||
}
|
||||
@if (hasRegistry)
|
||||
{
|
||||
<li>
|
||||
<button class="dropdown-item" @onclick="() => SelectRegistryProject(app.Id)">
|
||||
<i class="bi bi-archive me-2 text-primary"></i>Registry
|
||||
</button>
|
||||
</li>
|
||||
}
|
||||
<li>
|
||||
<button class="dropdown-item" @onclick="() => SelectSecretsApp(app)">
|
||||
<i class="bi bi-key me-2"></i>Secrets
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="dropdown-item" @onclick="() => SelectGovernanceApp(app)">
|
||||
<i class="bi bi-shield-check me-2"></i>Policy
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="dropdown-item" @onclick="() => SelectResourcesApp(app)">
|
||||
<i class="bi bi-diagram-3 me-2"></i>Resources
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -277,6 +345,12 @@ else
|
||||
<option value="@DeploymentType.Manual">Manual</option>
|
||||
<option value="@DeploymentType.Yaml">YAML</option>
|
||||
<option value="@DeploymentType.HelmChart">Helm Chart</option>
|
||||
@if (gitPolicies.Count > 0)
|
||||
{
|
||||
<option value="@DeploymentType.GitYaml">Git — YAML</option>
|
||||
<option value="@DeploymentType.GitHelm">Git — Helm</option>
|
||||
<option value="@DeploymentType.GitAppOfApps">Git — App of Apps</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
@@ -301,7 +375,15 @@ else
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small">Namespace</label>
|
||||
<input class="form-control form-control-sm" @bind="newDeployNamespace" placeholder="e.g. my-app" />
|
||||
@if (!string.IsNullOrWhiteSpace(app.Namespace))
|
||||
{
|
||||
<input class="form-control form-control-sm font-monospace" value="@app.Namespace" disabled />
|
||||
<div class="form-text small text-warning"><i class="bi bi-lock me-1"></i>Locked by governance policy.</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<input class="form-control form-control-sm" @bind="newDeployNamespace" placeholder="e.g. my-app" />
|
||||
}
|
||||
</div>
|
||||
@if (newDeployType == DeploymentType.HelmChart)
|
||||
{
|
||||
@@ -318,10 +400,52 @@ else
|
||||
<input class="form-control form-control-sm" @bind="newHelmChartVersion" placeholder="e.g. 1.0.0" />
|
||||
</div>
|
||||
}
|
||||
@if (newDeployType is DeploymentType.GitYaml or DeploymentType.GitHelm or DeploymentType.GitAppOfApps)
|
||||
{
|
||||
<div class="col-12">
|
||||
@if (customerGitCredentials.Count > 0)
|
||||
{
|
||||
<div class="alert alert-info py-2 small mb-2">
|
||||
<i class="bi bi-key-fill me-1"></i>
|
||||
<strong>@customerGitCredentials.Count stored credential@(customerGitCredentials.Count != 1 ? "s" : "") on file</strong>
|
||||
(@string.Join(", ", customerGitCredentials.Select(c => c.Name))) — no need to enter credentials.
|
||||
</div>
|
||||
}
|
||||
@if (gitPolicies.Count > 0)
|
||||
{
|
||||
<div class="alert alert-secondary py-2 small mb-2">
|
||||
<i class="bi bi-shield-check me-1"></i>
|
||||
Repository URL must match one of your allowed patterns:
|
||||
@foreach (CustomerGitRepoPolicy p in gitPolicies)
|
||||
{
|
||||
<code class="ms-2">@p.UrlPattern</code>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label small">Repository URL</label>
|
||||
<input class="form-control form-control-sm font-monospace @(newGitUrlError is not null ? "is-invalid" : "")"
|
||||
@bind="newGitUrl" @bind:event="oninput" @onchange="ValidateNewGitUrl"
|
||||
placeholder="https://github.com/org/repo" />
|
||||
@if (newGitUrlError is not null)
|
||||
{
|
||||
<div class="invalid-feedback">@newGitUrlError</div>
|
||||
}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small">Path <span class="text-muted">(leave blank for repo root)</span></label>
|
||||
<input class="form-control form-control-sm font-monospace" @bind="newGitPath" placeholder="." />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small">Branch / Revision <span class="text-muted">(leave blank for default)</span></label>
|
||||
<input class="form-control form-control-sm font-monospace" @bind="newGitRevision" placeholder="main" />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="d-flex gap-1">
|
||||
<button class="btn btn-sm btn-primary" @onclick="() => CreateDeployment(app.Id)"
|
||||
disabled="@(string.IsNullOrWhiteSpace(newDeployName) || newDeployEnvId == Guid.Empty || newDeployClusterId == Guid.Empty || string.IsNullOrWhiteSpace(newDeployNamespace) || creatingDeployment)">
|
||||
disabled="@(string.IsNullOrWhiteSpace(newDeployName) || newDeployEnvId == Guid.Empty || newDeployClusterId == Guid.Empty || string.IsNullOrWhiteSpace(newDeployNamespace) || creatingDeployment || newGitUrlError is not null || (IsGitType(newDeployType) && string.IsNullOrWhiteSpace(newGitUrl)))">
|
||||
@if (creatingDeployment) { <span class="spinner-border spinner-border-sm me-1"></span> }
|
||||
Create
|
||||
</button>
|
||||
@@ -363,7 +487,8 @@ else
|
||||
}
|
||||
else if (!showingCreateForm)
|
||||
{
|
||||
<p class="text-muted small mb-0">No deployments configured.</p>
|
||||
<EmptyState Icon="bi-rocket-takeoff"
|
||||
Message="No deployments configured for this app yet." />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -383,6 +508,9 @@ else
|
||||
DeploymentService="DeploymentService"
|
||||
PrometheusService="PrometheusService"
|
||||
AuditService="AuditService"
|
||||
CustomerId="selectedCustomer!.Id"
|
||||
TenantId="selectedCustomer!.TenantId"
|
||||
GovernanceNamespace="@(selectedCustomer?.Apps.FirstOrDefault(a => a.Id == selectedDeployment.AppId)?.Namespace)"
|
||||
OnBack="BackToApps"
|
||||
OnDeleted="OnDeploymentDeleted" />
|
||||
}
|
||||
@@ -422,6 +550,10 @@ else
|
||||
{
|
||||
<AppGovernancePanel AppId="selectedGovernanceApp.Id" AppName="selectedGovernanceApp.Name" />
|
||||
}
|
||||
else if (selectedResourcesApp is not null)
|
||||
{
|
||||
<AppResourcesPanel AppId="selectedResourcesApp.Id" />
|
||||
}
|
||||
}
|
||||
|
||||
@code {
|
||||
@@ -436,6 +568,7 @@ else
|
||||
private Data.App? selectedSecretsApp;
|
||||
private HarborProject? selectedRegistryProject;
|
||||
private Data.App? selectedGovernanceApp;
|
||||
private Data.App? selectedResourcesApp;
|
||||
|
||||
// Deployments indexed by app ID for the selected customer.
|
||||
private Dictionary<Guid, List<AppDeployment>> appDeployments = new();
|
||||
@@ -459,12 +592,19 @@ else
|
||||
private HashSet<Guid> AllCustomerClusterIds =>
|
||||
AllCustomerDeployments.Select(d => d.ClusterId).ToHashSet();
|
||||
|
||||
// Git policies and credentials for the selected customer.
|
||||
private List<CustomerGitRepoPolicy> gitPolicies = [];
|
||||
private List<CustomerGitCredential> customerGitCredentials = [];
|
||||
private bool showGitPolicies;
|
||||
|
||||
// Create deployment form (portal Admin).
|
||||
private Guid? createDeployAppId;
|
||||
private string newDeployName = "", newDeployNamespace = "";
|
||||
private DeploymentType newDeployType = DeploymentType.Manual;
|
||||
private Guid newDeployEnvId, newDeployClusterId;
|
||||
private string newHelmRepoUrl = "", newHelmChartName = "", newHelmChartVersion = "";
|
||||
private string newGitUrl = "", newGitPath = "", newGitRevision = "";
|
||||
private string? newGitUrlError;
|
||||
private bool creatingDeployment;
|
||||
private string? createDeployError;
|
||||
|
||||
@@ -489,6 +629,7 @@ else
|
||||
{
|
||||
selectedCustomer = customer;
|
||||
showMonitoringPanel = false;
|
||||
showGitPolicies = false;
|
||||
|
||||
// Look up the user's role for this customer.
|
||||
CustomerAccess? access = await CustomerAccessService.GetAccessAsync(currentUserId!, customer.Id);
|
||||
@@ -500,6 +641,10 @@ else
|
||||
appHarborProjects.Clear();
|
||||
createDeployAppId = null;
|
||||
|
||||
// Load git policies and credentials for this customer (all roles need policies to validate URLs).
|
||||
gitPolicies = await CustomerGitService.GetRepoPoliciesAsync(customer.Id);
|
||||
customerGitCredentials = await CustomerGitService.GetCredentialsAsync(customer.Id);
|
||||
|
||||
foreach (Data.App app in customer.Apps)
|
||||
{
|
||||
List<AppDeployment> deploys = await DeploymentService.GetDeploymentsAsync(app.Id);
|
||||
@@ -546,6 +691,9 @@ else
|
||||
selectedIdentityRealm = null;
|
||||
selectedSecretsApp = null;
|
||||
selectedRegistryProject = null;
|
||||
gitPolicies.Clear();
|
||||
customerGitCredentials.Clear();
|
||||
showGitPolicies = false;
|
||||
}
|
||||
|
||||
private void SelectSecretsApp(Data.App app)
|
||||
@@ -558,6 +706,11 @@ else
|
||||
selectedGovernanceApp = app;
|
||||
}
|
||||
|
||||
private void SelectResourcesApp(Data.App app)
|
||||
{
|
||||
selectedResourcesApp = app;
|
||||
}
|
||||
|
||||
private void BackToApps()
|
||||
{
|
||||
selectedDeployment = null;
|
||||
@@ -565,6 +718,8 @@ else
|
||||
selectedSecretsApp = null;
|
||||
selectedRegistryProject = null;
|
||||
selectedGovernanceApp = null;
|
||||
selectedResourcesApp = null;
|
||||
showGitPolicies = false;
|
||||
}
|
||||
|
||||
private async Task OnDeploymentDeleted()
|
||||
@@ -587,7 +742,11 @@ else
|
||||
else
|
||||
{
|
||||
createDeployAppId = appId;
|
||||
newDeployName = newDeployNamespace = newHelmRepoUrl = newHelmChartName = newHelmChartVersion = "";
|
||||
Data.App? app = selectedCustomer?.Apps.FirstOrDefault(a => a.Id == appId);
|
||||
newDeployNamespace = app?.Namespace ?? "";
|
||||
newDeployName = newHelmRepoUrl = newHelmChartName = newHelmChartVersion = "";
|
||||
newGitUrl = newGitPath = newGitRevision = "";
|
||||
newGitUrlError = null;
|
||||
newDeployType = DeploymentType.Manual;
|
||||
newDeployEnvId = newDeployClusterId = Guid.Empty;
|
||||
createDeployError = null;
|
||||
@@ -601,12 +760,36 @@ else
|
||||
|
||||
try
|
||||
{
|
||||
Guid? gitRepoId = null;
|
||||
|
||||
if (IsGitType(newDeployType) && !string.IsNullOrWhiteSpace(newGitUrl))
|
||||
{
|
||||
// Validate URL against policy one more time before creating.
|
||||
if (!CustomerGitService.MatchesAnyPolicy(newGitUrl, gitPolicies))
|
||||
{
|
||||
createDeployError = "The repository URL does not match any allowed policy pattern.";
|
||||
return;
|
||||
}
|
||||
|
||||
// Find or create a GitRepository backed by the first available credential.
|
||||
CustomerGitCredential? cred = customerGitCredentials.FirstOrDefault();
|
||||
if (cred is not null && selectedCustomer is not null)
|
||||
{
|
||||
GitRepository repo = await CustomerGitService.FindOrCreateRepositoryForCustomerAsync(
|
||||
selectedCustomer.TenantId, selectedCustomer.Id, newGitUrl, cred.Id);
|
||||
gitRepoId = repo.Id;
|
||||
}
|
||||
}
|
||||
|
||||
await DeploymentService.CreateDeploymentAsync(
|
||||
appId, newDeployName, newDeployType,
|
||||
newDeployEnvId, newDeployClusterId, newDeployNamespace,
|
||||
newDeployType == DeploymentType.HelmChart ? newHelmRepoUrl : null,
|
||||
newDeployType == DeploymentType.HelmChart ? newHelmChartName : null,
|
||||
newDeployType == DeploymentType.HelmChart ? newHelmChartVersion : null);
|
||||
newDeployType == DeploymentType.HelmChart ? newHelmChartVersion : null,
|
||||
gitRepositoryId: gitRepoId,
|
||||
gitPath: string.IsNullOrWhiteSpace(newGitPath) ? null : newGitPath,
|
||||
gitRevision: string.IsNullOrWhiteSpace(newGitRevision) ? null : newGitRevision);
|
||||
|
||||
createDeployAppId = null;
|
||||
appDeployments[appId] = await DeploymentService.GetDeploymentsAsync(appId);
|
||||
@@ -621,6 +804,22 @@ else
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateNewGitUrl(ChangeEventArgs e)
|
||||
{
|
||||
newGitUrl = e.Value?.ToString() ?? "";
|
||||
if (string.IsNullOrWhiteSpace(newGitUrl))
|
||||
{
|
||||
newGitUrlError = null;
|
||||
return;
|
||||
}
|
||||
newGitUrlError = CustomerGitService.MatchesAnyPolicy(newGitUrl, gitPolicies)
|
||||
? null
|
||||
: "URL does not match any allowed policy pattern.";
|
||||
}
|
||||
|
||||
private static bool IsGitType(DeploymentType type)
|
||||
=> type is DeploymentType.GitYaml or DeploymentType.GitHelm or DeploymentType.GitAppOfApps;
|
||||
|
||||
// ──────── Badge helpers ────────
|
||||
|
||||
private RenderFragment TypeBadge(DeploymentType type) => type switch
|
||||
@@ -628,6 +827,9 @@ else
|
||||
DeploymentType.Manual => @<span class="badge bg-info">Manual</span>,
|
||||
DeploymentType.Yaml => @<span class="badge bg-warning text-dark">YAML</span>,
|
||||
DeploymentType.HelmChart => @<span class="badge bg-purple text-white" style="background-color: #6f42c1 !important;">Helm</span>,
|
||||
DeploymentType.GitYaml => @<span class="badge bg-success"><i class="bi bi-git me-1"></i>Git YAML</span>,
|
||||
DeploymentType.GitHelm => @<span class="badge bg-success"><i class="bi bi-git me-1"></i>Git Helm</span>,
|
||||
DeploymentType.GitAppOfApps => @<span class="badge bg-success"><i class="bi bi-git me-1"></i>App of Apps</span>,
|
||||
_ => @<span class="badge bg-secondary">Unknown</span>
|
||||
};
|
||||
|
||||
|
||||
@@ -24,19 +24,12 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (isLoading)
|
||||
<LoadingPanel Loading="@isLoading" LoadingText="Loading status…">
|
||||
@if (!isLoading && rows.Count == 0)
|
||||
{
|
||||
<div class="text-center py-5">
|
||||
<div class="spinner-border text-primary" role="status"></div>
|
||||
<p class="text-muted mt-2">Loading status…</p>
|
||||
</div>
|
||||
}
|
||||
else if (rows.Count == 0)
|
||||
{
|
||||
<div class="text-center py-5 text-muted">
|
||||
<i class="bi bi-shield-lock display-4 d-block mb-2"></i>
|
||||
No applications to show. You may not have access to any customer yet.
|
||||
</div>
|
||||
<EmptyState Icon="bi-shield-lock"
|
||||
Title="No applications to show"
|
||||
Message="You may not have access to any customer yet." />
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -119,6 +112,7 @@ else
|
||||
Refreshed @lastRefreshed.ToLocalTime().ToString("HH:mm:ss") · Health snapshots every 5 minutes
|
||||
</div>
|
||||
}
|
||||
</LoadingPanel>
|
||||
|
||||
@code {
|
||||
private List<StatusRow> rows = [];
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
@inject IncidentService IncidentService
|
||||
@inject LokiService LokiService
|
||||
@inject GitSyncService GitSyncService
|
||||
@inject CustomerGitService CustomerGitService
|
||||
@inject IDbContextFactory<ApplicationDbContext> DbFactory
|
||||
|
||||
@* ═══════════════════════════════════════════════════════════════════
|
||||
@@ -63,7 +64,15 @@
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small">Namespace</label>
|
||||
<input class="form-control form-control-sm" @bind="editDeployNamespace" />
|
||||
@if (!string.IsNullOrWhiteSpace(GovernanceNamespace))
|
||||
{
|
||||
<input class="form-control form-control-sm font-monospace" value="@GovernanceNamespace" disabled />
|
||||
<div class="form-text small text-warning"><i class="bi bi-lock me-1"></i>Locked by governance policy.</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<input class="form-control form-control-sm" @bind="editDeployNamespace" />
|
||||
}
|
||||
</div>
|
||||
@if (Deployment.Type == DeploymentType.HelmChart)
|
||||
{
|
||||
@@ -94,17 +103,25 @@
|
||||
|
||||
@if (AccessRole >= CustomerAccessRole.Admin && confirmDeleteDeployment)
|
||||
{
|
||||
<div class="mt-3 p-2 bg-danger bg-opacity-10 rounded d-flex align-items-center justify-content-between">
|
||||
<span class="text-danger small">
|
||||
<div class="mt-3 p-3 bg-danger bg-opacity-10 rounded">
|
||||
<div class="text-danger small mb-2">
|
||||
<i class="bi bi-exclamation-triangle me-1"></i>
|
||||
Permanently delete <strong>@Deployment.Name</strong> and all its manifests?
|
||||
</span>
|
||||
<div class="d-flex gap-1">
|
||||
<button class="btn btn-sm btn-danger" @onclick="DeleteDeployment" disabled="@deletingDeployment">
|
||||
@if (deletingDeployment) { <span class="spinner-border spinner-border-sm me-1"></span> }
|
||||
Yes, delete
|
||||
What would you like to do with <strong>@Deployment.Name</strong>?
|
||||
</div>
|
||||
@if (deleteError is not null)
|
||||
{
|
||||
<div class="alert alert-danger py-1 small mb-2">@deleteError</div>
|
||||
}
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<button class="btn btn-sm btn-danger" @onclick="() => DeleteDeployment(true)" disabled="@deletingDeployment">
|
||||
@if (deletingDeployment && deleteFromCluster) { <span class="spinner-border spinner-border-sm me-1"></span> }
|
||||
<i class="bi bi-trash me-1"></i>Delete (remove cluster resources + DB)
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-secondary" @onclick="() => confirmDeleteDeployment = false">Cancel</button>
|
||||
<button class="btn btn-sm btn-outline-danger" @onclick="() => DeleteDeployment(false)" disabled="@deletingDeployment">
|
||||
@if (deletingDeployment && !deleteFromCluster) { <span class="spinner-border spinner-border-sm me-1"></span> }
|
||||
<i class="bi bi-database-x me-1"></i>Unregister (DB only, keep cluster resources)
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-secondary" @onclick="() => { confirmDeleteDeployment = false; deleteError = null; }">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -221,6 +238,11 @@
|
||||
<i class="bi bi-journal-text me-1"></i>Logs
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link @(activeTab == "git" ? "active" : "")" @onclick="LoadGitTab">
|
||||
<i class="bi bi-git me-1"></i>Git
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@* ══════════════════════════════════════════════════════════════
|
||||
@@ -246,27 +268,13 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (podsLoading)
|
||||
<LoadingPanel Loading="@podsLoading" LoadingText="Loading pods from cluster…" Error="@podsError">
|
||||
@if (!podsLoading && string.IsNullOrEmpty(podsError) && (pods is null || pods.Count == 0))
|
||||
{
|
||||
<div class="text-center py-4">
|
||||
<div class="spinner-border spinner-border-sm text-primary" role="status"></div>
|
||||
<p class="text-muted small mt-2">Loading pods from cluster...</p>
|
||||
</div>
|
||||
<EmptyState Icon="bi-cpu"
|
||||
Message="@($"No pods found in namespace {Deployment.Namespace}.")" />
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(podsError))
|
||||
{
|
||||
<div class="alert alert-warning py-2 small">
|
||||
<i class="bi bi-exclamation-triangle me-1"></i>@podsError
|
||||
</div>
|
||||
}
|
||||
else if (pods is null || pods.Count == 0)
|
||||
{
|
||||
<div class="text-center py-4">
|
||||
<i class="bi bi-cpu text-muted" style="font-size: 2rem;"></i>
|
||||
<p class="text-muted small mt-2">No pods found in namespace <code>@Deployment.Namespace</code>.</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
else if (!podsLoading && string.IsNullOrEmpty(podsError) && pods is not null && pods.Count > 0)
|
||||
{
|
||||
@* --- Pod log viewer (shown above pods when active) --- *@
|
||||
@if (showLogViewer)
|
||||
@@ -409,6 +417,7 @@
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</LoadingPanel>
|
||||
}
|
||||
|
||||
@* ══════════════════════════════════════════════════════════════
|
||||
@@ -475,20 +484,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
@if (manifests is null)
|
||||
<LoadingPanel Loading="@(manifests is null)">
|
||||
@if (manifests is not null && manifests.Count == 0)
|
||||
{
|
||||
<div class="text-center py-3">
|
||||
<div class="spinner-border spinner-border-sm text-primary" role="status"></div>
|
||||
</div>
|
||||
<EmptyState Icon="bi-file-earmark-code" Message="No manifests defined for this deployment." />
|
||||
}
|
||||
else if (manifests.Count == 0)
|
||||
{
|
||||
<div class="text-center py-4">
|
||||
<i class="bi bi-file-earmark-code text-muted" style="font-size: 2rem;"></i>
|
||||
<p class="text-muted small mt-2">No manifests defined for this deployment.</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
else if (manifests is not null)
|
||||
{
|
||||
@foreach (DeploymentManifest manifest in manifests)
|
||||
{
|
||||
@@ -536,6 +537,7 @@
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</LoadingPanel>
|
||||
|
||||
@if (yamlApplyOutput is not null)
|
||||
{
|
||||
@@ -836,6 +838,163 @@
|
||||
PresetNamespace="@Deployment.Namespace" />
|
||||
}
|
||||
|
||||
@* ══════════════════════════════════════════════════════════════
|
||||
Git Tab — per-deployment git source configuration
|
||||
══════════════════════════════════════════════════════════════ *@
|
||||
@if (activeTab == "git")
|
||||
{
|
||||
<div class="card shadow-sm mb-3">
|
||||
<div class="card-header bg-white py-2 d-flex align-items-center gap-2">
|
||||
<span class="bi bi-git text-primary"></span>
|
||||
<strong>Git Source</strong>
|
||||
@if (AccessRole >= CustomerAccessRole.Admin && !showGitEdit)
|
||||
{
|
||||
<button class="btn btn-sm btn-outline-secondary ms-auto" @onclick="StartGitEdit">
|
||||
<i class="bi bi-pencil"></i> Edit
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
||||
@* ── Credential notice ── *@
|
||||
@if (gitTabCredentials.Count > 0)
|
||||
{
|
||||
<div class="alert alert-info py-2 small mb-3">
|
||||
<i class="bi bi-key-fill me-1"></i>
|
||||
<strong>Stored credentials on file:</strong>
|
||||
@string.Join(", ", gitTabCredentials.Select(c => c.Name)) —
|
||||
no need to enter credentials when setting a repository URL.
|
||||
</div>
|
||||
}
|
||||
|
||||
@* ── Policies (read-only) ── *@
|
||||
@if (gitTabPolicies.Count > 0)
|
||||
{
|
||||
<div class="mb-3">
|
||||
<div class="text-muted small mb-1"><i class="bi bi-shield-check me-1"></i>Allowed repository URL patterns:</div>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
@foreach (CustomerGitRepoPolicy p in gitTabPolicies)
|
||||
{
|
||||
<code class="small bg-light px-2 py-1 rounded">@p.UrlPattern</code>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (showGitEdit && AccessRole >= CustomerAccessRole.Admin)
|
||||
{
|
||||
@* ── Edit form ── *@
|
||||
@if (gitEditError is not null)
|
||||
{
|
||||
<div class="alert alert-danger py-1 small mb-2">@gitEditError</div>
|
||||
}
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-12">
|
||||
<label class="form-label small">Repository URL</label>
|
||||
<input class="form-control form-control-sm font-monospace @(gitEditUrlError is not null ? "is-invalid" : "")"
|
||||
@bind="gitEditUrl" @bind:event="oninput" @onchange="ValidateGitEditUrl"
|
||||
placeholder="https://github.com/org/repo" />
|
||||
@if (gitEditUrlError is not null)
|
||||
{
|
||||
<div class="invalid-feedback">@gitEditUrlError</div>
|
||||
}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small">Path <span class="text-muted">(leave blank for root)</span></label>
|
||||
<input class="form-control form-control-sm font-monospace" @bind="gitEditPath" placeholder="." />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small">Branch / Revision <span class="text-muted">(leave blank for default)</span></label>
|
||||
<input class="form-control form-control-sm font-monospace" @bind="gitEditRevision" placeholder="main" />
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="gitAutoSync" @bind="gitEditAutoSync" />
|
||||
<label class="form-check-label small" for="gitAutoSync">Auto-sync (poll for changes every 3 minutes)</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex gap-1">
|
||||
<button class="btn btn-sm btn-primary" @onclick="SaveGitSettings"
|
||||
disabled="@(savingGitSettings || gitEditUrlError is not null || string.IsNullOrWhiteSpace(gitEditUrl))">
|
||||
@if (savingGitSettings) { <span class="spinner-border spinner-border-sm me-1"></span> }
|
||||
Save
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-secondary" @onclick="() => showGitEdit = false">Cancel</button>
|
||||
</div>
|
||||
}
|
||||
else if (Deployment.GitRepositoryId is not null)
|
||||
{
|
||||
@* ── Current settings (read) ── *@
|
||||
<dl class="row small mb-0">
|
||||
<dt class="col-sm-3">Repository</dt>
|
||||
<dd class="col-sm-9 font-monospace">@(Deployment.GitRepository?.Url ?? "—")</dd>
|
||||
|
||||
<dt class="col-sm-3">Path</dt>
|
||||
<dd class="col-sm-9 font-monospace">@(Deployment.GitPath ?? ".")</dd>
|
||||
|
||||
<dt class="col-sm-3">Revision</dt>
|
||||
<dd class="col-sm-9 font-monospace">@(Deployment.GitRevision ?? Deployment.GitRepository?.DefaultBranch ?? "main")</dd>
|
||||
|
||||
<dt class="col-sm-3">Auto-sync</dt>
|
||||
<dd class="col-sm-9">
|
||||
@if (Deployment.GitAutoSync)
|
||||
{
|
||||
<span class="badge bg-success">Enabled</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-secondary">Disabled</span>
|
||||
}
|
||||
</dd>
|
||||
|
||||
@if (Deployment.GitLastSyncedCommit is not null)
|
||||
{
|
||||
<dt class="col-sm-3">Last synced</dt>
|
||||
<dd class="col-sm-9">
|
||||
<code>@Deployment.GitLastSyncedCommit[..Math.Min(7, Deployment.GitLastSyncedCommit.Length)]</code>
|
||||
@if (Deployment.GitLastSyncedAt.HasValue)
|
||||
{
|
||||
<span class="text-muted ms-2">@Deployment.GitLastSyncedAt.Value.ToString("yyyy-MM-dd HH:mm")</span>
|
||||
}
|
||||
</dd>
|
||||
}
|
||||
</dl>
|
||||
|
||||
@if (AccessRole >= CustomerAccessRole.Operator)
|
||||
{
|
||||
<div class="mt-3">
|
||||
<button class="btn btn-sm btn-outline-primary" @onclick="TriggerGitSync" disabled="@syncingGit">
|
||||
@if (syncingGit) { <span class="spinner-border spinner-border-sm me-1"></span> }
|
||||
else { <i class="bi bi-arrow-repeat me-1"></i> }
|
||||
Sync now
|
||||
</button>
|
||||
@if (gitSyncMessage is not null)
|
||||
{
|
||||
<span class="ms-2 small @(gitSyncSuccess ? "text-success" : "text-danger")">
|
||||
<i class="bi @(gitSyncSuccess ? "bi-check-circle" : "bi-exclamation-triangle") me-1"></i>@gitSyncMessage
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@* ── Not configured ── *@
|
||||
<div class="text-muted small">
|
||||
No git source configured for this deployment.
|
||||
@if (AccessRole >= CustomerAccessRole.Admin)
|
||||
{
|
||||
<button class="btn btn-sm btn-outline-primary ms-2" @onclick="StartGitEdit">
|
||||
<i class="bi bi-plus me-1"></i>Configure Git
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter] public required AppDeployment Deployment { get; set; }
|
||||
[Parameter] public CustomerAccessRole AccessRole { get; set; }
|
||||
@@ -843,11 +1002,24 @@
|
||||
[Parameter] public required DeploymentService DeploymentService { get; set; }
|
||||
[Parameter] public required PrometheusService PrometheusService { get; set; }
|
||||
[Parameter] public required AuditService AuditService { get; set; }
|
||||
[Parameter] public Guid CustomerId { get; set; }
|
||||
[Parameter] public Guid TenantId { get; set; }
|
||||
[Parameter] public string? GovernanceNamespace { get; set; }
|
||||
[Parameter] public EventCallback OnBack { get; set; }
|
||||
[Parameter] public EventCallback OnDeleted { get; set; }
|
||||
|
||||
private string activeTab = "pods";
|
||||
|
||||
// Git tab
|
||||
private List<CustomerGitRepoPolicy> gitTabPolicies = [];
|
||||
private List<CustomerGitCredential> gitTabCredentials = [];
|
||||
private bool showGitEdit;
|
||||
private bool savingGitSettings;
|
||||
private string gitEditUrl = "", gitEditPath = "", gitEditRevision = "";
|
||||
private bool gitEditAutoSync = true;
|
||||
private string? gitEditUrlError;
|
||||
private string? gitEditError;
|
||||
|
||||
// Git sync
|
||||
private bool syncingGit;
|
||||
private string? gitSyncMessage;
|
||||
@@ -877,6 +1049,107 @@
|
||||
finally { syncingGit = false; }
|
||||
}
|
||||
|
||||
// ──────── Git tab ────────
|
||||
|
||||
private async Task LoadGitTab()
|
||||
{
|
||||
activeTab = "git";
|
||||
if (gitTabPolicies.Count == 0 && CustomerId != Guid.Empty)
|
||||
{
|
||||
gitTabPolicies = await CustomerGitService.GetRepoPoliciesAsync(CustomerId);
|
||||
gitTabCredentials = await CustomerGitService.GetCredentialsAsync(CustomerId);
|
||||
}
|
||||
}
|
||||
|
||||
private void StartGitEdit()
|
||||
{
|
||||
gitEditUrl = Deployment.GitRepository?.Url ?? "";
|
||||
gitEditPath = Deployment.GitPath ?? "";
|
||||
gitEditRevision = Deployment.GitRevision ?? "";
|
||||
gitEditAutoSync = Deployment.GitAutoSync;
|
||||
gitEditUrlError = null;
|
||||
gitEditError = null;
|
||||
showGitEdit = true;
|
||||
}
|
||||
|
||||
private void ValidateGitEditUrl(ChangeEventArgs e)
|
||||
{
|
||||
gitEditUrl = e.Value?.ToString() ?? "";
|
||||
if (string.IsNullOrWhiteSpace(gitEditUrl))
|
||||
{
|
||||
gitEditUrlError = null;
|
||||
return;
|
||||
}
|
||||
gitEditUrlError = gitTabPolicies.Count == 0 || CustomerGitService.MatchesAnyPolicy(gitEditUrl, gitTabPolicies)
|
||||
? null
|
||||
: "URL does not match any allowed policy pattern.";
|
||||
}
|
||||
|
||||
private async Task SaveGitSettings()
|
||||
{
|
||||
savingGitSettings = true;
|
||||
gitEditError = null;
|
||||
|
||||
try
|
||||
{
|
||||
if (gitTabPolicies.Count > 0 && !CustomerGitService.MatchesAnyPolicy(gitEditUrl, gitTabPolicies))
|
||||
{
|
||||
gitEditError = "The repository URL does not match any allowed policy pattern.";
|
||||
return;
|
||||
}
|
||||
|
||||
Guid? repoId = null;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(gitEditUrl))
|
||||
{
|
||||
CustomerGitCredential? cred = gitTabCredentials.FirstOrDefault();
|
||||
if (cred is not null)
|
||||
{
|
||||
GitRepository repo = await CustomerGitService.FindOrCreateRepositoryForCustomerAsync(
|
||||
TenantId, CustomerId, gitEditUrl, cred.Id);
|
||||
repoId = repo.Id;
|
||||
}
|
||||
else
|
||||
{
|
||||
// No customer credential — check if existing repo already has this URL.
|
||||
using ApplicationDbContext db = DbFactory.CreateDbContext();
|
||||
GitRepository? existing = await db.GitRepositories
|
||||
.FirstOrDefaultAsync(r => r.TenantId == TenantId && r.Url == gitEditUrl);
|
||||
repoId = existing?.Id;
|
||||
if (repoId is null)
|
||||
{
|
||||
gitEditError = "No stored credential found. Ask your administrator to add a git credential for your account.";
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await DeploymentService.UpdateGitSettingsAsync(
|
||||
Deployment.Id, repoId,
|
||||
string.IsNullOrWhiteSpace(gitEditPath) ? null : gitEditPath,
|
||||
string.IsNullOrWhiteSpace(gitEditRevision) ? null : gitEditRevision,
|
||||
gitEditAutoSync);
|
||||
|
||||
// Update local state so the UI reflects the change without reload.
|
||||
Deployment.GitRepositoryId = repoId;
|
||||
Deployment.GitPath = string.IsNullOrWhiteSpace(gitEditPath) ? null : gitEditPath;
|
||||
Deployment.GitRevision = string.IsNullOrWhiteSpace(gitEditRevision) ? null : gitEditRevision;
|
||||
Deployment.GitAutoSync = gitEditAutoSync;
|
||||
Deployment.GitLastSyncedCommit = null;
|
||||
Deployment.GitLastSyncedAt = null;
|
||||
|
||||
showGitEdit = false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
gitEditError = ex.Message;
|
||||
}
|
||||
finally
|
||||
{
|
||||
savingGitSettings = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Pods
|
||||
private List<PodInfo>? pods;
|
||||
private bool podsLoading;
|
||||
@@ -934,6 +1207,8 @@
|
||||
// Delete deployment
|
||||
private bool confirmDeleteDeployment;
|
||||
private bool deletingDeployment;
|
||||
private bool deleteFromCluster;
|
||||
private string? deleteError;
|
||||
|
||||
// Resources
|
||||
private List<DeploymentResource>? resourceTree;
|
||||
@@ -1125,7 +1400,7 @@
|
||||
private void StartEditDeployment()
|
||||
{
|
||||
editDeployName = Deployment.Name;
|
||||
editDeployNamespace = Deployment.Namespace;
|
||||
editDeployNamespace = !string.IsNullOrWhiteSpace(GovernanceNamespace) ? GovernanceNamespace : Deployment.Namespace;
|
||||
editHelmRepoUrl = Deployment.HelmRepoUrl ?? "";
|
||||
editHelmChartName = Deployment.HelmChartName ?? "";
|
||||
editHelmChartVersion = Deployment.HelmChartVersion ?? "";
|
||||
@@ -1140,14 +1415,16 @@
|
||||
editDeployError = null;
|
||||
try
|
||||
{
|
||||
string ns = !string.IsNullOrWhiteSpace(GovernanceNamespace) ? GovernanceNamespace : editDeployNamespace;
|
||||
|
||||
await DeploymentService.UpdateDeploymentAsync(
|
||||
Deployment.Id, editDeployName, editDeployNamespace,
|
||||
Deployment.Id, editDeployName, ns,
|
||||
Deployment.Type == DeploymentType.HelmChart ? editHelmRepoUrl : null,
|
||||
Deployment.Type == DeploymentType.HelmChart ? editHelmChartName : null,
|
||||
Deployment.Type == DeploymentType.HelmChart ? editHelmChartVersion : null);
|
||||
|
||||
Deployment.Name = editDeployName;
|
||||
Deployment.Namespace = editDeployNamespace;
|
||||
Deployment.Namespace = ns;
|
||||
if (Deployment.Type == DeploymentType.HelmChart)
|
||||
{
|
||||
Deployment.HelmRepoUrl = editHelmRepoUrl;
|
||||
@@ -1161,15 +1438,36 @@
|
||||
finally { savingEdit = false; }
|
||||
}
|
||||
|
||||
private async Task DeleteDeployment()
|
||||
private async Task DeleteDeployment(bool fromCluster)
|
||||
{
|
||||
deletingDeployment = true;
|
||||
deleteFromCluster = fromCluster;
|
||||
deleteError = null;
|
||||
StateHasChanged();
|
||||
|
||||
try
|
||||
{
|
||||
if (fromCluster)
|
||||
{
|
||||
KubernetesOperationResult<string> clusterResult =
|
||||
await K8sOps.DeleteDeploymentFromClusterAsync(Deployment.Id);
|
||||
|
||||
if (!clusterResult.IsSuccess)
|
||||
{
|
||||
deleteError = clusterResult.Error;
|
||||
deletingDeployment = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await DeploymentService.DeleteDeploymentAsync(Deployment.Id);
|
||||
await OnDeleted.InvokeAsync();
|
||||
}
|
||||
catch { deletingDeployment = false; confirmDeleteDeployment = false; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
deleteError = ex.Message;
|
||||
deletingDeployment = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ──────── Manifests ────────
|
||||
|
||||
@@ -36,11 +36,8 @@
|
||||
@* ── Users ── *@
|
||||
@if (activeTab == "users")
|
||||
{
|
||||
@if (loadingUsers)
|
||||
{
|
||||
<div class="spinner-border spinner-border-sm text-primary"></div>
|
||||
}
|
||||
else
|
||||
<LoadingPanel Loading="@loadingUsers">
|
||||
@if (!loadingUsers)
|
||||
{
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<span class="text-muted small">@(users?.Count ?? 0) users</span>
|
||||
@@ -126,16 +123,14 @@
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</LoadingPanel>
|
||||
}
|
||||
|
||||
@* ── Identity Providers ── *@
|
||||
@if (activeTab == "idp")
|
||||
{
|
||||
@if (loadingIdps)
|
||||
{
|
||||
<div class="spinner-border spinner-border-sm text-primary"></div>
|
||||
}
|
||||
else
|
||||
<LoadingPanel Loading="@loadingIdps">
|
||||
@if (!loadingIdps)
|
||||
{
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<span class="text-muted small">@(idps?.Count ?? 0) providers</span>
|
||||
@@ -207,16 +202,14 @@
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</LoadingPanel>
|
||||
}
|
||||
|
||||
@* ── Groups ── *@
|
||||
@if (activeTab == "groups")
|
||||
{
|
||||
@if (loadingGroups)
|
||||
{
|
||||
<div class="spinner-border spinner-border-sm text-primary"></div>
|
||||
}
|
||||
else
|
||||
<LoadingPanel Loading="@loadingGroups">
|
||||
@if (!loadingGroups)
|
||||
{
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<span class="text-muted small">@(groups?.Count ?? 0) groups</span>
|
||||
@@ -257,16 +250,14 @@
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</LoadingPanel>
|
||||
}
|
||||
|
||||
@* ── Organizations ── *@
|
||||
@if (activeTab == "orgs")
|
||||
{
|
||||
@if (loadingOrgs)
|
||||
{
|
||||
<div class="spinner-border spinner-border-sm text-primary"></div>
|
||||
}
|
||||
else
|
||||
<LoadingPanel Loading="@loadingOrgs">
|
||||
@if (!loadingOrgs)
|
||||
{
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<span class="text-muted small">@(orgs?.Count ?? 0) organizations (Keycloak 26+)</span>
|
||||
@@ -322,6 +313,7 @@
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</LoadingPanel>
|
||||
}
|
||||
|
||||
@code {
|
||||
|
||||
@@ -2,20 +2,8 @@
|
||||
@using EntKube.Web.Services
|
||||
@inject HarborService HarborService
|
||||
|
||||
@if (loading)
|
||||
{
|
||||
<div class="text-center py-5">
|
||||
<div class="spinner-border text-primary" role="status"></div>
|
||||
<p class="text-muted mt-2">Loading registry...</p>
|
||||
</div>
|
||||
}
|
||||
else if (loadError is not null)
|
||||
{
|
||||
<div class="alert alert-danger">
|
||||
<i class="bi bi-exclamation-triangle me-2"></i>@loadError
|
||||
</div>
|
||||
}
|
||||
else
|
||||
<LoadingPanel Loading="@loading" LoadingText="Loading registry…" Error="@loadError">
|
||||
@if (!loading && loadError is null)
|
||||
{
|
||||
<div class="d-flex align-items-center gap-2 mb-3">
|
||||
<i class="bi bi-archive fs-4 text-primary"></i>
|
||||
@@ -329,6 +317,7 @@ else
|
||||
}
|
||||
}
|
||||
}
|
||||
</LoadingPanel>
|
||||
|
||||
@code {
|
||||
[Parameter] public required HarborProject Project { get; set; }
|
||||
|
||||
@@ -102,24 +102,13 @@ else
|
||||
}
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
@if (secrets is null)
|
||||
<LoadingPanel Loading="@(secrets is null)">
|
||||
@if (secrets is not null && secrets.Count == 0)
|
||||
{
|
||||
<div class="text-center py-4">
|
||||
<div class="spinner-border spinner-border-sm text-primary" role="status"></div>
|
||||
</div>
|
||||
<EmptyState Icon="bi-lock"
|
||||
Message="@(AccessRole >= CustomerAccessRole.Admin ? "No secrets stored yet. Use the form above to add one." : "No secrets stored for this app yet.")" />
|
||||
}
|
||||
else if (secrets.Count == 0)
|
||||
{
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-lock text-muted" style="font-size: 2rem;"></i>
|
||||
<p class="text-muted small mt-2 mb-0">No secrets stored for this app yet.</p>
|
||||
@if (AccessRole >= CustomerAccessRole.Admin)
|
||||
{
|
||||
<p class="text-muted small mb-0">Use the form above to add one.</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
else if (secrets is not null)
|
||||
{
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
@@ -271,6 +260,7 @@ else
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</LoadingPanel>
|
||||
@if (syncOutput is not null)
|
||||
{
|
||||
<div class="m-3 p-2 bg-dark text-white rounded font-monospace small" style="white-space: pre-wrap; max-height: 200px; overflow-y: auto;">@syncOutput</div>
|
||||
|
||||
Reference in New Issue
Block a user