From f167808a2c1dc24c9715946cc1cfcbaa3f5b23b5 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 26 May 2026 05:18:00 -0400 Subject: [PATCH] feat(adminui): Deployments page with drift indicator and Deploy button --- .../Components/Pages/Deployments.razor | 132 ++++++++++++++++++ .../ZB.MOM.WW.OtOpcUa.AdminUI/_Imports.razor | 7 + 2 files changed, 139 insertions(+) create mode 100644 src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Deployments.razor create mode 100644 src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/_Imports.razor diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Deployments.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Deployments.razor new file mode 100644 index 0000000..573d1d6 --- /dev/null +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Deployments.razor @@ -0,0 +1,132 @@ +@page "/deployments" +@using Microsoft.AspNetCore.Authorization +@using Microsoft.EntityFrameworkCore +@using ZB.MOM.WW.OtOpcUa.Commons.Interfaces +@using ZB.MOM.WW.OtOpcUa.Commons.Messages.Admin +@using ZB.MOM.WW.OtOpcUa.Configuration +@using ZB.MOM.WW.OtOpcUa.Configuration.Entities +@using ZB.MOM.WW.OtOpcUa.Configuration.Enums +@using ZB.MOM.WW.OtOpcUa.ControlPlane.AdminOperations + +@attribute [Authorize(Roles = "FleetAdmin,ConfigEditor")] + +@inject IDbContextFactory DbFactory +@inject IAdminOperationsClient AdminOps +@rendermode InteractiveServer + +Deployments + +

Deployments

+ +
+ + + @if (_drift is not null) + { + + @(_drift.Value ? "Configuration drift" : "In sync") + + } +
+ +@if (_lastMessage is not null) +{ +
+ @_lastMessage +
+} + + + + + + + + + + + + + + @foreach (var d in _deployments) + { + + + + + + + + + } + +
DeploymentRevisionStatusCreated byCreated (UTC)Sealed (UTC)
@Short(d.DeploymentId)@d.RevisionHash[..12]…@d.Status@d.CreatedBy@d.CreatedAtUtc.ToString("u")@(d.SealedAtUtc?.ToString("u") ?? "—")
+ +@code { + private IReadOnlyList _deployments = Array.Empty(); + private bool _busy; + private bool _lastSuccess; + private string? _lastMessage; + private bool? _drift; + + protected override async Task OnInitializedAsync() + { + await ReloadAsync(); + } + + private async Task ReloadAsync() + { + await using var db = await DbFactory.CreateDbContextAsync(); + _deployments = await db.Deployments + .AsNoTracking() + .OrderByDescending(d => d.CreatedAtUtc) + .Take(50) + .ToListAsync(); + + // Drift: if no sealed deployment yet, no drift to report. Otherwise compare the latest + // sealed revision hash to a fresh snapshot of the live-edit state. + var latestSealed = _deployments.FirstOrDefault(d => d.Status == DeploymentStatus.Sealed); + if (latestSealed is null) + { + _drift = null; + return; + } + var current = await ConfigComposer.SnapshotAndFlattenAsync(db); + _drift = !string.Equals(current.RevisionHash, latestSealed.RevisionHash, StringComparison.Ordinal); + } + + private async Task StartDeploymentAsync() + { + _busy = true; + _lastMessage = null; + try + { + var result = await AdminOps.StartDeploymentAsync( + createdBy: "(current user)", // F18: thread HttpContext.User.Identity.Name through + ct: CancellationToken.None); + + _lastSuccess = result.Outcome == StartDeploymentOutcome.Accepted; + _lastMessage = result.Outcome switch + { + StartDeploymentOutcome.Accepted => $"Deployment {Short(result.DeploymentId!.Value.Value)} dispatched (rev {result.RevisionHash!.Value.Value[..12]}…).", + StartDeploymentOutcome.AnotherDeploymentInFlight => result.Message ?? "Another deployment is already in flight.", + StartDeploymentOutcome.NoChanges => "No changes detected since the last sealed deployment.", + _ => result.Message ?? "Deployment rejected.", + }; + await ReloadAsync(); + } + catch (Exception ex) + { + _lastSuccess = false; + _lastMessage = $"Deploy failed: {ex.Message}"; + } + finally + { + _busy = false; + } + } + + private static string Short(Guid id) => id.ToString("N")[..8]; +} diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/_Imports.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/_Imports.razor new file mode 100644 index 0000000..5ae69bb --- /dev/null +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/_Imports.razor @@ -0,0 +1,7 @@ +@using Microsoft.AspNetCore.Components +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.JSInterop