6a01358b9a
A parity guard already existed for DriverTypeNames <-> the /raw driver picker, but not for the two dispatch maps downstream, so a driver could be registered, offered in the picker, and then have no config form. That is exactly what happened to Calculation (G-1) and to Sql/MTConnect/Calculation on the device modal (G-2) — both survived review because nothing could see them. The maps had to become data first. A Razor @switch compiles into BuildRenderTree's IL, so no test can enumerate its cases; the picker guard works only because RawDriverTypeDialog keeps its data in a field the markup enumerates. Both modals now render from DriverConfigFormMap / DeviceFormMap through <DynamicComponent>, and being public those maps need none of the picker test's BindingFlags.NonPublic fragility. - G-1 CalculationDriverForm.razor — RunTimeout was unauthorable via the UI. - G-2 Sql/MTConnect/Calculation declared single-connection rather than given hollow device forms; each holds one connection at the driver level. - G-3 DriverConfigModal's hardcoded "Galaxy or Mqtt" -> IsSingleConnection, so Sql/MTConnect authors are no longer sent to a device form with no endpoint. - G-4 DriverFormMapParityTests (5, both directions) + DriverDispatchMapParityTests. - G-5 CsvColumnMap entries for Sql, Mqtt, MTConnect. - G-6 RawBrowseCommitMapper Sql branch — and the browser end, which the audit missed: SqlBrowseSession emitted NO AddressFields, so a browsed leaf carried only a column name, and a column alone cannot address a Sql tag. It now travels schema/table/column and the mapper builds a WideRow config from them. A branch alone would have produced a half-built blob. Two findings while writing the guards. The G-6 test showed Modbus and Calculation also hit the generic address fallback — correctly, as neither is browsable — so it asserts the fall-through set EQUALS a documented non-browsable list in both directions; a one-way check would let a newly browsable driver be quietly added to the exclusion list. And TagConfigDriverTypeNameGuardTests was hollow: it enumerated a hand-written TheoryData that had already drifted (omitting Galaxy, Sql and Mqtt), guarding renames but not gaps. Now enumerated from the map with a coverage test facing the other way. Live-verified on docker-dev, since this repo has no bUnit and no unit test reaches Blazor parameter binding: opened Calculation's config (previously "No typed config form"), typed 3500, saved, reopened — the value persisted, so the DynamicComponent two-way binding round-trips. Sql's form renders fully and now reads "This driver holds a single connection, authored above". AdminUI.Tests 930 passed.
106 lines
6.1 KiB
C#
106 lines
6.1 KiB
C#
using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.DeviceForms;
|
|
using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.Forms;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers;
|
|
|
|
/// <summary>
|
|
/// <c>DriverType → typed driver-config form component</c>, rendered by <c>DriverConfigModal</c> through
|
|
/// <c><DynamicComponent></c>.
|
|
/// <para><b>Why this is a map and not the <c>@switch</c> it replaced.</b> A Razor <c>@switch</c> compiles
|
|
/// into <c>BuildRenderTree</c>'s IL, so no test can enumerate its cases — which is exactly how the modal
|
|
/// came to be missing a <c>Calculation</c> arm while the sibling driver-picker guard stayed green
|
|
/// (<c>deferment.md</c> G-1/G-4). The picker test only works because <c>RawDriverTypeDialog</c> keeps its
|
|
/// data in a field the markup enumerates; this type gives the two config modals the same property, and
|
|
/// being <c>public</c> it needs none of that test's <c>BindingFlags.NonPublic</c> fragility.</para>
|
|
/// <para>Modelled on <c>TagConfigEditorMap</c>. Guarded by <c>DriverFormMapParityTests</c>.</para>
|
|
/// </summary>
|
|
public static class DriverConfigFormMap
|
|
{
|
|
private static readonly IReadOnlyDictionary<string, Type> Map =
|
|
new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
[DriverTypeNames.Modbus] = typeof(ModbusDriverForm),
|
|
[DriverTypeNames.S7] = typeof(S7DriverForm),
|
|
[DriverTypeNames.AbCip] = typeof(AbCipDriverForm),
|
|
[DriverTypeNames.AbLegacy] = typeof(AbLegacyDriverForm),
|
|
[DriverTypeNames.TwinCAT] = typeof(TwinCATDriverForm),
|
|
[DriverTypeNames.FOCAS] = typeof(FocasDriverForm),
|
|
[DriverTypeNames.OpcUaClient] = typeof(OpcUaClientDriverForm),
|
|
[DriverTypeNames.Galaxy] = typeof(GalaxyDriverForm),
|
|
[DriverTypeNames.Sql] = typeof(SqlDriverForm),
|
|
[DriverTypeNames.Mqtt] = typeof(MqttDriverForm),
|
|
[DriverTypeNames.MTConnect] = typeof(MTConnectDriverForm),
|
|
[DriverTypeNames.Calculation] = typeof(CalculationDriverForm),
|
|
};
|
|
|
|
/// <summary>The driver types that have a typed config form.</summary>
|
|
public static IReadOnlyCollection<string> MappedDriverTypes => (IReadOnlyCollection<string>)Map.Keys;
|
|
|
|
/// <summary>Resolves the form component for a driver type, or null when none is mapped (the modal
|
|
/// then renders its "no typed config form" warning rather than crashing).</summary>
|
|
/// <param name="driverType">The driver type name.</param>
|
|
/// <returns>The form component type, or <see langword="null"/>.</returns>
|
|
public static Type? Resolve(string? driverType) =>
|
|
driverType is not null && Map.TryGetValue(driverType, out var t) ? t : null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// <c>DriverType → typed device-config form component</c>, rendered by <c>DeviceModal</c>.
|
|
/// <para><b>Not every driver has one, by design.</b> The types in
|
|
/// <see cref="SingleConnectionDriverTypes"/> hold ONE connection per driver instance and author it on the
|
|
/// driver form, so a per-device endpoint editor would be meaningless. The parity test consults that set
|
|
/// rather than demanding total coverage — which keeps it a real guard instead of one that has to be
|
|
/// suppressed.</para>
|
|
/// </summary>
|
|
public static class DeviceFormMap
|
|
{
|
|
private static readonly IReadOnlyDictionary<string, Type> Map =
|
|
new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
[DriverTypeNames.Modbus] = typeof(ModbusDeviceForm),
|
|
[DriverTypeNames.S7] = typeof(S7DeviceForm),
|
|
[DriverTypeNames.AbCip] = typeof(AbCipDeviceForm),
|
|
[DriverTypeNames.AbLegacy] = typeof(AbLegacyDeviceForm),
|
|
[DriverTypeNames.TwinCAT] = typeof(TwinCATDeviceForm),
|
|
[DriverTypeNames.FOCAS] = typeof(FocasDeviceForm),
|
|
[DriverTypeNames.OpcUaClient] = typeof(OpcUaClientDeviceForm),
|
|
[DriverTypeNames.Galaxy] = typeof(GalaxyDeviceForm),
|
|
[DriverTypeNames.Mqtt] = typeof(MqttDeviceForm),
|
|
};
|
|
|
|
/// <summary>
|
|
/// Driver types that hold a single connection at the DRIVER level, so they legitimately have no
|
|
/// per-device form. Galaxy and Mqtt author a connection on the driver form and keep an informational
|
|
/// device form; Sql (one connection string), MTConnect (one Agent URI) and Calculation (no
|
|
/// connection at all) have no device form to render.
|
|
/// <para>Read by <c>DriverConfigModal</c> so the operator is told where the endpoint actually lives —
|
|
/// it previously hardcoded Galaxy-or-Mqtt and told Sql/MTConnect authors to go and find an endpoint
|
|
/// field on the device that does not exist (<c>deferment.md</c> G-3).</para>
|
|
/// </summary>
|
|
public static readonly IReadOnlySet<string> SingleConnectionDriverTypes =
|
|
new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
DriverTypeNames.Galaxy,
|
|
DriverTypeNames.Mqtt,
|
|
DriverTypeNames.Sql,
|
|
DriverTypeNames.MTConnect,
|
|
DriverTypeNames.Calculation,
|
|
};
|
|
|
|
/// <summary>The driver types that have a typed device form.</summary>
|
|
public static IReadOnlyCollection<string> MappedDriverTypes => (IReadOnlyCollection<string>)Map.Keys;
|
|
|
|
/// <summary>Resolves the device form component for a driver type, or null when none is mapped.</summary>
|
|
/// <param name="driverType">The driver type name.</param>
|
|
/// <returns>The form component type, or <see langword="null"/>.</returns>
|
|
public static Type? Resolve(string? driverType) =>
|
|
driverType is not null && Map.TryGetValue(driverType, out var t) ? t : null;
|
|
|
|
/// <summary>True when this driver authors its connection on the DRIVER form rather than per-device.</summary>
|
|
/// <param name="driverType">The driver type name.</param>
|
|
/// <returns><see langword="true"/> when the connection is driver-level.</returns>
|
|
public static bool IsSingleConnection(string? driverType) =>
|
|
driverType is not null && SingleConnectionDriverTypes.Contains(driverType);
|
|
}
|