@* 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 @using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers @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 { @* Driver config (JSON) — inlined; will be replaced by typed forms in Phase 4 *@
Driver config (JSON)
Schemaless per driver type — validated server-side at deploy time. JSON is reformatted on save.
} @code { [Parameter] public string ClusterId { get; set; } = ""; [Parameter] public string? DriverInstanceId { get; set; } private bool IsNew => string.IsNullOrEmpty(DriverInstanceId); private DriverIdentitySection.DriverIdentityModel _identityModel = new(); 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) { _identityModel = new DriverIdentitySection.DriverIdentityModel { DriverInstanceId = "", Name = "", DriverType = "ModbusTcp", NamespaceId = _namespaces.FirstOrDefault()?.NamespaceId ?? "", Enabled = true, }; _form = new FormModel { DriverConfig = "{}", }; } else { _existing = await db.DriverInstances.AsNoTracking() .FirstOrDefaultAsync(d => d.ClusterId == ClusterId && d.DriverInstanceId == DriverInstanceId); if (_existing is not null) { _identityModel = new DriverIdentitySection.DriverIdentityModel { DriverInstanceId = _existing.DriverInstanceId, Name = _existing.Name, DriverType = _existing.DriverType, NamespaceId = _existing.NamespaceId, Enabled = _existing.Enabled, }; _form = new FormModel { 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 == _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 = _identityModel.DriverType, Enabled = _identityModel.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 = _identityModel.NamespaceId; entity.Name = _identityModel.Name; entity.Enabled = _identityModel.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] public string DriverConfig { get; set; } = "{}"; public string? ResilienceConfig { get; set; } public byte[] RowVersion { get; set; } = []; } }