@* FOCAS address picker: 1. Static parameter group + parameter ID → axis:5 (always available). 2. (DriverOperator-gated, when the driver supports online discovery) Browse the FixedTree device model captured by the universal DiscoveryDriverBrowser. FOCAS opens take a few seconds longer than other drivers — its FixedTree cache fills post-connect and the browser runs an UntilStable settle loop; the _opening spinner covers the wait. *@ @implements IAsyncDisposable @using Microsoft.AspNetCore.Authorization @using ZB.MOM.WW.OtOpcUa.AdminUI.Browsing @using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.Pickers @using ZB.MOM.WW.OtOpcUa.Commons.Browsing @inject IBrowserSessionService BrowserService @inject AuthenticationStateProvider AuthState @inject IAuthorizationService AuthorizationService
@if (_canBrowse) {
@if (_token == Guid.Empty) { } else { Browser open } @if (_openError is not null) { @TruncatedError() }
@if (_token != Guid.Empty) {
Snapshot taken at open/refresh time — click Refresh to re-read the machine.
} }
Result: @_built
@code { [Parameter] public string CurrentAddress { get; set; } = ""; [Parameter] public EventCallback CurrentAddressChanged { get; set; } /// Driver type used to open a universal browse session (defaults to FOCAS). [Parameter] public string DriverType { get; set; } = "FOCAS"; /// Live accessor for the selected driver's DriverConfig JSON (browse connect config). [Parameter] public Func GetConfigJson { get; set; } = () => "{}"; private string _group = "axis"; private int _parameterId = 0; private string _built = ""; private Guid _token = Guid.Empty; private bool _opening; private bool _canBrowse; private string? _openError; protected override async Task OnInitializedAsync() { var auth = await AuthState.GetAuthenticationStateAsync(); var authResult = await AuthorizationService.AuthorizeAsync(auth.User, null, AdminUiPolicies.DriverOperator); // CanBrowse constructs a throwaway driver — evaluate once, not per render. _canBrowse = authResult.Succeeded && BrowserService.CanBrowse(DriverType, GetConfigJson() ?? "{}"); _built = FocasAddressBuilder.Build(_group, _parameterId); await CurrentAddressChanged.InvokeAsync(_built); } private async Task OpenBrowseAsync() { _opening = true; _openError = null; StateHasChanged(); try { var json = GetConfigJson() ?? "{}"; var result = await BrowserService.OpenAsync(DriverType, json, default); if (result.Ok) _token = result.Token; else _openError = result.Message; } finally { _opening = false; StateHasChanged(); } } private async Task RefreshBrowseAsync() { await CloseBrowseAsync(); await OpenBrowseAsync(); } 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) { // Folders fire the same callback — commit only on a leaf. The captured NodeId is the // driver FullName (FixedTree address), which the group/id builder cannot reconstruct, so // commit it directly rather than through OnChangedAsync. if (node.Kind != BrowseNodeKind.Leaf) return; _built = node.NodeId; await CurrentAddressChanged.InvokeAsync(_built); } private async Task OnChangedAsync() { _built = FocasAddressBuilder.Build(_group, _parameterId); await CurrentAddressChanged.InvokeAsync(_built); } private string TruncatedError() => _openError is null ? "" : (_openError.Length > 60 ? _openError[..60] + "…" : _openError); public ValueTask DisposeAsync() { if (_token != Guid.Empty) { // Fire-and-forget — don't block circuit teardown on a slow capture. _ = BrowserService.CloseAsync(_token); } return ValueTask.CompletedTask; } }