2a7d6facce
FilteredAlarms() ran the full filter+sort twice per render (Showing card and table) and Areas() once more, including every search keystroke render. Both now go through DashboardAlarmView, recomputed only when the alarm generation (bumped per poll) or the filter criteria change.
126 lines
5.4 KiB
C#
126 lines
5.4 KiB
C#
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
|
|
|
namespace ZB.MOM.WW.MxGateway.Server.Dashboard;
|
|
|
|
/// <summary>The Alarms page's filter inputs, compared by value to decide whether a filtered view is stale.</summary>
|
|
/// <param name="ShowActive">Include unacknowledged <c>Active</c> alarms.</param>
|
|
/// <param name="ShowAcked">Include <c>ActiveAcked</c> alarms.</param>
|
|
/// <param name="Area">Area to match (case-insensitive), or empty for all.</param>
|
|
/// <param name="MinSeverity">Lowest severity shown.</param>
|
|
/// <param name="MaxSeverity">Highest severity shown.</param>
|
|
/// <param name="Search">Reference/source/description text search; trimmed before matching.</param>
|
|
public sealed record DashboardAlarmFilterCriteria(
|
|
bool ShowActive,
|
|
bool ShowAcked,
|
|
string Area,
|
|
int MinSeverity,
|
|
int MaxSeverity,
|
|
string Search);
|
|
|
|
/// <summary>
|
|
/// Memoized projections of the Alarms page's alarm list: the filtered, sorted rows and
|
|
/// the distinct area list. A render used to run the full filter-and-sort twice (the
|
|
/// "Showing" card and the table) and the area projection once more, on every render —
|
|
/// including the renders that typing in the search box triggers per keystroke. Each
|
|
/// projection is now recomputed only when the alarm list (its generation) or, for the
|
|
/// rows, the filter criteria change.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Owned by one page instance and touched only from its render dispatcher, so it is not
|
|
/// synchronized.
|
|
/// </remarks>
|
|
public sealed class DashboardAlarmView
|
|
{
|
|
private long _filteredGeneration = -1;
|
|
private DashboardAlarmFilterCriteria? _filteredCriteria;
|
|
private IReadOnlyList<DashboardActiveAlarm> _filtered = [];
|
|
private long _areasGeneration = -1;
|
|
private IReadOnlyList<string> _areas = [];
|
|
|
|
/// <summary>Gets how many times the filtered rows were actually computed (diagnostic, for tests).</summary>
|
|
internal int FilterComputations { get; private set; }
|
|
|
|
/// <summary>Gets how many times the area list was actually computed (diagnostic, for tests).</summary>
|
|
internal int AreaComputations { get; private set; }
|
|
|
|
/// <summary>Returns the alarms matching <paramref name="criteria"/>, highest severity then newest first.</summary>
|
|
/// <param name="generation">
|
|
/// Version of <paramref name="alarms"/>; the caller bumps it whenever it replaces the list's contents.
|
|
/// </param>
|
|
/// <param name="criteria">The current filter inputs.</param>
|
|
/// <param name="alarms">The full active-alarm list.</param>
|
|
/// <returns>The filtered rows; the same instance while generation and criteria are unchanged.</returns>
|
|
public IReadOnlyList<DashboardActiveAlarm> Filtered(
|
|
long generation,
|
|
DashboardAlarmFilterCriteria criteria,
|
|
IReadOnlyList<DashboardActiveAlarm> alarms)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(criteria);
|
|
ArgumentNullException.ThrowIfNull(alarms);
|
|
|
|
if (generation == _filteredGeneration && criteria == _filteredCriteria)
|
|
{
|
|
return _filtered;
|
|
}
|
|
|
|
_filtered = Apply(criteria, alarms);
|
|
_filteredGeneration = generation;
|
|
_filteredCriteria = criteria;
|
|
FilterComputations++;
|
|
return _filtered;
|
|
}
|
|
|
|
/// <summary>Returns the distinct non-blank areas of <paramref name="alarms"/>, sorted case-insensitively.</summary>
|
|
/// <param name="generation">Version of <paramref name="alarms"/>.</param>
|
|
/// <param name="alarms">The full active-alarm list.</param>
|
|
/// <returns>The area list; the same instance while the generation is unchanged.</returns>
|
|
public IReadOnlyList<string> Areas(long generation, IReadOnlyList<DashboardActiveAlarm> alarms)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(alarms);
|
|
|
|
if (generation == _areasGeneration)
|
|
{
|
|
return _areas;
|
|
}
|
|
|
|
_areas = alarms
|
|
.Select(alarm => alarm.Area)
|
|
.Where(area => !string.IsNullOrWhiteSpace(area))
|
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
.OrderBy(area => area, StringComparer.OrdinalIgnoreCase)
|
|
.ToArray();
|
|
_areasGeneration = generation;
|
|
AreaComputations++;
|
|
return _areas;
|
|
}
|
|
|
|
private static DashboardActiveAlarm[] Apply(
|
|
DashboardAlarmFilterCriteria criteria,
|
|
IReadOnlyList<DashboardActiveAlarm> alarms)
|
|
{
|
|
string query = criteria.Search.Trim();
|
|
return alarms
|
|
.Where(alarm => MatchesState(criteria, alarm))
|
|
.Where(alarm => criteria.Area.Length == 0
|
|
|| string.Equals(alarm.Area, criteria.Area, StringComparison.OrdinalIgnoreCase))
|
|
.Where(alarm => alarm.Severity >= criteria.MinSeverity && alarm.Severity <= criteria.MaxSeverity)
|
|
.Where(alarm => query.Length == 0
|
|
|| alarm.Reference.Contains(query, StringComparison.OrdinalIgnoreCase)
|
|
|| alarm.Source.Contains(query, StringComparison.OrdinalIgnoreCase)
|
|
|| alarm.Description.Contains(query, StringComparison.OrdinalIgnoreCase))
|
|
.OrderByDescending(alarm => alarm.Severity)
|
|
.ThenByDescending(alarm => alarm.LastTransition ?? DateTimeOffset.MinValue)
|
|
.ToArray();
|
|
}
|
|
|
|
private static bool MatchesState(DashboardAlarmFilterCriteria criteria, DashboardActiveAlarm alarm)
|
|
{
|
|
return alarm.State switch
|
|
{
|
|
AlarmConditionState.Active => criteria.ShowActive,
|
|
AlarmConditionState.ActiveAcked => criteria.ShowAcked,
|
|
_ => true,
|
|
};
|
|
}
|
|
}
|