@oneuptime/common 12.0.22 → 12.0.24

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 (345) hide show
  1. package/Models/DatabaseModels/DetectionRule.ts +55 -0
  2. package/Models/DatabaseModels/Index.ts +2 -0
  3. package/Models/DatabaseModels/NetworkDevice.ts +44 -0
  4. package/Models/DatabaseModels/NetworkDeviceAutoImportRule.ts +584 -0
  5. package/Models/DatabaseModels/NetworkDeviceDiscoveryScan.ts +105 -0
  6. package/Models/DatabaseModels/Project.ts +68 -0
  7. package/Models/DatabaseModels/User.ts +68 -0
  8. package/Server/API/TelemetryAPI.ts +29 -0
  9. package/Server/Infrastructure/Postgres/SchemaMigrations/1789100000000-AddNetworkDeviceAutoImportRule.ts +60 -0
  10. package/Server/Infrastructure/Postgres/SchemaMigrations/1789200000000-AddSysObjectIdPatternToAutoImportRule.ts +26 -0
  11. package/Server/Infrastructure/Postgres/SchemaMigrations/1789300000000-AddAutoApplyVendorHealthTemplate.ts +26 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.ts +28 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.ts +49 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.ts +33 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +12 -0
  16. package/Server/Middleware/GzipRequestBody.ts +311 -0
  17. package/Server/Middleware/MultipartFormData.ts +87 -3
  18. package/Server/Services/DetectionRuleService.ts +73 -6
  19. package/Server/Services/Index.ts +207 -0
  20. package/Server/Services/NetworkDeviceAutoImportRuleEngineService.ts +917 -0
  21. package/Server/Services/NetworkDeviceAutoImportRuleService.ts +181 -0
  22. package/Server/Services/NetworkDeviceDiscoveryScanService.ts +59 -0
  23. package/Server/Services/NetworkDeviceService.ts +148 -0
  24. package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +65 -0
  25. package/Server/Services/OnCallDutyPolicyFeedService.ts +1 -1
  26. package/Server/Services/ProjectService.ts +29 -19
  27. package/Server/Services/SecurityEventService.ts +90 -13
  28. package/Server/Services/StatusPageResourceService.ts +223 -0
  29. package/Server/Services/TelemetryAttributeService.ts +17 -0
  30. package/Server/Services/UserService.ts +4 -6
  31. package/Server/Types/Markdown.ts +161 -67
  32. package/Server/Utils/AI/Toolbox/OnCallTools.ts +12 -4
  33. package/Server/Utils/AnalyticsDatabase/Statement.ts +13 -1
  34. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +297 -6
  35. package/Server/Utils/Attribution.ts +4 -0
  36. package/Server/Utils/Marketing/MarketingEventUtil.ts +45 -7
  37. package/Server/Utils/Monitor/NetworkInventoryUtil.ts +39 -1
  38. package/Server/Utils/SecurityEvent/DetectionRuleEvaluator.ts +55 -3
  39. package/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.ts +21 -0
  40. package/Server/Utils/StartServer.ts +12 -32
  41. package/Server/Utils/Telemetry/LlmCostBudgetEvaluator.ts +101 -4
  42. package/Server/Utils/Telemetry/LlmMetricSpend.ts +130 -0
  43. package/Server/Views/Partials/AnalyticsConsent.ejs +159 -102
  44. package/Server/Views/Partials/Head.ejs +2 -1
  45. package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +776 -38
  46. package/Tests/App/Dashboard/CorrelateFilterBuilder.test.tsx +359 -0
  47. package/Tests/App/Dashboard/CorrelateGraph.test.tsx +865 -0
  48. package/Tests/App/Dashboard/CorrelationGraph.test.ts +346 -0
  49. package/Tests/App/Dashboard/CriteriaFilterConditionDropdown.test.tsx +256 -0
  50. package/Tests/App/Dashboard/CriteriaFilterDefaults.test.ts +738 -0
  51. package/Tests/App/Dashboard/CriteriaFilterMonitorTypeChange.test.ts +1122 -0
  52. package/Tests/App/Dashboard/DiscoveryScanWizardValidation.test.tsx +283 -0
  53. package/Tests/App/Dashboard/ExternalStatusPageCriteriaFilter.test.ts +407 -0
  54. package/Tests/App/Dashboard/MetricSeriesInvestigateMenu.test.tsx +5 -14
  55. package/Tests/App/Dashboard/MonitorCriteriaActionToggles.test.tsx +426 -0
  56. package/Tests/App/Dashboard/MonitorStepsCriteriaAlignmentWiring.test.tsx +352 -0
  57. package/Tests/App/Dashboard/OnCallLayerCardOverrides.test.tsx +278 -0
  58. package/Tests/App/Dashboard/OnCallLayerShiftPreviewOverrides.test.ts +237 -0
  59. package/Tests/App/Dashboard/OnCallReadinessSurfaces.test.tsx +2 -2
  60. package/Tests/App/Dashboard/OnCallSchedulePreviewOverrides.test.tsx +460 -0
  61. package/Tests/App/Dashboard/SecurityEventAttributeUtil.test.ts +171 -0
  62. package/Tests/App/Dashboard/SecurityEventCorrelation.test.ts +857 -0
  63. package/Tests/App/Dashboard/SecurityEventDetailObservables.test.tsx +194 -0
  64. package/Tests/App/Dashboard/SecurityEventsDetectionRulesPage.test.tsx +124 -0
  65. package/Tests/App/Dashboard/SecurityEventsMonitorStepForm.test.tsx +79 -19
  66. package/Tests/App/Dashboard/SecurityEventsTableColumns.test.tsx +382 -0
  67. package/Tests/App/StatusPage/BulkAddStatusPageMonitors.test.ts +216 -0
  68. package/Tests/App/StatusPage/BulkAddStatusPageMonitorsModal.test.tsx +590 -11
  69. package/Tests/App/StatusPage/StatusPageResourceExplorer.test.tsx +757 -0
  70. package/Tests/Models/DiscoveryScanNameColumn.test.ts +293 -0
  71. package/Tests/Server/API/SecurityEventAttributesAPI.test.ts +564 -0
  72. package/Tests/Server/Infrastructure/Postgres/SchemaMigrationsOrdering.test.ts +120 -4
  73. package/Tests/Server/Middleware/GzipRequestBody.test.ts +725 -0
  74. package/Tests/Server/Middleware/MultipartFormData.test.ts +298 -0
  75. package/Tests/Server/Services/AddDetectionRuleDistinctCountColumnsMigration.test.ts +311 -0
  76. package/Tests/Server/Services/AutoImportScanCredentialSelect.test.ts +205 -0
  77. package/Tests/Server/Services/DetectionRuleService.test.ts +134 -2
  78. package/Tests/Server/Services/DiscoveryScanClaimHookFreeSafety.test.ts +7 -1
  79. package/Tests/Server/Services/FeedRetentionConsistency.test.ts +131 -0
  80. package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +933 -0
  81. package/Tests/Server/Services/NetworkDeviceDiscoveryScanService.test.ts +194 -0
  82. package/Tests/Server/Services/NetworkDeviceMonitorStatusStamp.test.ts +800 -0
  83. package/Tests/Server/Services/OnCallScheduleAttachRosterRefresh.test.ts +244 -0
  84. package/Tests/Server/Services/SecurityEventService.test.ts +376 -0
  85. package/Tests/Server/Services/StatusPageResourceDuplicates.test.ts +703 -0
  86. package/Tests/Server/Services/TelemetryAttributeServiceSecurityEvent.test.ts +256 -0
  87. package/Tests/Server/Types/Workflow/Components/ComponentRegistryParity.test.ts +417 -0
  88. package/Tests/Server/Utils/AnalyticsDatabase/StatementGeneratorWhereOperators.test.ts +677 -0
  89. package/Tests/Server/Utils/Marketing/MarketingEventUtil.test.ts +43 -0
  90. package/Tests/Server/Utils/Monitor/Criteria/ProfileMonitorCriteria.test.ts +15 -11
  91. package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +97 -1
  92. package/Tests/Server/Utils/SecurityEvent/DetectionRuleEvaluator.test.ts +369 -8
  93. package/Tests/Server/Utils/SecurityEvent/SigmaClickhouseCompiler.test.ts +35 -0
  94. package/Tests/Server/Utils/Telemetry/LlmCostBudgetEvaluator.test.ts +368 -4
  95. package/Tests/Server/Utils/Telemetry/LlmMetricSpend.test.ts +245 -0
  96. package/Tests/Server/Views/AnalyticsConsentPartial.test.ts +48 -5
  97. package/Tests/Types/Code/YamlSyntax.test.ts +396 -0
  98. package/Tests/Types/Monitor/MetricAndProfileDefaultCriteria.test.ts +957 -0
  99. package/Tests/Types/Monitor/MonitorCriteriaActionValidation.test.ts +598 -0
  100. package/Tests/Types/Monitor/MonitorCriteriaInstance.test.ts +49 -0
  101. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationNotificationMode.test.ts +6 -6
  102. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.test.ts +4 -4
  103. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationUtil.test.ts +4 -3
  104. package/Tests/Types/Telemetry/LlmMetricConventions.test.ts +193 -0
  105. package/Tests/UI/Components/AiInvestigationSettingsCard.test.tsx +0 -12
  106. package/Tests/UI/Components/CardModelDetailEdit.test.tsx +1 -17
  107. package/Tests/UI/Components/CodeEditor.test.tsx +474 -10
  108. package/Tests/UI/Components/Detail/YamlField.test.tsx +105 -0
  109. package/Tests/UI/Components/EntityDropdownLabelsBulkAdd.test.tsx +319 -0
  110. package/Tests/UI/Components/Forms/FormFieldYaml.test.tsx +231 -0
  111. package/Tests/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.test.ts +1 -0
  112. package/Tests/UI/Components/Forms/ValidationYAML.test.ts +234 -0
  113. package/Tests/UI/Components/ModelTable/AttributeColumns.test.tsx +573 -0
  114. package/Tests/UI/Components/ModelTable/BaseModelTableAttributeColumns.test.tsx +824 -0
  115. package/Tests/UI/Components/ModelTable/ColumnCustomizationModalAddableColumns.test.tsx +707 -0
  116. package/Tests/UI/Components/SideOver.test.tsx +1 -1
  117. package/Tests/UI/Components/StatusPage/ResourceGridSelection.test.tsx +294 -0
  118. package/Tests/UI/Components/StatusPage/ResourceListSelection.test.tsx +436 -0
  119. package/Tests/UI/Components/TableBulkCsvExport.test.tsx +6 -2
  120. package/Tests/UI/Components/Toggle.test.tsx +25 -0
  121. package/Tests/UI/Components/YamlEditor.test.tsx +847 -0
  122. package/Tests/Utils/NetworkDevice/DeviceReachabilityUtil.test.ts +252 -0
  123. package/Tests/Utils/NetworkDiscovery/AutoImportRuleMatcher.test.ts +516 -0
  124. package/Tests/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.test.ts +272 -0
  125. package/Tests/Utils/NetworkDiscovery/DiscoveredHostUtil.test.ts +221 -0
  126. package/Tests/Utils/NetworkDiscovery/ScanNameUtil.test.ts +312 -0
  127. package/Tests/Utils/NetworkDiscovery/ScanTargetUtil.test.ts +164 -0
  128. package/Tests/Utils/StatusPage/ResourceExplorer.test.ts +468 -0
  129. package/Tests/Utils/Telemetry/LlmMetricQuery.test.ts +472 -0
  130. package/Types/Code/YamlSyntax.ts +396 -0
  131. package/Types/Exception/ExceptionCode.ts +1 -0
  132. package/Types/Exception/PayloadTooLargeException.ts +8 -0
  133. package/Types/Marketing/Attribution.ts +10 -0
  134. package/Types/Marketing/MarketingEvent.ts +4 -0
  135. package/Types/Monitor/MonitorCriteriaInstance.ts +116 -56
  136. package/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.ts +2 -1
  137. package/Types/NetworkAutomation/RuleRunResult.ts +84 -0
  138. package/Types/Permission.ts +47 -0
  139. package/Types/SecurityEvent/DetectionFindingConstants.ts +11 -0
  140. package/Types/Telemetry/LlmMetricConventions.ts +124 -0
  141. package/UI/Components/AutocompleteTextInput/AutocompleteTextInput.tsx +3 -0
  142. package/UI/Components/BulkUpdate/BulkUpdateForm.tsx +10 -1
  143. package/UI/Components/CodeEditor/CodeEditor.tsx +241 -79
  144. package/UI/Components/CodeEditor/YamlEditor.tsx +369 -0
  145. package/UI/Components/Detail/Detail.tsx +10 -0
  146. package/UI/Components/EntityDropdown/EntityDropdown.tsx +33 -0
  147. package/UI/Components/Forms/Fields/FormField.tsx +31 -0
  148. package/UI/Components/Forms/Types/Field.ts +8 -0
  149. package/UI/Components/Forms/Types/FormFieldSchemaType.ts +1 -0
  150. package/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.ts +2 -0
  151. package/UI/Components/Forms/Validation.ts +46 -0
  152. package/UI/Components/Input/Input.tsx +3 -0
  153. package/UI/Components/ModelTable/AttributeColumns.tsx +321 -0
  154. package/UI/Components/ModelTable/BaseModelTable.tsx +263 -14
  155. package/UI/Components/ModelTable/Column.ts +8 -0
  156. package/UI/Components/ModelTable/ColumnCustomizationModal.tsx +310 -0
  157. package/UI/Components/ModelTable/ColumnPreference.ts +13 -1
  158. package/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.tsx +2 -2
  159. package/UI/Components/SideOver/SideOver.tsx +5 -5
  160. package/UI/Components/StatusPage/ResourceGrid.tsx +82 -2
  161. package/UI/Components/StatusPage/ResourceList.tsx +197 -38
  162. package/UI/Components/Toggle/Toggle.tsx +9 -1
  163. package/UI/Components/Types/FieldType.ts +1 -0
  164. package/Utils/NetworkDevice/DeviceReachabilityUtil.ts +73 -1
  165. package/Utils/NetworkDiscovery/AutoImportRuleMatcher.ts +257 -0
  166. package/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.ts +183 -0
  167. package/Utils/NetworkDiscovery/DiscoveredHostUtil.ts +95 -0
  168. package/Utils/NetworkDiscovery/DiscoveryImportEligibility.ts +66 -0
  169. package/Utils/NetworkDiscovery/ScanNameUtil.ts +181 -0
  170. package/Utils/NetworkDiscovery/ScanTargetUtil.ts +82 -0
  171. package/Utils/StatusPage/ResourceExplorer.ts +258 -0
  172. package/Utils/Telemetry/LlmMetricQuery.ts +204 -0
  173. package/build/dist/Models/DatabaseModels/DetectionRule.js +57 -0
  174. package/build/dist/Models/DatabaseModels/DetectionRule.js.map +1 -1
  175. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  176. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  177. package/build/dist/Models/DatabaseModels/NetworkDevice.js +45 -0
  178. package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
  179. package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js +609 -0
  180. package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js.map +1 -0
  181. package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js +97 -0
  182. package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js.map +1 -1
  183. package/build/dist/Models/DatabaseModels/Project.js +76 -0
  184. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  185. package/build/dist/Models/DatabaseModels/User.js +76 -0
  186. package/build/dist/Models/DatabaseModels/User.js.map +1 -1
  187. package/build/dist/Server/API/TelemetryAPI.js +17 -0
  188. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  189. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789100000000-AddNetworkDeviceAutoImportRule.js +35 -0
  190. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789100000000-AddNetworkDeviceAutoImportRule.js.map +1 -0
  191. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789200000000-AddSysObjectIdPatternToAutoImportRule.js +19 -0
  192. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789200000000-AddSysObjectIdPatternToAutoImportRule.js.map +1 -0
  193. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789300000000-AddAutoApplyVendorHealthTemplate.js +19 -0
  194. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789300000000-AddAutoApplyVendorHealthTemplate.js.map +1 -0
  195. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.js +21 -0
  196. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.js.map +1 -0
  197. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.js +42 -0
  198. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.js.map +1 -0
  199. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.js +22 -0
  200. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.js.map +1 -0
  201. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +12 -0
  202. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  203. package/build/dist/Server/Middleware/GzipRequestBody.js +224 -0
  204. package/build/dist/Server/Middleware/GzipRequestBody.js.map +1 -0
  205. package/build/dist/Server/Middleware/MultipartFormData.js +71 -2
  206. package/build/dist/Server/Middleware/MultipartFormData.js.map +1 -1
  207. package/build/dist/Server/Services/DetectionRuleService.js +49 -4
  208. package/build/dist/Server/Services/DetectionRuleService.js.map +1 -1
  209. package/build/dist/Server/Services/Index.js +206 -0
  210. package/build/dist/Server/Services/Index.js.map +1 -1
  211. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js +711 -0
  212. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js.map +1 -0
  213. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleService.js +155 -0
  214. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleService.js.map +1 -0
  215. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js +50 -0
  216. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js.map +1 -1
  217. package/build/dist/Server/Services/NetworkDeviceService.js +124 -0
  218. package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
  219. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +68 -13
  220. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
  221. package/build/dist/Server/Services/OnCallDutyPolicyFeedService.js +1 -1
  222. package/build/dist/Server/Services/OnCallDutyPolicyFeedService.js.map +1 -1
  223. package/build/dist/Server/Services/ProjectService.js +25 -45
  224. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  225. package/build/dist/Server/Services/SecurityEventService.js +57 -5
  226. package/build/dist/Server/Services/SecurityEventService.js.map +1 -1
  227. package/build/dist/Server/Services/StatusPageResourceService.js +140 -0
  228. package/build/dist/Server/Services/StatusPageResourceService.js.map +1 -1
  229. package/build/dist/Server/Services/TelemetryAttributeService.js +17 -0
  230. package/build/dist/Server/Services/TelemetryAttributeService.js.map +1 -1
  231. package/build/dist/Server/Services/UserService.js +2 -12
  232. package/build/dist/Server/Services/UserService.js.map +1 -1
  233. package/build/dist/Server/Types/Markdown.js +147 -61
  234. package/build/dist/Server/Types/Markdown.js.map +1 -1
  235. package/build/dist/Server/Utils/AI/Toolbox/OnCallTools.js +12 -4
  236. package/build/dist/Server/Utils/AI/Toolbox/OnCallTools.js.map +1 -1
  237. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js +12 -1
  238. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js.map +1 -1
  239. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +246 -9
  240. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  241. package/build/dist/Server/Utils/Attribution.js.map +1 -1
  242. package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js +27 -7
  243. package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js.map +1 -1
  244. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js +26 -0
  245. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js.map +1 -1
  246. package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js +42 -7
  247. package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js.map +1 -1
  248. package/build/dist/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.js +18 -0
  249. package/build/dist/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.js.map +1 -1
  250. package/build/dist/Server/Utils/StartServer.js +12 -21
  251. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  252. package/build/dist/Server/Utils/Telemetry/LlmCostBudgetEvaluator.js +76 -5
  253. package/build/dist/Server/Utils/Telemetry/LlmCostBudgetEvaluator.js.map +1 -1
  254. package/build/dist/Server/Utils/Telemetry/LlmMetricSpend.js +102 -0
  255. package/build/dist/Server/Utils/Telemetry/LlmMetricSpend.js.map +1 -0
  256. package/build/dist/Types/Code/YamlSyntax.js +212 -0
  257. package/build/dist/Types/Code/YamlSyntax.js.map +1 -0
  258. package/build/dist/Types/Exception/ExceptionCode.js +1 -0
  259. package/build/dist/Types/Exception/ExceptionCode.js.map +1 -1
  260. package/build/dist/Types/Exception/PayloadTooLargeException.js +8 -0
  261. package/build/dist/Types/Exception/PayloadTooLargeException.js.map +1 -0
  262. package/build/dist/Types/Marketing/Attribution.js +10 -0
  263. package/build/dist/Types/Marketing/Attribution.js.map +1 -1
  264. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js +112 -51
  265. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js.map +1 -1
  266. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.js +2 -1
  267. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.js.map +1 -1
  268. package/build/dist/Types/NetworkAutomation/RuleRunResult.js +24 -0
  269. package/build/dist/Types/NetworkAutomation/RuleRunResult.js.map +1 -1
  270. package/build/dist/Types/Permission.js +41 -0
  271. package/build/dist/Types/Permission.js.map +1 -1
  272. package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js +9 -0
  273. package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js.map +1 -1
  274. package/build/dist/Types/Telemetry/LlmMetricConventions.js +98 -0
  275. package/build/dist/Types/Telemetry/LlmMetricConventions.js.map +1 -0
  276. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js +1 -1
  277. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js.map +1 -1
  278. package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js +5 -2
  279. package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js.map +1 -1
  280. package/build/dist/UI/Components/CodeEditor/CodeEditor.js +206 -76
  281. package/build/dist/UI/Components/CodeEditor/CodeEditor.js.map +1 -1
  282. package/build/dist/UI/Components/CodeEditor/YamlEditor.js +201 -0
  283. package/build/dist/UI/Components/CodeEditor/YamlEditor.js.map +1 -0
  284. package/build/dist/UI/Components/Detail/Detail.js +9 -0
  285. package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
  286. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js +16 -1
  287. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js.map +1 -1
  288. package/build/dist/UI/Components/Forms/Fields/FormField.js +14 -1
  289. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  290. package/build/dist/UI/Components/Forms/Types/Field.js.map +1 -1
  291. package/build/dist/UI/Components/Forms/Types/FormFieldSchemaType.js +1 -0
  292. package/build/dist/UI/Components/Forms/Types/FormFieldSchemaType.js.map +1 -1
  293. package/build/dist/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.js +2 -0
  294. package/build/dist/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.js.map +1 -1
  295. package/build/dist/UI/Components/Forms/Validation.js +27 -0
  296. package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
  297. package/build/dist/UI/Components/Input/Input.js +1 -1
  298. package/build/dist/UI/Components/Input/Input.js.map +1 -1
  299. package/build/dist/UI/Components/ModelTable/AttributeColumns.js +159 -0
  300. package/build/dist/UI/Components/ModelTable/AttributeColumns.js.map +1 -0
  301. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +179 -17
  302. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  303. package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js +133 -2
  304. package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js.map +1 -1
  305. package/build/dist/UI/Components/ModelTable/ColumnPreference.js +7 -1
  306. package/build/dist/UI/Components/ModelTable/ColumnPreference.js.map +1 -1
  307. package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.js +2 -2
  308. package/build/dist/UI/Components/SideOver/SideOver.js +5 -5
  309. package/build/dist/UI/Components/StatusPage/ResourceGrid.js +28 -2
  310. package/build/dist/UI/Components/StatusPage/ResourceGrid.js.map +1 -1
  311. package/build/dist/UI/Components/StatusPage/ResourceList.js +87 -20
  312. package/build/dist/UI/Components/StatusPage/ResourceList.js.map +1 -1
  313. package/build/dist/UI/Components/Toggle/Toggle.js +9 -1
  314. package/build/dist/UI/Components/Toggle/Toggle.js.map +1 -1
  315. package/build/dist/UI/Components/Types/FieldType.js +1 -0
  316. package/build/dist/UI/Components/Types/FieldType.js.map +1 -1
  317. package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js +37 -1
  318. package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js.map +1 -1
  319. package/build/dist/Utils/NetworkDiscovery/AutoImportRuleMatcher.js +149 -0
  320. package/build/dist/Utils/NetworkDiscovery/AutoImportRuleMatcher.js.map +1 -0
  321. package/build/dist/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.js +121 -0
  322. package/build/dist/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.js.map +1 -0
  323. package/build/dist/Utils/NetworkDiscovery/DiscoveredHostUtil.js +75 -0
  324. package/build/dist/Utils/NetworkDiscovery/DiscoveredHostUtil.js.map +1 -0
  325. package/build/dist/Utils/NetworkDiscovery/DiscoveryImportEligibility.js +59 -0
  326. package/build/dist/Utils/NetworkDiscovery/DiscoveryImportEligibility.js.map +1 -0
  327. package/build/dist/Utils/NetworkDiscovery/ScanNameUtil.js +120 -0
  328. package/build/dist/Utils/NetworkDiscovery/ScanNameUtil.js.map +1 -0
  329. package/build/dist/Utils/NetworkDiscovery/ScanTargetUtil.js +60 -0
  330. package/build/dist/Utils/NetworkDiscovery/ScanTargetUtil.js.map +1 -1
  331. package/build/dist/Utils/StatusPage/ResourceExplorer.js +171 -0
  332. package/build/dist/Utils/StatusPage/ResourceExplorer.js.map +1 -1
  333. package/build/dist/Utils/Telemetry/LlmMetricQuery.js +119 -0
  334. package/build/dist/Utils/Telemetry/LlmMetricQuery.js.map +1 -0
  335. package/jest.config.json +2 -0
  336. package/package.json +1 -1
  337. package/tsconfig.json +2 -1
  338. package/Tests/UI/Components/Toast.test.tsx +0 -197
  339. package/Tests/UI/Components/ToastStacking.test.tsx +0 -198
  340. package/UI/Components/Toast/Toast.tsx +0 -189
  341. package/UI/Components/Toast/ToastInit.tsx +0 -110
  342. package/build/dist/UI/Components/Toast/Toast.js +0 -123
  343. package/build/dist/UI/Components/Toast/Toast.js.map +0 -1
  344. package/build/dist/UI/Components/Toast/ToastInit.js +0 -53
  345. package/build/dist/UI/Components/Toast/ToastInit.js.map +0 -1
@@ -5,7 +5,15 @@ import { beforeEach, describe, expect, jest, test } from "@jest/globals";
5
5
 
6
6
  const MONITOR_ONE_ID: string = "0198c8ec-2a1d-7f0c-9e75-384194161010";
7
7
  const MONITOR_TWO_ID: string = "0198c8ec-2a1d-7f0c-9e75-384194161011";
8
-
8
+ const LABEL_ONE_ID: string = "0198c8ec-2a1d-7f0c-9e75-384194161020";
9
+ const LABEL_TWO_ID: string = "0198c8ec-2a1d-7f0c-9e75-384194161021";
10
+
11
+ /*
12
+ * The picker, reduced to its two ways of filling the selection. "Bulk-add by
13
+ * label" is the one that matters here: the real control expands a label into
14
+ * the monitors carrying it and then discards the label, so onLabelsBulkAdded
15
+ * is the only thing that tells the modal a selection came from a label at all.
16
+ */
9
17
  jest.mock("Common/UI/Components/EntityDropdown/EntityDropdown", () => {
10
18
  return {
11
19
  __esModule: true,
@@ -13,6 +21,7 @@ jest.mock("Common/UI/Components/EntityDropdown/EntityDropdown", () => {
13
21
  isMultiSelect?: boolean;
14
22
  error?: string;
15
23
  onChange?: (value: Array<string>) => void;
24
+ onLabelsBulkAdded?: (labels: Array<{ id: string; name: string }>) => void;
16
25
  }) => {
17
26
  return (
18
27
  <div>
@@ -24,6 +33,28 @@ jest.mock("Common/UI/Components/EntityDropdown/EntityDropdown", () => {
24
33
  >
25
34
  {props.isMultiSelect ? "Select two monitors" : "Select one monitor"}
26
35
  </button>
36
+ <button
37
+ type="button"
38
+ onClick={() => {
39
+ props.onChange?.([MONITOR_TWO_ID, MONITOR_ONE_ID]);
40
+ props.onLabelsBulkAdded?.([
41
+ { id: LABEL_ONE_ID, name: "WB Digital" },
42
+ ]);
43
+ }}
44
+ >
45
+ Bulk-add by label
46
+ </button>
47
+ <button
48
+ type="button"
49
+ onClick={() => {
50
+ props.onChange?.([MONITOR_TWO_ID, MONITOR_ONE_ID]);
51
+ props.onLabelsBulkAdded?.([
52
+ { id: LABEL_TWO_ID, name: "WB Digital Service" },
53
+ ]);
54
+ }}
55
+ >
56
+ Bulk-add by a second label
57
+ </button>
27
58
  {props.error ? <p>{props.error}</p> : null}
28
59
  </div>
29
60
  );
@@ -63,6 +94,7 @@ jest.mock("Common/UI/Utils/ModelAPI/ModelAPI", () => {
63
94
  __esModule: true,
64
95
  default: {
65
96
  getList: jest.fn(),
97
+ create: jest.fn(),
66
98
  },
67
99
  };
68
100
  });
@@ -82,7 +114,10 @@ import bulkAddStatusPageMonitors, {
82
114
  BulkAddStatusPageMonitorsResult,
83
115
  } from "../../../../App/FeatureSet/Dashboard/src/Components/StatusPage/BulkAddStatusPageMonitors";
84
116
  import BulkAddStatusPageMonitorsModal from "../../../../App/FeatureSet/Dashboard/src/Components/StatusPage/BulkAddStatusPageMonitorsModal";
117
+ import Label from "../../../Models/DatabaseModels/Label";
85
118
  import Monitor from "../../../Models/DatabaseModels/Monitor";
119
+ import StatusPageMonitorRule from "../../../Models/DatabaseModels/StatusPageMonitorRule";
120
+ import StatusPageResource from "../../../Models/DatabaseModels/StatusPageResource";
86
121
  import ObjectID from "../../../Types/ObjectID";
87
122
  import ModelAPI from "../../../UI/Utils/ModelAPI/ModelAPI";
88
123
 
@@ -102,6 +137,9 @@ const mockBulkAdd: jest.MockedFunction<typeof bulkAddStatusPageMonitors> =
102
137
  const mockGetList: jest.MockedFunction<any> =
103
138
  ModelAPI.getList as unknown as jest.MockedFunction<any>;
104
139
 
140
+ const mockCreate: jest.MockedFunction<any> =
141
+ ModelAPI.create as unknown as jest.MockedFunction<any>;
142
+
105
143
  type MakeMonitorFunction = (
106
144
  id: string,
107
145
  name: string,
@@ -120,6 +158,43 @@ const makeMonitor: MakeMonitorFunction = (
120
158
  return monitor;
121
159
  };
122
160
 
161
+ type MakeResourceFunction = (monitorId: string) => StatusPageResource;
162
+
163
+ const makeResource: MakeResourceFunction = (
164
+ monitorId: string,
165
+ ): StatusPageResource => {
166
+ const resource: StatusPageResource = new StatusPageResource();
167
+ resource._id = `resource-for-${monitorId}`;
168
+ resource.monitorId = new ObjectID(monitorId);
169
+ return resource;
170
+ };
171
+
172
+ /*
173
+ * The modal reads the monitors it was handed and, separately, the resources
174
+ * the status page already has - so the list mock answers by model.
175
+ */
176
+ type MockListsFunction = (
177
+ existingResources?: Array<StatusPageResource>,
178
+ ) => void;
179
+
180
+ const mockLists: MockListsFunction = (
181
+ existingResources: Array<StatusPageResource> = [],
182
+ ): void => {
183
+ mockGetList.mockImplementation(async (data: any): Promise<any> => {
184
+ if (data.modelType === StatusPageResource) {
185
+ return { data: existingResources, count: existingResources.length };
186
+ }
187
+
188
+ return {
189
+ data: [
190
+ makeMonitor(MONITOR_ONE_ID, "Checkout API", "Takes payments"),
191
+ makeMonitor(MONITOR_TWO_ID, "Billing Worker", "Sends invoices"),
192
+ ],
193
+ count: 2,
194
+ };
195
+ });
196
+ };
197
+
123
198
  type RenderModalFunction = (
124
199
  overrides?: Partial<
125
200
  React.ComponentProps<typeof BulkAddStatusPageMonitorsModal>
@@ -158,27 +233,38 @@ const goToLastStep: GoToLastStepFunction = async (
158
233
  });
159
234
  };
160
235
 
236
+ const callOrder: Array<string> = [];
237
+
238
+ /** The StatusPageMonitorRule the modal asked ModelAPI to create, if any. */
239
+ type CreatedRuleFunction = () => StatusPageMonitorRule | null;
240
+
241
+ const createdRule: CreatedRuleFunction = (): StatusPageMonitorRule | null => {
242
+ const call: any = mockCreate.mock.calls[0];
243
+
244
+ return call ? (call[0].model as StatusPageMonitorRule) : null;
245
+ };
246
+
161
247
  describe("BulkAddStatusPageMonitorsModal", () => {
162
248
  beforeEach(() => {
163
249
  jest.clearAllMocks();
164
250
 
165
- mockGetList.mockImplementation(async (): Promise<any> => {
166
- return {
167
- data: [
168
- makeMonitor(MONITOR_ONE_ID, "Checkout API", "Takes payments"),
169
- makeMonitor(MONITOR_TWO_ID, "Billing Worker", "Sends invoices"),
170
- ],
171
- count: 2,
172
- };
173
- });
251
+ mockLists();
252
+
253
+ callOrder.length = 0;
174
254
 
175
255
  mockBulkAdd.mockImplementation(
176
256
  async (
177
257
  options: BulkAddStatusPageMonitorsOptions,
178
258
  ): Promise<BulkAddStatusPageMonitorsResult> => {
179
- return { succeeded: options.monitors, failed: [] };
259
+ callOrder.push("bulkAdd");
260
+ return { succeeded: options.monitors, failed: [], skipped: [] };
180
261
  },
181
262
  );
263
+
264
+ mockCreate.mockImplementation(async (request: any): Promise<any> => {
265
+ callOrder.push("create");
266
+ return request?.model;
267
+ });
182
268
  });
183
269
 
184
270
  test("asks for monitors and the resource options, but never a display name", () => {
@@ -279,6 +365,222 @@ describe("BulkAddStatusPageMonitorsModal", () => {
279
365
  expect(options.columnAxisValue).toBe("US East");
280
366
  });
281
367
 
368
+ /*
369
+ * Issue #3420: picking monitors by label re-selects every monitor carrying
370
+ * that label, the ones already on the page included. The modal has to tell
371
+ * the add which monitors are already there, or each of them gets a second
372
+ * resource and the public page lists it twice.
373
+ */
374
+ test("hands the status page's existing monitors to the bulk add", async () => {
375
+ mockLists([makeResource(MONITOR_ONE_ID)]);
376
+
377
+ const view: ReturnType<typeof render> = renderModal();
378
+
379
+ fireEvent.click(view.getByRole("button", { name: "Select two monitors" }));
380
+ await goToLastStep(view);
381
+ fireEvent.click(view.getByTestId("modal-footer-submit-button"));
382
+
383
+ await waitFor(() => {
384
+ expect(mockBulkAdd).toHaveBeenCalledTimes(1);
385
+ });
386
+
387
+ const options: BulkAddStatusPageMonitorsOptions =
388
+ mockBulkAdd.mock.calls[0]![0];
389
+ expect(options.existingMonitorIds).toEqual([MONITOR_ONE_ID]);
390
+ });
391
+
392
+ test("reads the whole status page, not only the group being added to", async () => {
393
+ mockLists([makeResource(MONITOR_ONE_ID)]);
394
+
395
+ const view: ReturnType<typeof render> = renderModal();
396
+
397
+ fireEvent.click(view.getByRole("button", { name: "Select two monitors" }));
398
+ await goToLastStep(view);
399
+ fireEvent.click(view.getByTestId("modal-footer-submit-button"));
400
+
401
+ await waitFor(() => {
402
+ expect(mockBulkAdd).toHaveBeenCalledTimes(1);
403
+ });
404
+
405
+ const resourceListCall: any = mockGetList.mock.calls.find((call: any) => {
406
+ return call[0].modelType === StatusPageResource;
407
+ });
408
+
409
+ expect(resourceListCall).toBeDefined();
410
+ expect(resourceListCall[0].query).toEqual({
411
+ statusPageId: STATUS_PAGE_ID,
412
+ projectId: PROJECT_ID,
413
+ });
414
+ expect(resourceListCall[0].query.statusPageGroupId).toBeUndefined();
415
+ expect(resourceListCall[0].select).toMatchObject({ monitorId: true });
416
+ });
417
+
418
+ test("passes no existing monitors when the status page has none", async () => {
419
+ const view: ReturnType<typeof render> = renderModal();
420
+
421
+ fireEvent.click(view.getByRole("button", { name: "Select two monitors" }));
422
+ await goToLastStep(view);
423
+ fireEvent.click(view.getByTestId("modal-footer-submit-button"));
424
+
425
+ await waitFor(() => {
426
+ expect(mockBulkAdd).toHaveBeenCalledTimes(1);
427
+ });
428
+
429
+ expect(mockBulkAdd.mock.calls[0]![0].existingMonitorIds).toEqual([]);
430
+ });
431
+
432
+ test("ignores resources that point at a monitor group rather than a monitor", async () => {
433
+ const monitorGroupResource: StatusPageResource = new StatusPageResource();
434
+ monitorGroupResource._id = "resource-for-a-monitor-group";
435
+
436
+ mockLists([monitorGroupResource, makeResource(MONITOR_TWO_ID)]);
437
+
438
+ const view: ReturnType<typeof render> = renderModal();
439
+
440
+ fireEvent.click(view.getByRole("button", { name: "Select two monitors" }));
441
+ await goToLastStep(view);
442
+ fireEvent.click(view.getByTestId("modal-footer-submit-button"));
443
+
444
+ await waitFor(() => {
445
+ expect(mockBulkAdd).toHaveBeenCalledTimes(1);
446
+ });
447
+
448
+ expect(mockBulkAdd.mock.calls[0]![0].existingMonitorIds).toEqual([
449
+ MONITOR_TWO_ID,
450
+ ]);
451
+ });
452
+
453
+ test("lists the monitors that were already on the page instead of failing them", async () => {
454
+ mockBulkAdd.mockImplementationOnce(
455
+ async (
456
+ options: BulkAddStatusPageMonitorsOptions,
457
+ ): Promise<BulkAddStatusPageMonitorsResult> => {
458
+ return {
459
+ succeeded: [options.monitors[0]!],
460
+ failed: [],
461
+ skipped: [options.monitors[1]!],
462
+ };
463
+ },
464
+ );
465
+
466
+ const view: ReturnType<typeof render> = renderModal();
467
+
468
+ fireEvent.click(view.getByRole("button", { name: "Select two monitors" }));
469
+ await goToLastStep(view);
470
+ fireEvent.click(view.getByTestId("modal-footer-submit-button"));
471
+
472
+ await waitFor(() => {
473
+ expect(
474
+ view.getByText(
475
+ "Added 1 of 2 selected monitors. 1 monitor was already on this status page and was skipped.",
476
+ ),
477
+ ).toBeVisible();
478
+ });
479
+
480
+ // Skipped monitors are not an error, so the summary is the "Monitors Added" one.
481
+ expect(view.getByText("Monitors Added")).toBeVisible();
482
+ expect(view.getByText("Added (1)")).toBeVisible();
483
+ expect(view.getByText("Already Added (1)")).toBeVisible();
484
+ expect(view.queryByText(/^Failed/)).not.toBeInTheDocument();
485
+ });
486
+
487
+ test("says nothing was added when every selected monitor is already on the page", async () => {
488
+ mockBulkAdd.mockImplementationOnce(
489
+ async (
490
+ options: BulkAddStatusPageMonitorsOptions,
491
+ ): Promise<BulkAddStatusPageMonitorsResult> => {
492
+ return {
493
+ succeeded: [],
494
+ failed: [],
495
+ skipped: options.monitors,
496
+ };
497
+ },
498
+ );
499
+
500
+ const view: ReturnType<typeof render> = renderModal();
501
+
502
+ fireEvent.click(view.getByRole("button", { name: "Select two monitors" }));
503
+ await goToLastStep(view);
504
+ fireEvent.click(view.getByTestId("modal-footer-submit-button"));
505
+
506
+ await waitFor(() => {
507
+ expect(
508
+ view.getByText(
509
+ "Added 0 of 2 selected monitors. 2 monitors were already on this status page and were skipped.",
510
+ ),
511
+ ).toBeVisible();
512
+ });
513
+
514
+ expect(view.queryByText(/^Added \(/)).not.toBeInTheDocument();
515
+ expect(view.getByText("Already Added (2)")).toBeVisible();
516
+ expect(view.getByText("Billing Worker")).toBeVisible();
517
+ expect(view.getByText("Checkout API")).toBeVisible();
518
+ });
519
+
520
+ test("counts skipped, added and failed monitors in one summary", async () => {
521
+ mockBulkAdd.mockImplementationOnce(
522
+ async (
523
+ options: BulkAddStatusPageMonitorsOptions,
524
+ ): Promise<BulkAddStatusPageMonitorsResult> => {
525
+ return {
526
+ succeeded: [options.monitors[0]!],
527
+ failed: [
528
+ {
529
+ monitor: options.monitors[1]!,
530
+ error: new Error("Permission denied"),
531
+ },
532
+ ],
533
+ skipped: [makeMonitor(MONITOR_ONE_ID, "Legacy API", "Already there")],
534
+ };
535
+ },
536
+ );
537
+
538
+ const view: ReturnType<typeof render> = renderModal();
539
+
540
+ fireEvent.click(view.getByRole("button", { name: "Select two monitors" }));
541
+ await goToLastStep(view);
542
+ fireEvent.click(view.getByTestId("modal-footer-submit-button"));
543
+
544
+ await waitFor(() => {
545
+ expect(
546
+ view.getByText(
547
+ "Added 1 of 3 selected monitors. 1 monitor was already on this status page and was skipped.",
548
+ ),
549
+ ).toBeVisible();
550
+ });
551
+
552
+ expect(view.getByText("Monitor Add Results")).toBeVisible();
553
+ expect(view.getByText("Added (1)")).toBeVisible();
554
+ expect(view.getByText("Already Added (1)")).toBeVisible();
555
+ expect(view.getByText("Failed (1)")).toBeVisible();
556
+ });
557
+
558
+ test("surfaces a failure to read the status page's existing resources", async () => {
559
+ mockGetList.mockImplementation(async (data: any): Promise<any> => {
560
+ if (data.modelType === StatusPageResource) {
561
+ throw new Error("Cannot read status page resources");
562
+ }
563
+
564
+ return {
565
+ data: [makeMonitor(MONITOR_ONE_ID, "Checkout API", "Takes payments")],
566
+ count: 1,
567
+ };
568
+ });
569
+
570
+ const view: ReturnType<typeof render> = renderModal();
571
+
572
+ fireEvent.click(view.getByRole("button", { name: "Select two monitors" }));
573
+ await goToLastStep(view);
574
+ fireEvent.click(view.getByTestId("modal-footer-submit-button"));
575
+
576
+ await waitFor(() => {
577
+ expect(view.getByText("Cannot read status page resources")).toBeVisible();
578
+ });
579
+
580
+ // Nothing is written when we cannot tell what is already there.
581
+ expect(mockBulkAdd).not.toHaveBeenCalled();
582
+ });
583
+
282
584
  test("shows a non-retryable completion summary for partial failures", async () => {
283
585
  mockBulkAdd.mockImplementationOnce(
284
586
  async (
@@ -292,6 +594,7 @@ describe("BulkAddStatusPageMonitorsModal", () => {
292
594
  error: new Error("Permission denied"),
293
595
  },
294
596
  ],
597
+ skipped: [],
295
598
  };
296
599
  },
297
600
  );
@@ -318,3 +621,279 @@ describe("BulkAddStatusPageMonitorsModal", () => {
318
621
  expect(mockBulkAdd).toHaveBeenCalledTimes(1);
319
622
  });
320
623
  });
624
+
625
+ /*
626
+ * #3418. Filling a group from a label used to be a one-time expansion: the
627
+ * label became a list of monitors, each became a resource, and the label was
628
+ * thrown away - so a monitor given that label the next day landed on no page,
629
+ * and with ten status pages that meant re-populating groups by hand daily.
630
+ *
631
+ * A StatusPageMonitorRule is the durable form of the same intent; the server
632
+ * re-runs it on every monitor create and relabel. These tests drive the real
633
+ * modal and assert on what it actually posts.
634
+ */
635
+ describe("BulkAddStatusPageMonitorsModal - keeping a label-filled group filled", () => {
636
+ beforeEach(() => {
637
+ jest.clearAllMocks();
638
+ callOrder.length = 0;
639
+
640
+ mockLists();
641
+
642
+ mockBulkAdd.mockImplementation(
643
+ async (
644
+ options: BulkAddStatusPageMonitorsOptions,
645
+ ): Promise<BulkAddStatusPageMonitorsResult> => {
646
+ callOrder.push("bulkAdd");
647
+ return { succeeded: options.monitors, failed: [], skipped: [] };
648
+ },
649
+ );
650
+
651
+ mockCreate.mockImplementation(async (request: any): Promise<any> => {
652
+ callOrder.push("create");
653
+ return request?.model;
654
+ });
655
+ });
656
+
657
+ /*
658
+ * The question only means something once a label has been used, so it is not
659
+ * asked of somebody picking monitors one at a time.
660
+ */
661
+ test("does not ask about syncing until a label has been used", () => {
662
+ const view: ReturnType<typeof render> = renderModal();
663
+
664
+ expect(
665
+ view.queryByText("Keep This Group In Sync With These Labels"),
666
+ ).not.toBeInTheDocument();
667
+
668
+ fireEvent.click(view.getByRole("button", { name: "Bulk-add by label" }));
669
+
670
+ expect(
671
+ view.getByText("Keep This Group In Sync With These Labels"),
672
+ ).toBeVisible();
673
+ });
674
+
675
+ test("writes no rule for monitors picked one at a time", async () => {
676
+ const view: ReturnType<typeof render> = renderModal();
677
+
678
+ fireEvent.click(view.getByRole("button", { name: "Select two monitors" }));
679
+ await goToLastStep(view);
680
+ fireEvent.click(view.getByTestId("modal-footer-submit-button"));
681
+
682
+ await waitFor(() => {
683
+ expect(mockBulkAdd).toHaveBeenCalledTimes(1);
684
+ });
685
+
686
+ expect(mockCreate).not.toHaveBeenCalled();
687
+ });
688
+
689
+ /*
690
+ * The headline. Before the fix nothing at all was written here, and the
691
+ * label was gone the moment the dialog closed.
692
+ */
693
+ test("writes a rule for the label the group was filled from", async () => {
694
+ const view: ReturnType<typeof render> = renderModal();
695
+
696
+ fireEvent.click(view.getByRole("button", { name: "Bulk-add by label" }));
697
+ await goToLastStep(view);
698
+ fireEvent.click(view.getByTestId("modal-footer-submit-button"));
699
+
700
+ await waitFor(() => {
701
+ expect(mockCreate).toHaveBeenCalledTimes(1);
702
+ });
703
+
704
+ const rule: StatusPageMonitorRule | null = createdRule();
705
+
706
+ expect(rule?.statusPageId?.toString()).toBe(STATUS_PAGE_ID.toString());
707
+ expect(rule?.statusPageGroupId?.toString()).toBe(GROUP_ID.toString());
708
+ expect(rule?.projectId?.toString()).toBe(PROJECT_ID.toString());
709
+ expect(rule?.isEnabled).toBe(true);
710
+ expect(
711
+ (rule?.monitorLabels || []).map((label: Label) => {
712
+ return label.id?.toString();
713
+ }),
714
+ ).toEqual([LABEL_ONE_ID]);
715
+ });
716
+
717
+ /*
718
+ * name is a required, non-nullable column. A rule built without one is
719
+ * rejected by the server, and the only symptom is the group quietly not
720
+ * being kept in sync - the original bug, wearing a hat.
721
+ */
722
+ test("gives the rule every column the model requires", async () => {
723
+ const view: ReturnType<typeof render> = renderModal();
724
+
725
+ fireEvent.click(view.getByRole("button", { name: "Bulk-add by label" }));
726
+ await goToLastStep(view);
727
+ fireEvent.click(view.getByTestId("modal-footer-submit-button"));
728
+
729
+ await waitFor(() => {
730
+ expect(mockCreate).toHaveBeenCalledTimes(1);
731
+ });
732
+
733
+ const rule: StatusPageMonitorRule = createdRule()!;
734
+
735
+ for (const column of rule.getRequiredColumns().columns) {
736
+ expect({
737
+ column: column,
738
+ value: (rule as unknown as Record<string, unknown>)[column],
739
+ }).toEqual({ column: column, value: expect.anything() });
740
+ }
741
+
742
+ expect(rule.name).toBe("Label: WB Digital");
743
+ });
744
+
745
+ /*
746
+ * The server's backfill skips monitors already on the page, so writing the
747
+ * rule second is the only thing stopping every monitor this add just created
748
+ * getting a second resource - and a public page listing each one twice.
749
+ */
750
+ test("writes the rule only after the resources exist", async () => {
751
+ const view: ReturnType<typeof render> = renderModal();
752
+
753
+ fireEvent.click(view.getByRole("button", { name: "Bulk-add by label" }));
754
+ await goToLastStep(view);
755
+ fireEvent.click(view.getByTestId("modal-footer-submit-button"));
756
+
757
+ await waitFor(() => {
758
+ expect(mockCreate).toHaveBeenCalledTimes(1);
759
+ });
760
+
761
+ expect(callOrder).toEqual(["bulkAdd", "create"]);
762
+ });
763
+
764
+ test("carries every label when the picker was used twice", async () => {
765
+ const view: ReturnType<typeof render> = renderModal();
766
+
767
+ fireEvent.click(view.getByRole("button", { name: "Bulk-add by label" }));
768
+ fireEvent.click(
769
+ view.getByRole("button", { name: "Bulk-add by a second label" }),
770
+ );
771
+ await goToLastStep(view);
772
+ fireEvent.click(view.getByTestId("modal-footer-submit-button"));
773
+
774
+ await waitFor(() => {
775
+ expect(mockCreate).toHaveBeenCalledTimes(1);
776
+ });
777
+
778
+ expect(
779
+ (createdRule()?.monitorLabels || []).map((label: Label) => {
780
+ return label.id?.toString();
781
+ }),
782
+ ).toEqual([LABEL_ONE_ID, LABEL_TWO_ID]);
783
+ });
784
+
785
+ test("names the labels it is keeping in sync with", async () => {
786
+ const view: ReturnType<typeof render> = renderModal();
787
+
788
+ fireEvent.click(view.getByRole("button", { name: "Bulk-add by label" }));
789
+ fireEvent.click(
790
+ view.getByRole("button", { name: "Bulk-add by a second label" }),
791
+ );
792
+ await goToLastStep(view);
793
+ fireEvent.click(view.getByTestId("modal-footer-submit-button"));
794
+
795
+ await waitFor(() => {
796
+ expect(mockCreate).toHaveBeenCalledTimes(1);
797
+ });
798
+
799
+ expect(createdRule()?.name).toBe("Labels: WB Digital, WB Digital Service");
800
+ });
801
+
802
+ /*
803
+ * Somebody who pruned the expanded list by hand is saying they want these
804
+ * monitors and not the label. Turning it off has to mean exactly the old
805
+ * behaviour.
806
+ */
807
+ test("writes no rule when the sync is turned off", async () => {
808
+ const view: ReturnType<typeof render> = renderModal();
809
+
810
+ fireEvent.click(view.getByRole("button", { name: "Bulk-add by label" }));
811
+
812
+ const toggle: HTMLElement = view.getByRole("switch", {
813
+ name: /Keep This Group In Sync With These Labels/,
814
+ });
815
+
816
+ expect(toggle.getAttribute("aria-checked")).toBe("true");
817
+ fireEvent.click(toggle);
818
+ expect(toggle.getAttribute("aria-checked")).toBe("false");
819
+
820
+ await goToLastStep(view);
821
+ fireEvent.click(view.getByTestId("modal-footer-submit-button"));
822
+
823
+ await waitFor(() => {
824
+ expect(mockBulkAdd).toHaveBeenCalledTimes(1);
825
+ });
826
+
827
+ expect(mockCreate).not.toHaveBeenCalled();
828
+ });
829
+
830
+ /*
831
+ * A grid group places every resource in a row/column cell, and the public
832
+ * page skips one that has neither. A rule has nowhere to record the cell, so
833
+ * every monitor it added later would be on the page and rendered nowhere -
834
+ * better not to offer the promise at all.
835
+ */
836
+ test("never offers the sync for a grid group", async () => {
837
+ const view: ReturnType<typeof render> = renderModal({
838
+ gridPlacement: {
839
+ rowLabel: "Environment",
840
+ rowValues: ["Production", "Staging"],
841
+ columnLabel: "Region",
842
+ columnValues: ["US East", "EU West"],
843
+ },
844
+ });
845
+
846
+ fireEvent.click(view.getByRole("button", { name: "Bulk-add by label" }));
847
+
848
+ expect(
849
+ view.queryByText("Keep This Group In Sync With These Labels"),
850
+ ).not.toBeInTheDocument();
851
+
852
+ fireEvent.click(
853
+ view.getByRole("button", { name: "Choose Select environment" }),
854
+ );
855
+ fireEvent.click(view.getByRole("button", { name: "Choose Select region" }));
856
+ await goToLastStep(view);
857
+ fireEvent.click(view.getByTestId("modal-footer-submit-button"));
858
+
859
+ await waitFor(() => {
860
+ expect(mockBulkAdd).toHaveBeenCalledTimes(1);
861
+ });
862
+
863
+ expect(mockCreate).not.toHaveBeenCalled();
864
+ });
865
+
866
+ test("tells the user the group is now kept in sync", async () => {
867
+ const view: ReturnType<typeof render> = renderModal();
868
+
869
+ fireEvent.click(view.getByRole("button", { name: "Bulk-add by label" }));
870
+ await goToLastStep(view);
871
+ fireEvent.click(view.getByTestId("modal-footer-submit-button"));
872
+
873
+ expect(
874
+ await view.findByText(/stays in sync with the labels you used/),
875
+ ).toBeVisible();
876
+ });
877
+
878
+ /*
879
+ * Creating a rule needs a permission adding a resource does not, so this
880
+ * happens to real people. It must not read as a failed add - the resources
881
+ * are there.
882
+ */
883
+ test("keeps the monitors it added when the rule is refused", async () => {
884
+ mockCreate.mockRejectedValue(new Error("Permission denied"));
885
+
886
+ const view: ReturnType<typeof render> = renderModal();
887
+
888
+ fireEvent.click(view.getByRole("button", { name: "Bulk-add by label" }));
889
+ await goToLastStep(view);
890
+ fireEvent.click(view.getByTestId("modal-footer-submit-button"));
891
+
892
+ expect(
893
+ await view.findByText("Added 2 of 2 selected monitors."),
894
+ ).toBeVisible();
895
+ expect(
896
+ view.getByText(/could not be kept in sync with the labels you used/),
897
+ ).toBeVisible();
898
+ });
899
+ });