-
Showing @_entries.Count entries
+
+ Showing @_entries.Count entries
+ @if (_droppedCount > 0)
+ {
+ (oldest @_droppedCount dropped — display is capped at @MaxRetainedRows rows)
+ }
+
@if (_hasMore)
{
@@ -194,6 +204,26 @@
// moved every expansion onto the wrong row. The id is stable by construction.
private readonly HashSet
_expandedRows = new();
+ /// Ceiling on rows retained across "Load more" pages (review 2026-10-21 07/M-3).
+ internal const int MaxRetainedRows = 1000;
+
+ private int _droppedCount;
+
+ ///
+ /// Appends a page to the retained list, dropping the oldest (head) rows beyond
+ /// and un-expanding any dropped row. Returns the number dropped.
+ ///
+ internal static int AppendCapped(
+ List entries, IEnumerable page, HashSet expandedRows, int cap)
+ {
+ entries.AddRange(page);
+ var excess = entries.Count - cap;
+ if (excess <= 0) return 0;
+ foreach (var dropped in entries.Take(excess)) expandedRows.Remove(dropped.Id);
+ entries.RemoveRange(0, excess);
+ return excess;
+ }
+
private int ActiveFilterCount
{
get
@@ -225,6 +255,7 @@
private async Task Search()
{
_entries = new();
+ _droppedCount = 0;
_continuationToken = null;
_expandedRows.Clear();
await FetchPage();
@@ -276,7 +307,7 @@
if (response.Success)
{
_entries ??= new();
- _entries.AddRange(response.Entries);
+ _droppedCount += AppendCapped(_entries, response.Entries, _expandedRows, MaxRetainedRows);
_hasMore = response.HasMore;
_continuationToken = response.ContinuationToken;
}
diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Monitoring/EventLogsPageTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Monitoring/EventLogsPageTests.cs
new file mode 100644
index 00000000..2abd7179
--- /dev/null
+++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Monitoring/EventLogsPageTests.cs
@@ -0,0 +1,71 @@
+using Bunit;
+using Microsoft.AspNetCore.Components.Authorization;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+using NSubstitute;
+using System.Security.Claims;
+using ZB.MOM.WW.ScadaBridge.CentralUI.Auth;
+using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Monitoring;
+using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
+using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
+using ZB.MOM.WW.ScadaBridge.Commons.Messages.RemoteQuery;
+using ZB.MOM.WW.ScadaBridge.Communication;
+
+namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Monitoring;
+
+/// Review 2026-10-21 07/M-3 (retained-row cap) and the lane D keyword-window hint.
+public class EventLogsPageTests : BunitContext
+{
+ private static EventLogEntry Entry(int n) =>
+ new($"id-{n}", DateTimeOffset.UnixEpoch.AddSeconds(n), "T", "Info", null, "src", "m", null);
+
+ private static List Page(int from, int count) =>
+ Enumerable.Range(from, count).Select(Entry).ToList();
+
+ /// Rows beyond the cap are dropped from the head and their expansion state cleared.
+ [Fact]
+ public void AppendCapped_DropsOldestRowsBeyondCap_AndUnexpandsThem()
+ {
+ var entries = Page(0, 8);
+ var expanded = new HashSet { "id-0", "id-7" };
+
+ var dropped = EventLogs.AppendCapped(entries, Page(8, 4), expanded, cap: 10);
+
+ Assert.Equal(2, dropped);
+ Assert.Equal(10, entries.Count);
+ Assert.Equal("id-2", entries[0].Id);
+ Assert.Equal("id-11", entries[^1].Id);
+ Assert.DoesNotContain("id-0", expanded);
+ Assert.Contains("id-7", expanded);
+ }
+
+ /// Under the cap nothing is dropped.
+ [Fact]
+ public void AppendCapped_UnderCap_KeepsEverything()
+ {
+ var entries = Page(0, 3);
+ Assert.Equal(0, EventLogs.AppendCapped(entries, Page(3, 3), new HashSet(), cap: 10));
+ Assert.Equal(6, entries.Count);
+ }
+
+ /// The page tells operators the keyword search window is bounded.
+ [Fact]
+ public void Page_ShowsKeywordWindowHint()
+ {
+ var identity = new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, "u") }, "T");
+ var auth = Substitute.For();
+ auth.GetAuthenticationStateAsync().Returns(new AuthenticationState(new ClaimsPrincipal(identity)));
+ Services.AddSingleton(auth);
+ Services.AddScoped(_ => new SiteScopeService(auth));
+ var siteRepo = Substitute.For();
+ siteRepo.GetAllSitesAsync().Returns(new List());
+ Services.AddSingleton(siteRepo);
+ Services.AddSingleton(new CommunicationService(
+ Options.Create(new CommunicationOptions()), NullLogger.Instance));
+
+ var cut = Render();
+
+ Assert.Contains("at most the last 7 days", cut.Markup);
+ }
+}