feat(adminui): wire Galaxy picker to live browser + attribute side-panel

This commit is contained in:
Joseph Doherty
2026-05-28 16:17:34 -04:00
parent 1b0baf7025
commit 7c9621040e
2 changed files with 153 additions and 10 deletions
@@ -58,7 +58,8 @@ else
CurrentAddress="@_pickedAddress"
OnPickAddress="@OnAddressPicked">
<GalaxyAddressPickerBody CurrentAddress="@_pickedAddress"
CurrentAddressChanged="@((s) => _pickedAddress = s)" />
CurrentAddressChanged="@((s) => _pickedAddress = s)"
GetConfigJson="@SerializeCurrentConfig" />
</DriverTagPicker>
@* mxaccessgw connection *@
@@ -1,10 +1,15 @@
@* Static Galaxy address builder: tag_name.AttributeName free text → verbatim.
Live Galaxy browse deferred to a follow-up phase. *@
<div class="alert alert-info py-2 px-3 mb-3 small">
<strong>Note:</strong> Live Galaxy browse is deferred to a follow-up phase.
Enter the tag and attribute name manually below.
</div>
@* 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
<div class="row g-3">
<div class="col-md-5">
@@ -21,6 +26,70 @@
</div>
</div>
@if (_canOperate)
{
<div class="mt-3 d-flex align-items-center gap-2">
@if (_token == Guid.Empty)
{
<button type="button" class="btn btn-outline-primary btn-sm" disabled="@_opening"
@onclick="OpenBrowseAsync">
@if (_opening) { <span class="spinner-border spinner-border-sm me-1"></span> }
Browse galaxy
</button>
}
else
{
<span class="chip chip-ok">Browser open</span>
<button type="button" class="btn btn-outline-secondary btn-sm" @onclick="CloseBrowseAsync">Close</button>
}
@if (_openError is not null) { <span class="chip chip-bad" title="@_openError">@TruncatedError()</span> }
</div>
@if (_token != Guid.Empty)
{
<div class="row g-3 mt-1">
<div class="col-md-7">
<label class="form-label small">Objects</label>
<DriverBrowseTree SessionToken="_token" OnNodeSelected="OnObjectSelectAsync"
SelectedNodeId="_tagName" />
</div>
<div class="col-md-5">
<label class="form-label small">Attributes of @(string.IsNullOrEmpty(_tagName) ? "—" : _tagName)</label>
<div class="border rounded p-2" style="max-height:420px; overflow:auto; min-height:240px">
@if (_attrsLoading)
{
<span class="spinner-border spinner-border-sm me-1"></span>
}
else if (_attrsError is not null)
{
<span class="text-danger small">@_attrsError</span>
}
else if (_attrs is null)
{
<span class="text-muted small">Pick an object.</span>
}
else if (_attrs.Count == 0)
{
<span class="text-muted small">No attributes.</span>
}
else
{
@foreach (var a in _attrs)
{
var sel = _attributeName == a.Name ? "bg-primary-subtle" : "";
<div class="d-flex justify-content-between align-items-center py-1 @sel"
style="cursor:pointer" @onclick="@(() => SelectAttributeAsync(a))">
<span class="mono small">@a.Name</span>
<span class="text-muted small">@a.DriverDataType@(a.IsArray ? "[]" : "") · @a.SecurityClass</span>
</div>
}
}
</div>
</div>
</div>
}
}
<div class="mt-3">
<span class="text-muted small">Result:</span>
<code class="mono ms-2">@_built</code>
@@ -29,15 +98,77 @@
@code {
[Parameter] public string CurrentAddress { get; set; } = "";
[Parameter] public EventCallback<string> CurrentAddressChanged { get; set; }
[Parameter, EditorRequired] public Func<string> 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<AttributeInfo>? _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 */ }
}
}
}