@oneuptime/common 13.0.8 → 14.0.1

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 (514) hide show
  1. package/Models/AnalyticsModels/AuditLog.ts +23 -15
  2. package/Models/DatabaseModels/GlobalConfig.ts +24 -0
  3. package/Models/DatabaseModels/Index.ts +4 -0
  4. package/Models/DatabaseModels/Monitor.ts +3 -2
  5. package/Models/DatabaseModels/MonitorTemplate.ts +2 -1
  6. package/Models/DatabaseModels/ServiceLevelObjectiveFeed.ts +2 -0
  7. package/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.ts +522 -0
  8. package/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.ts +38 -4
  9. package/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.ts +603 -0
  10. package/Scripts/generate-service-worker.js +65 -4
  11. package/Server/API/BaseAPI.ts +26 -5
  12. package/Server/API/GlobalConfigAPI.ts +228 -462
  13. package/Server/API/ProjectOIDC.ts +25 -16
  14. package/Server/API/ProjectSSO.ts +24 -16
  15. package/Server/API/StatusPageAPI.ts +100 -42
  16. package/Server/API/TeamMemberAPI.ts +10 -1
  17. package/Server/Enterprise/EnterpriseEdition.ts +649 -0
  18. package/Server/Enterprise/EnterpriseFeature.ts +48 -0
  19. package/Server/Enterprise/EnterpriseLicenseSnapshot.ts +192 -0
  20. package/Server/Enterprise/EnterpriseServerModule.ts +233 -0
  21. package/Server/EnvironmentConfig.ts +147 -2
  22. package/Server/Infrastructure/Postgres/SchemaMigrations/1793900000000-AddEnterpriseEditionFirstSeenAt.ts +25 -0
  23. package/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.ts +248 -0
  24. package/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.ts +19 -0
  25. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  26. package/Server/Middleware/UserAuthorization.ts +38 -16
  27. package/Server/Services/AuditLogService.ts +84 -1079
  28. package/Server/Services/DatabaseService.ts +10 -0
  29. package/Server/Services/EnterpriseLicenseService.ts +38 -0
  30. package/Server/Services/GlobalConfigService.ts +80 -1
  31. package/Server/Services/Index.ts +4 -0
  32. package/Server/Services/MonitorProbeService.ts +116 -20
  33. package/Server/Services/MonitorService.ts +59 -6
  34. package/Server/Services/MonitorStatusTimelineService.ts +284 -0
  35. package/Server/Services/MonitorTemplateService.ts +26 -0
  36. package/Server/Services/ProjectService.ts +284 -0
  37. package/Server/Services/RumSessionPinService.ts +25 -6
  38. package/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.ts +385 -0
  39. package/Server/Services/ServiceLevelObjectiveLabelRuleService.ts +51 -0
  40. package/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.ts +92 -16
  41. package/Server/Services/ServiceLevelObjectiveMonitorRuleService.ts +36 -1
  42. package/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.ts +521 -0
  43. package/Server/Services/ServiceLevelObjectiveOwnerRuleService.ts +51 -0
  44. package/Server/Services/ServiceLevelObjectiveService.ts +28 -0
  45. package/Server/Services/StatusPagePrivateUserService.ts +7 -3
  46. package/Server/Services/StatusPageService.ts +78 -1
  47. package/Server/Services/TeamMemberService.ts +11 -0
  48. package/Server/Services/TeamService.ts +10 -0
  49. package/Server/Services/UserService.ts +10 -17
  50. package/Server/Types/Database/Permissions/CreatePermission.ts +15 -0
  51. package/Server/Types/Database/Permissions/EditionPermission.ts +208 -18
  52. package/Server/Types/Database/Permissions/TablePermission.ts +10 -3
  53. package/Server/Types/Database/Permissions/UpdatePermission.ts +29 -0
  54. package/Server/Types/Markdown.ts +41 -0
  55. package/Server/Types/Workflow/Components/Telegram/SendMessageToChat.ts +27 -1
  56. package/Server/Utils/AI/Toolbox/SloTools.ts +1 -0
  57. package/Server/Utils/CommunityEditionSsoReport.ts +361 -0
  58. package/Server/Utils/EditionEnforcement.ts +233 -0
  59. package/Server/Utils/FrontendEnvironment.ts +41 -2
  60. package/Server/Utils/InstanceHealth/RedisHealth.ts +12 -277
  61. package/Server/Utils/Monitor/AffectedResourceList.ts +148 -0
  62. package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +1 -1
  63. package/Server/Utils/Monitor/Criteria/EvaluateOverTime.ts +10 -1
  64. package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +1 -1
  65. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +720 -465
  66. package/Server/Utils/Monitor/MonitoringIntervalValidator.ts +116 -0
  67. package/Server/Utils/Monitor/RootCauseList.ts +139 -0
  68. package/Server/Utils/Rules/RuleRun/RuleRunRegistry.ts +28 -0
  69. package/Server/Utils/Slo/SloRulePatternValidator.ts +42 -0
  70. package/Server/Utils/Workspace/Slack/Slack.ts +423 -10
  71. package/Tests/App/Accounts/LoginPasskey.test.tsx +18 -0
  72. package/Tests/App/Accounts/LoginSsoEdition.test.tsx +616 -0
  73. package/Tests/App/AdminDashboard/AdminAuthenticationSsoEdition.test.tsx +217 -0
  74. package/Tests/App/AdminDashboard/AdminEnterprisePluginPage.test.tsx +357 -0
  75. package/Tests/App/AdminDashboard/EnterpriseLicensesShell.test.tsx +214 -0
  76. package/Tests/App/AdminDashboard/GlobalSSOEnterpriseShells.test.tsx +271 -0
  77. package/Tests/App/AdminDashboard/HealthShells.test.tsx +659 -0
  78. package/Tests/App/Dashboard/AuditLogsAndComplianceShells.test.tsx +572 -0
  79. package/Tests/App/Dashboard/EnterpriseFeatureUpgrade.test.tsx +547 -0
  80. package/Tests/App/Dashboard/EnterprisePluginPage.test.tsx +667 -0
  81. package/Tests/App/Dashboard/FeedFilterSortIntegration.test.tsx +1632 -0
  82. package/Tests/App/Dashboard/FeedOptionsLocales.test.ts +250 -0
  83. package/Tests/App/Dashboard/FeedOrdering.test.ts +279 -9
  84. package/Tests/App/Dashboard/IdentityEnterpriseShells.test.tsx +319 -0
  85. package/Tests/App/Dashboard/InvestigationFeedRefresh.test.tsx +60 -1
  86. package/Tests/App/Dashboard/InvestigationPanel.test.tsx +594 -3
  87. package/Tests/App/Dashboard/OnboardingSsoEmptyState.test.tsx +185 -0
  88. package/Tests/App/Dashboard/ProductOrientation.test.tsx +646 -0
  89. package/Tests/App/Dashboard/ResourceFeed.test.tsx +612 -2
  90. package/Tests/App/Dashboard/ResourceFeedPageWiring.test.ts +30 -0
  91. package/Tests/App/Dashboard/ResourceFeedPagesFilterSort.test.tsx +1202 -0
  92. package/Tests/App/Dashboard/ScheduledMaintenanceFeedRefresh.test.tsx +52 -1
  93. package/Tests/App/Dashboard/SecurityEventConnectionDiagnostics.test.tsx +45 -9
  94. package/Tests/App/Dashboard/SecurityEventDetailPanel.test.tsx +399 -0
  95. package/Tests/App/Dashboard/SecurityEventListRow.test.tsx +214 -0
  96. package/Tests/App/Dashboard/SecurityEventsEmptyState.test.tsx +1 -83
  97. package/Tests/App/Dashboard/SecurityEventsNavTabs.test.tsx +248 -0
  98. package/Tests/App/Dashboard/SecurityEventsViewer.test.tsx +1203 -0
  99. package/Tests/App/Dashboard/SloFeed.test.tsx +106 -1
  100. package/Tests/App/Dashboard/SloMonitorRuleCriteria.test.tsx +566 -0
  101. package/Tests/App/Dashboard/SloNavigationWiring.test.tsx +6 -4
  102. package/Tests/App/Dashboard/SloNoticeBanner.test.tsx +186 -0
  103. package/Tests/App/Dashboard/SloOverviewKpiStrip.test.tsx +388 -44
  104. package/Tests/App/Dashboard/SloOverviewLayout.test.tsx +613 -0
  105. package/Tests/App/Dashboard/UserEmailCallSites.test.tsx +11 -91
  106. package/Tests/App/StatusPage/StatusPageSsoEmptyState.test.tsx +319 -0
  107. package/Tests/Helpers/RefreshAwareApiScan.ts +757 -0
  108. package/Tests/Helpers/TenantScopedRequestScan.ts +515 -0
  109. package/Tests/Models/DatabaseModels/ResourceFeedModels.test.ts +0 -8
  110. package/Tests/Models/DatabaseModels/SloLabelAndOwnerRuleModels.test.ts +404 -0
  111. package/Tests/Models/DatabaseModels/SloOverhaulModels.test.ts +17 -1
  112. package/Tests/Models/DatabaseModels/StaticRuleCriteriaInheritance.test.ts +4 -2
  113. package/Tests/Scripts/GenerateServiceWorker.test.ts +931 -0
  114. package/Tests/Server/API/BaseAPICreateItemDoesNotMutateModel.test.ts +130 -0
  115. package/Tests/Server/API/GlobalConfigLicense.test.ts +742 -497
  116. package/Tests/Server/API/OnCallReadinessAPI.test.ts +35 -909
  117. package/Tests/Server/API/SsoProviderListingsEdition.test.ts +432 -0
  118. package/Tests/Server/API/TeamMemberLeaveScimLockEdition.test.ts +252 -0
  119. package/Tests/Server/Enterprise/EnterpriseEdition.test.ts +824 -0
  120. package/Tests/Server/Enterprise/EnterpriseFeatureActive.test.ts +643 -0
  121. package/Tests/Server/Enterprise/EnterpriseLicenseSnapshot.test.ts +294 -0
  122. package/Tests/Server/Enterprise/EnterpriseModelFeatureMap.test.ts +93 -0
  123. package/Tests/Server/Enterprise/EnterpriseOnlyModulesLiveInEe.test.ts +582 -0
  124. package/Tests/Server/Enterprise/EnterpriseServerModule.test.ts +247 -0
  125. package/Tests/Server/Enterprise/FakeEnterpriseModule.ts +502 -0
  126. package/Tests/Server/Enterprise/TestBillingFlag.ts +70 -0
  127. package/Tests/Server/EnvironmentConfigEnterpriseEdition.test.ts +314 -0
  128. package/Tests/Server/Infrastructure/ContainerBootConfiguration.test.ts +289 -0
  129. package/Tests/Server/Infrastructure/E2EClickHouseFixtureAccess.test.ts +5 -1
  130. package/Tests/Server/Infrastructure/Postgres/AddConfigurableRuleCriteriaMigration.test.ts +3 -0
  131. package/Tests/Server/Infrastructure/Postgres/AddSloLabelAndOwnerRulesMigration.test.ts +407 -0
  132. package/Tests/Server/Infrastructure/Postgres/AddSloMonitorRuleMonitorTypeMigration.test.ts +171 -0
  133. package/Tests/Server/Infrastructure/Postgres/SloProductOverhaulMigration.test.ts +88 -4
  134. package/Tests/Server/Middleware/UserAuthorization.test.ts +37 -2
  135. package/Tests/Server/Middleware/UserAuthorizationEditionEnforcement.test.ts +516 -0
  136. package/Tests/Server/Services/AuditLogService.test.ts +460 -990
  137. package/Tests/Server/Services/CreateAndReadResponsesKeepIds.test.ts +263 -0
  138. package/Tests/Server/Services/DatabaseServiceRuleCriteriaValidation.test.ts +104 -0
  139. package/Tests/Server/Services/EnterpriseLicenseServiceKeyGeneration.test.ts +151 -0
  140. package/Tests/Server/Services/GlobalConfigServiceLicenseColumns.test.ts +357 -0
  141. package/Tests/Server/Services/MonitorDestinationInfoIPv6.test.ts +101 -0
  142. package/Tests/Server/Services/MonitorProbeServiceIntervalScheduling.test.ts +333 -0
  143. package/Tests/Server/Services/MonitorStatusTimelineDailyAggregates.test.ts +303 -0
  144. package/Tests/Server/Services/ProjectServiceAuditLogLicense.test.ts +701 -0
  145. package/Tests/Server/Services/ProjectServiceAuditLogSettingsCache.test.ts +31 -35
  146. package/Tests/Server/Services/RuleRun/LabelRuleEnginesGroupBRun.test.ts +48 -1
  147. package/Tests/Server/Services/RuleRun/OwnerRuleEnginesGroupBRun.test.ts +30 -0
  148. package/Tests/Server/Services/ScimPushGroupsTeamLocksEdition.test.ts +431 -0
  149. package/Tests/Server/Services/ServiceLevelObjectiveLabelAndOwnerRuleEngines.test.ts +1170 -0
  150. package/Tests/Server/Services/ServiceLevelObjectiveLabelAndOwnerRuleServices.test.ts +172 -0
  151. package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.test.ts +323 -0
  152. package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleService.test.ts +115 -0
  153. package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleTriggers.test.ts +21 -2
  154. package/Tests/Server/Services/ServiceLevelObjectiveMonitorTypePostgres.test.ts +480 -0
  155. package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +162 -0
  156. package/Tests/Server/Services/SsoRequirementReadMasking.test.ts +419 -0
  157. package/Tests/Server/Services/SsoRequirementWriteGuard.test.ts +769 -0
  158. package/Tests/Server/Services/StaticRuleCriteriaRuntimeCoverage.test.ts +45 -6
  159. package/Tests/Server/Services/StatusPagePrivateUserInvitationPolicy.test.ts +94 -0
  160. package/Tests/Server/Services/UserAuthenticationService.test.ts +35 -0
  161. package/Tests/Server/Services/UserServiceSeatLimit.test.ts +94 -56
  162. package/Tests/Server/Types/Database/FeedEventTypeFilterQuery.test.ts +745 -0
  163. package/Tests/Server/Types/Database/Permissions/EditionPermission.test.ts +1823 -0
  164. package/Tests/Server/Types/Markdown.test.ts +183 -1
  165. package/Tests/Server/Types/Workflow/Components/ApiComponentSsrf.test.ts +49 -0
  166. package/Tests/Server/Types/Workflow/Components/TelegramSendMessageToChat.test.ts +331 -0
  167. package/Tests/Server/Utils/AI/SloTools.test.ts +10 -0
  168. package/Tests/Server/Utils/CommunityEditionSsoReport.test.ts +565 -0
  169. package/Tests/Server/Utils/EditionEnforcement.test.ts +303 -0
  170. package/Tests/Server/Utils/FrontendEnvironment.test.ts +367 -0
  171. package/Tests/Server/Utils/InstanceHealth/RedisHealth.test.ts +38 -418
  172. package/Tests/Server/Utils/Monitor/AffectedResourceList.test.ts +518 -0
  173. package/Tests/Server/Utils/Monitor/Criteria/EvaluateOverTime.test.ts +24 -4
  174. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaAnomalyUnits.test.ts +1 -1
  175. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +1339 -96
  176. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorBreachingSamples.test.ts +1101 -0
  177. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorMetricUnits.test.ts +106 -80
  178. package/Tests/Server/Utils/Monitor/MonitoringIntervalValidator.test.ts +188 -0
  179. package/Tests/Server/Utils/Monitor/RootCauseList.test.ts +252 -0
  180. package/Tests/Server/Utils/Rules/RuleRun/RuleRunPermission.test.ts +154 -0
  181. package/Tests/Server/Utils/SSRFProtection.test.ts +111 -0
  182. package/Tests/Server/Utils/Slo/SloRulePatternValidator.test.ts +67 -0
  183. package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +213 -0
  184. package/Tests/Server/Utils/Workspace/Slack/SlackMarkdownSectionSplit.test.ts +1209 -0
  185. package/Tests/Types/API/HostnameFromAuthority.test.ts +20 -10
  186. package/Tests/Types/API/HostnameIPv6.test.ts +149 -0
  187. package/Tests/Types/API/Route.test.ts +56 -0
  188. package/Tests/Types/API/URLSchemeLikePath.test.ts +304 -0
  189. package/Tests/Types/Database/CanonicalModelInstance.test.ts +190 -0
  190. package/Tests/UI/Components/EditionLabelLapseCopy.ts +104 -0
  191. package/Tests/UI/Components/EditionLabelLicenseManagerWiring.test.tsx +254 -0
  192. package/Tests/UI/Components/EditionLabelLicenseRefresh.test.tsx +84 -178
  193. package/Tests/UI/Components/EditionLabelLicenseStatus.test.tsx +1586 -0
  194. package/Tests/UI/Components/EditionLabelLicenseWritesGuard.test.tsx +157 -0
  195. package/Tests/UI/Components/FeedOptions.test.ts +1404 -0
  196. package/Tests/UI/Components/FeedOptionsButton.test.tsx +2603 -0
  197. package/Tests/UI/Components/Graphs/DayUptimeGraph.test.tsx +49 -2
  198. package/Tests/UI/Components/Icon.test.tsx +26 -0
  199. package/Tests/UI/Components/MarkdownConverters.test.ts +684 -0
  200. package/Tests/UI/Components/ModelListEmptyState.test.tsx +289 -0
  201. package/Tests/UI/Components/RuleCriteria/RuleCriteriaModelTable.test.tsx +3 -3
  202. package/Tests/UI/Components/TelemetryViewer/TelemetryViewerEmptyContent.test.tsx +131 -0
  203. package/Tests/UI/Components/useFeedItemsViewKey.test.tsx +1017 -0
  204. package/Tests/UI/Components/useFeedOptions.test.tsx +1107 -0
  205. package/Tests/UI/EsbuildEnterpriseAlias.test.ts +1127 -0
  206. package/Tests/Utils/HostAddressUtil.test.ts +138 -0
  207. package/Tests/Utils/LabelRuleImportExport.test.ts +102 -0
  208. package/Tests/Utils/Monitor/MonitorDestinationUtil.test.ts +313 -0
  209. package/Tests/Utils/Monitor/MonitoringIntervalUtil.test.ts +304 -0
  210. package/Tests/Utils/Slo/SloDuration.test.ts +115 -24
  211. package/Tests/Utils/Slo/SloHealth.test.ts +93 -10
  212. package/Tests/Utils/Slo/SloMonitorRuleCriteria.test.ts +109 -0
  213. package/Tests/Utils/Slo/SloOverviewText.test.ts +64 -0
  214. package/Tests/Utils/StatusPage/UptimeDailyAggregateUtil.test.ts +248 -0
  215. package/Types/API/Hostname.ts +63 -16
  216. package/Types/API/Route.ts +19 -2
  217. package/Types/API/URL.ts +26 -2
  218. package/Types/Database/AccessControl/ColumnAccessControl.ts +19 -7
  219. package/Types/Database/AccessControl/ColumnBillingAccessControl.ts +19 -7
  220. package/Types/Database/AccessControl/OwnerOnlyColumn.ts +15 -6
  221. package/Types/Database/CanonicalModelInstance.ts +102 -0
  222. package/Types/Database/TableColumn.ts +20 -6
  223. package/Types/EnterpriseLicense/EnterpriseLicensePeriods.ts +29 -0
  224. package/Types/IP/IPv6.ts +6 -1
  225. package/Types/Monitor/MetricMonitor/MetricCriteriaContext.ts +6 -5
  226. package/Types/Monitor/MetricMonitor/MetricMonitorResponse.ts +9 -0
  227. package/Types/Monitor/MonitorStep.ts +6 -1
  228. package/Types/Monitor/ProxmoxAlertTemplates.ts +1 -1
  229. package/Types/Permission.ts +90 -0
  230. package/Types/Rules/RuleCriteriaFieldRegistry.ts +11 -0
  231. package/Types/Rules/RuleRun.ts +4 -0
  232. package/Types/StatusPage/UptimeDailyAggregate.ts +110 -0
  233. package/UI/Components/EditionLabel/EditionLabel.tsx +796 -1195
  234. package/UI/Components/EditionLabel/LicenseManager.ts +119 -0
  235. package/UI/Components/Feed/FeedOptions.ts +427 -0
  236. package/UI/Components/Feed/FeedOptionsButton.tsx +692 -0
  237. package/UI/Components/Feed/useFeedItems.ts +28 -9
  238. package/UI/Components/Feed/useFeedOptions.ts +225 -0
  239. package/UI/Components/Graphs/DayUptimeGraph.tsx +123 -15
  240. package/UI/Components/Icon/Icon.tsx +1 -1
  241. package/UI/Components/Markdown.tsx/MarkdownConverters.ts +0 -0
  242. package/UI/Components/ModelList/ModelList.tsx +11 -1
  243. package/UI/Components/MonitorGraphs/Uptime.tsx +69 -0
  244. package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +28 -12
  245. package/UI/esbuild-enterprise.js +263 -0
  246. package/Utils/EnterpriseLicense/EnterpriseLicenseUsage.ts +7 -2
  247. package/Utils/HostAddressUtil.ts +94 -0
  248. package/Utils/LabelRuleImportExport.ts +27 -9
  249. package/Utils/Monitor/MetricValueFormatter.ts +2 -2
  250. package/Utils/Monitor/MonitorDestinationUtil.ts +157 -0
  251. package/Utils/Monitor/MonitoringIntervalUtil.ts +436 -0
  252. package/Utils/Rules/MonitorTypeRuleCriteria.ts +31 -0
  253. package/Utils/Slo/SloDuration.ts +22 -6
  254. package/Utils/Slo/SloHealth.ts +8 -7
  255. package/Utils/Slo/SloMonitorRuleCriteria.ts +33 -1
  256. package/Utils/Slo/SloOverviewText.ts +33 -0
  257. package/Utils/Slo/SloProjection.ts +4 -3
  258. package/Utils/StatusPage/UptimeDailyAggregateUtil.ts +192 -0
  259. package/build/dist/Models/AnalyticsModels/AuditLog.js +23 -15
  260. package/build/dist/Models/AnalyticsModels/AuditLog.js.map +1 -1
  261. package/build/dist/Models/DatabaseModels/GlobalConfig.js +25 -0
  262. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  263. package/build/dist/Models/DatabaseModels/Index.js +4 -0
  264. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  265. package/build/dist/Models/DatabaseModels/Monitor.js +2 -2
  266. package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
  267. package/build/dist/Models/DatabaseModels/MonitorTemplate.js +2 -1
  268. package/build/dist/Models/DatabaseModels/MonitorTemplate.js.map +1 -1
  269. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveFeed.js +2 -0
  270. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveFeed.js.map +1 -1
  271. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.js +530 -0
  272. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.js.map +1 -0
  273. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.js +39 -4
  274. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.js.map +1 -1
  275. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.js +612 -0
  276. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.js.map +1 -0
  277. package/build/dist/Server/API/BaseAPI.js +26 -5
  278. package/build/dist/Server/API/BaseAPI.js.map +1 -1
  279. package/build/dist/Server/API/GlobalConfigAPI.js +148 -344
  280. package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
  281. package/build/dist/Server/API/ProjectOIDC.js +24 -16
  282. package/build/dist/Server/API/ProjectOIDC.js.map +1 -1
  283. package/build/dist/Server/API/ProjectSSO.js +24 -16
  284. package/build/dist/Server/API/ProjectSSO.js.map +1 -1
  285. package/build/dist/Server/API/StatusPageAPI.js +93 -43
  286. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  287. package/build/dist/Server/API/TeamMemberAPI.js +8 -1
  288. package/build/dist/Server/API/TeamMemberAPI.js.map +1 -1
  289. package/build/dist/Server/Enterprise/EnterpriseEdition.js +466 -0
  290. package/build/dist/Server/Enterprise/EnterpriseEdition.js.map +1 -0
  291. package/build/dist/Server/Enterprise/EnterpriseFeature.js +39 -0
  292. package/build/dist/Server/Enterprise/EnterpriseFeature.js.map +1 -0
  293. package/build/dist/Server/Enterprise/EnterpriseLicenseSnapshot.js +70 -0
  294. package/build/dist/Server/Enterprise/EnterpriseLicenseSnapshot.js.map +1 -0
  295. package/build/dist/Server/Enterprise/EnterpriseServerModule.js +84 -0
  296. package/build/dist/Server/Enterprise/EnterpriseServerModule.js.map +1 -0
  297. package/build/dist/Server/EnvironmentConfig.js +84 -2
  298. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  299. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1793900000000-AddEnterpriseEditionFirstSeenAt.js +18 -0
  300. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1793900000000-AddEnterpriseEditionFirstSeenAt.js.map +1 -0
  301. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.js +93 -0
  302. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.js.map +1 -0
  303. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.js +12 -0
  304. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.js.map +1 -0
  305. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  306. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  307. package/build/dist/Server/Middleware/UserAuthorization.js +30 -11
  308. package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
  309. package/build/dist/Server/Services/AuditLogService.js +71 -740
  310. package/build/dist/Server/Services/AuditLogService.js.map +1 -1
  311. package/build/dist/Server/Services/DatabaseService.js +7 -0
  312. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  313. package/build/dist/Server/Services/EnterpriseLicenseService.js +34 -0
  314. package/build/dist/Server/Services/EnterpriseLicenseService.js.map +1 -1
  315. package/build/dist/Server/Services/GlobalConfigService.js +63 -0
  316. package/build/dist/Server/Services/GlobalConfigService.js.map +1 -1
  317. package/build/dist/Server/Services/Index.js +4 -0
  318. package/build/dist/Server/Services/Index.js.map +1 -1
  319. package/build/dist/Server/Services/MonitorProbeService.js +90 -17
  320. package/build/dist/Server/Services/MonitorProbeService.js.map +1 -1
  321. package/build/dist/Server/Services/MonitorService.js +48 -6
  322. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  323. package/build/dist/Server/Services/MonitorStatusTimelineService.js +242 -0
  324. package/build/dist/Server/Services/MonitorStatusTimelineService.js.map +1 -1
  325. package/build/dist/Server/Services/MonitorTemplateService.js +16 -0
  326. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  327. package/build/dist/Server/Services/ProjectService.js +214 -0
  328. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  329. package/build/dist/Server/Services/RumSessionPinService.js +24 -6
  330. package/build/dist/Server/Services/RumSessionPinService.js.map +1 -1
  331. package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.js +295 -0
  332. package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.js.map +1 -0
  333. package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleService.js +56 -0
  334. package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleService.js.map +1 -0
  335. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js +59 -14
  336. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js.map +1 -1
  337. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleService.js +22 -1
  338. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleService.js.map +1 -1
  339. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.js +400 -0
  340. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.js.map +1 -0
  341. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleService.js +56 -0
  342. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleService.js.map +1 -0
  343. package/build/dist/Server/Services/ServiceLevelObjectiveService.js +21 -0
  344. package/build/dist/Server/Services/ServiceLevelObjectiveService.js.map +1 -1
  345. package/build/dist/Server/Services/StatusPagePrivateUserService.js +7 -3
  346. package/build/dist/Server/Services/StatusPagePrivateUserService.js.map +1 -1
  347. package/build/dist/Server/Services/StatusPageService.js +71 -0
  348. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  349. package/build/dist/Server/Services/TeamMemberService.js +10 -0
  350. package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
  351. package/build/dist/Server/Services/TeamService.js +9 -0
  352. package/build/dist/Server/Services/TeamService.js.map +1 -1
  353. package/build/dist/Server/Services/UserService.js +10 -16
  354. package/build/dist/Server/Services/UserService.js.map +1 -1
  355. package/build/dist/Server/Types/Database/Permissions/CreatePermission.js +10 -0
  356. package/build/dist/Server/Types/Database/Permissions/CreatePermission.js.map +1 -1
  357. package/build/dist/Server/Types/Database/Permissions/EditionPermission.js +154 -14
  358. package/build/dist/Server/Types/Database/Permissions/EditionPermission.js.map +1 -1
  359. package/build/dist/Server/Types/Database/Permissions/TablePermission.js +10 -3
  360. package/build/dist/Server/Types/Database/Permissions/TablePermission.js.map +1 -1
  361. package/build/dist/Server/Types/Database/Permissions/UpdatePermission.js +17 -0
  362. package/build/dist/Server/Types/Database/Permissions/UpdatePermission.js.map +1 -1
  363. package/build/dist/Server/Types/Markdown.js +31 -0
  364. package/build/dist/Server/Types/Markdown.js.map +1 -1
  365. package/build/dist/Server/Types/Workflow/Components/Telegram/SendMessageToChat.js +20 -1
  366. package/build/dist/Server/Types/Workflow/Components/Telegram/SendMessageToChat.js.map +1 -1
  367. package/build/dist/Server/Utils/AI/Toolbox/SloTools.js +1 -0
  368. package/build/dist/Server/Utils/AI/Toolbox/SloTools.js.map +1 -1
  369. package/build/dist/Server/Utils/CommunityEditionSsoReport.js +223 -0
  370. package/build/dist/Server/Utils/CommunityEditionSsoReport.js.map +1 -0
  371. package/build/dist/Server/Utils/EditionEnforcement.js +187 -0
  372. package/build/dist/Server/Utils/EditionEnforcement.js.map +1 -0
  373. package/build/dist/Server/Utils/FrontendEnvironment.js +31 -2
  374. package/build/dist/Server/Utils/FrontendEnvironment.js.map +1 -1
  375. package/build/dist/Server/Utils/InstanceHealth/RedisHealth.js +6 -193
  376. package/build/dist/Server/Utils/InstanceHealth/RedisHealth.js.map +1 -1
  377. package/build/dist/Server/Utils/Monitor/AffectedResourceList.js +67 -0
  378. package/build/dist/Server/Utils/Monitor/AffectedResourceList.js.map +1 -0
  379. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +1 -1
  380. package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js +8 -1
  381. package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js.map +1 -1
  382. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +1 -1
  383. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +585 -367
  384. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  385. package/build/dist/Server/Utils/Monitor/MonitoringIntervalValidator.js +88 -0
  386. package/build/dist/Server/Utils/Monitor/MonitoringIntervalValidator.js.map +1 -0
  387. package/build/dist/Server/Utils/Monitor/RootCauseList.js +101 -0
  388. package/build/dist/Server/Utils/Monitor/RootCauseList.js.map +1 -0
  389. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js +28 -0
  390. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js.map +1 -1
  391. package/build/dist/Server/Utils/Slo/SloRulePatternValidator.js +28 -0
  392. package/build/dist/Server/Utils/Slo/SloRulePatternValidator.js.map +1 -0
  393. package/build/dist/Server/Utils/Workspace/Slack/Slack.js +331 -11
  394. package/build/dist/Server/Utils/Workspace/Slack/Slack.js.map +1 -1
  395. package/build/dist/Types/API/Hostname.js +59 -11
  396. package/build/dist/Types/API/Hostname.js.map +1 -1
  397. package/build/dist/Types/API/Route.js +18 -2
  398. package/build/dist/Types/API/Route.js.map +1 -1
  399. package/build/dist/Types/API/URL.js +23 -2
  400. package/build/dist/Types/API/URL.js.map +1 -1
  401. package/build/dist/Types/Database/AccessControl/ColumnAccessControl.js +19 -7
  402. package/build/dist/Types/Database/AccessControl/ColumnAccessControl.js.map +1 -1
  403. package/build/dist/Types/Database/AccessControl/ColumnBillingAccessControl.js +19 -7
  404. package/build/dist/Types/Database/AccessControl/ColumnBillingAccessControl.js.map +1 -1
  405. package/build/dist/Types/Database/AccessControl/OwnerOnlyColumn.js +13 -6
  406. package/build/dist/Types/Database/AccessControl/OwnerOnlyColumn.js.map +1 -1
  407. package/build/dist/Types/Database/CanonicalModelInstance.js +88 -0
  408. package/build/dist/Types/Database/CanonicalModelInstance.js.map +1 -0
  409. package/build/dist/Types/Database/TableColumn.js +19 -6
  410. package/build/dist/Types/Database/TableColumn.js.map +1 -1
  411. package/build/dist/Types/EnterpriseLicense/EnterpriseLicensePeriods.js +28 -0
  412. package/build/dist/Types/EnterpriseLicense/EnterpriseLicensePeriods.js.map +1 -0
  413. package/build/dist/Types/IP/IPv6.js +6 -1
  414. package/build/dist/Types/IP/IPv6.js.map +1 -1
  415. package/build/dist/Types/Monitor/MonitorStep.js +5 -0
  416. package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
  417. package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js +1 -1
  418. package/build/dist/Types/Permission.js +84 -0
  419. package/build/dist/Types/Permission.js.map +1 -1
  420. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js +11 -0
  421. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js.map +1 -1
  422. package/build/dist/Types/Rules/RuleRun.js +4 -0
  423. package/build/dist/Types/Rules/RuleRun.js.map +1 -1
  424. package/build/dist/Types/StatusPage/UptimeDailyAggregate.js +32 -0
  425. package/build/dist/Types/StatusPage/UptimeDailyAggregate.js.map +1 -0
  426. package/build/dist/UI/Components/EditionLabel/EditionLabel.js +414 -585
  427. package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
  428. package/build/dist/UI/Components/EditionLabel/LicenseManager.js +2 -0
  429. package/build/dist/UI/Components/EditionLabel/LicenseManager.js.map +1 -0
  430. package/build/dist/UI/Components/Feed/FeedOptions.js +235 -0
  431. package/build/dist/UI/Components/Feed/FeedOptions.js.map +1 -0
  432. package/build/dist/UI/Components/Feed/FeedOptionsButton.js +346 -0
  433. package/build/dist/UI/Components/Feed/FeedOptionsButton.js.map +1 -0
  434. package/build/dist/UI/Components/Feed/useFeedItems.js +17 -7
  435. package/build/dist/UI/Components/Feed/useFeedItems.js.map +1 -1
  436. package/build/dist/UI/Components/Feed/useFeedOptions.js +130 -0
  437. package/build/dist/UI/Components/Feed/useFeedOptions.js.map +1 -0
  438. package/build/dist/UI/Components/Graphs/DayUptimeGraph.js +90 -17
  439. package/build/dist/UI/Components/Graphs/DayUptimeGraph.js.map +1 -1
  440. package/build/dist/UI/Components/Icon/Icon.js +1 -1
  441. package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
  442. package/build/dist/UI/Components/Markdown.tsx/MarkdownConverters.js +0 -0
  443. package/build/dist/UI/Components/Markdown.tsx/MarkdownConverters.js.map +1 -1
  444. package/build/dist/UI/Components/ModelList/ModelList.js +4 -1
  445. package/build/dist/UI/Components/ModelList/ModelList.js.map +1 -1
  446. package/build/dist/UI/Components/MonitorGraphs/Uptime.js +42 -1
  447. package/build/dist/UI/Components/MonitorGraphs/Uptime.js.map +1 -1
  448. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +2 -2
  449. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
  450. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseUsage.js +6 -2
  451. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseUsage.js.map +1 -1
  452. package/build/dist/Utils/HostAddressUtil.js +80 -0
  453. package/build/dist/Utils/HostAddressUtil.js.map +1 -0
  454. package/build/dist/Utils/LabelRuleImportExport.js +22 -6
  455. package/build/dist/Utils/LabelRuleImportExport.js.map +1 -1
  456. package/build/dist/Utils/Monitor/MetricValueFormatter.js +2 -2
  457. package/build/dist/Utils/Monitor/MonitorDestinationUtil.js +127 -0
  458. package/build/dist/Utils/Monitor/MonitorDestinationUtil.js.map +1 -0
  459. package/build/dist/Utils/Monitor/MonitoringIntervalUtil.js +374 -0
  460. package/build/dist/Utils/Monitor/MonitoringIntervalUtil.js.map +1 -0
  461. package/build/dist/Utils/Rules/MonitorTypeRuleCriteria.js +18 -0
  462. package/build/dist/Utils/Rules/MonitorTypeRuleCriteria.js.map +1 -0
  463. package/build/dist/Utils/Slo/SloDuration.js +13 -5
  464. package/build/dist/Utils/Slo/SloDuration.js.map +1 -1
  465. package/build/dist/Utils/Slo/SloHealth.js +8 -7
  466. package/build/dist/Utils/Slo/SloHealth.js.map +1 -1
  467. package/build/dist/Utils/Slo/SloMonitorRuleCriteria.js +16 -1
  468. package/build/dist/Utils/Slo/SloMonitorRuleCriteria.js.map +1 -1
  469. package/build/dist/Utils/Slo/SloOverviewText.js +16 -0
  470. package/build/dist/Utils/Slo/SloOverviewText.js.map +1 -1
  471. package/build/dist/Utils/Slo/SloProjection.js.map +1 -1
  472. package/build/dist/Utils/StatusPage/UptimeDailyAggregateUtil.js +148 -0
  473. package/build/dist/Utils/StatusPage/UptimeDailyAggregateUtil.js.map +1 -0
  474. package/jest.config.json +3 -1
  475. package/package.json +1 -1
  476. package/test-setup.sh +103 -34
  477. package/tsconfig.json +13 -0
  478. package/Server/API/EnterpriseLicenseAPI.ts +0 -925
  479. package/Server/API/TeamComplianceAPI.ts +0 -158
  480. package/Server/Middleware/SCIMAuthorization.ts +0 -141
  481. package/Server/Services/TeamComplianceService.ts +0 -733
  482. package/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.ts +0 -186
  483. package/Server/Utils/InstanceHealth/PostgresHealth.ts +0 -286
  484. package/Tests/App/AdminDashboard/EnterpriseLicenseInstanceActivity.test.tsx +0 -451
  485. package/Tests/App/Dashboard/AuditLogsTable.test.tsx +0 -598
  486. package/Tests/App/Dashboard/SecurityEventsExplorer.test.tsx +0 -761
  487. package/Tests/App/Dashboard/SecurityEventsSideMenu.test.tsx +0 -247
  488. package/Tests/App/Dashboard/SecurityEventsTableColumns.test.tsx +0 -382
  489. package/Tests/App/Dashboard/SecurityEventsVolumeChart.test.tsx +0 -321
  490. package/Tests/Server/API/EnterpriseLicenseReportUserCount.test.ts +0 -1851
  491. package/Tests/Server/Middleware/SCIMAuthorization.test.ts +0 -525
  492. package/Tests/Server/Services/TeamComplianceServiceBehaviour.test.ts +0 -1845
  493. package/Tests/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.test.ts +0 -495
  494. package/Tests/Server/Utils/InstanceHealth/PostgresHealth.test.ts +0 -337
  495. package/Tests/Utils/EnterpriseLicenseSeats.test.ts +0 -576
  496. package/Tests/Utils/EnterpriseLicenseSync.test.ts +0 -568
  497. package/Utils/EnterpriseLicense/EnterpriseLicenseSeats.ts +0 -287
  498. package/Utils/EnterpriseLicense/EnterpriseLicenseSync.ts +0 -244
  499. package/build/dist/Server/API/EnterpriseLicenseAPI.js +0 -646
  500. package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +0 -1
  501. package/build/dist/Server/API/TeamComplianceAPI.js +0 -111
  502. package/build/dist/Server/API/TeamComplianceAPI.js.map +0 -1
  503. package/build/dist/Server/Middleware/SCIMAuthorization.js +0 -124
  504. package/build/dist/Server/Middleware/SCIMAuthorization.js.map +0 -1
  505. package/build/dist/Server/Services/TeamComplianceService.js +0 -537
  506. package/build/dist/Server/Services/TeamComplianceService.js.map +0 -1
  507. package/build/dist/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.js +0 -145
  508. package/build/dist/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.js.map +0 -1
  509. package/build/dist/Server/Utils/InstanceHealth/PostgresHealth.js +0 -194
  510. package/build/dist/Server/Utils/InstanceHealth/PostgresHealth.js.map +0 -1
  511. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSeats.js +0 -168
  512. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSeats.js.map +0 -1
  513. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js +0 -174
  514. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js.map +0 -1
@@ -0,0 +1,1107 @@
1
+ /*
2
+ * jest itself is the global one: @jest/globals types spyOn(Storage.prototype,
3
+ * ...) as never, because Storage has a string index signature.
4
+ */
5
+ import { afterEach, describe, expect, test } from "@jest/globals";
6
+ import {
7
+ act,
8
+ cleanup,
9
+ renderHook,
10
+ RenderHookResult,
11
+ } from "@testing-library/react";
12
+ import SortOrder from "../../../Types/BaseDatabase/SortOrder";
13
+ import IconProp from "../../../Types/Icon/IconProp";
14
+ import {
15
+ DEFAULT_FEED_OPTIONS,
16
+ FeedEventTypeOption,
17
+ FeedOptions,
18
+ getFeedOptionsKey,
19
+ } from "../../../UI/Components/Feed/FeedOptions";
20
+ import useFeedOptions, {
21
+ getSortOrderStorageKey,
22
+ UseFeedOptionsProps,
23
+ UseFeedOptionsResult,
24
+ } from "../../../UI/Components/Feed/useFeedOptions";
25
+
26
+ /*
27
+ * A stand-in for a feed model's event type enum. It is declared out of label
28
+ * order on purpose, so the tests can tell the feed's own order (what the
29
+ * filter is sent in) apart from label order (what the checklist shows).
30
+ */
31
+ enum TestFeedEventType {
32
+ StateChanged = "StateChanged",
33
+ OwnerUserAdded = "OwnerUserAdded",
34
+ IncidentCreated = "IncidentCreated",
35
+ AutoRemediation = "AutoRemediation",
36
+ }
37
+
38
+ const TEST_EVENT_TYPES: Array<string> = Object.values(TestFeedEventType);
39
+
40
+ const TEST_EVENT_TYPE_ICONS: Record<TestFeedEventType, IconProp> = {
41
+ [TestFeedEventType.StateChanged]: IconProp.ArrowCircleRight,
42
+ [TestFeedEventType.OwnerUserAdded]: IconProp.User,
43
+ [TestFeedEventType.IncidentCreated]: IconProp.Alert,
44
+ [TestFeedEventType.AutoRemediation]: IconProp.Wrench,
45
+ };
46
+
47
+ /*
48
+ * Module-level, like the feeds' own helpers, so the functions keep their
49
+ * identity across renders.
50
+ */
51
+ const getTestEventTypeIcon: (eventType: string) => IconProp | undefined = (
52
+ eventType: string,
53
+ ): IconProp | undefined => {
54
+ return TEST_EVENT_TYPE_ICONS[eventType as TestFeedEventType];
55
+ };
56
+
57
+ const getTestEventTypeLabel: (eventType: string) => string | undefined = (
58
+ eventType: string,
59
+ ): string | undefined => {
60
+ return eventType === TestFeedEventType.StateChanged
61
+ ? "Status Changed"
62
+ : undefined;
63
+ };
64
+
65
+ const STORAGE_KEY: string = "monitor";
66
+ const STORED_SORT_ORDER_KEY: string = "feed-sort-order:monitor";
67
+
68
+ type FeedOptionsHook = RenderHookResult<
69
+ UseFeedOptionsResult,
70
+ UseFeedOptionsProps
71
+ >;
72
+
73
+ type RenderFeedOptions = (props: UseFeedOptionsProps) => FeedOptionsHook;
74
+
75
+ const renderFeedOptions: RenderFeedOptions = (
76
+ props: UseFeedOptionsProps,
77
+ ): FeedOptionsHook => {
78
+ return renderHook(
79
+ (hookProps: UseFeedOptionsProps): UseFeedOptionsResult => {
80
+ return useFeedOptions(hookProps);
81
+ },
82
+ { initialProps: props },
83
+ );
84
+ };
85
+
86
+ type SetFeedOptions = (hook: FeedOptionsHook, options: FeedOptions) => void;
87
+
88
+ // The same call FeedOptionsButton makes when the reader changes something.
89
+ const setFeedOptions: SetFeedOptions = (
90
+ hook: FeedOptionsHook,
91
+ options: FeedOptions,
92
+ ): void => {
93
+ act(() => {
94
+ hook.result.current.setOptions(options);
95
+ });
96
+ };
97
+
98
+ // What one render of the hook handed the feed page.
99
+ interface OptionsRender {
100
+ resetKey: string | undefined;
101
+ options: FeedOptions;
102
+ optionsKey: string;
103
+ isFiltered: boolean;
104
+ }
105
+
106
+ interface FeedOptionsHarness {
107
+ hook: FeedOptionsHook;
108
+ renders: Array<OptionsRender>;
109
+ }
110
+
111
+ type RenderFeedOptionsWithHistory = (
112
+ props: UseFeedOptionsProps,
113
+ ) => FeedOptionsHarness;
114
+
115
+ /*
116
+ * Records every time the hook runs, including a render React throws away and
117
+ * runs again because the hook adjusted its state while rendering. Asserting
118
+ * over all of them proves that no render - committed or not - handed the page
119
+ * options it could have built a request from that the reader never chose.
120
+ */
121
+ const renderFeedOptionsWithHistory: RenderFeedOptionsWithHistory = (
122
+ props: UseFeedOptionsProps,
123
+ ): FeedOptionsHarness => {
124
+ const renders: Array<OptionsRender> = [];
125
+
126
+ const hook: FeedOptionsHook = renderHook(
127
+ (hookProps: UseFeedOptionsProps): UseFeedOptionsResult => {
128
+ const result: UseFeedOptionsResult = useFeedOptions(hookProps);
129
+
130
+ renders.push({
131
+ resetKey: hookProps.resetKey,
132
+ options: result.options,
133
+ optionsKey: result.optionsKey,
134
+ isFiltered: result.isFiltered,
135
+ });
136
+
137
+ return result;
138
+ },
139
+ { initialProps: props },
140
+ );
141
+
142
+ return { hook: hook, renders: renders };
143
+ };
144
+
145
+ type GetUnfilteredRender = (
146
+ resetKey: string | undefined,
147
+ sortOrder: SortOrder,
148
+ ) => OptionsRender;
149
+
150
+ const getUnfilteredRender: GetUnfilteredRender = (
151
+ resetKey: string | undefined,
152
+ sortOrder: SortOrder,
153
+ ): OptionsRender => {
154
+ const options: FeedOptions = { sortOrder: sortOrder, eventTypes: [] };
155
+
156
+ return {
157
+ resetKey: resetKey,
158
+ options: options,
159
+ optionsKey: getFeedOptionsKey(options),
160
+ isFiltered: false,
161
+ };
162
+ };
163
+
164
+ type ExpectEveryRender = (
165
+ renders: Array<OptionsRender>,
166
+ expected: OptionsRender,
167
+ ) => void;
168
+
169
+ // Compared as a whole list, so a failure names the render that was wrong.
170
+ const expectEveryRender: ExpectEveryRender = (
171
+ renders: Array<OptionsRender>,
172
+ expected: OptionsRender,
173
+ ): void => {
174
+ expect(renders.length).toBeGreaterThan(0);
175
+ expect(renders).toEqual(
176
+ renders.map((): OptionsRender => {
177
+ return expected;
178
+ }),
179
+ );
180
+ };
181
+
182
+ type RecordStorageWrites = () => Array<string>;
183
+
184
+ /*
185
+ * Records every storage write instead of making it. Reads still reach the
186
+ * real storage, so a value stored before the call is read back as usual.
187
+ */
188
+ const recordStorageWrites: RecordStorageWrites = (): Array<string> => {
189
+ const writes: Array<string> = [];
190
+
191
+ jest
192
+ .spyOn(Storage.prototype, "setItem")
193
+ .mockImplementation((key: string, value: string): void => {
194
+ writes.push(`setItem ${key} ${value}`);
195
+ });
196
+ jest
197
+ .spyOn(Storage.prototype, "removeItem")
198
+ .mockImplementation((key: string): void => {
199
+ writes.push(`removeItem ${key}`);
200
+ });
201
+
202
+ return writes;
203
+ };
204
+
205
+ type GetStoredKeys = () => Array<string>;
206
+
207
+ const getStoredKeys: GetStoredKeys = (): Array<string> => {
208
+ const keys: Array<string> = [];
209
+
210
+ for (let index: number = 0; index < window.localStorage.length; index++) {
211
+ const key: string | null = window.localStorage.key(index);
212
+
213
+ if (key !== null) {
214
+ keys.push(key);
215
+ }
216
+ }
217
+
218
+ return keys;
219
+ };
220
+
221
+ afterEach(() => {
222
+ cleanup();
223
+ jest.restoreAllMocks();
224
+ window.localStorage.clear();
225
+ });
226
+
227
+ describe("useFeedOptions defaults", () => {
228
+ test("starts newest first with every event type shown", () => {
229
+ const hook: FeedOptionsHook = renderFeedOptions({
230
+ eventTypes: TEST_EVENT_TYPES,
231
+ storageKey: STORAGE_KEY,
232
+ });
233
+
234
+ expect(hook.result.current.options).toEqual({
235
+ sortOrder: SortOrder.Descending,
236
+ eventTypes: [],
237
+ });
238
+ expect(hook.result.current.options).toEqual(DEFAULT_FEED_OPTIONS);
239
+ expect(hook.result.current.isFiltered).toBe(false);
240
+ expect(hook.result.current.optionsKey).toBe(
241
+ getFeedOptionsKey(DEFAULT_FEED_OPTIONS),
242
+ );
243
+ });
244
+ });
245
+
246
+ describe("useFeedOptions event type options", () => {
247
+ test("lists each event type once, labelled and with its icon, alphabetically by label", () => {
248
+ const hook: FeedOptionsHook = renderFeedOptions({
249
+ eventTypes: [...TEST_EVENT_TYPES, TestFeedEventType.IncidentCreated],
250
+ getEventTypeLabel: getTestEventTypeLabel,
251
+ getEventTypeIcon: getTestEventTypeIcon,
252
+ });
253
+
254
+ expect(hook.result.current.eventTypeOptions).toEqual([
255
+ {
256
+ value: TestFeedEventType.AutoRemediation,
257
+ label: "Auto-Remediation",
258
+ icon: IconProp.Wrench,
259
+ },
260
+ {
261
+ value: TestFeedEventType.IncidentCreated,
262
+ label: "Incident Created",
263
+ icon: IconProp.Alert,
264
+ },
265
+ // The caller's own label wins over the generated "State Changed".
266
+ {
267
+ value: TestFeedEventType.StateChanged,
268
+ label: "Status Changed",
269
+ icon: IconProp.ArrowCircleRight,
270
+ },
271
+ {
272
+ value: TestFeedEventType.OwnerUserAdded,
273
+ label: "User Added as Owner",
274
+ icon: IconProp.User,
275
+ },
276
+ ]);
277
+ });
278
+
279
+ /*
280
+ * Every feed passes Object.values(its enum), a new array on each render. The
281
+ * checklist must not be rebuilt - and the options must not change identity -
282
+ * just because the parent re-rendered.
283
+ */
284
+ test("keeps the same list across re-renders when the caller passes a new array with the same values", () => {
285
+ const hook: RenderHookResult<UseFeedOptionsResult, unknown> = renderHook(
286
+ (): UseFeedOptionsResult => {
287
+ return useFeedOptions({
288
+ eventTypes: Object.values(TestFeedEventType),
289
+ getEventTypeLabel: getTestEventTypeLabel,
290
+ getEventTypeIcon: getTestEventTypeIcon,
291
+ });
292
+ },
293
+ );
294
+
295
+ const firstEventTypeOptions: Array<FeedEventTypeOption> =
296
+ hook.result.current.eventTypeOptions;
297
+ const firstOptions: FeedOptions = hook.result.current.options;
298
+ const firstSetOptions: (options: FeedOptions) => void =
299
+ hook.result.current.setOptions;
300
+
301
+ hook.rerender();
302
+ hook.rerender();
303
+
304
+ expect(hook.result.current.eventTypeOptions).toBe(firstEventTypeOptions);
305
+ expect(hook.result.current.options).toBe(firstOptions);
306
+ expect(hook.result.current.setOptions).toBe(firstSetOptions);
307
+ });
308
+
309
+ test("rebuilds the list when the event types change", () => {
310
+ const hook: FeedOptionsHook = renderFeedOptions({
311
+ eventTypes: [
312
+ TestFeedEventType.StateChanged,
313
+ TestFeedEventType.IncidentCreated,
314
+ ],
315
+ });
316
+
317
+ const firstEventTypeOptions: Array<FeedEventTypeOption> =
318
+ hook.result.current.eventTypeOptions;
319
+
320
+ hook.rerender({
321
+ eventTypes: [
322
+ TestFeedEventType.StateChanged,
323
+ TestFeedEventType.IncidentCreated,
324
+ ],
325
+ });
326
+
327
+ expect(hook.result.current.eventTypeOptions).toBe(firstEventTypeOptions);
328
+
329
+ hook.rerender({
330
+ eventTypes: [
331
+ TestFeedEventType.StateChanged,
332
+ TestFeedEventType.IncidentCreated,
333
+ TestFeedEventType.AutoRemediation,
334
+ ],
335
+ });
336
+
337
+ expect(hook.result.current.eventTypeOptions).not.toBe(
338
+ firstEventTypeOptions,
339
+ );
340
+ expect(
341
+ hook.result.current.eventTypeOptions.map(
342
+ (option: FeedEventTypeOption): string => {
343
+ return option.value;
344
+ },
345
+ ),
346
+ ).toEqual([
347
+ TestFeedEventType.AutoRemediation,
348
+ TestFeedEventType.IncidentCreated,
349
+ TestFeedEventType.StateChanged,
350
+ ]);
351
+ });
352
+ });
353
+
354
+ describe("useFeedOptions setOptions", () => {
355
+ test("keeps only event types the feed has, once each, in the feed's own order", () => {
356
+ const hook: FeedOptionsHook = renderFeedOptions({
357
+ eventTypes: TEST_EVENT_TYPES,
358
+ });
359
+
360
+ setFeedOptions(hook, {
361
+ sortOrder: SortOrder.Descending,
362
+ eventTypes: [
363
+ TestFeedEventType.AutoRemediation,
364
+ "NotAFeedEventType",
365
+ TestFeedEventType.StateChanged,
366
+ TestFeedEventType.AutoRemediation,
367
+ ],
368
+ });
369
+
370
+ const expectedOptions: FeedOptions = {
371
+ sortOrder: SortOrder.Descending,
372
+ eventTypes: [
373
+ TestFeedEventType.StateChanged,
374
+ TestFeedEventType.AutoRemediation,
375
+ ],
376
+ };
377
+
378
+ expect(hook.result.current.options).toEqual(expectedOptions);
379
+ expect(hook.result.current.isFiltered).toBe(true);
380
+ expect(hook.result.current.optionsKey).toBe(
381
+ getFeedOptionsKey(expectedOptions),
382
+ );
383
+ });
384
+
385
+ test("a filter made only of event types the feed does not have is no filter", () => {
386
+ const hook: FeedOptionsHook = renderFeedOptions({
387
+ eventTypes: TEST_EVENT_TYPES,
388
+ });
389
+
390
+ setFeedOptions(hook, {
391
+ sortOrder: SortOrder.Descending,
392
+ eventTypes: ["NotAFeedEventType"],
393
+ });
394
+
395
+ expect(hook.result.current.options.eventTypes).toEqual([]);
396
+ expect(hook.result.current.isFiltered).toBe(false);
397
+ expect(hook.result.current.optionsKey).toBe(
398
+ getFeedOptionsKey(DEFAULT_FEED_OPTIONS),
399
+ );
400
+ });
401
+
402
+ test("falls back to newest first for a sort order it does not know", () => {
403
+ const hook: FeedOptionsHook = renderFeedOptions({
404
+ eventTypes: TEST_EVENT_TYPES,
405
+ storageKey: STORAGE_KEY,
406
+ });
407
+
408
+ setFeedOptions(hook, { sortOrder: SortOrder.Ascending, eventTypes: [] });
409
+ expect(window.localStorage.getItem(STORED_SORT_ORDER_KEY)).toBe(
410
+ SortOrder.Ascending,
411
+ );
412
+
413
+ setFeedOptions(hook, {
414
+ sortOrder: "sideways" as unknown as SortOrder,
415
+ eventTypes: [],
416
+ });
417
+
418
+ expect(hook.result.current.options.sortOrder).toBe(SortOrder.Descending);
419
+ expect(window.localStorage.getItem(STORED_SORT_ORDER_KEY)).toBeNull();
420
+ });
421
+
422
+ test("changes optionsKey on a sort change and on a filter change", () => {
423
+ const hook: FeedOptionsHook = renderFeedOptions({
424
+ eventTypes: TEST_EVENT_TYPES,
425
+ });
426
+
427
+ const defaultKey: string = hook.result.current.optionsKey;
428
+
429
+ setFeedOptions(hook, { sortOrder: SortOrder.Ascending, eventTypes: [] });
430
+ const oldestFirstKey: string = hook.result.current.optionsKey;
431
+ expect(hook.result.current.isFiltered).toBe(false);
432
+
433
+ setFeedOptions(hook, {
434
+ sortOrder: SortOrder.Ascending,
435
+ eventTypes: [TestFeedEventType.IncidentCreated],
436
+ });
437
+ const oneTypeKey: string = hook.result.current.optionsKey;
438
+ expect(hook.result.current.isFiltered).toBe(true);
439
+
440
+ setFeedOptions(hook, {
441
+ sortOrder: SortOrder.Ascending,
442
+ eventTypes: [
443
+ TestFeedEventType.AutoRemediation,
444
+ TestFeedEventType.IncidentCreated,
445
+ ],
446
+ });
447
+ const twoTypesKey: string = hook.result.current.optionsKey;
448
+
449
+ expect(
450
+ new Set<string>([defaultKey, oldestFirstKey, oneTypeKey, twoTypesKey])
451
+ .size,
452
+ ).toBe(4);
453
+
454
+ // Ticking the same boxes in another order is the same view.
455
+ setFeedOptions(hook, {
456
+ sortOrder: SortOrder.Ascending,
457
+ eventTypes: [
458
+ TestFeedEventType.IncidentCreated,
459
+ TestFeedEventType.AutoRemediation,
460
+ ],
461
+ });
462
+ expect(hook.result.current.optionsKey).toBe(twoTypesKey);
463
+
464
+ setFeedOptions(hook, { sortOrder: SortOrder.Descending, eventTypes: [] });
465
+ expect(hook.result.current.optionsKey).toBe(defaultKey);
466
+ expect(hook.result.current.isFiltered).toBe(false);
467
+ });
468
+
469
+ test("drops a selected event type the feed no longer has", () => {
470
+ const hook: FeedOptionsHook = renderFeedOptions({
471
+ eventTypes: TEST_EVENT_TYPES,
472
+ });
473
+
474
+ setFeedOptions(hook, {
475
+ sortOrder: SortOrder.Descending,
476
+ eventTypes: [
477
+ TestFeedEventType.IncidentCreated,
478
+ TestFeedEventType.AutoRemediation,
479
+ ],
480
+ });
481
+
482
+ hook.rerender({
483
+ eventTypes: [
484
+ TestFeedEventType.StateChanged,
485
+ TestFeedEventType.IncidentCreated,
486
+ ],
487
+ });
488
+
489
+ expect(hook.result.current.options.eventTypes).toEqual([
490
+ TestFeedEventType.IncidentCreated,
491
+ ]);
492
+ expect(hook.result.current.optionsKey).toBe(
493
+ getFeedOptionsKey({
494
+ sortOrder: SortOrder.Descending,
495
+ eventTypes: [TestFeedEventType.IncidentCreated],
496
+ }),
497
+ );
498
+ });
499
+ });
500
+
501
+ describe("useFeedOptions sort order storage", () => {
502
+ test("remembers oldest first under feed-sort-order:<storageKey>", () => {
503
+ expect(getSortOrderStorageKey(STORAGE_KEY)).toBe(STORED_SORT_ORDER_KEY);
504
+
505
+ const hook: FeedOptionsHook = renderFeedOptions({
506
+ eventTypes: TEST_EVENT_TYPES,
507
+ storageKey: STORAGE_KEY,
508
+ });
509
+
510
+ expect(window.localStorage.getItem(STORED_SORT_ORDER_KEY)).toBeNull();
511
+
512
+ setFeedOptions(hook, { sortOrder: SortOrder.Ascending, eventTypes: [] });
513
+
514
+ expect(window.localStorage.getItem(STORED_SORT_ORDER_KEY)).toBe(
515
+ SortOrder.Ascending,
516
+ );
517
+
518
+ // A filter change on its own leaves the remembered order as it was.
519
+ setFeedOptions(hook, {
520
+ sortOrder: SortOrder.Ascending,
521
+ eventTypes: [TestFeedEventType.IncidentCreated],
522
+ });
523
+
524
+ expect(window.localStorage.getItem(STORED_SORT_ORDER_KEY)).toBe(
525
+ SortOrder.Ascending,
526
+ );
527
+ });
528
+
529
+ test("forgets the stored order when the reader goes back to newest first", () => {
530
+ const hook: FeedOptionsHook = renderFeedOptions({
531
+ eventTypes: TEST_EVENT_TYPES,
532
+ storageKey: STORAGE_KEY,
533
+ });
534
+
535
+ setFeedOptions(hook, { sortOrder: SortOrder.Ascending, eventTypes: [] });
536
+ setFeedOptions(hook, { sortOrder: SortOrder.Descending, eventTypes: [] });
537
+
538
+ expect(hook.result.current.options.sortOrder).toBe(SortOrder.Descending);
539
+ expect(window.localStorage.getItem(STORED_SORT_ORDER_KEY)).toBeNull();
540
+ expect(getStoredKeys()).toEqual([]);
541
+ });
542
+
543
+ test("restores the stored order on mount", () => {
544
+ window.localStorage.setItem(STORED_SORT_ORDER_KEY, SortOrder.Ascending);
545
+
546
+ const hook: FeedOptionsHook = renderFeedOptions({
547
+ eventTypes: TEST_EVENT_TYPES,
548
+ storageKey: STORAGE_KEY,
549
+ });
550
+
551
+ const expectedOptions: FeedOptions = {
552
+ sortOrder: SortOrder.Ascending,
553
+ eventTypes: [],
554
+ };
555
+
556
+ expect(hook.result.current.options).toEqual(expectedOptions);
557
+ expect(hook.result.current.optionsKey).toBe(
558
+ getFeedOptionsKey(expectedOptions),
559
+ );
560
+ expect(hook.result.current.isFiltered).toBe(false);
561
+
562
+ // Going back to newest first from a restored order forgets it too.
563
+ setFeedOptions(hook, { sortOrder: SortOrder.Descending, eventTypes: [] });
564
+
565
+ expect(window.localStorage.getItem(STORED_SORT_ORDER_KEY)).toBeNull();
566
+ });
567
+
568
+ test.each([
569
+ "sideways",
570
+ "asc",
571
+ "",
572
+ "null",
573
+ "42",
574
+ '["ASC"]',
575
+ '{"sortOrder":"ASC"}',
576
+ ])(
577
+ "ignores a stored value that is not a sort order: %p",
578
+ (storedValue: string) => {
579
+ window.localStorage.setItem(STORED_SORT_ORDER_KEY, storedValue);
580
+
581
+ const hook: FeedOptionsHook = renderFeedOptions({
582
+ eventTypes: TEST_EVENT_TYPES,
583
+ storageKey: STORAGE_KEY,
584
+ });
585
+
586
+ expect(hook.result.current.options).toEqual(DEFAULT_FEED_OPTIONS);
587
+ expect(hook.result.current.optionsKey).toBe(
588
+ getFeedOptionsKey(DEFAULT_FEED_OPTIONS),
589
+ );
590
+
591
+ // The reader's next choice replaces whatever was there.
592
+ setFeedOptions(hook, { sortOrder: SortOrder.Ascending, eventTypes: [] });
593
+
594
+ expect(window.localStorage.getItem(STORED_SORT_ORDER_KEY)).toBe(
595
+ SortOrder.Ascending,
596
+ );
597
+ },
598
+ );
599
+
600
+ test("falls back to newest first when reading storage throws, and still sorts", () => {
601
+ jest
602
+ .spyOn(Storage.prototype, "getItem")
603
+ .mockImplementation((): string | null => {
604
+ throw new Error("SecurityError: storage is disabled");
605
+ });
606
+
607
+ const hook: FeedOptionsHook = renderFeedOptions({
608
+ eventTypes: TEST_EVENT_TYPES,
609
+ storageKey: STORAGE_KEY,
610
+ });
611
+
612
+ expect(hook.result.current.options).toEqual(DEFAULT_FEED_OPTIONS);
613
+
614
+ setFeedOptions(hook, { sortOrder: SortOrder.Ascending, eventTypes: [] });
615
+
616
+ expect(hook.result.current.options.sortOrder).toBe(SortOrder.Ascending);
617
+ });
618
+
619
+ test("still applies a sort order that storage refuses to save", () => {
620
+ jest.spyOn(Storage.prototype, "setItem").mockImplementation((): void => {
621
+ throw new Error("QuotaExceededError: storage is full");
622
+ });
623
+
624
+ const hook: FeedOptionsHook = renderFeedOptions({
625
+ eventTypes: TEST_EVENT_TYPES,
626
+ storageKey: STORAGE_KEY,
627
+ });
628
+
629
+ const expectedOptions: FeedOptions = {
630
+ sortOrder: SortOrder.Ascending,
631
+ eventTypes: [TestFeedEventType.IncidentCreated],
632
+ };
633
+
634
+ setFeedOptions(hook, expectedOptions);
635
+
636
+ expect(hook.result.current.options).toEqual(expectedOptions);
637
+ expect(hook.result.current.optionsKey).toBe(
638
+ getFeedOptionsKey(expectedOptions),
639
+ );
640
+ });
641
+
642
+ test("still goes back to newest first when storage refuses to forget the old order", () => {
643
+ window.localStorage.setItem(STORED_SORT_ORDER_KEY, SortOrder.Ascending);
644
+
645
+ jest.spyOn(Storage.prototype, "removeItem").mockImplementation((): void => {
646
+ throw new Error("SecurityError: storage is disabled");
647
+ });
648
+
649
+ const hook: FeedOptionsHook = renderFeedOptions({
650
+ eventTypes: TEST_EVENT_TYPES,
651
+ storageKey: STORAGE_KEY,
652
+ });
653
+
654
+ expect(hook.result.current.options.sortOrder).toBe(SortOrder.Ascending);
655
+
656
+ setFeedOptions(hook, { sortOrder: SortOrder.Descending, eventTypes: [] });
657
+
658
+ expect(hook.result.current.options).toEqual(DEFAULT_FEED_OPTIONS);
659
+ expect(hook.result.current.optionsKey).toBe(
660
+ getFeedOptionsKey(DEFAULT_FEED_OPTIONS),
661
+ );
662
+ });
663
+
664
+ test.each([undefined, ""])(
665
+ "never touches storage when storageKey is %p",
666
+ (storageKey: string | undefined) => {
667
+ const storageCalls: Array<string> = [];
668
+
669
+ jest
670
+ .spyOn(Storage.prototype, "getItem")
671
+ .mockImplementation((key: string): string | null => {
672
+ storageCalls.push(`getItem ${key}`);
673
+ return null;
674
+ });
675
+ jest
676
+ .spyOn(Storage.prototype, "setItem")
677
+ .mockImplementation((key: string): void => {
678
+ storageCalls.push(`setItem ${key}`);
679
+ });
680
+ jest
681
+ .spyOn(Storage.prototype, "removeItem")
682
+ .mockImplementation((key: string): void => {
683
+ storageCalls.push(`removeItem ${key}`);
684
+ });
685
+
686
+ const hook: FeedOptionsHook = renderFeedOptions({
687
+ eventTypes: TEST_EVENT_TYPES,
688
+ storageKey: storageKey,
689
+ });
690
+
691
+ setFeedOptions(hook, { sortOrder: SortOrder.Ascending, eventTypes: [] });
692
+ setFeedOptions(hook, {
693
+ sortOrder: SortOrder.Descending,
694
+ eventTypes: [TestFeedEventType.IncidentCreated],
695
+ });
696
+
697
+ expect(hook.result.current.options).toEqual({
698
+ sortOrder: SortOrder.Descending,
699
+ eventTypes: [TestFeedEventType.IncidentCreated],
700
+ });
701
+ expect(storageCalls).toEqual([]);
702
+
703
+ // The spies do see a feed with a storage key: the check is not vacuous.
704
+ renderFeedOptions({
705
+ eventTypes: TEST_EVENT_TYPES,
706
+ storageKey: STORAGE_KEY,
707
+ });
708
+
709
+ expect(storageCalls).toEqual([`getItem ${STORED_SORT_ORDER_KEY}`]);
710
+ },
711
+ );
712
+
713
+ /*
714
+ * The filter narrows one investigation. Carried to the next page it would
715
+ * read as a feed with missing events, so only the sort order survives.
716
+ */
717
+ test("does not remember the event type filter", () => {
718
+ const firstVisit: FeedOptionsHook = renderFeedOptions({
719
+ eventTypes: TEST_EVENT_TYPES,
720
+ storageKey: STORAGE_KEY,
721
+ });
722
+
723
+ setFeedOptions(firstVisit, {
724
+ sortOrder: SortOrder.Ascending,
725
+ eventTypes: [TestFeedEventType.IncidentCreated],
726
+ });
727
+
728
+ expect(firstVisit.result.current.isFiltered).toBe(true);
729
+ expect(getStoredKeys()).toEqual([STORED_SORT_ORDER_KEY]);
730
+
731
+ firstVisit.unmount();
732
+
733
+ const nextVisit: FeedOptionsHook = renderFeedOptions({
734
+ eventTypes: TEST_EVENT_TYPES,
735
+ storageKey: STORAGE_KEY,
736
+ });
737
+
738
+ expect(nextVisit.result.current.options).toEqual({
739
+ sortOrder: SortOrder.Ascending,
740
+ eventTypes: [],
741
+ });
742
+ expect(nextVisit.result.current.isFiltered).toBe(false);
743
+ });
744
+
745
+ test("keeps each storage key's order separate", () => {
746
+ const incidentFeed: FeedOptionsHook = renderFeedOptions({
747
+ eventTypes: TEST_EVENT_TYPES,
748
+ storageKey: "incident",
749
+ });
750
+ const alertFeed: FeedOptionsHook = renderFeedOptions({
751
+ eventTypes: TEST_EVENT_TYPES,
752
+ storageKey: "alert",
753
+ });
754
+
755
+ setFeedOptions(incidentFeed, {
756
+ sortOrder: SortOrder.Ascending,
757
+ eventTypes: [],
758
+ });
759
+
760
+ expect(incidentFeed.result.current.options.sortOrder).toBe(
761
+ SortOrder.Ascending,
762
+ );
763
+ expect(alertFeed.result.current.options.sortOrder).toBe(
764
+ SortOrder.Descending,
765
+ );
766
+ expect(window.localStorage.getItem("feed-sort-order:incident")).toBe(
767
+ SortOrder.Ascending,
768
+ );
769
+ expect(window.localStorage.getItem("feed-sort-order:alert")).toBeNull();
770
+
771
+ incidentFeed.unmount();
772
+ alertFeed.unmount();
773
+
774
+ expect(
775
+ renderFeedOptions({
776
+ eventTypes: TEST_EVENT_TYPES,
777
+ storageKey: "incident",
778
+ }).result.current.options.sortOrder,
779
+ ).toBe(SortOrder.Ascending);
780
+ expect(
781
+ renderFeedOptions({
782
+ eventTypes: TEST_EVENT_TYPES,
783
+ storageKey: "alert",
784
+ }).result.current.options.sortOrder,
785
+ ).toBe(SortOrder.Descending);
786
+ });
787
+ });
788
+
789
+ /*
790
+ * A type alias rather than an interface on purpose: jest's object-form
791
+ * test.each is typed against Record<string, unknown>, which an interface does
792
+ * not satisfy (it has no implicit index signature).
793
+ */
794
+ type BatchedChangesCase = {
795
+ name: string;
796
+ storedBefore: SortOrder | null;
797
+ first: FeedOptions;
798
+ last: FeedOptions;
799
+ storedAfter: SortOrder | null;
800
+ };
801
+
802
+ describe("useFeedOptions stored sort order follows what the page shows", () => {
803
+ /*
804
+ * One event handler can call setOptions twice. React renders once, with the
805
+ * second call's options, so storage must end up with the second call's
806
+ * order too - not the first's, or the next visit opens in an order the page
807
+ * never showed.
808
+ */
809
+ test.each<BatchedChangesCase>([
810
+ {
811
+ name: "newest first -> oldest first -> newest first",
812
+ storedBefore: null,
813
+ first: { sortOrder: SortOrder.Ascending, eventTypes: [] },
814
+ last: {
815
+ sortOrder: SortOrder.Descending,
816
+ eventTypes: [TestFeedEventType.IncidentCreated],
817
+ },
818
+ storedAfter: null,
819
+ },
820
+ {
821
+ name: "oldest first -> newest first -> oldest first",
822
+ storedBefore: SortOrder.Ascending,
823
+ first: { sortOrder: SortOrder.Descending, eventTypes: [] },
824
+ last: {
825
+ sortOrder: SortOrder.Ascending,
826
+ eventTypes: [TestFeedEventType.IncidentCreated],
827
+ },
828
+ storedAfter: SortOrder.Ascending,
829
+ },
830
+ ])(
831
+ "two changes batched into one render leave storage matching the page: $name",
832
+ (testCase: BatchedChangesCase) => {
833
+ if (testCase.storedBefore !== null) {
834
+ window.localStorage.setItem(
835
+ STORED_SORT_ORDER_KEY,
836
+ testCase.storedBefore,
837
+ );
838
+ }
839
+
840
+ const hook: FeedOptionsHook = renderFeedOptions({
841
+ eventTypes: TEST_EVENT_TYPES,
842
+ storageKey: STORAGE_KEY,
843
+ });
844
+
845
+ act(() => {
846
+ const setOptions: (options: FeedOptions) => void =
847
+ hook.result.current.setOptions;
848
+
849
+ setOptions(testCase.first);
850
+ setOptions(testCase.last);
851
+ });
852
+
853
+ expect(hook.result.current.options).toEqual(testCase.last);
854
+ expect(window.localStorage.getItem(STORED_SORT_ORDER_KEY)).toBe(
855
+ testCase.storedAfter,
856
+ );
857
+
858
+ // So the next visit opens in the order the reader ended on.
859
+ hook.unmount();
860
+
861
+ expect(
862
+ renderFeedOptions({
863
+ eventTypes: TEST_EVENT_TYPES,
864
+ storageKey: STORAGE_KEY,
865
+ }).result.current.options.sortOrder,
866
+ ).toBe(testCase.last.sortOrder);
867
+ },
868
+ );
869
+
870
+ /*
871
+ * The first run of the hook only reads what is stored: writing it straight
872
+ * back would be a write on every page view of every feed. Nor is a
873
+ * re-render or a filter-only change a sort change.
874
+ */
875
+ test.each<[string, SortOrder | null, SortOrder, string]>([
876
+ [
877
+ "nothing stored",
878
+ null,
879
+ SortOrder.Ascending,
880
+ `setItem ${STORED_SORT_ORDER_KEY} ${SortOrder.Ascending}`,
881
+ ],
882
+ [
883
+ "oldest first stored",
884
+ SortOrder.Ascending,
885
+ SortOrder.Descending,
886
+ `removeItem ${STORED_SORT_ORDER_KEY}`,
887
+ ],
888
+ ])(
889
+ "mounting writes nothing to storage (%s); only a sort change does",
890
+ (
891
+ _name: string,
892
+ storedBefore: SortOrder | null,
893
+ nextSortOrder: SortOrder,
894
+ expectedWrite: string,
895
+ ) => {
896
+ if (storedBefore !== null) {
897
+ window.localStorage.setItem(STORED_SORT_ORDER_KEY, storedBefore);
898
+ }
899
+
900
+ const writes: Array<string> = recordStorageWrites();
901
+
902
+ const hook: FeedOptionsHook = renderFeedOptions({
903
+ eventTypes: TEST_EVENT_TYPES,
904
+ storageKey: STORAGE_KEY,
905
+ });
906
+
907
+ const mountedSortOrder: SortOrder = hook.result.current.options.sortOrder;
908
+
909
+ expect(mountedSortOrder).toBe(storedBefore ?? SortOrder.Descending);
910
+ expect(writes).toEqual([]);
911
+
912
+ hook.rerender({
913
+ eventTypes: [...TEST_EVENT_TYPES],
914
+ storageKey: STORAGE_KEY,
915
+ });
916
+ setFeedOptions(hook, {
917
+ sortOrder: mountedSortOrder,
918
+ eventTypes: [TestFeedEventType.IncidentCreated],
919
+ });
920
+
921
+ expect(writes).toEqual([]);
922
+
923
+ // The recorder does see this feed's writes: the checks above are not vacuous.
924
+ setFeedOptions(hook, {
925
+ sortOrder: nextSortOrder,
926
+ eventTypes: [TestFeedEventType.IncidentCreated],
927
+ });
928
+
929
+ expect(hook.result.current.options.sortOrder).toBe(nextSortOrder);
930
+ expect(writes).toEqual([expectedWrite]);
931
+ },
932
+ );
933
+
934
+ test("a remount after choosing oldest first starts oldest first, without writing", () => {
935
+ const firstVisit: FeedOptionsHook = renderFeedOptions({
936
+ eventTypes: TEST_EVENT_TYPES,
937
+ storageKey: STORAGE_KEY,
938
+ });
939
+
940
+ setFeedOptions(firstVisit, {
941
+ sortOrder: SortOrder.Ascending,
942
+ eventTypes: [],
943
+ });
944
+ firstVisit.unmount();
945
+
946
+ const writes: Array<string> = recordStorageWrites();
947
+
948
+ const nextVisit: FeedOptionsHook = renderFeedOptions({
949
+ eventTypes: TEST_EVENT_TYPES,
950
+ storageKey: STORAGE_KEY,
951
+ });
952
+
953
+ const expectedOptions: FeedOptions = {
954
+ sortOrder: SortOrder.Ascending,
955
+ eventTypes: [],
956
+ };
957
+
958
+ expect(nextVisit.result.current.options).toEqual(expectedOptions);
959
+ expect(nextVisit.result.current.optionsKey).toBe(
960
+ getFeedOptionsKey(expectedOptions),
961
+ );
962
+ expect(writes).toEqual([]);
963
+ });
964
+ });
965
+
966
+ describe("useFeedOptions resetKey", () => {
967
+ const FIRST_RESOURCE: string = "incident-1";
968
+ const NEXT_RESOURCE: string = "incident-2";
969
+
970
+ const FILTER_ON_FIRST_RESOURCE: FeedOptions = {
971
+ sortOrder: SortOrder.Ascending,
972
+ eventTypes: [TestFeedEventType.IncidentCreated],
973
+ };
974
+
975
+ type GetProps = (resetKey: string | undefined) => UseFeedOptionsProps;
976
+
977
+ // A new event type array on every call, as the feed pages pass.
978
+ const getProps: GetProps = (
979
+ resetKey: string | undefined,
980
+ ): UseFeedOptionsProps => {
981
+ return {
982
+ eventTypes: [...TEST_EVENT_TYPES],
983
+ storageKey: STORAGE_KEY,
984
+ resetKey: resetKey,
985
+ };
986
+ };
987
+
988
+ /*
989
+ * The dashboard moves from one incident to the next without remounting the
990
+ * feed. Were the filter dropped only after a render, that render would
991
+ * already have asked the API for the next incident's feed with the first
992
+ * incident's filter.
993
+ */
994
+ test("the first render for the next resource already drops the filter, and keeps the sort order", () => {
995
+ const feed: FeedOptionsHarness = renderFeedOptionsWithHistory(
996
+ getProps(FIRST_RESOURCE),
997
+ );
998
+
999
+ setFeedOptions(feed.hook, FILTER_ON_FIRST_RESOURCE);
1000
+
1001
+ expect(feed.hook.result.current.options).toEqual(FILTER_ON_FIRST_RESOURCE);
1002
+ expect(feed.hook.result.current.isFiltered).toBe(true);
1003
+
1004
+ const writes: Array<string> = recordStorageWrites();
1005
+ const firstNextResourceRender: number = feed.renders.length;
1006
+
1007
+ feed.hook.rerender(getProps(NEXT_RESOURCE));
1008
+
1009
+ expectEveryRender(
1010
+ feed.renders.slice(firstNextResourceRender),
1011
+ getUnfilteredRender(NEXT_RESOURCE, SortOrder.Ascending),
1012
+ );
1013
+
1014
+ // The same view as opening the next resource's feed from scratch.
1015
+ expect(feed.hook.result.current.optionsKey).toBe(
1016
+ renderFeedOptions(getProps(NEXT_RESOURCE)).result.current.optionsKey,
1017
+ );
1018
+
1019
+ // The sort order is a preference: moving on neither changes nor rewrites it.
1020
+ expect(writes).toEqual([]);
1021
+ expect(window.localStorage.getItem(STORED_SORT_ORDER_KEY)).toBe(
1022
+ SortOrder.Ascending,
1023
+ );
1024
+ });
1025
+
1026
+ test("going back to the first resource does not bring its filter back", () => {
1027
+ const feed: FeedOptionsHarness = renderFeedOptionsWithHistory(
1028
+ getProps(FIRST_RESOURCE),
1029
+ );
1030
+
1031
+ setFeedOptions(feed.hook, FILTER_ON_FIRST_RESOURCE);
1032
+ feed.hook.rerender(getProps(NEXT_RESOURCE));
1033
+
1034
+ const firstRenderBack: number = feed.renders.length;
1035
+
1036
+ feed.hook.rerender(getProps(FIRST_RESOURCE));
1037
+ feed.hook.rerender(getProps(FIRST_RESOURCE));
1038
+
1039
+ expectEveryRender(
1040
+ feed.renders.slice(firstRenderBack),
1041
+ getUnfilteredRender(FIRST_RESOURCE, SortOrder.Ascending),
1042
+ );
1043
+ });
1044
+
1045
+ test("a filter chosen after the switch applies to the new resource and stays while it is shown", () => {
1046
+ const feed: FeedOptionsHarness = renderFeedOptionsWithHistory(
1047
+ getProps(FIRST_RESOURCE),
1048
+ );
1049
+
1050
+ setFeedOptions(feed.hook, FILTER_ON_FIRST_RESOURCE);
1051
+ feed.hook.rerender(getProps(NEXT_RESOURCE));
1052
+
1053
+ const filterOnNextResource: FeedOptions = {
1054
+ sortOrder: SortOrder.Descending,
1055
+ eventTypes: [TestFeedEventType.AutoRemediation],
1056
+ };
1057
+
1058
+ setFeedOptions(feed.hook, filterOnNextResource);
1059
+
1060
+ expect(feed.hook.result.current.options).toEqual(filterOnNextResource);
1061
+ expect(feed.hook.result.current.isFiltered).toBe(true);
1062
+ expect(feed.hook.result.current.optionsKey).toBe(
1063
+ getFeedOptionsKey(filterOnNextResource),
1064
+ );
1065
+
1066
+ // Later renders for the same resource - a refresh, a parent update - keep it.
1067
+ feed.hook.rerender(getProps(NEXT_RESOURCE));
1068
+
1069
+ expect(feed.hook.result.current.options).toEqual(filterOnNextResource);
1070
+
1071
+ // And it belongs to that resource only.
1072
+ feed.hook.rerender(getProps(FIRST_RESOURCE));
1073
+
1074
+ expect(feed.hook.result.current.options).toEqual({
1075
+ sortOrder: SortOrder.Descending,
1076
+ eventTypes: [],
1077
+ });
1078
+ });
1079
+
1080
+ test.each([undefined, FIRST_RESOURCE])(
1081
+ "re-renders with the same resetKey (%p) keep the filter and the options object",
1082
+ (resetKey: string | undefined) => {
1083
+ const feed: FeedOptionsHarness = renderFeedOptionsWithHistory(
1084
+ getProps(resetKey),
1085
+ );
1086
+
1087
+ setFeedOptions(feed.hook, FILTER_ON_FIRST_RESOURCE);
1088
+
1089
+ const chosenOptions: FeedOptions = feed.hook.result.current.options;
1090
+ const firstRerender: number = feed.renders.length;
1091
+
1092
+ feed.hook.rerender(getProps(resetKey));
1093
+ feed.hook.rerender(getProps(resetKey));
1094
+
1095
+ expect(feed.hook.result.current.options).toBe(chosenOptions);
1096
+ expect(feed.hook.result.current.options).toEqual(
1097
+ FILTER_ON_FIRST_RESOURCE,
1098
+ );
1099
+ expectEveryRender(feed.renders.slice(firstRerender), {
1100
+ resetKey: resetKey,
1101
+ options: FILTER_ON_FIRST_RESOURCE,
1102
+ optionsKey: getFeedOptionsKey(FILTER_ON_FIRST_RESOURCE),
1103
+ isFiltered: true,
1104
+ });
1105
+ },
1106
+ );
1107
+ });