126 lines
6.3 KiB
Plaintext
126 lines
6.3 KiB
Plaintext
@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"
|
||
@ondblclick="() => OnNodeActivated.InvokeAsync(node.ExecutionId)">
|
||
<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"
|
||
OnNodeActivated="OnNodeActivated"
|
||
Depth="Depth + 1" />
|
||
}
|
||
</li>
|
||
}
|
||
</ul>
|