feat(ui): add Node column + filter to AuditLog grid

This commit is contained in:
Joseph Doherty
2026-05-23 18:01:36 -04:00
parent 466e1454fe
commit bb29d65a94
19 changed files with 392 additions and 8 deletions

View File

@@ -282,6 +282,70 @@ public class AuditLogQueryServiceTests
Assert.NotSame(resolvedRepos[0], resolvedRepos[1]);
}
// ─────────────────────────────────────────────────────────────────────────
// Task 15: SourceNode filter forwarding + distinct-nodes service contract.
// ─────────────────────────────────────────────────────────────────────────
[Fact]
public async Task QueryAsync_ForwardsSourceNodesFilter_ToRepository()
{
// The Audit Log page's new Node multi-select pushes its chip set into
// AuditLogQueryFilter.SourceNodes; the service must thread it through
// unchanged so the repository's IN-list applies.
var repo = Substitute.For<IAuditLogRepository>();
var filter = new AuditLogQueryFilter(
SourceNodes: new[] { "central-a", "site-plant-a-node-a" });
repo.QueryAsync(filter, Arg.Any<AuditLogPaging>(), Arg.Any<CancellationToken>())
.Returns(Task.FromResult<IReadOnlyList<AuditEvent>>(Array.Empty<AuditEvent>()));
var sut = new AuditLogQueryService(repo, EmptyAggregator());
await sut.QueryAsync(filter);
await repo.Received(1).QueryAsync(
Arg.Is<AuditLogQueryFilter>(f =>
f.SourceNodes != null
&& f.SourceNodes.Count == 2
&& f.SourceNodes.Contains("central-a")
&& f.SourceNodes.Contains("site-plant-a-node-a")),
Arg.Any<AuditLogPaging>(),
Arg.Any<CancellationToken>());
}
[Fact]
public async Task GetDistinctSourceNodesAsync_ForwardsToRepository_OnFirstCall()
{
var repo = Substitute.For<IAuditLogRepository>();
var expected = new[] { "central-a", "central-b", "site-plant-a-node-a" };
repo.GetDistinctSourceNodesAsync(Arg.Any<CancellationToken>())
.Returns(Task.FromResult<IReadOnlyList<string>>(expected));
var sut = new AuditLogQueryService(repo, EmptyAggregator());
var result = await sut.GetDistinctSourceNodesAsync();
Assert.Equal(expected, result);
await repo.Received(1).GetDistinctSourceNodesAsync(Arg.Any<CancellationToken>());
}
[Fact]
public async Task GetDistinctSourceNodesAsync_CachesSnapshot_AcrossRepeatedCalls()
{
// Two back-to-back calls within the 60s TTL must hit the repository
// exactly once — the filter bar should never produce N DB hits when
// the operator opens it twice in quick succession.
var repo = Substitute.For<IAuditLogRepository>();
repo.GetDistinctSourceNodesAsync(Arg.Any<CancellationToken>())
.Returns(Task.FromResult<IReadOnlyList<string>>(new[] { "central-a" }));
var sut = new AuditLogQueryService(repo, EmptyAggregator());
var first = await sut.GetDistinctSourceNodesAsync();
var second = await sut.GetDistinctSourceNodesAsync();
Assert.Equal(first, second);
await repo.Received(1).GetDistinctSourceNodesAsync(Arg.Any<CancellationToken>());
}
private static SiteHealthState StateWithBacklog(string siteId, int? pending)
{
SiteAuditBacklogSnapshot? backlog = pending.HasValue