feat(uns): per-equipment tag/virtual-tag list service methods

This commit is contained in:
Joseph Doherty
2026-06-11 14:19:46 -04:00
parent 7d91737dac
commit 7c22861598
4 changed files with 96 additions and 0 deletions
@@ -0,0 +1,10 @@
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Uns;
/// <summary>A tag row for the equipment page's Tags tab table — display columns plus the id used to
/// open the edit modal. RowVersion is re-read fresh on delete (matching the tree's delete path).</summary>
public sealed record EquipmentTagRow(string TagId, string Name, string DriverInstanceId, string DataType, TagAccessLevel AccessLevel);
/// <summary>A virtual-tag row for the equipment page's Virtual Tags tab table.</summary>
public sealed record EquipmentVirtualTagRow(string VirtualTagId, string Name, string DataType, string ScriptId, bool Enabled);
@@ -127,6 +127,28 @@ public interface IUnsTreeService
/// <returns>Tag nodes followed by VirtualTag nodes; empty if the equipment has none.</returns>
Task<IReadOnlyList<UnsNode>> LoadEquipmentChildrenAsync(string equipmentId, CancellationToken ct = default);
/// <summary>
/// Loads the driver tags bound to a single equipment as flat row projections for the equipment
/// page's Tags tab table, ordered by Name. Each row carries the display columns plus the
/// <c>TagId</c> the table uses to open the edit modal. Reads untracked. Returns an empty list when
/// the equipment has no tags.
/// </summary>
/// <param name="equipmentId">The equipment whose tags to load.</param>
/// <param name="ct">A token to cancel the load.</param>
/// <returns>The equipment's tag rows ordered by Name; empty if it has none.</returns>
Task<IReadOnlyList<EquipmentTagRow>> LoadTagsForEquipmentAsync(string equipmentId, CancellationToken ct = default);
/// <summary>
/// Loads the virtual tags scoped to a single equipment as flat row projections for the equipment
/// page's Virtual Tags tab table, ordered by Name. Each row carries the display columns plus the
/// <c>VirtualTagId</c> the table uses to open the edit modal. Reads untracked. Returns an empty list
/// when the equipment has no virtual tags.
/// </summary>
/// <param name="equipmentId">The equipment whose virtual tags to load.</param>
/// <param name="ct">A token to cancel the load.</param>
/// <returns>The equipment's virtual-tag rows ordered by Name; empty if it has none.</returns>
Task<IReadOnlyList<EquipmentVirtualTagRow>> LoadVirtualTagsForEquipmentAsync(string equipmentId, CancellationToken ct = default);
/// <summary>
/// Loads a single UNS area projected for editing, or <c>null</c> if it no longer exists.
/// Reads untracked and captures the current concurrency token for last-write-wins saves.
@@ -127,6 +127,28 @@ public sealed class UnsTreeService(IDbContextFactory<OtOpcUaConfigDbContext> dbF
return result;
}
/// <inheritdoc />
public async Task<IReadOnlyList<EquipmentTagRow>> LoadTagsForEquipmentAsync(string equipmentId, CancellationToken ct = default)
{
await using var db = await dbFactory.CreateDbContextAsync(ct);
return await db.Tags.AsNoTracking()
.Where(t => t.EquipmentId == equipmentId)
.OrderBy(t => t.Name)
.Select(t => new EquipmentTagRow(t.TagId, t.Name, t.DriverInstanceId, t.DataType, t.AccessLevel))
.ToListAsync(ct);
}
/// <inheritdoc />
public async Task<IReadOnlyList<EquipmentVirtualTagRow>> LoadVirtualTagsForEquipmentAsync(string equipmentId, CancellationToken ct = default)
{
await using var db = await dbFactory.CreateDbContextAsync(ct);
return await db.VirtualTags.AsNoTracking()
.Where(v => v.EquipmentId == equipmentId)
.OrderBy(v => v.Name)
.Select(v => new EquipmentVirtualTagRow(v.VirtualTagId, v.Name, v.DataType, v.ScriptId, v.Enabled))
.ToListAsync(ct);
}
/// <inheritdoc />
public async Task<AreaEditDto?> LoadAreaAsync(string unsAreaId, CancellationToken ct = default)
{
@@ -0,0 +1,42 @@
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.AdminUI.Uns;
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns;
[Trait("Category", "Unit")]
public sealed class UnsTreeServiceEquipmentChildRowsTests
{
private static UnsTreeService SeededService()
{
var dbName = $"uns-childrows-{Guid.NewGuid():N}";
UnsTreeTestDb.SeedNamed(dbName);
return new UnsTreeService(UnsTreeTestDb.Factory(dbName));
}
[Fact]
public async Task LoadTagsForEquipment_returns_tags_in_name_order_scoped()
{
var rows = await SeededService().LoadTagsForEquipmentAsync(UnsTreeTestDb.SeededEquipmentId);
rows.Count.ShouldBe(2); // the EquipmentId=null orphan tag is excluded
rows[0].TagId.ShouldBe("TAG-2"); // "running" < "speed"
rows[0].Name.ShouldBe("running");
rows[0].DataType.ShouldBe("Boolean");
rows[1].TagId.ShouldBe("TAG-1");
rows[1].DataType.ShouldBe("Float");
}
[Fact]
public async Task LoadVirtualTagsForEquipment_returns_vtags_in_name_order()
{
var rows = await SeededService().LoadVirtualTagsForEquipmentAsync(UnsTreeTestDb.SeededEquipmentId);
rows.Count.ShouldBe(1);
rows[0].VirtualTagId.ShouldBe("VTAG-1");
rows[0].Name.ShouldBe("computed");
rows[0].DataType.ShouldBe("Double");
}
[Fact]
public async Task LoadTagsForEquipment_empty_for_unknown_equipment()
=> (await SeededService().LoadTagsForEquipmentAsync("EQ-NONE")).ShouldBeEmpty();
}