Files
lmxopcua/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/OpcUaClientAddressPickerBody.razor
T

128 lines
4.3 KiB
Plaintext

@* 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
<div class="row g-3">
<div class="col-md-10">
<label class="form-label">NodeId</label>
<input type="text" class="form-control form-control-sm mono"
placeholder="ns=2;s=Channel.Device.Tag"
@bind="_nodeId" @bind:after="OnChangedAsync" />
<div class="form-text">
OPC UA NodeId string, e.g. <code>ns=2;s=Channel.Device.Tag</code> or
<code>i=1001</code>. Use Browse to navigate the remote server.
</div>
</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 remote server
</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) { <RefusalChip Message="@_openError" /> }
</div>
@if (_token != Guid.Empty)
{
<div class="mt-2">
<DriverBrowseTree SessionToken="_token" OnNodeSelected="OnTreeSelectAsync"
SelectedNodeId="@_nodeId" />
</div>
}
}
<div class="mt-3">
<span class="text-muted small">Result:</span>
<code class="mono ms-2" title="@_built">@DisplayText.Abbreviate(_built, 64)</code>
</div>
@code {
[Parameter] public string CurrentAddress { get; set; } = "";
[Parameter] public EventCallback<string> CurrentAddressChanged { get; set; }
[Parameter, EditorRequired] public Func<string> GetConfigJson { get; set; } = () => "{}";
private string _nodeId = "";
private string _built = "";
private Guid _token = Guid.Empty;
// Set first thing in DisposeAsync; an open that lands afterwards closes its own session.
private volatile bool _disposed;
private bool _opening;
private bool _canOperate;
private string? _openError;
protected override async Task OnInitializedAsync()
{
var auth = await AuthState.GetAuthenticationStateAsync();
var authResult = await AuthorizationService.AuthorizeAsync(auth.User, null, AdminUiPolicies.DriverOperator);
_canOperate = authResult.Succeeded;
_built = _nodeId;
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);
// Host disposed mid-open: close the session nobody will (R04 L4).
if (BrowseOpenGuard.CloseIfOrphaned(result, _disposed, BrowserService)) { return; }
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()
{
_built = _nodeId;
await CurrentAddressChanged.InvokeAsync(_built);
}
public ValueTask DisposeAsync()
{
_disposed = true;
if (_token != Guid.Empty)
{
// Fire-and-forget — don't block circuit teardown on a slow remote.
_ = BrowserService.CloseAsync(_token);
}
return ValueTask.CompletedTask;
}
}