@oneuptime/common 11.7.2 → 11.7.4

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 (414) hide show
  1. package/Models/AnalyticsModels/KubernetesCostAllocation.ts +103 -0
  2. package/Models/DatabaseModels/AlertEpisodeStateTimeline.ts +1 -0
  3. package/Models/DatabaseModels/AlertStateTimeline.ts +1 -0
  4. package/Models/DatabaseModels/IncidentEpisodeStateTimeline.ts +1 -0
  5. package/Models/DatabaseModels/IncidentStateTimeline.ts +1 -0
  6. package/Models/DatabaseModels/Index.ts +5 -0
  7. package/Models/DatabaseModels/LogDropFilter.ts +89 -0
  8. package/Models/DatabaseModels/NetworkDeviceDiscoveryScan.ts +13 -3
  9. package/Models/DatabaseModels/RecommendationDismissal.ts +463 -0
  10. package/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.ts +1 -0
  11. package/Models/DatabaseModels/StatusPageGroup.ts +95 -0
  12. package/Models/DatabaseModels/TableView.ts +40 -0
  13. package/Models/DatabaseModels/TraceDropFilter.ts +89 -0
  14. package/Models/DatabaseModels/WorkflowVariable.ts +5 -1
  15. package/Server/API/AlertAPI.ts +32 -17
  16. package/Server/API/BaseAPI.ts +31 -2
  17. package/Server/API/BaseAnalyticsAPI.ts +36 -2
  18. package/Server/API/CommonAPI.ts +57 -0
  19. package/Server/API/IncidentAPI.ts +60 -33
  20. package/Server/API/IncidentEpisodeAPI.ts +31 -16
  21. package/Server/API/MonitorAPI.ts +30 -0
  22. package/Server/API/MonitorTemplateAPI.ts +8 -0
  23. package/Server/API/ProbeAPI.ts +196 -15
  24. package/Server/API/ProjectAPI.ts +79 -1
  25. package/Server/API/ScheduledMaintenanceAPI.ts +33 -18
  26. package/Server/API/StatusPageAPI.ts +74 -37
  27. package/Server/API/WorkspaceNotificationRuleAPI.ts +34 -4
  28. package/Server/Infrastructure/Postgres/SchemaMigrations/1785241000000-AddColumnsToTableView.ts +13 -0
  29. package/Server/Infrastructure/Postgres/SchemaMigrations/1785320000000-RepairCrossProjectIncidentReferences.ts +527 -0
  30. package/Server/Infrastructure/Postgres/SchemaMigrations/1785329453269-AddParentGroupToStatusPageGroup.ts +31 -0
  31. package/Server/Infrastructure/Postgres/SchemaMigrations/1785397674289-AddRecommendationDismissalTable.ts +55 -0
  32. package/Server/Infrastructure/Postgres/SchemaMigrations/1785405581596-AddDroppedCountersToDropFilters.ts +53 -0
  33. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +10 -0
  34. package/Server/Services/AlertEpisodeService.ts +47 -0
  35. package/Server/Services/AlertService.ts +125 -0
  36. package/Server/Services/BillingService.ts +98 -0
  37. package/Server/Services/DatabaseService.ts +184 -20
  38. package/Server/Services/IncidentEpisodeService.ts +47 -0
  39. package/Server/Services/IncidentService.ts +123 -0
  40. package/Server/Services/IncidentTemplateService.ts +123 -1
  41. package/Server/Services/Index.ts +2 -0
  42. package/Server/Services/LogDropFilterService.ts +133 -0
  43. package/Server/Services/MonitorProbeService.ts +94 -4
  44. package/Server/Services/MonitorService.ts +48 -43
  45. package/Server/Services/NetworkDeviceDiscoveryScanService.ts +70 -0
  46. package/Server/Services/ProbeService.ts +66 -0
  47. package/Server/Services/ProjectService.ts +131 -7
  48. package/Server/Services/RecommendationDismissalService.ts +52 -0
  49. package/Server/Services/RunbookAgentJobService.ts +68 -7
  50. package/Server/Services/ScheduledMaintenanceService.ts +103 -0
  51. package/Server/Services/ScheduledMaintenanceTemplateService.ts +90 -0
  52. package/Server/Services/StatusPageGroupService.ts +186 -0
  53. package/Server/Services/TraceDropFilterService.ts +133 -0
  54. package/Server/Types/Workflow/Components/API/Delete.ts +12 -0
  55. package/Server/Types/Workflow/Components/API/Get.ts +12 -0
  56. package/Server/Types/Workflow/Components/API/Patch.ts +12 -0
  57. package/Server/Types/Workflow/Components/API/Post.ts +12 -0
  58. package/Server/Types/Workflow/Components/API/Put.ts +13 -0
  59. package/Server/Utils/Database/ProjectScopedReferenceValidator.ts +212 -0
  60. package/Server/Utils/DropFilterValidation.ts +127 -0
  61. package/Server/Utils/Errors.ts +5 -0
  62. package/Server/Utils/Monitor/MonitorAlert.ts +28 -1
  63. package/Server/Utils/Monitor/MonitorIncident.ts +31 -2
  64. package/Server/Utils/Telemetry/AppMetrics.ts +31 -0
  65. package/Tests/Models/AnalyticsModels/KubernetesCostAllocation.test.ts +16 -0
  66. package/Tests/Models/DatabaseModels/MonitorProbeColumnAccessControl.test.ts +196 -0
  67. package/Tests/Models/RecommendationDismissalModel.test.ts +269 -0
  68. package/Tests/Server/API/BaseAPIUpdatePayloadValidation.test.ts +320 -0
  69. package/Tests/Server/API/BaseAnalyticsAPIUpdatePayloadValidation.test.ts +293 -0
  70. package/Tests/Server/API/CommonAPIAuthGuard.test.ts +164 -0
  71. package/Tests/Server/API/ProbeAPI.test.ts +422 -53
  72. package/Tests/Server/API/ProjectAPI.test.ts +180 -0
  73. package/Tests/Server/API/ProjectAPIExtendTrialBillingDisabled.test.ts +138 -0
  74. package/Tests/Server/API/ProjectScopedCustomRouteAuth.test.ts +458 -0
  75. package/Tests/Server/Infrastructure/ContainerBootConfiguration.test.ts +359 -0
  76. package/Tests/Server/Infrastructure/ProcessSignalDelivery.test.ts +427 -0
  77. package/Tests/Server/Services/BillingServiceExtendTrial.test.ts +439 -0
  78. package/Tests/Server/Services/CrossProjectReferenceWriteGuard.test.ts +810 -0
  79. package/Tests/Server/Services/DatabaseServiceAtomicAddToColumns.test.ts +269 -0
  80. package/Tests/Server/Services/DatabaseServiceEmptyUpdate.test.ts +277 -0
  81. package/Tests/Server/Services/DatabaseServiceSortColumnSelect.test.ts +201 -0
  82. package/Tests/Server/Services/DiscoveryScanClaimHookFreeSafety.test.ts +81 -23
  83. package/Tests/Server/Services/DropFilterSaveValidation.test.ts +398 -0
  84. package/Tests/Server/Services/MonitorProbeDeleteHooks.test.ts +280 -0
  85. package/Tests/Server/Services/MonitorProbeServiceProbeTenancy.test.ts +304 -0
  86. package/Tests/Server/Services/NetworkDeviceDiscoveryScanService.test.ts +262 -0
  87. package/Tests/Server/Services/ProjectServiceExtendTrial.test.ts +614 -0
  88. package/Tests/Server/Services/RecommendationDismissalService.test.ts +306 -0
  89. package/Tests/Server/Services/RepairCrossProjectIncidentReferencesMigration.test.ts +528 -0
  90. package/Tests/Server/Services/StatusPageGroupNesting.test.ts +395 -0
  91. package/Tests/Server/TestingUtils/Services/BillingServiceHelper.ts +26 -21
  92. package/Tests/Server/Types/Database/Permissions/WorkflowVariableColumnPermission.test.ts +227 -0
  93. package/Tests/Server/Types/Workflow/Components/ApiComponentErrorPort.test.ts +285 -0
  94. package/Tests/Server/Utils/Database/ProjectScopedReferenceValidator.test.ts +463 -0
  95. package/Tests/Server/Utils/DropFilterValidation.test.ts +269 -0
  96. package/Tests/Server/Utils/Telemetry/SpanUtil.test.ts +94 -0
  97. package/Tests/Types/DateUserTimezone.test.ts +52 -0
  98. package/Tests/Types/JSONFunctions.test.ts +103 -1
  99. package/Tests/Types/Monitor/KubernetesAlertTemplates.test.ts +177 -9
  100. package/Tests/Types/Monitor/MonitorStepDnsMonitor.test.ts +105 -0
  101. package/Tests/Types/Monitor/MonitorStepDnssecMonitor.test.ts +131 -0
  102. package/Tests/Types/Monitor/MonitorStepDomainMonitor.test.ts +60 -0
  103. package/Tests/Types/Monitor/MonitorStepExternalStatusPageMonitor.test.ts +82 -0
  104. package/Tests/Types/Monitor/MonitorStepInfrastructureMonitors.test.ts +268 -0
  105. package/Tests/Types/Monitor/MonitorStepMetricMonitor.test.ts +386 -0
  106. package/Tests/Types/Monitor/MonitorStepProfileMonitor.test.ts +185 -0
  107. package/Tests/Types/Monitor/MonitorStepSnmpMonitor.test.ts +156 -0
  108. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCatalog.test.ts +371 -0
  109. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCoverage.test.ts +901 -0
  110. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationNotificationMode.test.ts +607 -0
  111. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.test.ts +754 -0
  112. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationUtil.test.ts +960 -0
  113. package/Tests/Types/RollingTime/RollingTimeUtil.test.ts +186 -0
  114. package/Tests/Types/Runbook/RunbookStepTimeout.test.ts +410 -0
  115. package/Tests/Types/Telemetry/DropFilterSampling.test.ts +241 -0
  116. package/Tests/Types/Workspace/NotificationRuleConditionUtil.test.ts +353 -0
  117. package/Tests/Types/Workspace/NotificationRuleUtil.test.ts +566 -0
  118. package/Tests/UI/Components/CodeEditor.test.tsx +174 -0
  119. package/Tests/UI/Components/ErrorBoundary.test.tsx +555 -24
  120. package/Tests/UI/Components/InfoCard.test.tsx +358 -0
  121. package/Tests/UI/Components/LogTimeRangePicker.test.tsx +138 -0
  122. package/Tests/UI/Components/Modal.test.tsx +43 -5
  123. package/Tests/UI/Components/ModelTable/BaseModelTableColumnCustomization.test.tsx +679 -0
  124. package/Tests/UI/Components/ModelTable/BaseModelTableSearchLabels.test.tsx +397 -0
  125. package/Tests/UI/Components/ModelTable/ColumnCustomizationModal.test.tsx +999 -0
  126. package/Tests/UI/Components/ModelTable/ColumnPreference.test.ts +1678 -0
  127. package/Tests/UI/Components/ModelTable/CustomFieldColumns.test.tsx +1094 -0
  128. package/Tests/UI/Components/Runbook/StepTimeoutInput.test.tsx +386 -0
  129. package/Tests/UI/Components/StatusPage/ResourceGroupSection.test.tsx +671 -0
  130. package/Tests/UI/Components/TelemetryTimeRangePicker.test.tsx +404 -0
  131. package/Tests/UI/Utils/DropdownAlignment.test.ts +306 -0
  132. package/Tests/UI/Utils/TableFilterUrlState.test.ts +292 -0
  133. package/Tests/Utils/Monitor/MonitorSummaryProbeUtil.test.ts +263 -0
  134. package/Tests/Utils/NetworkDiscovery/ScanTargetUtil.test.ts +583 -0
  135. package/Tests/Utils/StatusPage/GroupNestingLayout.test.ts +1072 -0
  136. package/Tests/Utils/StatusPage/GroupTree.test.ts +583 -0
  137. package/Tests/Utils/StatusPage/ResourceUptime.test.ts +411 -0
  138. package/Tests/Utils/UserPreferences.test.ts +563 -0
  139. package/Types/Date.ts +15 -6
  140. package/Types/JSONFunctions.ts +7 -2
  141. package/Types/Kubernetes/KubernetesCostIngest.ts +37 -1
  142. package/Types/Monitor/KubernetesAlertTemplates.ts +205 -0
  143. package/Types/Monitor/MonitorStep.ts +10 -1
  144. package/Types/Monitor/MonitorStepMetricMonitor.ts +46 -1
  145. package/Types/Monitor/Recommendation/MonitorRecommendationCatalog.ts +413 -0
  146. package/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.ts +168 -0
  147. package/Types/Monitor/Recommendation/MonitorRecommendationTypes.ts +175 -0
  148. package/Types/Monitor/Recommendation/MonitorRecommendationUtil.ts +743 -0
  149. package/Types/Permission.ts +45 -0
  150. package/Types/Recommendation/RecommendationType.ts +20 -0
  151. package/Types/Runbook/RunbookExecutionDeadline.ts +129 -0
  152. package/Types/Runbook/RunbookStepTimeout.ts +202 -0
  153. package/Types/Telemetry/DropFilterSampling.ts +92 -0
  154. package/UI/Components/AI/GenerateFromAIModal.tsx +0 -2
  155. package/UI/Components/Accordion/Accordion.tsx +10 -2
  156. package/UI/Components/BulkUpdate/BulkLabelActions.tsx +0 -1
  157. package/UI/Components/CodeEditor/CodeEditor.tsx +34 -20
  158. package/UI/Components/Date/RangeStartAndEndDateView.tsx +0 -1
  159. package/UI/Components/EditionLabel/EditionLabel.tsx +1 -27
  160. package/UI/Components/Error/PageError.tsx +0 -4
  161. package/UI/Components/ErrorBoundary.tsx +252 -15
  162. package/UI/Components/InfoCard/InfoCard.tsx +58 -1
  163. package/UI/Components/LogsViewer/components/LogTimeRangePicker.tsx +34 -1
  164. package/UI/Components/MasterPage/MasterPage.tsx +65 -0
  165. package/UI/Components/Modal/Modal.tsx +16 -60
  166. package/UI/Components/ModelFormModal/ModelFormModal.tsx +0 -8
  167. package/UI/Components/ModelTable/BaseModelTable.tsx +503 -71
  168. package/UI/Components/ModelTable/Column.ts +17 -0
  169. package/UI/Components/ModelTable/ColumnCustomizationModal.tsx +523 -0
  170. package/UI/Components/ModelTable/ColumnPreference.ts +482 -0
  171. package/UI/Components/ModelTable/CustomFieldColumns.tsx +326 -0
  172. package/UI/Components/ModelTable/TableView.tsx +24 -2
  173. package/UI/Components/ModelTable/useCustomFieldColumns.ts +150 -0
  174. package/UI/Components/Runbook/StepTimeoutInput.tsx +173 -0
  175. package/UI/Components/SideMenu/SideMenu.tsx +24 -4
  176. package/UI/Components/StatusPage/ResourceGroupSection.tsx +231 -0
  177. package/UI/Components/Table/Table.tsx +14 -1
  178. package/UI/Components/Table/TableRow.tsx +180 -175
  179. package/UI/Components/Table/Types/Column.ts +2 -0
  180. package/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.tsx +34 -1
  181. package/UI/Utils/DropdownAlignment.ts +162 -0
  182. package/UI/Utils/TableFilterUrlState.ts +59 -20
  183. package/Utils/API.ts +106 -2
  184. package/Utils/Monitor/MonitorSummaryProbeUtil.ts +130 -0
  185. package/Utils/NetworkDiscovery/ScanTargetUtil.ts +434 -0
  186. package/Utils/StatusPage/GroupNestingLayout.ts +343 -0
  187. package/Utils/StatusPage/GroupTree.ts +307 -0
  188. package/Utils/StatusPage/ResourceUptime.ts +90 -17
  189. package/Utils/UserPreferences.ts +53 -0
  190. package/build/dist/Models/AnalyticsModels/KubernetesCostAllocation.js +86 -0
  191. package/build/dist/Models/AnalyticsModels/KubernetesCostAllocation.js.map +1 -1
  192. package/build/dist/Models/DatabaseModels/AlertEpisodeStateTimeline.js +1 -0
  193. package/build/dist/Models/DatabaseModels/AlertEpisodeStateTimeline.js.map +1 -1
  194. package/build/dist/Models/DatabaseModels/AlertStateTimeline.js +1 -0
  195. package/build/dist/Models/DatabaseModels/AlertStateTimeline.js.map +1 -1
  196. package/build/dist/Models/DatabaseModels/IncidentEpisodeStateTimeline.js +1 -0
  197. package/build/dist/Models/DatabaseModels/IncidentEpisodeStateTimeline.js.map +1 -1
  198. package/build/dist/Models/DatabaseModels/IncidentStateTimeline.js +1 -0
  199. package/build/dist/Models/DatabaseModels/IncidentStateTimeline.js.map +1 -1
  200. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  201. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  202. package/build/dist/Models/DatabaseModels/LogDropFilter.js +91 -0
  203. package/build/dist/Models/DatabaseModels/LogDropFilter.js.map +1 -1
  204. package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js +14 -4
  205. package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js.map +1 -1
  206. package/build/dist/Models/DatabaseModels/RecommendationDismissal.js +484 -0
  207. package/build/dist/Models/DatabaseModels/RecommendationDismissal.js.map +1 -0
  208. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.js +1 -0
  209. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.js.map +1 -1
  210. package/build/dist/Models/DatabaseModels/StatusPageGroup.js +94 -0
  211. package/build/dist/Models/DatabaseModels/StatusPageGroup.js.map +1 -1
  212. package/build/dist/Models/DatabaseModels/TableView.js +40 -0
  213. package/build/dist/Models/DatabaseModels/TableView.js.map +1 -1
  214. package/build/dist/Models/DatabaseModels/TraceDropFilter.js +91 -0
  215. package/build/dist/Models/DatabaseModels/TraceDropFilter.js.map +1 -1
  216. package/build/dist/Models/DatabaseModels/WorkflowVariable.js +5 -1
  217. package/build/dist/Models/DatabaseModels/WorkflowVariable.js.map +1 -1
  218. package/build/dist/Server/API/AlertAPI.js +22 -13
  219. package/build/dist/Server/API/AlertAPI.js.map +1 -1
  220. package/build/dist/Server/API/BaseAPI.js +22 -1
  221. package/build/dist/Server/API/BaseAPI.js.map +1 -1
  222. package/build/dist/Server/API/BaseAnalyticsAPI.js +24 -1
  223. package/build/dist/Server/API/BaseAnalyticsAPI.js.map +1 -1
  224. package/build/dist/Server/API/CommonAPI.js +42 -0
  225. package/build/dist/Server/API/CommonAPI.js.map +1 -1
  226. package/build/dist/Server/API/IncidentAPI.js +43 -26
  227. package/build/dist/Server/API/IncidentAPI.js.map +1 -1
  228. package/build/dist/Server/API/IncidentEpisodeAPI.js +21 -12
  229. package/build/dist/Server/API/IncidentEpisodeAPI.js.map +1 -1
  230. package/build/dist/Server/API/MonitorAPI.js +23 -0
  231. package/build/dist/Server/API/MonitorAPI.js.map +1 -1
  232. package/build/dist/Server/API/MonitorTemplateAPI.js +4 -0
  233. package/build/dist/Server/API/MonitorTemplateAPI.js.map +1 -1
  234. package/build/dist/Server/API/ProbeAPI.js +131 -15
  235. package/build/dist/Server/API/ProbeAPI.js.map +1 -1
  236. package/build/dist/Server/API/ProjectAPI.js +62 -3
  237. package/build/dist/Server/API/ProjectAPI.js.map +1 -1
  238. package/build/dist/Server/API/ScheduledMaintenanceAPI.js +23 -14
  239. package/build/dist/Server/API/ScheduledMaintenanceAPI.js.map +1 -1
  240. package/build/dist/Server/API/StatusPageAPI.js +61 -33
  241. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  242. package/build/dist/Server/API/WorkspaceNotificationRuleAPI.js +26 -2
  243. package/build/dist/Server/API/WorkspaceNotificationRuleAPI.js.map +1 -1
  244. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785241000000-AddColumnsToTableView.js +12 -0
  245. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785241000000-AddColumnsToTableView.js.map +1 -0
  246. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785320000000-RepairCrossProjectIncidentReferences.js +397 -0
  247. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785320000000-RepairCrossProjectIncidentReferences.js.map +1 -0
  248. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785329453269-AddParentGroupToStatusPageGroup.js +16 -0
  249. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785329453269-AddParentGroupToStatusPageGroup.js.map +1 -0
  250. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785397674289-AddRecommendationDismissalTable.js +30 -0
  251. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785397674289-AddRecommendationDismissalTable.js.map +1 -0
  252. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785405581596-AddDroppedCountersToDropFilters.js +34 -0
  253. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785405581596-AddDroppedCountersToDropFilters.js.map +1 -0
  254. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +10 -0
  255. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  256. package/build/dist/Server/Services/AlertEpisodeService.js +39 -0
  257. package/build/dist/Server/Services/AlertEpisodeService.js.map +1 -1
  258. package/build/dist/Server/Services/AlertService.js +95 -0
  259. package/build/dist/Server/Services/AlertService.js.map +1 -1
  260. package/build/dist/Server/Services/BillingService.js +71 -0
  261. package/build/dist/Server/Services/BillingService.js.map +1 -1
  262. package/build/dist/Server/Services/DatabaseService.js +140 -17
  263. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  264. package/build/dist/Server/Services/IncidentEpisodeService.js +39 -0
  265. package/build/dist/Server/Services/IncidentEpisodeService.js.map +1 -1
  266. package/build/dist/Server/Services/IncidentService.js +100 -0
  267. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  268. package/build/dist/Server/Services/IncidentTemplateService.js +97 -0
  269. package/build/dist/Server/Services/IncidentTemplateService.js.map +1 -1
  270. package/build/dist/Server/Services/Index.js +2 -0
  271. package/build/dist/Server/Services/Index.js.map +1 -1
  272. package/build/dist/Server/Services/LogDropFilterService.js +93 -0
  273. package/build/dist/Server/Services/LogDropFilterService.js.map +1 -1
  274. package/build/dist/Server/Services/MonitorProbeService.js +73 -5
  275. package/build/dist/Server/Services/MonitorProbeService.js.map +1 -1
  276. package/build/dist/Server/Services/MonitorService.js +34 -39
  277. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  278. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js +71 -0
  279. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js.map +1 -1
  280. package/build/dist/Server/Services/ProbeService.js +64 -0
  281. package/build/dist/Server/Services/ProbeService.js.map +1 -1
  282. package/build/dist/Server/Services/ProjectService.js +103 -7
  283. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  284. package/build/dist/Server/Services/RecommendationDismissalService.js +50 -0
  285. package/build/dist/Server/Services/RecommendationDismissalService.js.map +1 -0
  286. package/build/dist/Server/Services/RunbookAgentJobService.js +63 -5
  287. package/build/dist/Server/Services/RunbookAgentJobService.js.map +1 -1
  288. package/build/dist/Server/Services/ScheduledMaintenanceService.js +83 -0
  289. package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
  290. package/build/dist/Server/Services/ScheduledMaintenanceTemplateService.js +65 -0
  291. package/build/dist/Server/Services/ScheduledMaintenanceTemplateService.js.map +1 -1
  292. package/build/dist/Server/Services/StatusPageGroupService.js +136 -0
  293. package/build/dist/Server/Services/StatusPageGroupService.js.map +1 -1
  294. package/build/dist/Server/Services/TraceDropFilterService.js +93 -0
  295. package/build/dist/Server/Services/TraceDropFilterService.js.map +1 -1
  296. package/build/dist/Server/Types/Workflow/Components/API/Delete.js +11 -0
  297. package/build/dist/Server/Types/Workflow/Components/API/Delete.js.map +1 -1
  298. package/build/dist/Server/Types/Workflow/Components/API/Get.js +11 -0
  299. package/build/dist/Server/Types/Workflow/Components/API/Get.js.map +1 -1
  300. package/build/dist/Server/Types/Workflow/Components/API/Patch.js +11 -0
  301. package/build/dist/Server/Types/Workflow/Components/API/Patch.js.map +1 -1
  302. package/build/dist/Server/Types/Workflow/Components/API/Post.js +11 -0
  303. package/build/dist/Server/Types/Workflow/Components/API/Post.js.map +1 -1
  304. package/build/dist/Server/Types/Workflow/Components/API/Put.js +12 -0
  305. package/build/dist/Server/Types/Workflow/Components/API/Put.js.map +1 -1
  306. package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js +132 -0
  307. package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js.map +1 -0
  308. package/build/dist/Server/Utils/DropFilterValidation.js +80 -0
  309. package/build/dist/Server/Utils/DropFilterValidation.js.map +1 -0
  310. package/build/dist/Server/Utils/Errors.js +3 -0
  311. package/build/dist/Server/Utils/Errors.js.map +1 -1
  312. package/build/dist/Server/Utils/Monitor/MonitorAlert.js +30 -12
  313. package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
  314. package/build/dist/Server/Utils/Monitor/MonitorIncident.js +38 -18
  315. package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
  316. package/build/dist/Server/Utils/Telemetry/AppMetrics.js +26 -0
  317. package/build/dist/Server/Utils/Telemetry/AppMetrics.js.map +1 -1
  318. package/build/dist/Types/Date.js +14 -4
  319. package/build/dist/Types/Date.js.map +1 -1
  320. package/build/dist/Types/JSONFunctions.js +8 -3
  321. package/build/dist/Types/JSONFunctions.js.map +1 -1
  322. package/build/dist/Types/Kubernetes/KubernetesCostIngest.js.map +1 -1
  323. package/build/dist/Types/Monitor/KubernetesAlertTemplates.js +192 -0
  324. package/build/dist/Types/Monitor/KubernetesAlertTemplates.js.map +1 -1
  325. package/build/dist/Types/Monitor/MonitorStep.js +6 -1
  326. package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
  327. package/build/dist/Types/Monitor/MonitorStepMetricMonitor.js +32 -1
  328. package/build/dist/Types/Monitor/MonitorStepMetricMonitor.js.map +1 -1
  329. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js +294 -0
  330. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js.map +1 -0
  331. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.js +110 -0
  332. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.js.map +1 -0
  333. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js +58 -0
  334. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js.map +1 -0
  335. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationUtil.js +451 -0
  336. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationUtil.js.map +1 -0
  337. package/build/dist/Types/Permission.js +40 -0
  338. package/build/dist/Types/Permission.js.map +1 -1
  339. package/build/dist/Types/Recommendation/RecommendationType.js +21 -0
  340. package/build/dist/Types/Recommendation/RecommendationType.js.map +1 -0
  341. package/build/dist/Types/Runbook/RunbookExecutionDeadline.js +93 -0
  342. package/build/dist/Types/Runbook/RunbookExecutionDeadline.js.map +1 -0
  343. package/build/dist/Types/Runbook/RunbookStepTimeout.js +142 -0
  344. package/build/dist/Types/Runbook/RunbookStepTimeout.js.map +1 -0
  345. package/build/dist/Types/Telemetry/DropFilterSampling.js +75 -0
  346. package/build/dist/Types/Telemetry/DropFilterSampling.js.map +1 -0
  347. package/build/dist/UI/Components/AI/GenerateFromAIModal.js +1 -2
  348. package/build/dist/UI/Components/AI/GenerateFromAIModal.js.map +1 -1
  349. package/build/dist/UI/Components/Accordion/Accordion.js +9 -2
  350. package/build/dist/UI/Components/Accordion/Accordion.js.map +1 -1
  351. package/build/dist/UI/Components/BulkUpdate/BulkLabelActions.js +1 -1
  352. package/build/dist/UI/Components/BulkUpdate/BulkLabelActions.js.map +1 -1
  353. package/build/dist/UI/Components/CodeEditor/CodeEditor.js +27 -14
  354. package/build/dist/UI/Components/CodeEditor/CodeEditor.js.map +1 -1
  355. package/build/dist/UI/Components/Date/RangeStartAndEndDateView.js +1 -1
  356. package/build/dist/UI/Components/Date/RangeStartAndEndDateView.js.map +1 -1
  357. package/build/dist/UI/Components/EditionLabel/EditionLabel.js +2 -20
  358. package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
  359. package/build/dist/UI/Components/Error/PageError.js +1 -3
  360. package/build/dist/UI/Components/Error/PageError.js.map +1 -1
  361. package/build/dist/UI/Components/ErrorBoundary.js +145 -11
  362. package/build/dist/UI/Components/ErrorBoundary.js.map +1 -1
  363. package/build/dist/UI/Components/InfoCard/InfoCard.js +28 -1
  364. package/build/dist/UI/Components/InfoCard/InfoCard.js.map +1 -1
  365. package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js +13 -2
  366. package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js.map +1 -1
  367. package/build/dist/UI/Components/MasterPage/MasterPage.js +50 -1
  368. package/build/dist/UI/Components/MasterPage/MasterPage.js.map +1 -1
  369. package/build/dist/UI/Components/Modal/Modal.js +4 -32
  370. package/build/dist/UI/Components/Modal/Modal.js.map +1 -1
  371. package/build/dist/UI/Components/ModelFormModal/ModelFormModal.js.map +1 -1
  372. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +308 -40
  373. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  374. package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js +230 -0
  375. package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js.map +1 -0
  376. package/build/dist/UI/Components/ModelTable/ColumnPreference.js +258 -0
  377. package/build/dist/UI/Components/ModelTable/ColumnPreference.js.map +1 -0
  378. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js +168 -0
  379. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -0
  380. package/build/dist/UI/Components/ModelTable/TableView.js +13 -2
  381. package/build/dist/UI/Components/ModelTable/TableView.js.map +1 -1
  382. package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js +84 -0
  383. package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js.map +1 -0
  384. package/build/dist/UI/Components/Runbook/StepTimeoutInput.js +101 -0
  385. package/build/dist/UI/Components/Runbook/StepTimeoutInput.js.map +1 -0
  386. package/build/dist/UI/Components/SideMenu/SideMenu.js +15 -5
  387. package/build/dist/UI/Components/SideMenu/SideMenu.js.map +1 -1
  388. package/build/dist/UI/Components/StatusPage/ResourceGroupSection.js +73 -0
  389. package/build/dist/UI/Components/StatusPage/ResourceGroupSection.js.map +1 -0
  390. package/build/dist/UI/Components/Table/Table.js +15 -2
  391. package/build/dist/UI/Components/Table/Table.js.map +1 -1
  392. package/build/dist/UI/Components/Table/TableRow.js +11 -6
  393. package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
  394. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js +13 -2
  395. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js.map +1 -1
  396. package/build/dist/UI/Utils/DropdownAlignment.js +98 -0
  397. package/build/dist/UI/Utils/DropdownAlignment.js.map +1 -0
  398. package/build/dist/UI/Utils/TableFilterUrlState.js +48 -15
  399. package/build/dist/UI/Utils/TableFilterUrlState.js.map +1 -1
  400. package/build/dist/Utils/API.js +39 -2
  401. package/build/dist/Utils/API.js.map +1 -1
  402. package/build/dist/Utils/Monitor/MonitorSummaryProbeUtil.js +97 -0
  403. package/build/dist/Utils/Monitor/MonitorSummaryProbeUtil.js.map +1 -0
  404. package/build/dist/Utils/NetworkDiscovery/ScanTargetUtil.js +330 -0
  405. package/build/dist/Utils/NetworkDiscovery/ScanTargetUtil.js.map +1 -0
  406. package/build/dist/Utils/StatusPage/GroupNestingLayout.js +280 -0
  407. package/build/dist/Utils/StatusPage/GroupNestingLayout.js.map +1 -0
  408. package/build/dist/Utils/StatusPage/GroupTree.js +214 -0
  409. package/build/dist/Utils/StatusPage/GroupTree.js.map +1 -0
  410. package/build/dist/Utils/StatusPage/ResourceUptime.js +60 -16
  411. package/build/dist/Utils/StatusPage/ResourceUptime.js.map +1 -1
  412. package/build/dist/Utils/UserPreferences.js +33 -0
  413. package/build/dist/Utils/UserPreferences.js.map +1 -1
  414. package/package.json +1 -1
@@ -0,0 +1,999 @@
1
+ import ColumnCustomizationModal from "../../../../UI/Components/ModelTable/ColumnCustomizationModal";
2
+ import {
3
+ CustomizableColumn,
4
+ ModelTableColumn,
5
+ } from "../../../../UI/Components/ModelTable/ColumnPreference";
6
+ import FieldType from "../../../../UI/Components/Types/FieldType";
7
+ import Monitor from "../../../../Models/DatabaseModels/Monitor";
8
+ import getJestMockFunction, { MockFunction } from "../../../MockType";
9
+ import "@testing-library/jest-dom";
10
+ import {
11
+ RenderResult,
12
+ cleanup,
13
+ fireEvent,
14
+ render,
15
+ } from "@testing-library/react";
16
+ import React from "react";
17
+ import {
18
+ afterEach,
19
+ beforeEach,
20
+ describe,
21
+ expect,
22
+ jest,
23
+ test,
24
+ } from "@jest/globals";
25
+
26
+ /*
27
+ * The "Customize Columns" picker. Two invariants carry real weight here and
28
+ * are pinned below:
29
+ *
30
+ * 1. Nothing leaves the modal until Save. Cancel has to be a genuine escape
31
+ * hatch, so every checkbox and every reorder is staged in local state.
32
+ *
33
+ * 2. The viewer can never end up with zero columns. A table with no columns
34
+ * renders as an empty shell with no header row, and the only control that
35
+ * could undo that is the picker they just closed. Both the per-row
36
+ * checkbox and "Hide all" have to respect that.
37
+ *
38
+ * Drag-and-drop is deliberately not exercised: react-beautiful-dnd renders
39
+ * fine in jsdom but a synthetic drag cannot be driven through it. The Up/Down
40
+ * buttons exist precisely so that reordering is reachable without a pointer,
41
+ * which also makes it the reorder path these tests use.
42
+ */
43
+
44
+ type MakeEntryFunction = (data: {
45
+ id: string;
46
+ title: string;
47
+ isVisible: boolean;
48
+ }) => CustomizableColumn<Monitor>;
49
+
50
+ const makeEntry: MakeEntryFunction = (data: {
51
+ id: string;
52
+ title: string;
53
+ isVisible: boolean;
54
+ }): CustomizableColumn<Monitor> => {
55
+ const column: ModelTableColumn<Monitor> = {
56
+ field: { name: true },
57
+ title: data.title,
58
+ type: FieldType.Text,
59
+ };
60
+
61
+ return {
62
+ id: data.id,
63
+ column: column,
64
+ isVisible: data.isVisible,
65
+ isPinned: false,
66
+ };
67
+ };
68
+
69
+ type GetDefaultColumnsFunction = () => Array<CustomizableColumn<Monitor>>;
70
+
71
+ // Three on, one off - so the count line has something to say from the start.
72
+ const getDefaultColumns: GetDefaultColumnsFunction = (): Array<
73
+ CustomizableColumn<Monitor>
74
+ > => {
75
+ return [
76
+ makeEntry({ id: "name", title: "Monitor Name", isVisible: true }),
77
+ makeEntry({
78
+ id: "currentMonitorStatus",
79
+ title: "Status",
80
+ isVisible: true,
81
+ }),
82
+ makeEntry({ id: "description", title: "Description", isVisible: true }),
83
+ makeEntry({ id: "createdAt", title: "Created At", isVisible: false }),
84
+ ];
85
+ };
86
+
87
+ interface RenderedModal {
88
+ view: RenderResult;
89
+ onSave: MockFunction;
90
+ onClose: MockFunction;
91
+ onReset: MockFunction;
92
+ }
93
+
94
+ type RenderModalFunction = (data?: {
95
+ columns?: Array<CustomizableColumn<Monitor>> | undefined;
96
+ isDefaultLayout?: boolean | undefined;
97
+ title?: string | undefined;
98
+ }) => RenderedModal;
99
+
100
+ const renderModal: RenderModalFunction = (data?: {
101
+ columns?: Array<CustomizableColumn<Monitor>> | undefined;
102
+ isDefaultLayout?: boolean | undefined;
103
+ title?: string | undefined;
104
+ }): RenderedModal => {
105
+ const onSave: MockFunction = getJestMockFunction();
106
+ const onClose: MockFunction = getJestMockFunction();
107
+ const onReset: MockFunction = getJestMockFunction();
108
+
109
+ const view: RenderResult = render(
110
+ <ColumnCustomizationModal<Monitor>
111
+ columns={data?.columns || getDefaultColumns()}
112
+ onSave={onSave}
113
+ onClose={onClose}
114
+ onReset={onReset}
115
+ isDefaultLayout={data?.isDefaultLayout}
116
+ title={data?.title}
117
+ />,
118
+ );
119
+
120
+ return { view, onSave, onClose, onReset };
121
+ };
122
+
123
+ type GetSavedFunction = (
124
+ onSave: MockFunction,
125
+ ) => Array<CustomizableColumn<Monitor>>;
126
+
127
+ const getSaved: GetSavedFunction = (
128
+ onSave: MockFunction,
129
+ ): Array<CustomizableColumn<Monitor>> => {
130
+ return onSave.mock.calls[0]![0] as Array<CustomizableColumn<Monitor>>;
131
+ };
132
+
133
+ type GetSavedIdsFunction = (onSave: MockFunction) => Array<string>;
134
+
135
+ const getSavedIds: GetSavedIdsFunction = (
136
+ onSave: MockFunction,
137
+ ): Array<string> => {
138
+ return getSaved(onSave).map((entry: CustomizableColumn<Monitor>) => {
139
+ return entry.id;
140
+ });
141
+ };
142
+
143
+ type TypeSearchFunction = (view: RenderResult, text: string) => void;
144
+
145
+ const typeSearch: TypeSearchFunction = (
146
+ view: RenderResult,
147
+ text: string,
148
+ ): void => {
149
+ fireEvent.change(view.getByTestId("column-customization-search"), {
150
+ target: { value: text },
151
+ });
152
+ };
153
+
154
+ describe("ColumnCustomizationModal", () => {
155
+ beforeEach(() => {
156
+ /*
157
+ * The whole Common suite runs --runInBand, so storage written by an
158
+ * earlier file is still around. Nothing here reads it, but leaving it
159
+ * behind is how the next test starts flaking.
160
+ */
161
+ window.localStorage.clear();
162
+ });
163
+
164
+ afterEach(() => {
165
+ cleanup();
166
+ jest.restoreAllMocks();
167
+ });
168
+
169
+ test("renders one row per column, with the titles visible", () => {
170
+ const { view }: RenderedModal = renderModal();
171
+
172
+ expect(view.getByTestId("column-customization-modal")).toBeInTheDocument();
173
+ expect(view.getAllByTestId(/^column-toggle-/)).toHaveLength(4);
174
+
175
+ expect(view.getByText("Monitor Name")).toBeInTheDocument();
176
+ expect(view.getByText("Status")).toBeInTheDocument();
177
+ expect(view.getByText("Description")).toBeInTheDocument();
178
+ expect(view.getByText("Created At")).toBeInTheDocument();
179
+
180
+ expect(view.getByTestId("column-toggle-name")).toBeChecked();
181
+ expect(view.getByTestId("column-toggle-createdAt")).not.toBeChecked();
182
+ });
183
+
184
+ test("counts the visible columns and keeps the count in step with the checkboxes", () => {
185
+ const { view }: RenderedModal = renderModal();
186
+
187
+ expect(view.getByTestId("column-customization-count")).toHaveTextContent(
188
+ "3 of 4 columns shown",
189
+ );
190
+
191
+ fireEvent.click(view.getByTestId("column-toggle-createdAt"));
192
+
193
+ expect(view.getByTestId("column-customization-count")).toHaveTextContent(
194
+ "4 of 4 columns shown",
195
+ );
196
+
197
+ fireEvent.click(view.getByTestId("column-toggle-description"));
198
+
199
+ expect(view.getByTestId("column-customization-count")).toHaveTextContent(
200
+ "3 of 4 columns shown",
201
+ );
202
+ });
203
+
204
+ test("stages changes locally: nothing is handed back before Save", () => {
205
+ const { view, onSave }: RenderedModal = renderModal();
206
+
207
+ fireEvent.click(view.getByTestId("column-toggle-description"));
208
+
209
+ expect(onSave).not.toHaveBeenCalled();
210
+ });
211
+
212
+ test("Cancel discards the staged changes", () => {
213
+ const { view, onSave, onClose }: RenderedModal = renderModal();
214
+
215
+ fireEvent.click(view.getByTestId("column-toggle-description"));
216
+ fireEvent.click(view.getByTestId("modal-footer-close-button"));
217
+
218
+ // The caller is told to close and is told nothing else - the edit is gone.
219
+ expect(onClose).toHaveBeenCalledTimes(1);
220
+ expect(onSave).not.toHaveBeenCalled();
221
+ });
222
+
223
+ test("Save hands back every entry with its new isVisible flag", () => {
224
+ const { view, onSave }: RenderedModal = renderModal();
225
+
226
+ fireEvent.click(view.getByTestId("column-toggle-description"));
227
+ fireEvent.click(view.getByTestId("column-toggle-createdAt"));
228
+ fireEvent.click(view.getByTestId("modal-footer-submit-button"));
229
+
230
+ expect(onSave).toHaveBeenCalledTimes(1);
231
+
232
+ /*
233
+ * Hidden columns are still in the list. The caller needs the full set to
234
+ * build the stored layout - "hidden" is not the complement of "order".
235
+ */
236
+ expect(
237
+ getSaved(onSave).map((entry: CustomizableColumn<Monitor>) => {
238
+ return { id: entry.id, isVisible: entry.isVisible };
239
+ }),
240
+ ).toEqual([
241
+ { id: "name", isVisible: true },
242
+ { id: "currentMonitorStatus", isVisible: true },
243
+ { id: "description", isVisible: false },
244
+ { id: "createdAt", isVisible: true },
245
+ ]);
246
+ });
247
+
248
+ test("the last visible column cannot be switched off", () => {
249
+ const { view, onSave }: RenderedModal = renderModal({
250
+ columns: [
251
+ makeEntry({ id: "name", title: "Monitor Name", isVisible: true }),
252
+ makeEntry({
253
+ id: "description",
254
+ title: "Description",
255
+ isVisible: false,
256
+ }),
257
+ makeEntry({ id: "createdAt", title: "Created At", isVisible: false }),
258
+ ],
259
+ });
260
+
261
+ const onlyVisible: HTMLElement = view.getByTestId("column-toggle-name");
262
+
263
+ // First line of defence: the viewer cannot reach the control at all.
264
+ expect(onlyVisible).toBeDisabled();
265
+
266
+ /*
267
+ * Second line of defence: even if a change does arrive - a stray
268
+ * programmatic event, a future restyle that drops `disabled` - the state
269
+ * update refuses rather than leaving the table with no columns to render.
270
+ */
271
+ fireEvent.click(onlyVisible);
272
+ fireEvent.change(onlyVisible, { target: { checked: false } });
273
+
274
+ expect(view.getByTestId("column-customization-count")).toHaveTextContent(
275
+ "1 of 3 columns shown",
276
+ );
277
+
278
+ fireEvent.click(view.getByTestId("modal-footer-submit-button"));
279
+
280
+ expect(
281
+ getSaved(onSave).filter((entry: CustomizableColumn<Monitor>) => {
282
+ return entry.isVisible;
283
+ }),
284
+ ).toHaveLength(1);
285
+ });
286
+
287
+ test("switching the second-to-last column off locks the survivor", () => {
288
+ const { view }: RenderedModal = renderModal({
289
+ columns: [
290
+ makeEntry({ id: "name", title: "Monitor Name", isVisible: true }),
291
+ makeEntry({ id: "description", title: "Description", isVisible: true }),
292
+ ],
293
+ });
294
+
295
+ expect(view.getByTestId("column-toggle-name")).not.toBeDisabled();
296
+
297
+ fireEvent.click(view.getByTestId("column-toggle-description"));
298
+
299
+ expect(view.getByTestId("column-customization-count")).toHaveTextContent(
300
+ "1 of 2 columns shown",
301
+ );
302
+ expect(view.getByTestId("column-toggle-name")).toBeDisabled();
303
+ });
304
+
305
+ test("Down moves a column later and Save reports the new order", () => {
306
+ const { view, onSave }: RenderedModal = renderModal();
307
+
308
+ fireEvent.click(view.getByTestId("column-move-down-name"));
309
+ fireEvent.click(view.getByTestId("modal-footer-submit-button"));
310
+
311
+ expect(getSavedIds(onSave)).toEqual([
312
+ "currentMonitorStatus",
313
+ "name",
314
+ "description",
315
+ "createdAt",
316
+ ]);
317
+ });
318
+
319
+ test("Up moves a column earlier and Save reports the new order", () => {
320
+ const { view, onSave }: RenderedModal = renderModal();
321
+
322
+ fireEvent.click(view.getByTestId("column-move-up-createdAt"));
323
+ fireEvent.click(view.getByTestId("column-move-up-createdAt"));
324
+ fireEvent.click(view.getByTestId("modal-footer-submit-button"));
325
+
326
+ expect(getSavedIds(onSave)).toEqual([
327
+ "name",
328
+ "createdAt",
329
+ "currentMonitorStatus",
330
+ "description",
331
+ ]);
332
+ });
333
+
334
+ test("reordering does not disturb the visibility flags", () => {
335
+ const { view, onSave }: RenderedModal = renderModal();
336
+
337
+ fireEvent.click(view.getByTestId("column-move-up-createdAt"));
338
+ fireEvent.click(view.getByTestId("modal-footer-submit-button"));
339
+
340
+ expect(
341
+ getSaved(onSave).map((entry: CustomizableColumn<Monitor>) => {
342
+ return { id: entry.id, isVisible: entry.isVisible };
343
+ }),
344
+ ).toEqual([
345
+ { id: "name", isVisible: true },
346
+ { id: "currentMonitorStatus", isVisible: true },
347
+ { id: "createdAt", isVisible: false },
348
+ { id: "description", isVisible: true },
349
+ ]);
350
+ });
351
+
352
+ test("Up is dead on the first row and Down on the last, and that follows the move", () => {
353
+ const { view }: RenderedModal = renderModal();
354
+
355
+ expect(view.getByTestId("column-move-up-name")).toBeDisabled();
356
+ expect(view.getByTestId("column-move-down-name")).not.toBeDisabled();
357
+ expect(view.getByTestId("column-move-down-createdAt")).toBeDisabled();
358
+ expect(view.getByTestId("column-move-up-createdAt")).not.toBeDisabled();
359
+
360
+ fireEvent.click(view.getByTestId("column-move-down-name"));
361
+
362
+ // "name" is now second, so both of its buttons are live and Status leads.
363
+ expect(view.getByTestId("column-move-up-name")).not.toBeDisabled();
364
+ expect(
365
+ view.getByTestId("column-move-up-currentMonitorStatus"),
366
+ ).toBeDisabled();
367
+ });
368
+
369
+ test("Show all turns every column on", () => {
370
+ const { view, onSave }: RenderedModal = renderModal();
371
+
372
+ fireEvent.click(view.getByTestId("column-customization-show-all"));
373
+
374
+ expect(view.getByTestId("column-customization-count")).toHaveTextContent(
375
+ "4 of 4 columns shown",
376
+ );
377
+
378
+ fireEvent.click(view.getByTestId("modal-footer-submit-button"));
379
+
380
+ expect(
381
+ getSaved(onSave).every((entry: CustomizableColumn<Monitor>) => {
382
+ return entry.isVisible;
383
+ }),
384
+ ).toBe(true);
385
+ });
386
+
387
+ test("Hide all leaves exactly one column standing - the first", () => {
388
+ const { view, onSave }: RenderedModal = renderModal();
389
+
390
+ fireEvent.click(view.getByTestId("column-customization-hide-all"));
391
+
392
+ /*
393
+ * Not zero: the leftmost column is kept because it is normally the one
394
+ * that identifies the row, and because a table with no columns cannot be
395
+ * recovered from.
396
+ */
397
+ expect(view.getByTestId("column-customization-count")).toHaveTextContent(
398
+ "1 of 4 columns shown",
399
+ );
400
+ expect(view.getByTestId("column-toggle-name")).toBeDisabled();
401
+
402
+ fireEvent.click(view.getByTestId("modal-footer-submit-button"));
403
+
404
+ expect(
405
+ getSaved(onSave)
406
+ .filter((entry: CustomizableColumn<Monitor>) => {
407
+ return entry.isVisible;
408
+ })
409
+ .map((entry: CustomizableColumn<Monitor>) => {
410
+ return entry.id;
411
+ }),
412
+ ).toEqual(["name"]);
413
+ });
414
+
415
+ test("Hide all keeps the first column even after the list has been reordered", () => {
416
+ const { view, onSave }: RenderedModal = renderModal();
417
+
418
+ fireEvent.click(view.getByTestId("column-move-up-createdAt"));
419
+ fireEvent.click(view.getByTestId("column-move-up-createdAt"));
420
+ fireEvent.click(view.getByTestId("column-move-up-createdAt"));
421
+ fireEvent.click(view.getByTestId("column-customization-hide-all"));
422
+ fireEvent.click(view.getByTestId("modal-footer-submit-button"));
423
+
424
+ // "createdAt" is now the leftmost column, so it is the one that survives.
425
+ expect(
426
+ getSaved(onSave)
427
+ .filter((entry: CustomizableColumn<Monitor>) => {
428
+ return entry.isVisible;
429
+ })
430
+ .map((entry: CustomizableColumn<Monitor>) => {
431
+ return entry.id;
432
+ }),
433
+ ).toEqual(["createdAt"]);
434
+ });
435
+
436
+ test("search filters the list by title", () => {
437
+ const { view }: RenderedModal = renderModal();
438
+
439
+ typeSearch(view, "desc");
440
+
441
+ expect(view.getAllByTestId(/^column-toggle-/)).toHaveLength(1);
442
+ expect(view.getByTestId("column-toggle-description")).toBeInTheDocument();
443
+ expect(view.queryByTestId("column-toggle-name")).not.toBeInTheDocument();
444
+ });
445
+
446
+ test("search is case-insensitive and ignores surrounding whitespace", () => {
447
+ const { view }: RenderedModal = renderModal();
448
+
449
+ typeSearch(view, " STATUS ");
450
+
451
+ expect(view.getAllByTestId(/^column-toggle-/)).toHaveLength(1);
452
+ expect(
453
+ view.getByTestId("column-toggle-currentMonitorStatus"),
454
+ ).toBeInTheDocument();
455
+ });
456
+
457
+ test("search says so when nothing matches", () => {
458
+ const { view }: RenderedModal = renderModal();
459
+
460
+ typeSearch(view, "no-such-column");
461
+
462
+ expect(view.queryAllByTestId(/^column-toggle-/)).toHaveLength(0);
463
+ expect(
464
+ view.getByTestId("column-customization-no-results"),
465
+ ).toHaveTextContent('No columns match "no-such-column".');
466
+ });
467
+
468
+ test("the move buttons are inert while a search is active", () => {
469
+ const { view }: RenderedModal = renderModal();
470
+
471
+ typeSearch(view, "e");
472
+
473
+ /*
474
+ * A filtered list renumbers the rows, so "move down" would land the column
475
+ * at a position that only means something inside the filter. Rather than
476
+ * guess what the viewer meant, reordering is switched off until the search
477
+ * is cleared.
478
+ */
479
+ expect(view.getByTestId("column-move-up-description")).toBeDisabled();
480
+ expect(view.getByTestId("column-move-down-description")).toBeDisabled();
481
+ expect(view.getByTestId("column-move-up-name")).toBeDisabled();
482
+ expect(view.getByTestId("column-move-down-name")).toBeDisabled();
483
+ });
484
+
485
+ test("clearing the search restores the full list and the move buttons", () => {
486
+ const { view, onSave }: RenderedModal = renderModal();
487
+
488
+ typeSearch(view, "desc");
489
+ expect(view.getAllByTestId(/^column-toggle-/)).toHaveLength(1);
490
+
491
+ typeSearch(view, "");
492
+
493
+ expect(view.getAllByTestId(/^column-toggle-/)).toHaveLength(4);
494
+ expect(view.getByTestId("column-move-down-name")).not.toBeDisabled();
495
+ expect(view.getByTestId("column-move-up-description")).not.toBeDisabled();
496
+
497
+ // And reordering works again once the buttons come back.
498
+ fireEvent.click(view.getByTestId("column-move-down-name"));
499
+ fireEvent.click(view.getByTestId("modal-footer-submit-button"));
500
+
501
+ expect(getSavedIds(onSave)).toEqual([
502
+ "currentMonitorStatus",
503
+ "name",
504
+ "description",
505
+ "createdAt",
506
+ ]);
507
+ });
508
+
509
+ test("a change made before searching survives the search", () => {
510
+ const { view, onSave }: RenderedModal = renderModal();
511
+
512
+ fireEvent.click(view.getByTestId("column-toggle-description"));
513
+ typeSearch(view, "created");
514
+ fireEvent.click(view.getByTestId("column-toggle-createdAt"));
515
+ typeSearch(view, "");
516
+ fireEvent.click(view.getByTestId("modal-footer-submit-button"));
517
+
518
+ expect(
519
+ getSaved(onSave).map((entry: CustomizableColumn<Monitor>) => {
520
+ return { id: entry.id, isVisible: entry.isVisible };
521
+ }),
522
+ ).toEqual([
523
+ { id: "name", isVisible: true },
524
+ { id: "currentMonitorStatus", isVisible: true },
525
+ { id: "description", isVisible: false },
526
+ { id: "createdAt", isVisible: true },
527
+ ]);
528
+ });
529
+
530
+ test("Reset is absent while the layout is still the one the table ships", () => {
531
+ const { view }: RenderedModal = renderModal({ isDefaultLayout: true });
532
+
533
+ // Nothing to reset to, so offering the button would only be confusing.
534
+ expect(
535
+ view.queryByTestId("column-customization-reset"),
536
+ ).not.toBeInTheDocument();
537
+ });
538
+
539
+ test("Reset appears once the layout has been customized and calls back", () => {
540
+ const { view, onReset, onSave }: RenderedModal = renderModal({
541
+ isDefaultLayout: false,
542
+ });
543
+
544
+ const reset: HTMLElement = view.getByTestId("column-customization-reset");
545
+
546
+ expect(reset).toBeInTheDocument();
547
+
548
+ fireEvent.click(reset);
549
+
550
+ // Reset is the caller's business: it drops the stored layout, not a draft.
551
+ expect(onReset).toHaveBeenCalledTimes(1);
552
+ expect(onSave).not.toHaveBeenCalled();
553
+ });
554
+
555
+ test("uses a custom title when one is given", () => {
556
+ const { view }: RenderedModal = renderModal({
557
+ title: "Pick your columns",
558
+ });
559
+
560
+ expect(view.getByTestId("modal-title")).toHaveTextContent(
561
+ "Pick your columns",
562
+ );
563
+ });
564
+
565
+ test("falls back to the default title", () => {
566
+ const { view }: RenderedModal = renderModal();
567
+
568
+ expect(view.getByTestId("modal-title")).toHaveTextContent(
569
+ "Customize Columns",
570
+ );
571
+ });
572
+
573
+ test("the header carries the title and description, and no icon", () => {
574
+ const { view }: RenderedModal = renderModal();
575
+
576
+ expect(view.getByTestId("modal-description")).toHaveTextContent(
577
+ "Choose which columns to show and drag them into the order you want.",
578
+ );
579
+
580
+ // The header icon was decorative; it repeated what the title already said.
581
+ expect(view.queryByTestId("icon")).not.toBeInTheDocument();
582
+ });
583
+
584
+ test("uses a custom description when one is given", () => {
585
+ const onSave: MockFunction = getJestMockFunction();
586
+ const onClose: MockFunction = getJestMockFunction();
587
+ const onReset: MockFunction = getJestMockFunction();
588
+
589
+ const view: RenderResult = render(
590
+ <ColumnCustomizationModal<Monitor>
591
+ columns={getDefaultColumns()}
592
+ onSave={onSave}
593
+ onClose={onClose}
594
+ onReset={onReset}
595
+ description="Pick the ones you care about."
596
+ />,
597
+ );
598
+
599
+ expect(view.getByTestId("modal-description")).toHaveTextContent(
600
+ "Pick the ones you care about.",
601
+ );
602
+ });
603
+
604
+ /*
605
+ * Below: the state the two bulk actions and the search box put the rest of
606
+ * the body into. These are the parts a viewer reads to work out why a
607
+ * control did nothing, so each one is pinned rather than left to the styling.
608
+ */
609
+
610
+ test("Show all is inert once every column is already on", () => {
611
+ const { view }: RenderedModal = renderModal();
612
+
613
+ const showAll: HTMLElement = view.getByTestId(
614
+ "column-customization-show-all",
615
+ );
616
+
617
+ expect(showAll).not.toBeDisabled();
618
+
619
+ fireEvent.click(showAll);
620
+
621
+ // Nothing left to show, so the control says so rather than doing nothing.
622
+ expect(showAll).toBeDisabled();
623
+ });
624
+
625
+ test("Show all comes back to life as soon as a column is switched off", () => {
626
+ const { view }: RenderedModal = renderModal();
627
+
628
+ fireEvent.click(view.getByTestId("column-customization-show-all"));
629
+ fireEvent.click(view.getByTestId("column-toggle-description"));
630
+
631
+ expect(
632
+ view.getByTestId("column-customization-show-all"),
633
+ ).not.toBeDisabled();
634
+ });
635
+
636
+ test("Hide all is inert once only the one required column is left", () => {
637
+ const { view }: RenderedModal = renderModal();
638
+
639
+ const hideAll: HTMLElement = view.getByTestId(
640
+ "column-customization-hide-all",
641
+ );
642
+
643
+ fireEvent.click(hideAll);
644
+
645
+ /*
646
+ * A second press could only try to hide the column that has to stay, so
647
+ * the button is switched off instead of quietly refusing.
648
+ */
649
+ expect(hideAll).toBeDisabled();
650
+ expect(view.getByTestId("column-customization-count")).toHaveTextContent(
651
+ "1 of 4 columns shown",
652
+ );
653
+ });
654
+
655
+ test("Hide all starts out inert when the caller opens with a single column on", () => {
656
+ const { view }: RenderedModal = renderModal({
657
+ columns: [
658
+ makeEntry({ id: "name", title: "Monitor Name", isVisible: true }),
659
+ makeEntry({
660
+ id: "description",
661
+ title: "Description",
662
+ isVisible: false,
663
+ }),
664
+ ],
665
+ });
666
+
667
+ expect(view.getByTestId("column-customization-hide-all")).toBeDisabled();
668
+ expect(
669
+ view.getByTestId("column-customization-show-all"),
670
+ ).not.toBeDisabled();
671
+ });
672
+
673
+ test("the column that cannot be switched off is marked as required", () => {
674
+ const { view }: RenderedModal = renderModal();
675
+
676
+ expect(view.queryByTestId("column-locked-name")).not.toBeInTheDocument();
677
+
678
+ fireEvent.click(view.getByTestId("column-customization-hide-all"));
679
+
680
+ /*
681
+ * The disabled checkbox alone reads as a glitch. The badge is the only
682
+ * thing on the row that explains why it will not move.
683
+ */
684
+ expect(view.getByTestId("column-locked-name")).toHaveTextContent(
685
+ "Required",
686
+ );
687
+ });
688
+
689
+ test("the required badge follows the column that is actually locked", () => {
690
+ const { view }: RenderedModal = renderModal({
691
+ columns: [
692
+ makeEntry({ id: "name", title: "Monitor Name", isVisible: true }),
693
+ makeEntry({ id: "description", title: "Description", isVisible: true }),
694
+ ],
695
+ });
696
+
697
+ fireEvent.click(view.getByTestId("column-toggle-name"));
698
+
699
+ expect(view.getByTestId("column-locked-description")).toBeInTheDocument();
700
+ expect(view.queryByTestId("column-locked-name")).not.toBeInTheDocument();
701
+ });
702
+
703
+ test("the clear button only exists while there is something to clear", () => {
704
+ const { view }: RenderedModal = renderModal();
705
+
706
+ expect(
707
+ view.queryByTestId("column-customization-search-clear"),
708
+ ).not.toBeInTheDocument();
709
+
710
+ typeSearch(view, "desc");
711
+
712
+ expect(
713
+ view.getByTestId("column-customization-search-clear"),
714
+ ).toBeInTheDocument();
715
+ });
716
+
717
+ test("the clear button empties the box and brings the whole list back", () => {
718
+ const { view }: RenderedModal = renderModal();
719
+
720
+ typeSearch(view, "desc");
721
+ expect(view.getAllByTestId(/^column-toggle-/)).toHaveLength(1);
722
+
723
+ fireEvent.click(view.getByTestId("column-customization-search-clear"));
724
+
725
+ expect(view.getByTestId("column-customization-search")).toHaveValue("");
726
+ expect(view.getAllByTestId(/^column-toggle-/)).toHaveLength(4);
727
+ expect(
728
+ view.queryByTestId("column-customization-search-clear"),
729
+ ).not.toBeInTheDocument();
730
+ });
731
+
732
+ test("clearing through the button re-arms the move buttons", () => {
733
+ const { view, onSave }: RenderedModal = renderModal();
734
+
735
+ typeSearch(view, "desc");
736
+ fireEvent.click(view.getByTestId("column-customization-search-clear"));
737
+ fireEvent.click(view.getByTestId("column-move-down-name"));
738
+ fireEvent.click(view.getByTestId("modal-footer-submit-button"));
739
+
740
+ expect(getSavedIds(onSave)).toEqual([
741
+ "currentMonitorStatus",
742
+ "name",
743
+ "description",
744
+ "createdAt",
745
+ ]);
746
+ });
747
+
748
+ test("a search that matches nothing still offers a way out", () => {
749
+ const { view }: RenderedModal = renderModal();
750
+
751
+ typeSearch(view, "no-such-column");
752
+
753
+ expect(
754
+ view.getByTestId("column-customization-no-results"),
755
+ ).toBeInTheDocument();
756
+
757
+ fireEvent.click(view.getByTestId("column-customization-search-clear"));
758
+
759
+ expect(
760
+ view.queryByTestId("column-customization-no-results"),
761
+ ).not.toBeInTheDocument();
762
+ expect(view.getAllByTestId(/^column-toggle-/)).toHaveLength(4);
763
+ });
764
+
765
+ test("the hint explains reordering, and explains its absence while searching", () => {
766
+ const { view }: RenderedModal = renderModal();
767
+
768
+ expect(view.getByTestId("column-customization-hint")).toHaveTextContent(
769
+ "Drag a row, or use the arrows, to change the column order.",
770
+ );
771
+
772
+ typeSearch(view, "desc");
773
+
774
+ /*
775
+ * The move buttons go grey during a search. Without this line the viewer
776
+ * is left to guess whether that is deliberate.
777
+ */
778
+ expect(view.getByTestId("column-customization-hint")).toHaveTextContent(
779
+ "Clear the search to reorder columns.",
780
+ );
781
+
782
+ typeSearch(view, "");
783
+
784
+ expect(view.getByTestId("column-customization-hint")).toHaveTextContent(
785
+ "Drag a row, or use the arrows, to change the column order.",
786
+ );
787
+ });
788
+
789
+ test("whitespace alone is not a search", () => {
790
+ const { view }: RenderedModal = renderModal();
791
+
792
+ typeSearch(view, " ");
793
+
794
+ // Nothing is filtered out, so nothing about the list should change either.
795
+ expect(view.getAllByTestId(/^column-toggle-/)).toHaveLength(4);
796
+ expect(view.getByTestId("column-customization-hint")).toHaveTextContent(
797
+ "Drag a row, or use the arrows, to change the column order.",
798
+ );
799
+ expect(view.getByTestId("column-move-down-name")).not.toBeDisabled();
800
+ });
801
+
802
+ test("a search that matches everything leaves every row in place", () => {
803
+ const { view }: RenderedModal = renderModal({
804
+ columns: [
805
+ makeEntry({ id: "one", title: "Alpha One", isVisible: true }),
806
+ makeEntry({ id: "two", title: "Alpha Two", isVisible: true }),
807
+ ],
808
+ });
809
+
810
+ typeSearch(view, "alpha");
811
+
812
+ expect(view.getAllByTestId(/^column-toggle-/)).toHaveLength(2);
813
+ // Still a search, so reordering stays off.
814
+ expect(view.getByTestId("column-move-down-one")).toBeDisabled();
815
+ });
816
+
817
+ test("a column can be toggled from inside a filtered list", () => {
818
+ const { view, onSave }: RenderedModal = renderModal();
819
+
820
+ typeSearch(view, "description");
821
+ fireEvent.click(view.getByTestId("column-toggle-description"));
822
+
823
+ expect(view.getByTestId("column-customization-count")).toHaveTextContent(
824
+ "2 of 4 columns shown",
825
+ );
826
+
827
+ fireEvent.click(view.getByTestId("modal-footer-submit-button"));
828
+
829
+ expect(
830
+ getSaved(onSave).find((entry: CustomizableColumn<Monitor>) => {
831
+ return entry.id === "description";
832
+ })?.isVisible,
833
+ ).toBe(false);
834
+ });
835
+
836
+ test("bulk actions reach the columns a search is hiding", () => {
837
+ const { view, onSave }: RenderedModal = renderModal();
838
+
839
+ typeSearch(view, "name");
840
+ fireEvent.click(view.getByTestId("column-customization-show-all"));
841
+ typeSearch(view, "");
842
+
843
+ /*
844
+ * "Show all" is not "show all of the matches" - it works on the layout,
845
+ * not on what happens to be on screen.
846
+ */
847
+ expect(view.getByTestId("column-customization-count")).toHaveTextContent(
848
+ "4 of 4 columns shown",
849
+ );
850
+
851
+ fireEvent.click(view.getByTestId("modal-footer-submit-button"));
852
+
853
+ expect(
854
+ getSaved(onSave).every((entry: CustomizableColumn<Monitor>) => {
855
+ return entry.isVisible;
856
+ }),
857
+ ).toBe(true);
858
+ });
859
+
860
+ test("Save reports the entries untouched when nothing was changed", () => {
861
+ const { view, onSave }: RenderedModal = renderModal();
862
+
863
+ fireEvent.click(view.getByTestId("modal-footer-submit-button"));
864
+
865
+ expect(getSavedIds(onSave)).toEqual([
866
+ "name",
867
+ "currentMonitorStatus",
868
+ "description",
869
+ "createdAt",
870
+ ]);
871
+ expect(
872
+ getSaved(onSave).map((entry: CustomizableColumn<Monitor>) => {
873
+ return entry.isVisible;
874
+ }),
875
+ ).toEqual([true, true, true, false]);
876
+ });
877
+
878
+ test("Escape closes the modal without saving", () => {
879
+ const { onSave, onClose }: RenderedModal = renderModal();
880
+
881
+ fireEvent.keyDown(document, { key: "Escape" });
882
+
883
+ expect(onClose).toHaveBeenCalledTimes(1);
884
+ expect(onSave).not.toHaveBeenCalled();
885
+ });
886
+
887
+ test("the header close button closes without saving", () => {
888
+ const { view, onSave, onClose }: RenderedModal = renderModal();
889
+
890
+ fireEvent.click(view.getByTestId("column-toggle-description"));
891
+ fireEvent.click(view.getByTestId("close-button"));
892
+
893
+ expect(onClose).toHaveBeenCalledTimes(1);
894
+ expect(onSave).not.toHaveBeenCalled();
895
+ });
896
+
897
+ test("a single-column table opens with everything locked down", () => {
898
+ const { view }: RenderedModal = renderModal({
899
+ columns: [
900
+ makeEntry({ id: "name", title: "Monitor Name", isVisible: true }),
901
+ ],
902
+ });
903
+
904
+ expect(view.getByTestId("column-toggle-name")).toBeDisabled();
905
+ expect(view.getByTestId("column-move-up-name")).toBeDisabled();
906
+ expect(view.getByTestId("column-move-down-name")).toBeDisabled();
907
+ expect(view.getByTestId("column-customization-show-all")).toBeDisabled();
908
+ expect(view.getByTestId("column-customization-hide-all")).toBeDisabled();
909
+ expect(view.getByTestId("column-customization-count")).toHaveTextContent(
910
+ "1 of 1 columns shown",
911
+ );
912
+ });
913
+
914
+ test("an empty column list renders without falling over", () => {
915
+ const { view }: RenderedModal = renderModal({ columns: [] });
916
+
917
+ expect(view.getByTestId("column-customization-modal")).toBeInTheDocument();
918
+ expect(view.queryAllByTestId(/^column-toggle-/)).toHaveLength(0);
919
+ expect(view.getByTestId("column-customization-count")).toHaveTextContent(
920
+ "0 of 0 columns shown",
921
+ );
922
+ });
923
+
924
+ test("columns with the same title are still addressed individually", () => {
925
+ const { view, onSave }: RenderedModal = renderModal({
926
+ columns: [
927
+ makeEntry({ id: "createdAt", title: "Date", isVisible: true }),
928
+ makeEntry({ id: "updatedAt", title: "Date", isVisible: true }),
929
+ ],
930
+ });
931
+
932
+ // The row is keyed by id, not by the title the viewer happens to see.
933
+ fireEvent.click(view.getByTestId("column-toggle-updatedAt"));
934
+ fireEvent.click(view.getByTestId("modal-footer-submit-button"));
935
+
936
+ expect(
937
+ getSaved(onSave).map((entry: CustomizableColumn<Monitor>) => {
938
+ return { id: entry.id, isVisible: entry.isVisible };
939
+ }),
940
+ ).toEqual([
941
+ { id: "createdAt", isVisible: true },
942
+ { id: "updatedAt", isVisible: false },
943
+ ]);
944
+ });
945
+
946
+ test("a column with no title is searchable without breaking the filter", () => {
947
+ const untitled: CustomizableColumn<Monitor> = makeEntry({
948
+ id: "untitled",
949
+ title: "",
950
+ isVisible: true,
951
+ });
952
+
953
+ const { view }: RenderedModal = renderModal({
954
+ columns: [
955
+ makeEntry({ id: "name", title: "Monitor Name", isVisible: true }),
956
+ untitled,
957
+ ],
958
+ });
959
+
960
+ expect(view.getAllByTestId(/^column-toggle-/)).toHaveLength(2);
961
+
962
+ typeSearch(view, "monitor");
963
+
964
+ // The untitled column simply does not match; it does not throw.
965
+ expect(view.getAllByTestId(/^column-toggle-/)).toHaveLength(1);
966
+ expect(view.getByTestId("column-toggle-name")).toBeInTheDocument();
967
+ });
968
+
969
+ test("Reset does not close the modal on the viewer's behalf", () => {
970
+ const { view, onReset, onClose }: RenderedModal = renderModal({
971
+ isDefaultLayout: false,
972
+ });
973
+
974
+ fireEvent.click(view.getByTestId("column-customization-reset"));
975
+
976
+ /*
977
+ * Closing is the caller's decision - it owns the stored layout and this
978
+ * modal only reports the click.
979
+ */
980
+ expect(onReset).toHaveBeenCalledTimes(1);
981
+ expect(onClose).not.toHaveBeenCalled();
982
+ });
983
+
984
+ test("the props handed to Save are a fresh list, not the caller's array", () => {
985
+ const columns: Array<CustomizableColumn<Monitor>> = getDefaultColumns();
986
+ const { view, onSave }: RenderedModal = renderModal({ columns });
987
+
988
+ fireEvent.click(view.getByTestId("column-move-down-name"));
989
+ fireEvent.click(view.getByTestId("modal-footer-submit-button"));
990
+
991
+ // The caller's own array is left exactly as it was passed in.
992
+ expect(
993
+ columns.map((entry: CustomizableColumn<Monitor>) => {
994
+ return entry.id;
995
+ }),
996
+ ).toEqual(["name", "currentMonitorStatus", "description", "createdAt"]);
997
+ expect(getSaved(onSave)).not.toBe(columns);
998
+ });
999
+ });