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
-
- OPC UA NodeId string, e.g. ns=2;s=Channel.Device.Tag or i=1001. + OPC UA NodeId string, e.g. ns=2;s=Channel.Device.Tag or + i=1001. Use Browse to navigate the remote server.
+@if (_canOperate) +{ +
+ @if (_token == Guid.Empty) + { + + } + else + { + Browser open + + } + @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 */ } + } + } }