2a7dee4afa
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.
82 lines
3.4 KiB
C#
82 lines
3.4 KiB
C#
using Microsoft.AspNetCore.Components.Authorization;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
|
|
using ZB.MOM.WW.ScadaBridge.Communication;
|
|
using ZB.MOM.WW.ScadaBridge.Security;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Services;
|
|
|
|
/// <summary>
|
|
/// Default <see cref="IBindingTester"/> implementation — a thin facade over
|
|
/// <see cref="CommunicationService.ReadTagValuesAsync"/> that enforces the
|
|
/// CentralUI-side <c>Design</c>-role trust boundary and translates transport
|
|
/// exceptions into a typed <see cref="ReadTagValuesFailure"/> result. Mirrors
|
|
/// <see cref="OpcUaBrowseService"/>.
|
|
/// </summary>
|
|
public sealed class BindingTester : IBindingTester
|
|
{
|
|
private readonly CommunicationService _communication;
|
|
private readonly AuthenticationStateProvider _auth;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="BindingTester"/>.
|
|
/// </summary>
|
|
/// <param name="communication">Central-side cluster communication service.</param>
|
|
/// <param name="auth">Authentication state provider used for the Design-role guard.</param>
|
|
public BindingTester(CommunicationService communication, AuthenticationStateProvider auth)
|
|
{
|
|
_communication = communication ?? throw new ArgumentNullException(nameof(communication));
|
|
_auth = auth ?? throw new ArgumentNullException(nameof(auth));
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async Task<ReadTagValuesResult> ReadAsync(
|
|
string siteId,
|
|
string connectionName,
|
|
IReadOnlyList<string> tagPaths,
|
|
CancellationToken ct = default)
|
|
{
|
|
// CentralUI-side role guard — sites don't enforce envelope-level
|
|
// roles, so the Design check must happen here before any cross-cluster
|
|
// traffic. Use HasClaim against JwtTokenService.RoleClaimType (not
|
|
// IsInRole, per c1e16cf).
|
|
var state = await _auth.GetAuthenticationStateAsync();
|
|
if (!state.User.HasClaim(JwtTokenService.RoleClaimType, "Design"))
|
|
{
|
|
return new ReadTagValuesResult(
|
|
Array.Empty<TagReadOutcome>(),
|
|
new ReadTagValuesFailure(ReadTagValuesFailureKind.ServerError, "Not authorized."));
|
|
}
|
|
|
|
try
|
|
{
|
|
return await _communication.ReadTagValuesAsync(
|
|
siteId,
|
|
new ReadTagValuesCommand(connectionName, tagPaths),
|
|
ct);
|
|
}
|
|
catch (TimeoutException ex)
|
|
{
|
|
// Akka Ask timed out — the site (or its OPC UA session) didn't
|
|
// answer within CommunicationOptions.QueryTimeout. Surface as a
|
|
// typed Timeout failure so the dialog can render an inline banner.
|
|
return new ReadTagValuesResult(
|
|
Array.Empty<TagReadOutcome>(),
|
|
new ReadTagValuesFailure(ReadTagValuesFailureKind.Timeout, ex.Message));
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
// Caller-initiated cancel — propagate so Blazor can drop the
|
|
// response cleanly. Distinct from Timeout.
|
|
throw;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// Any other transport / serialization failure: keep the dialog
|
|
// alive with a typed banner.
|
|
return new ReadTagValuesResult(
|
|
Array.Empty<TagReadOutcome>(),
|
|
new ReadTagValuesFailure(ReadTagValuesFailureKind.ServerError, ex.Message));
|
|
}
|
|
}
|
|
}
|