feat(uns): lazy per-equipment tag + virtual-tag load
Add LoadEquipmentChildrenAsync to IUnsTreeService and UnsTreeService; returns
Tag nodes (ordered by Name) then VirtualTag nodes (ordered by Name) as leaf
nodes with ChildCount=0, HasLazyChildren=false, keys tag:{id}/vtag:{id}.
This commit is contained in:
@@ -4,7 +4,7 @@ namespace ZB.MOM.WW.OtOpcUa.AdminUI.Uns;
|
|||||||
/// Loads the structural portion of the unified-namespace (UNS) browse tree —
|
/// Loads the structural portion of the unified-namespace (UNS) browse tree —
|
||||||
/// Enterprise → Cluster → Area → Line → Equipment — from the config database.
|
/// Enterprise → Cluster → Area → Line → Equipment — from the config database.
|
||||||
/// Equipment children (tags/virtual tags) are summarised by count only and loaded
|
/// Equipment children (tags/virtual tags) are summarised by count only and loaded
|
||||||
/// lazily by the renderer; this service never returns Tag/VirtualTag leaf nodes.
|
/// lazily by the renderer via <see cref="LoadEquipmentChildrenAsync"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IUnsTreeService
|
public interface IUnsTreeService
|
||||||
{
|
{
|
||||||
@@ -16,4 +16,14 @@ public interface IUnsTreeService
|
|||||||
/// <param name="ct">A token to cancel the load.</param>
|
/// <param name="ct">A token to cancel the load.</param>
|
||||||
/// <returns>The enterprise root nodes, each populated down to equipment.</returns>
|
/// <returns>The enterprise root nodes, each populated down to equipment.</returns>
|
||||||
Task<IReadOnlyList<UnsNode>> LoadStructureAsync(CancellationToken ct = default);
|
Task<IReadOnlyList<UnsNode>> LoadStructureAsync(CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lazily loads the Tag and VirtualTag leaf nodes for a single equipment node.
|
||||||
|
/// Tags are returned first (ordered by Name), followed by VirtualTags (ordered by Name).
|
||||||
|
/// Leaf nodes carry <c>ChildCount = 0</c> and <c>HasLazyChildren = false</c>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="equipmentId">The equipment whose children to load.</param>
|
||||||
|
/// <param name="ct">A token to cancel the load.</param>
|
||||||
|
/// <returns>Tag nodes followed by VirtualTag nodes; empty if the equipment has none.</returns>
|
||||||
|
Task<IReadOnlyList<UnsNode>> LoadEquipmentChildrenAsync(string equipmentId, CancellationToken ct = default);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,4 +76,49 @@ public sealed class UnsTreeService(IDbContextFactory<OtOpcUaConfigDbContext> dbF
|
|||||||
|
|
||||||
return UnsTreeAssembly.Build(clusters, areas, lines, equipment);
|
return UnsTreeAssembly.Build(clusters, areas, lines, equipment);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<IReadOnlyList<UnsNode>> LoadEquipmentChildrenAsync(
|
||||||
|
string equipmentId,
|
||||||
|
CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await using var db = await dbFactory.CreateDbContextAsync(ct);
|
||||||
|
|
||||||
|
var tagNodes = await db.Tags
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(t => t.EquipmentId == equipmentId)
|
||||||
|
.OrderBy(t => t.Name)
|
||||||
|
.Select(t => new UnsNode
|
||||||
|
{
|
||||||
|
Kind = UnsNodeKind.Tag,
|
||||||
|
Key = $"tag:{t.TagId}",
|
||||||
|
DisplayName = $"{t.Name} ({t.DataType})",
|
||||||
|
EntityId = t.TagId,
|
||||||
|
ClusterId = null,
|
||||||
|
ChildCount = 0,
|
||||||
|
HasLazyChildren = false,
|
||||||
|
})
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
var vtagNodes = await db.VirtualTags
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(v => v.EquipmentId == equipmentId)
|
||||||
|
.OrderBy(v => v.Name)
|
||||||
|
.Select(v => new UnsNode
|
||||||
|
{
|
||||||
|
Kind = UnsNodeKind.VirtualTag,
|
||||||
|
Key = $"vtag:{v.VirtualTagId}",
|
||||||
|
DisplayName = $"{v.Name} (VirtualTag)",
|
||||||
|
EntityId = v.VirtualTagId,
|
||||||
|
ClusterId = null,
|
||||||
|
ChildCount = 0,
|
||||||
|
HasLazyChildren = false,
|
||||||
|
})
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
var result = new List<UnsNode>(tagNodes.Count + vtagNodes.Count);
|
||||||
|
result.AddRange(tagNodes);
|
||||||
|
result.AddRange(vtagNodes);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
using Shouldly;
|
||||||
|
using Xunit;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.AdminUI.Uns;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies <see cref="UnsTreeService.LoadEquipmentChildrenAsync"/> returns Tag leaf nodes
|
||||||
|
/// followed by VirtualTag leaf nodes, in Name order, with the correct keys and display names.
|
||||||
|
/// </summary>
|
||||||
|
[Trait("Category", "Unit")]
|
||||||
|
public sealed class UnsTreeServiceLazyTests
|
||||||
|
{
|
||||||
|
private static UnsTreeService SeededService()
|
||||||
|
{
|
||||||
|
var dbName = $"uns-lazy-{Guid.NewGuid():N}";
|
||||||
|
UnsTreeTestDb.SeedNamed(dbName);
|
||||||
|
return new UnsTreeService(UnsTreeTestDb.Factory(dbName));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tags come first, ordered by Name, then VirtualTags; keys follow the tag:/vtag: scheme
|
||||||
|
/// and the Tag display name embeds the DataType.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task LoadEquipmentChildren_returns_tags_then_vtags()
|
||||||
|
{
|
||||||
|
var service = SeededService();
|
||||||
|
|
||||||
|
var children = await service.LoadEquipmentChildrenAsync(UnsTreeTestDb.SeededEquipmentId);
|
||||||
|
|
||||||
|
// Seed has 2 tags (speed=Float, running=Boolean) + 1 vtag (computed).
|
||||||
|
// Tags are ordered by Name: "running" < "speed".
|
||||||
|
children.Count.ShouldBe(3);
|
||||||
|
|
||||||
|
var running = children[0];
|
||||||
|
running.Kind.ShouldBe(UnsNodeKind.Tag);
|
||||||
|
running.Key.ShouldBe("tag:TAG-2");
|
||||||
|
running.EntityId.ShouldBe("TAG-2");
|
||||||
|
running.ClusterId.ShouldBeNull();
|
||||||
|
running.DisplayName.ShouldContain("running");
|
||||||
|
running.DisplayName.ShouldContain("Boolean");
|
||||||
|
running.ChildCount.ShouldBe(0);
|
||||||
|
running.HasLazyChildren.ShouldBeFalse();
|
||||||
|
running.Children.ShouldBeEmpty();
|
||||||
|
|
||||||
|
var speed = children[1];
|
||||||
|
speed.Kind.ShouldBe(UnsNodeKind.Tag);
|
||||||
|
speed.Key.ShouldBe("tag:TAG-1");
|
||||||
|
speed.EntityId.ShouldBe("TAG-1");
|
||||||
|
speed.DisplayName.ShouldContain("speed");
|
||||||
|
speed.DisplayName.ShouldContain("Float");
|
||||||
|
|
||||||
|
var vtag = children[2];
|
||||||
|
vtag.Kind.ShouldBe(UnsNodeKind.VirtualTag);
|
||||||
|
vtag.Key.ShouldBe("vtag:VTAG-1");
|
||||||
|
vtag.EntityId.ShouldBe("VTAG-1");
|
||||||
|
vtag.ClusterId.ShouldBeNull();
|
||||||
|
vtag.DisplayName.ShouldContain("computed");
|
||||||
|
vtag.DisplayName.ShouldContain("VirtualTag");
|
||||||
|
vtag.ChildCount.ShouldBe(0);
|
||||||
|
vtag.HasLazyChildren.ShouldBeFalse();
|
||||||
|
vtag.Children.ShouldBeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>An equipment with no tags or virtual tags returns an empty list.</summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task LoadEquipmentChildren_empty_for_equipment_with_none()
|
||||||
|
{
|
||||||
|
// Use a fresh named store and add an equipment with no tags/vtags.
|
||||||
|
var dbName = $"uns-lazy-empty-{Guid.NewGuid():N}";
|
||||||
|
UnsTreeTestDb.SeedNamed(dbName);
|
||||||
|
|
||||||
|
// The orphan equipment id is not in the seeded fixture, so just use a novel id.
|
||||||
|
const string emptyEquipmentId = "EQ-NO-TAGS";
|
||||||
|
var service = new UnsTreeService(UnsTreeTestDb.Factory(dbName));
|
||||||
|
|
||||||
|
var children = await service.LoadEquipmentChildrenAsync(emptyEquipmentId);
|
||||||
|
|
||||||
|
children.ShouldBeEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user