diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Dialogs/OpcUaBrowserDialog.razor b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Dialogs/OpcUaBrowserDialog.razor
index 381397d1..04f0042d 100644
--- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Dialogs/OpcUaBrowserDialog.razor
+++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Dialogs/OpcUaBrowserDialog.razor
@@ -22,7 +22,19 @@
}
-
+ @if (_rootNodes.Count == 0 && _failure is null)
+ {
+
Loading…
+ }
+ else
+ {
+
+ @foreach (var node in _rootNodes)
+ {
+
+ }
+
+ }
@@ -56,6 +68,28 @@
private string _manualNodeId = "";
private BrowseFailure? _failure;
private string _failureMessage = "";
+ private List _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? 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();
diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Dialogs/TreeRow.razor b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Dialogs/TreeRow.razor
new file mode 100644
index 00000000..5ae9038b
--- /dev/null
+++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Dialogs/TreeRow.razor
@@ -0,0 +1,53 @@
+@using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Protocol
+
+ OnToggle.InvokeAsync(Node)">
+ @if (Node.HasChildren)
+ {
+ @(Node.Expanded ? "▼" : "▶")
+ }
+ else
+ {
+
+ }
+
+
+ @if (Node.NodeClass == BrowseNodeClass.Variable)
+ {
+ OnSelect.InvokeAsync(Node)"
+ @ondblclick="() => OnSelect.InvokeAsync(Node)">
+ @Node.DisplayName (@Node.NodeId)
+
+ }
+ else
+ {
+ @Node.DisplayName
+ }
+
+ @if (Node.Loading)
+ {
+ loading…
+ }
+
+ @if (Node.Expanded && Node.Children is not null)
+ {
+
+ @foreach (var child in Node.Children)
+ {
+
+ }
+ @if (Node.Truncated)
+ {
+ - Results truncated — use manual entry if your tag isn't listed.
+ }
+
+ }
+
+
+@code {
+ [Parameter] public OpcUaBrowserDialog.TreeNode Node { get; set; } = default!;
+ [Parameter] public EventCallback OnToggle { get; set; }
+ [Parameter] public EventCallback OnSelect { get; set; }
+ [Parameter] public string? SelectedNodeId { get; set; }
+}