@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
@@ -1855,4 +1855,761 @@ describe("Status Page > Resources", () => {
1855
1855
  );
1856
1856
  });
1857
1857
  });
1858
+
1859
+ /*
1860
+ * Clearing a group, in one decision rather than twenty seven.
1861
+ *
1862
+ * The reporter had a group holding twenty seven monitors and no way to say so:
1863
+ * every removal was its own overflow menu, its own confirmation and its own
1864
+ * reload of the group underneath them (issue #3419). The list carries a box
1865
+ * per row and a box at the top of it now, and the bulk bar turns whatever is
1866
+ * ticked into one confirmation and one pass of deletes.
1867
+ *
1868
+ * What is worth pinning down is not that a checkbox renders. It is that the
1869
+ * selection means what it says: that "select all" under a search means the
1870
+ * search's matches, that a selection cannot outlive the group it was made in
1871
+ * or the rows it was made of, and that a removal which fails halfway says
1872
+ * which of them did not go.
1873
+ */
1874
+ describe("removing several resources at once", () => {
1875
+ type ResourceRowsFunction = () => Array<HTMLElement>;
1876
+
1877
+ const resourceRows: ResourceRowsFunction = (): Array<HTMLElement> => {
1878
+ return screen.queryAllByTestId("status-page-resource-row");
1879
+ };
1880
+
1881
+ type ResourceRowByNameFunction = (name: string) => HTMLElement;
1882
+
1883
+ const resourceRowByName: ResourceRowByNameFunction = (
1884
+ name: string,
1885
+ ): HTMLElement => {
1886
+ const row: HTMLElement | undefined = resourceRows().find(
1887
+ (candidate: HTMLElement) => {
1888
+ return (candidate.textContent || "").includes(name);
1889
+ },
1890
+ );
1891
+
1892
+ if (!row) {
1893
+ throw new Error(`No resource row called ${name} is on screen`);
1894
+ }
1895
+
1896
+ return row;
1897
+ };
1898
+
1899
+ type SelectResourceFunction = (name: string) => Promise<void>;
1900
+
1901
+ const selectResource: SelectResourceFunction = async (
1902
+ name: string,
1903
+ ): Promise<void> => {
1904
+ fireEvent.click(within(resourceRowByName(name)).getByRole("checkbox"));
1905
+
1906
+ await flushEffects();
1907
+ };
1908
+
1909
+ type SelectAllResourcesFunction = () => Promise<void>;
1910
+
1911
+ const selectAllResources: SelectAllResourcesFunction =
1912
+ async (): Promise<void> => {
1913
+ fireEvent.click(
1914
+ screen.getByTestId("status-page-resource-list-select-all"),
1915
+ );
1916
+
1917
+ await flushEffects();
1918
+ };
1919
+
1920
+ type BulkBarFunction = () => HTMLElement | null;
1921
+
1922
+ const bulkBar: BulkBarFunction = (): HTMLElement | null => {
1923
+ return screen.queryByTestId("status-page-resource-panel-bulk-actions");
1924
+ };
1925
+
1926
+ type OpenBulkMenuFunction = () => Promise<void>;
1927
+
1928
+ const openBulkMenu: OpenBulkMenuFunction = async (): Promise<void> => {
1929
+ fireEvent.click(screen.getByText("Bulk Actions"));
1930
+
1931
+ await flushEffects();
1932
+ };
1933
+
1934
+ /* Through the menu, the confirmation, and out the other side. */
1935
+ type ConfirmBulkRemoveFunction = () => Promise<void>;
1936
+
1937
+ const confirmBulkRemove: ConfirmBulkRemoveFunction =
1938
+ async (): Promise<void> => {
1939
+ await openBulkMenu();
1940
+
1941
+ fireEvent.click(screen.getByText("Remove from status page"));
1942
+ await flushEffects();
1943
+
1944
+ fireEvent.click(screen.getByText("Remove from status page"));
1945
+ await flushEffects();
1946
+ };
1947
+
1948
+ type DeletedIdsFunction = () => Array<string>;
1949
+
1950
+ const deletedIds: DeletedIdsFunction = (): Array<string> => {
1951
+ return mockDeleteItem.mock.calls.map((call: Array<any>) => {
1952
+ return call[0].id.toString();
1953
+ });
1954
+ };
1955
+
1956
+ type SetUpGroupFunction = (
1957
+ resources?: Array<StatusPageResource> | undefined,
1958
+ ) => Array<GetListCall>;
1959
+
1960
+ const setUpGroup: SetUpGroupFunction = (
1961
+ resources?: Array<StatusPageResource> | undefined,
1962
+ ): Array<GetListCall> => {
1963
+ return setUpApi({
1964
+ groups: buildHierarchy(),
1965
+ resources: resources || [
1966
+ makeResource({ id: "a", monitorName: "API", order: 1 }),
1967
+ makeResource({ id: "b", monitorName: "Database", order: 2 }),
1968
+ makeResource({ id: "c", monitorName: "Cache", order: 3 }),
1969
+ ],
1970
+ });
1971
+ };
1972
+
1973
+ describe("making a selection", () => {
1974
+ test("every row has a box, and the list is topped by one", async () => {
1975
+ setUpGroup();
1976
+
1977
+ renderPage();
1978
+
1979
+ await waitForExplorer();
1980
+
1981
+ expect(
1982
+ screen.queryAllByTestId("status-page-resource-row-select").length,
1983
+ ).toBe(3);
1984
+ expect(
1985
+ screen.getByTestId("status-page-resource-list-select-all"),
1986
+ ).toBeInTheDocument();
1987
+ });
1988
+
1989
+ test("nothing is offered until something is ticked", async () => {
1990
+ setUpGroup();
1991
+
1992
+ renderPage();
1993
+
1994
+ await waitForExplorer();
1995
+
1996
+ expect(bulkBar()).not.toBeInTheDocument();
1997
+
1998
+ await selectResource("Database");
1999
+
2000
+ expect(bulkBar()).toBeInTheDocument();
2001
+ });
2002
+
2003
+ test("the bar counts what is ticked, in the singular when there is one", async () => {
2004
+ setUpGroup();
2005
+
2006
+ renderPage();
2007
+
2008
+ await waitForExplorer();
2009
+
2010
+ await selectResource("Database");
2011
+
2012
+ expect(bulkBar()!.textContent).toContain("1 resource Selected");
2013
+
2014
+ await selectResource("Cache");
2015
+
2016
+ expect(bulkBar()!.textContent).toContain("2 resources Selected");
2017
+ });
2018
+
2019
+ test("un-ticking the last row puts the bar away again", async () => {
2020
+ setUpGroup();
2021
+
2022
+ renderPage();
2023
+
2024
+ await waitForExplorer();
2025
+
2026
+ await selectResource("Database");
2027
+ await selectResource("Database");
2028
+
2029
+ expect(bulkBar()).not.toBeInTheDocument();
2030
+ });
2031
+
2032
+ test("the box at the top selects the whole group, and clears it again", async () => {
2033
+ setUpGroup();
2034
+
2035
+ renderPage();
2036
+
2037
+ await waitForExplorer();
2038
+
2039
+ await selectAllResources();
2040
+
2041
+ expect(bulkBar()!.textContent).toContain("3 resources Selected");
2042
+
2043
+ resourceRows().forEach((row: HTMLElement) => {
2044
+ expect(row).toHaveAttribute("data-selected", "true");
2045
+ });
2046
+
2047
+ await selectAllResources();
2048
+
2049
+ expect(bulkBar()).not.toBeInTheDocument();
2050
+ });
2051
+
2052
+ /*
2053
+ * Half filled rather than empty. A box that can never reach "all
2054
+ * selected" and sits permanently empty reads as a selection that was
2055
+ * lost.
2056
+ */
2057
+ test("the box at the top is half filled while only some rows are", async () => {
2058
+ setUpGroup();
2059
+
2060
+ renderPage();
2061
+
2062
+ await waitForExplorer();
2063
+
2064
+ await selectResource("API");
2065
+
2066
+ const selectAll: HTMLInputElement = screen.getByTestId(
2067
+ "status-page-resource-list-select-all",
2068
+ ) as HTMLInputElement;
2069
+
2070
+ expect(selectAll.checked).toBe(false);
2071
+ expect(selectAll.indeterminate).toBe(true);
2072
+
2073
+ await selectResource("Database");
2074
+ await selectResource("Cache");
2075
+
2076
+ expect(
2077
+ (
2078
+ screen.getByTestId(
2079
+ "status-page-resource-list-select-all",
2080
+ ) as HTMLInputElement
2081
+ ).checked,
2082
+ ).toBe(true);
2083
+ });
2084
+
2085
+ /*
2086
+ * The search is the whole reason a "select all" is worth having on a
2087
+ * group of twenty seven: pick out the ones that share a name, then clear
2088
+ * them. So it has to mean the matches and nothing else.
2089
+ */
2090
+ test("select all under a search takes the matches and leaves the rest", async () => {
2091
+ setUpApi({
2092
+ groups: buildHierarchy(),
2093
+ resources: [
2094
+ makeResource({ id: "a", monitorName: "Prod API" }),
2095
+ makeResource({ id: "b", monitorName: "Prod Database" }),
2096
+ makeResource({ id: "c", monitorName: "Staging Cache" }),
2097
+ ],
2098
+ });
2099
+
2100
+ renderPage();
2101
+
2102
+ await waitForExplorer();
2103
+
2104
+ fireEvent.change(
2105
+ screen.getByTestId("status-page-resource-panel-search"),
2106
+ { target: { value: "prod" } },
2107
+ );
2108
+ await flushEffects();
2109
+
2110
+ expect(resourceRows().length).toBe(2);
2111
+
2112
+ await selectAllResources();
2113
+
2114
+ expect(bulkBar()!.textContent).toContain("2 resources Selected");
2115
+ });
2116
+
2117
+ /*
2118
+ * And a selection made before the search is still a selection: the rows
2119
+ * the filter hid are not rows the operator changed their mind about.
2120
+ */
2121
+ test("a selection survives a search being typed and cleared", async () => {
2122
+ setUpApi({
2123
+ groups: buildHierarchy(),
2124
+ resources: [
2125
+ makeResource({ id: "a", monitorName: "Prod API" }),
2126
+ makeResource({ id: "b", monitorName: "Staging Cache" }),
2127
+ ],
2128
+ });
2129
+
2130
+ renderPage();
2131
+
2132
+ await waitForExplorer();
2133
+
2134
+ await selectResource("Staging Cache");
2135
+
2136
+ fireEvent.change(
2137
+ screen.getByTestId("status-page-resource-panel-search"),
2138
+ { target: { value: "prod" } },
2139
+ );
2140
+ await flushEffects();
2141
+
2142
+ /* Off screen, still counted. */
2143
+ expect(resourceRows().length).toBe(1);
2144
+ expect(bulkBar()!.textContent).toContain("1 resource Selected");
2145
+
2146
+ fireEvent.change(
2147
+ screen.getByTestId("status-page-resource-panel-search"),
2148
+ { target: { value: "" } },
2149
+ );
2150
+ await flushEffects();
2151
+
2152
+ expect(resourceRowByName("Staging Cache")).toHaveAttribute(
2153
+ "data-selected",
2154
+ "true",
2155
+ );
2156
+ });
2157
+
2158
+ /*
2159
+ * A selection belongs to the group it was made in. Carrying it across
2160
+ * would aim a removal at rows the operator is no longer looking at.
2161
+ */
2162
+ test("a selection does not follow the operator to the next group", async () => {
2163
+ setUpApi({
2164
+ groups: buildHierarchy(),
2165
+ resources: [
2166
+ makeResource({ id: "loose", monitorName: "Loose" }),
2167
+ makeResource({
2168
+ id: "market",
2169
+ groupId: MARKET_ID,
2170
+ monitorName: "Market Monitor",
2171
+ }),
2172
+ ],
2173
+ });
2174
+
2175
+ renderPage();
2176
+
2177
+ await waitForExplorer();
2178
+
2179
+ await selectResource("Loose");
2180
+
2181
+ expect(bulkBar()).toBeInTheDocument();
2182
+
2183
+ await selectGroup("Market 1001");
2184
+
2185
+ expect(bulkBar()).not.toBeInTheDocument();
2186
+ });
2187
+
2188
+ /*
2189
+ * Ticking rows is only worth offering to somebody who can act on them,
2190
+ * and removing is the only thing this pane does with a selection.
2191
+ */
2192
+ test("a viewer who may not remove anything is offered no boxes", async () => {
2193
+ mockIsMasterAdmin.mockReturnValue(false);
2194
+
2195
+ setUpGroup();
2196
+
2197
+ renderPage();
2198
+
2199
+ await waitForExplorer();
2200
+
2201
+ expect(resourceRows().length).toBe(3);
2202
+ expect(
2203
+ screen.queryAllByTestId("status-page-resource-row-select").length,
2204
+ ).toBe(0);
2205
+ expect(
2206
+ screen.queryByTestId("status-page-resource-list-select-all"),
2207
+ ).not.toBeInTheDocument();
2208
+ });
2209
+ });
2210
+
2211
+ describe("removing what was selected", () => {
2212
+ test("asks once, naming the number, and says the monitors survive", async () => {
2213
+ setUpGroup();
2214
+
2215
+ renderPage();
2216
+
2217
+ await waitForExplorer();
2218
+
2219
+ await selectResource("API");
2220
+ await selectResource("Cache");
2221
+
2222
+ await openBulkMenu();
2223
+
2224
+ fireEvent.click(screen.getByText("Remove from status page"));
2225
+ await flushEffects();
2226
+
2227
+ expect(screen.getByText("Remove 2 resources")).toBeInTheDocument();
2228
+ expect(
2229
+ screen.getByText(
2230
+ "Are you sure you want to remove 2 resources from this status page? The monitors themselves are not deleted.",
2231
+ ),
2232
+ ).toBeInTheDocument();
2233
+
2234
+ /* Nothing has been written yet - the question is still on screen. */
2235
+ expect(mockDeleteItem).not.toHaveBeenCalled();
2236
+ });
2237
+
2238
+ test("deletes exactly the rows that were ticked, and no others", async () => {
2239
+ setUpGroup();
2240
+
2241
+ renderPage();
2242
+
2243
+ await waitForExplorer();
2244
+
2245
+ await selectResource("API");
2246
+ await selectResource("Cache");
2247
+
2248
+ await confirmBulkRemove();
2249
+
2250
+ await waitFor(() => {
2251
+ expect(mockDeleteItem).toHaveBeenCalledTimes(2);
2252
+ });
2253
+
2254
+ expect(deletedIds().sort()).toEqual(["a", "c"]);
2255
+ });
2256
+
2257
+ test("clears a whole group from one press of the box at the top", async () => {
2258
+ setUpGroup();
2259
+
2260
+ renderPage();
2261
+
2262
+ await waitForExplorer();
2263
+
2264
+ await selectAllResources();
2265
+
2266
+ await confirmBulkRemove();
2267
+
2268
+ await waitFor(() => {
2269
+ expect(mockDeleteItem).toHaveBeenCalledTimes(3);
2270
+ });
2271
+
2272
+ expect(deletedIds().sort()).toEqual(["a", "b", "c"]);
2273
+ });
2274
+
2275
+ /*
2276
+ * The report of what happened is the last thing on screen, and clearing
2277
+ * the selection is what takes it away - so the group is only re-read once
2278
+ * the operator has dismissed it.
2279
+ */
2280
+ test("re-reads the group and drops the selection once the report is closed", async () => {
2281
+ const calls: Array<GetListCall> = setUpGroup();
2282
+
2283
+ renderPage();
2284
+
2285
+ await waitForExplorer();
2286
+
2287
+ await selectResource("API");
2288
+
2289
+ const before: number = scopedResourceCalls(calls).length;
2290
+
2291
+ await confirmBulkRemove();
2292
+
2293
+ await waitFor(() => {
2294
+ expect(mockDeleteItem).toHaveBeenCalledTimes(1);
2295
+ });
2296
+
2297
+ /* Still on screen, still reporting, and the group not yet re-read. */
2298
+ expect(scopedResourceCalls(calls).length).toBe(before);
2299
+ expect(screen.getByText("Completed")).toBeInTheDocument();
2300
+
2301
+ fireEvent.click(screen.getByText("Close"));
2302
+
2303
+ await waitFor(() => {
2304
+ expect(scopedResourceCalls(calls).length).toBe(before + 1);
2305
+ });
2306
+
2307
+ await waitForSettled();
2308
+
2309
+ expect(bulkBar()).not.toBeInTheDocument();
2310
+ });
2311
+
2312
+ test("moves the group's count badge without re-reading the status page", async () => {
2313
+ const calls: Array<GetListCall> = setUpApi({
2314
+ groups: buildHierarchy(),
2315
+ resources: [
2316
+ makeResource({ id: "a", groupId: MARKET_ID, monitorName: "One" }),
2317
+ makeResource({ id: "b", groupId: MARKET_ID, monitorName: "Two" }),
2318
+ makeResource({
2319
+ id: "c",
2320
+ groupId: MARKET_ID,
2321
+ monitorName: "Three",
2322
+ }),
2323
+ ],
2324
+ });
2325
+
2326
+ renderPage();
2327
+
2328
+ await waitForExplorer();
2329
+
2330
+ await selectGroup("Market 1001");
2331
+
2332
+ expect(
2333
+ within(navigatorRowByName("Market 1001")).getByTestId(
2334
+ "status-page-resource-navigator-count",
2335
+ ).textContent,
2336
+ ).toBe("3");
2337
+
2338
+ const unscopedCallsBefore: number =
2339
+ resourceCalls(calls).length - scopedResourceCalls(calls).length;
2340
+
2341
+ await selectResource("One");
2342
+ await selectResource("Two");
2343
+
2344
+ /* Whatever is asked for next, the group now holds one resource. */
2345
+ mockGetList.mockImplementation(async (callData: any): Promise<any> => {
2346
+ calls.push({
2347
+ modelType: callData.modelType,
2348
+ query: callData.query || {},
2349
+ });
2350
+
2351
+ if (callData.modelType === StatusPageGroup) {
2352
+ return {
2353
+ data: buildHierarchy(),
2354
+ count: 4,
2355
+ skip: 0,
2356
+ limit: 4,
2357
+ };
2358
+ }
2359
+
2360
+ return {
2361
+ data: [
2362
+ makeResource({
2363
+ id: "c",
2364
+ groupId: MARKET_ID,
2365
+ monitorName: "Three",
2366
+ }),
2367
+ ],
2368
+ count: 1,
2369
+ skip: 0,
2370
+ limit: 1,
2371
+ };
2372
+ });
2373
+
2374
+ await confirmBulkRemove();
2375
+
2376
+ await waitFor(() => {
2377
+ expect(mockDeleteItem).toHaveBeenCalledTimes(2);
2378
+ });
2379
+
2380
+ fireEvent.click(screen.getByText("Close"));
2381
+
2382
+ await waitFor(() => {
2383
+ expect(
2384
+ within(navigatorRowByName("Market 1001")).getByTestId(
2385
+ "status-page-resource-navigator-count",
2386
+ ).textContent,
2387
+ ).toBe("1");
2388
+ });
2389
+
2390
+ expect(
2391
+ resourceCalls(calls).length - scopedResourceCalls(calls).length,
2392
+ ).toBe(unscopedCallsBefore);
2393
+ });
2394
+
2395
+ /*
2396
+ * There is no bulk delete endpoint, so a removal is a pass of single
2397
+ * deletes and any one of them can fail. The ones that went stay gone, and
2398
+ * the operator is told which did not - by name, because "1 failed" over
2399
+ * twenty seven rows is not something anybody can act on.
2400
+ */
2401
+ test("reports the ones that failed by name, and keeps the ones that went", async () => {
2402
+ setUpGroup();
2403
+
2404
+ renderPage();
2405
+
2406
+ await waitForExplorer();
2407
+
2408
+ mockDeleteItem.mockImplementation(async (data: any): Promise<any> => {
2409
+ if (data.id.toString() === "b") {
2410
+ throw new Error("Database is not yours to remove");
2411
+ }
2412
+
2413
+ return {};
2414
+ });
2415
+
2416
+ await selectAllResources();
2417
+
2418
+ await confirmBulkRemove();
2419
+
2420
+ await waitFor(() => {
2421
+ expect(mockDeleteItem).toHaveBeenCalledTimes(3);
2422
+ });
2423
+
2424
+ expect(screen.getByText("Completed")).toBeInTheDocument();
2425
+ expect(screen.getByText("2 resources succeeded")).toBeInTheDocument();
2426
+ expect(screen.getByText("1 resource failed")).toBeInTheDocument();
2427
+
2428
+ /*
2429
+ * Scoped to the failure the report drew, not to the page: the row it
2430
+ * names is still in the list behind the modal, because a removal that
2431
+ * failed removed nothing.
2432
+ */
2433
+ const failure: HTMLElement = screen.getByText(
2434
+ "Database is not yours to remove",
2435
+ ).parentElement as HTMLElement;
2436
+
2437
+ expect(within(failure).getByText("Database")).toBeInTheDocument();
2438
+
2439
+ expect(resourceRowByName("Database")).toBeInTheDocument();
2440
+ });
2441
+
2442
+ test("a removal that fails outright still names what it was", async () => {
2443
+ setUpGroup();
2444
+
2445
+ renderPage();
2446
+
2447
+ await waitForExplorer();
2448
+
2449
+ mockDeleteItem.mockImplementation(async (): Promise<any> => {
2450
+ throw new Error("Status page is read only");
2451
+ });
2452
+
2453
+ await selectResource("API");
2454
+
2455
+ await confirmBulkRemove();
2456
+
2457
+ await waitFor(() => {
2458
+ expect(mockDeleteItem).toHaveBeenCalledTimes(1);
2459
+ });
2460
+
2461
+ expect(screen.getByText("1 resource failed")).toBeInTheDocument();
2462
+ expect(
2463
+ screen.getByText("Status page is read only"),
2464
+ ).toBeInTheDocument();
2465
+ });
2466
+
2467
+ /*
2468
+ * The bar's own "Select all" is the whole group rather than the filtered
2469
+ * view - which is how it differs from the box at the top of the list, and
2470
+ * why it is offered at all once a search is on.
2471
+ */
2472
+ test("the bar offers the rest of the group when the list is filtered", async () => {
2473
+ setUpApi({
2474
+ groups: buildHierarchy(),
2475
+ resources: [
2476
+ makeResource({ id: "a", monitorName: "Prod API" }),
2477
+ makeResource({ id: "b", monitorName: "Staging Cache" }),
2478
+ ],
2479
+ });
2480
+
2481
+ renderPage();
2482
+
2483
+ await waitForExplorer();
2484
+
2485
+ fireEvent.change(
2486
+ screen.getByTestId("status-page-resource-panel-search"),
2487
+ { target: { value: "prod" } },
2488
+ );
2489
+ await flushEffects();
2490
+
2491
+ await selectAllResources();
2492
+
2493
+ expect(bulkBar()!.textContent).toContain("1 resource Selected");
2494
+
2495
+ fireEvent.click(screen.getByText("Select All resources"));
2496
+ await flushEffects();
2497
+
2498
+ expect(bulkBar()!.textContent).toContain("2 resources Selected");
2499
+ });
2500
+
2501
+ /*
2502
+ * A group past the request's row cap holds resources this pane never
2503
+ * loaded, so "everything" is everything that arrived. Saying so is the
2504
+ * difference between a cleared group and a group that looks cleared.
2505
+ */
2506
+ test("says so when the group holds more than the pane could load", async () => {
2507
+ setUpApi({ groups: buildHierarchy() });
2508
+
2509
+ /*
2510
+ * One row back, four thousand claimed. That is what a group past the
2511
+ * request's row cap looks like from here.
2512
+ */
2513
+ mockGetList.mockImplementation(async (callData: any): Promise<any> => {
2514
+ if (callData.modelType === StatusPageGroup) {
2515
+ return {
2516
+ data: buildHierarchy(),
2517
+ count: 4,
2518
+ skip: 0,
2519
+ limit: 4,
2520
+ };
2521
+ }
2522
+
2523
+ return {
2524
+ data: [makeResource({ id: "a", monitorName: "API" })],
2525
+ count: 4000,
2526
+ skip: 0,
2527
+ limit: 1,
2528
+ };
2529
+ });
2530
+
2531
+ renderPage();
2532
+
2533
+ await waitForExplorer();
2534
+
2535
+ await selectAllResources();
2536
+
2537
+ expect(bulkBar()!.textContent).toContain(
2538
+ `Selected 1 of ${(4000).toLocaleString()} matching resources`,
2539
+ );
2540
+ });
2541
+
2542
+ test("the whole bar goes away when the selection is cleared by hand", async () => {
2543
+ setUpGroup();
2544
+
2545
+ renderPage();
2546
+
2547
+ await waitForExplorer();
2548
+
2549
+ await selectAllResources();
2550
+
2551
+ fireEvent.click(screen.getByText("Clear Selection"));
2552
+ await flushEffects();
2553
+
2554
+ expect(bulkBar()).not.toBeInTheDocument();
2555
+ resourceRows().forEach((row: HTMLElement) => {
2556
+ expect(row).toHaveAttribute("data-selected", "false");
2557
+ });
2558
+ });
2559
+ });
2560
+
2561
+ /*
2562
+ * A grid group is the same resources arranged differently, and clearing
2563
+ * twenty seven of them one at a time is no better there.
2564
+ */
2565
+ describe("a grid group", () => {
2566
+ test("selects from its chips and removes them the same way", async () => {
2567
+ setUpApi({
2568
+ groups: buildHierarchy(),
2569
+ resources: [
2570
+ makeResource({ id: "loose", monitorName: "Loose" }),
2571
+ makeResource({
2572
+ id: "grid-a",
2573
+ groupId: GRID_ID,
2574
+ monitorName: "Auth US",
2575
+ rowAxisValue: "Auth",
2576
+ columnAxisValue: "US-East",
2577
+ }),
2578
+ makeResource({
2579
+ id: "grid-b",
2580
+ groupId: GRID_ID,
2581
+ monitorName: "API EU",
2582
+ rowAxisValue: "API",
2583
+ columnAxisValue: "EU-West",
2584
+ }),
2585
+ ],
2586
+ });
2587
+
2588
+ renderPage();
2589
+
2590
+ await waitForExplorer();
2591
+
2592
+ await selectGroup("Grid Group");
2593
+
2594
+ expect(
2595
+ screen.getByTestId("status-page-resource-grid"),
2596
+ ).toBeInTheDocument();
2597
+
2598
+ fireEvent.click(screen.getByLabelText("Select Auth US"));
2599
+ await flushEffects();
2600
+ fireEvent.click(screen.getByLabelText("Select API EU"));
2601
+ await flushEffects();
2602
+
2603
+ expect(bulkBar()!.textContent).toContain("2 resources Selected");
2604
+
2605
+ await confirmBulkRemove();
2606
+
2607
+ await waitFor(() => {
2608
+ expect(mockDeleteItem).toHaveBeenCalledTimes(2);
2609
+ });
2610
+
2611
+ expect(deletedIds().sort()).toEqual(["grid-a", "grid-b"]);
2612
+ });
2613
+ });
2614
+ });
1858
2615
  });