@page "/clusters/{ClusterId}/drivers/new" @page "/clusters/{ClusterId}/drivers/{DriverInstanceId}" @* Per Q1 of the AdminUI rebuild plan — JSON editor only, typed driver editors deferred. DriverInstance is the keystone for everything downstream (Equipment, Tag, VirtualTag, ScriptedAlarm all reference DriverInstanceId), so this is the second edit page after Namespace. *@ @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

@(IsNew ? "New driver instance" : "Edit driver instance") · @ClusterId

Cancel
@if (!_loaded) {

Loading…

} else if (!IsNew && _existing is null) {
Driver instance @DriverInstanceId was not found in cluster @ClusterId.
} else {
@(IsNew ? "Identity" : $"Edit {_form.DriverInstanceId}")
Cannot be changed after creation — drives the actor type that owns this instance.
@foreach (var ns in _namespaces) { }
Driver config (JSON)
Schemaless per driver type — validated server-side at deploy time. JSON is reformatted on save.
Resilience overrides (optional)
Polly pipeline overrides per docs/v2/driver-stability.md — bulkhead, retry counts, breaker thresholds. Null = use the driver type's tier defaults.
@if (!string.IsNullOrWhiteSpace(_error)) {
@_error
}
Cancel @if (!IsNew) { }
} @code { [Parameter] public string ClusterId { get; set; } = ""; [Parameter] public string? DriverInstanceId { get; set; } private bool IsNew => string.IsNullOrEmpty(DriverInstanceId); private FormModel _form = new(); private DriverInstance? _existing; private List _namespaces = new(); private bool _loaded; private bool _busy; private string? _error; 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) { _form = new FormModel { DriverInstanceId = "", Name = "", DriverType = "ModbusTcp", NamespaceId = _namespaces.FirstOrDefault()?.NamespaceId ?? "", Enabled = true, DriverConfig = "{}", }; } else { _existing = await db.DriverInstances.AsNoTracking() .FirstOrDefaultAsync(d => d.ClusterId == ClusterId && d.DriverInstanceId == DriverInstanceId); if (_existing is not null) { _form = new FormModel { DriverInstanceId = _existing.DriverInstanceId, Name = _existing.Name, DriverType = _existing.DriverType, NamespaceId = _existing.NamespaceId, Enabled = _existing.Enabled, DriverConfig = _existing.DriverConfig, ResilienceConfig = _existing.ResilienceConfig, RowVersion = _existing.RowVersion, }; } } _loaded = true; } private async Task SubmitAsync() { _busy = true; _error = null; try { var normalizedConfig = NormalizeJson(_form.DriverConfig); if (normalizedConfig is null) { _error = "DriverConfig is not valid JSON."; return; } var normalizedResilience = NormalizeOptionalJson(_form.ResilienceConfig); if (!string.IsNullOrWhiteSpace(_form.ResilienceConfig) && normalizedResilience is null) { _error = "ResilienceConfig is not valid JSON. Leave blank to use defaults."; return; } await using var db = await DbFactory.CreateDbContextAsync(); if (IsNew) { if (await db.DriverInstances.AnyAsync(d => d.DriverInstanceId == _form.DriverInstanceId)) { _error = $"Driver instance '{_form.DriverInstanceId}' already exists."; return; } db.DriverInstances.Add(new DriverInstance { DriverInstanceId = _form.DriverInstanceId, ClusterId = ClusterId, NamespaceId = _form.NamespaceId, Name = _form.Name, DriverType = _form.DriverType, Enabled = _form.Enabled, DriverConfig = normalizedConfig, ResilienceConfig = normalizedResilience, }); } 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 = _form.NamespaceId; entity.Name = _form.Name; entity.Enabled = _form.Enabled; entity.DriverConfig = normalizedConfig; entity.ResilienceConfig = normalizedResilience; } 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 static string? NormalizeJson(string? input) { if (string.IsNullOrWhiteSpace(input)) return null; try { using var doc = System.Text.Json.JsonDocument.Parse(input); return System.Text.Json.JsonSerializer.Serialize(doc.RootElement); } catch { return null; } } private static string? NormalizeOptionalJson(string? input) => string.IsNullOrWhiteSpace(input) ? null : NormalizeJson(input); private sealed class FormModel { [Required, RegularExpression("^[A-Za-z0-9_-]+$", ErrorMessage = "Use letters, digits, dash, underscore.")] public string DriverInstanceId { get; set; } = ""; [Required] public string Name { get; set; } = ""; [Required] public string DriverType { get; set; } = "ModbusTcp"; [Required] public string NamespaceId { get; set; } = ""; public bool Enabled { get; set; } = true; [Required] public string DriverConfig { get; set; } = "{}"; public string? ResilienceConfig { get; set; } public byte[] RowVersion { get; set; } = []; } }