From 0f286a70b84269e586b18c758cea087e0d832999 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Mon, 8 Jun 2026 12:21:38 -0400 Subject: [PATCH] feat(uns): recursive UnsTree renderer --- .../Components/Shared/Uns/UnsTree.razor | 142 ++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Uns/UnsTree.razor diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Uns/UnsTree.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Uns/UnsTree.razor new file mode 100644 index 00000000..d0f08156 --- /dev/null +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Uns/UnsTree.razor @@ -0,0 +1,142 @@ +@* 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) +{ + @RenderNode(root, 0) +} + +@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 node (e.g. "+ Area" on a cluster, "+ Tag" on equipment). + [Parameter] public EventCallback OnAddChild { get; set; } + + /// Raised for the equipment-only "+ Virtual tag" action, kept distinct from OnAddChild ("+ Tag"). + [Parameter] public EventCallback OnAddVirtualTag { get; set; } + + /// Raised when the user edits a node (Area/Line/Equipment/Tag/VirtualTag). + [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 applied to visible children by DisplayName. + [Parameter] public string? Filter { get; set; } + + private RenderFragment RenderNode(UnsNode node, int depth) => __builder => + { + var indent = $"padding-left:{depth * 18}px"; + var hasExpander = node.Children.Count > 0 || node.HasLazyChildren; +
+ @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 (node.Expanded) + { + @foreach (var child in FilterChildren(node)) + { + @RenderNode(child, depth + 1) + } + } + }; + + 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: + + + + + break; + + case UnsNodeKind.Tag: + case UnsNodeKind.VirtualTag: + + + break; + } + }; + + private IEnumerable FilterChildren(UnsNode node) + { + var f = Filter?.Trim(); + if (string.IsNullOrEmpty(f)) + { + return node.Children; + } + + return node.Children.Where(c => + c.DisplayName.Contains(f, StringComparison.OrdinalIgnoreCase)); + } +}