# Instance Picker — area-tree combobox for Flow Debugger and Debug View (design) **Date:** 2026-09-08 **Status:** approved (user, 2026-09-08: "Approve as designed"; scope: Flow Debugger + Debug View) **Supersedes:** the flat `` of every instance at the chosen site. A site with a few hundred instances across nested areas makes that list useless. Replace it with a **combobox**: a text field the operator can type an instance name into, whose dropdown is the site's **area tree** (all areas collapsed on open) with instances as leaves, so the operator can either drill down or search. Alternatives rejected: - A modal picker like Create Instance's *Choose…* button (`TemplatePickerDialog`): consistent, but two clicks and nothing typeable in the field. The user asked for a combobox. - A native `` typeahead: search only, no area drill-down. ## 2. Component — `InstancePicker` `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/InstancePicker.razor` (+ `.razor.cs`, `.razor.css`). Bootstrap-only, no third-party UI, no new JavaScript. **Field.** A `form-control form-control-sm` text input with a chevron button, sized like the sibling selects. Closed, it shows the selected instance's `UniqueName` (empty when nothing is selected, placeholder *Search or browse instances…*). Focus or typing opens the panel; the chevron toggles it. `role="combobox"`, `aria-expanded`, `aria-controls` → the panel id; `Disabled` disables both. **Panel.** A `position-absolute` dropdown under the field (z-index above page content, `max-height: 40vh`, scroll) containing the existing `TreeView`: - **Forest** = the site's areas nested by `ParentAreaId` (Topology's shape), each area's instances as leaves after its child areas, and instances with no area listed at the root **after** the root areas. Ordinal-ignore-case sort on both. - **All areas collapsed** when the panel opens with an empty filter (no `InitiallyExpanded`, no `StorageKey`). - Clicking an area row toggles it (the TreeView chevron; the label click also toggles). Clicking an instance row selects it and closes the panel. - An empty tree renders *No instances at this site* / *No instances match*. **Search.** The field text is a case-insensitive `Contains` on `UniqueName`. With a non-empty filter the forest is rebuilt to the matching instances plus their ancestor areas, and the tree is force-expanded (`ExpandAll` after the rebuild, as the template picker does). Clearing the text restores the full forest and collapses everything (`CollapseAll`). **Keyboard.** `Enter` selects the first visible matching instance (document order); `ArrowDown` from the field moves focus into the tree (its roving-tabindex arrow navigation; the picker passes `Selectable` + `SelectedKeyChanged` so `Enter`/`Space` on an instance row selects it and on an area row toggles it — the same handler the mouse uses); `Escape` closes and restores the field text to the current selection. Tabbing out does **not** close the panel (a correct focus-out needs `relatedTarget`, i.e. JS interop, which this design avoids); the next click anywhere lands on the backdrop and closes it, which is acceptable. **Outside click.** A transparent fixed backdrop (the same `tv-ctx-overlay` pattern `TreeView` uses for its context menu) sits under the panel while open and closes it on click. No JS interop. **Parameters.** | Parameter | Type | Notes | |---|---|---| | `Areas` | `IReadOnlyList` | the site's areas (flat, `ParentAreaId` links) | | `Instances` | `IReadOnlyList` | the site's instances | | `SelectedInstanceId` / `SelectedInstanceIdChanged` | `int` / `EventCallback` | two-way bind, `0` = none | | `Disabled` | `bool` | | | `ItemLabel` | `Func?` | row + field text; default `UniqueName`. Debug View passes `$"{i.UniqueName} ({i.State})"` | | `DataTestPrefix` | `string` | `flow-instance` / `debug-instance` → `{prefix}-picker`, `{prefix}-input`, `{prefix}-toggle`, `{prefix}-panel`, rows `{prefix}-row` with `data-instance-id` | | `Placeholder` | `string` | default *Search or browse instances…* | A parent that changes `Areas`/`Instances` (site change) resets the field text and closes the panel; a parent that changes `SelectedInstanceId` (URL / localStorage restore) updates the field text. **Pure builder.** `InstanceTreeBuilder.Build(areas, instances, filter)` → `IReadOnlyList` (`Components/Shared/InstanceTreeNode.cs`, `InstanceTreeBuilder.cs`; record `InstanceTreeNode(string Key, string Label, Area? Area, Instance? Instance)` with a read-only `Children` property the builder fills, keys `a:{id}` / `i:{id}`, `IsInstance => Instance != null`). Orphaned areas (parent id pointing nowhere) and orphaned instances (area id pointing nowhere) are treated as roots so a bad row can never hide a subtree; a `ParentAreaId` cycle is unreachable from any root and simply does not render (guarded against looping). `Topology.razor` keeps its own richer `TopoNode` — not refactored (YAGNI). ## 3. Page wiring - **Flow Debugger** (`FlowDebugger.razor` / `.razor.cs`): `LoadInstancesAsync` also loads `_areas` via `ITemplateEngineRepository.GetAreasBySiteIdAsync(siteId)`; the `` becomes the picker with `ItemLabel` = `UniqueName (State)`, `Disabled="@(_connected || _instancesLoading)"`, `DataTestPrefix="debug-instance"`, `SelectedInstanceIdChanged="OnInstancePicked"`. `OnInstanceSelectionChanged`, `?instanceId=` and localStorage reconnect unchanged. ## 4. Security None new. Site scoping stays on the site select (both pages already re-check `SiteScope.IsSiteAllowedAsync` on site change); the picker only ever receives the instances of the already-allowed site. Labels are rendered as text (Blazor-encoded). ## 5. Tests - `CentralUI.Tests/Shared/InstanceTreeBuilderTests.cs`: nesting, root instances after root areas, sort, orphaned area → root, filter keeps ancestors and drops non-matching branches, empty inputs. - `CentralUI.Tests/Shared/InstancePickerTests.cs` (bUnit): opens collapsed (no instance rows visible under a collapsed area); typing reveals matches expanded; clearing collapses; click selects + closes + raises `SelectedInstanceIdChanged`; `Enter` picks the first match; `Escape` restores text; `Disabled`; parent-driven `SelectedInstanceId` updates the field; `ItemLabel`. - `FlowDebuggerPageTests`: the five `flow-instance-select` `.Change("7")` call sites go through a test helper that opens the picker and clicks the row (`data-instance-id`). - Debug View bUnit tests: none drive the select today; add one page test that a picker row click sets the instance and enables *Connect*. - Playwright: `FlowDebuggerTests` (instance pick at line ~209), `DebugViewTests`, `DebugViewTreeTests` switch from `