ShowDetail(n)"
+ title="Double-click for full detail">
@ShortId(n.NotificationId) |
@n.Type |
@n.ListName |
@@ -162,7 +164,7 @@
@SiteName(n.SourceSiteId) |
|
|
-
+ |
@* Bundle D (#23 M7-T10) drill-in: NotificationId is the audit
CorrelationId, so the link deep-links into the central Audit
Log pre-filtered to this notification's lifecycle events. *@
@@ -206,6 +208,86 @@
}
+@* ── Row detail modal ── *@
+@if (_detailNotification != null)
+{
+ var d = _detailNotification;
+
+}
+
@code {
private const int _pageSize = 50;
@@ -220,6 +302,9 @@
private string? _listError;
private bool _actionInProgress;
+ // Row detail modal
+ private NotificationSummary? _detailNotification;
+
// Filters
private string _statusFilter = string.Empty;
private string _typeFilter = string.Empty;
@@ -355,6 +440,24 @@
_actionInProgress = false;
}
+ private void ShowDetail(NotificationSummary n) => _detailNotification = n;
+
+ private void CloseDetail() => _detailNotification = null;
+
+ private async Task RetryFromDetail(NotificationSummary n)
+ {
+ await RetryNotification(n);
+ // RefreshAll replaces the row list; close the modal so the user sees the
+ // refreshed grid rather than a now-stale detail snapshot.
+ CloseDetail();
+ }
+
+ private async Task DiscardFromDetail(NotificationSummary n)
+ {
+ await DiscardNotification(n);
+ CloseDetail();
+ }
+
private void ClearFilters()
{
_statusFilter = string.Empty;
diff --git a/tests/ScadaLink.CentralUI.Tests/Pages/NotificationReportDetailModalTests.cs b/tests/ScadaLink.CentralUI.Tests/Pages/NotificationReportDetailModalTests.cs
new file mode 100644
index 0000000..bbcd036
--- /dev/null
+++ b/tests/ScadaLink.CentralUI.Tests/Pages/NotificationReportDetailModalTests.cs
@@ -0,0 +1,182 @@
+using System.Security.Claims;
+using Akka.Actor;
+using Bunit;
+using Microsoft.AspNetCore.Components.Authorization;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+using ScadaLink.CentralUI.Components.Shared;
+using NSubstitute;
+using ScadaLink.Commons.Entities.Sites;
+using ScadaLink.Commons.Interfaces.Repositories;
+using ScadaLink.Commons.Messages.Notification;
+using ScadaLink.Communication;
+using NotificationReportPage = ScadaLink.CentralUI.Components.Pages.Notifications.NotificationReport;
+
+namespace ScadaLink.CentralUI.Tests.Pages;
+
+///
+/// bUnit tests for the Notification Report row-detail modal — double-clicking a
+/// notification row opens a Bootstrap modal showing that notification's full,
+/// untruncated details.
+///
+/// Mirrors 's seam: the report's
+/// calls route through an injected scripted
+/// actor (the notification-outbox proxy).
+///
+public class NotificationReportDetailModalTests : BunitContext
+{
+ private readonly ActorSystem _system = ActorSystem.Create("notif-report-modal-tests");
+ private readonly CommunicationService _comms;
+
+ private NotificationOutboxQueryResponse _queryReply =
+ new("q", true, null, new List
+ {
+ new("notif-aaaaaaaa-1111-full-id", "Email", "Ops On-Call", "Pump fault at Plant-A",
+ "Parked", RetryCount: 3, LastError: "SMTP timeout connecting to mail relay",
+ SourceSiteId: "plant-a", SourceInstanceId: "Pump-001",
+ CreatedAt: DateTimeOffset.UtcNow.AddMinutes(-30),
+ DeliveredAt: null, IsStuck: true),
+ new("notif-bbbbbbbb-2222-full-id", "Email", "Maintenance", "Daily summary",
+ "Delivered", RetryCount: 0, LastError: null, SourceSiteId: "plant-b",
+ SourceInstanceId: null, CreatedAt: DateTimeOffset.UtcNow.AddHours(-2),
+ DeliveredAt: DateTimeOffset.UtcNow.AddHours(-2), IsStuck: false),
+ }, TotalCount: 2);
+
+ public NotificationReportDetailModalTests()
+ {
+ _comms = new CommunicationService(
+ Options.Create(new CommunicationOptions()),
+ NullLogger.Instance);
+
+ var outbox = _system.ActorOf(Props.Create(() => new ScriptedOutboxActor(this)));
+ _comms.SetNotificationOutbox(outbox);
+
+ Services.AddSingleton(_comms);
+ Services.AddSingleton(new AlwaysConfirmDialogService());
+
+ var siteRepo = Substitute.For();
+ siteRepo.GetAllSitesAsync(Arg.Any())
+ .Returns(Task.FromResult>(new List
+ {
+ new("Plant A", "plant-a") { Id = 1 },
+ new("Plant B", "plant-b") { Id = 2 },
+ }));
+ Services.AddSingleton(siteRepo);
+
+ var claims = new[]
+ {
+ new Claim("Username", "tester"),
+ new Claim(ClaimTypes.Role, "Deployment"),
+ };
+ var user = new ClaimsPrincipal(new ClaimsIdentity(claims, "TestAuth"));
+ Services.AddSingleton(new TestAuthStateProvider(user));
+ Services.AddAuthorizationCore();
+ }
+
+ [Fact]
+ public void DoubleClickRow_OpensDetailModal()
+ {
+ var cut = Render();
+ cut.WaitForState(() => cut.Markup.Contains("Pump fault at Plant-A"));
+
+ // No modal initially.
+ Assert.Empty(cut.FindAll(".modal.show"));
+
+ var row = cut.FindAll("tbody tr")
+ .First(r => r.TextContent.Contains("Pump fault at Plant-A"));
+ row.DoubleClick();
+
+ cut.WaitForAssertion(() =>
+ {
+ var modal = cut.Find(".modal.show");
+ Assert.Contains("Pump fault at Plant-A", modal.TextContent);
+ Assert.Contains("Ops On-Call", modal.TextContent);
+ });
+ }
+
+ [Fact]
+ public void Modal_ShowsFullNotificationId_NotTruncated()
+ {
+ var cut = Render();
+ cut.WaitForState(() => cut.Markup.Contains("Pump fault at Plant-A"));
+
+ var row = cut.FindAll("tbody tr")
+ .First(r => r.TextContent.Contains("Pump fault at Plant-A"));
+ row.DoubleClick();
+
+ cut.WaitForAssertion(() =>
+ {
+ var modal = cut.Find(".modal.show");
+ // The grid renders ShortId(...) (first 12 chars); the modal must show
+ // the complete identifier.
+ Assert.Contains("notif-aaaaaaaa-1111-full-id", modal.TextContent);
+ });
+ }
+
+ [Fact]
+ public void CloseButton_DismissesModal()
+ {
+ var cut = Render();
+ cut.WaitForState(() => cut.Markup.Contains("Pump fault at Plant-A"));
+
+ var row = cut.FindAll("tbody tr")
+ .First(r => r.TextContent.Contains("Pump fault at Plant-A"));
+ row.DoubleClick();
+
+ cut.WaitForState(() => cut.FindAll(".modal.show").Count > 0);
+
+ var closeButton = cut.Find(".modal.show .modal-footer button");
+ closeButton.Click();
+
+ cut.WaitForAssertion(() => Assert.Empty(cut.FindAll(".modal.show")));
+ }
+
+ [Fact]
+ public void Modal_ShowsLastError_WhenPresent()
+ {
+ var cut = Render();
+ cut.WaitForState(() => cut.Markup.Contains("Pump fault at Plant-A"));
+
+ var row = cut.FindAll("tbody tr")
+ .First(r => r.TextContent.Contains("Pump fault at Plant-A"));
+ row.DoubleClick();
+
+ cut.WaitForAssertion(() =>
+ {
+ var modal = cut.Find(".modal.show");
+ Assert.Contains("SMTP timeout connecting to mail relay", modal.TextContent);
+ });
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _system.Terminate().Wait(TimeSpan.FromSeconds(5));
+ }
+ base.Dispose(disposing);
+ }
+
+ private sealed class ScriptedOutboxActor : ReceiveActor
+ {
+ public ScriptedOutboxActor(NotificationReportDetailModalTests test)
+ {
+ Receive(_ => Sender.Tell(test._queryReply));
+ Receive(r =>
+ Sender.Tell(new RetryNotificationResponse(r.CorrelationId, true, null)));
+ Receive(r =>
+ Sender.Tell(new DiscardNotificationResponse(r.CorrelationId, true, null)));
+ }
+ }
+
+ private sealed class AlwaysConfirmDialogService : IDialogService
+ {
+ public Task ConfirmAsync(string title, string message, bool danger = false)
+ => Task.FromResult(true);
+
+ public Task PromptAsync(
+ string title, string label, string initialValue = "", string? placeholder = null)
+ => Task.FromResult(null);
+ }
+}
|