feat(centralui+dcl): Test Bindings popup — one-shot live read of bound tags

Adds a Test Bindings button to the Connection Bindings table on the Configure
Instance page that opens a modal showing the live current value of every bound
attribute. Reuses the routing path that the OPC UA tag browser landed on:

  Central:  TestBindingsDialog → IBindingTester → CommunicationService
            → ReadTagValuesCommand → SiteEnvelope (Ask)
  Site:     SiteCommunicationActor → DeploymentManagerActor singleton
            → DataConnectionManagerActor → child DataConnectionActor
            → _adapter.ReadBatchAsync

Split mirrors the browse handler:
  • Manager owns ConnectionNotFound (only it sees the per-site connection set).
  • Child owns ConnectionNotConnected (pre-call status check, never stash —
    read is interactive design-time), Timeout (OperationCanceledException),
    ServerError (any other exception). Per-tag failures from ReadBatchAsync
    become failure TagReadOutcomes without aborting the batch.

CentralUI:
  • IBindingTester / BindingTester — Design-role guard via HasClaim against
    JwtTokenService.RoleClaimType (not IsInRole — see c1e16cf), typed
    transport-failure translation.
  • TestBindingsDialog — ShowAsync(siteId, rows, instanceLabel) method-arg
    pattern (no Razor parameter race; see 2c138b6), groups rows by connection
    and issues one ReadAsync per connection in parallel, per-row error subline
    + per-connection banner, Refresh button re-issues the reads.
  • InstanceConfigure.razor — Test Bindings button next to Save Bindings,
    disabled when no testable rows. OPC UA only today (other protocols have
    no ReadTagValuesCommand wiring yet).

Tests:
  • Commons: ReadTagValuesCommand discovered by ManagementCommandRegistry.
  • DataConnectionLayer: unknown connection → ConnectionNotFound,
    not-connected adapter → ConnectionNotConnected (ReadBatchAsync NOT called),
    success-path mapping (Good/Bad + per-tag error), cancellation → Timeout.
  • CentralUI: register IBindingTester (and the previously-missing
    IOpcUaBrowseService) on the existing InstanceConfigureAuditDrillinTests
    Bunit container so the page renders cleanly with the new dialog.
This commit is contained in:
Joseph Doherty
2026-05-28 13:25:48 -04:00
parent f401a9ea0e
commit 2a7dee4afa
14 changed files with 909 additions and 1 deletions
@@ -154,8 +154,18 @@
}
</tbody>
</table>
<div class="p-2">
<div class="p-2 d-flex gap-2">
<button class="btn btn-success btn-sm" @onclick="SaveBindings" disabled="@_saving">Save Bindings</button>
@* Test Bindings: one-shot live read of every bound attribute
whose row has a connection picked AND an effective tag
path. Disabled when no testable rows. Currently OPC UA
only — other protocols (none yet) would need their own
wire+adapter support to round-trip through ReadTagValuesCommand. *@
<button class="btn btn-outline-primary btn-sm"
@onclick="OpenTestBindings"
disabled="@(!HasTestableBindings())">
Test Bindings
</button>
</div>
}
</div>
@@ -362,6 +372,11 @@
ConnectionName="@_browserConnectionName"
InitialNodeId="@_browserInitial"
OnSelected="OnBrowserSelected" />
@* Test Bindings dialog — one-shot live read of every bound attribute.
Method-arg ShowAsync(siteId, rows) — no Razor parameter propagation
race (same pattern as OpcUaBrowserDialog). *@
<TestBindingsDialog @ref="_testBindingsRef" />
}
</div>
@@ -399,6 +414,10 @@
private string? _browserInitial;
private string _siteIdentifier = "";
// Test Bindings dialog — single instance, args passed via ShowAsync (no
// Razor parameter propagation race; same pattern as the OPC UA browser).
private TestBindingsDialog? _testBindingsRef;
// Overrides
private List<TemplateAttribute> _overrideAttrs = new();
private Dictionary<string, string?> _overrideValues = new();
@@ -583,6 +602,48 @@
_browserAttrInEdit = null;
}
// ── Test Bindings (one-shot live read of bound tags) ────────────────────
/// <summary>
/// Builds the list of testable rows: attributes that have a connection
/// picked AND a non-empty effective tag path AND an OPC UA connection
/// (the only protocol routed through <c>ReadTagValuesCommand</c> today).
/// </summary>
private List<TestBindingsDialog.BindingRowToTest> BuildTestableRows()
{
var rows = new List<TestBindingsDialog.BindingRowToTest>();
foreach (var attr in _bindingDataSourceAttrs)
{
var connId = GetBindingConnectionId(attr.Name);
if (connId <= 0) continue;
var conn = _siteConnections.FirstOrDefault(c => c.Id == connId);
if (conn is null) continue;
// OPC UA only — other protocols don't have a site-side
// ReadTagValuesCommand handler wired up yet.
if (!string.Equals(conn.Protocol, "OpcUa", StringComparison.OrdinalIgnoreCase))
continue;
var effectivePath = _bindingOverrides.GetValueOrDefault(attr.Name)
?? GetTemplateDefault(attr.Name);
if (string.IsNullOrWhiteSpace(effectivePath)) continue;
rows.Add(new TestBindingsDialog.BindingRowToTest(attr.Name, conn.Name, effectivePath));
}
return rows;
}
private bool HasTestableBindings() => BuildTestableRows().Count > 0;
private async Task OpenTestBindings()
{
if (_testBindingsRef is null) return;
var rows = BuildTestableRows();
if (rows.Count == 0) return;
await _testBindingsRef.ShowAsync(_siteIdentifier, rows, _instance?.UniqueName ?? "");
}
private async Task SaveBindings()
{
_saving = true;