docs(plan): design for driver-typed tag editors in the UNS TagModal (F-uns-1 / #135)
Approved design: 6 new per-driver TagConfig editor components (Modbus/S7/AbCip/AbLegacy/TwinCAT/Focas) dispatched by the selected driver's DriverType via a TagConfigEditorMap + DynamicComponent; the 3 unmapped drivers keep the generic raw-JSON editor; no raw-JSON toggle on typed drivers. Editors reuse the drivers' enums + JSON property names (not razor markup); driver pages untouched. Pure FromJson/ToJson/Validate helpers are unit-tested (no bUnit); live verify needs a non-Galaxy driver added to docker-dev. AdminUI-only, no data-model change.
This commit is contained in:
@@ -0,0 +1,170 @@
|
|||||||
|
# Driver-typed Tag Editors in the UNS TagModal — Design
|
||||||
|
|
||||||
|
**Date:** 2026-06-09
|
||||||
|
**Task:** #135 (F-uns-1, deferred follow-up from the global-UNS feature)
|
||||||
|
**Status:** Approved (design); pending implementation plan
|
||||||
|
**Scope:** AdminUI only. No data-model / migration change.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Replace the single generic raw-`TagConfig`-JSON textarea in the global UNS
|
||||||
|
`TagModal` (`Components/Shared/Uns/TagModal.razor`) with **per-driver typed tag
|
||||||
|
editors** that dispatch on the selected driver's `DriverType`, so an operator
|
||||||
|
authoring an equipment-bound tag fills in driver-shaped fields
|
||||||
|
(Modbus register/address/byte-order, S7 address, AbCip tag path, …) instead of
|
||||||
|
hand-writing JSON.
|
||||||
|
|
||||||
|
This was specified at a high level in the global-UNS design
|
||||||
|
(`docs/plans/2026-06-08-global-uns-management-design.md`, "TagModal —
|
||||||
|
driver-typed, mirroring DriverEditRouter"). This document settles the
|
||||||
|
implementation decisions.
|
||||||
|
|
||||||
|
## Decisions locked during brainstorming
|
||||||
|
|
||||||
|
1. **New UNS editors; reuse contracts, not markup.** Build per-driver tag-config
|
||||||
|
editor components specifically for the TagModal. They **reuse each driver's
|
||||||
|
enums** (`ModbusRegion`, `ModbusDataType`, …) and emit the driver's
|
||||||
|
**camelCase JSON property names**, but do **not** reuse the driver pages'
|
||||||
|
razor markup. The existing driver edit pages are left untouched (their field
|
||||||
|
set differs — see §"Field mapping"). Lower risk, contained.
|
||||||
|
2. **All 6 typed editors this pass.** Modbus, S7, AbCip, AbLegacy, TwinCAT,
|
||||||
|
Focas each get a typed editor. OpcUaClient, Galaxy (`GalaxyMxGateway`), and
|
||||||
|
Historian.Wonderware keep the existing generic raw-JSON editor (they have no
|
||||||
|
tag editor in their driver pages either; Galaxy tags are browse-sourced).
|
||||||
|
3. **No raw-JSON escape hatch on typed drivers.** Typed drivers render typed
|
||||||
|
fields only — there is no per-modal "edit raw JSON" toggle. Consequence: each
|
||||||
|
typed editor must cover the driver's **full curated field set** — the same
|
||||||
|
fields the driver page's tag `EditTemplate` exposes (minus the fields the
|
||||||
|
TagModal owns at top level). The generic textarea remains only for the 3
|
||||||
|
unmapped drivers.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Components (`Components/Shared/Uns/TagEditors/`)
|
||||||
|
|
||||||
|
One Razor component per typed driver:
|
||||||
|
`ModbusTagConfigEditor.razor`, `S7TagConfigEditor.razor`,
|
||||||
|
`AbCipTagConfigEditor.razor`, `AbLegacyTagConfigEditor.razor`,
|
||||||
|
`TwinCATTagConfigEditor.razor`, `FocasTagConfigEditor.razor`.
|
||||||
|
|
||||||
|
**Component contract (convention, not a C# interface — these are components):**
|
||||||
|
|
||||||
|
```
|
||||||
|
[Parameter] public string? ConfigJson { get; set; }
|
||||||
|
[Parameter] public EventCallback<string> ConfigJsonChanged { get; set; }
|
||||||
|
```
|
||||||
|
|
||||||
|
Behaviour: on `OnParametersSet`, parse `ConfigJson` → a typed working model
|
||||||
|
(falling back to defaults on null/blank/malformed). Render the fields bound to
|
||||||
|
the model. On any field change, serialize the model → JSON and raise
|
||||||
|
`ConfigJsonChanged`. The TagModal binds it as `@bind-ConfigJson="_form.TagConfig"`.
|
||||||
|
|
||||||
|
**Pure mapping helpers.** Each editor's JSON↔model mapping lives in a pure,
|
||||||
|
static, side-effect-free helper (e.g. `ModbusTagConfigModel.FromJson(string?)`,
|
||||||
|
`.ToJson()`, `.Validate()`), so the (de)serialization and validation are
|
||||||
|
unit-testable in `AdminUI.Tests` **without bUnit** (which this project lacks).
|
||||||
|
The razor component is a thin shell over the helper.
|
||||||
|
|
||||||
|
### Dispatch map
|
||||||
|
|
||||||
|
A static `TagConfigEditorMap` — `IReadOnlyDictionary<string, Type>` keyed by
|
||||||
|
`DriverType` (case-insensitive), mirroring `DriverEditRouter._componentMap`:
|
||||||
|
|
||||||
|
```
|
||||||
|
["ModbusTcp"] = typeof(ModbusTagConfigEditor),
|
||||||
|
["S7"] = typeof(S7TagConfigEditor),
|
||||||
|
["AbCip"] = typeof(AbCipTagConfigEditor),
|
||||||
|
["AbLegacy"] = typeof(AbLegacyTagConfigEditor),
|
||||||
|
["TwinCat"] = typeof(TwinCATTagConfigEditor),
|
||||||
|
["Focas"] = typeof(FocasTagConfigEditor),
|
||||||
|
```
|
||||||
|
|
||||||
|
Drivers not in the map → the generic textarea. (Kept deliberately separate from
|
||||||
|
`DriverEditRouter._componentMap` to avoid coupling tag editing to driver-page
|
||||||
|
editing; both are short and stable.)
|
||||||
|
|
||||||
|
### TagModal integration — learning the driver type
|
||||||
|
|
||||||
|
- `IUnsTreeService.LoadTagDriversForEquipmentAsync` changes its return shape from
|
||||||
|
`(string DriverInstanceId, string Display)` to
|
||||||
|
`(string DriverInstanceId, string Display, string DriverType)`. (`UnsTreeService`
|
||||||
|
already loads `DriverInstance`; it just projects `DriverType` too.)
|
||||||
|
- The TagModal keeps a lookup `DriverInstanceId → DriverType` from those options
|
||||||
|
and resolves the **currently-selected** driver's type (the driver is chosen in
|
||||||
|
the modal's existing dropdown and can change while open).
|
||||||
|
- Render logic in the modal body, where the raw textarea is today:
|
||||||
|
- no driver selected → a muted "pick a driver to configure this tag" hint;
|
||||||
|
- selected type in `TagConfigEditorMap` → `<DynamicComponent Type="…"
|
||||||
|
Parameters="…">` passing `ConfigJson = _form.TagConfig` and a
|
||||||
|
`ConfigJsonChanged` `EventCallback<string>` that writes back to
|
||||||
|
`_form.TagConfig`;
|
||||||
|
- otherwise → the existing raw `InputTextArea` over `_form.TagConfig`.
|
||||||
|
- When the selected driver changes to a different `DriverType`, the editor swaps;
|
||||||
|
`_form.TagConfig` carries over (a mismatched blob is simply re-parsed to
|
||||||
|
defaults by the new editor, and the user re-enters fields).
|
||||||
|
|
||||||
|
## Field mapping (Tag columns vs TagConfig)
|
||||||
|
|
||||||
|
These stay **top-level TagModal fields** (already present): `Tag.Name`,
|
||||||
|
`Tag.DataType` (the OPC UA type), `Tag.AccessLevel`, `Tag.WriteIdempotent`,
|
||||||
|
`Tag.PollGroupId`.
|
||||||
|
|
||||||
|
`TagConfig` holds **only the driver-specific addressing/encoding fields** — the
|
||||||
|
driver page's tag `EditTemplate` fields minus the ones the modal owns:
|
||||||
|
|
||||||
|
| Driver | TagConfig fields (editor) | Excluded (lives elsewhere) |
|
||||||
|
|---|---|---|
|
||||||
|
| Modbus (`ModbusTcp`) | region, address, dataType (wire), byteOrder, bitIndex, stringLength | name → Tag.Name; writable → AccessLevel |
|
||||||
|
| S7 | address, stringLength | name; dataType → Tag.DataType; writable |
|
||||||
|
| AbCip | deviceHostAddress, tagPath | name; dataType; writable |
|
||||||
|
| AbLegacy | deviceHostAddress, address | name; dataType; writable |
|
||||||
|
| TwinCAT | deviceHostAddress, symbolPath | name; dataType; writable |
|
||||||
|
| Focas | deviceHostAddress, address | name; dataType; writable |
|
||||||
|
|
||||||
|
> Note: a driver's *wire* data type (e.g. `ModbusDataType` = how to decode the
|
||||||
|
> register) is distinct from `Tag.DataType` (the OPC UA type) and belongs in
|
||||||
|
> TagConfig; S7/AbCip/… have no separate wire type, so their data type is the
|
||||||
|
> top-level `Tag.DataType` only. The exact per-driver field list is finalized in
|
||||||
|
> the plan against each driver's tag DTO property names.
|
||||||
|
|
||||||
|
**Assumption / out of scope:** this assumes the runtime materialises a per-driver
|
||||||
|
`TagDefinition` for equipment-bound tags from the `Tag` columns + `TagConfig`
|
||||||
|
JSON. Confirming or wiring that runtime consumption is **not** part of this UI
|
||||||
|
feature — the editors faithfully capture the driver-shaped fields under the
|
||||||
|
driver's JSON names so the config is correct when consumed.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
Each editor helper exposes a light `Validate()` (required fields / simple ranges,
|
||||||
|
e.g. Modbus requires an address, AbCip requires a tag path) returning an error
|
||||||
|
string or null. The TagModal surfaces it as its existing inline error and blocks
|
||||||
|
Save. The current "valid JSON" guard (`JsonException`) still protects the generic
|
||||||
|
textarea path. **Server-side deploy validation remains authoritative.**
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
No bUnit in this project, so:
|
||||||
|
|
||||||
|
- **Unit tests** (`AdminUI.Tests`, xUnit + Shouldly): each editor's
|
||||||
|
`FromJson` / `ToJson` / `Validate` helper — round-trip fidelity, defaults on
|
||||||
|
null/blank/malformed, property-name correctness (the emitted JSON matches the
|
||||||
|
driver's DTO names), and validation pass/fail cases.
|
||||||
|
- **Live verify** (docker-dev `/run`): the rig's Main cluster currently has only
|
||||||
|
the Galaxy driver (no typed editor), so verifying a typed editor first needs a
|
||||||
|
**non-Galaxy driver** in the rig. Steps: add a Modbus driver instance to the
|
||||||
|
Main cluster, bind an equipment to it, open the equipment's `+ Tag` modal →
|
||||||
|
confirm the Modbus typed fields render, fill them, Save, and inspect the
|
||||||
|
persisted `TagConfig` JSON. Spot-check a second driver (AbCip) and confirm an
|
||||||
|
OpcUaClient/Galaxy-bound equipment still shows the generic textarea.
|
||||||
|
|
||||||
|
Gate = build clean + `dotnet test` green + the docker-dev live verify above.
|
||||||
|
|
||||||
|
## Constraints / non-goals
|
||||||
|
|
||||||
|
- AdminUI only; **no** Configuration entity or migration change.
|
||||||
|
- Driver edit pages are **not** modified (their inline tag editors stay).
|
||||||
|
- Not changing how the runtime consumes tags; not adding fields the driver pages
|
||||||
|
don't already expose.
|
||||||
|
- Git safety: stage by path (never `git add .`), never stage `sql_login.txt` /
|
||||||
|
`pki/`, never echo the gateway API key into a new tracked file, no force-push /
|
||||||
|
no `--no-verify`.
|
||||||
Reference in New Issue
Block a user