9cff87fe85
Remove project bookkeeping citations from shipped code comments across the solution: hyphenated task IDs (WP-14, StoreAndForward-025), milestone/task/ issue refs (M3, Task 4, Audit Log #23, #21), Bundle X task-bundle labels, and C/D/K/S/T phase labels. Comment text only — no code logic, string/log literals, or XML-doc structure changed. Genuine descriptions are preserved (only the citation is stripped), and technical lookalikes are retained (UTF-8, SHA-256, T00:00:00, M365, UTC-5, pre-C4/pre-C5 schema versions). Flagged by the new CommentChecker TaskReferenceInComment / TrackingReferenceInComment checks plus targeted grep passes; full solution builds clean, append-only guard tests pass.
229 lines
10 KiB
C#
229 lines
10 KiB
C#
using System.Collections.Immutable;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Audit;
|
|
|
|
/// <summary>
|
|
/// UI-side binding model for <see cref="AuditFilterBar"/>.
|
|
///
|
|
/// <para>
|
|
/// The model mirrors <see cref="AuditLogQueryFilter"/> but allows multi-select chip
|
|
/// state for Channel / Kind / Status / Site (each a <see cref="HashSet{T}"/>) plus
|
|
/// extra UI-only fields the underlying filter does not carry: the Errors-only toggle,
|
|
/// the time-range preset, and free-text Instance / Script searches.
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// The repository filter contract (<see cref="AuditLogQueryFilter"/>) is multi-value
|
|
/// per dimension: the chip multi-selects map straight through to the
|
|
/// <c>Channels</c> / <c>Kinds</c> / <c>Statuses</c> / <c>SourceSiteIds</c> filter
|
|
/// lists when the model is published via <see cref="ToFilter"/> — an empty set means
|
|
/// "do not constrain". Instance and Script free-text remain UI-only: the underlying
|
|
/// filter has no matching columns, so they are dropped when the model is published.
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// The Errors-only toggle is a convenience: when true AND no explicit Status chips
|
|
/// are selected, <see cref="ToFilter"/> targets the full error-status set
|
|
/// {<see cref="AuditStatus.Failed"/>, <see cref="AuditStatus.Parked"/>,
|
|
/// <see cref="AuditStatus.Discarded"/>}. When Status chips ARE selected the toggle
|
|
/// is a no-op — the explicit Status chips win.
|
|
/// </para>
|
|
/// </summary>
|
|
public sealed class AuditQueryModel
|
|
{
|
|
/// <summary>Selected channel filter chips; empty means all channels.</summary>
|
|
public HashSet<AuditChannel> Channels { get; } = new();
|
|
/// <summary>Selected kind filter chips; empty means all kinds.</summary>
|
|
public HashSet<AuditKind> Kinds { get; } = new();
|
|
/// <summary>Selected status filter chips; empty means all statuses.</summary>
|
|
public HashSet<AuditStatus> Statuses { get; } = new();
|
|
/// <summary>Selected source-site identifier chips; empty means all sites.</summary>
|
|
public HashSet<string> SiteIdentifiers { get; } = new(StringComparer.OrdinalIgnoreCase);
|
|
|
|
/// <summary>
|
|
/// Selected source-node identifiers (e.g. <c>"central-a"</c>,
|
|
/// <c>"site-plant-a-node-a"</c>). Mirrors <see cref="SiteIdentifiers"/> —
|
|
/// chip multi-select state, empty = "do not constrain", mapped through to
|
|
/// <see cref="AuditLogQueryFilter.SourceNodes"/> by <see cref="ToFilter"/>.
|
|
/// </summary>
|
|
public HashSet<string> SourceNodes { get; } = new(StringComparer.OrdinalIgnoreCase);
|
|
|
|
/// <summary>Selected time-range preset controlling which historical window is queried.</summary>
|
|
public AuditTimeRangePreset TimeRange { get; set; } = AuditTimeRangePreset.LastHour;
|
|
/// <summary>Custom start of the time window; used only when <see cref="TimeRange"/> is <see cref="AuditTimeRangePreset.Custom"/>.</summary>
|
|
public DateTime? CustomFromUtc { get; set; }
|
|
/// <summary>Custom end of the time window; used only when <see cref="TimeRange"/> is <see cref="AuditTimeRangePreset.Custom"/>.</summary>
|
|
public DateTime? CustomToUtc { get; set; }
|
|
|
|
/// <summary>Free-text filter applied to instance names (UI-only; dropped when converting to <see cref="AuditLogQueryFilter"/>).</summary>
|
|
public string InstanceSearch { get; set; } = string.Empty;
|
|
/// <summary>Free-text filter applied to script names (UI-only; dropped when converting to <see cref="AuditLogQueryFilter"/>).</summary>
|
|
public string ScriptSearch { get; set; } = string.Empty;
|
|
/// <summary>Free-text filter applied to the target field (external system / DB name / notification list).</summary>
|
|
public string TargetSearch { get; set; } = string.Empty;
|
|
/// <summary>Free-text filter applied to the actor field (instance or inbound API key name).</summary>
|
|
public string ActorSearch { get; set; } = string.Empty;
|
|
|
|
/// <summary>
|
|
/// Paste-in ExecutionId filter — the operator pastes the universal per-run
|
|
/// correlation Guid. Stored as free text; <see cref="ToFilter"/> lax-parses it
|
|
/// through <see cref="Guid.TryParse(string?, out Guid)"/> so a blank or
|
|
/// unparseable value simply yields no constraint.
|
|
/// </summary>
|
|
public string ExecutionId { get; set; } = string.Empty;
|
|
|
|
/// <summary>
|
|
/// Paste-in ParentExecutionId filter — the operator pastes the spawner
|
|
/// execution's Guid to find every run it spawned. Stored as free text;
|
|
/// <see cref="ToFilter"/> lax-parses it through
|
|
/// <see cref="Guid.TryParse(string?, out Guid)"/> so a blank or unparseable
|
|
/// value simply yields no constraint, mirroring <see cref="ExecutionId"/>.
|
|
/// </summary>
|
|
public string ParentExecutionId { get; set; } = string.Empty;
|
|
|
|
/// <summary>When true and no explicit status chips are selected, the filter targets the full non-success status set.</summary>
|
|
public bool ErrorsOnly { get; set; }
|
|
|
|
/// <summary>
|
|
/// Maps each channel to the kinds it can emit (per Component-AuditLog.md §4).
|
|
/// <c>CachedSubmit</c> and <c>CachedResolve</c> appear under both
|
|
/// <see cref="AuditChannel.ApiOutbound"/> and <see cref="AuditChannel.DbOutbound"/>
|
|
/// because the cached-call lifecycle is channel-agnostic at submit/resolve time.
|
|
/// Used by the filter bar to narrow the Kind chip list once Channels are picked.
|
|
/// </summary>
|
|
public static readonly IReadOnlyDictionary<AuditChannel, ImmutableList<AuditKind>> KindsByChannel =
|
|
new Dictionary<AuditChannel, ImmutableList<AuditKind>>
|
|
{
|
|
[AuditChannel.ApiOutbound] = ImmutableList.Create(
|
|
AuditKind.ApiCall, AuditKind.ApiCallCached,
|
|
AuditKind.CachedSubmit, AuditKind.CachedResolve),
|
|
[AuditChannel.DbOutbound] = ImmutableList.Create(
|
|
AuditKind.DbWrite, AuditKind.DbWriteCached,
|
|
AuditKind.CachedSubmit, AuditKind.CachedResolve),
|
|
[AuditChannel.Notification] = ImmutableList.Create(
|
|
AuditKind.NotifySend, AuditKind.NotifyDeliver),
|
|
[AuditChannel.ApiInbound] = ImmutableList.Create(
|
|
AuditKind.InboundRequest, AuditKind.InboundAuthFailure),
|
|
};
|
|
|
|
/// <summary>
|
|
/// Returns the kinds visible in the Kind chip list given the currently selected
|
|
/// Channels. With no Channel selected, all 10 kinds are visible (no narrowing).
|
|
/// With one or more Channels selected, the union of the channel-specific kind
|
|
/// lists is returned (deduplicated and order-stable on first-seen).
|
|
/// </summary>
|
|
/// <returns>The deduplicated, order-stable list of <see cref="AuditKind"/> values applicable to the selected channels.</returns>
|
|
public IReadOnlyList<AuditKind> VisibleKinds()
|
|
{
|
|
if (Channels.Count == 0)
|
|
{
|
|
return Enum.GetValues<AuditKind>();
|
|
}
|
|
|
|
var seen = new HashSet<AuditKind>();
|
|
var result = new List<AuditKind>();
|
|
foreach (var ch in Channels)
|
|
{
|
|
if (!KindsByChannel.TryGetValue(ch, out var kinds))
|
|
{
|
|
continue;
|
|
}
|
|
foreach (var k in kinds)
|
|
{
|
|
if (seen.Add(k))
|
|
{
|
|
result.Add(k);
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Publishes this UI model as the repository's multi-value filter: each chip
|
|
/// multi-select maps straight through to its filter list (an empty set yields
|
|
/// <c>null</c> — "do not constrain"). See class doc for the Errors-only rule.
|
|
/// </summary>
|
|
/// <param name="utcNow">The current UTC timestamp used to compute relative time-range windows.</param>
|
|
/// <returns>A populated <see cref="AuditLogQueryFilter"/> ready for the repository.</returns>
|
|
public AuditLogQueryFilter ToFilter(DateTime utcNow)
|
|
{
|
|
var statuses = ResolveStatuses();
|
|
|
|
var (fromUtc, toUtc) = ResolveTimeWindow(utcNow);
|
|
|
|
// Lax-parse the pasted ExecutionId — blank or malformed text yields no
|
|
// constraint rather than an error, mirroring the optional-filter contract.
|
|
Guid? executionId = Guid.TryParse(ExecutionId, out var parsedExecutionId)
|
|
? parsedExecutionId
|
|
: null;
|
|
|
|
// Same lax-parse contract for the pasted ParentExecutionId.
|
|
Guid? parentExecutionId = Guid.TryParse(ParentExecutionId, out var parsedParentExecutionId)
|
|
? parsedParentExecutionId
|
|
: null;
|
|
|
|
return new AuditLogQueryFilter(
|
|
Channels: Channels.Count > 0 ? Channels.ToArray() : null,
|
|
Kinds: Kinds.Count > 0 ? Kinds.ToArray() : null,
|
|
Statuses: statuses,
|
|
SourceSiteIds: SiteIdentifiers.Count > 0 ? SiteIdentifiers.ToArray() : null,
|
|
Target: string.IsNullOrWhiteSpace(TargetSearch) ? null : TargetSearch.Trim(),
|
|
Actor: string.IsNullOrWhiteSpace(ActorSearch) ? null : ActorSearch.Trim(),
|
|
CorrelationId: null,
|
|
ExecutionId: executionId,
|
|
ParentExecutionId: parentExecutionId,
|
|
FromUtc: fromUtc,
|
|
ToUtc: toUtc,
|
|
SourceNodes: SourceNodes.Count > 0 ? SourceNodes.ToArray() : null);
|
|
}
|
|
|
|
/// <summary>The non-success statuses targeted by the Errors-only toggle.</summary>
|
|
private static readonly AuditStatus[] ErrorStatuses =
|
|
{ AuditStatus.Failed, AuditStatus.Parked, AuditStatus.Discarded };
|
|
|
|
private IReadOnlyList<AuditStatus>? ResolveStatuses()
|
|
{
|
|
if (Statuses.Count > 0)
|
|
{
|
|
// Explicit chips win — Errors-only is a no-op.
|
|
return Statuses.ToArray();
|
|
}
|
|
|
|
if (ErrorsOnly)
|
|
{
|
|
// Multi-value filter: Errors-only targets the full non-success set.
|
|
return ErrorStatuses;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private (DateTime? From, DateTime? To) ResolveTimeWindow(DateTime utcNow)
|
|
{
|
|
return TimeRange switch
|
|
{
|
|
AuditTimeRangePreset.Last5Minutes => (utcNow.AddMinutes(-5), null),
|
|
AuditTimeRangePreset.LastHour => (utcNow.AddHours(-1), null),
|
|
AuditTimeRangePreset.Last24Hours => (utcNow.AddHours(-24), null),
|
|
AuditTimeRangePreset.Custom => (CustomFromUtc, CustomToUtc),
|
|
_ => (null, null),
|
|
};
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Time-range presets surfaced in the filter bar. <see cref="Custom"/> reveals the
|
|
/// FromUtc / ToUtc datetime pickers; the other presets compute From relative to
|
|
/// "now" at the moment Apply is clicked.
|
|
/// </summary>
|
|
public enum AuditTimeRangePreset
|
|
{
|
|
Last5Minutes,
|
|
LastHour,
|
|
Last24Hours,
|
|
Custom,
|
|
}
|