docs(components): reference docs batch 4/4 — ManagementService, CLI, Transport, CentralUI, TraefikProxy, TreeView
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
# Tree View
|
||||
|
||||
`TreeView<TItem>` is a generic, reusable Blazor Server component that renders any tree-shaped data as an expandable/collapsible hierarchy with ARIA roles, optional guide lines, single or checkbox selection, and session-persistent expansion state.
|
||||
|
||||
## Overview
|
||||
|
||||
The component lives at `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/TreeView.razor` alongside its scoped stylesheet `TreeView.razor.css`. It is data-agnostic: the caller supplies the data source, a children accessor, a key function, and a `RenderFragment<TItem>` for node content — the component owns only the structural chrome (indentation, toggle, guide lines, ARIA attributes, selection highlight, context menu positioning).
|
||||
|
||||
Active uses within the Central UI:
|
||||
|
||||
- **Data Connections page** (`DataConnections.razor`) — two-level Site → Connection tree with a kebab action menu per node and search-based dimming.
|
||||
- **Topology page** (`Topology.razor`) — three-level Site → Area → Instance tree with inline rename, context menus, and `StorageKey = "topology-tree"` expansion persistence.
|
||||
- **`TemplateFolderTree`** (`TemplateFolderTree.razor`) — a domain-specific wrapper that projects `TemplateFolder` / `Template` entities into `TemplateTreeNode` items and delegates to `TreeView<TemplateTreeNode>`. Consumed by the Templates browser page (Single mode, click-to-navigate) and the Transport Export wizard (Checkbox mode, bulk template selection).
|
||||
|
||||
## Key Concepts
|
||||
|
||||
### Generic node typing
|
||||
|
||||
`@typeparam TItem` means the component imposes no node base class. The consumer brings its own model — a record, a class, an interface — and wires four `[EditorRequired]` delegates that teach the component how to navigate it. The `TemplateTreeNode` class in `Components/Shared/TemplateTreeNode.cs` is the shared adapter type for template/folder hierarchies; the Data Connections and Topology pages use local `record` types.
|
||||
|
||||
### Expansion state
|
||||
|
||||
Expanded-node keys are stored in `_expandedKeys` (`HashSet<string>`). Keys are stringified via `KeyStr(object) => key.ToString()!` for consistency with `sessionStorage`, which stores them as a JSON array when `StorageKey` is set. On first render the component reads `sessionStorage` via `treeviewStorage.load`; on every toggle it writes back via `treeviewStorage.save`. `InitiallyExpanded` applies only when no persisted state exists (or `StorageKey` is null). The two sources are always unioned — a `RevealNode` call before the async storage read completes is not clobbered.
|
||||
|
||||
### Selection modes
|
||||
|
||||
`TreeViewSelectionMode` (defined in `TreeViewSelectionMode.cs`) controls how nodes are selected:
|
||||
|
||||
```csharp
|
||||
public enum TreeViewSelectionMode
|
||||
{
|
||||
Single, // Default. Clicking node content fires SelectedKeyChanged with the node key.
|
||||
Checkbox, // Renders a tri-state checkbox per node. Folder check state is aggregated
|
||||
// from descendant leaves; only leaf keys enter SelectedKeys.
|
||||
}
|
||||
```
|
||||
|
||||
In `Single` mode the component uses `SelectedKey` / `SelectedKeyChanged` (two-way binding on a single `object?`). In `Checkbox` mode it uses `SelectedKeys` / `SelectedKeysChanged` (`HashSet<object>`). Checking a folder selects or deselects all of its descendant leaf keys. The `indeterminate` checkbox property is set via JS interop (`treeviewStorage.setIndeterminate`) after every render because Blazor does not bind `input.indeterminate` natively.
|
||||
|
||||
### Context menu
|
||||
|
||||
When `ContextMenu` is non-null, right-clicking any row suppresses the browser default and positions a Bootstrap `dropdown-menu show` div at the cursor coordinates using `position: fixed`. An invisible overlay behind the menu dismisses it on click-outside; Escape also dismisses it. The menu receives the `TItem` of the right-clicked node, so the consumer's fragment can branch on node type.
|
||||
|
||||
## Architecture
|
||||
|
||||
The component is a single `@typeparam` `.razor` file with a private `void RenderNode(TItem item, int depth)` local function that recurses the tree at render time — no intermediate view model is built inside the component. Every `<li>` carries `@key="key"` so Blazor can diff the list efficiently.
|
||||
|
||||
`IJSRuntime` is injected for two purposes: reading/writing `sessionStorage` for expansion persistence, and setting `input.indeterminate` for tri-state checkboxes. Both call sites guard `JSDisconnectedException` so a disconnected circuit never throws out of the lifecycle methods.
|
||||
|
||||
The public surface the caller can invoke via `@ref`:
|
||||
|
||||
```csharp
|
||||
public bool IsExpanded(object key); // Whether the given key is currently expanded.
|
||||
public void ExpandAll(); // Expand every branch node; persists if StorageKey set.
|
||||
public void CollapseAll(); // Collapse every node; clears persisted state.
|
||||
public async Task RevealNode(object key, bool select = false);
|
||||
// Expands all ancestors of the given key. Optionally selects the node.
|
||||
// No-op if the key does not exist in the current tree.
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
The Data Connections page binds a two-level Site → Connection tree with `StorageKey`, single selection, and a context menu for Edit and Delete actions on connection nodes:
|
||||
|
||||
```razor
|
||||
<TreeView @ref="_tree" TItem="DcTreeNode" Items="_treeRoots"
|
||||
ChildrenSelector="n => n.Children"
|
||||
HasChildrenSelector="n => n.Children.Count > 0"
|
||||
KeySelector="n => (object)n.Key"
|
||||
StorageKey="data-connections-tree"
|
||||
Selectable="true"
|
||||
SelectedKey="_selectedKey"
|
||||
SelectedKeyChanged="OnTreeNodeSelected">
|
||||
<NodeContent Context="node">
|
||||
@if (node.Kind == DcNodeKind.Site)
|
||||
{
|
||||
<span class="tv-label fw-semibold">@node.Label</span>
|
||||
<span class="badge bg-secondary ms-1">@node.Children.Count</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="tv-label">@node.Label</span>
|
||||
<span class="badge bg-info ms-2">@node.Connection!.Protocol</span>
|
||||
}
|
||||
</NodeContent>
|
||||
<ContextMenu Context="node">
|
||||
@if (node.Kind == DcNodeKind.DataConnection)
|
||||
{
|
||||
<button class="dropdown-item"
|
||||
@onclick='() => NavigationManager.NavigateTo($"/design/connections/{node.Connection!.Id}/edit")'>
|
||||
Edit
|
||||
</button>
|
||||
<div class="dropdown-divider"></div>
|
||||
<button class="dropdown-item text-danger"
|
||||
@onclick="() => DeleteConnection(node.Connection!)">
|
||||
Delete
|
||||
</button>
|
||||
}
|
||||
</ContextMenu>
|
||||
<EmptyContent>
|
||||
<span class="text-muted fst-italic">No sites configured.</span>
|
||||
</EmptyContent>
|
||||
</TreeView>
|
||||
|
||||
@code {
|
||||
record DcTreeNode(string Key, string Label, DcNodeKind Kind, List<DcTreeNode> Children,
|
||||
int? SiteId = null, DataConnection? Connection = null);
|
||||
enum DcNodeKind { Site, DataConnection }
|
||||
|
||||
private TreeView<DcTreeNode>? _tree;
|
||||
private object? _selectedKey;
|
||||
|
||||
private void OnTreeNodeSelected(object? key) => _selectedKey = key;
|
||||
}
|
||||
```
|
||||
|
||||
The `TemplateFolderTree` wrapper demonstrates `Checkbox` mode, where `SelectedKeys` / `SelectedKeysChanged` drive bulk template selection in the Transport Export wizard:
|
||||
|
||||
```razor
|
||||
<TreeView @ref="_tree" TItem="TemplateTreeNode"
|
||||
Items="_visibleRoots"
|
||||
ChildrenSelector="n => n.Children"
|
||||
HasChildrenSelector="n => n.Children.Count > 0"
|
||||
KeySelector="n => (object)n.Key"
|
||||
Selectable="@(SelectionMode == TreeViewSelectionMode.Single)"
|
||||
SelectionMode="SelectionMode"
|
||||
SelectedKeys="SelectedKeys"
|
||||
SelectedKeysChanged="SelectedKeysChanged"
|
||||
InitiallyExpanded="@(_initiallyExpanded)"
|
||||
StorageKey="@StorageKey">
|
||||
<NodeContent Context="node">
|
||||
<span class="tv-glyph"><i class="bi @(NodeGlyph(node))"></i></span>
|
||||
<span class="tv-label @(node.Children.Count > 0 ? "fw-semibold" : "")"
|
||||
title="@node.Name">@node.Name</span>
|
||||
</NodeContent>
|
||||
</TreeView>
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
All `[Parameter]` properties on `TreeView<TItem>`. Parameters marked **required** carry `[EditorRequired]` and must be supplied; omitting them produces a build warning.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `Items` | `IReadOnlyList<TItem>` | — | **Required.** Root-level nodes to render. |
|
||||
| `ChildrenSelector` | `Func<TItem, IReadOnlyList<TItem>>` | — | **Required.** Returns the ordered children of a node. |
|
||||
| `HasChildrenSelector` | `Func<TItem, bool>` | — | **Required.** Returns `true` for branch nodes. Determines whether the expand toggle is rendered. |
|
||||
| `KeySelector` | `Func<TItem, object>` | — | **Required.** Unique stable key per node. Used for expansion tracking, selection, and `@key` diffing. |
|
||||
| `NodeContent` | `RenderFragment<TItem>` | — | **Required.** Render fragment for node label content. Receives the `TItem`; responsible for all domain-specific markup (glyphs, labels, badges). |
|
||||
| `EmptyContent` | `RenderFragment?` | `null` | Shown when `Items` is empty or null. |
|
||||
| `ContextMenu` | `RenderFragment<TItem>?` | `null` | Right-click menu content. Receives the right-clicked node. If null, right-click is not intercepted. If the fragment renders nothing for a node type, the browser default is used. |
|
||||
| `IndentPx` | `int` | `24` | Pixels of left padding added per depth level via inline `style`. |
|
||||
| `ShowGuideLines` | `bool` | `true` | Adds `tv-guides` CSS class to the root `<ul>`, enabling the depth guide lines drawn by a `linear-gradient` pseudo-element in `TreeView.razor.css`. |
|
||||
| `InitiallyExpanded` | `Func<TItem, bool>?` | `null` | Predicate applied on first load (before storage is read) to expand matching nodes. Overridden by persisted state when `StorageKey` is set and storage contains prior data. |
|
||||
| `StorageKey` | `string?` | `null` | Browser `sessionStorage` key for expansion persistence (`treeview:{StorageKey}`). When null, expansion is in-memory only. |
|
||||
| `Selectable` | `bool` | `false` | Enables click-to-select on node content. Clicking the expand toggle never changes selection. |
|
||||
| `SelectedKey` | `object?` | `null` | Currently selected node key for `Single` mode. Supports two-way binding (`@bind-SelectedKey`). |
|
||||
| `SelectedKeyChanged` | `EventCallback<object?>` | — | Fires when selection changes in `Single` mode. Also fires on `RevealNode(..., select: true)`. Fires with `null` when the selected key disappears from the tree. |
|
||||
| `SelectedCssClass` | `string` | `"bg-primary bg-opacity-10"` | CSS class(es) applied to the selected node's row div in addition to `tv-selected`. |
|
||||
| `SelectionMode` | `TreeViewSelectionMode` | `Single` | Switches between single-key selection and tri-state checkbox selection. |
|
||||
| `SelectedKeys` | `HashSet<object>?` | `null` | Set of currently selected leaf keys for `Checkbox` mode. |
|
||||
| `SelectedKeysChanged` | `EventCallback<HashSet<object>>` | — | Fires with the updated set when any checkbox is toggled in `Checkbox` mode. Always fires with a fresh `HashSet` reference. |
|
||||
|
||||
### CSS utility classes in `NodeContent`
|
||||
|
||||
The scoped stylesheet defines layout slots that `NodeContent` fragments should use for consistent alignment:
|
||||
|
||||
| Class | Purpose |
|
||||
|---|---|
|
||||
| `tv-glyph` | 20 px flex slot for a Bootstrap Icon (`<i class="bi bi-…">`). |
|
||||
| `tv-label` | `flex: 1 1 auto; min-width: 0` — primary text with ellipsis overflow. |
|
||||
| `tv-meta` | `margin-left: auto` — right-aligned badges or trailing controls. |
|
||||
| `tv-kebab` | Opt-in hidden-by-default "more actions" slot; revealed on row hover. |
|
||||
|
||||
## Dependencies & Interactions
|
||||
|
||||
- **Bootstrap 5** — all state visuals use Bootstrap utility classes and CSS variables (`--bs-tertiary-bg`, `--bs-border-color`, `--bs-primary-rgb`). No third-party Blazor component frameworks.
|
||||
- **Bootstrap Icons** — static files served from `wwwroot/lib/bootstrap-icons/`; referenced once in `MainLayout.razor`. `NodeContent` fragments use `<i class="bi bi-…">` inside the `tv-glyph` slot.
|
||||
- **`IJSRuntime`** — injected for `treeviewStorage.load` / `treeviewStorage.save` (expansion persistence) and `treeviewStorage.setIndeterminate` (checkbox tri-state). The JS helpers live in the CentralUI's shared JS bundle.
|
||||
- **`TemplateFolderTree`** (`Components/Shared/TemplateFolderTree.razor`) — a domain-specific wrapper around `TreeView<TemplateTreeNode>` that handles folder/template tree construction, text filtering, and `ExtraTemplateChildren` injection. Consumers that need the template hierarchy use `TemplateFolderTree`; they do not wire `TreeView<TemplateTreeNode>` directly.
|
||||
- **`TemplateTreeNode` / `TemplateTreeNodeKind`** (`Components/Shared/TemplateTreeNode.cs`) — the shared node model used by `TemplateFolderTree` and its callers. Folder keys are prefixed `f:`, template keys `t:`, composition keys `c:`.
|
||||
- **Data Connections page** (`Components/Pages/Design/DataConnections.razor`) — binds `TreeView<DcTreeNode>` directly with a local two-level record type.
|
||||
- **Topology page** (`Components/Pages/Deployment/Topology.razor`) — binds `TreeView` for the Site → Area → Instance hierarchy; calls `ExpandAll`, `CollapseAll`, and `RevealNode` via `@ref`.
|
||||
- **Central UI component** — see [./CentralUI.md](./CentralUI.md) for the broader Blazor Server application context.
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Tree View design specification](../requirements/Component-TreeView.md)
|
||||
- [Central UI](./CentralUI.md)
|
||||
- [Template Engine](./TemplateEngine.md)
|
||||
Reference in New Issue
Block a user