feat(centralui): execution-chain tree view on the Audit Log page
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
@page "/audit/execution-tree"
|
||||
@attribute [Authorize(Policy = AuthorizationPolicies.OperationalAudit)]
|
||||
@using ScadaLink.CentralUI.Components.Audit
|
||||
@using ScadaLink.CentralUI.Services
|
||||
@using ScadaLink.Commons.Types.Audit
|
||||
@using ScadaLink.Security
|
||||
@inject IAuditLogQueryService AuditLogQueryService
|
||||
|
||||
<PageTitle>Execution Chain</PageTitle>
|
||||
|
||||
@* Execution-chain tree view (Audit Log ParentExecutionId feature, Task 10).
|
||||
A drill-in target reached from the Audit Log drawer's "View execution chain"
|
||||
action: /audit/execution-tree?executionId={guid}. The page parses the id,
|
||||
asks the query service for the whole chain (flat ExecutionTreeNode list), and
|
||||
hands it to the recursive ExecutionTree component. There is deliberately NO
|
||||
nav-menu entry — this page is only meaningful in the context of a specific
|
||||
execution, so it is reachable only via drill-in (the Audit nav group keeps
|
||||
just the Audit Log + Configuration Audit Log pages). *@
|
||||
|
||||
<div class="container-fluid mt-3">
|
||||
<h1 class="h4 mb-1">Execution Chain</h1>
|
||||
<p class="text-muted small mb-3">
|
||||
The full chain of script / inbound-request executions linked by
|
||||
<span class="font-monospace">ParentExecutionId</span>, rooted at the
|
||||
topmost ancestor. Select an execution to open the Audit Log filtered to
|
||||
its rows.
|
||||
</p>
|
||||
|
||||
@if (_executionId is null)
|
||||
{
|
||||
@* No (or unparseable) ?executionId= — render guidance rather than an
|
||||
empty tree. Mirrors the Audit Log page's silently-drop contract. *@
|
||||
<div class="alert alert-secondary small" data-test="execution-tree-no-id">
|
||||
No execution selected. Open this view from an audit row's
|
||||
<strong>View execution chain</strong> action.
|
||||
</div>
|
||||
}
|
||||
else if (_loading)
|
||||
{
|
||||
<div class="text-muted small" data-test="execution-tree-loading">Loading execution chain…</div>
|
||||
}
|
||||
else if (_error is not null)
|
||||
{
|
||||
<div class="alert alert-danger small" data-test="execution-tree-error">@_error</div>
|
||||
}
|
||||
else if (_nodes is { Count: > 0 })
|
||||
{
|
||||
<div class="mb-2">
|
||||
<a class="btn btn-outline-secondary btn-sm"
|
||||
data-test="execution-tree-back-to-log"
|
||||
href="@($"/audit/log?executionId={_executionId}")">
|
||||
View this execution in the Audit Log
|
||||
</a>
|
||||
</div>
|
||||
<ExecutionTree Nodes="_nodes" ArrivedFromExecutionId="_executionId.Value" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="alert alert-secondary small" data-test="execution-tree-empty">
|
||||
No execution chain found for this id.
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,93 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using ScadaLink.Commons.Types.Audit;
|
||||
|
||||
namespace ScadaLink.CentralUI.Components.Pages.Audit;
|
||||
|
||||
/// <summary>
|
||||
/// Code-behind for the execution-chain tree page (Audit Log ParentExecutionId
|
||||
/// feature, Task 10). Route <c>/audit/execution-tree</c>, reached via the Audit
|
||||
/// Log drilldown drawer's "View execution chain" action with
|
||||
/// <c>?executionId={guid}</c>.
|
||||
///
|
||||
/// <para>
|
||||
/// On initialization the page parses <c>?executionId=</c> (lax-parsed, matching
|
||||
/// the Audit Log page's drill-in contract — an absent or unparseable value
|
||||
/// leaves the page in a guidance state and issues NO service call), then asks
|
||||
/// <see cref="ScadaLink.CentralUI.Services.IAuditLogQueryService.GetExecutionTreeAsync"/>
|
||||
/// for the whole chain. The flat <see cref="ExecutionTreeNode"/> list is handed
|
||||
/// to the recursive <c>ExecutionTree</c> component, which assembles + renders
|
||||
/// the tree.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// The data path mirrors the Audit Log results grid: the page talks ONLY to the
|
||||
/// CentralUI <c>IAuditLogQueryService</c> facade, never <c>IAuditLogRepository</c>
|
||||
/// directly, so the page can be unit-tested with a substituted service.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public partial class ExecutionTreePage
|
||||
{
|
||||
[Inject] private NavigationManager Navigation { get; set; } = null!;
|
||||
|
||||
// The parsed ?executionId= value, or null when absent / unparseable.
|
||||
private Guid? _executionId;
|
||||
|
||||
// The flat chain returned by the query service; null until the load
|
||||
// completes (or when no id was supplied).
|
||||
private IReadOnlyList<ExecutionTreeNode>? _nodes;
|
||||
|
||||
private bool _loading;
|
||||
private string? _error;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
_executionId = ParseExecutionId();
|
||||
if (_executionId is null)
|
||||
{
|
||||
// No id — render guidance, do not touch the service.
|
||||
return;
|
||||
}
|
||||
|
||||
await LoadChainAsync(_executionId.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lax-parses <c>?executionId=</c>. Returns null when the param is absent or
|
||||
/// is not a valid <see cref="Guid"/> — the page then shows guidance instead
|
||||
/// of an error, consistent with the Audit Log page's drill-in handling.
|
||||
/// </summary>
|
||||
private Guid? ParseExecutionId()
|
||||
{
|
||||
var uri = Navigation.ToAbsoluteUri(Navigation.Uri);
|
||||
var query = QueryHelpers.ParseQuery(uri.Query);
|
||||
if (query.TryGetValue("executionId", out var values)
|
||||
&& Guid.TryParse(values.ToString(), out var parsed))
|
||||
{
|
||||
return parsed;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task LoadChainAsync(Guid executionId)
|
||||
{
|
||||
_loading = true;
|
||||
_error = null;
|
||||
try
|
||||
{
|
||||
_nodes = await AuditLogQueryService.GetExecutionTreeAsync(executionId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// A transient DB outage degrades this page to an error banner
|
||||
// rather than killing the circuit — the same defensive posture the
|
||||
// Audit Log grid takes around its query.
|
||||
_error = $"Could not load the execution chain: {ex.Message}";
|
||||
_nodes = null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user