@namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Forms @using ZB.MOM.WW.ScadaBridge.CentralUI.Services @using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management @using ZB.MOM.WW.ScadaBridge.Commons.Types.DataConnections @using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening @using ZB.MOM.WW.ScadaBridge.Security @inject IEndpointVerificationService VerificationService @inject ICertManagementService CertManagementService
@Title
@if (IsLegacy) {
This connection was migrated from a legacy format. Review the settings and Save to update.
} @* Each settings group gets a real boundary (fieldset + legend) rather than an unstyled muted caption. Fieldsets — not nested cards — because this editor is embedded inside DataConnectionForm's card; same idiom as TransportExport. *@
Connection
@RenderFieldError("EndpointUrl")
@if (_verifyResult is { } result) { @if (result.Success) { ✓ Endpoint reachable } else { @* The failure text is a raw driver exception message. Give it its own block below the button (rather than inline beside it) and clamp it, so a long message cannot reflow the whole button row. Full text stays reachable via the title. *@
@result.FailureKind: @result.Error
} @if (result.FailureKind == VerifyFailureKind.UntrustedCertificate && result.Cert is { } cert) {
Untrusted server certificate
@* Server-supplied X.509 DNs / thumbprints: clip so a long DN cannot stretch the panel; the full value stays in the title. *@
Subject
@cert.Subject
Issuer
@cert.Issuer
Thumbprint
@cert.Thumbprint
Not before
@cert.NotBeforeUtc.ToString("u")
Not after
@cert.NotAfterUtc.ToString("u")
Trusting adds this certificate to every node of the site's trusted-peer store (node-wide), then re-runs Verify.
An Administrator must trust this certificate (cert management).
@if (_trustError is { } trustError) { @* Raw exception text from the trust call — clamped, full text in title. *@
@trustError
} @if (_trustSucceeded) {
✓ Certificate trusted.
}
} }
Authentication @if (Config.UserIdentity is null) { } else {
@if (Config.UserIdentity.TokenType == OpcUaUserTokenType.UsernamePassword) {
@RenderFieldError("UserIdentity.Username")
} else if (Config.UserIdentity.TokenType == OpcUaUserTokenType.X509Certificate) {
@RenderFieldError("UserIdentity.CertificatePath")
}
}
Timing
@RenderFieldError("SessionTimeoutMs")
@RenderFieldError("OperationTimeoutMs")
Subscription
@RenderFieldError("PublishingIntervalMs")
@RenderFieldError("SamplingIntervalMs")
@RenderFieldError("QueueSize")
@RenderFieldError("KeepAliveCount")
@RenderFieldError("LifetimeCount")
@RenderFieldError("MaxNotificationsPerPublish")
Advanced subscription
@RenderFieldError("SubscriptionDisplayName")
Deadband filter @if (Config.Deadband is null) { } else {
@RenderFieldError("Deadband.Value")
}
Heartbeat @if (Config.Heartbeat is null) { } else {
@RenderFieldError("Heartbeat.TagPath")
@RenderFieldError("Heartbeat.MaxSilenceSeconds")
}
@code { [Parameter, EditorRequired] public OpcUaEndpointConfig Config { get; set; } = default!; [Parameter] public string Title { get; set; } = "Endpoint"; [Parameter] public string IdPrefix { get; set; } = "endpoint"; [Parameter] public bool IsLegacy { get; set; } [Parameter] public ValidationResult? Errors { get; set; } // Verify-endpoint context (M7): the site + connection identity the verify // probe targets. Supplied by DataConnectionForm (_formSiteId → SiteIdentifier, // _formName, _protocol). When SiteIdentifier is blank the connection has not been // assigned a site yet, so verification is unavailable. [Parameter] public string SiteIdentifier { get; set; } = string.Empty; [Parameter] public string ConnectionName { get; set; } = string.Empty; [Parameter] public string Protocol { get; set; } = "OpcUa"; private bool _verifying; private VerifyEndpointResult? _verifyResult; private bool _trusting; private bool _trustSucceeded; private string? _trustError; private Task VerifyEndpoint() => VerifyEndpoint(clearTrustNotes: true); private async Task VerifyEndpoint(bool clearTrustNotes) { _verifying = true; _verifyResult = null; // A fresh (user-initiated) verify supersedes any prior trust outcome — clear // the inline notes so a stale "trusted" / error banner doesn't linger across // probes. The trust-triggered re-verify keeps the success note so the operator // still sees confirmation even if the re-probe also surfaces a (different) cert. if (clearTrustNotes) { _trustSucceeded = false; _trustError = null; } try { _verifyResult = await VerificationService.VerifyAsync( SiteIdentifier, ConnectionName, Protocol, Config); } catch (Exception ex) { _verifyResult = new VerifyEndpointResult( false, VerifyFailureKind.ServerError, ex.Message, null); } finally { _verifying = false; } } // M7: trust the captured untrusted server certificate at every node of the // owning site, then re-run Verify (which should now succeed and clear the cert // panel). Administrator-gated by the AuthorizeView wrapping the button; the // CertManagementService enforces the same role server-side-of-the-trust-boundary. private async Task TrustCert(ServerCertInfo cert) { _trusting = true; _trustError = null; _trustSucceeded = false; try { var result = await CertManagementService.TrustAsync( SiteIdentifier, ConnectionName, cert.DerBase64, cert.Thumbprint); if (result.Success) { _trustSucceeded = true; // Re-run Verify so the editor reflects the now-trusted state (and // clears the cert panel on the expected success). Preserve the // success note (clearTrustNotes:false) so confirmation is shown. await VerifyEndpoint(clearTrustNotes: false); } else { _trustError = result.Error ?? "Failed to trust certificate."; } } catch (Exception ex) { _trustError = ex.Message; } finally { _trusting = false; } } private void EnableHeartbeat() => Config.Heartbeat = new OpcUaHeartbeatConfig(); private void EnableAuthentication() => Config.UserIdentity = new OpcUaUserIdentityConfig(); private void EnableDeadband() => Config.Deadband = new OpcUaDeadbandConfig(); private RenderFragment? RenderFieldError(string field) { var match = Errors?.Errors.FirstOrDefault(e => e.EntityName != null && (e.EntityName == field || e.EntityName.EndsWith("." + field))); return match is null ? null : @
@match.Message
; } }