fix(adminui): stale-open guards, validation summaries, key-delete confirm, error boundary (#604)

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-08 06:55:23 -04:00
parent 3baa332d6b
commit d3bca7afd4
12 changed files with 504 additions and 13 deletions
@@ -3,6 +3,7 @@
@rendermode RenderMode.InteractiveServer
@using ZB.MOM.WW.OtOpcUa.Configuration.Entities
@using ZB.MOM.WW.OtOpcUa.Core.Authorization
@using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Raw
@inject IApiKeyAdminService Keys
<div class="d-flex justify-content-between align-items-center mb-3">
@@ -104,7 +105,7 @@
@(k.Enabled ? "Revoke" : "Re-enable")
</button>
<button class="btn btn-sm btn-outline-danger"
@onclick="() => DeleteAsync(k.ApiKeyId)" disabled="@_busy">Delete</button>
@onclick="() => RequestDelete(k)" disabled="@_busy">Delete</button>
</div>
</td>
</tr>
@@ -114,6 +115,9 @@
}
</section>
<RawConfirmDialog @bind-Visible="_confirmDeleteVisible" Title="Delete API key"
Message="@_confirmDeleteMessage" OnConfirm="ConfirmDeleteAsync" />
@code {
private IReadOnlyList<ApiKey>? _keys;
private string _newName = "";
@@ -123,6 +127,12 @@
private string? _error;
private bool _busy;
// Delete is irreversible (the key stops authenticating at once), so it goes through a confirm dialog
// that names the key rather than firing from the row button.
private bool _confirmDeleteVisible;
private string? _confirmDeleteId;
private string _confirmDeleteMessage = "";
protected override async Task OnInitializedAsync() => await ReloadAsync();
private async Task ReloadAsync()
@@ -183,6 +193,19 @@
finally { _busy = false; }
}
private void RequestDelete(ApiKey key)
{
_confirmDeleteId = key.ApiKeyId;
_confirmDeleteMessage =
$"Permanently delete API key '{key.Name}' ({key.ApiKeyId})? Clients using it will stop authenticating immediately. This cannot be undone.";
_confirmDeleteVisible = true;
}
private async Task ConfirmDeleteAsync()
{
if (_confirmDeleteId is { } id) { await DeleteAsync(id); }
}
private async Task DeleteAsync(string apiKeyId)
{
_busy = true;
@@ -34,6 +34,7 @@ else
{
<EditForm Model="_form" OnValidSubmit="SubmitAsync" FormName="clusterEdit">
<DataAnnotationsValidator />
<ValidationSummary class="alert alert-danger py-2 px-3" />
<section class="panel rise" style="animation-delay:.02s">
<div class="panel-head">Identity</div>
<div style="padding:1rem">
@@ -17,6 +17,7 @@
<EditForm Model="_form" OnValidSubmit="SubmitAsync" FormName="newCluster">
<DataAnnotationsValidator />
<ValidationSummary class="alert alert-danger py-2 px-3" />
<section class="panel rise" style="animation-delay:.02s">
<div class="panel-head">Identity</div>
<div style="padding:1rem">
@@ -31,6 +31,7 @@ else
{
<EditForm Model="_form" OnValidSubmit="SubmitAsync" FormName="scriptEdit">
<DataAnnotationsValidator />
<ValidationSummary class="alert alert-danger py-2 px-3" />
<section class="panel rise">
<div class="panel-head">Identity</div>
<div style="padding:1rem">