@oneuptime/common 13.0.5 → 13.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (1566) hide show
  1. package/Models/AnalyticsModels/AuditLog.ts +81 -1
  2. package/Models/AnalyticsModels/SecurityEvent.ts +4 -4
  3. package/Models/DatabaseModels/AIAgentOwnerTeam.ts +6 -0
  4. package/Models/DatabaseModels/AIAgentOwnerUser.ts +6 -0
  5. package/Models/DatabaseModels/Alert.ts +74 -0
  6. package/Models/DatabaseModels/AlertEpisodeOwnerTeam.ts +6 -1
  7. package/Models/DatabaseModels/AlertEpisodeOwnerUser.ts +6 -1
  8. package/Models/DatabaseModels/AlertOwnerTeam.ts +6 -1
  9. package/Models/DatabaseModels/AlertOwnerUser.ts +6 -1
  10. package/Models/DatabaseModels/CephClusterOwnerTeam.ts +6 -0
  11. package/Models/DatabaseModels/CephClusterOwnerUser.ts +6 -0
  12. package/Models/DatabaseModels/CloudResourceOwnerTeam.ts +6 -0
  13. package/Models/DatabaseModels/CloudResourceOwnerUser.ts +6 -0
  14. package/Models/DatabaseModels/DashboardOwnerTeam.ts +6 -1
  15. package/Models/DatabaseModels/DashboardOwnerUser.ts +6 -1
  16. package/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.ts +8 -0
  17. package/Models/DatabaseModels/DockerHostOwnerTeam.ts +6 -0
  18. package/Models/DatabaseModels/DockerHostOwnerUser.ts +6 -0
  19. package/Models/DatabaseModels/DockerSwarmClusterOwnerTeam.ts +6 -0
  20. package/Models/DatabaseModels/DockerSwarmClusterOwnerUser.ts +6 -0
  21. package/Models/DatabaseModels/GlobalConfig.ts +1 -1
  22. package/Models/DatabaseModels/HostOwnerTeam.ts +6 -0
  23. package/Models/DatabaseModels/HostOwnerUser.ts +6 -0
  24. package/Models/DatabaseModels/Incident.ts +78 -0
  25. package/Models/DatabaseModels/IncidentEpisodeOwnerTeam.ts +6 -1
  26. package/Models/DatabaseModels/IncidentEpisodeOwnerUser.ts +6 -1
  27. package/Models/DatabaseModels/IncidentEpisodePublicNote.ts +74 -0
  28. package/Models/DatabaseModels/IncidentOwnerTeam.ts +6 -1
  29. package/Models/DatabaseModels/IncidentOwnerUser.ts +6 -1
  30. package/Models/DatabaseModels/IncidentPublicNote.ts +74 -0
  31. package/Models/DatabaseModels/IncidentTemplateOwnerTeam.ts +6 -0
  32. package/Models/DatabaseModels/IncidentTemplateOwnerUser.ts +6 -0
  33. package/Models/DatabaseModels/IncomingCallPolicyOwnerTeam.ts +6 -1
  34. package/Models/DatabaseModels/IncomingCallPolicyOwnerUser.ts +6 -1
  35. package/Models/DatabaseModels/Index.ts +6 -4
  36. package/Models/DatabaseModels/IoTFleetOwnerTeam.ts +6 -0
  37. package/Models/DatabaseModels/IoTFleetOwnerUser.ts +6 -0
  38. package/Models/DatabaseModels/KubernetesClusterOwnerTeam.ts +6 -0
  39. package/Models/DatabaseModels/KubernetesClusterOwnerUser.ts +6 -0
  40. package/Models/DatabaseModels/Monitor.ts +2 -3
  41. package/Models/DatabaseModels/MonitorGroupOwnerTeam.ts +6 -0
  42. package/Models/DatabaseModels/MonitorGroupOwnerUser.ts +6 -0
  43. package/Models/DatabaseModels/MonitorOwnerTeam.ts +6 -1
  44. package/Models/DatabaseModels/MonitorOwnerUser.ts +6 -1
  45. package/Models/DatabaseModels/MonitorTemplate.ts +2 -3
  46. package/Models/DatabaseModels/NetworkDevice.ts +67 -0
  47. package/Models/DatabaseModels/NetworkDeviceDiagnostic.ts +746 -0
  48. package/Models/DatabaseModels/NetworkDeviceDiscoveryScan.ts +151 -0
  49. package/Models/DatabaseModels/NetworkDeviceOwnerTeam.ts +6 -0
  50. package/Models/DatabaseModels/NetworkDeviceOwnerUser.ts +6 -0
  51. package/Models/DatabaseModels/NetworkSiteAssignmentRule.ts +1 -1
  52. package/Models/DatabaseModels/OnCallDutyPolicyOwnerTeam.ts +6 -1
  53. package/Models/DatabaseModels/OnCallDutyPolicyOwnerUser.ts +6 -1
  54. package/Models/DatabaseModels/OnCallDutyPolicyScheduleOwnerTeam.ts +6 -1
  55. package/Models/DatabaseModels/OnCallDutyPolicyScheduleOwnerUser.ts +6 -1
  56. package/Models/DatabaseModels/PodmanHostOwnerTeam.ts +6 -0
  57. package/Models/DatabaseModels/PodmanHostOwnerUser.ts +6 -0
  58. package/Models/DatabaseModels/ProbeOwnerTeam.ts +6 -0
  59. package/Models/DatabaseModels/ProbeOwnerUser.ts +6 -0
  60. package/Models/DatabaseModels/Project.ts +41 -0
  61. package/Models/DatabaseModels/ProxmoxClusterOwnerTeam.ts +6 -0
  62. package/Models/DatabaseModels/ProxmoxClusterOwnerUser.ts +6 -0
  63. package/Models/DatabaseModels/RumApplicationOwnerTeam.ts +6 -0
  64. package/Models/DatabaseModels/RumApplicationOwnerUser.ts +6 -0
  65. package/Models/DatabaseModels/RunbookOwnerTeam.ts +6 -1
  66. package/Models/DatabaseModels/RunbookOwnerUser.ts +6 -1
  67. package/Models/DatabaseModels/RunnerOwnerTeam.ts +6 -1
  68. package/Models/DatabaseModels/RunnerOwnerUser.ts +6 -1
  69. package/Models/DatabaseModels/ScheduledMaintenanceOwnerTeam.ts +6 -1
  70. package/Models/DatabaseModels/ScheduledMaintenanceOwnerUser.ts +6 -1
  71. package/Models/DatabaseModels/ScheduledMaintenancePublicNote.ts +74 -0
  72. package/Models/DatabaseModels/ScheduledMaintenanceTemplateOwnerTeam.ts +8 -0
  73. package/Models/DatabaseModels/ScheduledMaintenanceTemplateOwnerUser.ts +8 -0
  74. package/Models/DatabaseModels/SecurityEventConnection.ts +11 -11
  75. package/Models/DatabaseModels/ServerlessFunctionOwnerTeam.ts +6 -0
  76. package/Models/DatabaseModels/ServerlessFunctionOwnerUser.ts +6 -0
  77. package/Models/DatabaseModels/ServiceLevelObjective.ts +185 -8
  78. package/Models/DatabaseModels/ServiceLevelObjectiveBurnRateRule.ts +746 -43
  79. package/Models/DatabaseModels/ServiceLevelObjectiveFeed.ts +575 -0
  80. package/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.ts +582 -0
  81. package/Models/DatabaseModels/ServiceLevelObjectiveOwnerTeam.ts +22 -1
  82. package/Models/DatabaseModels/ServiceLevelObjectiveOwnerUser.ts +22 -1
  83. package/Models/DatabaseModels/ServiceOwnerTeam.ts +6 -0
  84. package/Models/DatabaseModels/ServiceOwnerUser.ts +6 -0
  85. package/Models/DatabaseModels/StatusPageAnnouncement.ts +75 -0
  86. package/Models/DatabaseModels/StatusPageOwnerTeam.ts +6 -1
  87. package/Models/DatabaseModels/StatusPageOwnerUser.ts +6 -1
  88. package/Models/DatabaseModels/ThreatIntelFeed.ts +1 -1
  89. package/Models/DatabaseModels/VMwareVCenterOwnerTeam.ts +6 -0
  90. package/Models/DatabaseModels/VMwareVCenterOwnerUser.ts +6 -0
  91. package/Models/DatabaseModels/WorkflowOwnerTeam.ts +6 -1
  92. package/Models/DatabaseModels/WorkflowOwnerUser.ts +6 -1
  93. package/Scripts/benchmark-fanin-capacity.js +2 -2
  94. package/Server/API/AIAgentDataAPI.ts +1 -1
  95. package/Server/API/AIChatAPI.ts +6 -4
  96. package/Server/API/AIInvestigationAPI.ts +7 -0
  97. package/Server/API/BaseAPI.ts +17 -0
  98. package/Server/API/BillingInvoiceAPI.ts +343 -30
  99. package/Server/API/BillingPaymentMethodAPI.ts +126 -5
  100. package/Server/API/DashboardAPI.ts +96 -2
  101. package/Server/API/SecurityEventConnectionAPI.ts +141 -11
  102. package/Server/API/StatusPageAPI.ts +321 -199
  103. package/Server/Infrastructure/Postgres/LocalMigrationGenerationDataSource.ts +1 -1
  104. package/Server/Infrastructure/Postgres/SchemaMigrations/1792800000000-AddNetworkDeviceDiagnostic.ts +83 -0
  105. package/Server/Infrastructure/Postgres/SchemaMigrations/1792900000000-AddNetworkDeviceDnsNameAndShortDeviceNames.ts +36 -0
  106. package/Server/Infrastructure/Postgres/SchemaMigrations/1793000000000-AddNetbiosLookupToNetworkDeviceDiscoveryScan.ts +29 -0
  107. package/Server/Infrastructure/Postgres/SchemaMigrations/1793100000000-SloProductOverhaul.ts +516 -0
  108. package/Server/Infrastructure/Postgres/SchemaMigrations/1793200000000-BackfillSloMonitorRulesAndAffectedResources.ts +98 -0
  109. package/Server/Infrastructure/Postgres/SchemaMigrations/1793300000000-AddDataResidencyToProject.ts +27 -0
  110. package/Server/Infrastructure/Postgres/SchemaMigrations/1793400000000-AddSubscriberUpdateNotificationStatus.ts +97 -0
  111. package/Server/Infrastructure/Postgres/SchemaMigrations/1793500000000-MakeOwnerRowsUnique.ts +1057 -0
  112. package/Server/Infrastructure/Postgres/SchemaMigrations/1793700000000-AddAIInvestigationDecision.ts +25 -0
  113. package/Server/Infrastructure/Postgres/SchemaMigrations/1793800000000-RemoveMonitorDescriptionLengthLimit.ts +31 -0
  114. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +20 -0
  115. package/Server/Infrastructure/Postgres/SchemaMigrations/Readme.md +6 -4
  116. package/Server/Middleware/IdentityRateLimit.ts +34 -1
  117. package/Server/Middleware/PublicDashboardRateLimit.ts +169 -39
  118. package/Server/Services/AlertEpisodeLabelRuleEngineService.ts +138 -67
  119. package/Server/Services/AlertEpisodeOwnerRuleEngineService.ts +213 -84
  120. package/Server/Services/AlertEpisodePrivacyRuleEngineService.ts +112 -35
  121. package/Server/Services/AlertEpisodeService.ts +13 -34
  122. package/Server/Services/AlertGroupingEngineService.ts +7 -4
  123. package/Server/Services/AlertLabelRuleEngineService.ts +299 -217
  124. package/Server/Services/AlertOwnerRuleEngineService.ts +572 -396
  125. package/Server/Services/AlertPrivacyRuleEngineService.ts +116 -40
  126. package/Server/Services/AlertService.ts +124 -36
  127. package/Server/Services/AuditLogService.ts +612 -59
  128. package/Server/Services/BillingInvoiceService.ts +112 -102
  129. package/Server/Services/BillingService.ts +539 -11
  130. package/Server/Services/CephClusterLabelRuleEngineService.ts +170 -109
  131. package/Server/Services/CephClusterOwnerRuleEngineService.ts +237 -111
  132. package/Server/Services/CloudResourceLabelRuleEngineService.ts +163 -100
  133. package/Server/Services/CloudResourceOwnerRuleEngineService.ts +240 -111
  134. package/Server/Services/DashboardLabelRuleEngineService.ts +145 -84
  135. package/Server/Services/DashboardOwnerRuleEngineService.ts +217 -91
  136. package/Server/Services/DatabaseService.ts +272 -22
  137. package/Server/Services/DockerHostLabelRuleEngineService.ts +171 -110
  138. package/Server/Services/DockerHostOwnerRuleEngineService.ts +237 -111
  139. package/Server/Services/DockerSwarmClusterLabelRuleEngineService.ts +181 -112
  140. package/Server/Services/DockerSwarmClusterOwnerRuleEngineService.ts +247 -114
  141. package/Server/Services/HostLabelRuleEngineService.ts +167 -106
  142. package/Server/Services/HostOwnerRuleEngineService.ts +233 -107
  143. package/Server/Services/IncidentEpisodeLabelRuleEngineService.ts +141 -67
  144. package/Server/Services/IncidentEpisodeOwnerRuleEngineService.ts +274 -72
  145. package/Server/Services/IncidentEpisodePrivacyRuleEngineService.ts +112 -35
  146. package/Server/Services/IncidentEpisodePublicNoteService.ts +24 -0
  147. package/Server/Services/IncidentEpisodeService.ts +15 -75
  148. package/Server/Services/IncidentGroupingEngineService.ts +7 -4
  149. package/Server/Services/IncidentLabelRuleEngineService.ts +279 -204
  150. package/Server/Services/IncidentOwnerRuleEngineService.ts +595 -425
  151. package/Server/Services/IncidentPrivacyRuleEngineService.ts +117 -44
  152. package/Server/Services/IncidentPublicNoteService.ts +24 -0
  153. package/Server/Services/IncidentService.ts +127 -35
  154. package/Server/Services/IncidentTemplateService.ts +13 -36
  155. package/Server/Services/IncomingCallPolicyLabelRuleEngineService.ts +147 -83
  156. package/Server/Services/IncomingCallPolicyOwnerRuleEngineService.ts +225 -96
  157. package/Server/Services/Index.ts +6 -4
  158. package/Server/Services/IoTFleetLabelRuleEngineService.ts +145 -84
  159. package/Server/Services/IoTFleetOwnerRuleEngineService.ts +217 -91
  160. package/Server/Services/KubernetesClusterLabelRuleEngineService.ts +181 -112
  161. package/Server/Services/KubernetesClusterOwnerRuleEngineService.ts +247 -114
  162. package/Server/Services/LabelService.ts +23 -28
  163. package/Server/Services/MonitorLabelRuleEngineService.ts +152 -92
  164. package/Server/Services/MonitorOwnerRuleEngineService.ts +220 -104
  165. package/Server/Services/MonitorProbeService.ts +20 -2
  166. package/Server/Services/MonitorService.ts +24 -41
  167. package/Server/Services/NetworkDeviceAutoImportRuleEngineService.ts +376 -5
  168. package/Server/Services/NetworkDeviceDiagnosticService.ts +417 -0
  169. package/Server/Services/NetworkDeviceLabelRuleEngineService.ts +147 -83
  170. package/Server/Services/NetworkDeviceOwnerRuleEngineService.ts +222 -93
  171. package/Server/Services/NetworkDeviceService.ts +39 -2
  172. package/Server/Services/OnCallDutyPolicyLabelRuleEngineService.ts +151 -80
  173. package/Server/Services/OnCallDutyPolicyOwnerRuleEngineService.ts +226 -93
  174. package/Server/Services/OnCallDutyPolicyScheduleLabelRuleEngineService.ts +154 -83
  175. package/Server/Services/OnCallDutyPolicyScheduleOwnerRuleEngineService.ts +229 -96
  176. package/Server/Services/PodmanHostLabelRuleEngineService.ts +171 -110
  177. package/Server/Services/PodmanHostOwnerRuleEngineService.ts +239 -111
  178. package/Server/Services/ProjectService.ts +111 -10
  179. package/Server/Services/ProxmoxClusterLabelRuleEngineService.ts +174 -110
  180. package/Server/Services/ProxmoxClusterOwnerRuleEngineService.ts +243 -112
  181. package/Server/Services/RumApplicationLabelRuleEngineService.ts +137 -73
  182. package/Server/Services/RumApplicationOwnerRuleEngineService.ts +204 -79
  183. package/Server/Services/RunbookLabelRuleEngineService.ts +146 -84
  184. package/Server/Services/RunbookOwnerRuleEngineService.ts +220 -91
  185. package/Server/Services/ScheduledMaintenanceLabelRuleEngineService.ts +324 -230
  186. package/Server/Services/ScheduledMaintenanceOwnerRuleEngineService.ts +613 -428
  187. package/Server/Services/ScheduledMaintenancePublicNoteService.ts +24 -0
  188. package/Server/Services/ScheduledMaintenanceService.ts +56 -48
  189. package/Server/Services/ScheduledMaintenanceTemplateService.ts +12 -34
  190. package/Server/Services/SecurityEventConnectionService.ts +56 -4
  191. package/Server/Services/ServerlessFunctionLabelRuleEngineService.ts +143 -74
  192. package/Server/Services/ServerlessFunctionOwnerRuleEngineService.ts +210 -81
  193. package/Server/Services/ServiceLabelRuleEngineService.ts +172 -110
  194. package/Server/Services/ServiceLevelObjectiveBurnRateRuleService.ts +1057 -18
  195. package/Server/Services/ServiceLevelObjectiveFeedService.ts +108 -0
  196. package/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.ts +793 -97
  197. package/Server/Services/ServiceLevelObjectiveMonitorRuleService.ts +1069 -0
  198. package/Server/Services/ServiceLevelObjectiveOwnerTeamService.ts +240 -1
  199. package/Server/Services/ServiceLevelObjectiveOwnerUserService.ts +257 -1
  200. package/Server/Services/ServiceLevelObjectiveService.ts +1356 -56
  201. package/Server/Services/ServiceOwnerRuleEngineService.ts +242 -112
  202. package/Server/Services/StatusPageAnnouncementService.ts +13 -0
  203. package/Server/Services/StatusPageLabelRuleEngineService.ts +146 -85
  204. package/Server/Services/StatusPageOwnerRuleEngineService.ts +210 -99
  205. package/Server/Services/StatusPagePrivateUserService.ts +38 -29
  206. package/Server/Services/StatusPagePrivateUserSessionService.ts +49 -1
  207. package/Server/Services/StatusPageService.ts +59 -36
  208. package/Server/Services/StatusPageSubscriberNotificationTemplateService.ts +28 -232
  209. package/Server/Services/StatusPageSubscriberService.ts +6 -0
  210. package/Server/Services/TelemetryUsageBillingService.ts +38 -7
  211. package/Server/Services/ThreatIntelFeedService.ts +1 -1
  212. package/Server/Services/UserSessionService.ts +6 -0
  213. package/Server/Services/VMwareVCenterLabelRuleEngineService.ts +173 -110
  214. package/Server/Services/VMwareVCenterOwnerRuleEngineService.ts +242 -112
  215. package/Server/Services/WorkflowLabelRuleEngineService.ts +145 -84
  216. package/Server/Services/WorkflowOwnerRuleEngineService.ts +219 -91
  217. package/Server/Static/Brand/README.md +7 -7
  218. package/Server/Static/Vendor/README.md +7 -7
  219. package/Server/Types/Database/Permissions/OwnedScopePermission.ts +18 -9
  220. package/Server/Types/Database/Permissions/OwnerTableRegistry.ts +82 -0
  221. package/Server/Types/Database/UpdateByID.ts +3 -0
  222. package/Server/Types/Database/UpdateOneBy.ts +7 -0
  223. package/Server/Utils/AI/AlertAIContextBuilder.ts +19 -0
  224. package/Server/Utils/AI/Chat/ChatAgentRunner.ts +21 -8
  225. package/Server/Utils/AI/Chat/ObservabilityChatPrompt.ts +41 -4
  226. package/Server/Utils/AI/CodeFix/CodeAgentWorkspaceGuard.ts +1 -1
  227. package/Server/Utils/AI/CodeFix/CodeFixAgentCompletion.ts +1 -1
  228. package/Server/Utils/AI/IncidentAIContextBuilder.ts +39 -0
  229. package/Server/Utils/AI/SRE/AIInvestigationEngine.ts +13 -5
  230. package/Server/Utils/AI/SRE/AlertInvestigationRunner.ts +53 -13
  231. package/Server/Utils/AI/SRE/IncidentInvestigationRunner.ts +51 -14
  232. package/Server/Utils/AI/SRE/InvestigationEligibility.ts +292 -0
  233. package/Server/Utils/AI/SRE/InvestigationQueue.ts +25 -0
  234. package/Server/Utils/AI/Toolbox/AlertTools.ts +37 -1
  235. package/Server/Utils/AI/Toolbox/IncidentTools.ts +22 -1
  236. package/Server/Utils/AI/Toolbox/Index.ts +43 -13
  237. package/Server/Utils/AI/Toolbox/ResourceTools.ts +952 -0
  238. package/Server/Utils/AI/Toolbox/RumTools.ts +424 -0
  239. package/Server/Utils/AI/Toolbox/SloTools.ts +416 -51
  240. package/Server/Utils/AI/Toolbox/ToolTypes.ts +10 -0
  241. package/Server/Utils/AI/Toolbox/TraceTools.ts +2 -1
  242. package/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.ts +151 -13
  243. package/Server/Utils/Dashboard/PublicDashboardSloHistoryPolicy.ts +17 -4
  244. package/Server/Utils/Dashboard/PublicDashboardSloWidget.ts +155 -8
  245. package/Server/Utils/Database/PostgresErrorTranslator.ts +14 -0
  246. package/Server/Utils/Database/ProjectScopedReferenceValidator.ts +16 -0
  247. package/Server/Utils/Database/RelationValueUtil.ts +106 -0
  248. package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +143 -127
  249. package/Server/Utils/Monitor/Criteria/EvaluateOverTime.ts +22 -27
  250. package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +4 -1
  251. package/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.ts +3 -1
  252. package/Server/Utils/Monitor/MonitorStepsProjectValidator.ts +50 -20
  253. package/Server/Utils/Monitor/NetworkDeviceHydrationUtil.ts +13 -1
  254. package/Server/Utils/Monitor/NetworkDeviceMetricUtil.ts +6 -11
  255. package/Server/Utils/Rules/MonitorRulePatternValidator.ts +51 -0
  256. package/Server/Utils/Rules/OwnerRuleAssignment.ts +261 -0
  257. package/Server/Utils/Rules/RuleRun/RuleApplication.ts +81 -0
  258. package/Server/Utils/Rules/RuleRun/RuleRunPermission.ts +188 -0
  259. package/Server/Utils/Rules/RuleRun/RuleRunRegistry.ts +803 -0
  260. package/Server/Utils/Rules/RuleRun/RuleRunner.ts +341 -0
  261. package/Server/Utils/SecurityEvent/ConnectorErrorMessage.ts +3 -2
  262. package/Server/Utils/SecurityEvent/Connectors/ConnectorPlatformHealth.ts +6 -4
  263. package/Server/Utils/SecurityEvent/{GoogleSecOps → Connectors/GoogleSecOps}/GoogleSecOpsClient.ts +263 -16
  264. package/Server/Utils/SecurityEvent/Connectors/GoogleSecOps/GoogleSecOpsConnector.ts +2101 -0
  265. package/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionPoller.ts +194 -14
  266. package/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionRunExecutor.ts +1 -2
  267. package/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionTester.ts +38 -7
  268. package/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectorRegistry.ts +2 -0
  269. package/Server/Utils/SecurityEvent/Connectors/Types.ts +242 -2
  270. package/Server/Utils/SecurityEvent/DetectionRuleEvaluator.ts +1 -1
  271. package/Server/Utils/SecurityEvent/SecurityEventDedupe.ts +3 -3
  272. package/Server/Utils/SecurityEvent/ThreatIntel/ThreatIntelFeedPoller.ts +2 -2
  273. package/Server/Utils/SessionReplay/SessionReplayReadService.ts +217 -56
  274. package/Server/Utils/Slo/SloFeedUtil.ts +200 -0
  275. package/Server/Utils/Slo/SloLegacyMonitorLabelAdoption.ts +173 -0
  276. package/Server/Utils/Slo/SloMetricUtil.ts +327 -0
  277. package/Server/Utils/Slo/SloOwnerReferenceValidator.ts +208 -0
  278. package/Server/Utils/Slo/SloRecordReferenceValidator.ts +169 -0
  279. package/Server/Utils/StatusPage/MonitorRulePatternValidator.ts +6 -43
  280. package/Server/Utils/StatusPageResource.ts +50 -5
  281. package/Server/Utils/Telemetry/TelemetryEntity.ts +205 -6
  282. package/SslCertificates/GlobalSign.crt +26 -0
  283. package/SslCertificates/Readme.md +3 -0
  284. package/Tests/App/AdminDashboard/EnterpriseLicenseInstanceActivity.test.tsx +451 -0
  285. package/Tests/App/Dashboard/AIInsightsChatPanel.test.tsx +881 -0
  286. package/Tests/App/Dashboard/AIInsightsPageContext.test.ts +458 -0
  287. package/Tests/App/Dashboard/AIInsightsResourceCitations.test.ts +124 -0
  288. package/Tests/App/Dashboard/AIInsightsResourcePageContext.test.ts +798 -0
  289. package/Tests/App/Dashboard/AIInvestigationHeaderStatus.test.tsx +664 -15
  290. package/Tests/App/Dashboard/AddWidgetModal.test.tsx +4 -1
  291. package/Tests/App/Dashboard/AffectedResourcesSlo.test.tsx +307 -0
  292. package/Tests/App/Dashboard/ArchiveResourceCard.test.tsx +289 -0
  293. package/Tests/App/Dashboard/AskAiSuggestedPrompts.test.ts +7 -0
  294. package/Tests/App/Dashboard/AuditLogsTable.test.tsx +598 -0
  295. package/Tests/App/Dashboard/BillingDefaultPaymentMethod.test.tsx +1046 -0
  296. package/Tests/App/Dashboard/BillingPaidUsageFlow.test.tsx +3 -0
  297. package/Tests/App/Dashboard/BillingPaymentMethodForm.test.tsx +201 -7
  298. package/Tests/App/Dashboard/BooleanSeriesCriteriaFilterText.test.ts +82 -0
  299. package/Tests/App/Dashboard/BreadcrumbCoverage.test.tsx +16 -1
  300. package/Tests/App/Dashboard/ChangeAlertStateAIHeader.test.tsx +116 -8
  301. package/Tests/App/Dashboard/ChatActivityFeed.test.tsx +168 -1
  302. package/Tests/App/Dashboard/ConnectionTestModal.test.tsx +1 -1
  303. package/Tests/App/Dashboard/ConnectorTestReportView.test.tsx +20 -3
  304. package/Tests/App/Dashboard/CorrelateFilterBuilder.test.tsx +1095 -1
  305. package/Tests/App/Dashboard/CorrelateFilterChips.test.tsx +910 -0
  306. package/Tests/App/Dashboard/CorrelateGraph.test.tsx +3747 -25
  307. package/Tests/App/Dashboard/CorrelateGraphHelpers.test.ts +1085 -0
  308. package/Tests/App/Dashboard/CorrelateInspector.test.tsx +1534 -0
  309. package/Tests/App/Dashboard/CorrelateNodeCard.test.tsx +965 -0
  310. package/Tests/App/Dashboard/CorrelateResultSummary.test.tsx +1525 -0
  311. package/Tests/App/Dashboard/CorrelationGraph.test.ts +1030 -0
  312. package/Tests/App/Dashboard/CorrelationGraphLayout.test.ts +526 -0
  313. package/Tests/App/Dashboard/DashboardChartWidgetZoom.test.tsx +104 -0
  314. package/Tests/App/Dashboard/DashboardCommandPalette.test.tsx +156 -64
  315. package/Tests/App/Dashboard/DeviceDiagnostics.test.tsx +787 -0
  316. package/Tests/App/Dashboard/DeviceLatencyTrend.test.tsx +278 -0
  317. package/Tests/App/Dashboard/DiscoveryReviewInventoryRefresh.test.tsx +279 -0
  318. package/Tests/App/Dashboard/DiscoveryScanEditForm.test.tsx +87 -0
  319. package/Tests/App/Dashboard/DiscoveryScanStatusMessage.test.tsx +970 -0
  320. package/Tests/App/Dashboard/DiscoveryScanWizardValidation.test.tsx +849 -20
  321. package/Tests/App/Dashboard/EmbeddedMetricEventScope.test.tsx +358 -0
  322. package/Tests/App/Dashboard/EntityFilterDropdownArchivedSlos.test.tsx +340 -0
  323. package/Tests/App/Dashboard/ErrorPatternEventScope.test.tsx +148 -0
  324. package/Tests/App/Dashboard/EventOverlayAdditionalResourceScope.test.ts +255 -0
  325. package/Tests/App/Dashboard/EventOverlayScope.test.ts +845 -0
  326. package/Tests/App/Dashboard/EventOverlayScopeHook.test.tsx +903 -0
  327. package/Tests/App/Dashboard/EventOverviewPages.test.tsx +98 -2
  328. package/Tests/App/Dashboard/ExceptionSegmentedControl.test.tsx +1020 -0
  329. package/Tests/App/Dashboard/ExternalStatusPageCriteriaFilter.test.ts +3 -3
  330. package/Tests/App/Dashboard/InvestigationDrawer.test.tsx +21 -0
  331. package/Tests/App/Dashboard/InvestigationEvidenceList.test.tsx +268 -47
  332. package/Tests/App/Dashboard/InvestigationNotStarted.test.tsx +761 -0
  333. package/Tests/App/Dashboard/InvestigationPanel.test.tsx +1122 -81
  334. package/Tests/App/Dashboard/InvestigationPanelStatus.test.tsx +139 -9
  335. package/Tests/App/Dashboard/InvestigationReportView.test.tsx +279 -176
  336. package/Tests/App/Dashboard/InvestigationRunDetails.test.tsx +1166 -0
  337. package/Tests/App/Dashboard/InvoicesPayInvoice.test.tsx +437 -0
  338. package/Tests/App/Dashboard/MetricsViewerEventScopeNavigation.test.tsx +264 -0
  339. package/Tests/App/Dashboard/MonitorSummaryTestMonitorButton.test.tsx +342 -0
  340. package/Tests/App/Dashboard/MonitorTestForm.test.tsx +612 -0
  341. package/Tests/App/Dashboard/NavigationSearchKeywords.test.tsx +301 -0
  342. package/Tests/App/Dashboard/NetworkDeviceBulkDelete.test.tsx +65 -0
  343. package/Tests/App/Dashboard/NetworkTopologyLiveView.test.tsx +5 -0
  344. package/Tests/App/Dashboard/ResourceAuditLogsSideMenus.test.tsx +74 -0
  345. package/Tests/App/Dashboard/ResourceFeed.test.tsx +125 -5
  346. package/Tests/App/Dashboard/ResourceFeedPageWiring.test.ts +8 -1
  347. package/Tests/App/Dashboard/SecurityEventConnectionDiagnostics.test.tsx +1764 -0
  348. package/Tests/App/Dashboard/SecurityEventConnectionFormModal.test.tsx +1000 -64
  349. package/Tests/App/Dashboard/SecurityEventConnectionsEmptyState.test.tsx +448 -0
  350. package/Tests/App/Dashboard/SecurityEventConnectionsTable.test.tsx +739 -189
  351. package/Tests/App/Dashboard/SecurityEventSeverityPill.test.tsx +233 -0
  352. package/Tests/App/Dashboard/SecurityEventsEmptyState.test.tsx +282 -0
  353. package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +74 -2
  354. package/Tests/App/Dashboard/SloActiveBurnEventsCard.test.tsx +538 -0
  355. package/Tests/App/Dashboard/SloBulkActionsArchive.test.tsx +252 -0
  356. package/Tests/App/Dashboard/SloDashboardTemplateWidgets.test.tsx +322 -518
  357. package/Tests/App/Dashboard/SloElement.test.tsx +194 -0
  358. package/Tests/App/Dashboard/SloFeed.test.tsx +289 -0
  359. package/Tests/App/Dashboard/SloHistoryCharts.test.tsx +471 -0
  360. package/Tests/App/Dashboard/SloHistorySeries.test.tsx +607 -0
  361. package/Tests/App/Dashboard/SloListWidget.test.tsx +783 -0
  362. package/Tests/App/Dashboard/SloMetricsPage.test.tsx +346 -0
  363. package/Tests/App/Dashboard/SloMonitorsPage.test.tsx +661 -0
  364. package/Tests/App/Dashboard/SloMonitorsSummaryCard.test.tsx +489 -0
  365. package/Tests/App/Dashboard/SloNavigationWiring.test.tsx +437 -0
  366. package/Tests/App/Dashboard/SloNoticeBanner.test.tsx +305 -0
  367. package/Tests/App/Dashboard/SloOverviewData.test.tsx +563 -0
  368. package/Tests/App/Dashboard/SloOverviewHero.test.tsx +311 -0
  369. package/Tests/App/Dashboard/SloOverviewKpiStrip.test.tsx +426 -0
  370. package/Tests/App/Dashboard/SloOverviewSidebarCards.test.tsx +493 -0
  371. package/Tests/App/Dashboard/SloSettings.test.tsx +633 -0
  372. package/Tests/App/Dashboard/SloStatusSummaryCards.test.tsx +403 -0
  373. package/Tests/App/Dashboard/SloViewSideMenuCounts.test.tsx +373 -0
  374. package/Tests/App/Dashboard/SloWidgetVariableBinding.test.tsx +689 -0
  375. package/Tests/App/Dashboard/TextClampLayoutHarness.ts +224 -0
  376. package/Tests/App/Dashboard/TopologyConnectivitySection.test.tsx +206 -0
  377. package/Tests/App/Dashboard/TracesEntityNames.test.tsx +47 -15
  378. package/Tests/App/Dashboard/UseBulkShortenDeviceNames.test.tsx +1312 -0
  379. package/Tests/App/Dashboard/WidgetCatalog.test.ts +9 -1
  380. package/Tests/Models/AnalyticsModels/RumSessionReplayColumns.test.ts +14 -16
  381. package/Tests/Models/DatabaseModels/AuditLogModelConfiguration.test.ts +442 -0
  382. package/Tests/Models/DatabaseModels/DomainRoleTierCoverage.test.ts +9 -10
  383. package/Tests/Models/DatabaseModels/ResourceFeedModels.test.ts +48 -1
  384. package/Tests/Models/DatabaseModels/SloOverhaulModels.test.ts +835 -0
  385. package/Tests/Models/DatabaseModels/StaticRuleCriteriaInheritance.test.ts +3 -2
  386. package/Tests/Models/DiscoveryScanNetbiosLookupColumn.test.ts +328 -0
  387. package/Tests/Models/DiscoveryScanShortDeviceNamesColumn.test.ts +335 -0
  388. package/Tests/Models/InventoryItemNaming.test.ts +54 -0
  389. package/Tests/Models/NetworkDeviceDiagnosticContract.test.ts +194 -0
  390. package/Tests/Models/NetworkDeviceDnsNameColumn.test.ts +321 -0
  391. package/Tests/Models/ProjectDataResidencyColumn.test.ts +154 -0
  392. package/Tests/Models/SecurityDomainAccessControl.test.ts +181 -50
  393. package/Tests/Server/API/AIChatRouteAuthorization.test.ts +161 -0
  394. package/Tests/Server/API/AIInvestigationAPI.test.ts +14 -0
  395. package/Tests/Server/API/AIInvestigationAvailabilityAuthorization.test.ts +260 -0
  396. package/Tests/Server/API/BaseAPI.test.ts +6 -0
  397. package/Tests/Server/API/BaseAPIUpdateMiscDataProps.test.ts +217 -0
  398. package/Tests/Server/API/BillingInvoiceAPI.test.ts +7 -0
  399. package/Tests/Server/API/BillingInvoiceAPIPaymentStates.test.ts +990 -0
  400. package/Tests/Server/API/BillingPaymentMethodAPI.test.ts +498 -0
  401. package/Tests/Server/API/DashboardPublicSloAPI.test.ts +405 -0
  402. package/Tests/Server/API/EnterpriseLicenseReportUserCount.test.ts +133 -0
  403. package/Tests/Server/API/SecurityEventConnectionAPI.test.ts +901 -7
  404. package/Tests/Server/API/SessionReplayAPI.test.ts +116 -0
  405. package/Tests/Server/API/StatusPageManageSubscriptionRecipient.test.ts +857 -0
  406. package/Tests/Server/API/StatusPageManageSubscriptionTemplateVariables.test.ts +1084 -0
  407. package/Tests/Server/API/StatusPageMasterPasswordRateLimit.test.ts +482 -0
  408. package/Tests/Server/EnvironmentConfigOnCallCalendarFeed.test.ts +1 -1
  409. package/Tests/Server/Infrastructure/ClickhouseKeepAliveOptions.test.ts +1 -1
  410. package/Tests/Server/Infrastructure/ContainerBootConfiguration.test.ts +38 -16
  411. package/Tests/Server/Infrastructure/E2EClickHouseFixtureAccess.test.ts +106 -31
  412. package/Tests/Server/Infrastructure/ExampleDockerfiles.test.ts +46 -12
  413. package/Tests/Server/Infrastructure/Postgres/AddConfigurableRuleCriteriaMigration.test.ts +33 -3
  414. package/Tests/Server/Infrastructure/Postgres/AddDataResidencyToProjectMigration.test.ts +202 -0
  415. package/Tests/Server/Infrastructure/Postgres/AddNetbiosLookupToNetworkDeviceDiscoveryScanMigration.test.ts +330 -0
  416. package/Tests/Server/Infrastructure/Postgres/AddNetworkDeviceDnsNameAndShortDeviceNamesMigration.test.ts +340 -0
  417. package/Tests/Server/Infrastructure/Postgres/BackfillSloMonitorRulesAndAffectedResourcesMigration.test.ts +343 -0
  418. package/Tests/Server/Infrastructure/Postgres/MonitorDescriptionMigration.test.ts +422 -0
  419. package/Tests/Server/Infrastructure/Postgres/SloProductOverhaulMigration.test.ts +801 -0
  420. package/Tests/Server/Infrastructure/TelemetryExporterDeploymentConfig.test.ts +1 -1
  421. package/Tests/Server/Infrastructure/ValkeyDeploymentConfig.test.ts +25 -4
  422. package/Tests/Server/Middleware/IdentityRateLimit.test.ts +72 -0
  423. package/Tests/Server/Middleware/PublicDashboardRateLimit.test.ts +12 -12
  424. package/Tests/Server/Middleware/PublicDashboardRateLimitStatusPage.test.ts +800 -0
  425. package/Tests/Server/Services/AIInvestigationDecisionPostgres.test.ts +346 -0
  426. package/Tests/Server/Services/AddOwnersIdempotency.test.ts +471 -0
  427. package/Tests/Server/Services/AddSubscriberUpdateNotificationStatusMigration.test.ts +307 -0
  428. package/Tests/Server/Services/AuditLogService.test.ts +1124 -0
  429. package/Tests/Server/Services/AutoImportScanCredentialSelect.test.ts +200 -1
  430. package/Tests/Server/Services/BillingInvoiceServiceMutexRelease.test.ts +254 -0
  431. package/Tests/Server/Services/BillingService.test.ts +70 -0
  432. package/Tests/Server/Services/BillingServicePayInvoiceStates.test.ts +723 -0
  433. package/Tests/Server/Services/BillingServicePaymentMethodReads.test.ts +14 -2
  434. package/Tests/Server/Services/BillingServiceSubscriptionPaymentMethodSync.test.ts +1634 -0
  435. package/Tests/Server/Services/DatabaseServiceAuditRelationDiff.test.ts +492 -0
  436. package/Tests/Server/Services/DatabaseServiceAutoOwnerOnCreate.test.ts +250 -0
  437. package/Tests/Server/Services/DatabaseServicePasswordFieldSecrets.test.ts +629 -0
  438. package/Tests/Server/Services/DatabaseServiceUniqueColumnsTogether.test.ts +622 -0
  439. package/Tests/Server/Services/IncidentAlertSloLinkSurvivesEdit.test.ts +431 -0
  440. package/Tests/Server/Services/IncidentAlertSloReferenceGuard.test.ts +358 -0
  441. package/Tests/Server/Services/MonitorDescriptionValidation.test.ts +211 -0
  442. package/Tests/Server/Services/MonitorProbeServicePastDueMonitoring.test.ts +362 -0
  443. package/Tests/Server/Services/MonitorTemplateDescriptionPropagation.test.ts +269 -0
  444. package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +1460 -1
  445. package/Tests/Server/Services/NetworkDeviceDiagnosticService.test.ts +962 -0
  446. package/Tests/Server/Services/NetworkDeviceDiscoveryScanService.test.ts +106 -0
  447. package/Tests/Server/Services/NetworkDeviceRuleEngines.test.ts +10 -1
  448. package/Tests/Server/Services/NetworkDeviceServicePastDueMonitoring.test.ts +335 -0
  449. package/Tests/Server/Services/NetworkDeviceSiteRuleDnsName.test.ts +568 -0
  450. package/Tests/Server/Services/OwnerRowUniqueness.test.ts +504 -0
  451. package/Tests/Server/Services/ProjectDataResidencyPostgres.test.ts +344 -0
  452. package/Tests/Server/Services/ProjectServiceAuditLogSettingsCache.test.ts +187 -0
  453. package/Tests/Server/Services/ProjectServiceDataResidency.test.ts +369 -0
  454. package/Tests/Server/Services/ProjectServiceDataResidencySelfHosted.test.ts +176 -0
  455. package/Tests/Server/Services/ProjectServicePastDueMonitoring.test.ts +289 -0
  456. package/Tests/Server/Services/ResourceFeedServices.test.ts +24 -8
  457. package/Tests/Server/Services/RuleRun/IncidentAlertMaintenanceRuleEnginesRun.test.ts +1396 -0
  458. package/Tests/Server/Services/RuleRun/LabelRuleEnginesGroupARun.test.ts +917 -0
  459. package/Tests/Server/Services/RuleRun/LabelRuleEnginesGroupBRun.test.ts +1262 -0
  460. package/Tests/Server/Services/RuleRun/OwnerRuleEnginesGroupARun.test.ts +945 -0
  461. package/Tests/Server/Services/RuleRun/OwnerRuleEnginesGroupBRun.test.ts +1163 -0
  462. package/Tests/Server/Services/RuleRun/PrivacyRuleEnginesRun.test.ts +843 -0
  463. package/Tests/Server/Services/ScheduledMaintenanceSubscriberTemplateFormats.test.ts +465 -0
  464. package/Tests/Server/Services/SecurityEventConnectionService.test.ts +525 -4
  465. package/Tests/Server/Services/SecurityEventLastErrorColumnWidth.test.ts +77 -23
  466. package/Tests/Server/Services/ServiceLevelObjectiveBurnRateRuleFeedWrites.test.ts +777 -0
  467. package/Tests/Server/Services/ServiceLevelObjectiveBurnRateRuleOptions.test.ts +1399 -0
  468. package/Tests/Server/Services/ServiceLevelObjectiveBurnRateRuleService.test.ts +67 -74
  469. package/Tests/Server/Services/ServiceLevelObjectiveChildRowTenancy.test.ts +906 -0
  470. package/Tests/Server/Services/ServiceLevelObjectiveDeleteTenancy.test.ts +1029 -0
  471. package/Tests/Server/Services/ServiceLevelObjectiveFeedService.test.ts +313 -0
  472. package/Tests/Server/Services/ServiceLevelObjectiveFeedWrites.test.ts +1214 -0
  473. package/Tests/Server/Services/ServiceLevelObjectiveMarkdownLink.test.ts +289 -0
  474. package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.test.ts +1455 -430
  475. package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleLegacyLabelAdoption.test.ts +634 -0
  476. package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleService.test.ts +1193 -0
  477. package/Tests/Server/Services/{SloMonitorLabelRuleHooks.test.ts → ServiceLevelObjectiveMonitorRuleTriggers.test.ts} +135 -41
  478. package/Tests/Server/Services/ServiceLevelObjectiveOwnerFeedWrites.test.ts +671 -0
  479. package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +180 -85
  480. package/Tests/Server/Services/ServiceLevelObjectiveServiceArchive.test.ts +537 -0
  481. package/Tests/Server/Services/ServiceLevelObjectiveServiceLegacyMonitorLabelWrite.test.ts +870 -0
  482. package/Tests/Server/Services/ServiceLevelObjectiveServiceMonitorRuleGuard.test.ts +343 -0
  483. package/Tests/Server/Services/ServiceLevelObjectiveServiceReEvaluation.test.ts +246 -0
  484. package/Tests/Server/Services/SessionRetention.test.ts +334 -0
  485. package/Tests/Server/Services/SloAffectedResourceCreatedFeed.test.ts +501 -0
  486. package/Tests/Server/Services/SloAffectedResourceLinkContract.test.ts +86 -0
  487. package/Tests/Server/Services/SloAffectedResourceMetricAttributes.test.ts +547 -0
  488. package/Tests/Server/Services/StaticRuleCriteriaRuntimeCoverage.test.ts +10 -4
  489. package/Tests/Server/Services/StatusPageMonitorRuleHooks.test.ts +7 -2
  490. package/Tests/Server/Services/StatusPagePrivateUserInvitationPolicy.test.ts +172 -0
  491. package/Tests/Server/Services/StatusPagePrivateUserReadAccess.test.ts +178 -0
  492. package/Tests/Server/Services/StatusPagePrivateUserReadAccessPostgres.test.ts +311 -0
  493. package/Tests/Server/Services/StatusPagePrivateUserSessionService.test.ts +216 -1
  494. package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateCompile.test.ts +174 -0
  495. package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateServiceVariables.test.ts +40 -0
  496. package/Tests/Server/Services/SubscriberUpdateNotificationHooks.test.ts +541 -0
  497. package/Tests/Server/Services/TeamComplianceServiceBehaviour.test.ts +1 -1
  498. package/Tests/Server/Services/TelemetryUsageBillingSloExclusion.test.ts +288 -0
  499. package/Tests/Server/Services/UserOnCallLogNoNotificationRules.test.ts +1 -1
  500. package/Tests/Server/TestingUtils/PastDueMonitoringClaimSql.ts +177 -0
  501. package/Tests/Server/TestingUtils/Services/BillingServiceHelper.ts +51 -10
  502. package/Tests/Server/TestingUtils/__mocks__/Stripe.mock.ts +16 -1
  503. package/Tests/Server/Types/Database/EventOverlayScopePostgres.test.ts +626 -0
  504. package/Tests/Server/Types/Database/Permissions/InfrastructureOwnershipScope.test.ts +668 -0
  505. package/Tests/Server/Types/Database/Permissions/ProjectDataResidencyColumn.test.ts +386 -0
  506. package/Tests/Server/Types/Database/Permissions/SecurityRoleAccess.test.ts +26 -36
  507. package/Tests/Server/Types/Database/Permissions/SelectRejectionMessageContract.test.ts +1 -0
  508. package/Tests/Server/Types/Database/Permissions/UserNotificationRuleScoping.test.ts +1 -1
  509. package/Tests/Server/Utils/AI/AIIncidentGating.test.ts +16 -9
  510. package/Tests/Server/Utils/AI/ChatAgentRunner.test.ts +137 -1
  511. package/Tests/Server/Utils/AI/InvestigationEligibility.test.ts +697 -0
  512. package/Tests/Server/Utils/AI/ObservabilityChatPrompt.test.ts +105 -0
  513. package/Tests/Server/Utils/AI/RemediationHandoff.test.ts +16 -5
  514. package/Tests/Server/Utils/AI/ResourceTools.test.ts +1304 -0
  515. package/Tests/Server/Utils/AI/RumTools.test.ts +769 -0
  516. package/Tests/Server/Utils/AI/SloAffectedResourceAIContext.test.ts +445 -0
  517. package/Tests/Server/Utils/AI/SloTools.test.ts +423 -1
  518. package/Tests/Server/Utils/AI/SloToolsArchive.test.ts +294 -0
  519. package/Tests/Server/Utils/AI/SloToolsRuleReadPermissions.test.ts +297 -0
  520. package/Tests/Server/Utils/Dashboard/PublicDashboardSloHistoryPolicy.test.ts +255 -10
  521. package/Tests/Server/Utils/Dashboard/SloTemplatePublicDashboard.test.ts +404 -750
  522. package/Tests/Server/Utils/Database/PostgresErrorTranslator.test.ts +63 -0
  523. package/Tests/Server/Utils/Database/ProjectScopedReferenceValidator.test.ts +45 -0
  524. package/Tests/Server/Utils/Database/RelationValueUtil.test.ts +157 -0
  525. package/Tests/Server/Utils/Monitor/Criteria/APIRequestCriteriaEvaluateOverTime.test.ts +11 -9
  526. package/Tests/Server/Utils/Monitor/Criteria/BooleanSeriesEvaluateOverTime.test.ts +260 -0
  527. package/Tests/Server/Utils/Monitor/Criteria/CompareCriteria.test.ts +62 -45
  528. package/Tests/Server/Utils/Monitor/Criteria/CompareCriteriaAggregation.test.ts +25 -24
  529. package/Tests/Server/Utils/Monitor/Criteria/CompareCriteriaHumanMessages.test.ts +513 -0
  530. package/Tests/Server/Utils/Monitor/Criteria/CustomCodeMonitorCriteria.test.ts +2 -2
  531. package/Tests/Server/Utils/Monitor/Criteria/DatabaseMonitorCriteria.test.ts +181 -0
  532. package/Tests/Server/Utils/Monitor/Criteria/DnsMonitorCriteria.test.ts +1 -1
  533. package/Tests/Server/Utils/Monitor/Criteria/EvaluateOverTime.test.ts +147 -30
  534. package/Tests/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.test.ts +9 -9
  535. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.ts +7 -5
  536. package/Tests/Server/Utils/Monitor/Criteria/ProfileMonitorCriteria.test.ts +2 -2
  537. package/Tests/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.test.ts +4 -3
  538. package/Tests/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.test.ts +2 -2
  539. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorMetricUnits.test.ts +3 -2
  540. package/Tests/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.test.ts +17 -0
  541. package/Tests/Server/Utils/Monitor/MonitorCriteriaHumanMessages.test.ts +529 -0
  542. package/Tests/Server/Utils/Monitor/MonitorStepsProjectValidator.test.ts +136 -0
  543. package/Tests/Server/Utils/Monitor/MonitorStepsProjectValidatorLoadOrder.test.ts +472 -0
  544. package/Tests/Server/Utils/Monitor/NetworkDeviceHydrationUtil.test.ts +48 -2
  545. package/Tests/Server/Utils/Monitor/SloTemplateMetricEmitterContract.test.ts +435 -475
  546. package/Tests/Server/Utils/Rules/MonitorRulePatternValidator.test.ts +77 -0
  547. package/Tests/Server/Utils/Rules/OwnerRuleAssignment.test.ts +585 -0
  548. package/Tests/Server/Utils/Rules/RuleRun/RuleRunPermission.test.ts +365 -0
  549. package/Tests/Server/Utils/Rules/RuleRun/RuleRunRegistry.test.ts +173 -0
  550. package/Tests/Server/Utils/Rules/RuleRun/RuleRunner.test.ts +675 -0
  551. package/Tests/Server/Utils/SecurityEvent/ConnectorErrorMessage.test.ts +8 -8
  552. package/Tests/Server/Utils/SecurityEvent/Connectors/ConnectorFetchSummary.test.ts +261 -0
  553. package/Tests/Server/Utils/SecurityEvent/Connectors/ConnectorPlatformHealth.test.ts +68 -13
  554. package/Tests/Server/Utils/SecurityEvent/{GoogleSecOpsAuth.test.ts → Connectors/GoogleSecOps/GoogleSecOpsAuth.test.ts} +5 -5
  555. package/Tests/Server/Utils/SecurityEvent/{GoogleSecOpsClient.test.ts → Connectors/GoogleSecOps/GoogleSecOpsClient.test.ts} +4 -4
  556. package/Tests/Server/Utils/SecurityEvent/Connectors/GoogleSecOps/GoogleSecOpsConnector.test.ts +483 -0
  557. package/Tests/Server/Utils/SecurityEvent/Connectors/GoogleSecOps/GoogleSecOpsConnectorCuratedAndLateAlerts.test.ts +676 -0
  558. package/Tests/Server/Utils/SecurityEvent/Connectors/GoogleSecOps/GoogleSecOpsConnectorFetchEvents.test.ts +1994 -0
  559. package/Tests/Server/Utils/SecurityEvent/Connectors/GoogleSecOps/GoogleSecOpsConnectorFixtures.ts +332 -0
  560. package/Tests/Server/Utils/SecurityEvent/Connectors/GoogleSecOps/GoogleSecOpsConnectorHttpIntegration.test.ts +680 -0
  561. package/Tests/Server/Utils/SecurityEvent/Connectors/GoogleSecOps/GoogleSecOpsConnectorRequestContract.test.ts +1206 -0
  562. package/Tests/Server/Utils/SecurityEvent/Connectors/GoogleSecOps/GoogleSecOpsConnectorTestConnection.test.ts +1136 -0
  563. package/Tests/Server/Utils/SecurityEvent/Connectors/GoogleSecOps/GoogleSecOpsCuratedRuleCounts.test.ts +407 -0
  564. package/Tests/Server/Utils/SecurityEvent/{GoogleSecOpsErrorHandling.test.ts → Connectors/GoogleSecOps/GoogleSecOpsErrorHandling.test.ts} +5 -5
  565. package/Tests/Server/Utils/SecurityEvent/Connectors/GoogleSecOps/GoogleSecOpsPollingAdaptiveCatchUp.test.ts +730 -0
  566. package/Tests/Server/Utils/SecurityEvent/{GoogleSecOpsClickhouseIntegration.test.ts → Connectors/GoogleSecOps/GoogleSecOpsPollingClickhouseIntegration.test.ts} +131 -36
  567. package/Tests/Server/Utils/SecurityEvent/Connectors/GoogleSecOps/GoogleSecOpsPollingFailureTaxonomy.test.ts +772 -0
  568. package/Tests/Server/Utils/SecurityEvent/Connectors/GoogleSecOps/GoogleSecOpsPollingFixtures.ts +648 -0
  569. package/Tests/Server/Utils/SecurityEvent/{GoogleSecOpsHttpIntegration.test.ts → Connectors/GoogleSecOps/GoogleSecOpsPollingHttpIntegration.test.ts} +230 -225
  570. package/Tests/Server/Utils/SecurityEvent/Connectors/GoogleSecOps/GoogleSecOpsPollingIntegration.test.ts +1390 -0
  571. package/Tests/Server/Utils/SecurityEvent/Connectors/GoogleSecOps/GoogleSecOpsPollingLoop.test.ts +981 -0
  572. package/Tests/Server/Utils/SecurityEvent/Connectors/GoogleSecOps/GoogleSecOpsPollingNothingImportedRegression.test.ts +290 -0
  573. package/Tests/Server/Utils/SecurityEvent/{GoogleSecOpsRequestContract.test.ts → Connectors/GoogleSecOps/GoogleSecOpsRequestContract.test.ts} +2 -2
  574. package/Tests/Server/Utils/SecurityEvent/{GoogleSecOpsResponseParsing.test.ts → Connectors/GoogleSecOps/GoogleSecOpsResponseParsing.test.ts} +5 -5
  575. package/Tests/Server/Utils/SecurityEvent/{GoogleSecOpsSearchDetections.test.ts → Connectors/GoogleSecOps/GoogleSecOpsSearchDetections.test.ts} +38 -5
  576. package/Tests/Server/Utils/SecurityEvent/Connectors/GoogleSecOps/GoogleSecOpsTesterIntegration.test.ts +421 -0
  577. package/Tests/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionPoller.test.ts +1445 -3
  578. package/Tests/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionRunExecutor.test.ts +14 -4
  579. package/Tests/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionTester.test.ts +313 -4
  580. package/Tests/Server/Utils/SessionReplay/SessionReplayReadServiceQueries.test.ts +352 -0
  581. package/Tests/Server/Utils/Slo/SloFeedUtil.test.ts +326 -0
  582. package/Tests/Server/Utils/Slo/SloLegacyMonitorLabelAdoption.test.ts +260 -0
  583. package/Tests/Server/Utils/Slo/SloMetricUtil.test.ts +592 -0
  584. package/Tests/Server/Utils/Slo/SloRecordReferenceValidator.test.ts +311 -0
  585. package/Tests/Server/Utils/StatusPageResource.test.ts +91 -3
  586. package/Tests/Server/Utils/TailwindBuildCopy.test.ts +14 -2
  587. package/Tests/Server/Utils/Telemetry/HostAssetAttributesFromOtlp.test.ts +235 -0
  588. package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +385 -2
  589. package/Tests/Server/Utils/VendorAssets.test.ts +17 -3
  590. package/Tests/Types/AI/AIChatPageContext.test.ts +0 -0
  591. package/Tests/Types/AI/AIResourceContext.test.ts +312 -0
  592. package/Tests/Types/Billing/SubscriptionStatus.test.ts +176 -0
  593. package/Tests/Types/Billing/SubscriptionStatusPastDueMonitoring.test.ts +187 -0
  594. package/Tests/Types/Dashboard/DashboardTemplateInvariants.test.ts +53 -7
  595. package/Tests/Types/Dashboard/SloDashboardTemplate.test.ts +816 -792
  596. package/Tests/Types/Database/UniqueColumnsTogether.test.ts +100 -0
  597. package/Tests/Types/Monitor/CriteriaFilter.test.ts +216 -24
  598. package/Tests/Types/Monitor/MetricAndProfileDefaultCriteria.test.ts +4 -4
  599. package/Tests/Types/Monitor/MonitorStepDnssecMonitor.test.ts +14 -1
  600. package/Tests/Types/Monitor/MonitorStepDomainMonitor.test.ts +14 -1
  601. package/Tests/Types/Monitor/MonitorStepExternalStatusPageMonitor.test.ts +14 -1
  602. package/Tests/Types/Monitor/MonitorStepRetriesParsing.test.ts +201 -0
  603. package/Tests/Types/Monitor/MonitorStepRetryPersistence.test.ts +125 -0
  604. package/Tests/Types/Rules/RuleRun.test.ts +315 -0
  605. package/Tests/Types/Rum/SessionReplayApiContracts.test.ts +60 -0
  606. package/Tests/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.test.ts +29 -4
  607. package/Tests/Types/StatusPage/SubscriberNotificationTemplateVariables.test.ts +217 -0
  608. package/Tests/Types/StatusPage/SubscriberUpdateNotification.test.ts +200 -0
  609. package/Tests/Types/WebsiteRequest.test.ts +44 -0
  610. package/Tests/UI/Components/ActiveFilterChipsLockedFilters.test.tsx +141 -87
  611. package/Tests/UI/Components/AlertBanner.test.tsx +319 -0
  612. package/Tests/UI/Components/BulkArchiveActionsCopy.test.tsx +310 -0
  613. package/Tests/UI/Components/Detail/DetailIdIsUnique.test.tsx +87 -0
  614. package/Tests/UI/Components/Dropdown/DropdownClearable.test.tsx +393 -0
  615. package/Tests/UI/Components/Dropdown/DropdownMultiSelectChipAccessibleName.test.tsx +64 -0
  616. package/Tests/UI/Components/EmptyState/EmptyState.test.tsx +335 -4
  617. package/Tests/UI/Components/EmptyState/EmptyStateGuideLinks.test.tsx +337 -0
  618. package/Tests/UI/Components/Forms/ModelFormJsonColumns.test.tsx +21 -20
  619. package/Tests/UI/Components/Forms/MonitorDescriptionForm.test.tsx +410 -0
  620. package/Tests/UI/Components/LabelRuleImportExport.test.tsx +15 -2
  621. package/Tests/UI/Components/LockedFilterChip.test.tsx +281 -144
  622. package/Tests/UI/Components/ModelTable/BaseModelTableReadPermissions.test.tsx +439 -0
  623. package/Tests/UI/Components/MoreMenu.test.tsx +133 -0
  624. package/Tests/UI/Components/MoreMenuMotion.test.tsx +84 -3
  625. package/Tests/UI/Components/NavBarMenuSearch.test.tsx +372 -0
  626. package/Tests/UI/Components/RuleCriteria/RuleCriteriaModelTable.test.tsx +3 -3
  627. package/Tests/UI/Components/RuleRun/RuleDetailFields.test.tsx +336 -0
  628. package/Tests/UI/Components/RuleRun/RuleTable.test.tsx +304 -0
  629. package/Tests/UI/Components/RuleRun/RunRuleNowModal.test.tsx +231 -0
  630. package/Tests/UI/Components/RuleRun/RunRulesBulkAction.test.ts +213 -0
  631. package/Tests/UI/Components/TelemetryActiveFilterChipsLockedFilters.test.tsx +124 -61
  632. package/Tests/UI/EsbuildConfig.test.ts +1 -1
  633. package/Tests/UI/Monitor/MonitorStepRetrySettings.test.tsx +616 -0
  634. package/Tests/UI/ReactRouterSingletonBuild.test.ts +4 -2
  635. package/Tests/UI/Rum/ReplayEngine.test.ts +333 -0
  636. package/Tests/UI/Rum/ReplayHeader.test.tsx +257 -123
  637. package/Tests/UI/Rum/ReplayPinControl.test.tsx +122 -0
  638. package/Tests/UI/Rum/ReplayPlayerChrome.test.tsx +245 -0
  639. package/Tests/UI/Rum/ReplayRail.test.tsx +294 -0
  640. package/Tests/UI/Rum/ReplayScrubber.test.tsx +301 -1
  641. package/Tests/UI/Rum/ReplayStage.test.tsx +565 -172
  642. package/Tests/UI/Rum/ReplayStageOverlays.test.tsx +343 -3
  643. package/Tests/UI/Rum/ReplayTabSwitcher.test.tsx +1424 -0
  644. package/Tests/UI/Rum/ReplayTimeline.test.tsx +222 -0
  645. package/Tests/UI/Utils/DashboardStackingLayers.test.ts +8 -1
  646. package/Tests/UI/Utils/RuleRunClient.test.ts +303 -0
  647. package/Tests/UI/Utils/Telemetry/TelemetryEntityNames.test.ts +1 -0
  648. package/Tests/Utils/API.test.ts +96 -0
  649. package/Tests/Utils/Dashboard/Components/DashboardComponentDefaults.test.ts +2 -0
  650. package/Tests/Utils/Dashboard/Components/DashboardSloListComponent.test.ts +206 -0
  651. package/Tests/Utils/Dashboard/DashboardSloComponent.test.ts +36 -2
  652. package/Tests/Utils/Dashboard/SloTemplateVariableScoping.test.ts +409 -588
  653. package/Tests/Utils/Dashboard/SloWidgetSource.test.ts +351 -0
  654. package/Tests/Utils/HTTPResponseBodyReader.test.ts +188 -1
  655. package/Tests/Utils/Markdown/MarkdownEscape.test.ts +361 -0
  656. package/Tests/Utils/Metrics/MetricEventScopeUrl.test.ts +101 -0
  657. package/Tests/Utils/Monitor/MonitorTestAvailabilityUtil.test.ts +256 -0
  658. package/Tests/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.test.ts +1425 -118
  659. package/Tests/Utils/NetworkDiscovery/DiscoveredHostUtil.test.ts +188 -3
  660. package/Tests/Utils/NetworkDiscovery/DiscoveryReverseDnsChain.test.ts +661 -13
  661. package/Tests/Utils/NetworkDiscovery/NetbiosNameUtil.test.ts +349 -0
  662. package/Tests/Utils/NetworkDiscovery/RunImportedDeviceNaming.test.ts +656 -0
  663. package/Tests/Utils/NetworkDiscovery/ShortDeviceNamePlanner.test.ts +1020 -0
  664. package/Tests/Utils/NetworkDiscovery/ShortHostnameUtil.test.ts +619 -0
  665. package/Tests/Utils/NetworkSite/CidrMatchUtil.test.ts +317 -0
  666. package/Tests/Utils/Project/DataResidency.test.ts +198 -0
  667. package/Tests/Utils/Rules/RuleRunSummary.test.ts +394 -0
  668. package/Tests/Utils/Slo/SloAffectedResourceMarkdown.test.ts +302 -0
  669. package/Tests/Utils/Slo/SloBurnRateTemplate.test.ts +468 -0
  670. package/Tests/Utils/Slo/SloEvaluation.test.ts +20 -4
  671. package/Tests/Utils/Slo/SloFeedMarkdown.test.ts +1185 -0
  672. package/Tests/Utils/Slo/SloHealth.test.ts +262 -0
  673. package/Tests/Utils/Slo/SloHistoryMetricName.test.ts +102 -0
  674. package/Tests/Utils/Slo/SloListWidgetFormat.test.ts +522 -0
  675. package/Tests/Utils/Slo/SloMetricType.test.ts +355 -0
  676. package/Tests/Utils/Slo/SloMonitorRuleCriteria.test.ts +387 -0
  677. package/Tests/Utils/Slo/SloMonitorStatusSummary.test.ts +210 -0
  678. package/Tests/Utils/Slo/SloOverviewText.test.ts +226 -0
  679. package/Tests/Utils/Slo/SloProjection.test.ts +1039 -0
  680. package/Tests/Utils/Slo/SloWidgetFormat.test.ts +47 -0
  681. package/Tests/Utils/Telemetry/HostIpAddresses.test.ts +105 -0
  682. package/Tests/Utils/Telemetry/LockedFilterSearch.test.ts +1 -80
  683. package/Types/AI/AIChatPageContext.ts +87 -2
  684. package/Types/AI/AIChatTypes.ts +4 -0
  685. package/Types/AI/AIResourceContext.ts +198 -0
  686. package/Types/AI/InvestigationNotStartedReason.ts +30 -0
  687. package/Types/BaseDatabase/EnableAuditLogOn.ts +41 -0
  688. package/Types/Billing/SubscriptionStatus.ts +39 -4
  689. package/Types/Dashboard/DashboardComponentType.ts +1 -0
  690. package/Types/Dashboard/DashboardComponents/ComponentArgument.ts +5 -0
  691. package/Types/Dashboard/DashboardComponents/DashboardSloComponent.ts +10 -0
  692. package/Types/Dashboard/DashboardComponents/DashboardSloListComponent.ts +29 -0
  693. package/Types/Dashboard/DashboardTemplates.ts +290 -297
  694. package/Types/Database/EnableAuditLog.ts +23 -1
  695. package/Types/Database/UniqueColumnsTogether.ts +55 -0
  696. package/Types/Email/EmailTemplateType.ts +4 -0
  697. package/Types/Metrics/MetricQueryConfigData.ts +8 -0
  698. package/Types/Monitor/CriteriaFilter.ts +55 -8
  699. package/Types/Monitor/MonitorStep.ts +5 -2
  700. package/Types/Monitor/MonitorStepDnsMonitor.ts +3 -1
  701. package/Types/Monitor/MonitorStepDnssecMonitor.ts +3 -1
  702. package/Types/Monitor/MonitorStepDomainMonitor.ts +3 -1
  703. package/Types/Monitor/MonitorStepExternalStatusPageMonitor.ts +3 -1
  704. package/Types/Monitor/MonitorStepRetries.ts +59 -0
  705. package/Types/Monitor/MonitorStepSnmpMonitor.ts +9 -2
  706. package/Types/NetworkDevice/NetworkDeviceDiagnosticResult.ts +104 -0
  707. package/Types/NetworkDevice/NetworkDeviceDiagnosticStatus.ts +47 -0
  708. package/Types/NetworkDevice/NetworkDeviceDiagnosticType.ts +47 -0
  709. package/Types/NetworkDevice/NetworkDevicePingMetricNames.ts +20 -0
  710. package/Types/Permission.ts +127 -0
  711. package/Types/Rules/RuleCriteriaFieldRegistry.ts +5 -0
  712. package/Types/Rules/RuleRun.ts +518 -0
  713. package/Types/Rum/SessionReplayApi.ts +18 -0
  714. package/Types/Rum/WebVitals.ts +71 -0
  715. package/Types/SecurityEvent/Connectors/ConnectorDiagnostics.ts +6 -0
  716. package/Types/SecurityEvent/Connectors/SecurityEventConnectionDiagnostics.ts +30 -4
  717. package/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.ts +108 -1
  718. package/Types/SecurityEvent/Connectors/SecurityEventConnectorProvider.ts +6 -2
  719. package/Types/ServiceLevelObjective/SloHistoryMetricName.ts +25 -0
  720. package/Types/ServiceLevelObjective/SloMetricType.ts +49 -0
  721. package/Types/StatusPage/StatusPageSubscriberNotificationEventType.ts +4 -0
  722. package/Types/StatusPage/SubscriberNotificationTemplateVariables.ts +331 -0
  723. package/Types/StatusPage/SubscriberNotificationTrigger.ts +12 -0
  724. package/Types/StatusPage/SubscriberUpdateNotification.ts +89 -0
  725. package/Types/Telemetry/LockedFilterDetail.ts +7 -28
  726. package/Types/Telemetry/ServiceType.ts +7 -0
  727. package/Types/WebsiteRequest.ts +2 -0
  728. package/Typings/Index.d.ts +53 -3
  729. package/UI/Components/AlertBanner/AlertBanner.tsx +85 -27
  730. package/UI/Components/BulkUpdate/BulkArchiveActions.tsx +21 -2
  731. package/UI/Components/Detail/Detail.tsx +9 -2
  732. package/UI/Components/Dropdown/Dropdown.tsx +64 -1
  733. package/UI/Components/EditionLabel/EditionLabel.tsx +7 -5
  734. package/UI/Components/EmptyState/EmptyState.tsx +27 -3
  735. package/UI/Components/EmptyState/EmptyStateGuideLinks.tsx +124 -0
  736. package/UI/Components/Forms/ModelForm.tsx +6 -5
  737. package/UI/Components/KeyboardShortcut/Screenshots/README.md +1 -1
  738. package/UI/Components/LabelRule/LabelRuleTable.tsx +8 -2
  739. package/UI/Components/LogsViewer/LogsViewer.tsx +2 -6
  740. package/UI/Components/LogsViewer/components/ActiveFilterChips.tsx +2 -13
  741. package/UI/Components/LogsViewer/types.ts +2 -2
  742. package/UI/Components/ModelTable/BaseModelTable.tsx +8 -0
  743. package/UI/Components/MoreMenu/MoreMenu.tsx +14 -3
  744. package/UI/Components/Navbar/NavBar.tsx +2 -0
  745. package/UI/Components/Navbar/NavBarMenuModal.tsx +6 -2
  746. package/UI/Components/RuleRun/RuleDetailFields.tsx +359 -0
  747. package/UI/Components/RuleRun/RuleTable.tsx +183 -0
  748. package/UI/Components/RuleRun/RuleView.tsx +170 -0
  749. package/UI/Components/RuleRun/RunRuleNowModal.tsx +147 -0
  750. package/UI/Components/RuleRun/RunRulesBulkAction.ts +103 -0
  751. package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +2 -6
  752. package/UI/Components/TelemetryViewer/components/LockedFilterChip.tsx +38 -104
  753. package/UI/Components/TelemetryViewer/components/TelemetryActiveFilterChips.tsx +2 -18
  754. package/UI/Components/TelemetryViewer/types.ts +2 -2
  755. package/UI/Utils/Rules/RuleRunClient.ts +178 -0
  756. package/UI/Utils/Telemetry/TelemetryEntityNames.ts +8 -0
  757. package/UI/esbuild-config.js +4 -4
  758. package/Utils/API.ts +10 -1
  759. package/Utils/Dashboard/Components/DashboardSloComponent.ts +19 -2
  760. package/Utils/Dashboard/Components/DashboardSloListComponent.ts +126 -0
  761. package/Utils/Dashboard/Components/Index.ts +7 -0
  762. package/Utils/Dashboard/HtmlWidgetDocument.ts +1 -1
  763. package/Utils/Dashboard/SloWidgetSource.ts +188 -0
  764. package/Utils/HTTPResponseBodyReader.ts +64 -8
  765. package/Utils/Markdown/MarkdownEscape.ts +63 -0
  766. package/Utils/Metrics/MetricExplorerUrl.ts +19 -0
  767. package/Utils/Monitor/MonitorTestAvailabilityUtil.ts +92 -0
  768. package/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.ts +161 -36
  769. package/Utils/NetworkDiscovery/DiscoveredHostUtil.ts +25 -0
  770. package/Utils/NetworkDiscovery/NetbiosNameUtil.ts +146 -0
  771. package/Utils/NetworkDiscovery/RunImportedDeviceNaming.ts +390 -0
  772. package/Utils/NetworkDiscovery/ShortDeviceNamePlanner.ts +291 -0
  773. package/Utils/NetworkDiscovery/ShortHostnameUtil.ts +91 -0
  774. package/Utils/NetworkSite/CidrMatchUtil.ts +37 -12
  775. package/Utils/Project/DataResidency.ts +63 -0
  776. package/Utils/Rules/RuleRunSummary.ts +323 -0
  777. package/Utils/Slo/SloAffectedResourceMarkdown.ts +123 -0
  778. package/Utils/Slo/SloBurnRateTemplate.ts +373 -0
  779. package/Utils/Slo/SloFeedMarkdown.ts +1507 -0
  780. package/Utils/Slo/SloHealth.ts +96 -9
  781. package/Utils/Slo/SloListWidgetFormat.ts +370 -0
  782. package/Utils/Slo/SloMetricType.ts +237 -0
  783. package/Utils/Slo/SloMonitorRuleCriteria.ts +364 -0
  784. package/Utils/Slo/SloMonitorStatusSummary.ts +210 -0
  785. package/Utils/Slo/SloOverviewText.ts +265 -0
  786. package/Utils/Slo/SloProjection.ts +967 -0
  787. package/Utils/Slo/SloWidgetFormat.ts +12 -2
  788. package/Utils/Telemetry/HostIpAddresses.ts +46 -2
  789. package/Utils/Telemetry/LockedFilterSearch.ts +3 -49
  790. package/build/dist/Models/AnalyticsModels/AuditLog.js +70 -1
  791. package/build/dist/Models/AnalyticsModels/AuditLog.js.map +1 -1
  792. package/build/dist/Models/AnalyticsModels/SecurityEvent.js +4 -4
  793. package/build/dist/Models/DatabaseModels/AIAgentOwnerTeam.js +4 -1
  794. package/build/dist/Models/DatabaseModels/AIAgentOwnerTeam.js.map +1 -1
  795. package/build/dist/Models/DatabaseModels/AIAgentOwnerUser.js +4 -1
  796. package/build/dist/Models/DatabaseModels/AIAgentOwnerUser.js.map +1 -1
  797. package/build/dist/Models/DatabaseModels/Alert.js +72 -0
  798. package/build/dist/Models/DatabaseModels/Alert.js.map +1 -1
  799. package/build/dist/Models/DatabaseModels/AlertEpisodeOwnerTeam.js +3 -1
  800. package/build/dist/Models/DatabaseModels/AlertEpisodeOwnerTeam.js.map +1 -1
  801. package/build/dist/Models/DatabaseModels/AlertEpisodeOwnerUser.js +3 -1
  802. package/build/dist/Models/DatabaseModels/AlertEpisodeOwnerUser.js.map +1 -1
  803. package/build/dist/Models/DatabaseModels/AlertOwnerTeam.js +3 -1
  804. package/build/dist/Models/DatabaseModels/AlertOwnerTeam.js.map +1 -1
  805. package/build/dist/Models/DatabaseModels/AlertOwnerUser.js +3 -1
  806. package/build/dist/Models/DatabaseModels/AlertOwnerUser.js.map +1 -1
  807. package/build/dist/Models/DatabaseModels/CephClusterOwnerTeam.js +4 -1
  808. package/build/dist/Models/DatabaseModels/CephClusterOwnerTeam.js.map +1 -1
  809. package/build/dist/Models/DatabaseModels/CephClusterOwnerUser.js +4 -1
  810. package/build/dist/Models/DatabaseModels/CephClusterOwnerUser.js.map +1 -1
  811. package/build/dist/Models/DatabaseModels/CloudResourceOwnerTeam.js +4 -1
  812. package/build/dist/Models/DatabaseModels/CloudResourceOwnerTeam.js.map +1 -1
  813. package/build/dist/Models/DatabaseModels/CloudResourceOwnerUser.js +4 -1
  814. package/build/dist/Models/DatabaseModels/CloudResourceOwnerUser.js.map +1 -1
  815. package/build/dist/Models/DatabaseModels/DashboardOwnerTeam.js +3 -1
  816. package/build/dist/Models/DatabaseModels/DashboardOwnerTeam.js.map +1 -1
  817. package/build/dist/Models/DatabaseModels/DashboardOwnerUser.js +3 -1
  818. package/build/dist/Models/DatabaseModels/DashboardOwnerUser.js.map +1 -1
  819. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js +3 -0
  820. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js.map +1 -1
  821. package/build/dist/Models/DatabaseModels/DockerHostOwnerTeam.js +4 -1
  822. package/build/dist/Models/DatabaseModels/DockerHostOwnerTeam.js.map +1 -1
  823. package/build/dist/Models/DatabaseModels/DockerHostOwnerUser.js +4 -1
  824. package/build/dist/Models/DatabaseModels/DockerHostOwnerUser.js.map +1 -1
  825. package/build/dist/Models/DatabaseModels/DockerSwarmClusterOwnerTeam.js +4 -1
  826. package/build/dist/Models/DatabaseModels/DockerSwarmClusterOwnerTeam.js.map +1 -1
  827. package/build/dist/Models/DatabaseModels/DockerSwarmClusterOwnerUser.js +4 -1
  828. package/build/dist/Models/DatabaseModels/DockerSwarmClusterOwnerUser.js.map +1 -1
  829. package/build/dist/Models/DatabaseModels/GlobalConfig.js +1 -1
  830. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  831. package/build/dist/Models/DatabaseModels/HostOwnerTeam.js +4 -1
  832. package/build/dist/Models/DatabaseModels/HostOwnerTeam.js.map +1 -1
  833. package/build/dist/Models/DatabaseModels/HostOwnerUser.js +4 -1
  834. package/build/dist/Models/DatabaseModels/HostOwnerUser.js.map +1 -1
  835. package/build/dist/Models/DatabaseModels/Incident.js +76 -0
  836. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  837. package/build/dist/Models/DatabaseModels/IncidentEpisodeOwnerTeam.js +3 -1
  838. package/build/dist/Models/DatabaseModels/IncidentEpisodeOwnerTeam.js.map +1 -1
  839. package/build/dist/Models/DatabaseModels/IncidentEpisodeOwnerUser.js +3 -1
  840. package/build/dist/Models/DatabaseModels/IncidentEpisodeOwnerUser.js.map +1 -1
  841. package/build/dist/Models/DatabaseModels/IncidentEpisodePublicNote.js +75 -0
  842. package/build/dist/Models/DatabaseModels/IncidentEpisodePublicNote.js.map +1 -1
  843. package/build/dist/Models/DatabaseModels/IncidentOwnerTeam.js +3 -1
  844. package/build/dist/Models/DatabaseModels/IncidentOwnerTeam.js.map +1 -1
  845. package/build/dist/Models/DatabaseModels/IncidentOwnerUser.js +3 -1
  846. package/build/dist/Models/DatabaseModels/IncidentOwnerUser.js.map +1 -1
  847. package/build/dist/Models/DatabaseModels/IncidentPublicNote.js +75 -0
  848. package/build/dist/Models/DatabaseModels/IncidentPublicNote.js.map +1 -1
  849. package/build/dist/Models/DatabaseModels/IncidentTemplateOwnerTeam.js +4 -1
  850. package/build/dist/Models/DatabaseModels/IncidentTemplateOwnerTeam.js.map +1 -1
  851. package/build/dist/Models/DatabaseModels/IncidentTemplateOwnerUser.js +4 -1
  852. package/build/dist/Models/DatabaseModels/IncidentTemplateOwnerUser.js.map +1 -1
  853. package/build/dist/Models/DatabaseModels/IncomingCallPolicyOwnerTeam.js +3 -1
  854. package/build/dist/Models/DatabaseModels/IncomingCallPolicyOwnerTeam.js.map +1 -1
  855. package/build/dist/Models/DatabaseModels/IncomingCallPolicyOwnerUser.js +3 -1
  856. package/build/dist/Models/DatabaseModels/IncomingCallPolicyOwnerUser.js.map +1 -1
  857. package/build/dist/Models/DatabaseModels/Index.js +6 -4
  858. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  859. package/build/dist/Models/DatabaseModels/IoTFleetOwnerTeam.js +4 -1
  860. package/build/dist/Models/DatabaseModels/IoTFleetOwnerTeam.js.map +1 -1
  861. package/build/dist/Models/DatabaseModels/IoTFleetOwnerUser.js +4 -1
  862. package/build/dist/Models/DatabaseModels/IoTFleetOwnerUser.js.map +1 -1
  863. package/build/dist/Models/DatabaseModels/KubernetesClusterOwnerTeam.js +4 -1
  864. package/build/dist/Models/DatabaseModels/KubernetesClusterOwnerTeam.js.map +1 -1
  865. package/build/dist/Models/DatabaseModels/KubernetesClusterOwnerUser.js +4 -1
  866. package/build/dist/Models/DatabaseModels/KubernetesClusterOwnerUser.js.map +1 -1
  867. package/build/dist/Models/DatabaseModels/Monitor.js +2 -3
  868. package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
  869. package/build/dist/Models/DatabaseModels/MonitorGroupOwnerTeam.js +4 -1
  870. package/build/dist/Models/DatabaseModels/MonitorGroupOwnerTeam.js.map +1 -1
  871. package/build/dist/Models/DatabaseModels/MonitorGroupOwnerUser.js +4 -1
  872. package/build/dist/Models/DatabaseModels/MonitorGroupOwnerUser.js.map +1 -1
  873. package/build/dist/Models/DatabaseModels/MonitorOwnerTeam.js +3 -1
  874. package/build/dist/Models/DatabaseModels/MonitorOwnerTeam.js.map +1 -1
  875. package/build/dist/Models/DatabaseModels/MonitorOwnerUser.js +3 -1
  876. package/build/dist/Models/DatabaseModels/MonitorOwnerUser.js.map +1 -1
  877. package/build/dist/Models/DatabaseModels/MonitorTemplate.js +2 -3
  878. package/build/dist/Models/DatabaseModels/MonitorTemplate.js.map +1 -1
  879. package/build/dist/Models/DatabaseModels/NetworkDevice.js +68 -0
  880. package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
  881. package/build/dist/Models/DatabaseModels/NetworkDeviceDiagnostic.js +772 -0
  882. package/build/dist/Models/DatabaseModels/NetworkDeviceDiagnostic.js.map +1 -0
  883. package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js +140 -0
  884. package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js.map +1 -1
  885. package/build/dist/Models/DatabaseModels/NetworkDeviceOwnerTeam.js +4 -1
  886. package/build/dist/Models/DatabaseModels/NetworkDeviceOwnerTeam.js.map +1 -1
  887. package/build/dist/Models/DatabaseModels/NetworkDeviceOwnerUser.js +4 -1
  888. package/build/dist/Models/DatabaseModels/NetworkDeviceOwnerUser.js.map +1 -1
  889. package/build/dist/Models/DatabaseModels/NetworkSiteAssignmentRule.js +1 -1
  890. package/build/dist/Models/DatabaseModels/NetworkSiteAssignmentRule.js.map +1 -1
  891. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyOwnerTeam.js +3 -1
  892. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyOwnerTeam.js.map +1 -1
  893. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyOwnerUser.js +3 -1
  894. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyOwnerUser.js.map +1 -1
  895. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleOwnerTeam.js +3 -1
  896. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleOwnerTeam.js.map +1 -1
  897. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleOwnerUser.js +3 -1
  898. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleOwnerUser.js.map +1 -1
  899. package/build/dist/Models/DatabaseModels/PodmanHostOwnerTeam.js +4 -1
  900. package/build/dist/Models/DatabaseModels/PodmanHostOwnerTeam.js.map +1 -1
  901. package/build/dist/Models/DatabaseModels/PodmanHostOwnerUser.js +4 -1
  902. package/build/dist/Models/DatabaseModels/PodmanHostOwnerUser.js.map +1 -1
  903. package/build/dist/Models/DatabaseModels/ProbeOwnerTeam.js +4 -1
  904. package/build/dist/Models/DatabaseModels/ProbeOwnerTeam.js.map +1 -1
  905. package/build/dist/Models/DatabaseModels/ProbeOwnerUser.js +4 -1
  906. package/build/dist/Models/DatabaseModels/ProbeOwnerUser.js.map +1 -1
  907. package/build/dist/Models/DatabaseModels/Project.js +42 -0
  908. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  909. package/build/dist/Models/DatabaseModels/ProxmoxClusterOwnerTeam.js +4 -1
  910. package/build/dist/Models/DatabaseModels/ProxmoxClusterOwnerTeam.js.map +1 -1
  911. package/build/dist/Models/DatabaseModels/ProxmoxClusterOwnerUser.js +4 -1
  912. package/build/dist/Models/DatabaseModels/ProxmoxClusterOwnerUser.js.map +1 -1
  913. package/build/dist/Models/DatabaseModels/RumApplicationOwnerTeam.js +4 -1
  914. package/build/dist/Models/DatabaseModels/RumApplicationOwnerTeam.js.map +1 -1
  915. package/build/dist/Models/DatabaseModels/RumApplicationOwnerUser.js +4 -1
  916. package/build/dist/Models/DatabaseModels/RumApplicationOwnerUser.js.map +1 -1
  917. package/build/dist/Models/DatabaseModels/RunbookOwnerTeam.js +3 -1
  918. package/build/dist/Models/DatabaseModels/RunbookOwnerTeam.js.map +1 -1
  919. package/build/dist/Models/DatabaseModels/RunbookOwnerUser.js +3 -1
  920. package/build/dist/Models/DatabaseModels/RunbookOwnerUser.js.map +1 -1
  921. package/build/dist/Models/DatabaseModels/RunnerOwnerTeam.js +3 -1
  922. package/build/dist/Models/DatabaseModels/RunnerOwnerTeam.js.map +1 -1
  923. package/build/dist/Models/DatabaseModels/RunnerOwnerUser.js +3 -1
  924. package/build/dist/Models/DatabaseModels/RunnerOwnerUser.js.map +1 -1
  925. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceOwnerTeam.js +3 -1
  926. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceOwnerTeam.js.map +1 -1
  927. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceOwnerUser.js +3 -1
  928. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceOwnerUser.js.map +1 -1
  929. package/build/dist/Models/DatabaseModels/ScheduledMaintenancePublicNote.js +75 -0
  930. package/build/dist/Models/DatabaseModels/ScheduledMaintenancePublicNote.js.map +1 -1
  931. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplateOwnerTeam.js +6 -1
  932. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplateOwnerTeam.js.map +1 -1
  933. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplateOwnerUser.js +6 -1
  934. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplateOwnerUser.js.map +1 -1
  935. package/build/dist/Models/DatabaseModels/SecurityEventConnection.js +11 -11
  936. package/build/dist/Models/DatabaseModels/ServerlessFunctionOwnerTeam.js +4 -1
  937. package/build/dist/Models/DatabaseModels/ServerlessFunctionOwnerTeam.js.map +1 -1
  938. package/build/dist/Models/DatabaseModels/ServerlessFunctionOwnerUser.js +4 -1
  939. package/build/dist/Models/DatabaseModels/ServerlessFunctionOwnerUser.js.map +1 -1
  940. package/build/dist/Models/DatabaseModels/ServiceLevelObjective.js +192 -10
  941. package/build/dist/Models/DatabaseModels/ServiceLevelObjective.js.map +1 -1
  942. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveBurnRateRule.js +798 -96
  943. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveBurnRateRule.js.map +1 -1
  944. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveFeed.js +596 -0
  945. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveFeed.js.map +1 -0
  946. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.js +590 -0
  947. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.js.map +1 -0
  948. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveOwnerTeam.js +19 -1
  949. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveOwnerTeam.js.map +1 -1
  950. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveOwnerUser.js +19 -1
  951. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveOwnerUser.js.map +1 -1
  952. package/build/dist/Models/DatabaseModels/ServiceOwnerTeam.js +4 -1
  953. package/build/dist/Models/DatabaseModels/ServiceOwnerTeam.js.map +1 -1
  954. package/build/dist/Models/DatabaseModels/ServiceOwnerUser.js +4 -1
  955. package/build/dist/Models/DatabaseModels/ServiceOwnerUser.js.map +1 -1
  956. package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js +75 -0
  957. package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js.map +1 -1
  958. package/build/dist/Models/DatabaseModels/StatusPageOwnerTeam.js +3 -1
  959. package/build/dist/Models/DatabaseModels/StatusPageOwnerTeam.js.map +1 -1
  960. package/build/dist/Models/DatabaseModels/StatusPageOwnerUser.js +3 -1
  961. package/build/dist/Models/DatabaseModels/StatusPageOwnerUser.js.map +1 -1
  962. package/build/dist/Models/DatabaseModels/ThreatIntelFeed.js +1 -1
  963. package/build/dist/Models/DatabaseModels/VMwareVCenterOwnerTeam.js +4 -1
  964. package/build/dist/Models/DatabaseModels/VMwareVCenterOwnerTeam.js.map +1 -1
  965. package/build/dist/Models/DatabaseModels/VMwareVCenterOwnerUser.js +4 -1
  966. package/build/dist/Models/DatabaseModels/VMwareVCenterOwnerUser.js.map +1 -1
  967. package/build/dist/Models/DatabaseModels/WorkflowOwnerTeam.js +3 -1
  968. package/build/dist/Models/DatabaseModels/WorkflowOwnerTeam.js.map +1 -1
  969. package/build/dist/Models/DatabaseModels/WorkflowOwnerUser.js +3 -1
  970. package/build/dist/Models/DatabaseModels/WorkflowOwnerUser.js.map +1 -1
  971. package/build/dist/Server/API/AIAgentDataAPI.js +1 -1
  972. package/build/dist/Server/API/AIChatAPI.js +3 -1
  973. package/build/dist/Server/API/AIChatAPI.js.map +1 -1
  974. package/build/dist/Server/API/AIInvestigationAPI.js +3 -0
  975. package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -1
  976. package/build/dist/Server/API/BaseAPI.js +14 -0
  977. package/build/dist/Server/API/BaseAPI.js.map +1 -1
  978. package/build/dist/Server/API/BillingInvoiceAPI.js +233 -19
  979. package/build/dist/Server/API/BillingInvoiceAPI.js.map +1 -1
  980. package/build/dist/Server/API/BillingPaymentMethodAPI.js +86 -5
  981. package/build/dist/Server/API/BillingPaymentMethodAPI.js.map +1 -1
  982. package/build/dist/Server/API/DashboardAPI.js +72 -2
  983. package/build/dist/Server/API/DashboardAPI.js.map +1 -1
  984. package/build/dist/Server/API/SecurityEventConnectionAPI.js +102 -10
  985. package/build/dist/Server/API/SecurityEventConnectionAPI.js.map +1 -1
  986. package/build/dist/Server/API/StatusPageAPI.js +245 -155
  987. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  988. package/build/dist/Server/Infrastructure/Postgres/LocalMigrationGenerationDataSource.js +1 -1
  989. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1792800000000-AddNetworkDeviceDiagnostic.js +34 -0
  990. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1792800000000-AddNetworkDeviceDiagnostic.js.map +1 -0
  991. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1792900000000-AddNetworkDeviceDnsNameAndShortDeviceNames.js +24 -0
  992. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1792900000000-AddNetworkDeviceDnsNameAndShortDeviceNames.js.map +1 -0
  993. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1793000000000-AddNetbiosLookupToNetworkDeviceDiscoveryScan.js +21 -0
  994. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1793000000000-AddNetbiosLookupToNetworkDeviceDiscoveryScan.js.map +1 -0
  995. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1793100000000-SloProductOverhaul.js +207 -0
  996. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1793100000000-SloProductOverhaul.js.map +1 -0
  997. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1793200000000-BackfillSloMonitorRulesAndAffectedResources.js +72 -0
  998. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1793200000000-BackfillSloMonitorRulesAndAffectedResources.js.map +1 -0
  999. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1793300000000-AddDataResidencyToProject.js +20 -0
  1000. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1793300000000-AddDataResidencyToProject.js.map +1 -0
  1001. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1793400000000-AddSubscriberUpdateNotificationStatus.js +46 -0
  1002. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1793400000000-AddSubscriberUpdateNotificationStatus.js.map +1 -0
  1003. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1793500000000-MakeOwnerRowsUnique.js +602 -0
  1004. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1793500000000-MakeOwnerRowsUnique.js.map +1 -0
  1005. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1793700000000-AddAIInvestigationDecision.js +14 -0
  1006. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1793700000000-AddAIInvestigationDecision.js.map +1 -0
  1007. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1793800000000-RemoveMonitorDescriptionLengthLimit.js +20 -0
  1008. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1793800000000-RemoveMonitorDescriptionLengthLimit.js.map +1 -0
  1009. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +20 -0
  1010. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  1011. package/build/dist/Server/Middleware/IdentityRateLimit.js +22 -1
  1012. package/build/dist/Server/Middleware/IdentityRateLimit.js.map +1 -1
  1013. package/build/dist/Server/Middleware/PublicDashboardRateLimit.js +117 -36
  1014. package/build/dist/Server/Middleware/PublicDashboardRateLimit.js.map +1 -1
  1015. package/build/dist/Server/Services/AlertEpisodeLabelRuleEngineService.js +112 -56
  1016. package/build/dist/Server/Services/AlertEpisodeLabelRuleEngineService.js.map +1 -1
  1017. package/build/dist/Server/Services/AlertEpisodeOwnerRuleEngineService.js +167 -68
  1018. package/build/dist/Server/Services/AlertEpisodeOwnerRuleEngineService.js.map +1 -1
  1019. package/build/dist/Server/Services/AlertEpisodePrivacyRuleEngineService.js +87 -26
  1020. package/build/dist/Server/Services/AlertEpisodePrivacyRuleEngineService.js.map +1 -1
  1021. package/build/dist/Server/Services/AlertEpisodeService.js +13 -31
  1022. package/build/dist/Server/Services/AlertEpisodeService.js.map +1 -1
  1023. package/build/dist/Server/Services/AlertGroupingEngineService.js +7 -4
  1024. package/build/dist/Server/Services/AlertGroupingEngineService.js.map +1 -1
  1025. package/build/dist/Server/Services/AlertLabelRuleEngineService.js +270 -203
  1026. package/build/dist/Server/Services/AlertLabelRuleEngineService.js.map +1 -1
  1027. package/build/dist/Server/Services/AlertOwnerRuleEngineService.js +498 -354
  1028. package/build/dist/Server/Services/AlertOwnerRuleEngineService.js.map +1 -1
  1029. package/build/dist/Server/Services/AlertPrivacyRuleEngineService.js +97 -34
  1030. package/build/dist/Server/Services/AlertPrivacyRuleEngineService.js.map +1 -1
  1031. package/build/dist/Server/Services/AlertService.js +104 -39
  1032. package/build/dist/Server/Services/AlertService.js.map +1 -1
  1033. package/build/dist/Server/Services/AuditLogService.js +408 -44
  1034. package/build/dist/Server/Services/AuditLogService.js.map +1 -1
  1035. package/build/dist/Server/Services/BillingInvoiceService.js +94 -85
  1036. package/build/dist/Server/Services/BillingInvoiceService.js.map +1 -1
  1037. package/build/dist/Server/Services/BillingService.js +406 -14
  1038. package/build/dist/Server/Services/BillingService.js.map +1 -1
  1039. package/build/dist/Server/Services/CephClusterLabelRuleEngineService.js +141 -92
  1040. package/build/dist/Server/Services/CephClusterLabelRuleEngineService.js.map +1 -1
  1041. package/build/dist/Server/Services/CephClusterOwnerRuleEngineService.js +191 -99
  1042. package/build/dist/Server/Services/CephClusterOwnerRuleEngineService.js.map +1 -1
  1043. package/build/dist/Server/Services/CloudResourceLabelRuleEngineService.js +135 -86
  1044. package/build/dist/Server/Services/CloudResourceLabelRuleEngineService.js.map +1 -1
  1045. package/build/dist/Server/Services/CloudResourceOwnerRuleEngineService.js +192 -99
  1046. package/build/dist/Server/Services/CloudResourceOwnerRuleEngineService.js.map +1 -1
  1047. package/build/dist/Server/Services/DashboardLabelRuleEngineService.js +117 -69
  1048. package/build/dist/Server/Services/DashboardLabelRuleEngineService.js.map +1 -1
  1049. package/build/dist/Server/Services/DashboardOwnerRuleEngineService.js +173 -81
  1050. package/build/dist/Server/Services/DashboardOwnerRuleEngineService.js.map +1 -1
  1051. package/build/dist/Server/Services/DatabaseService.js +196 -19
  1052. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  1053. package/build/dist/Server/Services/DockerHostLabelRuleEngineService.js +141 -92
  1054. package/build/dist/Server/Services/DockerHostLabelRuleEngineService.js.map +1 -1
  1055. package/build/dist/Server/Services/DockerHostOwnerRuleEngineService.js +191 -99
  1056. package/build/dist/Server/Services/DockerHostOwnerRuleEngineService.js.map +1 -1
  1057. package/build/dist/Server/Services/DockerSwarmClusterLabelRuleEngineService.js +145 -91
  1058. package/build/dist/Server/Services/DockerSwarmClusterLabelRuleEngineService.js.map +1 -1
  1059. package/build/dist/Server/Services/DockerSwarmClusterOwnerRuleEngineService.js +193 -99
  1060. package/build/dist/Server/Services/DockerSwarmClusterOwnerRuleEngineService.js.map +1 -1
  1061. package/build/dist/Server/Services/HostLabelRuleEngineService.js +141 -92
  1062. package/build/dist/Server/Services/HostLabelRuleEngineService.js.map +1 -1
  1063. package/build/dist/Server/Services/HostOwnerRuleEngineService.js +193 -101
  1064. package/build/dist/Server/Services/HostOwnerRuleEngineService.js.map +1 -1
  1065. package/build/dist/Server/Services/IncidentEpisodeLabelRuleEngineService.js +112 -56
  1066. package/build/dist/Server/Services/IncidentEpisodeLabelRuleEngineService.js.map +1 -1
  1067. package/build/dist/Server/Services/IncidentEpisodeOwnerRuleEngineService.js +222 -63
  1068. package/build/dist/Server/Services/IncidentEpisodeOwnerRuleEngineService.js.map +1 -1
  1069. package/build/dist/Server/Services/IncidentEpisodePrivacyRuleEngineService.js +87 -26
  1070. package/build/dist/Server/Services/IncidentEpisodePrivacyRuleEngineService.js.map +1 -1
  1071. package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js +24 -0
  1072. package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js.map +1 -1
  1073. package/build/dist/Server/Services/IncidentEpisodeService.js +15 -68
  1074. package/build/dist/Server/Services/IncidentEpisodeService.js.map +1 -1
  1075. package/build/dist/Server/Services/IncidentGroupingEngineService.js +7 -4
  1076. package/build/dist/Server/Services/IncidentGroupingEngineService.js.map +1 -1
  1077. package/build/dist/Server/Services/IncidentLabelRuleEngineService.js +258 -192
  1078. package/build/dist/Server/Services/IncidentLabelRuleEngineService.js.map +1 -1
  1079. package/build/dist/Server/Services/IncidentOwnerRuleEngineService.js +519 -376
  1080. package/build/dist/Server/Services/IncidentOwnerRuleEngineService.js.map +1 -1
  1081. package/build/dist/Server/Services/IncidentPrivacyRuleEngineService.js +95 -32
  1082. package/build/dist/Server/Services/IncidentPrivacyRuleEngineService.js.map +1 -1
  1083. package/build/dist/Server/Services/IncidentPublicNoteService.js +24 -0
  1084. package/build/dist/Server/Services/IncidentPublicNoteService.js.map +1 -1
  1085. package/build/dist/Server/Services/IncidentService.js +103 -36
  1086. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  1087. package/build/dist/Server/Services/IncidentTemplateService.js +13 -31
  1088. package/build/dist/Server/Services/IncidentTemplateService.js.map +1 -1
  1089. package/build/dist/Server/Services/IncomingCallPolicyLabelRuleEngineService.js +113 -65
  1090. package/build/dist/Server/Services/IncomingCallPolicyLabelRuleEngineService.js.map +1 -1
  1091. package/build/dist/Server/Services/IncomingCallPolicyOwnerRuleEngineService.js +173 -81
  1092. package/build/dist/Server/Services/IncomingCallPolicyOwnerRuleEngineService.js.map +1 -1
  1093. package/build/dist/Server/Services/Index.js +6 -4
  1094. package/build/dist/Server/Services/Index.js.map +1 -1
  1095. package/build/dist/Server/Services/IoTFleetLabelRuleEngineService.js +117 -69
  1096. package/build/dist/Server/Services/IoTFleetLabelRuleEngineService.js.map +1 -1
  1097. package/build/dist/Server/Services/IoTFleetOwnerRuleEngineService.js +173 -81
  1098. package/build/dist/Server/Services/IoTFleetOwnerRuleEngineService.js.map +1 -1
  1099. package/build/dist/Server/Services/KubernetesClusterLabelRuleEngineService.js +145 -91
  1100. package/build/dist/Server/Services/KubernetesClusterLabelRuleEngineService.js.map +1 -1
  1101. package/build/dist/Server/Services/KubernetesClusterOwnerRuleEngineService.js +193 -99
  1102. package/build/dist/Server/Services/KubernetesClusterOwnerRuleEngineService.js.map +1 -1
  1103. package/build/dist/Server/Services/LabelService.js +20 -25
  1104. package/build/dist/Server/Services/LabelService.js.map +1 -1
  1105. package/build/dist/Server/Services/MonitorLabelRuleEngineService.js +123 -77
  1106. package/build/dist/Server/Services/MonitorLabelRuleEngineService.js.map +1 -1
  1107. package/build/dist/Server/Services/MonitorOwnerRuleEngineService.js +167 -82
  1108. package/build/dist/Server/Services/MonitorOwnerRuleEngineService.js.map +1 -1
  1109. package/build/dist/Server/Services/MonitorProbeService.js +18 -2
  1110. package/build/dist/Server/Services/MonitorProbeService.js.map +1 -1
  1111. package/build/dist/Server/Services/MonitorService.js +24 -40
  1112. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  1113. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js +287 -5
  1114. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js.map +1 -1
  1115. package/build/dist/Server/Services/NetworkDeviceDiagnosticService.js +333 -0
  1116. package/build/dist/Server/Services/NetworkDeviceDiagnosticService.js.map +1 -0
  1117. package/build/dist/Server/Services/NetworkDeviceLabelRuleEngineService.js +117 -69
  1118. package/build/dist/Server/Services/NetworkDeviceLabelRuleEngineService.js.map +1 -1
  1119. package/build/dist/Server/Services/NetworkDeviceOwnerRuleEngineService.js +173 -81
  1120. package/build/dist/Server/Services/NetworkDeviceOwnerRuleEngineService.js.map +1 -1
  1121. package/build/dist/Server/Services/NetworkDeviceService.js +36 -2
  1122. package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
  1123. package/build/dist/Server/Services/OnCallDutyPolicyLabelRuleEngineService.js +117 -64
  1124. package/build/dist/Server/Services/OnCallDutyPolicyLabelRuleEngineService.js.map +1 -1
  1125. package/build/dist/Server/Services/OnCallDutyPolicyOwnerRuleEngineService.js +175 -81
  1126. package/build/dist/Server/Services/OnCallDutyPolicyOwnerRuleEngineService.js.map +1 -1
  1127. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLabelRuleEngineService.js +116 -65
  1128. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLabelRuleEngineService.js.map +1 -1
  1129. package/build/dist/Server/Services/OnCallDutyPolicyScheduleOwnerRuleEngineService.js +173 -81
  1130. package/build/dist/Server/Services/OnCallDutyPolicyScheduleOwnerRuleEngineService.js.map +1 -1
  1131. package/build/dist/Server/Services/PodmanHostLabelRuleEngineService.js +141 -92
  1132. package/build/dist/Server/Services/PodmanHostLabelRuleEngineService.js.map +1 -1
  1133. package/build/dist/Server/Services/PodmanHostOwnerRuleEngineService.js +191 -99
  1134. package/build/dist/Server/Services/PodmanHostOwnerRuleEngineService.js.map +1 -1
  1135. package/build/dist/Server/Services/ProjectService.js +82 -10
  1136. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  1137. package/build/dist/Server/Services/ProxmoxClusterLabelRuleEngineService.js +141 -92
  1138. package/build/dist/Server/Services/ProxmoxClusterLabelRuleEngineService.js.map +1 -1
  1139. package/build/dist/Server/Services/ProxmoxClusterOwnerRuleEngineService.js +191 -99
  1140. package/build/dist/Server/Services/ProxmoxClusterOwnerRuleEngineService.js.map +1 -1
  1141. package/build/dist/Server/Services/RumApplicationLabelRuleEngineService.js +111 -63
  1142. package/build/dist/Server/Services/RumApplicationLabelRuleEngineService.js.map +1 -1
  1143. package/build/dist/Server/Services/RumApplicationOwnerRuleEngineService.js +164 -76
  1144. package/build/dist/Server/Services/RumApplicationOwnerRuleEngineService.js.map +1 -1
  1145. package/build/dist/Server/Services/RunbookLabelRuleEngineService.js +117 -69
  1146. package/build/dist/Server/Services/RunbookLabelRuleEngineService.js.map +1 -1
  1147. package/build/dist/Server/Services/RunbookOwnerRuleEngineService.js +173 -81
  1148. package/build/dist/Server/Services/RunbookOwnerRuleEngineService.js.map +1 -1
  1149. package/build/dist/Server/Services/ScheduledMaintenanceLabelRuleEngineService.js +280 -209
  1150. package/build/dist/Server/Services/ScheduledMaintenanceLabelRuleEngineService.js.map +1 -1
  1151. package/build/dist/Server/Services/ScheduledMaintenanceOwnerRuleEngineService.js +521 -376
  1152. package/build/dist/Server/Services/ScheduledMaintenanceOwnerRuleEngineService.js.map +1 -1
  1153. package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js +24 -0
  1154. package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js.map +1 -1
  1155. package/build/dist/Server/Services/ScheduledMaintenanceService.js +37 -39
  1156. package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
  1157. package/build/dist/Server/Services/ScheduledMaintenanceTemplateService.js +12 -29
  1158. package/build/dist/Server/Services/ScheduledMaintenanceTemplateService.js.map +1 -1
  1159. package/build/dist/Server/Services/SecurityEventConnectionService.js +43 -3
  1160. package/build/dist/Server/Services/SecurityEventConnectionService.js.map +1 -1
  1161. package/build/dist/Server/Services/ServerlessFunctionLabelRuleEngineService.js +115 -62
  1162. package/build/dist/Server/Services/ServerlessFunctionLabelRuleEngineService.js.map +1 -1
  1163. package/build/dist/Server/Services/ServerlessFunctionOwnerRuleEngineService.js +166 -76
  1164. package/build/dist/Server/Services/ServerlessFunctionOwnerRuleEngineService.js.map +1 -1
  1165. package/build/dist/Server/Services/ServiceLabelRuleEngineService.js +141 -92
  1166. package/build/dist/Server/Services/ServiceLabelRuleEngineService.js.map +1 -1
  1167. package/build/dist/Server/Services/ServiceLevelObjectiveBurnRateRuleService.js +769 -18
  1168. package/build/dist/Server/Services/ServiceLevelObjectiveBurnRateRuleService.js.map +1 -1
  1169. package/build/dist/Server/Services/ServiceLevelObjectiveFeedService.js +96 -0
  1170. package/build/dist/Server/Services/ServiceLevelObjectiveFeedService.js.map +1 -0
  1171. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js +614 -87
  1172. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js.map +1 -1
  1173. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleService.js +844 -0
  1174. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleService.js.map +1 -0
  1175. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerTeamService.js +207 -1
  1176. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerTeamService.js.map +1 -1
  1177. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerUserService.js +221 -1
  1178. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerUserService.js.map +1 -1
  1179. package/build/dist/Server/Services/ServiceLevelObjectiveService.js +997 -50
  1180. package/build/dist/Server/Services/ServiceLevelObjectiveService.js.map +1 -1
  1181. package/build/dist/Server/Services/ServiceOwnerRuleEngineService.js +193 -100
  1182. package/build/dist/Server/Services/ServiceOwnerRuleEngineService.js.map +1 -1
  1183. package/build/dist/Server/Services/StatusPageAnnouncementService.js +12 -0
  1184. package/build/dist/Server/Services/StatusPageAnnouncementService.js.map +1 -1
  1185. package/build/dist/Server/Services/StatusPageLabelRuleEngineService.js +118 -70
  1186. package/build/dist/Server/Services/StatusPageLabelRuleEngineService.js.map +1 -1
  1187. package/build/dist/Server/Services/StatusPageOwnerRuleEngineService.js +158 -76
  1188. package/build/dist/Server/Services/StatusPageOwnerRuleEngineService.js.map +1 -1
  1189. package/build/dist/Server/Services/StatusPagePrivateUserService.js +33 -25
  1190. package/build/dist/Server/Services/StatusPagePrivateUserService.js.map +1 -1
  1191. package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js +36 -1
  1192. package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js.map +1 -1
  1193. package/build/dist/Server/Services/StatusPageService.js +50 -35
  1194. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  1195. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js +19 -215
  1196. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js.map +1 -1
  1197. package/build/dist/Server/Services/StatusPageSubscriberService.js +6 -0
  1198. package/build/dist/Server/Services/StatusPageSubscriberService.js.map +1 -1
  1199. package/build/dist/Server/Services/TelemetryUsageBillingService.js +27 -5
  1200. package/build/dist/Server/Services/TelemetryUsageBillingService.js.map +1 -1
  1201. package/build/dist/Server/Services/ThreatIntelFeedService.js +1 -1
  1202. package/build/dist/Server/Services/UserSessionService.js +5 -0
  1203. package/build/dist/Server/Services/UserSessionService.js.map +1 -1
  1204. package/build/dist/Server/Services/VMwareVCenterLabelRuleEngineService.js +141 -92
  1205. package/build/dist/Server/Services/VMwareVCenterLabelRuleEngineService.js.map +1 -1
  1206. package/build/dist/Server/Services/VMwareVCenterOwnerRuleEngineService.js +191 -99
  1207. package/build/dist/Server/Services/VMwareVCenterOwnerRuleEngineService.js.map +1 -1
  1208. package/build/dist/Server/Services/WorkflowLabelRuleEngineService.js +117 -69
  1209. package/build/dist/Server/Services/WorkflowLabelRuleEngineService.js.map +1 -1
  1210. package/build/dist/Server/Services/WorkflowOwnerRuleEngineService.js +173 -81
  1211. package/build/dist/Server/Services/WorkflowOwnerRuleEngineService.js.map +1 -1
  1212. package/build/dist/Server/Types/Database/Permissions/OwnedScopePermission.js +15 -10
  1213. package/build/dist/Server/Types/Database/Permissions/OwnedScopePermission.js.map +1 -1
  1214. package/build/dist/Server/Types/Database/Permissions/OwnerTableRegistry.js +82 -0
  1215. package/build/dist/Server/Types/Database/Permissions/OwnerTableRegistry.js.map +1 -1
  1216. package/build/dist/Server/Utils/AI/AlertAIContextBuilder.js +17 -0
  1217. package/build/dist/Server/Utils/AI/AlertAIContextBuilder.js.map +1 -1
  1218. package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js +12 -3
  1219. package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js.map +1 -1
  1220. package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js +32 -4
  1221. package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js.map +1 -1
  1222. package/build/dist/Server/Utils/AI/CodeFix/CodeAgentWorkspaceGuard.js +1 -1
  1223. package/build/dist/Server/Utils/AI/CodeFix/CodeFixAgentCompletion.js +1 -1
  1224. package/build/dist/Server/Utils/AI/IncidentAIContextBuilder.js +31 -0
  1225. package/build/dist/Server/Utils/AI/IncidentAIContextBuilder.js.map +1 -1
  1226. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js +8 -5
  1227. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js.map +1 -1
  1228. package/build/dist/Server/Utils/AI/SRE/AlertInvestigationRunner.js +33 -13
  1229. package/build/dist/Server/Utils/AI/SRE/AlertInvestigationRunner.js.map +1 -1
  1230. package/build/dist/Server/Utils/AI/SRE/IncidentInvestigationRunner.js +31 -10
  1231. package/build/dist/Server/Utils/AI/SRE/IncidentInvestigationRunner.js.map +1 -1
  1232. package/build/dist/Server/Utils/AI/SRE/InvestigationEligibility.js +205 -0
  1233. package/build/dist/Server/Utils/AI/SRE/InvestigationEligibility.js.map +1 -0
  1234. package/build/dist/Server/Utils/AI/SRE/InvestigationQueue.js +13 -0
  1235. package/build/dist/Server/Utils/AI/SRE/InvestigationQueue.js.map +1 -1
  1236. package/build/dist/Server/Utils/AI/Toolbox/AlertTools.js +24 -1
  1237. package/build/dist/Server/Utils/AI/Toolbox/AlertTools.js.map +1 -1
  1238. package/build/dist/Server/Utils/AI/Toolbox/IncidentTools.js +21 -1
  1239. package/build/dist/Server/Utils/AI/Toolbox/IncidentTools.js.map +1 -1
  1240. package/build/dist/Server/Utils/AI/Toolbox/Index.js +30 -6
  1241. package/build/dist/Server/Utils/AI/Toolbox/Index.js.map +1 -1
  1242. package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js +721 -0
  1243. package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js.map +1 -0
  1244. package/build/dist/Server/Utils/AI/Toolbox/RumTools.js +330 -0
  1245. package/build/dist/Server/Utils/AI/Toolbox/RumTools.js.map +1 -0
  1246. package/build/dist/Server/Utils/AI/Toolbox/SloTools.js +337 -53
  1247. package/build/dist/Server/Utils/AI/Toolbox/SloTools.js.map +1 -1
  1248. package/build/dist/Server/Utils/AI/Toolbox/ToolTypes.js.map +1 -1
  1249. package/build/dist/Server/Utils/AI/Toolbox/TraceTools.js +1 -1
  1250. package/build/dist/Server/Utils/AI/Toolbox/TraceTools.js.map +1 -1
  1251. package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js +108 -15
  1252. package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js.map +1 -1
  1253. package/build/dist/Server/Utils/Dashboard/PublicDashboardSloHistoryPolicy.js +2 -2
  1254. package/build/dist/Server/Utils/Dashboard/PublicDashboardSloHistoryPolicy.js.map +1 -1
  1255. package/build/dist/Server/Utils/Dashboard/PublicDashboardSloWidget.js +115 -6
  1256. package/build/dist/Server/Utils/Dashboard/PublicDashboardSloWidget.js.map +1 -1
  1257. package/build/dist/Server/Utils/Database/PostgresErrorTranslator.js +11 -0
  1258. package/build/dist/Server/Utils/Database/PostgresErrorTranslator.js.map +1 -1
  1259. package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js +13 -0
  1260. package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js.map +1 -1
  1261. package/build/dist/Server/Utils/Database/RelationValueUtil.js +82 -0
  1262. package/build/dist/Server/Utils/Database/RelationValueUtil.js.map +1 -0
  1263. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +115 -106
  1264. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
  1265. package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js +17 -21
  1266. package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js.map +1 -1
  1267. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +4 -2
  1268. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
  1269. package/build/dist/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.js +5 -4
  1270. package/build/dist/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.js.map +1 -1
  1271. package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js +50 -10
  1272. package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js.map +1 -1
  1273. package/build/dist/Server/Utils/Monitor/NetworkDeviceHydrationUtil.js +13 -1
  1274. package/build/dist/Server/Utils/Monitor/NetworkDeviceHydrationUtil.js.map +1 -1
  1275. package/build/dist/Server/Utils/Monitor/NetworkDeviceMetricUtil.js +6 -10
  1276. package/build/dist/Server/Utils/Monitor/NetworkDeviceMetricUtil.js.map +1 -1
  1277. package/build/dist/Server/Utils/Rules/MonitorRulePatternValidator.js +34 -0
  1278. package/build/dist/Server/Utils/Rules/MonitorRulePatternValidator.js.map +1 -0
  1279. package/build/dist/Server/Utils/Rules/OwnerRuleAssignment.js +163 -0
  1280. package/build/dist/Server/Utils/Rules/OwnerRuleAssignment.js.map +1 -0
  1281. package/build/dist/Server/Utils/Rules/RuleRun/RuleApplication.js +25 -0
  1282. package/build/dist/Server/Utils/Rules/RuleRun/RuleApplication.js.map +1 -0
  1283. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunPermission.js +128 -0
  1284. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunPermission.js.map +1 -0
  1285. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js +746 -0
  1286. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js.map +1 -0
  1287. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunner.js +221 -0
  1288. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunner.js.map +1 -0
  1289. package/build/dist/Server/Utils/SecurityEvent/ConnectorErrorMessage.js +3 -2
  1290. package/build/dist/Server/Utils/SecurityEvent/ConnectorErrorMessage.js.map +1 -1
  1291. package/build/dist/Server/Utils/SecurityEvent/Connectors/ConnectorPlatformHealth.js +6 -4
  1292. package/build/dist/Server/Utils/SecurityEvent/Connectors/ConnectorPlatformHealth.js.map +1 -1
  1293. package/build/dist/Server/Utils/SecurityEvent/{GoogleSecOps → Connectors/GoogleSecOps}/GoogleSecOpsClient.js +174 -14
  1294. package/build/dist/Server/Utils/SecurityEvent/Connectors/GoogleSecOps/GoogleSecOpsClient.js.map +1 -0
  1295. package/build/dist/Server/Utils/SecurityEvent/Connectors/GoogleSecOps/GoogleSecOpsConnector.js +1417 -0
  1296. package/build/dist/Server/Utils/SecurityEvent/Connectors/GoogleSecOps/GoogleSecOpsConnector.js.map +1 -0
  1297. package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionPoller.js +138 -18
  1298. package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionPoller.js.map +1 -1
  1299. package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionRunExecutor.js +1 -2
  1300. package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionRunExecutor.js.map +1 -1
  1301. package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionTester.js +23 -18
  1302. package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionTester.js.map +1 -1
  1303. package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectorRegistry.js +2 -0
  1304. package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectorRegistry.js.map +1 -1
  1305. package/build/dist/Server/Utils/SecurityEvent/Connectors/Types.js +95 -0
  1306. package/build/dist/Server/Utils/SecurityEvent/Connectors/Types.js.map +1 -1
  1307. package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js +1 -1
  1308. package/build/dist/Server/Utils/SecurityEvent/SecurityEventDedupe.js +3 -3
  1309. package/build/dist/Server/Utils/SecurityEvent/ThreatIntel/ThreatIntelFeedPoller.js +2 -2
  1310. package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js +118 -40
  1311. package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js.map +1 -1
  1312. package/build/dist/Server/Utils/Slo/SloFeedUtil.js +150 -0
  1313. package/build/dist/Server/Utils/Slo/SloFeedUtil.js.map +1 -0
  1314. package/build/dist/Server/Utils/Slo/SloLegacyMonitorLabelAdoption.js +136 -0
  1315. package/build/dist/Server/Utils/Slo/SloLegacyMonitorLabelAdoption.js.map +1 -0
  1316. package/build/dist/Server/Utils/Slo/SloMetricUtil.js +269 -0
  1317. package/build/dist/Server/Utils/Slo/SloMetricUtil.js.map +1 -0
  1318. package/build/dist/Server/Utils/Slo/SloOwnerReferenceValidator.js +134 -0
  1319. package/build/dist/Server/Utils/Slo/SloOwnerReferenceValidator.js.map +1 -0
  1320. package/build/dist/Server/Utils/Slo/SloRecordReferenceValidator.js +137 -0
  1321. package/build/dist/Server/Utils/Slo/SloRecordReferenceValidator.js.map +1 -0
  1322. package/build/dist/Server/Utils/StatusPage/MonitorRulePatternValidator.js +6 -26
  1323. package/build/dist/Server/Utils/StatusPage/MonitorRulePatternValidator.js.map +1 -1
  1324. package/build/dist/Server/Utils/StatusPageResource.js +31 -5
  1325. package/build/dist/Server/Utils/StatusPageResource.js.map +1 -1
  1326. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +171 -6
  1327. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
  1328. package/build/dist/Types/AI/AIChatPageContext.js +68 -2
  1329. package/build/dist/Types/AI/AIChatPageContext.js.map +1 -1
  1330. package/build/dist/Types/AI/AIChatTypes.js +4 -0
  1331. package/build/dist/Types/AI/AIChatTypes.js.map +1 -1
  1332. package/build/dist/Types/AI/AIResourceContext.js +162 -0
  1333. package/build/dist/Types/AI/AIResourceContext.js.map +1 -0
  1334. package/build/dist/Types/AI/InvestigationNotStartedReason.js +2 -0
  1335. package/build/dist/Types/AI/InvestigationNotStartedReason.js.map +1 -0
  1336. package/build/dist/Types/Billing/SubscriptionStatus.js +37 -3
  1337. package/build/dist/Types/Billing/SubscriptionStatus.js.map +1 -1
  1338. package/build/dist/Types/Dashboard/DashboardComponentType.js +1 -0
  1339. package/build/dist/Types/Dashboard/DashboardComponentType.js.map +1 -1
  1340. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js +5 -0
  1341. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js.map +1 -1
  1342. package/build/dist/Types/Dashboard/DashboardComponents/DashboardSloListComponent.js +2 -0
  1343. package/build/dist/Types/Dashboard/DashboardComponents/DashboardSloListComponent.js.map +1 -0
  1344. package/build/dist/Types/Dashboard/DashboardTemplates.js +275 -286
  1345. package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
  1346. package/build/dist/Types/Database/EnableAuditLog.js +19 -1
  1347. package/build/dist/Types/Database/EnableAuditLog.js.map +1 -1
  1348. package/build/dist/Types/Database/UniqueColumnsTogether.js +19 -0
  1349. package/build/dist/Types/Database/UniqueColumnsTogether.js.map +1 -0
  1350. package/build/dist/Types/Email/EmailTemplateType.js +4 -0
  1351. package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
  1352. package/build/dist/Types/Metrics/MetricQueryConfigData.js.map +1 -1
  1353. package/build/dist/Types/Monitor/CriteriaFilter.js +45 -6
  1354. package/build/dist/Types/Monitor/CriteriaFilter.js.map +1 -1
  1355. package/build/dist/Types/Monitor/MonitorStep.js +2 -0
  1356. package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
  1357. package/build/dist/Types/Monitor/MonitorStepDnsMonitor.js +3 -1
  1358. package/build/dist/Types/Monitor/MonitorStepDnsMonitor.js.map +1 -1
  1359. package/build/dist/Types/Monitor/MonitorStepDnssecMonitor.js +3 -1
  1360. package/build/dist/Types/Monitor/MonitorStepDnssecMonitor.js.map +1 -1
  1361. package/build/dist/Types/Monitor/MonitorStepDomainMonitor.js +3 -1
  1362. package/build/dist/Types/Monitor/MonitorStepDomainMonitor.js.map +1 -1
  1363. package/build/dist/Types/Monitor/MonitorStepExternalStatusPageMonitor.js +3 -1
  1364. package/build/dist/Types/Monitor/MonitorStepExternalStatusPageMonitor.js.map +1 -1
  1365. package/build/dist/Types/Monitor/MonitorStepRetries.js +47 -0
  1366. package/build/dist/Types/Monitor/MonitorStepRetries.js.map +1 -0
  1367. package/build/dist/Types/Monitor/MonitorStepSnmpMonitor.js +3 -1
  1368. package/build/dist/Types/Monitor/MonitorStepSnmpMonitor.js.map +1 -1
  1369. package/build/dist/Types/NetworkDevice/NetworkDeviceDiagnosticResult.js +29 -0
  1370. package/build/dist/Types/NetworkDevice/NetworkDeviceDiagnosticResult.js.map +1 -0
  1371. package/build/dist/Types/NetworkDevice/NetworkDeviceDiagnosticStatus.js +37 -0
  1372. package/build/dist/Types/NetworkDevice/NetworkDeviceDiagnosticStatus.js.map +1 -0
  1373. package/build/dist/Types/NetworkDevice/NetworkDeviceDiagnosticType.js +42 -0
  1374. package/build/dist/Types/NetworkDevice/NetworkDeviceDiagnosticType.js.map +1 -0
  1375. package/build/dist/Types/NetworkDevice/NetworkDevicePingMetricNames.js +20 -0
  1376. package/build/dist/Types/NetworkDevice/NetworkDevicePingMetricNames.js.map +1 -0
  1377. package/build/dist/Types/Permission.js +114 -0
  1378. package/build/dist/Types/Permission.js.map +1 -1
  1379. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js +5 -0
  1380. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js.map +1 -1
  1381. package/build/dist/Types/Rules/RuleRun.js +267 -0
  1382. package/build/dist/Types/Rules/RuleRun.js.map +1 -0
  1383. package/build/dist/Types/Rum/SessionReplayApi.js.map +1 -1
  1384. package/build/dist/Types/Rum/WebVitals.js +64 -0
  1385. package/build/dist/Types/Rum/WebVitals.js.map +1 -0
  1386. package/build/dist/Types/SecurityEvent/Connectors/ConnectorDiagnostics.js.map +1 -1
  1387. package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectionDiagnostics.js +16 -1
  1388. package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectionDiagnostics.js.map +1 -1
  1389. package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.js +65 -0
  1390. package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.js.map +1 -1
  1391. package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectorProvider.js +6 -2
  1392. package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectorProvider.js.map +1 -1
  1393. package/build/dist/Types/ServiceLevelObjective/SloHistoryMetricName.js +26 -0
  1394. package/build/dist/Types/ServiceLevelObjective/SloHistoryMetricName.js.map +1 -0
  1395. package/build/dist/Types/ServiceLevelObjective/SloMetricType.js +45 -0
  1396. package/build/dist/Types/ServiceLevelObjective/SloMetricType.js.map +1 -0
  1397. package/build/dist/Types/StatusPage/StatusPageSubscriberNotificationEventType.js +4 -0
  1398. package/build/dist/Types/StatusPage/StatusPageSubscriberNotificationEventType.js.map +1 -1
  1399. package/build/dist/Types/StatusPage/SubscriberNotificationTemplateVariables.js +295 -0
  1400. package/build/dist/Types/StatusPage/SubscriberNotificationTemplateVariables.js.map +1 -0
  1401. package/build/dist/Types/StatusPage/SubscriberNotificationTrigger.js +13 -0
  1402. package/build/dist/Types/StatusPage/SubscriberNotificationTrigger.js.map +1 -0
  1403. package/build/dist/Types/StatusPage/SubscriberUpdateNotification.js +67 -0
  1404. package/build/dist/Types/StatusPage/SubscriberUpdateNotification.js.map +1 -0
  1405. package/build/dist/Types/Telemetry/LockedFilterDetail.js +7 -7
  1406. package/build/dist/Types/Telemetry/ServiceType.js +7 -0
  1407. package/build/dist/Types/Telemetry/ServiceType.js.map +1 -1
  1408. package/build/dist/Types/WebsiteRequest.js +2 -0
  1409. package/build/dist/Types/WebsiteRequest.js.map +1 -1
  1410. package/build/dist/UI/Components/AlertBanner/AlertBanner.js +48 -19
  1411. package/build/dist/UI/Components/AlertBanner/AlertBanner.js.map +1 -1
  1412. package/build/dist/UI/Components/BulkUpdate/BulkArchiveActions.js +10 -2
  1413. package/build/dist/UI/Components/BulkUpdate/BulkArchiveActions.js.map +1 -1
  1414. package/build/dist/UI/Components/Detail/Detail.js +12 -3
  1415. package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
  1416. package/build/dist/UI/Components/Dropdown/Dropdown.js +33 -5
  1417. package/build/dist/UI/Components/Dropdown/Dropdown.js.map +1 -1
  1418. package/build/dist/UI/Components/EditionLabel/EditionLabel.js +7 -5
  1419. package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
  1420. package/build/dist/UI/Components/EmptyState/EmptyState.js +3 -3
  1421. package/build/dist/UI/Components/EmptyState/EmptyState.js.map +1 -1
  1422. package/build/dist/UI/Components/EmptyState/EmptyStateGuideLinks.js +47 -0
  1423. package/build/dist/UI/Components/EmptyState/EmptyStateGuideLinks.js.map +1 -0
  1424. package/build/dist/UI/Components/Forms/ModelForm.js +6 -5
  1425. package/build/dist/UI/Components/Forms/ModelForm.js.map +1 -1
  1426. package/build/dist/UI/Components/LabelRule/LabelRuleTable.js +6 -2
  1427. package/build/dist/UI/Components/LabelRule/LabelRuleTable.js.map +1 -1
  1428. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +1 -1
  1429. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  1430. package/build/dist/UI/Components/LogsViewer/components/ActiveFilterChips.js +0 -2
  1431. package/build/dist/UI/Components/LogsViewer/components/ActiveFilterChips.js.map +1 -1
  1432. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +7 -0
  1433. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  1434. package/build/dist/UI/Components/MoreMenu/MoreMenu.js +3 -1
  1435. package/build/dist/UI/Components/MoreMenu/MoreMenu.js.map +1 -1
  1436. package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
  1437. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js +4 -2
  1438. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js.map +1 -1
  1439. package/build/dist/UI/Components/RuleRun/RuleDetailFields.js +226 -0
  1440. package/build/dist/UI/Components/RuleRun/RuleDetailFields.js.map +1 -0
  1441. package/build/dist/UI/Components/RuleRun/RuleTable.js +97 -0
  1442. package/build/dist/UI/Components/RuleRun/RuleTable.js.map +1 -0
  1443. package/build/dist/UI/Components/RuleRun/RuleView.js +83 -0
  1444. package/build/dist/UI/Components/RuleRun/RuleView.js.map +1 -0
  1445. package/build/dist/UI/Components/RuleRun/RunRuleNowModal.js +74 -0
  1446. package/build/dist/UI/Components/RuleRun/RunRuleNowModal.js.map +1 -0
  1447. package/build/dist/UI/Components/RuleRun/RunRulesBulkAction.js +78 -0
  1448. package/build/dist/UI/Components/RuleRun/RunRulesBulkAction.js.map +1 -0
  1449. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +1 -1
  1450. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
  1451. package/build/dist/UI/Components/TelemetryViewer/components/LockedFilterChip.js +24 -50
  1452. package/build/dist/UI/Components/TelemetryViewer/components/LockedFilterChip.js.map +1 -1
  1453. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryActiveFilterChips.js +0 -4
  1454. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryActiveFilterChips.js.map +1 -1
  1455. package/build/dist/UI/Utils/Rules/RuleRunClient.js +98 -0
  1456. package/build/dist/UI/Utils/Rules/RuleRunClient.js.map +1 -0
  1457. package/build/dist/UI/Utils/Telemetry/TelemetryEntityNames.js +8 -0
  1458. package/build/dist/UI/Utils/Telemetry/TelemetryEntityNames.js.map +1 -1
  1459. package/build/dist/Utils/API.js +8 -4
  1460. package/build/dist/Utils/API.js.map +1 -1
  1461. package/build/dist/Utils/Dashboard/Components/DashboardSloComponent.js +16 -2
  1462. package/build/dist/Utils/Dashboard/Components/DashboardSloComponent.js.map +1 -1
  1463. package/build/dist/Utils/Dashboard/Components/DashboardSloListComponent.js +94 -0
  1464. package/build/dist/Utils/Dashboard/Components/DashboardSloListComponent.js.map +1 -0
  1465. package/build/dist/Utils/Dashboard/Components/Index.js +4 -0
  1466. package/build/dist/Utils/Dashboard/Components/Index.js.map +1 -1
  1467. package/build/dist/Utils/Dashboard/SloWidgetSource.js +125 -0
  1468. package/build/dist/Utils/Dashboard/SloWidgetSource.js.map +1 -0
  1469. package/build/dist/Utils/HTTPResponseBodyReader.js +50 -6
  1470. package/build/dist/Utils/HTTPResponseBodyReader.js.map +1 -1
  1471. package/build/dist/Utils/Markdown/MarkdownEscape.js +49 -0
  1472. package/build/dist/Utils/Markdown/MarkdownEscape.js.map +1 -0
  1473. package/build/dist/Utils/Metrics/MetricExplorerUrl.js +14 -3
  1474. package/build/dist/Utils/Metrics/MetricExplorerUrl.js.map +1 -1
  1475. package/build/dist/Utils/Monitor/MonitorTestAvailabilityUtil.js +68 -0
  1476. package/build/dist/Utils/Monitor/MonitorTestAvailabilityUtil.js.map +1 -0
  1477. package/build/dist/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.js +112 -34
  1478. package/build/dist/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.js.map +1 -1
  1479. package/build/dist/Utils/NetworkDiscovery/DiscoveredHostUtil.js +22 -0
  1480. package/build/dist/Utils/NetworkDiscovery/DiscoveredHostUtil.js.map +1 -1
  1481. package/build/dist/Utils/NetworkDiscovery/NetbiosNameUtil.js +129 -0
  1482. package/build/dist/Utils/NetworkDiscovery/NetbiosNameUtil.js.map +1 -0
  1483. package/build/dist/Utils/NetworkDiscovery/RunImportedDeviceNaming.js +255 -0
  1484. package/build/dist/Utils/NetworkDiscovery/RunImportedDeviceNaming.js.map +1 -0
  1485. package/build/dist/Utils/NetworkDiscovery/ShortDeviceNamePlanner.js +184 -0
  1486. package/build/dist/Utils/NetworkDiscovery/ShortDeviceNamePlanner.js.map +1 -0
  1487. package/build/dist/Utils/NetworkDiscovery/ShortHostnameUtil.js +79 -0
  1488. package/build/dist/Utils/NetworkDiscovery/ShortHostnameUtil.js.map +1 -0
  1489. package/build/dist/Utils/NetworkSite/CidrMatchUtil.js +21 -12
  1490. package/build/dist/Utils/NetworkSite/CidrMatchUtil.js.map +1 -1
  1491. package/build/dist/Utils/Project/DataResidency.js +51 -0
  1492. package/build/dist/Utils/Project/DataResidency.js.map +1 -0
  1493. package/build/dist/Utils/Rules/RuleRunSummary.js +197 -0
  1494. package/build/dist/Utils/Rules/RuleRunSummary.js.map +1 -0
  1495. package/build/dist/Utils/Slo/SloAffectedResourceMarkdown.js +51 -0
  1496. package/build/dist/Utils/Slo/SloAffectedResourceMarkdown.js.map +1 -0
  1497. package/build/dist/Utils/Slo/SloBurnRateTemplate.js +228 -0
  1498. package/build/dist/Utils/Slo/SloBurnRateTemplate.js.map +1 -0
  1499. package/build/dist/Utils/Slo/SloFeedMarkdown.js +934 -0
  1500. package/build/dist/Utils/Slo/SloFeedMarkdown.js.map +1 -0
  1501. package/build/dist/Utils/Slo/SloHealth.js +75 -9
  1502. package/build/dist/Utils/Slo/SloHealth.js.map +1 -1
  1503. package/build/dist/Utils/Slo/SloListWidgetFormat.js +217 -0
  1504. package/build/dist/Utils/Slo/SloListWidgetFormat.js.map +1 -0
  1505. package/build/dist/Utils/Slo/SloMetricType.js +212 -0
  1506. package/build/dist/Utils/Slo/SloMetricType.js.map +1 -0
  1507. package/build/dist/Utils/Slo/SloMonitorRuleCriteria.js +216 -0
  1508. package/build/dist/Utils/Slo/SloMonitorRuleCriteria.js.map +1 -0
  1509. package/build/dist/Utils/Slo/SloMonitorStatusSummary.js +129 -0
  1510. package/build/dist/Utils/Slo/SloMonitorStatusSummary.js.map +1 -0
  1511. package/build/dist/Utils/Slo/SloOverviewText.js +110 -0
  1512. package/build/dist/Utils/Slo/SloOverviewText.js.map +1 -0
  1513. package/build/dist/Utils/Slo/SloProjection.js +448 -0
  1514. package/build/dist/Utils/Slo/SloProjection.js.map +1 -0
  1515. package/build/dist/Utils/Slo/SloWidgetFormat.js +4 -2
  1516. package/build/dist/Utils/Slo/SloWidgetFormat.js.map +1 -1
  1517. package/build/dist/Utils/Telemetry/HostIpAddresses.js +42 -3
  1518. package/build/dist/Utils/Telemetry/HostIpAddresses.js.map +1 -1
  1519. package/build/dist/Utils/Telemetry/LockedFilterSearch.js +1 -26
  1520. package/build/dist/Utils/Telemetry/LockedFilterSearch.js.map +1 -1
  1521. package/package.json +5 -5
  1522. package/test-setup.sh +2 -2
  1523. package/Models/DatabaseModels/GoogleSecOpsConnection.ts +0 -447
  1524. package/Models/DatabaseModels/GoogleSecOpsConnectionRun.ts +0 -263
  1525. package/Server/API/GoogleSecOpsConnectionAPI.ts +0 -320
  1526. package/Server/Services/GoogleSecOpsConnectionRunService.ts +0 -10
  1527. package/Server/Services/GoogleSecOpsConnectionService.ts +0 -116
  1528. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsConnectionTester.ts +0 -777
  1529. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.ts +0 -1373
  1530. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsRunExecutor.ts +0 -512
  1531. package/Tests/App/Dashboard/GoogleSecOpsConnectionForm.test.tsx +0 -838
  1532. package/Tests/App/Dashboard/GoogleSecOpsConnectionsErrors.test.tsx +0 -441
  1533. package/Tests/App/Dashboard/GoogleSecOpsDiagnostics.test.tsx +0 -1001
  1534. package/Tests/Server/API/GoogleSecOpsConnectionAPI.test.ts +0 -657
  1535. package/Tests/Server/Services/GoogleSecOpsDiagnosticsModel.test.ts +0 -153
  1536. package/Tests/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsConnectionTester.test.ts +0 -784
  1537. package/Tests/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsRunExecutor.test.ts +0 -823
  1538. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsDiagnostics.test.ts +0 -918
  1539. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPoller.test.ts +0 -738
  1540. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerFailureTaxonomy.test.ts +0 -874
  1541. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerHardening.test.ts +0 -1297
  1542. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsThreePassPoller.test.ts +0 -1577
  1543. package/Tests/UI/Components/LockedFilterActions.test.tsx +0 -396
  1544. package/Types/SecurityEvent/GoogleSecOpsDiagnostics.ts +0 -111
  1545. package/UI/Components/TelemetryViewer/components/LockedFilterActions.tsx +0 -205
  1546. package/build/dist/Models/DatabaseModels/GoogleSecOpsConnection.js +0 -478
  1547. package/build/dist/Models/DatabaseModels/GoogleSecOpsConnection.js.map +0 -1
  1548. package/build/dist/Models/DatabaseModels/GoogleSecOpsConnectionRun.js +0 -287
  1549. package/build/dist/Models/DatabaseModels/GoogleSecOpsConnectionRun.js.map +0 -1
  1550. package/build/dist/Server/API/GoogleSecOpsConnectionAPI.js +0 -231
  1551. package/build/dist/Server/API/GoogleSecOpsConnectionAPI.js.map +0 -1
  1552. package/build/dist/Server/Services/GoogleSecOpsConnectionRunService.js +0 -9
  1553. package/build/dist/Server/Services/GoogleSecOpsConnectionRunService.js.map +0 -1
  1554. package/build/dist/Server/Services/GoogleSecOpsConnectionService.js +0 -70
  1555. package/build/dist/Server/Services/GoogleSecOpsConnectionService.js.map +0 -1
  1556. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js.map +0 -1
  1557. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsConnectionTester.js +0 -537
  1558. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsConnectionTester.js.map +0 -1
  1559. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js +0 -1034
  1560. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js.map +0 -1
  1561. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsRunExecutor.js +0 -410
  1562. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsRunExecutor.js.map +0 -1
  1563. package/build/dist/Types/SecurityEvent/GoogleSecOpsDiagnostics.js +0 -2
  1564. package/build/dist/Types/SecurityEvent/GoogleSecOpsDiagnostics.js.map +0 -1
  1565. package/build/dist/UI/Components/TelemetryViewer/components/LockedFilterActions.js +0 -88
  1566. package/build/dist/UI/Components/TelemetryViewer/components/LockedFilterActions.js.map +0 -1
@@ -1,5 +1,6 @@
1
1
  import "@testing-library/jest-dom";
2
2
  import {
3
+ act,
3
4
  cleanup,
4
5
  fireEvent,
5
6
  render,
@@ -8,6 +9,7 @@ import {
8
9
  within,
9
10
  } from "@testing-library/react";
10
11
  import React from "react";
12
+ import { Edge as FlowEdge, Node as FlowNode, ReactFlowProps } from "reactflow";
11
13
  import {
12
14
  afterEach,
13
15
  beforeEach,
@@ -17,6 +19,7 @@ import {
17
19
  test,
18
20
  } from "@jest/globals";
19
21
  import getJestMockFunction, { MockFunction } from "../../MockType";
22
+ import type { SpyInstance } from "jest-mock";
20
23
 
21
24
  /*
22
25
  * Security Events → Correlate (issue #3395): the quick single-observable
@@ -36,6 +39,49 @@ const navigateMock: MockFunction = getJestMockFunction();
36
39
 
37
40
  const PROJECT_ID_STRING: string = "11111111-1111-4111-8111-111111111111";
38
41
 
42
+ /*
43
+ * The props of the most recent ReactFlow render, so tests can inspect the
44
+ * nodes, edges and canvas configuration the component hands to it.
45
+ */
46
+ let mockFlowProps: ReactFlowProps | null = null;
47
+
48
+ /*
49
+ * The React Flow instance the stand-in hands to onInit. Tests steer what the
50
+ * canvas reports (the viewport) and inspect what the component asks of it
51
+ * (fitting and panning).
52
+ */
53
+ interface MockViewport {
54
+ x: number;
55
+ y: number;
56
+ zoom: number;
57
+ }
58
+
59
+ const mockViewport: MockViewport = { x: 0, y: 0, zoom: 1 };
60
+ const fitViewMock: MockFunction = getJestMockFunction();
61
+ const setCenterMock: MockFunction = getJestMockFunction();
62
+ const mockFlowInstance: {
63
+ fitView: (...args: Array<any>) => unknown;
64
+ getViewport: () => MockViewport;
65
+ setCenter: (...args: Array<any>) => unknown;
66
+ } = {
67
+ fitView: (...args: Array<any>): unknown => {
68
+ return fitViewMock(...args);
69
+ },
70
+ getViewport: (): MockViewport => {
71
+ return { ...mockViewport };
72
+ },
73
+ setCenter: (...args: Array<any>): unknown => {
74
+ return setCenterMock(...args);
75
+ },
76
+ };
77
+
78
+ /*
79
+ * The i18n language the component keys its node memo on, plus translations
80
+ * a test can switch on. Unlisted strings translate to themselves.
81
+ */
82
+ const mockI18n: { language: string } = { language: "en" };
83
+ const mockTranslations: Record<string, string> = {};
84
+
39
85
  /*
40
86
  * The arrow wrappers are load bearing: jest.mock is hoisted above the
41
87
  * compiled requires, so naming the mocks directly would capture them before
@@ -85,8 +131,9 @@ jest.mock("react-i18next", () => {
85
131
  useTranslation: () => {
86
132
  return {
87
133
  t: (key: string): string => {
88
- return key;
134
+ return mockTranslations[key] ?? key;
89
135
  },
136
+ i18n: mockI18n,
90
137
  };
91
138
  },
92
139
  };
@@ -95,39 +142,60 @@ jest.mock("react-i18next", () => {
95
142
  /*
96
143
  * ReactFlow renders nothing measurable in jsdom — replace it with a stand-in
97
144
  * that lists every node as a clickable button so node-click pivots are
98
- * testable, and forwards onNodeClick exactly like the real component.
145
+ * testable, and forwards onNodeClick exactly like the real component. It also
146
+ * records its props, hands onInit the fake instance once per mount, and stubs
147
+ * the handle exports the custom node card uses.
99
148
  */
100
149
  jest.mock("reactflow", () => {
150
+ const MockReactFlow: (props: any) => React.ReactElement = (
151
+ props: any,
152
+ ): React.ReactElement => {
153
+ mockFlowProps = props;
154
+ React.useEffect(() => {
155
+ if (props.onInit) {
156
+ props.onInit(mockFlowInstance);
157
+ }
158
+ }, []);
159
+ return React.createElement(
160
+ "div",
161
+ { "data-testid": "mock-react-flow" },
162
+ (props.nodes || []).map((node: any) => {
163
+ return React.createElement(
164
+ "button",
165
+ {
166
+ key: node.id,
167
+ type: "button",
168
+ "data-testid": `flow-node-${node.id}`,
169
+ onClick: (event: any) => {
170
+ if (props.onNodeClick) {
171
+ props.onNodeClick(event, node);
172
+ }
173
+ },
174
+ },
175
+ typeof node.data?.label === "string" ? node.data.label : node.id,
176
+ );
177
+ }),
178
+ );
179
+ };
180
+
101
181
  return {
102
182
  __esModule: true,
103
- default: (props: any) => {
104
- return React.createElement(
105
- "div",
106
- { "data-testid": "mock-react-flow" },
107
- (props.nodes || []).map((node: any) => {
108
- return React.createElement(
109
- "button",
110
- {
111
- key: node.id,
112
- type: "button",
113
- "data-testid": `flow-node-${node.id}`,
114
- onClick: (event: any) => {
115
- if (props.onNodeClick) {
116
- props.onNodeClick(event, node);
117
- }
118
- },
119
- },
120
- typeof node.data?.label === "string" ? node.data.label : node.id,
121
- );
122
- }),
123
- );
124
- },
183
+ default: MockReactFlow,
125
184
  Background: () => {
126
185
  return null;
127
186
  },
128
187
  Controls: () => {
129
188
  return null;
130
189
  },
190
+ Handle: () => {
191
+ return null;
192
+ },
193
+ Position: {
194
+ Top: "top",
195
+ Bottom: "bottom",
196
+ Left: "left",
197
+ Right: "right",
198
+ },
131
199
  BackgroundVariant: { Dots: "dots" },
132
200
  MarkerType: { ArrowClosed: "arrowclosed" },
133
201
  };
@@ -176,15 +244,27 @@ jest.mock(
176
244
  import SecurityEvent from "../../../Models/AnalyticsModels/SecurityEvent";
177
245
  import ObjectID from "../../../Types/ObjectID";
178
246
  import OcsfSeverity from "../../../Types/SecurityEvent/OcsfSeverity";
247
+ import OneUptimeDate from "../../../Types/Date";
179
248
  import InBetween from "../../../Types/BaseDatabase/InBetween";
180
249
  import Includes from "../../../Types/BaseDatabase/Includes";
250
+ import IncludesAll from "../../../Types/BaseDatabase/IncludesAll";
181
251
  import IncludesNone from "../../../Types/BaseDatabase/IncludesNone";
182
252
  import Search from "../../../Types/BaseDatabase/Search";
183
253
  import {
184
254
  CorrelationFilter,
255
+ describeCorrelationCondition,
256
+ describeCorrelationFilter,
185
257
  serializeCorrelationFilter,
186
258
  } from "../../../../App/FeatureSet/Dashboard/src/Utils/SecurityEventCorrelation";
187
- import CorrelateGraph from "../../../../App/FeatureSet/Dashboard/src/Components/SecurityEvents/CorrelateGraph";
259
+ import { CorrelationGraphNode } from "../../../../App/FeatureSet/Dashboard/src/Utils/CorrelationGraph";
260
+ import { LayoutPoint } from "../../../../App/FeatureSet/Dashboard/src/Utils/LayeredGraphLayout";
261
+ import {
262
+ CORRELATE_NODE_SIZES,
263
+ CorrelateNodeData,
264
+ } from "../../../../App/FeatureSet/Dashboard/src/Components/SecurityEvents/CorrelateNodeCard";
265
+ import CorrelateGraph, {
266
+ getCanvasHeight,
267
+ } from "../../../../App/FeatureSet/Dashboard/src/Components/SecurityEvents/CorrelateGraph";
188
268
 
189
269
  interface EventInput {
190
270
  id: string;
@@ -245,9 +325,20 @@ beforeEach(() => {
245
325
  getListMock.mockResolvedValue(listResult([]));
246
326
  setQueryStringMock.mockReset();
247
327
  navigateMock.mockReset();
328
+ mockFlowProps = null;
248
329
  for (const key of Object.keys(mockQueryParams)) {
249
330
  delete mockQueryParams[key];
250
331
  }
332
+ fitViewMock.mockReset();
333
+ fitViewMock.mockReturnValue(true);
334
+ setCenterMock.mockReset();
335
+ mockViewport.x = 0;
336
+ mockViewport.y = 0;
337
+ mockViewport.zoom = 1;
338
+ mockI18n.language = "en";
339
+ for (const key of Object.keys(mockTranslations)) {
340
+ delete mockTranslations[key];
341
+ }
251
342
  });
252
343
 
253
344
  afterEach(() => {
@@ -863,3 +954,3634 @@ describe("CorrelateGraph", () => {
863
954
  );
864
955
  });
865
956
  });
957
+
958
+ /*
959
+ * The redesigned page: a query panel with a segmented search-mode control, a
960
+ * results panel with a stat strip, a canvas of fixed-size node cards, a
961
+ * selection panel that floats over the canvas, and the overview lists below
962
+ * the legend. The helpers below drive it through the recording ReactFlow
963
+ * stand-in and a deferred getList where timing matters.
964
+ */
965
+
966
+ const SEARCHED_HOST: string = "wb-ubuntu-03";
967
+ const SELECTED_ACCENT: string = "#6366f1";
968
+
969
+ type MockListResult = ReturnType<typeof listResult>;
970
+ type CorrelateFlowNode = FlowNode<CorrelateNodeData>;
971
+
972
+ interface Deferred<T> {
973
+ promise: Promise<T>;
974
+ resolve: (value: T) => void;
975
+ reject: (reason: unknown) => void;
976
+ }
977
+
978
+ function createDeferred<T>(): Deferred<T> {
979
+ const deferred: Partial<Deferred<T>> = {};
980
+ deferred.promise = new Promise<T>(
981
+ (resolve: (value: T) => void, reject: (reason: unknown) => void): void => {
982
+ deferred.resolve = resolve;
983
+ deferred.reject = reject;
984
+ },
985
+ );
986
+ return deferred as Deferred<T>;
987
+ }
988
+
989
+ /*
990
+ * Four events around one host. After searching the host the graph has:
991
+ * Authentication (2 events, worst High), Process Activity (1, Critical) and
992
+ * Network Activity (1, no severity); alice (2, Authentication only),
993
+ * 10.0.0.5 (2, Authentication and Network Activity) and bash (1, Process
994
+ * Activity).
995
+ */
996
+ function standardEvents(): Array<SecurityEvent> {
997
+ return [
998
+ buildEvent({
999
+ id: "a1111111-1111-4111-8111-111111111111",
1000
+ className: "Authentication",
1001
+ severityName: OcsfSeverity.High,
1002
+ message: "failed password for alice",
1003
+ principalUser: "alice",
1004
+ observables: [SEARCHED_HOST, "alice", "10.0.0.5"],
1005
+ }),
1006
+ buildEvent({
1007
+ id: "a2222222-2222-4222-8222-222222222222",
1008
+ className: "Authentication",
1009
+ severityName: OcsfSeverity.Low,
1010
+ message: "accepted password for alice",
1011
+ observables: [SEARCHED_HOST, "alice"],
1012
+ }),
1013
+ buildEvent({
1014
+ id: "a3333333-3333-4333-8333-333333333333",
1015
+ className: "Process Activity",
1016
+ severityName: OcsfSeverity.Critical,
1017
+ message: "suspicious process",
1018
+ observables: [SEARCHED_HOST, "bash"],
1019
+ }),
1020
+ buildEvent({
1021
+ id: "a4444444-4444-4444-8444-444444444444",
1022
+ className: "Network Activity",
1023
+ message: "outbound connection",
1024
+ observables: [SEARCHED_HOST, "10.0.0.5"],
1025
+ }),
1026
+ ];
1027
+ }
1028
+
1029
+ function cappedEvents(count: number): Array<SecurityEvent> {
1030
+ const events: Array<SecurityEvent> = [];
1031
+ for (let eventIndex: number = 0; eventIndex < count; eventIndex++) {
1032
+ events.push(
1033
+ buildEvent({
1034
+ id: `b7777777-7777-4777-8777-${String(eventIndex).padStart(12, "0")}`,
1035
+ className: "Authentication",
1036
+ observables: [SEARCHED_HOST],
1037
+ }),
1038
+ );
1039
+ }
1040
+ return events;
1041
+ }
1042
+
1043
+ // One event that mentions the host and 35 other observables (5 over the cap).
1044
+ function crowdedEvents(): Array<SecurityEvent> {
1045
+ const observables: Array<string> = [SEARCHED_HOST];
1046
+ for (
1047
+ let observableIndex: number = 0;
1048
+ observableIndex < 35;
1049
+ observableIndex++
1050
+ ) {
1051
+ observables.push(`obs-${String(observableIndex).padStart(2, "0")}`);
1052
+ }
1053
+ return [
1054
+ buildEvent({
1055
+ id: "c1111111-1111-4111-8111-111111111111",
1056
+ className: "Authentication",
1057
+ severityName: OcsfSeverity.Medium,
1058
+ observables: observables,
1059
+ }),
1060
+ ];
1061
+ }
1062
+
1063
+ function twoConditionFilter(connector: "and" | "or"): CorrelationFilter {
1064
+ return {
1065
+ conditions: [
1066
+ { field: "principalIp", operator: "equals", value: "192.168.1.20" },
1067
+ { field: "principalHost", operator: "equals", value: SEARCHED_HOST },
1068
+ ],
1069
+ connector: connector,
1070
+ } as CorrelationFilter;
1071
+ }
1072
+
1073
+ function impossibleFilter(): CorrelationFilter {
1074
+ return {
1075
+ conditions: [
1076
+ { field: "principalHost", operator: "equals", value: "host-a" },
1077
+ { field: "principalHost", operator: "equals", value: "host-b" },
1078
+ ],
1079
+ connector: "and",
1080
+ } as CorrelationFilter;
1081
+ }
1082
+
1083
+ function twoObservableOrFilter(): CorrelationFilter {
1084
+ return {
1085
+ conditions: [
1086
+ { field: "observable", operator: "equals", value: SEARCHED_HOST },
1087
+ { field: "observable", operator: "equals", value: "192.168.1.20" },
1088
+ ],
1089
+ connector: "or",
1090
+ } as CorrelationFilter;
1091
+ }
1092
+
1093
+ // Resolves once the results panel is no longer waiting on a request.
1094
+ async function waitForSettled(): Promise<void> {
1095
+ await waitFor(() => {
1096
+ expect(screen.getByTestId("correlate-results")).toHaveAttribute(
1097
+ "aria-busy",
1098
+ "false",
1099
+ );
1100
+ });
1101
+ }
1102
+
1103
+ async function renderCorrelated(
1104
+ events: Array<SecurityEvent> = standardEvents(),
1105
+ ): Promise<void> {
1106
+ getListMock.mockResolvedValue(listResult(events));
1107
+ render(<CorrelateGraph />);
1108
+ runQuickSearch(SEARCHED_HOST);
1109
+ await waitForSettled();
1110
+ }
1111
+
1112
+ async function waitForNoResults(): Promise<void> {
1113
+ await waitFor(() => {
1114
+ expect(
1115
+ document.getElementById("security-events-correlate-no-results"),
1116
+ ).not.toBeNull();
1117
+ });
1118
+ }
1119
+
1120
+ function clickFlowNode(nodeId: string): void {
1121
+ fireEvent.click(screen.getByTestId(`flow-node-${nodeId}`));
1122
+ }
1123
+
1124
+ function getFlowProps(): ReactFlowProps {
1125
+ const props: ReactFlowProps | null = mockFlowProps;
1126
+ if (!props) {
1127
+ throw new Error("ReactFlow has not rendered");
1128
+ }
1129
+ return props;
1130
+ }
1131
+
1132
+ function getFlowNodes(): Array<CorrelateFlowNode> {
1133
+ return (getFlowProps().nodes || []) as Array<CorrelateFlowNode>;
1134
+ }
1135
+
1136
+ function getFlowNode(nodeId: string): CorrelateFlowNode {
1137
+ const node: CorrelateFlowNode | undefined = getFlowNodes().find(
1138
+ (candidate: CorrelateFlowNode): boolean => {
1139
+ return candidate.id === nodeId;
1140
+ },
1141
+ );
1142
+ if (!node) {
1143
+ throw new Error(`No flow node ${nodeId}`);
1144
+ }
1145
+ return node;
1146
+ }
1147
+
1148
+ function getFlowEdges(): Array<FlowEdge> {
1149
+ return getFlowProps().edges || [];
1150
+ }
1151
+
1152
+ function getFlowEdge(edgeId: string): FlowEdge {
1153
+ const edge: FlowEdge | undefined = getFlowEdges().find(
1154
+ (candidate: FlowEdge): boolean => {
1155
+ return candidate.id === edgeId;
1156
+ },
1157
+ );
1158
+ if (!edge) {
1159
+ throw new Error(`No flow edge ${edgeId}`);
1160
+ }
1161
+ return edge;
1162
+ }
1163
+
1164
+ function getNodeIdsWhere(
1165
+ predicate: (node: CorrelateFlowNode) => boolean,
1166
+ ): Array<string> {
1167
+ return getFlowNodes()
1168
+ .filter(predicate)
1169
+ .map((node: CorrelateFlowNode): string => {
1170
+ return node.id;
1171
+ })
1172
+ .sort();
1173
+ }
1174
+
1175
+ function getSelectedNodeIds(): Array<string> {
1176
+ return getNodeIdsWhere((node: CorrelateFlowNode): boolean => {
1177
+ return node.data.isSelected;
1178
+ });
1179
+ }
1180
+
1181
+ function getDimmedNodeIds(): Array<string> {
1182
+ return getNodeIdsWhere((node: CorrelateFlowNode): boolean => {
1183
+ return node.data.isDimmed;
1184
+ });
1185
+ }
1186
+
1187
+ function windowHoursOfCall(callIndex: number): number {
1188
+ const timeWindow: InBetween<Date> = queryOfCall(callIndex)[
1189
+ "time"
1190
+ ] as InBetween<Date>;
1191
+ const windowMs: number =
1192
+ new Date(timeWindow.endValue).getTime() -
1193
+ new Date(timeWindow.startValue).getTime();
1194
+ return Math.round(windowMs / (1000 * 60 * 60));
1195
+ }
1196
+
1197
+ function selectTimeRange(label: string): void {
1198
+ const timeRangeCombobox: HTMLElement = within(
1199
+ screen.getByTestId("security-events-correlate-time-range"),
1200
+ ).getByRole("combobox");
1201
+ fireEvent.keyDown(timeRangeCombobox, { key: "ArrowDown" });
1202
+ const option: HTMLElement = screen.getByText(label);
1203
+ fireEvent.mouseDown(option);
1204
+ fireEvent.click(option);
1205
+ }
1206
+
1207
+ function isBefore(first: HTMLElement, second: HTMLElement): boolean {
1208
+ return Boolean(
1209
+ first.compareDocumentPosition(second) & Node.DOCUMENT_POSITION_FOLLOWING,
1210
+ );
1211
+ }
1212
+
1213
+ describe("CorrelateGraph start state", () => {
1214
+ test("explains the three steps and names the page exactly once", () => {
1215
+ render(<CorrelateGraph />);
1216
+
1217
+ const steps: HTMLElement = screen.getByTestId("correlate-start-steps");
1218
+ const items: Array<HTMLElement> = within(steps).getAllByRole("listitem");
1219
+ expect(items).toHaveLength(3);
1220
+ expect(items[0]).toHaveTextContent("Search");
1221
+ expect(items[1]).toHaveTextContent("Read the graph");
1222
+ expect(items[2]).toHaveTextContent("Pivot");
1223
+
1224
+ expect(screen.getAllByText("Correlate security events")).toHaveLength(1);
1225
+ expect(
1226
+ document.getElementById("security-events-correlate-empty"),
1227
+ ).not.toBeNull();
1228
+ expect(screen.queryByTestId("correlate-applied-filter")).toBeNull();
1229
+ expect(screen.queryByTestId("mock-react-flow")).toBeNull();
1230
+ });
1231
+ });
1232
+
1233
+ describe("CorrelateGraph query panel", () => {
1234
+ test("has a Correlate heading and a two-option search mode control", () => {
1235
+ render(<CorrelateGraph />);
1236
+
1237
+ expect(
1238
+ screen.getByRole("heading", { level: 2, name: "Correlate" }),
1239
+ ).toBeInTheDocument();
1240
+
1241
+ const modeGroup: HTMLElement = screen.getByRole("radiogroup", {
1242
+ name: "Search mode",
1243
+ });
1244
+ expect(within(modeGroup).getAllByRole("radio")).toHaveLength(2);
1245
+
1246
+ const simpleToggle: HTMLElement = screen.getByTestId(
1247
+ "security-events-correlate-toggle-simple",
1248
+ );
1249
+ const builderToggle: HTMLElement = screen.getByTestId(
1250
+ "security-events-correlate-toggle-builder",
1251
+ );
1252
+ expect(simpleToggle).toHaveAttribute("role", "radio");
1253
+ expect(builderToggle).toHaveAttribute("role", "radio");
1254
+ expect(simpleToggle).toHaveTextContent("Observable");
1255
+ expect(builderToggle).toHaveTextContent("Conditions");
1256
+ expect(simpleToggle).toHaveAttribute("aria-checked", "true");
1257
+ expect(builderToggle).toHaveAttribute("aria-checked", "false");
1258
+
1259
+ fireEvent.click(builderToggle);
1260
+ expect(simpleToggle).toHaveAttribute("aria-checked", "false");
1261
+ expect(builderToggle).toHaveAttribute("aria-checked", "true");
1262
+
1263
+ fireEvent.click(simpleToggle);
1264
+ expect(simpleToggle).toHaveAttribute("aria-checked", "true");
1265
+ expect(builderToggle).toHaveAttribute("aria-checked", "false");
1266
+ });
1267
+
1268
+ test("builder mode swaps the quick input for the builder and moves the one Correlate button into its footer", () => {
1269
+ render(<CorrelateGraph />);
1270
+
1271
+ const toolbar: HTMLElement = screen.getByTestId("correlate-toolbar");
1272
+ expect(
1273
+ within(toolbar).getByTestId("security-events-correlate-button"),
1274
+ ).toBeInTheDocument();
1275
+ expect(
1276
+ screen.getByTestId("security-events-correlate-observable"),
1277
+ ).toBeInTheDocument();
1278
+
1279
+ fireEvent.click(
1280
+ screen.getByTestId("security-events-correlate-toggle-builder"),
1281
+ );
1282
+
1283
+ expect(
1284
+ screen.queryByTestId("security-events-correlate-observable"),
1285
+ ).toBeNull();
1286
+ expect(toolbar).toHaveTextContent(
1287
+ "Combine fields with AND or OR below, then press Correlate.",
1288
+ );
1289
+ const builder: HTMLElement = screen.getByTestId("correlate-filter-builder");
1290
+ const footer: HTMLElement = within(builder).getByTestId(
1291
+ "correlate-builder-footer",
1292
+ );
1293
+ expect(
1294
+ within(footer).getByTestId("security-events-correlate-button"),
1295
+ ).toBeInTheDocument();
1296
+ expect(
1297
+ within(toolbar).queryByTestId("security-events-correlate-button"),
1298
+ ).toBeNull();
1299
+ expect(
1300
+ screen.getAllByTestId("security-events-correlate-button"),
1301
+ ).toHaveLength(1);
1302
+
1303
+ fireEvent.click(
1304
+ screen.getByTestId("security-events-correlate-toggle-simple"),
1305
+ );
1306
+
1307
+ expect(screen.queryByTestId("correlate-filter-builder")).toBeNull();
1308
+ expect(
1309
+ within(toolbar).getByTestId("security-events-correlate-button"),
1310
+ ).toBeInTheDocument();
1311
+ expect(
1312
+ screen.getAllByTestId("security-events-correlate-button"),
1313
+ ).toHaveLength(1);
1314
+ });
1315
+
1316
+ test("the builder footer's Correlate button applies the draft and keeps the builder open", async () => {
1317
+ render(<CorrelateGraph />);
1318
+ fireEvent.click(
1319
+ screen.getByTestId("security-events-correlate-toggle-builder"),
1320
+ );
1321
+ fireEvent.change(screen.getByTestId("correlate-condition-value-0"), {
1322
+ target: { value: SEARCHED_HOST },
1323
+ });
1324
+ fireEvent.click(
1325
+ within(screen.getByTestId("correlate-filter-builder")).getByTestId(
1326
+ "security-events-correlate-button",
1327
+ ),
1328
+ );
1329
+
1330
+ await waitFor(() => {
1331
+ expect(getListMock).toHaveBeenCalledTimes(1);
1332
+ });
1333
+ expect((queryOfCall(0)["observables"] as Includes).values).toEqual([
1334
+ SEARCHED_HOST,
1335
+ ]);
1336
+ expect(screen.getByTestId("correlate-filter-builder")).toBeInTheDocument();
1337
+ expect(screen.getByTestId("correlate-filter-chip-0")).toHaveTextContent(
1338
+ SEARCHED_HOST,
1339
+ );
1340
+ });
1341
+
1342
+ test("switching back to Observable restores the applied single-observable value", async () => {
1343
+ render(<CorrelateGraph />);
1344
+ runQuickSearch(SEARCHED_HOST);
1345
+ await waitFor(() => {
1346
+ expect(getListMock).toHaveBeenCalledTimes(1);
1347
+ });
1348
+
1349
+ // Unapplied text seeds the builder but is dropped on the way back.
1350
+ fireEvent.change(
1351
+ screen.getByTestId("security-events-correlate-observable"),
1352
+ { target: { value: "not-applied" } },
1353
+ );
1354
+ fireEvent.click(
1355
+ screen.getByTestId("security-events-correlate-toggle-builder"),
1356
+ );
1357
+ expect(screen.getByTestId("correlate-condition-value-0")).toHaveValue(
1358
+ "not-applied",
1359
+ );
1360
+
1361
+ fireEvent.click(
1362
+ screen.getByTestId("security-events-correlate-toggle-simple"),
1363
+ );
1364
+ expect(
1365
+ screen.getByTestId("security-events-correlate-observable"),
1366
+ ).toHaveValue(SEARCHED_HOST);
1367
+ expect(getListMock).toHaveBeenCalledTimes(1);
1368
+ });
1369
+
1370
+ test("switching back leaves the quick input empty when the applied filter needs the builder", async () => {
1371
+ mockQueryParams["q"] = serializeCorrelationFilter(
1372
+ twoConditionFilter("and"),
1373
+ );
1374
+
1375
+ render(<CorrelateGraph />);
1376
+ await waitFor(() => {
1377
+ expect(getListMock).toHaveBeenCalledTimes(1);
1378
+ });
1379
+
1380
+ fireEvent.click(
1381
+ screen.getByTestId("security-events-correlate-toggle-simple"),
1382
+ );
1383
+ expect(
1384
+ screen.getByTestId("security-events-correlate-observable"),
1385
+ ).toHaveValue("");
1386
+ // The applied filter itself is untouched.
1387
+ expect(screen.getByTestId("correlate-filter-chip-1")).toBeInTheDocument();
1388
+ expect(getListMock).toHaveBeenCalledTimes(1);
1389
+ });
1390
+
1391
+ test("pressing Enter in the quick input applies the trimmed value", async () => {
1392
+ render(<CorrelateGraph />);
1393
+ const quickInput: HTMLElement = screen.getByTestId(
1394
+ "security-events-correlate-observable",
1395
+ );
1396
+ fireEvent.change(quickInput, { target: { value: ` ${SEARCHED_HOST} ` } });
1397
+ fireEvent.keyDown(quickInput, { key: "Enter" });
1398
+
1399
+ await waitFor(() => {
1400
+ expect(getListMock).toHaveBeenCalledTimes(1);
1401
+ });
1402
+ expect((queryOfCall(0)["observables"] as Includes).values).toEqual([
1403
+ SEARCHED_HOST,
1404
+ ]);
1405
+ });
1406
+
1407
+ test("pressing Enter in an empty quick input does nothing", () => {
1408
+ render(<CorrelateGraph />);
1409
+ const quickInput: HTMLElement = screen.getByTestId(
1410
+ "security-events-correlate-observable",
1411
+ );
1412
+ fireEvent.change(quickInput, { target: { value: " " } });
1413
+ fireEvent.keyDown(quickInput, { key: "Enter" });
1414
+
1415
+ expect(getListMock).not.toHaveBeenCalled();
1416
+ expect(screen.queryByTestId("correlate-applied-filter")).toBeNull();
1417
+ expect(
1418
+ screen.getByTestId("security-events-correlate-button"),
1419
+ ).toBeDisabled();
1420
+ });
1421
+
1422
+ test("the applied filter row appears only once a filter is applied", async () => {
1423
+ render(<CorrelateGraph />);
1424
+ expect(screen.queryByTestId("correlate-applied-filter")).toBeNull();
1425
+
1426
+ runQuickSearch(SEARCHED_HOST);
1427
+
1428
+ const appliedRow: HTMLElement = screen.getByTestId(
1429
+ "correlate-applied-filter",
1430
+ );
1431
+ expect(appliedRow).toHaveTextContent("Applied filter");
1432
+ expect(
1433
+ within(appliedRow).getByTestId("correlate-filter-chip-0"),
1434
+ ).toHaveTextContent(SEARCHED_HOST);
1435
+ await waitForSettled();
1436
+ });
1437
+
1438
+ test("the time range dropdown lists every preset and has no clear control", async () => {
1439
+ // A severity condition renders a clearable value dropdown: the control.
1440
+ mockQueryParams["q"] = serializeCorrelationFilter({
1441
+ conditions: [
1442
+ { field: "severityName", operator: "equals", value: "High" },
1443
+ ],
1444
+ connector: "and",
1445
+ } as CorrelationFilter);
1446
+
1447
+ render(<CorrelateGraph />);
1448
+ await waitFor(() => {
1449
+ expect(getListMock).toHaveBeenCalledTimes(1);
1450
+ });
1451
+
1452
+ const builderRow: HTMLElement = screen.getByTestId(
1453
+ "correlate-condition-row-0",
1454
+ );
1455
+ expect(
1456
+ builderRow.querySelectorAll(".ou-select__clear-indicator"),
1457
+ ).toHaveLength(1);
1458
+
1459
+ const timeRange: HTMLElement = screen.getByTestId(
1460
+ "security-events-correlate-time-range",
1461
+ );
1462
+ expect(timeRange).toHaveTextContent("Last 24 hours");
1463
+ expect(
1464
+ timeRange.querySelectorAll(".ou-select__clear-indicator"),
1465
+ ).toHaveLength(0);
1466
+
1467
+ fireEvent.keyDown(within(timeRange).getByRole("combobox"), {
1468
+ key: "ArrowDown",
1469
+ });
1470
+ const optionLabels: Array<string> = screen
1471
+ .getAllByRole("option")
1472
+ .map((option: HTMLElement): string => {
1473
+ return option.textContent || "";
1474
+ });
1475
+ expect(optionLabels).toEqual([
1476
+ "Last 1 hour",
1477
+ "Last 6 hours",
1478
+ "Last 24 hours",
1479
+ "Last 7 days",
1480
+ "Last 30 days",
1481
+ ]);
1482
+ });
1483
+ });
1484
+
1485
+ describe("CorrelateGraph results header and summary", () => {
1486
+ test("states the event count and the searched window once results load", async () => {
1487
+ await renderCorrelated();
1488
+
1489
+ const resultCount: HTMLElement = screen.getByTestId(
1490
+ "correlate-result-count",
1491
+ );
1492
+ expect(resultCount).toHaveAttribute("role", "status");
1493
+ expect(resultCount).toHaveTextContent(/^4 matching events\.$/);
1494
+ // The sentence lives in the header only.
1495
+ expect(screen.getByText(/4 matching events\./)).toBe(resultCount);
1496
+
1497
+ expect(screen.getByTestId("correlate-result-window")).toHaveTextContent(
1498
+ /^Searched .+ – .+$/,
1499
+ );
1500
+ expect(screen.queryByTestId("correlate-truncation-notice")).toBeNull();
1501
+ });
1502
+
1503
+ test("uses the singular for a single event", async () => {
1504
+ await renderCorrelated([
1505
+ buildEvent({
1506
+ id: "d1111111-1111-4111-8111-111111111111",
1507
+ className: "Authentication",
1508
+ observables: [SEARCHED_HOST],
1509
+ }),
1510
+ ]);
1511
+
1512
+ expect(screen.getByTestId("correlate-result-count")).toHaveTextContent(
1513
+ /^1 matching event\.$/,
1514
+ );
1515
+ });
1516
+
1517
+ test("summarises events, classes, observables and the worst severity in stat tiles", async () => {
1518
+ await renderCorrelated();
1519
+
1520
+ const eventsTile: HTMLElement = screen.getByTestId("correlate-stat-events");
1521
+ expect(within(eventsTile).getByText("4")).toBeInTheDocument();
1522
+ expect(eventsTile).toHaveTextContent("In the selected window");
1523
+
1524
+ const classesTile: HTMLElement = screen.getByTestId(
1525
+ "correlate-stat-classes",
1526
+ );
1527
+ expect(within(classesTile).getByText("3")).toBeInTheDocument();
1528
+ // Critical (Process Activity) and High (Authentication).
1529
+ expect(classesTile).toHaveTextContent("2 critical or high");
1530
+
1531
+ const observablesTile: HTMLElement = screen.getByTestId(
1532
+ "correlate-stat-observables",
1533
+ );
1534
+ expect(within(observablesTile).getByText("3")).toBeInTheDocument();
1535
+ expect(observablesTile).toHaveTextContent("All shown");
1536
+
1537
+ const severityTile: HTMLElement = screen.getByTestId(
1538
+ "correlate-stat-severity",
1539
+ );
1540
+ expect(within(severityTile).getByText("Critical")).toBeInTheDocument();
1541
+ expect(
1542
+ within(severityTile).getByTestId("correlate-stat-worst-class"),
1543
+ ).toHaveTextContent("Process Activity");
1544
+ });
1545
+
1546
+ test("the worst-class shortcut opens the highest-severity class", async () => {
1547
+ await renderCorrelated();
1548
+
1549
+ fireEvent.click(screen.getByTestId("correlate-stat-worst-class"));
1550
+
1551
+ const drillDown: HTMLElement = screen.getByTestId("correlate-drilldown");
1552
+ expect(within(drillDown).getByText("Process Activity")).toBeInTheDocument();
1553
+ expect(
1554
+ within(drillDown).getByText("suspicious process"),
1555
+ ).toBeInTheDocument();
1556
+ expect(
1557
+ within(drillDown).queryByText("failed password for alice"),
1558
+ ).toBeNull();
1559
+ expect(getSelectedNodeIds()).toEqual(["class:Process Activity"]);
1560
+ });
1561
+
1562
+ test("without any severity the severity tile says so and offers no shortcut", async () => {
1563
+ await renderCorrelated([
1564
+ buildEvent({
1565
+ id: "d2222222-2222-4222-8222-222222222222",
1566
+ className: "Authentication",
1567
+ observables: [SEARCHED_HOST, "alice"],
1568
+ }),
1569
+ ]);
1570
+
1571
+ const severityTile: HTMLElement = screen.getByTestId(
1572
+ "correlate-stat-severity",
1573
+ );
1574
+ expect(severityTile).toHaveTextContent("No severity recorded");
1575
+ expect(screen.queryByTestId("correlate-stat-worst-class")).toBeNull();
1576
+ expect(screen.getByTestId("correlate-stat-classes")).toHaveTextContent(
1577
+ "0 critical or high",
1578
+ );
1579
+ });
1580
+
1581
+ test("a capped search marks every count as a lower bound", async () => {
1582
+ await renderCorrelated(cappedEvents(200));
1583
+
1584
+ expect(screen.getByTestId("correlate-result-count")).toHaveTextContent(
1585
+ /^200\+ matching events\.$/,
1586
+ );
1587
+
1588
+ const eventsTile: HTMLElement = screen.getByTestId("correlate-stat-events");
1589
+ expect(within(eventsTile).getByText("200+")).toBeInTheDocument();
1590
+ expect(eventsTile).toHaveTextContent("Search limit reached");
1591
+
1592
+ const notice: HTMLElement = screen.getByTestId(
1593
+ "correlate-truncation-notice",
1594
+ );
1595
+ expect(notice).toHaveAttribute("role", "status");
1596
+ const lowerBoundTexts: Array<HTMLElement> = screen.getAllByText(
1597
+ /treat counts as lower bounds/,
1598
+ );
1599
+ expect(lowerBoundTexts).toHaveLength(1);
1600
+ expect(notice).toContainElement(lowerBoundTexts[0] as HTMLElement);
1601
+
1602
+ const classNode: CorrelateFlowNode = getFlowNode("class:Authentication");
1603
+ expect(classNode.data.label).toBe("Authentication (200)");
1604
+ expect(classNode.data.count).toBe(200);
1605
+ expect(classNode.data.countIsLowerBound).toBe(true);
1606
+
1607
+ expect(screen.getByTestId("correlate-overview-class-0")).toHaveTextContent(
1608
+ "200+",
1609
+ );
1610
+ });
1611
+
1612
+ test("an OR search is capped when any one of its queries hits the limit", async () => {
1613
+ // Different fields, so the chain fans out into two queries.
1614
+ mockQueryParams["q"] = serializeCorrelationFilter({
1615
+ conditions: [
1616
+ { field: "observable", operator: "equals", value: SEARCHED_HOST },
1617
+ { field: "message", operator: "contains", value: "failed" },
1618
+ ],
1619
+ connector: "or",
1620
+ } as CorrelationFilter);
1621
+ getListMock.mockImplementation((args: any) => {
1622
+ if ((args.query as QueryRecord)["observables"]) {
1623
+ return Promise.resolve(listResult(cappedEvents(200)));
1624
+ }
1625
+ return Promise.resolve(
1626
+ listResult([
1627
+ buildEvent({
1628
+ id: "d3333333-3333-4333-8333-333333333333",
1629
+ className: "Detection Finding",
1630
+ message: "failed logon burst",
1631
+ }),
1632
+ ]),
1633
+ );
1634
+ });
1635
+
1636
+ render(<CorrelateGraph />);
1637
+ await waitFor(() => {
1638
+ expect(getListMock).toHaveBeenCalledTimes(2);
1639
+ });
1640
+ await waitFor(() => {
1641
+ expect(screen.getByTestId("correlate-result-count")).toHaveTextContent(
1642
+ /^201\+ matching events\.$/,
1643
+ );
1644
+ });
1645
+ expect(
1646
+ screen.getByTestId("correlate-truncation-notice"),
1647
+ ).toBeInTheDocument();
1648
+ });
1649
+
1650
+ test("more than 30 co-occurring observables are capped and the drop is reported", async () => {
1651
+ await renderCorrelated(crowdedEvents());
1652
+
1653
+ const observablesTile: HTMLElement = screen.getByTestId(
1654
+ "correlate-stat-observables",
1655
+ );
1656
+ expect(within(observablesTile).getByText("30")).toBeInTheDocument();
1657
+ expect(observablesTile).toHaveTextContent("5 less frequent not shown");
1658
+
1659
+ const droppedNote: HTMLElement = screen.getByTestId(
1660
+ "correlate-dropped-observables",
1661
+ );
1662
+ expect(droppedNote).toHaveAttribute("role", "status");
1663
+ expect(droppedNote).toHaveTextContent(
1664
+ "5 less frequent observables not drawn",
1665
+ );
1666
+ expect(
1667
+ within(screen.getByTestId("correlate-legend")).getByTestId(
1668
+ "correlate-dropped-observables",
1669
+ ),
1670
+ ).toBe(droppedNote);
1671
+ expect(screen.getByTestId("correlate-overview-dropped")).toHaveTextContent(
1672
+ "5 less frequent observables not shown",
1673
+ );
1674
+
1675
+ expect(
1676
+ getNodeIdsWhere((node: CorrelateFlowNode): boolean => {
1677
+ return node.data.kind === "observable";
1678
+ }),
1679
+ ).toHaveLength(30);
1680
+ expect(screen.getByTestId("flow-node-observable:obs-29")).toBeTruthy();
1681
+ expect(screen.queryByTestId("flow-node-observable:obs-30")).toBeNull();
1682
+ });
1683
+
1684
+ test("the legend explains the canvas and reports nothing dropped on a small graph", async () => {
1685
+ await renderCorrelated();
1686
+
1687
+ const legend: HTMLElement = screen.getByTestId("correlate-legend");
1688
+ expect(legend).toHaveTextContent("Your filter");
1689
+ expect(legend).toHaveTextContent("Event class");
1690
+ expect(legend).toHaveTextContent("Co-occurring observable");
1691
+ expect(
1692
+ within(legend).getByTestId("correlate-legend-severity-critical"),
1693
+ ).toBeInTheDocument();
1694
+ expect(
1695
+ within(legend).getByTestId("correlate-legend-severity-none"),
1696
+ ).toBeInTheDocument();
1697
+ expect(screen.queryByTestId("correlate-dropped-observables")).toBeNull();
1698
+ expect(screen.queryByTestId("correlate-overview-dropped")).toBeNull();
1699
+ });
1700
+
1701
+ test("Refresh refetches the same query without touching the URL", async () => {
1702
+ await renderCorrelated();
1703
+ const urlSyncCount: number = setQueryStringMock.mock.calls.length;
1704
+
1705
+ fireEvent.click(screen.getByTestId("correlate-refresh"));
1706
+
1707
+ await waitFor(() => {
1708
+ expect(getListMock).toHaveBeenCalledTimes(2);
1709
+ });
1710
+ const firstQuery: QueryRecord = queryOfCall(0);
1711
+ const secondQuery: QueryRecord = queryOfCall(1);
1712
+ expect((secondQuery["projectId"] as ObjectID).toString()).toBe(
1713
+ (firstQuery["projectId"] as ObjectID).toString(),
1714
+ );
1715
+ expect((secondQuery["observables"] as Includes).values).toEqual(
1716
+ (firstQuery["observables"] as Includes).values,
1717
+ );
1718
+ expect(Object.keys(secondQuery).sort()).toEqual(
1719
+ Object.keys(firstQuery).sort(),
1720
+ );
1721
+ expect(windowHoursOfCall(1)).toBe(24);
1722
+ expect(setQueryStringMock).toHaveBeenCalledTimes(urlSyncCount);
1723
+
1724
+ await waitForSettled();
1725
+ expect(screen.getByTestId("correlate-result-count")).toHaveTextContent(
1726
+ "4 matching events.",
1727
+ );
1728
+ });
1729
+ });
1730
+
1731
+ describe("CorrelateGraph canvas", () => {
1732
+ test("configures React Flow as a read-only view that leaves page scrolling alone", async () => {
1733
+ await renderCorrelated();
1734
+
1735
+ const props: ReactFlowProps = getFlowProps();
1736
+ expect(props.zoomOnScroll).toBe(false);
1737
+ expect(props.preventScrolling).toBe(false);
1738
+ expect(props.zoomOnDoubleClick).toBe(false);
1739
+ expect(props.nodesDraggable).toBe(false);
1740
+ expect(props.nodesConnectable).toBe(false);
1741
+ expect(props.nodesFocusable).toBe(false);
1742
+ expect(props.edgesFocusable).toBe(false);
1743
+ expect(props.elementsSelectable).toBe(false);
1744
+ expect(props.minZoom).toBe(0.1);
1745
+ expect(props.fitView).toBe(true);
1746
+ expect(props.fitViewOptions?.maxZoom).toBe(1);
1747
+ expect(props.nodeOrigin).toEqual([0.5, 0.5]);
1748
+ expect(Object.keys(props.nodeTypes || {})).toEqual(["correlate"]);
1749
+ expect(props.onPaneClick).toEqual(expect.any(Function));
1750
+
1751
+ const canvas: HTMLElement = screen.getByRole("region", {
1752
+ name: "Correlation graph",
1753
+ });
1754
+ expect(canvas).toBe(screen.getByTestId("correlate-canvas"));
1755
+ expect(canvas).toContainElement(screen.getByTestId("mock-react-flow"));
1756
+ });
1757
+
1758
+ test("hands React Flow fixed-size correlate cards that keep the plain-text labels", async () => {
1759
+ await renderCorrelated();
1760
+
1761
+ expect(
1762
+ getNodeIdsWhere((node: CorrelateFlowNode): boolean => {
1763
+ return Boolean(node.id);
1764
+ }),
1765
+ ).toEqual([
1766
+ "center",
1767
+ "class:Authentication",
1768
+ "class:Network Activity",
1769
+ "class:Process Activity",
1770
+ "observable:10.0.0.5",
1771
+ "observable:alice",
1772
+ "observable:bash",
1773
+ ]);
1774
+
1775
+ for (const node of getFlowNodes()) {
1776
+ expect(node.type).toBe("correlate");
1777
+ expect(typeof node.width).toBe("number");
1778
+ expect(typeof node.height).toBe("number");
1779
+ expect(node.width).toBe(CORRELATE_NODE_SIZES[node.data.kind].width);
1780
+ expect(node.height).toBe(CORRELATE_NODE_SIZES[node.data.kind].height);
1781
+ expect(node.draggable).toBe(false);
1782
+ expect(node.selectable).toBe(false);
1783
+ expect(node.data.isSelected).toBe(false);
1784
+ expect(node.data.isDimmed).toBe(false);
1785
+ }
1786
+
1787
+ const center: CorrelateFlowNode = getFlowNode("center");
1788
+ expect(center.data.kind).toBe("center");
1789
+ expect(center.data.label).toBe(
1790
+ describeCorrelationCondition({
1791
+ field: "observable",
1792
+ operator: "equals",
1793
+ value: SEARCHED_HOST,
1794
+ } as CorrelationFilter["conditions"][number]),
1795
+ );
1796
+ expect(center.data.title).toBe(SEARCHED_HOST);
1797
+ expect(center.data.eyebrow).toBe("Your filter");
1798
+ expect(center.data.isMonospace).toBe(true);
1799
+
1800
+ const authentication: CorrelateFlowNode = getFlowNode(
1801
+ "class:Authentication",
1802
+ );
1803
+ expect(authentication.data).toEqual(
1804
+ expect.objectContaining({
1805
+ kind: "class",
1806
+ label: "Authentication (2)",
1807
+ title: "Authentication",
1808
+ count: 2,
1809
+ countIsLowerBound: false,
1810
+ worstSeverity: OcsfSeverity.High,
1811
+ }),
1812
+ );
1813
+ expect(getFlowNode("class:Process Activity").data.worstSeverity).toBe(
1814
+ OcsfSeverity.Critical,
1815
+ );
1816
+ expect(
1817
+ getFlowNode("class:Network Activity").data.worstSeverity,
1818
+ ).toBeUndefined();
1819
+ expect(
1820
+ screen.getByTestId("flow-node-class:Network Activity"),
1821
+ ).toHaveTextContent("Network Activity (1)");
1822
+
1823
+ const alice: CorrelateFlowNode = getFlowNode("observable:alice");
1824
+ expect(alice.data).toEqual(
1825
+ expect.objectContaining({
1826
+ kind: "observable",
1827
+ label: "alice",
1828
+ title: "alice",
1829
+ count: 2,
1830
+ }),
1831
+ );
1832
+ expect(getFlowNode("observable:bash").data.count).toBe(1);
1833
+ });
1834
+
1835
+ test.each<["and" | "or", string, string]>([
1836
+ ["and", "Match all", "2 conditions (ALL)"],
1837
+ ["or", "Match any", "2 conditions (ANY)"],
1838
+ ])(
1839
+ "a multi-condition %s filter labels the centre card with its connector",
1840
+ async (connector: "and" | "or", eyebrow: string, label: string) => {
1841
+ mockQueryParams["q"] = serializeCorrelationFilter(
1842
+ twoConditionFilter(connector),
1843
+ );
1844
+ getListMock.mockResolvedValue(listResult(standardEvents()));
1845
+
1846
+ render(<CorrelateGraph />);
1847
+ await waitForSettled();
1848
+
1849
+ const center: CorrelateFlowNode = getFlowNode("center");
1850
+ expect(center.data.eyebrow).toBe(eyebrow);
1851
+ expect(center.data.title).toBe("2 conditions");
1852
+ expect(center.data.isMonospace).toBeFalsy();
1853
+ expect(center.data.label).toBe(label);
1854
+ expect(center.data.tooltip).toContain(
1855
+ connector === "or" ? " OR " : " AND ",
1856
+ );
1857
+ },
1858
+ );
1859
+
1860
+ test("draws straight edges and labels only the class-to-observable counts on a small graph", async () => {
1861
+ await renderCorrelated();
1862
+
1863
+ const edges: Array<FlowEdge> = getFlowEdges();
1864
+ expect(edges).toHaveLength(7);
1865
+ for (const edge of edges) {
1866
+ expect(edge.type).toBe("straight");
1867
+ expect(edge.animated).toBeFalsy();
1868
+ expect(edge.markerEnd).toBeUndefined();
1869
+ expect(edge.style?.opacity).toBe(0.85);
1870
+ }
1871
+
1872
+ for (const classId of [
1873
+ "class:Authentication",
1874
+ "class:Network Activity",
1875
+ "class:Process Activity",
1876
+ ]) {
1877
+ const spoke: FlowEdge = getFlowEdge(`center->${classId}`);
1878
+ expect(spoke.source).toBe("center");
1879
+ expect(spoke.target).toBe(classId);
1880
+ expect(spoke.label).toBeUndefined();
1881
+ }
1882
+
1883
+ const aliceEdge: FlowEdge = getFlowEdge(
1884
+ "class:Authentication->observable:alice",
1885
+ );
1886
+ expect(aliceEdge.label).toBe("2");
1887
+ expect(aliceEdge.labelShowBg).toBe(true);
1888
+ expect(String(aliceEdge.labelBgStyle?.fill)).toMatch(/^var\(--ou-/);
1889
+ expect(String(aliceEdge.labelStyle?.fill)).toMatch(/^var\(--ou-/);
1890
+ expect(getFlowEdge("class:Authentication->observable:10.0.0.5").label).toBe(
1891
+ "1",
1892
+ );
1893
+ expect(
1894
+ getFlowEdge("class:Network Activity->observable:10.0.0.5").label,
1895
+ ).toBe("1");
1896
+ });
1897
+
1898
+ test("a large graph leaves every edge unlabelled until something is selected", async () => {
1899
+ await renderCorrelated(crowdedEvents());
1900
+
1901
+ // 1 spoke + 30 observable edges is over the label budget.
1902
+ expect(getFlowEdges()).toHaveLength(31);
1903
+ for (const edge of getFlowEdges()) {
1904
+ expect(edge.label).toBeUndefined();
1905
+ }
1906
+
1907
+ clickFlowNode("observable:obs-00");
1908
+ expect(getFlowEdge("class:Authentication->observable:obs-00").label).toBe(
1909
+ "1",
1910
+ );
1911
+ expect(
1912
+ getFlowEdge("class:Authentication->observable:obs-01").label,
1913
+ ).toBeUndefined();
1914
+ });
1915
+ });
1916
+
1917
+ describe("CorrelateGraph selection", () => {
1918
+ test("nothing is selected and no panel shows before a click", async () => {
1919
+ await renderCorrelated();
1920
+
1921
+ expect(getSelectedNodeIds()).toEqual([]);
1922
+ expect(getDimmedNodeIds()).toEqual([]);
1923
+ expect(screen.queryByTestId("correlate-inspector")).toBeNull();
1924
+ expect(screen.queryByTestId("correlate-drilldown")).toBeNull();
1925
+ expect(screen.queryByTestId("correlate-observable-actions")).toBeNull();
1926
+ });
1927
+
1928
+ test("selection is single: a class click replaces an observable selection", async () => {
1929
+ await renderCorrelated();
1930
+
1931
+ clickFlowNode("observable:alice");
1932
+ expect(
1933
+ screen.getByTestId("correlate-observable-actions"),
1934
+ ).toBeInTheDocument();
1935
+ expect(getSelectedNodeIds()).toEqual(["observable:alice"]);
1936
+
1937
+ clickFlowNode("class:Process Activity");
1938
+ expect(screen.queryByTestId("correlate-observable-actions")).toBeNull();
1939
+ const inspector: HTMLElement = screen.getByTestId("correlate-inspector");
1940
+ expect(
1941
+ within(inspector).getByTestId("correlate-drilldown"),
1942
+ ).toBeInTheDocument();
1943
+ expect(screen.getAllByTestId("correlate-inspector")).toHaveLength(1);
1944
+ expect(getSelectedNodeIds()).toEqual(["class:Process Activity"]);
1945
+
1946
+ // And back again.
1947
+ clickFlowNode("observable:bash");
1948
+ expect(screen.queryByTestId("correlate-drilldown")).toBeNull();
1949
+ expect(getSelectedNodeIds()).toEqual(["observable:bash"]);
1950
+ });
1951
+
1952
+ test("selecting an observable dims everything outside its neighbourhood", async () => {
1953
+ await renderCorrelated();
1954
+
1955
+ clickFlowNode("observable:alice");
1956
+
1957
+ expect(getDimmedNodeIds()).toEqual([
1958
+ "class:Network Activity",
1959
+ "class:Process Activity",
1960
+ "observable:10.0.0.5",
1961
+ "observable:bash",
1962
+ ]);
1963
+ expect(getFlowNode("center").data.isDimmed).toBe(false);
1964
+ expect(getFlowNode("class:Authentication").data.isDimmed).toBe(false);
1965
+ expect(getFlowNode("observable:alice").data.isSelected).toBe(true);
1966
+
1967
+ const litEdge: FlowEdge = getFlowEdge(
1968
+ "class:Authentication->observable:alice",
1969
+ );
1970
+ expect(litEdge.style?.stroke).toBe(SELECTED_ACCENT);
1971
+ expect(litEdge.style?.opacity).toBe(0.85);
1972
+ expect(litEdge.label).toBe("2");
1973
+
1974
+ const litSpoke: FlowEdge = getFlowEdge("center->class:Authentication");
1975
+ expect(litSpoke.style?.stroke).toBe(SELECTED_ACCENT);
1976
+ expect(litSpoke.label).toBeUndefined();
1977
+
1978
+ const fadedEdge: FlowEdge = getFlowEdge(
1979
+ "class:Process Activity->observable:bash",
1980
+ );
1981
+ expect(fadedEdge.style?.stroke).not.toBe(SELECTED_ACCENT);
1982
+ expect(fadedEdge.style?.opacity).toBe(0.15);
1983
+ // Outside the selection, counts are hidden even on a small graph.
1984
+ expect(fadedEdge.label).toBeUndefined();
1985
+ expect(getFlowEdge("center->class:Process Activity").style?.opacity).toBe(
1986
+ 0.15,
1987
+ );
1988
+ });
1989
+
1990
+ test("selecting a class lights its spoke and its observables", async () => {
1991
+ await renderCorrelated();
1992
+
1993
+ clickFlowNode("class:Authentication");
1994
+
1995
+ expect(getDimmedNodeIds()).toEqual([
1996
+ "class:Network Activity",
1997
+ "class:Process Activity",
1998
+ "observable:bash",
1999
+ ]);
2000
+ for (const edgeId of [
2001
+ "center->class:Authentication",
2002
+ "class:Authentication->observable:alice",
2003
+ "class:Authentication->observable:10.0.0.5",
2004
+ ]) {
2005
+ expect(getFlowEdge(edgeId).style?.stroke).toBe(SELECTED_ACCENT);
2006
+ }
2007
+ expect(
2008
+ getFlowEdge("class:Network Activity->observable:10.0.0.5").style?.opacity,
2009
+ ).toBe(0.15);
2010
+ });
2011
+
2012
+ test("clicking the empty canvas clears the selection", async () => {
2013
+ await renderCorrelated();
2014
+ clickFlowNode("observable:alice");
2015
+ expect(screen.getByTestId("correlate-inspector")).toBeInTheDocument();
2016
+
2017
+ act(() => {
2018
+ getFlowProps().onPaneClick?.({} as React.MouseEvent);
2019
+ });
2020
+
2021
+ expect(screen.queryByTestId("correlate-inspector")).toBeNull();
2022
+ expect(getSelectedNodeIds()).toEqual([]);
2023
+ expect(getDimmedNodeIds()).toEqual([]);
2024
+ });
2025
+
2026
+ test("clicking the centre card clears the selection", async () => {
2027
+ await renderCorrelated();
2028
+ clickFlowNode("class:Authentication");
2029
+ expect(screen.getByTestId("correlate-drilldown")).toBeInTheDocument();
2030
+
2031
+ clickFlowNode("center");
2032
+
2033
+ expect(screen.queryByTestId("correlate-inspector")).toBeNull();
2034
+ expect(getSelectedNodeIds()).toEqual([]);
2035
+ expect(getDimmedNodeIds()).toEqual([]);
2036
+ });
2037
+
2038
+ test("the selection panel floats over the canvas while the overview stays below the legend", async () => {
2039
+ await renderCorrelated();
2040
+
2041
+ const canvas: HTMLElement = screen.getByTestId("correlate-canvas");
2042
+ const legend: HTMLElement = screen.getByTestId("correlate-legend");
2043
+ const overview: HTMLElement = screen.getByTestId("correlate-overview");
2044
+ expect(isBefore(canvas, legend)).toBe(true);
2045
+ expect(isBefore(legend, overview)).toBe(true);
2046
+
2047
+ clickFlowNode("observable:alice");
2048
+
2049
+ const inspector: HTMLElement = screen.getByRole("complementary", {
2050
+ name: "Details",
2051
+ });
2052
+ expect(inspector).toBe(screen.getByTestId("correlate-inspector"));
2053
+ expect(inspector.tagName).toBe("ASIDE");
2054
+ // Focusable from script only, so a list selection can move focus to it.
2055
+ expect(inspector).toHaveAttribute("tabindex", "-1");
2056
+ /*
2057
+ * It shares the canvas's positioned wrapper and overlays it only from xl
2058
+ * up; below that it stacks under the canvas and scrolls on its own.
2059
+ */
2060
+ expect(inspector.parentElement).toBe(canvas.parentElement);
2061
+ expect(inspector.parentElement).toHaveClass("relative");
2062
+ expect(inspector).toHaveClass(
2063
+ "xl:absolute",
2064
+ "xl:right-3",
2065
+ "xl:top-3",
2066
+ "xl:bottom-3",
2067
+ "xl:w-[22rem]",
2068
+ "xl:max-h-none",
2069
+ "max-h-[28rem]",
2070
+ "overflow-y-auto",
2071
+ "border-t",
2072
+ );
2073
+ expect(inspector).not.toHaveClass("absolute");
2074
+ expect(inspector).not.toHaveClass("md:absolute");
2075
+ expect(inspector).not.toHaveClass("lg:absolute");
2076
+ expect(isBefore(canvas, inspector)).toBe(true);
2077
+ expect(canvas).not.toContainElement(inspector);
2078
+ expect(
2079
+ within(inspector).getByTestId("correlate-observable-actions"),
2080
+ ).toBeInTheDocument();
2081
+
2082
+ // The overview is not replaced by the selection.
2083
+ expect(screen.getByTestId("correlate-overview")).toBe(overview);
2084
+ expect(isBefore(inspector, overview)).toBe(true);
2085
+ expect(isBefore(legend, overview)).toBe(true);
2086
+ });
2087
+
2088
+ test("changing the time range closes an open drill-down", async () => {
2089
+ await renderCorrelated();
2090
+ clickFlowNode("class:Authentication");
2091
+ expect(screen.getByTestId("correlate-drilldown")).toBeInTheDocument();
2092
+
2093
+ selectTimeRange("Last 7 days");
2094
+
2095
+ expect(screen.queryByTestId("correlate-drilldown")).toBeNull();
2096
+ await waitFor(() => {
2097
+ expect(getListMock).toHaveBeenCalledTimes(2);
2098
+ });
2099
+ await waitForSettled();
2100
+ expect(screen.queryByTestId("correlate-inspector")).toBeNull();
2101
+ expect(getSelectedNodeIds()).toEqual([]);
2102
+ });
2103
+ });
2104
+
2105
+ describe("CorrelateGraph overview lists", () => {
2106
+ test("rank classes by worst severity and observables by frequency", async () => {
2107
+ await renderCorrelated();
2108
+
2109
+ const overview: HTMLElement = screen.getByTestId("correlate-overview");
2110
+ expect(
2111
+ within(overview).getByTestId("correlate-overview-class-0"),
2112
+ ).toHaveTextContent(/Process Activity.*Critical.*1/);
2113
+ expect(
2114
+ within(overview).getByTestId("correlate-overview-class-1"),
2115
+ ).toHaveTextContent(/Authentication.*High.*2/);
2116
+ expect(
2117
+ within(overview).getByTestId("correlate-overview-class-2"),
2118
+ ).toHaveTextContent(/Network Activity.*No severity.*1/);
2119
+ expect(screen.queryByTestId("correlate-overview-class-3")).toBeNull();
2120
+
2121
+ expect(
2122
+ within(overview).getByTestId("correlate-overview-observable-0"),
2123
+ ).toHaveTextContent(/10\.0\.0\.5.*2/);
2124
+ expect(
2125
+ within(overview).getByTestId("correlate-overview-observable-1"),
2126
+ ).toHaveTextContent(/alice.*2/);
2127
+ expect(
2128
+ within(overview).getByTestId("correlate-overview-observable-2"),
2129
+ ).toHaveTextContent(/bash.*1/);
2130
+ expect(screen.queryByTestId("correlate-overview-observable-3")).toBeNull();
2131
+ });
2132
+
2133
+ test("a class row opens that class's events (the keyboard path)", async () => {
2134
+ await renderCorrelated();
2135
+
2136
+ const row: HTMLElement = screen.getByTestId("correlate-overview-class-0");
2137
+ expect(row.tagName).toBe("BUTTON");
2138
+ expect(row).not.toBeDisabled();
2139
+ fireEvent.click(row);
2140
+
2141
+ const drillDown: HTMLElement = screen.getByTestId("correlate-drilldown");
2142
+ expect(within(drillDown).getByText("Process Activity")).toBeInTheDocument();
2143
+ expect(
2144
+ within(drillDown).getByTestId("correlate-drilldown-count"),
2145
+ ).toHaveTextContent("1 matching event");
2146
+ expect(getSelectedNodeIds()).toEqual(["class:Process Activity"]);
2147
+ // The overview is still there to pick something else.
2148
+ expect(screen.getByTestId("correlate-overview")).toBeInTheDocument();
2149
+ });
2150
+
2151
+ test("an observable row opens its pivots (the keyboard path)", async () => {
2152
+ await renderCorrelated();
2153
+
2154
+ const row: HTMLElement = screen.getByTestId(
2155
+ "correlate-overview-observable-0",
2156
+ );
2157
+ expect(row.tagName).toBe("BUTTON");
2158
+ fireEvent.click(row);
2159
+
2160
+ const actions: HTMLElement = screen.getByTestId(
2161
+ "correlate-observable-actions",
2162
+ );
2163
+ expect(within(actions).getByText("10.0.0.5")).toBe(
2164
+ within(actions).getByTestId("correlate-observable-value"),
2165
+ );
2166
+ expect(getSelectedNodeIds()).toEqual(["observable:10.0.0.5"]);
2167
+
2168
+ // Picking another row swaps the panel.
2169
+ fireEvent.click(screen.getByTestId("correlate-overview-class-1"));
2170
+ expect(screen.queryByTestId("correlate-observable-actions")).toBeNull();
2171
+ expect(
2172
+ within(screen.getByTestId("correlate-drilldown")).getByText(
2173
+ "Authentication",
2174
+ ),
2175
+ ).toBeInTheDocument();
2176
+ });
2177
+ });
2178
+
2179
+ describe("CorrelateGraph pivots", () => {
2180
+ test("Add appends an equality condition, refetches with hasAll and opens the builder", async () => {
2181
+ await renderCorrelated();
2182
+ const urlSyncCount: number = setQueryStringMock.mock.calls.length;
2183
+
2184
+ clickFlowNode("observable:alice");
2185
+ expect(
2186
+ within(screen.getByTestId("correlate-observable-actions")).getByText(
2187
+ "Keep only events that also mention it.",
2188
+ ),
2189
+ ).toBeInTheDocument();
2190
+ fireEvent.click(screen.getByTestId("correlate-action-add"));
2191
+
2192
+ await waitFor(() => {
2193
+ expect(getListMock).toHaveBeenCalledTimes(2);
2194
+ });
2195
+ const observables: IncludesAll = queryOfCall(1)[
2196
+ "observables"
2197
+ ] as IncludesAll;
2198
+ expect(observables).toBeInstanceOf(IncludesAll);
2199
+ expect(observables.values).toEqual([SEARCHED_HOST, "alice"]);
2200
+
2201
+ // A two-condition filter only fits the builder.
2202
+ expect(screen.getByTestId("correlate-filter-builder")).toBeInTheDocument();
2203
+ expect(
2204
+ screen.getByTestId("security-events-correlate-toggle-builder"),
2205
+ ).toHaveAttribute("aria-checked", "true");
2206
+ expect(screen.getByTestId("correlate-condition-value-1")).toHaveValue(
2207
+ "alice",
2208
+ );
2209
+ expect(screen.getByTestId("correlate-filter-chip-1")).toHaveTextContent(
2210
+ "alice",
2211
+ );
2212
+ expect(
2213
+ within(screen.getByTestId("correlate-filter-builder")).getByTestId(
2214
+ "security-events-correlate-button",
2215
+ ),
2216
+ ).toBeInTheDocument();
2217
+
2218
+ // The pivot clears the selection and lands in the URL.
2219
+ expect(screen.queryByTestId("correlate-inspector")).toBeNull();
2220
+ expect(setQueryStringMock).toHaveBeenCalledTimes(urlSyncCount + 1);
2221
+ expect(setQueryStringMock).toHaveBeenLastCalledWith(
2222
+ expect.objectContaining({
2223
+ q: expect.stringContaining("alice"),
2224
+ hours: "24",
2225
+ observable: null,
2226
+ }),
2227
+ );
2228
+ await waitForSettled();
2229
+ });
2230
+
2231
+ test("Exclude opens the builder with the Correlate button in its footer", async () => {
2232
+ await renderCorrelated();
2233
+
2234
+ clickFlowNode("observable:alice");
2235
+ fireEvent.click(screen.getByTestId("correlate-action-exclude"));
2236
+
2237
+ await waitFor(() => {
2238
+ expect(getListMock).toHaveBeenCalledTimes(2);
2239
+ });
2240
+ const operators: Array<unknown> = queryOfCall(1)[
2241
+ "observables"
2242
+ ] as Array<unknown>;
2243
+ expect(operators[0]).toBeInstanceOf(Includes);
2244
+ expect(operators[1]).toBeInstanceOf(IncludesNone);
2245
+ expect((operators[1] as IncludesNone).values).toEqual(["alice"]);
2246
+
2247
+ const builder: HTMLElement = screen.getByTestId("correlate-filter-builder");
2248
+ expect(
2249
+ within(builder).getByTestId("security-events-correlate-button"),
2250
+ ).toBeInTheDocument();
2251
+ expect(
2252
+ screen.queryByTestId("security-events-correlate-observable"),
2253
+ ).toBeNull();
2254
+ await waitForSettled();
2255
+ expect(
2256
+ within(builder).getByTestId("security-events-correlate-button"),
2257
+ ).not.toBeDisabled();
2258
+ });
2259
+
2260
+ test("Focus after an auto-opened builder closes it and fills the quick input", async () => {
2261
+ await renderCorrelated();
2262
+
2263
+ clickFlowNode("observable:alice");
2264
+ fireEvent.click(screen.getByTestId("correlate-action-exclude"));
2265
+ await waitFor(() => {
2266
+ expect(getListMock).toHaveBeenCalledTimes(2);
2267
+ });
2268
+ await waitForSettled();
2269
+ expect(screen.getByTestId("correlate-filter-builder")).toBeInTheDocument();
2270
+
2271
+ // "is not alice" does not remove alice from the co-occurring layer.
2272
+ clickFlowNode("observable:alice");
2273
+ fireEvent.click(screen.getByTestId("correlate-action-focus"));
2274
+
2275
+ await waitFor(() => {
2276
+ expect(getListMock).toHaveBeenCalledTimes(3);
2277
+ });
2278
+ expect((queryOfCall(2)["observables"] as Includes).values).toEqual([
2279
+ "alice",
2280
+ ]);
2281
+ expect(screen.queryByTestId("correlate-filter-builder")).toBeNull();
2282
+ expect(
2283
+ screen.getByTestId("security-events-correlate-toggle-simple"),
2284
+ ).toHaveAttribute("aria-checked", "true");
2285
+ expect(
2286
+ screen.getByTestId("security-events-correlate-observable"),
2287
+ ).toHaveValue("alice");
2288
+ expect(screen.queryByTestId("correlate-filter-chip-1")).toBeNull();
2289
+ await waitForSettled();
2290
+ });
2291
+
2292
+ test("OR mode: Add joins the value into the OR chain and keeps the builder open", async () => {
2293
+ mockQueryParams["q"] = serializeCorrelationFilter(twoObservableOrFilter());
2294
+ getListMock.mockResolvedValue(listResult(standardEvents()));
2295
+
2296
+ render(<CorrelateGraph />);
2297
+ // Same-field equalities under OR share one hasAny query.
2298
+ await waitFor(() => {
2299
+ expect(getListMock).toHaveBeenCalledTimes(1);
2300
+ });
2301
+ await waitForSettled();
2302
+
2303
+ clickFlowNode("observable:alice");
2304
+ expect(
2305
+ within(screen.getByTestId("correlate-observable-actions")).getByText(
2306
+ "Also include events that mention it.",
2307
+ ),
2308
+ ).toBeInTheDocument();
2309
+ fireEvent.click(screen.getByTestId("correlate-action-add"));
2310
+
2311
+ await waitFor(() => {
2312
+ expect(getListMock).toHaveBeenCalledTimes(2);
2313
+ });
2314
+ const observables: Includes = queryOfCall(1)["observables"] as Includes;
2315
+ expect(observables).toBeInstanceOf(Includes);
2316
+ expect(observables.values).toEqual([
2317
+ SEARCHED_HOST,
2318
+ "192.168.1.20",
2319
+ "alice",
2320
+ ]);
2321
+ expect(screen.getByTestId("correlate-filter-chip-2")).toHaveTextContent(
2322
+ "alice",
2323
+ );
2324
+ expect(screen.getByTestId("correlate-filter-chips")).toHaveTextContent(
2325
+ "OR",
2326
+ );
2327
+ expect(screen.getByTestId("correlate-filter-builder")).toBeInTheDocument();
2328
+ await waitForSettled();
2329
+ });
2330
+
2331
+ test("a Seen-in chip switches the panel to that class", async () => {
2332
+ await renderCorrelated();
2333
+
2334
+ clickFlowNode("observable:10.0.0.5");
2335
+ const actions: HTMLElement = screen.getByTestId(
2336
+ "correlate-observable-actions",
2337
+ );
2338
+ expect(
2339
+ within(actions).getByTestId("correlate-observable-fact-events"),
2340
+ ).toHaveTextContent("2");
2341
+ expect(
2342
+ within(actions).getByTestId("correlate-observable-fact-classes"),
2343
+ ).toHaveTextContent("2");
2344
+ expect(
2345
+ within(actions).getByTestId("correlate-observable-class-0"),
2346
+ ).toHaveTextContent("Authentication");
2347
+ expect(
2348
+ within(actions).getByTestId("correlate-observable-class-1"),
2349
+ ).toHaveTextContent("Network Activity");
2350
+
2351
+ fireEvent.click(
2352
+ within(actions).getByTestId("correlate-observable-class-0"),
2353
+ );
2354
+
2355
+ expect(screen.queryByTestId("correlate-observable-actions")).toBeNull();
2356
+ const drillDown: HTMLElement = screen.getByTestId("correlate-drilldown");
2357
+ expect(within(drillDown).getByText("Authentication")).toBeInTheDocument();
2358
+ expect(
2359
+ within(drillDown).getByTestId("correlate-drilldown-count"),
2360
+ ).toHaveTextContent("2 matching events");
2361
+ expect(
2362
+ within(drillDown).getByText("failed password for alice"),
2363
+ ).toBeInTheDocument();
2364
+ expect(
2365
+ within(drillDown).getByText("accepted password for alice"),
2366
+ ).toBeInTheDocument();
2367
+ expect(getSelectedNodeIds()).toEqual(["class:Authentication"]);
2368
+ expect(getListMock).toHaveBeenCalledTimes(1);
2369
+ });
2370
+
2371
+ test("the observable panel's dismiss button clears the selection", async () => {
2372
+ await renderCorrelated();
2373
+ clickFlowNode("observable:alice");
2374
+
2375
+ fireEvent.click(screen.getByTestId("correlate-action-dismiss"));
2376
+
2377
+ expect(screen.queryByTestId("correlate-inspector")).toBeNull();
2378
+ expect(getSelectedNodeIds()).toEqual([]);
2379
+ expect(getListMock).toHaveBeenCalledTimes(1);
2380
+ });
2381
+
2382
+ test("the drill-down close button clears the selection", async () => {
2383
+ await renderCorrelated();
2384
+ clickFlowNode("class:Authentication");
2385
+
2386
+ fireEvent.click(screen.getByTestId("correlate-drilldown-close"));
2387
+
2388
+ expect(screen.queryByTestId("correlate-inspector")).toBeNull();
2389
+ expect(getSelectedNodeIds()).toEqual([]);
2390
+ expect(getDimmedNodeIds()).toEqual([]);
2391
+ });
2392
+
2393
+ test("Filter to this class opens the builder for the new chain", async () => {
2394
+ await renderCorrelated();
2395
+ clickFlowNode("class:Authentication");
2396
+
2397
+ fireEvent.click(screen.getByTestId("correlate-drilldown-filter-class"));
2398
+
2399
+ await waitFor(() => {
2400
+ expect(getListMock).toHaveBeenCalledTimes(2);
2401
+ });
2402
+ expect(queryOfCall(1)["className"]).toBe("Authentication");
2403
+ expect(screen.getByTestId("correlate-filter-builder")).toBeInTheDocument();
2404
+ expect(screen.getByTestId("correlate-condition-value-1")).toHaveValue(
2405
+ "Authentication",
2406
+ );
2407
+ await waitForSettled();
2408
+ expect(
2409
+ screen.getByTestId("security-events-correlate-button"),
2410
+ ).not.toBeDisabled();
2411
+ });
2412
+
2413
+ test("the event detail's pivot closes the detail and the builder and re-correlates", async () => {
2414
+ mockQueryParams["q"] = serializeCorrelationFilter(
2415
+ twoConditionFilter("and"),
2416
+ );
2417
+ getListMock.mockResolvedValue(listResult(standardEvents()));
2418
+
2419
+ render(<CorrelateGraph />);
2420
+ await waitForSettled();
2421
+ expect(screen.getByTestId("correlate-filter-builder")).toBeInTheDocument();
2422
+
2423
+ clickFlowNode("class:Authentication");
2424
+ fireEvent.click(screen.getByTestId("correlate-drilldown-event-0"));
2425
+ expect(screen.getByTestId("mock-event-detail")).toBeInTheDocument();
2426
+
2427
+ fireEvent.click(screen.getByTestId("mock-event-detail-pivot"));
2428
+
2429
+ expect(screen.queryByTestId("mock-event-detail")).toBeNull();
2430
+ await waitFor(() => {
2431
+ expect(getListMock).toHaveBeenCalledTimes(2);
2432
+ });
2433
+ const query: QueryRecord = queryOfCall(1);
2434
+ expect((query["observables"] as Includes).values).toEqual([
2435
+ detailPivotObservable,
2436
+ ]);
2437
+ expect(query["principalIp"]).toBeUndefined();
2438
+ expect(screen.queryByTestId("correlate-filter-builder")).toBeNull();
2439
+ expect(
2440
+ screen.getByTestId("security-events-correlate-observable"),
2441
+ ).toHaveValue(detailPivotObservable);
2442
+ expect(screen.getByTestId("correlate-filter-chip-0")).toHaveTextContent(
2443
+ detailPivotObservable,
2444
+ );
2445
+ expect(screen.queryByTestId("correlate-filter-chip-1")).toBeNull();
2446
+ expect(screen.queryByTestId("correlate-inspector")).toBeNull();
2447
+ await waitForSettled();
2448
+ });
2449
+ });
2450
+
2451
+ describe("CorrelateGraph unclassified events and OR mode", () => {
2452
+ test("events without a class get an Unclassified node that cannot be filtered on", async () => {
2453
+ await renderCorrelated([
2454
+ buildEvent({
2455
+ id: "e1111111-1111-4111-8111-111111111111",
2456
+ message: "raw syslog line",
2457
+ observables: [SEARCHED_HOST],
2458
+ }),
2459
+ buildEvent({
2460
+ id: "e2222222-2222-4222-8222-222222222222",
2461
+ className: "Authentication",
2462
+ message: "failed password for bob",
2463
+ observables: [SEARCHED_HOST],
2464
+ }),
2465
+ ]);
2466
+
2467
+ expect(
2468
+ screen.getByTestId("flow-node-class:Unclassified"),
2469
+ ).toHaveTextContent("Unclassified (1)");
2470
+
2471
+ clickFlowNode("class:Unclassified");
2472
+ const drillDown: HTMLElement = screen.getByTestId("correlate-drilldown");
2473
+ expect(within(drillDown).getByText("raw syslog line")).toBeInTheDocument();
2474
+ expect(within(drillDown).queryByText("failed password for bob")).toBeNull();
2475
+ expect(screen.queryByTestId("correlate-drilldown-filter-class")).toBeNull();
2476
+ // This is an AND filter, so the OR explanation does not apply either.
2477
+ expect(screen.queryByTestId("correlate-drilldown-or-note")).toBeNull();
2478
+
2479
+ // A real class still offers the filter.
2480
+ clickFlowNode("class:Authentication");
2481
+ expect(
2482
+ screen.getByTestId("correlate-drilldown-filter-class"),
2483
+ ).toBeInTheDocument();
2484
+ });
2485
+
2486
+ test("OR mode explains why the class filter is missing", async () => {
2487
+ mockQueryParams["q"] = serializeCorrelationFilter(twoObservableOrFilter());
2488
+ getListMock.mockResolvedValue(listResult(standardEvents()));
2489
+
2490
+ render(<CorrelateGraph />);
2491
+ await waitForSettled();
2492
+
2493
+ clickFlowNode("class:Authentication");
2494
+
2495
+ expect(screen.queryByTestId("correlate-drilldown-filter-class")).toBeNull();
2496
+ expect(screen.getByTestId("correlate-drilldown-or-note")).toHaveTextContent(
2497
+ "Class filters apply only when matching all conditions.",
2498
+ );
2499
+ });
2500
+ });
2501
+
2502
+ describe("CorrelateGraph errors", () => {
2503
+ test("a failed request shows a retryable error instead of the graph", async () => {
2504
+ getListMock.mockRejectedValue(new Error("ClickHouse is down"));
2505
+
2506
+ render(<CorrelateGraph />);
2507
+ runQuickSearch(SEARCHED_HOST);
2508
+
2509
+ const requestError: HTMLElement = await screen.findByTestId(
2510
+ "correlate-request-error",
2511
+ );
2512
+ expect(requestError).toHaveTextContent("Couldn't load security events");
2513
+ expect(
2514
+ within(requestError).getByText(/ClickHouse is down/),
2515
+ ).toBeInTheDocument();
2516
+ expect(
2517
+ within(requestError).getByTestId("correlate-retry"),
2518
+ ).toHaveTextContent("Try again");
2519
+ // The banner announces itself; the result count stays mounted but silent.
2520
+ expect(requestError).toHaveAttribute("role", "alert");
2521
+ expect(screen.queryByTestId("mock-react-flow")).toBeNull();
2522
+ expect(screen.getByTestId("correlate-result-count")).toBeEmptyDOMElement();
2523
+ expect(screen.getByTestId("correlate-results-header")).toHaveClass(
2524
+ "sr-only",
2525
+ );
2526
+ expect(screen.queryByTestId("correlate-refresh")).toBeNull();
2527
+ expect(screen.queryByTestId("correlate-fit-view")).toBeNull();
2528
+ expect(screen.queryByTestId("correlate-result-window")).toBeNull();
2529
+ expect(screen.queryByTestId("correlate-filter-error")).toBeNull();
2530
+ // The filter itself is fine and stays applied.
2531
+ expect(screen.getByTestId("correlate-filter-chip-0")).toBeInTheDocument();
2532
+ });
2533
+
2534
+ test("Try again refetches an equivalent query and renders the graph", async () => {
2535
+ getListMock.mockRejectedValueOnce(new Error("ClickHouse is down"));
2536
+ getListMock.mockResolvedValue(listResult(standardEvents()));
2537
+
2538
+ render(<CorrelateGraph />);
2539
+ runQuickSearch(SEARCHED_HOST);
2540
+ await screen.findByTestId("correlate-request-error");
2541
+ const urlSyncCount: number = setQueryStringMock.mock.calls.length;
2542
+
2543
+ fireEvent.click(screen.getByTestId("correlate-retry"));
2544
+
2545
+ await waitFor(() => {
2546
+ expect(getListMock).toHaveBeenCalledTimes(2);
2547
+ });
2548
+ const firstQuery: QueryRecord = queryOfCall(0);
2549
+ const retryQuery: QueryRecord = queryOfCall(1);
2550
+ expect(Object.keys(retryQuery).sort()).toEqual(
2551
+ Object.keys(firstQuery).sort(),
2552
+ );
2553
+ expect((retryQuery["projectId"] as ObjectID).toString()).toBe(
2554
+ PROJECT_ID_STRING,
2555
+ );
2556
+ expect((retryQuery["observables"] as Includes).values).toEqual([
2557
+ SEARCHED_HOST,
2558
+ ]);
2559
+ expect(windowHoursOfCall(1)).toBe(24);
2560
+ expect(setQueryStringMock).toHaveBeenCalledTimes(urlSyncCount);
2561
+
2562
+ await waitFor(() => {
2563
+ expect(
2564
+ screen.getByTestId("flow-node-class:Authentication"),
2565
+ ).toBeInTheDocument();
2566
+ });
2567
+ expect(screen.queryByTestId("correlate-request-error")).toBeNull();
2568
+ expect(screen.getByTestId("correlate-result-count")).toHaveTextContent(
2569
+ "4 matching events.",
2570
+ );
2571
+ });
2572
+
2573
+ test("an impossible URL filter shows the filter error without fetching", async () => {
2574
+ mockQueryParams["q"] = serializeCorrelationFilter(impossibleFilter());
2575
+
2576
+ render(<CorrelateGraph />);
2577
+
2578
+ const filterError: HTMLElement = await screen.findByTestId(
2579
+ "correlate-filter-error",
2580
+ );
2581
+ expect(filterError).toHaveTextContent("This filter can't run");
2582
+ expect(
2583
+ within(filterError).getByText(/can never match/),
2584
+ ).toBeInTheDocument();
2585
+ expect(getListMock).not.toHaveBeenCalled();
2586
+ expect(screen.queryByTestId("correlate-request-error")).toBeNull();
2587
+ expect(screen.queryByTestId("mock-react-flow")).toBeNull();
2588
+ // The builder is already open, so there is nothing to offer.
2589
+ expect(screen.getByTestId("correlate-filter-builder")).toBeInTheDocument();
2590
+ expect(screen.queryByTestId("correlate-edit-conditions")).toBeNull();
2591
+ });
2592
+
2593
+ test("with the builder closed the filter error offers Edit conditions", async () => {
2594
+ mockQueryParams["q"] = serializeCorrelationFilter(impossibleFilter());
2595
+
2596
+ render(<CorrelateGraph />);
2597
+ await screen.findByTestId("correlate-filter-error");
2598
+
2599
+ fireEvent.click(
2600
+ screen.getByTestId("security-events-correlate-toggle-simple"),
2601
+ );
2602
+ const editButton: HTMLElement = within(
2603
+ screen.getByTestId("correlate-filter-error"),
2604
+ ).getByTestId("correlate-edit-conditions");
2605
+
2606
+ fireEvent.click(editButton);
2607
+
2608
+ expect(screen.getByTestId("correlate-filter-builder")).toBeInTheDocument();
2609
+ expect(screen.queryByTestId("correlate-edit-conditions")).toBeNull();
2610
+ // The builder is seeded from the applied (broken) filter.
2611
+ expect(screen.getByTestId("correlate-condition-value-1")).toHaveValue(
2612
+ "host-b",
2613
+ );
2614
+ expect(getListMock).not.toHaveBeenCalled();
2615
+ });
2616
+
2617
+ test("switching the connector to OR fixes the filter and fetches", async () => {
2618
+ mockQueryParams["q"] = serializeCorrelationFilter(impossibleFilter());
2619
+
2620
+ render(<CorrelateGraph />);
2621
+ await screen.findByTestId("correlate-filter-error");
2622
+
2623
+ fireEvent.click(screen.getByTestId("correlate-connector-or"));
2624
+ fireEvent.click(screen.getByTestId("security-events-correlate-button"));
2625
+
2626
+ // Either host now matches: one hasAny/IN query over both values.
2627
+ await waitFor(() => {
2628
+ expect(getListMock).toHaveBeenCalledTimes(1);
2629
+ });
2630
+ const hosts: Includes = queryOfCall(0)["principalHost"] as Includes;
2631
+ expect(hosts).toBeInstanceOf(Includes);
2632
+ expect(hosts.values).toEqual(["host-a", "host-b"]);
2633
+ await waitForNoResults();
2634
+ expect(screen.queryByTestId("correlate-filter-error")).toBeNull();
2635
+ });
2636
+ });
2637
+
2638
+ describe("CorrelateGraph no results", () => {
2639
+ test.each<[string, string]>([
2640
+ ["1", "Search last 6 hours"],
2641
+ ["6", "Search last 24 hours"],
2642
+ ["24", "Search last 7 days"],
2643
+ ["168", "Search last 30 days"],
2644
+ ])(
2645
+ "at ?hours=%s the widen action reads %s",
2646
+ async (hours: string, widenLabel: string) => {
2647
+ mockQueryParams["observable"] = SEARCHED_HOST;
2648
+ mockQueryParams["hours"] = hours;
2649
+
2650
+ render(<CorrelateGraph />);
2651
+ await waitForNoResults();
2652
+
2653
+ expect(screen.getByText("No events found")).toBeInTheDocument();
2654
+ expect(
2655
+ screen.getByTestId("correlate-no-results-widen"),
2656
+ ).toHaveTextContent(widenLabel);
2657
+ },
2658
+ );
2659
+
2660
+ test("there is nothing wider than 30 days", async () => {
2661
+ mockQueryParams["observable"] = SEARCHED_HOST;
2662
+ mockQueryParams["hours"] = "720";
2663
+
2664
+ render(<CorrelateGraph />);
2665
+ await waitForNoResults();
2666
+
2667
+ expect(screen.queryByTestId("correlate-no-results-widen")).toBeNull();
2668
+ expect(screen.getByTestId("correlate-no-results-edit")).toBeInTheDocument();
2669
+ });
2670
+
2671
+ test("widening refetches a 7-day window and syncs the URL", async () => {
2672
+ render(<CorrelateGraph />);
2673
+ runQuickSearch(SEARCHED_HOST);
2674
+ await waitForNoResults();
2675
+ expect(windowHoursOfCall(0)).toBe(24);
2676
+
2677
+ fireEvent.click(screen.getByTestId("correlate-no-results-widen"));
2678
+
2679
+ await waitFor(() => {
2680
+ expect(getListMock).toHaveBeenCalledTimes(2);
2681
+ });
2682
+ expect(windowHoursOfCall(1)).toBe(168);
2683
+ expect((queryOfCall(1)["observables"] as Includes).values).toEqual([
2684
+ SEARCHED_HOST,
2685
+ ]);
2686
+ expect(setQueryStringMock).toHaveBeenLastCalledWith(
2687
+ expect.objectContaining({
2688
+ q: expect.stringContaining(SEARCHED_HOST),
2689
+ hours: "168",
2690
+ observable: null,
2691
+ }),
2692
+ );
2693
+ expect(
2694
+ screen.getByTestId("security-events-correlate-time-range"),
2695
+ ).toHaveTextContent("Last 7 days");
2696
+
2697
+ await waitForSettled();
2698
+ await waitForNoResults();
2699
+ expect(screen.getByTestId("correlate-no-results-widen")).toHaveTextContent(
2700
+ "Search last 30 days",
2701
+ );
2702
+ });
2703
+
2704
+ test("Remove last condition appears only with several conditions and drops the last one", async () => {
2705
+ mockQueryParams["q"] = serializeCorrelationFilter(
2706
+ twoConditionFilter("and"),
2707
+ );
2708
+
2709
+ render(<CorrelateGraph />);
2710
+ await waitForNoResults();
2711
+
2712
+ const removeLast: HTMLElement = screen.getByTestId(
2713
+ "correlate-no-results-remove-last",
2714
+ );
2715
+ expect(removeLast).toHaveTextContent("Remove last condition");
2716
+ fireEvent.click(removeLast);
2717
+
2718
+ await waitFor(() => {
2719
+ expect(getListMock).toHaveBeenCalledTimes(2);
2720
+ });
2721
+ const query: QueryRecord = queryOfCall(1);
2722
+ expect(query["principalIp"]).toBe("192.168.1.20");
2723
+ expect(query["principalHost"]).toBeUndefined();
2724
+ expect(screen.getByTestId("correlate-filter-chip-0")).toHaveTextContent(
2725
+ "192.168.1.20",
2726
+ );
2727
+ expect(screen.queryByTestId("correlate-filter-chip-1")).toBeNull();
2728
+
2729
+ await waitForSettled();
2730
+ await waitForNoResults();
2731
+ expect(screen.queryByTestId("correlate-no-results-remove-last")).toBeNull();
2732
+ });
2733
+
2734
+ test("a single condition offers no Remove last condition", async () => {
2735
+ render(<CorrelateGraph />);
2736
+ runQuickSearch(SEARCHED_HOST);
2737
+ await waitForNoResults();
2738
+
2739
+ expect(screen.queryByTestId("correlate-no-results-remove-last")).toBeNull();
2740
+ });
2741
+
2742
+ test("Edit conditions opens the builder seeded from the filter and then hides", async () => {
2743
+ render(<CorrelateGraph />);
2744
+ runQuickSearch(SEARCHED_HOST);
2745
+ await waitForNoResults();
2746
+
2747
+ fireEvent.click(screen.getByTestId("correlate-no-results-edit"));
2748
+
2749
+ expect(screen.getByTestId("correlate-filter-builder")).toBeInTheDocument();
2750
+ expect(screen.getByTestId("correlate-condition-value-0")).toHaveValue(
2751
+ SEARCHED_HOST,
2752
+ );
2753
+ expect(screen.queryByTestId("correlate-no-results-edit")).toBeNull();
2754
+ expect(getListMock).toHaveBeenCalledTimes(1);
2755
+ });
2756
+
2757
+ test("Edit conditions is hidden while the builder is already open", async () => {
2758
+ mockQueryParams["q"] = serializeCorrelationFilter(
2759
+ twoConditionFilter("and"),
2760
+ );
2761
+
2762
+ render(<CorrelateGraph />);
2763
+ await waitForNoResults();
2764
+
2765
+ expect(screen.getByTestId("correlate-filter-builder")).toBeInTheDocument();
2766
+ expect(screen.queryByTestId("correlate-no-results-edit")).toBeNull();
2767
+ expect(
2768
+ screen.getByTestId("correlate-no-results-widen"),
2769
+ ).toBeInTheDocument();
2770
+ });
2771
+ });
2772
+
2773
+ describe("CorrelateGraph loading", () => {
2774
+ test("the first load shows a loader instead of the canvas and blocks another Correlate", async () => {
2775
+ const firstLoad: Deferred<MockListResult> =
2776
+ createDeferred<MockListResult>();
2777
+ getListMock.mockImplementationOnce(() => {
2778
+ return firstLoad.promise;
2779
+ });
2780
+
2781
+ render(<CorrelateGraph />);
2782
+ runQuickSearch(SEARCHED_HOST);
2783
+
2784
+ const loader: HTMLElement = screen.getByTestId("correlate-loading");
2785
+ // The (hidden) result count announces the load, not the loader block.
2786
+ expect(loader).not.toHaveAttribute("role");
2787
+ expect(loader).not.toHaveAttribute("aria-live");
2788
+ expect(loader).toHaveTextContent("Correlating events…");
2789
+ expect(screen.getByTestId("correlate-results")).toHaveAttribute(
2790
+ "aria-busy",
2791
+ "true",
2792
+ );
2793
+ expect(screen.queryByTestId("mock-react-flow")).toBeNull();
2794
+ const resultCount: HTMLElement = screen.getByTestId(
2795
+ "correlate-result-count",
2796
+ );
2797
+ expect(resultCount).toHaveTextContent(/^Correlating events…$/);
2798
+ expect(screen.getByTestId("correlate-results-header")).toHaveClass(
2799
+ "sr-only",
2800
+ );
2801
+ expect(screen.queryByTestId("correlate-refresh")).toBeNull();
2802
+ expect(
2803
+ document.getElementById("security-events-correlate-no-results"),
2804
+ ).toBeNull();
2805
+
2806
+ const correlateButton: HTMLElement = screen.getByTestId(
2807
+ "security-events-correlate-button",
2808
+ );
2809
+ expect(correlateButton).toBeDisabled();
2810
+ // Enter is blocked too while the request is in flight.
2811
+ fireEvent.keyDown(
2812
+ screen.getByTestId("security-events-correlate-observable"),
2813
+ { key: "Enter" },
2814
+ );
2815
+ expect(getListMock).toHaveBeenCalledTimes(1);
2816
+
2817
+ await act(async () => {
2818
+ firstLoad.resolve(listResult(standardEvents()));
2819
+ });
2820
+
2821
+ await waitForSettled();
2822
+ expect(screen.queryByTestId("correlate-loading")).toBeNull();
2823
+ expect(screen.getByTestId("mock-react-flow")).toBeInTheDocument();
2824
+ expect(correlateButton).not.toBeDisabled();
2825
+ // The same live region now carries the result, and the header shows.
2826
+ expect(screen.getByTestId("correlate-result-count")).toBe(resultCount);
2827
+ expect(resultCount).toHaveTextContent(/^4 matching events\.$/);
2828
+ expect(screen.getByTestId("correlate-results-header")).not.toHaveClass(
2829
+ "sr-only",
2830
+ );
2831
+ });
2832
+
2833
+ test("a reload keeps the previous graph under an overlay and hides the selection panel", async () => {
2834
+ await renderCorrelated();
2835
+ clickFlowNode("observable:alice");
2836
+ expect(screen.getByTestId("correlate-inspector")).toBeInTheDocument();
2837
+
2838
+ const reload: Deferred<MockListResult> = createDeferred<MockListResult>();
2839
+ getListMock.mockImplementationOnce(() => {
2840
+ return reload.promise;
2841
+ });
2842
+ fireEvent.click(screen.getByTestId("correlate-refresh"));
2843
+
2844
+ await waitFor(() => {
2845
+ expect(getListMock).toHaveBeenCalledTimes(2);
2846
+ });
2847
+ expect(screen.getByTestId("correlate-results")).toHaveAttribute(
2848
+ "aria-busy",
2849
+ "true",
2850
+ );
2851
+ expect(screen.getByTestId("mock-react-flow")).toBeInTheDocument();
2852
+ expect(
2853
+ within(screen.getByTestId("correlate-canvas")).getByTestId(
2854
+ "correlate-canvas-loading",
2855
+ ),
2856
+ ).toHaveTextContent("Correlating events…");
2857
+ expect(screen.queryByTestId("correlate-loading")).toBeNull();
2858
+ expect(screen.getByTestId("correlate-result-count")).toHaveTextContent(
2859
+ /^Correlating events…$/,
2860
+ );
2861
+
2862
+ // No selection panel or neighbourhood dimming over stale data.
2863
+ expect(screen.queryByTestId("correlate-inspector")).toBeNull();
2864
+ expect(getDimmedNodeIds()).toEqual([]);
2865
+ // The previous results stay listed but cannot be acted on.
2866
+ expect(screen.getByTestId("correlate-overview-class-0")).toBeDisabled();
2867
+ expect(screen.getByTestId("correlate-stat-worst-class")).toBeDisabled();
2868
+ expect(screen.getByTestId("correlate-refresh")).toBeDisabled();
2869
+ expect(
2870
+ screen.getByTestId("security-events-correlate-button"),
2871
+ ).toBeDisabled();
2872
+
2873
+ await act(async () => {
2874
+ reload.resolve(listResult(standardEvents()));
2875
+ });
2876
+ await waitForSettled();
2877
+
2878
+ expect(screen.queryByTestId("correlate-canvas-loading")).toBeNull();
2879
+ expect(screen.getByTestId("correlate-result-count")).toHaveTextContent(
2880
+ "4 matching events.",
2881
+ );
2882
+ // The node is still on the canvas, so its selection comes back.
2883
+ expect(
2884
+ within(screen.getByTestId("correlate-observable-actions")).getByText(
2885
+ "alice",
2886
+ ),
2887
+ ).toBeInTheDocument();
2888
+ expect(screen.getByTestId("correlate-overview-class-0")).not.toBeDisabled();
2889
+ });
2890
+ });
2891
+
2892
+ describe("CorrelateGraph applied filter chips", () => {
2893
+ test("a single condition still offers Clear all", async () => {
2894
+ await renderCorrelated();
2895
+
2896
+ const chips: HTMLElement = screen.getByTestId("correlate-filter-chips");
2897
+ expect(
2898
+ within(chips).getByTestId("correlate-filter-clear-all"),
2899
+ ).toHaveTextContent("Clear all");
2900
+ expect(within(chips).queryByText("AND")).toBeNull();
2901
+ });
2902
+
2903
+ test("removing the only chip returns to the start state", async () => {
2904
+ await renderCorrelated();
2905
+
2906
+ fireEvent.click(screen.getByTestId("correlate-filter-chip-remove-0"));
2907
+
2908
+ expect(screen.getAllByText("Correlate security events")).toHaveLength(1);
2909
+ expect(screen.getByTestId("correlate-start-steps")).toBeInTheDocument();
2910
+ expect(screen.queryByTestId("correlate-applied-filter")).toBeNull();
2911
+ expect(screen.queryByTestId("mock-react-flow")).toBeNull();
2912
+ expect(screen.queryByTestId("correlate-filter-builder")).toBeNull();
2913
+ expect(
2914
+ screen.getByTestId("security-events-correlate-observable"),
2915
+ ).toHaveValue("");
2916
+ expect(setQueryStringMock).toHaveBeenLastCalledWith(
2917
+ expect.objectContaining({ q: null, hours: null, observable: null }),
2918
+ );
2919
+ expect(getListMock).toHaveBeenCalledTimes(1);
2920
+ });
2921
+ });
2922
+
2923
+ describe("CorrelateGraph stale responses", () => {
2924
+ test("a slow response for an older query never overwrites newer results", async () => {
2925
+ const slow: Deferred<MockListResult> = createDeferred<MockListResult>();
2926
+ getListMock.mockImplementationOnce(() => {
2927
+ return slow.promise;
2928
+ });
2929
+ getListMock.mockResolvedValueOnce(
2930
+ listResult([
2931
+ buildEvent({
2932
+ id: "f1111111-1111-4111-8111-111111111111",
2933
+ className: "Fresh Class",
2934
+ observables: [SEARCHED_HOST],
2935
+ }),
2936
+ ]),
2937
+ );
2938
+
2939
+ render(<CorrelateGraph />);
2940
+ runQuickSearch(SEARCHED_HOST);
2941
+ expect(getListMock).toHaveBeenCalledTimes(1);
2942
+
2943
+ // The time range stays usable while loading and supersedes the request.
2944
+ selectTimeRange("Last 7 days");
2945
+ await waitFor(() => {
2946
+ expect(getListMock).toHaveBeenCalledTimes(2);
2947
+ });
2948
+ await waitFor(() => {
2949
+ expect(
2950
+ screen.getByTestId("flow-node-class:Fresh Class"),
2951
+ ).toBeInTheDocument();
2952
+ });
2953
+
2954
+ await act(async () => {
2955
+ slow.resolve(
2956
+ listResult([
2957
+ buildEvent({
2958
+ id: "f2222222-2222-4222-8222-222222222222",
2959
+ className: "Stale Class",
2960
+ observables: [SEARCHED_HOST],
2961
+ }),
2962
+ buildEvent({
2963
+ id: "f3333333-3333-4333-8333-333333333333",
2964
+ className: "Stale Class",
2965
+ observables: [SEARCHED_HOST],
2966
+ }),
2967
+ ]),
2968
+ );
2969
+ });
2970
+
2971
+ expect(screen.queryByTestId("flow-node-class:Stale Class")).toBeNull();
2972
+ expect(
2973
+ screen.getByTestId("flow-node-class:Fresh Class"),
2974
+ ).toBeInTheDocument();
2975
+ expect(screen.getByTestId("correlate-result-count")).toHaveTextContent(
2976
+ /^1 matching event\.$/,
2977
+ );
2978
+ expect(screen.getByTestId("correlate-results")).toHaveAttribute(
2979
+ "aria-busy",
2980
+ "false",
2981
+ );
2982
+ });
2983
+
2984
+ test("a slow failure for an older query does not replace newer results", async () => {
2985
+ const slow: Deferred<MockListResult> = createDeferred<MockListResult>();
2986
+ getListMock.mockImplementationOnce(() => {
2987
+ return slow.promise;
2988
+ });
2989
+ getListMock.mockResolvedValueOnce(listResult(standardEvents()));
2990
+
2991
+ render(<CorrelateGraph />);
2992
+ runQuickSearch(SEARCHED_HOST);
2993
+ selectTimeRange("Last 7 days");
2994
+ await waitForSettled();
2995
+ expect(screen.getByTestId("mock-react-flow")).toBeInTheDocument();
2996
+
2997
+ await act(async () => {
2998
+ slow.reject(new Error("ClickHouse is down"));
2999
+ });
3000
+
3001
+ expect(screen.queryByTestId("correlate-request-error")).toBeNull();
3002
+ expect(screen.getByTestId("mock-react-flow")).toBeInTheDocument();
3003
+ expect(screen.getByTestId("correlate-result-count")).toHaveTextContent(
3004
+ "4 matching events.",
3005
+ );
3006
+ });
3007
+
3008
+ test("a slow response that lands after Clear all is ignored", async () => {
3009
+ const slow: Deferred<MockListResult> = createDeferred<MockListResult>();
3010
+ getListMock.mockImplementationOnce(() => {
3011
+ return slow.promise;
3012
+ });
3013
+
3014
+ render(<CorrelateGraph />);
3015
+ runQuickSearch(SEARCHED_HOST);
3016
+ fireEvent.click(screen.getByTestId("correlate-filter-clear-all"));
3017
+ expect(screen.getByTestId("correlate-start-steps")).toBeInTheDocument();
3018
+
3019
+ await act(async () => {
3020
+ slow.resolve(listResult(standardEvents()));
3021
+ });
3022
+
3023
+ expect(screen.getByTestId("correlate-start-steps")).toBeInTheDocument();
3024
+ expect(screen.queryByTestId("mock-react-flow")).toBeNull();
3025
+ expect(screen.queryByTestId("correlate-result-count")).toBeNull();
3026
+ expect(getListMock).toHaveBeenCalledTimes(1);
3027
+ // Nothing is left spinning either.
3028
+ expect(
3029
+ screen.getByTestId("security-events-correlate-button"),
3030
+ ).toBeDisabled();
3031
+ fireEvent.change(
3032
+ screen.getByTestId("security-events-correlate-observable"),
3033
+ { target: { value: SEARCHED_HOST } },
3034
+ );
3035
+ expect(
3036
+ screen.getByTestId("security-events-correlate-button"),
3037
+ ).not.toBeDisabled();
3038
+ });
3039
+ });
3040
+
3041
+ /*
3042
+ * Accessibility, layout, fitting, reveal and focus. requestAnimationFrame is
3043
+ * swapped for a queue the tests flush one frame at a time, matchMedia for a
3044
+ * switchable stub and ResizeObserver for a recorder, so every responsive
3045
+ * branch is reachable in jsdom.
3046
+ */
3047
+
3048
+ const WIDE_CANVAS_QUERY: string = "(min-width: 768px)";
3049
+ const FLOATING_PANEL_QUERY: string = "(min-width: 1280px)";
3050
+ const FIT_VIEW_OPTIONS: { padding: number; maxZoom: number } = {
3051
+ padding: 0.16,
3052
+ maxZoom: 1,
3053
+ };
3054
+ const CANVAS_WIDTH: number = 1000;
3055
+ const CANVAS_HEIGHT: number = 500;
3056
+ const PANEL_WIDTH: number = 352;
3057
+ const REVEAL_MARGIN: number = 24;
3058
+ // Where a node lands when the free area (left of the panel) is centred.
3059
+ const FREE_AREA_SHIFT: number =
3060
+ CANVAS_WIDTH / 2 - (CANVAS_WIDTH - PANEL_WIDTH) / 2;
3061
+
3062
+ interface MediaState {
3063
+ wide: boolean;
3064
+ floating: boolean;
3065
+ }
3066
+
3067
+ interface SyncedFilter {
3068
+ v: number;
3069
+ j: string;
3070
+ c: Array<[string, string, string]>;
3071
+ }
3072
+
3073
+ interface Point {
3074
+ x: number;
3075
+ y: number;
3076
+ }
3077
+
3078
+ const mediaState: MediaState = { wide: true, floating: true };
3079
+ const mediaListeners: Set<() => void> = new Set<() => void>();
3080
+
3081
+ function installMatchMedia(state: MediaState): void {
3082
+ mediaState.wide = state.wide;
3083
+ mediaState.floating = state.floating;
3084
+ Object.defineProperty(window, "matchMedia", {
3085
+ configurable: true,
3086
+ writable: true,
3087
+ value: (query: string): MediaQueryList => {
3088
+ return {
3089
+ media: query,
3090
+ onchange: null,
3091
+ get matches(): boolean {
3092
+ if (query === FLOATING_PANEL_QUERY) {
3093
+ return mediaState.floating;
3094
+ }
3095
+ if (query === WIDE_CANVAS_QUERY) {
3096
+ return mediaState.wide;
3097
+ }
3098
+ return false;
3099
+ },
3100
+ addEventListener: (_type: string, listener: () => void): void => {
3101
+ mediaListeners.add(listener);
3102
+ },
3103
+ removeEventListener: (_type: string, listener: () => void): void => {
3104
+ mediaListeners.delete(listener);
3105
+ },
3106
+ addListener: (listener: () => void): void => {
3107
+ mediaListeners.add(listener);
3108
+ },
3109
+ removeListener: (listener: () => void): void => {
3110
+ mediaListeners.delete(listener);
3111
+ },
3112
+ dispatchEvent: (): boolean => {
3113
+ return true;
3114
+ },
3115
+ } as unknown as MediaQueryList;
3116
+ },
3117
+ });
3118
+ }
3119
+
3120
+ // Flips the stubbed screen size and fires every "change" listener.
3121
+ function changeMedia(state: MediaState): void {
3122
+ act(() => {
3123
+ mediaState.wide = state.wide;
3124
+ mediaState.floating = state.floating;
3125
+ for (const listener of Array.from(mediaListeners)) {
3126
+ listener();
3127
+ }
3128
+ });
3129
+ }
3130
+
3131
+ function removeMatchMedia(): void {
3132
+ delete (window as any).matchMedia;
3133
+ mediaListeners.clear();
3134
+ }
3135
+
3136
+ const pendingFrames: Map<number, FrameRequestCallback> = new Map<
3137
+ number,
3138
+ FrameRequestCallback
3139
+ >();
3140
+ let nextFrameId: number = 1;
3141
+ let frameSpies: Array<{ mockRestore: () => void }> = [];
3142
+
3143
+ function installFrameQueue(): void {
3144
+ pendingFrames.clear();
3145
+ frameSpies = [
3146
+ jest
3147
+ .spyOn(window, "requestAnimationFrame")
3148
+ .mockImplementation((callback: FrameRequestCallback): number => {
3149
+ const id: number = nextFrameId;
3150
+ nextFrameId += 1;
3151
+ pendingFrames.set(id, callback);
3152
+ return id;
3153
+ }),
3154
+ jest
3155
+ .spyOn(window, "cancelAnimationFrame")
3156
+ .mockImplementation((id: number): void => {
3157
+ pendingFrames.delete(id);
3158
+ }),
3159
+ ];
3160
+ }
3161
+
3162
+ function restoreFrameQueue(): void {
3163
+ for (const spy of frameSpies) {
3164
+ spy.mockRestore();
3165
+ }
3166
+ frameSpies = [];
3167
+ pendingFrames.clear();
3168
+ }
3169
+
3170
+ // Runs the callbacks queued so far — anything they queue waits a frame.
3171
+ function flushFrame(): void {
3172
+ const callbacks: Array<FrameRequestCallback> = Array.from(
3173
+ pendingFrames.values(),
3174
+ );
3175
+ pendingFrames.clear();
3176
+ act(() => {
3177
+ for (const callback of callbacks) {
3178
+ callback(0);
3179
+ }
3180
+ });
3181
+ }
3182
+
3183
+ function flushFrames(count: number): void {
3184
+ for (let frame: number = 0; frame < count; frame++) {
3185
+ flushFrame();
3186
+ }
3187
+ }
3188
+
3189
+ class RecordingResizeObserver {
3190
+ public static instances: Array<RecordingResizeObserver> = [];
3191
+ public readonly callback: ResizeObserverCallback;
3192
+ public readonly targets: Array<Element> = [];
3193
+ public isDisconnected: boolean = false;
3194
+
3195
+ public constructor(callback: ResizeObserverCallback) {
3196
+ this.callback = callback;
3197
+ RecordingResizeObserver.instances.push(this);
3198
+ }
3199
+
3200
+ public observe(target: Element): void {
3201
+ this.targets.push(target);
3202
+ }
3203
+
3204
+ public unobserve(target: Element): void {
3205
+ const index: number = this.targets.indexOf(target);
3206
+ if (index >= 0) {
3207
+ this.targets.splice(index, 1);
3208
+ }
3209
+ }
3210
+
3211
+ public disconnect(): void {
3212
+ this.isDisconnected = true;
3213
+ }
3214
+ }
3215
+
3216
+ function installResizeObserver(): void {
3217
+ RecordingResizeObserver.instances = [];
3218
+ Object.defineProperty(window, "ResizeObserver", {
3219
+ configurable: true,
3220
+ writable: true,
3221
+ value: RecordingResizeObserver,
3222
+ });
3223
+ }
3224
+
3225
+ function removeResizeObserver(): void {
3226
+ delete (window as any).ResizeObserver;
3227
+ RecordingResizeObserver.instances = [];
3228
+ }
3229
+
3230
+ function getCanvasObservers(
3231
+ canvas: HTMLElement,
3232
+ ): Array<RecordingResizeObserver> {
3233
+ return RecordingResizeObserver.instances.filter(
3234
+ (observer: RecordingResizeObserver): boolean => {
3235
+ return observer.targets.includes(canvas);
3236
+ },
3237
+ );
3238
+ }
3239
+
3240
+ function setElementSize(
3241
+ element: HTMLElement,
3242
+ width: number,
3243
+ height: number,
3244
+ ): void {
3245
+ Object.defineProperty(element, "clientWidth", {
3246
+ configurable: true,
3247
+ value: width,
3248
+ });
3249
+ Object.defineProperty(element, "clientHeight", {
3250
+ configurable: true,
3251
+ value: height,
3252
+ });
3253
+ }
3254
+
3255
+ // Gives the canvas a new width and tells its ResizeObserver about it.
3256
+ function resizeCanvas(width: number): void {
3257
+ const canvas: HTMLElement = screen.getByTestId("correlate-canvas");
3258
+ setElementSize(canvas, width, CANVAS_HEIGHT);
3259
+ const observers: Array<RecordingResizeObserver> = getCanvasObservers(canvas);
3260
+ expect(observers.length).toBeGreaterThan(0);
3261
+ act(() => {
3262
+ for (const observer of observers) {
3263
+ observer.callback([], observer as unknown as ResizeObserver);
3264
+ }
3265
+ });
3266
+ }
3267
+
3268
+ function setElementRect(
3269
+ element: HTMLElement,
3270
+ top: number,
3271
+ bottom: number,
3272
+ ): void {
3273
+ Object.defineProperty(element, "getBoundingClientRect", {
3274
+ configurable: true,
3275
+ value: (): DOMRect => {
3276
+ return {
3277
+ top: top,
3278
+ bottom: bottom,
3279
+ left: 0,
3280
+ right: 0,
3281
+ x: 0,
3282
+ y: top,
3283
+ width: 0,
3284
+ height: bottom - top,
3285
+ } as unknown as DOMRect;
3286
+ },
3287
+ });
3288
+ }
3289
+
3290
+ function stubScrollIntoView(element: HTMLElement): MockFunction {
3291
+ const scrollIntoViewMock: MockFunction = getJestMockFunction();
3292
+ Object.defineProperty(element, "scrollIntoView", {
3293
+ configurable: true,
3294
+ value: scrollIntoViewMock,
3295
+ });
3296
+ return scrollIntoViewMock;
3297
+ }
3298
+
3299
+ function getNodePoint(nodeId: string): Point {
3300
+ const position: Point = getFlowNode(nodeId).position;
3301
+ return { x: position.x, y: position.y };
3302
+ }
3303
+
3304
+ // Moves the fake viewport so the node's centre lands at (screenX, screenY).
3305
+ function placeNodeOnScreen(
3306
+ nodeId: string,
3307
+ screenX: number,
3308
+ screenY: number,
3309
+ zoom: number,
3310
+ ): void {
3311
+ const point: Point = getNodePoint(nodeId);
3312
+ mockViewport.zoom = zoom;
3313
+ mockViewport.x = screenX - point.x * zoom;
3314
+ mockViewport.y = screenY - point.y * zoom;
3315
+ }
3316
+
3317
+ function getClassPositions(): Map<string, Point> {
3318
+ const positions: Map<string, Point> = new Map<string, Point>();
3319
+ for (const node of getFlowNodes()) {
3320
+ if (node.data.kind === "class") {
3321
+ positions.set(node.id, getNodePoint(node.id));
3322
+ }
3323
+ }
3324
+ return positions;
3325
+ }
3326
+
3327
+ // What getCanvasHeight makes of the graph currently handed to React Flow.
3328
+ function expectedCanvasHeight(narrowCanvasWidth?: number): number {
3329
+ const graphNodes: Array<CorrelationGraphNode> = getFlowNodes().map(
3330
+ (node: CorrelateFlowNode): CorrelationGraphNode => {
3331
+ return { id: node.id, label: node.data.label, kind: node.data.kind };
3332
+ },
3333
+ );
3334
+ const positions: Map<string, LayoutPoint> = new Map<string, LayoutPoint>();
3335
+ for (const node of getFlowNodes()) {
3336
+ positions.set(node.id, getNodePoint(node.id));
3337
+ }
3338
+ return getCanvasHeight(graphNodes, positions, narrowCanvasWidth);
3339
+ }
3340
+
3341
+ /*
3342
+ * jsdom's CSS parser drops min() values, so the canvas height is read from
3343
+ * what React writes to style.height rather than from the element.
3344
+ */
3345
+ let heightSetterSpy: SpyInstance<(value: string) => void> | null = null;
3346
+
3347
+ function recordStyleHeights(): void {
3348
+ heightSetterSpy = jest.spyOn(CSSStyleDeclaration.prototype, "height", "set");
3349
+ }
3350
+
3351
+ function restoreStyleHeights(): void {
3352
+ heightSetterSpy?.mockRestore();
3353
+ heightSetterSpy = null;
3354
+ }
3355
+
3356
+ function getCanvasHeightStyle(): string {
3357
+ if (!heightSetterSpy) {
3358
+ throw new Error("Style heights are not being recorded");
3359
+ }
3360
+ const canvasHeights: Array<string> = heightSetterSpy.mock.calls
3361
+ .map((call: [value: string]): string => {
3362
+ return String(call[0]);
3363
+ })
3364
+ .filter((value: string): boolean => {
3365
+ return value.startsWith("min(");
3366
+ });
3367
+ return canvasHeights[canvasHeights.length - 1] || "";
3368
+ }
3369
+
3370
+ function lastSyncedFilter(): SyncedFilter {
3371
+ const calls: Array<Array<unknown>> = setQueryStringMock.mock.calls;
3372
+ const lastArgs: { q: string | null } | undefined = calls[
3373
+ calls.length - 1
3374
+ ]?.[0] as { q: string | null } | undefined;
3375
+ if (!lastArgs || !lastArgs.q) {
3376
+ throw new Error("No filter was synced to the URL");
3377
+ }
3378
+ return JSON.parse(lastArgs.q) as SyncedFilter;
3379
+ }
3380
+
3381
+ /*
3382
+ * Results land outside act(), so React may still owe the new graph its
3383
+ * passive effects (the fit scheduling, onInit, the ResizeObserver) when the
3384
+ * DOM already looks settled. Yielding a few macrotasks inside act() lets
3385
+ * them run before a test starts counting frames.
3386
+ */
3387
+ async function flushPendingEffects(): Promise<void> {
3388
+ await act(async () => {
3389
+ for (let tick: number = 0; tick < 3; tick++) {
3390
+ await new Promise<void>((resolve: () => void): void => {
3391
+ setTimeout(resolve, 0);
3392
+ });
3393
+ }
3394
+ });
3395
+ }
3396
+
3397
+ async function waitForGraph(): Promise<void> {
3398
+ await waitForSettled();
3399
+ await flushPendingEffects();
3400
+ }
3401
+
3402
+ async function renderGraph(
3403
+ events: Array<SecurityEvent> = standardEvents(),
3404
+ ): Promise<void> {
3405
+ await renderCorrelated(events);
3406
+ await flushPendingEffects();
3407
+ }
3408
+
3409
+ function focusAndClick(element: HTMLElement): void {
3410
+ act(() => {
3411
+ element.focus();
3412
+ });
3413
+ expect(document.activeElement).toBe(element);
3414
+ fireEvent.click(element);
3415
+ }
3416
+
3417
+ describe("CorrelateGraph live region", () => {
3418
+ test("no results: the hidden header says 0 matching events and offers no graph actions", async () => {
3419
+ render(<CorrelateGraph />);
3420
+ runQuickSearch(SEARCHED_HOST);
3421
+ await waitForNoResults();
3422
+ await waitForGraph();
3423
+
3424
+ const results: HTMLElement = screen.getByTestId("correlate-results");
3425
+ const header: HTMLElement = screen.getByTestId("correlate-results-header");
3426
+ expect(results.firstElementChild).toBe(header);
3427
+ expect(header).toHaveClass("sr-only");
3428
+
3429
+ const resultCount: HTMLElement = within(header).getByTestId(
3430
+ "correlate-result-count",
3431
+ );
3432
+ expect(resultCount.tagName).toBe("P");
3433
+ expect(resultCount).toHaveAttribute("role", "status");
3434
+ expect(resultCount).toHaveAttribute("aria-live", "polite");
3435
+ expect(resultCount).toHaveTextContent(/^0 matching events\.$/);
3436
+ expect(within(results).getAllByRole("status")).toEqual([resultCount]);
3437
+
3438
+ expect(screen.queryByTestId("correlate-fit-view")).toBeNull();
3439
+ expect(screen.queryByTestId("correlate-refresh")).toBeNull();
3440
+ expect(screen.queryByTestId("correlate-result-window")).toBeNull();
3441
+ });
3442
+
3443
+ test("results: the header shows with the count, the window, Fit and Refresh", async () => {
3444
+ await renderGraph();
3445
+
3446
+ const results: HTMLElement = screen.getByTestId("correlate-results");
3447
+ const header: HTMLElement = screen.getByTestId("correlate-results-header");
3448
+ expect(results.firstElementChild).toBe(header);
3449
+ expect(header).not.toHaveClass("sr-only");
3450
+ expect(header).toHaveClass("flex", "border-b");
3451
+
3452
+ const resultCount: HTMLElement = within(header).getByTestId(
3453
+ "correlate-result-count",
3454
+ );
3455
+ expect(resultCount).toHaveAttribute("aria-live", "polite");
3456
+ expect(resultCount).toHaveTextContent(/^4 matching events\.$/);
3457
+ // Nothing else in the results competes with it on a small graph.
3458
+ expect(within(results).getAllByRole("status")).toEqual([resultCount]);
3459
+
3460
+ expect(
3461
+ within(header).getByTestId("correlate-result-window"),
3462
+ ).toBeInTheDocument();
3463
+ expect(within(header).getByTestId("correlate-fit-view")).toHaveTextContent(
3464
+ "Fit to screen",
3465
+ );
3466
+ expect(within(header).getByTestId("correlate-refresh")).toHaveTextContent(
3467
+ "Refresh",
3468
+ );
3469
+ });
3470
+
3471
+ test("a filter error leaves the live region empty and the warning alerts", async () => {
3472
+ mockQueryParams["q"] = serializeCorrelationFilter(impossibleFilter());
3473
+
3474
+ render(<CorrelateGraph />);
3475
+ const filterError: HTMLElement = await screen.findByTestId(
3476
+ "correlate-filter-error",
3477
+ );
3478
+
3479
+ expect(filterError).toHaveAttribute("role", "alert");
3480
+ const resultCount: HTMLElement = screen.getByTestId(
3481
+ "correlate-result-count",
3482
+ );
3483
+ expect(resultCount).toHaveAttribute("role", "status");
3484
+ expect(resultCount).toBeEmptyDOMElement();
3485
+ expect(screen.getByTestId("correlate-results-header")).toHaveClass(
3486
+ "sr-only",
3487
+ );
3488
+ expect(screen.queryByTestId("correlate-refresh")).toBeNull();
3489
+ });
3490
+
3491
+ test("a reload announces itself in the same region and then the new count", async () => {
3492
+ await renderGraph();
3493
+ const resultCount: HTMLElement = screen.getByTestId(
3494
+ "correlate-result-count",
3495
+ );
3496
+
3497
+ const reload: Deferred<MockListResult> = createDeferred<MockListResult>();
3498
+ getListMock.mockImplementationOnce(() => {
3499
+ return reload.promise;
3500
+ });
3501
+ fireEvent.click(screen.getByTestId("correlate-refresh"));
3502
+
3503
+ await waitFor(() => {
3504
+ expect(resultCount).toHaveTextContent(/^Correlating events…$/);
3505
+ });
3506
+ expect(screen.getByTestId("correlate-result-count")).toBe(resultCount);
3507
+ // The stale graph is still up, so its header stays visible.
3508
+ expect(screen.getByTestId("correlate-results-header")).not.toHaveClass(
3509
+ "sr-only",
3510
+ );
3511
+
3512
+ await act(async () => {
3513
+ reload.resolve(
3514
+ listResult([
3515
+ buildEvent({
3516
+ id: "d4444444-4444-4444-8444-444444444444",
3517
+ className: "Authentication",
3518
+ observables: [SEARCHED_HOST],
3519
+ }),
3520
+ ]),
3521
+ );
3522
+ });
3523
+ await waitForGraph();
3524
+
3525
+ expect(screen.getByTestId("correlate-result-count")).toBe(resultCount);
3526
+ expect(resultCount).toHaveTextContent(/^1 matching event\.$/);
3527
+ });
3528
+
3529
+ test("a request error after results empties the region and hides the header", async () => {
3530
+ await renderGraph();
3531
+ const resultCount: HTMLElement = screen.getByTestId(
3532
+ "correlate-result-count",
3533
+ );
3534
+
3535
+ getListMock.mockRejectedValueOnce(new Error("ClickHouse is down"));
3536
+ fireEvent.click(screen.getByTestId("correlate-refresh"));
3537
+
3538
+ await screen.findByTestId("correlate-request-error");
3539
+ expect(screen.getByTestId("correlate-result-count")).toBe(resultCount);
3540
+ expect(resultCount).toBeEmptyDOMElement();
3541
+ expect(screen.getByTestId("correlate-results-header")).toHaveClass(
3542
+ "sr-only",
3543
+ );
3544
+ expect(screen.queryByTestId("correlate-fit-view")).toBeNull();
3545
+ expect(screen.queryByTestId("correlate-refresh")).toBeNull();
3546
+ expect(screen.queryByTestId("correlate-result-window")).toBeNull();
3547
+ });
3548
+ });
3549
+
3550
+ describe("CorrelateGraph toolbar", () => {
3551
+ test("is a wrapping row so the controls stack on narrow screens", () => {
3552
+ render(<CorrelateGraph />);
3553
+
3554
+ expect(screen.getByTestId("correlate-toolbar")).toHaveClass(
3555
+ "flex",
3556
+ "flex-wrap",
3557
+ "items-center",
3558
+ "gap-3",
3559
+ );
3560
+ });
3561
+ });
3562
+
3563
+ describe("CorrelateGraph filter normalization", () => {
3564
+ test("a one-condition OR link loads as AND and rewrites the URL", async () => {
3565
+ mockQueryParams["q"] = serializeCorrelationFilter({
3566
+ conditions: [
3567
+ { field: "observable", operator: "equals", value: SEARCHED_HOST },
3568
+ ],
3569
+ connector: "or",
3570
+ } as CorrelationFilter);
3571
+ getListMock.mockResolvedValue(listResult(standardEvents()));
3572
+
3573
+ render(<CorrelateGraph />);
3574
+ await waitForGraph();
3575
+
3576
+ expect(lastSyncedFilter()).toEqual({
3577
+ v: 1,
3578
+ j: "and",
3579
+ c: [["observable", "equals", SEARCHED_HOST]],
3580
+ });
3581
+ // It is the plain quick search again, so the builder stays closed.
3582
+ expect(
3583
+ screen.getByTestId("security-events-correlate-observable"),
3584
+ ).toHaveValue(SEARCHED_HOST);
3585
+ expect(screen.queryByTestId("correlate-filter-builder")).toBeNull();
3586
+ expect(getFlowNode("center").data.eyebrow).toBe("Your filter");
3587
+
3588
+ // The AND-only pivots are available.
3589
+ clickFlowNode("observable:alice");
3590
+ expect(screen.getByTestId("correlate-action-exclude")).toBeInTheDocument();
3591
+ clickFlowNode("class:Authentication");
3592
+ expect(
3593
+ screen.getByTestId("correlate-drilldown-filter-class"),
3594
+ ).toBeInTheDocument();
3595
+ expect(screen.queryByTestId("correlate-drilldown-or-note")).toBeNull();
3596
+ });
3597
+
3598
+ test("removing chips from an OR filter down to one condition makes it an AND filter", async () => {
3599
+ mockQueryParams["q"] = serializeCorrelationFilter(twoObservableOrFilter());
3600
+ getListMock.mockResolvedValue(listResult(standardEvents()));
3601
+
3602
+ render(<CorrelateGraph />);
3603
+ await waitForGraph();
3604
+ expect(screen.getByTestId("correlate-filter-chips")).toHaveTextContent(
3605
+ "OR",
3606
+ );
3607
+ clickFlowNode("observable:alice");
3608
+ expect(screen.queryByTestId("correlate-action-exclude")).toBeNull();
3609
+
3610
+ fireEvent.click(screen.getByTestId("correlate-filter-chip-remove-0"));
3611
+
3612
+ await waitFor(() => {
3613
+ expect(getListMock).toHaveBeenCalledTimes(2);
3614
+ });
3615
+ expect(lastSyncedFilter()).toEqual({
3616
+ v: 1,
3617
+ j: "and",
3618
+ c: [["observable", "equals", "192.168.1.20"]],
3619
+ });
3620
+ expect(screen.getByTestId("correlate-filter-chips")).not.toHaveTextContent(
3621
+ "OR",
3622
+ );
3623
+ await waitForGraph();
3624
+
3625
+ // Exclude is back and builds an AND chain.
3626
+ clickFlowNode("observable:alice");
3627
+ fireEvent.click(screen.getByTestId("correlate-action-exclude"));
3628
+ await waitFor(() => {
3629
+ expect(getListMock).toHaveBeenCalledTimes(3);
3630
+ });
3631
+ const operators: Array<unknown> = queryOfCall(2)[
3632
+ "observables"
3633
+ ] as Array<unknown>;
3634
+ expect(operators[0]).toBeInstanceOf(Includes);
3635
+ expect((operators[0] as Includes).values).toEqual(["192.168.1.20"]);
3636
+ expect(operators[1]).toBeInstanceOf(IncludesNone);
3637
+ expect((operators[1] as IncludesNone).values).toEqual(["alice"]);
3638
+ expect(lastSyncedFilter()).toEqual({
3639
+ v: 1,
3640
+ j: "and",
3641
+ c: [
3642
+ ["observable", "equals", "192.168.1.20"],
3643
+ ["observable", "not-equals", "alice"],
3644
+ ],
3645
+ });
3646
+ expect(screen.getByTestId("correlate-filter-chips")).toHaveTextContent(
3647
+ "AND",
3648
+ );
3649
+ await waitForGraph();
3650
+ });
3651
+
3652
+ test("Remove last condition on an OR filter leaves an AND filter", async () => {
3653
+ mockQueryParams["q"] = serializeCorrelationFilter(twoConditionFilter("or"));
3654
+
3655
+ render(<CorrelateGraph />);
3656
+ await waitForNoResults();
3657
+ // Two fields under OR fan out into two queries.
3658
+ expect(getListMock).toHaveBeenCalledTimes(2);
3659
+
3660
+ fireEvent.click(screen.getByTestId("correlate-no-results-remove-last"));
3661
+
3662
+ await waitFor(() => {
3663
+ expect(getListMock).toHaveBeenCalledTimes(3);
3664
+ });
3665
+ expect(lastSyncedFilter()).toEqual({
3666
+ v: 1,
3667
+ j: "and",
3668
+ c: [["principalIp", "equals", "192.168.1.20"]],
3669
+ });
3670
+ await waitForGraph();
3671
+ await waitForNoResults();
3672
+ });
3673
+
3674
+ test("applying a one-row builder draft left on OR sends an AND filter", async () => {
3675
+ render(<CorrelateGraph />);
3676
+ fireEvent.click(
3677
+ screen.getByTestId("security-events-correlate-toggle-builder"),
3678
+ );
3679
+ fireEvent.change(screen.getByTestId("correlate-condition-value-0"), {
3680
+ target: { value: SEARCHED_HOST },
3681
+ });
3682
+ fireEvent.click(screen.getByTestId("correlate-add-condition"));
3683
+ fireEvent.click(screen.getByTestId("correlate-connector-or"));
3684
+ fireEvent.click(screen.getByTestId("correlate-condition-delete-1"));
3685
+ expect(screen.queryByTestId("correlate-condition-row-1")).toBeNull();
3686
+
3687
+ fireEvent.click(
3688
+ within(screen.getByTestId("correlate-filter-builder")).getByTestId(
3689
+ "security-events-correlate-button",
3690
+ ),
3691
+ );
3692
+
3693
+ await waitFor(() => {
3694
+ expect(getListMock).toHaveBeenCalledTimes(1);
3695
+ });
3696
+ expect(lastSyncedFilter()).toEqual({
3697
+ v: 1,
3698
+ j: "and",
3699
+ c: [["observable", "equals", SEARCHED_HOST]],
3700
+ });
3701
+ await waitForGraph();
3702
+ });
3703
+ });
3704
+
3705
+ describe("CorrelateGraph centre card", () => {
3706
+ test.each<[string, string, string, string]>([
3707
+ ["observable", "not-equals", "bob", "Observable is not"],
3708
+ ["message", "contains", "failed", "Message contains"],
3709
+ ["className", "equals", "Authentication", "Event Class is"],
3710
+ ["principalHost", "equals", SEARCHED_HOST, "Principal Host is"],
3711
+ ["severityName", "not-equals", "Informational", "Severity is not"],
3712
+ ])(
3713
+ "a single %s %s condition names its field and operator above the value",
3714
+ async (field: string, operator: string, value: string, eyebrow: string) => {
3715
+ const filter: CorrelationFilter = {
3716
+ conditions: [{ field: field, operator: operator, value: value }],
3717
+ connector: "and",
3718
+ } as CorrelationFilter;
3719
+ mockQueryParams["q"] = serializeCorrelationFilter(filter);
3720
+ getListMock.mockResolvedValue(listResult(standardEvents()));
3721
+
3722
+ render(<CorrelateGraph />);
3723
+ await waitForGraph();
3724
+
3725
+ const center: CorrelateFlowNode = getFlowNode("center");
3726
+ expect(center.data.eyebrow).toBe(eyebrow);
3727
+ expect(center.data.eyebrow).not.toBe("Your filter");
3728
+ expect(center.data.title).toBe(value);
3729
+ expect(center.data.tooltip).toBe(describeCorrelationFilter(filter));
3730
+ expect(center.data.label).toBe(
3731
+ describeCorrelationCondition(filter.conditions[0]!),
3732
+ );
3733
+ },
3734
+ );
3735
+
3736
+ test("only the plain observable search reads Your filter", async () => {
3737
+ await renderGraph();
3738
+
3739
+ const center: CorrelateFlowNode = getFlowNode("center");
3740
+ expect(center.data.eyebrow).toBe("Your filter");
3741
+ expect(center.data.title).toBe(SEARCHED_HOST);
3742
+ });
3743
+
3744
+ test("the field and operator in the eyebrow are translated", async () => {
3745
+ mockTranslations["Observable"] = "Observable-T";
3746
+ mockTranslations["is not"] = "is-not-T";
3747
+ mockQueryParams["q"] = serializeCorrelationFilter({
3748
+ conditions: [
3749
+ { field: "observable", operator: "not-equals", value: "bob" },
3750
+ ],
3751
+ connector: "and",
3752
+ } as CorrelationFilter);
3753
+ getListMock.mockResolvedValue(listResult(standardEvents()));
3754
+
3755
+ render(<CorrelateGraph />);
3756
+ await waitForGraph();
3757
+
3758
+ expect(getFlowNode("center").data.eyebrow).toBe("Observable-T is-not-T");
3759
+ });
3760
+ });
3761
+
3762
+ describe("CorrelateGraph edge names", () => {
3763
+ test("name both ends and the number of shared events", async () => {
3764
+ await renderGraph();
3765
+
3766
+ const centerLabel: string = describeCorrelationFilter({
3767
+ conditions: [
3768
+ { field: "observable", operator: "equals", value: SEARCHED_HOST },
3769
+ ],
3770
+ connector: "and",
3771
+ } as CorrelationFilter);
3772
+ expect(getFlowEdge("center->class:Authentication").ariaLabel).toBe(
3773
+ `${centerLabel} – Authentication: 2 events`,
3774
+ );
3775
+ expect(getFlowEdge("center->class:Process Activity").ariaLabel).toBe(
3776
+ `${centerLabel} – Process Activity: 1 event`,
3777
+ );
3778
+ expect(
3779
+ getFlowEdge("class:Authentication->observable:alice").ariaLabel,
3780
+ ).toBe("Authentication – alice: 2 events");
3781
+ expect(
3782
+ getFlowEdge("class:Process Activity->observable:bash").ariaLabel,
3783
+ ).toBe("Process Activity – bash: 1 event");
3784
+
3785
+ for (const edge of getFlowEdges()) {
3786
+ expect(edge.ariaLabel).toMatch(/^.+ – .+: \d+ events?$/);
3787
+ expect(edge.ariaLabel).not.toContain("Edge from");
3788
+ expect(edge.focusable).toBe(false);
3789
+ }
3790
+ });
3791
+
3792
+ test("describe a multi-condition centre by the whole filter", async () => {
3793
+ const filter: CorrelationFilter = twoConditionFilter("or");
3794
+ mockQueryParams["q"] = serializeCorrelationFilter(filter);
3795
+ getListMock.mockResolvedValue(listResult(standardEvents()));
3796
+
3797
+ render(<CorrelateGraph />);
3798
+ await waitForGraph();
3799
+
3800
+ expect(getFlowEdge("center->class:Network Activity").ariaLabel).toBe(
3801
+ `${describeCorrelationFilter(filter)} – Network Activity: 1 event`,
3802
+ );
3803
+ });
3804
+
3805
+ test("translate the event word", async () => {
3806
+ mockI18n.language = "xx";
3807
+ mockTranslations["event"] = "evt";
3808
+ mockTranslations["events"] = "evts";
3809
+
3810
+ await renderGraph();
3811
+
3812
+ expect(
3813
+ getFlowEdge("class:Authentication->observable:alice").ariaLabel,
3814
+ ).toBe("Authentication – alice: 2 evts");
3815
+ expect(
3816
+ getFlowEdge("class:Process Activity->observable:bash").ariaLabel,
3817
+ ).toBe("Process Activity – bash: 1 evt");
3818
+ });
3819
+ });
3820
+
3821
+ describe("CorrelateGraph node memo", () => {
3822
+ test("keeps the same nodes across unrelated renders and rebuilds on a language switch", async () => {
3823
+ await renderGraph();
3824
+ const nodesBefore: Array<FlowNode> | undefined = getFlowProps().nodes;
3825
+ const edgesBefore: Array<FlowEdge> | undefined = getFlowProps().edges;
3826
+ expect(getFlowNode("center").data.eyebrow).toBe("Your filter");
3827
+
3828
+ // Typing re-renders the page but changes nothing the graph shows.
3829
+ fireEvent.change(
3830
+ screen.getByTestId("security-events-correlate-observable"),
3831
+ { target: { value: "typed" } },
3832
+ );
3833
+ expect(getFlowProps().nodes).toBe(nodesBefore);
3834
+ expect(getFlowProps().edges).toBe(edgesBefore);
3835
+
3836
+ // New strings alone don't rebuild; the language is the memo's key.
3837
+ mockTranslations["Your filter"] = "Filter-T";
3838
+ fireEvent.change(
3839
+ screen.getByTestId("security-events-correlate-observable"),
3840
+ { target: { value: "typed again" } },
3841
+ );
3842
+ expect(getFlowProps().nodes).toBe(nodesBefore);
3843
+
3844
+ mockI18n.language = "de";
3845
+ fireEvent.change(
3846
+ screen.getByTestId("security-events-correlate-observable"),
3847
+ { target: { value: "typed once more" } },
3848
+ );
3849
+ expect(getFlowProps().nodes).not.toBe(nodesBefore);
3850
+ expect(getFlowNode("center").data.eyebrow).toBe("Filter-T");
3851
+ });
3852
+ });
3853
+
3854
+ describe("CorrelateGraph stale selection", () => {
3855
+ test("a refresh that drops the selected node clears the selection for good", async () => {
3856
+ await renderGraph();
3857
+ clickFlowNode("observable:alice");
3858
+ expect(getSelectedNodeIds()).toEqual(["observable:alice"]);
3859
+
3860
+ // alice is gone from the next result…
3861
+ getListMock.mockResolvedValueOnce(
3862
+ listResult([
3863
+ buildEvent({
3864
+ id: "d5555555-5555-4555-8555-555555555555",
3865
+ className: "Authentication",
3866
+ observables: [SEARCHED_HOST, "bash"],
3867
+ }),
3868
+ ]),
3869
+ );
3870
+ fireEvent.click(screen.getByTestId("correlate-refresh"));
3871
+ await waitFor(() => {
3872
+ expect(getListMock).toHaveBeenCalledTimes(2);
3873
+ });
3874
+ await waitForGraph();
3875
+
3876
+ expect(screen.queryByTestId("flow-node-observable:alice")).toBeNull();
3877
+ expect(screen.queryByTestId("correlate-inspector")).toBeNull();
3878
+ expect(getSelectedNodeIds()).toEqual([]);
3879
+
3880
+ // …and coming back later must not reopen the panel on its own.
3881
+ getListMock.mockResolvedValueOnce(listResult(standardEvents()));
3882
+ fireEvent.click(screen.getByTestId("correlate-refresh"));
3883
+ await waitFor(() => {
3884
+ expect(getListMock).toHaveBeenCalledTimes(3);
3885
+ });
3886
+ await waitForGraph();
3887
+
3888
+ expect(
3889
+ screen.getByTestId("flow-node-observable:alice"),
3890
+ ).toBeInTheDocument();
3891
+ expect(screen.queryByTestId("correlate-inspector")).toBeNull();
3892
+ expect(getSelectedNodeIds()).toEqual([]);
3893
+ expect(getDimmedNodeIds()).toEqual([]);
3894
+ });
3895
+
3896
+ test("a refresh that keeps the selected node keeps the selection", async () => {
3897
+ await renderGraph();
3898
+ clickFlowNode("class:Authentication");
3899
+
3900
+ fireEvent.click(screen.getByTestId("correlate-refresh"));
3901
+ await waitFor(() => {
3902
+ expect(getListMock).toHaveBeenCalledTimes(2);
3903
+ });
3904
+ await waitForGraph();
3905
+
3906
+ expect(getSelectedNodeIds()).toEqual(["class:Authentication"]);
3907
+ expect(screen.getByTestId("correlate-drilldown")).toBeInTheDocument();
3908
+ });
3909
+ });
3910
+
3911
+ describe("CorrelateGraph fitting", () => {
3912
+ beforeEach(() => {
3913
+ installFrameQueue();
3914
+ });
3915
+
3916
+ afterEach(() => {
3917
+ cleanup();
3918
+ restoreFrameQueue();
3919
+ });
3920
+
3921
+ test("hands onInit to React Flow and fits a new graph only after two frames", async () => {
3922
+ await renderGraph();
3923
+ expect(getFlowProps().onInit).toEqual(expect.any(Function));
3924
+
3925
+ // React Flow has not measured the new pane yet.
3926
+ expect(fitViewMock).not.toHaveBeenCalled();
3927
+ flushFrame();
3928
+ expect(fitViewMock).not.toHaveBeenCalled();
3929
+ flushFrame();
3930
+ expect(fitViewMock).toHaveBeenCalledTimes(1);
3931
+ expect(fitViewMock).toHaveBeenCalledWith(FIT_VIEW_OPTIONS);
3932
+
3933
+ // A successful fit stops there.
3934
+ flushFrames(3);
3935
+ expect(fitViewMock).toHaveBeenCalledTimes(1);
3936
+ expect(pendingFrames.size).toBe(0);
3937
+ });
3938
+
3939
+ test("retries on later frames until React Flow can fit", async () => {
3940
+ fitViewMock.mockReturnValueOnce(false).mockReturnValueOnce(false);
3941
+ await renderGraph();
3942
+
3943
+ flushFrames(2);
3944
+ expect(fitViewMock).toHaveBeenCalledTimes(1);
3945
+ flushFrame();
3946
+ expect(fitViewMock).toHaveBeenCalledTimes(2);
3947
+ flushFrame();
3948
+ expect(fitViewMock).toHaveBeenCalledTimes(3);
3949
+ flushFrames(3);
3950
+ expect(fitViewMock).toHaveBeenCalledTimes(3);
3951
+ });
3952
+
3953
+ test("gives up after a bounded number of retries", async () => {
3954
+ fitViewMock.mockReturnValue(false);
3955
+ await renderGraph();
3956
+
3957
+ flushFrames(40);
3958
+ // Two waiting frames, the first attempt, then twenty retries.
3959
+ expect(fitViewMock).toHaveBeenCalledTimes(21);
3960
+ expect(pendingFrames.size).toBe(0);
3961
+ });
3962
+
3963
+ test("an empty result never asks React Flow to fit", async () => {
3964
+ render(<CorrelateGraph />);
3965
+ runQuickSearch(SEARCHED_HOST);
3966
+ await waitForNoResults();
3967
+
3968
+ flushFrames(30);
3969
+ expect(fitViewMock).not.toHaveBeenCalled();
3970
+ });
3971
+
3972
+ test("a selection does not refit the graph", async () => {
3973
+ await renderGraph();
3974
+ flushFrames(2);
3975
+ expect(fitViewMock).toHaveBeenCalledTimes(1);
3976
+ fitViewMock.mockClear();
3977
+
3978
+ clickFlowNode("class:Authentication");
3979
+ fireEvent.click(screen.getByTestId("correlate-overview-observable-1"));
3980
+ flushFrames(4);
3981
+
3982
+ expect(fitViewMock).not.toHaveBeenCalled();
3983
+ });
3984
+
3985
+ test("a refresh with the same nodes still refits because the window moved", async () => {
3986
+ const currentDate: SpyInstance<() => Date> = jest.spyOn(
3987
+ OneUptimeDate,
3988
+ "getCurrentDate",
3989
+ );
3990
+ try {
3991
+ currentDate.mockReturnValue(new Date("2026-08-25T12:00:00.000Z"));
3992
+ await renderGraph();
3993
+ flushFrames(2);
3994
+ expect(fitViewMock).toHaveBeenCalledTimes(1);
3995
+ fitViewMock.mockClear();
3996
+
3997
+ currentDate.mockReturnValue(new Date("2026-08-25T12:05:00.000Z"));
3998
+ fireEvent.click(screen.getByTestId("correlate-refresh"));
3999
+ await waitFor(() => {
4000
+ expect(getListMock).toHaveBeenCalledTimes(2);
4001
+ });
4002
+ await waitForGraph();
4003
+
4004
+ expect(fitViewMock).not.toHaveBeenCalled();
4005
+ flushFrame();
4006
+ expect(fitViewMock).not.toHaveBeenCalled();
4007
+ flushFrame();
4008
+ expect(fitViewMock).toHaveBeenCalledTimes(1);
4009
+ } finally {
4010
+ currentDate.mockRestore();
4011
+ }
4012
+ });
4013
+
4014
+ test("a different graph refits after two more frames", async () => {
4015
+ await renderGraph();
4016
+ flushFrames(2);
4017
+ fitViewMock.mockClear();
4018
+
4019
+ clickFlowNode("observable:alice");
4020
+ fireEvent.click(screen.getByTestId("correlate-action-focus"));
4021
+ await waitFor(() => {
4022
+ expect(getListMock).toHaveBeenCalledTimes(2);
4023
+ });
4024
+ await waitForGraph();
4025
+
4026
+ expect(fitViewMock).not.toHaveBeenCalled();
4027
+ flushFrame();
4028
+ expect(fitViewMock).not.toHaveBeenCalled();
4029
+ flushFrame();
4030
+ expect(fitViewMock).toHaveBeenCalledTimes(1);
4031
+ expect(fitViewMock).toHaveBeenCalledWith(FIT_VIEW_OPTIONS);
4032
+ });
4033
+
4034
+ test("Fit to screen fits straight away with an animation", async () => {
4035
+ await renderGraph();
4036
+
4037
+ fireEvent.click(screen.getByTestId("correlate-fit-view"));
4038
+
4039
+ expect(fitViewMock).toHaveBeenCalledTimes(1);
4040
+ expect(fitViewMock).toHaveBeenCalledWith({
4041
+ ...FIT_VIEW_OPTIONS,
4042
+ duration: 300,
4043
+ });
4044
+ });
4045
+ });
4046
+
4047
+ describe("CorrelateGraph responsive layout", () => {
4048
+ afterEach(() => {
4049
+ cleanup();
4050
+ restoreFrameQueue();
4051
+ restoreStyleHeights();
4052
+ removeMatchMedia();
4053
+ removeResizeObserver();
4054
+ });
4055
+
4056
+ test("without matchMedia the page assumes a wide screen", async () => {
4057
+ await renderGraph();
4058
+ const defaultPositions: Map<string, Point> = getClassPositions();
4059
+ cleanup();
4060
+
4061
+ installMatchMedia({ wide: true, floating: true });
4062
+ await renderGraph();
4063
+
4064
+ expect(getClassPositions()).toEqual(defaultPositions);
4065
+ });
4066
+
4067
+ test("a narrow screen draws the class ring round instead of wide", async () => {
4068
+ installMatchMedia({ wide: true, floating: true });
4069
+ await renderGraph();
4070
+ const widePositions: Map<string, Point> = getClassPositions();
4071
+ cleanup();
4072
+
4073
+ installMatchMedia({ wide: false, floating: false });
4074
+ await renderGraph();
4075
+ const narrowPositions: Map<string, Point> = getClassPositions();
4076
+
4077
+ expect(Array.from(narrowPositions.keys()).sort()).toEqual(
4078
+ Array.from(widePositions.keys()).sort(),
4079
+ );
4080
+ let compressedCount: number = 0;
4081
+ for (const [nodeId, widePoint] of Array.from(widePositions.entries())) {
4082
+ const narrowPoint: Point = narrowPositions.get(nodeId)!;
4083
+ // Same ring height, 1 / 1.6 of the horizontal spread.
4084
+ expect(narrowPoint.y).toBeCloseTo(widePoint.y, 0);
4085
+ expect(Math.abs(narrowPoint.x - widePoint.x / 1.6)).toBeLessThanOrEqual(
4086
+ 1,
4087
+ );
4088
+ if (Math.abs(narrowPoint.x) < Math.abs(widePoint.x) - 1) {
4089
+ compressedCount += 1;
4090
+ }
4091
+ }
4092
+ // Two of the three classes sit off the vertical axis.
4093
+ expect(compressedCount).toBe(2);
4094
+ });
4095
+
4096
+ test("switching between narrow and wide re-lays out and refits the graph", async () => {
4097
+ installFrameQueue();
4098
+ installMatchMedia({ wide: true, floating: true });
4099
+ await renderGraph();
4100
+ flushFrames(2);
4101
+ expect(fitViewMock).toHaveBeenCalledTimes(1);
4102
+ const widePositions: Map<string, Point> = getClassPositions();
4103
+ fitViewMock.mockClear();
4104
+
4105
+ changeMedia({ wide: false, floating: false });
4106
+
4107
+ const narrowPositions: Map<string, Point> = getClassPositions();
4108
+ expect(narrowPositions).not.toEqual(widePositions);
4109
+ expect(fitViewMock).not.toHaveBeenCalled();
4110
+ flushFrame();
4111
+ expect(fitViewMock).not.toHaveBeenCalled();
4112
+ flushFrame();
4113
+ expect(fitViewMock).toHaveBeenCalledTimes(1);
4114
+ fitViewMock.mockClear();
4115
+
4116
+ // Crossing only the floating-panel breakpoint changes nothing drawn.
4117
+ changeMedia({ wide: false, floating: true });
4118
+ expect(getClassPositions()).toEqual(narrowPositions);
4119
+ flushFrames(3);
4120
+ expect(fitViewMock).not.toHaveBeenCalled();
4121
+
4122
+ changeMedia({ wide: true, floating: true });
4123
+ expect(getClassPositions()).toEqual(widePositions);
4124
+ flushFrames(2);
4125
+ expect(fitViewMock).toHaveBeenCalledTimes(1);
4126
+ });
4127
+
4128
+ test("on a narrow screen the canvas height follows the drawing at the canvas width", async () => {
4129
+ installFrameQueue();
4130
+ recordStyleHeights();
4131
+ installResizeObserver();
4132
+ installMatchMedia({ wide: false, floating: false });
4133
+ await renderGraph();
4134
+ flushFrames(2);
4135
+ fitViewMock.mockClear();
4136
+
4137
+ // Unmeasured, the canvas keeps the desktop sizing.
4138
+ const unmeasuredHeight: number = expectedCanvasHeight();
4139
+ expect(unmeasuredHeight).toBeGreaterThanOrEqual(420);
4140
+ expect(getCanvasHeightStyle()).toBe(`min(${unmeasuredHeight}px, 72vh)`);
4141
+
4142
+ resizeCanvas(360);
4143
+
4144
+ const narrowHeight: number = expectedCanvasHeight(360);
4145
+ expect(narrowHeight).toBeGreaterThanOrEqual(280);
4146
+ expect(narrowHeight).toBeLessThanOrEqual(520);
4147
+ expect(narrowHeight).not.toBe(unmeasuredHeight);
4148
+ expect(getCanvasHeightStyle()).toBe(`min(${narrowHeight}px, 72vh)`);
4149
+
4150
+ // A new canvas height is a new pane for React Flow: refit.
4151
+ expect(fitViewMock).not.toHaveBeenCalled();
4152
+ flushFrames(2);
4153
+ expect(fitViewMock).toHaveBeenCalledTimes(1);
4154
+
4155
+ resizeCanvas(300);
4156
+ expect(getCanvasHeightStyle()).toBe(
4157
+ `min(${expectedCanvasHeight(300)}px, 72vh)`,
4158
+ );
4159
+ });
4160
+
4161
+ test("on a wide screen the canvas width does not change its height", async () => {
4162
+ installFrameQueue();
4163
+ recordStyleHeights();
4164
+ installResizeObserver();
4165
+ installMatchMedia({ wide: true, floating: true });
4166
+ await renderGraph();
4167
+ flushFrames(2);
4168
+ fitViewMock.mockClear();
4169
+
4170
+ const wideHeight: number = expectedCanvasHeight();
4171
+ expect(getCanvasHeightStyle()).toBe(`min(${wideHeight}px, 72vh)`);
4172
+
4173
+ resizeCanvas(360);
4174
+
4175
+ expect(getCanvasHeightStyle()).toBe(`min(${wideHeight}px, 72vh)`);
4176
+ flushFrames(3);
4177
+ expect(fitViewMock).not.toHaveBeenCalled();
4178
+ });
4179
+
4180
+ test("the canvas ResizeObserver is disconnected when the graph goes away", async () => {
4181
+ installResizeObserver();
4182
+ installMatchMedia({ wide: false, floating: false });
4183
+ await renderGraph();
4184
+
4185
+ const canvas: HTMLElement = screen.getByTestId("correlate-canvas");
4186
+ const observers: Array<RecordingResizeObserver> =
4187
+ getCanvasObservers(canvas);
4188
+ expect(observers).toHaveLength(1);
4189
+ expect(observers[0]!.isDisconnected).toBe(false);
4190
+
4191
+ fireEvent.click(screen.getByTestId("correlate-filter-clear-all"));
4192
+
4193
+ expect(screen.queryByTestId("correlate-canvas")).toBeNull();
4194
+ expect(observers[0]!.isDisconnected).toBe(true);
4195
+ });
4196
+
4197
+ test("matchMedia change listeners are removed on unmount", async () => {
4198
+ installMatchMedia({ wide: true, floating: true });
4199
+ render(<CorrelateGraph />);
4200
+ expect(mediaListeners.size).toBe(2);
4201
+
4202
+ cleanup();
4203
+
4204
+ expect(mediaListeners.size).toBe(0);
4205
+ });
4206
+ });
4207
+
4208
+ describe("CorrelateGraph revealing a selection", () => {
4209
+ afterEach(() => {
4210
+ cleanup();
4211
+ restoreFrameQueue();
4212
+ removeMatchMedia();
4213
+ });
4214
+
4215
+ async function renderMeasuredCanvas(state: MediaState): Promise<HTMLElement> {
4216
+ installFrameQueue();
4217
+ installMatchMedia(state);
4218
+ await renderGraph();
4219
+ const canvas: HTMLElement = screen.getByTestId("correlate-canvas");
4220
+ setElementSize(canvas, CANVAS_WIDTH, CANVAS_HEIGHT);
4221
+ return canvas;
4222
+ }
4223
+
4224
+ const visibleRight: number = CANVAS_WIDTH - PANEL_WIDTH - REVEAL_MARGIN;
4225
+
4226
+ test.each<[string, number, number, boolean]>([
4227
+ ["in the free area", 300, 250, false],
4228
+ ["under the panel", 800, 250, true],
4229
+ ["partly under the panel", visibleRight - 98 + 1, 250, true],
4230
+ ["just clear of the panel", visibleRight - 98, 250, false],
4231
+ ["past the left edge", REVEAL_MARGIN + 98 - 1, 250, true],
4232
+ ["above the top edge", 300, REVEAL_MARGIN + 28 - 1, true],
4233
+ [
4234
+ "below the bottom edge",
4235
+ 300,
4236
+ CANVAS_HEIGHT - REVEAL_MARGIN - 28 + 1,
4237
+ true,
4238
+ ],
4239
+ ])(
4240
+ "a floating panel pans a class node %s only when it is hidden",
4241
+ async (
4242
+ _where: string,
4243
+ screenX: number,
4244
+ screenY: number,
4245
+ shouldPan: boolean,
4246
+ ) => {
4247
+ await renderMeasuredCanvas({ wide: true, floating: true });
4248
+ const nodeId: string = "class:Process Activity";
4249
+ placeNodeOnScreen(nodeId, screenX, screenY, 1);
4250
+
4251
+ clickFlowNode(nodeId);
4252
+
4253
+ expect(getSelectedNodeIds()).toEqual([nodeId]);
4254
+ if (!shouldPan) {
4255
+ expect(setCenterMock).not.toHaveBeenCalled();
4256
+ return;
4257
+ }
4258
+ const point: Point = getNodePoint(nodeId);
4259
+ expect(setCenterMock).toHaveBeenCalledTimes(1);
4260
+ expect(setCenterMock).toHaveBeenCalledWith(
4261
+ point.x + FREE_AREA_SHIFT,
4262
+ point.y,
4263
+ { zoom: 1, duration: 300 },
4264
+ );
4265
+ },
4266
+ );
4267
+
4268
+ test("the free-area shift and the zoom follow the current viewport", async () => {
4269
+ await renderMeasuredCanvas({ wide: true, floating: true });
4270
+ const nodeId: string = "class:Authentication";
4271
+ // At half zoom the card is 98px wide, so 600 + 49 is past the panel.
4272
+ placeNodeOnScreen(nodeId, 600, 250, 0.5);
4273
+
4274
+ fireEvent.click(screen.getByTestId("correlate-overview-class-1"));
4275
+
4276
+ expect(getSelectedNodeIds()).toEqual([nodeId]);
4277
+ const point: Point = getNodePoint(nodeId);
4278
+ expect(setCenterMock).toHaveBeenCalledWith(
4279
+ point.x + FREE_AREA_SHIFT / 0.5,
4280
+ point.y,
4281
+ { zoom: 0.5, duration: 300 },
4282
+ );
4283
+ });
4284
+
4285
+ test("observable nodes are measured with the observable card size", async () => {
4286
+ await renderMeasuredCanvas({ wide: true, floating: true });
4287
+ const nodeId: string = "observable:alice";
4288
+
4289
+ // An observable card is 164px wide: 82px either side of its centre.
4290
+ placeNodeOnScreen(nodeId, visibleRight - 82, 250, 1);
4291
+ clickFlowNode(nodeId);
4292
+ expect(setCenterMock).not.toHaveBeenCalled();
4293
+
4294
+ act(() => {
4295
+ getFlowProps().onPaneClick?.({} as React.MouseEvent);
4296
+ });
4297
+ placeNodeOnScreen(nodeId, visibleRight - 81, 250, 1);
4298
+ clickFlowNode(nodeId);
4299
+ expect(setCenterMock).toHaveBeenCalledTimes(1);
4300
+ });
4301
+
4302
+ test("an unmeasured canvas is never panned", async () => {
4303
+ installFrameQueue();
4304
+ await renderGraph();
4305
+ placeNodeOnScreen("class:Process Activity", 5000, 5000, 1);
4306
+
4307
+ clickFlowNode("class:Process Activity");
4308
+
4309
+ expect(setCenterMock).not.toHaveBeenCalled();
4310
+ });
4311
+
4312
+ test("a stacked panel never pans the graph", async () => {
4313
+ await renderMeasuredCanvas({ wide: true, floating: false });
4314
+ placeNodeOnScreen("class:Process Activity", 800, 250, 1);
4315
+
4316
+ clickFlowNode("class:Process Activity");
4317
+ fireEvent.click(screen.getByTestId("correlate-overview-class-1"));
4318
+ flushFrames(2);
4319
+
4320
+ expect(setCenterMock).not.toHaveBeenCalled();
4321
+ });
4322
+
4323
+ test.each<[string, number, number, boolean]>([
4324
+ ["below the viewport", 900, 1300, true],
4325
+ ["above the viewport", -400, 100, true],
4326
+ ["inside the viewport", 100, 700, false],
4327
+ ])(
4328
+ "a stacked panel %s is scrolled into view on the next frame",
4329
+ async (
4330
+ _where: string,
4331
+ top: number,
4332
+ bottom: number,
4333
+ shouldScroll: boolean,
4334
+ ) => {
4335
+ const canvas: HTMLElement = await renderMeasuredCanvas({
4336
+ wide: false,
4337
+ floating: false,
4338
+ });
4339
+ const canvasScroll: MockFunction = stubScrollIntoView(canvas);
4340
+ setElementRect(canvas, -2000, -1500);
4341
+
4342
+ fireEvent.click(screen.getByTestId("correlate-overview-class-0"));
4343
+ const panel: HTMLElement = screen.getByTestId("correlate-inspector");
4344
+ const panelScroll: MockFunction = stubScrollIntoView(panel);
4345
+ setElementRect(panel, top, bottom);
4346
+ expect(panelScroll).not.toHaveBeenCalled();
4347
+
4348
+ flushFrame();
4349
+
4350
+ if (shouldScroll) {
4351
+ expect(panelScroll).toHaveBeenCalledTimes(1);
4352
+ expect(panelScroll).toHaveBeenCalledWith({
4353
+ block: "nearest",
4354
+ behavior: "smooth",
4355
+ });
4356
+ } else {
4357
+ expect(panelScroll).not.toHaveBeenCalled();
4358
+ }
4359
+ // The canvas is not what a stacked panel scrolls to.
4360
+ expect(canvasScroll).not.toHaveBeenCalled();
4361
+ },
4362
+ );
4363
+
4364
+ test.each<[string, number, number, boolean]>([
4365
+ ["above the viewport", -600, -100, true],
4366
+ ["below the viewport", 700, 1200, true],
4367
+ ["inside the viewport", 20, 520, false],
4368
+ ])(
4369
+ "a floating panel scrolls the canvas into view when it is %s",
4370
+ async (
4371
+ _where: string,
4372
+ top: number,
4373
+ bottom: number,
4374
+ shouldScroll: boolean,
4375
+ ) => {
4376
+ const canvas: HTMLElement = await renderMeasuredCanvas({
4377
+ wide: true,
4378
+ floating: true,
4379
+ });
4380
+ const canvasScroll: MockFunction = stubScrollIntoView(canvas);
4381
+ setElementRect(canvas, top, bottom);
4382
+
4383
+ fireEvent.click(screen.getByTestId("correlate-overview-observable-0"));
4384
+ const panel: HTMLElement = screen.getByTestId("correlate-inspector");
4385
+ const panelScroll: MockFunction = stubScrollIntoView(panel);
4386
+ setElementRect(panel, -5000, -4000);
4387
+ expect(canvasScroll).not.toHaveBeenCalled();
4388
+
4389
+ flushFrame();
4390
+
4391
+ if (shouldScroll) {
4392
+ expect(canvasScroll).toHaveBeenCalledTimes(1);
4393
+ expect(canvasScroll).toHaveBeenCalledWith({
4394
+ block: "nearest",
4395
+ behavior: "smooth",
4396
+ });
4397
+ } else {
4398
+ expect(canvasScroll).not.toHaveBeenCalled();
4399
+ }
4400
+ expect(panelScroll).not.toHaveBeenCalled();
4401
+ },
4402
+ );
4403
+ });
4404
+
4405
+ describe("CorrelateGraph selection focus", () => {
4406
+ beforeEach(() => {
4407
+ installFrameQueue();
4408
+ });
4409
+
4410
+ afterEach(() => {
4411
+ cleanup();
4412
+ restoreFrameQueue();
4413
+ });
4414
+
4415
+ test("an overview row moves focus into the panel on the next frame and Close returns it", async () => {
4416
+ await renderGraph();
4417
+ const row: HTMLElement = screen.getByTestId("correlate-overview-class-0");
4418
+
4419
+ focusAndClick(row);
4420
+
4421
+ const panel: HTMLElement = screen.getByTestId("correlate-inspector");
4422
+ const focusSpy: SpyInstance<(options?: FocusOptions) => void> = jest.spyOn(
4423
+ panel,
4424
+ "focus",
4425
+ );
4426
+ // The panel only exists from this render on, so focus waits a frame.
4427
+ expect(document.activeElement).toBe(row);
4428
+
4429
+ flushFrame();
4430
+
4431
+ expect(document.activeElement).toBe(panel);
4432
+ expect(focusSpy).toHaveBeenCalledWith({ preventScroll: true });
4433
+
4434
+ fireEvent.click(screen.getByTestId("correlate-drilldown-close"));
4435
+
4436
+ expect(screen.queryByTestId("correlate-inspector")).toBeNull();
4437
+ expect(document.activeElement).toBe(row);
4438
+ });
4439
+
4440
+ test("the observable panel's dismiss button returns focus to its row", async () => {
4441
+ await renderGraph();
4442
+ const row: HTMLElement = screen.getByTestId(
4443
+ "correlate-overview-observable-1",
4444
+ );
4445
+
4446
+ focusAndClick(row);
4447
+ flushFrame();
4448
+ expect(document.activeElement).toBe(
4449
+ screen.getByTestId("correlate-inspector"),
4450
+ );
4451
+
4452
+ fireEvent.click(screen.getByTestId("correlate-action-dismiss"));
4453
+
4454
+ expect(screen.queryByTestId("correlate-inspector")).toBeNull();
4455
+ expect(document.activeElement).toBe(row);
4456
+ });
4457
+
4458
+ test("the worst-class shortcut moves focus into the panel and gets it back", async () => {
4459
+ await renderGraph();
4460
+ const shortcut: HTMLElement = screen.getByTestId(
4461
+ "correlate-stat-worst-class",
4462
+ );
4463
+
4464
+ focusAndClick(shortcut);
4465
+ flushFrame();
4466
+
4467
+ expect(getSelectedNodeIds()).toEqual(["class:Process Activity"]);
4468
+ expect(document.activeElement).toBe(
4469
+ screen.getByTestId("correlate-inspector"),
4470
+ );
4471
+
4472
+ fireEvent.click(screen.getByTestId("correlate-drilldown-close"));
4473
+ expect(document.activeElement).toBe(shortcut);
4474
+ });
4475
+
4476
+ test("a graph click leaves focus alone and forgets an older return target", async () => {
4477
+ await renderGraph();
4478
+ const row: HTMLElement = screen.getByTestId("correlate-overview-class-0");
4479
+ focusAndClick(row);
4480
+ flushFrame();
4481
+ const panel: HTMLElement = screen.getByTestId("correlate-inspector");
4482
+ expect(document.activeElement).toBe(panel);
4483
+
4484
+ const focusSpy: SpyInstance<(options?: FocusOptions) => void> = jest.spyOn(
4485
+ panel,
4486
+ "focus",
4487
+ );
4488
+ const nodeButton: HTMLElement = screen.getByTestId(
4489
+ "flow-node-observable:alice",
4490
+ );
4491
+ focusAndClick(nodeButton);
4492
+ flushFrames(2);
4493
+
4494
+ expect(getSelectedNodeIds()).toEqual(["observable:alice"]);
4495
+ expect(screen.getByTestId("correlate-inspector")).toBe(panel);
4496
+ expect(focusSpy).not.toHaveBeenCalled();
4497
+ expect(document.activeElement).toBe(nodeButton);
4498
+
4499
+ // Closing now has nowhere to send focus back to.
4500
+ fireEvent.click(screen.getByTestId("correlate-action-dismiss"));
4501
+ expect(screen.queryByTestId("correlate-inspector")).toBeNull();
4502
+ expect(document.activeElement).toBe(nodeButton);
4503
+ expect(document.activeElement).not.toBe(row);
4504
+ });
4505
+
4506
+ test("a Seen-in chip keeps the row that opened the panel as the return target", async () => {
4507
+ await renderGraph();
4508
+ // 10.0.0.5 was seen in Authentication and Network Activity.
4509
+ const row: HTMLElement = screen.getByTestId(
4510
+ "correlate-overview-observable-0",
4511
+ );
4512
+ expect(row).toHaveTextContent("10.0.0.5");
4513
+ focusAndClick(row);
4514
+ flushFrame();
4515
+ const panel: HTMLElement = screen.getByTestId("correlate-inspector");
4516
+ expect(document.activeElement).toBe(panel);
4517
+
4518
+ focusAndClick(screen.getByTestId("correlate-observable-class-1"));
4519
+ expect(getSelectedNodeIds()).toEqual(["class:Network Activity"]);
4520
+ flushFrame();
4521
+
4522
+ expect(document.activeElement).toBe(
4523
+ screen.getByTestId("correlate-inspector"),
4524
+ );
4525
+
4526
+ fireEvent.click(screen.getByTestId("correlate-drilldown-close"));
4527
+ expect(screen.queryByTestId("correlate-inspector")).toBeNull();
4528
+ expect(document.activeElement).toBe(row);
4529
+ });
4530
+
4531
+ test("a later list selection replaces the return target", async () => {
4532
+ await renderGraph();
4533
+ const firstRow: HTMLElement = screen.getByTestId(
4534
+ "correlate-overview-class-0",
4535
+ );
4536
+ const secondRow: HTMLElement = screen.getByTestId(
4537
+ "correlate-overview-observable-2",
4538
+ );
4539
+
4540
+ focusAndClick(firstRow);
4541
+ flushFrame();
4542
+ focusAndClick(secondRow);
4543
+ flushFrame();
4544
+ expect(getSelectedNodeIds()).toEqual(["observable:bash"]);
4545
+
4546
+ fireEvent.click(screen.getByTestId("correlate-action-dismiss"));
4547
+ expect(document.activeElement).toBe(secondRow);
4548
+ });
4549
+
4550
+ test("clicking the empty canvas clears the selection without moving focus", async () => {
4551
+ await renderGraph();
4552
+ const row: HTMLElement = screen.getByTestId("correlate-overview-class-0");
4553
+ focusAndClick(row);
4554
+ flushFrame();
4555
+ const panel: HTMLElement = screen.getByTestId("correlate-inspector");
4556
+ expect(document.activeElement).toBe(panel);
4557
+ setCenterMock.mockClear();
4558
+
4559
+ act(() => {
4560
+ getFlowProps().onPaneClick?.({} as React.MouseEvent);
4561
+ });
4562
+ flushFrames(2);
4563
+
4564
+ expect(screen.queryByTestId("correlate-inspector")).toBeNull();
4565
+ expect(getSelectedNodeIds()).toEqual([]);
4566
+ expect(setCenterMock).not.toHaveBeenCalled();
4567
+ expect(pendingFrames.size).toBe(0);
4568
+ // The panel was removed under focus; nothing pulled focus to the row.
4569
+ expect(document.activeElement).not.toBe(row);
4570
+ });
4571
+
4572
+ test("a pivot from the panel does not throw focus at a stale row", async () => {
4573
+ await renderGraph();
4574
+ const row: HTMLElement = screen.getByTestId("correlate-overview-class-0");
4575
+ focusAndClick(row);
4576
+ flushFrame();
4577
+
4578
+ fireEvent.click(screen.getByTestId("correlate-drilldown-filter-class"));
4579
+
4580
+ expect(screen.queryByTestId("correlate-inspector")).toBeNull();
4581
+ expect(document.activeElement).not.toBe(row);
4582
+ await waitFor(() => {
4583
+ expect(getListMock).toHaveBeenCalledTimes(2);
4584
+ });
4585
+ await waitForGraph();
4586
+ });
4587
+ });