diff --git a/docs/plans/2026-05-22-execution-tree-node-modal-design.md b/docs/plans/2026-05-22-execution-tree-node-modal-design.md new file mode 100644 index 0000000..4ce989b --- /dev/null +++ b/docs/plans/2026-05-22-execution-tree-node-modal-design.md @@ -0,0 +1,91 @@ +# Execution-Tree Node Detail Modal (Design) + +**Date:** 2026-05-22 +**Status:** Validated — ready for implementation planning. + +## Problem + +On the Central UI execution-chain tree page (`/audit/execution-tree`, the +`ParentExecutionId` feature's Task 10), each node represents one execution and +shows a small inline summary. The only interaction is the short-id link, which +navigates away to `/audit/log?executionId=…`. There is no way to inspect an +execution's actual audit rows without leaving the tree. + +## Decision + +Double-clicking a tree node opens a **modal** showing that execution's audit +rows. The modal mirrors the `/audit/log` detail experience: a list of the +execution's rows, and clicking a row reveals that row's full field/payload +detail — the exact content the Audit Log drilldown drawer shows. + +Resolved during brainstorming: +- **Modal content** — the execution's audit rows, with per-row full detail. +- **Multi-row executions** — list the rows; clicking one shows its detail. A + single-row execution opens straight to the detail view. +- **Trigger** — double-click anywhere on the node. The short-id link keeps its + single-click navigation to the Audit Log grid (unchanged). + +### Considered and rejected + +- **Reuse `AuditDrilldownDrawer` directly.** The drawer renders one + `AuditEvent` by design; bending it into a list-or-detail hybrid is more + invasive to a well-tested component than a purpose-built modal. +- **Inline expansion under the node.** The user asked for a modal, and an + inline panel inside the recursive tree fights the existing expand/collapse + toggle and is visually messy. + +## Components + +| Component | Change | +|---|---| +| `AuditEventDetail.razor` | **New.** The single-`AuditEvent` field/payload/drill-in-button block, extracted verbatim from `AuditDrilldownDrawer`'s body. | +| `AuditDrilldownDrawer.razor` | **Modified.** Keeps its offcanvas chrome + close button; its body becomes ``. The one refactor with regression risk — existing drawer bUnit + Playwright tests guard it. | +| `ExecutionDetailModal.razor` (+ `.razor.cs` + `.razor.css`) | **New.** A custom Bootstrap modal — hand-rolled `modal` / `modal-backdrop` markup, Blazor-toggled, no component framework (the same way `AuditDrilldownDrawer` hand-rolls `offcanvas`). | +| `ExecutionTree.razor` / `.razor.cs` | **Modified.** `@ondblclick` on the node body invokes a new `OnNodeActivated` `EventCallback`; recursive child instances re-raise it upward so the event bubbles to the root. | +| `ExecutionTreePage.razor` / `.razor.cs` | **Modified.** Hosts one `ExecutionDetailModal`; wires the tree's `OnNodeActivated` to open it. | + +No database, repository, or service changes — purely Central UI. The +`IAuditLogQueryService.QueryAsync` method already filters by `ExecutionId`; the +modal reuses it (no new service method). + +## Data flow + +1. Double-click a node → `ExecutionTree` invokes `OnNodeActivated(node.ExecutionId)`. +2. The event bubbles up the recursive `ExecutionTree` instances to + `ExecutionTreePage`. +3. The page opens `ExecutionDetailModal` with the `ExecutionId`. +4. The modal calls `IAuditLogQueryService.QueryAsync(new AuditLogQueryFilter(ExecutionId: id), new AuditLogPaging(PageSize: 100))` → `IReadOnlyList`. +5. Render by row count: + - **≥ 2 rows** — a compact row list (kind / status / target / time, each row a button); clicking a row swaps to its `` with a "← Back to rows" control. + - **1 row** — opens straight to the detail view. + - **0 rows** — a stub execution; a friendly empty state. +6. Close via the X button, the backdrop, or Esc. + +The list rows are full `AuditEvent` objects (that is what `QueryAsync` returns), +so the list→detail transition needs no second fetch. + +## Error handling + +- A `QueryAsync` failure surfaces an inline error inside the modal ("Couldn't + load this execution's rows") and never tears down the SignalR circuit — + mirroring the tree page's existing `try/catch` degrade-gracefully pattern. +- An empty result renders the friendly empty state, not an error. + +## Testing + +- **bUnit** — `ExecutionTree` raises `OnNodeActivated` on `@ondblclick` and + bubbles it through a nested instance; `ExecutionDetailModal` list renders from + a fake query service, row click → detail, 1-row jump-straight, 0-row empty + state, close; `AuditEventDetail` renders the field block; the existing + `AuditDrilldownDrawer` tests stay green after the body extraction. +- **Playwright** — on `/audit/execution-tree`, double-click a node → modal opens + → (multi-row) row list → click a row → detail → close. Uses a seeded chain. +- `frontend-design` skill for the modal markup/CSS — clean corporate aesthetic, + custom Blazor + Bootstrap, no component frameworks. + +## Constraints + +- Central UI only — no DB / repository / service-contract changes. +- Custom Blazor + Bootstrap; no component frameworks. +- The short-id link's single-click navigation to `/audit/log?executionId=…` is + unchanged.