@oneuptime/common 12.0.23 → 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 (294) hide show
  1. package/Models/DatabaseModels/DetectionRule.ts +55 -0
  2. package/Models/DatabaseModels/NetworkDeviceAutoImportRule.ts +27 -7
  3. package/Models/DatabaseModels/NetworkDeviceDiscoveryScan.ts +67 -0
  4. package/Models/DatabaseModels/Project.ts +68 -0
  5. package/Models/DatabaseModels/User.ts +68 -0
  6. package/Server/API/TelemetryAPI.ts +29 -0
  7. package/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.ts +28 -0
  8. package/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.ts +49 -0
  9. package/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.ts +33 -0
  10. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  11. package/Server/Middleware/GzipRequestBody.ts +311 -0
  12. package/Server/Middleware/MultipartFormData.ts +87 -3
  13. package/Server/Services/DetectionRuleService.ts +73 -6
  14. package/Server/Services/Index.ts +205 -0
  15. package/Server/Services/NetworkDeviceDiscoveryScanService.ts +59 -0
  16. package/Server/Services/NetworkDeviceService.ts +148 -0
  17. package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +65 -0
  18. package/Server/Services/OnCallDutyPolicyFeedService.ts +1 -1
  19. package/Server/Services/ProjectService.ts +29 -19
  20. package/Server/Services/SecurityEventService.ts +90 -13
  21. package/Server/Services/StatusPageResourceService.ts +223 -0
  22. package/Server/Services/TelemetryAttributeService.ts +17 -0
  23. package/Server/Services/UserService.ts +4 -6
  24. package/Server/Types/Markdown.ts +161 -67
  25. package/Server/Utils/AI/Toolbox/OnCallTools.ts +12 -4
  26. package/Server/Utils/AnalyticsDatabase/Statement.ts +13 -1
  27. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +297 -6
  28. package/Server/Utils/Attribution.ts +4 -0
  29. package/Server/Utils/Marketing/MarketingEventUtil.ts +45 -7
  30. package/Server/Utils/SecurityEvent/DetectionRuleEvaluator.ts +55 -3
  31. package/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.ts +21 -0
  32. package/Server/Utils/StartServer.ts +12 -32
  33. package/Server/Utils/Telemetry/LlmCostBudgetEvaluator.ts +101 -4
  34. package/Server/Utils/Telemetry/LlmMetricSpend.ts +130 -0
  35. package/Server/Views/Partials/AnalyticsConsent.ejs +159 -102
  36. package/Server/Views/Partials/Head.ejs +2 -1
  37. package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +776 -38
  38. package/Tests/App/Dashboard/CorrelateFilterBuilder.test.tsx +359 -0
  39. package/Tests/App/Dashboard/CorrelateGraph.test.tsx +865 -0
  40. package/Tests/App/Dashboard/CorrelationGraph.test.ts +346 -0
  41. package/Tests/App/Dashboard/CriteriaFilterConditionDropdown.test.tsx +256 -0
  42. package/Tests/App/Dashboard/CriteriaFilterDefaults.test.ts +738 -0
  43. package/Tests/App/Dashboard/CriteriaFilterMonitorTypeChange.test.ts +1122 -0
  44. package/Tests/App/Dashboard/DiscoveryScanWizardValidation.test.tsx +283 -0
  45. package/Tests/App/Dashboard/ExternalStatusPageCriteriaFilter.test.ts +407 -0
  46. package/Tests/App/Dashboard/MetricSeriesInvestigateMenu.test.tsx +5 -14
  47. package/Tests/App/Dashboard/MonitorCriteriaActionToggles.test.tsx +426 -0
  48. package/Tests/App/Dashboard/MonitorStepsCriteriaAlignmentWiring.test.tsx +352 -0
  49. package/Tests/App/Dashboard/OnCallLayerCardOverrides.test.tsx +278 -0
  50. package/Tests/App/Dashboard/OnCallLayerShiftPreviewOverrides.test.ts +237 -0
  51. package/Tests/App/Dashboard/OnCallReadinessSurfaces.test.tsx +2 -2
  52. package/Tests/App/Dashboard/OnCallSchedulePreviewOverrides.test.tsx +460 -0
  53. package/Tests/App/Dashboard/SecurityEventAttributeUtil.test.ts +171 -0
  54. package/Tests/App/Dashboard/SecurityEventCorrelation.test.ts +857 -0
  55. package/Tests/App/Dashboard/SecurityEventDetailObservables.test.tsx +194 -0
  56. package/Tests/App/Dashboard/SecurityEventsDetectionRulesPage.test.tsx +124 -0
  57. package/Tests/App/Dashboard/SecurityEventsMonitorStepForm.test.tsx +79 -19
  58. package/Tests/App/Dashboard/SecurityEventsTableColumns.test.tsx +382 -0
  59. package/Tests/App/StatusPage/BulkAddStatusPageMonitors.test.ts +216 -0
  60. package/Tests/App/StatusPage/BulkAddStatusPageMonitorsModal.test.tsx +590 -11
  61. package/Tests/App/StatusPage/StatusPageResourceExplorer.test.tsx +757 -0
  62. package/Tests/Models/DiscoveryScanNameColumn.test.ts +293 -0
  63. package/Tests/Server/API/SecurityEventAttributesAPI.test.ts +564 -0
  64. package/Tests/Server/Infrastructure/Postgres/SchemaMigrationsOrdering.test.ts +120 -4
  65. package/Tests/Server/Middleware/GzipRequestBody.test.ts +725 -0
  66. package/Tests/Server/Middleware/MultipartFormData.test.ts +298 -0
  67. package/Tests/Server/Services/AddDetectionRuleDistinctCountColumnsMigration.test.ts +311 -0
  68. package/Tests/Server/Services/DetectionRuleService.test.ts +134 -2
  69. package/Tests/Server/Services/DiscoveryScanClaimHookFreeSafety.test.ts +7 -1
  70. package/Tests/Server/Services/FeedRetentionConsistency.test.ts +131 -0
  71. package/Tests/Server/Services/NetworkDeviceDiscoveryScanService.test.ts +194 -0
  72. package/Tests/Server/Services/NetworkDeviceMonitorStatusStamp.test.ts +800 -0
  73. package/Tests/Server/Services/OnCallScheduleAttachRosterRefresh.test.ts +244 -0
  74. package/Tests/Server/Services/SecurityEventService.test.ts +376 -0
  75. package/Tests/Server/Services/StatusPageResourceDuplicates.test.ts +703 -0
  76. package/Tests/Server/Services/TelemetryAttributeServiceSecurityEvent.test.ts +256 -0
  77. package/Tests/Server/Types/Workflow/Components/ComponentRegistryParity.test.ts +417 -0
  78. package/Tests/Server/Utils/AnalyticsDatabase/StatementGeneratorWhereOperators.test.ts +677 -0
  79. package/Tests/Server/Utils/Marketing/MarketingEventUtil.test.ts +43 -0
  80. package/Tests/Server/Utils/Monitor/Criteria/ProfileMonitorCriteria.test.ts +15 -11
  81. package/Tests/Server/Utils/SecurityEvent/DetectionRuleEvaluator.test.ts +369 -8
  82. package/Tests/Server/Utils/SecurityEvent/SigmaClickhouseCompiler.test.ts +35 -0
  83. package/Tests/Server/Utils/Telemetry/LlmCostBudgetEvaluator.test.ts +368 -4
  84. package/Tests/Server/Utils/Telemetry/LlmMetricSpend.test.ts +245 -0
  85. package/Tests/Server/Views/AnalyticsConsentPartial.test.ts +48 -5
  86. package/Tests/Types/Code/YamlSyntax.test.ts +396 -0
  87. package/Tests/Types/Monitor/MetricAndProfileDefaultCriteria.test.ts +957 -0
  88. package/Tests/Types/Monitor/MonitorCriteriaActionValidation.test.ts +598 -0
  89. package/Tests/Types/Monitor/MonitorCriteriaInstance.test.ts +49 -0
  90. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationNotificationMode.test.ts +6 -6
  91. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.test.ts +4 -4
  92. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationUtil.test.ts +4 -3
  93. package/Tests/Types/Telemetry/LlmMetricConventions.test.ts +193 -0
  94. package/Tests/UI/Components/AiInvestigationSettingsCard.test.tsx +0 -12
  95. package/Tests/UI/Components/CardModelDetailEdit.test.tsx +1 -17
  96. package/Tests/UI/Components/CodeEditor.test.tsx +474 -10
  97. package/Tests/UI/Components/Detail/YamlField.test.tsx +105 -0
  98. package/Tests/UI/Components/EntityDropdownLabelsBulkAdd.test.tsx +319 -0
  99. package/Tests/UI/Components/Forms/FormFieldYaml.test.tsx +231 -0
  100. package/Tests/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.test.ts +1 -0
  101. package/Tests/UI/Components/Forms/ValidationYAML.test.ts +234 -0
  102. package/Tests/UI/Components/ModelTable/AttributeColumns.test.tsx +573 -0
  103. package/Tests/UI/Components/ModelTable/BaseModelTableAttributeColumns.test.tsx +824 -0
  104. package/Tests/UI/Components/ModelTable/ColumnCustomizationModalAddableColumns.test.tsx +707 -0
  105. package/Tests/UI/Components/SideOver.test.tsx +1 -1
  106. package/Tests/UI/Components/StatusPage/ResourceGridSelection.test.tsx +294 -0
  107. package/Tests/UI/Components/StatusPage/ResourceListSelection.test.tsx +436 -0
  108. package/Tests/UI/Components/TableBulkCsvExport.test.tsx +6 -2
  109. package/Tests/UI/Components/Toggle.test.tsx +25 -0
  110. package/Tests/UI/Components/YamlEditor.test.tsx +847 -0
  111. package/Tests/Utils/NetworkDevice/DeviceReachabilityUtil.test.ts +252 -0
  112. package/Tests/Utils/NetworkDiscovery/DiscoveredHostUtil.test.ts +221 -0
  113. package/Tests/Utils/NetworkDiscovery/ScanNameUtil.test.ts +312 -0
  114. package/Tests/Utils/StatusPage/ResourceExplorer.test.ts +468 -0
  115. package/Tests/Utils/Telemetry/LlmMetricQuery.test.ts +472 -0
  116. package/Types/Code/YamlSyntax.ts +396 -0
  117. package/Types/Exception/ExceptionCode.ts +1 -0
  118. package/Types/Exception/PayloadTooLargeException.ts +8 -0
  119. package/Types/Marketing/Attribution.ts +10 -0
  120. package/Types/Marketing/MarketingEvent.ts +4 -0
  121. package/Types/Monitor/MonitorCriteriaInstance.ts +116 -56
  122. package/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.ts +2 -1
  123. package/Types/SecurityEvent/DetectionFindingConstants.ts +11 -0
  124. package/Types/Telemetry/LlmMetricConventions.ts +124 -0
  125. package/UI/Components/AutocompleteTextInput/AutocompleteTextInput.tsx +3 -0
  126. package/UI/Components/BulkUpdate/BulkUpdateForm.tsx +10 -1
  127. package/UI/Components/CodeEditor/CodeEditor.tsx +241 -79
  128. package/UI/Components/CodeEditor/YamlEditor.tsx +369 -0
  129. package/UI/Components/Detail/Detail.tsx +10 -0
  130. package/UI/Components/EntityDropdown/EntityDropdown.tsx +33 -0
  131. package/UI/Components/Forms/Fields/FormField.tsx +31 -0
  132. package/UI/Components/Forms/Types/Field.ts +8 -0
  133. package/UI/Components/Forms/Types/FormFieldSchemaType.ts +1 -0
  134. package/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.ts +2 -0
  135. package/UI/Components/Forms/Validation.ts +46 -0
  136. package/UI/Components/Input/Input.tsx +3 -0
  137. package/UI/Components/ModelTable/AttributeColumns.tsx +321 -0
  138. package/UI/Components/ModelTable/BaseModelTable.tsx +263 -14
  139. package/UI/Components/ModelTable/Column.ts +8 -0
  140. package/UI/Components/ModelTable/ColumnCustomizationModal.tsx +310 -0
  141. package/UI/Components/ModelTable/ColumnPreference.ts +13 -1
  142. package/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.tsx +2 -2
  143. package/UI/Components/SideOver/SideOver.tsx +5 -5
  144. package/UI/Components/StatusPage/ResourceGrid.tsx +82 -2
  145. package/UI/Components/StatusPage/ResourceList.tsx +197 -38
  146. package/UI/Components/Toggle/Toggle.tsx +9 -1
  147. package/UI/Components/Types/FieldType.ts +1 -0
  148. package/Utils/NetworkDevice/DeviceReachabilityUtil.ts +73 -1
  149. package/Utils/NetworkDiscovery/ScanNameUtil.ts +181 -0
  150. package/Utils/StatusPage/ResourceExplorer.ts +258 -0
  151. package/Utils/Telemetry/LlmMetricQuery.ts +204 -0
  152. package/build/dist/Models/DatabaseModels/DetectionRule.js +57 -0
  153. package/build/dist/Models/DatabaseModels/DetectionRule.js.map +1 -1
  154. package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js +27 -7
  155. package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js.map +1 -1
  156. package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js +69 -0
  157. package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js.map +1 -1
  158. package/build/dist/Models/DatabaseModels/Project.js +76 -0
  159. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  160. package/build/dist/Models/DatabaseModels/User.js +76 -0
  161. package/build/dist/Models/DatabaseModels/User.js.map +1 -1
  162. package/build/dist/Server/API/TelemetryAPI.js +17 -0
  163. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  164. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.js +21 -0
  165. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.js.map +1 -0
  166. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.js +42 -0
  167. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.js.map +1 -0
  168. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.js +22 -0
  169. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.js.map +1 -0
  170. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  171. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  172. package/build/dist/Server/Middleware/GzipRequestBody.js +224 -0
  173. package/build/dist/Server/Middleware/GzipRequestBody.js.map +1 -0
  174. package/build/dist/Server/Middleware/MultipartFormData.js +71 -2
  175. package/build/dist/Server/Middleware/MultipartFormData.js.map +1 -1
  176. package/build/dist/Server/Services/DetectionRuleService.js +49 -4
  177. package/build/dist/Server/Services/DetectionRuleService.js.map +1 -1
  178. package/build/dist/Server/Services/Index.js +204 -0
  179. package/build/dist/Server/Services/Index.js.map +1 -1
  180. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js +50 -0
  181. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js.map +1 -1
  182. package/build/dist/Server/Services/NetworkDeviceService.js +124 -0
  183. package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
  184. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +68 -13
  185. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
  186. package/build/dist/Server/Services/OnCallDutyPolicyFeedService.js +1 -1
  187. package/build/dist/Server/Services/OnCallDutyPolicyFeedService.js.map +1 -1
  188. package/build/dist/Server/Services/ProjectService.js +25 -45
  189. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  190. package/build/dist/Server/Services/SecurityEventService.js +57 -5
  191. package/build/dist/Server/Services/SecurityEventService.js.map +1 -1
  192. package/build/dist/Server/Services/StatusPageResourceService.js +140 -0
  193. package/build/dist/Server/Services/StatusPageResourceService.js.map +1 -1
  194. package/build/dist/Server/Services/TelemetryAttributeService.js +17 -0
  195. package/build/dist/Server/Services/TelemetryAttributeService.js.map +1 -1
  196. package/build/dist/Server/Services/UserService.js +2 -12
  197. package/build/dist/Server/Services/UserService.js.map +1 -1
  198. package/build/dist/Server/Types/Markdown.js +147 -61
  199. package/build/dist/Server/Types/Markdown.js.map +1 -1
  200. package/build/dist/Server/Utils/AI/Toolbox/OnCallTools.js +12 -4
  201. package/build/dist/Server/Utils/AI/Toolbox/OnCallTools.js.map +1 -1
  202. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js +12 -1
  203. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js.map +1 -1
  204. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +246 -9
  205. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  206. package/build/dist/Server/Utils/Attribution.js.map +1 -1
  207. package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js +27 -7
  208. package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js.map +1 -1
  209. package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js +42 -7
  210. package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js.map +1 -1
  211. package/build/dist/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.js +18 -0
  212. package/build/dist/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.js.map +1 -1
  213. package/build/dist/Server/Utils/StartServer.js +12 -21
  214. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  215. package/build/dist/Server/Utils/Telemetry/LlmCostBudgetEvaluator.js +76 -5
  216. package/build/dist/Server/Utils/Telemetry/LlmCostBudgetEvaluator.js.map +1 -1
  217. package/build/dist/Server/Utils/Telemetry/LlmMetricSpend.js +102 -0
  218. package/build/dist/Server/Utils/Telemetry/LlmMetricSpend.js.map +1 -0
  219. package/build/dist/Types/Code/YamlSyntax.js +212 -0
  220. package/build/dist/Types/Code/YamlSyntax.js.map +1 -0
  221. package/build/dist/Types/Exception/ExceptionCode.js +1 -0
  222. package/build/dist/Types/Exception/ExceptionCode.js.map +1 -1
  223. package/build/dist/Types/Exception/PayloadTooLargeException.js +8 -0
  224. package/build/dist/Types/Exception/PayloadTooLargeException.js.map +1 -0
  225. package/build/dist/Types/Marketing/Attribution.js +10 -0
  226. package/build/dist/Types/Marketing/Attribution.js.map +1 -1
  227. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js +112 -51
  228. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js.map +1 -1
  229. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.js +2 -1
  230. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.js.map +1 -1
  231. package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js +9 -0
  232. package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js.map +1 -1
  233. package/build/dist/Types/Telemetry/LlmMetricConventions.js +98 -0
  234. package/build/dist/Types/Telemetry/LlmMetricConventions.js.map +1 -0
  235. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js +1 -1
  236. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js.map +1 -1
  237. package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js +5 -2
  238. package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js.map +1 -1
  239. package/build/dist/UI/Components/CodeEditor/CodeEditor.js +206 -76
  240. package/build/dist/UI/Components/CodeEditor/CodeEditor.js.map +1 -1
  241. package/build/dist/UI/Components/CodeEditor/YamlEditor.js +201 -0
  242. package/build/dist/UI/Components/CodeEditor/YamlEditor.js.map +1 -0
  243. package/build/dist/UI/Components/Detail/Detail.js +9 -0
  244. package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
  245. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js +16 -1
  246. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js.map +1 -1
  247. package/build/dist/UI/Components/Forms/Fields/FormField.js +14 -1
  248. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  249. package/build/dist/UI/Components/Forms/Types/Field.js.map +1 -1
  250. package/build/dist/UI/Components/Forms/Types/FormFieldSchemaType.js +1 -0
  251. package/build/dist/UI/Components/Forms/Types/FormFieldSchemaType.js.map +1 -1
  252. package/build/dist/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.js +2 -0
  253. package/build/dist/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.js.map +1 -1
  254. package/build/dist/UI/Components/Forms/Validation.js +27 -0
  255. package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
  256. package/build/dist/UI/Components/Input/Input.js +1 -1
  257. package/build/dist/UI/Components/Input/Input.js.map +1 -1
  258. package/build/dist/UI/Components/ModelTable/AttributeColumns.js +159 -0
  259. package/build/dist/UI/Components/ModelTable/AttributeColumns.js.map +1 -0
  260. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +179 -17
  261. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  262. package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js +133 -2
  263. package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js.map +1 -1
  264. package/build/dist/UI/Components/ModelTable/ColumnPreference.js +7 -1
  265. package/build/dist/UI/Components/ModelTable/ColumnPreference.js.map +1 -1
  266. package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.js +2 -2
  267. package/build/dist/UI/Components/SideOver/SideOver.js +5 -5
  268. package/build/dist/UI/Components/StatusPage/ResourceGrid.js +28 -2
  269. package/build/dist/UI/Components/StatusPage/ResourceGrid.js.map +1 -1
  270. package/build/dist/UI/Components/StatusPage/ResourceList.js +87 -20
  271. package/build/dist/UI/Components/StatusPage/ResourceList.js.map +1 -1
  272. package/build/dist/UI/Components/Toggle/Toggle.js +9 -1
  273. package/build/dist/UI/Components/Toggle/Toggle.js.map +1 -1
  274. package/build/dist/UI/Components/Types/FieldType.js +1 -0
  275. package/build/dist/UI/Components/Types/FieldType.js.map +1 -1
  276. package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js +37 -1
  277. package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js.map +1 -1
  278. package/build/dist/Utils/NetworkDiscovery/ScanNameUtil.js +120 -0
  279. package/build/dist/Utils/NetworkDiscovery/ScanNameUtil.js.map +1 -0
  280. package/build/dist/Utils/StatusPage/ResourceExplorer.js +171 -0
  281. package/build/dist/Utils/StatusPage/ResourceExplorer.js.map +1 -1
  282. package/build/dist/Utils/Telemetry/LlmMetricQuery.js +119 -0
  283. package/build/dist/Utils/Telemetry/LlmMetricQuery.js.map +1 -0
  284. package/jest.config.json +2 -0
  285. package/package.json +1 -1
  286. package/tsconfig.json +2 -1
  287. package/Tests/UI/Components/Toast.test.tsx +0 -197
  288. package/Tests/UI/Components/ToastStacking.test.tsx +0 -198
  289. package/UI/Components/Toast/Toast.tsx +0 -189
  290. package/UI/Components/Toast/ToastInit.tsx +0 -110
  291. package/build/dist/UI/Components/Toast/Toast.js +0 -123
  292. package/build/dist/UI/Components/Toast/Toast.js.map +0 -1
  293. package/build/dist/UI/Components/Toast/ToastInit.js +0 -53
  294. package/build/dist/UI/Components/Toast/ToastInit.js.map +0 -1
@@ -0,0 +1,703 @@
1
+ import StatusPageResourceService from "../../../Server/Services/StatusPageResourceService";
2
+ import CreateBy from "../../../Server/Types/Database/CreateBy";
3
+ import UpdateBy from "../../../Server/Types/Database/UpdateBy";
4
+ import Monitor from "../../../Models/DatabaseModels/Monitor";
5
+ import MonitorGroup from "../../../Models/DatabaseModels/MonitorGroup";
6
+ import StatusPageResource from "../../../Models/DatabaseModels/StatusPageResource";
7
+ import BadDataException from "../../../Types/Exception/BadDataException";
8
+ import ObjectID from "../../../Types/ObjectID";
9
+ import PositiveNumber from "../../../Types/PositiveNumber";
10
+ import { afterEach, describe, expect, it, jest } from "@jest/globals";
11
+
12
+ /*
13
+ * Contract under test - a status page lists a monitor once.
14
+ *
15
+ * Nothing used to stop a second resource being created for a monitor that was
16
+ * already on the page. Selecting monitors by label re-selects every monitor
17
+ * carrying that label, so re-adding a label after one new monitor joined it
18
+ * wrote a duplicate resource for every monitor already there, and the public
19
+ * page listed each of them twice (issue #3420).
20
+ *
21
+ * The rule engine has always refused to add a monitor that is already on the
22
+ * page for exactly this reason. These cases pin the same promise onto the
23
+ * service, which is the one thing every other way of creating a resource -
24
+ * the resource form, the bulk add modal, the API - has to go through.
25
+ *
26
+ * Two properties matter and both are asserted from each write path:
27
+ *
28
+ * 1. the duplicate is judged per STATUS PAGE, not per group: a monitor in
29
+ * two groups is still a monitor a visitor sees twice;
30
+ *
31
+ * 2. the resource being edited is never a duplicate of itself, or saving
32
+ * the edit form without changing the monitor would start failing.
33
+ */
34
+
35
+ const STATUS_PAGE_ID: ObjectID = new ObjectID(
36
+ "11111111-1111-4111-8111-111111111111",
37
+ );
38
+ const OTHER_STATUS_PAGE_ID: ObjectID = new ObjectID(
39
+ "22222222-2222-4222-8222-222222222222",
40
+ );
41
+ const PROJECT_ID: ObjectID = new ObjectID(
42
+ "33333333-3333-4333-8333-333333333333",
43
+ );
44
+ const GROUP_ID: ObjectID = new ObjectID("44444444-4444-4444-8444-444444444444");
45
+ const OTHER_GROUP_ID: ObjectID = new ObjectID(
46
+ "55555555-5555-4555-8555-555555555555",
47
+ );
48
+
49
+ const MONITOR_ID: ObjectID = new ObjectID(
50
+ "66666666-6666-4666-8666-666666666666",
51
+ );
52
+ const OTHER_MONITOR_ID: ObjectID = new ObjectID(
53
+ "77777777-7777-4777-8777-777777777777",
54
+ );
55
+ const MONITOR_GROUP_ID: ObjectID = new ObjectID(
56
+ "88888888-8888-4888-8888-888888888888",
57
+ );
58
+
59
+ const DUPLICATE_MONITOR_MESSAGE: string =
60
+ "This monitor is already added to this status page. A monitor can only be added once so it is not shown twice to your customers.";
61
+ const DUPLICATE_MONITOR_GROUP_MESSAGE: string =
62
+ "This monitor group is already added to this status page. A monitor group can only be added once so it is not shown twice to your customers.";
63
+
64
+ function resourceId(index: number): ObjectID {
65
+ const suffix: string = index.toString().padStart(12, "0");
66
+ return new ObjectID(`aaaaaaaa-aaaa-4aaa-8aaa-${suffix}`);
67
+ }
68
+
69
+ function makeResource(data: {
70
+ id: ObjectID;
71
+ statusPageId?: ObjectID | undefined;
72
+ statusPageGroupId?: ObjectID | undefined;
73
+ monitorId?: ObjectID | undefined;
74
+ monitorGroupId?: ObjectID | undefined;
75
+ }): StatusPageResource {
76
+ const resource: StatusPageResource = new StatusPageResource();
77
+ resource._id = data.id.toString();
78
+ resource.projectId = PROJECT_ID;
79
+ resource.statusPageId = data.statusPageId || STATUS_PAGE_ID;
80
+
81
+ if (data.statusPageGroupId) {
82
+ resource.statusPageGroupId = data.statusPageGroupId;
83
+ }
84
+
85
+ if (data.monitorId) {
86
+ resource.monitorId = data.monitorId;
87
+ }
88
+
89
+ if (data.monitorGroupId) {
90
+ resource.monitorGroupId = data.monitorGroupId;
91
+ }
92
+
93
+ return resource;
94
+ }
95
+
96
+ /*
97
+ * `QueryHelper.notEquals` is a TypeORM Raw operator: the id it excludes lives
98
+ * in the bound parameter bag rather than on the query object, so the fake
99
+ * reads it back out the same way QueryHelper's own tests do.
100
+ */
101
+ function getExcludedId(value: unknown): string | null {
102
+ const parameters: Record<string, unknown> | undefined = (
103
+ value as { objectLiteralParameters?: Record<string, unknown> } | undefined
104
+ )?.objectLiteralParameters;
105
+
106
+ if (!parameters) {
107
+ return null;
108
+ }
109
+
110
+ const values: Array<unknown> = Object.values(parameters);
111
+
112
+ return values.length > 0 ? String(values[0]) : null;
113
+ }
114
+
115
+ function getDirectId(value: unknown): string | null {
116
+ if (typeof value === "string") {
117
+ return value;
118
+ }
119
+
120
+ if (value instanceof ObjectID) {
121
+ return value.toString();
122
+ }
123
+
124
+ return null;
125
+ }
126
+
127
+ /*
128
+ * Stands in for the resources table. findOneBy answers the two reads the hooks
129
+ * make - "is this monitor already on the page" and "which resource is being
130
+ * updated" - and the order bookkeeping is stubbed so these cases only exercise
131
+ * the duplicate rules.
132
+ */
133
+ function mockService(rows: Array<StatusPageResource>): {
134
+ findOneBy: jest.SpiedFunction<typeof StatusPageResourceService.findOneBy>;
135
+ countBy: jest.SpiedFunction<typeof StatusPageResourceService.countBy>;
136
+ updateOneBy: jest.SpiedFunction<typeof StatusPageResourceService.updateOneBy>;
137
+ } {
138
+ const findOneBy: any = jest
139
+ .spyOn(StatusPageResourceService, "findOneBy")
140
+ .mockImplementation(async (findBy: any) => {
141
+ const query: any = findBy.query || {};
142
+ const directId: string | null = getDirectId(query._id);
143
+ const excludedId: string | null = getExcludedId(query._id);
144
+
145
+ const match: StatusPageResource | undefined = rows.find(
146
+ (row: StatusPageResource) => {
147
+ if (directId && row._id !== directId) {
148
+ return false;
149
+ }
150
+
151
+ if (excludedId && row._id === excludedId) {
152
+ return false;
153
+ }
154
+
155
+ if (
156
+ query.statusPageId &&
157
+ row.statusPageId?.toString() !== query.statusPageId.toString()
158
+ ) {
159
+ return false;
160
+ }
161
+
162
+ if (
163
+ query.monitorId &&
164
+ row.monitorId?.toString() !== query.monitorId.toString()
165
+ ) {
166
+ return false;
167
+ }
168
+
169
+ if (
170
+ query.monitorGroupId &&
171
+ row.monitorGroupId?.toString() !== query.monitorGroupId.toString()
172
+ ) {
173
+ return false;
174
+ }
175
+
176
+ return true;
177
+ },
178
+ );
179
+
180
+ return match || null;
181
+ });
182
+
183
+ const countBy: any = jest
184
+ .spyOn(StatusPageResourceService, "countBy")
185
+ .mockResolvedValue(new PositiveNumber(rows.length) as never);
186
+
187
+ jest
188
+ .spyOn(StatusPageResourceService, "findBy")
189
+ .mockResolvedValue([] as never);
190
+
191
+ const updateOneBy: any = jest
192
+ .spyOn(StatusPageResourceService, "updateOneBy")
193
+ .mockResolvedValue(1 as never);
194
+
195
+ return { findOneBy, countBy, updateOneBy };
196
+ }
197
+
198
+ function createBy(data: {
199
+ statusPageId?: ObjectID | undefined;
200
+ omitStatusPageId?: boolean | undefined;
201
+ statusPageGroupId?: ObjectID | undefined;
202
+ monitorId?: ObjectID | undefined;
203
+ monitor?: ObjectID | undefined;
204
+ monitorGroupId?: ObjectID | undefined;
205
+ monitorGroup?: ObjectID | undefined;
206
+ }): CreateBy<StatusPageResource> {
207
+ const resource: StatusPageResource = new StatusPageResource();
208
+ resource.projectId = PROJECT_ID;
209
+ resource.displayName = "New Resource";
210
+
211
+ if (!data.omitStatusPageId) {
212
+ resource.statusPageId = data.statusPageId || STATUS_PAGE_ID;
213
+ }
214
+
215
+ if (data.statusPageGroupId) {
216
+ resource.statusPageGroupId = data.statusPageGroupId;
217
+ }
218
+
219
+ if (data.monitorId) {
220
+ resource.monitorId = data.monitorId;
221
+ }
222
+
223
+ if (data.monitor) {
224
+ const monitor: Monitor = new Monitor();
225
+ monitor._id = data.monitor.toString();
226
+ resource.monitor = monitor;
227
+ }
228
+
229
+ if (data.monitorGroupId) {
230
+ resource.monitorGroupId = data.monitorGroupId;
231
+ }
232
+
233
+ if (data.monitorGroup) {
234
+ const monitorGroup: MonitorGroup = new MonitorGroup();
235
+ monitorGroup._id = data.monitorGroup.toString();
236
+ resource.monitorGroup = monitorGroup;
237
+ }
238
+
239
+ return {
240
+ data: resource,
241
+ props: { isRoot: true },
242
+ } as CreateBy<StatusPageResource>;
243
+ }
244
+
245
+ /*
246
+ * The update body is NOT a model. BaseAPI.updateItem builds it with
247
+ * JSONFunctions.deserialize, which revives ObjectID values but never nested
248
+ * models - so a relation the dashboard posts arrives as the plain
249
+ * `{ _id: "<uuid>" }` the browser sent, with no `id` on it. Building these
250
+ * fixtures as `new Monitor()` instead is what let an earlier version of the
251
+ * guard read nothing on every edit-form save and still pass its tests.
252
+ */
253
+ function updateBy(data: {
254
+ id?: ObjectID | undefined;
255
+ monitorId?: ObjectID | undefined;
256
+ monitor?: ObjectID | undefined;
257
+ monitorGroupId?: ObjectID | undefined;
258
+ monitorGroup?: ObjectID | undefined;
259
+ displayName?: string | undefined;
260
+ }): UpdateBy<StatusPageResource> {
261
+ const updateData: Record<string, unknown> = {};
262
+
263
+ if (data.monitorId) {
264
+ updateData["monitorId"] = data.monitorId;
265
+ }
266
+
267
+ if (data.monitor) {
268
+ updateData["monitor"] = { _id: data.monitor.toString() };
269
+ }
270
+
271
+ if (data.monitorGroupId) {
272
+ updateData["monitorGroupId"] = data.monitorGroupId;
273
+ }
274
+
275
+ if (data.monitorGroup) {
276
+ updateData["monitorGroup"] = { _id: data.monitorGroup.toString() };
277
+ }
278
+
279
+ if (data.displayName) {
280
+ updateData["displayName"] = data.displayName;
281
+ }
282
+
283
+ return {
284
+ query: data.id ? { _id: data.id.toString() } : {},
285
+ data: updateData,
286
+ props: { isRoot: true },
287
+ } as unknown as UpdateBy<StatusPageResource>;
288
+ }
289
+
290
+ type OnBeforeCreateFunction = (
291
+ createBy: CreateBy<StatusPageResource>,
292
+ ) => Promise<unknown>;
293
+
294
+ const onBeforeCreate: OnBeforeCreateFunction = (
295
+ by: CreateBy<StatusPageResource>,
296
+ ): Promise<unknown> => {
297
+ return (StatusPageResourceService as any).onBeforeCreate(by);
298
+ };
299
+
300
+ type OnBeforeUpdateFunction = (
301
+ updateBy: UpdateBy<StatusPageResource>,
302
+ ) => Promise<unknown>;
303
+
304
+ const onBeforeUpdate: OnBeforeUpdateFunction = (
305
+ by: UpdateBy<StatusPageResource>,
306
+ ): Promise<unknown> => {
307
+ return (StatusPageResourceService as any).onBeforeUpdate(by);
308
+ };
309
+
310
+ describe("StatusPageResourceService duplicate rules", () => {
311
+ afterEach(() => {
312
+ jest.restoreAllMocks();
313
+ });
314
+
315
+ describe("on create", () => {
316
+ it("adds a monitor the status page does not list yet", async () => {
317
+ mockService([]);
318
+
319
+ await expect(
320
+ onBeforeCreate(createBy({ monitorId: MONITOR_ID })),
321
+ ).resolves.toBeDefined();
322
+ });
323
+
324
+ it("refuses a second resource for a monitor already on the page", async () => {
325
+ mockService([makeResource({ id: resourceId(1), monitorId: MONITOR_ID })]);
326
+
327
+ await expect(
328
+ onBeforeCreate(createBy({ monitorId: MONITOR_ID })),
329
+ ).rejects.toThrow(new BadDataException(DUPLICATE_MONITOR_MESSAGE));
330
+ });
331
+
332
+ /*
333
+ * The reported bug: the monitors already in the group come back selected
334
+ * when the label is picked again, and adding used to duplicate every one.
335
+ */
336
+ it("refuses re-adding a monitor to the same group it is already in", async () => {
337
+ mockService([
338
+ makeResource({
339
+ id: resourceId(1),
340
+ monitorId: MONITOR_ID,
341
+ statusPageGroupId: GROUP_ID,
342
+ }),
343
+ ]);
344
+
345
+ await expect(
346
+ onBeforeCreate(
347
+ createBy({ monitorId: MONITOR_ID, statusPageGroupId: GROUP_ID }),
348
+ ),
349
+ ).rejects.toThrow(new BadDataException(DUPLICATE_MONITOR_MESSAGE));
350
+ });
351
+
352
+ it("refuses a monitor that is on the page in a different group", async () => {
353
+ mockService([
354
+ makeResource({
355
+ id: resourceId(1),
356
+ monitorId: MONITOR_ID,
357
+ statusPageGroupId: GROUP_ID,
358
+ }),
359
+ ]);
360
+
361
+ await expect(
362
+ onBeforeCreate(
363
+ createBy({
364
+ monitorId: MONITOR_ID,
365
+ statusPageGroupId: OTHER_GROUP_ID,
366
+ }),
367
+ ),
368
+ ).rejects.toThrow(new BadDataException(DUPLICATE_MONITOR_MESSAGE));
369
+ });
370
+
371
+ it("allows the same monitor on a different status page", async () => {
372
+ mockService([
373
+ makeResource({
374
+ id: resourceId(1),
375
+ statusPageId: OTHER_STATUS_PAGE_ID,
376
+ monitorId: MONITOR_ID,
377
+ }),
378
+ ]);
379
+
380
+ await expect(
381
+ onBeforeCreate(createBy({ monitorId: MONITOR_ID })),
382
+ ).resolves.toBeDefined();
383
+ });
384
+
385
+ it("allows a different monitor onto a page that already has one", async () => {
386
+ mockService([makeResource({ id: resourceId(1), monitorId: MONITOR_ID })]);
387
+
388
+ await expect(
389
+ onBeforeCreate(createBy({ monitorId: OTHER_MONITOR_ID })),
390
+ ).resolves.toBeDefined();
391
+ });
392
+
393
+ /*
394
+ * The dashboard's resource form posts the relation rather than the foreign
395
+ * key column, so the check has to read both or the single-add form keeps
396
+ * writing duplicates.
397
+ */
398
+ it("resolves the monitor from the relation the resource form posts", async () => {
399
+ mockService([makeResource({ id: resourceId(1), monitorId: MONITOR_ID })]);
400
+
401
+ await expect(
402
+ onBeforeCreate(createBy({ monitor: MONITOR_ID })),
403
+ ).rejects.toThrow(new BadDataException(DUPLICATE_MONITOR_MESSAGE));
404
+ });
405
+
406
+ it("refuses a second resource for a monitor group already on the page", async () => {
407
+ mockService([
408
+ makeResource({ id: resourceId(1), monitorGroupId: MONITOR_GROUP_ID }),
409
+ ]);
410
+
411
+ await expect(
412
+ onBeforeCreate(createBy({ monitorGroupId: MONITOR_GROUP_ID })),
413
+ ).rejects.toThrow(new BadDataException(DUPLICATE_MONITOR_GROUP_MESSAGE));
414
+ });
415
+
416
+ it("resolves the monitor group from the relation the resource form posts", async () => {
417
+ mockService([
418
+ makeResource({ id: resourceId(1), monitorGroupId: MONITOR_GROUP_ID }),
419
+ ]);
420
+
421
+ await expect(
422
+ onBeforeCreate(createBy({ monitorGroup: MONITOR_GROUP_ID })),
423
+ ).rejects.toThrow(new BadDataException(DUPLICATE_MONITOR_GROUP_MESSAGE));
424
+ });
425
+
426
+ it("does not treat a monitor group resource as a duplicate of a monitor", async () => {
427
+ mockService([
428
+ makeResource({ id: resourceId(1), monitorGroupId: MONITOR_GROUP_ID }),
429
+ ]);
430
+
431
+ await expect(
432
+ onBeforeCreate(createBy({ monitorId: MONITOR_ID })),
433
+ ).resolves.toBeDefined();
434
+ });
435
+
436
+ it("looks for a duplicate by status page and monitor only", async () => {
437
+ const mocks: ReturnType<typeof mockService> = mockService([]);
438
+
439
+ await onBeforeCreate(
440
+ createBy({ monitorId: MONITOR_ID, statusPageGroupId: GROUP_ID }),
441
+ );
442
+
443
+ const duplicateLookup: any = (mocks.findOneBy.mock.calls[0] as any)[0];
444
+ expect(duplicateLookup.query).toEqual({
445
+ statusPageId: STATUS_PAGE_ID,
446
+ monitorId: MONITOR_ID,
447
+ });
448
+ expect(duplicateLookup.props.isRoot).toBe(true);
449
+ });
450
+
451
+ it("does not look for a duplicate when the resource names neither", async () => {
452
+ const mocks: ReturnType<typeof mockService> = mockService([]);
453
+
454
+ await onBeforeCreate(createBy({}));
455
+
456
+ expect(mocks.findOneBy).not.toHaveBeenCalled();
457
+ });
458
+
459
+ it("refuses before it renumbers the group", async () => {
460
+ const mocks: ReturnType<typeof mockService> = mockService([
461
+ makeResource({ id: resourceId(1), monitorId: MONITOR_ID }),
462
+ ]);
463
+
464
+ await expect(
465
+ onBeforeCreate(createBy({ monitorId: MONITOR_ID })),
466
+ ).rejects.toThrow(BadDataException);
467
+
468
+ expect(mocks.countBy).not.toHaveBeenCalled();
469
+ expect(mocks.updateOneBy).not.toHaveBeenCalled();
470
+ });
471
+
472
+ it("still requires a status page id", async () => {
473
+ const mocks: ReturnType<typeof mockService> = mockService([]);
474
+
475
+ await expect(
476
+ onBeforeCreate(
477
+ createBy({
478
+ omitStatusPageId: true,
479
+ monitorId: MONITOR_ID,
480
+ }),
481
+ ),
482
+ ).rejects.toThrow(
483
+ new BadDataException("Status Page Resource statusPageId is required"),
484
+ );
485
+
486
+ expect(mocks.findOneBy).not.toHaveBeenCalled();
487
+ });
488
+ });
489
+
490
+ describe("on update", () => {
491
+ it("refuses pointing a resource at a monitor the page already lists", async () => {
492
+ mockService([
493
+ makeResource({ id: resourceId(1), monitorId: OTHER_MONITOR_ID }),
494
+ makeResource({ id: resourceId(2), monitorId: MONITOR_ID }),
495
+ ]);
496
+
497
+ await expect(
498
+ onBeforeUpdate(updateBy({ id: resourceId(1), monitorId: MONITOR_ID })),
499
+ ).rejects.toThrow(new BadDataException(DUPLICATE_MONITOR_MESSAGE));
500
+ });
501
+
502
+ it("lets a resource keep the monitor it already has", async () => {
503
+ mockService([makeResource({ id: resourceId(1), monitorId: MONITOR_ID })]);
504
+
505
+ await expect(
506
+ onBeforeUpdate(updateBy({ id: resourceId(1), monitorId: MONITOR_ID })),
507
+ ).resolves.toBeDefined();
508
+ });
509
+
510
+ /*
511
+ * The edit form is a ModelForm: it posts the monitor on every save, even
512
+ * one that only renamed the row. Looking for a duplicate then would be
513
+ * both wasted work and, on a page that already carries a duplicate from
514
+ * before this rule existed, a refusal of a rename.
515
+ */
516
+ it("does not even look for a duplicate when the monitor is unchanged", async () => {
517
+ const mocks: ReturnType<typeof mockService> = mockService([
518
+ makeResource({ id: resourceId(1), monitorId: MONITOR_ID }),
519
+ ]);
520
+
521
+ await onBeforeUpdate(
522
+ updateBy({
523
+ id: resourceId(1),
524
+ monitorId: MONITOR_ID,
525
+ displayName: "Renamed",
526
+ }),
527
+ );
528
+
529
+ // One read: the resource being updated. No duplicate lookup after it.
530
+ expect(mocks.findOneBy).toHaveBeenCalledTimes(1);
531
+ expect(
532
+ (mocks.findOneBy.mock.calls[0] as any)[0].query.monitorId,
533
+ ).toBeUndefined();
534
+ });
535
+
536
+ it("still lets both rows of a pre-existing duplicate be renamed", async () => {
537
+ mockService([
538
+ makeResource({ id: resourceId(1), monitorId: MONITOR_ID }),
539
+ makeResource({ id: resourceId(2), monitorId: MONITOR_ID }),
540
+ ]);
541
+
542
+ await expect(
543
+ onBeforeUpdate(
544
+ updateBy({
545
+ id: resourceId(1),
546
+ monitor: MONITOR_ID,
547
+ displayName: "Renamed",
548
+ }),
549
+ ),
550
+ ).resolves.toBeDefined();
551
+
552
+ await expect(
553
+ onBeforeUpdate(
554
+ updateBy({
555
+ id: resourceId(2),
556
+ monitor: MONITOR_ID,
557
+ displayName: "Renamed too",
558
+ }),
559
+ ),
560
+ ).resolves.toBeDefined();
561
+ });
562
+
563
+ it("looks for the duplicate by status page and the new monitor", async () => {
564
+ const mocks: ReturnType<typeof mockService> = mockService([
565
+ makeResource({ id: resourceId(1), monitorId: OTHER_MONITOR_ID }),
566
+ ]);
567
+
568
+ await onBeforeUpdate(
569
+ updateBy({ id: resourceId(1), monitor: MONITOR_ID }),
570
+ );
571
+
572
+ const duplicateLookup: any = (mocks.findOneBy.mock.calls[1] as any)[0];
573
+ expect(duplicateLookup.query.statusPageId).toEqual(STATUS_PAGE_ID);
574
+ expect(duplicateLookup.query.monitorId?.toString()).toBe(
575
+ MONITOR_ID.toString(),
576
+ );
577
+ expect(getExcludedId(duplicateLookup.query._id)).toBe(
578
+ resourceId(1).toString(),
579
+ );
580
+ });
581
+
582
+ it("resolves the monitor from the relation the edit form posts", async () => {
583
+ mockService([
584
+ makeResource({ id: resourceId(1), monitorId: OTHER_MONITOR_ID }),
585
+ makeResource({ id: resourceId(2), monitorId: MONITOR_ID }),
586
+ ]);
587
+
588
+ await expect(
589
+ onBeforeUpdate(updateBy({ id: resourceId(1), monitor: MONITOR_ID })),
590
+ ).rejects.toThrow(new BadDataException(DUPLICATE_MONITOR_MESSAGE));
591
+ });
592
+
593
+ it("refuses pointing a resource at a monitor group the page already lists", async () => {
594
+ mockService([
595
+ makeResource({ id: resourceId(1), monitorId: MONITOR_ID }),
596
+ makeResource({ id: resourceId(2), monitorGroupId: MONITOR_GROUP_ID }),
597
+ ]);
598
+
599
+ await expect(
600
+ onBeforeUpdate(
601
+ updateBy({ id: resourceId(1), monitorGroupId: MONITOR_GROUP_ID }),
602
+ ),
603
+ ).rejects.toThrow(new BadDataException(DUPLICATE_MONITOR_GROUP_MESSAGE));
604
+ });
605
+
606
+ it("costs nothing when the update does not touch the monitor", async () => {
607
+ const mocks: ReturnType<typeof mockService> = mockService([
608
+ makeResource({ id: resourceId(1), monitorId: MONITOR_ID }),
609
+ ]);
610
+
611
+ await expect(
612
+ onBeforeUpdate(updateBy({ id: resourceId(1), displayName: "Renamed" })),
613
+ ).resolves.toBeDefined();
614
+
615
+ expect(mocks.findOneBy).not.toHaveBeenCalled();
616
+ });
617
+
618
+ it("skips the check when the update does not name a single resource", async () => {
619
+ const mocks: ReturnType<typeof mockService> = mockService([
620
+ makeResource({ id: resourceId(1), monitorId: MONITOR_ID }),
621
+ ]);
622
+
623
+ await expect(
624
+ onBeforeUpdate(updateBy({ monitorId: MONITOR_ID })),
625
+ ).resolves.toBeDefined();
626
+
627
+ expect(mocks.findOneBy).not.toHaveBeenCalled();
628
+ });
629
+
630
+ it("does nothing when the resource being updated has gone", async () => {
631
+ mockService([]);
632
+
633
+ await expect(
634
+ onBeforeUpdate(updateBy({ id: resourceId(1), monitorId: MONITOR_ID })),
635
+ ).resolves.toBeDefined();
636
+ });
637
+ });
638
+
639
+ describe("isResourceAlreadyOnStatusPage", () => {
640
+ it("reports a monitor that is on the page", async () => {
641
+ mockService([makeResource({ id: resourceId(1), monitorId: MONITOR_ID })]);
642
+
643
+ await expect(
644
+ StatusPageResourceService.isResourceAlreadyOnStatusPage({
645
+ statusPageId: STATUS_PAGE_ID,
646
+ monitorId: MONITOR_ID,
647
+ }),
648
+ ).resolves.toBe(true);
649
+ });
650
+
651
+ it("reports a monitor that is not on the page", async () => {
652
+ mockService([makeResource({ id: resourceId(1), monitorId: MONITOR_ID })]);
653
+
654
+ await expect(
655
+ StatusPageResourceService.isResourceAlreadyOnStatusPage({
656
+ statusPageId: STATUS_PAGE_ID,
657
+ monitorId: OTHER_MONITOR_ID,
658
+ }),
659
+ ).resolves.toBe(false);
660
+ });
661
+
662
+ it("does not count the resource it was told to exclude", async () => {
663
+ mockService([makeResource({ id: resourceId(1), monitorId: MONITOR_ID })]);
664
+
665
+ await expect(
666
+ StatusPageResourceService.isResourceAlreadyOnStatusPage({
667
+ statusPageId: STATUS_PAGE_ID,
668
+ monitorId: MONITOR_ID,
669
+ excludeResourceId: resourceId(1),
670
+ }),
671
+ ).resolves.toBe(false);
672
+ });
673
+
674
+ it("still reports a duplicate held by some other resource", async () => {
675
+ mockService([
676
+ makeResource({ id: resourceId(1), monitorId: MONITOR_ID }),
677
+ makeResource({ id: resourceId(2), monitorId: MONITOR_ID }),
678
+ ]);
679
+
680
+ await expect(
681
+ StatusPageResourceService.isResourceAlreadyOnStatusPage({
682
+ statusPageId: STATUS_PAGE_ID,
683
+ monitorId: MONITOR_ID,
684
+ excludeResourceId: resourceId(1),
685
+ }),
686
+ ).resolves.toBe(true);
687
+ });
688
+
689
+ it("does not read anything when no monitor or monitor group is named", async () => {
690
+ const mocks: ReturnType<typeof mockService> = mockService([
691
+ makeResource({ id: resourceId(1), monitorId: MONITOR_ID }),
692
+ ]);
693
+
694
+ await expect(
695
+ StatusPageResourceService.isResourceAlreadyOnStatusPage({
696
+ statusPageId: STATUS_PAGE_ID,
697
+ }),
698
+ ).resolves.toBe(false);
699
+
700
+ expect(mocks.findOneBy).not.toHaveBeenCalled();
701
+ });
702
+ });
703
+ });