@oneuptime/common 11.7.1 → 11.7.3

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 (285) hide show
  1. package/Models/AnalyticsModels/KubernetesCostAllocation.ts +103 -0
  2. package/Models/DatabaseModels/DashboardDomain.ts +1 -0
  3. package/Models/DatabaseModels/Incident.ts +2 -0
  4. package/Models/DatabaseModels/IncidentEpisode.ts +1 -0
  5. package/Models/DatabaseModels/IncidentEpisodePublicNote.ts +1 -0
  6. package/Models/DatabaseModels/IncidentEpisodeStateTimeline.ts +1 -0
  7. package/Models/DatabaseModels/IncidentPublicNote.ts +1 -0
  8. package/Models/DatabaseModels/IncidentStateTimeline.ts +1 -0
  9. package/Models/DatabaseModels/Monitor.ts +1 -0
  10. package/Models/DatabaseModels/MonitorStatusTimeline.ts +1 -0
  11. package/Models/DatabaseModels/NetworkSite.ts +1 -0
  12. package/Models/DatabaseModels/NetworkSiteStatusTimeline.ts +1 -0
  13. package/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.ts +1 -0
  14. package/Models/DatabaseModels/OnCallDutyPolicySchedule.ts +2 -0
  15. package/Models/DatabaseModels/Probe.ts +13 -13
  16. package/Models/DatabaseModels/ScheduledMaintenance.ts +1 -0
  17. package/Models/DatabaseModels/ScheduledMaintenancePublicNote.ts +1 -0
  18. package/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.ts +1 -0
  19. package/Models/DatabaseModels/ScheduledMaintenanceTemplate.ts +1 -0
  20. package/Models/DatabaseModels/StatusPage.ts +1 -0
  21. package/Models/DatabaseModels/StatusPageAnnouncement.ts +1 -0
  22. package/Models/DatabaseModels/StatusPageDomain.ts +1 -0
  23. package/Models/DatabaseModels/TableView.ts +40 -0
  24. package/Models/DatabaseModels/WorkspaceNotificationSummary.ts +1 -0
  25. package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +11 -0
  26. package/Server/API/BaseAPI.ts +17 -1
  27. package/Server/API/CommonAPI.ts +32 -0
  28. package/Server/API/MicrosoftTeamsAPI.ts +99 -2
  29. package/Server/API/ProbeAPI.ts +2 -0
  30. package/Server/API/SlackAPI.ts +62 -0
  31. package/Server/API/StatusPageAPI.ts +157 -192
  32. package/Server/Infrastructure/Postgres/SchemaMigrations/1785140242697-AddHotQueryIndexes.ts +89 -0
  33. package/Server/Infrastructure/Postgres/SchemaMigrations/1785148065137-AddHotQueryIndexesSecondPass.ts +61 -0
  34. package/Server/Infrastructure/Postgres/SchemaMigrations/1785240000000-RepairCrossProjectMonitorStatusReferences.ts +280 -0
  35. package/Server/Infrastructure/Postgres/SchemaMigrations/1785241000000-AddColumnsToTableView.ts +13 -0
  36. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
  37. package/Server/Services/DatabaseService.ts +11 -3
  38. package/Server/Services/MonitorGroupService.ts +145 -0
  39. package/Server/Services/MonitorService.ts +231 -30
  40. package/Server/Services/ProbeService.ts +319 -16
  41. package/Server/Services/StatusPageService.ts +72 -42
  42. package/Server/Services/WorkspaceNotificationRuleService.ts +232 -2
  43. package/Server/Utils/Database/PostgresErrorTranslator.ts +131 -0
  44. package/Server/Utils/Monitor/MonitorResource.ts +88 -35
  45. package/Server/Utils/Monitor/MonitorStatusTimeline.ts +21 -1
  46. package/Server/Utils/Monitor/MonitorStepsProjectValidator.ts +158 -0
  47. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +706 -6
  48. package/Tests/Models/AnalyticsModels/KubernetesCostAllocation.test.ts +16 -0
  49. package/Tests/Models/DatabaseModels/ProbeColumnAccessControl.test.ts +103 -0
  50. package/Tests/Server/API/BaseAPIUpdateNoOp.test.ts +135 -0
  51. package/Tests/Server/API/CommonAPIAuthGuard.test.ts +317 -0
  52. package/Tests/Server/API/MicrosoftTeamsManifest.test.ts +228 -0
  53. package/Tests/Server/API/ProbeAPI.test.ts +6 -0
  54. package/Tests/Server/API/ZZVerifyChatsAuth.test.ts +167 -0
  55. package/Tests/Server/Services/AddHotQueryIndexesMigration.test.ts +255 -0
  56. package/Tests/Server/Services/AddHotQueryIndexesSecondPassMigration.test.ts +258 -0
  57. package/Tests/Server/Services/DatabaseServiceSanitizeUpdateData.test.ts +1 -1
  58. package/Tests/Server/Services/DiscoveryScanClaimHookFreeSafety.test.ts +121 -0
  59. package/Tests/Server/Services/HeartbeatWriteFastPathSafety.test.ts +200 -0
  60. package/Tests/Server/Services/HookFreeWriteSafetySecondPass.test.ts +181 -0
  61. package/Tests/Server/Services/MonitorGroupServiceBatchedStatuses.test.ts +513 -0
  62. package/Tests/Server/Services/MonitorResourceProbeAgreementReuse.test.ts +492 -0
  63. package/Tests/Server/Services/MonitorServiceProbeSelection.test.ts +401 -0
  64. package/Tests/Server/Services/MonitorStatusTimelineFastPath.test.ts +453 -0
  65. package/Tests/Server/Services/ProbeAuthKeyCacheAndHeartbeat.test.ts +679 -0
  66. package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +3 -3
  67. package/Tests/Server/Services/StatusPageDomainResolutionCache.test.ts +245 -0
  68. package/Tests/Server/Services/StatusPageOverviewTimelineWindow.test.ts +283 -0
  69. package/Tests/Server/Services/StatusPageServiceMcp.test.ts +17 -1
  70. package/Tests/Server/Services/StatusPageServiceMonitorGroupBatching.test.ts +331 -0
  71. package/Tests/Server/Services/UserOnCallLogClaimNotificationRule.test.ts +12 -12
  72. package/Tests/Server/Services/WorkspaceNotificationRuleChats.test.ts +1390 -0
  73. package/Tests/Server/Utils/AI/Insights/InsightFixRouting.test.ts +4 -4
  74. package/Tests/Server/Utils/AI/Insights/InsightScanner.test.ts +2 -2
  75. package/Tests/Server/Utils/AI/Insights/InsightStore.test.ts +1 -3
  76. package/Tests/Server/Utils/AI/Insights/InsightStoreHardening.test.ts +1 -3
  77. package/Tests/Server/Utils/AI/Insights/InsightTriage.test.ts +1 -1
  78. package/Tests/Server/Utils/AI/Insights/InsightTriageRunner.test.ts +5 -5
  79. package/Tests/Server/Utils/Alert/AlertPrivacyFilter.test.ts +187 -0
  80. package/Tests/Server/Utils/AlertEpisode/AlertEpisodePrivacyFilter.test.ts +170 -0
  81. package/Tests/Server/Utils/Database/PostgresErrorTranslator.test.ts +125 -0
  82. package/Tests/Server/Utils/Incident/IncidentPrivacyFilter.test.ts +264 -0
  83. package/Tests/Server/Utils/IncidentEpisode/IncidentEpisodePrivacyFilter.test.ts +175 -0
  84. package/Tests/Server/Utils/Monitor/MonitorStepsProjectValidator.test.ts +181 -0
  85. package/Tests/Server/Utils/Monitor/NetworkDeviceWalkUtil.test.ts +1 -1
  86. package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +2 -2
  87. package/Tests/Server/Utils/Workspace/MicrosoftTeamsChannelsList.test.ts +341 -0
  88. package/Tests/Server/Utils/Workspace/MicrosoftTeamsChats.test.ts +2175 -0
  89. package/Tests/Server/Utils/Workspace/SlackChannelsList.test.ts +585 -0
  90. package/Tests/Types/Billing/MeteredPlan.test.ts +46 -0
  91. package/Tests/Types/JSONFunctions.test.ts +103 -1
  92. package/Tests/Types/Workspace/NotificationRules/NotificationRuleCondition.test.ts +219 -1
  93. package/Tests/UI/Components/BulkUpdateForm.test.tsx +174 -0
  94. package/Tests/UI/Components/CardModelDetailEdit.test.tsx +315 -0
  95. package/Tests/UI/Components/ModelDetailSelect.test.tsx +229 -0
  96. package/Tests/UI/Components/ModelTable/BaseModelTableBulkSelectAll.test.tsx +1258 -0
  97. package/Tests/UI/Components/ModelTable/BaseModelTableColumnCustomization.test.tsx +679 -0
  98. package/Tests/UI/Components/ModelTable/ColumnCustomizationModal.test.tsx +572 -0
  99. package/Tests/UI/Components/ModelTable/ColumnPreference.test.ts +1678 -0
  100. package/Tests/UI/Components/ModelTable/CustomFieldColumns.test.tsx +1094 -0
  101. package/Tests/UI/Components/ModelTableExportFromColumns.test.ts +153 -0
  102. package/Tests/UI/Components/MonacoLoader.test.ts +67 -0
  103. package/Tests/UI/Components/MonacoRuntime.test.ts +121 -0
  104. package/Tests/UI/Components/TableBulkCsvExport.test.tsx +212 -3
  105. package/Tests/UI/Utils/Project.test.ts +809 -0
  106. package/Tests/UI/Utils/TableColumnsToCsv.test.ts +360 -0
  107. package/Tests/Utils/Alerts/AlertMetricType.test.ts +143 -0
  108. package/Tests/Utils/Dashboard/VariableUrlState.test.ts +219 -0
  109. package/Tests/Utils/Incident/IncidentMetricType.test.ts +174 -0
  110. package/Tests/Utils/Monitor/MonitorProbeSelectionUtil.test.ts +145 -0
  111. package/Tests/Utils/StatusPage/ResourceUptime.test.ts +263 -0
  112. package/Tests/Utils/UserPreferences.test.ts +563 -0
  113. package/Types/JSONFunctions.ts +7 -2
  114. package/Types/Kubernetes/KubernetesCostIngest.ts +37 -1
  115. package/Types/Workspace/NotificationRules/BaseNotificationRule.ts +4 -0
  116. package/Types/Workspace/NotificationRules/NotificationRuleCondition.ts +25 -10
  117. package/Types/Workspace/WorkspaceMessagePayload.ts +1 -0
  118. package/UI/Components/BulkUpdate/BulkUpdateForm.tsx +63 -8
  119. package/UI/Components/CodeEditor/CodeEditor.tsx +3 -0
  120. package/UI/Components/CodeEditor/MonacoLoader.ts +36 -0
  121. package/UI/Components/Forms/ModelForm.tsx +41 -18
  122. package/UI/Components/MasterPage/MasterPage.tsx +65 -0
  123. package/UI/Components/ModelDetail/CardModelDetail.tsx +23 -1
  124. package/UI/Components/ModelDetail/ModelDetail.tsx +46 -1
  125. package/UI/Components/ModelTable/BaseModelTable.tsx +687 -34
  126. package/UI/Components/ModelTable/Column.ts +29 -0
  127. package/UI/Components/ModelTable/ColumnCustomizationModal.tsx +420 -0
  128. package/UI/Components/ModelTable/ColumnPreference.ts +482 -0
  129. package/UI/Components/ModelTable/CustomFieldColumns.tsx +326 -0
  130. package/UI/Components/ModelTable/ExportFromColumns.ts +57 -0
  131. package/UI/Components/ModelTable/TableView.tsx +24 -2
  132. package/UI/Components/ModelTable/useCustomFieldColumns.ts +150 -0
  133. package/UI/Components/Navbar/NavBar.tsx +66 -1
  134. package/UI/Components/SideMenu/SideMenu.tsx +24 -4
  135. package/UI/Components/Table/Table.tsx +66 -14
  136. package/UI/Components/Table/TableRow.tsx +180 -175
  137. package/UI/Components/Table/Types/Column.ts +20 -0
  138. package/UI/Utils/Project.ts +214 -18
  139. package/UI/Utils/TableColumnsToCsv.ts +86 -6
  140. package/UI/esbuild-config.js +34 -0
  141. package/Utils/Monitor/MonitorProbeSelectionUtil.ts +72 -0
  142. package/Utils/UserPreferences.ts +53 -0
  143. package/build/dist/Models/AnalyticsModels/KubernetesCostAllocation.js +86 -0
  144. package/build/dist/Models/AnalyticsModels/KubernetesCostAllocation.js.map +1 -1
  145. package/build/dist/Models/DatabaseModels/DashboardDomain.js +1 -0
  146. package/build/dist/Models/DatabaseModels/DashboardDomain.js.map +1 -1
  147. package/build/dist/Models/DatabaseModels/Incident.js +2 -0
  148. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  149. package/build/dist/Models/DatabaseModels/IncidentEpisode.js +1 -0
  150. package/build/dist/Models/DatabaseModels/IncidentEpisode.js.map +1 -1
  151. package/build/dist/Models/DatabaseModels/IncidentEpisodePublicNote.js +1 -0
  152. package/build/dist/Models/DatabaseModels/IncidentEpisodePublicNote.js.map +1 -1
  153. package/build/dist/Models/DatabaseModels/IncidentEpisodeStateTimeline.js +1 -0
  154. package/build/dist/Models/DatabaseModels/IncidentEpisodeStateTimeline.js.map +1 -1
  155. package/build/dist/Models/DatabaseModels/IncidentPublicNote.js +1 -0
  156. package/build/dist/Models/DatabaseModels/IncidentPublicNote.js.map +1 -1
  157. package/build/dist/Models/DatabaseModels/IncidentStateTimeline.js +1 -0
  158. package/build/dist/Models/DatabaseModels/IncidentStateTimeline.js.map +1 -1
  159. package/build/dist/Models/DatabaseModels/Monitor.js +1 -0
  160. package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
  161. package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js +1 -0
  162. package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js.map +1 -1
  163. package/build/dist/Models/DatabaseModels/NetworkSite.js +1 -0
  164. package/build/dist/Models/DatabaseModels/NetworkSite.js.map +1 -1
  165. package/build/dist/Models/DatabaseModels/NetworkSiteStatusTimeline.js +1 -0
  166. package/build/dist/Models/DatabaseModels/NetworkSiteStatusTimeline.js.map +1 -1
  167. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js +1 -0
  168. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js.map +1 -1
  169. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js +2 -0
  170. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js.map +1 -1
  171. package/build/dist/Models/DatabaseModels/Probe.js +13 -13
  172. package/build/dist/Models/DatabaseModels/Probe.js.map +1 -1
  173. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +1 -0
  174. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
  175. package/build/dist/Models/DatabaseModels/ScheduledMaintenancePublicNote.js +1 -0
  176. package/build/dist/Models/DatabaseModels/ScheduledMaintenancePublicNote.js.map +1 -1
  177. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.js +1 -0
  178. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.js.map +1 -1
  179. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js +1 -0
  180. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js.map +1 -1
  181. package/build/dist/Models/DatabaseModels/StatusPage.js +1 -0
  182. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  183. package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js +1 -0
  184. package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js.map +1 -1
  185. package/build/dist/Models/DatabaseModels/StatusPageDomain.js +1 -0
  186. package/build/dist/Models/DatabaseModels/StatusPageDomain.js.map +1 -1
  187. package/build/dist/Models/DatabaseModels/TableView.js +40 -0
  188. package/build/dist/Models/DatabaseModels/TableView.js.map +1 -1
  189. package/build/dist/Models/DatabaseModels/WorkspaceNotificationSummary.js +1 -0
  190. package/build/dist/Models/DatabaseModels/WorkspaceNotificationSummary.js.map +1 -1
  191. package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
  192. package/build/dist/Server/API/BaseAPI.js +12 -1
  193. package/build/dist/Server/API/BaseAPI.js.map +1 -1
  194. package/build/dist/Server/API/CommonAPI.js +21 -0
  195. package/build/dist/Server/API/CommonAPI.js.map +1 -1
  196. package/build/dist/Server/API/MicrosoftTeamsAPI.js +72 -2
  197. package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
  198. package/build/dist/Server/API/ProbeAPI.js +2 -0
  199. package/build/dist/Server/API/ProbeAPI.js.map +1 -1
  200. package/build/dist/Server/API/SlackAPI.js +44 -0
  201. package/build/dist/Server/API/SlackAPI.js.map +1 -1
  202. package/build/dist/Server/API/StatusPageAPI.js +98 -152
  203. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  204. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785140242697-AddHotQueryIndexes.js +36 -0
  205. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785140242697-AddHotQueryIndexes.js.map +1 -0
  206. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785148065137-AddHotQueryIndexesSecondPass.js +26 -0
  207. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785148065137-AddHotQueryIndexesSecondPass.js.map +1 -0
  208. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785240000000-RepairCrossProjectMonitorStatusReferences.js +254 -0
  209. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785240000000-RepairCrossProjectMonitorStatusReferences.js.map +1 -0
  210. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785241000000-AddColumnsToTableView.js +12 -0
  211. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785241000000-AddColumnsToTableView.js.map +1 -0
  212. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
  213. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  214. package/build/dist/Server/Services/DatabaseService.js +10 -2
  215. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  216. package/build/dist/Server/Services/MonitorGroupService.js +118 -0
  217. package/build/dist/Server/Services/MonitorGroupService.js.map +1 -1
  218. package/build/dist/Server/Services/MonitorService.js +198 -24
  219. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  220. package/build/dist/Server/Services/ProbeService.js +238 -9
  221. package/build/dist/Server/Services/ProbeService.js.map +1 -1
  222. package/build/dist/Server/Services/StatusPageService.js +46 -39
  223. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  224. package/build/dist/Server/Services/WorkspaceNotificationRuleService.js +178 -3
  225. package/build/dist/Server/Services/WorkspaceNotificationRuleService.js.map +1 -1
  226. package/build/dist/Server/Utils/Database/PostgresErrorTranslator.js +80 -0
  227. package/build/dist/Server/Utils/Database/PostgresErrorTranslator.js.map +1 -0
  228. package/build/dist/Server/Utils/Monitor/MonitorResource.js +67 -29
  229. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  230. package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js +31 -14
  231. package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js.map +1 -1
  232. package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js +117 -0
  233. package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js.map +1 -0
  234. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +549 -7
  235. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  236. package/build/dist/Types/JSONFunctions.js +8 -3
  237. package/build/dist/Types/JSONFunctions.js.map +1 -1
  238. package/build/dist/Types/Kubernetes/KubernetesCostIngest.js.map +1 -1
  239. package/build/dist/Types/Workspace/NotificationRules/NotificationRuleCondition.js +17 -7
  240. package/build/dist/Types/Workspace/NotificationRules/NotificationRuleCondition.js.map +1 -1
  241. package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js +37 -11
  242. package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js.map +1 -1
  243. package/build/dist/UI/Components/CodeEditor/CodeEditor.js +2 -0
  244. package/build/dist/UI/Components/CodeEditor/CodeEditor.js.map +1 -1
  245. package/build/dist/UI/Components/CodeEditor/MonacoLoader.js +34 -0
  246. package/build/dist/UI/Components/CodeEditor/MonacoLoader.js.map +1 -0
  247. package/build/dist/UI/Components/Forms/ModelForm.js +37 -17
  248. package/build/dist/UI/Components/Forms/ModelForm.js.map +1 -1
  249. package/build/dist/UI/Components/MasterPage/MasterPage.js +50 -1
  250. package/build/dist/UI/Components/MasterPage/MasterPage.js.map +1 -1
  251. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js +22 -1
  252. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js.map +1 -1
  253. package/build/dist/UI/Components/ModelDetail/ModelDetail.js +31 -1
  254. package/build/dist/UI/Components/ModelDetail/ModelDetail.js.map +1 -1
  255. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +472 -27
  256. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  257. package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js +189 -0
  258. package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js.map +1 -0
  259. package/build/dist/UI/Components/ModelTable/ColumnPreference.js +258 -0
  260. package/build/dist/UI/Components/ModelTable/ColumnPreference.js.map +1 -0
  261. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js +168 -0
  262. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -0
  263. package/build/dist/UI/Components/ModelTable/ExportFromColumns.js +30 -0
  264. package/build/dist/UI/Components/ModelTable/ExportFromColumns.js.map +1 -0
  265. package/build/dist/UI/Components/ModelTable/TableView.js +13 -2
  266. package/build/dist/UI/Components/ModelTable/TableView.js.map +1 -1
  267. package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js +84 -0
  268. package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js.map +1 -0
  269. package/build/dist/UI/Components/Navbar/NavBar.js +39 -1
  270. package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
  271. package/build/dist/UI/Components/SideMenu/SideMenu.js +15 -5
  272. package/build/dist/UI/Components/SideMenu/SideMenu.js.map +1 -1
  273. package/build/dist/UI/Components/Table/Table.js +46 -13
  274. package/build/dist/UI/Components/Table/Table.js.map +1 -1
  275. package/build/dist/UI/Components/Table/TableRow.js +11 -6
  276. package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
  277. package/build/dist/UI/Utils/Project.js +161 -15
  278. package/build/dist/UI/Utils/Project.js.map +1 -1
  279. package/build/dist/UI/Utils/TableColumnsToCsv.js +67 -6
  280. package/build/dist/UI/Utils/TableColumnsToCsv.js.map +1 -1
  281. package/build/dist/Utils/Monitor/MonitorProbeSelectionUtil.js +52 -0
  282. package/build/dist/Utils/Monitor/MonitorProbeSelectionUtil.js.map +1 -0
  283. package/build/dist/Utils/UserPreferences.js +33 -0
  284. package/build/dist/Utils/UserPreferences.js.map +1 -1
  285. package/package.json +4 -3
@@ -0,0 +1,326 @@
1
+ import Column from "./Column";
2
+ import Columns from "./Columns";
3
+ import FieldType from "../Types/FieldType";
4
+ import AnalyticsBaseModel from "../../../Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel";
5
+ import BaseModel from "../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
6
+ import CustomFieldType from "../../../Types/CustomField/CustomFieldType";
7
+ import { JSONObject } from "../../../Types/JSON";
8
+ import React, { ReactElement } from "react";
9
+
10
+ /*
11
+ * ---------------------------------------------------------------------------
12
+ * One table column per custom field
13
+ * ---------------------------------------------------------------------------
14
+ *
15
+ * Custom field *definitions* live in a per-resource table (MonitorCustomField,
16
+ * IncidentCustomField, ...); the *values* live in a single `customFields`
17
+ * jsonb column on the resource itself, keyed by the definition's name. So a
18
+ * generated column reads the whole JSON column and picks one key out of it:
19
+ *
20
+ * field: { customFields: true } <- a real model column
21
+ * selectedProperty: "Severity" <- the key inside it
22
+ *
23
+ * which BaseModelTable turns into the column key "customFields.Severity".
24
+ *
25
+ * Two things about that shape are load-bearing:
26
+ *
27
+ * - the declared field has to be the real column. A synthetic
28
+ * `field: { "customFields.Severity": true }` would fail the model's column
29
+ * check in getSelectFromColumns and throw the whole table away.
30
+ *
31
+ * - sorting has to be off. Nothing downstream validates `sort` before it
32
+ * reaches TypeORM, so a clickable header here would send an unknown
33
+ * property into the query builder. Postgres can only order by a jsonb
34
+ * key through an explicit ->> expression, which the sort path has no
35
+ * concept of.
36
+ *
37
+ * Cells render through `getElement` for the same class of reason: the typed
38
+ * cell renderers index `item[column.key]` directly, so they would look for a
39
+ * literal "customFields.Severity" property and find nothing. Only the
40
+ * fall-through text branch understands a dotted key.
41
+ */
42
+
43
+ export interface CustomFieldDefinition {
44
+ name: string;
45
+ description?: string | undefined;
46
+ customFieldType?: CustomFieldType | undefined;
47
+ dropdownOptions?: string | undefined;
48
+ }
49
+
50
+ export const CustomFieldsColumnKey: string = "customFields";
51
+
52
+ export type ParseDropdownOptionsFunction = (
53
+ value: string | undefined | null,
54
+ ) => Array<string>;
55
+
56
+ /*
57
+ * Dropdown options are stored as one option per line, the same format the
58
+ * settings page writes.
59
+ */
60
+ export const parseDropdownOptions: ParseDropdownOptionsFunction = (
61
+ value: string | undefined | null,
62
+ ): Array<string> => {
63
+ if (typeof value !== "string" || !value) {
64
+ return [];
65
+ }
66
+
67
+ return value
68
+ .split("\n")
69
+ .map((line: string) => {
70
+ return line.trim();
71
+ })
72
+ .filter((line: string) => {
73
+ return line.length > 0;
74
+ });
75
+ };
76
+
77
+ export type GetCustomFieldDefinitionsFunction = (
78
+ items: Array<BaseModel | JSONObject>,
79
+ ) => Array<CustomFieldDefinition>;
80
+
81
+ /*
82
+ * Normalise whatever the definition API returned into a plain, sorted list.
83
+ *
84
+ * Sorting matters: the definition models carry no ordering column and the
85
+ * list endpoint is queried with an empty sort, so Postgres is free to hand
86
+ * back a different order on every request. Without a deterministic order
87
+ * here, a viewer's saved column layout would appear to shuffle itself
88
+ * between page loads.
89
+ */
90
+ export const getCustomFieldDefinitions: GetCustomFieldDefinitionsFunction = (
91
+ items: Array<BaseModel | JSONObject>,
92
+ ): Array<CustomFieldDefinition> => {
93
+ const definitions: Array<CustomFieldDefinition> = [];
94
+ const seenNames: Set<string> = new Set();
95
+
96
+ for (const item of items || []) {
97
+ const name: unknown = (item as JSONObject)?.["name"];
98
+
99
+ if (typeof name !== "string" || name.trim().length === 0) {
100
+ continue;
101
+ }
102
+
103
+ const trimmedName: string = name.trim();
104
+
105
+ if (seenNames.has(trimmedName)) {
106
+ continue;
107
+ }
108
+
109
+ seenNames.add(trimmedName);
110
+
111
+ const description: unknown = (item as JSONObject)["description"];
112
+ const customFieldType: unknown = (item as JSONObject)["customFieldType"];
113
+ const dropdownOptions: unknown = (item as JSONObject)["dropdownOptions"];
114
+
115
+ definitions.push({
116
+ name: trimmedName,
117
+ description: typeof description === "string" ? description : undefined,
118
+ customFieldType:
119
+ typeof customFieldType === "string"
120
+ ? (customFieldType as CustomFieldType)
121
+ : undefined,
122
+ dropdownOptions:
123
+ typeof dropdownOptions === "string" ? dropdownOptions : undefined,
124
+ });
125
+ }
126
+
127
+ return definitions.sort(
128
+ (a: CustomFieldDefinition, b: CustomFieldDefinition) => {
129
+ return a.name.localeCompare(b.name);
130
+ },
131
+ );
132
+ };
133
+
134
+ export type GetCustomFieldValueFunction = (
135
+ item: unknown,
136
+ name: string,
137
+ ) => unknown;
138
+
139
+ export const getCustomFieldValue: GetCustomFieldValueFunction = (
140
+ item: unknown,
141
+ name: string,
142
+ ): unknown => {
143
+ const customFields: unknown = (item as JSONObject | undefined)?.[
144
+ CustomFieldsColumnKey
145
+ ];
146
+
147
+ if (!customFields || typeof customFields !== "object") {
148
+ return undefined;
149
+ }
150
+
151
+ return (customFields as JSONObject)[name];
152
+ };
153
+
154
+ type BadgeFunction = (data: { label: string; key: string }) => ReactElement;
155
+
156
+ const badge: BadgeFunction = (data: {
157
+ label: string;
158
+ key: string;
159
+ }): ReactElement => {
160
+ return (
161
+ <span
162
+ key={data.key}
163
+ className="inline-flex items-center gap-x-1.5 px-2 py-1 rounded-md bg-indigo-50 text-indigo-700 text-xs font-medium ring-1 ring-inset ring-indigo-700/10"
164
+ >
165
+ <svg
166
+ className="h-1.5 w-1.5 fill-indigo-500"
167
+ viewBox="0 0 6 6"
168
+ aria-hidden="true"
169
+ >
170
+ <circle cx={3} cy={3} r={3} />
171
+ </svg>
172
+ {data.label}
173
+ </span>
174
+ );
175
+ };
176
+
177
+ export type RenderCustomFieldValueFunction = (data: {
178
+ value: unknown;
179
+ definition: CustomFieldDefinition;
180
+ noValueMessage?: string | undefined;
181
+ }) => ReactElement;
182
+
183
+ export const renderCustomFieldValue: RenderCustomFieldValueFunction = (data: {
184
+ value: unknown;
185
+ definition: CustomFieldDefinition;
186
+ noValueMessage?: string | undefined;
187
+ }): ReactElement => {
188
+ const { value, definition } = data;
189
+ const noValueMessage: string = data.noValueMessage ?? "-";
190
+
191
+ const empty: ReactElement = (
192
+ <span className="text-gray-400">{noValueMessage}</span>
193
+ );
194
+
195
+ if (value === undefined || value === null || value === "") {
196
+ return empty;
197
+ }
198
+
199
+ if (definition.customFieldType === CustomFieldType.Boolean) {
200
+ /*
201
+ * A boolean custom field renders "No" rather than falling into the empty
202
+ * branch above - `false` is a real answer, not a missing one.
203
+ */
204
+ const isTrue: boolean = value === true || value === "true";
205
+
206
+ return (
207
+ <span
208
+ className={
209
+ isTrue
210
+ ? "inline-flex items-center px-2 py-1 rounded-md bg-green-50 text-green-700 text-xs font-medium ring-1 ring-inset ring-green-600/20"
211
+ : "inline-flex items-center px-2 py-1 rounded-md bg-gray-50 text-gray-600 text-xs font-medium ring-1 ring-inset ring-gray-500/10"
212
+ }
213
+ >
214
+ {isTrue ? "Yes" : "No"}
215
+ </span>
216
+ );
217
+ }
218
+
219
+ if (definition.customFieldType === CustomFieldType.MultiSelectDropdown) {
220
+ /*
221
+ * Tolerant on the way out: an older single-select value that was later
222
+ * switched to multi-select is still a bare string in the JSON.
223
+ */
224
+ const values: Array<unknown> = Array.isArray(value) ? value : [value];
225
+
226
+ const labels: Array<string> = values
227
+ .filter((entry: unknown) => {
228
+ return entry !== undefined && entry !== null && entry !== "";
229
+ })
230
+ .map((entry: unknown) => {
231
+ return String(entry);
232
+ });
233
+
234
+ if (labels.length === 0) {
235
+ return empty;
236
+ }
237
+
238
+ return (
239
+ <div className="flex flex-wrap gap-1.5">
240
+ {labels.map((label: string, index: number) => {
241
+ return badge({ label, key: `${label}-${index}` });
242
+ })}
243
+ </div>
244
+ );
245
+ }
246
+
247
+ if (definition.customFieldType === CustomFieldType.Dropdown) {
248
+ return badge({ label: String(value), key: String(value) });
249
+ }
250
+
251
+ if (Array.isArray(value)) {
252
+ return <span>{value.join(", ")}</span>;
253
+ }
254
+
255
+ if (typeof value === "object") {
256
+ return <span>{JSON.stringify(value)}</span>;
257
+ }
258
+
259
+ return <span>{String(value)}</span>;
260
+ };
261
+
262
+ export type GetCustomFieldColumnsFunction = <
263
+ TBaseModel extends BaseModel | AnalyticsBaseModel,
264
+ >(data: {
265
+ definitions: Array<CustomFieldDefinition>;
266
+ isHiddenByDefault?: boolean | undefined;
267
+ }) => Columns<TBaseModel>;
268
+
269
+ /*
270
+ * Custom field columns default to hidden. A project is free to define a dozen
271
+ * of them, and turning every one on by default would push the columns the
272
+ * table was designed around off the side of the screen the first time anyone
273
+ * opens it. They are all listed in the column picker, one click away.
274
+ */
275
+ export const getCustomFieldColumns: GetCustomFieldColumnsFunction = <
276
+ TBaseModel extends BaseModel | AnalyticsBaseModel,
277
+ >(data: {
278
+ definitions: Array<CustomFieldDefinition>;
279
+ isHiddenByDefault?: boolean | undefined;
280
+ }): Columns<TBaseModel> => {
281
+ const isHidden: boolean = data.isHiddenByDefault !== false;
282
+
283
+ return (data.definitions || []).map(
284
+ (definition: CustomFieldDefinition): Column<TBaseModel> => {
285
+ return {
286
+ id: `${CustomFieldsColumnKey}.${definition.name}`,
287
+ field: {
288
+ [CustomFieldsColumnKey]: true,
289
+ } as Column<TBaseModel>["field"],
290
+ selectedProperty: definition.name,
291
+ title: definition.name,
292
+ description: definition.description,
293
+ type: FieldType.Element,
294
+ /*
295
+ * See the file header: sorting a jsonb key is not something the
296
+ * query path can express.
297
+ */
298
+ disableSort: true,
299
+ isHiddenByDefault: isHidden,
300
+ getExportValue: (item: TBaseModel): string => {
301
+ const value: unknown = getCustomFieldValue(item, definition.name);
302
+
303
+ if (value === undefined || value === null) {
304
+ return "";
305
+ }
306
+
307
+ if (Array.isArray(value)) {
308
+ return value.join(", ");
309
+ }
310
+
311
+ if (typeof value === "object") {
312
+ return JSON.stringify(value);
313
+ }
314
+
315
+ return String(value);
316
+ },
317
+ getElement: (item: TBaseModel): ReactElement => {
318
+ return renderCustomFieldValue({
319
+ value: getCustomFieldValue(item, definition.name),
320
+ definition: definition,
321
+ });
322
+ },
323
+ };
324
+ },
325
+ );
326
+ };
@@ -0,0 +1,57 @@
1
+ import Column from "./Column";
2
+ import AnalyticsBaseModel from "../../../Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel";
3
+ import BaseModel from "../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
4
+
5
+ /*
6
+ * Derives the fields a ModelTable column contributes to a CSV export from its
7
+ * column definition, and mirrors getSelectFromColumns: a column may declare
8
+ * MORE THAN ONE field when its cell is composed from several of them, and all
9
+ * of them are fetched. The Table column's `key` only ever holds the first, so
10
+ * an exporter reading `key` alone drops the rest - the alert "Affected
11
+ * Resources" cell spans hosts / kubernetesClusters / dockerHosts /
12
+ * podmanHosts / services, and only the hosts made it into the file.
13
+ */
14
+
15
+ export function getExportKeysFromColumn<
16
+ TBaseModel extends BaseModel | AnalyticsBaseModel,
17
+ >(data: {
18
+ column: Column<TBaseModel>;
19
+ /*
20
+ * The key the Table column renders and sorts by. It is the first declared
21
+ * field, with `selectedProperty` appended when the column picks a single
22
+ * property off a relation ("label.name"), so the export reads the same path
23
+ * the cell does.
24
+ */
25
+ columnKey: string | null;
26
+ /*
27
+ * Secondary fields are gated on this exactly as they are in the select: a
28
+ * field the caller cannot read was never fetched, so exporting it would
29
+ * only ever produce an empty cell.
30
+ */
31
+ hasPermissionToReadField?: ((field: string) => boolean) | undefined;
32
+ }): Array<string> {
33
+ const exportKeys: Array<string> = [];
34
+
35
+ if (data.columnKey) {
36
+ exportKeys.push(data.columnKey);
37
+ }
38
+
39
+ if (!data.column.field) {
40
+ return exportKeys;
41
+ }
42
+
43
+ const declaredKeys: Array<string> = Object.keys(data.column.field);
44
+
45
+ // The primary field is already covered by columnKey.
46
+ for (const key of declaredKeys.slice(1)) {
47
+ if (data.hasPermissionToReadField && !data.hasPermissionToReadField(key)) {
48
+ continue;
49
+ }
50
+
51
+ if (!exportKeys.includes(key)) {
52
+ exportKeys.push(key);
53
+ }
54
+ }
55
+
56
+ return exportKeys;
57
+ }
@@ -40,6 +40,13 @@ export interface ComponentProps<T extends GenericObject> {
40
40
  * `TableView.facets`). Owner: the parent's filter hook.
41
41
  */
42
42
  currentFacetState?: JSONObject | undefined;
43
+ /**
44
+ * The viewer's current column layout ({ order, hidden }), persisted on
45
+ * `TableView.columns` so a saved view restores the columns it was saved
46
+ * with. `undefined` means "the table's default layout", which is stored as
47
+ * no value at all.
48
+ */
49
+ currentColumns?: JSONObject | undefined;
43
50
  tableView: TableView | null;
44
51
  }
45
52
 
@@ -90,6 +97,7 @@ const TableViewElement: <T extends DatabaseBaseModel | AnalyticsBaseModel>(
90
97
  query: true,
91
98
  name: true,
92
99
  facets: true,
100
+ columns: true,
93
101
  },
94
102
  sort: {
95
103
  name: SortOrder.Ascending,
@@ -223,6 +231,17 @@ const TableViewElement: <T extends DatabaseBaseModel | AnalyticsBaseModel>(
223
231
 
224
232
  const hasActiveFilters: boolean = hasQueryFilters || hasFacetState;
225
233
 
234
+ /*
235
+ * A customized column layout is worth saving on its own - "the columns I
236
+ * care about, in my order" is a view even when nothing is filtered.
237
+ */
238
+ const hasColumnLayout: boolean = Boolean(
239
+ props.currentColumns &&
240
+ Object.keys(props.currentColumns as JSONObject).length > 0,
241
+ );
242
+
243
+ const hasSavableState: boolean = hasActiveFilters || hasColumnLayout;
244
+
226
245
  const getMenuContents: GetReactElementArrayFunction =
227
246
  (): Array<ReactElement> => {
228
247
  if (isLoading) {
@@ -239,7 +258,7 @@ const TableViewElement: <T extends DatabaseBaseModel | AnalyticsBaseModel>(
239
258
  );
240
259
  }
241
260
 
242
- if (hasActiveFilters) {
261
+ if (hasSavableState) {
243
262
  elements.push(
244
263
  <MoreMenuItem
245
264
  key={"save-new-view"}
@@ -359,6 +378,9 @@ const TableViewElement: <T extends DatabaseBaseModel | AnalyticsBaseModel>(
359
378
  if (props.currentFacetState) {
360
379
  tableView.facets = props.currentFacetState;
361
380
  }
381
+ if (props.currentColumns) {
382
+ tableView.columns = props.currentColumns;
383
+ }
362
384
  return Promise.resolve(tableView);
363
385
  }}
364
386
  onSuccess={async (tableView: TableView) => {
@@ -476,7 +498,7 @@ const TableViewElement: <T extends DatabaseBaseModel | AnalyticsBaseModel>(
476
498
  if (
477
499
  !isLoading &&
478
500
  allTableViews.length === 0 &&
479
- !hasActiveFilters &&
501
+ !hasSavableState &&
480
502
  !currentlySelectedView
481
503
  ) {
482
504
  return <></>;
@@ -0,0 +1,150 @@
1
+ import Columns from "./Columns";
2
+ import {
3
+ CustomFieldDefinition,
4
+ getCustomFieldColumns,
5
+ getCustomFieldDefinitions,
6
+ } from "./CustomFieldColumns";
7
+ import ModelAPI from "../../Utils/ModelAPI/ModelAPI";
8
+ import ProjectUtil from "../../Utils/Project";
9
+ import { Logger } from "../../Utils/Logger";
10
+ import AnalyticsBaseModel from "../../../Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel";
11
+ import BaseModel from "../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
12
+ import ListResult from "../../../Types/BaseDatabase/ListResult";
13
+ import { LIMIT_PER_PROJECT } from "../../../Types/Database/LimitMax";
14
+ import ObjectID from "../../../Types/ObjectID";
15
+ import { useEffect, useMemo, useState } from "react";
16
+
17
+ export interface CustomFieldColumnsResult<
18
+ TBaseModel extends BaseModel | AnalyticsBaseModel,
19
+ > {
20
+ columns: Columns<TBaseModel>;
21
+ definitions: Array<CustomFieldDefinition>;
22
+ isLoading: boolean;
23
+ error: string;
24
+ }
25
+
26
+ /*
27
+ * Loads a resource's custom field *definitions* and turns them into table
28
+ * columns.
29
+ *
30
+ * The definitions arrive after the table's first render, which is fine: they
31
+ * only add columns, and every one of them is hidden until the viewer switches
32
+ * it on. What must NOT wait on this request is the `customFields` entry in the
33
+ * API select - the table does not refetch when its column set changes, so the
34
+ * select has to be right from the first request. BaseModelTable handles that
35
+ * from the synchronous `customFieldsModelType` prop instead.
36
+ *
37
+ * Pass `undefined` for the model type to switch the whole thing off; the hook
38
+ * still runs (hooks cannot be conditional) but makes no request.
39
+ */
40
+ export type UseCustomFieldColumnsFunction = <
41
+ TBaseModel extends BaseModel | AnalyticsBaseModel,
42
+ >(data: {
43
+ customFieldsModelType?: { new (): BaseModel } | undefined;
44
+ projectId?: ObjectID | null | undefined;
45
+ }) => CustomFieldColumnsResult<TBaseModel>;
46
+
47
+ const useCustomFieldColumns: UseCustomFieldColumnsFunction = <
48
+ TBaseModel extends BaseModel | AnalyticsBaseModel,
49
+ >(data: {
50
+ customFieldsModelType?: { new (): BaseModel } | undefined;
51
+ projectId?: ObjectID | null | undefined;
52
+ }): CustomFieldColumnsResult<TBaseModel> => {
53
+ const { customFieldsModelType } = data;
54
+
55
+ const [definitions, setDefinitions] = useState<Array<CustomFieldDefinition>>(
56
+ [],
57
+ );
58
+ const [isLoading, setIsLoading] = useState<boolean>(false);
59
+ const [error, setError] = useState<string>("");
60
+
61
+ const projectId: ObjectID | null =
62
+ data.projectId ?? ProjectUtil.getCurrentProjectId();
63
+
64
+ const projectIdString: string = projectId?.toString() || "";
65
+ const modelName: string = customFieldsModelType?.name || "";
66
+
67
+ useEffect(() => {
68
+ if (!customFieldsModelType || !projectId) {
69
+ setDefinitions([]);
70
+ return;
71
+ }
72
+
73
+ let isCancelled: boolean = false;
74
+
75
+ const load: () => Promise<void> = async (): Promise<void> => {
76
+ setIsLoading(true);
77
+ setError("");
78
+
79
+ try {
80
+ const result: ListResult<BaseModel> = await ModelAPI.getList<BaseModel>(
81
+ {
82
+ modelType: customFieldsModelType,
83
+ query: {
84
+ projectId: projectId,
85
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
86
+ } as any,
87
+ limit: LIMIT_PER_PROJECT,
88
+ skip: 0,
89
+ select: {
90
+ name: true,
91
+ customFieldType: true,
92
+ description: true,
93
+ dropdownOptions: true,
94
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
95
+ } as any,
96
+ sort: {},
97
+ },
98
+ );
99
+
100
+ if (isCancelled) {
101
+ return;
102
+ }
103
+
104
+ setDefinitions(getCustomFieldDefinitions(result.data || []));
105
+ } catch (err) {
106
+ if (isCancelled) {
107
+ return;
108
+ }
109
+
110
+ /*
111
+ * Custom fields are a paid feature and the caller may not have read
112
+ * access to the definition table, so a failure here is expected in
113
+ * normal operation. It costs the viewer some optional columns; it must
114
+ * never take the table down with it.
115
+ */
116
+ Logger.warn(
117
+ `Could not load custom field definitions for ${modelName}: ${
118
+ (err as Error)?.message || String(err)
119
+ }`,
120
+ );
121
+ setDefinitions([]);
122
+ setError((err as Error)?.message || "");
123
+ } finally {
124
+ if (!isCancelled) {
125
+ setIsLoading(false);
126
+ }
127
+ }
128
+ };
129
+
130
+ load().catch(() => {
131
+ // load() already funnels every failure into the catch above.
132
+ });
133
+
134
+ return () => {
135
+ isCancelled = true;
136
+ };
137
+ }, [modelName, projectIdString]);
138
+
139
+ const columns: Columns<TBaseModel> = useMemo(() => {
140
+ if (definitions.length === 0) {
141
+ return [];
142
+ }
143
+
144
+ return getCustomFieldColumns<TBaseModel>({ definitions });
145
+ }, [definitions]);
146
+
147
+ return { columns, definitions, isLoading, error };
148
+ };
149
+
150
+ export default useCustomFieldColumns;
@@ -69,6 +69,13 @@ export interface ComponentProps {
69
69
  children?: ReactElement | Array<ReactElement>;
70
70
  }
71
71
 
72
+ /*
73
+ * Breathing room kept between the bottom of the open mobile menu and the bottom
74
+ * of the viewport, and the smallest height we will ever shrink the menu to.
75
+ */
76
+ const MOBILE_MENU_BOTTOM_GAP_IN_PX: number = 16;
77
+ const MOBILE_MENU_MIN_HEIGHT_IN_PX: number = 160;
78
+
72
79
  const Navbar: FunctionComponent<ComponentProps> = (
73
80
  props: ComponentProps,
74
81
  ): ReactElement => {
@@ -76,6 +83,9 @@ const Navbar: FunctionComponent<ComponentProps> = (
76
83
  const [isMobileMenuVisible, setIsMobileMenuVisible] =
77
84
  useState<boolean>(false);
78
85
  const [isMoreMenuVisible, setIsMoreMenuVisible] = useState<boolean>(false);
86
+ const [mobileMenuMaxHeight, setMobileMenuMaxHeight] = useState<
87
+ number | undefined
88
+ >(undefined);
79
89
 
80
90
  // Use the existing outside click hook for mobile menu
81
91
  const {
@@ -103,6 +113,54 @@ const Navbar: FunctionComponent<ComponentProps> = (
103
113
  };
104
114
  }, []);
105
115
 
116
+ /*
117
+ * The mobile menu is absolutely positioned inside a `sticky top-0` header, so
118
+ * anything that overflows past the bottom of the viewport is unreachable —
119
+ * scrolling the page just drags the sticky header (and the menu pinned to it)
120
+ * along. Cap the menu to the space left below it so it scrolls on its own.
121
+ */
122
+ useEffect(() => {
123
+ if (!isMobileMenuOpen) {
124
+ return;
125
+ }
126
+
127
+ const updateMobileMenuMaxHeight: () => void = (): void => {
128
+ const menuTop: number =
129
+ mobileMenuRef.current?.getBoundingClientRect().top ?? 0;
130
+ const viewportHeight: number =
131
+ window.visualViewport?.height ?? window.innerHeight;
132
+
133
+ setMobileMenuMaxHeight(
134
+ Math.max(
135
+ viewportHeight - menuTop - MOBILE_MENU_BOTTOM_GAP_IN_PX,
136
+ MOBILE_MENU_MIN_HEIGHT_IN_PX,
137
+ ),
138
+ );
139
+ };
140
+
141
+ updateMobileMenuMaxHeight();
142
+
143
+ window.addEventListener("resize", updateMobileMenuMaxHeight);
144
+ window.addEventListener("orientationchange", updateMobileMenuMaxHeight);
145
+ // The address bar collapsing on mobile only shows up on the visual viewport.
146
+ window.visualViewport?.addEventListener(
147
+ "resize",
148
+ updateMobileMenuMaxHeight,
149
+ );
150
+
151
+ return () => {
152
+ window.removeEventListener("resize", updateMobileMenuMaxHeight);
153
+ window.removeEventListener(
154
+ "orientationchange",
155
+ updateMobileMenuMaxHeight,
156
+ );
157
+ window.visualViewport?.removeEventListener(
158
+ "resize",
159
+ updateMobileMenuMaxHeight,
160
+ );
161
+ };
162
+ }, [isMobileMenuOpen]);
163
+
106
164
  // Open/close the products menu with Cmd/Ctrl + K from anywhere.
107
165
  useEffect(() => {
108
166
  const handleGlobalKeyDown: (event: KeyboardEvent) => void = (
@@ -235,7 +293,14 @@ const Navbar: FunctionComponent<ComponentProps> = (
235
293
  ref={mobileMenuRef}
236
294
  className="absolute top-full left-0 right-0 z-50 mt-1 transition-all duration-200 ease-in-out"
237
295
  >
238
- <nav className="bg-white rounded-lg shadow-lg px-3 py-3 space-y-1 border border-gray-200">
296
+ <nav
297
+ className="bg-white rounded-lg shadow-lg px-3 py-3 space-y-1 border border-gray-200 overflow-y-auto overscroll-contain"
298
+ style={
299
+ mobileMenuMaxHeight
300
+ ? { maxHeight: `${mobileMenuMaxHeight}px` }
301
+ : undefined
302
+ }
303
+ >
239
304
  {allNavItems.map((item: any) => {
240
305
  return (
241
306
  <div key={item.id} className="block w-full">