feat(centralui): ParentExecutionId column, filter and parent drill-in on the Audit Log page
This commit is contained in:
@@ -58,6 +58,9 @@
|
||||
<dt class="col-4 text-muted fw-normal">ExecutionId</dt>
|
||||
<dd class="col-8 font-monospace" data-test="field-ExecutionId">@(Event.ExecutionId?.ToString() ?? "—")</dd>
|
||||
|
||||
<dt class="col-4 text-muted fw-normal">ParentExecutionId</dt>
|
||||
<dd class="col-8 font-monospace" data-test="field-ParentExecutionId">@(Event.ParentExecutionId?.ToString() ?? "—")</dd>
|
||||
|
||||
<dt class="col-4 text-muted fw-normal">OccurredAtUtc</dt>
|
||||
<dd class="col-8 font-monospace" data-test="field-OccurredAtUtc">@FormatTimestamp(Event.OccurredAtUtc)</dd>
|
||||
|
||||
@@ -162,6 +165,14 @@
|
||||
View this execution
|
||||
</button>
|
||||
}
|
||||
@if (Event.ParentExecutionId is not null)
|
||||
{
|
||||
<button class="btn btn-outline-secondary btn-sm"
|
||||
data-test="view-parent-execution"
|
||||
@onclick="ViewParentExecution">
|
||||
View parent execution
|
||||
</button>
|
||||
}
|
||||
<button class="btn btn-primary btn-sm ms-auto"
|
||||
data-test="drawer-close-footer"
|
||||
@onclick="HandleClose">
|
||||
|
||||
@@ -49,7 +49,10 @@ namespace ScadaLink.CentralUI.Components.Audit;
|
||||
/// the "Show all events" button navigates to
|
||||
/// <c>/audit/log?correlationId={id}</c>. Likewise, when
|
||||
/// <see cref="AuditEvent.ExecutionId"/> is set the "View this execution"
|
||||
/// button navigates to <c>/audit/log?executionId={id}</c>. Both are deep
|
||||
/// button navigates to <c>/audit/log?executionId={id}</c>. Likewise, when
|
||||
/// <see cref="AuditEvent.ParentExecutionId"/> is set the "View parent
|
||||
/// execution" button navigates to <c>/audit/log?executionId={parentId}</c>
|
||||
/// — the spawner's id used as the per-run drill-in target. All are deep
|
||||
/// links the Audit Log page deserializes on init (Bundle D) and auto-loads.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
@@ -291,6 +294,21 @@ public partial class AuditDrilldownDrawer
|
||||
Navigation.NavigateTo(uri);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drill-in to the spawner execution: a routed (child) row carries a non-null
|
||||
/// <see cref="AuditEvent.ParentExecutionId"/>. Navigates to
|
||||
/// <c>/audit/log?executionId={ParentExecutionId}</c> so the user sees the
|
||||
/// spawner execution's own rows — the parent's id becomes the <c>?executionId=</c>
|
||||
/// drill-in target. The button is only rendered when
|
||||
/// <see cref="AuditEvent.ParentExecutionId"/> is non-null, so this is total.
|
||||
/// </summary>
|
||||
private void ViewParentExecution()
|
||||
{
|
||||
if (Event?.ParentExecutionId is not { } parentExec) return;
|
||||
var uri = $"/audit/log?executionId={parentExec}";
|
||||
Navigation.NavigateTo(uri);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Build a cURL command from an audit event. The URL comes from
|
||||
/// <c>Target</c>; when the RequestSummary parses as
|
||||
|
||||
@@ -127,6 +127,16 @@
|
||||
placeholder="paste GUID…" @bind="_model.ExecutionId" />
|
||||
</div>
|
||||
|
||||
@* ParentExecutionId is an exact-match Guid filter — the operator pastes
|
||||
the spawner execution's id to find every run it spawned. Lax-parsed
|
||||
in ToFilter, exactly like ExecutionId above. *@
|
||||
<div class="col-auto" data-test="filter-parent-execution-id">
|
||||
<label class="form-label small mb-1" for="audit-parent-execution-id">Parent execution ID</label>
|
||||
<input id="audit-parent-execution-id" type="text"
|
||||
class="form-control form-control-sm font-monospace"
|
||||
placeholder="paste GUID…" @bind="_model.ParentExecutionId" />
|
||||
</div>
|
||||
|
||||
<div class="col-auto" data-test="filter-errors-only">
|
||||
<div class="form-check mb-1">
|
||||
<input class="form-check-input" type="checkbox" id="audit-errors-only"
|
||||
|
||||
@@ -136,6 +136,7 @@ public partial class AuditFilterBar
|
||||
_model.TargetSearch = string.Empty;
|
||||
_model.ActorSearch = string.Empty;
|
||||
_model.ExecutionId = string.Empty;
|
||||
_model.ParentExecutionId = string.Empty;
|
||||
_model.ErrorsOnly = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,15 @@ public sealed class AuditQueryModel
|
||||
/// </summary>
|
||||
public string ExecutionId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Paste-in ParentExecutionId filter — the operator pastes the spawner
|
||||
/// execution's Guid to find every run it spawned. Stored as free text;
|
||||
/// <see cref="ToFilter"/> lax-parses it through
|
||||
/// <see cref="Guid.TryParse(string?, out Guid)"/> so a blank or unparseable
|
||||
/// value simply yields no constraint, mirroring <see cref="ExecutionId"/>.
|
||||
/// </summary>
|
||||
public string ParentExecutionId { get; set; } = string.Empty;
|
||||
|
||||
public bool ErrorsOnly { get; set; }
|
||||
|
||||
/// <summary>
|
||||
@@ -128,6 +137,11 @@ public sealed class AuditQueryModel
|
||||
? parsedExecutionId
|
||||
: null;
|
||||
|
||||
// Same lax-parse contract for the pasted ParentExecutionId.
|
||||
Guid? parentExecutionId = Guid.TryParse(ParentExecutionId, out var parsedParentExecutionId)
|
||||
? parsedParentExecutionId
|
||||
: null;
|
||||
|
||||
return new AuditLogQueryFilter(
|
||||
Channels: Channels.Count > 0 ? Channels.ToArray() : null,
|
||||
Kinds: Kinds.Count > 0 ? Kinds.ToArray() : null,
|
||||
@@ -137,6 +151,7 @@ public sealed class AuditQueryModel
|
||||
Actor: string.IsNullOrWhiteSpace(ActorSearch) ? null : ActorSearch.Trim(),
|
||||
CorrelationId: null,
|
||||
ExecutionId: executionId,
|
||||
ParentExecutionId: parentExecutionId,
|
||||
FromUtc: fromUtc,
|
||||
ToUtc: toUtc);
|
||||
}
|
||||
|
||||
@@ -132,6 +132,18 @@
|
||||
<span class="small text-muted">—</span>
|
||||
}
|
||||
break;
|
||||
case "ParentExecutionId":
|
||||
@if (row.ParentExecutionId is { } parentExecutionId)
|
||||
{
|
||||
<span class="small font-monospace"
|
||||
data-test="parent-execution-id-@row.EventId"
|
||||
title="@parentExecutionId">@ShortGuid(parentExecutionId)</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="small text-muted">—</span>
|
||||
}
|
||||
break;
|
||||
case "DurationMs":
|
||||
<span class="small font-monospace">@(row.DurationMs?.ToString() ?? "—")</span>
|
||||
break;
|
||||
|
||||
@@ -11,7 +11,8 @@ namespace ScadaLink.CentralUI.Components.Audit;
|
||||
/// Keyset-paged results grid for the central Audit Log page (#23 M7-T3).
|
||||
/// Renders the columns named in Component-AuditLog.md §10 — OccurredAtUtc,
|
||||
/// Site, Channel, Kind, Status, Target, Actor, DurationMs, HttpStatus,
|
||||
/// ErrorMessage — plus the ExecutionId per-run correlation column. Talks to
|
||||
/// ErrorMessage — plus the ExecutionId per-run correlation column and the
|
||||
/// ParentExecutionId spawner-correlation column. Talks to
|
||||
/// <see cref="Services.IAuditLogQueryService"/>
|
||||
/// — never to <c>IAuditLogRepository</c> directly — so tests can stub the data
|
||||
/// source without standing up EF Core.
|
||||
@@ -123,6 +124,7 @@ public partial class AuditResultsGrid : IAsyncDisposable
|
||||
("Target", "Target"),
|
||||
("Actor", "Actor"),
|
||||
("ExecutionId", "ExecutionId"),
|
||||
("ParentExecutionId", "ParentExecutionId"),
|
||||
("DurationMs", "DurationMs"),
|
||||
("HttpStatus", "HttpStatus"),
|
||||
("ErrorMessage", "ErrorMessage"),
|
||||
|
||||
@@ -23,7 +23,9 @@ namespace ScadaLink.CentralUI.Components.Pages.Audit;
|
||||
/// <c>?instance=</c> are read on initialization. Bundle E (M7-T13) extends
|
||||
/// this with <c>?status=</c> so the Health-dashboard Audit error-rate tile can
|
||||
/// drill in to <c>?status=Failed</c>. The ExecutionId follow-up adds
|
||||
/// <c>?executionId=</c> for the "View this execution" drill-in. When any param is present we allocate a
|
||||
/// <c>?executionId=</c> for the "View this execution" drill-in, and the
|
||||
/// ParentExecutionId follow-up adds <c>?parentExecutionId=</c> for the
|
||||
/// "View parent execution" drill-in. When any param is present we allocate a
|
||||
/// fresh <see cref="AuditLogQueryFilter"/> and assign it to
|
||||
/// <see cref="_currentFilter"/>, which kicks the results grid into auto-load
|
||||
/// without the user clicking Apply. Unknown values (e.g. an invalid enum name)
|
||||
@@ -71,6 +73,15 @@ public partial class AuditLogPage
|
||||
executionId = parsedExec;
|
||||
}
|
||||
|
||||
// ?parentExecutionId= constrains to runs spawned by a given execution.
|
||||
// Lax-parsed like ?executionId=: an unparseable value is silently dropped.
|
||||
Guid? parentExecutionId = null;
|
||||
if (query.TryGetValue("parentExecutionId", out var parentExecValues)
|
||||
&& Guid.TryParse(parentExecValues.ToString(), out var parsedParentExec))
|
||||
{
|
||||
parentExecutionId = parsedParentExec;
|
||||
}
|
||||
|
||||
string? target = null;
|
||||
if (query.TryGetValue("target", out var targetValues))
|
||||
{
|
||||
@@ -128,7 +139,8 @@ public partial class AuditLogPage
|
||||
// auto-loads. Pure ?instance= deep links (UI-only) do not trigger auto-load
|
||||
// because the filter contract has no instance column — the user still needs
|
||||
// to refine + Apply for those.
|
||||
if (correlationId is null && executionId is null && target is null && actor is null
|
||||
if (correlationId is null && executionId is null && parentExecutionId is null
|
||||
&& target is null && actor is null
|
||||
&& sites is null && channels is null && kinds is null && statuses is null)
|
||||
{
|
||||
return;
|
||||
@@ -142,7 +154,8 @@ public partial class AuditLogPage
|
||||
Target: target,
|
||||
Actor: actor,
|
||||
CorrelationId: correlationId,
|
||||
ExecutionId: executionId);
|
||||
ExecutionId: executionId,
|
||||
ParentExecutionId: parentExecutionId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -252,6 +265,10 @@ public partial class AuditLogPage
|
||||
{
|
||||
parts.Add(new("executionId", exec.ToString()));
|
||||
}
|
||||
if (filter.ParentExecutionId is { } parentExec)
|
||||
{
|
||||
parts.Add(new("parentExecutionId", parentExec.ToString()));
|
||||
}
|
||||
if (filter.FromUtc is { } from)
|
||||
{
|
||||
parts.Add(new("from", from.ToString("O", CultureInfo.InvariantCulture)));
|
||||
|
||||
Reference in New Issue
Block a user