feat(centralui): open ExecutionDetailModal on tree-node double-click
This commit is contained in:
@@ -52,7 +52,14 @@
|
|||||||
View this execution in the Audit Log
|
View this execution in the Audit Log
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<ExecutionTree Nodes="_nodes" ArrivedFromExecutionId="_executionId.Value" />
|
<ExecutionTree Nodes="_nodes" ArrivedFromExecutionId="_executionId.Value"
|
||||||
|
OnNodeActivated="HandleNodeActivated" />
|
||||||
|
|
||||||
|
@* Double-clicking a tree node raises OnNodeActivated, which opens this
|
||||||
|
modal for that execution. The modal renders nothing while IsOpen is
|
||||||
|
false, so it is safe to place unconditionally here. *@
|
||||||
|
<ExecutionDetailModal ExecutionId="_modalExecutionId" IsOpen="_modalOpen"
|
||||||
|
OnClose="HandleModalClose" />
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -40,6 +40,15 @@ public partial class ExecutionTreePage
|
|||||||
private bool _loading;
|
private bool _loading;
|
||||||
private string? _error;
|
private string? _error;
|
||||||
|
|
||||||
|
// Execution-Tree Node Detail Modal feature (Task 4) — state backing the
|
||||||
|
// <ExecutionDetailModal>. A double-click on a tree node sets
|
||||||
|
// _modalExecutionId + flips _modalOpen true; the modal loads that
|
||||||
|
// execution's audit rows on the closed → open transition. _modalOpen is the
|
||||||
|
// visibility gate — _modalExecutionId is left intact across a close (it is
|
||||||
|
// harmless while the modal is hidden and avoids a flicker if reopened).
|
||||||
|
private Guid? _modalExecutionId;
|
||||||
|
private bool _modalOpen;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
_executionId = ParseExecutionId();
|
_executionId = ParseExecutionId();
|
||||||
@@ -90,4 +99,22 @@ public partial class ExecutionTreePage
|
|||||||
_loading = false;
|
_loading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Raised by <c>ExecutionTree</c> (bubbled up from a node double-click) with
|
||||||
|
/// the activated node's <c>ExecutionId</c>. Opens the
|
||||||
|
/// <c>ExecutionDetailModal</c> for that execution — the modal loads its
|
||||||
|
/// audit rows on the closed → open transition.
|
||||||
|
/// </summary>
|
||||||
|
private void HandleNodeActivated(Guid executionId)
|
||||||
|
{
|
||||||
|
_modalExecutionId = executionId;
|
||||||
|
_modalOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Raised by <c>ExecutionDetailModal</c> when the user dismisses it. Flips
|
||||||
|
/// the visibility gate closed; <see cref="_modalExecutionId"/> is left as-is.
|
||||||
|
/// </summary>
|
||||||
|
private void HandleModalClose() => _modalOpen = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ using Microsoft.AspNetCore.Components.Authorization;
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using ScadaLink.CentralUI.Services;
|
using ScadaLink.CentralUI.Services;
|
||||||
|
using ScadaLink.Commons.Entities.Audit;
|
||||||
using ScadaLink.Commons.Types.Audit;
|
using ScadaLink.Commons.Types.Audit;
|
||||||
|
using ScadaLink.Commons.Types.Enums;
|
||||||
using ScadaLink.Security;
|
using ScadaLink.Security;
|
||||||
using ExecutionTreePage = ScadaLink.CentralUI.Components.Pages.Audit.ExecutionTreePage;
|
using ExecutionTreePage = ScadaLink.CentralUI.Components.Pages.Audit.ExecutionTreePage;
|
||||||
|
|
||||||
@@ -111,6 +113,68 @@ public class ExecutionTreePageTests : BunitContext
|
|||||||
_queryService.DidNotReceive().GetExecutionTreeAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>());
|
_queryService.DidNotReceive().GetExecutionTreeAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DoubleClickTreeNode_OpensExecutionDetailModal()
|
||||||
|
{
|
||||||
|
var root = Guid.Parse("33333333-3333-3333-3333-333333333333");
|
||||||
|
var child = Guid.Parse("44444444-4444-4444-4444-444444444444");
|
||||||
|
_queryService = Substitute.For<IAuditLogQueryService>();
|
||||||
|
_queryService.GetExecutionTreeAsync(child, Arg.Any<CancellationToken>())
|
||||||
|
.Returns(Task.FromResult<IReadOnlyList<ExecutionTreeNode>>(new List<ExecutionTreeNode>
|
||||||
|
{
|
||||||
|
Node(root, null),
|
||||||
|
Node(child, root),
|
||||||
|
}));
|
||||||
|
// The modal loads the double-clicked execution's audit rows on open.
|
||||||
|
_queryService.QueryAsync(Arg.Any<AuditLogQueryFilter>(), Arg.Any<AuditLogPaging?>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(Task.FromResult<IReadOnlyList<AuditEvent>>(Array.Empty<AuditEvent>()));
|
||||||
|
// AuditEventDetail (reachable from the modal) owns a clipboard interop call.
|
||||||
|
JSInterop.Mode = JSRuntimeMode.Loose;
|
||||||
|
|
||||||
|
var cut = RenderPage($"executionId={child}", "Admin");
|
||||||
|
|
||||||
|
// The modal is absent until a node is activated.
|
||||||
|
Assert.Empty(cut.FindAll("[data-test=\"execution-detail-modal\"]"));
|
||||||
|
|
||||||
|
var body = cut.Find($"[data-test=\"tree-node-{child}\"] .execution-tree-body");
|
||||||
|
body.DoubleClick();
|
||||||
|
|
||||||
|
cut.WaitForAssertion(() =>
|
||||||
|
Assert.NotEmpty(cut.FindAll("[data-test=\"execution-detail-modal\"]")));
|
||||||
|
_queryService.Received().QueryAsync(
|
||||||
|
Arg.Is<AuditLogQueryFilter>(f => f.ExecutionId == child),
|
||||||
|
Arg.Any<AuditLogPaging?>(),
|
||||||
|
Arg.Any<CancellationToken>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ClosingExecutionDetailModal_HidesIt()
|
||||||
|
{
|
||||||
|
var root = Guid.Parse("55555555-5555-5555-5555-555555555555");
|
||||||
|
var child = Guid.Parse("66666666-6666-6666-6666-666666666666");
|
||||||
|
_queryService = Substitute.For<IAuditLogQueryService>();
|
||||||
|
_queryService.GetExecutionTreeAsync(child, Arg.Any<CancellationToken>())
|
||||||
|
.Returns(Task.FromResult<IReadOnlyList<ExecutionTreeNode>>(new List<ExecutionTreeNode>
|
||||||
|
{
|
||||||
|
Node(root, null),
|
||||||
|
Node(child, root),
|
||||||
|
}));
|
||||||
|
_queryService.QueryAsync(Arg.Any<AuditLogQueryFilter>(), Arg.Any<AuditLogPaging?>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(Task.FromResult<IReadOnlyList<AuditEvent>>(Array.Empty<AuditEvent>()));
|
||||||
|
JSInterop.Mode = JSRuntimeMode.Loose;
|
||||||
|
|
||||||
|
var cut = RenderPage($"executionId={child}", "Admin");
|
||||||
|
|
||||||
|
cut.Find($"[data-test=\"tree-node-{child}\"] .execution-tree-body").DoubleClick();
|
||||||
|
cut.WaitForAssertion(() =>
|
||||||
|
Assert.NotEmpty(cut.FindAll("[data-test=\"execution-detail-modal\"]")));
|
||||||
|
|
||||||
|
cut.Find("[data-test=\"execution-detail-close\"]").Click();
|
||||||
|
|
||||||
|
cut.WaitForAssertion(() =>
|
||||||
|
Assert.Empty(cut.FindAll("[data-test=\"execution-detail-modal\"]")));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ExecutionTreePage_HasOperationalAuditAuthorizeAttribute()
|
public void ExecutionTreePage_HasOperationalAuditAuthorizeAttribute()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user