@using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Protocol @using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management @using ZB.MOM.WW.ScadaBridge.CentralUI.Services @inject IBrowseService BrowseService @if (_isVisible) { } @code { [Parameter] public string SiteId { get; set; } = ""; /// /// Name of the site-local data connection. Serves both as the modal-header /// display label AND as the routing key for the browse round-trip — the /// site's DataConnectionManagerActor indexes its children by /// connection name (no id-keyed lookup at the site). /// [Parameter] public string ConnectionName { get; set; } = ""; [Parameter] public string? InitialNodeId { get; set; } [Parameter] public EventCallback OnSelected { get; set; } [Parameter] public EventCallback OnCancelled { get; set; } private bool _isVisible; private string? _selectedNodeId; 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; } } private string _runtimeSiteId = ""; private string _runtimeConnectionName = ""; public async Task ShowAsync(string siteId, string connectionName, string? initialNodeId) { // Snapshot at click time. Razor parameter binding propagates on the next // render, which would race the immediate LoadRootAsync below. _runtimeSiteId = siteId; _runtimeConnectionName = connectionName; _isVisible = true; _manualNodeId = initialNodeId ?? ""; _selectedNodeId = initialNodeId; await LoadRootAsync(); } private async Task LoadRootAsync() { _failure = null; _rootNodes = new(); StateHasChanged(); var result = await BrowseService.BrowseChildrenAsync(_runtimeSiteId, _runtimeConnectionName, 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(_runtimeSiteId, _runtimeConnectionName, 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; } // Task 17: map each BrowseFailureKind to a friendly UI message. The raw // failure.Message is surfaced verbatim only for ServerError (which carries // the OPC UA SDK's own Bad_* text) and as the default fallback for any // future failure kind added without a UI mapping. private void SetFailure(BrowseFailure failure) { _failure = failure; _failureMessage = failure.Kind switch { BrowseFailureKind.ConnectionNotFound => "Connection no longer exists at the site.", BrowseFailureKind.ConnectionNotConnected => "OPC UA session not connected — retry shortly or use manual entry.", BrowseFailureKind.NotBrowsable => "This connection does not support browsing.", BrowseFailureKind.Timeout => "Browse timed out — the server may be slow. Try again or enter the node id manually.", BrowseFailureKind.ServerError => $"OPC UA server error: {failure.Message}", _ => failure.Message }; StateHasChanged(); } private Task RetryRootLoad() => LoadRootAsync(); private void UseManual() { _selectedNodeId = _manualNodeId.Trim(); } private async Task Confirm() { _isVisible = false; if (!string.IsNullOrWhiteSpace(_selectedNodeId)) await OnSelected.InvokeAsync(_selectedNodeId!); } private async Task Cancel() { _isVisible = false; await OnCancelled.InvokeAsync(); } }