@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,1202 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import {
4
+ afterEach,
5
+ beforeEach,
6
+ describe,
7
+ expect,
8
+ jest,
9
+ test,
10
+ } from "@jest/globals";
11
+ import "@testing-library/jest-dom";
12
+ import {
13
+ RenderResult,
14
+ cleanup,
15
+ fireEvent,
16
+ render,
17
+ screen,
18
+ waitFor,
19
+ within,
20
+ } from "@testing-library/react";
21
+ import * as React from "react";
22
+
23
+ /*
24
+ * ModelAPI is the only thing between these pages and the network. Stubbed
25
+ * inline (jest.mock is hoisted above the imports) with the "mock" prefix jest
26
+ * requires of anything the factory closes over - same as ResourceFeed.test.
27
+ */
28
+ type GetListRequest = Record<string, unknown>;
29
+
30
+ const mockGetListCalls: Array<GetListRequest> = [];
31
+
32
+ jest.mock("../../../UI/Utils/ModelAPI/ModelAPI", () => {
33
+ return {
34
+ __esModule: true,
35
+ default: {
36
+ getList: (request: GetListRequest): Promise<unknown> => {
37
+ mockGetListCalls.push(request);
38
+ return Promise.resolve({ data: [], count: 0 });
39
+ },
40
+ },
41
+ };
42
+ });
43
+
44
+ /*
45
+ * The real Icon draws an SVG that says nothing about which icon it is. This
46
+ * one names the icon, so the checklist's icons can be read back off the page.
47
+ * Everything else the module exports (SizeProp, ThickProp, IconType) stays
48
+ * real - components across the page read those at render time.
49
+ */
50
+ jest.mock("../../../UI/Components/Icon/Icon", () => {
51
+ const actualIconModule: Record<string, unknown> = jest.requireActual(
52
+ "../../../UI/Components/Icon/Icon",
53
+ ) as Record<string, unknown>;
54
+
55
+ return {
56
+ ...actualIconModule,
57
+ __esModule: true,
58
+ default: (props: { icon: string }) => {
59
+ return <span data-icon={props.icon} aria-hidden="true" />;
60
+ },
61
+ };
62
+ });
63
+
64
+ import KubernetesClusterFeedPage from "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Feed";
65
+ import DockerHostFeedPage from "../../../../App/FeatureSet/Dashboard/src/Pages/Docker/View/Feed";
66
+ import DockerSwarmClusterFeedPage from "../../../../App/FeatureSet/Dashboard/src/Pages/DockerSwarm/View/Feed";
67
+ import PodmanHostFeedPage from "../../../../App/FeatureSet/Dashboard/src/Pages/Podman/View/Feed";
68
+ import ProxmoxClusterFeedPage from "../../../../App/FeatureSet/Dashboard/src/Pages/Proxmox/View/Feed";
69
+ import CephClusterFeedPage from "../../../../App/FeatureSet/Dashboard/src/Pages/Ceph/View/Feed";
70
+ import VMwareVCenterFeedPage from "../../../../App/FeatureSet/Dashboard/src/Pages/VMware/View/Feed";
71
+ import HostFeedPage from "../../../../App/FeatureSet/Dashboard/src/Pages/Host/View/Feed";
72
+ import CloudResourceFeedPage from "../../../../App/FeatureSet/Dashboard/src/Pages/Cloud/View/Feed";
73
+ import ServiceFeedPage from "../../../../App/FeatureSet/Dashboard/src/Pages/Service/View/Feed";
74
+ import SloFeedPage from "../../../../App/FeatureSet/Dashboard/src/Pages/Slo/View/Feed";
75
+ import SloFeed from "../../../../App/FeatureSet/Dashboard/src/Components/Slo/SloFeed";
76
+ import { getSloFeedEventIcon } from "../../../../App/FeatureSet/Dashboard/src/Components/Slo/SloFeedIcon";
77
+ import {
78
+ GetResourceFeedIconFunction,
79
+ ResourceFeedModel,
80
+ getIconForEventType,
81
+ resolveResourceFeedIcon,
82
+ } from "../../../../App/FeatureSet/Dashboard/src/Components/ResourceFeed/ResourceFeed";
83
+ import type PageComponentProps from "../../../../App/FeatureSet/Dashboard/src/Pages/PageComponentProps";
84
+ import PageMap from "../../../../App/FeatureSet/Dashboard/src/Utils/PageMap";
85
+ import RouteMap from "../../../../App/FeatureSet/Dashboard/src/Utils/RouteMap";
86
+ import RouteParams from "../../../../App/FeatureSet/Dashboard/src/Utils/RouteParams";
87
+ import KubernetesClusterFeed, {
88
+ KubernetesClusterFeedEventType,
89
+ } from "../../../Models/DatabaseModels/KubernetesClusterFeed";
90
+ import DockerHostFeed, {
91
+ DockerHostFeedEventType,
92
+ } from "../../../Models/DatabaseModels/DockerHostFeed";
93
+ import DockerSwarmClusterFeed, {
94
+ DockerSwarmClusterFeedEventType,
95
+ } from "../../../Models/DatabaseModels/DockerSwarmClusterFeed";
96
+ import PodmanHostFeed, {
97
+ PodmanHostFeedEventType,
98
+ } from "../../../Models/DatabaseModels/PodmanHostFeed";
99
+ import ProxmoxClusterFeed, {
100
+ ProxmoxClusterFeedEventType,
101
+ } from "../../../Models/DatabaseModels/ProxmoxClusterFeed";
102
+ import CephClusterFeed, {
103
+ CephClusterFeedEventType,
104
+ } from "../../../Models/DatabaseModels/CephClusterFeed";
105
+ import VMwareVCenterFeed, {
106
+ VMwareVCenterFeedEventType,
107
+ } from "../../../Models/DatabaseModels/VMwareVCenterFeed";
108
+ import HostFeed, {
109
+ HostFeedEventType,
110
+ } from "../../../Models/DatabaseModels/HostFeed";
111
+ import CloudResourceFeed, {
112
+ CloudResourceFeedEventType,
113
+ } from "../../../Models/DatabaseModels/CloudResourceFeed";
114
+ import ServiceFeed, {
115
+ ServiceFeedEventType,
116
+ } from "../../../Models/DatabaseModels/ServiceFeed";
117
+ import ServiceLevelObjectiveFeed, {
118
+ ServiceLevelObjectiveFeedEventType,
119
+ } from "../../../Models/DatabaseModels/ServiceLevelObjectiveFeed";
120
+ import Route from "../../../Types/API/Route";
121
+ import Includes from "../../../Types/BaseDatabase/Includes";
122
+ import SortOrder from "../../../Types/BaseDatabase/SortOrder";
123
+ import { DEFAULT_LIMIT } from "../../../Types/Database/LimitMax";
124
+ import IconProp from "../../../Types/Icon/IconProp";
125
+ import ObjectID from "../../../Types/ObjectID";
126
+ import {
127
+ DEFAULT_FEED_OPTIONS,
128
+ FEED_OPTIONS_TEXT,
129
+ FILTERED_FEED_NO_ITEMS_MESSAGE,
130
+ FeedOptions,
131
+ getFeedEventTypeLabel,
132
+ getFeedOptionsSummary,
133
+ } from "../../../UI/Components/Feed/FeedOptions";
134
+ import { getSortOrderStorageKey } from "../../../UI/Components/Feed/useFeedOptions";
135
+
136
+ /*
137
+ * ResourceFeed.test pins the shared component with hand-written props. What
138
+ * it cannot see is the eleven real pages that hand it those props - each one a
139
+ * near copy of the next, with two column names and an enum pasted in as
140
+ * strings. A slip there compiles and fails quietly: the wrong resource column
141
+ * is an empty feed, the wrong event type column is a filter the API cannot
142
+ * apply, another product's enum is a checklist of events this feed never
143
+ * records, and a shared storage key makes "Oldest first" on one product flip
144
+ * every other product's feed.
145
+ *
146
+ * So every real page is rendered at its real route with only the network
147
+ * stubbed, and driven through the real Filter & Sort button - plus the SLO
148
+ * feed embedded on the SLO overview, which renders the same ResourceFeed from
149
+ * a component instead of a page.
150
+ */
151
+
152
+ const DASHBOARD_SRC: string = path.join(
153
+ __dirname,
154
+ "..",
155
+ "..",
156
+ "..",
157
+ "..",
158
+ "App",
159
+ "FeatureSet",
160
+ "Dashboard",
161
+ "src",
162
+ );
163
+
164
+ const PROJECT_ID: ObjectID = ObjectID.generate();
165
+
166
+ type FeedModelType = new () => ResourceFeedModel;
167
+
168
+ type MountFunction = (resourceId: ObjectID) => React.ReactElement;
169
+
170
+ interface ResourceFeedSurface {
171
+ // Names the surface in test titles.
172
+ product: string;
173
+ /*
174
+ * The product whose feed this surface shows. Two surfaces can show one
175
+ * product's feed - the SLO Feed page and the feed on the SLO overview - and
176
+ * those are meant to agree about the reader's sort order.
177
+ */
178
+ feedProduct: string;
179
+ // The file that renders <ResourceFeed>, relative to the dashboard's src.
180
+ file: string;
181
+ // That file's default export - the coverage guard checks the two agree.
182
+ component: unknown;
183
+ modelType: FeedModelType;
184
+ resourceIdColumn: string;
185
+ eventTypeColumn: string;
186
+ enumObject: Record<string, string>;
187
+ // The feed's own icons, for the event types the shared rules do not know.
188
+ getIcon?: GetResourceFeedIconFunction | undefined;
189
+ /*
190
+ * Puts the resource id where this surface reads it, and returns the element.
191
+ * Re-rendering a mounted surface with a new one's element is the dashboard
192
+ * moving that surface on to another resource.
193
+ */
194
+ mount: MountFunction;
195
+ }
196
+
197
+ interface FeedPageSpec {
198
+ product: string;
199
+ file: string;
200
+ Page: React.FunctionComponent<PageComponentProps>;
201
+ route: Route;
202
+ modelType: FeedModelType;
203
+ resourceIdColumn: string;
204
+ eventTypeColumn: string;
205
+ enumObject: Record<string, string>;
206
+ getIcon?: GetResourceFeedIconFunction | undefined;
207
+ }
208
+
209
+ const FEED_PAGES: Array<FeedPageSpec> = [
210
+ {
211
+ product: "Kubernetes",
212
+ file: "Pages/Kubernetes/View/Feed.tsx",
213
+ Page: KubernetesClusterFeedPage,
214
+ route: RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_FEED] as Route,
215
+ modelType: KubernetesClusterFeed,
216
+ resourceIdColumn: "kubernetesClusterId",
217
+ eventTypeColumn: "kubernetesClusterFeedEventType",
218
+ enumObject: KubernetesClusterFeedEventType,
219
+ },
220
+ {
221
+ product: "Docker",
222
+ file: "Pages/Docker/View/Feed.tsx",
223
+ Page: DockerHostFeedPage,
224
+ route: RouteMap[PageMap.DOCKER_HOST_VIEW_FEED] as Route,
225
+ modelType: DockerHostFeed,
226
+ resourceIdColumn: "dockerHostId",
227
+ eventTypeColumn: "dockerHostFeedEventType",
228
+ enumObject: DockerHostFeedEventType,
229
+ },
230
+ {
231
+ product: "Docker Swarm",
232
+ file: "Pages/DockerSwarm/View/Feed.tsx",
233
+ Page: DockerSwarmClusterFeedPage,
234
+ route: RouteMap[PageMap.DOCKER_SWARM_CLUSTER_VIEW_FEED] as Route,
235
+ modelType: DockerSwarmClusterFeed,
236
+ resourceIdColumn: "dockerSwarmClusterId",
237
+ eventTypeColumn: "dockerSwarmClusterFeedEventType",
238
+ enumObject: DockerSwarmClusterFeedEventType,
239
+ },
240
+ {
241
+ product: "Podman",
242
+ file: "Pages/Podman/View/Feed.tsx",
243
+ Page: PodmanHostFeedPage,
244
+ route: RouteMap[PageMap.PODMAN_HOST_VIEW_FEED] as Route,
245
+ modelType: PodmanHostFeed,
246
+ resourceIdColumn: "podmanHostId",
247
+ eventTypeColumn: "podmanHostFeedEventType",
248
+ enumObject: PodmanHostFeedEventType,
249
+ },
250
+ {
251
+ product: "Proxmox",
252
+ file: "Pages/Proxmox/View/Feed.tsx",
253
+ Page: ProxmoxClusterFeedPage,
254
+ route: RouteMap[PageMap.PROXMOX_CLUSTER_VIEW_FEED] as Route,
255
+ modelType: ProxmoxClusterFeed,
256
+ resourceIdColumn: "proxmoxClusterId",
257
+ eventTypeColumn: "proxmoxClusterFeedEventType",
258
+ enumObject: ProxmoxClusterFeedEventType,
259
+ },
260
+ {
261
+ product: "Ceph",
262
+ file: "Pages/Ceph/View/Feed.tsx",
263
+ Page: CephClusterFeedPage,
264
+ route: RouteMap[PageMap.CEPH_CLUSTER_VIEW_FEED] as Route,
265
+ modelType: CephClusterFeed,
266
+ resourceIdColumn: "cephClusterId",
267
+ eventTypeColumn: "cephClusterFeedEventType",
268
+ enumObject: CephClusterFeedEventType,
269
+ },
270
+ {
271
+ product: "VMware",
272
+ file: "Pages/VMware/View/Feed.tsx",
273
+ Page: VMwareVCenterFeedPage,
274
+ route: RouteMap[PageMap.VMWARE_VCENTER_VIEW_FEED] as Route,
275
+ modelType: VMwareVCenterFeed,
276
+ resourceIdColumn: "vmwareVCenterId",
277
+ eventTypeColumn: "vmwareVCenterFeedEventType",
278
+ enumObject: VMwareVCenterFeedEventType,
279
+ },
280
+ {
281
+ product: "Host",
282
+ file: "Pages/Host/View/Feed.tsx",
283
+ Page: HostFeedPage,
284
+ route: RouteMap[PageMap.HOST_VIEW_FEED] as Route,
285
+ modelType: HostFeed,
286
+ resourceIdColumn: "hostId",
287
+ eventTypeColumn: "hostFeedEventType",
288
+ enumObject: HostFeedEventType,
289
+ },
290
+ {
291
+ product: "Cloud",
292
+ file: "Pages/Cloud/View/Feed.tsx",
293
+ Page: CloudResourceFeedPage,
294
+ route: RouteMap[PageMap.CLOUD_RESOURCE_VIEW_FEED] as Route,
295
+ modelType: CloudResourceFeed,
296
+ resourceIdColumn: "cloudResourceId",
297
+ eventTypeColumn: "cloudResourceFeedEventType",
298
+ enumObject: CloudResourceFeedEventType,
299
+ },
300
+ {
301
+ product: "Service Catalog",
302
+ file: "Pages/Service/View/Feed.tsx",
303
+ Page: ServiceFeedPage,
304
+ route: RouteMap[PageMap.SERVICE_VIEW_FEED] as Route,
305
+ modelType: ServiceFeed,
306
+ resourceIdColumn: "serviceId",
307
+ eventTypeColumn: "serviceFeedEventType",
308
+ enumObject: ServiceFeedEventType,
309
+ },
310
+ {
311
+ product: "SLO",
312
+ file: "Pages/Slo/View/Feed.tsx",
313
+ Page: SloFeedPage,
314
+ route: RouteMap[PageMap.SLO_VIEW_FEED] as Route,
315
+ modelType: ServiceLevelObjectiveFeed,
316
+ resourceIdColumn: "serviceLevelObjectiveId",
317
+ eventTypeColumn: "serviceLevelObjectiveFeedEventType",
318
+ enumObject: ServiceLevelObjectiveFeedEventType,
319
+ getIcon: getSloFeedEventIcon,
320
+ },
321
+ ];
322
+
323
+ type GetFeedPageUrl = (spec: FeedPageSpec, resourceId: ObjectID) => string;
324
+
325
+ // The page's own route with the project and resource filled in.
326
+ const getFeedPageUrl: GetFeedPageUrl = (
327
+ spec: FeedPageSpec,
328
+ resourceId: ObjectID,
329
+ ): string => {
330
+ return spec.route
331
+ .toString()
332
+ .replace(RouteParams.ProjectID, PROJECT_ID.toString())
333
+ .replace(RouteParams.ModelID, resourceId.toString());
334
+ };
335
+
336
+ type ToSurface = (spec: FeedPageSpec) => ResourceFeedSurface;
337
+
338
+ /*
339
+ * A page reads its resource from the address bar (the route is
340
+ * <modelId>/feed, so Navigation.getLastParamAsObjectID(1)), so mounting one
341
+ * means going to its URL first. It reads it on every render, so going to
342
+ * another resource's URL and rendering the page again is what the router does
343
+ * when the reader moves between two resources of one product: same route,
344
+ * same page, still mounted.
345
+ */
346
+ const toSurface: ToSurface = (spec: FeedPageSpec): ResourceFeedSurface => {
347
+ return {
348
+ product: `${spec.product} feed page`,
349
+ feedProduct: spec.product,
350
+ file: spec.file,
351
+ component: spec.Page,
352
+ modelType: spec.modelType,
353
+ resourceIdColumn: spec.resourceIdColumn,
354
+ eventTypeColumn: spec.eventTypeColumn,
355
+ enumObject: spec.enumObject,
356
+ getIcon: spec.getIcon,
357
+ mount: (resourceId: ObjectID): React.ReactElement => {
358
+ window.history.pushState({}, "", getFeedPageUrl(spec, resourceId));
359
+
360
+ const pageProps: PageComponentProps = {
361
+ pageRoute: spec.route,
362
+ currentProject: null,
363
+ hasPaymentMethod: true,
364
+ };
365
+
366
+ return <spec.Page {...pageProps} />;
367
+ },
368
+ };
369
+ };
370
+
371
+ const SLO_FEED_PAGE: FeedPageSpec = FEED_PAGES.find((spec: FeedPageSpec) => {
372
+ return spec.file === "Pages/Slo/View/Feed.tsx";
373
+ })!;
374
+
375
+ /*
376
+ * The SLO overview embeds the feed through SloFeed, which takes the SLO from
377
+ * its props rather than the URL - so the address bar is pointed somewhere
378
+ * else entirely, and only the prop can put the right id in the query.
379
+ */
380
+ const EMBEDDED_SLO_FEED: ResourceFeedSurface = {
381
+ product: "Embedded SLO feed",
382
+ feedProduct: SLO_FEED_PAGE.product,
383
+ file: "Components/Slo/SloFeed.tsx",
384
+ component: SloFeed,
385
+ modelType: ServiceLevelObjectiveFeed,
386
+ resourceIdColumn: "serviceLevelObjectiveId",
387
+ eventTypeColumn: "serviceLevelObjectiveFeedEventType",
388
+ enumObject: ServiceLevelObjectiveFeedEventType,
389
+ getIcon: getSloFeedEventIcon,
390
+ mount: (resourceId: ObjectID): React.ReactElement => {
391
+ window.history.pushState(
392
+ {},
393
+ "",
394
+ getFeedPageUrl(SLO_FEED_PAGE, ObjectID.generate()),
395
+ );
396
+
397
+ return <SloFeed sloId={resourceId} />;
398
+ },
399
+ };
400
+
401
+ const SLO_FEED_PAGE_SURFACE: ResourceFeedSurface = toSurface(SLO_FEED_PAGE);
402
+
403
+ const SURFACES: Array<ResourceFeedSurface> = [
404
+ ...FEED_PAGES.map(toSurface),
405
+ EMBEDDED_SLO_FEED,
406
+ ];
407
+
408
+ type GetEventTypes = (surface: ResourceFeedSurface) => Array<string>;
409
+
410
+ const getEventTypes: GetEventTypes = (
411
+ surface: ResourceFeedSurface,
412
+ ): Array<string> => {
413
+ return Object.values(surface.enumObject);
414
+ };
415
+
416
+ type GetQuery = (request: GetListRequest) => Record<string, unknown>;
417
+
418
+ const getQuery: GetQuery = (
419
+ request: GetListRequest,
420
+ ): Record<string, unknown> => {
421
+ return request["query"] as Record<string, unknown>;
422
+ };
423
+
424
+ type WaitForFeed = (requestCount: number) => Promise<void>;
425
+
426
+ // The feed has asked the API `requestCount` times and drawn the last answer.
427
+ const waitForFeed: WaitForFeed = async (
428
+ requestCount: number,
429
+ ): Promise<void> => {
430
+ await waitFor(() => {
431
+ expect(mockGetListCalls).toHaveLength(requestCount);
432
+ expect(screen.queryByTestId("component-loader")).not.toBeInTheDocument();
433
+ });
434
+ };
435
+
436
+ type GetFilterAndSortButton = () => HTMLElement;
437
+
438
+ /*
439
+ * The feed's header has exactly one Filter & Sort button, found the way a
440
+ * screen reader finds it: by its name. What the feed is showing is the
441
+ * button's description, not part of its name, so the name stays the label
442
+ * whatever the reader has chosen.
443
+ */
444
+ const getFilterAndSortButton: GetFilterAndSortButton = (): HTMLElement => {
445
+ const triggers: Array<HTMLElement> = screen.getAllByRole("button", {
446
+ name: FEED_OPTIONS_TEXT.triggerLabel,
447
+ });
448
+
449
+ expect(triggers).toHaveLength(1);
450
+
451
+ return triggers[0]!;
452
+ };
453
+
454
+ type OpenFeedOptions = () => HTMLElement;
455
+
456
+ // Presses Filter & Sort and returns the panel it opens.
457
+ const openFeedOptions: OpenFeedOptions = (): HTMLElement => {
458
+ fireEvent.click(getFilterAndSortButton());
459
+
460
+ return screen.getByRole("dialog", { name: FEED_OPTIONS_TEXT.panelLabel });
461
+ };
462
+
463
+ type CloseFeedOptions = () => void;
464
+
465
+ // A press anywhere outside the panel - on a link to another resource, say.
466
+ const closeFeedOptions: CloseFeedOptions = (): void => {
467
+ fireEvent.mouseDown(document.body);
468
+
469
+ expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
470
+ };
471
+
472
+ type GetExpectedSummary = (
473
+ surface: ResourceFeedSurface,
474
+ options: FeedOptions,
475
+ ) => string;
476
+
477
+ /*
478
+ * The sentence the trigger is described by, counted over this surface's own
479
+ * event types. Built by the same function the button uses, so the wording can
480
+ * change without this suite noticing - what is pinned is that the description
481
+ * follows the feed's live order and filter, over the right product's enum.
482
+ */
483
+ const getExpectedSummary: GetExpectedSummary = (
484
+ surface: ResourceFeedSurface,
485
+ options: FeedOptions,
486
+ ): string => {
487
+ return getFeedOptionsSummary({
488
+ options: options,
489
+ eventTypeCount: getEventTypes(surface).length,
490
+ });
491
+ };
492
+
493
+ interface ChecklistEntry {
494
+ label: string;
495
+ icon: string | null;
496
+ checkbox: HTMLElement;
497
+ }
498
+
499
+ type ReadChecklist = (panel: HTMLElement) => Array<ChecklistEntry>;
500
+
501
+ // The event type checklist as the reader sees it, top to bottom.
502
+ const readChecklist: ReadChecklist = (
503
+ panel: HTMLElement,
504
+ ): Array<ChecklistEntry> => {
505
+ return within(panel)
506
+ .getAllByRole("checkbox")
507
+ .map((checkbox: HTMLElement): ChecklistEntry => {
508
+ const label: HTMLLabelElement | null = checkbox.closest("label");
509
+
510
+ return {
511
+ label: label?.textContent?.trim() || "",
512
+ icon:
513
+ label?.querySelector("[data-icon]")?.getAttribute("data-icon") ||
514
+ null,
515
+ checkbox: checkbox,
516
+ };
517
+ });
518
+ };
519
+
520
+ interface ChecklistRow {
521
+ label: string;
522
+ icon: string | null;
523
+ }
524
+
525
+ type ToChecklistRows = (entries: Array<ChecklistEntry>) => Array<ChecklistRow>;
526
+
527
+ const toChecklistRows: ToChecklistRows = (
528
+ entries: Array<ChecklistEntry>,
529
+ ): Array<ChecklistRow> => {
530
+ return entries.map((entry: ChecklistEntry): ChecklistRow => {
531
+ return { label: entry.label, icon: entry.icon };
532
+ });
533
+ };
534
+
535
+ type GetStoredSortOrderKeys = () => Array<string>;
536
+
537
+ /*
538
+ * Every remembered feed sort order in this browser, whichever feed wrote it.
539
+ * The prefix is whatever getSortOrderStorageKey puts in front of a feed's
540
+ * name, so it is read from there rather than copied.
541
+ */
542
+ const getStoredSortOrderKeys: GetStoredSortOrderKeys = (): Array<string> => {
543
+ const prefix: string = getSortOrderStorageKey("");
544
+ const keys: Array<string> = [];
545
+
546
+ for (let index: number = 0; index < window.localStorage.length; index++) {
547
+ const key: string | null = window.localStorage.key(index);
548
+
549
+ if (key && key.startsWith(prefix)) {
550
+ keys.push(key);
551
+ }
552
+ }
553
+
554
+ return keys;
555
+ };
556
+
557
+ type ChooseSortOrder = (label: string) => void;
558
+
559
+ const chooseSortOrder: ChooseSortOrder = (label: string): void => {
560
+ const panel: HTMLElement = openFeedOptions();
561
+
562
+ fireEvent.click(within(panel).getByRole("radio", { name: label }));
563
+ };
564
+
565
+ beforeEach(() => {
566
+ mockGetListCalls.length = 0;
567
+ // The chosen sort order is remembered, so one test's choice must not leak.
568
+ window.localStorage.clear();
569
+ window.history.pushState({}, "", "/");
570
+ });
571
+
572
+ afterEach(() => {
573
+ cleanup();
574
+ });
575
+
576
+ describe("Resource feed pages - Filter & Sort on the real pages", () => {
577
+ test.each(SURFACES)(
578
+ "$product asks the API for this resource's own feed, newest first",
579
+ async (surface: ResourceFeedSurface) => {
580
+ const resourceId: ObjectID = ObjectID.generate();
581
+
582
+ render(surface.mount(resourceId));
583
+ await waitForFeed(1);
584
+
585
+ const request: GetListRequest = mockGetListCalls[0]!;
586
+ const query: Record<string, unknown> = getQuery(request);
587
+
588
+ expect(request["modelType"]).toBe(surface.modelType);
589
+
590
+ /*
591
+ * Exactly the resource and nothing else: an untouched Filter & Sort adds
592
+ * nothing to the query, and the id is the one in the address (or, for
593
+ * the embedded feed, the prop) - not the project id one segment over.
594
+ */
595
+ expect(Object.keys(query)).toEqual([surface.resourceIdColumn]);
596
+ expect(query[surface.resourceIdColumn]).toBeInstanceOf(ObjectID);
597
+ expect(String(query[surface.resourceIdColumn])).toBe(
598
+ resourceId.toString(),
599
+ );
600
+
601
+ expect(request["sort"]).toEqual({ postedAt: SortOrder.Descending });
602
+ expect(request["skip"]).toBe(0);
603
+ expect(request["limit"]).toBe(DEFAULT_LIMIT);
604
+
605
+ // The icon is chosen from the event type, so it has to be selected.
606
+ expect(
607
+ (request["select"] as Record<string, unknown>)[surface.eventTypeColumn],
608
+ ).toBe(true);
609
+
610
+ /*
611
+ * The button says what the feed is showing without being opened, as
612
+ * its description and its tooltip.
613
+ */
614
+ const summary: string = getExpectedSummary(surface, DEFAULT_FEED_OPTIONS);
615
+ const trigger: HTMLElement = getFilterAndSortButton();
616
+
617
+ expect(trigger).toHaveAccessibleDescription(summary);
618
+ expect(trigger).toHaveAttribute("title", summary);
619
+ },
620
+ );
621
+
622
+ test.each(SURFACES)(
623
+ "$product offers every event type of its own feed model, labelled and in order",
624
+ async (surface: ResourceFeedSurface) => {
625
+ render(surface.mount(ObjectID.generate()));
626
+ await waitForFeed(1);
627
+
628
+ const eventTypes: Array<string> = getEventTypes(surface);
629
+ const entries: Array<ChecklistEntry> = readChecklist(openFeedOptions());
630
+
631
+ expect(eventTypes.length).toBeGreaterThan(0);
632
+ expect(entries).toHaveLength(eventTypes.length);
633
+
634
+ /*
635
+ * Each entry reads as its event type's label and carries the icon the
636
+ * feed's items of that type carry, alphabetical so the reader can scan
637
+ * for the one they want. Another product's enum pasted into the page
638
+ * fails here by label.
639
+ */
640
+ const expectedRows: Array<ChecklistRow> = eventTypes
641
+ .map((eventType: string): ChecklistRow => {
642
+ return {
643
+ label: getFeedEventTypeLabel(eventType),
644
+ icon: resolveResourceFeedIcon({
645
+ eventType: eventType,
646
+ getIcon: surface.getIcon,
647
+ }),
648
+ };
649
+ })
650
+ .sort((a: ChecklistRow, b: ChecklistRow): number => {
651
+ return a.label.localeCompare(b.label);
652
+ });
653
+
654
+ expect(toChecklistRows(entries)).toEqual(expectedRows);
655
+
656
+ // Two entries reading the same would leave the reader guessing.
657
+ const labels: Array<string> = entries.map((entry: ChecklistEntry) => {
658
+ return entry.label;
659
+ });
660
+ expect(new Set(labels).size).toBe(labels.length);
661
+
662
+ // Nothing is ticked until the reader ticks it.
663
+ for (const entry of entries) {
664
+ expect(entry.checkbox).not.toBeChecked();
665
+ }
666
+ },
667
+ );
668
+
669
+ test.each(SURFACES)(
670
+ "$product filters on its own event type column in the API, keeping the resource",
671
+ async (surface: ResourceFeedSurface) => {
672
+ const resourceId: ObjectID = ObjectID.generate();
673
+
674
+ render(surface.mount(resourceId));
675
+ await waitForFeed(1);
676
+
677
+ const entries: Array<ChecklistEntry> = readChecklist(openFeedOptions());
678
+ const firstEntry: ChecklistEntry | undefined = entries[0];
679
+
680
+ expect(firstEntry).toBeDefined();
681
+
682
+ const tickedEventType: string | undefined = getEventTypes(surface).find(
683
+ (eventType: string) => {
684
+ return getFeedEventTypeLabel(eventType) === firstEntry!.label;
685
+ },
686
+ );
687
+
688
+ expect(tickedEventType).toBeDefined();
689
+
690
+ fireEvent.click(firstEntry!.checkbox);
691
+ await waitForFeed(2);
692
+
693
+ const request: GetListRequest = mockGetListCalls[1]!;
694
+ const query: Record<string, unknown> = getQuery(request);
695
+
696
+ expect(Object.keys(query).sort()).toEqual(
697
+ [surface.resourceIdColumn, surface.eventTypeColumn].sort(),
698
+ );
699
+ expect(String(query[surface.resourceIdColumn])).toBe(
700
+ resourceId.toString(),
701
+ );
702
+
703
+ const eventTypeFilter: unknown = query[surface.eventTypeColumn];
704
+
705
+ expect(eventTypeFilter).toBeInstanceOf(Includes);
706
+ expect((eventTypeFilter as Includes).values).toEqual([tickedEventType]);
707
+
708
+ // A new filter reads the newest window again, not the rows on screen.
709
+ expect(request["sort"]).toEqual({ postedAt: SortOrder.Descending });
710
+ expect(request["skip"]).toBe(0);
711
+ expect(request["limit"]).toBe(DEFAULT_LIMIT);
712
+
713
+ // Nothing matched, and the feed says the filter is why.
714
+ expect(
715
+ await screen.findByText(FILTERED_FEED_NO_ITEMS_MESSAGE),
716
+ ).toBeInTheDocument();
717
+
718
+ /*
719
+ * The button's description now says the feed is narrowed - to one of
720
+ * this product's event types, not one of some other enum's.
721
+ */
722
+ expect(getFilterAndSortButton()).toHaveAccessibleDescription(
723
+ getExpectedSummary(surface, {
724
+ sortOrder: SortOrder.Descending,
725
+ eventTypes: [tickedEventType!],
726
+ }),
727
+ );
728
+
729
+ /*
730
+ * The filter narrows one look at one resource. It is not remembered,
731
+ * so the next visit shows every event again.
732
+ */
733
+ expect(getStoredSortOrderKeys()).toEqual([]);
734
+
735
+ cleanup();
736
+ mockGetListCalls.length = 0;
737
+
738
+ render(surface.mount(resourceId));
739
+ await waitForFeed(1);
740
+
741
+ expect(Object.keys(getQuery(mockGetListCalls[0]!))).toEqual([
742
+ surface.resourceIdColumn,
743
+ ]);
744
+ },
745
+ );
746
+
747
+ test.each(SURFACES)(
748
+ "$product starts the next resource's feed unfiltered, from its first request",
749
+ async (surface: ResourceFeedSurface) => {
750
+ /*
751
+ * Moving between two resources of one product keeps the page mounted
752
+ * (see toSurface), so the filter ticked on the first is still in the
753
+ * feed's state when the second arrives. It was chosen for the first
754
+ * resource's investigation: carried over, the second resource's feed
755
+ * would open with events silently missing - and a reset that only
756
+ * lands after that first request still sends the API the old filter
757
+ * for the new resource.
758
+ */
759
+ const firstResourceId: ObjectID = ObjectID.generate();
760
+ const nextResourceId: ObjectID = ObjectID.generate();
761
+
762
+ const view: RenderResult = render(surface.mount(firstResourceId));
763
+ await waitForFeed(1);
764
+
765
+ fireEvent.click(readChecklist(openFeedOptions())[0]!.checkbox);
766
+ await waitForFeed(2);
767
+
768
+ // The first resource's feed really was narrowed.
769
+ const filteredQuery: Record<string, unknown> = getQuery(
770
+ mockGetListCalls[1]!,
771
+ );
772
+
773
+ expect(String(filteredQuery[surface.resourceIdColumn])).toBe(
774
+ firstResourceId.toString(),
775
+ );
776
+ expect(filteredQuery[surface.eventTypeColumn]).toBeInstanceOf(Includes);
777
+ expect(
778
+ await screen.findByText(FILTERED_FEED_NO_ITEMS_MESSAGE),
779
+ ).toBeInTheDocument();
780
+
781
+ // The reader follows a link to another resource of the same product.
782
+ closeFeedOptions();
783
+ view.rerender(surface.mount(nextResourceId));
784
+ await waitForFeed(3);
785
+
786
+ /*
787
+ * One request for the new resource, and it was already unfiltered -
788
+ * not a filtered one that a later reset replaced.
789
+ */
790
+ const request: GetListRequest = mockGetListCalls[2]!;
791
+ const query: Record<string, unknown> = getQuery(request);
792
+
793
+ expect(Object.keys(query)).toEqual([surface.resourceIdColumn]);
794
+ expect(String(query[surface.resourceIdColumn])).toBe(
795
+ nextResourceId.toString(),
796
+ );
797
+ expect(request["sort"]).toEqual({ postedAt: SortOrder.Descending });
798
+ expect(request["skip"]).toBe(0);
799
+ expect(request["limit"]).toBe(DEFAULT_LIMIT);
800
+
801
+ /*
802
+ * The page agrees with the request: an empty feed is the resource's
803
+ * own empty state, not "nothing matches", and the button no longer
804
+ * says the feed is narrowed.
805
+ */
806
+ expect(
807
+ screen.queryByText(FILTERED_FEED_NO_ITEMS_MESSAGE),
808
+ ).not.toBeInTheDocument();
809
+ expect(
810
+ screen.queryByTestId("feed-options-count"),
811
+ ).not.toBeInTheDocument();
812
+ expect(getFilterAndSortButton()).toHaveAccessibleDescription(
813
+ getExpectedSummary(surface, DEFAULT_FEED_OPTIONS),
814
+ );
815
+
816
+ for (const entry of readChecklist(openFeedOptions())) {
817
+ expect(entry.checkbox).not.toBeChecked();
818
+ }
819
+
820
+ // And nothing asked for the old filter afterwards either.
821
+ expect(mockGetListCalls).toHaveLength(3);
822
+ },
823
+ );
824
+
825
+ test.each(SURFACES)(
826
+ "$product sends Oldest first to the API and remembers it under one key",
827
+ async (surface: ResourceFeedSurface) => {
828
+ const resourceId: ObjectID = ObjectID.generate();
829
+
830
+ render(surface.mount(resourceId));
831
+ await waitForFeed(1);
832
+
833
+ // Nothing is remembered until the reader changes something.
834
+ expect(getStoredSortOrderKeys()).toEqual([]);
835
+
836
+ chooseSortOrder("Oldest first");
837
+ await waitForFeed(2);
838
+
839
+ const request: GetListRequest = mockGetListCalls[1]!;
840
+
841
+ // Reversing the loaded rows would show the newest ten upside down.
842
+ expect(request["sort"]).toEqual({ postedAt: SortOrder.Ascending });
843
+ expect(request["skip"]).toBe(0);
844
+ expect(request["limit"]).toBe(DEFAULT_LIMIT);
845
+ expect(Object.keys(getQuery(request))).toEqual([
846
+ surface.resourceIdColumn,
847
+ ]);
848
+
849
+ expect(getFilterAndSortButton()).toHaveAccessibleDescription(
850
+ getExpectedSummary(surface, {
851
+ sortOrder: SortOrder.Ascending,
852
+ eventTypes: [],
853
+ }),
854
+ );
855
+
856
+ /*
857
+ * One key written, holding the new order. Which key it is does not
858
+ * matter here - that no two products share one is checked below, from
859
+ * the keys the pages are seen writing.
860
+ */
861
+ const storedKeys: Array<string> = getStoredSortOrderKeys();
862
+
863
+ expect(storedKeys).toHaveLength(1);
864
+ expect(window.localStorage.getItem(storedKeys[0]!)).toBe(
865
+ SortOrder.Ascending,
866
+ );
867
+
868
+ // The next visit to this feed starts oldest first.
869
+ cleanup();
870
+ mockGetListCalls.length = 0;
871
+
872
+ render(surface.mount(ObjectID.generate()));
873
+ await waitForFeed(1);
874
+
875
+ expect(mockGetListCalls[0]?.["sort"]).toEqual({
876
+ postedAt: SortOrder.Ascending,
877
+ });
878
+ expect(
879
+ within(openFeedOptions()).getByRole("radio", { name: "Oldest first" }),
880
+ ).toHaveAttribute("aria-checked", "true");
881
+ },
882
+ );
883
+
884
+ test("each product remembers its sort order under a key of its own, and both SLO feeds under the same one", async () => {
885
+ /*
886
+ * The keys are read off what each real surface writes when the reader
887
+ * picks Oldest first, not worked out from this file's table - a table
888
+ * can only agree with itself. Storage is emptied between surfaces, so
889
+ * each one's key is its own and not one left behind by the one before.
890
+ */
891
+ interface WrittenSortOrder {
892
+ surface: ResourceFeedSurface;
893
+ keys: Array<string>;
894
+ values: Array<string | null>;
895
+ }
896
+
897
+ const written: Array<WrittenSortOrder> = [];
898
+
899
+ for (const surface of SURFACES) {
900
+ render(surface.mount(ObjectID.generate()));
901
+ await waitForFeed(1);
902
+
903
+ chooseSortOrder("Oldest first");
904
+ await waitForFeed(2);
905
+
906
+ const keys: Array<string> = getStoredSortOrderKeys();
907
+
908
+ written.push({
909
+ surface: surface,
910
+ keys: keys,
911
+ values: keys.map((key: string): string | null => {
912
+ return window.localStorage.getItem(key);
913
+ }),
914
+ });
915
+
916
+ window.localStorage.clear();
917
+ cleanup();
918
+ mockGetListCalls.length = 0;
919
+ }
920
+
921
+ // Every surface wrote exactly one key, holding the order just chosen.
922
+ expect(
923
+ written.map((entry: WrittenSortOrder) => {
924
+ return { product: entry.surface.product, values: entry.values };
925
+ }),
926
+ ).toEqual(
927
+ SURFACES.map((surface: ResourceFeedSurface) => {
928
+ return { product: surface.product, values: [SortOrder.Ascending] };
929
+ }),
930
+ );
931
+
932
+ // Which products' feeds wrote each key.
933
+ const productsByKey: Map<string, Set<string>> = new Map<
934
+ string,
935
+ Set<string>
936
+ >();
937
+
938
+ for (const entry of written) {
939
+ const key: string = entry.keys[0]!;
940
+
941
+ productsByKey.set(
942
+ key,
943
+ new Set<string>([
944
+ ...(productsByKey.get(key) || []),
945
+ entry.surface.feedProduct,
946
+ ]),
947
+ );
948
+ }
949
+
950
+ type ByName = (a: Array<string>, b: Array<string>) => number;
951
+
952
+ const byName: ByName = (a: Array<string>, b: Array<string>): number => {
953
+ return a.join(",").localeCompare(b.join(","));
954
+ };
955
+
956
+ /*
957
+ * Exactly one product per key, and each product once. A key two products
958
+ * share would show up as one entry naming both: Oldest first on one of
959
+ * them would turn the other's feed over. A product with two keys would
960
+ * show up twice: the SLO Feed page and the SLO overview's feed, one feed
961
+ * shown in two places, disagreeing about which way time runs.
962
+ */
963
+ expect(
964
+ Array.from(productsByKey.values())
965
+ .map((products: Set<string>): Array<string> => {
966
+ return Array.from(products).sort();
967
+ })
968
+ .sort(byName),
969
+ ).toEqual(
970
+ FEED_PAGES.map((spec: FeedPageSpec): Array<string> => {
971
+ return [spec.product];
972
+ }).sort(byName),
973
+ );
974
+
975
+ // Spelled out for the one product shown twice.
976
+ type GetWrittenKey = (surface: ResourceFeedSurface) => string | undefined;
977
+
978
+ const getWrittenKey: GetWrittenKey = (
979
+ surface: ResourceFeedSurface,
980
+ ): string | undefined => {
981
+ return written.find((entry: WrittenSortOrder) => {
982
+ return entry.surface.product === surface.product;
983
+ })?.keys[0];
984
+ };
985
+
986
+ expect(getWrittenKey(EMBEDDED_SLO_FEED)).toBeDefined();
987
+ expect(getWrittenKey(EMBEDDED_SLO_FEED)).toBe(
988
+ getWrittenKey(SLO_FEED_PAGE_SURFACE),
989
+ );
990
+ });
991
+ });
992
+
993
+ /*
994
+ * The SLO Feed page and the feed embedded on the SLO overview are the same
995
+ * feed shown in two places. The reader should not find them disagreeing
996
+ * about which way time runs, what can be filtered, or what an event looks
997
+ * like.
998
+ */
999
+ describe("SLO feed - page and embedded feed", () => {
1000
+ type ReadSurfaceChecklist = (
1001
+ surface: ResourceFeedSurface,
1002
+ ) => Promise<Array<ChecklistRow>>;
1003
+
1004
+ const readSurfaceChecklist: ReadSurfaceChecklist = async (
1005
+ surface: ResourceFeedSurface,
1006
+ ): Promise<Array<ChecklistRow>> => {
1007
+ render(surface.mount(ObjectID.generate()));
1008
+ await waitForFeed(1);
1009
+
1010
+ const rows: Array<ChecklistRow> = toChecklistRows(
1011
+ readChecklist(openFeedOptions()),
1012
+ );
1013
+
1014
+ cleanup();
1015
+ mockGetListCalls.length = 0;
1016
+
1017
+ return rows;
1018
+ };
1019
+
1020
+ test("offer the same event types, with the same labels and icons", async () => {
1021
+ const pageRows: Array<ChecklistRow> = await readSurfaceChecklist(
1022
+ SLO_FEED_PAGE_SURFACE,
1023
+ );
1024
+ const embeddedRows: Array<ChecklistRow> =
1025
+ await readSurfaceChecklist(EMBEDDED_SLO_FEED);
1026
+
1027
+ expect(pageRows).toHaveLength(
1028
+ Object.values(ServiceLevelObjectiveFeedEventType).length,
1029
+ );
1030
+ expect(embeddedRows).toEqual(pageRows);
1031
+ });
1032
+
1033
+ test("share one remembered sort order", async () => {
1034
+ // Oldest first on the Feed page...
1035
+ render(SLO_FEED_PAGE_SURFACE.mount(ObjectID.generate()));
1036
+ await waitForFeed(1);
1037
+ chooseSortOrder("Oldest first");
1038
+ await waitForFeed(2);
1039
+
1040
+ expect(getStoredSortOrderKeys()).toHaveLength(1);
1041
+
1042
+ cleanup();
1043
+ mockGetListCalls.length = 0;
1044
+
1045
+ // ...is how the overview's feed opens.
1046
+ render(EMBEDDED_SLO_FEED.mount(ObjectID.generate()));
1047
+ await waitForFeed(1);
1048
+
1049
+ expect(mockGetListCalls[0]?.["sort"]).toEqual({
1050
+ postedAt: SortOrder.Ascending,
1051
+ });
1052
+
1053
+ // And back to Newest first there is how the Feed page opens next.
1054
+ chooseSortOrder("Newest first");
1055
+ await waitForFeed(2);
1056
+
1057
+ expect(mockGetListCalls[1]?.["sort"]).toEqual({
1058
+ postedAt: SortOrder.Descending,
1059
+ });
1060
+
1061
+ cleanup();
1062
+ mockGetListCalls.length = 0;
1063
+
1064
+ render(SLO_FEED_PAGE_SURFACE.mount(ObjectID.generate()));
1065
+ await waitForFeed(1);
1066
+
1067
+ expect(mockGetListCalls[0]?.["sort"]).toEqual({
1068
+ postedAt: SortOrder.Descending,
1069
+ });
1070
+ });
1071
+
1072
+ test("show the SLO feed's own icons for the SLO's own event types", async () => {
1073
+ /*
1074
+ * StatusChanged, the burn rate events, the rule and monitor changes: the
1075
+ * shared suffix rules know none of them and would draw each as the same
1076
+ * anonymous dot. getSloFeedEventIcon is what tells them apart, and it is
1077
+ * what both checklists must show.
1078
+ */
1079
+ const sloOwnEventTypes: Array<string> = Object.values(
1080
+ ServiceLevelObjectiveFeedEventType,
1081
+ ).filter((eventType: string) => {
1082
+ return getSloFeedEventIcon(eventType) !== undefined;
1083
+ });
1084
+
1085
+ expect(sloOwnEventTypes.length).toBeGreaterThan(0);
1086
+
1087
+ for (const eventType of sloOwnEventTypes) {
1088
+ const ownIcon: IconProp = getSloFeedEventIcon(eventType)!;
1089
+
1090
+ expect(
1091
+ resolveResourceFeedIcon({
1092
+ eventType: eventType,
1093
+ getIcon: getSloFeedEventIcon,
1094
+ }),
1095
+ ).toBe(ownIcon);
1096
+ expect(ownIcon).not.toBe(getIconForEventType(eventType));
1097
+ }
1098
+
1099
+ for (const surface of [SLO_FEED_PAGE_SURFACE, EMBEDDED_SLO_FEED]) {
1100
+ const rows: Array<ChecklistRow> = await readSurfaceChecklist(surface);
1101
+
1102
+ for (const eventType of sloOwnEventTypes) {
1103
+ const row: ChecklistRow | undefined = rows.find(
1104
+ (candidate: ChecklistRow) => {
1105
+ return candidate.label === getFeedEventTypeLabel(eventType);
1106
+ },
1107
+ );
1108
+
1109
+ expect(row).toBeDefined();
1110
+ expect(row!.icon).toBe(getSloFeedEventIcon(eventType));
1111
+ }
1112
+ }
1113
+ });
1114
+ });
1115
+
1116
+ type FindResourceFeedRenderers = (directory: string) => Array<string>;
1117
+
1118
+ const TYPESCRIPT_FILE: RegExp = /\.tsx?$/;
1119
+
1120
+ const RESOURCE_FEED_ELEMENT: RegExp = /<ResourceFeed[\s</>]/;
1121
+
1122
+ /*
1123
+ * Every file under the dashboard's src that renders <ResourceFeed>, relative
1124
+ * to src with forward slashes. Matches the JSX opening tag - with or without
1125
+ * a type argument - and not ResourceFeedModel or other longer names.
1126
+ */
1127
+ const findResourceFeedRenderers: FindResourceFeedRenderers = (
1128
+ directory: string,
1129
+ ): Array<string> => {
1130
+ const renderers: Array<string> = [];
1131
+
1132
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
1133
+ const entryPath: string = path.join(directory, entry.name);
1134
+
1135
+ if (entry.isDirectory()) {
1136
+ renderers.push(...findResourceFeedRenderers(entryPath));
1137
+ continue;
1138
+ }
1139
+
1140
+ if (!TYPESCRIPT_FILE.test(entry.name)) {
1141
+ continue;
1142
+ }
1143
+
1144
+ if (RESOURCE_FEED_ELEMENT.test(fs.readFileSync(entryPath, "utf8"))) {
1145
+ renderers.push(
1146
+ path.relative(DASHBOARD_SRC, entryPath).split(path.sep).join("/"),
1147
+ );
1148
+ }
1149
+ }
1150
+
1151
+ return renderers;
1152
+ };
1153
+
1154
+ /*
1155
+ * The sweep above is only as good as its table. A new product's feed page
1156
+ * copies one of these and would otherwise ship untested - so the dashboard is
1157
+ * scanned, and whatever renders ResourceFeed has to be in the table.
1158
+ */
1159
+ describe("Resource feed pages - the sweep covers every ResourceFeed", () => {
1160
+ test("every file that renders <ResourceFeed> is driven above, and nothing else is", () => {
1161
+ const renderers: Array<string> = findResourceFeedRenderers(DASHBOARD_SRC);
1162
+ const covered: Array<string> = SURFACES.map(
1163
+ (surface: ResourceFeedSurface) => {
1164
+ return surface.file;
1165
+ },
1166
+ );
1167
+
1168
+ expect(renderers.length).toBeGreaterThan(0);
1169
+
1170
+ // A renderer missing here is a feed nobody drives: add it to FEED_PAGES.
1171
+ expect(
1172
+ renderers.filter((file: string) => {
1173
+ return !covered.includes(file);
1174
+ }),
1175
+ ).toEqual([]);
1176
+
1177
+ // A row missing there is a file that no longer renders ResourceFeed.
1178
+ expect(
1179
+ covered.filter((file: string) => {
1180
+ return !renderers.includes(file);
1181
+ }),
1182
+ ).toEqual([]);
1183
+
1184
+ expect(new Set(covered).size).toBe(covered.length);
1185
+ });
1186
+
1187
+ test.each(SURFACES)(
1188
+ "$product is the component $file exports",
1189
+ async (surface: ResourceFeedSurface) => {
1190
+ /*
1191
+ * The row's file is what the scan matched, and the row's component is
1192
+ * what was rendered - so the two must be the same module, or a new page
1193
+ * could be "covered" by a row that still renders an old one.
1194
+ */
1195
+ const feedModule: { default: unknown } = await import(
1196
+ path.join(DASHBOARD_SRC, surface.file)
1197
+ );
1198
+
1199
+ expect(feedModule.default).toBe(surface.component);
1200
+ },
1201
+ );
1202
+ });