feat(adminui): wire Galaxy live-browse picker into the standard TagModal
This commit is contained in:
@@ -298,7 +298,7 @@ else
|
||||
private bool _tagModalVisible;
|
||||
private bool _tagModalIsNew;
|
||||
private TagEditDto? _tagModalExisting;
|
||||
private IReadOnlyList<(string Id, string Display, string DriverType)> _tagDriverOptions = Array.Empty<(string, string, string)>();
|
||||
private IReadOnlyList<(string Id, string Display, string DriverType, string DriverConfig)> _tagDriverOptions = Array.Empty<(string, string, string, string)>();
|
||||
|
||||
// --- Virtual Tags tab state. _vtags is null until the tab is first activated. ---
|
||||
private IReadOnlyList<EquipmentVirtualTagRow>? _vtags;
|
||||
|
||||
@@ -5,9 +5,12 @@
|
||||
either, the owning equipment is fixed. On a successful save it raises OnSaved so the host can
|
||||
refresh the equipment's children in place. *@
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@using System.Text.Json
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using ZB.MOM.WW.OtOpcUa.AdminUI.Uns
|
||||
@using ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors
|
||||
@using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers
|
||||
@using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.Pickers
|
||||
@using ZB.MOM.WW.OtOpcUa.Configuration.Enums
|
||||
@inject IUnsTreeService Svc
|
||||
|
||||
@@ -44,9 +47,9 @@
|
||||
<label class="form-label" for="tag-driver">Driver instance</label>
|
||||
<InputSelect id="tag-driver" @bind-Value="_form.DriverInstanceId" @bind-Value:after="OnDriverChanged" class="form-select form-select-sm">
|
||||
<option value="">— pick a driver —</option>
|
||||
@foreach (var (id, display, _) in Drivers)
|
||||
@foreach (var d in Drivers)
|
||||
{
|
||||
<option value="@id">@display</option>
|
||||
<option value="@d.Id">@d.Display</option>
|
||||
}
|
||||
</InputSelect>
|
||||
<ValidationMessage For="@(() => _form.DriverInstanceId)" />
|
||||
@@ -90,6 +93,35 @@
|
||||
{
|
||||
<div class="form-text">Pick a driver above to configure this tag.</div>
|
||||
}
|
||||
else if (IsGalaxyDriver)
|
||||
{
|
||||
@* GalaxyMxGateway has no typed TagConfigEditorMap editor; instead a Galaxy point is
|
||||
authored as {"FullName":"tag_name.AttributeName"}. Offer the live-browse picker
|
||||
(against the selected gateway's DriverConfig) plus a manual raw-JSON fallback. *@
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm mb-2"
|
||||
@onclick="@(() => _showGalaxyPicker = true)">
|
||||
Browse Galaxy
|
||||
</button>
|
||||
<InputTextArea id="tag-config" @bind-Value="_form.TagConfig" rows="3"
|
||||
class="form-control form-control-sm mono"
|
||||
placeholder='{ "FullName": "DelmiaReceiver_001.DownloadPath" }' />
|
||||
<div class="form-text">
|
||||
The Galaxy reference, stored as <code>{"FullName":"tag_name.AttributeName"}</code>.
|
||||
Pick one via <strong>Browse Galaxy</strong> or edit the JSON directly.
|
||||
</div>
|
||||
|
||||
@if (_showGalaxyPicker)
|
||||
{
|
||||
<DriverTagPicker @bind-Visible="_showGalaxyPicker"
|
||||
Title="Galaxy address"
|
||||
CurrentAddress="@_galaxyAddress"
|
||||
OnPickAddress="@OnGalaxyAddressPicked">
|
||||
<GalaxyAddressPickerBody CurrentAddress="@_galaxyAddress"
|
||||
CurrentAddressChanged="@((s) => _galaxyAddress = s)"
|
||||
GetConfigJson="@(() => SelectedDriverConfig)" />
|
||||
</DriverTagPicker>
|
||||
}
|
||||
}
|
||||
else if (editorType is not null)
|
||||
{
|
||||
<DynamicComponent Type="editorType" Parameters="BuildEditorParameters()" />
|
||||
@@ -139,8 +171,10 @@
|
||||
/// <summary>The tag being edited, when <see cref="IsNew"/> is <c>false</c>.</summary>
|
||||
[Parameter] public TagEditDto? Existing { get; set; }
|
||||
|
||||
/// <summary>The candidate drivers — scoped to the equipment's cluster by the host — as <c>(Id, Display)</c> pairs.</summary>
|
||||
[Parameter] public IReadOnlyList<(string Id, string Display, string DriverType)> Drivers { get; set; } = Array.Empty<(string, string, string)>();
|
||||
/// <summary>The candidate drivers — scoped to the equipment's cluster by the host — as
|
||||
/// <c>(Id, Display, DriverType, DriverConfig)</c> tuples. <c>DriverType</c> drives typed-editor dispatch;
|
||||
/// <c>DriverConfig</c> feeds the Galaxy live-browse picker for GalaxyMxGateway drivers.</summary>
|
||||
[Parameter] public IReadOnlyList<(string Id, string Display, string DriverType, string DriverConfig)> Drivers { get; set; } = Array.Empty<(string, string, string, string)>();
|
||||
|
||||
/// <summary>Raised after a successful create/save so the host can refresh the equipment's children and close.</summary>
|
||||
[Parameter] public EventCallback OnSaved { get; set; }
|
||||
@@ -152,14 +186,47 @@
|
||||
private bool _busy;
|
||||
private string? _error;
|
||||
|
||||
// Galaxy live-browse picker state. Only meaningful when the selected driver is a GalaxyMxGateway.
|
||||
private bool _showGalaxyPicker;
|
||||
private string _galaxyAddress = "";
|
||||
|
||||
// The DriverType of the currently-selected driver (drives editor dispatch). Null when no driver chosen.
|
||||
private string? SelectedDriverType =>
|
||||
Drivers.FirstOrDefault(d => d.Id == _form.DriverInstanceId).DriverType;
|
||||
|
||||
// The DriverConfig JSON of the currently-selected driver — fed to the Galaxy picker so it browses the
|
||||
// right gateway. Defaults to "{}" when no driver is chosen or the config is empty.
|
||||
private string SelectedDriverConfig
|
||||
{
|
||||
get
|
||||
{
|
||||
var cfg = Drivers.FirstOrDefault(d => d.Id == _form.DriverInstanceId).DriverConfig;
|
||||
return string.IsNullOrEmpty(cfg) ? "{}" : cfg;
|
||||
}
|
||||
}
|
||||
|
||||
// True when the selected driver is a GalaxyMxGateway — Galaxy points are authored as
|
||||
// {"FullName":"tag_name.AttributeName"} via the live-browse picker rather than a typed editor.
|
||||
private bool IsGalaxyDriver => SelectedDriverType == "GalaxyMxGateway";
|
||||
|
||||
// When the operator switches drivers, the previous driver's TagConfig schema no longer applies —
|
||||
// reset it so the newly-dispatched typed editor starts clean (no stale/leaked keys from the old
|
||||
// driver). Fires only on a user dropdown change (@bind-Value:after), not on the initial edit-load.
|
||||
private void OnDriverChanged() => _form.TagConfig = "{}";
|
||||
private void OnDriverChanged()
|
||||
{
|
||||
_form.TagConfig = "{}";
|
||||
// The Galaxy reference belongs to the previous driver; clear the picker's working address too.
|
||||
_galaxyAddress = "";
|
||||
}
|
||||
|
||||
// The operator picked a Galaxy reference (tag_name.AttributeName); store it as the canonical
|
||||
// {"FullName":"..."} TagConfig the Galaxy driver resolves to an MXAccess reference. Default
|
||||
// (PascalCase) serialization yields the "FullName" key the driver/walker reads.
|
||||
private void OnGalaxyAddressPicked(string address)
|
||||
{
|
||||
_galaxyAddress = address;
|
||||
_form.TagConfig = JsonSerializer.Serialize(new { FullName = address });
|
||||
}
|
||||
|
||||
private IDictionary<string, object> BuildEditorParameters() => new Dictionary<string, object>
|
||||
{
|
||||
@@ -189,6 +256,28 @@
|
||||
};
|
||||
}
|
||||
_error = null;
|
||||
_showGalaxyPicker = false;
|
||||
// Seed the picker's working address from any existing {"FullName":"..."} so it opens pre-populated.
|
||||
_galaxyAddress = ReadFullName(_form.TagConfig);
|
||||
}
|
||||
|
||||
// Best-effort extraction of FullName from a Galaxy TagConfig; returns "" when absent or unparseable.
|
||||
private static string ReadFullName(string? configJson)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(configJson)) return "";
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(configJson);
|
||||
return doc.RootElement.ValueKind == JsonValueKind.Object
|
||||
&& doc.RootElement.TryGetProperty("FullName", out var fn)
|
||||
&& fn.ValueKind == JsonValueKind.String
|
||||
? fn.GetString() ?? ""
|
||||
: "";
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SaveAsync()
|
||||
|
||||
@@ -340,9 +340,11 @@ public interface IUnsTreeService
|
||||
/// </summary>
|
||||
/// <param name="equipmentId">The equipment whose candidate drivers to load.</param>
|
||||
/// <param name="ct">A token to cancel the load.</param>
|
||||
/// <returns>The eligible drivers projected to <c>(DriverInstanceId, Display, DriverType)</c> triples,
|
||||
/// where <c>DriverType</c> lets the TagModal dispatch to a per-driver-type typed config editor.</returns>
|
||||
Task<IReadOnlyList<(string DriverInstanceId, string Display, string DriverType)>> LoadTagDriversForEquipmentAsync(string equipmentId, CancellationToken ct = default);
|
||||
/// <returns>The eligible drivers projected to <c>(DriverInstanceId, Display, DriverType, DriverConfig)</c>
|
||||
/// tuples, where <c>DriverType</c> lets the TagModal dispatch to a per-driver-type typed config editor
|
||||
/// and <c>DriverConfig</c> feeds the live-browse address picker (e.g. the Galaxy gateway picker opens its
|
||||
/// browse session against the selected GalaxyMxGateway's config).</returns>
|
||||
Task<IReadOnlyList<(string DriverInstanceId, string Display, string DriverType, string DriverConfig)>> LoadTagDriversForEquipmentAsync(string equipmentId, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new equipment-bound tag. <c>FolderPath</c> is always <c>null</c> (decision #110 —
|
||||
|
||||
@@ -711,7 +711,7 @@ public sealed class UnsTreeService(IDbContextFactory<OtOpcUaConfigDbContext> dbF
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<(string DriverInstanceId, string Display, string DriverType)>> LoadTagDriversForEquipmentAsync(
|
||||
public async Task<IReadOnlyList<(string DriverInstanceId, string Display, string DriverType, string DriverConfig)>> LoadTagDriversForEquipmentAsync(
|
||||
string equipmentId,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
@@ -720,10 +720,12 @@ public sealed class UnsTreeService(IDbContextFactory<OtOpcUaConfigDbContext> dbF
|
||||
var equipmentCluster = await ResolveEquipmentClusterAsync(db, equipmentId, ct);
|
||||
if (equipmentCluster is null)
|
||||
{
|
||||
return Array.Empty<(string, string, string)>();
|
||||
return Array.Empty<(string, string, string, string)>();
|
||||
}
|
||||
|
||||
// Drivers in the equipment's cluster whose namespace is Equipment-kind (decision #110).
|
||||
// GalaxyMxGateway is an ordinary Equipment-kind driver post-de-split, so it surfaces here
|
||||
// alongside the PLC drivers; its DriverConfig feeds the Galaxy live-browse address picker.
|
||||
var equipmentNamespaceIds = await db.Namespaces
|
||||
.Where(n => n.ClusterId == equipmentCluster && n.Kind == NamespaceKind.Equipment)
|
||||
.Select(n => n.NamespaceId)
|
||||
@@ -732,11 +734,11 @@ public sealed class UnsTreeService(IDbContextFactory<OtOpcUaConfigDbContext> dbF
|
||||
var drivers = await db.DriverInstances
|
||||
.Where(d => d.ClusterId == equipmentCluster && equipmentNamespaceIds.Contains(d.NamespaceId))
|
||||
.OrderBy(d => d.DriverInstanceId)
|
||||
.Select(d => new { d.DriverInstanceId, d.Name, d.DriverType })
|
||||
.Select(d => new { d.DriverInstanceId, d.Name, d.DriverType, d.DriverConfig })
|
||||
.ToListAsync(ct);
|
||||
|
||||
return drivers
|
||||
.Select(d => (d.DriverInstanceId, Display: $"{d.DriverInstanceId} — {d.Name}", d.DriverType))
|
||||
.Select(d => (d.DriverInstanceId, Display: $"{d.DriverInstanceId} — {d.Name}", d.DriverType, d.DriverConfig))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
|
||||
@@ -8,17 +8,19 @@ namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns;
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that <see cref="UnsTreeService.LoadTagDriversForEquipmentAsync"/> surfaces each
|
||||
/// candidate driver's <c>DriverType</c> alongside its id and display string, so the UNS TagModal
|
||||
/// can later dispatch to a per-driver-type typed tag-config editor (F-uns-1).
|
||||
/// candidate driver's <c>DriverType</c> and <c>DriverConfig</c> alongside its id and display string,
|
||||
/// so the UNS TagModal can dispatch to a per-driver-type typed tag-config editor (F-uns-1) and feed
|
||||
/// the selected gateway's config to the Galaxy live-browse address picker.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class UnsTreeServiceTagDriversTests
|
||||
{
|
||||
/// <summary>
|
||||
/// A driver loaded for an equipment carries its <c>DriverType</c> in the returned tuple.
|
||||
/// A driver loaded for an equipment carries its <c>DriverType</c> and <c>DriverConfig</c> in the
|
||||
/// returned tuple.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task LoadTagDriversForEquipment_surfaces_driver_type()
|
||||
public async Task LoadTagDriversForEquipment_surfaces_driver_type_and_config()
|
||||
{
|
||||
var dbName = $"uns-tagdrivers-{Guid.NewGuid():N}";
|
||||
|
||||
@@ -57,7 +59,7 @@ public sealed class UnsTreeServiceTagDriversTests
|
||||
NamespaceId = "NS-EQ",
|
||||
Name = "equipment driver",
|
||||
DriverType = "ModbusTcp",
|
||||
DriverConfig = "{}",
|
||||
DriverConfig = """{"endpoint":"10.0.0.1:502"}""",
|
||||
});
|
||||
db.SaveChanges();
|
||||
}
|
||||
@@ -69,5 +71,7 @@ public sealed class UnsTreeServiceTagDriversTests
|
||||
drivers.Count.ShouldBe(1);
|
||||
drivers[0].DriverInstanceId.ShouldBe("DRV-EQ");
|
||||
drivers[0].DriverType.ShouldBe("ModbusTcp");
|
||||
// The picker (Galaxy live-browse) opens its session against the selected driver's config.
|
||||
drivers[0].DriverConfig.ShouldBe("""{"endpoint":"10.0.0.1:502"}""");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user