@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,1404 @@
1
+ import {
2
+ DEFAULT_FEED_OPTIONS,
3
+ DEFAULT_FEED_SORT_ORDER,
4
+ FEED_OPTIONS_TEXT,
5
+ FEED_SORT_ORDER_OPTIONS,
6
+ FILTERED_FEED_NO_ITEMS_MESSAGE,
7
+ FeedEventTypeOption,
8
+ FeedOptions,
9
+ FeedSortOrderOption,
10
+ getFeedEventTypeLabel,
11
+ getFeedEventTypeOptions,
12
+ getFeedEventTypeQuery,
13
+ getFeedNoItemsMessage,
14
+ getFeedOptionsKey,
15
+ getFeedOptionsSummary,
16
+ isDefaultFeedOptions,
17
+ isFeedFiltered,
18
+ isFeedSortOrder,
19
+ sanitizeFeedEventTypes,
20
+ translateFeedOptionsText,
21
+ } from "../../../UI/Components/Feed/FeedOptions";
22
+ import { AlertEpisodeFeedEventType } from "../../../Models/DatabaseModels/AlertEpisodeFeed";
23
+ import { AlertFeedEventType } from "../../../Models/DatabaseModels/AlertFeed";
24
+ import { CephClusterFeedEventType } from "../../../Models/DatabaseModels/CephClusterFeed";
25
+ import { CloudResourceFeedEventType } from "../../../Models/DatabaseModels/CloudResourceFeed";
26
+ import { DockerHostFeedEventType } from "../../../Models/DatabaseModels/DockerHostFeed";
27
+ import { DockerSwarmClusterFeedEventType } from "../../../Models/DatabaseModels/DockerSwarmClusterFeed";
28
+ import { HostFeedEventType } from "../../../Models/DatabaseModels/HostFeed";
29
+ import { IncidentEpisodeFeedEventType } from "../../../Models/DatabaseModels/IncidentEpisodeFeed";
30
+ import IncidentFeed, {
31
+ IncidentFeedEventType,
32
+ } from "../../../Models/DatabaseModels/IncidentFeed";
33
+ import { KubernetesClusterFeedEventType } from "../../../Models/DatabaseModels/KubernetesClusterFeed";
34
+ import MonitorFeed, {
35
+ MonitorFeedEventType,
36
+ } from "../../../Models/DatabaseModels/MonitorFeed";
37
+ import { OnCallDutyPolicyFeedEventType } from "../../../Models/DatabaseModels/OnCallDutyPolicyFeed";
38
+ import { PodmanHostFeedEventType } from "../../../Models/DatabaseModels/PodmanHostFeed";
39
+ import { ProxmoxClusterFeedEventType } from "../../../Models/DatabaseModels/ProxmoxClusterFeed";
40
+ import { ScheduledMaintenanceFeedEventType } from "../../../Models/DatabaseModels/ScheduledMaintenanceFeed";
41
+ import { ServiceFeedEventType } from "../../../Models/DatabaseModels/ServiceFeed";
42
+ import { ServiceLevelObjectiveFeedEventType } from "../../../Models/DatabaseModels/ServiceLevelObjectiveFeed";
43
+ import { VMwareVCenterFeedEventType } from "../../../Models/DatabaseModels/VMwareVCenterFeed";
44
+ import Includes from "../../../Types/BaseDatabase/Includes";
45
+ import Query from "../../../Types/BaseDatabase/Query";
46
+ import SortOrder from "../../../Types/BaseDatabase/SortOrder";
47
+ import IconProp from "../../../Types/Icon/IconProp";
48
+ import { ObjectType } from "../../../Types/JSON";
49
+ import { describe, expect, test } from "@jest/globals";
50
+ import fs from "fs";
51
+ import path from "path";
52
+
53
+ interface FeedEventTypeSpec {
54
+ // The feed model's file under Models/DatabaseModels, without ".ts".
55
+ name: string;
56
+ // The *FeedEventType enum that file exports.
57
+ enumName: string;
58
+ eventTypes: Array<string>;
59
+ }
60
+
61
+ /*
62
+ * Every dashboard activity feed that gets the "Filter & Sort" button. Each
63
+ * one's event types become a checklist, so every label below has to read well
64
+ * and be told apart from its neighbours. "the feed inventory" below checks
65
+ * this table against the models on disk, so a new feed model cannot slip
66
+ * past the label checks.
67
+ */
68
+ const FEEDS: Array<FeedEventTypeSpec> = [
69
+ {
70
+ name: "AlertEpisodeFeed",
71
+ enumName: "AlertEpisodeFeedEventType",
72
+ eventTypes: Object.values(AlertEpisodeFeedEventType),
73
+ },
74
+ {
75
+ name: "AlertFeed",
76
+ enumName: "AlertFeedEventType",
77
+ eventTypes: Object.values(AlertFeedEventType),
78
+ },
79
+ {
80
+ name: "CephClusterFeed",
81
+ enumName: "CephClusterFeedEventType",
82
+ eventTypes: Object.values(CephClusterFeedEventType),
83
+ },
84
+ {
85
+ name: "CloudResourceFeed",
86
+ enumName: "CloudResourceFeedEventType",
87
+ eventTypes: Object.values(CloudResourceFeedEventType),
88
+ },
89
+ {
90
+ name: "DockerHostFeed",
91
+ enumName: "DockerHostFeedEventType",
92
+ eventTypes: Object.values(DockerHostFeedEventType),
93
+ },
94
+ {
95
+ name: "DockerSwarmClusterFeed",
96
+ enumName: "DockerSwarmClusterFeedEventType",
97
+ eventTypes: Object.values(DockerSwarmClusterFeedEventType),
98
+ },
99
+ {
100
+ name: "HostFeed",
101
+ enumName: "HostFeedEventType",
102
+ eventTypes: Object.values(HostFeedEventType),
103
+ },
104
+ {
105
+ name: "IncidentEpisodeFeed",
106
+ enumName: "IncidentEpisodeFeedEventType",
107
+ eventTypes: Object.values(IncidentEpisodeFeedEventType),
108
+ },
109
+ {
110
+ name: "IncidentFeed",
111
+ enumName: "IncidentFeedEventType",
112
+ eventTypes: Object.values(IncidentFeedEventType),
113
+ },
114
+ {
115
+ name: "KubernetesClusterFeed",
116
+ enumName: "KubernetesClusterFeedEventType",
117
+ eventTypes: Object.values(KubernetesClusterFeedEventType),
118
+ },
119
+ {
120
+ name: "MonitorFeed",
121
+ enumName: "MonitorFeedEventType",
122
+ eventTypes: Object.values(MonitorFeedEventType),
123
+ },
124
+ {
125
+ name: "OnCallDutyPolicyFeed",
126
+ enumName: "OnCallDutyPolicyFeedEventType",
127
+ eventTypes: Object.values(OnCallDutyPolicyFeedEventType),
128
+ },
129
+ {
130
+ name: "PodmanHostFeed",
131
+ enumName: "PodmanHostFeedEventType",
132
+ eventTypes: Object.values(PodmanHostFeedEventType),
133
+ },
134
+ {
135
+ name: "ProxmoxClusterFeed",
136
+ enumName: "ProxmoxClusterFeedEventType",
137
+ eventTypes: Object.values(ProxmoxClusterFeedEventType),
138
+ },
139
+ {
140
+ name: "ScheduledMaintenanceFeed",
141
+ enumName: "ScheduledMaintenanceFeedEventType",
142
+ eventTypes: Object.values(ScheduledMaintenanceFeedEventType),
143
+ },
144
+ {
145
+ name: "ServiceFeed",
146
+ enumName: "ServiceFeedEventType",
147
+ eventTypes: Object.values(ServiceFeedEventType),
148
+ },
149
+ {
150
+ name: "ServiceLevelObjectiveFeed",
151
+ enumName: "ServiceLevelObjectiveFeedEventType",
152
+ eventTypes: Object.values(ServiceLevelObjectiveFeedEventType),
153
+ },
154
+ {
155
+ name: "VMwareVCenterFeed",
156
+ enumName: "VMwareVCenterFeedEventType",
157
+ eventTypes: Object.values(VMwareVCenterFeedEventType),
158
+ },
159
+ ];
160
+
161
+ const MODELS_DIRECTORY: string = path.resolve(
162
+ __dirname,
163
+ "..",
164
+ "..",
165
+ "..",
166
+ "Models",
167
+ "DatabaseModels",
168
+ );
169
+
170
+ /*
171
+ * A feed model is recognised by the enum of event types its timeline holds;
172
+ * that enum is exactly what the "Filter & Sort" checklist is built from.
173
+ */
174
+ const FEED_EVENT_TYPE_ENUM_PATTERN: RegExp =
175
+ /export enum (\w+FeedEventType)\b/g;
176
+
177
+ interface FeedModelOnDisk {
178
+ name: string;
179
+ enumName: string;
180
+ }
181
+
182
+ type FindFeedModelsOnDisk = () => Array<FeedModelOnDisk>;
183
+
184
+ const findFeedModelsOnDisk: FindFeedModelsOnDisk =
185
+ (): Array<FeedModelOnDisk> => {
186
+ const found: Array<FeedModelOnDisk> = [];
187
+
188
+ for (const entry of fs.readdirSync(MODELS_DIRECTORY, {
189
+ withFileTypes: true,
190
+ })) {
191
+ if (!entry.isFile() || !entry.name.endsWith(".ts")) {
192
+ continue;
193
+ }
194
+
195
+ const source: string = fs.readFileSync(
196
+ path.join(MODELS_DIRECTORY, entry.name),
197
+ "utf8",
198
+ );
199
+
200
+ for (const match of source.matchAll(FEED_EVENT_TYPE_ENUM_PATTERN)) {
201
+ found.push({
202
+ name: entry.name.replace(/\.ts$/, ""),
203
+ enumName: match[1]!,
204
+ });
205
+ }
206
+ }
207
+
208
+ return found;
209
+ };
210
+
211
+ type GetFeedIdentity = (feed: { name: string; enumName: string }) => string;
212
+
213
+ const getFeedIdentity: GetFeedIdentity = (feed: {
214
+ name: string;
215
+ enumName: string;
216
+ }): string => {
217
+ return `${feed.name}.${feed.enumName}`;
218
+ };
219
+
220
+ // "IncidentStateChanged" is several words run together; "Created" is one.
221
+ const MULTI_WORD_CAMEL_CASE: RegExp = /[a-z0-9][A-Z]/;
222
+
223
+ type GetLabels = (eventTypes: Array<string>) => Array<string>;
224
+
225
+ const getLabels: GetLabels = (eventTypes: Array<string>): Array<string> => {
226
+ return eventTypes.map((eventType: string): string => {
227
+ return getFeedEventTypeLabel(eventType);
228
+ });
229
+ };
230
+
231
+ type GetOptionValues = (options: Array<FeedEventTypeOption>) => Array<string>;
232
+
233
+ const getOptionValues: GetOptionValues = (
234
+ options: Array<FeedEventTypeOption>,
235
+ ): Array<string> => {
236
+ return options.map((option: FeedEventTypeOption): string => {
237
+ return option.value;
238
+ });
239
+ };
240
+
241
+ type GetOptionLabels = (options: Array<FeedEventTypeOption>) => Array<string>;
242
+
243
+ const getOptionLabels: GetOptionLabels = (
244
+ options: Array<FeedEventTypeOption>,
245
+ ): Array<string> => {
246
+ return options.map((option: FeedEventTypeOption): string => {
247
+ return option.label;
248
+ });
249
+ };
250
+
251
+ describe("getFeedEventTypeLabel across every dashboard feed", () => {
252
+ test("the feed inventory is exactly the *FeedEventType enums in Models/DatabaseModels", () => {
253
+ /*
254
+ * Guards every test.each below against passing on a short list: a feed
255
+ * model added later fails here until it joins FEEDS, and so gets its
256
+ * labels checked too.
257
+ */
258
+ const onDisk: Array<string> = findFeedModelsOnDisk()
259
+ .map(getFeedIdentity)
260
+ .sort();
261
+
262
+ // An empty scan would make the comparison below vacuous.
263
+ expect(onDisk.length).toBeGreaterThan(0);
264
+ expect(FEEDS.map(getFeedIdentity).sort()).toEqual(onDisk);
265
+ });
266
+
267
+ test.each(FEEDS)(
268
+ "$name: the table lists $enumName's own values",
269
+ (feed: FeedEventTypeSpec) => {
270
+ /*
271
+ * Loaded by name, so a row that pairs one model with another model's
272
+ * enum (a copy-paste slip the static imports cannot catch) fails.
273
+ */
274
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
275
+ const model: Record<string, unknown> = require(
276
+ path.join(MODELS_DIRECTORY, feed.name),
277
+ );
278
+ const eventTypeEnum: unknown = model[feed.enumName];
279
+
280
+ expect(eventTypeEnum).toBeDefined();
281
+ expect(feed.eventTypes).toEqual(
282
+ Object.values(eventTypeEnum as Record<string, string>),
283
+ );
284
+ expect(feed.eventTypes.length).toBeGreaterThan(0);
285
+ },
286
+ );
287
+
288
+ test.each(FEEDS)(
289
+ "$name: every label is a tidy, capitalised phrase",
290
+ (feed: FeedEventTypeSpec) => {
291
+ for (const eventType of feed.eventTypes) {
292
+ const label: string = getFeedEventTypeLabel(eventType);
293
+
294
+ expect(typeof label).toBe("string");
295
+ expect(label.length).toBeGreaterThan(0);
296
+ expect(label).toBe(label.trim());
297
+ expect(label).not.toContain(" ");
298
+ expect(label).toMatch(/^[A-Z]/);
299
+ }
300
+ },
301
+ );
302
+
303
+ test.each(FEEDS)(
304
+ "$name: no multi-word event type is shown as its raw enum value",
305
+ (feed: FeedEventTypeSpec) => {
306
+ const multiWordEventTypes: Array<string> = feed.eventTypes.filter(
307
+ (eventType: string) => {
308
+ return MULTI_WORD_CAMEL_CASE.test(eventType);
309
+ },
310
+ );
311
+
312
+ // Every feed event type is a phrase, so this list is never empty.
313
+ expect(multiWordEventTypes.length).toBeGreaterThan(0);
314
+
315
+ for (const eventType of multiWordEventTypes) {
316
+ const label: string = getFeedEventTypeLabel(eventType);
317
+
318
+ expect(label).not.toBe(eventType);
319
+ // Split into words, or joined as one ("Auto-Remediation").
320
+ expect(label).toMatch(/[ -]/);
321
+ }
322
+ },
323
+ );
324
+
325
+ test.each(FEEDS)(
326
+ "$name: labels are unique, so no two checkboxes read the same",
327
+ (feed: FeedEventTypeSpec) => {
328
+ const labels: Array<string> = getLabels(feed.eventTypes);
329
+
330
+ expect(new Set<string>(labels).size).toBe(labels.length);
331
+ },
332
+ );
333
+
334
+ test.each(FEEDS)(
335
+ "$name: the checklist has one alphabetical entry per event type",
336
+ (feed: FeedEventTypeSpec) => {
337
+ const options: Array<FeedEventTypeOption> = getFeedEventTypeOptions({
338
+ eventTypes: feed.eventTypes,
339
+ });
340
+ const labels: Array<string> = getOptionLabels(options);
341
+
342
+ expect([...getOptionValues(options)].sort()).toEqual(
343
+ [...feed.eventTypes].sort(),
344
+ );
345
+ expect(new Set<string>(labels).size).toBe(options.length);
346
+
347
+ for (let index: number = 1; index < labels.length; index++) {
348
+ expect(
349
+ labels[index - 1]!.localeCompare(labels[index]!),
350
+ ).toBeLessThanOrEqual(0);
351
+ }
352
+ },
353
+ );
354
+ });
355
+
356
+ describe("getFeedEventTypeLabel", () => {
357
+ const cases: Array<[string, string]> = [
358
+ // Plain camel case splits into title-case words.
359
+ [IncidentFeedEventType.IncidentStateChanged, "Incident State Changed"],
360
+ [IncidentFeedEventType.IncidentCreated, "Incident Created"],
361
+ [IncidentFeedEventType.RootCause, "Root Cause"],
362
+ [IncidentFeedEventType.PostmortemNote, "Postmortem Note"],
363
+ [IncidentFeedEventType.IncidentMemberAdded, "Incident Member Added"],
364
+ [
365
+ IncidentFeedEventType.SubscriberNotificationSent,
366
+ "Subscriber Notification Sent",
367
+ ],
368
+ [IncidentFeedEventType.LabelRuleExecuted, "Label Rule Executed"],
369
+ [MonitorFeedEventType.MonitorStatusChanged, "Monitor Status Changed"],
370
+ [
371
+ DockerSwarmClusterFeedEventType.DockerSwarmClusterArchived,
372
+ "Docker Swarm Cluster Archived",
373
+ ],
374
+ [OnCallDutyPolicyFeedEventType.RosterHandoff, "Roster Handoff"],
375
+ [OnCallDutyPolicyFeedEventType.CoverageGapStarted, "Coverage Gap Started"],
376
+ [OnCallDutyPolicyFeedEventType.UserAdded, "User Added"],
377
+ [
378
+ ServiceLevelObjectiveFeedEventType.BurnRateAlertRaised,
379
+ "Burn Rate Alert Raised",
380
+ ],
381
+ [ServiceLevelObjectiveFeedEventType.MonitorsAttached, "Monitors Attached"],
382
+ // "OnCall" reads "On-Call", wherever it sits in the value.
383
+ [IncidentFeedEventType.OnCallPolicy, "On-Call Policy"],
384
+ [IncidentFeedEventType.OnCallNotification, "On-Call Notification"],
385
+ [IncidentFeedEventType.OnCallRuleExecuted, "On-Call Rule Executed"],
386
+ [
387
+ OnCallDutyPolicyFeedEventType.OnCallDutyPolicyCreated,
388
+ "On-Call Duty Policy Created",
389
+ ],
390
+ [
391
+ OnCallDutyPolicyFeedEventType.OnCallDutyScheduleAdded,
392
+ "On-Call Duty Schedule Added",
393
+ ],
394
+ // "VMware" and "vCenter" keep their product spelling.
395
+ [VMwareVCenterFeedEventType.VMwareVCenterCreated, "VMware vCenter Created"],
396
+ [
397
+ VMwareVCenterFeedEventType.VMwareVCenterRestored,
398
+ "VMware vCenter Restored",
399
+ ],
400
+ // The SLO feed's long prefix is "SLO" everywhere else in the dashboard.
401
+ [
402
+ ServiceLevelObjectiveFeedEventType.ServiceLevelObjectiveCreated,
403
+ "SLO Created",
404
+ ],
405
+ [
406
+ ServiceLevelObjectiveFeedEventType.ServiceLevelObjectiveDisabled,
407
+ "SLO Disabled",
408
+ ],
409
+ // Joining words stay lower case inside a title.
410
+ [AlertFeedEventType.AddedToEpisode, "Added to Episode"],
411
+ [AlertFeedEventType.RemovedFromEpisode, "Removed from Episode"],
412
+ // Shared owner events read as a sentence, not "Owner User Added".
413
+ [IncidentFeedEventType.OwnerUserAdded, "User Added as Owner"],
414
+ [IncidentFeedEventType.OwnerUserRemoved, "User Removed as Owner"],
415
+ [IncidentFeedEventType.OwnerTeamAdded, "Team Added as Owner"],
416
+ [IncidentFeedEventType.OwnerTeamRemoved, "Team Removed as Owner"],
417
+ [IncidentFeedEventType.AutoRemediation, "Auto-Remediation"],
418
+ // Values no feed has yet still get a sensible label.
419
+ ["Escalated", "Escalated"],
420
+ ["escalated", "Escalated"],
421
+ ["URLChanged", "URL Changed"],
422
+ ["PutOnHold", "Put on Hold"],
423
+ ["OnHold", "On Hold"],
424
+ ["", ""],
425
+ ];
426
+
427
+ test.each(cases)("%p reads %p", (eventType: string, label: string) => {
428
+ expect(getFeedEventTypeLabel(eventType)).toBe(label);
429
+ });
430
+
431
+ test("a joining word that opens the label stays capitalised", () => {
432
+ expect(getFeedEventTypeLabel("ToBeReviewed")).toBe("To Be Reviewed");
433
+ });
434
+
435
+ test("an owner event keeps its sentence label in every feed that has it", () => {
436
+ const feedsWithOwnerEvents: Array<FeedEventTypeSpec> = FEEDS.filter(
437
+ (feed: FeedEventTypeSpec) => {
438
+ return feed.eventTypes.includes(IncidentFeedEventType.OwnerUserAdded);
439
+ },
440
+ );
441
+
442
+ expect(feedsWithOwnerEvents.length).toBeGreaterThan(1);
443
+
444
+ for (const feed of feedsWithOwnerEvents) {
445
+ const options: Array<FeedEventTypeOption> = getFeedEventTypeOptions({
446
+ eventTypes: feed.eventTypes,
447
+ });
448
+
449
+ expect(
450
+ options.find((option: FeedEventTypeOption) => {
451
+ return option.value === IncidentFeedEventType.OwnerUserAdded;
452
+ })?.label,
453
+ ).toBe("User Added as Owner");
454
+ }
455
+ });
456
+ });
457
+
458
+ describe("getFeedEventTypeOptions", () => {
459
+ test("sorts by label, not by value", () => {
460
+ const options: Array<FeedEventTypeOption> = getFeedEventTypeOptions({
461
+ eventTypes: [
462
+ IncidentFeedEventType.RootCause,
463
+ AlertFeedEventType.AddedToEpisode,
464
+ IncidentFeedEventType.OwnerUserAdded,
465
+ IncidentFeedEventType.PublicNote,
466
+ ],
467
+ });
468
+
469
+ /*
470
+ * By value "OwnerUserAdded" would come before "PublicNote"; its label
471
+ * "User Added as Owner" puts it last.
472
+ */
473
+ expect(options).toEqual([
474
+ { value: "AddedToEpisode", label: "Added to Episode", icon: undefined },
475
+ { value: "PublicNote", label: "Public Note", icon: undefined },
476
+ { value: "RootCause", label: "Root Cause", icon: undefined },
477
+ {
478
+ value: "OwnerUserAdded",
479
+ label: "User Added as Owner",
480
+ icon: undefined,
481
+ },
482
+ ]);
483
+ });
484
+
485
+ test("lists each event type once and drops empty values", () => {
486
+ const options: Array<FeedEventTypeOption> = getFeedEventTypeOptions({
487
+ eventTypes: [
488
+ IncidentFeedEventType.PrivateNote,
489
+ "",
490
+ IncidentFeedEventType.IncidentCreated,
491
+ IncidentFeedEventType.PrivateNote,
492
+ "",
493
+ IncidentFeedEventType.IncidentCreated,
494
+ ],
495
+ });
496
+
497
+ expect(getOptionValues(options)).toEqual([
498
+ IncidentFeedEventType.IncidentCreated,
499
+ IncidentFeedEventType.PrivateNote,
500
+ ]);
501
+ });
502
+
503
+ test("an empty list gives an empty checklist", () => {
504
+ expect(getFeedEventTypeOptions({ eventTypes: [] })).toEqual([]);
505
+ expect(getFeedEventTypeOptions({ eventTypes: ["", ""] })).toEqual([]);
506
+ });
507
+
508
+ test("does not reorder the caller's list", () => {
509
+ const eventTypes: Array<string> = [
510
+ IncidentFeedEventType.RootCause,
511
+ IncidentFeedEventType.IncidentCreated,
512
+ IncidentFeedEventType.RootCause,
513
+ ];
514
+
515
+ getFeedEventTypeOptions({ eventTypes });
516
+
517
+ expect(eventTypes).toEqual([
518
+ IncidentFeedEventType.RootCause,
519
+ IncidentFeedEventType.IncidentCreated,
520
+ IncidentFeedEventType.RootCause,
521
+ ]);
522
+ });
523
+
524
+ test("uses the feed's own label, and sorts by it", () => {
525
+ const options: Array<FeedEventTypeOption> = getFeedEventTypeOptions({
526
+ eventTypes: [
527
+ IncidentFeedEventType.PublicNote,
528
+ IncidentFeedEventType.RootCause,
529
+ ],
530
+ getEventTypeLabel: (eventType: string): string | undefined => {
531
+ return eventType === IncidentFeedEventType.PublicNote
532
+ ? "Status Page Note"
533
+ : undefined;
534
+ },
535
+ });
536
+
537
+ expect(options).toEqual([
538
+ { value: "RootCause", label: "Root Cause", icon: undefined },
539
+ { value: "PublicNote", label: "Status Page Note", icon: undefined },
540
+ ]);
541
+ });
542
+
543
+ test("falls back to the split label when the feed has no name for an event", () => {
544
+ const options: Array<FeedEventTypeOption> = getFeedEventTypeOptions({
545
+ eventTypes: [
546
+ IncidentFeedEventType.OnCallPolicy,
547
+ IncidentFeedEventType.IncidentStateChanged,
548
+ ],
549
+ getEventTypeLabel: (): string | undefined => {
550
+ return undefined;
551
+ },
552
+ });
553
+
554
+ expect(getOptionLabels(options)).toEqual([
555
+ "Incident State Changed",
556
+ "On-Call Policy",
557
+ ]);
558
+ });
559
+
560
+ test("an empty label from the feed also falls back, so no checkbox is blank", () => {
561
+ const options: Array<FeedEventTypeOption> = getFeedEventTypeOptions({
562
+ eventTypes: [IncidentFeedEventType.RootCause],
563
+ getEventTypeLabel: (): string | undefined => {
564
+ return "";
565
+ },
566
+ });
567
+
568
+ expect(getOptionLabels(options)).toEqual(["Root Cause"]);
569
+ });
570
+
571
+ test("asks the feed for each event type's label once", () => {
572
+ const askedFor: Array<string> = [];
573
+
574
+ getFeedEventTypeOptions({
575
+ eventTypes: [
576
+ IncidentFeedEventType.RootCause,
577
+ "",
578
+ IncidentFeedEventType.RootCause,
579
+ IncidentFeedEventType.PublicNote,
580
+ ],
581
+ getEventTypeLabel: (eventType: string): string | undefined => {
582
+ askedFor.push(eventType);
583
+ return undefined;
584
+ },
585
+ });
586
+
587
+ expect(askedFor.sort()).toEqual([
588
+ IncidentFeedEventType.PublicNote,
589
+ IncidentFeedEventType.RootCause,
590
+ ]);
591
+ });
592
+
593
+ test("attaches the feed's icon to each entry", () => {
594
+ const options: Array<FeedEventTypeOption> = getFeedEventTypeOptions({
595
+ eventTypes: [
596
+ IncidentFeedEventType.OnCallNotification,
597
+ IncidentFeedEventType.PrivateNote,
598
+ IncidentFeedEventType.RootCause,
599
+ ],
600
+ getEventTypeIcon: (eventType: string): IconProp | undefined => {
601
+ if (eventType === IncidentFeedEventType.OnCallNotification) {
602
+ return IconProp.Bell;
603
+ }
604
+
605
+ if (eventType === IncidentFeedEventType.PrivateNote) {
606
+ return IconProp.Info;
607
+ }
608
+
609
+ return undefined;
610
+ },
611
+ });
612
+
613
+ expect(options).toEqual([
614
+ {
615
+ value: "OnCallNotification",
616
+ label: "On-Call Notification",
617
+ icon: IconProp.Bell,
618
+ },
619
+ { value: "PrivateNote", label: "Private Note", icon: IconProp.Info },
620
+ { value: "RootCause", label: "Root Cause", icon: undefined },
621
+ ]);
622
+ });
623
+
624
+ test("leaves the icon undefined when the feed has no icons", () => {
625
+ const options: Array<FeedEventTypeOption> = getFeedEventTypeOptions({
626
+ eventTypes: Object.values(MonitorFeedEventType),
627
+ });
628
+
629
+ expect(options.length).toBe(Object.values(MonitorFeedEventType).length);
630
+
631
+ for (const option of options) {
632
+ expect(option.icon).toBeUndefined();
633
+ }
634
+ });
635
+ });
636
+
637
+ describe("sanitizeFeedEventTypes", () => {
638
+ const knownEventTypes: Array<string> = [
639
+ IncidentFeedEventType.IncidentCreated,
640
+ IncidentFeedEventType.IncidentStateChanged,
641
+ IncidentFeedEventType.PublicNote,
642
+ IncidentFeedEventType.RootCause,
643
+ ];
644
+
645
+ test("returns the selection in the feed's order, whatever order it was ticked in", () => {
646
+ const expected: Array<string> = [
647
+ IncidentFeedEventType.IncidentCreated,
648
+ IncidentFeedEventType.PublicNote,
649
+ IncidentFeedEventType.RootCause,
650
+ ];
651
+
652
+ expect(
653
+ sanitizeFeedEventTypes({
654
+ selectedEventTypes: [
655
+ IncidentFeedEventType.RootCause,
656
+ IncidentFeedEventType.IncidentCreated,
657
+ IncidentFeedEventType.PublicNote,
658
+ ],
659
+ knownEventTypes,
660
+ }),
661
+ ).toEqual(expected);
662
+
663
+ expect(
664
+ sanitizeFeedEventTypes({
665
+ selectedEventTypes: [
666
+ IncidentFeedEventType.PublicNote,
667
+ IncidentFeedEventType.RootCause,
668
+ IncidentFeedEventType.IncidentCreated,
669
+ ],
670
+ knownEventTypes,
671
+ }),
672
+ ).toEqual(expected);
673
+ });
674
+
675
+ test("keeps each event type once", () => {
676
+ expect(
677
+ sanitizeFeedEventTypes({
678
+ selectedEventTypes: [
679
+ IncidentFeedEventType.RootCause,
680
+ IncidentFeedEventType.RootCause,
681
+ IncidentFeedEventType.IncidentCreated,
682
+ IncidentFeedEventType.RootCause,
683
+ ],
684
+ knownEventTypes,
685
+ }),
686
+ ).toEqual([
687
+ IncidentFeedEventType.IncidentCreated,
688
+ IncidentFeedEventType.RootCause,
689
+ ]);
690
+ });
691
+
692
+ test("keeps each event type once even when the feed's own list repeats it", () => {
693
+ expect(
694
+ sanitizeFeedEventTypes({
695
+ selectedEventTypes: [IncidentFeedEventType.RootCause],
696
+ knownEventTypes: [
697
+ IncidentFeedEventType.RootCause,
698
+ IncidentFeedEventType.PublicNote,
699
+ IncidentFeedEventType.RootCause,
700
+ ],
701
+ }),
702
+ ).toEqual([IncidentFeedEventType.RootCause]);
703
+ });
704
+
705
+ test("drops event types the feed does not have", () => {
706
+ expect(
707
+ sanitizeFeedEventTypes({
708
+ selectedEventTypes: [
709
+ "NotAnEventType",
710
+ IncidentFeedEventType.PublicNote,
711
+ MonitorFeedEventType.MonitorStatusChanged,
712
+ "",
713
+ ],
714
+ knownEventTypes,
715
+ }),
716
+ ).toEqual([IncidentFeedEventType.PublicNote]);
717
+ });
718
+
719
+ test("an empty selection stays empty - no filter", () => {
720
+ expect(
721
+ sanitizeFeedEventTypes({ selectedEventTypes: [], knownEventTypes }),
722
+ ).toEqual([]);
723
+ });
724
+
725
+ test("a selection made only of unknown event types becomes no filter", () => {
726
+ expect(
727
+ sanitizeFeedEventTypes({
728
+ selectedEventTypes: ["NotAnEventType"],
729
+ knownEventTypes,
730
+ }),
731
+ ).toEqual([]);
732
+ });
733
+
734
+ test("a feed with no event types keeps nothing", () => {
735
+ expect(
736
+ sanitizeFeedEventTypes({
737
+ selectedEventTypes: [IncidentFeedEventType.RootCause],
738
+ knownEventTypes: [],
739
+ }),
740
+ ).toEqual([]);
741
+ });
742
+
743
+ test("does not change the lists it is given", () => {
744
+ const selectedEventTypes: Array<string> = [
745
+ IncidentFeedEventType.RootCause,
746
+ "NotAnEventType",
747
+ ];
748
+ const known: Array<string> = [...knownEventTypes];
749
+
750
+ sanitizeFeedEventTypes({ selectedEventTypes, knownEventTypes: known });
751
+
752
+ expect(selectedEventTypes).toEqual([
753
+ IncidentFeedEventType.RootCause,
754
+ "NotAnEventType",
755
+ ]);
756
+ expect(known).toEqual(knownEventTypes);
757
+ });
758
+ });
759
+
760
+ describe("isFeedSortOrder", () => {
761
+ test("accepts both sort orders", () => {
762
+ expect(isFeedSortOrder(SortOrder.Ascending)).toBe(true);
763
+ expect(isFeedSortOrder(SortOrder.Descending)).toBe(true);
764
+ });
765
+
766
+ test("accepts the stored string form of both sort orders", () => {
767
+ // What localStorage hands back after a reload.
768
+ expect(isFeedSortOrder("ASC")).toBe(true);
769
+ expect(isFeedSortOrder("DESC")).toBe(true);
770
+ });
771
+
772
+ test("rejects anything else", () => {
773
+ const values: Array<unknown> = [
774
+ "asc",
775
+ "desc",
776
+ "Ascending",
777
+ "Descending",
778
+ "",
779
+ undefined,
780
+ null,
781
+ 1,
782
+ -1,
783
+ true,
784
+ {},
785
+ [SortOrder.Ascending],
786
+ ];
787
+
788
+ for (const value of values) {
789
+ expect(isFeedSortOrder(value)).toBe(false);
790
+ }
791
+ });
792
+ });
793
+
794
+ describe("getFeedOptionsKey", () => {
795
+ test("equal options give equal keys", () => {
796
+ expect(
797
+ getFeedOptionsKey({
798
+ sortOrder: SortOrder.Ascending,
799
+ eventTypes: [
800
+ IncidentFeedEventType.PublicNote,
801
+ IncidentFeedEventType.RootCause,
802
+ ],
803
+ }),
804
+ ).toBe(
805
+ getFeedOptionsKey({
806
+ sortOrder: SortOrder.Ascending,
807
+ eventTypes: [
808
+ IncidentFeedEventType.PublicNote,
809
+ IncidentFeedEventType.RootCause,
810
+ ],
811
+ }),
812
+ );
813
+
814
+ expect(getFeedOptionsKey(DEFAULT_FEED_OPTIONS)).toBe(
815
+ getFeedOptionsKey({ sortOrder: SortOrder.Descending, eventTypes: [] }),
816
+ );
817
+ });
818
+
819
+ test("any change of sort order or filter gives a different key", () => {
820
+ const eventTypes: Array<string> = [
821
+ IncidentFeedEventType.IncidentCreated,
822
+ IncidentFeedEventType.PublicNote,
823
+ IncidentFeedEventType.RootCause,
824
+ ];
825
+ const keys: Array<string> = [];
826
+
827
+ // Every sort order with every subset of the three event types.
828
+ for (const sortOrder of [SortOrder.Descending, SortOrder.Ascending]) {
829
+ for (let mask: number = 0; mask < 1 << eventTypes.length; mask++) {
830
+ keys.push(
831
+ getFeedOptionsKey({
832
+ sortOrder,
833
+ eventTypes: eventTypes.filter(
834
+ (_eventType: string, index: number) => {
835
+ return (mask & (1 << index)) !== 0;
836
+ },
837
+ ),
838
+ }),
839
+ );
840
+ }
841
+ }
842
+
843
+ expect(keys.length).toBe(16);
844
+ expect(new Set<string>(keys).size).toBe(keys.length);
845
+ });
846
+
847
+ test("a sort order change is not mistaken for a filter change", () => {
848
+ const sortedOnly: string = getFeedOptionsKey({
849
+ sortOrder: SortOrder.Ascending,
850
+ eventTypes: [],
851
+ });
852
+ const filteredOnly: string = getFeedOptionsKey({
853
+ sortOrder: SortOrder.Descending,
854
+ eventTypes: [SortOrder.Ascending],
855
+ });
856
+
857
+ expect(sortedOnly).not.toBe(filteredOnly);
858
+ expect(sortedOnly).not.toBe(getFeedOptionsKey(DEFAULT_FEED_OPTIONS));
859
+ expect(filteredOnly).not.toBe(getFeedOptionsKey(DEFAULT_FEED_OPTIONS));
860
+ });
861
+
862
+ test("a sanitized selection gives the same key whatever order it was ticked in", () => {
863
+ const knownEventTypes: Array<string> = Object.values(IncidentFeedEventType);
864
+
865
+ const tickedOneWay: string = getFeedOptionsKey({
866
+ sortOrder: SortOrder.Descending,
867
+ eventTypes: sanitizeFeedEventTypes({
868
+ selectedEventTypes: [
869
+ IncidentFeedEventType.RootCause,
870
+ IncidentFeedEventType.PublicNote,
871
+ ],
872
+ knownEventTypes,
873
+ }),
874
+ });
875
+ const tickedTheOther: string = getFeedOptionsKey({
876
+ sortOrder: SortOrder.Descending,
877
+ eventTypes: sanitizeFeedEventTypes({
878
+ selectedEventTypes: [
879
+ IncidentFeedEventType.PublicNote,
880
+ IncidentFeedEventType.RootCause,
881
+ ],
882
+ knownEventTypes,
883
+ }),
884
+ });
885
+
886
+ expect(tickedOneWay).toBe(tickedTheOther);
887
+ });
888
+ });
889
+
890
+ describe("isFeedFiltered", () => {
891
+ test("no event types means no filter", () => {
892
+ expect(isFeedFiltered(DEFAULT_FEED_OPTIONS)).toBe(false);
893
+ expect(
894
+ isFeedFiltered({ sortOrder: SortOrder.Ascending, eventTypes: [] }),
895
+ ).toBe(false);
896
+ });
897
+
898
+ test("any event type means a filter", () => {
899
+ expect(
900
+ isFeedFiltered({
901
+ sortOrder: SortOrder.Descending,
902
+ eventTypes: [IncidentFeedEventType.RootCause],
903
+ }),
904
+ ).toBe(true);
905
+ expect(
906
+ isFeedFiltered({
907
+ sortOrder: SortOrder.Ascending,
908
+ eventTypes: Object.values(IncidentFeedEventType),
909
+ }),
910
+ ).toBe(true);
911
+ });
912
+ });
913
+
914
+ describe("isDefaultFeedOptions", () => {
915
+ test("newest first with no filter is the default", () => {
916
+ expect(isDefaultFeedOptions(DEFAULT_FEED_OPTIONS)).toBe(true);
917
+ expect(
918
+ isDefaultFeedOptions({ sortOrder: SortOrder.Descending, eventTypes: [] }),
919
+ ).toBe(true);
920
+ });
921
+
922
+ test("oldest first is not the default", () => {
923
+ expect(
924
+ isDefaultFeedOptions({ sortOrder: SortOrder.Ascending, eventTypes: [] }),
925
+ ).toBe(false);
926
+ });
927
+
928
+ test("a filter is not the default", () => {
929
+ expect(
930
+ isDefaultFeedOptions({
931
+ sortOrder: SortOrder.Descending,
932
+ eventTypes: [IncidentFeedEventType.RootCause],
933
+ }),
934
+ ).toBe(false);
935
+ expect(
936
+ isDefaultFeedOptions({
937
+ sortOrder: SortOrder.Ascending,
938
+ eventTypes: [IncidentFeedEventType.RootCause],
939
+ }),
940
+ ).toBe(false);
941
+ });
942
+ });
943
+
944
+ describe("feed option defaults", () => {
945
+ test("a feed opens newest first with no filter", () => {
946
+ expect(DEFAULT_FEED_SORT_ORDER).toBe(SortOrder.Descending);
947
+ expect(DEFAULT_FEED_OPTIONS).toEqual({
948
+ sortOrder: SortOrder.Descending,
949
+ eventTypes: [],
950
+ });
951
+ });
952
+
953
+ test("the sort choices are newest first, then oldest first", () => {
954
+ expect(
955
+ FEED_SORT_ORDER_OPTIONS.map(
956
+ (option: FeedSortOrderOption): { value: SortOrder; label: string } => {
957
+ return { value: option.value, label: option.label };
958
+ },
959
+ ),
960
+ ).toEqual([
961
+ { value: SortOrder.Descending, label: "Newest first" },
962
+ { value: SortOrder.Ascending, label: "Oldest first" },
963
+ ]);
964
+ });
965
+
966
+ test("each sort choice names a different IconProp", () => {
967
+ /*
968
+ * This only guards the table. Whether the two IconProps also DRAW
969
+ * different glyphs (BarsArrowUp was once a copy of BarsArrowDown) is
970
+ * checked where the icon is rendered, in the button and Icon tests.
971
+ */
972
+ const icons: Array<IconProp> = FEED_SORT_ORDER_OPTIONS.map(
973
+ (option: FeedSortOrderOption): IconProp => {
974
+ return option.icon;
975
+ },
976
+ );
977
+
978
+ for (const icon of icons) {
979
+ expect(Object.values(IconProp)).toContain(icon);
980
+ }
981
+
982
+ expect(new Set<IconProp>(icons).size).toBe(icons.length);
983
+ });
984
+
985
+ test("the default sort order is one of the choices", () => {
986
+ expect(
987
+ FEED_SORT_ORDER_OPTIONS.some((option: FeedSortOrderOption) => {
988
+ return option.value === DEFAULT_FEED_SORT_ORDER;
989
+ }),
990
+ ).toBe(true);
991
+ });
992
+ });
993
+
994
+ describe("getFeedEventTypeQuery", () => {
995
+ test("an unfiltered feed adds nothing to the query", () => {
996
+ expect(
997
+ getFeedEventTypeQuery<IncidentFeed>(
998
+ "incidentFeedEventType",
999
+ DEFAULT_FEED_OPTIONS,
1000
+ ),
1001
+ ).toEqual({});
1002
+
1003
+ const query: Query<IncidentFeed> = getFeedEventTypeQuery<IncidentFeed>(
1004
+ "incidentFeedEventType",
1005
+ { sortOrder: SortOrder.Ascending, eventTypes: [] },
1006
+ );
1007
+
1008
+ expect(Object.keys(query)).toEqual([]);
1009
+ });
1010
+
1011
+ test("a filtered feed asks for the selected event types in its own column", () => {
1012
+ const options: FeedOptions = {
1013
+ sortOrder: SortOrder.Descending,
1014
+ eventTypes: [
1015
+ IncidentFeedEventType.IncidentStateChanged,
1016
+ IncidentFeedEventType.RootCause,
1017
+ ],
1018
+ };
1019
+
1020
+ const query: Record<string, unknown> = getFeedEventTypeQuery<IncidentFeed>(
1021
+ "incidentFeedEventType",
1022
+ options,
1023
+ ) as unknown as Record<string, unknown>;
1024
+
1025
+ expect(Object.keys(query)).toEqual(["incidentFeedEventType"]);
1026
+
1027
+ const includes: unknown = query["incidentFeedEventType"];
1028
+
1029
+ expect(includes).toBeInstanceOf(Includes);
1030
+ expect((includes as Includes).values).toEqual([
1031
+ IncidentFeedEventType.IncidentStateChanged,
1032
+ IncidentFeedEventType.RootCause,
1033
+ ]);
1034
+ });
1035
+
1036
+ test("the key follows the column it is given", () => {
1037
+ const query: Record<string, unknown> = getFeedEventTypeQuery<MonitorFeed>(
1038
+ "monitorFeedEventType",
1039
+ {
1040
+ sortOrder: SortOrder.Ascending,
1041
+ eventTypes: [MonitorFeedEventType.MonitorStatusChanged],
1042
+ },
1043
+ ) as unknown as Record<string, unknown>;
1044
+
1045
+ expect(Object.keys(query)).toEqual(["monitorFeedEventType"]);
1046
+ expect((query["monitorFeedEventType"] as Includes).values).toEqual([
1047
+ MonitorFeedEventType.MonitorStatusChanged,
1048
+ ]);
1049
+ });
1050
+
1051
+ test("the sort order does not leak into the query", () => {
1052
+ const eventTypes: Array<string> = [IncidentFeedEventType.PublicNote];
1053
+
1054
+ const newestFirst: Record<string, unknown> =
1055
+ getFeedEventTypeQuery<IncidentFeed>("incidentFeedEventType", {
1056
+ sortOrder: SortOrder.Descending,
1057
+ eventTypes,
1058
+ }) as unknown as Record<string, unknown>;
1059
+ const oldestFirst: Record<string, unknown> =
1060
+ getFeedEventTypeQuery<IncidentFeed>("incidentFeedEventType", {
1061
+ sortOrder: SortOrder.Ascending,
1062
+ eventTypes,
1063
+ }) as unknown as Record<string, unknown>;
1064
+
1065
+ expect((newestFirst["incidentFeedEventType"] as Includes).toJSON()).toEqual(
1066
+ (oldestFirst["incidentFeedEventType"] as Includes).toJSON(),
1067
+ );
1068
+ });
1069
+
1070
+ test("holds a copy of the selection, not the reader's live list", () => {
1071
+ const options: FeedOptions = {
1072
+ sortOrder: SortOrder.Descending,
1073
+ eventTypes: [IncidentFeedEventType.PublicNote],
1074
+ };
1075
+
1076
+ const query: Record<string, unknown> = getFeedEventTypeQuery<IncidentFeed>(
1077
+ "incidentFeedEventType",
1078
+ options,
1079
+ ) as unknown as Record<string, unknown>;
1080
+ const includes: Includes = query["incidentFeedEventType"] as Includes;
1081
+
1082
+ expect(includes.values).not.toBe(options.eventTypes);
1083
+
1084
+ options.eventTypes.push(IncidentFeedEventType.RootCause);
1085
+ options.eventTypes[0] = IncidentFeedEventType.PrivateNote;
1086
+
1087
+ expect(includes.values).toEqual([IncidentFeedEventType.PublicNote]);
1088
+ });
1089
+
1090
+ test("serialises as an Includes operator for the API", () => {
1091
+ const query: Record<string, unknown> = getFeedEventTypeQuery<IncidentFeed>(
1092
+ "incidentFeedEventType",
1093
+ {
1094
+ sortOrder: SortOrder.Descending,
1095
+ eventTypes: [
1096
+ IncidentFeedEventType.OnCallPolicy,
1097
+ IncidentFeedEventType.OwnerUserAdded,
1098
+ ],
1099
+ },
1100
+ ) as unknown as Record<string, unknown>;
1101
+
1102
+ expect((query["incidentFeedEventType"] as Includes).toJSON()).toEqual({
1103
+ _type: ObjectType.Includes,
1104
+ value: [
1105
+ IncidentFeedEventType.OnCallPolicy,
1106
+ IncidentFeedEventType.OwnerUserAdded,
1107
+ ],
1108
+ });
1109
+ });
1110
+ });
1111
+
1112
+ describe("getFeedNoItemsMessage", () => {
1113
+ const noItemsMessage: string = "No activity yet for this incident.";
1114
+
1115
+ test("an unfiltered feed keeps its own message", () => {
1116
+ expect(
1117
+ getFeedNoItemsMessage({ options: DEFAULT_FEED_OPTIONS, noItemsMessage }),
1118
+ ).toBe(noItemsMessage);
1119
+ expect(
1120
+ getFeedNoItemsMessage({
1121
+ options: { sortOrder: SortOrder.Ascending, eventTypes: [] },
1122
+ noItemsMessage,
1123
+ }),
1124
+ ).toBe(noItemsMessage);
1125
+ });
1126
+
1127
+ test("a filtered feed says the filter matched nothing", () => {
1128
+ // Otherwise the check below would pass on a blank empty state.
1129
+ expect(FILTERED_FEED_NO_ITEMS_MESSAGE.trim()).not.toBe("");
1130
+
1131
+ expect(
1132
+ getFeedNoItemsMessage({
1133
+ options: {
1134
+ sortOrder: SortOrder.Descending,
1135
+ eventTypes: [IncidentFeedEventType.RootCause],
1136
+ },
1137
+ noItemsMessage,
1138
+ }),
1139
+ ).toBe(FILTERED_FEED_NO_ITEMS_MESSAGE);
1140
+ });
1141
+ });
1142
+
1143
+ describe("getFeedOptionsSummary", () => {
1144
+ test("an unfiltered feed shows all event types", () => {
1145
+ expect(
1146
+ getFeedOptionsSummary({
1147
+ options: DEFAULT_FEED_OPTIONS,
1148
+ eventTypeCount: 12,
1149
+ }),
1150
+ ).toBe("Newest first, all event types");
1151
+ });
1152
+
1153
+ test("names the oldest-first order", () => {
1154
+ expect(
1155
+ getFeedOptionsSummary({
1156
+ options: { sortOrder: SortOrder.Ascending, eventTypes: [] },
1157
+ eventTypeCount: 12,
1158
+ }),
1159
+ ).toBe("Oldest first, all event types");
1160
+ });
1161
+
1162
+ test("counts the selection against the feed's event types", () => {
1163
+ expect(
1164
+ getFeedOptionsSummary({
1165
+ options: {
1166
+ sortOrder: SortOrder.Descending,
1167
+ eventTypes: [
1168
+ IncidentFeedEventType.PublicNote,
1169
+ IncidentFeedEventType.RootCause,
1170
+ ],
1171
+ },
1172
+ eventTypeCount: 5,
1173
+ }),
1174
+ ).toBe("Newest first, 2 of 5 event types");
1175
+
1176
+ expect(
1177
+ getFeedOptionsSummary({
1178
+ options: {
1179
+ sortOrder: SortOrder.Ascending,
1180
+ eventTypes: [IncidentFeedEventType.PublicNote],
1181
+ },
1182
+ eventTypeCount: 5,
1183
+ }),
1184
+ ).toBe("Oldest first, 1 of 5 event types");
1185
+ });
1186
+
1187
+ test("a feed with one event type reads in the singular", () => {
1188
+ expect(
1189
+ getFeedOptionsSummary({
1190
+ options: {
1191
+ sortOrder: SortOrder.Descending,
1192
+ eventTypes: [IncidentFeedEventType.PublicNote],
1193
+ },
1194
+ eventTypeCount: 1,
1195
+ }),
1196
+ ).toBe("Newest first, 1 of 1 event type");
1197
+ });
1198
+
1199
+ test("an unknown sort order reads as the default", () => {
1200
+ expect(
1201
+ getFeedOptionsSummary({
1202
+ options: {
1203
+ sortOrder: "sideways" as unknown as SortOrder,
1204
+ eventTypes: [],
1205
+ },
1206
+ eventTypeCount: 3,
1207
+ }),
1208
+ ).toBe("Newest first, all event types");
1209
+ });
1210
+ });
1211
+
1212
+ describe("event types named like Object.prototype members", () => {
1213
+ /*
1214
+ * The shared label table is a Map. As an object literal, looking up
1215
+ * "constructor" or "toString" returned the inherited function, and the
1216
+ * checklist's sort then threw on a.label.localeCompare.
1217
+ */
1218
+ test.each<[string, string]>([
1219
+ ["constructor", "Constructor"],
1220
+ ["toString", "To String"],
1221
+ ["valueOf", "Value of"],
1222
+ ["hasOwnProperty", "Has Own Property"],
1223
+ ["__proto__", "Proto"],
1224
+ ])("%s gets a string label", (eventType: string, label: string) => {
1225
+ const result: unknown = getFeedEventTypeLabel(eventType);
1226
+
1227
+ expect(typeof result).toBe("string");
1228
+ expect(result).toBe(label);
1229
+ });
1230
+
1231
+ test("a checklist containing them still builds and sorts", () => {
1232
+ const options: Array<FeedEventTypeOption> = getFeedEventTypeOptions({
1233
+ eventTypes: ["toString", "constructor", "OwnerUserAdded"],
1234
+ });
1235
+
1236
+ expect(
1237
+ options.map((option: FeedEventTypeOption) => {
1238
+ return option.label;
1239
+ }),
1240
+ ).toEqual(["Constructor", "To String", "User Added as Owner"]);
1241
+ });
1242
+ });
1243
+
1244
+ describe("translateFeedOptionsText", () => {
1245
+ test("returns the English text when there is no translator", () => {
1246
+ expect(translateFeedOptionsText({ text: "Sort by time" })).toBe(
1247
+ "Sort by time",
1248
+ );
1249
+ });
1250
+
1251
+ test("uses the translation when there is one", () => {
1252
+ expect(
1253
+ translateFeedOptionsText({
1254
+ text: "Sort by time",
1255
+ translate: (text: string | undefined): string | undefined => {
1256
+ return text === "Sort by time" ? "Nach Zeit sortieren" : text;
1257
+ },
1258
+ }),
1259
+ ).toBe("Nach Zeit sortieren");
1260
+ });
1261
+
1262
+ test("falls back to the English text when the translator has nothing", () => {
1263
+ expect(
1264
+ translateFeedOptionsText({
1265
+ text: "Sort by time",
1266
+ translate: (): string | undefined => {
1267
+ return undefined;
1268
+ },
1269
+ }),
1270
+ ).toBe("Sort by time");
1271
+ expect(
1272
+ translateFeedOptionsText({
1273
+ text: "Sort by time",
1274
+ translate: (): string | undefined => {
1275
+ return "";
1276
+ },
1277
+ }),
1278
+ ).toBe("Sort by time");
1279
+ });
1280
+
1281
+ test("fills placeholders after looking the whole sentence up", () => {
1282
+ const lookedUp: Array<string | undefined> = [];
1283
+
1284
+ expect(
1285
+ translateFeedOptionsText({
1286
+ text: FEED_OPTIONS_TEXT.showingSomeEventTypes,
1287
+ values: { selected: 2, total: 23 },
1288
+ translate: (text: string | undefined): string | undefined => {
1289
+ lookedUp.push(text);
1290
+ return "{{total}} Ereignistypen, davon {{selected}} angezeigt.";
1291
+ },
1292
+ }),
1293
+ ).toBe("23 Ereignistypen, davon 2 angezeigt.");
1294
+ expect(lookedUp).toEqual([FEED_OPTIONS_TEXT.showingSomeEventTypes]);
1295
+ });
1296
+
1297
+ test("leaves a placeholder with no value visible", () => {
1298
+ expect(
1299
+ translateFeedOptionsText({
1300
+ text: "Showing {{selected}} of {{total}} event types.",
1301
+ values: { selected: 1 },
1302
+ }),
1303
+ ).toBe("Showing 1 of {{total}} event types.");
1304
+ });
1305
+
1306
+ test("tolerates spaces inside the braces and repeats", () => {
1307
+ expect(
1308
+ translateFeedOptionsText({
1309
+ text: "{{ a }} and {{a}}",
1310
+ values: { a: "x" },
1311
+ }),
1312
+ ).toBe("x and x");
1313
+ });
1314
+
1315
+ test("every template names only the placeholders the control fills", () => {
1316
+ const allowed: Set<string> = new Set<string>([
1317
+ "selected",
1318
+ "total",
1319
+ "search",
1320
+ "sortOrder",
1321
+ "count",
1322
+ ]);
1323
+
1324
+ const unfilled: Array<string> = [];
1325
+
1326
+ for (const text of Object.values(FEED_OPTIONS_TEXT)) {
1327
+ for (const match of text.matchAll(/\{\{\s*([A-Za-z0-9_]+)\s*\}\}/g)) {
1328
+ if (!allowed.has(match[1]!)) {
1329
+ unfilled.push(`${match[1]} in "${text}"`);
1330
+ }
1331
+ }
1332
+ }
1333
+
1334
+ // Anything listed here would show raw braces to the reader.
1335
+ expect(unfilled).toEqual([]);
1336
+ });
1337
+
1338
+ test("the trigger has no separate accessible-name template", () => {
1339
+ /*
1340
+ * The button's name is its visible "Filter & Sort" label and the state
1341
+ * summary is its description, so the old "Filter and sort feed:
1342
+ * {{summary}}" sentence is gone rather than left untranslated.
1343
+ */
1344
+ expect(Object.keys(FEED_OPTIONS_TEXT)).not.toContain(
1345
+ "triggerAccessibleName",
1346
+ );
1347
+ });
1348
+
1349
+ test("the search announcement counts the matching event types", () => {
1350
+ expect(FEED_OPTIONS_TEXT.searchResults).toBe(
1351
+ "Matching event types: {{count}}",
1352
+ );
1353
+ });
1354
+ });
1355
+
1356
+ describe("getFeedOptionsSummary in another language", () => {
1357
+ const GERMAN: Record<string, string> = {
1358
+ "Newest first": "Neueste zuerst",
1359
+ "Oldest first": "\u00c4lteste zuerst",
1360
+ "{{sortOrder}}, all event types": "{{sortOrder}}, alle Ereignistypen",
1361
+ "{{sortOrder}}, {{selected}} of {{total}} event types":
1362
+ "{{sortOrder}}, {{selected}} von {{total}} Ereignistypen",
1363
+ };
1364
+
1365
+ const translate: (text: string | undefined) => string | undefined = (
1366
+ text: string | undefined,
1367
+ ): string | undefined => {
1368
+ return text === undefined ? undefined : GERMAN[text];
1369
+ };
1370
+
1371
+ test("translates the order and the sentence around it", () => {
1372
+ expect(
1373
+ getFeedOptionsSummary({
1374
+ options: DEFAULT_FEED_OPTIONS,
1375
+ eventTypeCount: 12,
1376
+ translate,
1377
+ }),
1378
+ ).toBe("Neueste zuerst, alle Ereignistypen");
1379
+
1380
+ expect(
1381
+ getFeedOptionsSummary({
1382
+ options: {
1383
+ sortOrder: SortOrder.Ascending,
1384
+ eventTypes: [IncidentFeedEventType.PublicNote],
1385
+ },
1386
+ eventTypeCount: 12,
1387
+ translate,
1388
+ }),
1389
+ ).toBe("\u00c4lteste zuerst, 1 von 12 Ereignistypen");
1390
+ });
1391
+
1392
+ test("a sentence with no translation stays English, with its numbers", () => {
1393
+ expect(
1394
+ getFeedOptionsSummary({
1395
+ options: {
1396
+ sortOrder: SortOrder.Descending,
1397
+ eventTypes: [IncidentFeedEventType.PublicNote],
1398
+ },
1399
+ eventTypeCount: 1,
1400
+ translate,
1401
+ }),
1402
+ ).toBe("Neueste zuerst, 1 of 1 event type");
1403
+ });
1404
+ });