diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/OpcUaClientDriverPage.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/OpcUaClientDriverPage.razor
index 16b68beb..a11e841e 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/OpcUaClientDriverPage.razor
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/OpcUaClientDriverPage.razor
@@ -58,7 +58,8 @@ else
CurrentAddress="@_pickedAddress"
OnPickAddress="@OnAddressPicked">
+ CurrentAddressChanged="@((s) => _pickedAddress = s)"
+ GetConfigJson="@SerializeCurrentConfig" />
@* Endpoint *@
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/OpcUaClientAddressPickerBody.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/OpcUaClientAddressPickerBody.razor
index 3e5d7334..1d04cf1c 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/OpcUaClientAddressPickerBody.razor
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/OpcUaClientAddressPickerBody.razor
@@ -1,22 +1,57 @@
-@* Static OPC UA Client address builder: NodeId free text → verbatim.
- Live browse deferred to a follow-up phase. *@
-
-
- Note: Live OPC UA node browse is deferred to a follow-up phase.
- Enter the NodeId string manually below.
-
+@* OPC UA Client address picker:
+ 1. Manual NodeId entry (always available)
+ 2. (DriverOperator-gated) Browse remote server with live tree, lazy expand. *@
+@implements IAsyncDisposable
+@using Microsoft.AspNetCore.Authorization
+@using ZB.MOM.WW.OtOpcUa.AdminUI.Browsing
+@using ZB.MOM.WW.OtOpcUa.Commons.Browsing
+@inject IBrowserSessionService BrowserService
+@inject AuthenticationStateProvider AuthState
+@inject IAuthorizationService AuthorizationService
+@if (_canOperate)
+{
+
+ @if (_token == Guid.Empty)
+ {
+
+ @if (_opening) { }
+ Browse remote server
+
+ }
+ else
+ {
+ Browser open
+
+ Close
+
+ }
+ @if (_openError is not null) { @TruncatedError() }
+
+
+ @if (_token != Guid.Empty)
+ {
+
+
+
+ }
+}
+
Result:
@_built
@@ -25,14 +60,47 @@
@code {
[Parameter] public string CurrentAddress { get; set; } = "";
[Parameter] public EventCallback CurrentAddressChanged { get; set; }
+ [Parameter, EditorRequired] public Func GetConfigJson { get; set; } = () => "{}";
private string _nodeId = "";
private string _built = "";
+ private Guid _token = Guid.Empty;
+ private bool _opening;
+ private bool _canOperate;
+ private string? _openError;
- protected override void OnInitialized()
+ protected override async Task OnInitializedAsync()
{
+ var auth = await AuthState.GetAuthenticationStateAsync();
+ var authResult = await AuthorizationService.AuthorizeAsync(auth.User, null, "DriverOperator");
+ _canOperate = authResult.Succeeded;
_built = _nodeId;
- _ = CurrentAddressChanged.InvokeAsync(_built);
+ await CurrentAddressChanged.InvokeAsync(_built);
+ }
+
+ private async Task OpenBrowseAsync()
+ {
+ _opening = true; _openError = null; StateHasChanged();
+ try
+ {
+ var json = GetConfigJson() ?? "{}";
+ var result = await BrowserService.OpenAsync("OpcUaClient", json, default);
+ if (result.Ok) _token = result.Token;
+ else _openError = result.Message;
+ }
+ finally { _opening = false; StateHasChanged(); }
+ }
+
+ private async Task CloseBrowseAsync()
+ {
+ var t = _token; _token = Guid.Empty; StateHasChanged();
+ if (t != Guid.Empty) await BrowserService.CloseAsync(t);
+ }
+
+ private async Task OnTreeSelectAsync(BrowseNode node)
+ {
+ _nodeId = node.NodeId;
+ await OnChangedAsync();
}
private async Task OnChangedAsync()
@@ -40,4 +108,15 @@
_built = _nodeId;
await CurrentAddressChanged.InvokeAsync(_built);
}
+
+ private string TruncatedError() =>
+ _openError is null ? "" : (_openError.Length > 60 ? _openError[..60] + "…" : _openError);
+
+ public async ValueTask DisposeAsync()
+ {
+ if (_token != Guid.Empty)
+ {
+ try { await BrowserService.CloseAsync(_token); } catch { /* circuit teardown */ }
+ }
+ }
}