fix(adminui): confirm before deleting role grants, clusters, nodes and ACLs (lmxopcua#785)
v2-ci / unit-tests (push) Waiting to run

The four Delete buttons deleted on the first click. They now open the shared
RawConfirmDialog naming the item; only Confirm deletes. Adds bUnit (test-only)
render tests: modal opens without deleting, Cancel keeps the item, Confirm deletes.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
Joseph Doherty
2026-11-09 22:22:47 -05:00
parent 20820bbf76
commit 42c73bfb83
6 changed files with 232 additions and 4 deletions
@@ -4,6 +4,7 @@
@rendermode RenderMode.InteractiveServer
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.EntityFrameworkCore
@using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Raw
@using System.ComponentModel.DataAnnotations
@using ZB.MOM.WW.OtOpcUa.AdminUI.Acls
@using ZB.MOM.WW.OtOpcUa.Configuration
@@ -148,12 +149,16 @@ else
<button type="submit" class="btn btn-primary" disabled="@_busy">@(IsNew ? "Create" : "Save changes")</button>
<a href="/clusters/@ClusterId/acls" class="btn btn-outline-secondary">Cancel</a>
</div>
@if (!IsNew) { <button type="button" class="btn btn-outline-danger ms-auto" @onclick="DeleteAsync" disabled="@_busy">Delete</button> }
@if (!IsNew) { <button type="button" class="btn btn-outline-danger ms-auto" @onclick="() => _confirmDeleteVisible = true" disabled="@_busy">Delete</button> }
</div>
</EditForm>
<AclScopePickerModal Visible="_pickerVisible" ClusterId="@ClusterId"
OnPicked="ScopePicked" OnCancel="() => _pickerVisible = false" />
<RawConfirmDialog @bind-Visible="_confirmDeleteVisible" Title="Delete ACL"
Message="@($"Delete ACL '{NodeAclId}' (principal '{_form.Principal}') from cluster '{ClusterId}'? This cannot be undone.")"
OnConfirm="DeleteAsync" />
}
</PageErrorBoundary>
@@ -175,6 +180,7 @@ else
private NodeAcl? _existing;
private bool _loaded;
private bool _busy;
private bool _confirmDeleteVisible;
private string? _error;
private bool _pickerVisible;
@@ -5,6 +5,7 @@
@rendermode RenderMode.InteractiveServer
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.EntityFrameworkCore
@using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Raw
@using System.ComponentModel.DataAnnotations
@using ZB.MOM.WW.OtOpcUa.Configuration
@using ZB.MOM.WW.OtOpcUa.Configuration.Entities
@@ -99,11 +100,15 @@ else
</button>
<a href="/clusters/@ClusterId" class="btn btn-outline-secondary">Cancel</a>
</div>
<button type="button" class="btn btn-outline-danger ms-auto" @onclick="DeleteAsync" disabled="@_busy">
<button type="button" class="btn btn-outline-danger ms-auto" @onclick="() => _confirmDeleteVisible = true" disabled="@_busy">
Delete cluster
</button>
</div>
</EditForm>
<RawConfirmDialog @bind-Visible="_confirmDeleteVisible" Title="Delete cluster"
Message="@($"Delete cluster '{ClusterId}'? This cannot be undone.")"
OnConfirm="DeleteAsync" />
}
</PageErrorBoundary>
@@ -114,6 +119,7 @@ else
private ServerCluster? _existing;
private bool _loaded;
private bool _busy;
private bool _confirmDeleteVisible;
private string? _error;
protected override async Task OnInitializedAsync()
@@ -6,6 +6,7 @@
@rendermode RenderMode.InteractiveServer
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.EntityFrameworkCore
@using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Raw
@using System.ComponentModel.DataAnnotations
@using ZB.MOM.WW.OtOpcUa.Configuration
@using ZB.MOM.WW.OtOpcUa.Configuration.Entities
@@ -116,12 +117,16 @@ else
</div>
@if (!IsNew)
{
<button type="button" class="btn btn-outline-danger ms-auto" @onclick="DeleteAsync" disabled="@_busy">
<button type="button" class="btn btn-outline-danger ms-auto" @onclick="() => _confirmDeleteVisible = true" disabled="@_busy">
Delete
</button>
}
</div>
</EditForm>
<RawConfirmDialog @bind-Visible="_confirmDeleteVisible" Title="Delete node"
Message="@($"Delete node '{NodeId}' from cluster '{ClusterId}'? This cannot be undone.")"
OnConfirm="DeleteAsync" />
}
</PageErrorBoundary>
@@ -135,6 +140,7 @@ else
private ClusterNode? _existing;
private bool _loaded;
private bool _busy;
private bool _confirmDeleteVisible;
private string? _error;
protected override async Task OnInitializedAsync()
@@ -2,6 +2,7 @@
@attribute [Authorize(Policy = AdminUiPolicies.FleetAdmin)]
@rendermode RenderMode.InteractiveServer
@using Microsoft.Extensions.Options
@using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Raw
@using ZB.MOM.WW.OtOpcUa.Configuration.Entities
@using ZB.MOM.WW.OtOpcUa.Configuration.Enums
@using ZB.MOM.WW.OtOpcUa.Configuration.Services
@@ -65,7 +66,7 @@
<tr>
<td title="@r.LdapGroup"><span class="mono">@DisplayText.Abbreviate(r.LdapGroup, 48)</span></td>
<td><span class="chip chip-idle">@r.Role</span></td>
<td><button class="btn btn-sm btn-outline-danger" @onclick="() => DeleteAsync(r.Id)" disabled="@_busy">Delete</button></td>
<td><button class="btn btn-sm btn-outline-danger" @onclick="() => _pendingDelete = r" disabled="@_busy">Delete</button></td>
</tr>
}
}
@@ -74,6 +75,11 @@
</div>
</section>
<RawConfirmDialog Visible="@(_pendingDelete is not null)" VisibleChanged="@(v => { if (!v) _pendingDelete = null; })"
Title="Delete role grant"
Message="@($"Delete the role grant mapping LDAP group '{_pendingDelete?.LdapGroup}' to role {_pendingDelete?.Role}? This cannot be undone.")"
OnConfirm="ConfirmDeleteAsync" />
@if (_options is not null)
{
<section class="panel rise mt-3" style="animation-delay:.14s">
@@ -113,6 +119,7 @@
private AdminRole _newRole = AdminRole.Viewer;
private string? _error;
private bool _busy;
private LdapGroupRoleMapping? _pendingDelete;
protected override async Task OnInitializedAsync()
{
@@ -143,6 +150,13 @@
finally { _busy = false; }
}
private async Task ConfirmDeleteAsync()
{
var id = _pendingDelete?.Id;
_pendingDelete = null;
if (id is Guid g) await DeleteAsync(g);
}
private async Task DeleteAsync(Guid id)
{
_error = null; _busy = true;