perf(adminui): raw tree renders tag rows from a projection without TagConfig

This commit is contained in:
Joseph Doherty
2026-08-16 04:04:32 -04:00
parent fa36d64e8f
commit ba977dbe0d
2 changed files with 44 additions and 1 deletions
@@ -173,6 +173,7 @@ public sealed class RawTreeService(IDbContextFactory<OtOpcUaConfigDbContext> dbF
var tags = await db.Tags.AsNoTracking()
.Where(t => t.DeviceId == deviceId && t.TagGroupId == null)
.OrderBy(t => t.Name)
.Select(t => new TagRow(t.TagId, t.Name, t.RowVersion))
.ToListAsync(ct);
var nodes = new List<RawNode>(groups.Count + tags.Count);
@@ -194,6 +195,7 @@ public sealed class RawTreeService(IDbContextFactory<OtOpcUaConfigDbContext> dbF
var tags = await db.Tags.AsNoTracking()
.Where(t => t.TagGroupId == groupId)
.OrderBy(t => t.Name)
.Select(t => new TagRow(t.TagId, t.Name, t.RowVersion))
.ToListAsync(ct);
var nodes = new List<RawNode>(subGroups.Count + tags.Count);
@@ -299,7 +301,13 @@ public sealed class RawTreeService(IDbContextFactory<OtOpcUaConfigDbContext> dbF
}).ToList();
}
private static RawNode BuildTagNode(RawNode parent, Tag t) => new()
/// <summary>
/// The only columns a tree row needs from a <see cref="Tag" />. Projecting to this keeps the
/// <c>TagConfig</c> nvarchar(max) JSON blob out of the tree's SELECT — it is never rendered.
/// </summary>
private sealed record TagRow(string TagId, string Name, byte[] RowVersion);
private static RawNode BuildTagNode(RawNode parent, TagRow t) => new()
{
Kind = RawNodeKind.Tag,
Key = $"tag:{t.TagId}",
@@ -102,6 +102,41 @@ public sealed class RawTreeServiceTests
(await service.LoadChildrenAsync(tags[0])).ShouldBeEmpty(); // leaf
}
/// <summary>
/// Tag rows are read through a narrow projection that leaves the <c>TagConfig</c> JSON blob in the
/// database, so this pins that the three fields a tree row actually renders — identity, name and the
/// concurrency token the mutation handles round-trip — still arrive intact, from both tag loaders
/// (a tag under a group, and a tag hanging directly off a device).
/// </summary>
[Fact]
public async Task LoadChildren_yields_tag_rows_carrying_id_name_and_row_version()
{
var (service, dbName) = Seeded();
var cluster = (await service.LoadRootsAsync()).Single().Children.Single();
// Group-child loader: two tags under "Motors".
var folder = (await service.LoadChildrenAsync(cluster)).Single(n => n.Kind == RawNodeKind.Folder);
var driver = (await service.LoadChildrenAsync(folder)).ShouldHaveSingleItem();
var device = (await service.LoadChildrenAsync(driver)).ShouldHaveSingleItem();
var group = (await service.LoadChildrenAsync(device)).ShouldHaveSingleItem();
var plain = (await service.LoadChildrenAsync(group)).Single(t => t.EntityId == RawTreeTestDb.PlainTagId);
plain.DisplayName.ShouldBe("state");
plain.RowVersion.ShouldBe(
RowVersionOf(dbName, db => db.Tags.Single(t => t.TagId == RawTreeTestDb.PlainTagId).RowVersion));
// Device-child loader: the cluster-root device's own tag.
var rootDriver = (await service.LoadChildrenAsync(cluster)).Single(n => n.Kind == RawNodeKind.Driver);
var rootDevice = (await service.LoadChildrenAsync(rootDriver)).ShouldHaveSingleItem();
var referenced = (await service.LoadChildrenAsync(rootDevice))
.Single(t => t.EntityId == RawTreeTestDb.ReferencedTagId);
referenced.Kind.ShouldBe(RawNodeKind.Tag);
referenced.DisplayName.ShouldBe("temperature");
referenced.RowVersion.ShouldBe(
RowVersionOf(dbName, db => db.Tags.Single(t => t.TagId == RawTreeTestDb.ReferencedTagId).RowVersion));
}
// -------------------------------------------------------------------------------- Create/happy
[Fact]