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
+@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" : "";
+
SelectAttributeAsync(a))">
+ @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 */ }
+ }
+ }
}