Mark subproject as dirty in CMKS - Infra
This commit is contained in:
840
src/EntKube.Web/Components/Pages/Tenants/AppDetail.razor
Normal file
840
src/EntKube.Web/Components/Pages/Tenants/AppDetail.razor
Normal file
@@ -0,0 +1,840 @@
|
||||
@using EntKube.Web.Data
|
||||
@using EntKube.Web.Services
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@inject TenantService TenantService
|
||||
@inject DeploymentService DeploymentService
|
||||
|
||||
@* ═══════════════════════════════════════════════════════════════════
|
||||
App Detail — a full-page view for a single application.
|
||||
Shows app info, environment links, and is the extension point
|
||||
for future app-level features (config, deployments, secrets, etc.).
|
||||
═══════════════════════════════════════════════════════════════════ *@
|
||||
|
||||
@* --- Back button --- *@
|
||||
<nav class="mb-3">
|
||||
<button class="btn btn-sm btn-link text-decoration-none p-0" @onclick="OnBack">
|
||||
<i class="bi bi-arrow-left me-1"></i>Back to @CustomerName
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
@* --- App header --- *@
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-start justify-content-between">
|
||||
<div class="d-flex align-items-start">
|
||||
<i class="bi bi-app-indicator fs-3 me-3 text-primary"></i>
|
||||
<div>
|
||||
<h4 class="mb-1">@App.Name</h4>
|
||||
<div class="d-flex flex-wrap gap-2 text-muted small">
|
||||
<span><i class="bi bi-person me-1"></i>@CustomerName</span>
|
||||
<span><i class="bi bi-calendar me-1"></i>Created @App.CreatedAt.ToString("MMM d, yyyy")</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!confirmDelete)
|
||||
{
|
||||
<button class="btn btn-sm btn-outline-danger" @onclick="() => confirmDelete = true" title="Delete app">
|
||||
<i class="bi bi-trash me-1"></i>Delete
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (confirmDelete)
|
||||
{
|
||||
<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">
|
||||
<i class="bi bi-exclamation-triangle me-1"></i>
|
||||
Permanently delete <strong>@App.Name</strong> and all its data?
|
||||
</span>
|
||||
<div>
|
||||
<button class="btn btn-sm btn-danger me-1" @onclick="DeleteApp">Yes, delete</button>
|
||||
<button class="btn btn-sm btn-outline-secondary" @onclick="() => confirmDelete = false">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* --- App sub-sections (tabs for future expansion) --- *@
|
||||
<ul class="nav nav-pills mb-3 gap-1">
|
||||
<li class="nav-item">
|
||||
<button class="nav-link @(section == "environments" ? "active" : "")" @onclick='() => section = "environments"'>
|
||||
<i class="bi bi-layers me-1"></i>Environments
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link @(section == "deployments" ? "active" : "")" @onclick="SwitchToDeployments">
|
||||
<i class="bi bi-rocket-takeoff me-1"></i>Deployments
|
||||
@if (deployments is not null && deployments.Count > 0)
|
||||
{
|
||||
<span class="badge bg-primary ms-1">@deployments.Count</span>
|
||||
}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@if (section == "environments")
|
||||
{
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-white py-2">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="bi bi-layers me-2 text-primary"></i>
|
||||
<strong>Environment Links</strong>
|
||||
</div>
|
||||
<small class="text-muted">Select which environments this app is deployed to.</small>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if (environments is null)
|
||||
{
|
||||
<div class="text-center py-3">
|
||||
<div class="spinner-border spinner-border-sm text-primary" role="status"></div>
|
||||
</div>
|
||||
}
|
||||
else if (environments.Count == 0)
|
||||
{
|
||||
<div class="text-center py-4">
|
||||
<i class="bi bi-layers text-muted" style="font-size: 2rem;"></i>
|
||||
<p class="text-muted small mt-2 mb-0">No environments exist for this tenant yet. Create environments first.</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-2">
|
||||
@foreach (Data.Environment env in environments)
|
||||
{
|
||||
bool isLinked = linkedEnvIds.Contains(env.Id);
|
||||
|
||||
<div class="col">
|
||||
<div class="card @(isLinked ? "border-success bg-success bg-opacity-10" : "border-light")"
|
||||
role="button" style="cursor: pointer;"
|
||||
@onclick="() => ToggleEnvironment(env.Id, isLinked)">
|
||||
<div class="card-body py-2 d-flex align-items-center">
|
||||
<i class="bi @(isLinked ? "bi-check-circle-fill text-success" : "bi-circle text-muted") me-2 fs-5"></i>
|
||||
<div class="flex-grow-1">
|
||||
<span class="fw-medium">@env.Name</span>
|
||||
</div>
|
||||
@if (isLinked)
|
||||
{
|
||||
<span class="badge bg-success">Active</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@* ═══════════════════════════════════════════════════════════════════
|
||||
Deployments Tab — manage deployments (Manual, YAML, Helm) targeting
|
||||
specific clusters. Shows ArgoCD-style sync/health status.
|
||||
═══════════════════════════════════════════════════════════════════ *@
|
||||
@if (section == "deployments")
|
||||
{
|
||||
@* --- Deployment detail view (drill-down into a single deployment) --- *@
|
||||
@if (selectedDeployment is not null)
|
||||
{
|
||||
<nav class="mb-3">
|
||||
<button class="btn btn-sm btn-link text-decoration-none p-0" @onclick="() => selectedDeployment = null">
|
||||
<i class="bi bi-arrow-left me-1"></i>Back to deployments
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<div class="card shadow-sm mb-3">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-start justify-content-between">
|
||||
<div>
|
||||
<h5 class="mb-1">
|
||||
<i class="bi bi-rocket-takeoff me-2 text-primary"></i>@selectedDeployment.Name
|
||||
</h5>
|
||||
<div class="d-flex flex-wrap gap-2 text-muted small">
|
||||
<span>@GetTypeBadge(selectedDeployment.Type)</span>
|
||||
<span><i class="bi bi-layers me-1"></i>@selectedDeployment.Environment?.Name</span>
|
||||
<span><i class="bi bi-hdd-network me-1"></i>@selectedDeployment.Cluster?.Name</span>
|
||||
<span><i class="bi bi-box me-1"></i>@selectedDeployment.Namespace</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-start gap-1">
|
||||
@GetSyncBadge(selectedDeployment.SyncStatus)
|
||||
@GetHealthBadge(selectedDeployment.HealthStatus)
|
||||
|
||||
@if (!confirmDeleteDeployment)
|
||||
{
|
||||
<button class="btn btn-sm btn-outline-danger ms-2" @onclick="() => confirmDeleteDeployment = true" title="Delete deployment">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (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">
|
||||
<i class="bi bi-exclamation-triangle me-1"></i>
|
||||
Permanently delete <strong>@selectedDeployment.Name</strong> and all its manifests and resources?
|
||||
</span>
|
||||
<div>
|
||||
<button class="btn btn-sm btn-danger me-1" @onclick="DeleteDeployment">Yes, delete</button>
|
||||
<button class="btn btn-sm btn-outline-secondary" @onclick="() => confirmDeleteDeployment = false">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (selectedDeployment.Type == DeploymentType.HelmChart)
|
||||
{
|
||||
<div class="mt-3 p-2 bg-light rounded">
|
||||
<small class="text-muted">
|
||||
<i class="bi bi-box-seam me-1"></i>
|
||||
@selectedDeployment.HelmChartName
|
||||
<span class="mx-1">@selectedDeployment.HelmChartVersion</span>
|
||||
<span class="text-muted">from @selectedDeployment.HelmRepoUrl</span>
|
||||
</small>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* --- Sub-tabs: Manifests / Resources / Helm Values --- *@
|
||||
<ul class="nav nav-tabs mb-3">
|
||||
@if (selectedDeployment.Type != DeploymentType.HelmChart)
|
||||
{
|
||||
<li class="nav-item">
|
||||
<button class="nav-link @(deploymentSection == "manifests" ? "active" : "")"
|
||||
@onclick='() => deploymentSection = "manifests"'>
|
||||
<i class="bi bi-file-earmark-code me-1"></i>Manifests
|
||||
</button>
|
||||
</li>
|
||||
}
|
||||
|
||||
@if (selectedDeployment.Type == DeploymentType.HelmChart)
|
||||
{
|
||||
<li class="nav-item">
|
||||
<button class="nav-link @(deploymentSection == "helm-values" ? "active" : "")"
|
||||
@onclick='() => deploymentSection = "helm-values"'>
|
||||
<i class="bi bi-sliders me-1"></i>Values
|
||||
</button>
|
||||
</li>
|
||||
}
|
||||
|
||||
<li class="nav-item">
|
||||
<button class="nav-link @(deploymentSection == "resources" ? "active" : "")"
|
||||
@onclick="OpenResourcesTab">
|
||||
<i class="bi bi-diagram-3 me-1"></i>Resources
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@* ── Manifests sub-tab ── *@
|
||||
@if (deploymentSection == "manifests")
|
||||
{
|
||||
<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-file-earmark-code me-2 text-primary"></i>
|
||||
<strong>Manifests</strong>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-outline-primary" @onclick="() => showAddManifest = !showAddManifest">
|
||||
<i class="bi bi-plus me-1"></i>Add Manifest
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if (showAddManifest)
|
||||
{
|
||||
<div class="card border-primary mb-3">
|
||||
<div class="card-body">
|
||||
<h6 class="mb-3"><i class="bi bi-plus-circle me-1"></i>Add YAML Manifest</h6>
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small">Kind</label>
|
||||
<input class="form-control form-control-sm" @bind="newManifestKind" placeholder="e.g. Deployment" />
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small">Name</label>
|
||||
<input class="form-control form-control-sm" @bind="newManifestName" placeholder="e.g. billing-api" />
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small">Sort Order</label>
|
||||
<input class="form-control form-control-sm" type="number" @bind="newManifestSortOrder" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">YAML Content</label>
|
||||
<textarea class="form-control form-control-sm font-monospace" rows="8"
|
||||
@bind="newManifestYaml" placeholder="apiVersion: v1 kind: Service ..."></textarea>
|
||||
</div>
|
||||
<div class="d-flex gap-1">
|
||||
<button class="btn btn-sm btn-primary" @onclick="AddManifest"
|
||||
disabled="@(string.IsNullOrWhiteSpace(newManifestKind) || string.IsNullOrWhiteSpace(newManifestYaml))">
|
||||
Add
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-secondary" @onclick="() => showAddManifest = false">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (manifests is null)
|
||||
{
|
||||
<div class="text-center py-3">
|
||||
<div class="spinner-border spinner-border-sm text-primary" role="status"></div>
|
||||
</div>
|
||||
}
|
||||
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 mb-0">No manifests yet. Add YAML manifests to define what gets deployed.</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
@foreach (DeploymentManifest manifest in manifests)
|
||||
{
|
||||
<div class="card mb-2">
|
||||
<div class="card-header bg-white py-2 d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<span class="badge bg-secondary me-1">@manifest.Kind</span>
|
||||
<span class="fw-medium">@manifest.Name</span>
|
||||
<span class="text-muted small ms-2">#@manifest.SortOrder</span>
|
||||
</div>
|
||||
<div class="d-flex gap-1">
|
||||
@if (editingManifestId == manifest.Id)
|
||||
{
|
||||
<button class="btn btn-sm btn-primary" @onclick="() => SaveManifest(manifest.Id)">Save</button>
|
||||
<button class="btn btn-sm btn-outline-secondary" @onclick="() => editingManifestId = null">Cancel</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button class="btn btn-sm btn-outline-primary" @onclick="() => StartEditManifest(manifest)">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger" @onclick="() => DeleteManifest(manifest.Id)">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (editingManifestId == manifest.Id)
|
||||
{
|
||||
<div class="card-body p-2">
|
||||
<textarea class="form-control form-control-sm font-monospace" rows="10"
|
||||
@bind="editManifestYaml"></textarea>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="card-body p-2">
|
||||
<pre class="mb-0 small" style="max-height: 200px; overflow-y: auto;">@manifest.YamlContent</pre>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@* ── Helm Values sub-tab ── *@
|
||||
@if (deploymentSection == "helm-values")
|
||||
{
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-white py-2">
|
||||
<i class="bi bi-sliders me-2 text-primary"></i>
|
||||
<strong>Helm Values</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted small mb-2">Override the chart's default values (YAML format).</p>
|
||||
<textarea class="form-control font-monospace" rows="12"
|
||||
@bind="helmValuesYaml"
|
||||
placeholder="replicas: 3 persistence: size: 10Gi"></textarea>
|
||||
<button class="btn btn-sm btn-primary mt-2" @onclick="SaveHelmValues">
|
||||
<i class="bi bi-save me-1"></i>Save Values
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@* ── Resources sub-tab (ArgoCD-style tree) ── *@
|
||||
@if (deploymentSection == "resources")
|
||||
{
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-white py-2">
|
||||
<i class="bi bi-diagram-3 me-2 text-primary"></i>
|
||||
<strong>Resource Tree</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if (resourceTree is null)
|
||||
{
|
||||
<div class="text-center py-3">
|
||||
<div class="spinner-border spinner-border-sm text-primary" role="status"></div>
|
||||
</div>
|
||||
}
|
||||
else if (resourceTree.Count == 0)
|
||||
{
|
||||
<div class="text-center py-4">
|
||||
<i class="bi bi-diagram-3 text-muted" style="font-size: 2rem;"></i>
|
||||
<p class="text-muted small mt-2 mb-0">No tracked resources yet. Resources appear here after the deployment syncs to the cluster.</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
@foreach (DeploymentResource root in resourceTree)
|
||||
{
|
||||
<div class="mb-2">
|
||||
@RenderResource(root, 0)
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@* --- Deployment list view --- *@
|
||||
<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-rocket-takeoff me-2 text-primary"></i>
|
||||
<strong>Deployments</strong>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-outline-primary" @onclick="() => showCreateDeployment = !showCreateDeployment">
|
||||
<i class="bi bi-plus me-1"></i>New Deployment
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@* --- Create deployment form --- *@
|
||||
@if (showCreateDeployment)
|
||||
{
|
||||
<div class="card border-primary mb-3">
|
||||
<div class="card-body">
|
||||
<h6 class="mb-3"><i class="bi bi-plus-circle me-1"></i>Create Deployment</h6>
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small">Name</label>
|
||||
<input class="form-control form-control-sm" @bind="newDeployName" placeholder="e.g. billing-api-prod" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small">Type</label>
|
||||
<select class="form-select form-select-sm" @bind="newDeployType">
|
||||
<option value="@DeploymentType.Manual">Manual</option>
|
||||
<option value="@DeploymentType.Yaml">YAML</option>
|
||||
<option value="@DeploymentType.HelmChart">Helm Chart</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small">Environment</label>
|
||||
<select class="form-select form-select-sm" @bind="newDeployEnvId">
|
||||
<option value="">Select...</option>
|
||||
@if (environments is not null)
|
||||
{
|
||||
@foreach (Data.Environment env in environments)
|
||||
{
|
||||
<option value="@env.Id">@env.Name</option>
|
||||
}
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small">Cluster</label>
|
||||
<select class="form-select form-select-sm" @bind="newDeployClusterId">
|
||||
<option value="">Select...</option>
|
||||
@if (clusters is not null)
|
||||
{
|
||||
@foreach (KubernetesCluster cluster in clusters.Where(c => newDeployEnvId == Guid.Empty || c.EnvironmentId == newDeployEnvId))
|
||||
{
|
||||
<option value="@cluster.Id">@cluster.Name</option>
|
||||
}
|
||||
}
|
||||
</select>
|
||||
</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. billing" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (newDeployType == DeploymentType.HelmChart)
|
||||
{
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small">Helm Repo URL</label>
|
||||
<input class="form-control form-control-sm" @bind="newHelmRepoUrl"
|
||||
placeholder="https://charts.bitnami.com/bitnami" />
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small">Chart Name</label>
|
||||
<input class="form-control form-control-sm" @bind="newHelmChartName"
|
||||
placeholder="e.g. postgresql" />
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small">Chart Version</label>
|
||||
<input class="form-control form-control-sm" @bind="newHelmChartVersion"
|
||||
placeholder="e.g. 15.5.0" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrEmpty(createDeployError))
|
||||
{
|
||||
<div class="alert alert-danger py-1 small mb-2">@createDeployError</div>
|
||||
}
|
||||
|
||||
<div class="d-flex gap-1">
|
||||
<button class="btn btn-sm btn-primary" @onclick="CreateDeployment"
|
||||
disabled="@(string.IsNullOrWhiteSpace(newDeployName) || newDeployEnvId == Guid.Empty || newDeployClusterId == Guid.Empty)">
|
||||
Create
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-secondary" @onclick="() => showCreateDeployment = false">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@* --- Deployment list --- *@
|
||||
@if (deployments is null)
|
||||
{
|
||||
<div class="text-center py-3">
|
||||
<div class="spinner-border spinner-border-sm text-primary" role="status"></div>
|
||||
</div>
|
||||
}
|
||||
else if (deployments.Count == 0)
|
||||
{
|
||||
<div class="text-center py-4">
|
||||
<i class="bi bi-rocket-takeoff text-muted" style="font-size: 2rem;"></i>
|
||||
<p class="text-muted small mt-2 mb-0">No deployments yet. Create one to start deploying to Kubernetes.</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="row row-cols-1 row-cols-lg-2 g-2">
|
||||
@foreach (AppDeployment deploy in deployments)
|
||||
{
|
||||
<div class="col">
|
||||
<div class="card border-light h-100" role="button" style="cursor: pointer;"
|
||||
@onclick="() => SelectDeployment(deploy)">
|
||||
<div class="card-body py-2">
|
||||
<div class="d-flex align-items-center justify-content-between mb-1">
|
||||
<span class="fw-medium">
|
||||
<i class="bi bi-rocket-takeoff me-1 text-primary"></i>@deploy.Name
|
||||
</span>
|
||||
<div class="d-flex gap-1">
|
||||
@GetSyncBadge(deploy.SyncStatus)
|
||||
@GetHealthBadge(deploy.HealthStatus)
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-2 text-muted small">
|
||||
@GetTypeBadge(deploy.Type)
|
||||
<span><i class="bi bi-layers me-1"></i>@deploy.Environment?.Name</span>
|
||||
<span><i class="bi bi-hdd-network me-1"></i>@deploy.Cluster?.Name</span>
|
||||
<span><i class="bi bi-box me-1"></i>@deploy.Namespace</span>
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrEmpty(deploy.StatusMessage))
|
||||
{
|
||||
<div class="text-muted small mt-1 text-truncate">@deploy.StatusMessage</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter] public required Data.App App { get; set; }
|
||||
[Parameter] public Guid TenantId { get; set; }
|
||||
[Parameter] public string CustomerName { get; set; } = "";
|
||||
[Parameter] public EventCallback OnBack { get; set; }
|
||||
[Parameter] public EventCallback OnDeleted { get; set; }
|
||||
|
||||
private string section = "environments";
|
||||
private bool confirmDelete;
|
||||
|
||||
// Environments
|
||||
private List<Data.Environment>? environments;
|
||||
private HashSet<Guid> linkedEnvIds = new();
|
||||
|
||||
// Deployments
|
||||
private List<AppDeployment>? deployments;
|
||||
private List<KubernetesCluster>? clusters;
|
||||
private AppDeployment? selectedDeployment;
|
||||
private string deploymentSection = "manifests";
|
||||
private bool confirmDeleteDeployment;
|
||||
|
||||
// Create deployment form
|
||||
private bool showCreateDeployment;
|
||||
private string newDeployName = "";
|
||||
private DeploymentType newDeployType = DeploymentType.Manual;
|
||||
private Guid newDeployEnvId;
|
||||
private Guid newDeployClusterId;
|
||||
private string newDeployNamespace = "";
|
||||
private string newHelmRepoUrl = "";
|
||||
private string newHelmChartName = "";
|
||||
private string newHelmChartVersion = "";
|
||||
private string? createDeployError;
|
||||
|
||||
// Manifest management
|
||||
private List<DeploymentManifest>? manifests;
|
||||
private bool showAddManifest;
|
||||
private string newManifestKind = "";
|
||||
private string newManifestName = "";
|
||||
private string newManifestYaml = "";
|
||||
private int newManifestSortOrder;
|
||||
private Guid? editingManifestId;
|
||||
private string editManifestYaml = "";
|
||||
|
||||
// Helm values
|
||||
private string helmValuesYaml = "";
|
||||
|
||||
// Resource tree
|
||||
private List<DeploymentResource>? resourceTree;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadEnvironments();
|
||||
}
|
||||
|
||||
// ──────── App ────────
|
||||
|
||||
private async Task DeleteApp()
|
||||
{
|
||||
await TenantService.DeleteAppAsync(App.Id);
|
||||
await OnDeleted.InvokeAsync();
|
||||
}
|
||||
|
||||
// ──────── Environments ────────
|
||||
|
||||
private async Task LoadEnvironments()
|
||||
{
|
||||
environments = await TenantService.GetEnvironmentsAsync(TenantId);
|
||||
|
||||
// Refresh the app to get current environment links.
|
||||
List<Data.App> apps = await TenantService.GetAppsAsync(App.CustomerId);
|
||||
Data.App? freshApp = apps.FirstOrDefault(a => a.Id == App.Id);
|
||||
|
||||
linkedEnvIds = freshApp?.AppEnvironments.Select(ae => ae.EnvironmentId).ToHashSet() ?? new();
|
||||
}
|
||||
|
||||
private async Task ToggleEnvironment(Guid envId, bool currentlyLinked)
|
||||
{
|
||||
if (currentlyLinked)
|
||||
{
|
||||
await TenantService.UnlinkAppFromEnvironmentAsync(App.Id, envId);
|
||||
}
|
||||
else
|
||||
{
|
||||
await TenantService.LinkAppToEnvironmentAsync(App.Id, envId);
|
||||
}
|
||||
|
||||
await LoadEnvironments();
|
||||
}
|
||||
|
||||
// ──────── Deployments ────────
|
||||
|
||||
private async Task SwitchToDeployments()
|
||||
{
|
||||
section = "deployments";
|
||||
|
||||
if (deployments is null)
|
||||
{
|
||||
await LoadDeployments();
|
||||
}
|
||||
|
||||
if (clusters is null)
|
||||
{
|
||||
clusters = await TenantService.GetClustersAsync(TenantId);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadDeployments()
|
||||
{
|
||||
deployments = await DeploymentService.GetDeploymentsAsync(App.Id);
|
||||
}
|
||||
|
||||
private async Task CreateDeployment()
|
||||
{
|
||||
createDeployError = null;
|
||||
|
||||
try
|
||||
{
|
||||
await DeploymentService.CreateDeploymentAsync(
|
||||
App.Id, newDeployName, newDeployType,
|
||||
newDeployEnvId, newDeployClusterId, newDeployNamespace,
|
||||
newDeployType == DeploymentType.HelmChart ? newHelmRepoUrl : null,
|
||||
newDeployType == DeploymentType.HelmChart ? newHelmChartName : null,
|
||||
newDeployType == DeploymentType.HelmChart ? newHelmChartVersion : null);
|
||||
|
||||
// Reset form and reload.
|
||||
showCreateDeployment = false;
|
||||
newDeployName = "";
|
||||
newDeployNamespace = "";
|
||||
newHelmRepoUrl = "";
|
||||
newHelmChartName = "";
|
||||
newHelmChartVersion = "";
|
||||
await LoadDeployments();
|
||||
}
|
||||
catch (DbUpdateException)
|
||||
{
|
||||
createDeployError = $"A deployment named '{newDeployName}' already exists for this app.";
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SelectDeployment(AppDeployment deploy)
|
||||
{
|
||||
selectedDeployment = deploy;
|
||||
confirmDeleteDeployment = false;
|
||||
|
||||
// Default to the appropriate sub-tab based on type.
|
||||
if (deploy.Type == DeploymentType.HelmChart)
|
||||
{
|
||||
deploymentSection = "helm-values";
|
||||
helmValuesYaml = deploy.HelmValues ?? "";
|
||||
}
|
||||
else
|
||||
{
|
||||
deploymentSection = "manifests";
|
||||
await LoadManifests();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteDeployment()
|
||||
{
|
||||
if (selectedDeployment is not null)
|
||||
{
|
||||
await DeploymentService.DeleteDeploymentAsync(selectedDeployment.Id);
|
||||
selectedDeployment = null;
|
||||
confirmDeleteDeployment = false;
|
||||
await LoadDeployments();
|
||||
}
|
||||
}
|
||||
|
||||
// ──────── Manifests ────────
|
||||
|
||||
private async Task LoadManifests()
|
||||
{
|
||||
if (selectedDeployment is not null)
|
||||
{
|
||||
manifests = await DeploymentService.GetManifestsAsync(selectedDeployment.Id);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AddManifest()
|
||||
{
|
||||
if (selectedDeployment is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await DeploymentService.AddManifestAsync(
|
||||
selectedDeployment.Id, newManifestKind, newManifestName, newManifestYaml, newManifestSortOrder);
|
||||
|
||||
showAddManifest = false;
|
||||
newManifestKind = "";
|
||||
newManifestName = "";
|
||||
newManifestYaml = "";
|
||||
newManifestSortOrder = 0;
|
||||
await LoadManifests();
|
||||
}
|
||||
|
||||
private void StartEditManifest(DeploymentManifest manifest)
|
||||
{
|
||||
editingManifestId = manifest.Id;
|
||||
editManifestYaml = manifest.YamlContent;
|
||||
}
|
||||
|
||||
private async Task SaveManifest(Guid manifestId)
|
||||
{
|
||||
await DeploymentService.UpdateManifestAsync(manifestId, editManifestYaml);
|
||||
editingManifestId = null;
|
||||
await LoadManifests();
|
||||
}
|
||||
|
||||
private async Task DeleteManifest(Guid manifestId)
|
||||
{
|
||||
await DeploymentService.DeleteManifestAsync(manifestId);
|
||||
await LoadManifests();
|
||||
}
|
||||
|
||||
// ──────── Helm Values ────────
|
||||
|
||||
private async Task SaveHelmValues()
|
||||
{
|
||||
if (selectedDeployment is not null)
|
||||
{
|
||||
await DeploymentService.UpdateHelmValuesAsync(selectedDeployment.Id, helmValuesYaml);
|
||||
}
|
||||
}
|
||||
|
||||
// ──────── Resources ────────
|
||||
|
||||
private async Task OpenResourcesTab()
|
||||
{
|
||||
deploymentSection = "resources";
|
||||
|
||||
if (selectedDeployment is not null)
|
||||
{
|
||||
resourceTree = await DeploymentService.GetResourceTreeAsync(selectedDeployment.Id);
|
||||
}
|
||||
}
|
||||
|
||||
// ──────── Rendering helpers ────────
|
||||
|
||||
private RenderFragment GetTypeBadge(DeploymentType type) => type switch
|
||||
{
|
||||
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>,
|
||||
_ => @<span class="badge bg-secondary">Unknown</span>
|
||||
};
|
||||
|
||||
private RenderFragment GetSyncBadge(SyncStatus status) => status switch
|
||||
{
|
||||
SyncStatus.Synced => @<span class="badge bg-success"><i class="bi bi-check-circle me-1"></i>Synced</span>,
|
||||
SyncStatus.OutOfSync => @<span class="badge bg-warning text-dark"><i class="bi bi-exclamation-circle me-1"></i>OutOfSync</span>,
|
||||
SyncStatus.Syncing => @<span class="badge bg-info"><i class="bi bi-arrow-repeat me-1"></i>Syncing</span>,
|
||||
SyncStatus.Failed => @<span class="badge bg-danger"><i class="bi bi-x-circle me-1"></i>Failed</span>,
|
||||
_ => @<span class="badge bg-secondary">Unknown</span>
|
||||
};
|
||||
|
||||
private RenderFragment GetHealthBadge(HealthStatus status) => status switch
|
||||
{
|
||||
HealthStatus.Healthy => @<span class="badge bg-success"><i class="bi bi-heart-fill me-1"></i>Healthy</span>,
|
||||
HealthStatus.Progressing => @<span class="badge bg-info"><i class="bi bi-hourglass-split me-1"></i>Progressing</span>,
|
||||
HealthStatus.Degraded => @<span class="badge bg-warning text-dark"><i class="bi bi-exclamation-triangle me-1"></i>Degraded</span>,
|
||||
HealthStatus.Missing => @<span class="badge bg-danger"><i class="bi bi-question-circle me-1"></i>Missing</span>,
|
||||
HealthStatus.Suspended => @<span class="badge bg-secondary"><i class="bi bi-pause-circle me-1"></i>Suspended</span>,
|
||||
_ => @<span class="badge bg-secondary">Unknown</span>
|
||||
};
|
||||
|
||||
private RenderFragment RenderResource(DeploymentResource resource, int depth) => __builder =>
|
||||
{
|
||||
<div class="d-flex align-items-center py-1 border-bottom" style="padding-left: @(depth * 24)px;">
|
||||
<span class="badge bg-secondary me-2 small">@resource.Kind</span>
|
||||
<span class="fw-medium small flex-grow-1">@resource.Name</span>
|
||||
<div class="d-flex gap-1">
|
||||
@GetSyncBadge(resource.SyncStatus)
|
||||
@GetHealthBadge(resource.HealthStatus)
|
||||
</div>
|
||||
</div>
|
||||
@if (resource.ChildResources?.Count > 0)
|
||||
{
|
||||
@foreach (DeploymentResource child in resource.ChildResources)
|
||||
{
|
||||
@RenderResource(child, depth + 1)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user