feat(adminui): B2-WP3 driver/device config modals + page-to-form refactor

Extract the 8 typed driver pages into embeddable <Driver>DriverForm bodies
(channel/protocol config) and add per-driver <Driver>DeviceForm bodies
(connection endpoint -> Device.DeviceConfig, v3 endpoint split). New
DriverConfigModal + DeviceModal dispatch by DriverType (DriverTypeNames) for
the /raw tree; Test-connect inside DeviceModal builds the merged Driver+Device
config transiently via DriverDeviceConfigMerger. Routed pages now host the
form bodies and keep working. Round-trip + enum-serialization guard tests for
the Modbus driver form + Modbus device model; retargeted the existing
page-form serialization tests + the _jsonOpts converter guard to the forms.

Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox
This commit is contained in:
Joseph Doherty
2026-07-16 03:26:24 -04:00
parent 54ab413396
commit 844f93f64f
44 changed files with 3728 additions and 2474 deletions
@@ -3,13 +3,14 @@ using System.Text.Json.Serialization;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Pages.Clusters.Drivers;
using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.Forms;
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Config;
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests;
public sealed class GalaxyDriverPageFormSerializationTests
{
// Matches GalaxyDriverPage._jsonOpts (camelCase, no PropertyNameCaseInsensitive).
// Matches GalaxyDriverForm._jsonOpts (camelCase, no PropertyNameCaseInsensitive).
private static readonly JsonSerializerOptions _opts = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
@@ -142,7 +143,7 @@ public sealed class GalaxyDriverPageFormSerializationTests
var driverOpts = JsonSerializer.Deserialize<GalaxyDriverOptions>(seededJson, _pageOpts);
driverOpts.ShouldNotBeNull();
var form = GalaxyDriverPage.GalaxyFormModel.FromRecord(driverOpts!);
var form = GalaxyDriverForm.GalaxyFormModel.FromRecord(driverOpts!);
// Assert REAL seeded values — not defaults.
form.GatewayEndpoint.ShouldBe("http://10.100.0.48:5120");
@@ -155,7 +156,7 @@ public sealed class GalaxyDriverPageFormSerializationTests
/// <summary>
/// Defence-in-depth: a config that genuinely OMITS a section (no Reconnect key at all)
/// must not throw — <see cref="GalaxyDriverPage.GalaxyFormModel.FromRecord"/> must
/// must not throw — <see cref="GalaxyDriverForm.GalaxyFormModel.FromRecord"/> must
/// null-coalesce the missing section to its default value.
/// </summary>
[Fact]
@@ -175,7 +176,7 @@ public sealed class GalaxyDriverPageFormSerializationTests
driverOpts.ShouldNotBeNull();
// FromRecord must not throw even though Reconnect (and other sections) is null.
var form = Should.NotThrow(() => GalaxyDriverPage.GalaxyFormModel.FromRecord(driverOpts!));
var form = Should.NotThrow(() => GalaxyDriverForm.GalaxyFormModel.FromRecord(driverOpts!));
// Omitted Reconnect section falls back to GalaxyReconnectOptions() defaults.
var defaultRc = new GalaxyReconnectOptions();
@@ -185,7 +186,7 @@ public sealed class GalaxyDriverPageFormSerializationTests
}
/// <summary>
/// Confirms that <see cref="GalaxyDriverPage.GalaxyFormModel.FromRecord"/> still
/// Confirms that <see cref="GalaxyDriverForm.GalaxyFormModel.FromRecord"/> still
/// round-trips correctly when all nested records are populated (non-regressed path).
/// </summary>
[Fact]
@@ -216,7 +217,7 @@ public sealed class GalaxyDriverPageFormSerializationTests
ProbeTimeoutSeconds = 20,
};
var form = GalaxyDriverPage.GalaxyFormModel.FromRecord(original);
var form = GalaxyDriverForm.GalaxyFormModel.FromRecord(original);
form.GatewayEndpoint.ShouldBe("https://gw.example.com:5001");
form.GatewayApiKeySecretRef.ShouldBe("env:MY_KEY");