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