feat(centralui): tree rendering + lazy load + selection in OpcUaBrowserDialog

This commit is contained in:
Joseph Doherty
2026-05-28 11:58:59 -04:00
parent 0b4b4c02f6
commit 1d2e2c1614
2 changed files with 148 additions and 2 deletions
@@ -22,7 +22,19 @@
}
<div class="opcua-browser-tree">
<!-- Task 16 fills this in -->
@if (_rootNodes.Count == 0 && _failure is null)
{
<em class="text-muted">Loading…</em>
}
else
{
<ul class="list-unstyled mb-0">
@foreach (var node in _rootNodes)
{
<TreeRow Node="node" OnToggle="ToggleAsync" OnSelect="Select" SelectedNodeId="@_selectedNodeId" />
}
</ul>
}
</div>
<hr />
@@ -56,6 +68,28 @@
private string _manualNodeId = "";
private BrowseFailure? _failure;
private string _failureMessage = "";
private List<TreeNode> _rootNodes = new();
public sealed class TreeNode
{
public TreeNode(string nodeId, string displayName, BrowseNodeClass nodeClass, bool hasChildren)
{
NodeId = nodeId;
DisplayName = displayName;
NodeClass = nodeClass;
HasChildren = hasChildren;
}
public string NodeId { get; }
public string DisplayName { get; }
public BrowseNodeClass NodeClass { get; }
public bool HasChildren { get; }
public List<TreeNode>? Children { get; set; } // null = not loaded yet
public bool Expanded { get; set; }
public bool Loading { get; set; }
public bool Truncated { get; set; }
}
public async Task ShowAsync()
{
@@ -67,7 +101,66 @@
private async Task LoadRootAsync()
{
// Task 16
_failure = null;
_rootNodes = new();
StateHasChanged();
var result = await BrowseService.BrowseChildrenAsync(SiteId, DataConnectionId, parentNodeId: null);
if (result.Failure is not null)
{
SetFailure(result.Failure);
return;
}
_rootNodes = result.Children.Select(c => new TreeNode(c.NodeId, c.DisplayName, c.NodeClass, c.HasChildren)).ToList();
StateHasChanged();
}
private async Task ToggleAsync(TreeNode node)
{
if (!node.HasChildren) return;
if (node.Expanded)
{
node.Expanded = false;
return;
}
if (node.Children is null)
{
node.Loading = true;
StateHasChanged();
var result = await BrowseService.BrowseChildrenAsync(SiteId, DataConnectionId, node.NodeId);
node.Loading = false;
if (result.Failure is not null)
{
SetFailure(result.Failure);
return;
}
node.Children = result.Children
.Select(c => new TreeNode(c.NodeId, c.DisplayName, c.NodeClass, c.HasChildren))
.ToList();
node.Truncated = result.Truncated;
}
node.Expanded = true;
}
private void Select(TreeNode node)
{
if (node.NodeClass != BrowseNodeClass.Variable) return;
_selectedNodeId = node.NodeId;
_manualNodeId = node.NodeId;
}
// NOTE: Task 17 will replace this body with the full BrowseFailureKind switch
// that maps each failure kind to a friendly UI message.
private void SetFailure(BrowseFailure failure)
{
_failure = failure;
_failureMessage = failure.Message;
}
private Task RetryRootLoad() => LoadRootAsync();
@@ -0,0 +1,53 @@
@using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Protocol
<li>
<span style="cursor: @(Node.HasChildren ? "pointer" : "default");" @onclick="() => OnToggle.InvokeAsync(Node)">
@if (Node.HasChildren)
{
<span>@(Node.Expanded ? "▼" : "▶")</span>
}
else
{
<span style="display:inline-block; width:1em;">&nbsp;</span>
}
</span>
@if (Node.NodeClass == BrowseNodeClass.Variable)
{
<a href="javascript:void(0)"
class="@(Node.NodeId == SelectedNodeId ? "fw-bold text-primary" : "")"
@onclick="() => OnSelect.InvokeAsync(Node)"
@ondblclick="() => OnSelect.InvokeAsync(Node)">
@Node.DisplayName <small class="text-muted">(@Node.NodeId)</small>
</a>
}
else
{
<span class="text-muted">@Node.DisplayName</span>
}
@if (Node.Loading)
{
<em class="ms-2 text-muted">loading…</em>
}
@if (Node.Expanded && Node.Children is not null)
{
<ul class="list-unstyled ms-4">
@foreach (var child in Node.Children)
{
<TreeRow Node="child" OnToggle="OnToggle" OnSelect="OnSelect" SelectedNodeId="@SelectedNodeId" />
}
@if (Node.Truncated)
{
<li><small class="text-warning">Results truncated — use manual entry if your tag isn't listed.</small></li>
}
</ul>
}
</li>
@code {
[Parameter] public OpcUaBrowserDialog.TreeNode Node { get; set; } = default!;
[Parameter] public EventCallback<OpcUaBrowserDialog.TreeNode> OnToggle { get; set; }
[Parameter] public EventCallback<OpcUaBrowserDialog.TreeNode> OnSelect { get; set; }
[Parameter] public string? SelectedNodeId { get; set; }
}