@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,1017 @@
1
+ import { afterEach, describe, expect, test } from "@jest/globals";
2
+ import {
3
+ act,
4
+ cleanup,
5
+ renderHook,
6
+ RenderHookResult,
7
+ } from "@testing-library/react";
8
+ import SortOrder from "../../../Types/BaseDatabase/SortOrder";
9
+ import { Blue500 } from "../../../Types/BrandColors";
10
+ import { DEFAULT_LIMIT } from "../../../Types/Database/LimitMax";
11
+ import IconProp from "../../../Types/Icon/IconProp";
12
+ import { FeedItemProps } from "../../../UI/Components/Feed/FeedItem";
13
+ import {
14
+ FeedOptions,
15
+ getFeedOptionsKey,
16
+ } from "../../../UI/Components/Feed/FeedOptions";
17
+ import useFeedItems, {
18
+ FeedPage,
19
+ UseFeedItemsResult,
20
+ } from "../../../UI/Components/Feed/useFeedItems";
21
+ import useFeedOptions, {
22
+ UseFeedOptionsResult,
23
+ } from "../../../UI/Components/Feed/useFeedOptions";
24
+
25
+ /*
26
+ * The viewKey contract of useFeedItems: a change of sort order or event type
27
+ * filter re-reads the feed from its first window, exactly like moving to
28
+ * another resource. LimitMax is deliberately not mocked here, so every window
29
+ * below is the real DEFAULT_LIMIT.
30
+ */
31
+
32
+ interface TestFeedModel {
33
+ id: string;
34
+ }
35
+
36
+ // What a feed page passes the hook on one render.
37
+ interface FeedView {
38
+ resourceKey: string;
39
+ viewKey?: string | undefined;
40
+ refreshToken?: number | undefined;
41
+ }
42
+
43
+ interface FeedRequestTarget {
44
+ resourceKey: string;
45
+ viewKey: string | undefined;
46
+ limit: number;
47
+ }
48
+
49
+ /*
50
+ * One getItems call. The fake API never answers on its own: each test answers
51
+ * or fails the requests itself, in whatever order the race it is about needs.
52
+ */
53
+ interface FeedRequest extends FeedRequestTarget {
54
+ respond: (itemNames: Array<string>, count?: number) => Promise<void>;
55
+ fail: (message: string) => Promise<void>;
56
+ }
57
+
58
+ interface FakeFeedApi {
59
+ requests: Array<FeedRequest>;
60
+ getItems: (target: FeedRequestTarget) => Promise<FeedPage<TestFeedModel>>;
61
+ }
62
+
63
+ // Plenty more rows on the server, so More stays available.
64
+ const SERVER_ROW_COUNT: number = 1000;
65
+
66
+ const NEWEST_FIRST: string = getFeedOptionsKey({
67
+ sortOrder: SortOrder.Descending,
68
+ eventTypes: [],
69
+ });
70
+ const OLDEST_FIRST: string = getFeedOptionsKey({
71
+ sortOrder: SortOrder.Ascending,
72
+ eventTypes: [],
73
+ });
74
+
75
+ type GetItemKey = (
76
+ target: { resourceKey: string; viewKey: string | undefined },
77
+ itemName: string,
78
+ ) => string;
79
+
80
+ // Every row names the resource and view it was read for.
81
+ const getItemKey: GetItemKey = (
82
+ target: { resourceKey: string; viewKey: string | undefined },
83
+ itemName: string,
84
+ ): string => {
85
+ return `${target.resourceKey}/${String(target.viewKey)}/${itemName}`;
86
+ };
87
+
88
+ type GetRequestName = (
89
+ resourceKey: string,
90
+ viewKey: string | undefined,
91
+ limit: number,
92
+ ) => string;
93
+
94
+ const getRequestName: GetRequestName = (
95
+ resourceKey: string,
96
+ viewKey: string | undefined,
97
+ limit: number,
98
+ ): string => {
99
+ return `${resourceKey} ${String(viewKey)} ${limit}`;
100
+ };
101
+
102
+ type CreateFakeFeedApi = () => FakeFeedApi;
103
+
104
+ const createFakeFeedApi: CreateFakeFeedApi = (): FakeFeedApi => {
105
+ const requests: Array<FeedRequest> = [];
106
+
107
+ return {
108
+ requests: requests,
109
+ getItems: (target: FeedRequestTarget): Promise<FeedPage<TestFeedModel>> => {
110
+ return new Promise<FeedPage<TestFeedModel>>(
111
+ (
112
+ resolvePage: (page: FeedPage<TestFeedModel>) => void,
113
+ rejectPage: (error: Error) => void,
114
+ ): void => {
115
+ requests.push({
116
+ ...target,
117
+ respond: async (
118
+ itemNames: Array<string>,
119
+ count: number = SERVER_ROW_COUNT,
120
+ ): Promise<void> => {
121
+ await act(async (): Promise<void> => {
122
+ resolvePage({
123
+ data: itemNames.map((itemName: string): TestFeedModel => {
124
+ return { id: getItemKey(target, itemName) };
125
+ }),
126
+ count: count,
127
+ });
128
+ await Promise.resolve();
129
+ });
130
+ },
131
+ fail: async (message: string): Promise<void> => {
132
+ await act(async (): Promise<void> => {
133
+ rejectPage(new Error(message));
134
+ await Promise.resolve();
135
+ });
136
+ },
137
+ });
138
+ },
139
+ );
140
+ },
141
+ };
142
+ };
143
+
144
+ type GetRequest = (api: FakeFeedApi, index: number) => FeedRequest;
145
+
146
+ const getRequest: GetRequest = (
147
+ api: FakeFeedApi,
148
+ index: number,
149
+ ): FeedRequest => {
150
+ const request: FeedRequest | undefined = api.requests[index];
151
+
152
+ if (!request) {
153
+ throw new Error(`No feed request #${index} was made`);
154
+ }
155
+
156
+ return request;
157
+ };
158
+
159
+ type GetLatestRequest = (api: FakeFeedApi) => FeedRequest;
160
+
161
+ const getLatestRequest: GetLatestRequest = (api: FakeFeedApi): FeedRequest => {
162
+ return getRequest(api, api.requests.length - 1);
163
+ };
164
+
165
+ type GetRequestNames = (api: FakeFeedApi) => Array<string>;
166
+
167
+ const getRequestNames: GetRequestNames = (api: FakeFeedApi): Array<string> => {
168
+ return api.requests.map((request: FeedRequest): string => {
169
+ return getRequestName(request.resourceKey, request.viewKey, request.limit);
170
+ });
171
+ };
172
+
173
+ type MapTestItems = (items: Array<TestFeedModel>) => Array<FeedItemProps>;
174
+
175
+ const mapTestItems: MapTestItems = (
176
+ items: Array<TestFeedModel>,
177
+ ): Array<FeedItemProps> => {
178
+ return items.map((item: TestFeedModel): FeedItemProps => {
179
+ return {
180
+ key: item.id,
181
+ textInMarkdown: item.id,
182
+ itemDateTime: new Date("2026-09-21T12:00:00.000Z"),
183
+ icon: IconProp.Activity,
184
+ color: Blue500,
185
+ };
186
+ });
187
+ };
188
+
189
+ type GetItemKeys = (feed: UseFeedItemsResult) => Array<string>;
190
+
191
+ const getItemKeys: GetItemKeys = (feed: UseFeedItemsResult): Array<string> => {
192
+ return feed.feedItems.map((item: FeedItemProps): string => {
193
+ return item.key;
194
+ });
195
+ };
196
+
197
+ // What one render of the page would have had to work with.
198
+ interface RenderSnapshot {
199
+ resourceKey: string;
200
+ viewKey: string | undefined;
201
+ isCurrentFeedLoaded: boolean;
202
+ itemKeys: Array<string>;
203
+ }
204
+
205
+ interface FeedHarness {
206
+ result: { current: UseFeedItemsResult };
207
+ renders: Array<RenderSnapshot>;
208
+ show: (view: FeedView) => void;
209
+ }
210
+
211
+ type RenderFeed = (api: FakeFeedApi, view: FeedView) => FeedHarness;
212
+
213
+ const renderFeed: RenderFeed = (
214
+ api: FakeFeedApi,
215
+ view: FeedView,
216
+ ): FeedHarness => {
217
+ const renders: Array<RenderSnapshot> = [];
218
+
219
+ const hook: RenderHookResult<UseFeedItemsResult, FeedView> = renderHook(
220
+ (props: FeedView): UseFeedItemsResult => {
221
+ const feed: UseFeedItemsResult = useFeedItems<TestFeedModel>({
222
+ resourceKey: props.resourceKey,
223
+ viewKey: props.viewKey,
224
+ refreshToken: props.refreshToken,
225
+ // A new closure over this render's view, as on the feed pages.
226
+ getItems: (limit: number): Promise<FeedPage<TestFeedModel>> => {
227
+ return api.getItems({
228
+ resourceKey: props.resourceKey,
229
+ viewKey: props.viewKey,
230
+ limit: limit,
231
+ });
232
+ },
233
+ mapItems: mapTestItems,
234
+ });
235
+
236
+ renders.push({
237
+ resourceKey: props.resourceKey,
238
+ viewKey: props.viewKey,
239
+ isCurrentFeedLoaded: feed.isCurrentFeedLoaded,
240
+ itemKeys: getItemKeys(feed),
241
+ });
242
+
243
+ return feed;
244
+ },
245
+ { initialProps: view },
246
+ );
247
+
248
+ return {
249
+ result: hook.result,
250
+ renders: renders,
251
+ show: (nextView: FeedView): void => {
252
+ hook.rerender(nextView);
253
+ },
254
+ };
255
+ };
256
+
257
+ type LoadMore = (feed: FeedHarness) => void;
258
+
259
+ // The reader pressing More.
260
+ const loadMore: LoadMore = (feed: FeedHarness): void => {
261
+ act(() => {
262
+ feed.result.current.loadMore();
263
+ });
264
+ };
265
+
266
+ enum TestFeedEventType {
267
+ IncidentCreated = "IncidentCreated",
268
+ IncidentStateChanged = "IncidentStateChanged",
269
+ }
270
+
271
+ type DescribeOptions = (options: FeedOptions) => string;
272
+
273
+ // What a feed page's getItems puts in its query and sort.
274
+ const describeOptions: DescribeOptions = (options: FeedOptions): string => {
275
+ return `sort=${options.sortOrder} types=${options.eventTypes.join(",")}`;
276
+ };
277
+
278
+ interface OptionsFeed {
279
+ feedOptions: UseFeedOptionsResult;
280
+ feed: UseFeedItemsResult;
281
+ }
282
+
283
+ interface OptionsFeedProps {
284
+ resourceKey: string;
285
+ }
286
+
287
+ type OptionsFeedHook = RenderHookResult<OptionsFeed, OptionsFeedProps>;
288
+
289
+ type RenderOptionsFeed = (
290
+ api: FakeFeedApi,
291
+ resourceKey: string,
292
+ ) => OptionsFeedHook;
293
+
294
+ /*
295
+ * Wired the way every dashboard feed wires the two hooks: the resource id is
296
+ * both useFeedOptions' resetKey and useFeedItems' resourceKey, optionsKey is
297
+ * the viewKey, and getItems builds its request from this render's options.
298
+ */
299
+ const renderOptionsFeed: RenderOptionsFeed = (
300
+ api: FakeFeedApi,
301
+ resourceKey: string,
302
+ ): OptionsFeedHook => {
303
+ return renderHook(
304
+ (props: OptionsFeedProps): OptionsFeed => {
305
+ const feedOptions: UseFeedOptionsResult = useFeedOptions({
306
+ eventTypes: Object.values(TestFeedEventType),
307
+ resetKey: props.resourceKey,
308
+ });
309
+
310
+ const feed: UseFeedItemsResult = useFeedItems<TestFeedModel>({
311
+ resourceKey: props.resourceKey,
312
+ viewKey: feedOptions.optionsKey,
313
+ getItems: (limit: number): Promise<FeedPage<TestFeedModel>> => {
314
+ return api.getItems({
315
+ resourceKey: props.resourceKey,
316
+ viewKey: describeOptions(feedOptions.options),
317
+ limit: limit,
318
+ });
319
+ },
320
+ mapItems: mapTestItems,
321
+ });
322
+
323
+ return { feedOptions: feedOptions, feed: feed };
324
+ },
325
+ { initialProps: { resourceKey: resourceKey } },
326
+ );
327
+ };
328
+
329
+ type GetForeignPaintedItems = (renders: Array<RenderSnapshot>) => Array<string>;
330
+
331
+ /*
332
+ * The feed pages paint rows only once isCurrentFeedLoaded is true, so that is
333
+ * when a row read for another resource or view must never be in feedItems.
334
+ */
335
+ const getForeignPaintedItems: GetForeignPaintedItems = (
336
+ renders: Array<RenderSnapshot>,
337
+ ): Array<string> => {
338
+ const foreignItems: Array<string> = [];
339
+
340
+ for (const snapshot of renders) {
341
+ if (!snapshot.isCurrentFeedLoaded) {
342
+ continue;
343
+ }
344
+
345
+ const ownPrefix: string = getItemKey(snapshot, "");
346
+
347
+ for (const itemKey of snapshot.itemKeys) {
348
+ if (!itemKey.startsWith(ownPrefix)) {
349
+ foreignItems.push(`${itemKey} painted under ${ownPrefix}`);
350
+ }
351
+ }
352
+ }
353
+
354
+ return foreignItems;
355
+ };
356
+
357
+ type GetPaintedItemKeys = (renders: Array<RenderSnapshot>) => Array<string>;
358
+
359
+ const getPaintedItemKeys: GetPaintedItemKeys = (
360
+ renders: Array<RenderSnapshot>,
361
+ ): Array<string> => {
362
+ return renders
363
+ .filter((snapshot: RenderSnapshot): boolean => {
364
+ return snapshot.isCurrentFeedLoaded;
365
+ })
366
+ .flatMap((snapshot: RenderSnapshot): Array<string> => {
367
+ return snapshot.itemKeys;
368
+ });
369
+ };
370
+
371
+ afterEach(() => {
372
+ cleanup();
373
+ });
374
+
375
+ describe("useFeedItems viewKey", () => {
376
+ test("re-reads from DEFAULT_LIMIT when the view changes, even after More widened the window", async () => {
377
+ const api: FakeFeedApi = createFakeFeedApi();
378
+ const feed: FeedHarness = renderFeed(api, {
379
+ resourceKey: "monitor-1",
380
+ viewKey: NEWEST_FIRST,
381
+ });
382
+
383
+ await getLatestRequest(api).respond(["newest"]);
384
+ loadMore(feed);
385
+ await getLatestRequest(api).respond(["newest", "next"]);
386
+
387
+ expect(getRequestNames(api)).toEqual([
388
+ getRequestName("monitor-1", NEWEST_FIRST, DEFAULT_LIMIT),
389
+ getRequestName("monitor-1", NEWEST_FIRST, DEFAULT_LIMIT * 2),
390
+ ]);
391
+
392
+ feed.show({ resourceKey: "monitor-1", viewKey: OLDEST_FIRST });
393
+
394
+ expect(getRequestNames(api)).toHaveLength(3);
395
+ expect(getRequestNames(api)[2]).toBe(
396
+ getRequestName("monitor-1", OLDEST_FIRST, DEFAULT_LIMIT),
397
+ );
398
+
399
+ await getLatestRequest(api).respond(["oldest"]);
400
+
401
+ expect(feed.result.current.isCurrentFeedLoaded).toBe(true);
402
+ expect(getItemKeys(feed.result.current)).toEqual([
403
+ getItemKey({ resourceKey: "monitor-1", viewKey: OLDEST_FIRST }, "oldest"),
404
+ ]);
405
+ expect(feed.result.current.hasMore).toBe(true);
406
+
407
+ // More on the new view widens from its own first window.
408
+ loadMore(feed);
409
+
410
+ expect(getRequestNames(api)[3]).toBe(
411
+ getRequestName("monitor-1", OLDEST_FIRST, DEFAULT_LIMIT * 2),
412
+ );
413
+ });
414
+
415
+ /*
416
+ * The feed pages build the query and sort inside getItems from the options
417
+ * of the render that made it, so the re-read must use the new render's
418
+ * getItems - not the one the effect saw first.
419
+ */
420
+ test("hands getItems the sort order and filter the reader just chose", async () => {
421
+ const api: FakeFeedApi = createFakeFeedApi();
422
+ const hook: OptionsFeedHook = renderOptionsFeed(api, "incident-1");
423
+
424
+ await getLatestRequest(api).respond(["newest"]);
425
+ act(() => {
426
+ hook.result.current.feed.loadMore();
427
+ });
428
+ await getLatestRequest(api).respond(["newest", "next"]);
429
+
430
+ act(() => {
431
+ hook.result.current.feedOptions.setOptions({
432
+ sortOrder: SortOrder.Ascending,
433
+ eventTypes: [TestFeedEventType.IncidentStateChanged],
434
+ });
435
+ });
436
+
437
+ expect(hook.result.current.feed.isCurrentFeedLoaded).toBe(false);
438
+
439
+ await getLatestRequest(api).respond(["first-state-change"]);
440
+
441
+ expect(hook.result.current.feed.isCurrentFeedLoaded).toBe(true);
442
+
443
+ act(() => {
444
+ hook.result.current.feedOptions.setOptions({
445
+ sortOrder: SortOrder.Ascending,
446
+ eventTypes: [],
447
+ });
448
+ });
449
+
450
+ expect(getRequestNames(api)).toEqual([
451
+ getRequestName("incident-1", "sort=DESC types=", DEFAULT_LIMIT),
452
+ getRequestName("incident-1", "sort=DESC types=", DEFAULT_LIMIT * 2),
453
+ getRequestName(
454
+ "incident-1",
455
+ "sort=ASC types=IncidentStateChanged",
456
+ DEFAULT_LIMIT,
457
+ ),
458
+ getRequestName("incident-1", "sort=ASC types=", DEFAULT_LIMIT),
459
+ ]);
460
+ });
461
+
462
+ /*
463
+ * The dashboard moves from one incident to the next without remounting the
464
+ * feed. The filter was chosen for the first incident, so not even the first
465
+ * read for the next one may carry it; the sort order is a preference and
466
+ * carries on.
467
+ */
468
+ test("the first read for the next resource drops the previous resource's filter and keeps its sort order", async () => {
469
+ const api: FakeFeedApi = createFakeFeedApi();
470
+ const hook: OptionsFeedHook = renderOptionsFeed(api, "incident-1");
471
+
472
+ await getLatestRequest(api).respond(["newest"]);
473
+
474
+ act(() => {
475
+ hook.result.current.feedOptions.setOptions({
476
+ sortOrder: SortOrder.Ascending,
477
+ eventTypes: [TestFeedEventType.IncidentStateChanged],
478
+ });
479
+ });
480
+ await getLatestRequest(api).respond(["first-state-change"]);
481
+
482
+ hook.rerender({ resourceKey: "incident-2" });
483
+
484
+ // Exactly one read for the next incident, and it is unfiltered.
485
+ expect(getRequestNames(api)).toEqual([
486
+ getRequestName("incident-1", "sort=DESC types=", DEFAULT_LIMIT),
487
+ getRequestName(
488
+ "incident-1",
489
+ "sort=ASC types=IncidentStateChanged",
490
+ DEFAULT_LIMIT,
491
+ ),
492
+ getRequestName("incident-2", "sort=ASC types=", DEFAULT_LIMIT),
493
+ ]);
494
+
495
+ await getLatestRequest(api).respond(["oldest"]);
496
+
497
+ expect(hook.result.current.feed.isCurrentFeedLoaded).toBe(true);
498
+ expect(hook.result.current.feedOptions.isFiltered).toBe(false);
499
+
500
+ // Back on the first incident, its old filter does not come back either.
501
+ hook.rerender({ resourceKey: "incident-1" });
502
+
503
+ expect(getRequestNames(api).slice(3)).toEqual([
504
+ getRequestName("incident-1", "sort=ASC types=", DEFAULT_LIMIT),
505
+ ]);
506
+ });
507
+
508
+ test("while the new view loads, the feed is not loaded and the old view's rows are gone", async () => {
509
+ const api: FakeFeedApi = createFakeFeedApi();
510
+ const feed: FeedHarness = renderFeed(api, {
511
+ resourceKey: "monitor-1",
512
+ viewKey: NEWEST_FIRST,
513
+ });
514
+
515
+ await getLatestRequest(api).respond(["newest", "next"]);
516
+
517
+ expect(feed.result.current.isCurrentFeedLoaded).toBe(true);
518
+ expect(feed.result.current.feedItems).toHaveLength(2);
519
+
520
+ feed.show({ resourceKey: "monitor-1", viewKey: OLDEST_FIRST });
521
+
522
+ const firstNewViewRender: RenderSnapshot | undefined = feed.renders.find(
523
+ (snapshot: RenderSnapshot): boolean => {
524
+ return snapshot.viewKey === OLDEST_FIRST;
525
+ },
526
+ );
527
+
528
+ // Not even the render that first sees the new view counts as loaded.
529
+ expect(firstNewViewRender?.isCurrentFeedLoaded).toBe(false);
530
+ expect(feed.result.current.isCurrentFeedLoaded).toBe(false);
531
+ expect(feed.result.current.isLoading).toBe(true);
532
+ expect(feed.result.current.feedItems).toEqual([]);
533
+ expect(feed.result.current.hasMore).toBe(false);
534
+
535
+ await getLatestRequest(api).respond(["oldest"]);
536
+
537
+ expect(feed.result.current.isCurrentFeedLoaded).toBe(true);
538
+ expect(feed.result.current.isLoading).toBe(false);
539
+ expect(getItemKeys(feed.result.current)).toEqual([
540
+ getItemKey({ resourceKey: "monitor-1", viewKey: OLDEST_FIRST }, "oldest"),
541
+ ]);
542
+ expect(getForeignPaintedItems(feed.renders)).toEqual([]);
543
+ });
544
+
545
+ test("ignores a slow response for the old view that lands after the switch", async () => {
546
+ const api: FakeFeedApi = createFakeFeedApi();
547
+ const feed: FeedHarness = renderFeed(api, {
548
+ resourceKey: "monitor-1",
549
+ viewKey: NEWEST_FIRST,
550
+ });
551
+
552
+ const slowOldViewRequest: FeedRequest = getLatestRequest(api);
553
+
554
+ feed.show({ resourceKey: "monitor-1", viewKey: OLDEST_FIRST });
555
+
556
+ const newViewRequest: FeedRequest = getLatestRequest(api);
557
+
558
+ await slowOldViewRequest.respond(["late-newest"]);
559
+
560
+ expect(feed.result.current.isCurrentFeedLoaded).toBe(false);
561
+ expect(feed.result.current.isLoading).toBe(true);
562
+ expect(feed.result.current.feedItems).toEqual([]);
563
+
564
+ await newViewRequest.respond(["oldest"]);
565
+
566
+ expect(feed.result.current.isCurrentFeedLoaded).toBe(true);
567
+ expect(getItemKeys(feed.result.current)).toEqual([
568
+ getItemKey({ resourceKey: "monitor-1", viewKey: OLDEST_FIRST }, "oldest"),
569
+ ]);
570
+ expect(getForeignPaintedItems(feed.renders)).toEqual([]);
571
+ });
572
+
573
+ test("ignores a slow response for the old view that lands after the new view's own", async () => {
574
+ const api: FakeFeedApi = createFakeFeedApi();
575
+ const feed: FeedHarness = renderFeed(api, {
576
+ resourceKey: "monitor-1",
577
+ viewKey: NEWEST_FIRST,
578
+ });
579
+
580
+ const slowOldViewRequest: FeedRequest = getLatestRequest(api);
581
+
582
+ feed.show({ resourceKey: "monitor-1", viewKey: OLDEST_FIRST });
583
+
584
+ await getLatestRequest(api).respond(["oldest"]);
585
+ await slowOldViewRequest.respond(["late-newest"]);
586
+
587
+ expect(feed.result.current.isCurrentFeedLoaded).toBe(true);
588
+ expect(feed.result.current.isLoading).toBe(false);
589
+ expect(getItemKeys(feed.result.current)).toEqual([
590
+ getItemKey({ resourceKey: "monitor-1", viewKey: OLDEST_FIRST }, "oldest"),
591
+ ]);
592
+ expect(getForeignPaintedItems(feed.renders)).toEqual([]);
593
+ });
594
+
595
+ test("ignores a failure for the old view that lands after the switch", async () => {
596
+ const api: FakeFeedApi = createFakeFeedApi();
597
+ const feed: FeedHarness = renderFeed(api, {
598
+ resourceKey: "monitor-1",
599
+ viewKey: NEWEST_FIRST,
600
+ });
601
+
602
+ const slowOldViewRequest: FeedRequest = getLatestRequest(api);
603
+
604
+ feed.show({ resourceKey: "monitor-1", viewKey: OLDEST_FIRST });
605
+
606
+ await slowOldViewRequest.fail("The old view's request failed");
607
+
608
+ expect(feed.result.current.error).toBeUndefined();
609
+ expect(feed.result.current.isCurrentFeedLoaded).toBe(false);
610
+ expect(feed.result.current.isLoading).toBe(true);
611
+
612
+ await getLatestRequest(api).respond(["oldest"]);
613
+
614
+ expect(feed.result.current.error).toBeUndefined();
615
+ expect(feed.result.current.isCurrentFeedLoaded).toBe(true);
616
+ });
617
+
618
+ test("a More still in flight for the old view neither lands nor blocks More on the new view", async () => {
619
+ const api: FakeFeedApi = createFakeFeedApi();
620
+ const feed: FeedHarness = renderFeed(api, {
621
+ resourceKey: "monitor-1",
622
+ viewKey: NEWEST_FIRST,
623
+ });
624
+
625
+ await getLatestRequest(api).respond(["newest"]);
626
+ loadMore(feed);
627
+
628
+ const oldViewMoreRequest: FeedRequest = getLatestRequest(api);
629
+
630
+ expect(feed.result.current.isLoadingMore).toBe(true);
631
+
632
+ feed.show({ resourceKey: "monitor-1", viewKey: OLDEST_FIRST });
633
+
634
+ expect(feed.result.current.isLoadingMore).toBe(false);
635
+
636
+ const newViewRequest: FeedRequest = getLatestRequest(api);
637
+
638
+ await oldViewMoreRequest.respond(["newest", "next"]);
639
+
640
+ expect(feed.result.current.isCurrentFeedLoaded).toBe(false);
641
+ expect(feed.result.current.feedItems).toEqual([]);
642
+
643
+ await newViewRequest.respond(["oldest"]);
644
+
645
+ expect(feed.result.current.isCurrentFeedLoaded).toBe(true);
646
+ expect(feed.result.current.hasMore).toBe(true);
647
+
648
+ loadMore(feed);
649
+
650
+ expect(getRequestNames(api)).toEqual([
651
+ getRequestName("monitor-1", NEWEST_FIRST, DEFAULT_LIMIT),
652
+ getRequestName("monitor-1", NEWEST_FIRST, DEFAULT_LIMIT * 2),
653
+ getRequestName("monitor-1", OLDEST_FIRST, DEFAULT_LIMIT),
654
+ getRequestName("monitor-1", OLDEST_FIRST, DEFAULT_LIMIT * 2),
655
+ ]);
656
+ expect(getForeignPaintedItems(feed.renders)).toEqual([]);
657
+ });
658
+
659
+ /*
660
+ * Back on the first view, the response it was waiting for before the switch
661
+ * carries the same resource and view - only the request generation tells it
662
+ * apart from the fresh one.
663
+ */
664
+ test("switching A -> B -> A ends on A's fresh data, not the response A was waiting for before", async () => {
665
+ const api: FakeFeedApi = createFakeFeedApi();
666
+ const feed: FeedHarness = renderFeed(api, {
667
+ resourceKey: "monitor-1",
668
+ viewKey: NEWEST_FIRST,
669
+ });
670
+
671
+ const firstNewestRequest: FeedRequest = getLatestRequest(api);
672
+
673
+ feed.show({ resourceKey: "monitor-1", viewKey: OLDEST_FIRST });
674
+
675
+ const oldestRequest: FeedRequest = getLatestRequest(api);
676
+
677
+ feed.show({ resourceKey: "monitor-1", viewKey: NEWEST_FIRST });
678
+
679
+ const freshNewestRequest: FeedRequest = getLatestRequest(api);
680
+
681
+ expect(getRequestNames(api)).toEqual([
682
+ getRequestName("monitor-1", NEWEST_FIRST, DEFAULT_LIMIT),
683
+ getRequestName("monitor-1", OLDEST_FIRST, DEFAULT_LIMIT),
684
+ getRequestName("monitor-1", NEWEST_FIRST, DEFAULT_LIMIT),
685
+ ]);
686
+
687
+ await firstNewestRequest.respond(["stale"]);
688
+
689
+ expect(feed.result.current.isCurrentFeedLoaded).toBe(false);
690
+ expect(feed.result.current.feedItems).toEqual([]);
691
+
692
+ await oldestRequest.respond(["oldest"]);
693
+
694
+ expect(feed.result.current.isCurrentFeedLoaded).toBe(false);
695
+ expect(feed.result.current.feedItems).toEqual([]);
696
+
697
+ await freshNewestRequest.respond(["fresh"]);
698
+
699
+ expect(feed.result.current.isCurrentFeedLoaded).toBe(true);
700
+ expect(feed.result.current.isLoading).toBe(false);
701
+ expect(getItemKeys(feed.result.current)).toEqual([
702
+ getItemKey({ resourceKey: "monitor-1", viewKey: NEWEST_FIRST }, "fresh"),
703
+ ]);
704
+ expect(getPaintedItemKeys(feed.renders)).not.toContain(
705
+ getItemKey({ resourceKey: "monitor-1", viewKey: NEWEST_FIRST }, "stale"),
706
+ );
707
+ expect(getForeignPaintedItems(feed.renders)).toEqual([]);
708
+ });
709
+
710
+ test("switching A -> B -> A starts A from its first window again", async () => {
711
+ const api: FakeFeedApi = createFakeFeedApi();
712
+ const feed: FeedHarness = renderFeed(api, {
713
+ resourceKey: "monitor-1",
714
+ viewKey: NEWEST_FIRST,
715
+ });
716
+
717
+ await getLatestRequest(api).respond(["newest"]);
718
+ loadMore(feed);
719
+ await getLatestRequest(api).respond(["newest", "next"]);
720
+
721
+ feed.show({ resourceKey: "monitor-1", viewKey: OLDEST_FIRST });
722
+ await getLatestRequest(api).respond(["oldest"]);
723
+
724
+ feed.show({ resourceKey: "monitor-1", viewKey: NEWEST_FIRST });
725
+
726
+ expect(feed.result.current.isCurrentFeedLoaded).toBe(false);
727
+ expect(feed.result.current.feedItems).toEqual([]);
728
+
729
+ await getLatestRequest(api).respond(["fresh"]);
730
+
731
+ expect(getRequestNames(api)).toEqual([
732
+ getRequestName("monitor-1", NEWEST_FIRST, DEFAULT_LIMIT),
733
+ getRequestName("monitor-1", NEWEST_FIRST, DEFAULT_LIMIT * 2),
734
+ getRequestName("monitor-1", OLDEST_FIRST, DEFAULT_LIMIT),
735
+ getRequestName("monitor-1", NEWEST_FIRST, DEFAULT_LIMIT),
736
+ ]);
737
+ expect(getItemKeys(feed.result.current)).toEqual([
738
+ getItemKey({ resourceKey: "monitor-1", viewKey: NEWEST_FIRST }, "fresh"),
739
+ ]);
740
+ expect(getForeignPaintedItems(feed.renders)).toEqual([]);
741
+ });
742
+
743
+ test("does not refetch when the viewKey is unchanged across re-renders", async () => {
744
+ const api: FakeFeedApi = createFakeFeedApi();
745
+ const feed: FeedHarness = renderFeed(api, {
746
+ resourceKey: "monitor-1",
747
+ viewKey: OLDEST_FIRST,
748
+ });
749
+
750
+ await getLatestRequest(api).respond(["oldest"]);
751
+
752
+ // New props, a new getItems closure and a freshly built, equal key.
753
+ feed.show({
754
+ resourceKey: "monitor-1",
755
+ viewKey: getFeedOptionsKey({
756
+ sortOrder: SortOrder.Ascending,
757
+ eventTypes: [],
758
+ }),
759
+ });
760
+ feed.show({ resourceKey: "monitor-1", viewKey: OLDEST_FIRST });
761
+
762
+ expect(getRequestNames(api)).toEqual([
763
+ getRequestName("monitor-1", OLDEST_FIRST, DEFAULT_LIMIT),
764
+ ]);
765
+ expect(feed.result.current.isCurrentFeedLoaded).toBe(true);
766
+ expect(feed.result.current.isLoading).toBe(false);
767
+ expect(getItemKeys(feed.result.current)).toEqual([
768
+ getItemKey({ resourceKey: "monitor-1", viewKey: OLDEST_FIRST }, "oldest"),
769
+ ]);
770
+ });
771
+
772
+ test("without a viewKey the feed is keyed by its resource alone, as before", async () => {
773
+ const api: FakeFeedApi = createFakeFeedApi();
774
+ const feed: FeedHarness = renderFeed(api, { resourceKey: "monitor-1" });
775
+
776
+ await getLatestRequest(api).respond(["first"]);
777
+
778
+ expect(feed.result.current.isCurrentFeedLoaded).toBe(true);
779
+
780
+ loadMore(feed);
781
+ await getLatestRequest(api).respond(["first", "second"]);
782
+
783
+ feed.show({ resourceKey: "monitor-1" });
784
+ feed.show({ resourceKey: "monitor-1", viewKey: undefined });
785
+
786
+ expect(getRequestNames(api)).toEqual([
787
+ getRequestName("monitor-1", undefined, DEFAULT_LIMIT),
788
+ getRequestName("monitor-1", undefined, DEFAULT_LIMIT * 2),
789
+ ]);
790
+ expect(feed.result.current.isCurrentFeedLoaded).toBe(true);
791
+ expect(feed.result.current.feedItems).toHaveLength(2);
792
+
793
+ // The reader moves on while a More for the first resource is in flight.
794
+ loadMore(feed);
795
+
796
+ const slowFirstResourceRequest: FeedRequest = getLatestRequest(api);
797
+
798
+ feed.show({ resourceKey: "monitor-2" });
799
+
800
+ expect(getRequestNames(api).slice(2)).toEqual([
801
+ getRequestName("monitor-1", undefined, DEFAULT_LIMIT * 3),
802
+ getRequestName("monitor-2", undefined, DEFAULT_LIMIT),
803
+ ]);
804
+ expect(feed.result.current.isCurrentFeedLoaded).toBe(false);
805
+ expect(feed.result.current.feedItems).toEqual([]);
806
+
807
+ await getLatestRequest(api).respond(["other"]);
808
+
809
+ expect(feed.result.current.isCurrentFeedLoaded).toBe(true);
810
+ expect(getItemKeys(feed.result.current)).toEqual([
811
+ getItemKey({ resourceKey: "monitor-2", viewKey: undefined }, "other"),
812
+ ]);
813
+
814
+ // That late More for the first resource lands nowhere.
815
+ await slowFirstResourceRequest.respond(["late"]);
816
+
817
+ expect(getItemKeys(feed.result.current)).toEqual([
818
+ getItemKey({ resourceKey: "monitor-2", viewKey: undefined }, "other"),
819
+ ]);
820
+ expect(getForeignPaintedItems(feed.renders)).toEqual([]);
821
+ });
822
+
823
+ test("a refreshToken bump re-reads the current view at its current window", async () => {
824
+ const api: FakeFeedApi = createFakeFeedApi();
825
+ const feed: FeedHarness = renderFeed(api, {
826
+ resourceKey: "monitor-1",
827
+ viewKey: NEWEST_FIRST,
828
+ refreshToken: 0,
829
+ });
830
+
831
+ await getLatestRequest(api).respond(["newest"]);
832
+
833
+ feed.show({
834
+ resourceKey: "monitor-1",
835
+ viewKey: OLDEST_FIRST,
836
+ refreshToken: 0,
837
+ });
838
+ await getLatestRequest(api).respond(["oldest"]);
839
+ loadMore(feed);
840
+ await getLatestRequest(api).respond(["oldest", "next"]);
841
+
842
+ feed.show({
843
+ resourceKey: "monitor-1",
844
+ viewKey: OLDEST_FIRST,
845
+ refreshToken: 1,
846
+ });
847
+
848
+ expect(getRequestNames(api)).toEqual([
849
+ getRequestName("monitor-1", NEWEST_FIRST, DEFAULT_LIMIT),
850
+ getRequestName("monitor-1", OLDEST_FIRST, DEFAULT_LIMIT),
851
+ getRequestName("monitor-1", OLDEST_FIRST, DEFAULT_LIMIT * 2),
852
+ getRequestName("monitor-1", OLDEST_FIRST, DEFAULT_LIMIT * 2),
853
+ ]);
854
+
855
+ // A refresh keeps the rows on screen while it reads.
856
+ expect(feed.result.current.isCurrentFeedLoaded).toBe(true);
857
+ expect(getItemKeys(feed.result.current)).toEqual([
858
+ getItemKey({ resourceKey: "monitor-1", viewKey: OLDEST_FIRST }, "oldest"),
859
+ getItemKey({ resourceKey: "monitor-1", viewKey: OLDEST_FIRST }, "next"),
860
+ ]);
861
+
862
+ await getLatestRequest(api).respond(["oldest", "next", "new"]);
863
+
864
+ expect(feed.result.current.isCurrentFeedLoaded).toBe(true);
865
+ expect(feed.result.current.feedItems).toHaveLength(3);
866
+ expect(getForeignPaintedItems(feed.renders)).toEqual([]);
867
+ });
868
+
869
+ /*
870
+ * The view change itself always reads DEFAULT_LIMIT rows. What a refresh
871
+ * reads is the remembered window, and until the new view's first read lands
872
+ * nothing has replaced the window More widened on the old view - so the
873
+ * view change has to reset it, or a refresh in that gap reads the old view's
874
+ * wider window for the new one.
875
+ */
876
+ test("a refresh before the new view's first read lands asks for DEFAULT_LIMIT, not the old view's widened window", async () => {
877
+ const api: FakeFeedApi = createFakeFeedApi();
878
+ const feed: FeedHarness = renderFeed(api, {
879
+ resourceKey: "monitor-1",
880
+ viewKey: NEWEST_FIRST,
881
+ refreshToken: 0,
882
+ });
883
+
884
+ await getLatestRequest(api).respond(["newest"]);
885
+ loadMore(feed);
886
+ await getLatestRequest(api).respond(["newest", "next"]);
887
+
888
+ feed.show({
889
+ resourceKey: "monitor-1",
890
+ viewKey: OLDEST_FIRST,
891
+ refreshToken: 0,
892
+ });
893
+
894
+ const supersededRequest: FeedRequest = getLatestRequest(api);
895
+
896
+ feed.show({
897
+ resourceKey: "monitor-1",
898
+ viewKey: OLDEST_FIRST,
899
+ refreshToken: 1,
900
+ });
901
+
902
+ expect(getRequestNames(api)).toEqual([
903
+ getRequestName("monitor-1", NEWEST_FIRST, DEFAULT_LIMIT),
904
+ getRequestName("monitor-1", NEWEST_FIRST, DEFAULT_LIMIT * 2),
905
+ getRequestName("monitor-1", OLDEST_FIRST, DEFAULT_LIMIT),
906
+ getRequestName("monitor-1", OLDEST_FIRST, DEFAULT_LIMIT),
907
+ ]);
908
+
909
+ const refreshRequest: FeedRequest = getLatestRequest(api);
910
+
911
+ await supersededRequest.respond(["superseded"]);
912
+ await refreshRequest.respond(["oldest"]);
913
+
914
+ expect(feed.result.current.isCurrentFeedLoaded).toBe(true);
915
+ expect(getItemKeys(feed.result.current)).toEqual([
916
+ getItemKey({ resourceKey: "monitor-1", viewKey: OLDEST_FIRST }, "oldest"),
917
+ ]);
918
+ expect(getPaintedItemKeys(feed.renders)).not.toContain(
919
+ getItemKey(
920
+ { resourceKey: "monitor-1", viewKey: OLDEST_FIRST },
921
+ "superseded",
922
+ ),
923
+ );
924
+
925
+ // More on the new view widens from its own first window.
926
+ loadMore(feed);
927
+
928
+ expect(getRequestNames(api).slice(4)).toEqual([
929
+ getRequestName("monitor-1", OLDEST_FIRST, DEFAULT_LIMIT * 2),
930
+ ]);
931
+ });
932
+
933
+ /*
934
+ * A failed read leaves the remembered window as it was, so after the new
935
+ * view's first read fails only the view change's own reset keeps the
936
+ * reader's retry from reading the old view's wider window.
937
+ */
938
+ test("a refresh after the new view's first read failed asks for DEFAULT_LIMIT", async () => {
939
+ const api: FakeFeedApi = createFakeFeedApi();
940
+ const feed: FeedHarness = renderFeed(api, {
941
+ resourceKey: "monitor-1",
942
+ viewKey: NEWEST_FIRST,
943
+ });
944
+
945
+ await getLatestRequest(api).respond(["newest"]);
946
+ loadMore(feed);
947
+ await getLatestRequest(api).respond(["newest", "next"]);
948
+
949
+ feed.show({ resourceKey: "monitor-1", viewKey: OLDEST_FIRST });
950
+ await getLatestRequest(api).fail("The new view's read failed");
951
+
952
+ expect(typeof feed.result.current.error).toBe("string");
953
+ expect(feed.result.current.isCurrentFeedLoaded).toBe(true);
954
+
955
+ // The reader presses the feed's Refresh button.
956
+ act(() => {
957
+ feed.result.current.refresh().catch(() => {
958
+ // fetchItems converts request failures into the error state.
959
+ });
960
+ });
961
+
962
+ expect(getRequestNames(api)).toEqual([
963
+ getRequestName("monitor-1", NEWEST_FIRST, DEFAULT_LIMIT),
964
+ getRequestName("monitor-1", NEWEST_FIRST, DEFAULT_LIMIT * 2),
965
+ getRequestName("monitor-1", OLDEST_FIRST, DEFAULT_LIMIT),
966
+ getRequestName("monitor-1", OLDEST_FIRST, DEFAULT_LIMIT),
967
+ ]);
968
+
969
+ await getLatestRequest(api).respond(["oldest"]);
970
+
971
+ expect(feed.result.current.error).toBeUndefined();
972
+ expect(feed.result.current.isCurrentFeedLoaded).toBe(true);
973
+ expect(getItemKeys(feed.result.current)).toEqual([
974
+ getItemKey({ resourceKey: "monitor-1", viewKey: OLDEST_FIRST }, "oldest"),
975
+ ]);
976
+
977
+ loadMore(feed);
978
+
979
+ expect(getRequestNames(api).slice(4)).toEqual([
980
+ getRequestName("monitor-1", OLDEST_FIRST, DEFAULT_LIMIT * 2),
981
+ ]);
982
+ });
983
+
984
+ test("changing resourceKey while a viewKey is set still resets the feed", async () => {
985
+ const api: FakeFeedApi = createFakeFeedApi();
986
+ const feed: FeedHarness = renderFeed(api, {
987
+ resourceKey: "monitor-1",
988
+ viewKey: OLDEST_FIRST,
989
+ });
990
+
991
+ await getLatestRequest(api).respond(["oldest"]);
992
+ loadMore(feed);
993
+ await getLatestRequest(api).respond(["oldest", "next"]);
994
+
995
+ feed.show({ resourceKey: "monitor-2", viewKey: OLDEST_FIRST });
996
+
997
+ expect(getRequestNames(api)).toEqual([
998
+ getRequestName("monitor-1", OLDEST_FIRST, DEFAULT_LIMIT),
999
+ getRequestName("monitor-1", OLDEST_FIRST, DEFAULT_LIMIT * 2),
1000
+ getRequestName("monitor-2", OLDEST_FIRST, DEFAULT_LIMIT),
1001
+ ]);
1002
+ expect(feed.result.current.isCurrentFeedLoaded).toBe(false);
1003
+ expect(feed.result.current.feedItems).toEqual([]);
1004
+ expect(feed.result.current.hasMore).toBe(false);
1005
+
1006
+ await getLatestRequest(api).respond(["other-oldest"]);
1007
+
1008
+ expect(feed.result.current.isCurrentFeedLoaded).toBe(true);
1009
+ expect(getItemKeys(feed.result.current)).toEqual([
1010
+ getItemKey(
1011
+ { resourceKey: "monitor-2", viewKey: OLDEST_FIRST },
1012
+ "other-oldest",
1013
+ ),
1014
+ ]);
1015
+ expect(getForeignPaintedItems(feed.renders)).toEqual([]);
1016
+ });
1017
+ });