Files
Entkube/src/EntKube.Web/Components/Pages/Tenants/AppDetail.razor
Nils Blomgren 5da40698ad
Some checks failed
Build and Deploy / Build and push image (push) Failing after 5m50s
Build and Deploy / Deploy to server (push) Has been skipped
server management
2026-06-11 15:09:31 +02:00

220 lines
9.4 KiB
Plaintext

@using EntKube.Web.Data
@using EntKube.Web.Services
@inject TenantService TenantService
@* ═══════════════════════════════════════════════════════════════════
App Detail — app-level header + environment cards.
All per-environment content (deployments, resources, governance,
metrics, external access) is handled by AppEnvironmentDetail.
═══════════════════════════════════════════════════════════════════ *@
@if (selectedEnvironment is not null)
{
<AppEnvironmentDetail App="App"
Environment="selectedEnvironment"
TenantId="TenantId"
IsPortalView="false"
OnBack="() => selectedEnvironment = null" />
}
else
{
@* --- 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>
<ConfirmDialog Visible="confirmDelete"
Message="@($"Permanently delete {App.Name} and all its data?")"
ConfirmText="Yes, delete"
IsBusy="deleting"
OnConfirm="DeleteApp"
OnCancel="() => confirmDelete = false" />
</div>
</div>
@* --- Linked environments (clickable cards → per-env detail) --- *@
<div class="d-flex align-items-center justify-content-between mb-3">
<h6 class="mb-0 fw-semibold">
<i class="bi bi-layers me-2 text-primary"></i>Environments
</h6>
<button class="btn btn-sm btn-link p-0 text-muted" @onclick="() => showLinkPanel = !showLinkPanel">
<i class="bi @(showLinkPanel ? "bi-chevron-up" : "bi-link-45deg") me-1"></i>
@(showLinkPanel ? "Hide" : "Link / Unlink")
</button>
</div>
@if (showLinkPanel)
{
<div class="card mb-3">
<div class="card-header bg-white py-2">
<small class="text-muted">Select which environments this app is deployed in.</small>
</div>
<div class="card-body py-2">
<LoadingPanel Loading="@(allEnvironments is null)">
@if (allEnvironments is not null && allEnvironments.Count == 0)
{
<EmptyState Icon="bi-layers"
Message="No environments exist for this tenant yet. Create environments first." />
}
else if (allEnvironments is not null)
{
<div class="row row-cols-1 row-cols-md-3 g-2">
@foreach (Data.Environment env in allEnvironments)
{
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>
<span class="fw-medium">@env.Name</span>
@if (isLinked) { <span class="badge bg-success ms-auto">Active</span> }
</div>
</div>
</div>
}
</div>
}
</LoadingPanel>
</div>
</div>
}
<LoadingPanel Loading="@(linkedEnvironments is null)">
@if (linkedEnvironments is not null && linkedEnvironments.Count == 0)
{
<div class="card border-dashed">
<div class="card-body">
<EmptyState Icon="bi-layers"
Title="No environments linked yet"
Message="Use the Link / Unlink button above to connect this app to an environment." />
</div>
</div>
}
else if (linkedEnvironments is not null)
{
<div class="row row-cols-1 row-cols-md-2 row-cols-xl-3 g-3">
@foreach (Data.Environment env in linkedEnvironments)
{
int deployCount = deployCountByEnv.GetValueOrDefault(env.Id, 0);
<div class="col">
<div class="card shadow-sm border-light h-100 env-card" role="button" style="cursor: pointer;"
@onclick="() => selectedEnvironment = env">
<div class="card-body">
<div class="d-flex align-items-start justify-content-between mb-2">
<div class="d-flex align-items-center gap-2">
<i class="bi bi-layers fs-4 text-primary"></i>
<h6 class="mb-0 fw-semibold">@env.Name</h6>
</div>
<i class="bi bi-chevron-right text-muted small mt-1"></i>
</div>
<div class="d-flex flex-wrap gap-2 small text-muted">
@if (deployCount > 0)
{
<span><i class="bi bi-rocket-takeoff me-1"></i>@deployCount deployment@(deployCount == 1 ? "" : "s")</span>
}
else
{
<span class="text-muted fst-italic">No deployments</span>
}
</div>
</div>
</div>
</div>
}
</div>
}
</LoadingPanel>
}
<style>
.env-card { transition: box-shadow .14s, transform .1s; }
.env-card:hover { box-shadow: 0 4px 14px rgba(0,0,0,.13) !important; transform: translateY(-1px); }
.border-dashed { border-style: dashed !important; }
</style>
@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 bool confirmDelete;
private bool deleting;
private bool showLinkPanel;
private List<Data.Environment>? allEnvironments;
private List<Data.Environment>? linkedEnvironments;
private HashSet<Guid> linkedEnvIds = new();
private Dictionary<Guid, int> deployCountByEnv = new();
private Data.Environment? selectedEnvironment;
protected override async Task OnInitializedAsync()
{
await LoadEnvironments();
}
private async Task DeleteApp()
{
deleting = true;
await TenantService.DeleteAppAsync(App.Id);
deleting = false;
confirmDelete = false;
await OnDeleted.InvokeAsync();
}
private async Task LoadEnvironments()
{
allEnvironments = await TenantService.GetEnvironmentsAsync(TenantId);
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();
linkedEnvironments = allEnvironments.Where(e => linkedEnvIds.Contains(e.Id)).ToList();
deployCountByEnv = (freshApp?.Deployments ?? [])
.GroupBy(d => d.EnvironmentId)
.ToDictionary(g => g.Key, g => g.Count());
}
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();
}
}