@oneuptime/common 13.0.7 → 13.0.8

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 (463) hide show
  1. package/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.ts +1 -1
  2. package/Models/DatabaseModels/ProjectOnCallCalendarFeed.ts +1 -1
  3. package/Models/DatabaseModels/UserOnCallCalendarFeed.ts +1 -1
  4. package/Server/API/AIBillingAPI.ts +1 -0
  5. package/Server/API/AIInsightAPI.ts +1 -4
  6. package/Server/API/AIInvestigationAPI.ts +1 -4
  7. package/Server/API/AIReadinessAPI.ts +4 -5
  8. package/Server/API/AlertAPI.ts +1 -0
  9. package/Server/API/AutoRemediationAPI.ts +1 -4
  10. package/Server/API/BaseAPI.ts +13 -0
  11. package/Server/API/BaseAnalyticsAPI.ts +80 -9
  12. package/Server/API/BillingAPI.ts +2 -0
  13. package/Server/API/BillingInvoiceAPI.ts +1 -0
  14. package/Server/API/BillingPaymentMethodAPI.ts +2 -0
  15. package/Server/API/CodeFixRunAPI.ts +1 -3
  16. package/Server/API/CommonAPI.ts +68 -4
  17. package/Server/API/DashboardAPI.ts +12 -12
  18. package/Server/API/GlobalConfigAPI.ts +17 -0
  19. package/Server/API/IncidentAPI.ts +2 -0
  20. package/Server/API/IncidentEpisodeAPI.ts +1 -0
  21. package/Server/API/NotificationAPI.ts +1 -0
  22. package/Server/API/OnCallCalendarAPI.ts +4 -5
  23. package/Server/API/OnCallDutyPolicyAPI.ts +7 -0
  24. package/Server/API/OnCallReadinessAPI.ts +4 -1
  25. package/Server/API/OnCallScheduleTimelineAPI.ts +731 -0
  26. package/Server/API/ProjectAPI.ts +1 -0
  27. package/Server/API/ScheduledMaintenanceAPI.ts +1 -0
  28. package/Server/API/StatusPageAPI.ts +161 -32
  29. package/Server/API/TeamMemberAPI.ts +3 -0
  30. package/Server/API/UserCallAPI.ts +2 -0
  31. package/Server/API/UserEmailAPI.ts +2 -0
  32. package/Server/API/UserIncomingCallNumberAPI.ts +2 -0
  33. package/Server/API/UserMicrosoftTeamsAPI.ts +1 -0
  34. package/Server/API/UserSlackAPI.ts +1 -0
  35. package/Server/API/UserSmsAPI.ts +2 -0
  36. package/Server/API/UserTotpAuthAPI.ts +1 -0
  37. package/Server/API/UserWebAuthnAPI.ts +2 -0
  38. package/Server/API/UserWebhookAPI.ts +1 -0
  39. package/Server/API/UserWhatsAppAPI.ts +2 -0
  40. package/Server/Middleware/MasterAdminAuthorization.ts +13 -2
  41. package/Server/Middleware/UserAuthorization.ts +79 -5
  42. package/Server/Services/ApiKeyPermissionService.ts +101 -4
  43. package/Server/Services/DatabaseService.ts +245 -0
  44. package/Server/Services/DetectionRuleService.ts +5 -4
  45. package/Server/Services/OnCallDutyPolicyChildService.ts +261 -0
  46. package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +2 -2
  47. package/Server/Services/OnCallDutyPolicyEscalationRuleService.ts +2 -2
  48. package/Server/Services/OnCallDutyPolicyEscalationRuleTeamService.ts +2 -2
  49. package/Server/Services/OnCallDutyPolicyEscalationRuleUserService.ts +2 -2
  50. package/Server/Services/PayAsYouGoBillingService.ts +23 -28
  51. package/Server/Services/ProjectService.ts +18 -0
  52. package/Server/Services/RumSessionPinService.ts +274 -22
  53. package/Server/Services/TeamPermissionService.ts +120 -26
  54. package/Server/Services/ThreatIntelFeedService.ts +6 -3
  55. package/Server/Types/Database/Permissions/BasePermission.ts +9 -7
  56. package/Server/Types/Database/Permissions/Index.ts +75 -11
  57. package/Server/Types/Database/Permissions/TablePermission.ts +24 -28
  58. package/Server/Utils/AI/AIChatPrivacyFilter.ts +8 -0
  59. package/Server/Utils/AI/AIRunPrivacyFilter.ts +8 -0
  60. package/Server/Utils/MasterAdminPageAccess.ts +61 -0
  61. package/Server/Utils/Monitor/MonitorAlert.ts +3 -1
  62. package/Server/Utils/Monitor/MonitorIncident.ts +3 -1
  63. package/Server/Utils/OnCall/OnCallCalendarFeedRenderer.ts +1 -1
  64. package/Server/Utils/Realtime.ts +163 -45
  65. package/Server/Utils/SecurityEvent/DetectionRuleEvaluator.ts +11 -28
  66. package/Server/Utils/SecurityEvent/GoogleSecOpsSeverityRepair.ts +170 -0
  67. package/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.ts +1 -1
  68. package/Server/Utils/SecurityEvent/ThreatIntel/ThreatIntelFeedPoller.ts +6 -2
  69. package/Server/Utils/SecurityEvent/ThreatIntel/ThreatIntelMatcher.ts +10 -9
  70. package/Tests/App/Dashboard/AffectedResourcesPicker.test.tsx +1059 -0
  71. package/Tests/App/Dashboard/AffectedResourcesPickerEditForm.test.tsx +376 -0
  72. package/Tests/App/Dashboard/AffectedResourcesPickerHiddenTypes.test.tsx +757 -0
  73. package/Tests/App/Dashboard/AffectedResourcesSummaryStep.test.tsx +307 -0
  74. package/Tests/App/Dashboard/ArgumentsFormGroupByUnlistedAttributes.test.tsx +131 -0
  75. package/Tests/App/Dashboard/DashboardHeaderRequests.test.tsx +1 -1
  76. package/Tests/App/Dashboard/DashboardMonitorLabelVariable.test.tsx +9 -1
  77. package/Tests/App/Dashboard/DashboardVariableAllContract.test.tsx +24 -0
  78. package/Tests/App/Dashboard/DashboardVariableSelector.test.tsx +162 -8
  79. package/Tests/App/Dashboard/EntityFilterDropdownArchivedSlos.test.tsx +105 -6
  80. package/Tests/App/Dashboard/EntityFilterDropdownUnlistedSelection.test.tsx +434 -0
  81. package/Tests/App/Dashboard/EscalationRuleReadiness.test.tsx +276 -1
  82. package/Tests/App/Dashboard/OnCallCalendarFeedSideMenus.test.tsx +9 -1
  83. package/Tests/App/Dashboard/PayAsYouGoConsentGate.test.tsx +58 -42
  84. package/Tests/App/Dashboard/PayAsYouGoNotices.test.tsx +42 -5
  85. package/Tests/App/Dashboard/ResourceAuditLogsSideMenus.test.tsx +34 -23
  86. package/Tests/App/Dashboard/ScheduleTimeline.test.tsx +1580 -0
  87. package/Tests/App/Dashboard/ScheduleTimelineAPI.test.ts +280 -0
  88. package/Tests/App/Dashboard/ScheduleTimelineModel.test.ts +673 -0
  89. package/Tests/App/Dashboard/SecurityEventConnectionDiagnostics.test.tsx +95 -68
  90. package/Tests/App/Dashboard/SecurityEventsDetectionRulesPage.test.tsx +86 -3
  91. package/Tests/App/Dashboard/SecurityEventsEmptyState.test.tsx +28 -3
  92. package/Tests/App/Dashboard/SecurityEventsExplorer.test.tsx +761 -0
  93. package/Tests/App/Dashboard/SecurityEventsGuideContent.test.ts +1313 -0
  94. package/Tests/App/Dashboard/SecurityEventsHowItWorksCard.test.tsx +536 -0
  95. package/Tests/App/Dashboard/SecurityEventsNoResults.test.tsx +222 -0
  96. package/Tests/App/Dashboard/SecurityEventsThreatIntelPage.test.tsx +199 -0
  97. package/Tests/App/Dashboard/SecurityEventsVolumeChart.test.tsx +321 -0
  98. package/Tests/App/Dashboard/SloOverviewHero.test.tsx +171 -36
  99. package/Tests/App/Dashboard/SloViewSideMenu.test.tsx +215 -0
  100. package/Tests/App/Dashboard/SloViewSideMenuCounts.test.tsx +40 -3
  101. package/Tests/App/PublicDashboard/DashboardVariableSelector.test.tsx +12 -4
  102. package/Tests/App/PublicDashboard/PublicDashboardWidgetListAuth.test.tsx +623 -0
  103. package/Tests/Models/OnCallCalendarFeedModels.test.ts +23 -1
  104. package/Tests/Server/API/AIChatRouteAuthorization.test.ts +138 -5
  105. package/Tests/Server/API/AIGenerationProjectAIToggle.test.ts +187 -0
  106. package/Tests/Server/API/AIInvestigationAvailabilityAuthorization.test.ts +51 -3
  107. package/Tests/Server/API/AIInvestigationEvidenceAPI.test.ts +55 -2
  108. package/Tests/Server/API/BaseAnalyticsAPIAggregateCache.test.ts +729 -0
  109. package/Tests/Server/API/BillingPayAsYouGoStatus.test.ts +93 -7
  110. package/Tests/Server/API/BillingPaymentMethodAPI.test.ts +36 -3
  111. package/Tests/Server/API/CommonAPIAuthGuard.test.ts +1003 -24
  112. package/Tests/Server/API/DashboardPublicRateLimit.test.ts +6 -3
  113. package/Tests/Server/API/ExpiredSessionAIRoutes.test.ts +708 -0
  114. package/Tests/Server/API/ExpiredSessionCustomRoutes.test.ts +928 -0
  115. package/Tests/Server/API/ExpiredSessionMonitorTemplate.test.ts +319 -0
  116. package/Tests/Server/API/ExpiredSessionOnCallDutyPolicy.test.ts +336 -0
  117. package/Tests/Server/API/ExpiredSessionRouteGuards.test.ts +820 -0
  118. package/Tests/Server/API/GlobalConfigLicense.test.ts +140 -0
  119. package/Tests/Server/API/NotificationChannelVerificationAPI.test.ts +115 -12
  120. package/Tests/Server/API/OnCallCalendarAPI.test.ts +375 -8
  121. package/Tests/Server/API/OnCallReadinessAPI.test.ts +175 -17
  122. package/Tests/Server/API/OnCallScheduleTimelineAPI.test.ts +1513 -0
  123. package/Tests/Server/API/OnCallSetupReminderAPI.test.ts +119 -7
  124. package/Tests/Server/API/ProjectScopedCustomRouteAuth.test.ts +111 -4
  125. package/Tests/Server/API/PublicRouteInvalidAccessToken.test.ts +615 -0
  126. package/Tests/Server/API/SecurityEventAttributesAPI.test.ts +89 -2
  127. package/Tests/Server/API/SessionReplayAPI.test.ts +121 -0
  128. package/Tests/Server/API/StatusPageMasterPasswordRateLimit.test.ts +4 -2
  129. package/Tests/Server/API/StatusPagePostmortemVisibility.test.ts +441 -0
  130. package/Tests/Server/API/StatusPageTestEmailReportAuth.test.ts +664 -0
  131. package/Tests/Server/API/TeamMemberRemoveUserFromProjectAPI.test.ts +7 -0
  132. package/Tests/Server/API/UserIncomingCallNumberApi.test.ts +6 -0
  133. package/Tests/Server/API/UserNotificationSettingAPI.test.ts +65 -4
  134. package/Tests/Server/API/UserProjectsAPI.test.ts +63 -3
  135. package/Tests/Server/API/UserTotpAuthAPI.test.ts +48 -2
  136. package/Tests/Server/API/UserWebAuthnAuthenticationOptionsRoute.test.ts +11 -2
  137. package/Tests/Server/API/UserWebAuthnBackupCodeMinting.test.ts +46 -3
  138. package/Tests/Server/API/UserWebAuthnPasskeyAPI.test.ts +18 -2
  139. package/Tests/Server/API/ZZVerifyChatsAuth.test.ts +24 -7
  140. package/Tests/Server/Middleware/ExpiredSessionUserMiddleware.test.ts +563 -0
  141. package/Tests/Server/Middleware/MasterAdminAuthorization.test.ts +299 -19
  142. package/Tests/Server/Middleware/PublicRouteUserMiddleware.test.ts +453 -0
  143. package/Tests/Server/Services/AIChatPrivacyPin.test.ts +24 -0
  144. package/Tests/Server/Services/ApiKeyPermissionSecurity.test.ts +2 -0
  145. package/Tests/Server/Services/ApiKeyPermissionService.test.ts +3 -3
  146. package/Tests/Server/Services/DatabaseServiceCreateSuppliedId.test.ts +147 -0
  147. package/Tests/Server/Services/DatabaseServiceLoginBeforeHooks.test.ts +1078 -0
  148. package/Tests/Server/Services/DatabaseServiceTenantRelationGuard.test.ts +259 -0
  149. package/Tests/Server/Services/DatabaseServiceWriteOperatorValues.test.ts +147 -0
  150. package/Tests/Server/Services/MonitorPayAsYouGoBilling.test.ts +115 -1
  151. package/Tests/Server/Services/OnCallChildCreateAuthorizationPostgres.test.ts +711 -0
  152. package/Tests/Server/Services/OnCallDutyPolicyChildService.test.ts +733 -0
  153. package/Tests/Server/Services/PayAsYouGoBillingService.test.ts +190 -140
  154. package/Tests/Server/Services/PayAsYouGoStripeRateLimitBudget.test.ts +4 -9
  155. package/Tests/Server/Services/PermissionBlockDeletion.test.ts +780 -0
  156. package/Tests/Server/Services/PermissionBlockDeletionPostgres.test.ts +628 -0
  157. package/Tests/Server/Services/ProjectServiceCreateSubscriptionStarted.test.ts +77 -1
  158. package/Tests/Server/Services/RumSessionPinCreatePermission.test.ts +715 -0
  159. package/Tests/Server/Services/TeamMemberMultiTenantReadPostgres.test.ts +394 -0
  160. package/Tests/Server/Services/TeamPrivilegeEscalation.test.ts +6 -0
  161. package/Tests/Server/Services/TelemetryPayAsYouGoBilling.test.ts +131 -0
  162. package/Tests/Server/Services/UserNotificationRuleAdminGuards.test.ts +13 -2
  163. package/Tests/Server/Types/Database/Permissions/ModelPermissionAnonymousRefusal.test.ts +518 -0
  164. package/Tests/Server/Types/Database/Permissions/MultiTenantPermissions.test.ts +350 -0
  165. package/Tests/Server/Types/Database/Permissions/OnCallCalendarFeedPermissions.test.ts +282 -25
  166. package/Tests/Server/Types/Database/Permissions/TableLevelBlockPermission.test.ts +459 -0
  167. package/Tests/Server/Utils/AI/AIRunPrivacyFilter.test.ts +101 -9
  168. package/Tests/Server/Utils/AI/ChatAgentHelpers.test.ts +88 -4
  169. package/Tests/Server/Utils/AI/Toolbox/ExceptionTools.test.ts +459 -0
  170. package/Tests/Server/Utils/AI/Toolbox/LogTools.test.ts +794 -0
  171. package/Tests/Server/Utils/AI/Toolbox/RecentChangesTools.test.ts +574 -0
  172. package/Tests/Server/Utils/AI/Toolbox/SecurityEventTools.test.ts +596 -0
  173. package/Tests/Server/Utils/CaptchaRequestAndLogging.test.ts +395 -0
  174. package/Tests/Server/Utils/MasterAdminPageAccess.test.ts +251 -0
  175. package/Tests/Server/Utils/Monitor/MonitorActionTimestamps.test.ts +616 -0
  176. package/Tests/Server/Utils/Realtime.test.ts +502 -0
  177. package/Tests/Server/Utils/SecurityEvent/Connectors/GoogleSecOps/GoogleSecOpsConnectorSeverity.test.ts +216 -0
  178. package/Tests/Server/Utils/SecurityEvent/DetectionRuleEvaluator.test.ts +142 -0
  179. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsSeverityRepair.test.ts +140 -0
  180. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsSeverityRepairClickhouseIntegration.test.ts +542 -0
  181. package/Tests/Server/Utils/Telemetry/ChangeEventRow.test.ts +743 -0
  182. package/Tests/Server/Utils/Telemetry/RuntimeMetrics.test.ts +465 -0
  183. package/Tests/Server/Utils/Workspace/MicrosoftTeams/Messages/Alert.test.ts +202 -0
  184. package/Tests/Server/Utils/Workspace/MicrosoftTeams/Messages/Incident.test.ts +194 -0
  185. package/Tests/Server/Utils/Workspace/MicrosoftTeams/Messages/ScheduledMaintenance.test.ts +231 -0
  186. package/Tests/Server/Utils/Workspace/Slack/Messages/Alert.test.ts +194 -0
  187. package/Tests/Server/Utils/Workspace/Slack/Messages/AlertEpisode.test.ts +194 -0
  188. package/Tests/Server/Utils/Workspace/Slack/Messages/Incident.test.ts +194 -0
  189. package/Tests/Server/Utils/Workspace/Slack/Messages/IncidentEpisode.test.ts +194 -0
  190. package/Tests/Server/Utils/Workspace/Slack/Messages/ScheduledMaintenance.test.ts +247 -0
  191. package/Tests/Types/AI/AIInsightEvidence.test.ts +206 -0
  192. package/Tests/Types/BaseDatabase/DatabaseCommonInteractionPropsUtilAnonymous.test.ts +144 -0
  193. package/Tests/Types/Call/CallProvider.test.ts +528 -0
  194. package/Tests/Types/Kubernetes/KubernetesCostIngest.test.ts +236 -0
  195. package/Tests/Types/OnCallDutyPolicy/ScheduleTimeline.test.ts +736 -0
  196. package/Tests/Types/OnCallDutyPolicy/ScheduleTimelineLayout.test.ts +996 -0
  197. package/Tests/Types/SecurityEvent/SigmaRule.test.ts +142 -0
  198. package/Tests/Types/Workflow/Components/JSON.test.ts +182 -0
  199. package/Tests/UI/Components/BasicFormDropdownNormalization.test.tsx +116 -1
  200. package/Tests/UI/Components/Dropdown/DropdownAsyncOptions.test.tsx +81 -0
  201. package/Tests/UI/Components/EditionLabelLicenseRefresh.test.tsx +89 -1
  202. package/Tests/UI/Components/EventAttachmentList.test.tsx +174 -0
  203. package/Tests/UI/Components/Forms/BasicFormBurnRateTemplateDefaults.test.tsx +395 -0
  204. package/Tests/UI/Components/Forms/BasicFormCollapsibleSections.test.tsx +286 -0
  205. package/Tests/UI/Components/Forms/FormSummary.test.tsx +128 -0
  206. package/Tests/UI/Components/ModelTable/BaseModelTableQueryChange.test.tsx +365 -0
  207. package/Tests/UI/Components/OpenAuthenticatedUrl.test.ts +358 -0
  208. package/Tests/UI/Components/SessionAwareImage.test.tsx +496 -0
  209. package/Tests/UI/Components/TelemetryHistogramZeroBuckets.test.tsx +190 -0
  210. package/Tests/UI/Components/TooltipInteractive.test.tsx +97 -0
  211. package/Tests/UI/MicrosoftTeams/MicrosoftTeamsChatsCardInstallGuidance.test.tsx +1 -1
  212. package/Tests/UI/Monitor/EvaluationLogListTimestamps.test.tsx +367 -0
  213. package/Tests/UI/Utils/API/BaseAPIRefreshSession.test.ts +1087 -0
  214. package/Tests/UI/Utils/API/DashboardHost.ts +12 -0
  215. package/Tests/UI/Utils/API/FakeAxiosServer.ts +183 -0
  216. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +10 -0
  217. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +2 -0
  218. package/Tests/UI/Utils/Cookie.test.ts +440 -0
  219. package/Tests/UI/Utils/ModelAPIRequestClient.test.ts +278 -0
  220. package/Tests/UI/Utils/Navigation.test.ts +33 -0
  221. package/Tests/UI/Utils/PricingPlan.test.ts +295 -0
  222. package/Tests/UI/Utils/Realtime.test.ts +759 -0
  223. package/Tests/UI/Utils/Telemetry/Telemetry.test.ts +500 -0
  224. package/Tests/Utils/APIInstanceDispatch.test.ts +311 -0
  225. package/Tests/Utils/SecurityEvent/GoogleSecOpsAlertNormalizerSeverity.test.ts +565 -0
  226. package/Types/BaseDatabase/DatabaseCommonInteractionPropsUtil.ts +40 -0
  227. package/Types/Monitor/MonitorEvaluationSummary.ts +3 -0
  228. package/Types/OnCallDutyPolicy/ScheduleTimeline.ts +507 -0
  229. package/Types/OnCallDutyPolicy/ScheduleTimelineLayout.ts +589 -0
  230. package/Types/Realtime/EventName.ts +11 -0
  231. package/Types/SecurityEvent/DetectionFindingConstants.ts +14 -0
  232. package/Types/SecurityEvent/OcsfSeverity.ts +31 -26
  233. package/Types/SecurityEvent/SigmaRule.ts +51 -0
  234. package/Types/SecurityEvent/ThreatIntelConstants.ts +18 -0
  235. package/UI/Components/AttachmentList/EventAttachmentList.tsx +30 -2
  236. package/UI/Components/Dashboard/DashboardVariableControl.tsx +12 -2
  237. package/UI/Components/Dropdown/Dropdown.tsx +6 -1
  238. package/UI/Components/EditionLabel/EditionLabel.tsx +12 -0
  239. package/UI/Components/EventItem/EventItem.tsx +12 -1
  240. package/UI/Components/Forms/BasicForm.tsx +151 -126
  241. package/UI/Components/Forms/CollapsibleFormSection.tsx +63 -0
  242. package/UI/Components/Forms/Fields/FormField.tsx +10 -4
  243. package/UI/Components/Forms/FormSummary.tsx +7 -0
  244. package/UI/Components/Forms/Types/Field.ts +10 -0
  245. package/UI/Components/Markdown.tsx/MarkdownViewer.tsx +2 -1
  246. package/UI/Components/Markdown.tsx/SessionAwareImage.tsx +122 -0
  247. package/UI/Components/ModelTable/BaseModelTable.tsx +34 -5
  248. package/UI/Components/ModelTable/TableView.tsx +1 -1
  249. package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +8 -1
  250. package/UI/Components/Tooltip/Tooltip.tsx +8 -1
  251. package/UI/Utils/API/API.ts +173 -12
  252. package/UI/Utils/API/RequestOptions.ts +11 -0
  253. package/UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI.ts +31 -7
  254. package/UI/Utils/ModelAPI/ModelAPI.ts +30 -9
  255. package/UI/Utils/Navigation.ts +9 -0
  256. package/UI/Utils/OpenAuthenticatedUrl.ts +154 -0
  257. package/UI/Utils/Realtime.ts +176 -30
  258. package/Utils/API.ts +14 -5
  259. package/Utils/SecurityEvent/GoogleSecOpsAlertNormalizer.ts +157 -7
  260. package/Utils/SecurityEvent/Sigma/SigmaRuleParser.ts +9 -17
  261. package/Utils/Slo/SloBurnRateTemplate.ts +2 -2
  262. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.js +1 -1
  263. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.js.map +1 -1
  264. package/build/dist/Models/DatabaseModels/ProjectOnCallCalendarFeed.js +1 -1
  265. package/build/dist/Models/DatabaseModels/ProjectOnCallCalendarFeed.js.map +1 -1
  266. package/build/dist/Models/DatabaseModels/UserOnCallCalendarFeed.js +1 -1
  267. package/build/dist/Models/DatabaseModels/UserOnCallCalendarFeed.js.map +1 -1
  268. package/build/dist/Server/API/AIBillingAPI.js +1 -1
  269. package/build/dist/Server/API/AIBillingAPI.js.map +1 -1
  270. package/build/dist/Server/API/AIInsightAPI.js +1 -4
  271. package/build/dist/Server/API/AIInsightAPI.js.map +1 -1
  272. package/build/dist/Server/API/AIInvestigationAPI.js +1 -4
  273. package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -1
  274. package/build/dist/Server/API/AIReadinessAPI.js +1 -3
  275. package/build/dist/Server/API/AIReadinessAPI.js.map +1 -1
  276. package/build/dist/Server/API/AlertAPI.js +1 -1
  277. package/build/dist/Server/API/AlertAPI.js.map +1 -1
  278. package/build/dist/Server/API/AutoRemediationAPI.js +1 -4
  279. package/build/dist/Server/API/AutoRemediationAPI.js.map +1 -1
  280. package/build/dist/Server/API/BaseAPI.js +12 -0
  281. package/build/dist/Server/API/BaseAPI.js.map +1 -1
  282. package/build/dist/Server/API/BaseAnalyticsAPI.js +55 -10
  283. package/build/dist/Server/API/BaseAnalyticsAPI.js.map +1 -1
  284. package/build/dist/Server/API/BillingAPI.js +2 -2
  285. package/build/dist/Server/API/BillingAPI.js.map +1 -1
  286. package/build/dist/Server/API/BillingInvoiceAPI.js +1 -1
  287. package/build/dist/Server/API/BillingInvoiceAPI.js.map +1 -1
  288. package/build/dist/Server/API/BillingPaymentMethodAPI.js +2 -2
  289. package/build/dist/Server/API/BillingPaymentMethodAPI.js.map +1 -1
  290. package/build/dist/Server/API/CodeFixRunAPI.js +1 -3
  291. package/build/dist/Server/API/CodeFixRunAPI.js.map +1 -1
  292. package/build/dist/Server/API/CommonAPI.js +50 -5
  293. package/build/dist/Server/API/CommonAPI.js.map +1 -1
  294. package/build/dist/Server/API/DashboardAPI.js +12 -12
  295. package/build/dist/Server/API/DashboardAPI.js.map +1 -1
  296. package/build/dist/Server/API/GlobalConfigAPI.js +12 -0
  297. package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
  298. package/build/dist/Server/API/IncidentAPI.js +2 -2
  299. package/build/dist/Server/API/IncidentAPI.js.map +1 -1
  300. package/build/dist/Server/API/IncidentEpisodeAPI.js +1 -1
  301. package/build/dist/Server/API/IncidentEpisodeAPI.js.map +1 -1
  302. package/build/dist/Server/API/NotificationAPI.js +1 -1
  303. package/build/dist/Server/API/NotificationAPI.js.map +1 -1
  304. package/build/dist/Server/API/OnCallCalendarAPI.js +1 -4
  305. package/build/dist/Server/API/OnCallCalendarAPI.js.map +1 -1
  306. package/build/dist/Server/API/OnCallDutyPolicyAPI.js +6 -0
  307. package/build/dist/Server/API/OnCallDutyPolicyAPI.js.map +1 -1
  308. package/build/dist/Server/API/OnCallReadinessAPI.js +4 -1
  309. package/build/dist/Server/API/OnCallReadinessAPI.js.map +1 -1
  310. package/build/dist/Server/API/OnCallScheduleTimelineAPI.js +494 -0
  311. package/build/dist/Server/API/OnCallScheduleTimelineAPI.js.map +1 -0
  312. package/build/dist/Server/API/ProjectAPI.js +1 -1
  313. package/build/dist/Server/API/ProjectAPI.js.map +1 -1
  314. package/build/dist/Server/API/ScheduledMaintenanceAPI.js +1 -1
  315. package/build/dist/Server/API/ScheduledMaintenanceAPI.js.map +1 -1
  316. package/build/dist/Server/API/StatusPageAPI.js +125 -33
  317. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  318. package/build/dist/Server/API/TeamMemberAPI.js +3 -3
  319. package/build/dist/Server/API/TeamMemberAPI.js.map +1 -1
  320. package/build/dist/Server/API/UserCallAPI.js +2 -2
  321. package/build/dist/Server/API/UserCallAPI.js.map +1 -1
  322. package/build/dist/Server/API/UserEmailAPI.js +2 -2
  323. package/build/dist/Server/API/UserEmailAPI.js.map +1 -1
  324. package/build/dist/Server/API/UserIncomingCallNumberAPI.js +2 -2
  325. package/build/dist/Server/API/UserIncomingCallNumberAPI.js.map +1 -1
  326. package/build/dist/Server/API/UserMicrosoftTeamsAPI.js +1 -1
  327. package/build/dist/Server/API/UserMicrosoftTeamsAPI.js.map +1 -1
  328. package/build/dist/Server/API/UserSlackAPI.js +1 -1
  329. package/build/dist/Server/API/UserSlackAPI.js.map +1 -1
  330. package/build/dist/Server/API/UserSmsAPI.js +2 -2
  331. package/build/dist/Server/API/UserSmsAPI.js.map +1 -1
  332. package/build/dist/Server/API/UserTotpAuthAPI.js +1 -1
  333. package/build/dist/Server/API/UserTotpAuthAPI.js.map +1 -1
  334. package/build/dist/Server/API/UserWebAuthnAPI.js +2 -2
  335. package/build/dist/Server/API/UserWebAuthnAPI.js.map +1 -1
  336. package/build/dist/Server/API/UserWebhookAPI.js +1 -1
  337. package/build/dist/Server/API/UserWebhookAPI.js.map +1 -1
  338. package/build/dist/Server/API/UserWhatsAppAPI.js +2 -2
  339. package/build/dist/Server/API/UserWhatsAppAPI.js.map +1 -1
  340. package/build/dist/Server/Middleware/MasterAdminAuthorization.js +11 -2
  341. package/build/dist/Server/Middleware/MasterAdminAuthorization.js.map +1 -1
  342. package/build/dist/Server/Middleware/UserAuthorization.js +64 -4
  343. package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
  344. package/build/dist/Server/Services/ApiKeyPermissionService.js +80 -4
  345. package/build/dist/Server/Services/ApiKeyPermissionService.js.map +1 -1
  346. package/build/dist/Server/Services/DatabaseService.js +171 -0
  347. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  348. package/build/dist/Server/Services/DetectionRuleService.js +4 -4
  349. package/build/dist/Server/Services/DetectionRuleService.js.map +1 -1
  350. package/build/dist/Server/Services/OnCallDutyPolicyChildService.js +170 -0
  351. package/build/dist/Server/Services/OnCallDutyPolicyChildService.js.map +1 -0
  352. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +2 -2
  353. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
  354. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleService.js +2 -2
  355. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleService.js.map +1 -1
  356. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleTeamService.js +2 -2
  357. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleTeamService.js.map +1 -1
  358. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleUserService.js +2 -2
  359. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleUserService.js.map +1 -1
  360. package/build/dist/Server/Services/PayAsYouGoBillingService.js +20 -27
  361. package/build/dist/Server/Services/PayAsYouGoBillingService.js.map +1 -1
  362. package/build/dist/Server/Services/ProjectService.js +17 -0
  363. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  364. package/build/dist/Server/Services/RumSessionPinService.js +180 -16
  365. package/build/dist/Server/Services/RumSessionPinService.js.map +1 -1
  366. package/build/dist/Server/Services/TeamPermissionService.js +89 -27
  367. package/build/dist/Server/Services/TeamPermissionService.js.map +1 -1
  368. package/build/dist/Server/Services/ThreatIntelFeedService.js +5 -4
  369. package/build/dist/Server/Services/ThreatIntelFeedService.js.map +1 -1
  370. package/build/dist/Server/Types/Database/Permissions/BasePermission.js +9 -6
  371. package/build/dist/Server/Types/Database/Permissions/BasePermission.js.map +1 -1
  372. package/build/dist/Server/Types/Database/Permissions/Index.js +60 -6
  373. package/build/dist/Server/Types/Database/Permissions/Index.js.map +1 -1
  374. package/build/dist/Server/Types/Database/Permissions/TablePermission.js +14 -13
  375. package/build/dist/Server/Types/Database/Permissions/TablePermission.js.map +1 -1
  376. package/build/dist/Server/Utils/AI/AIChatPrivacyFilter.js +7 -0
  377. package/build/dist/Server/Utils/AI/AIChatPrivacyFilter.js.map +1 -1
  378. package/build/dist/Server/Utils/AI/AIRunPrivacyFilter.js +7 -0
  379. package/build/dist/Server/Utils/AI/AIRunPrivacyFilter.js.map +1 -1
  380. package/build/dist/Server/Utils/MasterAdminPageAccess.js +39 -0
  381. package/build/dist/Server/Utils/MasterAdminPageAccess.js.map +1 -0
  382. package/build/dist/Server/Utils/Monitor/MonitorAlert.js +3 -1
  383. package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
  384. package/build/dist/Server/Utils/Monitor/MonitorIncident.js +3 -1
  385. package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
  386. package/build/dist/Server/Utils/Realtime.js +126 -37
  387. package/build/dist/Server/Utils/Realtime.js.map +1 -1
  388. package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js +7 -25
  389. package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js.map +1 -1
  390. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOpsSeverityRepair.js +127 -0
  391. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOpsSeverityRepair.js.map +1 -0
  392. package/build/dist/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.js +1 -1
  393. package/build/dist/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.js.map +1 -1
  394. package/build/dist/Server/Utils/SecurityEvent/ThreatIntel/ThreatIntelFeedPoller.js +3 -2
  395. package/build/dist/Server/Utils/SecurityEvent/ThreatIntel/ThreatIntelFeedPoller.js.map +1 -1
  396. package/build/dist/Server/Utils/SecurityEvent/ThreatIntel/ThreatIntelMatcher.js +4 -8
  397. package/build/dist/Server/Utils/SecurityEvent/ThreatIntel/ThreatIntelMatcher.js.map +1 -1
  398. package/build/dist/Types/BaseDatabase/DatabaseCommonInteractionPropsUtil.js +31 -1
  399. package/build/dist/Types/BaseDatabase/DatabaseCommonInteractionPropsUtil.js.map +1 -1
  400. package/build/dist/Types/OnCallDutyPolicy/ScheduleTimeline.js +321 -0
  401. package/build/dist/Types/OnCallDutyPolicy/ScheduleTimeline.js.map +1 -0
  402. package/build/dist/Types/OnCallDutyPolicy/ScheduleTimelineLayout.js +372 -0
  403. package/build/dist/Types/OnCallDutyPolicy/ScheduleTimelineLayout.js.map +1 -0
  404. package/build/dist/Types/Realtime/EventName.js +10 -0
  405. package/build/dist/Types/Realtime/EventName.js.map +1 -1
  406. package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js +11 -0
  407. package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js.map +1 -1
  408. package/build/dist/Types/SecurityEvent/OcsfSeverity.js +30 -25
  409. package/build/dist/Types/SecurityEvent/OcsfSeverity.js.map +1 -1
  410. package/build/dist/Types/SecurityEvent/SigmaRule.js +47 -0
  411. package/build/dist/Types/SecurityEvent/SigmaRule.js.map +1 -1
  412. package/build/dist/Types/SecurityEvent/ThreatIntelConstants.js +14 -0
  413. package/build/dist/Types/SecurityEvent/ThreatIntelConstants.js.map +1 -1
  414. package/build/dist/UI/Components/AttachmentList/EventAttachmentList.js +9 -4
  415. package/build/dist/UI/Components/AttachmentList/EventAttachmentList.js.map +1 -1
  416. package/build/dist/UI/Components/Dashboard/DashboardVariableControl.js +10 -2
  417. package/build/dist/UI/Components/Dashboard/DashboardVariableControl.js.map +1 -1
  418. package/build/dist/UI/Components/Dropdown/Dropdown.js +5 -1
  419. package/build/dist/UI/Components/Dropdown/Dropdown.js.map +1 -1
  420. package/build/dist/UI/Components/EditionLabel/EditionLabel.js +11 -0
  421. package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
  422. package/build/dist/UI/Components/EventItem/EventItem.js +2 -2
  423. package/build/dist/UI/Components/EventItem/EventItem.js.map +1 -1
  424. package/build/dist/UI/Components/Forms/BasicForm.js +70 -64
  425. package/build/dist/UI/Components/Forms/BasicForm.js.map +1 -1
  426. package/build/dist/UI/Components/Forms/CollapsibleFormSection.js +27 -0
  427. package/build/dist/UI/Components/Forms/CollapsibleFormSection.js.map +1 -0
  428. package/build/dist/UI/Components/Forms/Fields/FormField.js +10 -7
  429. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  430. package/build/dist/UI/Components/Forms/FormSummary.js +7 -0
  431. package/build/dist/UI/Components/Forms/FormSummary.js.map +1 -1
  432. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js +2 -1
  433. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js.map +1 -1
  434. package/build/dist/UI/Components/Markdown.tsx/SessionAwareImage.js +84 -0
  435. package/build/dist/UI/Components/Markdown.tsx/SessionAwareImage.js.map +1 -0
  436. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +14 -1
  437. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  438. package/build/dist/UI/Components/ModelTable/TableView.js +1 -1
  439. package/build/dist/UI/Components/ModelTable/TableView.js.map +1 -1
  440. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +8 -1
  441. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
  442. package/build/dist/UI/Components/Tooltip/Tooltip.js +2 -1
  443. package/build/dist/UI/Components/Tooltip/Tooltip.js.map +1 -1
  444. package/build/dist/UI/Utils/API/API.js +143 -19
  445. package/build/dist/UI/Utils/API/API.js.map +1 -1
  446. package/build/dist/UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI.js +21 -7
  447. package/build/dist/UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI.js.map +1 -1
  448. package/build/dist/UI/Utils/ModelAPI/ModelAPI.js +26 -9
  449. package/build/dist/UI/Utils/ModelAPI/ModelAPI.js.map +1 -1
  450. package/build/dist/UI/Utils/Navigation.js +8 -0
  451. package/build/dist/UI/Utils/Navigation.js.map +1 -1
  452. package/build/dist/UI/Utils/OpenAuthenticatedUrl.js +92 -0
  453. package/build/dist/UI/Utils/OpenAuthenticatedUrl.js.map +1 -0
  454. package/build/dist/UI/Utils/Realtime.js +120 -15
  455. package/build/dist/UI/Utils/Realtime.js.map +1 -1
  456. package/build/dist/Utils/API.js +13 -5
  457. package/build/dist/Utils/API.js.map +1 -1
  458. package/build/dist/Utils/SecurityEvent/GoogleSecOpsAlertNormalizer.js +123 -6
  459. package/build/dist/Utils/SecurityEvent/GoogleSecOpsAlertNormalizer.js.map +1 -1
  460. package/build/dist/Utils/SecurityEvent/Sigma/SigmaRuleParser.js +4 -18
  461. package/build/dist/Utils/SecurityEvent/Sigma/SigmaRuleParser.js.map +1 -1
  462. package/build/dist/Utils/Slo/SloBurnRateTemplate.js +2 -2
  463. package/package.json +1 -1
@@ -0,0 +1,1059 @@
1
+ import "@testing-library/jest-dom";
2
+ import {
3
+ afterEach,
4
+ beforeEach,
5
+ describe,
6
+ expect,
7
+ jest,
8
+ test,
9
+ } from "@jest/globals";
10
+ import {
11
+ act,
12
+ cleanup,
13
+ fireEvent,
14
+ render,
15
+ RenderResult,
16
+ screen,
17
+ waitFor,
18
+ } from "@testing-library/react";
19
+ import React, { ReactElement, useState } from "react";
20
+ import getJestMockFunction, { MockFunction } from "../../MockType";
21
+
22
+ /*
23
+ * The affected-resources picker is the chip input on every incident, alert and
24
+ * scheduled maintenance form. In an edit form it is handed bare IDs: ModelForm
25
+ * flattens every relation it loads into Array<string>, and on a fresh mount
26
+ * the picker's name cache is empty. It used to render each of those as
27
+ * "Unnamed Monitor" (or "Unnamed Host", ...) - the Edit modal of an incident's
28
+ * Affected Resources card showed "MONITOR Unnamed Monitor" for a monitor that
29
+ * very much had a name. The picker now looks the names up.
30
+ */
31
+
32
+ const getListMock: MockFunction = getJestMockFunction();
33
+
34
+ jest.mock("../../../UI/Utils/ModelAPI/ModelAPI", () => {
35
+ return {
36
+ __esModule: true,
37
+ default: {
38
+ getList: (...args: Array<unknown>) => {
39
+ return getListMock(...args);
40
+ },
41
+ },
42
+ };
43
+ });
44
+
45
+ jest.mock("../../../UI/Utils/User", () => {
46
+ return {
47
+ __esModule: true,
48
+ default: {
49
+ isMasterAdmin: (): boolean => {
50
+ return true;
51
+ },
52
+ },
53
+ };
54
+ });
55
+
56
+ import AffectedResourcesPicker, {
57
+ AffectedResourceItem,
58
+ AffectedResourcesPayload,
59
+ AffectedResourceType,
60
+ ComponentProps,
61
+ NAME_LOADING_PLACEHOLDER,
62
+ NAME_LOOKUP_BATCH_SIZE,
63
+ getUnknownResourceLabel,
64
+ getUnnamedResourceLabel,
65
+ toItems,
66
+ } from "../../../../App/FeatureSet/Dashboard/src/Components/AffectedResources/AffectedResourcesPicker";
67
+ import BaseModel from "../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
68
+ import CephCluster from "../../../Models/DatabaseModels/CephCluster";
69
+ import DockerHost from "../../../Models/DatabaseModels/DockerHost";
70
+ import DockerSwarmCluster from "../../../Models/DatabaseModels/DockerSwarmCluster";
71
+ import Host from "../../../Models/DatabaseModels/Host";
72
+ import IoTFleet from "../../../Models/DatabaseModels/IoTFleet";
73
+ import KubernetesCluster from "../../../Models/DatabaseModels/KubernetesCluster";
74
+ import Monitor from "../../../Models/DatabaseModels/Monitor";
75
+ import NetworkSite from "../../../Models/DatabaseModels/NetworkSite";
76
+ import PodmanHost from "../../../Models/DatabaseModels/PodmanHost";
77
+ import ProxmoxCluster from "../../../Models/DatabaseModels/ProxmoxCluster";
78
+ import Service from "../../../Models/DatabaseModels/Service";
79
+ import VMwareVCenter from "../../../Models/DatabaseModels/VMwareVCenter";
80
+ import Includes from "../../../Types/BaseDatabase/Includes";
81
+ import FormSummary from "../../../UI/Components/Forms/FormSummary";
82
+ import Fields from "../../../UI/Components/Forms/Types/Fields";
83
+ import FormFieldSchemaType from "../../../UI/Components/Forms/Types/FormFieldSchemaType";
84
+ import FormValues from "../../../UI/Components/Forms/Types/FormValues";
85
+ import { JSONObject } from "../../../Types/JSON";
86
+ import Search from "../../../Types/BaseDatabase/Search";
87
+
88
+ type ModelClass = { new (): BaseModel };
89
+
90
+ interface FakeRow {
91
+ _id: string;
92
+ name?: string | undefined;
93
+ }
94
+
95
+ interface GetListArgs {
96
+ modelType: ModelClass;
97
+ query: Record<string, unknown>;
98
+ limit: number;
99
+ skip: number;
100
+ select: Record<string, unknown>;
101
+ sort: Record<string, unknown>;
102
+ }
103
+
104
+ interface Deferred {
105
+ promise: Promise<void>;
106
+ resolve: () => void;
107
+ }
108
+
109
+ const makeDeferred: () => Deferred = (): Deferred => {
110
+ let resolve: () => void = (): void => {};
111
+ const promise: Promise<void> = new Promise<void>((res: () => void) => {
112
+ resolve = res;
113
+ });
114
+ return { promise, resolve };
115
+ };
116
+
117
+ const id: (n: number) => string = (n: number): string => {
118
+ return `00000000-0000-4000-8000-${String(n).padStart(12, "0")}`;
119
+ };
120
+
121
+ const MONITOR_ID: string = id(1);
122
+ const MONITOR_NAME: string = "Checkout API";
123
+ const SECOND_MONITOR_ID: string = id(2);
124
+ const SECOND_MONITOR_NAME: string = "Orders DB";
125
+
126
+ /*
127
+ * A tiny in-memory stand-in for the API. Each model type has its own rows;
128
+ * getList honours `_id: Includes([...])` and `name: Search(...)` the way the
129
+ * server does, and returns real model instances.
130
+ */
131
+ let fakeDb: Map<ModelClass, Array<FakeRow>> = new Map();
132
+ let failingModelTypes: Set<ModelClass> = new Set();
133
+ let gate: Deferred | null = null;
134
+
135
+ const seed: (modelType: ModelClass, rows: Array<FakeRow>) => void = (
136
+ modelType: ModelClass,
137
+ rows: Array<FakeRow>,
138
+ ): void => {
139
+ fakeDb.set(modelType, [...(fakeDb.get(modelType) || []), ...rows]);
140
+ };
141
+
142
+ const fakeGetList: (args: GetListArgs) => Promise<{
143
+ data: Array<BaseModel>;
144
+ count: number;
145
+ skip: number;
146
+ limit: number;
147
+ }> = async (
148
+ args: GetListArgs,
149
+ ): Promise<{
150
+ data: Array<BaseModel>;
151
+ count: number;
152
+ skip: number;
153
+ limit: number;
154
+ }> => {
155
+ if (gate) {
156
+ await gate.promise;
157
+ }
158
+ if (failingModelTypes.has(args.modelType)) {
159
+ throw new Error("Request failed");
160
+ }
161
+ let rows: Array<FakeRow> = fakeDb.get(args.modelType) || [];
162
+ const idFilter: unknown = args.query["_id"];
163
+ if (idFilter instanceof Includes) {
164
+ const wanted: Set<string> = new Set(
165
+ (idFilter.values as Array<string>).map((v: string) => {
166
+ return String(v);
167
+ }),
168
+ );
169
+ rows = rows.filter((row: FakeRow) => {
170
+ return wanted.has(row._id);
171
+ });
172
+ }
173
+ const nameFilter: unknown = args.query["name"];
174
+ if (nameFilter instanceof Search) {
175
+ const needle: string = String(nameFilter.toString()).toLowerCase();
176
+ rows = rows.filter((row: FakeRow) => {
177
+ return (row.name || "").toLowerCase().includes(needle);
178
+ });
179
+ }
180
+ if (args.query["description"] !== undefined) {
181
+ rows = [];
182
+ }
183
+ const data: Array<BaseModel> = rows
184
+ .slice(0, args.limit)
185
+ .map((row: FakeRow) => {
186
+ const model: BaseModel = new args.modelType();
187
+ model._id = row._id;
188
+ (model as unknown as { name?: string | undefined }).name = row.name;
189
+ return model;
190
+ });
191
+ return { data, count: data.length, skip: 0, limit: args.limit };
192
+ };
193
+
194
+ const lookupCalls: () => Array<GetListArgs> = (): Array<GetListArgs> => {
195
+ return getListMock.mock.calls
196
+ .map((call: Array<unknown>) => {
197
+ return call[0] as GetListArgs;
198
+ })
199
+ .filter((args: GetListArgs) => {
200
+ return args.query["_id"] instanceof Includes;
201
+ });
202
+ };
203
+
204
+ const lookedUpIds: (args: GetListArgs) => Array<string> = (
205
+ args: GetListArgs,
206
+ ): Array<string> => {
207
+ return ((args.query["_id"] as Includes).values as Array<string>).map(
208
+ (v: string) => {
209
+ return String(v);
210
+ },
211
+ );
212
+ };
213
+
214
+ const ALL_RESOURCE_TYPES: Array<AffectedResourceType> = [
215
+ "Monitor",
216
+ "Host",
217
+ "KubernetesCluster",
218
+ "DockerHost",
219
+ "PodmanHost",
220
+ "ProxmoxCluster",
221
+ "VMwareVCenter",
222
+ "CephCluster",
223
+ "DockerSwarmCluster",
224
+ "IoTFleet",
225
+ "NetworkSite",
226
+ "Service",
227
+ ];
228
+
229
+ const noop: (payload: AffectedResourcesPayload) => void = (): void => {};
230
+
231
+ const renderPicker: (props: Partial<ComponentProps>) => RenderResult = (
232
+ props: Partial<ComponentProps>,
233
+ ): RenderResult => {
234
+ return render(<AffectedResourcesPicker onChange={noop} {...props} />);
235
+ };
236
+
237
+ const asModels: <T>(ids: Array<string>) => Array<T> = <T,>(
238
+ ids: Array<string>,
239
+ ): Array<T> => {
240
+ // What an edit form actually passes: the prop is typed Array<Model>, the value is Array<string>.
241
+ return ids as unknown as Array<T>;
242
+ };
243
+
244
+ beforeEach(() => {
245
+ fakeDb = new Map();
246
+ failingModelTypes = new Set();
247
+ gate = null;
248
+ getListMock.mockReset();
249
+ getListMock.mockImplementation((...args: Array<unknown>) => {
250
+ return fakeGetList(args[0] as GetListArgs);
251
+ });
252
+ });
253
+
254
+ afterEach(() => {
255
+ cleanup();
256
+ });
257
+
258
+ describe("toItems", () => {
259
+ test("a bare ID nobody has named yet is waiting for its name, not 'Unnamed'", () => {
260
+ const cache: Map<string, string> = new Map();
261
+
262
+ const items: Array<AffectedResourceItem> = toItems(
263
+ [MONITOR_ID],
264
+ "Monitor",
265
+ cache,
266
+ new Set(),
267
+ );
268
+
269
+ expect(items).toEqual([
270
+ {
271
+ _id: MONITOR_ID,
272
+ name: NAME_LOADING_PLACEHOLDER,
273
+ type: "Monitor",
274
+ isNameLoading: true,
275
+ },
276
+ ]);
277
+ expect(items[0]!.name).not.toBe(getUnnamedResourceLabel("Monitor"));
278
+ });
279
+
280
+ test("a bare ID whose name is cached shows that name", () => {
281
+ const cache: Map<string, string> = new Map([
282
+ [`Monitor:${MONITOR_ID}`, MONITOR_NAME],
283
+ ]);
284
+
285
+ expect(toItems([MONITOR_ID], "Monitor", cache, new Set())).toEqual([
286
+ { _id: MONITOR_ID, name: MONITOR_NAME, type: "Monitor" },
287
+ ]);
288
+ });
289
+
290
+ test("the cache is keyed per type, so a host never borrows a monitor's name", () => {
291
+ const cache: Map<string, string> = new Map([
292
+ [`Monitor:${MONITOR_ID}`, MONITOR_NAME],
293
+ ]);
294
+
295
+ const items: Array<AffectedResourceItem> = toItems(
296
+ [MONITOR_ID],
297
+ "Host",
298
+ cache,
299
+ new Set(),
300
+ );
301
+
302
+ expect(items[0]!.isNameLoading).toBe(true);
303
+ expect(items[0]!.name).not.toBe(MONITOR_NAME);
304
+ });
305
+
306
+ test("a model instance that carries its name is shown and cached", () => {
307
+ const cache: Map<string, string> = new Map();
308
+ const monitor: Monitor = new Monitor();
309
+ monitor._id = MONITOR_ID;
310
+ monitor.name = MONITOR_NAME;
311
+
312
+ expect(toItems([monitor], "Monitor", cache, new Set())).toEqual([
313
+ { _id: MONITOR_ID, name: MONITOR_NAME, type: "Monitor" },
314
+ ]);
315
+ expect(cache.get(`Monitor:${MONITOR_ID}`)).toBe(MONITOR_NAME);
316
+ });
317
+
318
+ test("a plain object may use `id` instead of `_id`", () => {
319
+ expect(
320
+ toItems(
321
+ [{ id: MONITOR_ID, name: MONITOR_NAME }],
322
+ "Monitor",
323
+ new Map(),
324
+ new Set(),
325
+ ),
326
+ ).toEqual([{ _id: MONITOR_ID, name: MONITOR_NAME, type: "Monitor" }]);
327
+ });
328
+
329
+ test("an `{ _id }`-only object - a relation selected as `true` - is looked up like a bare ID", () => {
330
+ const items: Array<AffectedResourceItem> = toItems(
331
+ [{ _id: MONITOR_ID }],
332
+ "Monitor",
333
+ new Map(),
334
+ new Set(),
335
+ );
336
+
337
+ expect(items).toEqual([
338
+ {
339
+ _id: MONITOR_ID,
340
+ name: NAME_LOADING_PLACEHOLDER,
341
+ type: "Monitor",
342
+ isNameLoading: true,
343
+ },
344
+ ]);
345
+ });
346
+
347
+ test("an object with an empty name falls back to the cache before anything else", () => {
348
+ const cache: Map<string, string> = new Map([
349
+ [`Monitor:${MONITOR_ID}`, MONITOR_NAME],
350
+ ]);
351
+
352
+ expect(
353
+ toItems([{ _id: MONITOR_ID, name: "" }], "Monitor", cache, new Set()),
354
+ ).toEqual([{ _id: MONITOR_ID, name: MONITOR_NAME, type: "Monitor" }]);
355
+ });
356
+
357
+ test("an ID whose lookup failed is shown as unknown and no longer waits", () => {
358
+ const items: Array<AffectedResourceItem> = toItems(
359
+ [MONITOR_ID],
360
+ "Monitor",
361
+ new Map(),
362
+ new Set([`Monitor:${MONITOR_ID}`]),
363
+ );
364
+
365
+ expect(items).toEqual([
366
+ { _id: MONITOR_ID, name: "Unknown Monitor", type: "Monitor" },
367
+ ]);
368
+ });
369
+
370
+ test("a name that turns up after a failed lookup wins over 'unknown'", () => {
371
+ const cache: Map<string, string> = new Map([
372
+ [`Monitor:${MONITOR_ID}`, MONITOR_NAME],
373
+ ]);
374
+
375
+ expect(
376
+ toItems(
377
+ [MONITOR_ID],
378
+ "Monitor",
379
+ cache,
380
+ new Set([`Monitor:${MONITOR_ID}`]),
381
+ ),
382
+ ).toEqual([{ _id: MONITOR_ID, name: MONITOR_NAME, type: "Monitor" }]);
383
+ });
384
+
385
+ test("placeholders are never written to the cache", () => {
386
+ const cache: Map<string, string> = new Map();
387
+
388
+ toItems(
389
+ [MONITOR_ID, { _id: SECOND_MONITOR_ID }],
390
+ "Monitor",
391
+ cache,
392
+ new Set([`Monitor:${SECOND_MONITOR_ID}`]),
393
+ );
394
+
395
+ expect(cache.size).toBe(0);
396
+ });
397
+
398
+ test("empty entries and objects without an ID are skipped", () => {
399
+ expect(
400
+ toItems(
401
+ [null, undefined, "", { name: "orphan" }, MONITOR_ID],
402
+ "Monitor",
403
+ new Map(),
404
+ new Set(),
405
+ ).map((item: AffectedResourceItem) => {
406
+ return item._id;
407
+ }),
408
+ ).toEqual([MONITOR_ID]);
409
+ });
410
+
411
+ test("an undefined or empty list gives no items", () => {
412
+ expect(toItems(undefined, "Monitor", new Map(), new Set())).toEqual([]);
413
+ expect(toItems([], "Monitor", new Map(), new Set())).toEqual([]);
414
+ });
415
+
416
+ test("a value that is not a list - the picker's own payload, mid form update - gives no items instead of throwing", () => {
417
+ const payload: AffectedResourcesPayload = {
418
+ __affectedResourcesPayload: true,
419
+ monitors: [MONITOR_ID],
420
+ hosts: [],
421
+ kubernetesClusters: [],
422
+ dockerHosts: [],
423
+ podmanHosts: [],
424
+ proxmoxClusters: [],
425
+ vmwareVCenters: [],
426
+ cephClusters: [],
427
+ dockerSwarmClusters: [],
428
+ iotFleets: [],
429
+ networkSites: [],
430
+ services: [],
431
+ };
432
+
433
+ expect(
434
+ toItems(
435
+ payload as unknown as Array<unknown>,
436
+ "Monitor",
437
+ new Map(),
438
+ new Set(),
439
+ ),
440
+ ).toEqual([]);
441
+ expect(
442
+ toItems(
443
+ MONITOR_ID as unknown as Array<unknown>,
444
+ "Monitor",
445
+ new Map(),
446
+ new Set(),
447
+ ),
448
+ ).toEqual([]);
449
+ });
450
+
451
+ test("fallback labels use each type's display label", () => {
452
+ expect(getUnnamedResourceLabel("KubernetesCluster")).toBe(
453
+ "Unnamed Kubernetes Cluster",
454
+ );
455
+ expect(getUnknownResourceLabel("KubernetesCluster")).toBe(
456
+ "Unknown Kubernetes Cluster",
457
+ );
458
+ expect(getUnknownResourceLabel("VMwareVCenter")).toBe("Unknown vCenter");
459
+ expect(getUnknownResourceLabel("Service")).toBe("Unknown Service");
460
+ });
461
+ });
462
+
463
+ describe("AffectedResourcesPicker chips for resources that arrive as bare IDs", () => {
464
+ test("shows the monitor's real name, never 'Unnamed Monitor' (the Edit Incident modal regression)", async () => {
465
+ seed(Monitor, [{ _id: MONITOR_ID, name: MONITOR_NAME }]);
466
+
467
+ renderPicker({
468
+ monitors: asModels<Monitor>([MONITOR_ID]),
469
+ resourceTypes: ["Monitor"],
470
+ });
471
+
472
+ // While the lookup is in flight the chip says so, rather than inventing a name.
473
+ const pending: HTMLElement = screen.getByText(NAME_LOADING_PLACEHOLDER);
474
+ expect(pending).toHaveAttribute("aria-busy", "true");
475
+ expect(screen.queryByText("Unnamed Monitor")).toBeNull();
476
+
477
+ expect(await screen.findByText(MONITOR_NAME)).toBeInTheDocument();
478
+ expect(screen.queryByText("Unnamed Monitor")).toBeNull();
479
+ expect(screen.queryByText(NAME_LOADING_PLACEHOLDER)).toBeNull();
480
+ expect(screen.getByText(MONITOR_NAME)).not.toHaveAttribute("aria-busy");
481
+ });
482
+
483
+ test("asks the API for exactly the IDs it needs, by ID, selecting only the name", async () => {
484
+ seed(Monitor, [
485
+ { _id: MONITOR_ID, name: MONITOR_NAME },
486
+ { _id: SECOND_MONITOR_ID, name: SECOND_MONITOR_NAME },
487
+ ]);
488
+
489
+ renderPicker({
490
+ monitors: asModels<Monitor>([MONITOR_ID, SECOND_MONITOR_ID]),
491
+ resourceTypes: ["Monitor"],
492
+ });
493
+
494
+ await screen.findByText(SECOND_MONITOR_NAME);
495
+
496
+ expect(getListMock).toHaveBeenCalledTimes(1);
497
+ const call: GetListArgs = lookupCalls()[0]!;
498
+ expect(call.modelType).toBe(Monitor);
499
+ expect(lookedUpIds(call)).toEqual([MONITOR_ID, SECOND_MONITOR_ID]);
500
+ expect(call.select).toEqual({ _id: true, name: true });
501
+ expect(call.limit).toBe(2);
502
+ expect(call.skip).toBe(0);
503
+ });
504
+
505
+ test("resolves `{ _id }`-only objects too", async () => {
506
+ seed(Monitor, [{ _id: MONITOR_ID, name: MONITOR_NAME }]);
507
+
508
+ renderPicker({
509
+ monitors: [{ _id: MONITOR_ID } as unknown as Monitor],
510
+ resourceTypes: ["Monitor"],
511
+ });
512
+
513
+ expect(await screen.findByText(MONITOR_NAME)).toBeInTheDocument();
514
+ });
515
+
516
+ test("makes no request when every resource already carries its name", async () => {
517
+ const monitor: Monitor = new Monitor();
518
+ monitor._id = MONITOR_ID;
519
+ monitor.name = MONITOR_NAME;
520
+
521
+ renderPicker({ monitors: [monitor], resourceTypes: ["Monitor"] });
522
+
523
+ expect(screen.getByText(MONITOR_NAME)).toBeInTheDocument();
524
+ // Give any stray effect a chance to fire before asserting it did not.
525
+ await act(async () => {
526
+ await Promise.resolve();
527
+ });
528
+ expect(getListMock).not.toHaveBeenCalled();
529
+ });
530
+
531
+ test("names every resource type with its own model, one request per type", async () => {
532
+ const models: Record<AffectedResourceType, ModelClass> = {
533
+ Monitor: Monitor,
534
+ Host: Host,
535
+ KubernetesCluster: KubernetesCluster,
536
+ DockerHost: DockerHost,
537
+ PodmanHost: PodmanHost,
538
+ ProxmoxCluster: ProxmoxCluster,
539
+ VMwareVCenter: VMwareVCenter,
540
+ CephCluster: CephCluster,
541
+ DockerSwarmCluster: DockerSwarmCluster,
542
+ IoTFleet: IoTFleet,
543
+ NetworkSite: NetworkSite,
544
+ Service: Service,
545
+ };
546
+ ALL_RESOURCE_TYPES.forEach((type: AffectedResourceType, index: number) => {
547
+ seed(models[type], [{ _id: id(100 + index), name: `${type} one` }]);
548
+ });
549
+
550
+ const idFor: (type: AffectedResourceType) => Array<never> = (
551
+ type: AffectedResourceType,
552
+ ): Array<never> => {
553
+ return asModels<never>([id(100 + ALL_RESOURCE_TYPES.indexOf(type))]);
554
+ };
555
+
556
+ renderPicker({
557
+ monitors: idFor("Monitor"),
558
+ hosts: idFor("Host"),
559
+ kubernetesClusters: idFor("KubernetesCluster"),
560
+ dockerHosts: idFor("DockerHost"),
561
+ podmanHosts: idFor("PodmanHost"),
562
+ proxmoxClusters: idFor("ProxmoxCluster"),
563
+ vmwareVCenters: idFor("VMwareVCenter"),
564
+ cephClusters: idFor("CephCluster"),
565
+ dockerSwarmClusters: idFor("DockerSwarmCluster"),
566
+ iotFleets: idFor("IoTFleet"),
567
+ networkSites: idFor("NetworkSite"),
568
+ services: idFor("Service"),
569
+ resourceTypes: ALL_RESOURCE_TYPES,
570
+ });
571
+
572
+ for (const type of ALL_RESOURCE_TYPES) {
573
+ expect(await screen.findByText(`${type} one`)).toBeInTheDocument();
574
+ }
575
+ expect(screen.queryByText(/^Unnamed /)).toBeNull();
576
+ expect(screen.queryByText(/^Unknown /)).toBeNull();
577
+
578
+ const calls: Array<GetListArgs> = lookupCalls();
579
+ expect(calls).toHaveLength(ALL_RESOURCE_TYPES.length);
580
+ for (const type of ALL_RESOURCE_TYPES) {
581
+ const forType: Array<GetListArgs> = calls.filter((call: GetListArgs) => {
582
+ return call.modelType === models[type];
583
+ });
584
+ expect(forType).toHaveLength(1);
585
+ expect(lookedUpIds(forType[0]!)).toEqual([
586
+ id(100 + ALL_RESOURCE_TYPES.indexOf(type)),
587
+ ]);
588
+ }
589
+ });
590
+
591
+ test("does not look up a type the page does not offer", async () => {
592
+ seed(Monitor, [{ _id: MONITOR_ID, name: MONITOR_NAME }]);
593
+ seed(Host, [{ _id: id(50), name: "web-01" }]);
594
+
595
+ renderPicker({
596
+ monitors: asModels<Monitor>([MONITOR_ID]),
597
+ hosts: asModels<Host>([id(50)]),
598
+ resourceTypes: ["Monitor"],
599
+ });
600
+
601
+ await screen.findByText(MONITOR_NAME);
602
+
603
+ expect(
604
+ lookupCalls().map((call: GetListArgs) => {
605
+ return call.modelType;
606
+ }),
607
+ ).toEqual([Monitor]);
608
+ expect(screen.queryByText("web-01")).toBeNull();
609
+ });
610
+
611
+ test(`splits a large selection into batches of ${NAME_LOOKUP_BATCH_SIZE}`, async () => {
612
+ const count: number = NAME_LOOKUP_BATCH_SIZE * 2 + 50;
613
+ const ids: Array<string> = [];
614
+ for (let i: number = 0; i < count; i++) {
615
+ ids.push(id(1000 + i));
616
+ }
617
+ seed(
618
+ Monitor,
619
+ ids.map((monitorId: string, i: number) => {
620
+ return { _id: monitorId, name: `monitor-${i}` };
621
+ }),
622
+ );
623
+
624
+ renderPicker({
625
+ monitors: asModels<Monitor>(ids),
626
+ resourceTypes: ["Monitor"],
627
+ });
628
+
629
+ // The first chip is always rendered, even past the visible-chip cap.
630
+ expect(await screen.findByText("monitor-0")).toBeInTheDocument();
631
+ await waitFor(() => {
632
+ expect(screen.queryByText(NAME_LOADING_PLACEHOLDER)).toBeNull();
633
+ });
634
+
635
+ const calls: Array<GetListArgs> = lookupCalls();
636
+ expect(
637
+ calls.map((call: GetListArgs) => {
638
+ return lookedUpIds(call).length;
639
+ }),
640
+ ).toEqual([NAME_LOOKUP_BATCH_SIZE, NAME_LOOKUP_BATCH_SIZE, 50]);
641
+ expect(
642
+ calls.flatMap((call: GetListArgs) => {
643
+ return lookedUpIds(call);
644
+ }),
645
+ ).toEqual(ids);
646
+
647
+ // Every chip is named once revealed, including the ones in the last batch.
648
+ fireEvent.click(screen.getByRole("button", { name: /more/ }));
649
+ expect(screen.getByText(`monitor-${count - 1}`)).toBeInTheDocument();
650
+ expect(screen.queryByText("Unnamed Monitor")).toBeNull();
651
+ });
652
+
653
+ test("a re-render with the same IDs does not ask again, in flight or after", async () => {
654
+ seed(Monitor, [{ _id: MONITOR_ID, name: MONITOR_NAME }]);
655
+ gate = makeDeferred();
656
+
657
+ const view: RenderResult = renderPicker({
658
+ monitors: asModels<Monitor>([MONITOR_ID]),
659
+ resourceTypes: ["Monitor"],
660
+ });
661
+
662
+ // New array identity, same IDs, while the first lookup is still pending.
663
+ view.rerender(
664
+ <AffectedResourcesPicker
665
+ onChange={noop}
666
+ monitors={asModels<Monitor>([MONITOR_ID])}
667
+ resourceTypes={["Monitor"]}
668
+ />,
669
+ );
670
+ expect(getListMock).toHaveBeenCalledTimes(1);
671
+
672
+ await act(async () => {
673
+ gate!.resolve();
674
+ });
675
+ expect(await screen.findByText(MONITOR_NAME)).toBeInTheDocument();
676
+
677
+ view.rerender(
678
+ <AffectedResourcesPicker
679
+ onChange={noop}
680
+ monitors={asModels<Monitor>([MONITOR_ID])}
681
+ resourceTypes={["Monitor"]}
682
+ />,
683
+ );
684
+ await act(async () => {
685
+ await Promise.resolve();
686
+ });
687
+ expect(getListMock).toHaveBeenCalledTimes(1);
688
+ expect(screen.getByText(MONITOR_NAME)).toBeInTheDocument();
689
+ });
690
+
691
+ test("when the selection grows, only the new ID is looked up", async () => {
692
+ seed(Monitor, [
693
+ { _id: MONITOR_ID, name: MONITOR_NAME },
694
+ { _id: SECOND_MONITOR_ID, name: SECOND_MONITOR_NAME },
695
+ ]);
696
+
697
+ const view: RenderResult = renderPicker({
698
+ monitors: asModels<Monitor>([MONITOR_ID]),
699
+ resourceTypes: ["Monitor"],
700
+ });
701
+ await screen.findByText(MONITOR_NAME);
702
+
703
+ view.rerender(
704
+ <AffectedResourcesPicker
705
+ onChange={noop}
706
+ monitors={asModels<Monitor>([MONITOR_ID, SECOND_MONITOR_ID])}
707
+ resourceTypes={["Monitor"]}
708
+ />,
709
+ );
710
+
711
+ expect(await screen.findByText(SECOND_MONITOR_NAME)).toBeInTheDocument();
712
+ expect(screen.getByText(MONITOR_NAME)).toBeInTheDocument();
713
+ const calls: Array<GetListArgs> = lookupCalls();
714
+ expect(calls).toHaveLength(2);
715
+ expect(lookedUpIds(calls[1]!)).toEqual([SECOND_MONITOR_ID]);
716
+ });
717
+
718
+ test("a resource that really has no name is the one case that reads 'Unnamed'", async () => {
719
+ seed(Monitor, [{ _id: MONITOR_ID, name: "" }]);
720
+
721
+ renderPicker({
722
+ monitors: asModels<Monitor>([MONITOR_ID]),
723
+ resourceTypes: ["Monitor"],
724
+ });
725
+
726
+ expect(await screen.findByText("Unnamed Monitor")).toBeInTheDocument();
727
+ });
728
+
729
+ test("an ID the API does not return reads 'Unknown' and is not asked for again", async () => {
730
+ seed(Monitor, [{ _id: MONITOR_ID, name: MONITOR_NAME }]);
731
+
732
+ const view: RenderResult = renderPicker({
733
+ monitors: asModels<Monitor>([MONITOR_ID, SECOND_MONITOR_ID]),
734
+ resourceTypes: ["Monitor"],
735
+ });
736
+
737
+ expect(await screen.findByText("Unknown Monitor")).toBeInTheDocument();
738
+ expect(screen.getByText(MONITOR_NAME)).toBeInTheDocument();
739
+ expect(screen.queryByText("Unnamed Monitor")).toBeNull();
740
+
741
+ view.rerender(
742
+ <AffectedResourcesPicker
743
+ onChange={noop}
744
+ monitors={asModels<Monitor>([MONITOR_ID, SECOND_MONITOR_ID])}
745
+ resourceTypes={["Monitor"]}
746
+ />,
747
+ );
748
+ await act(async () => {
749
+ await Promise.resolve();
750
+ });
751
+ expect(getListMock).toHaveBeenCalledTimes(1);
752
+ });
753
+
754
+ test("a failed lookup for one type leaves the other types named", async () => {
755
+ seed(Monitor, [{ _id: MONITOR_ID, name: MONITOR_NAME }]);
756
+ seed(Host, [{ _id: id(50), name: "web-01" }]);
757
+ failingModelTypes.add(Host);
758
+
759
+ renderPicker({
760
+ monitors: asModels<Monitor>([MONITOR_ID]),
761
+ hosts: asModels<Host>([id(50)]),
762
+ resourceTypes: ["Monitor", "Host"],
763
+ });
764
+
765
+ expect(await screen.findByText(MONITOR_NAME)).toBeInTheDocument();
766
+ expect(await screen.findByText("Unknown Host")).toBeInTheDocument();
767
+ expect(screen.queryByText(NAME_LOADING_PLACEHOLDER)).toBeNull();
768
+ });
769
+
770
+ test("a chip can be removed while its name is still loading", async () => {
771
+ seed(Monitor, [
772
+ { _id: MONITOR_ID, name: MONITOR_NAME },
773
+ { _id: SECOND_MONITOR_ID, name: SECOND_MONITOR_NAME },
774
+ ]);
775
+ gate = makeDeferred();
776
+ const onChange: MockFunction = getJestMockFunction();
777
+
778
+ renderPicker({
779
+ monitors: asModels<Monitor>([MONITOR_ID, SECOND_MONITOR_ID]),
780
+ resourceTypes: ["Monitor"],
781
+ onChange: onChange as unknown as ComponentProps["onChange"],
782
+ });
783
+
784
+ const removeButtons: Array<HTMLElement> = screen.getAllByRole("button", {
785
+ name: "Remove monitor",
786
+ });
787
+ expect(removeButtons).toHaveLength(2);
788
+ fireEvent.click(removeButtons[0]!);
789
+
790
+ const payload: AffectedResourcesPayload = onChange.mock
791
+ .calls[0]![0] as AffectedResourcesPayload;
792
+ expect(payload.monitors).toEqual([SECOND_MONITOR_ID]);
793
+
794
+ await act(async () => {
795
+ gate!.resolve();
796
+ });
797
+ });
798
+
799
+ test("the remove button is labelled with the real name once it lands", async () => {
800
+ seed(Monitor, [{ _id: MONITOR_ID, name: MONITOR_NAME }]);
801
+
802
+ renderPicker({
803
+ monitors: asModels<Monitor>([MONITOR_ID]),
804
+ resourceTypes: ["Monitor"],
805
+ });
806
+
807
+ expect(
808
+ await screen.findByRole("button", { name: `Remove ${MONITOR_NAME}` }),
809
+ ).toBeInTheDocument();
810
+ expect(
811
+ screen.queryByRole("button", { name: "Remove Unnamed Monitor" }),
812
+ ).toBeNull();
813
+ });
814
+
815
+ test("a lookup that lands after the picker is gone does not throw", async () => {
816
+ seed(Monitor, [{ _id: MONITOR_ID, name: MONITOR_NAME }]);
817
+ gate = makeDeferred();
818
+
819
+ const view: RenderResult = renderPicker({
820
+ monitors: asModels<Monitor>([MONITOR_ID]),
821
+ resourceTypes: ["Monitor"],
822
+ });
823
+ view.unmount();
824
+
825
+ await act(async () => {
826
+ gate!.resolve();
827
+ await gate!.promise;
828
+ });
829
+ expect(getListMock).toHaveBeenCalledTimes(1);
830
+ });
831
+
832
+ test("works under StrictMode's double effect without leaving a chip on 'Loading...'", async () => {
833
+ seed(Monitor, [{ _id: MONITOR_ID, name: MONITOR_NAME }]);
834
+
835
+ render(
836
+ <React.StrictMode>
837
+ <AffectedResourcesPicker
838
+ onChange={noop}
839
+ monitors={asModels<Monitor>([MONITOR_ID])}
840
+ resourceTypes={["Monitor"]}
841
+ />
842
+ </React.StrictMode>,
843
+ );
844
+
845
+ expect(await screen.findByText(MONITOR_NAME)).toBeInTheDocument();
846
+ expect(screen.queryByText(NAME_LOADING_PLACEHOLDER)).toBeNull();
847
+ expect(lookupCalls()).toHaveLength(1);
848
+ });
849
+ });
850
+
851
+ /*
852
+ * The form round trip the picker lives in: it reports IDs, the page's onChange
853
+ * writes those IDs back into the form values, and the picker re-renders from
854
+ * bare IDs. This harness plays the form.
855
+ */
856
+ const FormHarness: (props: {
857
+ initialMonitors: Array<string>;
858
+ }) => ReactElement = (props: {
859
+ initialMonitors: Array<string>;
860
+ }): ReactElement => {
861
+ const [monitors, setMonitors] = useState<Array<string>>(
862
+ props.initialMonitors,
863
+ );
864
+ return (
865
+ <AffectedResourcesPicker
866
+ monitors={asModels<Monitor>(monitors)}
867
+ resourceTypes={["Monitor"]}
868
+ onChange={(payload: AffectedResourcesPayload) => {
869
+ // Monitor is a type this picker shows, so it always comes back as an array.
870
+ setMonitors(payload.monitors || []);
871
+ }}
872
+ />
873
+ );
874
+ };
875
+
876
+ describe("AffectedResourcesPicker inside a form round trip", () => {
877
+ test("a monitor picked from the search keeps its name once the form stores only its ID", async () => {
878
+ seed(Monitor, [
879
+ { _id: MONITOR_ID, name: MONITOR_NAME },
880
+ { _id: SECOND_MONITOR_ID, name: SECOND_MONITOR_NAME },
881
+ ]);
882
+
883
+ render(<FormHarness initialMonitors={[]} />);
884
+
885
+ fireEvent.focus(screen.getByRole("combobox"));
886
+ fireEvent.click(
887
+ await screen.findByRole("option", { name: SECOND_MONITOR_NAME }),
888
+ );
889
+
890
+ /*
891
+ * The chip reads from a bare ID now; the search result already named it.
892
+ * The dropdown stays open after a pick, so check for the chip's own
893
+ * remove button (named, so not stuck on "Loading...") and that the
894
+ * option left the list — the same text would otherwise match either.
895
+ */
896
+ expect(
897
+ await screen.findByRole("button", {
898
+ name: `Remove ${SECOND_MONITOR_NAME}`,
899
+ }),
900
+ ).toBeInTheDocument();
901
+ expect(
902
+ screen.queryByRole("option", { name: SECOND_MONITOR_NAME }),
903
+ ).toBeNull();
904
+ expect(screen.queryByText("Unnamed Monitor")).toBeNull();
905
+ expect(screen.queryByText(NAME_LOADING_PLACEHOLDER)).toBeNull();
906
+ expect(lookupCalls()).toHaveLength(0);
907
+ });
908
+
909
+ test("an edit form that opens on saved IDs names them, and removing one keeps the rest named", async () => {
910
+ seed(Monitor, [
911
+ { _id: MONITOR_ID, name: MONITOR_NAME },
912
+ { _id: SECOND_MONITOR_ID, name: SECOND_MONITOR_NAME },
913
+ ]);
914
+
915
+ render(<FormHarness initialMonitors={[MONITOR_ID, SECOND_MONITOR_ID]} />);
916
+
917
+ await screen.findByText(MONITOR_NAME);
918
+ await screen.findByText(SECOND_MONITOR_NAME);
919
+
920
+ fireEvent.click(
921
+ screen.getByRole("button", { name: `Remove ${MONITOR_NAME}` }),
922
+ );
923
+
924
+ await waitFor(() => {
925
+ expect(screen.queryByText(MONITOR_NAME)).toBeNull();
926
+ });
927
+ expect(screen.getByText(SECOND_MONITOR_NAME)).toBeInTheDocument();
928
+ expect(lookupCalls()).toHaveLength(1);
929
+ });
930
+ });
931
+
932
+ describe("AffectedResourcesPicker read-only", () => {
933
+ test("names bare IDs, with no search input and no remove buttons", async () => {
934
+ seed(Host, [{ _id: id(50), name: "web-01" }]);
935
+ seed(Service, [{ _id: id(60), name: "checkout-api" }]);
936
+
937
+ renderPicker({
938
+ readOnly: true,
939
+ hosts: asModels<Host>([id(50)]),
940
+ services: asModels<Service>([id(60)]),
941
+ resourceTypes: ["Host", "Service"],
942
+ });
943
+
944
+ expect(await screen.findByText("web-01")).toBeInTheDocument();
945
+ expect(await screen.findByText("checkout-api")).toBeInTheDocument();
946
+ expect(screen.queryByRole("combobox")).toBeNull();
947
+ expect(screen.queryByRole("button", { name: /^Remove / })).toBeNull();
948
+ expect(screen.queryByRole("listbox")).toBeNull();
949
+ });
950
+
951
+ test("a long selection can still be expanded, but offers no Clear all", async () => {
952
+ const ids: Array<string> = [];
953
+ for (let i: number = 0; i < 60; i++) {
954
+ ids.push(id(2000 + i));
955
+ }
956
+ seed(
957
+ Monitor,
958
+ ids.map((monitorId: string, i: number) => {
959
+ return { _id: monitorId, name: `monitor-${i}` };
960
+ }),
961
+ );
962
+
963
+ renderPicker({
964
+ readOnly: true,
965
+ monitors: asModels<Monitor>(ids),
966
+ resourceTypes: ["Monitor"],
967
+ });
968
+
969
+ await screen.findByText("monitor-0");
970
+ expect(screen.queryByRole("button", { name: "Clear all" })).toBeNull();
971
+
972
+ fireEvent.click(screen.getByRole("button", { name: "+ 10 more" }));
973
+ expect(await screen.findByText("monitor-59")).toBeInTheDocument();
974
+ });
975
+
976
+ test("disabled is not read-only: a disabled picker still shows its (disabled) input", () => {
977
+ renderPicker({
978
+ disabled: true,
979
+ monitors: [],
980
+ resourceTypes: ["Monitor"],
981
+ });
982
+
983
+ expect(screen.getByRole("combobox")).toBeDisabled();
984
+ });
985
+ });
986
+
987
+ /*
988
+ * Create Alert's summary step. The wizard stores the picked resources as
989
+ * bare IDs; the field's own summary element hands them to a read-only picker
990
+ * so the summary names them, instead of the generic summary printing the
991
+ * host IDs raw and dropping every other type.
992
+ */
993
+ describe("AffectedResourcesPicker as a wizard summary", () => {
994
+ const HOST_ID: string = id(70);
995
+ const CLUSTER_ID: string = id(71);
996
+ const PODMAN_ID: string = id(72);
997
+ const SERVICE_ID: string = id(73);
998
+
999
+ const SUMMARY_FIELDS: Fields<JSONObject> = [
1000
+ {
1001
+ field: { hosts: true },
1002
+ title: "Other Affected Resources",
1003
+ fieldType: FormFieldSchemaType.CustomComponent,
1004
+ getSummaryElement: (item: FormValues<JSONObject>): ReactElement => {
1005
+ return (
1006
+ <AffectedResourcesPicker
1007
+ readOnly={true}
1008
+ hosts={item["hosts"] as Array<Host>}
1009
+ kubernetesClusters={
1010
+ item["kubernetesClusters"] as Array<KubernetesCluster>
1011
+ }
1012
+ dockerHosts={item["dockerHosts"] as Array<DockerHost>}
1013
+ podmanHosts={item["podmanHosts"] as Array<PodmanHost>}
1014
+ services={item["services"] as Array<Service>}
1015
+ resourceTypes={[
1016
+ "Host",
1017
+ "KubernetesCluster",
1018
+ "DockerHost",
1019
+ "PodmanHost",
1020
+ "Service",
1021
+ ]}
1022
+ onChange={noop}
1023
+ />
1024
+ );
1025
+ },
1026
+ },
1027
+ ];
1028
+
1029
+ test("names every picked resource type, not only hosts, and prints no IDs", async () => {
1030
+ seed(Host, [{ _id: HOST_ID, name: "web-01" }]);
1031
+ seed(KubernetesCluster, [{ _id: CLUSTER_ID, name: "prod-eu" }]);
1032
+ seed(PodmanHost, [{ _id: PODMAN_ID, name: "podman-1" }]);
1033
+ seed(Service, [{ _id: SERVICE_ID, name: "checkout-api" }]);
1034
+
1035
+ render(
1036
+ <FormSummary<JSONObject>
1037
+ formValues={
1038
+ {
1039
+ hosts: [HOST_ID],
1040
+ kubernetesClusters: [CLUSTER_ID],
1041
+ dockerHosts: [],
1042
+ podmanHosts: [PODMAN_ID],
1043
+ services: [SERVICE_ID],
1044
+ } as FormValues<JSONObject>
1045
+ }
1046
+ formFields={SUMMARY_FIELDS}
1047
+ formSteps={undefined}
1048
+ />,
1049
+ );
1050
+
1051
+ expect(await screen.findByText("web-01")).toBeInTheDocument();
1052
+ expect(await screen.findByText("prod-eu")).toBeInTheDocument();
1053
+ expect(await screen.findByText("podman-1")).toBeInTheDocument();
1054
+ expect(await screen.findByText("checkout-api")).toBeInTheDocument();
1055
+ for (const rawId of [HOST_ID, CLUSTER_ID, PODMAN_ID, SERVICE_ID]) {
1056
+ expect(screen.queryByText(rawId, { exact: false })).toBeNull();
1057
+ }
1058
+ });
1059
+ });