@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,707 @@
1
+ import ColumnCustomizationModal, {
2
+ AddableColumnsConfig,
3
+ } from "../../../../UI/Components/ModelTable/ColumnCustomizationModal";
4
+ import {
5
+ CustomizableColumn,
6
+ ModelTableColumn,
7
+ } from "../../../../UI/Components/ModelTable/ColumnPreference";
8
+ import { getAttributeColumns } from "../../../../UI/Components/ModelTable/AttributeColumns";
9
+ import Column from "../../../../UI/Components/ModelTable/Column";
10
+ import Columns from "../../../../UI/Components/ModelTable/Columns";
11
+ import FieldType from "../../../../UI/Components/Types/FieldType";
12
+ import SecurityEvent from "../../../../Models/AnalyticsModels/SecurityEvent";
13
+ import getJestMockFunction, { MockFunction } from "../../../MockType";
14
+ import "@testing-library/jest-dom";
15
+ import {
16
+ RenderResult,
17
+ cleanup,
18
+ fireEvent,
19
+ render,
20
+ } from "@testing-library/react";
21
+ import React from "react";
22
+ import {
23
+ afterEach,
24
+ beforeEach,
25
+ describe,
26
+ expect,
27
+ jest,
28
+ test,
29
+ } from "@jest/globals";
30
+
31
+ /*
32
+ * The "add a column" half of the picker.
33
+ *
34
+ * It exists because some columns cannot be a checklist: the keys inside a
35
+ * security event's `attributes` map are whatever the source sent, differ per
36
+ * event class, and there can be several hundred of them. A checkbox each would
37
+ * bury the columns the table was designed around and hand "Show all" a way to
38
+ * put 500 columns on screen. So they are offered through a search box, and
39
+ * only what someone asks for is materialized.
40
+ *
41
+ * The invariants that carry weight, all pinned below:
42
+ *
43
+ * 1. Adding is still staging. Nothing leaves the modal before Save, exactly
44
+ * like every checkbox and every reorder.
45
+ * 2. A column already in the list is not offered again - otherwise adding it
46
+ * twice would put two columns with the same id into the layout.
47
+ * 3. Removing is hiding plus forgetting, so it is bound by the same rule:
48
+ * the last column standing cannot go.
49
+ * 4. Only columns that say they are removable get a remove button. A column
50
+ * the table ships must never be removable, because nothing would put it
51
+ * back.
52
+ */
53
+
54
+ type MakeEntryFunction = (data: {
55
+ id: string;
56
+ title: string;
57
+ isVisible: boolean;
58
+ isRemovable?: boolean | undefined;
59
+ }) => CustomizableColumn<SecurityEvent>;
60
+
61
+ const makeEntry: MakeEntryFunction = (data: {
62
+ id: string;
63
+ title: string;
64
+ isVisible: boolean;
65
+ isRemovable?: boolean | undefined;
66
+ }): CustomizableColumn<SecurityEvent> => {
67
+ const column: ModelTableColumn<SecurityEvent> = {
68
+ field: { message: true },
69
+ title: data.title,
70
+ type: FieldType.Text,
71
+ };
72
+
73
+ return {
74
+ id: data.id,
75
+ column: column,
76
+ isVisible: data.isVisible,
77
+ isPinned: false,
78
+ isRemovable: data.isRemovable,
79
+ };
80
+ };
81
+
82
+ type GetDeclaredColumnsFunction = () => Array<
83
+ CustomizableColumn<SecurityEvent>
84
+ >;
85
+
86
+ const getDeclaredColumns: GetDeclaredColumnsFunction = (): Array<
87
+ CustomizableColumn<SecurityEvent>
88
+ > => {
89
+ return [
90
+ makeEntry({ id: "time", title: "Time", isVisible: true }),
91
+ makeEntry({ id: "severityName", title: "Severity", isVisible: true }),
92
+ makeEntry({ id: "eventUid", title: "Event UID", isVisible: false }),
93
+ ];
94
+ };
95
+
96
+ /*
97
+ * Real generated attribute columns rather than placeholders: the ids the
98
+ * layout is keyed on come out of that generator, so a pool of fake entries
99
+ * would not exercise the same identity the app uses.
100
+ */
101
+ type MakePoolFunction = (
102
+ attributeKeys: Array<string>,
103
+ ) => Array<CustomizableColumn<SecurityEvent>>;
104
+
105
+ const makePool: MakePoolFunction = (
106
+ attributeKeys: Array<string>,
107
+ ): Array<CustomizableColumn<SecurityEvent>> => {
108
+ const columns: Columns<SecurityEvent> = getAttributeColumns<SecurityEvent>({
109
+ attributesColumnKey: "attributes",
110
+ attributeKeys: attributeKeys,
111
+ });
112
+
113
+ return columns.map(
114
+ (column: Column<SecurityEvent>): CustomizableColumn<SecurityEvent> => {
115
+ return {
116
+ id: column.id as string,
117
+ column: column,
118
+ isVisible: true,
119
+ isPinned: false,
120
+ isRemovable: true,
121
+ };
122
+ },
123
+ );
124
+ };
125
+
126
+ const POOL_KEYS: Array<string> = [
127
+ "activity_name",
128
+ "class_uid",
129
+ "device.hostname",
130
+ "finding_info.title",
131
+ "metadata.product.name",
132
+ ];
133
+
134
+ interface RenderedModal {
135
+ view: RenderResult;
136
+ onSave: MockFunction;
137
+ onClose: MockFunction;
138
+ onReset: MockFunction;
139
+ }
140
+
141
+ type RenderModalFunction = (data?: {
142
+ columns?: Array<CustomizableColumn<SecurityEvent>> | undefined;
143
+ addableColumns?: AddableColumnsConfig<SecurityEvent> | undefined | null;
144
+ }) => RenderedModal;
145
+
146
+ const renderModal: RenderModalFunction = (data?: {
147
+ columns?: Array<CustomizableColumn<SecurityEvent>> | undefined;
148
+ addableColumns?: AddableColumnsConfig<SecurityEvent> | undefined | null;
149
+ }): RenderedModal => {
150
+ const onSave: MockFunction = getJestMockFunction();
151
+ const onClose: MockFunction = getJestMockFunction();
152
+ const onReset: MockFunction = getJestMockFunction();
153
+
154
+ const addableColumns: AddableColumnsConfig<SecurityEvent> | undefined =
155
+ data?.addableColumns === null
156
+ ? undefined
157
+ : data?.addableColumns || {
158
+ title: "Add Attribute Column",
159
+ description: "Attributes differ per event.",
160
+ placeholder: "Search attributes...",
161
+ columns: makePool(POOL_KEYS),
162
+ };
163
+
164
+ const view: RenderResult = render(
165
+ <ColumnCustomizationModal<SecurityEvent>
166
+ columns={data?.columns || getDeclaredColumns()}
167
+ addableColumns={addableColumns}
168
+ onSave={onSave}
169
+ onClose={onClose}
170
+ onReset={onReset}
171
+ />,
172
+ );
173
+
174
+ return { view, onSave, onClose, onReset };
175
+ };
176
+
177
+ type GetSavedIdsFunction = (onSave: MockFunction) => Array<string>;
178
+
179
+ const getSavedIds: GetSavedIdsFunction = (
180
+ onSave: MockFunction,
181
+ ): Array<string> => {
182
+ return (
183
+ onSave.mock.calls[0]![0] as Array<CustomizableColumn<SecurityEvent>>
184
+ ).map((entry: CustomizableColumn<SecurityEvent>) => {
185
+ return entry.id;
186
+ });
187
+ };
188
+
189
+ type TypeAddSearchFunction = (view: RenderResult, text: string) => void;
190
+
191
+ const typeAddSearch: TypeAddSearchFunction = (
192
+ view: RenderResult,
193
+ text: string,
194
+ ): void => {
195
+ fireEvent.change(view.getByTestId("add-column-search"), {
196
+ target: { value: text },
197
+ });
198
+ };
199
+
200
+ describe("ColumnCustomizationModal - addable columns", () => {
201
+ beforeEach(() => {
202
+ window.localStorage.clear();
203
+ });
204
+
205
+ afterEach(() => {
206
+ cleanup();
207
+ jest.restoreAllMocks();
208
+ });
209
+
210
+ test("the section is absent entirely when no pool is configured", () => {
211
+ const { view }: RenderedModal = renderModal({ addableColumns: null });
212
+
213
+ expect(view.queryByTestId("add-column-section")).not.toBeInTheDocument();
214
+ expect(view.queryByTestId("add-column-search")).not.toBeInTheDocument();
215
+ });
216
+
217
+ test("renders its own title, description and search box", () => {
218
+ const { view }: RenderedModal = renderModal();
219
+
220
+ expect(view.getByTestId("add-column-section")).toBeInTheDocument();
221
+ expect(view.getByText("Add Attribute Column")).toBeInTheDocument();
222
+ expect(view.getByText("Attributes differ per event.")).toBeInTheDocument();
223
+ expect(view.getByTestId("add-column-search")).toHaveAttribute(
224
+ "placeholder",
225
+ "Search attributes...",
226
+ );
227
+ });
228
+
229
+ test("offers every key in the pool", () => {
230
+ const { view }: RenderedModal = renderModal();
231
+
232
+ expect(view.getAllByTestId(/^add-column-attributes\./)).toHaveLength(
233
+ POOL_KEYS.length,
234
+ );
235
+ expect(view.getByText("device.hostname")).toBeInTheDocument();
236
+ });
237
+
238
+ /*
239
+ * The pool is offered against the column TITLE, which for an attribute
240
+ * column is the raw key - so searching for the key someone read in the
241
+ * detail panel is what finds it.
242
+ */
243
+ test("searching narrows the pool by key", () => {
244
+ const { view }: RenderedModal = renderModal();
245
+
246
+ typeAddSearch(view, "device");
247
+
248
+ expect(view.getAllByTestId(/^add-column-attributes\./)).toHaveLength(1);
249
+ expect(
250
+ view.getByTestId("add-column-attributes.device.hostname"),
251
+ ).toBeInTheDocument();
252
+ });
253
+
254
+ test("the search is case-insensitive and matches anywhere in the key", () => {
255
+ const { view }: RenderedModal = renderModal();
256
+
257
+ typeAddSearch(view, "PRODUCT");
258
+
259
+ expect(
260
+ view.getByTestId("add-column-attributes.metadata.product.name"),
261
+ ).toBeInTheDocument();
262
+ });
263
+
264
+ test("surrounding whitespace in the search is ignored", () => {
265
+ const { view }: RenderedModal = renderModal();
266
+
267
+ typeAddSearch(view, " class_uid ");
268
+
269
+ expect(view.getAllByTestId(/^add-column-attributes\./)).toHaveLength(1);
270
+ });
271
+
272
+ test("a search that matches nothing says so", () => {
273
+ const { view }: RenderedModal = renderModal();
274
+
275
+ typeAddSearch(view, "no-such-key");
276
+
277
+ expect(view.getByTestId("add-column-no-results")).toHaveTextContent(
278
+ 'No matches for "no-such-key"',
279
+ );
280
+ expect(view.queryAllByTestId(/^add-column-attributes\./)).toHaveLength(0);
281
+ });
282
+
283
+ test("the search can be cleared, which brings the whole pool back", () => {
284
+ const { view }: RenderedModal = renderModal();
285
+
286
+ typeAddSearch(view, "device");
287
+ expect(view.getAllByTestId(/^add-column-attributes\./)).toHaveLength(1);
288
+
289
+ fireEvent.click(view.getByTestId("add-column-search-clear"));
290
+
291
+ expect(view.getAllByTestId(/^add-column-attributes\./)).toHaveLength(
292
+ POOL_KEYS.length,
293
+ );
294
+ });
295
+
296
+ test("adding puts the column in the list, visible, at the end", () => {
297
+ const { view }: RenderedModal = renderModal();
298
+
299
+ fireEvent.click(view.getByTestId("add-column-attributes.device.hostname"));
300
+
301
+ expect(
302
+ view.getByTestId("column-toggle-attributes.device.hostname"),
303
+ ).toBeChecked();
304
+
305
+ expect(view.getByTestId("column-customization-count")).toHaveTextContent(
306
+ "3 of 4 columns shown",
307
+ );
308
+ });
309
+
310
+ test("an added column is no longer offered in the pool", () => {
311
+ const { view }: RenderedModal = renderModal();
312
+
313
+ fireEvent.click(view.getByTestId("add-column-attributes.device.hostname"));
314
+
315
+ expect(
316
+ view.queryByTestId("add-column-attributes.device.hostname"),
317
+ ).not.toBeInTheDocument();
318
+ expect(view.getAllByTestId(/^add-column-attributes\./)).toHaveLength(
319
+ POOL_KEYS.length - 1,
320
+ );
321
+ });
322
+
323
+ test("adding stages: nothing is handed back before Save", () => {
324
+ const { view, onSave }: RenderedModal = renderModal();
325
+
326
+ fireEvent.click(view.getByTestId("add-column-attributes.device.hostname"));
327
+
328
+ expect(onSave).not.toHaveBeenCalled();
329
+ });
330
+
331
+ test("Save hands back the added column alongside the declared ones", () => {
332
+ const { view, onSave }: RenderedModal = renderModal();
333
+
334
+ fireEvent.click(view.getByTestId("add-column-attributes.device.hostname"));
335
+ fireEvent.click(view.getByTestId("add-column-attributes.class_uid"));
336
+ fireEvent.click(view.getByTestId("modal-footer-submit-button"));
337
+
338
+ expect(getSavedIds(onSave)).toEqual([
339
+ "time",
340
+ "severityName",
341
+ "eventUid",
342
+ "attributes.device.hostname",
343
+ "attributes.class_uid",
344
+ ]);
345
+ });
346
+
347
+ test("Cancel discards an added column", () => {
348
+ const { view, onSave, onClose }: RenderedModal = renderModal();
349
+
350
+ fireEvent.click(view.getByTestId("add-column-attributes.device.hostname"));
351
+ fireEvent.click(view.getByTestId("modal-footer-close-button"));
352
+
353
+ expect(onSave).not.toHaveBeenCalled();
354
+ expect(onClose).toHaveBeenCalled();
355
+ });
356
+
357
+ test("an added column can be reordered like any other", () => {
358
+ const { view, onSave }: RenderedModal = renderModal();
359
+
360
+ fireEvent.click(view.getByTestId("add-column-attributes.device.hostname"));
361
+ fireEvent.click(
362
+ view.getByTestId("column-move-up-attributes.device.hostname"),
363
+ );
364
+ fireEvent.click(view.getByTestId("modal-footer-submit-button"));
365
+
366
+ expect(getSavedIds(onSave)).toEqual([
367
+ "time",
368
+ "severityName",
369
+ "attributes.device.hostname",
370
+ "eventUid",
371
+ ]);
372
+ });
373
+
374
+ test("an added column can be switched off without being removed", () => {
375
+ const { view, onSave }: RenderedModal = renderModal();
376
+
377
+ fireEvent.click(view.getByTestId("add-column-attributes.device.hostname"));
378
+ fireEvent.click(
379
+ view.getByTestId("column-toggle-attributes.device.hostname"),
380
+ );
381
+ fireEvent.click(view.getByTestId("modal-footer-submit-button"));
382
+
383
+ const saved: Array<CustomizableColumn<SecurityEvent>> = onSave.mock
384
+ .calls[0]![0] as Array<CustomizableColumn<SecurityEvent>>;
385
+
386
+ const added: CustomizableColumn<SecurityEvent> | undefined = saved.find(
387
+ (entry: CustomizableColumn<SecurityEvent>) => {
388
+ return entry.id === "attributes.device.hostname";
389
+ },
390
+ );
391
+
392
+ expect(added).toBeDefined();
393
+ expect(added!.isVisible).toBe(false);
394
+ });
395
+
396
+ test("the same pool entry cannot be added twice", () => {
397
+ /*
398
+ * Not reachable through the UI (an added column leaves the pool), but the
399
+ * guard is what keeps two columns with one id out of the stored layout if
400
+ * a double click ever races the re-render.
401
+ */
402
+ const pool: Array<CustomizableColumn<SecurityEvent>> = makePool([
403
+ "device.hostname",
404
+ ]);
405
+
406
+ const { view, onSave }: RenderedModal = renderModal({
407
+ columns: [
408
+ ...getDeclaredColumns(),
409
+ { ...(pool[0] as CustomizableColumn<SecurityEvent>) },
410
+ ],
411
+ addableColumns: {
412
+ title: "Add Attribute Column",
413
+ columns: pool,
414
+ },
415
+ });
416
+
417
+ // Already staged, so the pool has nothing left to offer.
418
+ expect(view.getByTestId("add-column-empty")).toBeInTheDocument();
419
+
420
+ fireEvent.click(view.getByTestId("modal-footer-submit-button"));
421
+
422
+ expect(
423
+ getSavedIds(onSave).filter((id: string) => {
424
+ return id === "attributes.device.hostname";
425
+ }),
426
+ ).toHaveLength(1);
427
+ });
428
+ });
429
+
430
+ describe("ColumnCustomizationModal - removing a column", () => {
431
+ beforeEach(() => {
432
+ window.localStorage.clear();
433
+ });
434
+
435
+ afterEach(() => {
436
+ cleanup();
437
+ jest.restoreAllMocks();
438
+ });
439
+
440
+ test("only removable columns get a remove button", () => {
441
+ const { view }: RenderedModal = renderModal();
442
+
443
+ expect(view.queryByTestId("column-remove-time")).not.toBeInTheDocument();
444
+ expect(
445
+ view.queryByTestId("column-remove-severityName"),
446
+ ).not.toBeInTheDocument();
447
+
448
+ fireEvent.click(view.getByTestId("add-column-attributes.device.hostname"));
449
+
450
+ expect(
451
+ view.getByTestId("column-remove-attributes.device.hostname"),
452
+ ).toBeInTheDocument();
453
+ });
454
+
455
+ test("removing takes the column out of the list", () => {
456
+ const { view }: RenderedModal = renderModal();
457
+
458
+ fireEvent.click(view.getByTestId("add-column-attributes.device.hostname"));
459
+ fireEvent.click(
460
+ view.getByTestId("column-remove-attributes.device.hostname"),
461
+ );
462
+
463
+ expect(
464
+ view.queryByTestId("column-toggle-attributes.device.hostname"),
465
+ ).not.toBeInTheDocument();
466
+ expect(view.getByTestId("column-customization-count")).toHaveTextContent(
467
+ "2 of 3 columns shown",
468
+ );
469
+ });
470
+
471
+ test("a removed column goes back into the pool", () => {
472
+ const { view }: RenderedModal = renderModal();
473
+
474
+ fireEvent.click(view.getByTestId("add-column-attributes.device.hostname"));
475
+ fireEvent.click(
476
+ view.getByTestId("column-remove-attributes.device.hostname"),
477
+ );
478
+
479
+ expect(
480
+ view.getByTestId("add-column-attributes.device.hostname"),
481
+ ).toBeInTheDocument();
482
+ });
483
+
484
+ test("Save without the removed column is what drops it for good", () => {
485
+ const { view, onSave }: RenderedModal = renderModal({
486
+ columns: [
487
+ ...getDeclaredColumns(),
488
+ ...makePool(["device.hostname"]).map(
489
+ (entry: CustomizableColumn<SecurityEvent>) => {
490
+ return { ...entry, isVisible: true };
491
+ },
492
+ ),
493
+ ],
494
+ });
495
+
496
+ fireEvent.click(
497
+ view.getByTestId("column-remove-attributes.device.hostname"),
498
+ );
499
+ fireEvent.click(view.getByTestId("modal-footer-submit-button"));
500
+
501
+ expect(getSavedIds(onSave)).toEqual(["time", "severityName", "eventUid"]);
502
+ });
503
+
504
+ /*
505
+ * Removing is hiding plus forgetting, so it inherits the rule that keeps a
506
+ * table from becoming an empty shell with no header row and no way back.
507
+ */
508
+ test("the last visible column cannot be removed", () => {
509
+ const only: Array<CustomizableColumn<SecurityEvent>> = makePool([
510
+ "device.hostname",
511
+ ]);
512
+
513
+ const { view }: RenderedModal = renderModal({
514
+ columns: only,
515
+ addableColumns: { title: "Add Attribute Column", columns: [] },
516
+ });
517
+
518
+ const removeButton: HTMLElement = view.getByTestId(
519
+ "column-remove-attributes.device.hostname",
520
+ );
521
+
522
+ expect(removeButton).toBeDisabled();
523
+
524
+ fireEvent.click(removeButton);
525
+
526
+ expect(
527
+ view.getByTestId("column-toggle-attributes.device.hostname"),
528
+ ).toBeInTheDocument();
529
+ });
530
+
531
+ test("a hidden removable column can still be removed while another is on", () => {
532
+ const { view, onSave }: RenderedModal = renderModal();
533
+
534
+ fireEvent.click(view.getByTestId("add-column-attributes.device.hostname"));
535
+ fireEvent.click(
536
+ view.getByTestId("column-toggle-attributes.device.hostname"),
537
+ );
538
+ fireEvent.click(
539
+ view.getByTestId("column-remove-attributes.device.hostname"),
540
+ );
541
+ fireEvent.click(view.getByTestId("modal-footer-submit-button"));
542
+
543
+ expect(getSavedIds(onSave)).toEqual(["time", "severityName", "eventUid"]);
544
+ });
545
+ });
546
+
547
+ describe("ColumnCustomizationModal - pool states", () => {
548
+ afterEach(() => {
549
+ cleanup();
550
+ jest.restoreAllMocks();
551
+ });
552
+
553
+ test("says it is loading rather than reading as empty", () => {
554
+ const { view }: RenderedModal = renderModal({
555
+ addableColumns: {
556
+ title: "Add Attribute Column",
557
+ isLoading: true,
558
+ columns: [],
559
+ },
560
+ });
561
+
562
+ expect(view.getByTestId("add-column-loading")).toBeInTheDocument();
563
+ expect(view.queryByTestId("add-column-empty")).not.toBeInTheDocument();
564
+ });
565
+
566
+ test("surfaces a failure instead of pretending there is nothing to add", () => {
567
+ const { view }: RenderedModal = renderModal({
568
+ addableColumns: {
569
+ title: "Add Attribute Column",
570
+ errorMessage: "Could not load attributes.",
571
+ columns: [],
572
+ },
573
+ });
574
+
575
+ expect(view.getByTestId("add-column-error")).toHaveTextContent(
576
+ "Could not load attributes.",
577
+ );
578
+ });
579
+
580
+ /*
581
+ * An error takes precedence over the loading state: a retry that fails
582
+ * again must not flash back to "Loading..." and hide why.
583
+ */
584
+ test("an error wins over a concurrent loading flag", () => {
585
+ const { view }: RenderedModal = renderModal({
586
+ addableColumns: {
587
+ title: "Add Attribute Column",
588
+ isLoading: true,
589
+ errorMessage: "Could not load attributes.",
590
+ columns: [],
591
+ },
592
+ });
593
+
594
+ expect(view.getByTestId("add-column-error")).toBeInTheDocument();
595
+ expect(view.queryByTestId("add-column-loading")).not.toBeInTheDocument();
596
+ });
597
+
598
+ test("an empty pool shows the caller's message", () => {
599
+ const { view }: RenderedModal = renderModal({
600
+ addableColumns: {
601
+ title: "Add Attribute Column",
602
+ emptyMessage: "No attributes seen on recent events.",
603
+ columns: [],
604
+ },
605
+ });
606
+
607
+ expect(view.getByTestId("add-column-empty")).toHaveTextContent(
608
+ "No attributes seen on recent events.",
609
+ );
610
+ });
611
+
612
+ test("a pool whose entries are all already added says something different", () => {
613
+ const pool: Array<CustomizableColumn<SecurityEvent>> = makePool([
614
+ "device.hostname",
615
+ ]);
616
+
617
+ const { view }: RenderedModal = renderModal({
618
+ columns: [...getDeclaredColumns(), ...pool],
619
+ addableColumns: {
620
+ title: "Add Attribute Column",
621
+ emptyMessage: "No attributes seen on recent events.",
622
+ columns: pool,
623
+ },
624
+ });
625
+
626
+ expect(view.getByTestId("add-column-empty")).toHaveTextContent(
627
+ "already in the list above",
628
+ );
629
+ });
630
+
631
+ /*
632
+ * A project can carry thousands of attribute keys. Rendering all of them
633
+ * would put the whole list in the DOM and make the picker crawl, so the
634
+ * list is capped and the cap is said out loud - otherwise someone whose key
635
+ * is not shown has no reason to believe typing more would find it.
636
+ */
637
+ test("caps how many matches are rendered at once", () => {
638
+ const keys: Array<string> = Array.from(
639
+ { length: 40 },
640
+ (_unused: unknown, index: number) => {
641
+ return `attr.key${String(index).padStart(2, "0")}`;
642
+ },
643
+ );
644
+
645
+ const { view }: RenderedModal = renderModal({
646
+ addableColumns: {
647
+ title: "Add Attribute Column",
648
+ columns: makePool(keys),
649
+ maxResults: 10,
650
+ },
651
+ });
652
+
653
+ expect(view.getAllByTestId(/^add-column-attributes\./)).toHaveLength(10);
654
+ expect(view.getByTestId("add-column-hint")).toHaveTextContent(
655
+ "Showing 10 of 40",
656
+ );
657
+ });
658
+
659
+ test("the cap notice disappears once the search fits inside it", () => {
660
+ const keys: Array<string> = Array.from(
661
+ { length: 40 },
662
+ (_unused: unknown, index: number) => {
663
+ return `attr.key${String(index).padStart(2, "0")}`;
664
+ },
665
+ );
666
+
667
+ const { view }: RenderedModal = renderModal({
668
+ addableColumns: {
669
+ title: "Add Attribute Column",
670
+ columns: makePool(keys),
671
+ maxResults: 10,
672
+ },
673
+ });
674
+
675
+ typeAddSearch(view, "key07");
676
+
677
+ expect(view.getAllByTestId(/^add-column-attributes\./)).toHaveLength(1);
678
+ expect(view.queryByTestId("add-column-hint")).not.toBeInTheDocument();
679
+ });
680
+
681
+ test("a key beyond the cap is still reachable by searching for it", () => {
682
+ const keys: Array<string> = Array.from(
683
+ { length: 40 },
684
+ (_unused: unknown, index: number) => {
685
+ return `attr.key${String(index).padStart(2, "0")}`;
686
+ },
687
+ );
688
+
689
+ const { view, onSave }: RenderedModal = renderModal({
690
+ addableColumns: {
691
+ title: "Add Attribute Column",
692
+ columns: makePool(keys),
693
+ maxResults: 10,
694
+ },
695
+ });
696
+
697
+ expect(
698
+ view.queryByTestId("add-column-attributes.attr.key39"),
699
+ ).not.toBeInTheDocument();
700
+
701
+ typeAddSearch(view, "key39");
702
+ fireEvent.click(view.getByTestId("add-column-attributes.attr.key39"));
703
+ fireEvent.click(view.getByTestId("modal-footer-submit-button"));
704
+
705
+ expect(getSavedIds(onSave)).toContain("attributes.attr.key39");
706
+ });
707
+ });
@@ -46,7 +46,7 @@ const PAGE_CHROME_Z_INDEXES: Array<number> = [
46
46
  10 /* MasterPage sticky header */, 20 /* topology + logs viewer toolbars */,
47
47
  ];
48
48
  const OVERLAY_Z_INDEXES_ABOVE_SIDE_OVER: Array<number> = [
49
- 40 /* Toast, AIChatPanel */, 50 /* Modal */, 60 /* portalled dropdowns */,
49
+ 40 /* AIChatPanel */, 50 /* Modal */, 60 /* portalled dropdowns */,
50
50
  ];
51
51
 
52
52
  type ZIndexOfFunction = (element: HTMLElement) => number | null;