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; /// /// UI-side binding model for . /// /// /// The model mirrors but allows multi-select chip /// state for Channel / Kind / Status / Site (each a ) 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. /// /// /// /// The repository filter contract () is multi-value /// per dimension: the chip multi-selects map straight through to the /// Channels / Kinds / Statuses / SourceSiteIds filter /// lists when the model is published via — 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. /// /// /// /// The Errors-only toggle is a convenience: when true AND no explicit Status chips /// are selected, targets the full error-status set /// {, , /// }. When Status chips ARE selected the toggle /// is a no-op — the explicit Status chips win. /// /// public sealed class AuditQueryModel { /// Selected channel filter chips; empty means all channels. public HashSet Channels { get; } = new(); /// Selected kind filter chips; empty means all kinds. public HashSet Kinds { get; } = new(); /// Selected status filter chips; empty means all statuses. public HashSet Statuses { get; } = new(); /// Selected source-site identifier chips; empty means all sites. public HashSet SiteIdentifiers { get; } = new(StringComparer.OrdinalIgnoreCase); /// /// Selected source-node identifiers (e.g. "central-a", /// "site-plant-a-node-a"). Mirrors — /// chip multi-select state, empty = "do not constrain", mapped through to /// by . /// public HashSet SourceNodes { get; } = new(StringComparer.OrdinalIgnoreCase); /// Selected time-range preset controlling which historical window is queried. public AuditTimeRangePreset TimeRange { get; set; } = AuditTimeRangePreset.LastHour; /// Custom start of the time window; used only when is . public DateTime? CustomFromUtc { get; set; } /// Custom end of the time window; used only when is . public DateTime? CustomToUtc { get; set; } /// Free-text filter applied to instance names (UI-only; dropped when converting to ). public string InstanceSearch { get; set; } = string.Empty; /// Free-text filter applied to script names (UI-only; dropped when converting to ). public string ScriptSearch { get; set; } = string.Empty; /// Free-text filter applied to the target field (external system / DB name / notification list). public string TargetSearch { get; set; } = string.Empty; /// Free-text filter applied to the actor field (instance or inbound API key name). public string ActorSearch { get; set; } = string.Empty; /// /// Paste-in ExecutionId filter — the operator pastes the universal per-run /// correlation Guid. Stored as free text; lax-parses it /// through so a blank or /// unparseable value simply yields no constraint. /// public string ExecutionId { get; set; } = string.Empty; /// /// Paste-in ParentExecutionId filter — the operator pastes the spawner /// execution's Guid to find every run it spawned. Stored as free text; /// lax-parses it through /// so a blank or unparseable /// value simply yields no constraint, mirroring . /// public string ParentExecutionId { get; set; } = string.Empty; /// When true and no explicit status chips are selected, the filter targets the full non-success status set. public bool ErrorsOnly { get; set; } /// /// Maps each channel to the kinds it can emit (per Component-AuditLog.md §4). /// CachedSubmit and CachedResolve appear under both /// and /// 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. /// public static readonly IReadOnlyDictionary> KindsByChannel = new Dictionary> { [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), }; /// /// 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). /// /// The deduplicated, order-stable list of values applicable to the selected channels. public IReadOnlyList VisibleKinds() { if (Channels.Count == 0) { return Enum.GetValues(); } var seen = new HashSet(); var result = new List(); 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; } /// /// 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 /// null — "do not constrain"). See class doc for the Errors-only rule. /// /// The current UTC timestamp used to compute relative time-range windows. /// A populated ready for the repository. 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); } /// The non-success statuses targeted by the Errors-only toggle. private static readonly AuditStatus[] ErrorStatuses = { AuditStatus.Failed, AuditStatus.Parked, AuditStatus.Discarded }; private IReadOnlyList? 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), }; } } /// /// Time-range presets surfaced in the filter bar. reveals the /// FromUtc / ToUtc datetime pickers; the other presets compute From relative to /// "now" at the moment Apply is clicked. /// public enum AuditTimeRangePreset { Last5Minutes, LastHour, Last24Hours, Custom, }