feat(browse): Wave-0 Batch G — AbCip/TwinCAT/FOCAS picker bodies gain universal browse
Task 14/15/16: each picker body keeps its manual builder and appends a DriverOperator-gated browse affordance (mirrors OpcUaClientAddressPickerBody) — Browse/Refresh/Close, DriverBrowseTree over the universal DiscoveryDriverBrowser session, leaf-only commit (OnNodeSelected fires for folders too), snapshot-at-open label, fire-and-forget close on dispose. _canBrowse evaluated once in OnInitializedAsync (CanBrowse constructs a throwaway driver). FOCAS commits the leaf FullName directly (the group/id builder can't reconstruct it). Editors pass DriverType + GetConfigJson through. Razor-live-verified at Task 18.
This commit is contained in:
@@ -15,9 +15,9 @@
|
|||||||
{"id": 11, "subject": "Task 11: BrowserSessionService universal fallback + CanBrowse", "status": "completed", "blockedBy": [8], "parallelWith": [9]},
|
{"id": 11, "subject": "Task 11: BrowserSessionService universal fallback + CanBrowse", "status": "completed", "blockedBy": [8], "parallelWith": [9]},
|
||||||
{"id": 12, "subject": "Task 12: DI registration in AddAdminUI", "status": "completed", "blockedBy": [11], "parallelWith": [10]},
|
{"id": 12, "subject": "Task 12: DI registration in AddAdminUI", "status": "completed", "blockedBy": [11], "parallelWith": [10]},
|
||||||
{"id": 13, "subject": "Task 13: TagModal -> editor parameter plumbing (DriverType + GetDriverConfigJson)", "status": "completed", "blockedBy": [0], "parallelWith": [1, 2]},
|
{"id": 13, "subject": "Task 13: TagModal -> editor parameter plumbing (DriverType + GetDriverConfigJson)", "status": "completed", "blockedBy": [0], "parallelWith": [1, 2]},
|
||||||
{"id": 14, "subject": "Task 14: AbCip picker body universal browse mode", "status": "pending", "blockedBy": [10, 12, 13, 3], "parallelWith": [15, 16]},
|
{"id": 14, "subject": "Task 14: AbCip picker body universal browse mode", "status": "completed", "blockedBy": [10, 12, 13, 3], "parallelWith": [15, 16]},
|
||||||
{"id": 15, "subject": "Task 15: TwinCAT picker body universal browse mode", "status": "pending", "blockedBy": [10, 12, 13, 4], "parallelWith": [14, 16]},
|
{"id": 15, "subject": "Task 15: TwinCAT picker body universal browse mode", "status": "completed", "blockedBy": [10, 12, 13, 4], "parallelWith": [14, 16]},
|
||||||
{"id": 16, "subject": "Task 16: FOCAS picker body universal browse mode", "status": "pending", "blockedBy": [10, 12, 13, 5], "parallelWith": [14, 15]},
|
{"id": 16, "subject": "Task 16: FOCAS picker body universal browse mode", "status": "completed", "blockedBy": [10, 12, 13, 5], "parallelWith": [14, 15]},
|
||||||
{"id": 17, "subject": "Task 17: Full build/test sweep + doc status flip", "status": "pending", "blockedBy": [14, 15, 16]},
|
{"id": 17, "subject": "Task 17: Full build/test sweep + doc status flip", "status": "pending", "blockedBy": [14, 15, 16]},
|
||||||
{"id": 18, "subject": "Task 18: Live /run verify on docker-dev (GATE)", "status": "pending", "blockedBy": [17]}
|
{"id": 18, "subject": "Task 18: Live /run verify on docker-dev (GATE)", "status": "pending", "blockedBy": [17]}
|
||||||
],
|
],
|
||||||
|
|||||||
+109
-3
@@ -1,4 +1,14 @@
|
|||||||
@* Static AB CIP address builder: tag name + optional element index → tag[idx] or tag *@
|
@* AB CIP address picker:
|
||||||
|
1. Static tag name + optional element index → tag[idx] or tag (always available).
|
||||||
|
2. (DriverOperator-gated, when the driver supports online discovery) Browse the controller
|
||||||
|
symbol tree captured by the universal DiscoveryDriverBrowser. *@
|
||||||
|
@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="row g-3">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
@@ -22,6 +32,42 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if (_canBrowse)
|
||||||
|
{
|
||||||
|
<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 controller
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span class="chip chip-ok">Browser open</span>
|
||||||
|
<button type="button" class="btn btn-outline-secondary btn-sm"
|
||||||
|
disabled="@_opening" @onclick="RefreshBrowseAsync">
|
||||||
|
@if (_opening) { <span class="spinner-border spinner-border-sm me-1"></span> }
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
|
<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="mt-2">
|
||||||
|
<div class="form-text mb-1">Snapshot taken at open/refresh time — click Refresh to re-read the controller.</div>
|
||||||
|
<DriverBrowseTree SessionToken="_token" OnNodeSelected="OnTreeSelectAsync"
|
||||||
|
SelectedNodeId="@_tagName" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<span class="text-muted small">Result:</span>
|
<span class="text-muted small">Result:</span>
|
||||||
<code class="mono ms-2">@_built</code>
|
<code class="mono ms-2">@_built</code>
|
||||||
@@ -30,16 +76,63 @@
|
|||||||
@code {
|
@code {
|
||||||
[Parameter] public string CurrentAddress { get; set; } = "";
|
[Parameter] public string CurrentAddress { get; set; } = "";
|
||||||
[Parameter] public EventCallback<string> CurrentAddressChanged { get; set; }
|
[Parameter] public EventCallback<string> CurrentAddressChanged { get; set; }
|
||||||
|
/// <summary>Driver type used to open a universal browse session (defaults to AbCip).</summary>
|
||||||
|
[Parameter] public string DriverType { get; set; } = "AbCip";
|
||||||
|
/// <summary>Live accessor for the selected driver's DriverConfig JSON (browse connect config).</summary>
|
||||||
|
[Parameter] public Func<string> GetConfigJson { get; set; } = () => "{}";
|
||||||
|
|
||||||
private string _tagName = "";
|
private string _tagName = "";
|
||||||
private int _elementIndex = 0;
|
private int _elementIndex = 0;
|
||||||
private bool _useIndex = false;
|
private bool _useIndex = false;
|
||||||
private string _built = "";
|
private string _built = "";
|
||||||
|
|
||||||
protected override void OnInitialized()
|
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 = Build();
|
_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(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 CIP tag path).
|
||||||
|
if (node.Kind != BrowseNodeKind.Leaf) return;
|
||||||
|
_tagName = node.NodeId;
|
||||||
|
_useIndex = false;
|
||||||
|
await OnChangedAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OnChangedAsync()
|
private async Task OnChangedAsync()
|
||||||
@@ -54,4 +147,17 @@
|
|||||||
return "";
|
return "";
|
||||||
return _useIndex ? $"{_tagName}[{_elementIndex}]" : _tagName;
|
return _useIndex ? $"{_tagName}[{_elementIndex}]" : _tagName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+112
-3
@@ -1,5 +1,17 @@
|
|||||||
@* Static FOCAS address builder: parameter group + parameter ID → axis:5 *@
|
@* 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.AdminUI.Components.Shared.Drivers.Pickers
|
||||||
|
@using ZB.MOM.WW.OtOpcUa.Commons.Browsing
|
||||||
|
@inject IBrowserSessionService BrowserService
|
||||||
|
@inject AuthenticationStateProvider AuthState
|
||||||
|
@inject IAuthorizationService AuthorizationService
|
||||||
|
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
@@ -18,6 +30,42 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if (_canBrowse)
|
||||||
|
{
|
||||||
|
<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 machine
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span class="chip chip-ok">Browser open</span>
|
||||||
|
<button type="button" class="btn btn-outline-secondary btn-sm"
|
||||||
|
disabled="@_opening" @onclick="RefreshBrowseAsync">
|
||||||
|
@if (_opening) { <span class="spinner-border spinner-border-sm me-1"></span> }
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
|
<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="mt-2">
|
||||||
|
<div class="form-text mb-1">Snapshot taken at open/refresh time — click Refresh to re-read the machine.</div>
|
||||||
|
<DriverBrowseTree SessionToken="_token" OnNodeSelected="OnTreeSelectAsync"
|
||||||
|
SelectedNodeId="@_built" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<span class="text-muted small">Result:</span>
|
<span class="text-muted small">Result:</span>
|
||||||
<code class="mono ms-2">@_built</code>
|
<code class="mono ms-2">@_built</code>
|
||||||
@@ -26,15 +74,63 @@
|
|||||||
@code {
|
@code {
|
||||||
[Parameter] public string CurrentAddress { get; set; } = "";
|
[Parameter] public string CurrentAddress { get; set; } = "";
|
||||||
[Parameter] public EventCallback<string> CurrentAddressChanged { get; set; }
|
[Parameter] public EventCallback<string> CurrentAddressChanged { get; set; }
|
||||||
|
/// <summary>Driver type used to open a universal browse session (defaults to FOCAS).</summary>
|
||||||
|
[Parameter] public string DriverType { get; set; } = "FOCAS";
|
||||||
|
/// <summary>Live accessor for the selected driver's DriverConfig JSON (browse connect config).</summary>
|
||||||
|
[Parameter] public Func<string> GetConfigJson { get; set; } = () => "{}";
|
||||||
|
|
||||||
private string _group = "axis";
|
private string _group = "axis";
|
||||||
private int _parameterId = 0;
|
private int _parameterId = 0;
|
||||||
private string _built = "";
|
private string _built = "";
|
||||||
|
|
||||||
protected override void OnInitialized()
|
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);
|
_built = FocasAddressBuilder.Build(_group, _parameterId);
|
||||||
_ = 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(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()
|
private async Task OnChangedAsync()
|
||||||
@@ -42,4 +138,17 @@
|
|||||||
_built = FocasAddressBuilder.Build(_group, _parameterId);
|
_built = FocasAddressBuilder.Build(_group, _parameterId);
|
||||||
await CurrentAddressChanged.InvokeAsync(_built);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+108
-3
@@ -1,4 +1,14 @@
|
|||||||
@* Static TwinCAT address builder: ADS variable name (free text) → verbatim *@
|
@* TwinCAT address picker:
|
||||||
|
1. Static ADS variable name (free text) → verbatim (always available).
|
||||||
|
2. (DriverOperator-gated, when the driver supports online discovery) Browse the ADS symbol
|
||||||
|
tree captured by the universal DiscoveryDriverBrowser. *@
|
||||||
|
@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="row g-3">
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
@@ -11,6 +21,42 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if (_canBrowse)
|
||||||
|
{
|
||||||
|
<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 controller
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span class="chip chip-ok">Browser open</span>
|
||||||
|
<button type="button" class="btn btn-outline-secondary btn-sm"
|
||||||
|
disabled="@_opening" @onclick="RefreshBrowseAsync">
|
||||||
|
@if (_opening) { <span class="spinner-border spinner-border-sm me-1"></span> }
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
|
<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="mt-2">
|
||||||
|
<div class="form-text mb-1">Snapshot taken at open/refresh time — click Refresh to re-read the controller.</div>
|
||||||
|
<DriverBrowseTree SessionToken="_token" OnNodeSelected="OnTreeSelectAsync"
|
||||||
|
SelectedNodeId="@_varName" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<span class="text-muted small">Result:</span>
|
<span class="text-muted small">Result:</span>
|
||||||
<code class="mono ms-2">@_built</code>
|
<code class="mono ms-2">@_built</code>
|
||||||
@@ -19,14 +65,60 @@
|
|||||||
@code {
|
@code {
|
||||||
[Parameter] public string CurrentAddress { get; set; } = "";
|
[Parameter] public string CurrentAddress { get; set; } = "";
|
||||||
[Parameter] public EventCallback<string> CurrentAddressChanged { get; set; }
|
[Parameter] public EventCallback<string> CurrentAddressChanged { get; set; }
|
||||||
|
/// <summary>Driver type used to open a universal browse session (defaults to TwinCAT).</summary>
|
||||||
|
[Parameter] public string DriverType { get; set; } = "TwinCAT";
|
||||||
|
/// <summary>Live accessor for the selected driver's DriverConfig JSON (browse connect config).</summary>
|
||||||
|
[Parameter] public Func<string> GetConfigJson { get; set; } = () => "{}";
|
||||||
|
|
||||||
private string _varName = "";
|
private string _varName = "";
|
||||||
private string _built = "";
|
private string _built = "";
|
||||||
|
|
||||||
protected override void OnInitialized()
|
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 = _varName;
|
_built = _varName;
|
||||||
_ = 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(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 ADS symbol path).
|
||||||
|
if (node.Kind != BrowseNodeKind.Leaf) return;
|
||||||
|
_varName = node.NodeId;
|
||||||
|
await OnChangedAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OnChangedAsync()
|
private async Task OnChangedAsync()
|
||||||
@@ -34,4 +126,17 @@
|
|||||||
_built = _varName;
|
_built = _varName;
|
||||||
await CurrentAddressChanged.InvokeAsync(_built);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -34,7 +34,9 @@
|
|||||||
CurrentAddress="@_pickerAddress"
|
CurrentAddress="@_pickerAddress"
|
||||||
OnPickAddress="@OnAddressPicked">
|
OnPickAddress="@OnAddressPicked">
|
||||||
<AbCipAddressPickerBody CurrentAddress="@_pickerAddress"
|
<AbCipAddressPickerBody CurrentAddress="@_pickerAddress"
|
||||||
CurrentAddressChanged="@((s) => _pickerAddress = s)" />
|
CurrentAddressChanged="@((s) => _pickerAddress = s)"
|
||||||
|
DriverType="@DriverType"
|
||||||
|
GetConfigJson="@GetDriverConfigJson" />
|
||||||
</DriverTagPicker>
|
</DriverTagPicker>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -32,7 +32,9 @@
|
|||||||
CurrentAddress="@_pickerAddress"
|
CurrentAddress="@_pickerAddress"
|
||||||
OnPickAddress="@OnAddressPicked">
|
OnPickAddress="@OnAddressPicked">
|
||||||
<FOCASAddressPickerBody CurrentAddress="@_pickerAddress"
|
<FOCASAddressPickerBody CurrentAddress="@_pickerAddress"
|
||||||
CurrentAddressChanged="@((s) => _pickerAddress = s)" />
|
CurrentAddressChanged="@((s) => _pickerAddress = s)"
|
||||||
|
DriverType="@DriverType"
|
||||||
|
GetConfigJson="@GetDriverConfigJson" />
|
||||||
</DriverTagPicker>
|
</DriverTagPicker>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -34,7 +34,9 @@
|
|||||||
CurrentAddress="@_pickerAddress"
|
CurrentAddress="@_pickerAddress"
|
||||||
OnPickAddress="@OnAddressPicked">
|
OnPickAddress="@OnAddressPicked">
|
||||||
<TwinCATAddressPickerBody CurrentAddress="@_pickerAddress"
|
<TwinCATAddressPickerBody CurrentAddress="@_pickerAddress"
|
||||||
CurrentAddressChanged="@((s) => _pickerAddress = s)" />
|
CurrentAddressChanged="@((s) => _pickerAddress = s)"
|
||||||
|
DriverType="@DriverType"
|
||||||
|
GetConfigJson="@GetDriverConfigJson" />
|
||||||
</DriverTagPicker>
|
</DriverTagPicker>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user