feat(adminui): DriverTagPicker modal + 9 static address builders

- DriverTagPicker shell: modal chrome + per-driver picker body
  rendered as ChildContent.
- 9 picker bodies (Modbus/AbCip/AbLegacy/S7/TwinCat/FOCAS/
  OpcUaClient/Galaxy/Historian.Wonderware). 5 have computed
  builder logic + unit tests; 4 are free-text passthroughs
  (live browse for OPC UA + Galaxy is a documented follow-up).
- Each typed driver page gets a "Pick address" button that opens
  the modal with the matching body. Picked address surfaces in
  the modal footer for manual copy — no JS interop in v1.
This commit is contained in:
Joseph Doherty
2026-05-28 11:21:33 -04:00
parent ffcc8d1065
commit 063005fefa
29 changed files with 873 additions and 0 deletions
@@ -0,0 +1,17 @@
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.Pickers;
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Pickers;
public sealed class AbLegacyAddressBuilderTests
{
[Theory]
[InlineData("N", 7, 0, "N7:0")]
[InlineData("B", 3, 1, "B3:1")]
[InlineData("F", 8, 12, "F8:12")]
[InlineData("T", 4, 0, "T4:0")]
[InlineData("C", 5, 2, "C5:2")]
public void Build_Canonical(string fileType, int fileNumber, int element, string expected)
=> AbLegacyAddressBuilder.Build(fileType, fileNumber, element).ShouldBe(expected);
}
@@ -0,0 +1,16 @@
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.Pickers;
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Pickers;
public sealed class FocasAddressBuilderTests
{
[Theory]
[InlineData("axis", 5, "axis:5")]
[InlineData("spindle", 0, "spindle:0")]
[InlineData("program", 100, "program:100")]
[InlineData("status", 1, "status:1")]
public void Build_Canonical(string group, int parameterId, string expected)
=> FocasAddressBuilder.Build(group, parameterId).ShouldBe(expected);
}
@@ -0,0 +1,15 @@
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.Pickers;
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Pickers;
public sealed class HistorianWonderwareAddressBuilderTests
{
[Theory]
[InlineData("SysTimeHour", "Cyclic", 60, "SysTimeHour?mode=Cyclic&interval=60")]
[InlineData("ReactorTemp", "Last", 1, "ReactorTemp?mode=Last&interval=1")]
[InlineData("FlowRate", "Delta", 30, "FlowRate?mode=Delta&interval=30")]
public void Build_Canonical(string tag, string mode, int interval, string expected)
=> HistorianWonderwareAddressBuilder.Build(tag, mode, interval).ShouldBe(expected);
}
@@ -0,0 +1,21 @@
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.Pickers;
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Pickers;
public sealed class ModbusAddressBuilderTests
{
[Theory]
[InlineData("Holding", 1, 1, "4x00001-1")]
[InlineData("Coil", 0, 1, "0x00000-1")]
[InlineData("Holding", 123, 4, "4x00123-4")]
[InlineData("DiscreteInput", 5, 1, "1x00005-1")]
[InlineData("Input", 99999, 125, "3x99999-125")]
public void Build_Canonical(string type, int offset, int length, string expected)
=> ModbusAddressBuilder.Build(type, offset, length).ShouldBe(expected);
[Fact]
public void Build_UnknownType_FallsBackToHolding()
=> ModbusAddressBuilder.Build("Unknown", 1, 1).ShouldBe("4x00001-1");
}
@@ -0,0 +1,20 @@
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.Pickers;
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Pickers;
public sealed class S7AddressBuilderTests
{
[Theory]
[InlineData("DB", 10, 20, "REAL", "DB10.DBD20:REAL")]
[InlineData("DB", 1, 0, "X", "DB1.DBX0.0:X")]
[InlineData("DB", 5, 4, "W", "DB5.DBW4:W")]
[InlineData("DB", 2, 6, "B", "DB2.DBB6:B")]
[InlineData("M", 1, 0, "X", "M0.0:X")]
[InlineData("M", 1, 4, "W", "M4:W")]
[InlineData("I", 1, 0, "B", "I0:B")]
[InlineData("Q", 1, 2, "W", "Q2:W")]
public void Build_Canonical(string area, int dbNumber, int offset, string s7Type, string expected)
=> S7AddressBuilder.Build(area, dbNumber, offset, s7Type).ShouldBe(expected);
}