feat(centralui): ParentExecutionId column, filter and parent drill-in on the Audit Log page
This commit is contained in:
@@ -41,6 +41,7 @@ public class AuditDrilldownDrawerTests : BunitContext
|
||||
string? extra = null,
|
||||
Guid? correlationId = null,
|
||||
Guid? executionId = null,
|
||||
Guid? parentExecutionId = null,
|
||||
string? errorMessage = null,
|
||||
string? errorDetail = null,
|
||||
string? target = "demo-target")
|
||||
@@ -53,6 +54,7 @@ public class AuditDrilldownDrawerTests : BunitContext
|
||||
Kind = kind,
|
||||
CorrelationId = correlationId,
|
||||
ExecutionId = executionId,
|
||||
ParentExecutionId = parentExecutionId,
|
||||
SourceSiteId = "plant-a",
|
||||
SourceInstanceId = "boiler-3",
|
||||
SourceScript = "OnAlarm.csx",
|
||||
@@ -258,6 +260,49 @@ public class AuditDrilldownDrawerTests : BunitContext
|
||||
Assert.Contains($"/audit/log?executionId={exec}", nav.Uri);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Drawer_NullParentExecutionId_HidesViewParentExecutionButton()
|
||||
{
|
||||
var ev = MakeEvent(parentExecutionId: null);
|
||||
|
||||
var cut = Render<AuditDrilldownDrawer>(p => p
|
||||
.Add(c => c.Event, ev)
|
||||
.Add(c => c.IsOpen, true));
|
||||
|
||||
Assert.DoesNotContain("data-test=\"view-parent-execution\"", cut.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Drawer_NonNullParentExecutionId_ShowsViewParentExecutionButton()
|
||||
{
|
||||
var ev = MakeEvent(parentExecutionId: Guid.Parse("bbbbbbbb-1111-2222-3333-444444444444"));
|
||||
|
||||
var cut = Render<AuditDrilldownDrawer>(p => p
|
||||
.Add(c => c.Event, ev)
|
||||
.Add(c => c.IsOpen, true));
|
||||
|
||||
Assert.Contains("data-test=\"view-parent-execution\"", cut.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ViewParentExecution_Navigates_WithExecutionIdQueryString()
|
||||
{
|
||||
// A routed (child) row drills in to its spawner: the "View parent
|
||||
// execution" action navigates to /audit/log?executionId={ParentExecutionId}
|
||||
// so the user sees the spawner execution's rows.
|
||||
var parent = Guid.Parse("eeeeeeee-dddd-cccc-bbbb-aaaaaaaaaaaa");
|
||||
var ev = MakeEvent(parentExecutionId: parent);
|
||||
|
||||
var cut = Render<AuditDrilldownDrawer>(p => p
|
||||
.Add(c => c.Event, ev)
|
||||
.Add(c => c.IsOpen, true));
|
||||
|
||||
cut.Find("[data-test=\"view-parent-execution\"]").Click();
|
||||
|
||||
var nav = (BunitNavigationManager)Services.GetRequiredService<NavigationManager>();
|
||||
Assert.Contains($"/audit/log?executionId={parent}", nav.Uri);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CopyAsCurl_InvokesClipboard_WithCurlString()
|
||||
{
|
||||
|
||||
@@ -62,6 +62,7 @@ public class AuditFilterBarTests : BunitContext
|
||||
"data-test=\"filter-target\"",
|
||||
"data-test=\"filter-actor\"",
|
||||
"data-test=\"filter-execution-id\"",
|
||||
"data-test=\"filter-parent-execution-id\"",
|
||||
"data-test=\"filter-errors-only\"",
|
||||
};
|
||||
foreach (var marker in markers)
|
||||
@@ -215,6 +216,42 @@ public class AuditFilterBarTests : BunitContext
|
||||
Assert.Null(captured!.ExecutionId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Apply_WithPastedParentExecutionId_MapsThroughToFilter()
|
||||
{
|
||||
// The operator pastes a Guid into the Parent execution ID box; Apply must
|
||||
// map it straight onto AuditLogQueryFilter.ParentExecutionId.
|
||||
var parentExecutionId = Guid.Parse("11112222-3333-4444-5555-666677778888");
|
||||
AuditLogQueryFilter? captured = null;
|
||||
var cut = Render<AuditFilterBar>(p => p
|
||||
.Add(c => c.OnFilterChanged, EventCallback.Factory.Create<AuditLogQueryFilter>(this, f => captured = f)));
|
||||
|
||||
cut.Find("[data-test=\"filter-parent-execution-id\"] input").Change(parentExecutionId.ToString());
|
||||
cut.Find("[data-test=\"filter-apply\"]").Click();
|
||||
|
||||
Assert.NotNull(captured);
|
||||
Assert.Equal(parentExecutionId, captured!.ParentExecutionId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Apply_WithBlankOrUnparseableParentExecutionId_LeavesFilterParentExecutionIdNull()
|
||||
{
|
||||
// Lax parsing: a blank box yields no constraint; garbage text likewise.
|
||||
AuditLogQueryFilter? captured = null;
|
||||
var cut = Render<AuditFilterBar>(p => p
|
||||
.Add(c => c.OnFilterChanged, EventCallback.Factory.Create<AuditLogQueryFilter>(this, f => captured = f)));
|
||||
|
||||
// Blank — never typed into.
|
||||
cut.Find("[data-test=\"filter-apply\"]").Click();
|
||||
Assert.NotNull(captured);
|
||||
Assert.Null(captured!.ParentExecutionId);
|
||||
|
||||
// Unparseable paste — still dropped, no error.
|
||||
cut.Find("[data-test=\"filter-parent-execution-id\"] input").Change("not-a-guid");
|
||||
cut.Find("[data-test=\"filter-apply\"]").Click();
|
||||
Assert.Null(captured!.ParentExecutionId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TimeRange_LastHour_PopulatesFromUtc_ApproxOneHourAgo()
|
||||
{
|
||||
|
||||
@@ -22,7 +22,7 @@ public class AuditResultsGridTests : BunitContext
|
||||
private readonly IAuditLogQueryService _service;
|
||||
private readonly List<(AuditLogQueryFilter Filter, AuditLogPaging? Paging)> _calls = new();
|
||||
|
||||
private static AuditEvent MakeEvent(DateTime occurredAtUtc, AuditStatus status, AuditChannel channel = AuditChannel.ApiOutbound, AuditKind kind = AuditKind.ApiCall, string? site = "plant-a", Guid? executionId = null)
|
||||
private static AuditEvent MakeEvent(DateTime occurredAtUtc, AuditStatus status, AuditChannel channel = AuditChannel.ApiOutbound, AuditKind kind = AuditKind.ApiCall, string? site = "plant-a", Guid? executionId = null, Guid? parentExecutionId = null)
|
||||
=> new()
|
||||
{
|
||||
EventId = Guid.NewGuid(),
|
||||
@@ -34,6 +34,7 @@ public class AuditResultsGridTests : BunitContext
|
||||
Target = "demo-target",
|
||||
Actor = "tester",
|
||||
ExecutionId = executionId,
|
||||
ParentExecutionId = parentExecutionId,
|
||||
DurationMs = 42,
|
||||
HttpStatus = status == AuditStatus.Delivered ? 200 : 500,
|
||||
ErrorMessage = status == AuditStatus.Failed ? "boom — unreachable" : null,
|
||||
@@ -165,6 +166,49 @@ public class AuditResultsGridTests : BunitContext
|
||||
Assert.Empty(cut.FindAll($"[data-test=\"execution-id-{row.EventId}\"]"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Render_IncludesParentExecutionIdColumn()
|
||||
{
|
||||
StubPage(new List<AuditEvent>
|
||||
{
|
||||
MakeEvent(DateTime.UtcNow.AddMinutes(-1), AuditStatus.Delivered),
|
||||
});
|
||||
|
||||
var cut = Render<AuditResultsGrid>(p => p.Add(c => c.Filter, new AuditLogQueryFilter()));
|
||||
|
||||
// The ParentExecutionId column header is present alongside the spec columns.
|
||||
Assert.Contains("data-test=\"col-header-ParentExecutionId\"", cut.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParentExecutionId_NonNullRow_RendersShortMonospaceValue()
|
||||
{
|
||||
var parentExecutionId = Guid.Parse("fedcba98-2222-3333-4444-555555555555");
|
||||
var row = MakeEvent(DateTime.UtcNow.AddMinutes(-1), AuditStatus.Delivered, parentExecutionId: parentExecutionId);
|
||||
StubPage(new[] { row });
|
||||
|
||||
var cut = Render<AuditResultsGrid>(p => p.Add(c => c.Filter, new AuditLogQueryFilter()));
|
||||
|
||||
var cell = cut.Find($"[data-test=\"parent-execution-id-{row.EventId}\"]");
|
||||
// Short form: first 8 hex digits of the "N" form — mirrors ExecutionId.
|
||||
Assert.Equal("fedcba98", cell.TextContent.Trim());
|
||||
// Monospace presentation; full value retained in the title attribute.
|
||||
Assert.Contains("font-monospace", cell.GetAttribute("class") ?? string.Empty);
|
||||
Assert.Equal(parentExecutionId.ToString(), cell.GetAttribute("title"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParentExecutionId_NullRow_RendersBlankPlaceholder_NoParentExecutionIdCell()
|
||||
{
|
||||
var row = MakeEvent(DateTime.UtcNow.AddMinutes(-1), AuditStatus.Delivered, parentExecutionId: null);
|
||||
StubPage(new[] { row });
|
||||
|
||||
var cut = Render<AuditResultsGrid>(p => p.Add(c => c.Filter, new AuditLogQueryFilter()));
|
||||
|
||||
// A null ParentExecutionId renders the em-dash placeholder, not a value cell.
|
||||
Assert.Empty(cut.FindAll($"[data-test=\"parent-execution-id-{row.EventId}\"]"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Status_FailedRow_HasErrorBadgeClass()
|
||||
{
|
||||
@@ -193,7 +237,8 @@ public class AuditResultsGridTests : BunitContext
|
||||
private static readonly string[] DefaultOrder =
|
||||
{
|
||||
"OccurredAtUtc", "Site", "Channel", "Kind", "Status",
|
||||
"Target", "Actor", "DurationMs", "HttpStatus", "ErrorMessage",
|
||||
"Target", "Actor", "ExecutionId", "ParentExecutionId",
|
||||
"DurationMs", "HttpStatus", "ErrorMessage",
|
||||
};
|
||||
|
||||
private static int HeaderIndex(string markup, string key)
|
||||
|
||||
Reference in New Issue
Block a user