@page "/clusters/{ClusterId}/nodes/new" @page "/clusters/{ClusterId}/nodes/{NodeId}" @* ClusterNode CRUD. ApplicationUri is fleet-wide unique — the EF unique index enforces this at SaveChanges. ServiceLevelBase defaults: 200 primary, 150 secondary. *@ @attribute [Microsoft.AspNetCore.Authorization.Authorize] @rendermode RenderMode.InteractiveServer @using Microsoft.AspNetCore.Components.Forms @using Microsoft.EntityFrameworkCore @using System.ComponentModel.DataAnnotations @using ZB.MOM.WW.OtOpcUa.Configuration @using ZB.MOM.WW.OtOpcUa.Configuration.Entities @inject IDbContextFactory DbFactory @inject NavigationManager Nav @inject AuthenticationStateProvider AuthState

@(IsNew ? "New node" : "Edit node") · @ClusterId

Cancel
@if (!_loaded) {

Loading…

} else if (!IsNew && _existing is null) {
Node @NodeId was not found in cluster @ClusterId.
} else {
Identity
Must be unique fleet-wide. Clients pin trust here — never silently rewrite based on Host.
Redundancy + behaviour
200 = primary preference, 150 = secondary preference. Live ServiceLevel adjusts down on faults.
Per-node merge over cluster-level DriverInstance.DriverConfig. Minimal by design — heavy node-specific config is a smell.
@if (!string.IsNullOrWhiteSpace(_error)) {
@_error
}
Cancel @if (!IsNew) { }
} @code { [Parameter] public string ClusterId { get; set; } = ""; [Parameter] public string? NodeId { get; set; } private bool IsNew => string.IsNullOrEmpty(NodeId); private FormModel _form = new(); private ClusterNode? _existing; private bool _loaded; private bool _busy; private string? _error; protected override async Task OnInitializedAsync() { if (IsNew) { _form = new FormModel { NodeId = "", Host = "", OpcUaPort = 4840, DashboardPort = 8081, ApplicationUri = "", ServiceLevelBase = 200, Enabled = true, }; } else { await using var db = await DbFactory.CreateDbContextAsync(); _existing = await db.ClusterNodes.AsNoTracking() .FirstOrDefaultAsync(n => n.ClusterId == ClusterId && n.NodeId == NodeId); if (_existing is not null) { _form = new FormModel { NodeId = _existing.NodeId, Host = _existing.Host, OpcUaPort = _existing.OpcUaPort, DashboardPort = _existing.DashboardPort, ApplicationUri = _existing.ApplicationUri, ServiceLevelBase = _existing.ServiceLevelBase, Enabled = _existing.Enabled, DriverConfigOverridesJson = _existing.DriverConfigOverridesJson, }; } } _loaded = true; } private async Task SubmitAsync() { _busy = true; _error = null; try { if (!string.IsNullOrWhiteSpace(_form.DriverConfigOverridesJson)) { try { using var _ = System.Text.Json.JsonDocument.Parse(_form.DriverConfigOverridesJson); } catch { _error = "DriverConfigOverridesJson is not valid JSON."; return; } } await using var db = await DbFactory.CreateDbContextAsync(); if (IsNew) { if (await db.ClusterNodes.AnyAsync(n => n.NodeId == _form.NodeId)) { _error = $"Node '{_form.NodeId}' already exists."; return; } var auth = await AuthState.GetAuthenticationStateAsync(); db.ClusterNodes.Add(new ClusterNode { NodeId = _form.NodeId, ClusterId = ClusterId, Host = _form.Host, OpcUaPort = _form.OpcUaPort, DashboardPort = _form.DashboardPort, ApplicationUri = _form.ApplicationUri, ServiceLevelBase = _form.ServiceLevelBase, Enabled = _form.Enabled, DriverConfigOverridesJson = string.IsNullOrWhiteSpace(_form.DriverConfigOverridesJson) ? null : _form.DriverConfigOverridesJson, CreatedAt = DateTime.UtcNow, CreatedBy = auth.User.Identity?.Name ?? "(anonymous)", }); } else { var entity = await db.ClusterNodes.FirstOrDefaultAsync( n => n.ClusterId == ClusterId && n.NodeId == NodeId); if (entity is null) { _error = "Row no longer exists."; return; } entity.Host = _form.Host; entity.OpcUaPort = _form.OpcUaPort; entity.DashboardPort = _form.DashboardPort; entity.ApplicationUri = _form.ApplicationUri; entity.ServiceLevelBase = _form.ServiceLevelBase; entity.Enabled = _form.Enabled; entity.DriverConfigOverridesJson = string.IsNullOrWhiteSpace(_form.DriverConfigOverridesJson) ? null : _form.DriverConfigOverridesJson; } await db.SaveChangesAsync(); Nav.NavigateTo($"/clusters/{ClusterId}"); } 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.ClusterNodes.FirstOrDefaultAsync( n => n.ClusterId == ClusterId && n.NodeId == NodeId); if (entity is null) { Nav.NavigateTo($"/clusters/{ClusterId}"); return; } db.ClusterNodes.Remove(entity); await db.SaveChangesAsync(); Nav.NavigateTo($"/clusters/{ClusterId}"); } catch (Exception ex) { _error = ex.Message; } finally { _busy = false; } } private sealed class FormModel { [Required, RegularExpression("^[A-Za-z0-9_-]+$")] public string NodeId { get; set; } = ""; [Required] public string Host { get; set; } = ""; [Range(1, 65535)] public int OpcUaPort { get; set; } = 4840; [Range(1, 65535)] public int DashboardPort { get; set; } = 8081; [Required, RegularExpression("^urn:[A-Za-z0-9_:./-]+$")] public string ApplicationUri { get; set; } = ""; [Range(0, 255)] public byte ServiceLevelBase { get; set; } = 200; public bool Enabled { get; set; } = true; public string? DriverConfigOverridesJson { get; set; } } }