From 7c2582d10562a79be8396b94ce40d1a26b162201 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Fri, 21 Aug 2026 07:44:15 -0400 Subject: [PATCH] =?UTF-8?q?feat(centralui):=20Templates=20tree=20context?= =?UTF-8?q?=20menu=20=E2=80=94=20"New=20Instance=E2=80=A6"=20(Deployer-gat?= =?UTF-8?q?ed)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01G2GHPnbbKGvyyfbr2XBRVH --- .../Components/Pages/Design/Templates.razor | 13 ++++ .../TemplatesPageTests.cs | 64 +++++++++++++++++-- 2 files changed, 71 insertions(+), 6 deletions(-) diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/Templates.razor b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/Templates.razor index a8d3e0e0..8c9c4591 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/Templates.razor +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/Templates.razor @@ -130,10 +130,16 @@ private bool _loading = true; private string? _errorMessage; + // "New Instance…" leaves the Design area for a RequireDeployment page; only offer + // it to users who hold that role so the menu never leads to a 403. + private bool _canCreateInstances; + private ToastNotification _toast = default!; protected override async Task OnInitializedAsync() { + var state = await AuthStateProvider.GetAuthenticationStateAsync(); + _canCreateInstances = state.User.HasClaim(JwtTokenService.RoleClaimType, Roles.Deployer); await LoadTemplatesAsync(); } @@ -302,6 +308,13 @@ { } + @if (_canCreateInstances) + { + + + + } @if (tmpl != null) diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/TemplatesPageTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/TemplatesPageTests.cs index 255af811..45494b64 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/TemplatesPageTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/TemplatesPageTests.cs @@ -47,15 +47,16 @@ public class TemplatesPageTests : BunitContext JSInterop.SetupVoid("treeviewStorage.save", _ => true); } - private void AddTestAuth() + private void AddTestAuth(params string[] roles) { // The page resolves the current user via the "Username" claim in // GetCurrentUserAsync(); supply a stub so OnInitializedAsync doesn't crash. - var claims = new[] - { - new Claim(JwtTokenService.UsernameClaimType, "tester"), - new Claim(JwtTokenService.RoleClaimType, "Designer") - }; + // Calling this again from a test replaces the constructor's Designer-only + // default (MS DI resolves the LAST registration of a service), which is how + // a test renders the page as a Deployer. + if (roles.Length == 0) roles = new[] { Roles.Designer }; + var claims = new List { new(JwtTokenService.UsernameClaimType, "tester") }; + claims.AddRange(roles.Select(r => new Claim(JwtTokenService.RoleClaimType, r))); var identity = new ClaimsIdentity(claims, "TestAuth"); var user = new ClaimsPrincipal(identity); Services.AddSingleton(new TestAuthStateProvider(user)); @@ -396,6 +397,57 @@ public class TemplatesPageTests : BunitContext Assert.EndsWith("/design/templates/create", nav.Uri); } + // --------------------------------------------------------------- + // "New Instance…" context-menu item (Deployer-gated) + // --------------------------------------------------------------- + + private void SeedOneTemplate(int id, string name) + { + _repo.GetAllTemplatesAsync(Arg.Any()) + .Returns(Task.FromResult>(new List