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 _tagModalVisible;
|
||||||
private bool _tagModalIsNew;
|
private bool _tagModalIsNew;
|
||||||
private TagEditDto? _tagModalExisting;
|
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. ---
|
// --- Virtual Tags tab state. _vtags is null until the tab is first activated. ---
|
||||||
private IReadOnlyList<EquipmentVirtualTagRow>? _vtags;
|
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
|
either, the owning equipment is fixed. On a successful save it raises OnSaved so the host can
|
||||||
refresh the equipment's children in place. *@
|
refresh the equipment's children in place. *@
|
||||||
@using System.ComponentModel.DataAnnotations
|
@using System.ComponentModel.DataAnnotations
|
||||||
|
@using System.Text.Json
|
||||||
@using Microsoft.AspNetCore.Components.Forms
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
@using ZB.MOM.WW.OtOpcUa.AdminUI.Uns
|
@using ZB.MOM.WW.OtOpcUa.AdminUI.Uns
|
||||||
@using ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors
|
@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
|
@using ZB.MOM.WW.OtOpcUa.Configuration.Enums
|
||||||
@inject IUnsTreeService Svc
|
@inject IUnsTreeService Svc
|
||||||
|
|
||||||
@@ -44,9 +47,9 @@
|
|||||||
<label class="form-label" for="tag-driver">Driver instance</label>
|
<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">
|
<InputSelect id="tag-driver" @bind-Value="_form.DriverInstanceId" @bind-Value:after="OnDriverChanged" class="form-select form-select-sm">
|
||||||
<option value="">— pick a driver —</option>
|
<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>
|
</InputSelect>
|
||||||
<ValidationMessage For="@(() => _form.DriverInstanceId)" />
|
<ValidationMessage For="@(() => _form.DriverInstanceId)" />
|
||||||
@@ -90,6 +93,35 @@
|
|||||||
{
|
{
|
||||||
<div class="form-text">Pick a driver above to configure this tag.</div>
|
<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)
|
else if (editorType is not null)
|
||||||
{
|
{
|
||||||
<DynamicComponent Type="editorType" Parameters="BuildEditorParameters()" />
|
<DynamicComponent Type="editorType" Parameters="BuildEditorParameters()" />
|
||||||
@@ -139,8 +171,10 @@
|
|||||||
/// <summary>The tag being edited, when <see cref="IsNew"/> is <c>false</c>.</summary>
|
/// <summary>The tag being edited, when <see cref="IsNew"/> is <c>false</c>.</summary>
|
||||||
[Parameter] public TagEditDto? Existing { get; set; }
|
[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>
|
/// <summary>The candidate drivers — scoped to the equipment's cluster by the host — as
|
||||||
[Parameter] public IReadOnlyList<(string Id, string Display, string DriverType)> Drivers { get; set; } = Array.Empty<(string, string, string)>();
|
/// <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>
|
/// <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; }
|
[Parameter] public EventCallback OnSaved { get; set; }
|
||||||
@@ -152,14 +186,47 @@
|
|||||||
private bool _busy;
|
private bool _busy;
|
||||||
private string? _error;
|
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.
|
// The DriverType of the currently-selected driver (drives editor dispatch). Null when no driver chosen.
|
||||||
private string? SelectedDriverType =>
|
private string? SelectedDriverType =>
|
||||||
Drivers.FirstOrDefault(d => d.Id == _form.DriverInstanceId).DriverType;
|
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 —
|
// 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
|
// 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.
|
// 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>
|
private IDictionary<string, object> BuildEditorParameters() => new Dictionary<string, object>
|
||||||
{
|
{
|
||||||
@@ -189,6 +256,28 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
_error = null;
|
_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()
|
private async Task SaveAsync()
|
||||||
|
|||||||
@@ -340,9 +340,11 @@ public interface IUnsTreeService
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="equipmentId">The equipment whose candidate drivers to load.</param>
|
/// <param name="equipmentId">The equipment whose candidate drivers to load.</param>
|
||||||
/// <param name="ct">A token to cancel the load.</param>
|
/// <param name="ct">A token to cancel the load.</param>
|
||||||
/// <returns>The eligible drivers projected to <c>(DriverInstanceId, Display, DriverType)</c> triples,
|
/// <returns>The eligible drivers projected to <c>(DriverInstanceId, Display, DriverType, DriverConfig)</c>
|
||||||
/// where <c>DriverType</c> lets the TagModal dispatch to a per-driver-type typed config editor.</returns>
|
/// tuples, where <c>DriverType</c> lets the TagModal dispatch to a per-driver-type typed config editor
|
||||||
Task<IReadOnlyList<(string DriverInstanceId, string Display, string DriverType)>> LoadTagDriversForEquipmentAsync(string equipmentId, CancellationToken ct = default);
|
/// 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>
|
/// <summary>
|
||||||
/// Creates a new equipment-bound tag. <c>FolderPath</c> is always <c>null</c> (decision #110 —
|
/// 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 />
|
/// <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,
|
string equipmentId,
|
||||||
CancellationToken ct = default)
|
CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
@@ -720,10 +720,12 @@ public sealed class UnsTreeService(IDbContextFactory<OtOpcUaConfigDbContext> dbF
|
|||||||
var equipmentCluster = await ResolveEquipmentClusterAsync(db, equipmentId, ct);
|
var equipmentCluster = await ResolveEquipmentClusterAsync(db, equipmentId, ct);
|
||||||
if (equipmentCluster is null)
|
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).
|
// 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
|
var equipmentNamespaceIds = await db.Namespaces
|
||||||
.Where(n => n.ClusterId == equipmentCluster && n.Kind == NamespaceKind.Equipment)
|
.Where(n => n.ClusterId == equipmentCluster && n.Kind == NamespaceKind.Equipment)
|
||||||
.Select(n => n.NamespaceId)
|
.Select(n => n.NamespaceId)
|
||||||
@@ -732,11 +734,11 @@ public sealed class UnsTreeService(IDbContextFactory<OtOpcUaConfigDbContext> dbF
|
|||||||
var drivers = await db.DriverInstances
|
var drivers = await db.DriverInstances
|
||||||
.Where(d => d.ClusterId == equipmentCluster && equipmentNamespaceIds.Contains(d.NamespaceId))
|
.Where(d => d.ClusterId == equipmentCluster && equipmentNamespaceIds.Contains(d.NamespaceId))
|
||||||
.OrderBy(d => d.DriverInstanceId)
|
.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);
|
.ToListAsync(ct);
|
||||||
|
|
||||||
return drivers
|
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();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,17 +8,19 @@ namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Verifies that <see cref="UnsTreeService.LoadTagDriversForEquipmentAsync"/> surfaces each
|
/// Verifies that <see cref="UnsTreeService.LoadTagDriversForEquipmentAsync"/> surfaces each
|
||||||
/// candidate driver's <c>DriverType</c> alongside its id and display string, so the UNS TagModal
|
/// candidate driver's <c>DriverType</c> and <c>DriverConfig</c> alongside its id and display string,
|
||||||
/// can later dispatch to a per-driver-type typed tag-config editor (F-uns-1).
|
/// 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>
|
/// </summary>
|
||||||
[Trait("Category", "Unit")]
|
[Trait("Category", "Unit")]
|
||||||
public sealed class UnsTreeServiceTagDriversTests
|
public sealed class UnsTreeServiceTagDriversTests
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task LoadTagDriversForEquipment_surfaces_driver_type()
|
public async Task LoadTagDriversForEquipment_surfaces_driver_type_and_config()
|
||||||
{
|
{
|
||||||
var dbName = $"uns-tagdrivers-{Guid.NewGuid():N}";
|
var dbName = $"uns-tagdrivers-{Guid.NewGuid():N}";
|
||||||
|
|
||||||
@@ -57,7 +59,7 @@ public sealed class UnsTreeServiceTagDriversTests
|
|||||||
NamespaceId = "NS-EQ",
|
NamespaceId = "NS-EQ",
|
||||||
Name = "equipment driver",
|
Name = "equipment driver",
|
||||||
DriverType = "ModbusTcp",
|
DriverType = "ModbusTcp",
|
||||||
DriverConfig = "{}",
|
DriverConfig = """{"endpoint":"10.0.0.1:502"}""",
|
||||||
});
|
});
|
||||||
db.SaveChanges();
|
db.SaveChanges();
|
||||||
}
|
}
|
||||||
@@ -69,5 +71,7 @@ public sealed class UnsTreeServiceTagDriversTests
|
|||||||
drivers.Count.ShouldBe(1);
|
drivers.Count.ShouldBe(1);
|
||||||
drivers[0].DriverInstanceId.ShouldBe("DRV-EQ");
|
drivers[0].DriverInstanceId.ShouldBe("DRV-EQ");
|
||||||
drivers[0].DriverType.ShouldBe("ModbusTcp");
|
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