fix(adminui): guard the driver dispatch maps and close G-1..G-6 (§8.4)
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.
This commit is contained in:
@@ -3,6 +3,7 @@ using ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.Types;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Sql.Contracts;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Mqtt;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Uns;
|
||||
@@ -139,9 +140,19 @@ public static class RawBrowseCommitMapper
|
||||
return new MTConnectTagConfigModel { FullName = address }.ToJson();
|
||||
if (Is(driverType, DriverTypeNames.Galaxy))
|
||||
return WriteSingleKey("attributeRef", address);
|
||||
// Sql: a browsed leaf is a COLUMN, and a column name alone cannot address a tag — SqlTagConfigModel
|
||||
// needs the table too. SqlBrowseSession therefore travels schema/table/columnName in AddressFields.
|
||||
// Without this branch a Sql commit fell through to the generic "address" key below, which the typed
|
||||
// Sql editor does not read: it would open with empty fields and blank them on save — the exact
|
||||
// failure the MTConnect branch above was added to avoid (deferment.md G-6). Sql IS browsable
|
||||
// (SqlDriverBrowser), so the fallback's "browsable drivers are all handled above" was untrue.
|
||||
if (Is(driverType, DriverTypeNames.Sql))
|
||||
return BuildSqlTagConfig(address, addressFields);
|
||||
|
||||
// Unknown/flat-address driver (e.g. Modbus is not browsable): record the reference under a generic
|
||||
// "address" key so nothing is lost. Browsable drivers are all handled above.
|
||||
// "address" key so nothing is lost. Every BROWSABLE driver is handled above — guarded by
|
||||
// RawBrowseCommitMapperParityTests, which enumerates DriverTypeNames.All rather than trusting
|
||||
// this comment.
|
||||
return WriteSingleKey("address", address);
|
||||
}
|
||||
|
||||
@@ -261,6 +272,41 @@ public static class RawBrowseCommitMapper
|
||||
private static bool Is(string driverType, string name)
|
||||
=> string.Equals(driverType, name, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// Builds a <c>SqlTagConfigModel</c> blob from a browsed COLUMN leaf. Maps to
|
||||
/// <c>SqlTagModel.WideRow</c> — the browse tree is schema → table → column, which is precisely the
|
||||
/// wide-row shape (one row holds many signals as columns). A KeyValue (EAV) tag cannot be derived
|
||||
/// from a browse pick because its key VALUE is data, not schema, so the operator authors that in the
|
||||
/// typed editor.
|
||||
/// <para>Falls back to the generic key when the fields are absent — an older browser build, or a
|
||||
/// hand-made selection — rather than emitting a half-built Sql blob.</para>
|
||||
/// </summary>
|
||||
private static string BuildSqlTagConfig(string address, IReadOnlyDictionary<string, string>? addressFields)
|
||||
{
|
||||
if (addressFields is null
|
||||
|| !addressFields.TryGetValue("table", out var table)
|
||||
|| string.IsNullOrWhiteSpace(table))
|
||||
{
|
||||
return WriteSingleKey("address", address);
|
||||
}
|
||||
|
||||
addressFields.TryGetValue("schema", out var schema);
|
||||
addressFields.TryGetValue("columnName", out var columnName);
|
||||
|
||||
// Qualify the table with its schema when the schema is not the default, so a tag authored against
|
||||
// "sales.Readings" cannot silently resolve to "dbo.Readings".
|
||||
var qualified = !string.IsNullOrWhiteSpace(schema) && !string.Equals(schema, "dbo", StringComparison.OrdinalIgnoreCase)
|
||||
? $"{schema}.{table}"
|
||||
: table;
|
||||
|
||||
return new SqlTagConfigModel
|
||||
{
|
||||
Model = SqlTagModel.WideRow,
|
||||
Table = qualified,
|
||||
ColumnName = string.IsNullOrWhiteSpace(columnName) ? address : columnName,
|
||||
}.ToJson();
|
||||
}
|
||||
|
||||
private static string WriteSingleKey(string key, string value)
|
||||
{
|
||||
var o = new JsonObject { [key] = value };
|
||||
|
||||
@@ -319,9 +319,11 @@ public static class CsvColumnMap
|
||||
public static IReadOnlyList<string> CommonColumns { get; } =
|
||||
[.. CommonLeadingColumns, TagConfigJsonColumn];
|
||||
|
||||
/// <summary>The Calculation (virtual-tag) driver-type string. Not yet a <see cref="DriverTypeNames"/>
|
||||
/// constant (its factory lands in a later Batch-2 package), so it is pinned here for the CSV surface.</summary>
|
||||
public const string CalculationDriverType = "Calculation";
|
||||
/// <summary>The Calculation (virtual-tag) driver-type string.</summary>
|
||||
/// <remarks>Retained as an alias for <see cref="DriverTypeNames.Calculation"/>, which now exists — the
|
||||
/// original note here ("not yet a DriverTypeNames constant; its factory lands in a later Batch-2
|
||||
/// package") went stale once the factory registered.</remarks>
|
||||
public const string CalculationDriverType = DriverTypeNames.Calculation;
|
||||
|
||||
private static readonly IReadOnlyDictionary<string, CsvDriverTagMap> Maps = BuildMaps();
|
||||
|
||||
@@ -449,6 +451,46 @@ public static class CsvColumnMap
|
||||
new CsvRawKeyColumn(new CsvTypedColumn("ChangeTriggered", "changeTriggered", null), CsvRawKeyKind.Bool),
|
||||
new CsvRawKeyColumn(new CsvTypedColumn("TimerIntervalMs", "timerIntervalMs", null), CsvRawKeyKind.Int),
|
||||
}),
|
||||
|
||||
// deferment.md G-5 — Sql, Mqtt and MTConnect all have typed tag editors but had NO CSV map, so
|
||||
// import/export silently degraded them to the raw TagConfigJson fallback while the seven older
|
||||
// drivers got typed columns. Raw-key maps (as Galaxy and Calculation use) rather than
|
||||
// model-backed ones: they address the identity keys directly, which is what a CSV round-trip
|
||||
// needs, without duplicating each model's full accessor surface.
|
||||
new RawKeyCsvDriverTagMap(DriverTypeNames.Sql, new[]
|
||||
{
|
||||
new CsvRawKeyColumn(new CsvTypedColumn("Model", "model", null), CsvRawKeyKind.String),
|
||||
new CsvRawKeyColumn(new CsvTypedColumn("Table", "table", null), CsvRawKeyKind.String),
|
||||
new CsvRawKeyColumn(new CsvTypedColumn("KeyColumn", "keyColumn", null), CsvRawKeyKind.String),
|
||||
new CsvRawKeyColumn(new CsvTypedColumn("KeyValue", "keyValue", null), CsvRawKeyKind.String),
|
||||
new CsvRawKeyColumn(new CsvTypedColumn("ValueColumn", "valueColumn", null), CsvRawKeyKind.String),
|
||||
new CsvRawKeyColumn(new CsvTypedColumn("ColumnName", "columnName", null), CsvRawKeyKind.String),
|
||||
new CsvRawKeyColumn(new CsvTypedColumn("SqlType", "type", null), CsvRawKeyKind.String),
|
||||
}),
|
||||
|
||||
new RawKeyCsvDriverTagMap(DriverTypeNames.Mqtt, new[]
|
||||
{
|
||||
new CsvRawKeyColumn(new CsvTypedColumn("Mode", "mode", null), CsvRawKeyKind.String),
|
||||
new CsvRawKeyColumn(new CsvTypedColumn("Topic", "topic", null), CsvRawKeyKind.String),
|
||||
new CsvRawKeyColumn(new CsvTypedColumn("PayloadFormat", "payloadFormat", null), CsvRawKeyKind.String),
|
||||
new CsvRawKeyColumn(new CsvTypedColumn("JsonPath", "jsonPath", null), CsvRawKeyKind.String),
|
||||
new CsvRawKeyColumn(new CsvTypedColumn("MqttDataType", "dataType", null), CsvRawKeyKind.String),
|
||||
new CsvRawKeyColumn(new CsvTypedColumn("GroupId", "groupId", null), CsvRawKeyKind.String),
|
||||
new CsvRawKeyColumn(new CsvTypedColumn("EdgeNodeId", "edgeNodeId", null), CsvRawKeyKind.String),
|
||||
new CsvRawKeyColumn(new CsvTypedColumn("MqttDeviceId", "deviceId", null), CsvRawKeyKind.String),
|
||||
}),
|
||||
|
||||
new RawKeyCsvDriverTagMap(DriverTypeNames.MTConnect, new[]
|
||||
{
|
||||
new CsvRawKeyColumn(new CsvTypedColumn("DataItemId", "fullName", null), CsvRawKeyKind.String),
|
||||
new CsvRawKeyColumn(new CsvTypedColumn("MtDataType", "dataType", null), CsvRawKeyKind.String),
|
||||
new CsvRawKeyColumn(new CsvTypedColumn("MtCategory", "mtCategory", null), CsvRawKeyKind.String),
|
||||
new CsvRawKeyColumn(new CsvTypedColumn("MtType", "mtType", null), CsvRawKeyKind.String),
|
||||
new CsvRawKeyColumn(new CsvTypedColumn("MtSubType", "mtSubType", null), CsvRawKeyKind.String),
|
||||
new CsvRawKeyColumn(new CsvTypedColumn("MtUnits", "units", null), CsvRawKeyKind.String),
|
||||
new CsvRawKeyColumn(new CsvTypedColumn("MtDevice", "mtDevice", null), CsvRawKeyKind.String),
|
||||
new CsvRawKeyColumn(new CsvTypedColumn("MtComponent", "mtComponent", null), CsvRawKeyKind.String),
|
||||
}),
|
||||
};
|
||||
|
||||
return maps.ToDictionary(m => m.DriverType, m => m, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
@@ -22,9 +22,9 @@ public static class TagConfigEditorMap
|
||||
[DriverTypeNames.FOCAS] = typeof(Components.Shared.Uns.TagEditors.FocasTagConfigEditor),
|
||||
[DriverTypeNames.OpcUaClient] = typeof(Components.Shared.Uns.TagEditors.OpcUaClientTagConfigEditor),
|
||||
[DriverTypeNames.Calculation] = typeof(Components.Shared.Uns.TagEditors.CalculationTagConfigEditor),
|
||||
// Keyed off SqlDriver.DriverTypeName (= "Sql") rather than DriverTypeNames.Sql: that shared
|
||||
// constant is deliberately absent until Task 11 wires the factory (DriverTypeNamesGuardTests
|
||||
// asserts bidirectional parity). Task 11 repoints all Sql keys onto DriverTypeNames.Sql.
|
||||
// Keyed off SqlDriver.DriverTypeName, which is value-identical to DriverTypeNames.Sql. The
|
||||
// note that used to sit here — "that shared constant is deliberately absent until Task 11 wires
|
||||
// the factory" — went stale when the constant landed; the key is correct either way.
|
||||
[SqlDriver.DriverTypeName] = typeof(Components.Shared.Uns.TagEditors.SqlTagConfigEditor),
|
||||
[DriverTypeNames.Mqtt] = typeof(Components.Shared.Uns.TagEditors.MqttTagConfigEditor),
|
||||
[DriverTypeNames.MTConnect] = typeof(Components.Shared.Uns.TagEditors.MTConnectTagConfigEditor),
|
||||
|
||||
Reference in New Issue
Block a user