feat(centralui): tree rendering + lazy load + selection in OpcUaBrowserDialog
This commit is contained in:
@@ -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;"> </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; }
|
||||
}
|
||||
Reference in New Issue
Block a user