fix(centralui): apply status/stuck query-string filters on the Site Calls page
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using Microsoft.AspNetCore.WebUtilities;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using ScadaLink.CentralUI.Components.Shared;
|
using ScadaLink.CentralUI.Components.Shared;
|
||||||
using ScadaLink.Commons.Entities.Sites;
|
using ScadaLink.Commons.Entities.Sites;
|
||||||
@@ -26,11 +28,32 @@ namespace ScadaLink.CentralUI.Components.Pages.SiteCalls;
|
|||||||
/// a relay that never reaches the site is a transient transport condition, surfaced
|
/// a relay that never reaches the site is a transient transport condition, surfaced
|
||||||
/// to the operator differently from a generic failure.
|
/// to the operator differently from a generic failure.
|
||||||
/// </para>
|
/// </para>
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// Query-string drill-in: the Health-dashboard Site Call KPI tiles deep-link here
|
||||||
|
/// with <c>?status=Parked</c> (Parked tile) or <c>?stuck=true</c> (Stuck tile). On
|
||||||
|
/// initialization those params seed <see cref="_statusFilter"/> / <see cref="_stuckOnly"/>
|
||||||
|
/// BEFORE the first <see cref="RefreshAll"/>, so the first grid load is already
|
||||||
|
/// filtered and the filter card controls reflect the seeded values. Parsing is lax
|
||||||
|
/// — an absent, blank, or unrecognised value is silently dropped and the page loads
|
||||||
|
/// unfiltered, mirroring <c>AuditLogPage</c>'s drill-in convention.
|
||||||
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class SiteCallsReport
|
public partial class SiteCallsReport
|
||||||
{
|
{
|
||||||
private const int PageSize = 50;
|
private const int PageSize = 50;
|
||||||
|
|
||||||
|
[Inject] private NavigationManager Navigation { get; set; } = null!;
|
||||||
|
|
||||||
|
// The Status filter <select> options — the exact strings the dropdown binds and
|
||||||
|
// the KPI tiles emit (e.g. ?status=Parked). A query-string status only seeds the
|
||||||
|
// filter when it matches one of these (case-insensitively); anything else is
|
||||||
|
// dropped so a hand-crafted bad URL still renders the page unfiltered.
|
||||||
|
private static readonly string[] ValidStatuses =
|
||||||
|
{
|
||||||
|
"Submitted", "Forwarded", "Attempted", "Delivered", "Parked", "Failed", "Discarded",
|
||||||
|
};
|
||||||
|
|
||||||
private ToastNotification _toast = default!;
|
private ToastNotification _toast = default!;
|
||||||
private List<Site> _sites = new();
|
private List<Site> _sites = new();
|
||||||
|
|
||||||
@@ -77,9 +100,51 @@ public partial class SiteCallsReport
|
|||||||
Logger.LogWarning(ex, "Failed to load sites for the Site Calls source-site filter.");
|
Logger.LogWarning(ex, "Failed to load sites for the Site Calls source-site filter.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Seed filters from ?status= / ?stuck= BEFORE the first fetch so the initial
|
||||||
|
// grid load is already filtered (and the filter card controls reflect it).
|
||||||
|
ApplyQueryStringFilters();
|
||||||
|
|
||||||
await RefreshAll();
|
await RefreshAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pre-apply the Health-dashboard KPI-tile drill-in filters from the URL query
|
||||||
|
/// string. <c>?status=<status></c> seeds <see cref="_statusFilter"/> when it
|
||||||
|
/// matches a known status (case-insensitive); <c>?stuck=true</c> seeds
|
||||||
|
/// <see cref="_stuckOnly"/>. Lax parsing — an absent, blank, or unrecognised value
|
||||||
|
/// is silently dropped, leaving the filter empty (the no-param behaviour).
|
||||||
|
/// </summary>
|
||||||
|
private void ApplyQueryStringFilters()
|
||||||
|
{
|
||||||
|
var uri = Navigation.ToAbsoluteUri(Navigation.Uri);
|
||||||
|
var query = QueryHelpers.ParseQuery(uri.Query);
|
||||||
|
|
||||||
|
if (query.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.TryGetValue("status", out var statusValues))
|
||||||
|
{
|
||||||
|
var v = statusValues.ToString();
|
||||||
|
// Round-trip the dropdown's own option strings (the KPI tile emits the
|
||||||
|
// canonical casing, e.g. ?status=Parked); normalise to that casing so the
|
||||||
|
// <select> binds. An unrecognised value leaves the filter unset.
|
||||||
|
var match = ValidStatuses.FirstOrDefault(
|
||||||
|
s => string.Equals(s, v?.Trim(), StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (match is not null)
|
||||||
|
{
|
||||||
|
_statusFilter = match;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.TryGetValue("stuck", out var stuckValues)
|
||||||
|
&& bool.TryParse(stuckValues.ToString(), out var stuck))
|
||||||
|
{
|
||||||
|
_stuckOnly = stuck;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Re-fetch the current page (Refresh button, and after a relay action).</summary>
|
/// <summary>Re-fetch the current page (Refresh button, and after a relay action).</summary>
|
||||||
private async Task RefreshAll()
|
private async Task RefreshAll()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using Akka.Actor;
|
using Akka.Actor;
|
||||||
using Bunit;
|
using Bunit;
|
||||||
|
using Bunit.TestDoubles;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
using Microsoft.AspNetCore.Components.Authorization;
|
using Microsoft.AspNetCore.Components.Authorization;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
@@ -411,6 +413,77 @@ public class SiteCallsReportPageTests : BunitContext
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
// Query-string drill-in — the Health-dashboard Site Call KPI tiles deep-link
|
||||||
|
// here with ?status=Parked (Parked tile) and ?stuck=true (Stuck tile). The
|
||||||
|
// params must seed the filter BEFORE the first query so the initial grid load
|
||||||
|
// is already filtered, and the filter card controls must reflect the values.
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NavigateWithStatusParkedParam_LoadsGridPreFilteredToParked()
|
||||||
|
{
|
||||||
|
// The Parked KPI tile emits ?status=Parked — set the URI before render.
|
||||||
|
var nav = (BunitNavigationManager)Services.GetRequiredService<NavigationManager>();
|
||||||
|
nav.NavigateTo("/site-calls/report?status=Parked");
|
||||||
|
|
||||||
|
var cut = Render<SiteCallsReportPage>();
|
||||||
|
|
||||||
|
cut.WaitForAssertion(() =>
|
||||||
|
{
|
||||||
|
// The first (and only) query the page issues carries the Parked
|
||||||
|
// status filter — the grid load is pre-filtered, not unfiltered.
|
||||||
|
Assert.Single(_queryRequests);
|
||||||
|
Assert.Equal("Parked", _queryRequests[0].StatusFilter);
|
||||||
|
|
||||||
|
// The Status <select> control reflects the seeded value so the
|
||||||
|
// operator sees the filter and can Clear it.
|
||||||
|
var statusSelect = cut.Find("#sc-status");
|
||||||
|
Assert.Equal("Parked", statusSelect.GetAttribute("value"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NavigateWithStuckTrueParam_LoadsGridWithStuckFilterApplied()
|
||||||
|
{
|
||||||
|
// The Stuck KPI tile emits ?stuck=true.
|
||||||
|
var nav = (BunitNavigationManager)Services.GetRequiredService<NavigationManager>();
|
||||||
|
nav.NavigateTo("/site-calls/report?stuck=true");
|
||||||
|
|
||||||
|
var cut = Render<SiteCallsReportPage>();
|
||||||
|
|
||||||
|
cut.WaitForAssertion(() =>
|
||||||
|
{
|
||||||
|
// The first query carries StuckOnly = true.
|
||||||
|
Assert.Single(_queryRequests);
|
||||||
|
Assert.True(_queryRequests[0].StuckOnly);
|
||||||
|
|
||||||
|
// The "Stuck only" checkbox is checked.
|
||||||
|
var stuckCheckbox = cut.Find("#sc-stuck-only");
|
||||||
|
Assert.True(stuckCheckbox.HasAttribute("checked"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NavigateWithNoQueryParams_LoadsGridUnfiltered()
|
||||||
|
{
|
||||||
|
// No drill-in params — the page loads exactly as before: an unfiltered
|
||||||
|
// query and no status/stuck filter set on the controls.
|
||||||
|
var cut = Render<SiteCallsReportPage>();
|
||||||
|
|
||||||
|
cut.WaitForAssertion(() =>
|
||||||
|
{
|
||||||
|
Assert.Single(_queryRequests);
|
||||||
|
Assert.Null(_queryRequests[0].StatusFilter);
|
||||||
|
Assert.False(_queryRequests[0].StuckOnly);
|
||||||
|
|
||||||
|
var statusSelect = cut.Find("#sc-status");
|
||||||
|
Assert.True(string.IsNullOrEmpty(statusSelect.GetAttribute("value")));
|
||||||
|
var stuckCheckbox = cut.Find("#sc-stuck-only");
|
||||||
|
Assert.False(stuckCheckbox.HasAttribute("checked"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool disposing)
|
protected override void Dispose(bool disposing)
|
||||||
{
|
{
|
||||||
if (disposing)
|
if (disposing)
|
||||||
|
|||||||
Reference in New Issue
Block a user