feat(centralui): execution-chain tree view on the Audit Log page
This commit is contained in:
123
src/ScadaLink.CentralUI/Components/Audit/ExecutionTree.razor
Normal file
123
src/ScadaLink.CentralUI/Components/Audit/ExecutionTree.razor
Normal file
@@ -0,0 +1,123 @@
|
||||
@using ScadaLink.Commons.Types.Audit
|
||||
|
||||
@* Execution-chain tree (Audit Log ParentExecutionId feature, Task 10).
|
||||
A custom recursive Blazor tree: the host hands in the FLAT ExecutionTreeNode
|
||||
list the repository returns; this component assembles it into a tree (joining
|
||||
ParentExecutionId → a parent's ExecutionId), then renders depth-first.
|
||||
|
||||
Recursion is expressed by the component rendering <ExecutionTree> for each
|
||||
child subtree. To keep that recursion finite even on corrupt/cyclic input,
|
||||
the assembled subtree is computed ONCE at the root (Depth == 0) and threaded
|
||||
downward via the PreBuiltRoots parameter — child instances never re-run the
|
||||
flat-list assembly, and the assembly itself tracks visited ExecutionIds so a
|
||||
cycle is broken on first revisit. *@
|
||||
|
||||
@if (_rootsToRender.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
<ul class="execution-tree @(Depth == 0 ? "execution-tree--root" : "")"
|
||||
data-test="execution-tree@(Depth == 0 ? "" : "-subtree")">
|
||||
@foreach (var subtree in _rootsToRender)
|
||||
{
|
||||
var node = subtree.Node;
|
||||
var isCurrent = node.ExecutionId == ArrivedFromExecutionId;
|
||||
var isStub = node.RowCount == 0;
|
||||
<li class="execution-tree-item" @key="node.ExecutionId">
|
||||
<div class="execution-tree-node @(isCurrent ? "execution-tree-node--current" : "") @(isStub ? "execution-tree-node--stub" : "")"
|
||||
data-test="tree-node-@node.ExecutionId">
|
||||
@if (subtree.Children.Count > 0)
|
||||
{
|
||||
<button type="button"
|
||||
class="execution-tree-toggle"
|
||||
data-test="tree-toggle-@node.ExecutionId"
|
||||
aria-expanded="@(IsExpanded(node.ExecutionId) ? "true" : "false")"
|
||||
aria-label="@(IsExpanded(node.ExecutionId) ? "Collapse" : "Expand") child executions"
|
||||
@onclick="() => ToggleExpand(node.ExecutionId)">
|
||||
<span class="execution-tree-toggle-glyph" aria-hidden="true">
|
||||
@(IsExpanded(node.ExecutionId) ? "−" : "+")
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="execution-tree-toggle execution-tree-toggle--leaf" aria-hidden="true"></span>
|
||||
}
|
||||
|
||||
<div class="execution-tree-body">
|
||||
<div class="execution-tree-headline">
|
||||
<a class="execution-tree-link font-monospace"
|
||||
data-test="tree-node-link-@node.ExecutionId"
|
||||
href="@AuditLogUrl(node.ExecutionId)"
|
||||
title="Open the Audit Log filtered to execution @node.ExecutionId">
|
||||
@ShortId(node.ExecutionId)
|
||||
</a>
|
||||
@if (isCurrent)
|
||||
{
|
||||
<span class="badge text-bg-primary execution-tree-tag"
|
||||
data-test="tree-current-tag-@node.ExecutionId">Arrived from</span>
|
||||
}
|
||||
@if (isStub)
|
||||
{
|
||||
<span class="badge text-bg-secondary execution-tree-tag"
|
||||
data-test="stub-node-@node.ExecutionId">No audited actions</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="execution-tree-rowcount text-muted small"
|
||||
data-test="tree-rowcount-@node.ExecutionId">
|
||||
@node.RowCount audit @(node.RowCount == 1 ? "row" : "rows")
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (isStub)
|
||||
{
|
||||
<div class="execution-tree-meta text-muted small">
|
||||
Execution with no audited actions — referenced as a parent, but it
|
||||
emitted no audit rows of its own (or its rows have been purged).
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="execution-tree-meta small">
|
||||
<span class="execution-tree-meta-item">
|
||||
<span class="text-muted">Source</span>
|
||||
@(node.SourceSiteId ?? "—")@(node.SourceInstanceId is null ? "" : " / " + node.SourceInstanceId)
|
||||
</span>
|
||||
@if (node.Channels.Count > 0)
|
||||
{
|
||||
<span class="execution-tree-meta-item">
|
||||
<span class="text-muted">Channels</span>
|
||||
@string.Join(", ", node.Channels)
|
||||
</span>
|
||||
}
|
||||
@if (node.Statuses.Count > 0)
|
||||
{
|
||||
<span class="execution-tree-meta-item">
|
||||
<span class="text-muted">Statuses</span>
|
||||
@string.Join(", ", node.Statuses)
|
||||
</span>
|
||||
}
|
||||
<span class="execution-tree-meta-item">
|
||||
<span class="text-muted">Time span</span>
|
||||
@FormatSpan(node.FirstOccurredAtUtc, node.LastOccurredAtUtc)
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (subtree.Children.Count > 0 && IsExpanded(node.ExecutionId))
|
||||
{
|
||||
@* Recurse: each child subtree is already assembled, so the
|
||||
nested instance renders directly from PreBuiltRoots and skips
|
||||
the flat-list assembly entirely. *@
|
||||
<ExecutionTree PreBuiltRoots="subtree.Children"
|
||||
ArrivedFromExecutionId="ArrivedFromExecutionId"
|
||||
Depth="Depth + 1" />
|
||||
}
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
Reference in New Issue
Block a user