@oneuptime/common 12.0.23 → 12.0.25

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 (323) hide show
  1. package/Models/AnalyticsModels/Span.ts +101 -0
  2. package/Models/DatabaseModels/DetectionRule.ts +55 -0
  3. package/Models/DatabaseModels/NetworkDeviceAutoImportRule.ts +27 -7
  4. package/Models/DatabaseModels/NetworkDeviceDiscoveryScan.ts +67 -0
  5. package/Models/DatabaseModels/Project.ts +68 -0
  6. package/Models/DatabaseModels/User.ts +68 -0
  7. package/Server/API/BaseAPI.ts +0 -24
  8. package/Server/API/SlackAPI.ts +0 -2
  9. package/Server/API/TelemetryAPI.ts +29 -0
  10. package/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.ts +28 -0
  11. package/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.ts +49 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.ts +33 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  14. package/Server/Middleware/GzipRequestBody.ts +311 -0
  15. package/Server/Middleware/MultipartFormData.ts +87 -3
  16. package/Server/Middleware/SlackAuthorization.ts +96 -18
  17. package/Server/Services/DetectionRuleService.ts +73 -6
  18. package/Server/Services/Index.ts +205 -0
  19. package/Server/Services/NetworkDeviceDiscoveryScanService.ts +59 -0
  20. package/Server/Services/NetworkDeviceService.ts +148 -0
  21. package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +65 -0
  22. package/Server/Services/OnCallDutyPolicyFeedService.ts +1 -1
  23. package/Server/Services/ProjectService.ts +29 -19
  24. package/Server/Services/SecurityEventService.ts +90 -13
  25. package/Server/Services/StatusPageResourceService.ts +223 -0
  26. package/Server/Services/TelemetryAttributeService.ts +17 -0
  27. package/Server/Services/UserService.ts +4 -6
  28. package/Server/Types/Markdown.ts +161 -67
  29. package/Server/Utils/AI/Toolbox/OnCallTools.ts +12 -4
  30. package/Server/Utils/AnalyticsDatabase/Statement.ts +13 -1
  31. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +297 -6
  32. package/Server/Utils/Attribution.ts +4 -0
  33. package/Server/Utils/Marketing/MarketingEventUtil.ts +45 -7
  34. package/Server/Utils/SecurityEvent/DetectionRuleEvaluator.ts +55 -3
  35. package/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.ts +21 -0
  36. package/Server/Utils/StartServer.ts +12 -32
  37. package/Server/Utils/Telemetry/LlmCostBudgetEvaluator.ts +101 -4
  38. package/Server/Utils/Telemetry/LlmMetricSpend.ts +181 -0
  39. package/Server/Utils/Telemetry/LlmSpan.ts +46 -0
  40. package/Server/Utils/Workspace/Slack/Actions/Auth.ts +0 -12
  41. package/Server/Views/Partials/AnalyticsConsent.ejs +159 -102
  42. package/Server/Views/Partials/Head.ejs +2 -1
  43. package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +776 -38
  44. package/Tests/App/Dashboard/CorrelateFilterBuilder.test.tsx +359 -0
  45. package/Tests/App/Dashboard/CorrelateGraph.test.tsx +865 -0
  46. package/Tests/App/Dashboard/CorrelationGraph.test.ts +346 -0
  47. package/Tests/App/Dashboard/CriteriaFilterConditionDropdown.test.tsx +256 -0
  48. package/Tests/App/Dashboard/CriteriaFilterDefaults.test.ts +738 -0
  49. package/Tests/App/Dashboard/CriteriaFilterMonitorTypeChange.test.ts +1122 -0
  50. package/Tests/App/Dashboard/DiscoveryScanWizardValidation.test.tsx +283 -0
  51. package/Tests/App/Dashboard/ExternalStatusPageCriteriaFilter.test.ts +407 -0
  52. package/Tests/App/Dashboard/LlmCallsTableIdentity.test.tsx +322 -0
  53. package/Tests/App/Dashboard/LlmOverview.test.tsx +335 -0
  54. package/Tests/App/Dashboard/LlmSpanDisplay.test.ts +391 -0
  55. package/Tests/App/Dashboard/LlmUsageBreakdown.test.tsx +1007 -0
  56. package/Tests/App/Dashboard/MetricSeriesInvestigateMenu.test.tsx +5 -14
  57. package/Tests/App/Dashboard/MonitorCriteriaActionToggles.test.tsx +426 -0
  58. package/Tests/App/Dashboard/MonitorStepsCriteriaAlignmentWiring.test.tsx +352 -0
  59. package/Tests/App/Dashboard/OnCallLayerCardOverrides.test.tsx +278 -0
  60. package/Tests/App/Dashboard/OnCallLayerShiftPreviewOverrides.test.ts +237 -0
  61. package/Tests/App/Dashboard/OnCallReadinessSurfaces.test.tsx +2 -2
  62. package/Tests/App/Dashboard/OnCallSchedulePreviewOverrides.test.tsx +460 -0
  63. package/Tests/App/Dashboard/SecurityEventAttributeUtil.test.ts +171 -0
  64. package/Tests/App/Dashboard/SecurityEventCorrelation.test.ts +857 -0
  65. package/Tests/App/Dashboard/SecurityEventDetailObservables.test.tsx +194 -0
  66. package/Tests/App/Dashboard/SecurityEventsDetectionRulesPage.test.tsx +124 -0
  67. package/Tests/App/Dashboard/SecurityEventsMonitorStepForm.test.tsx +79 -19
  68. package/Tests/App/Dashboard/SecurityEventsTableColumns.test.tsx +382 -0
  69. package/Tests/App/StatusPage/BulkAddStatusPageMonitors.test.ts +216 -0
  70. package/Tests/App/StatusPage/BulkAddStatusPageMonitorsModal.test.tsx +590 -11
  71. package/Tests/App/StatusPage/StatusPageResourceExplorer.test.tsx +757 -0
  72. package/Tests/Models/DiscoveryScanNameColumn.test.ts +293 -0
  73. package/Tests/Server/API/BaseAPI.test.ts +41 -0
  74. package/Tests/Server/API/BaseAPIUpdatePayloadValidation.test.ts +9 -16
  75. package/Tests/Server/API/SecurityEventAttributesAPI.test.ts +564 -0
  76. package/Tests/Server/Infrastructure/Postgres/SchemaMigrationsOrdering.test.ts +120 -4
  77. package/Tests/Server/Middleware/GzipRequestBody.test.ts +725 -0
  78. package/Tests/Server/Middleware/MultipartFormData.test.ts +298 -0
  79. package/Tests/Server/Middleware/SlackAuthorization.test.ts +262 -5
  80. package/Tests/Server/Services/AddDetectionRuleDistinctCountColumnsMigration.test.ts +311 -0
  81. package/Tests/Server/Services/DetectionRuleService.test.ts +134 -2
  82. package/Tests/Server/Services/DiscoveryScanClaimHookFreeSafety.test.ts +7 -1
  83. package/Tests/Server/Services/FeedRetentionConsistency.test.ts +131 -0
  84. package/Tests/Server/Services/NetworkDeviceDiscoveryScanService.test.ts +194 -0
  85. package/Tests/Server/Services/NetworkDeviceMonitorStatusStamp.test.ts +800 -0
  86. package/Tests/Server/Services/OnCallScheduleAttachRosterRefresh.test.ts +244 -0
  87. package/Tests/Server/Services/SecurityEventService.test.ts +376 -0
  88. package/Tests/Server/Services/StatusPageResourceDuplicates.test.ts +703 -0
  89. package/Tests/Server/Services/TelemetryAttributeServiceSecurityEvent.test.ts +256 -0
  90. package/Tests/Server/Types/Workflow/Components/ComponentRegistryParity.test.ts +417 -0
  91. package/Tests/Server/Utils/AnalyticsDatabase/StatementGeneratorWhereOperators.test.ts +677 -0
  92. package/Tests/Server/Utils/Marketing/MarketingEventUtil.test.ts +43 -0
  93. package/Tests/Server/Utils/Monitor/Criteria/ProfileMonitorCriteria.test.ts +15 -11
  94. package/Tests/Server/Utils/SecurityEvent/DetectionRuleEvaluator.test.ts +369 -8
  95. package/Tests/Server/Utils/SecurityEvent/SigmaClickhouseCompiler.test.ts +35 -0
  96. package/Tests/Server/Utils/Telemetry/LlmCostBudgetEvaluator.test.ts +387 -4
  97. package/Tests/Server/Utils/Telemetry/LlmMetricSpend.test.ts +386 -0
  98. package/Tests/Server/Utils/Telemetry/LlmSpan.test.ts +804 -0
  99. package/Tests/Server/Views/AnalyticsConsentPartial.test.ts +48 -5
  100. package/Tests/Types/Code/YamlSyntax.test.ts +396 -0
  101. package/Tests/Types/Monitor/MetricAndProfileDefaultCriteria.test.ts +957 -0
  102. package/Tests/Types/Monitor/MonitorCriteriaActionValidation.test.ts +598 -0
  103. package/Tests/Types/Monitor/MonitorCriteriaInstance.test.ts +49 -0
  104. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationNotificationMode.test.ts +6 -6
  105. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.test.ts +4 -4
  106. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationUtil.test.ts +4 -3
  107. package/Tests/Types/Telemetry/LlmMetricConventions.test.ts +584 -0
  108. package/Tests/UI/Components/AiInvestigationSettingsCard.test.tsx +0 -12
  109. package/Tests/UI/Components/CardModelDetailEdit.test.tsx +1 -17
  110. package/Tests/UI/Components/CodeEditor.test.tsx +474 -10
  111. package/Tests/UI/Components/Detail/YamlField.test.tsx +105 -0
  112. package/Tests/UI/Components/EntityDropdownLabelsBulkAdd.test.tsx +319 -0
  113. package/Tests/UI/Components/Forms/FormFieldYaml.test.tsx +231 -0
  114. package/Tests/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.test.ts +1 -0
  115. package/Tests/UI/Components/Forms/ValidationYAML.test.ts +234 -0
  116. package/Tests/UI/Components/ModelTable/AttributeColumns.test.tsx +573 -0
  117. package/Tests/UI/Components/ModelTable/BaseModelTableAttributeColumns.test.tsx +824 -0
  118. package/Tests/UI/Components/ModelTable/ColumnCustomizationModalAddableColumns.test.tsx +707 -0
  119. package/Tests/UI/Components/SideOver.test.tsx +1 -1
  120. package/Tests/UI/Components/StatusPage/ResourceGridSelection.test.tsx +294 -0
  121. package/Tests/UI/Components/StatusPage/ResourceListSelection.test.tsx +436 -0
  122. package/Tests/UI/Components/TableBulkCsvExport.test.tsx +6 -2
  123. package/Tests/UI/Components/Toggle.test.tsx +25 -0
  124. package/Tests/UI/Components/YamlEditor.test.tsx +847 -0
  125. package/Tests/Utils/NetworkDevice/DeviceReachabilityUtil.test.ts +252 -0
  126. package/Tests/Utils/NetworkDiscovery/DiscoveredHostUtil.test.ts +221 -0
  127. package/Tests/Utils/NetworkDiscovery/ScanNameUtil.test.ts +312 -0
  128. package/Tests/Utils/StatusPage/ResourceExplorer.test.ts +468 -0
  129. package/Tests/Utils/Telemetry/LlmMetricQuery.test.ts +770 -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/SecurityEvent/DetectionFindingConstants.ts +11 -0
  138. package/Types/Telemetry/LlmConventions.ts +255 -0
  139. package/Types/Telemetry/LlmMetricConventions.ts +329 -0
  140. package/UI/Components/AutocompleteTextInput/AutocompleteTextInput.tsx +3 -0
  141. package/UI/Components/BulkUpdate/BulkUpdateForm.tsx +10 -1
  142. package/UI/Components/CodeEditor/CodeEditor.tsx +241 -79
  143. package/UI/Components/CodeEditor/YamlEditor.tsx +369 -0
  144. package/UI/Components/Detail/Detail.tsx +10 -0
  145. package/UI/Components/EntityDropdown/EntityDropdown.tsx +33 -0
  146. package/UI/Components/Forms/Fields/FormField.tsx +31 -0
  147. package/UI/Components/Forms/Types/Field.ts +8 -0
  148. package/UI/Components/Forms/Types/FormFieldSchemaType.ts +1 -0
  149. package/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.ts +2 -0
  150. package/UI/Components/Forms/Validation.ts +46 -0
  151. package/UI/Components/Input/Input.tsx +3 -0
  152. package/UI/Components/ModelTable/AttributeColumns.tsx +321 -0
  153. package/UI/Components/ModelTable/BaseModelTable.tsx +263 -14
  154. package/UI/Components/ModelTable/Column.ts +8 -0
  155. package/UI/Components/ModelTable/ColumnCustomizationModal.tsx +310 -0
  156. package/UI/Components/ModelTable/ColumnPreference.ts +13 -1
  157. package/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.tsx +2 -2
  158. package/UI/Components/SideOver/SideOver.tsx +5 -5
  159. package/UI/Components/StatusPage/ResourceGrid.tsx +82 -2
  160. package/UI/Components/StatusPage/ResourceList.tsx +197 -38
  161. package/UI/Components/Toggle/Toggle.tsx +9 -1
  162. package/UI/Components/Types/FieldType.ts +1 -0
  163. package/Utils/NetworkDevice/DeviceReachabilityUtil.ts +73 -1
  164. package/Utils/NetworkDiscovery/ScanNameUtil.ts +181 -0
  165. package/Utils/StatusPage/ResourceExplorer.ts +258 -0
  166. package/Utils/Telemetry/LlmMetricQuery.ts +287 -0
  167. package/build/dist/Models/AnalyticsModels/Span.js +89 -0
  168. package/build/dist/Models/AnalyticsModels/Span.js.map +1 -1
  169. package/build/dist/Models/DatabaseModels/DetectionRule.js +57 -0
  170. package/build/dist/Models/DatabaseModels/DetectionRule.js.map +1 -1
  171. package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js +27 -7
  172. package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js.map +1 -1
  173. package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js +69 -0
  174. package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js.map +1 -1
  175. package/build/dist/Models/DatabaseModels/Project.js +76 -0
  176. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  177. package/build/dist/Models/DatabaseModels/User.js +76 -0
  178. package/build/dist/Models/DatabaseModels/User.js.map +1 -1
  179. package/build/dist/Server/API/BaseAPI.js +3 -19
  180. package/build/dist/Server/API/BaseAPI.js.map +1 -1
  181. package/build/dist/Server/API/SlackAPI.js +0 -2
  182. package/build/dist/Server/API/SlackAPI.js.map +1 -1
  183. package/build/dist/Server/API/TelemetryAPI.js +17 -0
  184. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  185. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.js +21 -0
  186. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.js.map +1 -0
  187. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.js +42 -0
  188. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.js.map +1 -0
  189. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.js +22 -0
  190. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.js.map +1 -0
  191. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  192. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  193. package/build/dist/Server/Middleware/GzipRequestBody.js +224 -0
  194. package/build/dist/Server/Middleware/GzipRequestBody.js.map +1 -0
  195. package/build/dist/Server/Middleware/MultipartFormData.js +71 -2
  196. package/build/dist/Server/Middleware/MultipartFormData.js.map +1 -1
  197. package/build/dist/Server/Middleware/SlackAuthorization.js +58 -7
  198. package/build/dist/Server/Middleware/SlackAuthorization.js.map +1 -1
  199. package/build/dist/Server/Services/DetectionRuleService.js +49 -4
  200. package/build/dist/Server/Services/DetectionRuleService.js.map +1 -1
  201. package/build/dist/Server/Services/Index.js +204 -0
  202. package/build/dist/Server/Services/Index.js.map +1 -1
  203. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js +50 -0
  204. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js.map +1 -1
  205. package/build/dist/Server/Services/NetworkDeviceService.js +124 -0
  206. package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
  207. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +68 -13
  208. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
  209. package/build/dist/Server/Services/OnCallDutyPolicyFeedService.js +1 -1
  210. package/build/dist/Server/Services/OnCallDutyPolicyFeedService.js.map +1 -1
  211. package/build/dist/Server/Services/ProjectService.js +25 -45
  212. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  213. package/build/dist/Server/Services/SecurityEventService.js +57 -5
  214. package/build/dist/Server/Services/SecurityEventService.js.map +1 -1
  215. package/build/dist/Server/Services/StatusPageResourceService.js +140 -0
  216. package/build/dist/Server/Services/StatusPageResourceService.js.map +1 -1
  217. package/build/dist/Server/Services/TelemetryAttributeService.js +17 -0
  218. package/build/dist/Server/Services/TelemetryAttributeService.js.map +1 -1
  219. package/build/dist/Server/Services/UserService.js +2 -12
  220. package/build/dist/Server/Services/UserService.js.map +1 -1
  221. package/build/dist/Server/Types/Markdown.js +147 -61
  222. package/build/dist/Server/Types/Markdown.js.map +1 -1
  223. package/build/dist/Server/Utils/AI/Toolbox/OnCallTools.js +12 -4
  224. package/build/dist/Server/Utils/AI/Toolbox/OnCallTools.js.map +1 -1
  225. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js +12 -1
  226. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js.map +1 -1
  227. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +246 -9
  228. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  229. package/build/dist/Server/Utils/Attribution.js.map +1 -1
  230. package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js +27 -7
  231. package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js.map +1 -1
  232. package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js +42 -7
  233. package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js.map +1 -1
  234. package/build/dist/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.js +18 -0
  235. package/build/dist/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.js.map +1 -1
  236. package/build/dist/Server/Utils/StartServer.js +12 -21
  237. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  238. package/build/dist/Server/Utils/Telemetry/LlmCostBudgetEvaluator.js +76 -5
  239. package/build/dist/Server/Utils/Telemetry/LlmCostBudgetEvaluator.js.map +1 -1
  240. package/build/dist/Server/Utils/Telemetry/LlmMetricSpend.js +151 -0
  241. package/build/dist/Server/Utils/Telemetry/LlmMetricSpend.js.map +1 -0
  242. package/build/dist/Server/Utils/Telemetry/LlmSpan.js +24 -1
  243. package/build/dist/Server/Utils/Telemetry/LlmSpan.js.map +1 -1
  244. package/build/dist/Server/Utils/Workspace/Slack/Actions/Auth.js +0 -10
  245. package/build/dist/Server/Utils/Workspace/Slack/Actions/Auth.js.map +1 -1
  246. package/build/dist/Types/Code/YamlSyntax.js +212 -0
  247. package/build/dist/Types/Code/YamlSyntax.js.map +1 -0
  248. package/build/dist/Types/Exception/ExceptionCode.js +1 -0
  249. package/build/dist/Types/Exception/ExceptionCode.js.map +1 -1
  250. package/build/dist/Types/Exception/PayloadTooLargeException.js +8 -0
  251. package/build/dist/Types/Exception/PayloadTooLargeException.js.map +1 -0
  252. package/build/dist/Types/Marketing/Attribution.js +10 -0
  253. package/build/dist/Types/Marketing/Attribution.js.map +1 -1
  254. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js +112 -51
  255. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js.map +1 -1
  256. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.js +2 -1
  257. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.js.map +1 -1
  258. package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js +9 -0
  259. package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js.map +1 -1
  260. package/build/dist/Types/Telemetry/LlmConventions.js +236 -0
  261. package/build/dist/Types/Telemetry/LlmConventions.js.map +1 -1
  262. package/build/dist/Types/Telemetry/LlmMetricConventions.js +290 -0
  263. package/build/dist/Types/Telemetry/LlmMetricConventions.js.map +1 -0
  264. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js +1 -1
  265. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js.map +1 -1
  266. package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js +5 -2
  267. package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js.map +1 -1
  268. package/build/dist/UI/Components/CodeEditor/CodeEditor.js +206 -76
  269. package/build/dist/UI/Components/CodeEditor/CodeEditor.js.map +1 -1
  270. package/build/dist/UI/Components/CodeEditor/YamlEditor.js +201 -0
  271. package/build/dist/UI/Components/CodeEditor/YamlEditor.js.map +1 -0
  272. package/build/dist/UI/Components/Detail/Detail.js +9 -0
  273. package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
  274. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js +16 -1
  275. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js.map +1 -1
  276. package/build/dist/UI/Components/Forms/Fields/FormField.js +14 -1
  277. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  278. package/build/dist/UI/Components/Forms/Types/Field.js.map +1 -1
  279. package/build/dist/UI/Components/Forms/Types/FormFieldSchemaType.js +1 -0
  280. package/build/dist/UI/Components/Forms/Types/FormFieldSchemaType.js.map +1 -1
  281. package/build/dist/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.js +2 -0
  282. package/build/dist/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.js.map +1 -1
  283. package/build/dist/UI/Components/Forms/Validation.js +27 -0
  284. package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
  285. package/build/dist/UI/Components/Input/Input.js +1 -1
  286. package/build/dist/UI/Components/Input/Input.js.map +1 -1
  287. package/build/dist/UI/Components/ModelTable/AttributeColumns.js +159 -0
  288. package/build/dist/UI/Components/ModelTable/AttributeColumns.js.map +1 -0
  289. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +179 -17
  290. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  291. package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js +133 -2
  292. package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js.map +1 -1
  293. package/build/dist/UI/Components/ModelTable/ColumnPreference.js +7 -1
  294. package/build/dist/UI/Components/ModelTable/ColumnPreference.js.map +1 -1
  295. package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.js +2 -2
  296. package/build/dist/UI/Components/SideOver/SideOver.js +5 -5
  297. package/build/dist/UI/Components/StatusPage/ResourceGrid.js +28 -2
  298. package/build/dist/UI/Components/StatusPage/ResourceGrid.js.map +1 -1
  299. package/build/dist/UI/Components/StatusPage/ResourceList.js +87 -20
  300. package/build/dist/UI/Components/StatusPage/ResourceList.js.map +1 -1
  301. package/build/dist/UI/Components/Toggle/Toggle.js +9 -1
  302. package/build/dist/UI/Components/Toggle/Toggle.js.map +1 -1
  303. package/build/dist/UI/Components/Types/FieldType.js +1 -0
  304. package/build/dist/UI/Components/Types/FieldType.js.map +1 -1
  305. package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js +37 -1
  306. package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js.map +1 -1
  307. package/build/dist/Utils/NetworkDiscovery/ScanNameUtil.js +120 -0
  308. package/build/dist/Utils/NetworkDiscovery/ScanNameUtil.js.map +1 -0
  309. package/build/dist/Utils/StatusPage/ResourceExplorer.js +171 -0
  310. package/build/dist/Utils/StatusPage/ResourceExplorer.js.map +1 -1
  311. package/build/dist/Utils/Telemetry/LlmMetricQuery.js +175 -0
  312. package/build/dist/Utils/Telemetry/LlmMetricQuery.js.map +1 -0
  313. package/jest.config.json +2 -0
  314. package/package.json +1 -1
  315. package/tsconfig.json +2 -1
  316. package/Tests/UI/Components/Toast.test.tsx +0 -197
  317. package/Tests/UI/Components/ToastStacking.test.tsx +0 -198
  318. package/UI/Components/Toast/Toast.tsx +0 -189
  319. package/UI/Components/Toast/ToastInit.tsx +0 -110
  320. package/build/dist/UI/Components/Toast/Toast.js +0 -123
  321. package/build/dist/UI/Components/Toast/Toast.js.map +0 -1
  322. package/build/dist/UI/Components/Toast/ToastInit.js +0 -53
  323. package/build/dist/UI/Components/Toast/ToastInit.js.map +0 -1
@@ -0,0 +1,382 @@
1
+ import "@testing-library/jest-dom";
2
+ import { cleanup, render } from "@testing-library/react";
3
+ import { afterEach, describe, expect, test } from "@jest/globals";
4
+
5
+ import securityEventColumns from "../../../../App/FeatureSet/Dashboard/src/Components/SecurityEvents/SecurityEventsTableColumns";
6
+ import Column from "../../../UI/Components/ModelTable/Column";
7
+ import { getColumnIds } from "../../../UI/Components/ModelTable/ColumnPreference";
8
+ import { getSelectFromColumns } from "../../../UI/Components/ModelTable/SelectFromColumns";
9
+ import FieldType from "../../../UI/Components/Types/FieldType";
10
+ import SecurityEvent from "../../../Models/AnalyticsModels/SecurityEvent";
11
+ import OcsfSeverity from "../../../Types/SecurityEvent/OcsfSeverity";
12
+ import Select from "../../../Types/BaseDatabase/Select";
13
+ import { JSONObject } from "../../../Types/JSON";
14
+
15
+ /*
16
+ * ---------------------------------------------------------------------------
17
+ * The security events table's column set
18
+ * ---------------------------------------------------------------------------
19
+ *
20
+ * The table used to offer seven columns while the detail panel showed twelve
21
+ * fields, so answering "which vendor produced all of these?" meant opening
22
+ * every event one at a time (OneUptime/oneuptime#3399). Every field the detail
23
+ * panel shows is now a column; the seven that were the whole table are still
24
+ * the seven that show by default, and the rest are one click away in the
25
+ * picker.
26
+ *
27
+ * Three classes of mistake are invisible on screen and each is pinned below:
28
+ *
29
+ * - a field name that is not a column on the model. getSelectFromColumns
30
+ * throws a BadDataException for the primary field, which blanks the entire
31
+ * table rather than one cell.
32
+ * - a hidden column marked isNotCustomizable, which would put it in neither
33
+ * the table nor the picker: unreachable.
34
+ * - a column that silently changes the DEFAULT layout, which would reshuffle
35
+ * the table for every viewer who has never opened the picker.
36
+ */
37
+
38
+ afterEach(() => {
39
+ cleanup();
40
+ });
41
+
42
+ type TitlesOfFunction = (
43
+ columns: Array<Column<SecurityEvent>>,
44
+ ) => Array<string>;
45
+
46
+ const titlesOf: TitlesOfFunction = (
47
+ columns: Array<Column<SecurityEvent>>,
48
+ ): Array<string> => {
49
+ return columns.map((column: Column<SecurityEvent>) => {
50
+ return column.title;
51
+ });
52
+ };
53
+
54
+ const visibleColumns: Array<Column<SecurityEvent>> =
55
+ securityEventColumns.filter((column: Column<SecurityEvent>) => {
56
+ return !column.isHiddenByDefault;
57
+ });
58
+
59
+ type ColumnByTitleFunction = (title: string) => Column<SecurityEvent>;
60
+
61
+ const columnByTitle: ColumnByTitleFunction = (
62
+ title: string,
63
+ ): Column<SecurityEvent> => {
64
+ const column: Column<SecurityEvent> | undefined = securityEventColumns.find(
65
+ (candidate: Column<SecurityEvent>) => {
66
+ return candidate.title === title;
67
+ },
68
+ );
69
+
70
+ if (!column) {
71
+ throw new Error(`No security event column titled "${title}"`);
72
+ }
73
+
74
+ return column;
75
+ };
76
+
77
+ type MakeEventFunction = (data: JSONObject) => SecurityEvent;
78
+
79
+ const makeEvent: MakeEventFunction = (data: JSONObject): SecurityEvent => {
80
+ const event: SecurityEvent = new SecurityEvent();
81
+
82
+ for (const key of Object.keys(data)) {
83
+ (event as unknown as JSONObject)[key] = data[key];
84
+ }
85
+
86
+ return event;
87
+ };
88
+
89
+ describe("SecurityEventsTable columns - the default layout", () => {
90
+ /*
91
+ * These seven, in this order, are what the table showed before the rest were
92
+ * added. Anything new has to ship hidden, or every existing viewer's table
93
+ * silently rearranges itself.
94
+ */
95
+ test("shows exactly the seven columns it always did, in the same order", () => {
96
+ expect(titlesOf(visibleColumns)).toEqual([
97
+ "Time",
98
+ "Severity",
99
+ "Event Class",
100
+ "Message",
101
+ "Principal User",
102
+ "Principal Host",
103
+ "Vendor",
104
+ ]);
105
+ });
106
+
107
+ test("everything else ships hidden", () => {
108
+ const hidden: Array<Column<SecurityEvent>> = securityEventColumns.filter(
109
+ (column: Column<SecurityEvent>) => {
110
+ return Boolean(column.isHiddenByDefault);
111
+ },
112
+ );
113
+
114
+ expect(hidden.length).toBe(securityEventColumns.length - 7);
115
+ expect(hidden.length).toBeGreaterThan(0);
116
+ });
117
+ });
118
+
119
+ describe("SecurityEventsTable columns - coverage of the detail panel", () => {
120
+ /*
121
+ * The issue's actual complaint: these are the fields the detail panel showed
122
+ * but the picker did not offer. Named one by one rather than counted, so a
123
+ * regression says which one went missing.
124
+ */
125
+ const requestedInIssue: Array<string> = [
126
+ "Category",
127
+ "Activity",
128
+ "Vendor",
129
+ "Product",
130
+ "Rule Name",
131
+ "Observables",
132
+ "Event UID",
133
+ ];
134
+
135
+ test.each(requestedInIssue)("offers a %s column", (title: string) => {
136
+ expect(titlesOf(securityEventColumns)).toContain(title);
137
+ });
138
+
139
+ /*
140
+ * The rest of what the detail panel renders. Parity is the point: anything
141
+ * worth reading on one event is worth scanning down the table.
142
+ */
143
+ const restOfTheDetailPanel: Array<string> = [
144
+ "Time",
145
+ "Severity",
146
+ "Event Class",
147
+ "Status",
148
+ "Message",
149
+ "Rule ID",
150
+ "MITRE Tactics",
151
+ "MITRE Techniques",
152
+ "Principal User",
153
+ "Principal Host",
154
+ "Principal IP",
155
+ "Principal Process",
156
+ "Target User",
157
+ "Target Host",
158
+ "Target IP",
159
+ "Target Port",
160
+ "Target Resource",
161
+ ];
162
+
163
+ test.each(restOfTheDetailPanel)(
164
+ "also offers a %s column",
165
+ (title: string) => {
166
+ expect(titlesOf(securityEventColumns)).toContain(title);
167
+ },
168
+ );
169
+
170
+ test("the picker offers materially more than the seven it used to", () => {
171
+ expect(securityEventColumns.length).toBe(
172
+ requestedInIssue.length + restOfTheDetailPanel.length,
173
+ );
174
+ });
175
+ });
176
+
177
+ describe("SecurityEventsTable columns - shape", () => {
178
+ test("every declared field is a real column on the model", () => {
179
+ const model: SecurityEvent = new SecurityEvent();
180
+
181
+ for (const column of securityEventColumns) {
182
+ for (const field of Object.keys(column.field as JSONObject)) {
183
+ expect({
184
+ title: column.title,
185
+ field,
186
+ exists: model.hasColumn(field),
187
+ }).toEqual({ title: column.title, field, exists: true });
188
+ }
189
+ }
190
+ });
191
+
192
+ /*
193
+ * The real check that the above is enough: this is the call the table makes
194
+ * on every fetch, and it throws rather than degrading.
195
+ */
196
+ test("building the API select does not throw", () => {
197
+ const select: Select<SecurityEvent> = getSelectFromColumns<SecurityEvent>({
198
+ columns: securityEventColumns,
199
+ model: new SecurityEvent(),
200
+ });
201
+
202
+ expect((select as JSONObject)["categoryName"]).toBe(true);
203
+ expect((select as JSONObject)["observables"]).toBe(true);
204
+ expect((select as JSONObject)["eventUid"]).toBe(true);
205
+ });
206
+
207
+ test("column ids are unique, so a stored layout cannot confuse two of them", () => {
208
+ const ids: Array<string> =
209
+ getColumnIds<SecurityEvent>(securityEventColumns);
210
+
211
+ expect(new Set(ids).size).toBe(ids.length);
212
+ });
213
+
214
+ test("titles are unique, so the picker's search is unambiguous", () => {
215
+ const titles: Array<string> = titlesOf(securityEventColumns);
216
+
217
+ expect(new Set(titles).size).toBe(titles.length);
218
+ });
219
+
220
+ /*
221
+ * A hidden AND non-customizable column would be in neither the table nor the
222
+ * picker.
223
+ */
224
+ test("no column is both hidden by default and kept out of the picker", () => {
225
+ for (const column of securityEventColumns) {
226
+ expect({
227
+ title: column.title,
228
+ unreachable: Boolean(
229
+ column.isHiddenByDefault && column.isNotCustomizable,
230
+ ),
231
+ }).toEqual({ title: column.title, unreachable: false });
232
+ }
233
+ });
234
+
235
+ /*
236
+ * These columns are the table's own, not the viewer's - "remove" is only for
237
+ * attribute columns, which are added from the picker in the first place.
238
+ */
239
+ test("no declared column is removable", () => {
240
+ for (const column of securityEventColumns) {
241
+ expect({
242
+ title: column.title,
243
+ removable: Boolean(column.isRemovable),
244
+ }).toEqual({ title: column.title, removable: false });
245
+ }
246
+ });
247
+ });
248
+
249
+ describe("SecurityEventsTable columns - cells", () => {
250
+ test("a text column with no value renders a dash rather than blank", () => {
251
+ expect(columnByTitle("Category").noValueMessage).toBe("-");
252
+ expect(columnByTitle("Rule Name").noValueMessage).toBe("-");
253
+ expect(columnByTitle("Event UID").noValueMessage).toBe("-");
254
+ });
255
+
256
+ test("Observables joins the list into one scannable line", () => {
257
+ const column: Column<SecurityEvent> = columnByTitle("Observables");
258
+
259
+ const { container } = render(
260
+ column.getElement!(
261
+ makeEvent({ observables: ["web-1", "10.0.0.4", "bob@example.com"] }),
262
+ ),
263
+ );
264
+
265
+ expect(container.textContent).toBe("web-1, 10.0.0.4, bob@example.com");
266
+ });
267
+
268
+ test("Observables keeps the full list available on hover", () => {
269
+ const column: Column<SecurityEvent> = columnByTitle("Observables");
270
+
271
+ const { container } = render(
272
+ column.getElement!(makeEvent({ observables: ["web-1", "10.0.0.4"] })),
273
+ );
274
+
275
+ expect(container.querySelector("span")).toHaveAttribute(
276
+ "title",
277
+ "web-1, 10.0.0.4",
278
+ );
279
+ });
280
+
281
+ test("an empty array column renders the placeholder, not an empty cell", () => {
282
+ const column: Column<SecurityEvent> = columnByTitle("MITRE Tactics");
283
+
284
+ const { container } = render(
285
+ column.getElement!(makeEvent({ mitreTactics: [] })),
286
+ );
287
+
288
+ expect(container.textContent).toBe("-");
289
+ });
290
+
291
+ test("blank entries inside an array are dropped rather than joined as commas", () => {
292
+ const column: Column<SecurityEvent> = columnByTitle("MITRE Techniques");
293
+
294
+ const { container } = render(
295
+ column.getElement!(
296
+ makeEvent({ mitreTechniques: ["T1110", "", "T1078"] }),
297
+ ),
298
+ );
299
+
300
+ expect(container.textContent).toBe("T1110, T1078");
301
+ });
302
+
303
+ test("array columns export the same joined text they render", () => {
304
+ const column: Column<SecurityEvent> = columnByTitle("Observables");
305
+
306
+ expect(
307
+ column.getExportValue!(makeEvent({ observables: ["web-1", "10.0.0.4"] })),
308
+ ).toBe("web-1, 10.0.0.4");
309
+
310
+ expect(column.getExportValue!(makeEvent({ observables: [] }))).toBe("");
311
+ });
312
+
313
+ /*
314
+ * ClickHouse cannot meaningfully order by an Array(String), and nothing
315
+ * between the header and the query builder would reject the attempt.
316
+ */
317
+ test("array columns are not sortable", () => {
318
+ expect(columnByTitle("Observables").disableSort).toBe(true);
319
+ expect(columnByTitle("MITRE Tactics").disableSort).toBe(true);
320
+ expect(columnByTitle("MITRE Techniques").disableSort).toBe(true);
321
+ });
322
+
323
+ /*
324
+ * targetPort is non-nullable with a 0 default, so 0 means "the source did
325
+ * not say" far more often than it means port zero.
326
+ */
327
+ test("Target Port renders a dash when the source did not say", () => {
328
+ const column: Column<SecurityEvent> = columnByTitle("Target Port");
329
+
330
+ const { container } = render(
331
+ column.getElement!(makeEvent({ targetPort: 0 })),
332
+ );
333
+
334
+ expect(container.textContent).toBe("-");
335
+ expect(column.getExportValue!(makeEvent({ targetPort: 0 }))).toBe("");
336
+ });
337
+
338
+ test("Target Port renders a real port", () => {
339
+ const column: Column<SecurityEvent> = columnByTitle("Target Port");
340
+
341
+ const { container } = render(
342
+ column.getElement!(makeEvent({ targetPort: 443 })),
343
+ );
344
+
345
+ expect(container.textContent).toBe("443");
346
+ expect(column.getExportValue!(makeEvent({ targetPort: 443 }))).toBe("443");
347
+ });
348
+
349
+ test("Severity still renders its pill", () => {
350
+ const column: Column<SecurityEvent> = columnByTitle("Severity");
351
+
352
+ const { container } = render(
353
+ column.getElement!(makeEvent({ severityName: OcsfSeverity.Critical })),
354
+ );
355
+
356
+ expect(container.textContent).toContain("Critical");
357
+ });
358
+
359
+ test("Time renders a relative and an absolute reading", () => {
360
+ const column: Column<SecurityEvent> = columnByTitle("Time");
361
+
362
+ const { container } = render(
363
+ column.getElement!(makeEvent({ time: new Date("2024-03-01T10:00:00Z") })),
364
+ );
365
+
366
+ expect(container.textContent).toContain("2024");
367
+ });
368
+
369
+ test("Time with nothing to show renders the placeholder", () => {
370
+ const column: Column<SecurityEvent> = columnByTitle("Time");
371
+
372
+ const { container } = render(column.getElement!(makeEvent({})));
373
+
374
+ expect(container.textContent).toBe("-");
375
+ });
376
+
377
+ test("long free-text columns are typed as long text so they wrap", () => {
378
+ expect(columnByTitle("Message").type).toBe(FieldType.LongText);
379
+ expect(columnByTitle("Principal Process").type).toBe(FieldType.LongText);
380
+ expect(columnByTitle("Target Resource").type).toBe(FieldType.LongText);
381
+ });
382
+ });
@@ -225,6 +225,222 @@ describe("bulkAddStatusPageMonitors", () => {
225
225
  expect(result.failed).toEqual([]);
226
226
  });
227
227
 
228
+ test("reports no skipped monitors when the status page is empty", async () => {
229
+ const monitor: Monitor = makeMonitor(
230
+ "0198c8ec-2a1d-7f0c-9e75-384194160080",
231
+ "First",
232
+ );
233
+
234
+ const result: BulkAddStatusPageMonitorsResult =
235
+ await bulkAddStatusPageMonitors({
236
+ monitors: [monitor],
237
+ projectId: PROJECT_ID,
238
+ statusPageId: STATUS_PAGE_ID,
239
+ createResource: async (): Promise<void> => {},
240
+ });
241
+
242
+ expect(result.succeeded).toEqual([monitor]);
243
+ expect(result.skipped).toEqual([]);
244
+ });
245
+
246
+ /*
247
+ * Issue #3420. Selecting a label re-selects every monitor carrying it, so a
248
+ * re-add after one new monitor joined the label arrives here as "the three
249
+ * already on the page, plus the new one". Only the new one is a create.
250
+ */
251
+ test("adds only the monitors the status page does not already list", async () => {
252
+ const alreadyAdded: Monitor = makeMonitor(
253
+ "0198c8ec-2a1d-7f0c-9e75-384194160090",
254
+ "Checkout API",
255
+ );
256
+ const alsoAlreadyAdded: Monitor = makeMonitor(
257
+ "0198c8ec-2a1d-7f0c-9e75-384194160091",
258
+ "Billing Worker",
259
+ );
260
+ const newlyLabelled: Monitor = makeMonitor(
261
+ "0198c8ec-2a1d-7f0c-9e75-384194160092",
262
+ "Search API",
263
+ );
264
+ const created: Array<StatusPageResource> = [];
265
+
266
+ const result: BulkAddStatusPageMonitorsResult =
267
+ await bulkAddStatusPageMonitors({
268
+ monitors: [alreadyAdded, alsoAlreadyAdded, newlyLabelled],
269
+ projectId: PROJECT_ID,
270
+ statusPageId: STATUS_PAGE_ID,
271
+ statusPageGroupId: GROUP_ID,
272
+ existingMonitorIds: [alreadyAdded.id!, alsoAlreadyAdded.id!],
273
+ createResource: async (resource: StatusPageResource): Promise<void> => {
274
+ created.push(resource);
275
+ },
276
+ });
277
+
278
+ expect(created).toHaveLength(1);
279
+ expect(created[0]!.monitorId?.toString()).toBe(newlyLabelled._id);
280
+ expect(result.succeeded).toEqual([newlyLabelled]);
281
+ expect(result.skipped).toEqual([alreadyAdded, alsoAlreadyAdded]);
282
+ expect(result.failed).toEqual([]);
283
+ });
284
+
285
+ test("re-adding the same label a second time writes nothing at all", async () => {
286
+ const monitors: Array<Monitor> = [
287
+ makeMonitor("0198c8ec-2a1d-7f0c-9e75-384194160100", "First"),
288
+ makeMonitor("0198c8ec-2a1d-7f0c-9e75-384194160101", "Second"),
289
+ ];
290
+ const createResource: (resource: StatusPageResource) => Promise<void> =
291
+ jest.fn(async (_resource: StatusPageResource): Promise<void> => {});
292
+
293
+ const result: BulkAddStatusPageMonitorsResult =
294
+ await bulkAddStatusPageMonitors({
295
+ monitors: monitors,
296
+ projectId: PROJECT_ID,
297
+ statusPageId: STATUS_PAGE_ID,
298
+ existingMonitorIds: monitors.map((monitor: Monitor) => {
299
+ return monitor.id!;
300
+ }),
301
+ createResource,
302
+ });
303
+
304
+ expect(createResource).not.toHaveBeenCalled();
305
+ expect(result.succeeded).toEqual([]);
306
+ expect(result.failed).toEqual([]);
307
+ expect(result.skipped).toEqual(monitors);
308
+ });
309
+
310
+ test("accepts existing monitor ids as ObjectIDs or as strings", async () => {
311
+ const byObjectId: Monitor = makeMonitor(
312
+ "0198c8ec-2a1d-7f0c-9e75-384194160110",
313
+ "By Object ID",
314
+ );
315
+ const byString: Monitor = makeMonitor(
316
+ "0198c8ec-2a1d-7f0c-9e75-384194160111",
317
+ "By String",
318
+ );
319
+ const created: Array<StatusPageResource> = [];
320
+
321
+ const result: BulkAddStatusPageMonitorsResult =
322
+ await bulkAddStatusPageMonitors({
323
+ monitors: [byObjectId, byString],
324
+ projectId: PROJECT_ID,
325
+ statusPageId: STATUS_PAGE_ID,
326
+ existingMonitorIds: [new ObjectID(byObjectId._id!), byString._id!],
327
+ createResource: async (resource: StatusPageResource): Promise<void> => {
328
+ created.push(resource);
329
+ },
330
+ });
331
+
332
+ expect(created).toEqual([]);
333
+ expect(result.skipped).toEqual([byObjectId, byString]);
334
+ });
335
+
336
+ test("ignores blank entries in the existing monitor ids", async () => {
337
+ const monitor: Monitor = makeMonitor(
338
+ "0198c8ec-2a1d-7f0c-9e75-384194160120",
339
+ "First",
340
+ );
341
+ const created: Array<StatusPageResource> = [];
342
+
343
+ const result: BulkAddStatusPageMonitorsResult =
344
+ await bulkAddStatusPageMonitors({
345
+ monitors: [monitor],
346
+ projectId: PROJECT_ID,
347
+ statusPageId: STATUS_PAGE_ID,
348
+ existingMonitorIds: ["", undefined as unknown as string],
349
+ createResource: async (resource: StatusPageResource): Promise<void> => {
350
+ created.push(resource);
351
+ },
352
+ });
353
+
354
+ expect(created).toHaveLength(1);
355
+ expect(result.succeeded).toEqual([monitor]);
356
+ expect(result.skipped).toEqual([]);
357
+ });
358
+
359
+ test("keeps the display order of the monitors it does add", async () => {
360
+ const first: Monitor = makeMonitor(
361
+ "0198c8ec-2a1d-7f0c-9e75-384194160130",
362
+ "First",
363
+ );
364
+ const alreadyAdded: Monitor = makeMonitor(
365
+ "0198c8ec-2a1d-7f0c-9e75-384194160131",
366
+ "Already Added",
367
+ );
368
+ const last: Monitor = makeMonitor(
369
+ "0198c8ec-2a1d-7f0c-9e75-384194160132",
370
+ "Last",
371
+ );
372
+ const createdNames: Array<string> = [];
373
+
374
+ await bulkAddStatusPageMonitors({
375
+ monitors: [first, alreadyAdded, last],
376
+ projectId: PROJECT_ID,
377
+ statusPageId: STATUS_PAGE_ID,
378
+ existingMonitorIds: [alreadyAdded.id!],
379
+ createResource: async (resource: StatusPageResource): Promise<void> => {
380
+ createdNames.push(resource.displayName!);
381
+ },
382
+ });
383
+
384
+ expect(createdNames).toEqual(["First", "Last"]);
385
+ });
386
+
387
+ test("reports a monitor that is both selected twice and already added once", async () => {
388
+ const alreadyAdded: Monitor = makeMonitor(
389
+ "0198c8ec-2a1d-7f0c-9e75-384194160140",
390
+ "Already Added",
391
+ );
392
+ const alreadyAddedAgain: Monitor = makeMonitor(
393
+ "0198c8ec-2a1d-7f0c-9e75-384194160140",
394
+ "Already Added Duplicate",
395
+ );
396
+ const createResource: (resource: StatusPageResource) => Promise<void> =
397
+ jest.fn(async (_resource: StatusPageResource): Promise<void> => {});
398
+
399
+ const result: BulkAddStatusPageMonitorsResult =
400
+ await bulkAddStatusPageMonitors({
401
+ monitors: [alreadyAdded, alreadyAddedAgain],
402
+ projectId: PROJECT_ID,
403
+ statusPageId: STATUS_PAGE_ID,
404
+ existingMonitorIds: [alreadyAdded.id!],
405
+ createResource,
406
+ });
407
+
408
+ expect(createResource).not.toHaveBeenCalled();
409
+ expect(result.skipped).toEqual([alreadyAdded]);
410
+ });
411
+
412
+ test("still reports invalid selections while skipping the already added ones", async () => {
413
+ const missingId: Monitor = makeMonitor(null, "Missing ID");
414
+ const alreadyAdded: Monitor = makeMonitor(
415
+ "0198c8ec-2a1d-7f0c-9e75-384194160150",
416
+ "Already Added",
417
+ );
418
+ const valid: Monitor = makeMonitor(
419
+ "0198c8ec-2a1d-7f0c-9e75-384194160151",
420
+ "Valid",
421
+ );
422
+ const created: Array<StatusPageResource> = [];
423
+
424
+ const result: BulkAddStatusPageMonitorsResult =
425
+ await bulkAddStatusPageMonitors({
426
+ monitors: [missingId, alreadyAdded, valid],
427
+ projectId: PROJECT_ID,
428
+ statusPageId: STATUS_PAGE_ID,
429
+ existingMonitorIds: [alreadyAdded.id!],
430
+ createResource: async (resource: StatusPageResource): Promise<void> => {
431
+ created.push(resource);
432
+ },
433
+ });
434
+
435
+ expect(created).toHaveLength(1);
436
+ expect(result.succeeded).toEqual([valid]);
437
+ expect(result.skipped).toEqual([alreadyAdded]);
438
+ expect(result.failed).toHaveLength(1);
439
+ expect((result.failed[0]!.error as Error).message).toBe(
440
+ "Monitor ID is required.",
441
+ );
442
+ });
443
+
228
444
  test("accepts plain monitor objects returned by the multi-select list", async () => {
229
445
  const selectedMonitor: Monitor = {
230
446
  _id: "0198c8ec-2a1d-7f0c-9e75-384194160045",