@page "/clusters/{ClusterId}/drivers/new/opcuaclient" @attribute [Microsoft.AspNetCore.Authorization.Authorize] @rendermode RenderMode.InteractiveServer @using Microsoft.AspNetCore.Components.Forms @using Microsoft.EntityFrameworkCore @using ZB.MOM.WW.OtOpcUa.AdminUI.Clients @using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers @using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.Pickers @using ZB.MOM.WW.OtOpcUa.Configuration @using ZB.MOM.WW.OtOpcUa.Configuration.Entities @using ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient @inject IDbContextFactory DbFactory @inject NavigationManager Nav

@(IsNew ? "New OPC UA Client driver" : "Edit OPC UA Client driver") · @ClusterId

Cancel
@if (!_loaded) {

Loading…

} else if (!IsNew && _existing is null) {
Driver instance @DriverInstanceId was not found in cluster @ClusterId.
} else { @if (!IsNew && !string.IsNullOrEmpty(DriverInstanceId)) { }
@* Endpoint *@
Endpoint
Single-endpoint shortcut. When EndpointUrls list is non-empty, this field is ignored.
Restrict mirroring to a sub-tree.
Default 3 s — failover sweep budget.
Default 10 s — steady-state reads/writes.
Default 120 s.
Default 5 s.
Initial delay after session drop. Default 5 s.
Default 10000.
Default 10.
@* Endpoint URLs list — read-only JSON view (full list-editor is a follow-up) *@
@_endpointUrlsJson
@* Security *@
Security
@foreach (var e in Enum.GetValues()) { }
@foreach (var e in Enum.GetValues()) { }
@* Authentication *@
Authentication
@foreach (var e in Enum.GetValues()) { }
@if (_form.OpcUa.AuthType == OpcUaAuthType.Username) {
} @if (_form.OpcUa.AuthType == OpcUaAuthType.Certificate) {
}
@* Namespace mapping *@
Namespace mapping
@foreach (var e in Enum.GetValues()) { }
Equipment = raw data re-mapped to UNS. SystemPlatform = processed data; hierarchy preserved as-is.
@_unsMappingTableJson
Keys = remote browse-path prefixes; values = UNS Area/Line/Name paths. Required when TargetNamespaceKind = Equipment.
@* Diagnostics *@
Diagnostics
Max 60. Used by Test Connect. Default 15.
} @code { [Parameter] public string ClusterId { get; set; } = ""; [Parameter] public string? DriverInstanceId { get; set; } private const string DriverTypeKey = "OpcUaClient"; private bool IsNew => string.IsNullOrEmpty(DriverInstanceId); private static readonly System.Text.Json.JsonSerializerOptions _jsonOpts = new() { PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase, UnmappedMemberHandling = System.Text.Json.Serialization.JsonUnmappedMemberHandling.Skip, WriteIndented = false, }; private FormModel _form = new(); private DriverIdentitySection.DriverIdentityModel _identityModel = new() { DriverType = DriverTypeKey }; private DriverInstance? _existing; private List _namespaces = new(); private bool _loaded; private bool _busy; private string? _error; // Address picker state private bool _showPicker; private string _pickedAddress = ""; private void OnAddressPicked(string address) => _pickedAddress = address; // Read-only JSON snippets for collections that have no list editor yet. private string _endpointUrlsJson = "[]"; private string _unsMappingTableJson = "{}"; protected override async Task OnInitializedAsync() { await using var db = await DbFactory.CreateDbContextAsync(); _namespaces = await db.Namespaces.AsNoTracking() .Where(n => n.ClusterId == ClusterId) .OrderBy(n => n.NamespaceId) .ToListAsync(); if (IsNew) { _identityModel = new() { DriverInstanceId = "", Name = "", DriverType = DriverTypeKey, NamespaceId = _namespaces.FirstOrDefault()?.NamespaceId ?? "", Enabled = true, }; _form = new FormModel(); } else { _existing = await db.DriverInstances.AsNoTracking() .FirstOrDefaultAsync(d => d.ClusterId == ClusterId && d.DriverInstanceId == DriverInstanceId); if (_existing is not null) { _identityModel = new() { DriverInstanceId = _existing.DriverInstanceId, Name = _existing.Name, DriverType = _existing.DriverType, NamespaceId = _existing.NamespaceId, Enabled = _existing.Enabled, }; var opts = TryDeserialize(_existing.DriverConfig) ?? new OpcUaClientDriverOptions(); _form = new FormModel(); _form.OpcUa = OpcUaClientFormModel.FromRecord(opts); _endpointUrlsJson = System.Text.Json.JsonSerializer.Serialize(opts.EndpointUrls, _jsonOpts); _unsMappingTableJson = System.Text.Json.JsonSerializer.Serialize(opts.UnsMappingTable, _jsonOpts); _form.ResilienceConfig = _existing.ResilienceConfig; _form.RowVersion = _existing.RowVersion; } } _loaded = true; } private async Task SubmitAsync() { _busy = true; _error = null; try { var opts = _form.OpcUa.ToRecord(); var configJson = System.Text.Json.JsonSerializer.Serialize(opts, _jsonOpts); await using var db = await DbFactory.CreateDbContextAsync(); if (IsNew) { if (await db.DriverInstances.AnyAsync(d => d.DriverInstanceId == _identityModel.DriverInstanceId)) { _error = $"Driver instance '{_identityModel.DriverInstanceId}' already exists."; return; } db.DriverInstances.Add(new DriverInstance { DriverInstanceId = _identityModel.DriverInstanceId, ClusterId = ClusterId, NamespaceId = _identityModel.NamespaceId, Name = _identityModel.Name, DriverType = DriverTypeKey, Enabled = _identityModel.Enabled, DriverConfig = configJson, ResilienceConfig = string.IsNullOrWhiteSpace(_form.ResilienceConfig) ? null : _form.ResilienceConfig, }); } else { var entity = await db.DriverInstances.FirstOrDefaultAsync( d => d.ClusterId == ClusterId && d.DriverInstanceId == DriverInstanceId); if (entity is null) { _error = "Row no longer exists."; return; } db.Entry(entity).Property(e => e.RowVersion).OriginalValue = _form.RowVersion; entity.NamespaceId = _identityModel.NamespaceId; entity.Name = _identityModel.Name; entity.Enabled = _identityModel.Enabled; entity.DriverConfig = configJson; entity.ResilienceConfig = string.IsNullOrWhiteSpace(_form.ResilienceConfig) ? null : _form.ResilienceConfig; } await db.SaveChangesAsync(); Nav.NavigateTo($"/clusters/{ClusterId}/drivers"); } catch (DbUpdateConcurrencyException) { _error = "Another user changed this driver instance while you were editing. Reload to see the latest values, then re-apply your changes."; } catch (Exception ex) { _error = ex.Message; } finally { _busy = false; } } private async Task DeleteAsync() { if (IsNew) return; _busy = true; _error = null; try { await using var db = await DbFactory.CreateDbContextAsync(); var entity = await db.DriverInstances.FirstOrDefaultAsync( d => d.ClusterId == ClusterId && d.DriverInstanceId == DriverInstanceId); if (entity is null) { Nav.NavigateTo($"/clusters/{ClusterId}/drivers"); return; } db.Entry(entity).Property(e => e.RowVersion).OriginalValue = _form.RowVersion; db.DriverInstances.Remove(entity); await db.SaveChangesAsync(); Nav.NavigateTo($"/clusters/{ClusterId}/drivers"); } catch (DbUpdateConcurrencyException) { _error = "Another user changed this driver instance while you were viewing it. Reload before deleting."; } catch (Exception ex) { _error = $"Delete failed: {ex.Message}. (Likely because equipment/tags still reference this driver — remove them first.)"; } finally { _busy = false; } } private string SerializeCurrentConfig() => System.Text.Json.JsonSerializer.Serialize(_form.OpcUa.ToRecord(), _jsonOpts); private static OpcUaClientDriverOptions? TryDeserialize(string json) { try { return System.Text.Json.JsonSerializer.Deserialize(json, _jsonOpts); } catch { return null; } } public sealed class FormModel { public OpcUaClientFormModel OpcUa { get; set; } = new(); public string? ResilienceConfig { get; set; } public byte[] RowVersion { get; set; } = []; } /// /// Mutable mirror of with int wrappers for /// TimeSpan fields so Blazor InputNumber can bind them. /// EndpointUrls and UnsMappingTable are shown as read-only JSON; they survive round-trip /// via the original deserialized record and are re-serialized unchanged. /// public sealed class OpcUaClientFormModel { // Connection public string EndpointUrl { get; set; } = "opc.tcp://localhost:4840"; public string? BrowseRoot { get; set; } public string ApplicationUri { get; set; } = "urn:localhost:OtOpcUa:GatewayClient"; public string SessionName { get; set; } = "OtOpcUa-Gateway"; public bool AutoAcceptCertificates { get; set; } = false; public int PerEndpointConnectTimeoutSeconds { get; set; } = 3; public int TimeoutSeconds { get; set; } = 10; public int SessionTimeoutSeconds { get; set; } = 120; public int KeepAliveIntervalSeconds { get; set; } = 5; public int ReconnectPeriodSeconds { get; set; } = 5; public int MaxDiscoveredNodes { get; set; } = 10_000; public int MaxBrowseDepth { get; set; } = 10; // Security public OpcUaSecurityMode SecurityMode { get; set; } = OpcUaSecurityMode.None; public OpcUaSecurityPolicy SecurityPolicy { get; set; } = OpcUaSecurityPolicy.None; // Authentication public OpcUaAuthType AuthType { get; set; } = OpcUaAuthType.Anonymous; public string? Username { get; set; } public string? Password { get; set; } public string? UserCertificatePath { get; set; } public string? UserCertificatePassword { get; set; } // Namespace mapping public OpcUaTargetNamespaceKind TargetNamespaceKind { get; set; } = OpcUaTargetNamespaceKind.Equipment; // Diagnostics public int ProbeTimeoutSeconds { get; set; } = 15; // Preserved read-only collections (round-tripped unchanged from original record) internal IReadOnlyList _endpointUrls = []; internal IReadOnlyDictionary _unsMappingTable = new System.Collections.Generic.Dictionary(); public static OpcUaClientFormModel FromRecord(OpcUaClientDriverOptions r) => new() { EndpointUrl = r.EndpointUrl, BrowseRoot = r.BrowseRoot, ApplicationUri = r.ApplicationUri, SessionName = r.SessionName, AutoAcceptCertificates = r.AutoAcceptCertificates, PerEndpointConnectTimeoutSeconds = (int)r.PerEndpointConnectTimeout.TotalSeconds, TimeoutSeconds = (int)r.Timeout.TotalSeconds, SessionTimeoutSeconds = (int)r.SessionTimeout.TotalSeconds, KeepAliveIntervalSeconds = (int)r.KeepAliveInterval.TotalSeconds, ReconnectPeriodSeconds = (int)r.ReconnectPeriod.TotalSeconds, MaxDiscoveredNodes = r.MaxDiscoveredNodes, MaxBrowseDepth = r.MaxBrowseDepth, SecurityMode = r.SecurityMode, SecurityPolicy = r.SecurityPolicy, AuthType = r.AuthType, Username = r.Username, Password = r.Password, UserCertificatePath = r.UserCertificatePath, UserCertificatePassword = r.UserCertificatePassword, TargetNamespaceKind = r.TargetNamespaceKind, ProbeTimeoutSeconds = r.ProbeTimeoutSeconds, _endpointUrls = r.EndpointUrls, _unsMappingTable = r.UnsMappingTable, }; public OpcUaClientDriverOptions ToRecord() => new() { EndpointUrl = EndpointUrl, EndpointUrls = _endpointUrls, BrowseRoot = string.IsNullOrWhiteSpace(BrowseRoot) ? null : BrowseRoot, ApplicationUri = ApplicationUri, SessionName = SessionName, AutoAcceptCertificates = AutoAcceptCertificates, PerEndpointConnectTimeout = TimeSpan.FromSeconds(PerEndpointConnectTimeoutSeconds), Timeout = TimeSpan.FromSeconds(TimeoutSeconds), SessionTimeout = TimeSpan.FromSeconds(SessionTimeoutSeconds), KeepAliveInterval = TimeSpan.FromSeconds(KeepAliveIntervalSeconds), ReconnectPeriod = TimeSpan.FromSeconds(ReconnectPeriodSeconds), MaxDiscoveredNodes = MaxDiscoveredNodes, MaxBrowseDepth = MaxBrowseDepth, SecurityMode = SecurityMode, SecurityPolicy = SecurityPolicy, AuthType = AuthType, Username = string.IsNullOrWhiteSpace(Username) ? null : Username, Password = string.IsNullOrWhiteSpace(Password) ? null : Password, UserCertificatePath = string.IsNullOrWhiteSpace(UserCertificatePath) ? null : UserCertificatePath, UserCertificatePassword = string.IsNullOrWhiteSpace(UserCertificatePassword) ? null : UserCertificatePassword, TargetNamespaceKind = TargetNamespaceKind, UnsMappingTable = _unsMappingTable, ProbeTimeoutSeconds = ProbeTimeoutSeconds, }; } }