@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
@@ -25,6 +25,11 @@ import OrderedStatesList from "../OrderedStatesList/OrderedStatesList";
25
25
  import Pill from "../Pill/Pill";
26
26
  import Table from "../Table/Table";
27
27
  import FieldType from "../Types/FieldType";
28
+ import ColumnCustomizationModal from "./ColumnCustomizationModal";
29
+ import { applyColumnPreference, buildColumnPreference, fromJSON as columnPreferenceFromJSON, getCustomizableColumns, isEmptyColumnPreference, sanitizeColumnPreference, getColumnIds, toJSON as columnPreferenceToJSON, } from "./ColumnPreference";
30
+ import { CustomFieldsColumnKey } from "./CustomFieldColumns";
31
+ import useCustomFieldColumns from "./useCustomFieldColumns";
32
+ import { getExportKeysFromColumn } from "./ExportFromColumns";
28
33
  import { getRelationSelectFromColumns, getSelectFromColumns, } from "./SelectFromColumns";
29
34
  import AnalyticsBaseModel from "../../../Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel";
30
35
  import BaseModel from "../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
@@ -49,6 +54,20 @@ export var ShowAs;
49
54
  ShowAs[ShowAs["List"] = 1] = "List";
50
55
  ShowAs[ShowAs["OrderedStatesList"] = 2] = "OrderedStatesList";
51
56
  })(ShowAs || (ShowAs = {}));
57
+ /*
58
+ * "Select All" loads the whole filtered result set - up to LIMIT_PER_PROJECT
59
+ * rows - with the same columns the table itself fetches. Asking for all of
60
+ * them in one request would mean a single wide, relation-joined query for up
61
+ * to 10,000 rows, which is exactly the kind of query the database statement
62
+ * timeout kills. It is paged instead: each request is comparable in cost to a
63
+ * large page load, so a big selection degrades in latency rather than failing.
64
+ *
65
+ * The page size trades those two costs off. Every list request also runs an
66
+ * unbounded count over the filtered set and pays a growing OFFSET, so smaller
67
+ * pages multiply that overhead; 1,000 keeps each query well inside the
68
+ * statement timeout while capping a maxed-out selection at ten round trips.
69
+ */
70
+ export const BULK_SELECT_ALL_PAGE_SIZE = 1000;
52
71
  export var ModalTableBulkDefaultActions;
53
72
  (function (ModalTableBulkDefaultActions) {
54
73
  ModalTableBulkDefaultActions["Delete"] = "Delete";
@@ -69,6 +88,23 @@ const BaseModelTable = (props) => {
69
88
  const matchBulkSelectedItemByField = ((_a = props.bulkActions) === null || _a === void 0 ? void 0 : _a.matchBulkSelectedItemByField) || "_id";
70
89
  const model = new props.modelType();
71
90
  const [bulkSelectedItems, setBulkSelectedItems] = useState([]);
91
+ /*
92
+ * "Select All" gets its own loading / error state instead of sharing
93
+ * `isLoading` and `error` with fetchItems. They ran concurrently before, so
94
+ * a page refresh landing mid-selection flipped the spinner off and cleared
95
+ * the other's error - which is how a failed select-all could leave the user
96
+ * with only the current page selected and nothing on screen to say so.
97
+ */
98
+ const [isBulkSelectAllLoading, setIsBulkSelectAllLoading] = useState(false);
99
+ const [bulkSelectionError, setBulkSelectionError] = useState("");
100
+ const [bulkSelectionTotalCount, setBulkSelectionTotalCount] = useState(0);
101
+ const [isBulkSelectionTruncated, setIsBulkSelectionTruncated] = useState(false);
102
+ /*
103
+ * The paged select-all spans several round trips, so the user can clear the
104
+ * selection or start another one while it is still running. Every run takes
105
+ * a token and drops its results if a newer one has since started.
106
+ */
107
+ const bulkSelectAllToken = React.useRef(0);
72
108
  let showAs = props.showAs;
73
109
  if (!showAs) {
74
110
  showAs = ShowAs.Table;
@@ -78,6 +114,83 @@ const BaseModelTable = (props) => {
78
114
  const [showHelpModal, setShowHelpModal] = useState(false);
79
115
  const [viewId, setViewId] = useState(null);
80
116
  const [tableColumns, setColumns] = useState([]);
117
+ /*
118
+ * ---------------------------------------------------------------------
119
+ * Viewer-customizable columns
120
+ * ---------------------------------------------------------------------
121
+ *
122
+ * The viewer's layout (which columns are on, and in what order) is a
123
+ * personal, presentational preference, so it lives in localStorage next to
124
+ * the page size rather than on the server: saved views are named, explicit,
125
+ * plan-gated and need write permission, none of which should stand between
126
+ * someone and hiding a column they don't care about. A saved view can still
127
+ * carry a layout of its own, and when one is active it wins.
128
+ *
129
+ * Custom fields are appended to the declared columns as additional,
130
+ * off-by-default columns; from here on they are ordinary columns.
131
+ */
132
+ const customFieldColumns = useCustomFieldColumns({
133
+ customFieldsModelType: props.customFieldsModelType,
134
+ });
135
+ const isColumnCustomizationEnabled = Boolean(!props.disableColumnCustomization &&
136
+ props.userPreferencesKey &&
137
+ showAs !== ShowAs.OrderedStatesList);
138
+ const allColumns = useMemo(() => {
139
+ if (customFieldColumns.columns.length === 0) {
140
+ return props.columns || [];
141
+ }
142
+ return [...(props.columns || []), ...customFieldColumns.columns];
143
+ }, [props.columns, customFieldColumns.columns]);
144
+ const readStoredColumnPreference = () => {
145
+ if (!props.userPreferencesKey) {
146
+ return null;
147
+ }
148
+ return columnPreferenceFromJSON(UserPreferences.getUserPreferenceByTypeAsJSON({
149
+ key: props.userPreferencesKey,
150
+ userPreferenceType: UserPreferenceType.BaseModelTableColumns,
151
+ }));
152
+ };
153
+ const [columnPreference, setColumnPreference] = useState(readStoredColumnPreference);
154
+ const [showColumnCustomizationModal, setShowColumnCustomizationModal] = useState(false);
155
+ /*
156
+ * Ids are derived over the *whole* declared set, so a column dropping out
157
+ * for lack of permission can never renumber the ones that remain.
158
+ */
159
+ const allColumnIds = useMemo(() => {
160
+ return getColumnIds(allColumns);
161
+ }, [allColumns]);
162
+ const effectiveColumnPreference = useMemo(() => {
163
+ if (!isColumnCustomizationEnabled) {
164
+ return null;
165
+ }
166
+ /*
167
+ * A stored layout outlives the release that wrote it, so anything naming
168
+ * a column this table no longer has is dropped before it is applied.
169
+ */
170
+ return sanitizeColumnPreference({
171
+ preference: columnPreference,
172
+ knownColumnIds: allColumnIds,
173
+ });
174
+ }, [columnPreference, allColumnIds, isColumnCustomizationEnabled]);
175
+ const saveColumnPreference = (preference) => {
176
+ setColumnPreference(preference);
177
+ if (!props.userPreferencesKey) {
178
+ return;
179
+ }
180
+ const json = columnPreferenceToJSON(preference);
181
+ if (!json) {
182
+ UserPreferences.removeUserPreferenceByType({
183
+ key: props.userPreferencesKey,
184
+ userPreferenceType: UserPreferenceType.BaseModelTableColumns,
185
+ });
186
+ return;
187
+ }
188
+ UserPreferences.saveUserPreferenceByTypeAsJSON({
189
+ key: props.userPreferencesKey,
190
+ userPreferenceType: UserPreferenceType.BaseModelTableColumns,
191
+ value: json,
192
+ });
193
+ };
81
194
  const [classicTableFilters, setClassicTableFilters] = useState([]);
82
195
  const [cardButtons, setCardButtons] = useState([]);
83
196
  const [actionButtonSchema, setActionButtonSchema] = useState([]);
@@ -464,8 +577,11 @@ const BaseModelTable = (props) => {
464
577
  }
465
578
  }, [tableColumns]);
466
579
  const getRelationSelect = () => {
580
+ /*
581
+ * Deliberately the *unfiltered* column set. See getSelect() below.
582
+ */
467
583
  return getRelationSelectFromColumns({
468
- columns: props.columns || [],
584
+ columns: allColumns,
469
585
  model: model,
470
586
  });
471
587
  };
@@ -498,7 +614,21 @@ const BaseModelTable = (props) => {
498
614
  selectFields = props.callbacks.addSlugToSelect(selectFields);
499
615
  const userPermissions = getUserPermissions();
500
616
  const accessControl = model.getColumnAccessControlForAllColumns();
501
- for (const column of props.columns || []) {
617
+ /*
618
+ * The viewer's layout is applied to the *input* of this loop, never to
619
+ * the finished array: the Actions column is appended below and has to
620
+ * stay last, and it is generated rather than declared, so it is not the
621
+ * viewer's to move or switch off.
622
+ */
623
+ const columnsToRender = applyColumnPreference({
624
+ columns: allColumns,
625
+ preference: effectiveColumnPreference,
626
+ });
627
+ const columnIdByColumn = new Map();
628
+ allColumns.forEach((column, index) => {
629
+ columnIdByColumn.set(column, allColumnIds[index]);
630
+ });
631
+ for (const column of columnsToRender) {
502
632
  const hasPermission = hasPermissionToReadColumn(column) || User.isMasterAdmin();
503
633
  const key = getColumnKey(column);
504
634
  if (hasPermission) {
@@ -518,7 +648,19 @@ const BaseModelTable = (props) => {
518
648
  "." +
519
649
  column.selectedProperty)
520
650
  : key;
521
- columns.push(Object.assign(Object.assign({}, column), { disableSort: column.disableSort || shouldDisableSort(key), key: columnKey, tooltipText, getElement: column.getElement }));
651
+ columns.push(Object.assign(Object.assign({}, column), { id: columnIdByColumn.get(column), disableSort: column.disableSort || shouldDisableSort(key), key: columnKey,
652
+ /*
653
+ * `key` is only the first declared field, so a cell composed from
654
+ * several of them would export just that one. Hand the exporter
655
+ * every field the column declares (and that we actually selected).
656
+ */
657
+ exportKeys: getExportKeysFromColumn({
658
+ column: column,
659
+ columnKey: columnKey ? String(columnKey) : null,
660
+ hasPermissionToReadField: (field) => {
661
+ return hasPermissionToReadField(field);
662
+ },
663
+ }), tooltipText, getElement: column.getElement }));
522
664
  if (key) {
523
665
  selectFields[key] = true;
524
666
  }
@@ -760,28 +902,194 @@ const BaseModelTable = (props) => {
760
902
  }
761
903
  return fragment;
762
904
  };
905
+ /*
906
+ * Selects every row matching the table's current query, filters and search -
907
+ * not just the rows on screen - and returns whether it succeeded.
908
+ *
909
+ * It fetches the SAME columns as fetchItems. It used to ask for
910
+ * `select: { _id: true }`, which picked the right rows but stripped every
911
+ * field off them, so "Export CSV" over a select-all wrote one blank line per
912
+ * row and any bulk action that reads a field (or renders an item's name) saw
913
+ * undefined. Routing the projection through getSelect() also keeps
914
+ * `selectMoreFields` and per-field read permissions in play, which a
915
+ * hand-rolled column list would silently drop.
916
+ */
763
917
  const fetchAllBulkItems = async () => {
764
- setError("");
765
- setIsLoading(true);
918
+ var _a;
919
+ const token = ++bulkSelectAllToken.current;
920
+ setBulkSelectionError("");
921
+ setIsBulkSelectionTruncated(false);
922
+ setIsBulkSelectAllLoading(true);
923
+ const itemsSelected = [];
766
924
  try {
767
- const listResult = await props.callbacks.getList({
768
- modelType: props.modelType,
769
- query: Object.assign(Object.assign(Object.assign({}, props.query), query), buildSearchQueryFragment()),
770
- limit: LIMIT_PER_PROJECT,
771
- skip: 0,
772
- select: {
773
- _id: true,
774
- },
775
- sort: {},
776
- requestOptions: props.fetchRequestOptions,
777
- });
778
- setBulkSelectedItems(listResult.data);
925
+ /*
926
+ * An unsorted table is served in the API's default order, so the
927
+ * selection has to ask for that order explicitly - once any sort is
928
+ * sent, the server stops applying its default. Without this the rows
929
+ * came back in random UUID order, which both scrambled the exported
930
+ * CSV and, on a table past the selection ceiling, kept an arbitrary
931
+ * slice of history instead of the newest rows the user was looking at.
932
+ */
933
+ const defaultSortColumn = model.defaultSortColumn || "createdAt";
934
+ const bulkSort = (sortBy
935
+ ? { [sortBy]: sortOrder }
936
+ : { [defaultSortColumn]: SortOrder.Descending });
937
+ /*
938
+ * skip/limit paging is only stable over a total order. The field that
939
+ * identifies a selected row (`_id` unless a table overrides it) is
940
+ * appended as a tiebreaker so rows sharing a sort value - 1,200 labels
941
+ * imported in one transaction all share a createdAt - cannot land on
942
+ * two pages while another row is never returned at all.
943
+ */
944
+ if (bulkSort[matchBulkSelectedItemByField] === undefined) {
945
+ bulkSort[matchBulkSelectedItemByField] = SortOrder.Ascending;
946
+ }
947
+ /*
948
+ * getSelect() throws on a misconfigured column, so it is built inside
949
+ * the try: otherwise the button would be left spinning forever with an
950
+ * unhandled rejection instead of a message.
951
+ */
952
+ const bulkSelect = Object.assign(Object.assign({}, getSelect()), getRelationSelect());
953
+ /*
954
+ * Every ordered column has to be selected for the database to order by
955
+ * it. `_id` always is; the default sort column usually is not, and a
956
+ * table matching its selection on some field other than `_id` would
957
+ * otherwise tick no row checkboxes at all.
958
+ */
959
+ for (const sortColumn of Object.keys(bulkSort)) {
960
+ if (bulkSelect[sortColumn] === undefined &&
961
+ model.hasColumn(sortColumn)) {
962
+ bulkSelect[sortColumn] = true;
963
+ }
964
+ }
965
+ let totalCount = 0;
966
+ let hasMore = false;
967
+ let rowsFetched = 0;
968
+ /*
969
+ * Offset paging over a table that is still receiving writes can hand
970
+ * back a row that an earlier page already returned. Selecting it twice
971
+ * would duplicate it in the export and inflate the selected count.
972
+ */
973
+ const seenItemIds = new Set();
974
+ while (itemsSelected.length < LIMIT_PER_PROJECT) {
975
+ const limit = Math.min(BULK_SELECT_ALL_PAGE_SIZE, LIMIT_PER_PROJECT - itemsSelected.length);
976
+ const listResult = await props.callbacks.getList({
977
+ modelType: props.modelType,
978
+ query: Object.assign(Object.assign(Object.assign({}, props.query), query), buildSearchQueryFragment()),
979
+ groupBy: Object.assign({}, props.groupBy),
980
+ limit: limit,
981
+ skip: rowsFetched,
982
+ select: bulkSelect,
983
+ sort: bulkSort,
984
+ requestOptions: props.fetchRequestOptions,
985
+ });
986
+ if (token !== bulkSelectAllToken.current) {
987
+ // A newer select-all, or a cleared selection, superseded this run.
988
+ return false;
989
+ }
990
+ totalCount = listResult.count || totalCount;
991
+ hasMore = Boolean(listResult.hasMore);
992
+ rowsFetched += listResult.data.length;
993
+ for (const item of listResult.data) {
994
+ const itemId = ((_a = item[matchBulkSelectedItemByField]) === null || _a === void 0 ? void 0 : _a.toString()) || "";
995
+ if (itemId) {
996
+ if (seenItemIds.has(itemId)) {
997
+ continue;
998
+ }
999
+ seenItemIds.add(itemId);
1000
+ }
1001
+ itemsSelected.push(item);
1002
+ }
1003
+ if (listResult.data.length < limit) {
1004
+ // A short page is the last page.
1005
+ hasMore = false;
1006
+ break;
1007
+ }
1008
+ if (listResult.hasMore === false) {
1009
+ break;
1010
+ }
1011
+ if (totalCount > 0 && rowsFetched >= totalCount) {
1012
+ /*
1013
+ * Everything matching has been read. Stopping here saves the
1014
+ * empty round trip a result set that is an exact multiple of the
1015
+ * page size would otherwise cost - and each of those requests
1016
+ * runs a full count over the filtered set.
1017
+ */
1018
+ break;
1019
+ }
1020
+ }
1021
+ setBulkSelectionTotalCount(Math.max(totalCount, itemsSelected.length));
1022
+ setIsBulkSelectionTruncated(totalCount > rowsFetched || hasMore);
1023
+ setBulkSelectedItems(itemsSelected);
1024
+ return true;
779
1025
  }
780
1026
  catch (err) {
781
- setError(API.getFriendlyMessage(err));
1027
+ if (token === bulkSelectAllToken.current) {
1028
+ /*
1029
+ * Leave the existing selection alone. A half-loaded selection that
1030
+ * claims to be everything is worse than a failed one, and the user
1031
+ * can retry because the Select All button stays on screen.
1032
+ */
1033
+ setBulkSelectionError(API.getFriendlyMessage(err));
1034
+ }
1035
+ return false;
1036
+ }
1037
+ finally {
1038
+ if (token === bulkSelectAllToken.current) {
1039
+ setIsBulkSelectAllLoading(false);
1040
+ }
782
1041
  }
783
- setIsLoading(false);
784
1042
  };
1043
+ const clearBulkSelectionState = () => {
1044
+ // Supersede any select-all still in flight so it cannot repopulate this.
1045
+ bulkSelectAllToken.current++;
1046
+ setIsBulkSelectAllLoading(false);
1047
+ setBulkSelectionError("");
1048
+ setIsBulkSelectionTruncated(false);
1049
+ setBulkSelectionTotalCount(0);
1050
+ setBulkSelectedItems((existingItems) => {
1051
+ /*
1052
+ * Keep the same array when there is nothing to clear, so calling this on
1053
+ * every query change (below) costs an idle table no re-render.
1054
+ */
1055
+ return existingItems.length === 0 ? existingItems : [];
1056
+ });
1057
+ };
1058
+ /*
1059
+ * Everything that decides *which rows match* - the caller's query, the
1060
+ * column filters, the search term and the label chips. Serialised because
1061
+ * callers routinely pass `query` as a fresh object literal on every render,
1062
+ * and the label chips get their display names hydrated after mount without
1063
+ * the set of ids ever changing.
1064
+ *
1065
+ * Sort and page are deliberately left out: they re-order or re-window the
1066
+ * same matching set, so a selection made under them is still a selection of
1067
+ * rows that match. Selecting rows across pages is a real workflow, and a
1068
+ * truncated select-all keeps saying "selected N of M matching" after a
1069
+ * re-sort, which stays true.
1070
+ */
1071
+ const effectiveQueryKey = JSON.stringify({
1072
+ props: props.query || {},
1073
+ filter: query,
1074
+ search: debouncedSearchText.trim(),
1075
+ labels: selectedLabelIdsKey,
1076
+ });
1077
+ /*
1078
+ * A selection refers to the rows the *previous* query returned. Once the
1079
+ * query changes, the table underneath the bulk bar is a different result
1080
+ * set, so carrying the selection over leaves the bar claiming "6,000 Alerts
1081
+ * Selected" above a filtered-down list of 12 - with the Select All button
1082
+ * hidden, as though the selection matched what is on screen, and Delete
1083
+ * still wired to all 6,000 rows the user can no longer see.
1084
+ *
1085
+ * Dropping it here also bumps `bulkSelectAllToken`, so a filter change that
1086
+ * lands while a paged select-all is still running supersedes that run
1087
+ * instead of letting it finish and repopulate the selection against the old
1088
+ * query.
1089
+ */
1090
+ useEffect(() => {
1091
+ clearBulkSelectionState();
1092
+ }, [effectiveQueryKey]);
785
1093
  const fetchItems = async () => {
786
1094
  setError("");
787
1095
  setIsLoading(true);
@@ -855,13 +1163,35 @@ const BaseModelTable = (props) => {
855
1163
  });
856
1164
  }, []);
857
1165
  const getSelect = () => {
1166
+ /*
1167
+ * Every declared column, including the ones the viewer has switched off.
1168
+ *
1169
+ * Hiding must not narrow the request. A column's `getElement` is arbitrary
1170
+ * caller code that can read any field on the row — a "Status" cell that
1171
+ * also reads `currentMonitorStatus`, say — so dropping a hidden column's
1172
+ * fields from the select would silently blank a *different*, visible cell,
1173
+ * and there is no way to detect that statically. The sort field is
1174
+ * likewise independent of what is on screen. The cost is a few unused
1175
+ * fields on the wire.
1176
+ */
858
1177
  const selectFields = getSelectFromColumns({
859
- columns: props.columns || [],
1178
+ columns: allColumns,
860
1179
  model: model,
861
1180
  hasPermissionToReadField: (field) => {
862
1181
  return hasPermissionToReadField(field);
863
1182
  },
864
1183
  });
1184
+ /*
1185
+ * Custom field columns arrive asynchronously, but the table does not
1186
+ * refetch when its column set changes — so the JSON column that backs
1187
+ * every one of them is selected up front, off the synchronous prop, and
1188
+ * the values are there the moment the definitions land.
1189
+ */
1190
+ if (props.customFieldsModelType &&
1191
+ model.hasColumn(CustomFieldsColumnKey) &&
1192
+ hasPermissionToReadField(CustomFieldsColumnKey)) {
1193
+ selectFields[CustomFieldsColumnKey] = true;
1194
+ }
865
1195
  const selectMoreFields = props.selectMoreFields
866
1196
  ? Object.keys(props.selectMoreFields)
867
1197
  : [];
@@ -892,7 +1222,7 @@ const BaseModelTable = (props) => {
892
1222
  return React.createElement(React.Fragment, null);
893
1223
  }
894
1224
  if (props.saveFilterProps && props.saveFilterProps.tableId) {
895
- return (React.createElement(TableViewElement, { tableId: props.saveFilterProps.tableId, tableView: tableView, currentQuery: query, currentSortBy: sortBy, currentItemsOnPage: itemsOnPage, currentSortOrder: sortOrder, currentFacetState: props.currentFacetState, onViewChange: async (tableView) => {
1225
+ return (React.createElement(TableViewElement, { tableId: props.saveFilterProps.tableId, tableView: tableView, currentQuery: query, currentSortBy: sortBy, currentItemsOnPage: itemsOnPage, currentSortOrder: sortOrder, currentFacetState: props.currentFacetState, currentColumns: columnPreferenceToJSON(effectiveColumnPreference) || undefined, onViewChange: async (tableView) => {
896
1226
  var _a;
897
1227
  setTableView(tableView);
898
1228
  if (tableView) {
@@ -918,6 +1248,15 @@ const BaseModelTable = (props) => {
918
1248
  if (props.onFacetStateRestored) {
919
1249
  props.onFacetStateRestored(tableView.facets || null);
920
1250
  }
1251
+ /*
1252
+ * A saved view carries the columns it was saved with. Views
1253
+ * created before this existed have none, which restores the
1254
+ * table's default layout rather than leaving the previous
1255
+ * view's columns in place.
1256
+ */
1257
+ if (isColumnCustomizationEnabled) {
1258
+ saveColumnPreference(columnPreferenceFromJSON(tableView.columns || null));
1259
+ }
921
1260
  }
922
1261
  else {
923
1262
  setQuery({});
@@ -935,6 +1274,12 @@ const BaseModelTable = (props) => {
935
1274
  if (props.onFacetStateRestored) {
936
1275
  props.onFacetStateRestored(null);
937
1276
  }
1277
+ /*
1278
+ * Columns are deliberately left alone here. Clearing a saved
1279
+ * view means "stop filtering like that", not "throw away the
1280
+ * column layout I built" - which the viewer may well have set
1281
+ * up long before this view existed.
1282
+ */
938
1283
  }
939
1284
  } }));
940
1285
  }
@@ -1042,6 +1387,22 @@ const BaseModelTable = (props) => {
1042
1387
  icon: IconProp.Filter,
1043
1388
  });
1044
1389
  }
1390
+ /*
1391
+ * Only worth offering once there is a choice to make. A single-column
1392
+ * table has nothing to hide and nothing to reorder.
1393
+ */
1394
+ if (isColumnCustomizationEnabled && allColumns.length > 1) {
1395
+ headerbuttons.push({
1396
+ title: "",
1397
+ buttonStyle: ButtonStyleType.ICON,
1398
+ buttonSize: ButtonSize.Small,
1399
+ className: "",
1400
+ onClick: () => {
1401
+ setShowColumnCustomizationModal(true);
1402
+ },
1403
+ icon: IconProp.TableCells,
1404
+ });
1405
+ }
1045
1406
  setCardButtons(headerbuttons);
1046
1407
  };
1047
1408
  useEffect(() => {
@@ -1140,6 +1501,13 @@ const BaseModelTable = (props) => {
1140
1501
  useEffect(() => {
1141
1502
  serializeToTableColumns();
1142
1503
  }, [props.columns]);
1504
+ /*
1505
+ * The viewer changed their layout, or the project's custom field
1506
+ * definitions finally arrived.
1507
+ */
1508
+ useEffect(() => {
1509
+ serializeToTableColumns();
1510
+ }, [effectiveColumnPreference, customFieldColumns.columns]);
1143
1511
  const setActionSchema = () => {
1144
1512
  const permissions = PermissionUtil.getAllPermissions();
1145
1513
  const actionsSchema = [];
@@ -1412,11 +1780,14 @@ const BaseModelTable = (props) => {
1412
1780
  }),
1413
1781
  };
1414
1782
  })(), onBulkActionEnd: async () => {
1415
- setBulkSelectedItems([]);
1783
+ clearBulkSelectionState();
1416
1784
  await fetchItems();
1417
1785
  }, onBulkActionStart: () => { }, bulkSelectedItems: bulkSelectedItems, onBulkSelectedItemAdded: (item) => {
1786
+ // The user has moved on from the failed select-all.
1787
+ setBulkSelectionError("");
1418
1788
  setBulkSelectedItems([...bulkSelectedItems, item]);
1419
1789
  }, onBulkSelectedItemRemoved: (item) => {
1790
+ setBulkSelectionError("");
1420
1791
  setBulkSelectedItems(bulkSelectedItems.filter((i) => {
1421
1792
  var _a, _b;
1422
1793
  return (((_a = i[matchBulkSelectedItemByField]) === null || _a === void 0 ? void 0 : _a.toString()) !==
@@ -1435,10 +1806,10 @@ const BaseModelTable = (props) => {
1435
1806
  });
1436
1807
  setBulkSelectedItems(uniqueItems);
1437
1808
  }, onBulkClearAllItems: () => {
1438
- setBulkSelectedItems([]);
1809
+ clearBulkSelectionState();
1439
1810
  }, onBulkSelectAllItems: async () => {
1440
- await fetchAllBulkItems();
1441
- }, matchBulkSelectedItemByField: matchBulkSelectedItemByField || "_id", bulkItemToString: (item) => {
1811
+ return await fetchAllBulkItems();
1812
+ }, bulkSelectionError: bulkSelectionError, isBulkSelectAllLoading: isBulkSelectAllLoading, isBulkSelectionTruncated: isBulkSelectionTruncated, bulkSelectionTotalCount: bulkSelectionTotalCount, matchBulkSelectedItemByField: matchBulkSelectedItemByField || "_id", bulkItemToString: (item) => {
1442
1813
  var _a, _b;
1443
1814
  const label = props.singularName || item.singularName || "";
1444
1815
  const name = ((_a = item["name"]) === null || _a === void 0 ? void 0 : _a.toString()) ||
@@ -1878,6 +2249,8 @@ const BaseModelTable = (props) => {
1878
2249
  return "Watch Demo";
1879
2250
  case IconProp.Search:
1880
2251
  return "Search";
2252
+ case IconProp.TableCells:
2253
+ return "Columns";
1881
2254
  default:
1882
2255
  return "Action";
1883
2256
  }
@@ -1973,6 +2346,77 @@ const BaseModelTable = (props) => {
1973
2346
  const getCardHeaderTitle = (originalTitle) => {
1974
2347
  return originalTitle;
1975
2348
  };
2349
+ /*
2350
+ * A column the viewer cannot read is left out of the picker entirely.
2351
+ * Listing it would advertise a field they have no access to, and switching
2352
+ * it on would put that field in the select — which the API rejects for the
2353
+ * whole request, blanking the table.
2354
+ */
2355
+ const isPickableColumn = (column) => {
2356
+ if (column.isNotCustomizable) {
2357
+ return false;
2358
+ }
2359
+ return hasPermissionToReadColumn(column) || User.isMasterAdmin();
2360
+ };
2361
+ const getPickerEntries = (preference) => {
2362
+ return getCustomizableColumns({
2363
+ columns: allColumns,
2364
+ preference: preference,
2365
+ }).filter((entry) => {
2366
+ return isPickableColumn(entry.column);
2367
+ });
2368
+ };
2369
+ // Mirrors how serializeToTableColumns derives the key the header sorts on.
2370
+ const getColumnSortKey = (column) => {
2371
+ const key = getColumnKey(column);
2372
+ if (!key) {
2373
+ return null;
2374
+ }
2375
+ return column.selectedProperty
2376
+ ? `${String(key)}.${column.selectedProperty}`
2377
+ : String(key);
2378
+ };
2379
+ const onColumnCustomizationSave = (entries) => {
2380
+ const preference = buildColumnPreference(entries);
2381
+ /*
2382
+ * If the layout the viewer just saved matches what the table ships with,
2383
+ * store nothing. Otherwise every table anyone ever opened the picker on
2384
+ * would be pinned to the column set of the release they opened it in, and
2385
+ * columns added later would arrive already stale.
2386
+ */
2387
+ const defaultPreference = buildColumnPreference(getPickerEntries(null));
2388
+ const isBackToDefault = JSON.stringify(preference) === JSON.stringify(defaultPreference);
2389
+ /*
2390
+ * Sorting by a column that is no longer on screen leaves the viewer with
2391
+ * an ordering they can neither see nor undo — there is no header left to
2392
+ * click. Fall back to the table's own default sort.
2393
+ */
2394
+ if (sortBy) {
2395
+ const hiddenIds = new Set(preference.hidden);
2396
+ const isSortedColumnHidden = entries.some((entry) => {
2397
+ return (hiddenIds.has(entry.id) &&
2398
+ getColumnSortKey(entry.column) === String(sortBy));
2399
+ });
2400
+ if (isSortedColumnHidden) {
2401
+ setSortBy(props.sortBy || null);
2402
+ setSortOrder(props.sortOrder || SortOrder.Ascending);
2403
+ setCurrentPageNumber(1);
2404
+ }
2405
+ }
2406
+ saveColumnPreference(isBackToDefault ? null : preference);
2407
+ setShowColumnCustomizationModal(false);
2408
+ };
2409
+ const getColumnCustomizationModal = () => {
2410
+ if (!showColumnCustomizationModal || !isColumnCustomizationEnabled) {
2411
+ return null;
2412
+ }
2413
+ return (React.createElement(ColumnCustomizationModal, { columns: getPickerEntries(effectiveColumnPreference), isDefaultLayout: isEmptyColumnPreference(effectiveColumnPreference), title: tx("Customize Columns"), onSave: onColumnCustomizationSave, onReset: () => {
2414
+ saveColumnPreference(null);
2415
+ setShowColumnCustomizationModal(false);
2416
+ }, onClose: () => {
2417
+ setShowColumnCustomizationModal(false);
2418
+ } }));
2419
+ };
1976
2420
  const getCardComponent = () => {
1977
2421
  const headerButtons = getHeaderButtonsWithSearch();
1978
2422
  if (showAs === ShowAs.Table || showAs === ShowAs.List) {
@@ -1980,7 +2424,7 @@ const BaseModelTable = (props) => {
1980
2424
  props.cardProps && (React.createElement(Card, Object.assign({}, props.cardProps, { buttons: headerButtons, bodyClassName: showAs === ShowAs.List
1981
2425
  ? "-ml-6 -mr-6 bg-gray-50 border-top"
1982
2426
  : "", title: getCardHeaderTitle(getCardTitle(props.cardProps.title || "")) }),
1983
- tableColumns.length === 0 && props.columns.length > 0 ? (React.createElement(ErrorMessage, { message: `You are not authorized to view this table. You need any one of these permissions: ${PermissionHelper.getPermissionTitles(model.getReadPermissions()).join(", ")}` })) : (React.createElement(React.Fragment, null)),
2427
+ tableColumns.length === 0 && allColumns.length > 0 ? (React.createElement(ErrorMessage, { message: `You are not authorized to view this table. You need any one of these permissions: ${PermissionHelper.getPermissionTitles(model.getReadPermissions()).join(", ")}` })) : (React.createElement(React.Fragment, null)),
1984
2428
  props.topContent || React.createElement(React.Fragment, null),
1985
2429
  tableColumns.length > 0 && showAs === ShowAs.Table ? (getTable()) : (React.createElement(React.Fragment, null)),
1986
2430
  tableColumns.length > 0 && showAs === ShowAs.List ? (getList()) : (React.createElement(React.Fragment, null)))),
@@ -2066,7 +2510,8 @@ const BaseModelTable = (props) => {
2066
2510
  setShowHelpModal(false);
2067
2511
  } },
2068
2512
  React.createElement("div", { className: "p-2" },
2069
- React.createElement(MarkdownViewer, { text: props.helpContent.markdown }))))));
2513
+ React.createElement(MarkdownViewer, { text: props.helpContent.markdown })))),
2514
+ getColumnCustomizationModal()));
2070
2515
  };
2071
2516
  export default BaseModelTable;
2072
2517
  //# sourceMappingURL=BaseModelTable.js.map