- @if (secrets is null)
+
+ @if (secrets is not null && secrets.Count == 0)
{
-
+
}
- else if (secrets.Count == 0)
- {
-
-
-
No secrets stored for this app yet.
- @if (AccessRole >= CustomerAccessRole.Admin)
- {
-
Use the form above to add one.
- }
-
- }
- else
+ else if (secrets is not null)
{
}
+
@if (syncOutput is not null)
{
@syncOutput
diff --git a/src/EntKube.Web/Components/Pages/Shared/EmptyState.razor b/src/EntKube.Web/Components/Pages/Shared/EmptyState.razor
new file mode 100644
index 0000000..e68c8d9
--- /dev/null
+++ b/src/EntKube.Web/Components/Pages/Shared/EmptyState.razor
@@ -0,0 +1,25 @@
+
+
+ @if (!string.IsNullOrEmpty(Title))
+ {
+
@Title
+ }
+ @if (!string.IsNullOrEmpty(Message))
+ {
+
@Message
+ }
+ @if (OnAction.HasDelegate && !string.IsNullOrEmpty(ActionLabel))
+ {
+
+ @ActionLabel
+
+ }
+
+
+@code {
+ [Parameter] public string Icon { get; set; } = "bi-inbox";
+ [Parameter] public string? Title { get; set; }
+ [Parameter] public string? Message { get; set; }
+ [Parameter] public string? ActionLabel { get; set; }
+ [Parameter] public EventCallback OnAction { get; set; }
+}
diff --git a/src/EntKube.Web/Components/Pages/Shared/LoadingPanel.razor b/src/EntKube.Web/Components/Pages/Shared/LoadingPanel.razor
new file mode 100644
index 0000000..0ae555f
--- /dev/null
+++ b/src/EntKube.Web/Components/Pages/Shared/LoadingPanel.razor
@@ -0,0 +1,27 @@
+@if (Loading)
+{
+
+
+ @if (!string.IsNullOrEmpty(LoadingText))
+ {
+
@LoadingText
+ }
+
+}
+else if (!string.IsNullOrEmpty(Error))
+{
+
+ @Error
+
+}
+else
+{
+ @ChildContent
+}
+
+@code {
+ [Parameter] public bool Loading { get; set; }
+ [Parameter] public string? Error { get; set; }
+ [Parameter] public string? LoadingText { get; set; }
+ [Parameter] public RenderFragment? ChildContent { get; set; }
+}
diff --git a/src/EntKube.Web/Components/Pages/Shared/ResourceTreePanel.razor b/src/EntKube.Web/Components/Pages/Shared/ResourceTreePanel.razor
index a43b401..d896a3d 100644
--- a/src/EntKube.Web/Components/Pages/Shared/ResourceTreePanel.razor
+++ b/src/EntKube.Web/Components/Pages/Shared/ResourceTreePanel.razor
@@ -3,112 +3,265 @@
@inject KubernetesOperationsService K8sOps
@* ═══════════════════════════════════════════════════════════════════
- ResourceTreePanel — ArgoCD-style resource ownership tree.
+ ResourceTreePanel — proper ArgoCD-style graph.
- Each node renders as a card with a health-coloured left border,
- kind icon, kind badge, resource name, status and health label.
- Children are indented beneath a dashed vertical guide line,
- matching the ArgoCD Application detail layout.
+ Layout: horizontal tree, each depth level is a column.
+ Subtrees are centred over their children using a two-pass
+ layout algorithm.
+ Lines: SVG cubic bezier curves drawn on an absolute-positioned
+ overlay that covers the whole graph container.
+ Cards: absolute-positioned; left-border colour = health status;
+ kind icon, name, status, health badge and action buttons.
+ Actions: floating panel positioned under (or above) the active card.
═══════════════════════════════════════════════════════════════════ *@
-@if (Loading)
+
+@if (!Loading && Error is null && (Resources is null || Resources.Count == 0))
{
-
+
}
-else if (Error is not null)
+else if (!Loading && Error is null && Resources is not null && Resources.Count > 0)
{
-
- @Error
-
-}
-else if (Resources is null || Resources.Count == 0)
-{
-
-
-
- No resources found in namespace @Namespace.
-
-
-}
-else
-{
- @if (_actionFeedback is not null)
+ @if (_feedback is not null)
{
-
-
- @_actionFeedback
-
_actionFeedback = null">
+
+ @_feedback
+ _feedback = null">
}
-
- @foreach (DeploymentResource root in Resources)
- {
- @RenderNode(root)
- }
+
+
+
+ @* ── SVG bezier connectors ── *@
+
+ @foreach (SvgLine ln in _lines)
+ {
+
+ }
+
+
+ @* ── Node cards ── *@
+ @foreach (NodeLayout nd in _nodes)
+ {
+ var cx = nd.Col * ColW + 12;
+ var cy = nd.Y + 12;
+ var hc = HealthColor(nd.Node.HealthStatus);
+ var isActive = _activePanelNodeId == nd.Node.Id;
+
+
+
+
+
+
+
@nd.Node.Name
+
+ @if (!string.IsNullOrEmpty(nd.Node.StatusMessage))
+ {
+
@nd.Node.StatusMessage
+ }
+ else
+ {
+
@HealthLabel(nd.Node.HealthStatus)
+ }
+
+
+ @* ── Action buttons ── *@
+
+ @if (nd.Node.Kind == "Pod" && CanLogs)
+ {
+ PanelLogs(nd.Node)">
+ Logs
+
+ }
+ @if (CanEvents)
+ {
+ PanelEvents(nd.Node)">
+ Events
+
+ }
+ @if ((nd.Node.Kind is "Deployment" or "StatefulSet" or "DaemonSet") && CanRestart)
+ {
+ PanelRestart(nd.Node)">
+
+
+ }
+ @if ((nd.Node.Kind is "Deployment" or "StatefulSet" or "ReplicaSet") && CanScale)
+ {
+ PanelScale(nd.Node)">
+
+
+ }
+ @if ((nd.Node.Kind == "Pod" || nd.Node.Kind == "Job") && CanDelete)
+ {
+ PanelDelete(nd.Node)">
+
+
+ }
+
+
+
+ @* ── Floating action panel for this node ── *@
+ @if (isActive && _panelMode is not null)
+ {
+ @RenderPanel(nd.Node, cx, cy + CardH + 6)
+ }
+ }
+
}
+
@code {
[Parameter] public bool Loading { get; set; }
@@ -116,17 +269,41 @@ else
[Parameter] public List
? Resources { get; set; }
[Parameter] public string? Namespace { get; set; }
[Parameter] public Guid DeploymentId { get; set; }
- /// Null = full admin access (tenant view).
[Parameter] public CustomerAccessRole? AccessRole { get; set; }
+ // ── Access gates ────────────────────────────────────────────────────────
+
private bool CanLogs => AccessRole is null || AccessRole >= CustomerAccessRole.Viewer;
private bool CanEvents => AccessRole is null || AccessRole >= CustomerAccessRole.Viewer;
private bool CanRestart => AccessRole is null || AccessRole >= CustomerAccessRole.Operator;
private bool CanDelete => AccessRole is null || AccessRole >= CustomerAccessRole.Operator;
private bool CanScale => AccessRole is null || AccessRole >= CustomerAccessRole.Admin;
- private Guid? _activePanelId;
+ // ── Layout constants ─────────────────────────────────────────────────────
+
+ private const double CardW = 204;
+ private const double CardH = 86; // body + action bar
+ private const double ColGap = 72; // horizontal gap between columns (lines travel here)
+ private const double RowGap = 28;
+ private const double ColW = CardW + ColGap;
+ private const double RowH = CardH + RowGap;
+
+ // ── Layout state ─────────────────────────────────────────────────────────
+
+ private record NodeLayout(DeploymentResource Node, int Col, double Y, Guid? ParentId);
+ private record SvgLine(double X1, double Y1, double X2, double Y2);
+
+ private List _nodes = [];
+ private List _lines = [];
+ private double _graphW;
+ private double _graphH;
+
+ // ── Panel state ──────────────────────────────────────────────────────────
+
+ private Guid? _activePanelNodeId;
+ private DeploymentResource? _activePanelNode;
private string? _panelMode;
+
private bool _panelLoading;
private string? _panelText;
private string? _panelError;
@@ -134,10 +311,309 @@ else
private string? _selectedContainer;
private int _scaleValue = 1;
private bool _opBusy;
- private string? _actionFeedback;
- private bool _actionFeedbackOk;
- // ── Static visual helpers ────────────────────────────────────────────────
+ private string? _feedback;
+ private bool _feedbackOk;
+
+ // ── Lifecycle ────────────────────────────────────────────────────────────
+
+ protected override void OnParametersSet()
+ {
+ ComputeLayout();
+ }
+
+ // ── Layout engine ────────────────────────────────────────────────────────
+
+ private void ComputeLayout()
+ {
+ _nodes.Clear();
+ _lines.Clear();
+ if (Resources is null || Resources.Count == 0) return;
+
+ double y = 0;
+
+ void Layout(DeploymentResource node, int col, Guid? parentId)
+ {
+ double top = y;
+
+ if (node.ChildResources is { Count: > 0 } kids)
+ foreach (DeploymentResource kid in kids)
+ Layout(kid, col + 1, node.Id);
+ else
+ y += RowH;
+
+ double centerY = (top + y) / 2 - CardH / 2;
+ _nodes.Add(new NodeLayout(node, col, centerY, parentId));
+ }
+
+ foreach (DeploymentResource root in Resources)
+ Layout(root, 0, null);
+
+ Dictionary byId = _nodes.ToDictionary(n => n.Node.Id);
+
+ foreach (NodeLayout n in _nodes.Where(n => n.ParentId.HasValue))
+ {
+ if (byId.TryGetValue(n.ParentId!.Value, out NodeLayout? p))
+ {
+ _lines.Add(new SvgLine(
+ p.Col * ColW + CardW + 12, // parent right edge (+ canvas padding)
+ p.Y + CardH / 2 + 12, // parent vertical centre
+ n.Col * ColW + 12, // child left edge
+ n.Y + CardH / 2 + 12 // child vertical centre
+ ));
+ }
+ }
+
+ int maxCol = _nodes.Count > 0 ? _nodes.Max(n => n.Col) : 0;
+ _graphW = (maxCol + 1) * ColW;
+ _graphH = y;
+ }
+
+ private static string BezierPath(SvgLine l)
+ {
+ double dx = Math.Abs(l.X2 - l.X1) * 0.55;
+ return FormattableString.Invariant(
+ $"M {l.X1:F1} {l.Y1:F1} C {l.X1 + dx:F1} {l.Y1:F1} {l.X2 - dx:F1} {l.Y2:F1} {l.X2:F1} {l.Y2:F1}");
+ }
+
+ // ── Panel helpers ────────────────────────────────────────────────────────
+
+ private bool IsPanel(Guid id, string mode) =>
+ _activePanelNodeId == id && _panelMode == mode;
+
+ // String-literal-safe wrappers — avoids embedding "string" inside @onclick="..." attributes.
+ private void PanelLogs(DeploymentResource n) => TogglePanel(n.Id, n, "logs");
+ private void PanelEvents(DeploymentResource n) => TogglePanel(n.Id, n, "events");
+ private void PanelRestart(DeploymentResource n) => TogglePanel(n.Id, n, "restart");
+ private void PanelScale(DeploymentResource n) => TogglePanel(n.Id, n, "scale");
+ private void PanelDelete(DeploymentResource n) => TogglePanel(n.Id, n, "delete");
+
+ private void TogglePanel(Guid id, DeploymentResource node, string mode)
+ {
+ if (_activePanelNodeId == id && _panelMode == mode)
+ {
+ ClosePanel(); return;
+ }
+ ClosePanel();
+ _activePanelNodeId = id;
+ _activePanelNode = node;
+ _panelMode = mode;
+
+ if (mode == "logs") _ = LoadLogsAsync(node);
+ if (mode == "events") _ = LoadEventsAsync(node);
+ if (mode == "scale") _scaleValue = ParseDesiredReplicas(node.StatusMessage) ?? 1;
+ }
+
+ private void ClosePanel()
+ {
+ _activePanelNodeId = null;
+ _activePanelNode = null;
+ _panelMode = null;
+ _panelText = null;
+ _panelError = null;
+ _containers = [];
+ _selectedContainer = null;
+ }
+
+ // ── Async operations (fire-and-forget from sync context) ─────────────────
+
+ private async Task LoadLogsAsync(DeploymentResource node)
+ {
+ _panelLoading = true; _panelText = null; _panelError = null;
+ StateHasChanged();
+
+ var cr = await K8sOps.GetPodContainersAsync(DeploymentId, node.Name);
+ if (!cr.IsSuccess) { _panelError = cr.Error; _panelLoading = false; StateHasChanged(); return; }
+
+ _containers = cr.Data ?? [];
+ _selectedContainer = _containers.FirstOrDefault();
+ await FetchLogsAsync(node.Name);
+ }
+
+ private async Task FetchLogsAsync(string podName)
+ {
+ _panelLoading = true; _panelText = null; _panelError = null;
+ StateHasChanged();
+ var r = await K8sOps.GetPodLogsAsync(DeploymentId, podName, _selectedContainer);
+ _panelLoading = false;
+ if (r.IsSuccess) _panelText = r.Data ?? "(no output)"; else _panelError = r.Error;
+ StateHasChanged();
+ }
+
+ private async Task LoadEventsAsync(DeploymentResource node)
+ {
+ _panelLoading = true; _panelText = null; _panelError = null;
+ StateHasChanged();
+ var r = await K8sOps.GetResourceEventsAsync(DeploymentId, node.Kind, node.Name);
+ _panelLoading = false;
+ if (r.IsSuccess)
+ _panelText = r.Data is { Count: > 0 } l ? string.Join("\n", l) : "(no events)";
+ else _panelError = r.Error;
+ StateHasChanged();
+ }
+
+ private void SwitchContainer(string podName, string container)
+ {
+ _selectedContainer = container;
+ _ = FetchLogsAsync(podName);
+ }
+
+ private async Task ApplyScaleAsync(DeploymentResource node)
+ {
+ _opBusy = true; StateHasChanged();
+ var r = await K8sOps.ScaleWorkloadAsync(DeploymentId, node.Kind, node.Name, _scaleValue);
+ _opBusy = false; ClosePanel();
+ SetFeedback(r.IsSuccess, r.IsSuccess
+ ? $"{node.Kind} {node.Name} scaled to {_scaleValue}."
+ : r.Error ?? "Scale failed.");
+ }
+
+ private async Task ApplyRestartAsync(DeploymentResource node)
+ {
+ _opBusy = true; StateHasChanged();
+ var r = await K8sOps.RestartWorkloadAsync(DeploymentId, node.Kind, node.Name);
+ _opBusy = false; ClosePanel();
+ SetFeedback(r.IsSuccess, r.IsSuccess
+ ? $"Rolling restart triggered for {node.Name}."
+ : r.Error ?? "Restart failed.");
+ }
+
+ private async Task ApplyDeleteAsync(DeploymentResource node)
+ {
+ _opBusy = true; StateHasChanged();
+ var r = await K8sOps.DeleteResourceAsync(DeploymentId, node.Kind, node.Name);
+ _opBusy = false; ClosePanel();
+ SetFeedback(r.IsSuccess, r.IsSuccess
+ ? $"{node.Kind} {node.Name} deleted."
+ : r.Error ?? "Delete failed.");
+ }
+
+ private void SetFeedback(bool ok, string msg)
+ {
+ _feedbackOk = ok; _feedback = msg; StateHasChanged();
+ }
+
+ // ── Floating panel renderer ──────────────────────────────────────────────
+
+ private RenderFragment RenderPanel(DeploymentResource node, double px, double py) => __builder =>
+ {
+
+
+
+ @if (_panelMode == "logs")
+ {
+ @if (_containers.Count > 1)
+ {
+
+ Container:
+ @foreach (string c in _containers)
+ {
+ SwitchContainer(node.Name, c)">@c
+ }
+
+ }
+ @if (_panelLoading)
+ {
+
+ }
+ else if (_panelError is not null)
+ {
+
@_panelError
+ }
+ else
+ {
+
@_panelText
+ }
+ }
+
+ @if (_panelMode == "events")
+ {
+ @if (_panelLoading)
+ {
+
+ }
+ else if (_panelError is not null)
+ {
+
@_panelError
+ }
+ else
+ {
+
@_panelText
+ }
+ }
+
+ @if (_panelMode == "scale")
+ {
+
+ Replicas for @node.Name
+
+ ApplyScaleAsync(node)">
+ @if (_opBusy) { } Apply
+
+ Cancel
+
+ }
+
+ @if (_panelMode == "restart")
+ {
+
+ Rolling restart @node.Name ?
+ ApplyRestartAsync(node)">
+ @if (_opBusy) { }
+ Restart
+
+ Cancel
+
+ }
+
+ @if (_panelMode == "delete")
+ {
+
+
+ Delete @node.Kind @node.Name ?
+ @if (node.Kind == "Pod") { K8s will restart it. }
+
+ ApplyDeleteAsync(node)">
+ @if (_opBusy) { }
+ Delete
+
+ Cancel
+
+ }
+
+ };
+
+ // ── Visual helpers ───────────────────────────────────────────────────────
private static string HealthColor(HealthStatus h) => h switch
{
@@ -188,383 +664,11 @@ else
_ => "text-muted",
};
- // ── Panel open/close ─────────────────────────────────────────────────────
-
- private void ClosePanel()
- {
- _activePanelId = null;
- _panelMode = null;
- _panelText = null;
- _panelError = null;
- _containers = [];
- _selectedContainer = null;
- }
-
- private bool IsPanelOpen(Guid id, string mode) =>
- _activePanelId == id && _panelMode == mode;
-
- // ── Action handlers ──────────────────────────────────────────────────────
-
- private async Task OpenLogPanel(DeploymentResource node)
- {
- if (_activePanelId == node.Id && _panelMode == "logs") { ClosePanel(); return; }
- ClosePanel();
- _activePanelId = node.Id;
- _panelMode = "logs";
- _panelLoading = true;
- StateHasChanged();
-
- var cr = await K8sOps.GetPodContainersAsync(DeploymentId, node.Name);
- if (!cr.IsSuccess) { _panelError = cr.Error; _panelLoading = false; return; }
-
- _containers = cr.Data ?? [];
- _selectedContainer = _containers.Count > 0 ? _containers[0] : null;
- await FetchLogs(node.Name);
- }
-
- private async Task FetchLogs(string podName)
- {
- _panelLoading = true;
- _panelText = null;
- _panelError = null;
- StateHasChanged();
-
- var r = await K8sOps.GetPodLogsAsync(DeploymentId, podName, _selectedContainer);
- _panelLoading = false;
- if (r.IsSuccess) _panelText = r.Data ?? "(no output)";
- else _panelError = r.Error;
- }
-
- private async Task OpenEventsPanel(DeploymentResource node)
- {
- if (_activePanelId == node.Id && _panelMode == "events") { ClosePanel(); return; }
- ClosePanel();
- _activePanelId = node.Id;
- _panelMode = "events";
- _panelLoading = true;
- StateHasChanged();
-
- var r = await K8sOps.GetResourceEventsAsync(DeploymentId, node.Kind, node.Name);
- _panelLoading = false;
- if (r.IsSuccess)
- _panelText = r.Data is { Count: > 0 } lines ? string.Join("\n", lines) : "(no events)";
- else _panelError = r.Error;
- }
-
- private void OpenScalePanel(DeploymentResource node)
- {
- if (_activePanelId == node.Id && _panelMode == "scale") { ClosePanel(); return; }
- ClosePanel();
- _activePanelId = node.Id;
- _panelMode = "scale";
- _scaleValue = ParseDesiredReplicas(node.StatusMessage) ?? 1;
- }
-
- private async Task ApplyScale(DeploymentResource node)
- {
- _opBusy = true;
- StateHasChanged();
- var r = await K8sOps.ScaleWorkloadAsync(DeploymentId, node.Kind, node.Name, _scaleValue);
- _opBusy = false;
- ClosePanel();
- Feedback(r.IsSuccess, r.IsSuccess
- ? $"{node.Kind} {node.Name} scaled to {_scaleValue}."
- : r.Error ?? "Scale failed.");
- }
-
- private void OpenRestartConfirm(DeploymentResource node)
- {
- if (_activePanelId == node.Id && _panelMode == "restart-confirm") { ClosePanel(); return; }
- ClosePanel();
- _activePanelId = node.Id;
- _panelMode = "restart-confirm";
- }
-
- private async Task ConfirmRestart(DeploymentResource node)
- {
- _opBusy = true;
- StateHasChanged();
- var r = await K8sOps.RestartWorkloadAsync(DeploymentId, node.Kind, node.Name);
- _opBusy = false;
- ClosePanel();
- Feedback(r.IsSuccess, r.IsSuccess
- ? $"Rolling restart triggered for {node.Name}."
- : r.Error ?? "Restart failed.");
- }
-
- private void OpenDeleteConfirm(DeploymentResource node)
- {
- if (_activePanelId == node.Id && _panelMode == "delete-confirm") { ClosePanel(); return; }
- ClosePanel();
- _activePanelId = node.Id;
- _panelMode = "delete-confirm";
- }
-
- private async Task ConfirmDelete(DeploymentResource node)
- {
- _opBusy = true;
- StateHasChanged();
- var r = await K8sOps.DeleteResourceAsync(DeploymentId, node.Kind, node.Name);
- _opBusy = false;
- ClosePanel();
- Feedback(r.IsSuccess, r.IsSuccess
- ? $"{node.Kind} {node.Name} deleted."
- : r.Error ?? "Delete failed.");
- }
-
- private void Feedback(bool ok, string msg)
- {
- _actionFeedbackOk = ok;
- _actionFeedback = msg;
- StateHasChanged();
- }
-
private static int? ParseDesiredReplicas(string? s)
{
if (string.IsNullOrEmpty(s)) return null;
int slash = s.IndexOf('/');
if (slash < 0) return null;
- string after = s[(slash + 1)..].Split(' ')[0];
- return int.TryParse(after, out int n) ? n : null;
- }
-
- // ── Recursive card renderer ──────────────────────────────────────────────
- //
- // Each call emits one node card, then (if its panel is open) the action
- // panel, then recursively all children inside .argo-children.
- //
- // IMPORTANT: keep this method free of async lambdas and complex captures.
- // Only use simple @onclick="() => SyncMethod(node)" references here.
-
- private RenderFragment RenderNode(DeploymentResource node) => __builder =>
- {
- string hColor = HealthColor(node.HealthStatus);
- bool panelOpen = _activePanelId == node.Id;
- bool hasKids = node.ChildResources is { Count: > 0 };
-
- // ── Card ─────────────────────────────────────────────────────────────
-
-
-
-
-
@node.Kind
-
-
@node.Name
-
- @if (!string.IsNullOrEmpty(node.StatusMessage))
- {
-
@node.StatusMessage
- }
-
-
- ● @HealthLabel(node.HealthStatus)
-
-
-
- @if (node.Kind == "Pod" && CanLogs)
- {
- OpenLogPanel(node)">
-
-
- }
- @if (CanEvents)
- {
- OpenEventsPanel(node)">
-
-
- }
- @if (node.Kind is "Deployment" or "StatefulSet" or "DaemonSet" && CanRestart)
- {
- OpenRestartConfirm(node)">
-
-
- }
- @if (node.Kind is "Deployment" or "StatefulSet" or "ReplicaSet" && CanScale)
- {
- OpenScalePanel(node)">
-
-
- }
- @if (node.Kind == "Pod" && CanDelete)
- {
- OpenDeleteConfirm(node)">
-
-
- }
- @if (node.Kind == "Job" && CanScale)
- {
- OpenDeleteConfirm(node)">
-
-
- }
-
-
-
- @* ── Action panel (between card and children) ─────────────────────── *@
- @if (panelOpen && _panelMode is not null)
- {
- @RenderActionPanel(node)
- }
-
- @* ── Children ─────────────────────────────────────────────────────── *@
- @if (hasKids)
- {
-
- @foreach (DeploymentResource child in node.ChildResources!)
- {
- @RenderNode(child)
- }
-
- }
- };
-
- // Separate method so the heavy panel HTML is not inside the recursive lambda.
- private RenderFragment RenderActionPanel(DeploymentResource node) => __builder =>
- {
-
-
-
- @if (_panelMode == "logs")
- {
- @:Logs — @node.Name
- }
- else if (_panelMode == "events")
- {
- @:Events — @node.Name
- }
- else if (_panelMode == "scale")
- {
- @:Scale — @node.Name
- }
- else if (_panelMode == "restart-confirm")
- {
- @:Restart — @node.Name
- }
- else if (_panelMode == "delete-confirm")
- {
- @:Delete — @node.Name
- }
-
-
-
-
-
-
- @if (_panelMode == "logs")
- {
- @if (_containers.Count > 1)
- {
-
- Container:
- @foreach (string c in _containers)
- {
- string containerName = c;
- SwitchContainer(node.Name, containerName)">@containerName
- }
-
- }
- @if (_panelLoading)
- {
-
- }
- else if (_panelError is not null)
- {
-
@_panelError
- }
- else
- {
-
@_panelText
- }
- }
-
- @if (_panelMode == "events")
- {
- @if (_panelLoading)
- {
-
- }
- else if (_panelError is not null)
- {
-
@_panelError
- }
- else
- {
-
@_panelText
- }
- }
-
- @if (_panelMode == "scale")
- {
-
- Replicas for @node.Name
-
- ApplyScale(node)">
- @if (_opBusy) { }
- Apply
-
- Cancel
-
- }
-
- @if (_panelMode == "restart-confirm")
- {
-
- Rolling restart @node.Name ?
- ConfirmRestart(node)">
- @if (_opBusy) { }
- Restart
-
- Cancel
-
- }
-
- @if (_panelMode == "delete-confirm")
- {
-
-
- Delete @node.Kind @node.Name ?
- @if (node.Kind == "Pod")
- {
- K8s will restart it automatically.
- }
-
- ConfirmDelete(node)">
- @if (_opBusy) { }
- Delete
-
- Cancel
-
- }
-
- };
-
- // Non-async wrapper to avoid async lambdas inside RenderFragment.
- private void SwitchContainer(string podName, string containerName)
- {
- _selectedContainer = containerName;
- _ = FetchLogs(podName);
+ return int.TryParse(s[(slash + 1)..].Split(' ')[0], out int n) ? n : null;
}
}
diff --git a/src/EntKube.Web/Components/Pages/Tenants/AlertPanel.razor b/src/EntKube.Web/Components/Pages/Tenants/AlertPanel.razor
index 95aa7af..6569730 100644
--- a/src/EntKube.Web/Components/Pages/Tenants/AlertPanel.razor
+++ b/src/EntKube.Web/Components/Pages/Tenants/AlertPanel.razor
@@ -30,27 +30,15 @@
- @if (loading)
- {
-
- }
- else if (!string.IsNullOrEmpty(errorMessage))
- {
-
- @errorMessage
-
- }
- else if (view == "alerts")
+
+ @if (!loading && string.IsNullOrEmpty(errorMessage) && view == "alerts")
{
@* ── Active Alerts ── *@
@if (alerts is null || alerts.Count == 0)
{
-
-
-
No active alerts. All clear!
-
+
}
else
{
@@ -93,15 +81,12 @@
}
}
- else if (view == "silences")
+ else if (!loading && string.IsNullOrEmpty(errorMessage) && view == "silences")
{
@* ── Silences ── *@
@if (silences is null || silences.Count == 0)
{
-
-
-
No silences configured.
-
+
}
else
{
@@ -134,6 +119,8 @@
}
}
+
+
@* ── Create Silence Form ── *@
@if (showCreateSilence)
{
diff --git a/src/EntKube.Web/Components/Pages/Tenants/AppDetail.razor b/src/EntKube.Web/Components/Pages/Tenants/AppDetail.razor
index f9f2e0c..a2f804c 100644
--- a/src/EntKube.Web/Components/Pages/Tenants/AppDetail.razor
+++ b/src/EntKube.Web/Components/Pages/Tenants/AppDetail.razor
@@ -5,6 +5,7 @@
@inject TenantService TenantService
@inject DeploymentService DeploymentService
@inject CnpgService CnpgService
+@inject MongoService MongoService
@inject KubernetesOperationsService K8sOps
@inject PrometheusService PrometheusService
@inject AuditService AuditService
@@ -110,20 +111,13 @@
Select which environments this app is deployed to.
- @if (environments is null)
+
+ @if (environments is not null && environments.Count == 0)
{
-
+
}
- else if (environments.Count == 0)
- {
-
-
-
No environments exist for this tenant yet. Create environments first.
-
- }
- else
+ else if (environments is not null)
{
@foreach (Data.Environment env in environments)
@@ -149,6 +143,7 @@
}
}
+
}
@@ -183,8 +178,14 @@
- @GetSyncBadge(selectedDeployment.SyncStatus)
- @GetHealthBadge(selectedDeployment.HealthStatus)
+
+ @GetSyncBadge(selectedDeployment.SyncStatus)
+
+
+ @GetHealthBadge(selectedDeployment.HealthStatus)
+
@@ -436,20 +437,13 @@
}
- @if (manifests is null)
+