c49fccbe0c
Driver collection editors (modal-per-row shared shell), resilience typed form, editable DB-backed LDAP->role map (global roles, live on next sign-in), and stale-comment/note cleanup. Roles intentionally global — no per-cluster permissions.
7.7 KiB
7.7 KiB
Design — Complete AdminUI deferred follow-ups
Date: 2026-05-29 Status: Approved (design); implementation plan to follow Author: Joseph Doherty (with Claude Code)
Background
The AdminUI carried a family of "deferred / Phase C.2 follow-up" notes. A prior change stripped the stale rendered roadmap banners from the cluster list pages. Three remaining note groups were investigated to decide what real work they hide:
- Group 1 — driver-page inline notes ("list-editor coming in a follow-up phase" for tags/devices/endpoints; "typed-form-ifying Polly is a follow-up"). → Real pending UI work.
- Group 2 — RoleGrants ("UI-driven editing of the mapping is deferred — it implies a config-reload mechanism that doesn't exist yet"). → Real work; half the infra already exists.
- Group 3 — source comments (F15 Razor migration, F16 FleetStatusHub bridge,
"Phase 4" identity section,
TODO(3.3/3.4)route collision). → ~90% stale; the referenced work already shipped (the F16 bridge is wired; the legacyDriverEdit.razorno longer exists). Only the Polly typed form is real, and it is already counted in Group 1.
Key facts established during exploration
- Driver-embedded tag/device lists in
DriverConfigJSON are the runtime source of truth. Driver factories deserialize them and poll exactly those rows; the canonicalTagtable is orthogonal (OPC UA browse-tree only, never read by drivers). So inline editors are meaningful, not redundant — editing them changes what the driver polls on the next publish/reinitialize. - Resilience already has a strongly-typed model:
DriverResilienceOptions(BulkheadMaxConcurrent,BulkheadMaxQueue,RecycleIntervalSeconds,CapabilityPolicies: {DriverCapability → (TimeoutSeconds, RetryCount, BreakerFailureThreshold)}) with tier A/B/C defaults viaGetTierDefaults(tier)and aDriverResilienceOptionsParser. The stored JSON is an override shape; null/absent keys fall back to tier defaults. - LDAP role map: the
LdapGroupRoleMappingentity + migration +ILdapGroupRoleMappingService(CRUD) already exist but are not wired into login.LdapAuthServicestill reads the static appsettingsGroupToRole(Dictionary<string,string>).RoleGrants.razoris read-only. - Testing: no bUnit. Established pattern = test
FromOptions/ToOptionsround-trips (xUnit + Shouldly inAdminUI.Tests) and services with in-memory EF (Configuration.Tests).
Decisions
- Scope: full build — all real follow-ups in Groups 1 & 2, plus Group 3 comment cleanup.
- List-editor UX: modal-per-row with a shared shell component.
- LDAP reload semantics: DB-backed, live on the user's next sign-in
(per-login DB query; no restart, no new infra). appsettings
GroupToRolebecomes a bootstrap fallback layer. - Roles are GLOBAL. No cluster-level permissions / no per-cluster enforcement
(explicitly chosen for simplicity, reversing an earlier cluster-scoping answer).
Every
LdapGroupRoleMappingrow isIsSystemWide=true,ClusterId=null.
Workstreams
WS1 — Driver collection editors (modal-per-row + shared shell)
- New generic
CollectionEditor<TRow>component inComponents/Shared/Drivers/: compact read-only table +[+ Add]/ per-rowEdit/Delete, and a Bootstrap modal editing a working copy of a row (commit on modal-Save, discard on Cancel). Parameters:List<TRow> Items(bound), header fragment, read-only-cells fragment, modal-body fragment,NewRowfactory, optionalValidatedelegate. - Each driver page swaps its read-only
<pre>for aCollectionEditorsupplying its own columns + modal fields. Edits mutate the in-memoryList<T>already in the page'sFormModel; the page's existing Save serializes it intoDriverConfig— no new persistence path. - Coverage: tags (Modbus, AbCip, AbLegacy, TwinCAT, S7, FOCAS); devices (AbCip, AbLegacy, TwinCAT, FOCAS); endpoints (OpcUaClient).
- Errors/validation: required fields, duplicate Name within list, driver-specific address format; delete confirm; list mutates only on valid commit.
- Testing: per-driver
NewRowfactories +Validatemethods unit-tested directly; existing*FormSerializationTestsextended for add/remove via the form model. Modal interaction verified manually via/run.
WS2 — Resilience typed form
- Replace the textarea in
DriverResilienceSection.razorwith a typed form bound to a new mutableResilienceFormModel(all fields nullable; null = tier default): bulkhead concurrent/queue, recycle interval, and an 8-capability grid (Read, Write, Discover, Subscribe, Probe, AlarmSubscribe, AlarmAcknowledge, HistoryRead) of (timeout / retry / breaker-threshold). FromJson/ToJsonemit only non-null overrides (blank →null, preserving the current "null = tier defaults" contract). The section gains aDriverTierparameter; each driver page passes its known tier soGetTierDefaults(tier)renders as placeholders. A collapsible "raw JSON" view remains as escape hatch.- Errors: non-negative / sane-range numeric validation; emitted JSON must
re-parse cleanly through
DriverResilienceOptionsParser. - Testing:
ResilienceFormModelround-trip tests inAdminUI.Tests— blank→null, partial-override-preserved, emit→parse-back compatibility.
WS3 — Editable LDAP→role map (DB-backed, global, live on next sign-in)
RoleGrants.razor→ full CRUD overLdapGroupRoleMappingvia the existingILdapGroupRoleMappingService. Global only:IsSystemWide=true,ClusterId=null; no cluster UI. Fields: LDAP group,AdminRole(ConfigViewer/ConfigEditor/FleetAdmin), notes. A group may carry several roles (multiple rows). Edit page gated to FleetAdmin (add a minimal FleetAdmin authorization policy; confirm existing role-policy plumbing during plan-writing).- Wire the service into
LdapAuthService: at login → resolve groups →GetByGroupsAsync(indexed) → map roles → merge appsettingsGroupToRoleas a fallback layer (used when no DB row covers a group). Edits take effect on the user's next sign-in. DB rows authoritative + editable; appsettings entries shown read-only as "fallback." - Errors: DB unreachable at login → catch, log, fall back to appsettings;
login never blocks. CRUD: no duplicate
(LdapGroup, Role); group/role required. - Testing: extend
LdapGroupRoleMappingServiceTests(in-memory EF) for CRUD + dedupe; newRoleMapperoverloadMap(groups, dbRows, fallbackDict)unit-tested for merge + fallback precedence + DB-error fallback.
WS4 — Cleanup (runs last, after the features exist)
- Delete stale comments:
FleetStatusHub.cs("passive channel / until the bridge lands"),EndpointRouteBuilderExtensions.cs(F15),DriverIdentitySection.razor("Phase 4 / generic DriverEdit"),DriverEditRouter.razor+DriverTypePicker.razor(TODO(3.3/3.4)+ the "falls back to legacy DriverEdit" path — verify & clean, legacy file is gone), and updateDriverResilienceSection.razor's comment. - Strip rendered notes now true: per-driver "list-editor coming in a follow-up phase" notes, the OpcUaClient endpoint note, the resilience "typed-form-ifying Polly is a follow-up" note, and the RoleGrants "UI-driven editing is deferred" note.
Cross-cutting
- No DB schema change —
LdapGroupRoleMappingmigration already applied;DriverConfig/ResilienceConfigcolumns unchanged. - Definition of done: build clean +
dotnet testgreen + a/runpass exercising the modal editors and role-map CRUD. - Suggested sequence: WS1 shared shell + Modbus tags as proof → remaining drivers → WS2 → WS3 → WS4.