diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Uns/EquipmentPage.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Uns/EquipmentPage.razor index 5e2d6a10..636d2ced 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Uns/EquipmentPage.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Uns/EquipmentPage.razor @@ -298,7 +298,7 @@ else private bool _tagModalVisible; private bool _tagModalIsNew; private TagEditDto? _tagModalExisting; - private IReadOnlyList<(string Id, string Display, string DriverType)> _tagDriverOptions = Array.Empty<(string, string, string)>(); + private IReadOnlyList<(string Id, string Display, string DriverType, string DriverConfig)> _tagDriverOptions = Array.Empty<(string, string, string, string)>(); // --- Virtual Tags tab state. _vtags is null until the tab is first activated. --- private IReadOnlyList? _vtags; diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Uns/TagModal.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Uns/TagModal.razor index aff7f627..b156576d 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Uns/TagModal.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Uns/TagModal.razor @@ -5,9 +5,12 @@ either, the owning equipment is fixed. On a successful save it raises OnSaved so the host can refresh the equipment's children in place. *@ @using System.ComponentModel.DataAnnotations +@using System.Text.Json @using Microsoft.AspNetCore.Components.Forms @using ZB.MOM.WW.OtOpcUa.AdminUI.Uns @using ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors +@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.Enums @inject IUnsTreeService Svc @@ -44,9 +47,9 @@ - @foreach (var (id, display, _) in Drivers) + @foreach (var d in Drivers) { - + } @@ -90,6 +93,35 @@ {
Pick a driver above to configure this tag.
} + else if (IsGalaxyDriver) + { + @* GalaxyMxGateway has no typed TagConfigEditorMap editor; instead a Galaxy point is + authored as {"FullName":"tag_name.AttributeName"}. Offer the live-browse picker + (against the selected gateway's DriverConfig) plus a manual raw-JSON fallback. *@ + + +
+ The Galaxy reference, stored as {"FullName":"tag_name.AttributeName"}. + Pick one via Browse Galaxy or edit the JSON directly. +
+ + @if (_showGalaxyPicker) + { + + + + } + } else if (editorType is not null) { @@ -139,8 +171,10 @@ /// The tag being edited, when is false. [Parameter] public TagEditDto? Existing { get; set; } - /// The candidate drivers — scoped to the equipment's cluster by the host — as (Id, Display) pairs. - [Parameter] public IReadOnlyList<(string Id, string Display, string DriverType)> Drivers { get; set; } = Array.Empty<(string, string, string)>(); + /// The candidate drivers — scoped to the equipment's cluster by the host — as + /// (Id, Display, DriverType, DriverConfig) tuples. DriverType drives typed-editor dispatch; + /// DriverConfig feeds the Galaxy live-browse picker for GalaxyMxGateway drivers. + [Parameter] public IReadOnlyList<(string Id, string Display, string DriverType, string DriverConfig)> Drivers { get; set; } = Array.Empty<(string, string, string, string)>(); /// Raised after a successful create/save so the host can refresh the equipment's children and close. [Parameter] public EventCallback OnSaved { get; set; } @@ -152,14 +186,47 @@ private bool _busy; private string? _error; + // Galaxy live-browse picker state. Only meaningful when the selected driver is a GalaxyMxGateway. + private bool _showGalaxyPicker; + private string _galaxyAddress = ""; + // The DriverType of the currently-selected driver (drives editor dispatch). Null when no driver chosen. private string? SelectedDriverType => Drivers.FirstOrDefault(d => d.Id == _form.DriverInstanceId).DriverType; + // The DriverConfig JSON of the currently-selected driver — fed to the Galaxy picker so it browses the + // right gateway. Defaults to "{}" when no driver is chosen or the config is empty. + private string SelectedDriverConfig + { + get + { + var cfg = Drivers.FirstOrDefault(d => d.Id == _form.DriverInstanceId).DriverConfig; + return string.IsNullOrEmpty(cfg) ? "{}" : cfg; + } + } + + // True when the selected driver is a GalaxyMxGateway — Galaxy points are authored as + // {"FullName":"tag_name.AttributeName"} via the live-browse picker rather than a typed editor. + private bool IsGalaxyDriver => SelectedDriverType == "GalaxyMxGateway"; + // When the operator switches drivers, the previous driver's TagConfig schema no longer applies — // reset it so the newly-dispatched typed editor starts clean (no stale/leaked keys from the old // driver). Fires only on a user dropdown change (@bind-Value:after), not on the initial edit-load. - private void OnDriverChanged() => _form.TagConfig = "{}"; + private void OnDriverChanged() + { + _form.TagConfig = "{}"; + // The Galaxy reference belongs to the previous driver; clear the picker's working address too. + _galaxyAddress = ""; + } + + // The operator picked a Galaxy reference (tag_name.AttributeName); store it as the canonical + // {"FullName":"..."} TagConfig the Galaxy driver resolves to an MXAccess reference. Default + // (PascalCase) serialization yields the "FullName" key the driver/walker reads. + private void OnGalaxyAddressPicked(string address) + { + _galaxyAddress = address; + _form.TagConfig = JsonSerializer.Serialize(new { FullName = address }); + } private IDictionary BuildEditorParameters() => new Dictionary { @@ -189,6 +256,28 @@ }; } _error = null; + _showGalaxyPicker = false; + // Seed the picker's working address from any existing {"FullName":"..."} so it opens pre-populated. + _galaxyAddress = ReadFullName(_form.TagConfig); + } + + // Best-effort extraction of FullName from a Galaxy TagConfig; returns "" when absent or unparseable. + private static string ReadFullName(string? configJson) + { + if (string.IsNullOrWhiteSpace(configJson)) return ""; + try + { + using var doc = JsonDocument.Parse(configJson); + return doc.RootElement.ValueKind == JsonValueKind.Object + && doc.RootElement.TryGetProperty("FullName", out var fn) + && fn.ValueKind == JsonValueKind.String + ? fn.GetString() ?? "" + : ""; + } + catch (JsonException) + { + return ""; + } } private async Task SaveAsync() diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/IUnsTreeService.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/IUnsTreeService.cs index 4f12c413..74bda42f 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/IUnsTreeService.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/IUnsTreeService.cs @@ -340,9 +340,11 @@ public interface IUnsTreeService /// /// The equipment whose candidate drivers to load. /// A token to cancel the load. - /// The eligible drivers projected to (DriverInstanceId, Display, DriverType) triples, - /// where DriverType lets the TagModal dispatch to a per-driver-type typed config editor. - Task> LoadTagDriversForEquipmentAsync(string equipmentId, CancellationToken ct = default); + /// The eligible drivers projected to (DriverInstanceId, Display, DriverType, DriverConfig) + /// tuples, where DriverType lets the TagModal dispatch to a per-driver-type typed config editor + /// and DriverConfig feeds the live-browse address picker (e.g. the Galaxy gateway picker opens its + /// browse session against the selected GalaxyMxGateway's config). + Task> LoadTagDriversForEquipmentAsync(string equipmentId, CancellationToken ct = default); /// /// Creates a new equipment-bound tag. FolderPath is always null (decision #110 — 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 e0c0e656..a92ae909 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/UnsTreeService.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/UnsTreeService.cs @@ -711,7 +711,7 @@ public sealed class UnsTreeService(IDbContextFactory dbF } /// - public async Task> LoadTagDriversForEquipmentAsync( + public async Task> LoadTagDriversForEquipmentAsync( string equipmentId, CancellationToken ct = default) { @@ -720,10 +720,12 @@ public sealed class UnsTreeService(IDbContextFactory dbF var equipmentCluster = await ResolveEquipmentClusterAsync(db, equipmentId, ct); if (equipmentCluster is null) { - return Array.Empty<(string, string, string)>(); + return Array.Empty<(string, string, string, string)>(); } // Drivers in the equipment's cluster whose namespace is Equipment-kind (decision #110). + // 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) @@ -732,11 +734,11 @@ public sealed class UnsTreeService(IDbContextFactory dbF 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 }) + .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)) + .Select(d => (d.DriverInstanceId, Display: $"{d.DriverInstanceId} — {d.Name}", d.DriverType, d.DriverConfig)) .ToList(); } diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceTagDriversTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceTagDriversTests.cs index 11976c3d..c61ba1f5 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceTagDriversTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceTagDriversTests.cs @@ -8,17 +8,19 @@ namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns; /// /// Verifies that surfaces each -/// candidate driver's DriverType alongside its id and display string, so the UNS TagModal -/// can later dispatch to a per-driver-type typed tag-config editor (F-uns-1). +/// candidate driver's DriverType and DriverConfig alongside its id and display string, +/// so the UNS TagModal can dispatch to a per-driver-type typed tag-config editor (F-uns-1) and feed +/// the selected gateway's config to the Galaxy live-browse address picker. /// [Trait("Category", "Unit")] public sealed class UnsTreeServiceTagDriversTests { /// - /// A driver loaded for an equipment carries its DriverType in the returned tuple. + /// A driver loaded for an equipment carries its DriverType and DriverConfig in the + /// returned tuple. /// [Fact] - public async Task LoadTagDriversForEquipment_surfaces_driver_type() + public async Task LoadTagDriversForEquipment_surfaces_driver_type_and_config() { var dbName = $"uns-tagdrivers-{Guid.NewGuid():N}"; @@ -57,7 +59,7 @@ public sealed class UnsTreeServiceTagDriversTests NamespaceId = "NS-EQ", Name = "equipment driver", DriverType = "ModbusTcp", - DriverConfig = "{}", + DriverConfig = """{"endpoint":"10.0.0.1:502"}""", }); db.SaveChanges(); } @@ -69,5 +71,7 @@ public sealed class UnsTreeServiceTagDriversTests drivers.Count.ShouldBe(1); drivers[0].DriverInstanceId.ShouldBe("DRV-EQ"); drivers[0].DriverType.ShouldBe("ModbusTcp"); + // The picker (Galaxy live-browse) opens its session against the selected driver's config. + drivers[0].DriverConfig.ShouldBe("""{"endpoint":"10.0.0.1:502"}"""); } }