@page "/clusters/{ClusterId}/drivers/new/galaxy" @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.Galaxy.Config @inject IDbContextFactory DbFactory @inject NavigationManager Nav

@(IsNew ? "New AVEVA Galaxy driver" : "Edit AVEVA Galaxy 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)) { }
@* mxaccessgw connection *@
mxaccessgw connection
gRPC endpoint of the mxaccessgw process.
Forms: env:NAME, file:PATH, dev:KEY. Cleartext is accepted but triggers a startup warning.
Default 10 s.
Default 30 s.
Default 0 (lifetime of driver).
@* MXAccess *@
MXAccess
Must be unique per OtOpcUa instance — redundancy pairs each get a distinct name.
Default 1000 ms.
0 = anonymous.
Default 50000. Raise if events.dropped appears.
@* Repository *@
Galaxy repository
Default 5000 objects per page.
Triggers address-space rebuild on Galaxy re-deploy.
@* Reconnect *@
Reconnect backoff
Default 500 ms.
Default 30000 ms.
@* Diagnostics *@
Diagnostics
Max 60. Used by Test Connect. Default 30.
} @code { [Parameter] public string ClusterId { get; set; } = ""; [Parameter] public string? DriverInstanceId { get; set; } private const string DriverTypeKey = "GalaxyMxGateway"; private bool IsNew => string.IsNullOrEmpty(DriverInstanceId); private static readonly System.Text.Json.JsonSerializerOptions _jsonOpts = new() { PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase, PropertyNameCaseInsensitive = true, 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; 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) ?? CreateDefaultOptions(); _form = new FormModel(); _form.Galaxy = GalaxyFormModel.FromRecord(opts); _form.ResilienceConfig = _existing.ResilienceConfig; _form.RowVersion = _existing.RowVersion; } } _loaded = true; } private static GalaxyDriverOptions CreateDefaultOptions() => new( Gateway: new GalaxyGatewayOptions("https://localhost:5001", "env:MX_API_KEY"), MxAccess: new GalaxyMxAccessOptions("OtOpcUa"), Repository: new GalaxyRepositoryOptions(), Reconnect: new GalaxyReconnectOptions()); private async Task SubmitAsync() { _busy = true; _error = null; try { var opts = _form.Galaxy.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.Galaxy.ToRecord(), _jsonOpts); private static GalaxyDriverOptions? TryDeserialize(string json) { try { return System.Text.Json.JsonSerializer.Deserialize(json, _jsonOpts); } catch { return null; } } public sealed class FormModel { public GalaxyFormModel Galaxy { get; set; } = new(); public string? ResilienceConfig { get; set; } public byte[] RowVersion { get; set; } = []; } /// /// Mutable flat mirror of and its nested records. /// All positional-record fields are flattened with a section prefix to avoid name /// collisions. / handle translation. /// public sealed class GalaxyFormModel { // GalaxyGatewayOptions public string GatewayEndpoint { get; set; } = "https://localhost:5001"; public string GatewayApiKeySecretRef { get; set; } = "env:MX_API_KEY"; public bool GatewayUseTls { get; set; } = true; public string? GatewayCaCertificatePath { get; set; } public int GatewayConnectTimeoutSeconds { get; set; } = 10; public int GatewayDefaultCallTimeoutSeconds { get; set; } = 30; public int GatewayStreamTimeoutSeconds { get; set; } = 0; // GalaxyMxAccessOptions public string MxClientName { get; set; } = "OtOpcUa"; public int MxPublishingIntervalMs { get; set; } = 1000; public int MxWriteUserId { get; set; } = 0; public int MxEventPumpChannelCapacity { get; set; } = 50_000; // GalaxyRepositoryOptions public int RepositoryDiscoverPageSize { get; set; } = 5000; public bool RepositoryWatchDeployEvents { get; set; } = true; // GalaxyReconnectOptions public int ReconnectInitialBackoffMs { get; set; } = 500; public int ReconnectMaxBackoffMs { get; set; } = 30_000; public bool ReconnectReplayOnSessionLost { get; set; } = true; // GalaxyDriverOptions top-level public int ProbeTimeoutSeconds { get; set; } = 30; public static GalaxyFormModel FromRecord(GalaxyDriverOptions r) { // Null-coalesce each nested record to its default so that persisted configs // that pre-date a section (e.g. no Reconnect key, or PascalCase keys that // don't match the camelCase deserializer) don't cause a NullReferenceException. var gw = r.Gateway ?? new GalaxyGatewayOptions("https://localhost:5001", "env:MX_API_KEY"); var mx = r.MxAccess ?? new GalaxyMxAccessOptions("OtOpcUa"); var repo = r.Repository ?? new GalaxyRepositoryOptions(); var rc = r.Reconnect ?? new GalaxyReconnectOptions(); return new() { GatewayEndpoint = gw.Endpoint, GatewayApiKeySecretRef = gw.ApiKeySecretRef, GatewayUseTls = gw.UseTls, GatewayCaCertificatePath = gw.CaCertificatePath, GatewayConnectTimeoutSeconds = gw.ConnectTimeoutSeconds, GatewayDefaultCallTimeoutSeconds = gw.DefaultCallTimeoutSeconds, GatewayStreamTimeoutSeconds = gw.StreamTimeoutSeconds, MxClientName = mx.ClientName, MxPublishingIntervalMs = mx.PublishingIntervalMs, MxWriteUserId = mx.WriteUserId, MxEventPumpChannelCapacity = mx.EventPumpChannelCapacity, RepositoryDiscoverPageSize = repo.DiscoverPageSize, RepositoryWatchDeployEvents = repo.WatchDeployEvents, ReconnectInitialBackoffMs = rc.InitialBackoffMs, ReconnectMaxBackoffMs = rc.MaxBackoffMs, ReconnectReplayOnSessionLost = rc.ReplayOnSessionLost, ProbeTimeoutSeconds = r.ProbeTimeoutSeconds, }; } public GalaxyDriverOptions ToRecord() => new( Gateway: new GalaxyGatewayOptions( Endpoint: GatewayEndpoint, ApiKeySecretRef: GatewayApiKeySecretRef, UseTls: GatewayUseTls, CaCertificatePath: string.IsNullOrWhiteSpace(GatewayCaCertificatePath) ? null : GatewayCaCertificatePath, ConnectTimeoutSeconds: GatewayConnectTimeoutSeconds, DefaultCallTimeoutSeconds: GatewayDefaultCallTimeoutSeconds, StreamTimeoutSeconds: GatewayStreamTimeoutSeconds), MxAccess: new GalaxyMxAccessOptions( ClientName: MxClientName, PublishingIntervalMs: MxPublishingIntervalMs, WriteUserId: MxWriteUserId, EventPumpChannelCapacity: MxEventPumpChannelCapacity), Repository: new GalaxyRepositoryOptions( DiscoverPageSize: RepositoryDiscoverPageSize, WatchDeployEvents: RepositoryWatchDeployEvents), Reconnect: new GalaxyReconnectOptions( InitialBackoffMs: ReconnectInitialBackoffMs, MaxBackoffMs: ReconnectMaxBackoffMs, ReplayOnSessionLost: ReconnectReplayOnSessionLost)) { ProbeTimeoutSeconds = ProbeTimeoutSeconds, }; } }