From 7c9621040e779ea181aba192ac8a67475c7c82ef Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 28 May 2026 16:17:34 -0400 Subject: [PATCH] feat(adminui): wire Galaxy picker to live browser + attribute side-panel --- .../Clusters/Drivers/GalaxyDriverPage.razor | 3 +- .../Pickers/GalaxyAddressPickerBody.razor | 160 +++++++++++++++++- 2 files changed, 153 insertions(+), 10 deletions(-) diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/GalaxyDriverPage.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/GalaxyDriverPage.razor index 826fb765..c190e956 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/GalaxyDriverPage.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/GalaxyDriverPage.razor @@ -58,7 +58,8 @@ else CurrentAddress="@_pickedAddress" OnPickAddress="@OnAddressPicked"> + CurrentAddressChanged="@((s) => _pickedAddress = s)" + GetConfigJson="@SerializeCurrentConfig" /> @* mxaccessgw connection *@ diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/GalaxyAddressPickerBody.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/GalaxyAddressPickerBody.razor index 151dc6a5..fd546f12 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/GalaxyAddressPickerBody.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/GalaxyAddressPickerBody.razor @@ -1,10 +1,15 @@ -@* Static Galaxy address builder: tag_name.AttributeName free text → verbatim. - Live Galaxy browse deferred to a follow-up phase. *@ - -
- Note: Live Galaxy browse is deferred to a follow-up phase. - Enter the tag and attribute name manually below. -
+@* Galaxy address picker: + 1. Manual tag/attribute entry (always available). + 2. (DriverOperator-gated) Live browse: object tree on the left, + attribute side-panel on the right. Clicking an attribute commits + tag_name.AttributeName into the result. *@ +@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
@@ -21,6 +26,70 @@
+@if (_canOperate) +{ +
+ @if (_token == Guid.Empty) + { + + } + else + { + Browser open + + } + @if (_openError is not null) { @TruncatedError() } +
+ + @if (_token != Guid.Empty) + { +
+
+ + +
+
+ +
+ @if (_attrsLoading) + { + + } + else if (_attrsError is not null) + { + @_attrsError + } + else if (_attrs is null) + { + Pick an object. + } + else if (_attrs.Count == 0) + { + No attributes. + } + else + { + @foreach (var a in _attrs) + { + var sel = _attributeName == a.Name ? "bg-primary-subtle" : ""; +
+ @a.Name + @a.DriverDataType@(a.IsArray ? "[]" : "") · @a.SecurityClass +
+ } + } +
+
+
+ } +} +
Result: @_built @@ -29,15 +98,77 @@ @code { [Parameter] public string CurrentAddress { get; set; } = ""; [Parameter] public EventCallback CurrentAddressChanged { get; set; } + [Parameter, EditorRequired] public Func GetConfigJson { get; set; } = () => "{}"; private string _tagName = ""; private string _attributeName = ""; private string _built = ""; + private Guid _token = Guid.Empty; + private bool _opening; + private bool _attrsLoading; + private bool _canOperate; + private string? _openError; + private string? _attrsError; + private IReadOnlyList? _attrs; - 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 = Build(); - _ = 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("Galaxy", 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; + _attrs = null; + StateHasChanged(); + if (t != Guid.Empty) await BrowserService.CloseAsync(t); + } + + private async Task OnObjectSelectAsync(BrowseNode node) + { + _tagName = node.NodeId; + _attributeName = ""; + _attrs = null; + _attrsLoading = true; + _attrsError = null; + StateHasChanged(); + try + { + _attrs = await BrowserService.AttributesAsync(_token, _tagName, default); + } + catch (Exception ex) + { + _attrsError = ex.Message; + } + finally + { + _attrsLoading = false; + await OnChangedAsync(); + } + } + + private async Task SelectAttributeAsync(AttributeInfo a) + { + _attributeName = a.Name; + await OnChangedAsync(); } private async Task OnChangedAsync() @@ -54,4 +185,15 @@ return _tagName; return $"{_tagName}.{_attributeName}"; } + + 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 */ } + } + } }