fix(adminui): converter UI — try/catch + confirm + FleetAdmin gate on per-equipment convert (review)

This commit is contained in:
Joseph Doherty
2026-06-11 21:54:21 -04:00
parent 7682d185fb
commit e77fd3eec0
2 changed files with 106 additions and 67 deletions
@@ -145,7 +145,11 @@ else
} }
<button type="button" class="btn btn-outline-primary btn-sm" @onclick="OpenAddAlias" disabled="@(_gateways.Count == 0)">Add alias (browse Galaxy)</button> <button type="button" class="btn btn-outline-primary btn-sm" @onclick="OpenAddAlias" disabled="@(_gateways.Count == 0)">Add alias (browse Galaxy)</button>
<button type="button" class="btn btn-outline-primary btn-sm" @onclick="OpenAddTag">Add tag</button> <button type="button" class="btn btn-outline-primary btn-sm" @onclick="OpenAddTag">Add tag</button>
<button type="button" class="btn btn-outline-secondary btn-sm" @onclick="PreviewConvertRelaysAsync" disabled="@(_gateways.Count == 0 || _convertBusy)">Convert relay virtual-tags…</button> <AuthorizeView Policy="FleetAdmin">
<Authorized>
<button type="button" class="btn btn-outline-secondary btn-sm" @onclick="PreviewConvertRelaysAsync" disabled="@(_gateways.Count == 0 || _convertBusy)">Convert relay virtual-tags…</button>
</Authorized>
</AuthorizeView>
</div> </div>
@if (!string.IsNullOrWhiteSpace(_tagError)) @if (!string.IsNullOrWhiteSpace(_tagError))
{ {
@@ -202,70 +206,93 @@ else
<div class="text-success small mb-2">@_convertMessage</div> <div class="text-success small mb-2">@_convertMessage</div>
} }
@if (_convertPreview is not null) <AuthorizeView Policy="FleetAdmin">
{ <Authorized>
<section class="panel rise mt-2"> @if (!string.IsNullOrWhiteSpace(_convertError))
<div class="panel-head d-flex justify-content-between align-items-center"> {
<span>Convert relay virtual-tags to aliases</span> <div class="text-danger small mb-2">@_convertError</div>
<button type="button" class="btn btn-sm btn-link" @onclick="@(() => { _convertPreview = null; })">Close</button> }
</div>
<div style="padding:1rem">
<h6 class="text-muted">Will convert (@_convertPreview.Converted.Count)</h6>
@if (_convertPreview.Converted.Count == 0)
{
<p class="text-muted small mb-3">No relay virtual-tags to convert.</p>
}
else
{
<table class="table table-sm mb-3">
<thead><tr><th>Virtual tag</th><th>Full name</th><th>Data type</th></tr></thead>
<tbody>
@foreach (var c in _convertPreview.Converted)
{
<tr>
<td>@c.VirtualTagName</td>
<td class="mono">@c.FullName</td>
<td>@c.DataType</td>
</tr>
}
</tbody>
</table>
}
<h6 class="text-muted">Skipped (@_convertPreview.Skipped.Count)</h6> @if (_convertPreview is not null)
@if (_convertPreview.Skipped.Count == 0) {
{ <section class="panel rise mt-2">
<p class="text-muted small mb-0">Nothing skipped.</p> <div class="panel-head d-flex justify-content-between align-items-center">
} <span>Convert relay virtual-tags to aliases</span>
else <button type="button" class="btn btn-sm btn-link" @onclick="@(() => { _convertPreview = null; _convertConfirming = false; })">Close</button>
{ </div>
<table class="table table-sm mb-0"> <div style="padding:1rem">
<thead><tr><th>Virtual tag</th><th>Reason</th></tr></thead> <h6 class="text-muted">Will convert (@_convertPreview.Converted.Count)</h6>
<tbody> @if (_convertPreview.Converted.Count == 0)
@foreach (var s in _convertPreview.Skipped) {
{ <p class="text-muted small mb-3">No relay virtual-tags to convert.</p>
<tr> }
<td>@s.VirtualTagName</td> else
<td>@s.Reason</td> {
</tr> <table class="table table-sm mb-3">
} <thead><tr><th>Virtual tag</th><th>Full name</th><th>Data type</th></tr></thead>
</tbody> <tbody>
</table> @foreach (var c in _convertPreview.Converted)
} {
<tr @key="c.FullName">
<td>@c.VirtualTagName</td>
<td class="mono">@c.FullName</td>
<td>@c.DataType</td>
</tr>
}
</tbody>
</table>
}
<div class="mt-3 d-flex gap-2"> <h6 class="text-muted">Skipped (@_convertPreview.Skipped.Count)</h6>
@if (_convertPreview.Converted.Count > 0) @if (_convertPreview.Skipped.Count == 0)
{ {
<button type="button" class="btn btn-primary btn-sm" @onclick="ApplyConvertRelaysAsync" disabled="@_convertBusy"> <p class="text-muted small mb-0">Nothing skipped.</p>
@if (_convertBusy) { <span class="spinner-border spinner-border-sm me-1"></span> } }
Apply else
</button> {
} <table class="table table-sm mb-0">
<button type="button" class="btn btn-outline-secondary btn-sm" @onclick="@(() => { _convertPreview = null; })" disabled="@_convertBusy">Cancel</button> <thead><tr><th>Virtual tag</th><th>Reason</th></tr></thead>
</div> <tbody>
</div> @foreach (var s in _convertPreview.Skipped)
</section> {
} <tr @key="s.VirtualTagName">
<td>@s.VirtualTagName</td>
<td>@s.Reason</td>
</tr>
}
</tbody>
</table>
}
<div class="mt-3 d-flex gap-2 align-items-center">
@if (_convertPreview.Converted.Count > 0)
{
@if (!_convertConfirming)
{
<button type="button" class="btn btn-primary btn-sm" @onclick="() => _convertConfirming = true" disabled="@_convertBusy">
Apply…
</button>
}
else
{
<span class="small">Convert @_convertPreview.Converted.Count relay virtual-tag(s)?</span>
<button type="button" class="btn btn-danger btn-sm" @onclick="ApplyConvertRelaysAsync" disabled="@_convertBusy">
@if (_convertBusy) { <span class="spinner-border spinner-border-sm me-1"></span> }
Yes, apply
</button>
<button type="button" class="btn btn-outline-secondary btn-sm" @onclick="() => _convertConfirming = false" disabled="@_convertBusy">Cancel</button>
}
}
@if (!_convertConfirming)
{
<button type="button" class="btn btn-outline-secondary btn-sm" @onclick="@(() => { _convertPreview = null; _convertConfirming = false; })" disabled="@_convertBusy">Close</button>
}
</div>
</div>
</section>
}
</Authorized>
</AuthorizeView>
<TagModal Visible="_tagModalVisible" IsNew="_tagModalIsNew" EquipmentId="@EquipmentId" <TagModal Visible="_tagModalVisible" IsNew="_tagModalIsNew" EquipmentId="@EquipmentId"
Existing="_tagModalExisting" Drivers="_tagDriverOptions" Existing="_tagModalExisting" Drivers="_tagDriverOptions"
@@ -403,10 +430,14 @@ else
private AliasTagEditDto? _aliasModalExisting; private AliasTagEditDto? _aliasModalExisting;
// --- Relay→alias converter (per-equipment). _convertPreview holds the dry-run result while the // --- Relay→alias converter (per-equipment). _convertPreview holds the dry-run result while the
// inline preview panel is open; null = panel closed. _convertMessage is a brief post-apply summary. --- // inline preview panel is open; null = panel closed. _convertMessage is a brief post-apply summary.
// _convertError surfaces exceptions so a service failure doesn't crash the circuit. _convertConfirming
// guards the Apply button (two-step confirm, mirrors the fleet page). ---
private RelayConversionResult? _convertPreview; private RelayConversionResult? _convertPreview;
private bool _convertBusy; private bool _convertBusy;
private bool _convertConfirming;
private string? _convertMessage; private string? _convertMessage;
private string? _convertError;
// --- Virtual Tags tab state. _vtags is null until the tab is first activated. --- // --- Virtual Tags tab state. _vtags is null until the tab is first activated. ---
private IReadOnlyList<EquipmentVirtualTagRow>? _vtags; private IReadOnlyList<EquipmentVirtualTagRow>? _vtags;
@@ -524,26 +555,32 @@ else
private async Task PreviewConvertRelaysAsync() private async Task PreviewConvertRelaysAsync()
{ {
_convertMessage = null; _convertMessage = null;
_convertError = null;
_convertConfirming = false;
_convertBusy = true; _convertBusy = true;
try try
{ {
_convertPreview = await Svc.ConvertRelayVirtualTagsToAliasesAsync(EquipmentId!, dryRun: true); _convertPreview = await Svc.ConvertRelayVirtualTagsToAliasesAsync(EquipmentId!, dryRun: true);
} }
catch (Exception ex) { _convertPreview = null; _convertError = ex.Message; }
finally { _convertBusy = false; } finally { _convertBusy = false; }
} }
private async Task ApplyConvertRelaysAsync() private async Task ApplyConvertRelaysAsync()
{ {
_convertBusy = true; _convertBusy = true;
_convertError = null;
try try
{ {
var r = await Svc.ConvertRelayVirtualTagsToAliasesAsync(EquipmentId!, dryRun: false); var r = await Svc.ConvertRelayVirtualTagsToAliasesAsync(EquipmentId!, dryRun: false);
_convertPreview = null; _convertPreview = null;
_convertConfirming = false;
_convertMessage = $"Converted {r.Converted.Count}, skipped {r.Skipped.Count}."; _convertMessage = $"Converted {r.Converted.Count}, skipped {r.Skipped.Count}.";
// The converted virtual tags become aliases, so both lists change. // The converted virtual tags become aliases, so both lists change.
await ReloadTagsAsync(); await ReloadTagsAsync();
await ReloadVirtualTagsAsync(); await ReloadVirtualTagsAsync();
} }
catch (Exception ex) { _convertError = ex.Message; _convertConfirming = false; }
finally { _convertBusy = false; } finally { _convertBusy = false; }
} }
@@ -649,9 +686,11 @@ else
_tags = null; _tags = null;
_vtags = null; _vtags = null;
_alarms = null; _alarms = null;
// Drop any open relay-conversion preview/summary so it can't leak across equipment changes. // Drop any open relay-conversion preview/summary/confirm so it can't leak across equipment changes.
_convertPreview = null; _convertPreview = null;
_convertMessage = null; _convertMessage = null;
_convertError = null;
_convertConfirming = false;
if (!IsNew) if (!IsNew)
{ {
_equipment = await Svc.LoadEquipmentAsync(EquipmentId!); _equipment = await Svc.LoadEquipmentAsync(EquipmentId!);
@@ -52,7 +52,7 @@
<tbody> <tbody>
@foreach (var c in _preview.Converted) @foreach (var c in _preview.Converted)
{ {
<tr> <tr @key="(c.EquipmentId, c.FullName)">
<td class="mono">@c.EquipmentId</td> <td class="mono">@c.EquipmentId</td>
<td>@c.VirtualTagName</td> <td>@c.VirtualTagName</td>
<td class="mono">@c.FullName</td> <td class="mono">@c.FullName</td>
@@ -79,7 +79,7 @@
<tbody> <tbody>
@foreach (var s in _preview.Skipped) @foreach (var s in _preview.Skipped)
{ {
<tr> <tr @key="(s.EquipmentId, s.VirtualTagName)">
<td class="mono">@s.EquipmentId</td> <td class="mono">@s.EquipmentId</td>
<td>@s.VirtualTagName</td> <td>@s.VirtualTagName</td>
<td>@s.Reason</td> <td>@s.Reason</td>