@* Recursive, read-only UNS browse-tree renderer. Modelled on DriverBrowseTree: per-node indent, expand chevron and Bootstrap/theme styling. This component owns no state and calls no service — it reads node.Expanded/Loading/Error/ Children and raises callbacks; the host page owns expansion + lazy loading. *@ @using ZB.MOM.WW.OtOpcUa.AdminUI.Uns @foreach (var root in Roots.Where(r => VisibleUnder(r, false))) { @RenderNode(root, 0, false) } @code { /// The top-level UNS nodes to render (typically the enterprise roots). [Parameter, EditorRequired] public IReadOnlyList Roots { get; set; } = default!; /// Raised for the primary "add child" action of a structural node (e.g. "+ Area" on a cluster, "+ Equipment" on a line). [Parameter] public EventCallback OnAddChild { get; set; } /// Raised when the user edits a node (Area/Line). [Parameter] public EventCallback OnEdit { get; set; } /// Raised when the user deletes a node (Area/Line/Equipment/Tag/VirtualTag). [Parameter] public EventCallback OnDelete { get; set; } /// Raised when the user toggles a node's expansion; the host owns the resulting state + lazy load. [Parameter] public EventCallback OnToggleExpand { get; set; } /// /// Optional case-insensitive substring filter on DisplayName. A node is shown when it matches, /// sits under a matching ancestor, or has a matching descendant, and the path to matches /// auto-expands. Lazy tag children are only considered once their equipment has been loaded. /// [Parameter] public string? Filter { get; set; } private RenderFragment RenderNode(UnsNode node, int depth, bool ancestorMatched) => __builder => { var indent = $"padding-left:{depth * 18}px"; var hasExpander = node.Children.Count > 0 || node.HasLazyChildren; // Under an active filter the matched path auto-expands, so the chevron and the child block // follow `childrenShown` (derived from the filter) rather than the user's node.Expanded flag. var matched = ancestorMatched || SelfMatches(node); var visibleChildren = HasFilter ? VisibleChildren(node, matched).ToList() : node.Children; var childrenShown = HasFilter ? visibleChildren.Count > 0 : node.Expanded;
@if (hasExpander) { } else { } @node.DisplayName @if (node.ChildCount > 0) { @node.ChildCount } @RenderActions(node)
@if (node.Expanded && node.Loading) {
Loading…
} else if (node.Expanded && node.Error is not null) {
@node.Error
} else if (childrenShown) { @foreach (var child in visibleChildren) { @RenderNode(child, depth + 1, matched) } } }; private RenderFragment RenderActions(UnsNode node) => __builder => { switch (node.Kind) { case UnsNodeKind.Enterprise: // No actions on the enterprise root. break; case UnsNodeKind.Cluster: ⚙ settings break; case UnsNodeKind.Area: break; case UnsNodeKind.Line: break; case UnsNodeKind.Equipment: Open break; } }; /// Whether a non-empty filter is currently active. private bool HasFilter => !string.IsNullOrWhiteSpace(Filter); /// Whether the node's own DisplayName matches the active filter (case-insensitive substring). private bool SelfMatches(UnsNode node) { var f = Filter?.Trim(); return !string.IsNullOrEmpty(f) && node.DisplayName.Contains(f, StringComparison.OrdinalIgnoreCase); } /// /// Whether the node should appear under the active filter: it self-matches, sits under a matched /// ancestor, or has a (loaded) descendant that matches. Unloaded lazy tag children can't be /// inspected, so an unexpanded equipment matches on its own name only. The structural tree is /// bounded, so this recursive walk is cheap. /// private bool VisibleUnder(UnsNode node, bool ancestorMatched) { if (!HasFilter) { return true; } if (ancestorMatched || SelfMatches(node)) { return true; } return node.Children.Any(c => VisibleUnder(c, false)); } /// /// The children to render under the active filter: the whole child list when the node (or an /// ancestor) matched, otherwise only the branches that still contain a match. /// private IEnumerable VisibleChildren(UnsNode node, bool matched) => matched ? node.Children : node.Children.Where(c => VisibleUnder(c, false)); }