9.2 KiB
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). Branchfeat/stillpending-phase-6-adminuioff master156aa900. 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)
DriverAttributeInfo.IsAlarmalready exists (Core.Abstractions/DriverAttributeInfo.cs:55, plusIsArray/ArrayDim). Item 4 needs no Commons contract change — only the picker→modal pre-fill wiring.- 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.cswith unit tests undertests/.../AdminUI.Tests/Pickers/). They are wired into the per-cluster driver pages (ModbusDriverPage.razor…). Only the Galaxy picker is also wired into the/unsTagModal.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. ctx.SetVirtualTag(...)completions/hover are already wired —SetVirtualTagis already inScriptAnalysisService.TryGetTagPathLiteral. Item 8's residue is only create-new-script from the inline panel.HistorizeToAvevais a first-class field on scripted alarms (ScriptedAlarmentity +Phase7Composer.ScriptedAlarmInfo; the ScriptedAlarmModal toggle already ships). Item 3's residue is the native-alarm side: theTagConfig.alarmobject's AVEVA opt-out + its UI.- No bUnit anywhere — the AdminUI test project (
AdminUI.Tests) is pure C# (modelFromJson/ToJson/ Validatetests,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(pureFromJson/ToJson/Validate, preserve-unknown-keys, camelCase) + their razor editor shells underComponents/Shared/Uns/TagEditors/, register both inTagConfigEditorMap+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
TagConfigJSON (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 ahistorizeToAveva(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 inHistorianAdapterActor(the scripted path already gates onis not false). NO migration (ridesTagConfig). Unit-test the model field; live/runthe toggle.
D. Galaxy picker pre-fills alarm fields from IsAlarm (Item 4) — small
DriverAttributeInfo.IsAlarmalready exists. When the Galaxy address picker (wired inTagModal.razor:115) returns a selection whose attributeIsAlarm == 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
*AddressPickerBodyinside the sharedDriverTagPickershell (exactly as Galaxy is wired atTagModal.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'sRenderActionshides Delete for those kinds. AddIUnsTreeService.DeleteClusterAsync(clusterId, rowVersion)(+ClusterRow.RowVersion, queried inLoadStructureAsync), theUnsTreeDelete action for Cluster, and theGlobalUnsbranch. 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 (mirrorUnsTreeService*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 (whenScriptIdis 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.razoris 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-
/runonly). - No EF migration, no Commons contract change (IsAlarm/IsArray already on
DriverAttributeInfo; historized- native-alarm-historize ride
TagConfigJSON;ScriptedAlarm.HistorizeToAvevacolumn already exists).
- native-alarm-historize ride
- The
*TagConfigModelpreserve-unknown-keys contract (perdocs/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 inAdminUI.Tests(model tests +UnsTreeServicein-memory-EF tests). No bUnit. dotnet buildclean +dotnet testgreen before merge.- Live
/run(agent-driven, login disabled on the local rig): drivehttp://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 testof the integration suite is network-sandboxed — run live checks against the docker-dev rig accordingly; bring up the rig withdocker 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 stagesql_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.