From 34e464edab056bd4b690253b05061abe2374f593 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 19 May 2026 06:03:15 -0400 Subject: [PATCH] refactor(central-ui): split Notification Report out of the Outbox page --- .../NotificationReport.razor} | 100 +----------------- .../Pages/HealthPageTests.cs | 4 +- ...ests.cs => NotificationReportPageTests.cs} | 63 +++-------- 3 files changed, 21 insertions(+), 146 deletions(-) rename src/ScadaLink.CentralUI/Components/Pages/{Monitoring/NotificationOutbox.razor => Notifications/NotificationReport.razor} (81%) rename tests/ScadaLink.CentralUI.Tests/Pages/{NotificationOutboxPageTests.cs => NotificationReportPageTests.cs} (78%) diff --git a/src/ScadaLink.CentralUI/Components/Pages/Monitoring/NotificationOutbox.razor b/src/ScadaLink.CentralUI/Components/Pages/Notifications/NotificationReport.razor similarity index 81% rename from src/ScadaLink.CentralUI/Components/Pages/Monitoring/NotificationOutbox.razor rename to src/ScadaLink.CentralUI/Components/Pages/Notifications/NotificationReport.razor index 2b0ed38..1a66d3d 100644 --- a/src/ScadaLink.CentralUI/Components/Pages/Monitoring/NotificationOutbox.razor +++ b/src/ScadaLink.CentralUI/Components/Pages/Notifications/NotificationReport.razor @@ -1,4 +1,4 @@ -@page "/monitoring/notification-outbox" +@page "/notifications/report" @attribute [Authorize(Policy = ScadaLink.Security.AuthorizationPolicies.RequireDeployment)] @using ScadaLink.Commons.Entities.Sites @using ScadaLink.Commons.Interfaces.Repositories @@ -7,70 +7,19 @@ @inject CommunicationService CommunicationService @inject ISiteRepository SiteRepository @inject IDialogService Dialog -@inject ILogger Logger +@inject ILogger Logger
-

Notification Outbox

+

Notification Report

- @* ── KPI tiles ── *@ - @if (_kpiError != null) - { -
KPIs unavailable: @_kpiError
- } - else - { -
-
-
-
-

@_kpi.QueueDepth

- Queue Depth -
-
-
-
-
-
-

@_kpi.StuckCount

- Stuck -
-
-
-
-
-
-

@_kpi.ParkedCount

- Parked -
-
-
-
-
-
-

@_kpi.DeliveredLastInterval

- Delivered (last interval) -
-
-
-
-
-
-

@FormatAge(_kpi.OldestPendingAge)

- Oldest Pending Age -
-
-
-
- } - @* ── Filters ── *@
@@ -259,11 +208,6 @@ private ToastNotification _toast = default!; private List _sites = new(); - // KPIs - private NotificationKpiResponse _kpi = - new(string.Empty, true, null, 0, 0, 0, 0, null); - private string? _kpiError; - // List private List? _notifications; private int _totalCount; @@ -291,7 +235,7 @@ catch (Exception ex) { // Non-fatal — source-site filter just falls back to the raw site IDs. - Logger.LogWarning(ex, "Failed to load sites for the outbox source-site filter."); + Logger.LogWarning(ex, "Failed to load sites for the report source-site filter."); } await RefreshAll(); @@ -299,31 +243,7 @@ private async Task RefreshAll() { - // Race-free despite both tasks mutating component fields: Blazor Server runs - // every continuation on the circuit's single-threaded synchronization context. - await Task.WhenAll(LoadKpis(), FetchPage()); - } - - private async Task LoadKpis() - { - try - { - var response = await CommunicationService.GetNotificationKpisAsync( - new NotificationKpiRequest(Guid.NewGuid().ToString("N"))); - if (response.Success) - { - _kpi = response; - _kpiError = null; - } - else - { - _kpiError = response.ErrorMessage ?? "KPI query failed."; - } - } - catch (Exception ex) - { - _kpiError = $"KPI query failed: {ex.Message}"; - } + await FetchPage(); } private async Task Search() @@ -463,16 +383,6 @@ private static string ShortId(string id) => id[..Math.Min(12, id.Length)]; - private static string FormatAge(TimeSpan? age) - { - if (age == null) return "—"; - var t = age.Value; - if (t.TotalSeconds < 60) return $"{(int)t.TotalSeconds}s"; - if (t.TotalMinutes < 60) return $"{(int)t.TotalMinutes}m"; - if (t.TotalHours < 24) return $"{(int)t.TotalHours}h"; - return $"{(int)t.TotalDays}d"; - } - private static string StatusBadgeClass(string status) => status switch { "Delivered" => "bg-success", diff --git a/tests/ScadaLink.CentralUI.Tests/Pages/HealthPageTests.cs b/tests/ScadaLink.CentralUI.Tests/Pages/HealthPageTests.cs index 0d6eb13..b372e4c 100644 --- a/tests/ScadaLink.CentralUI.Tests/Pages/HealthPageTests.cs +++ b/tests/ScadaLink.CentralUI.Tests/Pages/HealthPageTests.cs @@ -22,8 +22,8 @@ namespace ScadaLink.CentralUI.Tests.Pages; /// is an interface (mockable), but /// is a concrete class whose outbox calls /// route through an injected notification-outbox ; the -/// tests reuse the scripted-actor seam established by the Notification Outbox -/// page tests (see NotificationOutboxPageTests). +/// tests reuse the scripted-actor seam established by the Notification Report +/// page tests (see NotificationReportPageTests). /// public class HealthPageTests : BunitContext { diff --git a/tests/ScadaLink.CentralUI.Tests/Pages/NotificationOutboxPageTests.cs b/tests/ScadaLink.CentralUI.Tests/Pages/NotificationReportPageTests.cs similarity index 78% rename from tests/ScadaLink.CentralUI.Tests/Pages/NotificationOutboxPageTests.cs rename to tests/ScadaLink.CentralUI.Tests/Pages/NotificationReportPageTests.cs index e760722..81cef44 100644 --- a/tests/ScadaLink.CentralUI.Tests/Pages/NotificationOutboxPageTests.cs +++ b/tests/ScadaLink.CentralUI.Tests/Pages/NotificationReportPageTests.cs @@ -13,30 +13,26 @@ using ScadaLink.Commons.Interfaces.Repositories; using ScadaLink.Commons.Messages.Notification; using ScadaLink.Communication; using ScadaLink.Security; -using NotificationOutboxPage = ScadaLink.CentralUI.Components.Pages.Monitoring.NotificationOutbox; +using NotificationReportPage = ScadaLink.CentralUI.Components.Pages.Notifications.NotificationReport; namespace ScadaLink.CentralUI.Tests.Pages; /// -/// bUnit rendering tests for the Notification Outbox monitoring page (Task 23). +/// bUnit rendering tests for the Notification Report page. /// /// Testability note: is a concrete class with -/// non-virtual methods, so NSubstitute cannot intercept it. The outbox calls all +/// non-virtual methods, so NSubstitute cannot intercept it. The report calls all /// route through an injected (the notification-outbox /// proxy), so the tests wire a real, lightweight with a /// scripted that replies with fixed responses — the /// same seam SetNotificationOutbox exists for. /// -public class NotificationOutboxPageTests : BunitContext +public class NotificationReportPageTests : BunitContext { - private readonly ActorSystem _system = ActorSystem.Create("notif-outbox-tests"); + private readonly ActorSystem _system = ActorSystem.Create("notif-report-tests"); private readonly CommunicationService _comms; - // Mutable scripted replies — individual tests can override before rendering. - private NotificationKpiResponse _kpiReply = - new("k", true, null, QueueDepth: 7, StuckCount: 2, ParkedCount: 1, - DeliveredLastInterval: 42, OldestPendingAge: TimeSpan.FromMinutes(9)); - + // Mutable scripted reply — individual tests can override before rendering. private NotificationOutboxQueryResponse _queryReply = new("q", true, null, new List { @@ -54,7 +50,7 @@ public class NotificationOutboxPageTests : BunitContext private readonly List _retryRequests = new(); private readonly List _discardRequests = new(); - public NotificationOutboxPageTests() + public NotificationReportPageTests() { _comms = new CommunicationService( Options.Create(new CommunicationOptions()), @@ -88,7 +84,7 @@ public class NotificationOutboxPageTests : BunitContext [Fact] public void Page_RequiresDeploymentPolicy() { - var attr = typeof(NotificationOutboxPage) + var attr = typeof(NotificationReportPage) .GetCustomAttributes(typeof(AuthorizeAttribute), true) .Cast() .FirstOrDefault(); @@ -97,28 +93,10 @@ public class NotificationOutboxPageTests : BunitContext Assert.Equal(AuthorizationPolicies.RequireDeployment, attr!.Policy); } - [Fact] - public void Renders_KpiTiles_WithValues() - { - var cut = Render(); - - // KPI data arrives via an async actor Ask after first render. - cut.WaitForAssertion(() => - { - Assert.Contains("Queue Depth", cut.Markup); - Assert.Contains("Stuck", cut.Markup); - Assert.Contains("Parked", cut.Markup); - Assert.Contains("Delivered", cut.Markup); - // KPI numeric values surface in the tiles. - Assert.Contains(">7<", cut.Markup); // QueueDepth - Assert.Contains(">42<", cut.Markup); // DeliveredLastInterval - }); - } - [Fact] public void Renders_NotificationRows() { - var cut = Render(); + var cut = Render(); cut.WaitForAssertion(() => { @@ -131,7 +109,7 @@ public class NotificationOutboxPageTests : BunitContext [Fact] public void StuckRow_IsBadged() { - var cut = Render(); + var cut = Render(); cut.WaitForAssertion(() => { @@ -147,7 +125,7 @@ public class NotificationOutboxPageTests : BunitContext [Fact] public void ClickRetry_OnParkedRow_CallsRetryNotification() { - var cut = Render(); + var cut = Render(); cut.WaitForState(() => cut.Markup.Contains("Pump fault at Plant-A")); @@ -168,7 +146,7 @@ public class NotificationOutboxPageTests : BunitContext [Fact] public void ClickDiscard_OnParkedRow_CallsDiscardNotification() { - var cut = Render(); + var cut = Render(); cut.WaitForState(() => cut.Markup.Contains("Pump fault at Plant-A")); @@ -186,18 +164,6 @@ public class NotificationOutboxPageTests : BunitContext }); } - [Fact] - public void KpiFailure_ShowsErrorMessage() - { - _kpiReply = new NotificationKpiResponse( - "k", false, "outbox repository unavailable", 0, 0, 0, 0, null); - - var cut = Render(); - - cut.WaitForAssertion(() => - Assert.Contains("outbox repository unavailable", cut.Markup)); - } - [Fact] public void QueryFailure_ShowsErrorMessage() { @@ -205,7 +171,7 @@ public class NotificationOutboxPageTests : BunitContext "q", false, "outbox query backend unavailable", new List(), TotalCount: 0); - var cut = Render(); + var cut = Render(); cut.WaitForAssertion(() => Assert.Contains("outbox query backend unavailable", cut.Markup)); @@ -226,9 +192,8 @@ public class NotificationOutboxPageTests : BunitContext /// private sealed class ScriptedOutboxActor : ReceiveActor { - public ScriptedOutboxActor(NotificationOutboxPageTests test) + public ScriptedOutboxActor(NotificationReportPageTests test) { - Receive(_ => Sender.Tell(test._kpiReply)); Receive(_ => Sender.Tell(test._queryReply)); Receive(r => {