feat(centralui): add OPC UA browse button + override column to InstanceConfigure

This commit is contained in:
Joseph Doherty
2026-05-28 12:14:26 -04:00
parent e6f9f91bb3
commit 3162370a8f
@@ -9,6 +9,7 @@
@using ZB.MOM.WW.ScadaBridge.TemplateEngine.Flattening
@using ZB.MOM.WW.ScadaBridge.TemplateEngine.Services
@using ZB.MOM.WW.ScadaBridge.DeploymentManager
@using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Dialogs
@attribute [Authorize(Policy = AuthorizationPolicies.RequireDeployment)]
@inject ITemplateEngineRepository TemplateEngineRepository
@inject ISiteRepository SiteRepository
@@ -108,17 +109,22 @@
<th>Attribute</th>
<th>Tag Path</th>
<th style="width: 280px;">Connection</th>
<th>Override</th>
<th style="width: 110px;"></th>
</tr>
</thead>
<tbody>
@foreach (var attr in _bindingDataSourceAttrs)
{
var connId = GetBindingConnectionId(attr.Name);
var canBrowse = connId > 0;
var isOpcUa = IsOpcUa(connId);
<tr>
<td class="small">@attr.Name</td>
<td class="small text-muted font-monospace">@attr.DataSourceReference</td>
<td>
<select class="form-select form-select-sm"
value="@GetBindingConnectionId(attr.Name)"
value="@connId"
@onchange="(e) => OnBindingChanged(attr.Name, e)">
<option value="0">— none —</option>
@foreach (var c in _siteConnections)
@@ -127,6 +133,23 @@
}
</select>
</td>
<td>
<input class="form-control form-control-sm"
value="@GetOverrideForAttr(attr.Name)"
@onchange="(e) => OnOverrideForAttrChanged(attr.Name, e)"
placeholder="@(attr.DataSourceReference ?? "(no default)")" />
</td>
<td>
@if (isOpcUa)
{
<button class="btn btn-sm btn-outline-primary"
disabled="@(!canBrowse)"
title="@(canBrowse ? "Browse OPC UA address space" : "Pick a connection first")"
@onclick="() => OpenBrowser(attr.Name)">
Browse…
</button>
}
</td>
</tr>
}
</tbody>
@@ -331,6 +354,14 @@
</div>
</div>
</div>
@* OPC UA Tag Browser dialog (Task 18) — rendered once; OpenBrowser
tracks which binding row's override input receives the picked node id. *@
<OpcUaBrowserDialog @ref="_browserRef"
SiteId="@_browserSiteIdentifier"
ConnectionName="@_browserConnectionName"
InitialNodeId="@_browserInitial"
OnSelected="OnBrowserSelected" />
}
</div>
@@ -350,8 +381,24 @@
private List<TemplateAttribute> _bindingDataSourceAttrs = new();
private List<DataConnection> _siteConnections = new();
private Dictionary<string, int> _bindingSelections = new();
/// <summary>
/// Per-attribute <c>DataSourceReferenceOverride</c> values (Task 18). Mirrors
/// <see cref="_bindingSelections"/> by attribute name. Loaded from the
/// existing <see cref="InstanceConnectionBinding"/> rows on init; round-tripped
/// through <see cref="ConnectionBinding"/> on <c>SaveBindings</c>.
/// </summary>
private Dictionary<string, string?> _bindingOverrides = new();
private int _bulkConnectionId;
// OPC UA tag browser (Task 18) — single dialog rendered at page bottom;
// _browserAttrInEdit tracks which row gets the picked node id on Select.
private OpcUaBrowserDialog? _browserRef;
private string? _browserAttrInEdit;
private string _browserSiteIdentifier = "";
private string _browserConnectionName = "";
private string? _browserInitial;
private string _siteIdentifier = "";
// Overrides
private List<TemplateAttribute> _overrideAttrs = new();
private Dictionary<string, string?> _overrideValues = new();
@@ -407,7 +454,11 @@
_templateName = template?.Name ?? $"#{_instance.TemplateId}";
var sites = await SiteRepository.GetAllSitesAsync();
_siteName = sites.FirstOrDefault(s => s.Id == _instance.SiteId)?.Name ?? $"#{_instance.SiteId}";
var site = sites.FirstOrDefault(s => s.Id == _instance.SiteId);
_siteName = site?.Name ?? $"#{_instance.SiteId}";
// Task 18: cache the site's machine identifier — the OPC UA browse
// dialog routes by SiteIdentifier (string), not the numeric site id.
_siteIdentifier = site?.SiteIdentifier ?? "";
// Areas
_siteAreas = (await TemplateEngineRepository.GetAreasBySiteIdAsync(_instance.SiteId)).ToList();
@@ -420,7 +471,11 @@
_siteConnections = (await SiteRepository.GetDataConnectionsBySiteIdAsync(_instance.SiteId)).ToList();
var existingBindings = await TemplateEngineRepository.GetBindingsByInstanceIdAsync(Id);
foreach (var b in existingBindings)
{
_bindingSelections[b.AttributeName] = b.DataConnectionId;
if (!string.IsNullOrEmpty(b.DataSourceReferenceOverride))
_bindingOverrides[b.AttributeName] = b.DataSourceReferenceOverride;
}
// Overrides
_overrideAttrs = attrs.Where(a => !a.IsLocked).ToList();
@@ -474,12 +529,74 @@
_bindingSelections[attr.Name] = _bulkConnectionId;
}
// ── Task 18: per-attribute override input + OPC UA tag browser ──────────
private string? GetOverrideForAttr(string attrName)
=> _bindingOverrides.GetValueOrDefault(attrName);
private void OnOverrideForAttrChanged(string attrName, ChangeEventArgs e)
{
var val = e.Value?.ToString();
if (string.IsNullOrWhiteSpace(val))
_bindingOverrides.Remove(attrName);
else
_bindingOverrides[attrName] = val;
}
/// <summary>Looks up the template default <c>DataSourceReference</c> for an attribute.</summary>
private string? GetTemplateDefault(string attrName)
=> _bindingDataSourceAttrs.FirstOrDefault(a => a.Name == attrName)?.DataSourceReference;
/// <summary>True when the row's selected data connection is OPC UA.</summary>
private bool IsOpcUa(int connectionId)
=> connectionId > 0
&& string.Equals(
_siteConnections.FirstOrDefault(c => c.Id == connectionId)?.Protocol,
"OpcUa",
StringComparison.OrdinalIgnoreCase);
/// <summary>
/// Opens the OPC UA tag browser dialog for the given attribute row. Remembers
/// which attribute is being edited so <see cref="OnBrowserSelected"/> can
/// write the picked node id back to the right override input.
/// </summary>
private async Task OpenBrowser(string attrName)
{
var connId = GetBindingConnectionId(attrName);
var conn = _siteConnections.FirstOrDefault(c => c.Id == connId);
if (conn is null) return;
_browserAttrInEdit = attrName;
_browserConnectionName = conn.Name;
_browserSiteIdentifier = _siteIdentifier;
_browserInitial = _bindingOverrides.GetValueOrDefault(attrName)
?? GetTemplateDefault(attrName);
if (_browserRef is not null)
await _browserRef.ShowAsync();
}
private void OnBrowserSelected(string nodeId)
{
if (_browserAttrInEdit is null) return;
_bindingOverrides[_browserAttrInEdit] = nodeId;
_browserAttrInEdit = null;
}
private async Task SaveBindings()
{
_saving = true;
try
{
var bindings = _bindingSelections.Select(kv => new ConnectionBinding(kv.Key, kv.Value)).ToList();
// Task 18: include the per-attribute DataSourceReferenceOverride on
// the wire record so it round-trips through SetConnectionBindingsAsync
// into the InstanceConnectionBinding entity.
var bindings = _bindingSelections
.Select(kv => new ConnectionBinding(
kv.Key,
kv.Value,
_bindingOverrides.GetValueOrDefault(kv.Key)))
.ToList();
var user = await GetCurrentUserAsync();
var result = await InstanceService.SetConnectionBindingsAsync(Id, bindings, user);
if (result.IsSuccess)