From 11576460a70bad72c843e5ef668eaec33bff8d15 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Wed, 15 Jul 2026 21:09:04 -0400 Subject: [PATCH] v3(b1-wp4b): make AdminUI compile against greenfield schema behind Batch-2/3 stubs Namespace entity + NamespaceKind retired; DriverInstance dropped NamespaceId; Tag is raw-only (no EquipmentId/DriverInstanceId/FullName/FolderPath); driver options replaced pre-declared Tags with RawTags. Stub the retired surfaces so the AdminUI + Host build green; real Raw/UNS authoring arrives in v3 Batch 2/3. - ClusterNamespaces/NamespaceEdit: bodies replaced with 'Namespaces retired' banner - DriverIdentitySection: dropped Namespace dropdown + NamespaceId model field - 8 driver pages: dropped Namespace binding; retired the pre-declared Tags editor (replaced with a '/raw Batch 2' note); multi-device pages keep their Devices editor - UnsTreeService: equipment-tag counts/list -> empty; Create/UpdateTag -> failure result ('Tag authoring moved to the Raw tree (/raw) in v3 Batch 2'); Namespace/ driver-binding queries removed - IScriptTagCatalog: project surviving Tag columns only (path from TagConfig FullName, else Name) - OpcUaClientTagConfigEditor: rebind stale FullName -> model's NodeId - ClusterDrivers: drop retired NamespaceId column AdminUI + Host build 0/0. AdminUI.Tests / Host.IntegrationTests remain red (WP6). --- .../Pages/Clusters/ClusterDrivers.razor | 1 - .../Pages/Clusters/ClusterNamespaces.razor | 76 +----- .../Clusters/Drivers/AbCipDriverPage.razor | 110 +------- .../Clusters/Drivers/AbLegacyDriverPage.razor | 106 +------- .../Clusters/Drivers/FocasDriverPage.razor | 106 +------- .../Clusters/Drivers/GalaxyDriverPage.razor | 11 +- .../Clusters/Drivers/ModbusDriverPage.razor | 121 +-------- .../Drivers/OpcUaClientDriverPage.razor | 11 +- .../Pages/Clusters/Drivers/S7DriverPage.razor | 106 +------- .../Clusters/Drivers/TwinCATDriverPage.razor | 110 +------- .../Pages/Clusters/NamespaceEdit.razor | 238 +--------------- .../Drivers/DriverIdentitySection.razor | 12 - .../OpcUaClientTagConfigEditor.razor | 6 +- .../ScriptAnalysis/IScriptTagCatalog.cs | 31 ++- .../Uns/UnsTreeService.cs | 257 +++--------------- 15 files changed, 155 insertions(+), 1147 deletions(-) diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/ClusterDrivers.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/ClusterDrivers.razor index 2aa283c9..2f9afce2 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/ClusterDrivers.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/ClusterDrivers.razor @@ -35,7 +35,6 @@ else · @d.Name · @d.DriverType @if (!d.Enabled) { Disabled } - ns=@d.NamespaceId
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/ClusterNamespaces.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/ClusterNamespaces.razor index 3e9110ef..6cbf39a8 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/ClusterNamespaces.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/ClusterNamespaces.razor @@ -1,81 +1,23 @@ @page "/clusters/{ClusterId}/namespaces" @attribute [Authorize(Policy = AdminUiPolicies.AuthenticatedRead)] @rendermode RenderMode.InteractiveServer -@using Microsoft.EntityFrameworkCore -@using ZB.MOM.WW.OtOpcUa.Configuration -@using ZB.MOM.WW.OtOpcUa.Configuration.Entities -@inject IDbContextFactory DbFactory

Namespaces · @ClusterId

- New namespace
-@if (_rows is null) -{ -

Loading…

-} -else -{ -
- Namespaces are content (decision #123) — they're served at the OPC UA endpoint and bound - to driver instances. NamespaceUri must be unique fleet-wide. -
- -
-
@_rows.Count namespace@(_rows.Count == 1 ? "" : "s")
- @if (_rows.Count == 0) - { -
No namespaces defined for this cluster.
- } - else - { -
- - - - - - - - - - - - - @foreach (var n in _rows) - { - - - - - - - - - } - -
NamespaceIdKindURIStatusNotes
@n.NamespaceId@n.Kind@n.NamespaceUri - @if (n.Enabled) { Enabled } - else { Disabled } - @(n.Notes ?? "")Edit
-
- } -
-} +
+
Namespaces are retired in v3
+
+ The Namespace entity has been removed. The v3 Raw/UNS split replaces it — + raw driver folders and the unified namespace are managed on the /raw and + /uns pages, arriving in v3 Batch 2/3. This page is a placeholder so existing + links do not 404. +
+
@code { [Parameter] public string ClusterId { get; set; } = ""; - private List? _rows; - - protected override async Task OnInitializedAsync() - { - await using var db = await DbFactory.CreateDbContextAsync(); - _rows = await db.Namespaces.AsNoTracking() - .Where(n => n.ClusterId == ClusterId) - .OrderBy(n => n.NamespaceId) - .ToListAsync(); - } } diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/AbCipDriverPage.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/AbCipDriverPage.razor index f5f108c0..cb379509 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/AbCipDriverPage.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/AbCipDriverPage.razor @@ -36,7 +36,7 @@ else CancelHref="@($"/clusters/{ClusterId}/drivers")" OnDelete="@(IsNew ? null : (EventCallback?)EventCallback.Factory.Create(this, DeleteAsync))"> - + @if (!IsNew && !string.IsNullOrEmpty(DriverInstanceId)) { @@ -175,37 +175,13 @@ else @* Tags *@ - - - NameDeviceTag pathTypeWritable - - - @t.Name@t.DeviceHostAddress - @t.TagPath@t.DataType@(t.Writable ? "yes" : "no") - - -
-
-
-
-
-
-
-
-
-
- -
-
-
-
+
+
Tags
+
+ Tag authoring moves to the Raw tree (/raw) in v3 Batch 2. + Pre-declared driver tags have been retired. +
+
@@ -231,7 +207,6 @@ else 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; @@ -242,17 +217,12 @@ else private void OnAddressPicked(string address) => _pickedAddress = address; - // Held separately because Devices/Tags are collections — edited via the CollectionEditor modal. + // Held separately because Devices is a collection — edited via the CollectionEditor modal. private List _devices = []; - private List _tags = []; 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) { @@ -261,7 +231,6 @@ else DriverInstanceId = "", Name = "", DriverType = DriverTypeKey, - NamespaceId = _namespaces.FirstOrDefault()?.NamespaceId ?? "", Enabled = true, }; _form = new FormModel(); @@ -277,7 +246,6 @@ else DriverInstanceId = _existing.DriverInstanceId, Name = _existing.Name, DriverType = _existing.DriverType, - NamespaceId = _existing.NamespaceId, Enabled = _existing.Enabled, }; var opts = TryDeserialize(_existing.DriverConfig) ?? new AbCipDriverOptions(); @@ -285,7 +253,6 @@ else _form.ResilienceConfig = _existing.ResilienceConfig; _form.RowVersion = _existing.RowVersion; _devices = opts.Devices.Select(AbCipDeviceRow.FromDefinition).ToList(); - _tags = opts.Tags.Select(AbCipTagRow.FromDefinition).ToList(); } } _loaded = true; @@ -298,8 +265,7 @@ else { var configJson = System.Text.Json.JsonSerializer.Serialize( _form.ToOptions( - _devices.Select(r => r.ToDefinition()).ToList(), - _tags.Select(r => r.ToDefinition()).ToList()), + _devices.Select(r => r.ToDefinition()).ToList()), _jsonOpts); await using var db = await DbFactory.CreateDbContextAsync(); if (IsNew) @@ -313,7 +279,6 @@ else { DriverInstanceId = _identityModel.DriverInstanceId, ClusterId = ClusterId, - NamespaceId = _identityModel.NamespaceId, Name = _identityModel.Name, DriverType = DriverTypeKey, Enabled = _identityModel.Enabled, @@ -327,7 +292,6 @@ else 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; @@ -373,8 +337,7 @@ else private string SerializeCurrentConfig() => System.Text.Json.JsonSerializer.Serialize( _form.ToOptions( - _devices.Select(r => r.ToDefinition()).ToList(), - _tags.Select(r => r.ToDefinition()).ToList()), + _devices.Select(r => r.ToDefinition()).ToList()), _jsonOpts); private static AbCipDriverOptions? TryDeserialize(string json) @@ -423,53 +386,8 @@ else } } - // Mutable VM for the modal editor — AbCipTagDefinition is an immutable record. - public sealed class AbCipTagRow - { - public string Name { get; set; } = ""; - public string DeviceHostAddress { get; set; } = ""; - public string TagPath { get; set; } = ""; - public AbCipDataType DataType { get; set; } = AbCipDataType.DInt; - public bool Writable { get; set; } = true; - - // Original record (null for newly-added rows). Preserves fields the editor doesn't expose - // (WriteIdempotent, Members, SafetyTag) across a load→save. - private AbCipTagDefinition? _source; - - public AbCipTagRow Clone() => (AbCipTagRow)MemberwiseClone(); // _source is an immutable record ref — safe to share - - public static AbCipTagRow FromDefinition(AbCipTagDefinition d) => new() - { - Name = d.Name, DeviceHostAddress = d.DeviceHostAddress, TagPath = d.TagPath, - DataType = d.DataType, Writable = d.Writable, - _source = d, - }; - - public AbCipTagDefinition ToDefinition() - { - var baseDef = _source ?? new AbCipTagDefinition(Name.Trim(), DeviceHostAddress.Trim(), TagPath.Trim(), DataType); - return baseDef with - { - Name = Name.Trim(), - DeviceHostAddress = DeviceHostAddress.Trim(), - TagPath = TagPath.Trim(), - DataType = DataType, - Writable = Writable, - }; - } - - public static string? ValidateRow(AbCipTagRow row, IReadOnlyList all, int? editIndex) - { - if (string.IsNullOrWhiteSpace(row.Name)) return "Name is required."; - for (var i = 0; i < all.Count; i++) - if (i != editIndex && string.Equals(all[i].Name, row.Name, StringComparison.OrdinalIgnoreCase)) - return $"Duplicate tag name '{row.Name}'."; - return null; - } - } - // Flat mutable model — all scalar properties settable for Blazor @bind-Value. - // Collections (Devices, Tags) are kept on the component and passed in on ToOptions(). + // Collections (Devices) are kept on the component and passed in on ToOptions(). public sealed class FormModel { // Operation @@ -509,11 +427,9 @@ else }; public AbCipDriverOptions ToOptions( - IReadOnlyList devices, - IReadOnlyList tags) => new() + IReadOnlyList devices) => new() { Devices = devices, - Tags = tags, Probe = new AbCipProbeOptions { Enabled = ProbeEnabled, diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/AbLegacyDriverPage.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/AbLegacyDriverPage.razor index fd582119..1d833e51 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/AbLegacyDriverPage.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/AbLegacyDriverPage.razor @@ -37,7 +37,7 @@ else CancelHref="@($"/clusters/{ClusterId}/drivers")" OnDelete="@(IsNew ? null : (EventCallback?)EventCallback.Factory.Create(this, DeleteAsync))"> - + @if (!IsNew && !string.IsNullOrEmpty(DriverInstanceId)) { @@ -140,37 +140,13 @@ else @* Tags *@ - - - NameDeviceAddressTypeWritable - - - @t.Name@t.DeviceHostAddress - @t.Address@t.DataType@(t.Writable ? "yes" : "no") - - -
-
-
-
-
-
-
-
-
-
- -
-
-
-
+
+
Tags
+
+ Tag authoring moves to the Raw tree (/raw) in v3 Batch 2. + Pre-declared driver tags have been retired. +
+
@@ -196,7 +172,6 @@ else 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; @@ -209,15 +184,10 @@ else // Held separately because Devices/Tags are collections — edited via the CollectionEditor modal. private List _devices = []; - private List _tags = []; 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) { @@ -226,7 +196,6 @@ else DriverInstanceId = "", Name = "", DriverType = DriverTypeKey, - NamespaceId = _namespaces.FirstOrDefault()?.NamespaceId ?? "", Enabled = true, }; _form = new FormModel(); @@ -242,7 +211,6 @@ else DriverInstanceId = _existing.DriverInstanceId, Name = _existing.Name, DriverType = _existing.DriverType, - NamespaceId = _existing.NamespaceId, Enabled = _existing.Enabled, }; var opts = TryDeserialize(_existing.DriverConfig) ?? new AbLegacyDriverOptions(); @@ -250,7 +218,6 @@ else _form.ResilienceConfig = _existing.ResilienceConfig; _form.RowVersion = _existing.RowVersion; _devices = opts.Devices.Select(AbLegacyDeviceRow.FromDefinition).ToList(); - _tags = opts.Tags.Select(AbLegacyTagRow.FromDefinition).ToList(); } } _loaded = true; @@ -263,8 +230,7 @@ else { var configJson = System.Text.Json.JsonSerializer.Serialize( _form.ToOptions( - _devices.Select(r => r.ToDefinition()).ToList(), - _tags.Select(r => r.ToDefinition()).ToList()), + _devices.Select(r => r.ToDefinition()).ToList()), _jsonOpts); await using var db = await DbFactory.CreateDbContextAsync(); if (IsNew) @@ -278,7 +244,6 @@ else { DriverInstanceId = _identityModel.DriverInstanceId, ClusterId = ClusterId, - NamespaceId = _identityModel.NamespaceId, Name = _identityModel.Name, DriverType = DriverTypeKey, Enabled = _identityModel.Enabled, @@ -292,7 +257,6 @@ else 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; @@ -338,8 +302,7 @@ else private string SerializeCurrentConfig() => System.Text.Json.JsonSerializer.Serialize( _form.ToOptions( - _devices.Select(r => r.ToDefinition()).ToList(), - _tags.Select(r => r.ToDefinition()).ToList()), + _devices.Select(r => r.ToDefinition()).ToList()), _jsonOpts); private static AbLegacyDriverOptions? TryDeserialize(string json) @@ -388,51 +351,6 @@ else } } - // Mutable VM for the modal editor — AbLegacyTagDefinition is an immutable record. - public sealed class AbLegacyTagRow - { - public string Name { get; set; } = ""; - public string DeviceHostAddress { get; set; } = ""; - public string Address { get; set; } = ""; - public AbLegacyDataType DataType { get; set; } = AbLegacyDataType.Int; - public bool Writable { get; set; } = true; - - // Original record (null for newly-added rows). Preserves fields the editor doesn't expose - // (WriteIdempotent) across a load→save. - private AbLegacyTagDefinition? _source; - - public AbLegacyTagRow Clone() => (AbLegacyTagRow)MemberwiseClone(); // _source is an immutable record ref — safe to share - - public static AbLegacyTagRow FromDefinition(AbLegacyTagDefinition d) => new() - { - Name = d.Name, DeviceHostAddress = d.DeviceHostAddress, Address = d.Address, - DataType = d.DataType, Writable = d.Writable, - _source = d, - }; - - public AbLegacyTagDefinition ToDefinition() - { - var baseDef = _source ?? new AbLegacyTagDefinition(Name.Trim(), DeviceHostAddress.Trim(), Address.Trim(), DataType); - return baseDef with - { - Name = Name.Trim(), - DeviceHostAddress = DeviceHostAddress.Trim(), - Address = Address.Trim(), - DataType = DataType, - Writable = Writable, - }; - } - - public static string? ValidateRow(AbLegacyTagRow row, IReadOnlyList all, int? editIndex) - { - if (string.IsNullOrWhiteSpace(row.Name)) return "Name is required."; - for (var i = 0; i < all.Count; i++) - if (i != editIndex && string.Equals(all[i].Name, row.Name, StringComparison.OrdinalIgnoreCase)) - return $"Duplicate tag name '{row.Name}'."; - return null; - } - } - // Flat mutable model — all scalar properties settable for Blazor @bind-Value. // Collections (Devices, Tags) are kept on the component and passed in on ToOptions(). public sealed class FormModel @@ -464,11 +382,9 @@ else }; public AbLegacyDriverOptions ToOptions( - IReadOnlyList devices, - IReadOnlyList tags) => new() + IReadOnlyList devices) => new() { Devices = devices, - Tags = tags, Probe = new AbLegacyProbeOptions { Enabled = ProbeEnabled, diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/FocasDriverPage.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/FocasDriverPage.razor index 213a2904..db1fdfe5 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/FocasDriverPage.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/FocasDriverPage.razor @@ -36,7 +36,7 @@ else CancelHref="@($"/clusters/{ClusterId}/drivers")" OnDelete="@(IsNew ? null : (EventCallback?)EventCallback.Factory.Create(this, DeleteAsync))"> - + @if (!IsNew && !string.IsNullOrEmpty(DriverInstanceId)) { @@ -218,37 +218,13 @@ else @* Tags *@ - - - NameDeviceAddressTypeWritable - - - @t.Name@t.DeviceHostAddress - @t.Address@t.DataType@(t.Writable ? "yes" : "no") - - -
-
-
-
-
-
-
-
-
-
- -
-
-
-
+
+
Tags
+
+ Tag authoring moves to the Raw tree (/raw) in v3 Batch 2. + Pre-declared driver tags have been retired. +
+
@@ -274,7 +250,6 @@ else private FormModel _form = new(); private DriverIdentitySection.DriverIdentityModel _identityModel = new() { DriverType = DriverTypeKey }; private DriverInstance? _existing; - private List _namespaces = new(); private bool _loaded, _busy; private string? _error; @@ -286,18 +261,14 @@ else // Held separately because Devices/Tags are collections — edited via the CollectionEditor modal. private List _devices = []; - private List _tags = []; 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() { DriverType = DriverTypeKey, NamespaceId = _namespaces.FirstOrDefault()?.NamespaceId ?? "", Enabled = true }; + _identityModel = new() { DriverType = DriverTypeKey, Enabled = true }; _form = FormModel.FromOptions(new FocasDriverOptions()); } else @@ -311,7 +282,6 @@ else DriverInstanceId = _existing.DriverInstanceId, Name = _existing.Name, DriverType = _existing.DriverType, - NamespaceId = _existing.NamespaceId, Enabled = _existing.Enabled, }; var opts = TryDeserialize(_existing.DriverConfig) ?? new FocasDriverOptions(); @@ -319,7 +289,6 @@ else _form.ResilienceConfig = _existing.ResilienceConfig; _form.RowVersion = _existing.RowVersion; _devices = opts.Devices.Select(FocasDeviceRow.FromDefinition).ToList(); - _tags = opts.Tags.Select(FocasTagRow.FromDefinition).ToList(); } } _loaded = true; @@ -331,8 +300,7 @@ else try { var opts = _form.ToOptions( - _devices.Select(r => r.ToDefinition()).ToList(), - _tags.Select(r => r.ToDefinition()).ToList()); + _devices.Select(r => r.ToDefinition()).ToList()); var configJson = System.Text.Json.JsonSerializer.Serialize(opts, _jsonOpts); await using var db = await DbFactory.CreateDbContextAsync(); if (IsNew) @@ -345,7 +313,6 @@ else { DriverInstanceId = _identityModel.DriverInstanceId, ClusterId = ClusterId, - NamespaceId = _identityModel.NamespaceId, Name = _identityModel.Name, DriverType = DriverTypeKey, Enabled = _identityModel.Enabled, @@ -359,7 +326,6 @@ else 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; @@ -405,8 +371,7 @@ else private string SerializeCurrentConfig() => System.Text.Json.JsonSerializer.Serialize( _form.ToOptions( - _devices.Select(r => r.ToDefinition()).ToList(), - _tags.Select(r => r.ToDefinition()).ToList()), + _devices.Select(r => r.ToDefinition()).ToList()), _jsonOpts); private static FocasDriverOptions? TryDeserialize(string json) @@ -455,51 +420,6 @@ else } } - // Mutable VM for the modal editor — FocasTagDefinition is an immutable record. - public sealed class FocasTagRow - { - public string Name { get; set; } = ""; - public string DeviceHostAddress { get; set; } = ""; - public string Address { get; set; } = ""; - public FocasDataType DataType { get; set; } = FocasDataType.Int32; - public bool Writable { get; set; } = true; - - // Original record (null for newly-added rows). Preserves fields the editor doesn't expose - // (WriteIdempotent) across a load→save. - private FocasTagDefinition? _source; - - public FocasTagRow Clone() => (FocasTagRow)MemberwiseClone(); // _source is an immutable record ref — safe to share - - public static FocasTagRow FromDefinition(FocasTagDefinition d) => new() - { - Name = d.Name, DeviceHostAddress = d.DeviceHostAddress, Address = d.Address, - DataType = d.DataType, Writable = d.Writable, - _source = d, - }; - - public FocasTagDefinition ToDefinition() - { - var baseDef = _source ?? new FocasTagDefinition(Name.Trim(), DeviceHostAddress.Trim(), Address.Trim(), DataType); - return baseDef with - { - Name = Name.Trim(), - DeviceHostAddress = DeviceHostAddress.Trim(), - Address = Address.Trim(), - DataType = DataType, - Writable = Writable, - }; - } - - public static string? ValidateRow(FocasTagRow row, IReadOnlyList all, int? editIndex) - { - if (string.IsNullOrWhiteSpace(row.Name)) return "Name is required."; - for (var i = 0; i < all.Count; i++) - if (i != editIndex && string.Equals(all[i].Name, row.Name, StringComparison.OrdinalIgnoreCase)) - return $"Duplicate tag name '{row.Name}'."; - return null; - } - } - // Flat mutable model — all scalar properties settable for Blazor @bind-Value. // Collections (Devices, Tags) are kept on the component and passed in on ToOptions(). public sealed class FormModel @@ -549,8 +469,7 @@ else }; public FocasDriverOptions ToOptions( - IReadOnlyList devices, - IReadOnlyList tags) => new() + IReadOnlyList devices) => new() { Timeout = TimeSpan.FromSeconds(TimeoutSeconds), Probe = new FocasProbeOptions @@ -578,7 +497,6 @@ else TimerPollInterval = TimeSpan.FromSeconds(FixedTreeTimerPollIntervalSeconds), }, Devices = devices, - Tags = tags, }; } } diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/GalaxyDriverPage.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/GalaxyDriverPage.razor index 3620f4da..12e8c060 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/GalaxyDriverPage.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/GalaxyDriverPage.razor @@ -36,7 +36,7 @@ else CancelHref="@($"/clusters/{ClusterId}/drivers")" OnDelete="@(IsNew ? null : (EventCallback?)EventCallback.Factory.Create(this, DeleteAsync))"> - + @if (!IsNew && !string.IsNullOrEmpty(DriverInstanceId)) { @@ -224,7 +224,6 @@ else 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; @@ -238,10 +237,6 @@ else 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) { @@ -250,7 +245,6 @@ else DriverInstanceId = "", Name = "", DriverType = DriverTypeKey, - NamespaceId = _namespaces.FirstOrDefault()?.NamespaceId ?? "", Enabled = true, }; _form = new FormModel(); @@ -266,7 +260,6 @@ else DriverInstanceId = _existing.DriverInstanceId, Name = _existing.Name, DriverType = _existing.DriverType, - NamespaceId = _existing.NamespaceId, Enabled = _existing.Enabled, }; var opts = TryDeserialize(_existing.DriverConfig) ?? CreateDefaultOptions(); @@ -303,7 +296,6 @@ else { DriverInstanceId = _identityModel.DriverInstanceId, ClusterId = ClusterId, - NamespaceId = _identityModel.NamespaceId, Name = _identityModel.Name, DriverType = DriverTypeKey, Enabled = _identityModel.Enabled, @@ -317,7 +309,6 @@ else 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; diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/ModbusDriverPage.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/ModbusDriverPage.razor index 489ab9c9..a09e50f5 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/ModbusDriverPage.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/ModbusDriverPage.razor @@ -36,7 +36,7 @@ else CancelHref="@($"/clusters/{ClusterId}/drivers")" OnDelete="@(IsNew ? null : (EventCallback?)EventCallback.Factory.Create(this, DeleteAsync))"> - + @if (!IsNew && !string.IsNullOrEmpty(DriverInstanceId)) { @@ -273,44 +273,13 @@ else
- - - NameRegionAddressTypeWritable - - - @t.Name@t.Region@t.Address - @t.DataType@(t.Writable ? "yes" : "no") - - -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
+
+
Tags
+
+ Tag authoring moves to the Raw tree (/raw) in v3 Batch 2. + Pre-declared driver tags have been retired. +
+
@@ -336,7 +305,6 @@ else 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; @@ -347,16 +315,9 @@ else private void OnAddressPicked(string address) => _pickedAddress = address; - // Held separately because Tags is a collection — edited via the CollectionEditor modal. - private List _tags = []; - 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) { @@ -365,7 +326,6 @@ else DriverInstanceId = "", Name = "", DriverType = DriverTypeKey, - NamespaceId = _namespaces.FirstOrDefault()?.NamespaceId ?? "", Enabled = true, }; _form = new FormModel(); @@ -381,14 +341,12 @@ else DriverInstanceId = _existing.DriverInstanceId, Name = _existing.Name, DriverType = _existing.DriverType, - NamespaceId = _existing.NamespaceId, Enabled = _existing.Enabled, }; var opts = TryDeserialize(_existing.DriverConfig) ?? new ModbusDriverOptions(); _form = FormModel.FromOptions(opts); _form.ResilienceConfig = _existing.ResilienceConfig; _form.RowVersion = _existing.RowVersion; - _tags = opts.Tags.Select(ModbusTagRow.FromDefinition).ToList(); } } _loaded = true; @@ -399,7 +357,7 @@ else _busy = true; _error = null; try { - var configJson = System.Text.Json.JsonSerializer.Serialize(_form.ToOptions(_tags.Select(r => r.ToDefinition()).ToList()), _jsonOpts); + var configJson = System.Text.Json.JsonSerializer.Serialize(_form.ToOptions(), _jsonOpts); await using var db = await DbFactory.CreateDbContextAsync(); if (IsNew) { @@ -412,7 +370,6 @@ else { DriverInstanceId = _identityModel.DriverInstanceId, ClusterId = ClusterId, - NamespaceId = _identityModel.NamespaceId, Name = _identityModel.Name, DriverType = DriverTypeKey, Enabled = _identityModel.Enabled, @@ -426,7 +383,6 @@ else 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; @@ -470,7 +426,7 @@ else } private string SerializeCurrentConfig() - => System.Text.Json.JsonSerializer.Serialize(_form.ToOptions(_tags.Select(r => r.ToDefinition()).ToList()), _jsonOpts); + => System.Text.Json.JsonSerializer.Serialize(_form.ToOptions(), _jsonOpts); private static ModbusDriverOptions? TryDeserialize(string json) { @@ -478,60 +434,6 @@ else catch { return null; } } - // Mutable VM for the modal editor — ModbusTagDefinition is an immutable record. - public sealed class ModbusTagRow - { - public string Name { get; set; } = ""; - public ModbusRegion Region { get; set; } = ModbusRegion.HoldingRegisters; - public int Address { get; set; } - public ModbusDataType DataType { get; set; } = ModbusDataType.Int16; - public bool Writable { get; set; } = true; - public ModbusByteOrder ByteOrder { get; set; } = ModbusByteOrder.BigEndian; - public int BitIndex { get; set; } - public int StringLength { get; set; } - public bool WriteIdempotent { get; set; } - - // Original record (null for newly-added rows). Preserves fields the editor doesn't expose - // (StringByteOrder, ArrayCount, Deadband, UnitId, CoalesceProhibited) across a load→save. - private ModbusTagDefinition? _source; - - public ModbusTagRow Clone() => (ModbusTagRow)MemberwiseClone(); // _source is an immutable record ref — safe to share - - public static ModbusTagRow FromDefinition(ModbusTagDefinition d) => new() - { - Name = d.Name, Region = d.Region, Address = d.Address, DataType = d.DataType, - Writable = d.Writable, ByteOrder = d.ByteOrder, BitIndex = d.BitIndex, - StringLength = d.StringLength, WriteIdempotent = d.WriteIdempotent, - _source = d, - }; - - public ModbusTagDefinition ToDefinition() - { - var baseDef = _source ?? new ModbusTagDefinition(Name.Trim(), Region, 0, DataType); - return baseDef with - { - Name = Name.Trim(), - Region = Region, - Address = (ushort)Math.Clamp(Address, 0, 65535), - DataType = DataType, - Writable = Writable, - ByteOrder = ByteOrder, - BitIndex = (byte)Math.Clamp(BitIndex, 0, 255), - StringLength = (ushort)Math.Clamp(StringLength, 0, 65535), - WriteIdempotent = WriteIdempotent, - }; - } - - public static string? ValidateRow(ModbusTagRow row, IReadOnlyList all, int? editIndex) - { - if (string.IsNullOrWhiteSpace(row.Name)) return "Name is required."; - for (var i = 0; i < all.Count; i++) - if (i != editIndex && string.Equals(all[i].Name, row.Name, StringComparison.OrdinalIgnoreCase)) - return $"Duplicate tag name '{row.Name}'."; - return null; - } - } - // Flat mutable model — all scalars exposed as settable properties so Blazor @bind-Value works. // Collection (Tags) is kept on the component (_tags) and passed in when building the final Options. public sealed class FormModel @@ -623,13 +525,12 @@ else AdminProbeTimeoutSeconds = o.ProbeTimeoutSeconds, }; - public ModbusDriverOptions ToOptions(IReadOnlyList tags) => new() + public ModbusDriverOptions ToOptions() => new() { Host = Host, Port = Port, UnitId = (byte)Math.Clamp(UnitId, 0, 255), Timeout = TimeSpan.FromSeconds(TimeoutSeconds), - Tags = tags, Probe = new ModbusProbeOptions { Enabled = ProbeEnabled, diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/OpcUaClientDriverPage.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/OpcUaClientDriverPage.razor index 28d10777..ea632ae5 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/OpcUaClientDriverPage.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/OpcUaClientDriverPage.razor @@ -36,7 +36,7 @@ else CancelHref="@($"/clusters/{ClusterId}/drivers")" OnDelete="@(IsNew ? null : (EventCallback?)EventCallback.Factory.Create(this, DeleteAsync))"> - + @if (!IsNew && !string.IsNullOrEmpty(DriverInstanceId)) { @@ -285,7 +285,6 @@ else 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; @@ -305,10 +304,6 @@ else 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) { @@ -317,7 +312,6 @@ else DriverInstanceId = "", Name = "", DriverType = DriverTypeKey, - NamespaceId = _namespaces.FirstOrDefault()?.NamespaceId ?? "", Enabled = true, }; _form = new FormModel(); @@ -333,7 +327,6 @@ else DriverInstanceId = _existing.DriverInstanceId, Name = _existing.Name, DriverType = _existing.DriverType, - NamespaceId = _existing.NamespaceId, Enabled = _existing.Enabled, }; var opts = TryDeserialize(_existing.DriverConfig) ?? new OpcUaClientDriverOptions(); @@ -366,7 +359,6 @@ else { DriverInstanceId = _identityModel.DriverInstanceId, ClusterId = ClusterId, - NamespaceId = _identityModel.NamespaceId, Name = _identityModel.Name, DriverType = DriverTypeKey, Enabled = _identityModel.Enabled, @@ -380,7 +372,6 @@ else 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; diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/S7DriverPage.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/S7DriverPage.razor index 994e2c97..8aaad814 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/S7DriverPage.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/S7DriverPage.razor @@ -36,7 +36,7 @@ else CancelHref="@($"/clusters/{ClusterId}/drivers")" OnDelete="@(IsNew ? null : (EventCallback?)EventCallback.Factory.Create(this, DeleteAsync))"> - + @if (!IsNew && !string.IsNullOrEmpty(DriverInstanceId)) { @@ -146,37 +146,13 @@ else @* Tags *@ - - - NameAddressTypeWritable - - - @t.Name@t.Address - @t.DataType@(t.Writable ? "yes" : "no") - - -
-
-
-
-
-
-
-
- -
Only for String type. Max 254.
-
- -
-
-
-
+
+
Tags
+
+ Tag authoring moves to the Raw tree (/raw) in v3 Batch 2. + Pre-declared driver tags have been retired. +
+
@@ -202,7 +178,6 @@ else private FormModel _form = new(); private DriverIdentitySection.DriverIdentityModel _identityModel = new() { DriverType = DriverTypeKey }; private DriverInstance? _existing; - private List _namespaces = new(); private bool _loaded, _busy; private string? _error; @@ -212,19 +187,13 @@ else private void OnAddressPicked(string address) => _pickedAddress = address; - // Held separately because Tags is a collection — edited via the CollectionEditor modal. - private List _tags = []; - 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() { DriverType = DriverTypeKey, NamespaceId = _namespaces.FirstOrDefault()?.NamespaceId ?? "", Enabled = true }; + _identityModel = new() { DriverType = DriverTypeKey, Enabled = true }; _form = FormModel.FromOptions(new S7DriverOptions()); } else @@ -238,14 +207,12 @@ else DriverInstanceId = _existing.DriverInstanceId, Name = _existing.Name, DriverType = _existing.DriverType, - NamespaceId = _existing.NamespaceId, Enabled = _existing.Enabled, }; var opts = TryDeserialize(_existing.DriverConfig) ?? new S7DriverOptions(); _form = FormModel.FromOptions(opts); _form.ResilienceConfig = _existing.ResilienceConfig; _form.RowVersion = _existing.RowVersion; - _tags = opts.Tags.Select(S7TagRow.FromDefinition).ToList(); } } _loaded = true; @@ -256,7 +223,7 @@ else _busy = true; _error = null; try { - var opts = _form.ToOptions(_tags.Select(r => r.ToDefinition()).ToList()); + var opts = _form.ToOptions(); var configJson = System.Text.Json.JsonSerializer.Serialize(opts, _jsonOpts); await using var db = await DbFactory.CreateDbContextAsync(); if (IsNew) @@ -269,7 +236,6 @@ else { DriverInstanceId = _identityModel.DriverInstanceId, ClusterId = ClusterId, - NamespaceId = _identityModel.NamespaceId, Name = _identityModel.Name, DriverType = DriverTypeKey, Enabled = _identityModel.Enabled, @@ -283,7 +249,6 @@ else 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; @@ -328,7 +293,7 @@ else private string SerializeCurrentConfig() => System.Text.Json.JsonSerializer.Serialize( - _form.ToOptions(_tags.Select(r => r.ToDefinition()).ToList()), _jsonOpts); + _form.ToOptions(), _jsonOpts); private static S7DriverOptions? TryDeserialize(string json) { @@ -336,53 +301,7 @@ else catch { return null; } } - // Mutable VM for the modal editor — S7TagDefinition is an immutable record. - public sealed class S7TagRow - { - public string Name { get; set; } = ""; - public string Address { get; set; } = ""; - public S7DataType DataType { get; set; } = S7DataType.Int16; - public bool Writable { get; set; } = true; - public int StringLength { get; set; } = 254; - - // Original record (null for newly-added rows). Preserves fields the editor doesn't expose - // (WriteIdempotent) across a load→save. - private S7TagDefinition? _source; - - public S7TagRow Clone() => (S7TagRow)MemberwiseClone(); // _source is an immutable record ref — safe to share - - public static S7TagRow FromDefinition(S7TagDefinition d) => new() - { - Name = d.Name, Address = d.Address, DataType = d.DataType, - Writable = d.Writable, StringLength = d.StringLength, - _source = d, - }; - - public S7TagDefinition ToDefinition() - { - var baseDef = _source ?? new S7TagDefinition(Name.Trim(), Address.Trim(), DataType); - return baseDef with - { - Name = Name.Trim(), - Address = Address.Trim(), - DataType = DataType, - Writable = Writable, - StringLength = StringLength, - }; - } - - public static string? ValidateRow(S7TagRow row, IReadOnlyList all, int? editIndex) - { - if (string.IsNullOrWhiteSpace(row.Name)) return "Name is required."; - for (var i = 0; i < all.Count; i++) - if (i != editIndex && string.Equals(all[i].Name, row.Name, StringComparison.OrdinalIgnoreCase)) - return $"Duplicate tag name '{row.Name}'."; - return null; - } - } - // Flat mutable model — all scalar properties settable for Blazor @bind-Value. - // Collection (Tags) is kept on the component (_tags) and passed in on ToOptions(). public sealed class FormModel { // Connection @@ -417,7 +336,7 @@ else AdminProbeTimeoutSeconds = o.ProbeTimeoutSeconds, }; - public S7DriverOptions ToOptions(IReadOnlyList tags) => new() + public S7DriverOptions ToOptions() => new() { Host = Host, Port = Port, @@ -432,7 +351,6 @@ else Timeout = TimeSpan.FromSeconds(ProbeTimeoutSeconds), }, ProbeTimeoutSeconds = AdminProbeTimeoutSeconds, - Tags = tags, }; } } diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/TwinCATDriverPage.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/TwinCATDriverPage.razor index d8d9bc82..a23fb466 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/TwinCATDriverPage.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/TwinCATDriverPage.razor @@ -36,7 +36,7 @@ else CancelHref="@($"/clusters/{ClusterId}/drivers")" OnDelete="@(IsNew ? null : (EventCallback?)EventCallback.Factory.Create(this, DeleteAsync))"> - + @if (!IsNew && !string.IsNullOrEmpty(DriverInstanceId)) { @@ -157,37 +157,13 @@ else @* Tags *@ - - - NameDeviceSymbol pathTypeWritable - - - @t.Name@t.DeviceHostAddress - @t.SymbolPath@t.DataType@(t.Writable ? "yes" : "no") - - -
-
-
-
-
-
-
-
-
-
- -
-
-
-
+
+
Tags
+
+ Tag authoring moves to the Raw tree (/raw) in v3 Batch 2. + Pre-declared driver tags have been retired. +
+
@@ -213,7 +189,6 @@ else private FormModel _form = new(); private DriverIdentitySection.DriverIdentityModel _identityModel = new() { DriverType = DriverTypeKey }; private DriverInstance? _existing; - private List _namespaces = new(); private bool _loaded, _busy; private string? _error; @@ -223,20 +198,16 @@ else private void OnAddressPicked(string address) => _pickedAddress = address; - // Held separately because Devices/Tags are collections — edited via the CollectionEditor modal. + // Held separately because Devices is a collection — edited via the CollectionEditor modal. private List _devices = []; - private List _tags = []; 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() { DriverType = DriverTypeKey, NamespaceId = _namespaces.FirstOrDefault()?.NamespaceId ?? "", Enabled = true }; + _identityModel = new() { DriverType = DriverTypeKey, Enabled = true }; _form = FormModel.FromOptions(new TwinCATDriverOptions()); } else @@ -250,7 +221,6 @@ else DriverInstanceId = _existing.DriverInstanceId, Name = _existing.Name, DriverType = _existing.DriverType, - NamespaceId = _existing.NamespaceId, Enabled = _existing.Enabled, }; var opts = TryDeserialize(_existing.DriverConfig) ?? new TwinCATDriverOptions(); @@ -258,7 +228,6 @@ else _form.ResilienceConfig = _existing.ResilienceConfig; _form.RowVersion = _existing.RowVersion; _devices = opts.Devices.Select(TwinCATDeviceRow.FromDefinition).ToList(); - _tags = opts.Tags.Select(TwinCATTagRow.FromDefinition).ToList(); } } _loaded = true; @@ -271,8 +240,7 @@ else { var configJson = System.Text.Json.JsonSerializer.Serialize( _form.ToOptions( - _devices.Select(r => r.ToDefinition()).ToList(), - _tags.Select(r => r.ToDefinition()).ToList()), + _devices.Select(r => r.ToDefinition()).ToList()), _jsonOpts); await using var db = await DbFactory.CreateDbContextAsync(); if (IsNew) @@ -285,7 +253,6 @@ else { DriverInstanceId = _identityModel.DriverInstanceId, ClusterId = ClusterId, - NamespaceId = _identityModel.NamespaceId, Name = _identityModel.Name, DriverType = DriverTypeKey, Enabled = _identityModel.Enabled, @@ -299,7 +266,6 @@ else 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; @@ -345,8 +311,7 @@ else private string SerializeCurrentConfig() => System.Text.Json.JsonSerializer.Serialize( _form.ToOptions( - _devices.Select(r => r.ToDefinition()).ToList(), - _tags.Select(r => r.ToDefinition()).ToList()), + _devices.Select(r => r.ToDefinition()).ToList()), _jsonOpts); private static TwinCATDriverOptions? TryDeserialize(string json) @@ -393,53 +358,8 @@ else } } - // Mutable VM for the modal editor — TwinCATTagDefinition is an immutable record. - public sealed class TwinCATTagRow - { - public string Name { get; set; } = ""; - public string DeviceHostAddress { get; set; } = ""; - public string SymbolPath { get; set; } = ""; - public TwinCATDataType DataType { get; set; } = TwinCATDataType.DInt; - public bool Writable { get; set; } = true; - - // Original record (null for newly-added rows). Preserves fields the editor doesn't expose - // (WriteIdempotent) across a load→save. - private TwinCATTagDefinition? _source; - - public TwinCATTagRow Clone() => (TwinCATTagRow)MemberwiseClone(); // _source is an immutable record ref — safe to share - - public static TwinCATTagRow FromDefinition(TwinCATTagDefinition d) => new() - { - Name = d.Name, DeviceHostAddress = d.DeviceHostAddress, SymbolPath = d.SymbolPath, - DataType = d.DataType, Writable = d.Writable, - _source = d, - }; - - public TwinCATTagDefinition ToDefinition() - { - var baseDef = _source ?? new TwinCATTagDefinition(Name.Trim(), DeviceHostAddress.Trim(), SymbolPath.Trim(), DataType); - return baseDef with - { - Name = Name.Trim(), - DeviceHostAddress = DeviceHostAddress.Trim(), - SymbolPath = SymbolPath.Trim(), - DataType = DataType, - Writable = Writable, - }; - } - - public static string? ValidateRow(TwinCATTagRow row, IReadOnlyList all, int? editIndex) - { - if (string.IsNullOrWhiteSpace(row.Name)) return "Name is required."; - for (var i = 0; i < all.Count; i++) - if (i != editIndex && string.Equals(all[i].Name, row.Name, StringComparison.OrdinalIgnoreCase)) - return $"Duplicate tag name '{row.Name}'."; - return null; - } - } - // Flat mutable model — all scalar properties settable for Blazor @bind-Value. - // Collections (Devices, Tags) are kept on the component and passed in on ToOptions(). + // The Devices collection is kept on the component and passed in on ToOptions(). public sealed class FormModel { // Options @@ -471,8 +391,7 @@ else }; public TwinCATDriverOptions ToOptions( - IReadOnlyList devices, - IReadOnlyList tags) => new() + IReadOnlyList devices) => new() { Timeout = TimeSpan.FromSeconds(TimeoutSeconds), UseNativeNotifications = UseNativeNotifications, @@ -486,7 +405,6 @@ else }, ProbeTimeoutSeconds = AdminProbeTimeoutSeconds, Devices = devices, - Tags = tags, }; } } diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/NamespaceEdit.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/NamespaceEdit.razor index 5193e508..3649e207 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/NamespaceEdit.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/NamespaceEdit.razor @@ -1,243 +1,25 @@ @page "/clusters/{ClusterId}/namespaces/new" @page "/clusters/{ClusterId}/namespaces/{NamespaceId}" -@* Live-edit form pattern — one page handles both create (NamespaceId is null) and update. - RowVersion is preserved across post-back so EF Core enforces last-write-wins; concurrency - conflicts surface as a toast and reload the current row. *@ @attribute [Authorize(Policy = AdminUiPolicies.ConfigEditor)] @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.Configuration.Enums -@inject IDbContextFactory DbFactory -@inject NavigationManager Nav
-

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

- Cancel +

Namespaces · @ClusterId

+ Back
-@if (!_loaded) -{ -

Loading…

-} -else if (!IsNew && _existing is null) -{ -
- Namespace @NamespaceId was not found in cluster @ClusterId. -
-} -else -{ - - -
-
@(IsNew ? "Identity" : $"Edit {_form.NamespaceId}")
-
-
- - -
-
-
- - - - - -
-
- -
- - -
-
-
-
- - -
Must be unique fleet-wide. Clients pin discovery here.
-
-
- - -
-
-
- - @if (!string.IsNullOrWhiteSpace(_error)) - { -
@_error
- } - -
- - Cancel - @if (!IsNew) - { - - } -
-
-} +
+
Namespaces are retired in v3
+
+ The Namespace entity has been removed. The v3 Raw/UNS split replaces it — + raw driver folders and the unified namespace are managed on the /raw and + /uns pages, arriving in v3 Batch 2/3. This editor no longer applies. +
+
@code { [Parameter] public string ClusterId { get; set; } = ""; [Parameter] public string? NamespaceId { get; set; } - - private bool IsNew => string.IsNullOrEmpty(NamespaceId); - - private FormModel _form = new(); - private Namespace? _existing; - private bool _loaded; - private bool _busy; - private string? _error; - - protected override async Task OnInitializedAsync() - { - if (IsNew) - { - _form = new FormModel - { - NamespaceId = "", - Kind = NamespaceKind.Equipment, - NamespaceUri = "", - Enabled = true, - }; - } - else - { - await using var db = await DbFactory.CreateDbContextAsync(); - _existing = await db.Namespaces.AsNoTracking() - .FirstOrDefaultAsync(n => n.ClusterId == ClusterId && n.NamespaceId == NamespaceId); - if (_existing is not null) - { - _form = new FormModel - { - NamespaceId = _existing.NamespaceId, - Kind = _existing.Kind, - NamespaceUri = _existing.NamespaceUri, - Enabled = _existing.Enabled, - Notes = _existing.Notes, - RowVersion = _existing.RowVersion, - }; - } - } - _loaded = true; - } - - private async Task SubmitAsync() - { - _busy = true; - _error = null; - try - { - await using var db = await DbFactory.CreateDbContextAsync(); - if (IsNew) - { - if (await db.Namespaces.AnyAsync(n => n.NamespaceId == _form.NamespaceId)) - { - _error = $"Namespace '{_form.NamespaceId}' already exists."; - return; - } - db.Namespaces.Add(new Namespace - { - NamespaceId = _form.NamespaceId, - ClusterId = ClusterId, - Kind = _form.Kind, - NamespaceUri = _form.NamespaceUri, - Enabled = _form.Enabled, - Notes = string.IsNullOrWhiteSpace(_form.Notes) ? null : _form.Notes, - }); - } - else - { - var entity = await db.Namespaces.FirstOrDefaultAsync( - n => n.ClusterId == ClusterId && n.NamespaceId == NamespaceId); - if (entity is null) - { - _error = "Row no longer exists."; - return; - } - db.Entry(entity).Property(e => e.RowVersion).OriginalValue = _form.RowVersion; - entity.Kind = _form.Kind; - entity.NamespaceUri = _form.NamespaceUri; - entity.Enabled = _form.Enabled; - entity.Notes = string.IsNullOrWhiteSpace(_form.Notes) ? null : _form.Notes; - } - await db.SaveChangesAsync(); - Nav.NavigateTo($"/clusters/{ClusterId}/namespaces"); - } - catch (DbUpdateConcurrencyException) - { - _error = "Another user changed this namespace 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.Namespaces.FirstOrDefaultAsync( - n => n.ClusterId == ClusterId && n.NamespaceId == NamespaceId); - if (entity is null) - { - Nav.NavigateTo($"/clusters/{ClusterId}/namespaces"); - return; - } - db.Entry(entity).Property(e => e.RowVersion).OriginalValue = _form.RowVersion; - db.Namespaces.Remove(entity); - await db.SaveChangesAsync(); - Nav.NavigateTo($"/clusters/{ClusterId}/namespaces"); - } - catch (DbUpdateConcurrencyException) - { - _error = "Another user changed this namespace while you were viewing it. Reload before deleting."; - } - catch (Exception ex) - { - _error = ex.Message; - } - finally - { - _busy = false; - } - } - - private sealed class FormModel - { - [Required, RegularExpression("^[A-Za-z0-9_-]+$", ErrorMessage = "Use letters, digits, dash, underscore.")] - public string NamespaceId { get; set; } = ""; - public NamespaceKind Kind { get; set; } = NamespaceKind.Equipment; - [Required, RegularExpression("^urn:[A-Za-z0-9_:./-]+$", ErrorMessage = "Use a URN, e.g. urn:zb:warsaw-west:equipment.")] - public string NamespaceUri { get; set; } = ""; - public bool Enabled { get; set; } = true; - public string? Notes { get; set; } - public byte[] RowVersion { get; set; } = []; - } } diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/DriverIdentitySection.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/DriverIdentitySection.razor index 5b3a9039..5bf707fc 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/DriverIdentitySection.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/DriverIdentitySection.razor @@ -40,16 +40,6 @@
Cannot be changed after creation — drives the actor type that owns this instance.
} -
- - - @foreach (var ns in Namespaces) - { - - } - - -
@@ -63,7 +53,6 @@ @code { [Parameter, EditorRequired] public DriverIdentityModel Model { get; set; } = new(); - [Parameter, EditorRequired] public IReadOnlyList Namespaces { get; set; } = []; [Parameter] public bool IsNew { get; set; } [Parameter] public bool ShowDriverType { get; set; } @@ -73,7 +62,6 @@ public string DriverInstanceId { get; set; } = ""; [Required] public string Name { get; set; } = ""; [Required] public string DriverType { get; set; } = "Modbus"; - [Required] public string NamespaceId { get; set; } = ""; public bool Enabled { get; set; } = true; } } diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Uns/TagEditors/OpcUaClientTagConfigEditor.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Uns/TagEditors/OpcUaClientTagConfigEditor.razor index ef4c9d34..c8b378c7 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Uns/TagEditors/OpcUaClientTagConfigEditor.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Uns/TagEditors/OpcUaClientTagConfigEditor.razor @@ -1,10 +1,10 @@ @using ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors
-
- + + @onchange="@(e => Update(() => _m.NodeId = e.Value?.ToString() ?? string.Empty))" />
The remote OPC UA NodeId the driver reads/writes/subscribes against. Use the browse picker on the driver page to find it.
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/ScriptAnalysis/IScriptTagCatalog.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/ScriptAnalysis/IScriptTagCatalog.cs index 8e8eea22..e6f03fd9 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/ScriptAnalysis/IScriptTagCatalog.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/ScriptAnalysis/IScriptTagCatalog.cs @@ -140,12 +140,13 @@ public sealed class ScriptTagCatalog(IDbContextFactory d { await using var db = await dbFactory.CreateDbContextAsync(ct); - // Only the resolvable keys are projected, so no UNS join is needed: the equipment-tag key is - // the FullName from TagConfig, the FolderPath-scoped tag key is FolderPath/Name, and the - // virtual-tag key is its own Name. Pull just those columns, untracked. + // v3: Tag is Raw-only — EquipmentId / FolderPath / DriverInstanceId were retired (the + // equipment↔Tag + driver bindings are gone). The resolvable key is the driver FullName carried + // in TagConfig; project only the surviving columns. (The Raw/UNS tag catalog is rebuilt in + // Batch 2/3.) var tagRows = await db.Tags .AsNoTracking() - .Select(t => new { t.EquipmentId, t.Name, t.FolderPath, t.DataType, t.DriverInstanceId, t.TagConfig }) + .Select(t => new { t.Name, t.DataType, t.TagConfig }) .ToListAsync(ct); var vtagRows = await db.VirtualTags @@ -157,13 +158,10 @@ public sealed class ScriptTagCatalog(IDbContextFactory d foreach (var t in tagRows) { - var path = t.EquipmentId is null - // FolderPath-scoped tag (EquipmentId null) — subscribes by the MXAccess dot-ref, which - // is "FolderPath.Name" when a folder is set, else just "Name". - ? (string.IsNullOrWhiteSpace(t.FolderPath) ? t.Name : $"{t.FolderPath}.{t.Name}") - // Equipment driver tag — the runtime GetTag key is the driver FullName from TagConfig. - : ExtractFullNameFromTagConfig(t.TagConfig); - entries.Add(new ScriptTagInfo(path, "Tag", t.DataType, t.DriverInstanceId)); + // The runtime GetTag key is the driver FullName from TagConfig; fall back to Name if absent. + var full = ExtractFullNameFromTagConfig(t.TagConfig); + var path = string.IsNullOrWhiteSpace(full) ? t.Name : full; + entries.Add(new ScriptTagInfo(path, "Tag", t.DataType, null)); } foreach (var v in vtagRows) @@ -185,7 +183,10 @@ public sealed class ScriptTagCatalog(IDbContextFactory d /// private static string ExtractFullNameFromTagConfig(string tagConfig) { - if (string.IsNullOrWhiteSpace(tagConfig)) return tagConfig; + // Best-effort: pull the driver FullName out of the TagConfig JSON blob if present. + // Returns "" when absent/unparseable so the caller falls back to the tag Name — never + // the raw blob (which would surface as a garbage completion path). + if (string.IsNullOrWhiteSpace(tagConfig)) return ""; try { using var doc = System.Text.Json.JsonDocument.Parse(tagConfig); @@ -193,10 +194,10 @@ public sealed class ScriptTagCatalog(IDbContextFactory d && doc.RootElement.TryGetProperty("FullName", out var fullName) && fullName.ValueKind == System.Text.Json.JsonValueKind.String) { - return fullName.GetString() ?? tagConfig; + return fullName.GetString() ?? ""; } } - catch (System.Text.Json.JsonException) { /* fall through to raw blob */ } - return tagConfig; + catch (System.Text.Json.JsonException) { /* fall through to tag Name */ } + return ""; } } diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/UnsTreeService.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/UnsTreeService.cs index 1150a9b0..0a12e7ce 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/UnsTreeService.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/UnsTreeService.cs @@ -51,14 +51,10 @@ public sealed class UnsTreeService(IDbContextFactory dbF }) .ToListAsync(ct); - // Per-equipment driver-tag counts (tags with no equipment are excluded). - var tagCounts = (await db.Tags - .AsNoTracking() - .Where(t => t.EquipmentId != null) - .GroupBy(t => t.EquipmentId) - .Select(g => new { EquipmentId = g.Key!, Count = g.Count() }) - .ToListAsync(ct)) - .ToDictionary(x => x.EquipmentId, x => x.Count, StringComparer.Ordinal); + // Per-equipment driver-tag counts: the equipment↔Tag binding was retired in v3 + // (Tag is now Raw-only; authoring moved to the Raw tree in Batch 2/3). No binding + // exists to count against, so every equipment reports zero driver tags. + var tagCounts = new Dictionary(StringComparer.Ordinal); // Per-equipment virtual-tag counts (virtual tags with no equipment are excluded). var vtagCounts = (await db.VirtualTags @@ -83,14 +79,11 @@ public sealed class UnsTreeService(IDbContextFactory dbF } /// - public async Task> LoadTagsForEquipmentAsync(string equipmentId, CancellationToken ct = default) + public Task> LoadTagsForEquipmentAsync(string equipmentId, CancellationToken ct = default) { - await using var db = await dbFactory.CreateDbContextAsync(ct); - return await db.Tags.AsNoTracking() - .Where(t => t.EquipmentId == equipmentId) - .OrderBy(t => t.Name) - .Select(t => new EquipmentTagRow(t.TagId, t.Name, t.DriverInstanceId, t.DataType, t.AccessLevel)) - .ToListAsync(ct); + // The equipment↔Tag binding was retired in v3 (Tag is now Raw-only; authoring moved to + // the Raw tree in Batch 2/3). No equipment-bound tags exist to list. + return Task.FromResult>(Array.Empty()); } /// @@ -141,7 +134,8 @@ public sealed class UnsTreeService(IDbContextFactory dbF e.Name, e.MachineCode, e.UnsLineId, - e.DriverInstanceId, + // Equipment↔driver binding retired in v3 — no DriverInstanceId column remains. + null, e.ZTag, e.SAPID, e.Manufacturer, @@ -168,9 +162,10 @@ public sealed class UnsTreeService(IDbContextFactory dbF .Where(t => t.TagId == tagId) .Select(t => new TagEditDto( t.TagId, - t.EquipmentId!, + // Tag is Raw-only in v3 — equipment binding + driver binding retired. + string.Empty, t.Name, - t.DriverInstanceId, + string.Empty, t.DataType, t.AccessLevel, t.WriteIdempotent, @@ -293,37 +288,8 @@ public sealed class UnsTreeService(IDbContextFactory dbF db.Entry(entity).Property(e => e.RowVersion).OriginalValue = rowVersion; - // A cluster move must not orphan driver-bound equipment from its driver's - // cluster. Any equipment under this area that is bound to a driver in a different cluster - // than the target blocks the move. - if (newClusterId != entity.ClusterId) - { - var lineIds = await db.UnsLines - .Where(l => l.UnsAreaId == unsAreaId) - .Select(l => l.UnsLineId) - .ToListAsync(ct); - - var boundEquipment = await db.Equipment - .Where(eq => lineIds.Contains(eq.UnsLineId) && eq.DriverInstanceId != null) - .Select(eq => new { eq.EquipmentId, eq.DriverInstanceId }) - .ToListAsync(ct); - - foreach (var eq in boundEquipment) - { - var driverCluster = await db.DriverInstances - .Where(d => d.DriverInstanceId == eq.DriverInstanceId) - .Select(d => d.ClusterId) - .FirstOrDefaultAsync(ct); - - if (driverCluster is not null && driverCluster != newClusterId) - { - return new UnsMutationResult( - false, - $"Cannot move area to '{newClusterId}': equipment '{eq.EquipmentId}' is bound to a driver in cluster '{driverCluster}' (decision #122). Re-home or unbind it first."); - } - } - } - + // The equipment↔driver binding was retired in v3, so a cluster move can no longer + // orphan driver-bound equipment — the old cross-cluster binding guard is gone. entity.Name = name; entity.Notes = string.IsNullOrWhiteSpace(notes) ? null : notes; entity.ClusterId = newClusterId; @@ -416,40 +382,8 @@ public sealed class UnsTreeService(IDbContextFactory dbF db.Entry(entity).Property(e => e.RowVersion).OriginalValue = rowVersion; - // A reparent to a different area must not orphan driver-bound equipment - // from its driver's cluster. Resolve the new area's cluster and check every bound - // equipment item under this line against it. - if (newUnsAreaId != entity.UnsAreaId) - { - var newAreaCluster = await db.UnsAreas - .Where(a => a.UnsAreaId == newUnsAreaId) - .Select(a => (string?)a.ClusterId) - .FirstOrDefaultAsync(ct); - - if (newAreaCluster is not null) - { - var boundEquipment = await db.Equipment - .Where(eq => eq.UnsLineId == unsLineId && eq.DriverInstanceId != null) - .Select(eq => new { eq.EquipmentId, eq.DriverInstanceId }) - .ToListAsync(ct); - - foreach (var eq in boundEquipment) - { - var driverCluster = await db.DriverInstances - .Where(d => d.DriverInstanceId == eq.DriverInstanceId) - .Select(d => d.ClusterId) - .FirstOrDefaultAsync(ct); - - if (driverCluster is not null && driverCluster != newAreaCluster) - { - return new UnsMutationResult( - false, - $"Cannot move line to area '{newUnsAreaId}': equipment '{eq.EquipmentId}' is bound to a driver in cluster '{driverCluster}' (decision #122). Re-home or unbind it first."); - } - } - } - } - + // The equipment↔driver binding was retired in v3, so a line reparent can no longer + // orphan driver-bound equipment — the old cross-cluster binding guard is gone. entity.UnsAreaId = newUnsAreaId; entity.Name = name; entity.Notes = string.IsNullOrWhiteSpace(notes) ? null : notes; @@ -525,7 +459,7 @@ public sealed class UnsTreeService(IDbContextFactory dbF { EquipmentId = equipmentId, EquipmentUuid = uuid, - DriverInstanceId = string.IsNullOrWhiteSpace(input.DriverInstanceId) ? null : input.DriverInstanceId, + // Equipment↔driver binding retired in v3 — no DriverInstanceId column remains. UnsLineId = input.UnsLineId, Name = input.Name, MachineCode = input.MachineCode, @@ -596,7 +530,7 @@ public sealed class UnsTreeService(IDbContextFactory dbF { EquipmentId = equipmentId, EquipmentUuid = uuid, - DriverInstanceId = string.IsNullOrWhiteSpace(row.DriverInstanceId) ? null : row.DriverInstanceId, + // Equipment↔driver binding retired in v3 — no DriverInstanceId column remains. UnsLineId = row.UnsLineId, Name = row.Name, MachineCode = row.MachineCode, @@ -650,7 +584,7 @@ public sealed class UnsTreeService(IDbContextFactory dbF return guard.Value; } - entity.DriverInstanceId = string.IsNullOrWhiteSpace(input.DriverInstanceId) ? null : input.DriverInstanceId; + // Equipment↔driver binding retired in v3 — no DriverInstanceId column remains. entity.UnsLineId = input.UnsLineId; entity.Name = input.Name; entity.MachineCode = input.MachineCode; @@ -803,10 +737,7 @@ public sealed class UnsTreeService(IDbContextFactory dbF return "cluster nodes"; } - if (await db.Namespaces.AnyAsync(ns => ns.ClusterId == clusterId, ct)) - { - return "namespaces"; - } + // The Namespace entity was retired in v3 (Raw/UNS split); no namespaces child-check remains. if (await db.UnsAreas.AnyAsync(a => a.ClusterId == clusterId, ct)) { @@ -826,141 +757,40 @@ public sealed class UnsTreeService(IDbContextFactory dbF $"Delete failed: {blocker} still reference cluster '{clusterId}' — remove them first."; /// - public async Task> LoadTagDriversForEquipmentAsync( + public Task> LoadTagDriversForEquipmentAsync( string equipmentId, CancellationToken ct = default) { - await using var db = await dbFactory.CreateDbContextAsync(ct); - - var equipmentCluster = await ResolveEquipmentClusterAsync(db, equipmentId, ct); - if (equipmentCluster is null) - { - return Array.Empty<(string, string, string, string)>(); - } - - // Drivers in the equipment's cluster whose namespace is Equipment-kind. - // GalaxyMxGateway is an ordinary Equipment-kind driver post-de-split, so it surfaces here - // alongside the PLC drivers; its DriverConfig feeds the Galaxy live-browse address picker. - var equipmentNamespaceIds = await db.Namespaces - .Where(n => n.ClusterId == equipmentCluster && n.Kind == NamespaceKind.Equipment) - .Select(n => n.NamespaceId) - .ToListAsync(ct); - - var drivers = await db.DriverInstances - .Where(d => d.ClusterId == equipmentCluster && equipmentNamespaceIds.Contains(d.NamespaceId)) - .OrderBy(d => d.DriverInstanceId) - .Select(d => new { d.DriverInstanceId, d.Name, d.DriverType, d.DriverConfig }) - .ToListAsync(ct); - - return drivers - .Select(d => (d.DriverInstanceId, Display: $"{d.DriverInstanceId} — {d.Name}", d.DriverType, d.DriverConfig)) - .ToList(); + // The equipment↔driver binding and the Namespace entity were retired in v3 (Raw/UNS split). + // Equipment no longer scopes a candidate-driver list; tag/driver authoring moved to the Raw + // tree in Batch 2/3. No candidate drivers to offer. + return Task.FromResult>( + Array.Empty<(string, string, string, string)>()); } /// - public async Task CreateTagAsync( + public Task CreateTagAsync( string equipmentId, TagInput input, CancellationToken ct = default) { - await using var db = await dbFactory.CreateDbContextAsync(ct); - - if (await db.Tags.AnyAsync(t => t.TagId == input.TagId, ct)) - { - return new UnsMutationResult(false, $"Tag '{input.TagId}' already exists."); - } - - if (!IsValidJson(input.TagConfig)) - { - return new UnsMutationResult(false, "TagConfig is not valid JSON."); - } - - if (!await db.Equipment.AnyAsync(e => e.EquipmentId == equipmentId, ct)) - return new UnsMutationResult(false, $"Equipment '{equipmentId}' not found."); - - var equipmentCluster = await ResolveEquipmentClusterAsync(db, equipmentId, ct); - var guard = await CheckTagDriverGuardAsync(db, input.DriverInstanceId, equipmentCluster, ct); - if (guard is not null) - { - return guard.Value; - } - - if (await db.Tags.AnyAsync(t => t.EquipmentId == equipmentId && t.Name == input.Name, ct)) - { - return new UnsMutationResult(false, $"A tag named '{input.Name}' already exists on this equipment."); - } - - db.Tags.Add(new Tag - { - TagId = input.TagId, - DriverInstanceId = input.DriverInstanceId, - EquipmentId = equipmentId, - Name = input.Name, - FolderPath = null, - DataType = input.DataType, - AccessLevel = input.AccessLevel, - WriteIdempotent = input.WriteIdempotent, - PollGroupId = string.IsNullOrWhiteSpace(input.PollGroupId) ? null : input.PollGroupId, - TagConfig = input.TagConfig, - }); - await db.SaveChangesAsync(ct); - return new UnsMutationResult(true, null); + // Equipment-bound tag authoring was retired in v3: Tag is now Raw-only and the equipment↔Tag + // binding no longer exists. Authoring moved to the Raw tree (/raw) in v3 Batch 2. + return Task.FromResult(new UnsMutationResult( + false, "Tag authoring moved to the Raw tree (/raw) in v3 Batch 2.")); } /// - public async Task UpdateTagAsync( + public Task UpdateTagAsync( string tagId, TagInput input, byte[] rowVersion, CancellationToken ct = default) { - await using var db = await dbFactory.CreateDbContextAsync(ct); - - var entity = await db.Tags.FirstOrDefaultAsync(t => t.TagId == tagId, ct); - if (entity is null) - { - return new UnsMutationResult(false, "Row no longer exists."); - } - - db.Entry(entity).Property(e => e.RowVersion).OriginalValue = rowVersion; - - if (!IsValidJson(input.TagConfig)) - { - return new UnsMutationResult(false, "TagConfig is not valid JSON."); - } - - var equipmentCluster = await ResolveEquipmentClusterAsync(db, entity.EquipmentId, ct); - var guard = await CheckTagDriverGuardAsync(db, input.DriverInstanceId, equipmentCluster, ct); - if (guard is not null) - { - return guard.Value; - } - - if (await db.Tags.AnyAsync( - t => t.EquipmentId == entity.EquipmentId && t.Name == input.Name && t.TagId != tagId, - ct)) - { - return new UnsMutationResult(false, $"A tag named '{input.Name}' already exists on this equipment."); - } - - // EquipmentId and FolderPath (null) are preserved — tree tags are always equipment-bound. - entity.DriverInstanceId = input.DriverInstanceId; - entity.Name = input.Name; - entity.DataType = input.DataType; - entity.AccessLevel = input.AccessLevel; - entity.WriteIdempotent = input.WriteIdempotent; - entity.PollGroupId = string.IsNullOrWhiteSpace(input.PollGroupId) ? null : input.PollGroupId; - entity.TagConfig = input.TagConfig; - - try - { - await db.SaveChangesAsync(ct); - return new UnsMutationResult(true, null); - } - catch (DbUpdateConcurrencyException) - { - return new UnsMutationResult(false, "Another user changed this tag while you were editing."); - } + // Equipment-bound tag editing was retired in v3: Tag is now Raw-only and the equipment↔Tag + // binding no longer exists. Authoring moved to the Raw tree (/raw) in v3 Batch 2. + return Task.FromResult(new UnsMutationResult( + false, "Tag authoring moved to the Raw tree (/raw) in v3 Batch 2.")); } /// @@ -1048,9 +878,9 @@ public sealed class UnsTreeService(IDbContextFactory dbF .Select(s => s.SourceCode).FirstOrDefaultAsync(ct); if (!EquipmentScriptPaths.ContainsEquipToken(src)) return null; - var configs = await db.Tags.Where(t => t.EquipmentId == equipmentId) - .Select(t => t.TagConfig).ToListAsync(ct); - var fullNames = configs.Select(TagConfigFullName.Extract); + // The equipment↔Tag binding was retired in v3 (Tag is Raw-only), so no equipment-bound driver + // tags exist to derive an equipment tag base from. The {{equip}} token can't be resolved here. + var fullNames = Array.Empty(); if (EquipmentScriptPaths.DeriveEquipmentBase(fullNames) is null) { return new UnsMutationResult(false, @@ -1350,11 +1180,8 @@ public sealed class UnsTreeService(IDbContextFactory dbF return new UnsMutationResult(false, $"Driver '{driverInstanceId}' not found."); } - var ns = await db.Namespaces.FirstOrDefaultAsync(n => n.NamespaceId == driver.NamespaceId, ct); - if (ns?.Kind != NamespaceKind.Equipment) - { - return new UnsMutationResult(false, $"Driver '{driverInstanceId}' is not in an Equipment-kind namespace."); - } + // The Namespace entity + NamespaceKind were retired in v3 (Raw/UNS split); the + // driver-in-Equipment-namespace check no longer applies. if (driver.ClusterId != equipmentCluster) {