refactor(browse): rename OPC-UA browse service + dialog to protocol-agnostic
IOpcUaBrowseService/OpcUaBrowseService -> IBrowseService/BrowseService, OpcUaBrowserDialog -> NodeBrowserDialog, and neutralize 'Browse OPC UA' UI strings to 'Browse'. Updates DI, InstanceConfigure, TestBindingsDialog, TreeRow, BindingTester, and tests. 574 CentralUI tests green.
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
@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)
|
||||
{
|
||||
<div class="modal show d-block" tabindex="-1" role="dialog" style="background-color: rgba(0,0,0,0.5);">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Browse — @ConnectionName</h5>
|
||||
<button type="button" class="btn-close" @onclick="Cancel"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
@if (_failure is not null)
|
||||
{
|
||||
<div class="alert alert-danger">
|
||||
@_failureMessage
|
||||
<button class="btn btn-sm btn-outline-danger ms-2" @onclick="RetryRootLoad">Retry</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="opcua-browser-tree">
|
||||
@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 />
|
||||
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">Manual node id:</span>
|
||||
<input class="form-control" @bind="_manualNodeId" placeholder="ns=2;s=..." />
|
||||
<button class="btn btn-outline-secondary" @onclick="UseManual" disabled="@string.IsNullOrWhiteSpace(_manualNodeId)">Use</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<span class="me-auto text-muted">Selected: <code>@(_selectedNodeId ?? "(none)")</code></span>
|
||||
<button class="btn btn-secondary" @onclick="Cancel">Cancel</button>
|
||||
<button class="btn btn-primary" @onclick="Confirm" disabled="@string.IsNullOrWhiteSpace(_selectedNodeId)">Select</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter] public string SiteId { get; set; } = "";
|
||||
/// <summary>
|
||||
/// 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 <c>DataConnectionManagerActor</c> indexes its children by
|
||||
/// connection name (no id-keyed lookup at the site).
|
||||
/// </summary>
|
||||
[Parameter] public string ConnectionName { get; set; } = "";
|
||||
[Parameter] public string? InitialNodeId { get; set; }
|
||||
[Parameter] public EventCallback<string> 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<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; }
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user