@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
@@ -2,6 +2,7 @@ import StatusPageResource from "../../../Models/DatabaseModels/StatusPageResourc
2
2
  import IconProp from "../../../Types/Icon/IconProp";
3
3
  import StatusPageResourceExplorerUtil from "../../../Utils/StatusPage/ResourceExplorer";
4
4
  import Button, { ButtonSize, ButtonStyleType } from "../Button/Button";
5
+ import CheckboxElement from "../Checkbox/Checkbox";
5
6
  import Icon from "../Icon/Icon";
6
7
  import MoreMenu from "../MoreMenu/MoreMenu";
7
8
  import MoreMenuItem from "../MoreMenu/MoreMenuItem";
@@ -40,6 +41,28 @@ export interface ComponentProps {
40
41
  */
41
42
  isReorderable: boolean;
42
43
 
44
+ /*
45
+ * Whether the rows carry checkboxes at all.
46
+ *
47
+ * Off for anybody who cannot remove a resource. Removing is the only thing a
48
+ * selection is for here, so without it the column would be a row of controls
49
+ * that lead nowhere - and it would cost every row 28px of the width its
50
+ * monitor's name is drawn in.
51
+ */
52
+ isSelectable: boolean;
53
+ /* Held by the caller: a selection outlives this list's re-renders. */
54
+ selectedResourceIds: Set<string>;
55
+ /*
56
+ * The state of the box at the top of the list, computed by the caller over
57
+ * everything this list was handed - which is the whole point of it being
58
+ * computed there. A filtered list's "select all" means the rows the search
59
+ * matched, and rows held back behind "Show more" are matched rows too.
60
+ */
61
+ isAllSelected: boolean;
62
+ isSelectionIndeterminate: boolean;
63
+ onToggleResourceSelected: (statusPageResource: StatusPageResource) => void;
64
+ onToggleAllSelected: (isSelected: boolean) => void;
65
+
43
66
  onEdit: (statusPageResource: StatusPageResource) => void;
44
67
  onDelete: (statusPageResource: StatusPageResource) => void;
45
68
  onShowId: (statusPageResource: StatusPageResource) => void;
@@ -90,6 +113,12 @@ export interface ComponentProps {
90
113
  * rather than one copy per breakpoint.
91
114
  */
92
115
  const GRIP_COLUMN_CLASS_NAME: string = "w-5 flex-shrink-0";
116
+ /*
117
+ * Wider than the 16px box it holds: the checkbox draws an empty label beside
118
+ * itself with a 12px gutter, and a narrower column would push the grip out from
119
+ * under the header's own box.
120
+ */
121
+ const SELECT_COLUMN_CLASS_NAME: string = "flex w-7 flex-shrink-0 items-center";
93
122
  /*
94
123
  * Held back on a phone, where 24px of number costs a fifth of what is left for
95
124
  * the monitor's name and the rows are already in the order it describes.
@@ -239,6 +268,49 @@ const ResourceList: FunctionComponent<ComponentProps> = (
239
268
  );
240
269
  };
241
270
 
271
+ type RenderSelectFunction = (
272
+ statusPageResource: StatusPageResource,
273
+ ) => ReactElement;
274
+
275
+ /*
276
+ * One row's checkbox. Nothing is drawn at all when selection is off, rather
277
+ * than an empty column: unlike the grip - which is held open so that typing
278
+ * into the search box does not shift every row sideways - the checkbox column
279
+ * appears and disappears with the operator's permissions, which do not change
280
+ * while they are looking at the list.
281
+ */
282
+ const renderSelect: RenderSelectFunction = (
283
+ statusPageResource: StatusPageResource,
284
+ ): ReactElement => {
285
+ if (!props.isSelectable) {
286
+ return <></>;
287
+ }
288
+
289
+ const resourceId: string | null =
290
+ StatusPageResourceExplorerUtil.getResourceId(statusPageResource);
291
+ const name: string =
292
+ StatusPageResourceExplorerUtil.getResourceName(statusPageResource);
293
+
294
+ return (
295
+ <span
296
+ className={SELECT_COLUMN_CLASS_NAME}
297
+ data-testid="status-page-resource-row-select"
298
+ >
299
+ <CheckboxElement
300
+ value={Boolean(
301
+ resourceId && props.selectedResourceIds.has(resourceId),
302
+ )}
303
+ disabled={!resourceId}
304
+ ariaLabel={`Select ${name}`}
305
+ hoverText={`Select ${name}`}
306
+ onChange={() => {
307
+ props.onToggleResourceSelected(statusPageResource);
308
+ }}
309
+ />
310
+ </span>
311
+ );
312
+ };
313
+
242
314
  type RenderRowBodyFunction = (
243
315
  statusPageResource: StatusPageResource,
244
316
  index: number,
@@ -256,6 +328,8 @@ const ResourceList: FunctionComponent<ComponentProps> = (
256
328
 
257
329
  return (
258
330
  <>
331
+ {renderSelect(statusPageResource)}
332
+
259
333
  {dragHandle}
260
334
 
261
335
  {/*
@@ -347,32 +421,60 @@ const ResourceList: FunctionComponent<ComponentProps> = (
347
421
  );
348
422
  };
349
423
 
350
- type GetRowClassNameFunction = (
351
- isDragging: boolean,
352
- isBusy: boolean,
353
- isLastRow: boolean,
354
- ) => string;
355
-
356
- const getRowClassName: GetRowClassNameFunction = (
357
- isDragging: boolean,
358
- isBusy: boolean,
359
- isLastRow: boolean,
360
- ): string => {
424
+ type GetRowClassNameFunction = (data: {
425
+ isDragging: boolean;
426
+ isBusy: boolean;
427
+ isLastRow: boolean;
428
+ isSelected: boolean;
429
+ }) => string;
430
+
431
+ const getRowClassName: GetRowClassNameFunction = (data: {
432
+ isDragging: boolean;
433
+ isBusy: boolean;
434
+ isLastRow: boolean;
435
+ isSelected: boolean;
436
+ }): string => {
437
+ /*
438
+ * A selected row is filled, so that a selection made near the top of a long
439
+ * list is still visible from the bottom of it - the count in the bulk bar
440
+ * says how many, and this says which.
441
+ *
442
+ * Un-suffixed colour tokens throughout, for the same reason the dragged row
443
+ * uses them: the dark theme remaps those and not the transparent ones, so
444
+ * an opacity modifier leaves a light row in a dark list.
445
+ */
446
+ const restingClassName: string = data.isSelected
447
+ ? "bg-indigo-50 hover:bg-indigo-100"
448
+ : "bg-white hover:bg-gray-50";
449
+
361
450
  return `${ROW_CLASS_NAME} ${
362
- isDragging
451
+ data.isDragging
363
452
  ? /*
364
- * bg-indigo-50 rather than bg-indigo-50/70: the dark theme remaps the
365
- * un-suffixed colour tokens and not the transparent ones, so an
366
- * opacity modifier leaves a light row in a dark list.
367
- *
368
453
  * A lifted row is rounded on all four corners because it is off the
369
454
  * list while it is being dragged; a settled one only rounds where the
370
455
  * container does, so its hover fill cannot square off the card's own
371
456
  * bottom corners.
372
457
  */
373
458
  "rounded-xl bg-indigo-50 shadow-md ring-1 ring-indigo-200"
374
- : `bg-white hover:bg-gray-50 ${isLastRow ? "rounded-b-xl" : ""}`
375
- } ${isBusy ? "opacity-60" : ""}`;
459
+ : `${restingClassName} ${data.isLastRow ? "rounded-b-xl" : ""}`
460
+ } ${data.isBusy ? "opacity-60" : ""}`;
461
+ };
462
+
463
+ type IsResourceSelectedFunction = (
464
+ statusPageResource: StatusPageResource,
465
+ ) => boolean;
466
+
467
+ const isResourceSelected: IsResourceSelectedFunction = (
468
+ statusPageResource: StatusPageResource,
469
+ ): boolean => {
470
+ if (!props.isSelectable) {
471
+ return false;
472
+ }
473
+
474
+ const resourceId: string | null =
475
+ StatusPageResourceExplorerUtil.getResourceId(statusPageResource);
476
+
477
+ return Boolean(resourceId && props.selectedResourceIds.has(resourceId));
376
478
  };
377
479
 
378
480
  type IsLastRowFunction = (index: number) => boolean;
@@ -388,32 +490,82 @@ const ResourceList: FunctionComponent<ComponentProps> = (
388
490
  type RenderHeaderFunction = () => ReactElement;
389
491
 
390
492
  /*
391
- * Column headings, from sm up. They are what makes the second column read as
392
- * "the name visitors see" rather than as a stray subtitle, and they are the
393
- * only place the position column can be explained at all.
493
+ * Column headings, from sm up, and the box that selects the whole list.
494
+ *
495
+ * The headings are what makes the second column read as "the name visitors
496
+ * see" rather than as a stray subtitle, and they are the only place the
497
+ * position column can be explained at all. Below sm the published name wraps
498
+ * under the monitor instead of sitting beside it, so they would be labelling
499
+ * columns that are not there - and the row is hidden.
394
500
  *
395
- * Below sm the published name wraps under the monitor instead of sitting
396
- * beside it, so a header row would be labelling columns that are not there.
501
+ * The select-all box is not a heading, though, and a phone is exactly where
502
+ * removing twenty seven monitors one at a time hurts most. So when there is a
503
+ * checkbox column the row is drawn at every width, with the headings held
504
+ * back below sm and the box given a visible label of its own instead.
505
+ *
506
+ * aria-hidden sits on the individual headings rather than on the row, because
507
+ * the row now contains a control: hiding a focusable checkbox from the
508
+ * accessibility tree while leaving it in the tab order is worse than not
509
+ * offering it.
397
510
  */
398
511
  const renderHeader: RenderHeaderFunction = (): ReactElement => {
399
512
  return (
400
513
  <div
401
- className="hidden items-center gap-x-3 rounded-t-xl border-b border-gray-200 bg-gray-50 px-4 py-2 text-[11px] font-semibold uppercase tracking-wide text-gray-500 sm:flex"
514
+ className={`${
515
+ props.isSelectable ? "flex" : "hidden sm:flex"
516
+ } items-center gap-x-3 rounded-t-xl border-b border-gray-200 bg-gray-50 px-4 py-2 text-[11px] font-semibold uppercase tracking-wide text-gray-500 sm:flex`}
402
517
  data-testid="status-page-resource-list-header"
403
- aria-hidden="true"
404
518
  >
405
- <span className={GRIP_COLUMN_CLASS_NAME} />
519
+ {props.isSelectable ? (
520
+ <span className={SELECT_COLUMN_CLASS_NAME}>
521
+ <CheckboxElement
522
+ value={props.isAllSelected}
523
+ isIndeterminate={props.isSelectionIndeterminate}
524
+ ariaLabel="Select all resources"
525
+ hoverText="Select all resources"
526
+ dataTestId="status-page-resource-list-select-all"
527
+ onChange={(value: boolean) => {
528
+ props.onToggleAllSelected(value);
529
+ }}
530
+ />
531
+ </span>
532
+ ) : (
533
+ <></>
534
+ )}
535
+
536
+ {props.isSelectable ? (
537
+ <span className="sm:hidden">Select all</span>
538
+ ) : (
539
+ <></>
540
+ )}
541
+
542
+ <span
543
+ className={`${GRIP_COLUMN_CLASS_NAME} hidden sm:block`}
544
+ aria-hidden="true"
545
+ />
406
546
  <span
407
547
  className={POSITION_COLUMN_CLASS_NAME}
408
548
  title="Position on the status page"
549
+ aria-hidden="true"
409
550
  >
410
551
  #
411
552
  </span>
412
- <span className={MONITOR_COLUMN_CLASS_NAME}>Monitor</span>
413
- <span className={PUBLISHED_NAME_COLUMN_CLASS_NAME}>
553
+ <span
554
+ className={`${MONITOR_COLUMN_CLASS_NAME} hidden sm:block`}
555
+ aria-hidden="true"
556
+ >
557
+ Monitor
558
+ </span>
559
+ <span
560
+ className={`${PUBLISHED_NAME_COLUMN_CLASS_NAME} hidden sm:block`}
561
+ aria-hidden="true"
562
+ >
414
563
  Shown on the status page as
415
564
  </span>
416
- <span className={ACTIONS_COLUMN_CLASS_NAME} />
565
+ <span
566
+ className={`${ACTIONS_COLUMN_CLASS_NAME} hidden sm:flex`}
567
+ aria-hidden="true"
568
+ />
417
569
  </div>
418
570
  );
419
571
  };
@@ -437,11 +589,13 @@ const ResourceList: FunctionComponent<ComponentProps> = (
437
589
  key={resourceId}
438
590
  data-testid="status-page-resource-row"
439
591
  data-resource-id={resourceId}
440
- className={getRowClassName(
441
- false,
442
- props.busyResourceId === resourceId,
443
- isLastRow(index),
444
- )}
592
+ data-selected={isResourceSelected(statusPageResource)}
593
+ className={getRowClassName({
594
+ isDragging: false,
595
+ isBusy: props.busyResourceId === resourceId,
596
+ isLastRow: isLastRow(index),
597
+ isSelected: isResourceSelected(statusPageResource),
598
+ })}
445
599
  >
446
600
  {renderRowBody(
447
601
  statusPageResource,
@@ -497,11 +651,16 @@ const ResourceList: FunctionComponent<ComponentProps> = (
497
651
  {...draggableProvided.draggableProps}
498
652
  data-testid="status-page-resource-row"
499
653
  data-resource-id={resourceId}
500
- className={getRowClassName(
501
- snapshot.isDragging,
502
- props.busyResourceId === resourceId,
503
- isLastRow(index),
654
+ data-selected={isResourceSelected(
655
+ statusPageResource,
504
656
  )}
657
+ className={getRowClassName({
658
+ isDragging: snapshot.isDragging,
659
+ isBusy: props.busyResourceId === resourceId,
660
+ isLastRow: isLastRow(index),
661
+ isSelected:
662
+ isResourceSelected(statusPageResource),
663
+ })}
505
664
  >
506
665
  {renderRowBody(
507
666
  statusPageResource,
@@ -78,8 +78,16 @@ const Toggle: FunctionComponent<ComponentProps> = (
78
78
  if (props.onBlur) {
79
79
  props.onBlur();
80
80
  }
81
+ /*
82
+ * handleChange already calls props.onChange. Calling it a second
83
+ * time here ran every consumer's handler twice per click, which an
84
+ * idempotent handler never notices and a non-idempotent one cannot
85
+ * survive. The monitor criteria switches are one click away from
86
+ * that: they seed a blank incident / alert row on the way on, and
87
+ * only the "is the array still empty" guard around that seed kept
88
+ * the second pass from adding a second row.
89
+ */
81
90
  handleChange(!isChecked);
82
- props.onChange(!isChecked);
83
91
  }}
84
92
  onFocus={() => {
85
93
  if (props.onFocus) {
@@ -31,6 +31,7 @@ enum FieldType {
31
31
  JavaScript = "JavaScript",
32
32
  DictionaryOfStrings = "DictionaryOfStrings",
33
33
  JSON = "JSON",
34
+ YAML = "YAML",
34
35
  USDCents = "USDCents",
35
36
  Element = "Element",
36
37
  Minutes = "Minutes",
@@ -1,4 +1,5 @@
1
1
  import OneUptimeDate from "../../Types/Date";
2
+ import { NetworkDeviceMonitoringMethodUtil } from "../../Types/NetworkDevice/NetworkDeviceMonitoringMethod";
2
3
 
3
4
  /*
4
5
  * The one place that decides whether a NetworkDevice is reachable.
@@ -23,6 +24,14 @@ import OneUptimeDate from "../../Types/Date";
23
24
  * NetworkDevice.isReachable, stamped by the walk pipeline on every ingested
24
25
  * walk — and freshness is only a backstop for "the polling pipeline itself
25
26
  * stopped", measured generously against the device's own interval.
27
+ *
28
+ * All of which is about a device that gets POLLED. A monitor-backed device
29
+ * (monitoringMethod "Monitor") is never polled at all: no probe, no
30
+ * credentials, no walk. Every poll column on such a row is NULL forever, so
31
+ * the rule above can only ever answer "Pending" for it — which is
32
+ * OneUptime/oneuptime#3392, a correctly bound ping-only device stuck on
33
+ * Pending. Its health comes from the Monitor bound to it, stamped onto
34
+ * NetworkDevice.currentMonitorStatusId, and that is the branch below.
26
35
  */
27
36
 
28
37
  export enum NetworkDeviceReachability {
@@ -71,6 +80,28 @@ export interface DeviceReachabilityInput {
71
80
  lastSeenAt?: Date | string | null | undefined;
72
81
  // The device's own schedule; sizes the staleness backstop.
73
82
  pollingIntervalInMinutes?: number | null | undefined;
83
+ /*
84
+ * How this device's health is established. NULL, empty and anything
85
+ * unrecognised read as SNMP — see NetworkDeviceMonitoringMethodUtil.parse,
86
+ * which is why an omitted value keeps every existing caller on the poll
87
+ * rule unchanged.
88
+ */
89
+ monitoringMethod?: string | null | undefined;
90
+ /*
91
+ * The OFFLINE end of the device's stamped MonitorStatus row, and only
92
+ * consulted for a monitor-backed device. `undefined` means no monitor has
93
+ * reported yet (nothing bound, or bound and never evaluated), which is a
94
+ * real "Pending" rather than a healthy default.
95
+ *
96
+ * The offline end and not the operational end, because MonitorStatus is a
97
+ * ladder rather than a pair: a "Degraded" row is NEITHER operational nor
98
+ * offline, and the device map already resolves that ladder with
99
+ * `isOfflineState ? down : up`. Reading the operational flag here instead
100
+ * would paint every degraded-but-reachable device red on one surface and
101
+ * green on the other. See DeviceHealthStateUtil, which reads it the same
102
+ * way for the same reason.
103
+ */
104
+ monitorStatusIsOffline?: boolean | null | undefined;
74
105
  }
75
106
 
76
107
  export interface DeviceReachabilityResult {
@@ -87,6 +118,13 @@ export interface DeviceReachabilityResult {
87
118
  staleWindowInMinutes: number;
88
119
  // Latest of lastPolledAt / lastSeenAt; null when neither is set.
89
120
  lastContactAt: Date | null;
121
+ /*
122
+ * True when this verdict came from the bound Monitor rather than from an
123
+ * SNMP poll. Carried so a surface can word its pill and its tooltip for
124
+ * what actually decided the answer — "the last SNMP poll reached this
125
+ * device" is a lie on a device nothing polls.
126
+ */
127
+ isMonitorBacked: boolean;
90
128
  }
91
129
 
92
130
  const MS_PER_MINUTE: number = 60 * 1000;
@@ -153,6 +191,11 @@ export default class DeviceReachabilityUtil {
153
191
  staleWindowInMinutes * MS_PER_MINUTE
154
192
  : false;
155
193
 
194
+ const isMonitorBacked: boolean =
195
+ NetworkDeviceMonitoringMethodUtil.isMonitorBacked(
196
+ device.monitoringMethod,
197
+ );
198
+
156
199
  const result: (
157
200
  status: NetworkDeviceReachability,
158
201
  ) => DeviceReachabilityResult = (
@@ -160,12 +203,41 @@ export default class DeviceReachabilityUtil {
160
203
  ): DeviceReachabilityResult => {
161
204
  return {
162
205
  status: status,
163
- isStale: isStale,
206
+ /*
207
+ * Staleness is "nothing has polled this device lately", so it can
208
+ * only ever be false for a device nothing polls BY DESIGN. Letting
209
+ * it through would put an amber "check this device's probe" pill on
210
+ * every monitor-backed device forever — and it has no probe.
211
+ */
212
+ isStale: isMonitorBacked ? false : isStale,
164
213
  staleWindowInMinutes: staleWindowInMinutes,
165
214
  lastContactAt: lastContactAt,
215
+ isMonitorBacked: isMonitorBacked,
166
216
  };
167
217
  };
168
218
 
219
+ /*
220
+ * Monitor-backed: the bound Monitor's verdict IS the device's, and the
221
+ * poll columns below are meaningless for it (they are NULL forever, and
222
+ * on a device switched over from SNMP they are worse than meaningless —
223
+ * they are the last thing a probe found before it stopped asking).
224
+ *
225
+ * No stamped status means Pending, and it is honest in both of the ways
226
+ * it happens: no monitor is bound yet (discovery import creates devices
227
+ * that way on purpose), or one is bound and has not been evaluated yet.
228
+ */
229
+ if (isMonitorBacked) {
230
+ if (device.monitorStatusIsOffline === true) {
231
+ return result(NetworkDeviceReachability.Down);
232
+ }
233
+
234
+ if (device.monitorStatusIsOffline === false) {
235
+ return result(NetworkDeviceReachability.Up);
236
+ }
237
+
238
+ return result(NetworkDeviceReachability.Pending);
239
+ }
240
+
169
241
  /*
170
242
  * The probe asked and got nothing. Authoritative however long ago it
171
243
  * was — nothing since has said otherwise.
@@ -0,0 +1,181 @@
1
+ import ColumnLength from "../../Types/Database/ColumnLength";
2
+
3
+ /*
4
+ * The optional operator-supplied name on a Network Device Discovery Scan.
5
+ *
6
+ * WHY IT EXISTS
7
+ *
8
+ * A scan used to be identified by its target alone — `10.15.128.0-255`,
9
+ * `10.240-249.0-254.220-226`. Half a dozen rows in, nothing on the Discovery
10
+ * Scans list says which one was the router sweep and which one was the switch
11
+ * range for a region; the only way to find out is to open each scan and read
12
+ * the range back against a subnet plan kept somewhere else (OneUptime issue
13
+ * #3391). A name is that missing sentence, stored with the scan rather than in
14
+ * a spreadsheet beside it.
15
+ *
16
+ * It is deliberately OPTIONAL and deliberately NOT unique: it is a label for
17
+ * humans, not an identifier. Every scan that existed before this column did
18
+ * has none, and every surface that shows a name therefore has to keep working
19
+ * when it is absent — which is what getScanLabel() below is for.
20
+ *
21
+ * WHY IT LIVES IN Common
22
+ *
23
+ * The same two questions — "is this name storable?" and "what do I call this
24
+ * scan?" — are asked by the create form (App Dashboard), the write hooks
25
+ * (Common server service), the recurring-scan worker (App Workers) and the
26
+ * sweep itself (Probe). Answering them in one module is what keeps the form's
27
+ * error message identical to the server's, and the probe's log line readable
28
+ * against the row the operator is looking at. Same reason ScanTargetUtil sits
29
+ * next door.
30
+ */
31
+
32
+ /*
33
+ * The parts of a scan that answer "which scan is this?". Structural rather
34
+ * than the model type, so a partially-selected row — the workers and the probe
35
+ * both select two or three columns — satisfies it, and so this module stays
36
+ * importable from the Probe without dragging a database model behind it.
37
+ *
38
+ * Both are nullable: `cidr` is NOT NULL in the database, but a caller that did
39
+ * not select it hands over a row where it is simply missing.
40
+ */
41
+ export interface DiscoveryScanIdentity {
42
+ name?: string | null | undefined;
43
+ cidr?: string | null | undefined;
44
+ }
45
+
46
+ export class ScanNameUtil {
47
+ /*
48
+ * Matches the column: NetworkDeviceDiscoveryScan.name is a ShortText, so a
49
+ * longer value does not truncate in Postgres — it throws, failing the whole
50
+ * INSERT. Read off ColumnLength rather than written as 100 so the two cannot
51
+ * drift apart if the column type ever changes.
52
+ */
53
+ public static readonly MAX_SCAN_NAME_LENGTH: number = ColumnLength.ShortText;
54
+
55
+ /*
56
+ * ASCII control characters, including DEL. Written as escapes rather than as
57
+ * a literal range so the characters this guards against cannot end up inside
58
+ * the guard itself, and so the file stays readable in a diff.
59
+ *
60
+ * no-control-regex is disabled deliberately, the same way
61
+ * MetricResourceAttributeUtil disables it: a control character inside a
62
+ * user-typed name is precisely what this has to match in order to remove it.
63
+ */
64
+ // eslint-disable-next-line no-control-regex
65
+ private static readonly CONTROL_CHARACTERS: RegExp = /[\u0000-\u001F\u007F]/g;
66
+
67
+ /*
68
+ * The name as it should be stored, or null when the operator supplied none.
69
+ *
70
+ * Three things happen here, in this order:
71
+ *
72
+ * - anything that is not a string becomes null. The value arrives straight
73
+ * from request JSON (BaseAPI assigns ShortText columns verbatim), so a
74
+ * client really can hand over a number, an object or an array. Callers
75
+ * that want to REJECT those call getValidationError() first; this
76
+ * function's job is only to say what would be stored.
77
+ * - control characters — a newline pasted out of a spreadsheet cell, a
78
+ * stray tab — become spaces, and every run of whitespace collapses to a
79
+ * single space. A name is rendered on one line in a table cell and
80
+ * inlined into log messages; a multi-line one breaks both, and nothing
81
+ * downstream would ever repair it.
82
+ * - the result is trimmed, and an empty result is null rather than "".
83
+ * "Not named" is one state, not two: an empty string in the column would
84
+ * read as a name everywhere `name ?` is asked, and render as a blank
85
+ * first line above the scan target.
86
+ */
87
+ public static normalize(name: unknown): string | null {
88
+ if (typeof name !== "string") {
89
+ return null;
90
+ }
91
+
92
+ const collapsed: string = name
93
+ .replace(ScanNameUtil.CONTROL_CHARACTERS, " ")
94
+ .replace(/\s+/g, " ")
95
+ .trim();
96
+
97
+ return collapsed.length > 0 ? collapsed : null;
98
+ }
99
+
100
+ /*
101
+ * The single validation entry point for a scan name, shared by the create
102
+ * form and both write hooks. Returns null when the name is storable —
103
+ * INCLUDING when there is no name at all, because the field is optional and
104
+ * "" is a perfectly good way to say so.
105
+ *
106
+ * `name` is typed unknown for the same reason ScanTargetUtil's target is:
107
+ * the server hook runs before the model's own type and length checks, so it
108
+ * is the first thing to see whatever the client actually sent.
109
+ */
110
+ public static getValidationError(name: unknown): string | null {
111
+ if (name === undefined || name === null) {
112
+ return null;
113
+ }
114
+
115
+ if (typeof name !== "string") {
116
+ return "A scan name must be text.";
117
+ }
118
+
119
+ const normalized: string | null = ScanNameUtil.normalize(name);
120
+
121
+ if (normalized === null) {
122
+ return null;
123
+ }
124
+
125
+ /*
126
+ * Measured against the NORMALIZED value, which is what would be stored:
127
+ * rejecting a 101-character value that is 100 characters plus a trailing
128
+ * newline would be refusing to save something this module was about to fix
129
+ * anyway.
130
+ */
131
+ if (normalized.length > ScanNameUtil.MAX_SCAN_NAME_LENGTH) {
132
+ return (
133
+ `A scan name cannot be longer than ${ScanNameUtil.MAX_SCAN_NAME_LENGTH} characters. ` +
134
+ `This one is ${normalized.length}.`
135
+ );
136
+ }
137
+
138
+ return null;
139
+ }
140
+
141
+ /*
142
+ * The name to show for a scan, or null when it has none — so a caller can
143
+ * render the scan target instead rather than an empty line.
144
+ *
145
+ * Normalized on the way out as well as on the way in, because rows written
146
+ * before this column existed, and rows written by any writer that bypasses
147
+ * the hooks (migrations, the probe-ingest endpoints), are not guaranteed to
148
+ * have been through normalize().
149
+ */
150
+ public static getDisplayName(scan: DiscoveryScanIdentity): string | null {
151
+ return ScanNameUtil.normalize(scan.name);
152
+ }
153
+
154
+ /*
155
+ * How to refer to a scan in one line of running text — a log message, a
156
+ * modal's description, a status note.
157
+ *
158
+ * A named scan carries its target along with it (`Router Discovery — Region
159
+ * 1100 (10.15.128.0-255)`): the name says which scan, the target says what
160
+ * it sweeps, and an operator reading a log line usually wants both. An
161
+ * unnamed one is exactly what it was before this column existed — the target
162
+ * alone.
163
+ *
164
+ * Returns "" when the row has neither, which happens only when the caller
165
+ * selected neither column. Callers append their own fallback (the scan id)
166
+ * rather than having one invented for them here.
167
+ */
168
+ public static getScanLabel(scan: DiscoveryScanIdentity): string {
169
+ const name: string | null = ScanNameUtil.getDisplayName(scan);
170
+ const target: string =
171
+ typeof scan.cidr === "string" ? scan.cidr.trim() : "";
172
+
173
+ if (name && target) {
174
+ return `${name} (${target})`;
175
+ }
176
+
177
+ return name || target || "";
178
+ }
179
+ }
180
+
181
+ export default ScanNameUtil;