Files
lmxopcua/docs/plans/2026-06-16-stillpending-phase-6-adminui-design.md
T

9.2 KiB
Raw Blame History

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 05 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 wiredSetVirtualTag 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 78.

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 *TagConfigModels (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.