@oneuptime/common 12.0.2 → 12.0.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 (384) hide show
  1. package/Models/DatabaseModels/CephCluster.ts +14 -0
  2. package/Models/DatabaseModels/CodeRepository.ts +14 -6
  3. package/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.ts +14 -0
  4. package/Models/DatabaseModels/DockerHost.ts +14 -0
  5. package/Models/DatabaseModels/GlobalOidcProject.ts +18 -0
  6. package/Models/DatabaseModels/GlobalSsoProject.ts +18 -0
  7. package/Models/DatabaseModels/Index.ts +2 -0
  8. package/Models/DatabaseModels/IoTFleet.ts +14 -0
  9. package/Models/DatabaseModels/KubernetesCluster.ts +14 -0
  10. package/Models/DatabaseModels/LogDropFilter.ts +9 -20
  11. package/Models/DatabaseModels/NetworkDevice.ts +14 -0
  12. package/Models/DatabaseModels/NetworkInterface.ts +18 -0
  13. package/Models/DatabaseModels/Project.ts +16 -2
  14. package/Models/DatabaseModels/ProxmoxCluster.ts +14 -0
  15. package/Models/DatabaseModels/Service.ts +30 -0
  16. package/Models/DatabaseModels/StatusPageMonitorRule.ts +911 -0
  17. package/Models/DatabaseModels/StatusPagePrivateUser.ts +30 -0
  18. package/Models/DatabaseModels/StatusPageResource.ts +73 -0
  19. package/Models/DatabaseModels/TraceDropFilter.ts +9 -20
  20. package/Models/DatabaseModels/User.ts +30 -0
  21. package/Server/API/AIAgentDataAPI.ts +31 -0
  22. package/Server/API/BaseAPI.ts +13 -2
  23. package/Server/API/DashboardAPI.ts +458 -12
  24. package/Server/API/GitHubAPI.ts +119 -283
  25. package/Server/API/UserAPI.ts +263 -1
  26. package/Server/EnvironmentConfig.ts +38 -4
  27. package/Server/Infrastructure/GlobalCache.ts +152 -0
  28. package/Server/Infrastructure/Postgres/DataSourceOptions.ts +16 -0
  29. package/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.ts +123 -0
  30. package/Server/Infrastructure/Postgres/SchemaMigrations/1786018109307-AddPerUserPasswordSalt.ts +21 -0
  31. package/Server/Infrastructure/Postgres/SchemaMigrations/1786023262402-WidenHashedStringColumnsForScrypt.ts +68 -0
  32. package/Server/Infrastructure/Postgres/SchemaMigrations/1786100000000-RestoreServiceLowerNameIndex.ts +100 -0
  33. package/Server/Infrastructure/Postgres/SchemaMigrations/1786200000000-RestoreDroppedUniqueIndexes.ts +214 -0
  34. package/Server/Infrastructure/Postgres/SchemaMigrations/1786300000000-QuarantineUnboundGitHubInstallations.ts +69 -0
  35. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +12 -0
  36. package/Server/Services/AlertStateTimelineService.ts +20 -0
  37. package/Server/Services/AnalyticsDatabaseService.ts +72 -1
  38. package/Server/Services/CephClusterService.ts +26 -86
  39. package/Server/Services/CloudResourceService.ts +23 -83
  40. package/Server/Services/CodeRepositoryService.ts +105 -2
  41. package/Server/Services/DatabaseService.ts +296 -2
  42. package/Server/Services/DockerHostService.ts +19 -79
  43. package/Server/Services/DockerSwarmClusterService.ts +27 -88
  44. package/Server/Services/HostService.ts +46 -99
  45. package/Server/Services/IncidentStateTimelineService.ts +20 -0
  46. package/Server/Services/Index.ts +2 -0
  47. package/Server/Services/IoTFleetService.ts +19 -79
  48. package/Server/Services/KubernetesClusterService.ts +17 -78
  49. package/Server/Services/LabelService.ts +57 -10
  50. package/Server/Services/MetricTypeService.ts +84 -0
  51. package/Server/Services/MonitorService.ts +89 -14
  52. package/Server/Services/MonitorStatusTimelineService.ts +29 -0
  53. package/Server/Services/MonitorTemplateService.ts +65 -0
  54. package/Server/Services/OpenTelemetryIngestService.ts +230 -26
  55. package/Server/Services/PodmanHostService.ts +19 -79
  56. package/Server/Services/ProxmoxClusterService.ts +23 -83
  57. package/Server/Services/RumApplicationService.ts +19 -80
  58. package/Server/Services/ScheduledMaintenanceStateTimelineService.ts +21 -0
  59. package/Server/Services/ServerlessFunctionService.ts +24 -85
  60. package/Server/Services/ServiceService.ts +299 -73
  61. package/Server/Services/StatusPageMonitorRuleEngineService.ts +752 -0
  62. package/Server/Services/StatusPageMonitorRuleService.ts +471 -0
  63. package/Server/Services/TelemetryEntityRelationshipService.ts +1 -0
  64. package/Server/Services/TelemetryEntityService.ts +1 -0
  65. package/Server/Services/UserService.ts +184 -0
  66. package/Server/Utils/AI/Toolbox/CodeTools.ts +45 -4
  67. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +13 -0
  68. package/Server/Utils/CodeRepository/GitHub/GitHub.ts +157 -4
  69. package/Server/Utils/CodeRepository/GitHub/GitHubInstallationBinding.ts +130 -0
  70. package/Server/Utils/Database/ProjectScopedReferenceValidator.ts +170 -39
  71. package/Server/Utils/Monitor/MonitorResource.ts +63 -36
  72. package/Server/Utils/Monitor/MonitorStatusTimeline.ts +36 -0
  73. package/Server/Utils/Monitor/MonitorStepsProjectValidator.ts +148 -127
  74. package/Server/Utils/Monitor/MonitorStepsReferenceExtractor.ts +434 -0
  75. package/Server/Utils/PasswordHash.ts +306 -0
  76. package/Server/Utils/SingleFlight.ts +83 -0
  77. package/Server/Utils/StatusPage/MonitorRulePatternValidator.ts +46 -0
  78. package/Server/Utils/Telemetry/EntityRegistry.ts +107 -26
  79. package/Server/Utils/Telemetry/ResourceHeartbeat.ts +314 -0
  80. package/Server/Utils/Telemetry/Telemetry.ts +351 -122
  81. package/Tests/App/Dashboard/OverviewCustomFields.test.tsx +333 -0
  82. package/Tests/App/Dashboard/UserCustomFields.test.tsx +392 -0
  83. package/Tests/App/Dashboard/WidgetCatalog.test.ts +2 -0
  84. package/Tests/Models/DatabaseModels/ColumnTransformerInvariants.test.ts +306 -0
  85. package/Tests/Models/DatabaseModels/NumberColumnDeserialization.test.ts +333 -0
  86. package/Tests/Models/DatabaseModels/StatusPageMonitorRuleModel.test.ts +190 -0
  87. package/Tests/Server/API/AIAgentDataRepositoryToken.test.ts +381 -0
  88. package/Tests/Server/API/DashboardPublicAttributeValuesAPI.test.ts +943 -0
  89. package/Tests/Server/API/DashboardPublicMetricsAggregateAPI.test.ts +1296 -0
  90. package/Tests/Server/API/DashboardPublicResourceListAPI.test.ts +26 -0
  91. package/Tests/Server/API/DashboardPublicTemplatePayloads.test.ts +598 -0
  92. package/Tests/Server/API/GitHubAppInstallationBindingAPI.test.ts +539 -0
  93. package/Tests/Server/API/Helpers.ts +6 -1
  94. package/Tests/Server/API/UserProjectsAPI.test.ts +852 -0
  95. package/Tests/Server/Infrastructure/GlobalCache.test.ts +354 -0
  96. package/Tests/Server/Infrastructure/PostgresLockTimeoutOptions.test.ts +181 -0
  97. package/Tests/Server/Infrastructure/SemaphoreMutex.test.ts +215 -0
  98. package/Tests/Server/Services/AddPerUserPasswordSaltMigration.test.ts +178 -0
  99. package/Tests/Server/Services/AnalyticsDatabasePaginationStability.test.ts +582 -0
  100. package/Tests/Server/Services/AnalyticsDatabaseService.test.ts +5 -0
  101. package/Tests/Server/Services/CodeRepositoryInstallationBinding.test.ts +305 -0
  102. package/Tests/Server/Services/CodeRepositoryResolutionBinding.test.ts +147 -0
  103. package/Tests/Server/Services/DatabaseServicePerUserPasswordSalt.test.ts +870 -0
  104. package/Tests/Server/Services/DatabaseServiceUpdateColumnsIfUnlocked.test.ts +342 -0
  105. package/Tests/Server/Services/DropFilterSaveValidation.test.ts +245 -0
  106. package/Tests/Server/Services/HostServiceUpdateLastSeen.test.ts +163 -15
  107. package/Tests/Server/Services/MetricTypeAttachServices.test.ts +222 -0
  108. package/Tests/Server/Services/MissingReferenceWriteGuard.test.ts +614 -0
  109. package/Tests/Server/Services/MonitorStatusTimelineFastPath.test.ts +23 -0
  110. package/Tests/Server/Services/OpenTelemetryServiceResolutionCache.test.ts +568 -0
  111. package/Tests/Server/Services/ResourceUpdateLastSeenLivenessFallback.test.ts +305 -56
  112. package/Tests/Server/Services/RestoreDroppedUniqueIndexesMigration.test.ts +386 -0
  113. package/Tests/Server/Services/RestoreServiceLowerNameIndexMigration.test.ts +240 -0
  114. package/Tests/Server/Services/ServiceUpdateLastSeenSplitGates.test.ts +705 -0
  115. package/Tests/Server/Services/StatusPageMonitorRuleEngineService.test.ts +1719 -0
  116. package/Tests/Server/Services/StatusPageMonitorRuleHooks.test.ts +1212 -0
  117. package/Tests/Server/Services/StatusPageMonitorRuleMonitorCreateHook.test.ts +228 -0
  118. package/Tests/Server/Services/UserServiceFirstMasterAdminElection.test.ts +885 -0
  119. package/Tests/Server/Services/WidenHashedStringColumnsForScryptMigration.test.ts +220 -0
  120. package/Tests/Server/Utils/AI/CodeTools.test.ts +47 -1
  121. package/Tests/Server/Utils/AI/CodeWriteTools.test.ts +10 -0
  122. package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +267 -0
  123. package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +120 -0
  124. package/Tests/Server/Utils/CodeRepository/GitHubInstallationBinding.test.ts +292 -0
  125. package/Tests/Server/Utils/Database/ProjectScopedReferenceValidator.test.ts +260 -13
  126. package/Tests/Server/Utils/EntityRegistryRowFence.test.ts +302 -0
  127. package/Tests/Server/Utils/GitHubInstallationOwnershipVerification.test.ts +340 -0
  128. package/Tests/Server/Utils/GitHubWebhookAndTreeCacheIsolation.test.ts +250 -0
  129. package/Tests/Server/Utils/MetricTypeIndexConvergence.test.ts +598 -0
  130. package/Tests/Server/Utils/Monitor/MonitorStatusTimelineUnusableStatus.test.ts +154 -0
  131. package/Tests/Server/Utils/Monitor/MonitorStepsProjectValidator.test.ts +638 -92
  132. package/Tests/Server/Utils/Monitor/MonitorStepsReferenceExtractor.test.ts +724 -0
  133. package/Tests/Server/Utils/PasswordHash.test.ts +555 -0
  134. package/Tests/Server/Utils/SingleFlight.test.ts +221 -0
  135. package/Tests/Types/Dashboard/NetworkDashboardTemplate.test.ts +793 -0
  136. package/Tests/Types/Database/BigIntColumnTransformer.test.ts +147 -0
  137. package/Tests/Types/Database/ColumnLength.test.ts +7 -1
  138. package/Tests/Types/Database/DatabasePropertyTransformer.test.ts +140 -0
  139. package/Tests/Types/Database/NumericColumnValue.test.ts +127 -0
  140. package/Tests/Types/Decimal.test.ts +37 -0
  141. package/Tests/Types/HashedStringPerUserSalt.test.ts +476 -0
  142. package/Tests/Types/Monitor/DockerAlertTemplates.test.ts +293 -0
  143. package/Tests/Types/Monitor/HostAlertTemplates.test.ts +249 -0
  144. package/Tests/Types/Monitor/IotAlertTemplates.test.ts +320 -0
  145. package/Tests/Types/Monitor/PodmanAlertTemplates.test.ts +271 -0
  146. package/Tests/Types/Monitor/SnmpVersionParsing.test.ts +152 -0
  147. package/Tests/Types/Port.test.ts +38 -0
  148. package/Tests/UI/Components/CustomFields/CustomFieldsDetail.test.tsx +696 -0
  149. package/Tests/UI/Components/LogsHistogram.test.tsx +276 -0
  150. package/Tests/UI/Components/UseLiveLogsRefresh.test.tsx +489 -0
  151. package/Tests/UI/Components/UseLogsHistogram.test.tsx +500 -0
  152. package/Tests/UI/Utils/UserProjectsModelAPI.test.ts +714 -0
  153. package/Tests/Utils/Dashboard/Components/DashboardComponentsUtil.test.ts +120 -0
  154. package/Tests/Utils/Dashboard/DashboardNetworkMapComponent.test.ts +327 -0
  155. package/Tests/Utils/TeamMembersByProject.test.ts +655 -0
  156. package/Types/Dashboard/DashboardComponentType.ts +1 -0
  157. package/Types/Dashboard/DashboardComponents/ComponentArgument.ts +1 -0
  158. package/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.ts +40 -0
  159. package/Types/Dashboard/DashboardTemplates.ts +397 -0
  160. package/Types/Database/BigIntColumnTransformer.ts +74 -0
  161. package/Types/Database/ColumnLength.ts +9 -1
  162. package/Types/Database/DatabaseProperty.ts +26 -0
  163. package/Types/Database/NumericColumnValue.ts +128 -0
  164. package/Types/Database/TableColumn.ts +11 -0
  165. package/Types/Database/UnsynchronizedIndex.ts +48 -0
  166. package/Types/Decimal.ts +16 -4
  167. package/Types/HashedString.ts +139 -4
  168. package/Types/Permission.ts +48 -0
  169. package/Types/Port.ts +16 -4
  170. package/UI/Components/CustomFields/CustomFieldsDetail.tsx +85 -11
  171. package/UI/Components/LogsViewer/useLiveLogsRefresh.ts +86 -0
  172. package/UI/Components/LogsViewer/useLogsHistogram.ts +113 -0
  173. package/UI/Components/Navbar/NavBar.tsx +12 -2
  174. package/UI/Components/Navbar/NavBarMenuModal.tsx +19 -7
  175. package/UI/Utils/ModelAPI/UserProjectsModelAPI.ts +257 -0
  176. package/Utils/Dashboard/Components/DashboardNetworkMapComponent.ts +136 -0
  177. package/Utils/Dashboard/Components/Index.ts +7 -0
  178. package/Utils/TeamMembersByProject.ts +237 -0
  179. package/build/dist/Models/DatabaseModels/CephCluster.js +16 -1
  180. package/build/dist/Models/DatabaseModels/CephCluster.js.map +1 -1
  181. package/build/dist/Models/DatabaseModels/CodeRepository.js +14 -6
  182. package/build/dist/Models/DatabaseModels/CodeRepository.js.map +1 -1
  183. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js +12 -0
  184. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js.map +1 -1
  185. package/build/dist/Models/DatabaseModels/DockerHost.js +16 -1
  186. package/build/dist/Models/DatabaseModels/DockerHost.js.map +1 -1
  187. package/build/dist/Models/DatabaseModels/GlobalOidcProject.js +16 -1
  188. package/build/dist/Models/DatabaseModels/GlobalOidcProject.js.map +1 -1
  189. package/build/dist/Models/DatabaseModels/GlobalSsoProject.js +16 -1
  190. package/build/dist/Models/DatabaseModels/GlobalSsoProject.js.map +1 -1
  191. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  192. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  193. package/build/dist/Models/DatabaseModels/IoTFleet.js +16 -1
  194. package/build/dist/Models/DatabaseModels/IoTFleet.js.map +1 -1
  195. package/build/dist/Models/DatabaseModels/KubernetesCluster.js +16 -1
  196. package/build/dist/Models/DatabaseModels/KubernetesCluster.js.map +1 -1
  197. package/build/dist/Models/DatabaseModels/LogDropFilter.js +9 -20
  198. package/build/dist/Models/DatabaseModels/LogDropFilter.js.map +1 -1
  199. package/build/dist/Models/DatabaseModels/NetworkDevice.js +16 -1
  200. package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
  201. package/build/dist/Models/DatabaseModels/NetworkInterface.js +16 -1
  202. package/build/dist/Models/DatabaseModels/NetworkInterface.js.map +1 -1
  203. package/build/dist/Models/DatabaseModels/Project.js +16 -2
  204. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  205. package/build/dist/Models/DatabaseModels/ProxmoxCluster.js +16 -1
  206. package/build/dist/Models/DatabaseModels/ProxmoxCluster.js.map +1 -1
  207. package/build/dist/Models/DatabaseModels/Service.js +28 -1
  208. package/build/dist/Models/DatabaseModels/Service.js.map +1 -1
  209. package/build/dist/Models/DatabaseModels/StatusPageMonitorRule.js +925 -0
  210. package/build/dist/Models/DatabaseModels/StatusPageMonitorRule.js.map +1 -0
  211. package/build/dist/Models/DatabaseModels/StatusPagePrivateUser.js +32 -0
  212. package/build/dist/Models/DatabaseModels/StatusPagePrivateUser.js.map +1 -1
  213. package/build/dist/Models/DatabaseModels/StatusPageResource.js +72 -0
  214. package/build/dist/Models/DatabaseModels/StatusPageResource.js.map +1 -1
  215. package/build/dist/Models/DatabaseModels/TraceDropFilter.js +9 -20
  216. package/build/dist/Models/DatabaseModels/TraceDropFilter.js.map +1 -1
  217. package/build/dist/Models/DatabaseModels/User.js +32 -0
  218. package/build/dist/Models/DatabaseModels/User.js.map +1 -1
  219. package/build/dist/Server/API/AIAgentDataAPI.js +21 -2
  220. package/build/dist/Server/API/AIAgentDataAPI.js.map +1 -1
  221. package/build/dist/Server/API/BaseAPI.js +11 -1
  222. package/build/dist/Server/API/BaseAPI.js.map +1 -1
  223. package/build/dist/Server/API/DashboardAPI.js +343 -10
  224. package/build/dist/Server/API/DashboardAPI.js.map +1 -1
  225. package/build/dist/Server/API/GitHubAPI.js +94 -167
  226. package/build/dist/Server/API/GitHubAPI.js.map +1 -1
  227. package/build/dist/Server/API/UserAPI.js +187 -2
  228. package/build/dist/Server/API/UserAPI.js.map +1 -1
  229. package/build/dist/Server/EnvironmentConfig.js +34 -4
  230. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  231. package/build/dist/Server/Infrastructure/GlobalCache.js +120 -0
  232. package/build/dist/Server/Infrastructure/GlobalCache.js.map +1 -1
  233. package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js +5 -3
  234. package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js.map +1 -1
  235. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.js +48 -0
  236. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.js.map +1 -0
  237. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786018109307-AddPerUserPasswordSalt.js +14 -0
  238. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786018109307-AddPerUserPasswordSalt.js.map +1 -0
  239. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786023262402-WidenHashedStringColumnsForScrypt.js +59 -0
  240. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786023262402-WidenHashedStringColumnsForScrypt.js.map +1 -0
  241. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786100000000-RestoreServiceLowerNameIndex.js +93 -0
  242. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786100000000-RestoreServiceLowerNameIndex.js.map +1 -0
  243. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786200000000-RestoreDroppedUniqueIndexes.js +101 -0
  244. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786200000000-RestoreDroppedUniqueIndexes.js.map +1 -0
  245. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786300000000-QuarantineUnboundGitHubInstallations.js +66 -0
  246. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786300000000-QuarantineUnboundGitHubInstallations.js.map +1 -0
  247. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +12 -0
  248. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  249. package/build/dist/Server/Services/AlertStateTimelineService.js +19 -0
  250. package/build/dist/Server/Services/AlertStateTimelineService.js.map +1 -1
  251. package/build/dist/Server/Services/AnalyticsDatabaseService.js +57 -1
  252. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  253. package/build/dist/Server/Services/CephClusterService.js +26 -76
  254. package/build/dist/Server/Services/CephClusterService.js.map +1 -1
  255. package/build/dist/Server/Services/CloudResourceService.js +23 -73
  256. package/build/dist/Server/Services/CloudResourceService.js.map +1 -1
  257. package/build/dist/Server/Services/CodeRepositoryService.js +90 -2
  258. package/build/dist/Server/Services/CodeRepositoryService.js.map +1 -1
  259. package/build/dist/Server/Services/DatabaseService.js +246 -2
  260. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  261. package/build/dist/Server/Services/DockerHostService.js +19 -69
  262. package/build/dist/Server/Services/DockerHostService.js.map +1 -1
  263. package/build/dist/Server/Services/DockerSwarmClusterService.js +27 -78
  264. package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
  265. package/build/dist/Server/Services/HostService.js +58 -102
  266. package/build/dist/Server/Services/HostService.js.map +1 -1
  267. package/build/dist/Server/Services/IncidentStateTimelineService.js +19 -0
  268. package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
  269. package/build/dist/Server/Services/Index.js +2 -0
  270. package/build/dist/Server/Services/Index.js.map +1 -1
  271. package/build/dist/Server/Services/IoTFleetService.js +19 -69
  272. package/build/dist/Server/Services/IoTFleetService.js.map +1 -1
  273. package/build/dist/Server/Services/KubernetesClusterService.js +17 -68
  274. package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
  275. package/build/dist/Server/Services/LabelService.js +42 -7
  276. package/build/dist/Server/Services/LabelService.js.map +1 -1
  277. package/build/dist/Server/Services/MetricTypeService.js +74 -0
  278. package/build/dist/Server/Services/MetricTypeService.js.map +1 -1
  279. package/build/dist/Server/Services/MonitorService.js +81 -9
  280. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  281. package/build/dist/Server/Services/MonitorStatusTimelineService.js +28 -0
  282. package/build/dist/Server/Services/MonitorStatusTimelineService.js.map +1 -1
  283. package/build/dist/Server/Services/MonitorTemplateService.js +60 -0
  284. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  285. package/build/dist/Server/Services/OpenTelemetryIngestService.js +155 -25
  286. package/build/dist/Server/Services/OpenTelemetryIngestService.js.map +1 -1
  287. package/build/dist/Server/Services/PodmanHostService.js +19 -69
  288. package/build/dist/Server/Services/PodmanHostService.js.map +1 -1
  289. package/build/dist/Server/Services/ProxmoxClusterService.js +23 -73
  290. package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -1
  291. package/build/dist/Server/Services/RumApplicationService.js +19 -70
  292. package/build/dist/Server/Services/RumApplicationService.js.map +1 -1
  293. package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js +20 -0
  294. package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js.map +1 -1
  295. package/build/dist/Server/Services/ServerlessFunctionService.js +24 -75
  296. package/build/dist/Server/Services/ServerlessFunctionService.js.map +1 -1
  297. package/build/dist/Server/Services/ServiceService.js +236 -61
  298. package/build/dist/Server/Services/ServiceService.js.map +1 -1
  299. package/build/dist/Server/Services/StatusPageMonitorRuleEngineService.js +603 -0
  300. package/build/dist/Server/Services/StatusPageMonitorRuleEngineService.js.map +1 -0
  301. package/build/dist/Server/Services/StatusPageMonitorRuleService.js +401 -0
  302. package/build/dist/Server/Services/StatusPageMonitorRuleService.js.map +1 -0
  303. package/build/dist/Server/Services/TelemetryEntityRelationshipService.js +1 -0
  304. package/build/dist/Server/Services/TelemetryEntityRelationshipService.js.map +1 -1
  305. package/build/dist/Server/Services/TelemetryEntityService.js +1 -0
  306. package/build/dist/Server/Services/TelemetryEntityService.js.map +1 -1
  307. package/build/dist/Server/Services/UserService.js +165 -0
  308. package/build/dist/Server/Services/UserService.js.map +1 -1
  309. package/build/dist/Server/Utils/AI/Toolbox/CodeTools.js +31 -1
  310. package/build/dist/Server/Utils/AI/Toolbox/CodeTools.js.map +1 -1
  311. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +13 -0
  312. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  313. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js +126 -4
  314. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js.map +1 -1
  315. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubInstallationBinding.js +136 -0
  316. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubInstallationBinding.js.map +1 -0
  317. package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js +105 -29
  318. package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js.map +1 -1
  319. package/build/dist/Server/Utils/Monitor/MonitorResource.js +56 -32
  320. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  321. package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js +30 -3
  322. package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js.map +1 -1
  323. package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js +110 -98
  324. package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js.map +1 -1
  325. package/build/dist/Server/Utils/Monitor/MonitorStepsReferenceExtractor.js +312 -0
  326. package/build/dist/Server/Utils/Monitor/MonitorStepsReferenceExtractor.js.map +1 -0
  327. package/build/dist/Server/Utils/PasswordHash.js +237 -0
  328. package/build/dist/Server/Utils/PasswordHash.js.map +1 -0
  329. package/build/dist/Server/Utils/SingleFlight.js +76 -0
  330. package/build/dist/Server/Utils/SingleFlight.js.map +1 -0
  331. package/build/dist/Server/Utils/StatusPage/MonitorRulePatternValidator.js +29 -0
  332. package/build/dist/Server/Utils/StatusPage/MonitorRulePatternValidator.js.map +1 -0
  333. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +78 -19
  334. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
  335. package/build/dist/Server/Utils/Telemetry/ResourceHeartbeat.js +160 -0
  336. package/build/dist/Server/Utils/Telemetry/ResourceHeartbeat.js.map +1 -0
  337. package/build/dist/Server/Utils/Telemetry/Telemetry.js +279 -104
  338. package/build/dist/Server/Utils/Telemetry/Telemetry.js.map +1 -1
  339. package/build/dist/Types/Dashboard/DashboardComponentType.js +1 -0
  340. package/build/dist/Types/Dashboard/DashboardComponentType.js.map +1 -1
  341. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js +1 -0
  342. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js.map +1 -1
  343. package/build/dist/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.js +2 -0
  344. package/build/dist/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.js.map +1 -0
  345. package/build/dist/Types/Dashboard/DashboardTemplates.js +368 -0
  346. package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
  347. package/build/dist/Types/Database/BigIntColumnTransformer.js +66 -0
  348. package/build/dist/Types/Database/BigIntColumnTransformer.js.map +1 -0
  349. package/build/dist/Types/Database/ColumnLength.js +9 -1
  350. package/build/dist/Types/Database/ColumnLength.js.map +1 -1
  351. package/build/dist/Types/Database/DatabaseProperty.js +25 -0
  352. package/build/dist/Types/Database/DatabaseProperty.js.map +1 -1
  353. package/build/dist/Types/Database/NumericColumnValue.js +97 -0
  354. package/build/dist/Types/Database/NumericColumnValue.js.map +1 -0
  355. package/build/dist/Types/Database/TableColumn.js.map +1 -1
  356. package/build/dist/Types/Database/UnsynchronizedIndex.js +46 -0
  357. package/build/dist/Types/Database/UnsynchronizedIndex.js.map +1 -0
  358. package/build/dist/Types/Decimal.js +14 -3
  359. package/build/dist/Types/Decimal.js.map +1 -1
  360. package/build/dist/Types/HashedString.js +95 -5
  361. package/build/dist/Types/HashedString.js.map +1 -1
  362. package/build/dist/Types/Permission.js +42 -0
  363. package/build/dist/Types/Permission.js.map +1 -1
  364. package/build/dist/Types/Port.js +15 -5
  365. package/build/dist/Types/Port.js.map +1 -1
  366. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +51 -10
  367. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
  368. package/build/dist/UI/Components/LogsViewer/useLiveLogsRefresh.js +44 -0
  369. package/build/dist/UI/Components/LogsViewer/useLiveLogsRefresh.js.map +1 -0
  370. package/build/dist/UI/Components/LogsViewer/useLogsHistogram.js +75 -0
  371. package/build/dist/UI/Components/LogsViewer/useLogsHistogram.js.map +1 -0
  372. package/build/dist/UI/Components/Navbar/NavBar.js +3 -1
  373. package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
  374. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js +19 -7
  375. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js.map +1 -1
  376. package/build/dist/UI/Utils/ModelAPI/UserProjectsModelAPI.js +156 -0
  377. package/build/dist/UI/Utils/ModelAPI/UserProjectsModelAPI.js.map +1 -0
  378. package/build/dist/Utils/Dashboard/Components/DashboardNetworkMapComponent.js +114 -0
  379. package/build/dist/Utils/Dashboard/Components/DashboardNetworkMapComponent.js.map +1 -0
  380. package/build/dist/Utils/Dashboard/Components/Index.js +4 -0
  381. package/build/dist/Utils/Dashboard/Components/Index.js.map +1 -1
  382. package/build/dist/Utils/TeamMembersByProject.js +145 -0
  383. package/build/dist/Utils/TeamMembersByProject.js.map +1 -0
  384. package/package.json +1 -1
@@ -0,0 +1,696 @@
1
+ import { afterEach, describe, expect, jest, test } from "@jest/globals";
2
+ import "@testing-library/jest-dom";
3
+ import {
4
+ cleanup,
5
+ fireEvent,
6
+ render,
7
+ screen,
8
+ waitFor,
9
+ } from "@testing-library/react";
10
+ import * as React from "react";
11
+ import getJestMockFunction, { MockFunction } from "../../../MockType";
12
+
13
+ /*
14
+ * CustomFieldsDetail used to have exactly one caller per resource: a dedicated
15
+ * "Custom Fields" page reached from the side menu, where a card that says "no
16
+ * custom fields have been added" is the whole point of the page.
17
+ *
18
+ * It is now also mounted on every resource OVERVIEW page, where that same
19
+ * card would be noise on a page nobody opened to look at custom fields - and
20
+ * where a failed schema read (custom fields are gated on both a permission
21
+ * and a billing plan) would staple an error the operator can do nothing about
22
+ * onto every incident they open.
23
+ *
24
+ * `hideIfEmpty` is what separates the two, so most of what follows pins the
25
+ * difference between the two call sites rather than the rendering itself.
26
+ */
27
+
28
+ const getListMock: MockFunction = getJestMockFunction();
29
+ const getItemMock: MockFunction = getJestMockFunction();
30
+ const updateByIdMock: MockFunction = getJestMockFunction();
31
+
32
+ /*
33
+ * The arrow wrappers are load bearing: jest.mock is hoisted above the compiled
34
+ * requires, so naming the mocks directly would capture them before their
35
+ * initializers have run.
36
+ */
37
+ jest.mock("../../../../UI/Utils/ModelAPI/ModelAPI", () => {
38
+ return {
39
+ __esModule: true,
40
+ default: {
41
+ getList: (...args: Array<any>) => {
42
+ return getListMock(...args);
43
+ },
44
+ getItem: (...args: Array<any>) => {
45
+ return getItemMock(...args);
46
+ },
47
+ updateById: (...args: Array<any>) => {
48
+ return updateByIdMock(...args);
49
+ },
50
+ },
51
+ };
52
+ });
53
+
54
+ import CustomFieldsDetail from "../../../../UI/Components/CustomFields/CustomFieldsDetail";
55
+ import BaseModel from "../../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
56
+ import Incident from "../../../../Models/DatabaseModels/Incident";
57
+ import IncidentCustomField from "../../../../Models/DatabaseModels/IncidentCustomField";
58
+ import CustomFieldType from "../../../../Types/CustomField/CustomFieldType";
59
+ import { JSONObject } from "../../../../Types/JSON";
60
+ import ObjectID from "../../../../Types/ObjectID";
61
+
62
+ const PROJECT_ID: ObjectID = new ObjectID(
63
+ "11111111-1111-4111-8111-111111111111",
64
+ );
65
+ const INCIDENT_ID: ObjectID = new ObjectID(
66
+ "22222222-2222-4222-8222-222222222222",
67
+ );
68
+
69
+ const NO_FIELDS_MESSAGE: RegExp = /No custom fields have been added/i;
70
+
71
+ interface SchemaFieldInput {
72
+ name: string;
73
+ description?: string | undefined;
74
+ customFieldType?: CustomFieldType | undefined;
75
+ dropdownOptions?: string | undefined;
76
+ }
77
+
78
+ type BuildSchemaFunction = (
79
+ fields: Array<SchemaFieldInput>,
80
+ ) => Array<IncidentCustomField>;
81
+
82
+ const buildSchema: BuildSchemaFunction = (
83
+ fields: Array<SchemaFieldInput>,
84
+ ): Array<IncidentCustomField> => {
85
+ return fields.map((field: SchemaFieldInput) => {
86
+ const schemaItem: IncidentCustomField = new IncidentCustomField();
87
+ schemaItem.name = field.name;
88
+ schemaItem.description = field.description || "";
89
+ schemaItem.customFieldType = field.customFieldType || CustomFieldType.Text;
90
+
91
+ if (field.dropdownOptions) {
92
+ schemaItem.dropdownOptions = field.dropdownOptions;
93
+ }
94
+
95
+ return schemaItem;
96
+ });
97
+ };
98
+
99
+ type BuildIncidentFunction = (customFields: JSONObject | undefined) => Incident;
100
+
101
+ const buildIncident: BuildIncidentFunction = (
102
+ customFields: JSONObject | undefined,
103
+ ): Incident => {
104
+ const incident: Incident = new Incident();
105
+ incident.id = INCIDENT_ID;
106
+
107
+ if (customFields) {
108
+ incident.customFields = customFields;
109
+ }
110
+
111
+ return incident;
112
+ };
113
+
114
+ /*
115
+ * The component fires both requests from the same effect. Resolving them from
116
+ * a queue rather than a fixed value keeps each test able to say "the schema
117
+ * loads but the record does not" without reaching into call ordering.
118
+ */
119
+ type ResolveListFunction = (data: Array<BaseModel>) => void;
120
+
121
+ const resolveListWith: ResolveListFunction = (data: Array<BaseModel>): void => {
122
+ getListMock.mockResolvedValue({
123
+ data: data,
124
+ count: data.length,
125
+ skip: 0,
126
+ limit: data.length,
127
+ } as never);
128
+ };
129
+
130
+ interface RenderOptions {
131
+ hideIfEmpty?: boolean | undefined;
132
+ isEditable?: boolean | undefined;
133
+ }
134
+
135
+ type RenderCardFunction = (options?: RenderOptions) => void;
136
+
137
+ const renderCard: RenderCardFunction = (options?: RenderOptions): void => {
138
+ render(
139
+ <CustomFieldsDetail
140
+ title="Custom Fields"
141
+ description="Custom fields for this incident."
142
+ modelType={Incident}
143
+ customFieldType={IncidentCustomField}
144
+ name="Incident Custom Fields"
145
+ projectId={PROJECT_ID}
146
+ modelId={INCIDENT_ID}
147
+ hideIfEmpty={options?.hideIfEmpty}
148
+ isEditable={options?.isEditable}
149
+ />,
150
+ );
151
+ };
152
+
153
+ afterEach(() => {
154
+ cleanup();
155
+ getListMock.mockReset();
156
+ getItemMock.mockReset();
157
+ updateByIdMock.mockReset();
158
+ });
159
+
160
+ describe("CustomFieldsDetail - rendering the values", () => {
161
+ test("shows every defined field with the value stored on the record", async () => {
162
+ resolveListWith(
163
+ buildSchema([
164
+ { name: "Customer", description: "Who reported this" },
165
+ { name: "Ticket Number", customFieldType: CustomFieldType.Number },
166
+ ]),
167
+ );
168
+ getItemMock.mockResolvedValue(
169
+ buildIncident({ Customer: "Acme Corp", "Ticket Number": 4711 }) as never,
170
+ );
171
+
172
+ renderCard();
173
+
174
+ expect(await screen.findByText("Customer")).toBeInTheDocument();
175
+ expect(screen.getByText("Who reported this")).toBeInTheDocument();
176
+ expect(screen.getByText("Acme Corp")).toBeInTheDocument();
177
+ expect(screen.getByText("Ticket Number")).toBeInTheDocument();
178
+ expect(screen.getByText("4711")).toBeInTheDocument();
179
+ });
180
+
181
+ test("shows the placeholder for a defined field the record never filled in", async () => {
182
+ resolveListWith(buildSchema([{ name: "Customer" }]));
183
+ getItemMock.mockResolvedValue(buildIncident({}) as never);
184
+
185
+ renderCard();
186
+
187
+ expect(await screen.findByText("Customer")).toBeInTheDocument();
188
+ expect(screen.getByText("No data entered")).toBeInTheDocument();
189
+ });
190
+
191
+ test("renders a record whose customFields column was never written", async () => {
192
+ resolveListWith(buildSchema([{ name: "Customer" }]));
193
+ getItemMock.mockResolvedValue(buildIncident(undefined) as never);
194
+
195
+ renderCard();
196
+
197
+ expect(await screen.findByText("Customer")).toBeInTheDocument();
198
+ expect(screen.getByText("No data entered")).toBeInTheDocument();
199
+ });
200
+
201
+ test("reads the schema for the project it was given, and the record by id", async () => {
202
+ resolveListWith(buildSchema([{ name: "Customer" }]));
203
+ getItemMock.mockResolvedValue(buildIncident({}) as never);
204
+
205
+ renderCard();
206
+
207
+ await screen.findByText("Customer");
208
+
209
+ const listArgs: JSONObject = getListMock.mock.calls[0]![0] as JSONObject;
210
+ expect(listArgs["modelType"]).toBe(IncidentCustomField);
211
+ expect((listArgs["query"] as JSONObject)["projectId"]).toBe(PROJECT_ID);
212
+
213
+ const itemArgs: JSONObject = getItemMock.mock.calls[0]![0] as JSONObject;
214
+ expect(itemArgs["modelType"]).toBe(Incident);
215
+ expect(itemArgs["id"]).toBe(INCIDENT_ID);
216
+ expect((itemArgs["select"] as JSONObject)["customFields"]).toBe(true);
217
+ });
218
+
219
+ test("renders a dropdown value as its configured option", async () => {
220
+ resolveListWith(
221
+ buildSchema([
222
+ {
223
+ name: "Impact",
224
+ customFieldType: CustomFieldType.Dropdown,
225
+ dropdownOptions: JSON.stringify([
226
+ { value: "Low", color: "#22c55e" },
227
+ { value: "High", color: "#ef4444" },
228
+ ]),
229
+ },
230
+ ]),
231
+ );
232
+ getItemMock.mockResolvedValue(buildIncident({ Impact: "High" }) as never);
233
+
234
+ renderCard();
235
+
236
+ expect(await screen.findByText("Impact")).toBeInTheDocument();
237
+ expect(screen.getByText("High")).toBeInTheDocument();
238
+ expect(screen.queryByText("Low")).not.toBeInTheDocument();
239
+ });
240
+
241
+ test("renders every selection of a multi-select dropdown", async () => {
242
+ resolveListWith(
243
+ buildSchema([
244
+ {
245
+ name: "Regions",
246
+ customFieldType: CustomFieldType.MultiSelectDropdown,
247
+ dropdownOptions: "us-east\nus-west\neu-central",
248
+ },
249
+ ]),
250
+ );
251
+ getItemMock.mockResolvedValue(
252
+ buildIncident({ Regions: ["us-east", "eu-central"] }) as never,
253
+ );
254
+
255
+ renderCard();
256
+
257
+ expect(await screen.findByText("Regions")).toBeInTheDocument();
258
+ expect(screen.getByText("us-east")).toBeInTheDocument();
259
+ expect(screen.getByText("eu-central")).toBeInTheDocument();
260
+ expect(screen.queryByText("us-west")).not.toBeInTheDocument();
261
+ });
262
+ });
263
+
264
+ describe("CustomFieldsDetail - the dedicated page (no hideIfEmpty)", () => {
265
+ test("explains that the project has defined no fields", async () => {
266
+ resolveListWith([]);
267
+ getItemMock.mockResolvedValue(buildIncident({}) as never);
268
+
269
+ renderCard();
270
+
271
+ expect(await screen.findByText(NO_FIELDS_MESSAGE)).toBeInTheDocument();
272
+ });
273
+
274
+ test("surfaces a failed schema read instead of hiding it", async () => {
275
+ getListMock.mockRejectedValue(new Error("Plan does not include this"));
276
+ getItemMock.mockResolvedValue(buildIncident({}) as never);
277
+
278
+ renderCard();
279
+
280
+ expect(
281
+ await screen.findByText(/Plan does not include this/i),
282
+ ).toBeInTheDocument();
283
+ });
284
+
285
+ test("says the record is missing once the fetch settles with nothing", async () => {
286
+ resolveListWith(buildSchema([{ name: "Customer" }]));
287
+ getItemMock.mockResolvedValue(null as never);
288
+
289
+ renderCard();
290
+
291
+ expect(await screen.findByText("Item not found")).toBeInTheDocument();
292
+ });
293
+
294
+ /*
295
+ * `model` is null on the very first render too. Announcing "Item not found"
296
+ * then reads as a broken record rather than a pending one, and it flashed on
297
+ * every load before the guard was tightened.
298
+ */
299
+ test("does not claim the record is missing while the fetch is in flight", async () => {
300
+ let resolveItem: (value: Incident) => void = (): void => {};
301
+ resolveListWith(buildSchema([{ name: "Customer" }]));
302
+ getItemMock.mockReturnValue(
303
+ new Promise<Incident>((resolve: (value: Incident) => void) => {
304
+ resolveItem = resolve;
305
+ }) as never,
306
+ );
307
+
308
+ renderCard();
309
+
310
+ await waitFor(() => {
311
+ expect(getItemMock).toHaveBeenCalled();
312
+ });
313
+ expect(screen.queryByText("Item not found")).not.toBeInTheDocument();
314
+
315
+ resolveItem(buildIncident({ Customer: "Acme Corp" }));
316
+
317
+ expect(await screen.findByText("Acme Corp")).toBeInTheDocument();
318
+ expect(screen.queryByText("Item not found")).not.toBeInTheDocument();
319
+ });
320
+ });
321
+
322
+ describe("CustomFieldsDetail - hideIfEmpty, as the overview pages mount it", () => {
323
+ test("renders nothing at all when the project defined no fields", async () => {
324
+ resolveListWith([]);
325
+ getItemMock.mockResolvedValue(buildIncident({}) as never);
326
+
327
+ const { container } = render(
328
+ <CustomFieldsDetail
329
+ title="Custom Fields"
330
+ description="Custom fields for this incident."
331
+ modelType={Incident}
332
+ customFieldType={IncidentCustomField}
333
+ name="Incident Custom Fields"
334
+ projectId={PROJECT_ID}
335
+ modelId={INCIDENT_ID}
336
+ hideIfEmpty={true}
337
+ />,
338
+ );
339
+
340
+ await waitFor(() => {
341
+ expect(getItemMock).toHaveBeenCalled();
342
+ });
343
+
344
+ expect(screen.queryByText(NO_FIELDS_MESSAGE)).not.toBeInTheDocument();
345
+ expect(screen.queryByText("Custom Fields")).not.toBeInTheDocument();
346
+ expect(container).toBeEmptyDOMElement();
347
+ });
348
+
349
+ test("renders nothing while the schema is still loading, so nothing flashes", () => {
350
+ getListMock.mockReturnValue(new Promise<never>(() => {}) as never);
351
+ getItemMock.mockReturnValue(new Promise<never>(() => {}) as never);
352
+
353
+ const { container } = render(
354
+ <CustomFieldsDetail
355
+ title="Custom Fields"
356
+ description="Custom fields for this incident."
357
+ modelType={Incident}
358
+ customFieldType={IncidentCustomField}
359
+ name="Incident Custom Fields"
360
+ projectId={PROJECT_ID}
361
+ modelId={INCIDENT_ID}
362
+ hideIfEmpty={true}
363
+ />,
364
+ );
365
+
366
+ expect(container).toBeEmptyDOMElement();
367
+ });
368
+
369
+ /*
370
+ * Reading the custom field schema is gated on a billing plan as well as a
371
+ * permission, so a project below that plan fails this request on every
372
+ * resource it opens. On the overview that has to be silent.
373
+ */
374
+ test("renders nothing when the schema read fails", async () => {
375
+ getListMock.mockRejectedValue(new Error("Plan does not include this"));
376
+ getItemMock.mockResolvedValue(buildIncident({}) as never);
377
+
378
+ const { container } = render(
379
+ <CustomFieldsDetail
380
+ title="Custom Fields"
381
+ description="Custom fields for this incident."
382
+ modelType={Incident}
383
+ customFieldType={IncidentCustomField}
384
+ name="Incident Custom Fields"
385
+ projectId={PROJECT_ID}
386
+ modelId={INCIDENT_ID}
387
+ hideIfEmpty={true}
388
+ />,
389
+ );
390
+
391
+ await waitFor(() => {
392
+ expect(getListMock).toHaveBeenCalled();
393
+ });
394
+
395
+ expect(
396
+ screen.queryByText(/Plan does not include this/i),
397
+ ).not.toBeInTheDocument();
398
+ expect(container).toBeEmptyDOMElement();
399
+ });
400
+
401
+ /*
402
+ * The overview page is right there displaying the record, so a card telling
403
+ * the reader it does not exist is a contradiction rather than information.
404
+ */
405
+ test("renders nothing when the record itself comes back empty", async () => {
406
+ resolveListWith(buildSchema([{ name: "Customer" }]));
407
+ getItemMock.mockResolvedValue(null as never);
408
+
409
+ const { container } = render(
410
+ <CustomFieldsDetail
411
+ title="Custom Fields"
412
+ description="Custom fields for this incident."
413
+ modelType={Incident}
414
+ customFieldType={IncidentCustomField}
415
+ name="Incident Custom Fields"
416
+ projectId={PROJECT_ID}
417
+ modelId={INCIDENT_ID}
418
+ hideIfEmpty={true}
419
+ />,
420
+ );
421
+
422
+ await waitFor(() => {
423
+ expect(getItemMock).toHaveBeenCalled();
424
+ });
425
+
426
+ expect(screen.queryByText("Item not found")).not.toBeInTheDocument();
427
+ expect(container).toBeEmptyDOMElement();
428
+ });
429
+
430
+ test("shows the card as normal once the project has a field", async () => {
431
+ resolveListWith(buildSchema([{ name: "Customer" }]));
432
+ getItemMock.mockResolvedValue(
433
+ buildIncident({ Customer: "Acme Corp" }) as never,
434
+ );
435
+
436
+ renderCard({ hideIfEmpty: true });
437
+
438
+ expect(await screen.findByText("Acme Corp")).toBeInTheDocument();
439
+ expect(screen.getByText("Custom Fields")).toBeInTheDocument();
440
+ });
441
+ });
442
+
443
+ describe("CustomFieldsDetail - editing", () => {
444
+ test("offers the edit button by default", async () => {
445
+ resolveListWith(buildSchema([{ name: "Customer" }]));
446
+ getItemMock.mockResolvedValue(buildIncident({}) as never);
447
+
448
+ renderCard();
449
+
450
+ expect(await screen.findByText("Edit Fields")).toBeInTheDocument();
451
+ });
452
+
453
+ test("withholds the edit button when the card is read only", async () => {
454
+ resolveListWith(buildSchema([{ name: "Customer" }]));
455
+ getItemMock.mockResolvedValue(buildIncident({}) as never);
456
+
457
+ renderCard({ isEditable: false });
458
+
459
+ expect(await screen.findByText("Customer")).toBeInTheDocument();
460
+ expect(screen.queryByText("Edit Fields")).not.toBeInTheDocument();
461
+ });
462
+
463
+ /*
464
+ * The button used to render from the very first paint, before either
465
+ * request had come back. Clicking it then threw reading customFields off a
466
+ * null record and took the card down with it - reachable by anyone who
467
+ * clicked as the page settled.
468
+ */
469
+ test("withholds the edit button for the whole first load", async () => {
470
+ resolveListWith(buildSchema([{ name: "Customer" }]));
471
+ let resolveItem: (value: Incident) => void = (): void => {};
472
+ getItemMock.mockReturnValue(
473
+ new Promise<Incident>((resolve: (value: Incident) => void) => {
474
+ resolveItem = resolve;
475
+ }) as never,
476
+ );
477
+
478
+ renderCard();
479
+
480
+ await waitFor(() => {
481
+ expect(getItemMock).toHaveBeenCalled();
482
+ });
483
+ expect(screen.queryByText("Edit Fields")).not.toBeInTheDocument();
484
+
485
+ resolveItem(buildIncident({ Customer: "Acme Corp" }));
486
+
487
+ expect(await screen.findByText("Edit Fields")).toBeInTheDocument();
488
+ });
489
+
490
+ /*
491
+ * The schema and the record are stored together at the end of onLoad, so
492
+ * the test above cannot tell the "no fields yet" guard apart from the "no
493
+ * record yet" one. This is the case that isolates the record: fields are
494
+ * defined, the fetch has settled, and it settled with nothing. Without that
495
+ * guard the card offers an edit modal over a record that is not there.
496
+ */
497
+ test("withholds the edit button when the record could not be found", async () => {
498
+ resolveListWith(buildSchema([{ name: "Customer" }]));
499
+ getItemMock.mockResolvedValue(null as never);
500
+
501
+ renderCard();
502
+
503
+ expect(await screen.findByText("Item not found")).toBeInTheDocument();
504
+ expect(screen.queryByText("Edit Fields")).not.toBeInTheDocument();
505
+ });
506
+
507
+ /* And this is the case that isolates the in-flight guard on its own. */
508
+ test("withholds the edit button while a save is still in flight", async () => {
509
+ resolveListWith(buildSchema([{ name: "Customer" }]));
510
+ getItemMock.mockResolvedValue(
511
+ buildIncident({ Customer: "Acme Corp" }) as never,
512
+ );
513
+ let finishSave: () => void = (): void => {};
514
+ updateByIdMock.mockReturnValue(
515
+ new Promise<void>((resolve: () => void) => {
516
+ finishSave = resolve;
517
+ }) as never,
518
+ );
519
+
520
+ renderCard();
521
+
522
+ fireEvent.click(await screen.findByText("Edit Fields"));
523
+ fireEvent.click(await screen.findByText("Save"));
524
+
525
+ await waitFor(() => {
526
+ expect(updateByIdMock).toHaveBeenCalled();
527
+ });
528
+ expect(screen.queryByText("Edit Fields")).not.toBeInTheDocument();
529
+
530
+ finishSave();
531
+
532
+ expect(await screen.findByText("Edit Fields")).toBeInTheDocument();
533
+ });
534
+
535
+ test("withholds the edit button when there are no fields to edit", async () => {
536
+ resolveListWith([]);
537
+ getItemMock.mockResolvedValue(buildIncident({}) as never);
538
+
539
+ renderCard();
540
+
541
+ expect(await screen.findByText(NO_FIELDS_MESSAGE)).toBeInTheDocument();
542
+ expect(screen.queryByText("Edit Fields")).not.toBeInTheDocument();
543
+ });
544
+
545
+ test("writes the edited values back under customFields and reloads", async () => {
546
+ resolveListWith(buildSchema([{ name: "Customer" }]));
547
+ getItemMock.mockResolvedValue(
548
+ buildIncident({ Customer: "Acme Corp" }) as never,
549
+ );
550
+ updateByIdMock.mockResolvedValue(undefined as never);
551
+
552
+ renderCard();
553
+
554
+ fireEvent.click(await screen.findByText("Edit Fields"));
555
+
556
+ const input: HTMLInputElement = (await screen.findByDisplayValue(
557
+ "Acme Corp",
558
+ )) as HTMLInputElement;
559
+ fireEvent.change(input, { target: { value: "Globex" } });
560
+ fireEvent.click(screen.getByText("Save"));
561
+
562
+ await waitFor(() => {
563
+ expect(updateByIdMock).toHaveBeenCalled();
564
+ });
565
+
566
+ const updateArgs: JSONObject = updateByIdMock.mock
567
+ .calls[0]![0] as JSONObject;
568
+ expect(updateArgs["modelType"]).toBe(Incident);
569
+ expect(updateArgs["id"]).toBe(INCIDENT_ID);
570
+ expect((updateArgs["data"] as JSONObject)["customFields"]).toEqual({
571
+ Customer: "Globex",
572
+ });
573
+
574
+ // the card re-reads rather than trusting its own local copy.
575
+ await waitFor(() => {
576
+ expect(getItemMock.mock.calls.length).toBeGreaterThan(1);
577
+ });
578
+ });
579
+ });
580
+
581
+ /*
582
+ * A rejected save is the operator's own edit coming back refused - no update
583
+ * permission on the column, a server validation error, a dropped connection.
584
+ * It is nothing like a failed schema read, which hideIfEmpty is allowed to
585
+ * swallow, and the two used to share one `error` slot. That made a failed
586
+ * save on an overview page delete the entire card, taking the edit and any
587
+ * explanation with it.
588
+ */
589
+ describe("CustomFieldsDetail - a rejected save", () => {
590
+ type FailTheSaveFunction = () => Promise<void>;
591
+
592
+ const editAndFailTheSave: FailTheSaveFunction = async (): Promise<void> => {
593
+ fireEvent.click(await screen.findByText("Edit Fields"));
594
+
595
+ const input: HTMLInputElement = (await screen.findByDisplayValue(
596
+ "Acme Corp",
597
+ )) as HTMLInputElement;
598
+ fireEvent.change(input, { target: { value: "Globex" } });
599
+ fireEvent.click(screen.getByText("Save"));
600
+
601
+ await waitFor(() => {
602
+ expect(updateByIdMock).toHaveBeenCalled();
603
+ });
604
+ };
605
+
606
+ test("is reported on the dedicated page, over the unchanged values", async () => {
607
+ resolveListWith(buildSchema([{ name: "Customer" }]));
608
+ getItemMock.mockResolvedValue(
609
+ buildIncident({ Customer: "Acme Corp" }) as never,
610
+ );
611
+ updateByIdMock.mockRejectedValue(new Error("Permission denied"));
612
+
613
+ renderCard();
614
+ await editAndFailTheSave();
615
+
616
+ expect(await screen.findByText(/Permission denied/i)).toBeInTheDocument();
617
+ // what is on screen is still what is stored.
618
+ expect(screen.getByText("Acme Corp")).toBeInTheDocument();
619
+ });
620
+
621
+ test("is reported on an overview page rather than hiding the card", async () => {
622
+ resolveListWith(buildSchema([{ name: "Customer" }]));
623
+ getItemMock.mockResolvedValue(
624
+ buildIncident({ Customer: "Acme Corp" }) as never,
625
+ );
626
+ updateByIdMock.mockRejectedValue(new Error("Permission denied"));
627
+
628
+ renderCard({ hideIfEmpty: true });
629
+ await editAndFailTheSave();
630
+
631
+ expect(await screen.findByText(/Permission denied/i)).toBeInTheDocument();
632
+ expect(screen.getByText("Custom Fields")).toBeInTheDocument();
633
+ expect(screen.getByText("Acme Corp")).toBeInTheDocument();
634
+ });
635
+
636
+ /*
637
+ * Telling someone their save failed while removing the only control that
638
+ * would let them try again is worse than saying nothing.
639
+ */
640
+ test("leaves the edit button in place so the save can be retried", async () => {
641
+ resolveListWith(buildSchema([{ name: "Customer" }]));
642
+ getItemMock.mockResolvedValue(
643
+ buildIncident({ Customer: "Acme Corp" }) as never,
644
+ );
645
+ updateByIdMock.mockRejectedValue(new Error("Permission denied"));
646
+
647
+ renderCard();
648
+ await editAndFailTheSave();
649
+
650
+ await screen.findByText(/Permission denied/i);
651
+ expect(screen.getByText("Edit Fields")).toBeInTheDocument();
652
+
653
+ updateByIdMock.mockResolvedValue(undefined as never);
654
+
655
+ fireEvent.click(screen.getByText("Edit Fields"));
656
+ fireEvent.click(await screen.findByText("Save"));
657
+
658
+ await waitFor(() => {
659
+ expect(updateByIdMock.mock.calls.length).toBe(2);
660
+ });
661
+
662
+ // and the failure notice goes away once the retry lands.
663
+ await waitFor(() => {
664
+ expect(screen.queryByText(/Permission denied/i)).not.toBeInTheDocument();
665
+ });
666
+ });
667
+
668
+ /*
669
+ * The read failure keeps its old behaviour on both call sites: hidden on an
670
+ * overview, reported on the dedicated page. Pinned next to the save cases
671
+ * so the asymmetry is deliberate rather than accidental.
672
+ */
673
+ test("a failed read still hides the card on an overview", async () => {
674
+ getListMock.mockRejectedValue(new Error("Upgrade your plan"));
675
+ getItemMock.mockResolvedValue(buildIncident({}) as never);
676
+
677
+ const { container } = render(
678
+ <CustomFieldsDetail
679
+ title="Custom Fields"
680
+ description="Custom fields for this incident."
681
+ modelType={Incident}
682
+ customFieldType={IncidentCustomField}
683
+ name="Incident Custom Fields"
684
+ projectId={PROJECT_ID}
685
+ modelId={INCIDENT_ID}
686
+ hideIfEmpty={true}
687
+ />,
688
+ );
689
+
690
+ await waitFor(() => {
691
+ expect(getListMock).toHaveBeenCalled();
692
+ });
693
+
694
+ expect(container).toBeEmptyDOMElement();
695
+ });
696
+ });