cb0d17dabd
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.
209 lines
7.8 KiB
Plaintext
209 lines
7.8 KiB
Plaintext
@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();
|
|
}
|
|
}
|