feat(dcl+ui): rename BrowseOpcUaNode -> ConnectionName-keyed; implement site handler + dialog failure mapping

- BrowseOpcUaNodeCommand: int DataConnectionId -> string ConnectionName
  (site DataConnectionManagerActor indexes children by name; CentralUI
  already has the connection name in scope via the dropdown — no extra
  plumbing across the trust boundary).
- IOpcUaBrowseService / OpcUaBrowseService: parameter renamed accordingly.
- OpcUaBrowserDialog: collapse the duplicate ConnectionName parameters
  (display label and routing key are the same string).
- Task 10: DataConnectionManagerActor forwards BrowseOpcUaNodeCommand to
  its child by name (owns ConnectionNotFound); DataConnectionActor adds
  the receive across all three lifecycle states (Connecting / Connected
  / Reconnecting) and maps adapter outcomes to BrowseFailureKind
  (NotBrowsable / ConnectionNotConnected / Timeout / ServerError).
- Task 17: SetFailure in OpcUaBrowserDialog implements the full
  BrowseFailureKind switch with friendly UI messages.
- Tests: DataConnectionManagerBrowseHandlerTests covers ConnectionNotFound,
  NotBrowsable, success, and ConnectionNotConnectedException paths.
This commit is contained in:
Joseph Doherty
2026-05-28 12:09:43 -04:00
parent 6999aedc60
commit d285174597
7 changed files with 313 additions and 13 deletions
@@ -57,7 +57,12 @@
@code {
[Parameter] public string SiteId { get; set; } = "";
[Parameter] public int DataConnectionId { 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; }
@@ -105,7 +110,7 @@
_rootNodes = new();
StateHasChanged();
var result = await BrowseService.BrowseChildrenAsync(SiteId, DataConnectionId, parentNodeId: null);
var result = await BrowseService.BrowseChildrenAsync(SiteId, ConnectionName, parentNodeId: null);
if (result.Failure is not null)
{
SetFailure(result.Failure);
@@ -130,7 +135,7 @@
{
node.Loading = true;
StateHasChanged();
var result = await BrowseService.BrowseChildrenAsync(SiteId, DataConnectionId, node.NodeId);
var result = await BrowseService.BrowseChildrenAsync(SiteId, ConnectionName, node.NodeId);
node.Loading = false;
if (result.Failure is not null)
@@ -155,12 +160,23 @@
_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.
// 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.Message;
_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();
@@ -21,17 +21,17 @@ public interface IOpcUaBrowseService
{
/// <summary>
/// Enumerates the immediate children of an OPC UA node on the live server
/// backing <paramref name="dataConnectionId"/> at <paramref name="siteId"/>.
/// backing <paramref name="connectionName"/> at <paramref name="siteId"/>.
/// Pass <c>null</c> for <paramref name="parentNodeId"/> to browse from the
/// server root (ObjectsFolder).
/// </summary>
/// <param name="siteId">The target site identifier.</param>
/// <param name="dataConnectionId">Id of the site-local data connection to browse against.</param>
/// <param name="connectionName">Name of the site-local data connection to browse against — the site's <c>DataConnectionManagerActor</c> indexes its children by name.</param>
/// <param name="parentNodeId">Node to browse, or <c>null</c> to browse from the server root.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task<BrowseOpcUaNodeResult> BrowseChildrenAsync(
string siteId,
int dataConnectionId,
string connectionName,
string? parentNodeId,
CancellationToken cancellationToken = default);
}
@@ -37,7 +37,7 @@ public sealed class OpcUaBrowseService : IOpcUaBrowseService
/// <inheritdoc/>
public async Task<BrowseOpcUaNodeResult> BrowseChildrenAsync(
string siteId,
int dataConnectionId,
string connectionName,
string? parentNodeId,
CancellationToken cancellationToken = default)
{
@@ -56,7 +56,7 @@ public sealed class OpcUaBrowseService : IOpcUaBrowseService
{
return await _communication.BrowseOpcUaNodeAsync(
siteId,
new BrowseOpcUaNodeCommand(dataConnectionId, parentNodeId),
new BrowseOpcUaNodeCommand(connectionName, parentNodeId),
cancellationToken);
}
catch (TimeoutException ex)