@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,153 @@
1
+ import Column from "../../../UI/Components/ModelTable/Column";
2
+ import { getExportKeysFromColumn } from "../../../UI/Components/ModelTable/ExportFromColumns";
3
+ import FieldType from "../../../UI/Components/Types/FieldType";
4
+ import Alert from "../../../Models/DatabaseModels/Alert";
5
+ import ProxmoxCluster from "../../../Models/DatabaseModels/ProxmoxCluster";
6
+ import { describe, expect, test } from "@jest/globals";
7
+
8
+ describe("ModelTable ExportFromColumns", () => {
9
+ describe("getExportKeysFromColumn", () => {
10
+ test("it returns every field a column declares, not just the first", () => {
11
+ /*
12
+ * The alert "Affected Resources" cell renders five relations at once.
13
+ * All five are selected, but the Table column's `key` only holds the
14
+ * first, so the CSV export used to carry the hosts and drop the rest.
15
+ */
16
+ const column: Column<Alert> = {
17
+ field: {
18
+ hosts: { name: true },
19
+ kubernetesClusters: { name: true },
20
+ dockerHosts: { name: true },
21
+ podmanHosts: { name: true },
22
+ services: { name: true },
23
+ },
24
+ title: "Affected Resources",
25
+ type: FieldType.EntityArray,
26
+ };
27
+
28
+ expect(
29
+ getExportKeysFromColumn<Alert>({
30
+ column: column,
31
+ columnKey: "hosts",
32
+ }),
33
+ ).toEqual([
34
+ "hosts",
35
+ "kubernetesClusters",
36
+ "dockerHosts",
37
+ "podmanHosts",
38
+ "services",
39
+ ]);
40
+ });
41
+
42
+ test("it returns a single key for an ordinary column", () => {
43
+ const column: Column<ProxmoxCluster> = {
44
+ field: { name: true },
45
+ title: "Name",
46
+ type: FieldType.Text,
47
+ };
48
+
49
+ expect(
50
+ getExportKeysFromColumn<ProxmoxCluster>({
51
+ column: column,
52
+ columnKey: "name",
53
+ }),
54
+ ).toEqual(["name"]);
55
+ });
56
+
57
+ test("it keeps the selectedProperty path the cell renders", () => {
58
+ /*
59
+ * columnKey already carries the "relation.property" path, and that is
60
+ * the path the export has to read - not the bare relation.
61
+ */
62
+ const column: Column<Alert> = {
63
+ field: { alertSeverity: { name: true } },
64
+ selectedProperty: "name",
65
+ title: "Severity",
66
+ type: FieldType.Text,
67
+ };
68
+
69
+ expect(
70
+ getExportKeysFromColumn<Alert>({
71
+ column: column,
72
+ columnKey: "alertSeverity.name",
73
+ }),
74
+ ).toEqual(["alertSeverity.name"]);
75
+ });
76
+
77
+ test("it drops secondary fields the user cannot read", () => {
78
+ /*
79
+ * Mirrors getSelectFromColumns: a field that was never selected because
80
+ * of permissions is not on the row, so exporting it would only ever
81
+ * produce an empty cell.
82
+ */
83
+ const column: Column<ProxmoxCluster> = {
84
+ field: { nodeCount: true, onlineNodeCount: true },
85
+ title: "Nodes",
86
+ type: FieldType.Element,
87
+ };
88
+
89
+ expect(
90
+ getExportKeysFromColumn<ProxmoxCluster>({
91
+ column: column,
92
+ columnKey: "nodeCount",
93
+ hasPermissionToReadField: (field: string): boolean => {
94
+ return field !== "onlineNodeCount";
95
+ },
96
+ }),
97
+ ).toEqual(["nodeCount"]);
98
+ });
99
+
100
+ test("it keeps the primary field even when the permission check rejects it", () => {
101
+ /*
102
+ * The primary field is what the column sorts and renders by, and
103
+ * BaseModelTable has already decided the column is visible by this
104
+ * point. Only secondary fields are gated here.
105
+ */
106
+ const column: Column<ProxmoxCluster> = {
107
+ field: { nodeCount: true, onlineNodeCount: true },
108
+ title: "Nodes",
109
+ type: FieldType.Element,
110
+ };
111
+
112
+ expect(
113
+ getExportKeysFromColumn<ProxmoxCluster>({
114
+ column: column,
115
+ columnKey: "nodeCount",
116
+ hasPermissionToReadField: (): boolean => {
117
+ return false;
118
+ },
119
+ }),
120
+ ).toEqual(["nodeCount"]);
121
+ });
122
+
123
+ test("it does not repeat the primary field when it is declared again", () => {
124
+ const column: Column<ProxmoxCluster> = {
125
+ field: { name: true, nodeCount: true },
126
+ title: "Name",
127
+ type: FieldType.Element,
128
+ };
129
+
130
+ expect(
131
+ getExportKeysFromColumn<ProxmoxCluster>({
132
+ column: column,
133
+ columnKey: "name",
134
+ }),
135
+ ).toEqual(["name", "nodeCount"]);
136
+ });
137
+
138
+ test("it returns nothing for a column with no key", () => {
139
+ const column: Column<ProxmoxCluster> = {
140
+ field: {},
141
+ title: "Actions",
142
+ type: FieldType.Actions,
143
+ };
144
+
145
+ expect(
146
+ getExportKeysFromColumn<ProxmoxCluster>({
147
+ column: column,
148
+ columnKey: null,
149
+ }),
150
+ ).toEqual([]);
151
+ });
152
+ });
153
+ });
@@ -0,0 +1,67 @@
1
+ import getJestMockFunction, { MockFunction } from "../../../Tests/MockType";
2
+ import { afterEach, describe, expect, jest, test } from "@jest/globals";
3
+
4
+ const config: MockFunction = getJestMockFunction();
5
+
6
+ jest.mock("@monaco-editor/react", () => {
7
+ return {
8
+ loader: {
9
+ config,
10
+ },
11
+ };
12
+ });
13
+
14
+ describe("configureMonacoLoader", () => {
15
+ afterEach(() => {
16
+ config.mockClear();
17
+ delete process.env["MONACO_ASSET_PATH"];
18
+ jest.resetModules();
19
+ });
20
+
21
+ type LoadLoader = () => Promise<() => void>;
22
+
23
+ const loadLoader: LoadLoader = async (): Promise<() => void> => {
24
+ const { default: configureMonacoLoader } = await import(
25
+ "../../../UI/Components/CodeEditor/MonacoLoader"
26
+ );
27
+
28
+ return configureMonacoLoader;
29
+ };
30
+
31
+ test("points Monaco at the path the build baked in", async () => {
32
+ process.env["MONACO_ASSET_PATH"] = "/dashboard/assets/monaco/vs";
33
+
34
+ (await loadLoader())();
35
+
36
+ /*
37
+ * Absolute, not the root-relative value the build baked in: Monaco resolves
38
+ * its language-service workers from this inside a blob: worker, which has no
39
+ * base to resolve a root-relative path against.
40
+ */
41
+ expect(config).toHaveBeenCalledWith({
42
+ paths: {
43
+ vs: `${window.location.origin}/dashboard/assets/monaco/vs`,
44
+ },
45
+ });
46
+ });
47
+
48
+ test("gives Monaco an absolute path its blob workers can resolve", async () => {
49
+ process.env["MONACO_ASSET_PATH"] = "/dashboard/assets/monaco/vs";
50
+
51
+ (await loadLoader())();
52
+
53
+ const configuredPath: string = (
54
+ config.mock.calls[0] as unknown as Array<{ paths: { vs: string } }>
55
+ )[0]!.paths.vs;
56
+
57
+ expect(() => {
58
+ return new URL(configuredPath);
59
+ }).not.toThrow();
60
+ });
61
+
62
+ test("keeps the bundled default when the build set no path", async () => {
63
+ (await loadLoader())();
64
+
65
+ expect(config).not.toHaveBeenCalled();
66
+ });
67
+ });
@@ -0,0 +1,121 @@
1
+ import { describe, expect, test } from "@jest/globals";
2
+ import fs from "fs";
3
+ import path from "path";
4
+
5
+ /*
6
+ * The build copies monaco-editor's min/vs next to each frontend bundle and
7
+ * MonacoLoader points @monaco-editor/loader at it, so the version we install is
8
+ * the version that actually runs in the browser.
9
+ *
10
+ * The loader resolves init() with whatever the AMD module `vs/editor/editor.main`
11
+ * exports, and that shape is not stable across Monaco releases. 0.53 and 0.54
12
+ * nest the API under `exports.m`; against a loader that does not unwrap that,
13
+ * `monaco.editor` comes back undefined and every editor throws
14
+ * "Cannot read properties of undefined (reading 'getModel')" instead of
15
+ * mounting - with the assets all serving 200, so nothing else looks wrong.
16
+ *
17
+ * @monaco-editor/loader learned to unwrap it in 1.7.0. These tests fail if the
18
+ * two are ever moved into a combination that cannot work.
19
+ */
20
+
21
+ // [from, to) - the Monaco releases whose AMD module nests the API under `exports.m`.
22
+ const NESTED_API_RANGE: { from: string; to: string } = {
23
+ from: "0.53.0",
24
+ to: "0.55.0",
25
+ };
26
+
27
+ // The first @monaco-editor/loader that unwraps `exports.m`.
28
+ const LOADER_UNWRAPS_FROM: string = "1.7.0";
29
+
30
+ type ReadVersion = (packageName: string) => string;
31
+
32
+ const readVersion: ReadVersion = (packageName: string): string => {
33
+ return JSON.parse(
34
+ fs.readFileSync(require.resolve(`${packageName}/package.json`), "utf8"),
35
+ ).version;
36
+ };
37
+
38
+ type Compare = (a: string, b: string) => number;
39
+
40
+ const compare: Compare = (a: string, b: string): number => {
41
+ const left: Array<number> = a.split(".").map(Number);
42
+ const right: Array<number> = b.split(".").map(Number);
43
+
44
+ for (let i: number = 0; i < 3; i++) {
45
+ const difference: number = (left[i] || 0) - (right[i] || 0);
46
+
47
+ if (difference !== 0) {
48
+ return difference;
49
+ }
50
+ }
51
+
52
+ return 0;
53
+ };
54
+
55
+ describe("Monaco runtime", () => {
56
+ test("does not pair a nested-API Monaco with a loader that cannot unwrap it", () => {
57
+ const monacoVersion: string = readVersion("monaco-editor");
58
+ const loaderVersion: string = readVersion("@monaco-editor/loader");
59
+
60
+ const nestsApi: boolean =
61
+ compare(monacoVersion, NESTED_API_RANGE.from) >= 0 &&
62
+ compare(monacoVersion, NESTED_API_RANGE.to) < 0;
63
+
64
+ if (!nestsApi) {
65
+ return;
66
+ }
67
+
68
+ expect({
69
+ monacoVersion,
70
+ loaderVersion,
71
+ unwrapsNestedApi: compare(loaderVersion, LOADER_UNWRAPS_FROM) >= 0,
72
+ }).toEqual({
73
+ monacoVersion,
74
+ loaderVersion,
75
+ unwrapsNestedApi: true,
76
+ });
77
+ });
78
+
79
+ test("ships the files the loader asks for by path", () => {
80
+ const monacoRoot: string = path.dirname(
81
+ require.resolve("monaco-editor/package.json"),
82
+ );
83
+
84
+ /*
85
+ * The loader builds a script tag for `${paths.vs}/loader.js` and then
86
+ * require()s `vs/editor/editor.main`. Everything else Monaco pulls in is
87
+ * relative to those two, so a missing one is the difference between a
88
+ * working editor and a silent 404 on an air-gapped install.
89
+ */
90
+ for (const asset of ["loader.js", "editor/editor.main.js"]) {
91
+ expect(fs.existsSync(path.join(monacoRoot, "min", "vs", asset))).toBe(
92
+ true,
93
+ );
94
+ }
95
+ });
96
+
97
+ test("does not bring a second copy of anything Common already depends on", () => {
98
+ /*
99
+ * Monaco releases from 0.55 on depend on marked and dompurify, both of which
100
+ * Common already pins at the top level. npm nests Monaco's own copies
101
+ * because it pins them exactly, so the top-level pin does not reach them and
102
+ * the tree ends up carrying two versions - the older one being whatever
103
+ * Monaco vendored at release time.
104
+ */
105
+ const monacoDependencies: Array<string> = Object.keys(
106
+ JSON.parse(
107
+ fs.readFileSync(require.resolve("monaco-editor/package.json"), "utf8"),
108
+ ).dependencies || {},
109
+ );
110
+
111
+ const commonDependencies: Record<string, string> = JSON.parse(
112
+ fs.readFileSync(path.join(__dirname, "../../../package.json"), "utf8"),
113
+ ).dependencies;
114
+
115
+ expect(
116
+ monacoDependencies.filter((name: string) => {
117
+ return Boolean(commonDependencies[name]);
118
+ }),
119
+ ).toEqual([]);
120
+ });
121
+ });
@@ -1,8 +1,10 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from "@jest/globals";
2
2
  import { fireEvent, render, RenderResult } from "@testing-library/react";
3
3
  import * as React from "react";
4
+ import { ReactElement } from "react";
4
5
  import getJestMockFunction, { MockFunction } from "../../MockType";
5
6
  import Table, { BulkActionProps } from "../../../UI/Components/Table/Table";
7
+ import TableColumnsToCsv from "../../../UI/Utils/TableColumnsToCsv";
6
8
  import Columns from "../../../UI/Components/Table/Types/Columns";
7
9
  import FieldType from "../../../UI/Components/Types/FieldType";
8
10
  import SortOrder from "../../../Types/BaseDatabase/SortOrder";
@@ -27,21 +29,26 @@ jest.mock("react-i18next", () => {
27
29
  interface Row {
28
30
  _id?: string | undefined;
29
31
  name?: string | undefined;
32
+ description?: string | undefined;
33
+ hosts?: Array<{ name: string }> | undefined;
34
+ services?: Array<{ name: string }> | undefined;
30
35
  }
31
36
 
32
37
  const columns: Columns<Row> = [
33
38
  { title: "Name", type: FieldType.Text, key: "name" },
39
+ { title: "Description", type: FieldType.Text, key: "description" },
34
40
  ];
35
41
 
36
42
  const data: Array<Row> = [
37
- { _id: "1", name: "Alpha" },
38
- { _id: "2", name: "Beta" },
43
+ { _id: "1", name: "Alpha", description: "First" },
44
+ { _id: "2", name: "Beta", description: "Second" },
39
45
  ];
40
46
 
41
47
  interface RenderTableOptions {
42
48
  bulkActions?: BulkActionProps<Row> | undefined;
43
49
  bulkSelectedItems?: Array<Row> | undefined;
44
50
  disableBulkCsvExport?: boolean | undefined;
51
+ columns?: Columns<Row> | undefined;
45
52
  }
46
53
 
47
54
  type RenderTableFunction = (options: RenderTableOptions) => RenderResult;
@@ -53,7 +60,7 @@ const renderTable: RenderTableFunction = (
53
60
  <Table<Row>
54
61
  id="test-table"
55
62
  data={data}
56
- columns={columns}
63
+ columns={options.columns || columns}
57
64
  currentPageNumber={1}
58
65
  totalItemsCount={data.length}
59
66
  itemsOnPage={10}
@@ -89,6 +96,8 @@ describe("Table bulk CSV export", () => {
89
96
  let createObjectURLMock: MockFunction;
90
97
  let revokeObjectURLMock: MockFunction;
91
98
  let clickSpy: jest.SpyInstance;
99
+ let downloadedCsvFiles: Array<{ csv: string; filename: string }> = [];
100
+ let downloadCsvSpy: jest.SpyInstance;
92
101
 
93
102
  beforeEach(() => {
94
103
  createObjectURLMock = getJestMockFunction();
@@ -103,10 +112,28 @@ describe("Table bulk CSV export", () => {
103
112
  clickSpy = jest
104
113
  .spyOn(HTMLAnchorElement.prototype, "click")
105
114
  .mockImplementation(() => {});
115
+
116
+ downloadedCsvFiles = [];
106
117
  });
107
118
 
119
+ type SpyOnDownloadFunction = () => void;
120
+
121
+ /*
122
+ * Asserting on the CSV text the exporter hands to the browser, rather than
123
+ * on the opaque Blob, is what lets these tests see an export whose rows are
124
+ * all blank - the shape the bug produced.
125
+ */
126
+ const spyOnDownload: SpyOnDownloadFunction = (): void => {
127
+ downloadCsvSpy = jest
128
+ .spyOn(TableColumnsToCsv, "downloadCsv")
129
+ .mockImplementation((...args: Array<unknown>): void => {
130
+ downloadedCsvFiles.push(args[0] as { csv: string; filename: string });
131
+ });
132
+ };
133
+
108
134
  afterEach(() => {
109
135
  clickSpy.mockRestore();
136
+ downloadCsvSpy?.mockRestore();
110
137
  });
111
138
 
112
139
  test("shows an Export CSV action when the table has bulk actions and rows are selected", () => {
@@ -165,4 +192,186 @@ describe("Table bulk CSV export", () => {
165
192
  expect(queryByText("Bulk Actions")).toBeNull();
166
193
  expect(queryByText("Export CSV")).toBeNull();
167
194
  });
195
+
196
+ test("the exported CSV carries the values of every selected row", () => {
197
+ spyOnDownload();
198
+
199
+ const { getByText } = renderTable({
200
+ bulkActions: customBulkActions,
201
+ bulkSelectedItems: data,
202
+ });
203
+
204
+ fireEvent.click(getByText("Bulk Actions"));
205
+ fireEvent.click(getByText("Export CSV"));
206
+
207
+ expect(downloadedCsvFiles.length).toBe(1);
208
+ expect(downloadedCsvFiles[0]!.csv.split("\r\n")).toEqual([
209
+ "Name,Description",
210
+ "Alpha,First",
211
+ "Beta,Second",
212
+ ]);
213
+ });
214
+
215
+ test("it exports the selection, not the rows on screen", () => {
216
+ spyOnDownload();
217
+
218
+ /*
219
+ * The selection outgrows the page as soon as the user selects across
220
+ * pages or picks "Select All", so the export has to follow
221
+ * bulkSelectedItems rather than the rendered data.
222
+ */
223
+ const selection: Array<Row> = [];
224
+
225
+ for (let i: number = 0; i < 30; i++) {
226
+ selection.push({
227
+ _id: `${i}`,
228
+ name: `Row ${i}`,
229
+ description: `Desc ${i}`,
230
+ });
231
+ }
232
+
233
+ const { getByText } = renderTable({
234
+ bulkActions: customBulkActions,
235
+ bulkSelectedItems: selection,
236
+ });
237
+
238
+ fireEvent.click(getByText("Bulk Actions"));
239
+ fireEvent.click(getByText("Export CSV"));
240
+
241
+ const lines: Array<string> = downloadedCsvFiles[0]!.csv.split("\r\n");
242
+
243
+ // Header plus every selected row, even though the table renders two.
244
+ expect(lines.length).toBe(31);
245
+ expect(lines[30]).toBe("Row 29,Desc 29");
246
+ });
247
+
248
+ test("rows stripped down to an id export as blank cells", () => {
249
+ spyOnDownload();
250
+
251
+ /*
252
+ * Characterisation test. TableColumnsToCsv writes what it is handed, so
253
+ * a selection of id-only models produces a header and nothing else -
254
+ * which is exactly what the "Select All then Export CSV" bug looked
255
+ * like. The fix belongs where the selection is fetched
256
+ * (BaseModelTable.fetchAllBulkItems), not here.
257
+ */
258
+ const { getByText } = renderTable({
259
+ bulkActions: customBulkActions,
260
+ bulkSelectedItems: [{ _id: "1" }, { _id: "2" }],
261
+ });
262
+
263
+ fireEvent.click(getByText("Bulk Actions"));
264
+ fireEvent.click(getByText("Export CSV"));
265
+
266
+ expect(downloadedCsvFiles[0]!.csv.split("\r\n")).toEqual([
267
+ "Name,Description",
268
+ ",",
269
+ ",",
270
+ ]);
271
+ });
272
+
273
+ test("a placeholder id column is left out instead of exporting a raw uuid", () => {
274
+ spyOnDownload();
275
+
276
+ /*
277
+ * The "Owners" column on alerts / incidents / monitors renders entirely
278
+ * through getElement and declares `field: { _id: true }` only so the row
279
+ * gets fetched. It used to put the row's UUID in the file under the
280
+ * header "Owners".
281
+ */
282
+ const { getByText } = renderTable({
283
+ bulkActions: customBulkActions,
284
+ bulkSelectedItems: [data[0]!],
285
+ columns: [
286
+ { title: "Name", type: FieldType.Text, key: "name" },
287
+ {
288
+ title: "Owners",
289
+ type: FieldType.Element,
290
+ key: "_id",
291
+ exportKeys: ["_id"],
292
+ getElement: (): ReactElement => {
293
+ return <span>Owners</span>;
294
+ },
295
+ },
296
+ ],
297
+ });
298
+
299
+ fireEvent.click(getByText("Bulk Actions"));
300
+ fireEvent.click(getByText("Export CSV"));
301
+
302
+ expect(downloadedCsvFiles[0]!.csv.split("\r\n")).toEqual(["Name", "Alpha"]);
303
+ });
304
+
305
+ test("a placeholder id column that supplies an export value is exported", () => {
306
+ spyOnDownload();
307
+
308
+ const { getByText } = renderTable({
309
+ bulkActions: customBulkActions,
310
+ bulkSelectedItems: [data[0]!],
311
+ columns: [
312
+ { title: "Name", type: FieldType.Text, key: "name" },
313
+ {
314
+ title: "Owners",
315
+ type: FieldType.Element,
316
+ key: "_id",
317
+ exportKeys: ["_id"],
318
+ getElement: (): ReactElement => {
319
+ return <span>Owners</span>;
320
+ },
321
+ getExportValue: (item: Row): string => {
322
+ return `owner-of-${item.name}`;
323
+ },
324
+ },
325
+ ],
326
+ });
327
+
328
+ fireEvent.click(getByText("Bulk Actions"));
329
+ fireEvent.click(getByText("Export CSV"));
330
+
331
+ expect(downloadedCsvFiles[0]!.csv.split("\r\n")).toEqual([
332
+ "Name,Owners",
333
+ "Alpha,owner-of-Alpha",
334
+ ]);
335
+ });
336
+
337
+ test("a column that renders several relations exports all of them", () => {
338
+ spyOnDownload();
339
+
340
+ /*
341
+ * The alert "Affected Resources" cell spans hosts / kubernetesClusters /
342
+ * dockerHosts / podmanHosts / services. All of them are fetched, but only
343
+ * the first reached the file.
344
+ */
345
+ const { getByText } = renderTable({
346
+ bulkActions: customBulkActions,
347
+ bulkSelectedItems: [
348
+ {
349
+ _id: "1",
350
+ name: "Alpha",
351
+ hosts: [{ name: "web-01" }],
352
+ services: [{ name: "checkout" }],
353
+ },
354
+ ],
355
+ columns: [
356
+ { title: "Name", type: FieldType.Text, key: "name" },
357
+ {
358
+ title: "Affected Resources",
359
+ type: FieldType.EntityArray,
360
+ key: "hosts",
361
+ exportKeys: ["hosts", "services"],
362
+ getElement: (): ReactElement => {
363
+ return <span>Resources</span>;
364
+ },
365
+ },
366
+ ],
367
+ });
368
+
369
+ fireEvent.click(getByText("Bulk Actions"));
370
+ fireEvent.click(getByText("Export CSV"));
371
+
372
+ expect(downloadedCsvFiles[0]!.csv.split("\r\n")).toEqual([
373
+ "Name,Affected Resources",
374
+ "Alpha,web-01; checkout",
375
+ ]);
376
+ });
168
377
  });