docs(phase6): design — AdminUI typed editors, pickers & UX (destaled scope, #7 deferred)
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
# Still-Pending Phase 6 — AdminUI typed editors, pickers & UX — design
|
||||
|
||||
> **Status:** approved 2026-06-16 (scope = "Everything achievable", AskUserQuestion). Parent roadmap:
|
||||
> `docs/plans/2026-06-15-stillpending-backlog-design.md` (Phase 6). Branch `feat/stillpending-phase-6-adminui`
|
||||
> off master `156aa900`. Phases 0–5 already shipped. **Execution is deferred to AFTER a /compact** — this doc +
|
||||
> the implementation plan are the durable handoff.
|
||||
|
||||
## Goal
|
||||
|
||||
Close the AdminUI authoring/UX gaps in `stillpending.md` §4 so the `/uns` Tag authoring experience is complete:
|
||||
typed editors for every driver, historized fields as first-class controls, native-alarm AVEVA opt-out, the
|
||||
existing address pickers reachable from the Tag modal, UNS-tree deletes for the top node kinds, and
|
||||
create-new-script from the inline virtual-tag panel. **NO EF migration; Razor proven only by live `/run` (no
|
||||
bUnit) — the pure-C# `*TagConfigModel` / service / builder cores ARE unit-tested in
|
||||
`tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/`.**
|
||||
|
||||
## Grounding that reshaped the scope (the roadmap was materially stale)
|
||||
|
||||
1. **`DriverAttributeInfo.IsAlarm` already exists** (`Core.Abstractions/DriverAttributeInfo.cs:55`, plus
|
||||
`IsArray`/`ArrayDim`). Item 4 needs **no Commons contract change** — only the picker→modal pre-fill wiring.
|
||||
2. **All 9 driver address-picker bodies + builders already exist** under
|
||||
`Components/Shared/Drivers/Pickers/` (`{Modbus,S7,AbCip,AbLegacy,TwinCAT,FOCAS,Galaxy,OpcUaClient,
|
||||
HistorianWonderware}AddressPickerBody.razor` + the `*AddressBuilder.cs` with unit tests under
|
||||
`tests/.../AdminUI.Tests/Pickers/`). They are **wired into the per-cluster driver pages**
|
||||
(`ModbusDriverPage.razor` …). Only the **Galaxy** picker is also wired into the `/uns` `TagModal.razor`
|
||||
(`:115`). So the roadmap's "only OpcUaClient + Galaxy pickers shipped" is **stale**; Item 5's residue is
|
||||
just wiring a "Build address" affordance into the typed editors in the Tag modal.
|
||||
3. **`ctx.SetVirtualTag(...)` completions/hover are already wired** — `SetVirtualTag` is already in
|
||||
`ScriptAnalysisService.TryGetTagPathLiteral`. Item 8's residue is only **create-new-script** from the
|
||||
inline panel.
|
||||
4. **`HistorizeToAveva` is a first-class field on *scripted* alarms** (`ScriptedAlarm` entity +
|
||||
`Phase7Composer.ScriptedAlarmInfo`; the ScriptedAlarmModal toggle already ships). Item 3's residue is the
|
||||
**native-alarm** side: the `TagConfig.alarm` object's AVEVA opt-out + its UI.
|
||||
5. **No bUnit anywhere** — the AdminUI test project (`AdminUI.Tests`) is pure C# (model `FromJson/ToJson/
|
||||
Validate` tests, `UnsTreeService*` against in-memory EF, `ScriptAnalysis/*`, `Pickers/*AddressBuilder`).
|
||||
|
||||
## In scope — the genuinely-remaining work (Item 7 deferred)
|
||||
|
||||
### A. Typed editors for OpcUaClient + Historian.Wonderware (Item 1) — `standard`
|
||||
- `TagConfigEditorMap` (`Uns/TagEditors/TagConfigEditorMap.cs`) maps only ModbusTcp/S7/AbCip/AbLegacy/TwinCat/
|
||||
Focas → a typed editor; OpcUaClient/Galaxy/Historian.Wonderware fall back to raw JSON.
|
||||
- Add `OpcUaClientTagConfigModel` + `HistorianWonderwareTagConfigModel` (pure `FromJson`/`ToJson`/`Validate`,
|
||||
preserve-unknown-keys, camelCase) + their razor editor shells under `Components/Shared/Uns/TagEditors/`,
|
||||
register both in `TagConfigEditorMap` + `TagConfigValidator`. Mirror the Modbus template.
|
||||
- **Unit-test the two models** (mirror `ModbusTagConfigModelTests`); editors proven by live `/run`.
|
||||
|
||||
### B. `isHistorized` / `historianTagname` as first-class fields (Item 2) — `standard`
|
||||
- Today they ride the raw `TagConfig` JSON (the server already reads them — `docs/Historian.md`).
|
||||
- Surface a shared **"Historize this tag" checkbox + optional "Historian tagname" textbox** in the Tag modal,
|
||||
driver-agnostic (applies to any equipment tag). Thread them through the typed-editor models' `FromJson`/
|
||||
`ToJson` (and the raw-JSON fallback) so a toggle round-trips byte-parity and is preserved across edits.
|
||||
- **Unit-test** the field round-trip on the models.
|
||||
|
||||
### C. Native-alarm `HistorizeToAveva` opt-out (Item 3) — `standard`
|
||||
- The native-alarm carrier is `TagConfig.alarm` (`Phase7Composer.EquipmentTagAlarmInfo`). Add a
|
||||
`historizeToAveva` (`bool?`, default-historize when absent — mirror the scripted-alarm wire contract) to the
|
||||
native-alarm authoring surface + the alarm sub-model, and **confirm the server honors it for native alarms**
|
||||
in `HistorianAdapterActor` (the scripted path already gates on `is not false`). NO migration (rides
|
||||
`TagConfig`). Unit-test the model field; live `/run` the toggle.
|
||||
|
||||
### D. Galaxy picker pre-fills alarm fields from `IsAlarm` (Item 4) — `small`
|
||||
- `DriverAttributeInfo.IsAlarm` already exists. When the Galaxy address picker (wired in `TagModal.razor:115`)
|
||||
returns a selection whose attribute `IsAlarm == true`, pre-fill the native-alarm sub-form (from C) with
|
||||
sensible defaults so the operator can author the alarm in one pass. Wiring only; live `/run`.
|
||||
|
||||
### E. Address pickers reachable from the typed editors (Item 5) — `small`
|
||||
- The picker bodies + builders exist and are wired on the driver pages. Add a **"Build address"** button to each
|
||||
protocol-driver typed editor (Modbus/S7/AbCip/AbLegacy/TwinCAT/Focas) in the Tag modal that opens the
|
||||
matching `*AddressPickerBody` inside the shared `DriverTagPicker` shell (exactly as Galaxy is wired at
|
||||
`TagModal.razor:115`), writing the built address back into the editor's address field. Reuses existing bodies
|
||||
— no new picker logic. Live `/run`.
|
||||
|
||||
### F. UNS-tree Delete for Cluster / Enterprise (Item 6) — `standard`
|
||||
- `GlobalUns.ConfirmDeleteAsync` (`:306`) guards Enterprise/Cluster as "not yet available"; `UnsTree`'s
|
||||
`RenderActions` hides Delete for those kinds. Add `IUnsTreeService.DeleteClusterAsync(clusterId, rowVersion)`
|
||||
(+ `ClusterRow.RowVersion`, queried in `LoadStructureAsync`), the `UnsTree` Delete action for Cluster, and the
|
||||
`GlobalUns` branch. **Enterprise** = delete all clusters under that label (Enterprise is a tree label, not a
|
||||
DB entity). **Cluster cascade is the danger** (it owns driver-instances + areas→lines→equipment→tags) — match
|
||||
the area/line pattern (refuse-if-children OR ordered cascade) and require RowVersion concurrency. **Service-
|
||||
layer unit-tested** against in-memory EF (mirror `UnsTreeService*` tests); the tree action is live `/run`.
|
||||
|
||||
### G. Create-new-script from the inline virtual-tag panel (Item 8) — `small`
|
||||
- `VirtualTagModal.razor` (`:66`) shows the inline Monaco editor only for an **existing** bound script. Add a
|
||||
**"New script"** branch (when `ScriptId` is empty) → `IUnsTreeService.CreateScriptAsync(...)` (new Script row,
|
||||
generated id, blank source) → bind it → expand the inline editor. **Service-layer unit-tested**; UX live `/run`.
|
||||
|
||||
## Out of scope / deferred
|
||||
|
||||
- **Item 7 — Hosts page per-driver-instance rows: DEFERRED** (`Hosts.razor` is per-Akka-member; its own comment
|
||||
says driver-instance child rows "land with F7", which is not yet implemented → blocked on runtime work, not an
|
||||
AdminUI gap). Recorded as a Phase-6b/F7 follow-up.
|
||||
- Phase-4b driver structural work; Phases 7–8.
|
||||
|
||||
## Architecture / risk notes
|
||||
|
||||
- **Independence:** A+B are coupled (same model/editor files — do as one pass). C+D are coupled (D pre-fills C's
|
||||
form). E, F, G are independent. F + G are the service-layer (in-memory-EF-testable) items; A,B,C are
|
||||
model-layer testable; D,E are pure Razor wiring (live-`/run` only).
|
||||
- **No EF migration, no Commons contract change** (IsAlarm/IsArray already on `DriverAttributeInfo`; historized
|
||||
+ native-alarm-historize ride `TagConfig` JSON; `ScriptedAlarm.HistorizeToAveva` column already exists).
|
||||
- The `*TagConfigModel` preserve-unknown-keys contract (per `docs/plans/2026-06-09-driver-typed-tag-editors-
|
||||
design.md`) MUST hold so adding historized fields never drops a driver's existing keys.
|
||||
|
||||
## Testing & verification
|
||||
|
||||
- TDD red→green (xUnit + Shouldly) for every pure core: the 2 new `*TagConfigModel`s (A), the historized-field
|
||||
round-trip (B), the native-alarm-historize model field (C), `DeleteClusterAsync` (F), `CreateScriptAsync` (G)
|
||||
— all in `AdminUI.Tests` (model tests + `UnsTreeService` in-memory-EF tests). **No bUnit.**
|
||||
- `dotnet build` clean + `dotnet test` green before merge.
|
||||
- **Live `/run` (agent-driven, login disabled on the local rig):** drive `http://localhost:9200` — author an
|
||||
OpcUaClient/Historian tag via the new typed editor; toggle Historize + set a historian tagname and confirm
|
||||
round-trip on re-open + deploy; toggle native-alarm AVEVA opt-out; open a protocol-driver "Build address"
|
||||
picker and use a built address; delete a (disposable) Cluster; create-new-script from the vtag panel.
|
||||
**NOTE (Phase-5 lesson): `dotnet test` of the integration suite is network-sandboxed — run live checks against
|
||||
the docker-dev rig accordingly; bring up the rig with `docker compose -f docker-dev/docker-compose.yml up -d
|
||||
central-1`.** Final integration review before merge.
|
||||
|
||||
## Hard constraints (carried from the parent roadmap)
|
||||
|
||||
- **NO Configuration entity / EF migration.** No Commons contract change. Stage by path — never `git add .`.
|
||||
Never stage `sql_login.txt`, `src/Server/.../Host/pki/`, `pending.md`, `current.md`,
|
||||
`docker-dev/docker-compose.yml`, `stillpending.md`. Never echo or commit secrets. No force-push, no
|
||||
`--no-verify`. Finish = merge to master + push.
|
||||
Reference in New Issue
Block a user