@oneuptime/common 11.7.1 → 11.7.2

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 (237) hide show
  1. package/Models/DatabaseModels/DashboardDomain.ts +1 -0
  2. package/Models/DatabaseModels/Incident.ts +2 -0
  3. package/Models/DatabaseModels/IncidentEpisode.ts +1 -0
  4. package/Models/DatabaseModels/IncidentEpisodePublicNote.ts +1 -0
  5. package/Models/DatabaseModels/IncidentEpisodeStateTimeline.ts +1 -0
  6. package/Models/DatabaseModels/IncidentPublicNote.ts +1 -0
  7. package/Models/DatabaseModels/IncidentStateTimeline.ts +1 -0
  8. package/Models/DatabaseModels/Monitor.ts +1 -0
  9. package/Models/DatabaseModels/MonitorStatusTimeline.ts +1 -0
  10. package/Models/DatabaseModels/NetworkSite.ts +1 -0
  11. package/Models/DatabaseModels/NetworkSiteStatusTimeline.ts +1 -0
  12. package/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.ts +1 -0
  13. package/Models/DatabaseModels/OnCallDutyPolicySchedule.ts +2 -0
  14. package/Models/DatabaseModels/Probe.ts +13 -13
  15. package/Models/DatabaseModels/ScheduledMaintenance.ts +1 -0
  16. package/Models/DatabaseModels/ScheduledMaintenancePublicNote.ts +1 -0
  17. package/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.ts +1 -0
  18. package/Models/DatabaseModels/ScheduledMaintenanceTemplate.ts +1 -0
  19. package/Models/DatabaseModels/StatusPage.ts +1 -0
  20. package/Models/DatabaseModels/StatusPageAnnouncement.ts +1 -0
  21. package/Models/DatabaseModels/StatusPageDomain.ts +1 -0
  22. package/Models/DatabaseModels/WorkspaceNotificationSummary.ts +1 -0
  23. package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +11 -0
  24. package/Server/API/BaseAPI.ts +17 -1
  25. package/Server/API/CommonAPI.ts +32 -0
  26. package/Server/API/MicrosoftTeamsAPI.ts +99 -2
  27. package/Server/API/ProbeAPI.ts +2 -0
  28. package/Server/API/SlackAPI.ts +62 -0
  29. package/Server/API/StatusPageAPI.ts +157 -192
  30. package/Server/Infrastructure/Postgres/SchemaMigrations/1785140242697-AddHotQueryIndexes.ts +89 -0
  31. package/Server/Infrastructure/Postgres/SchemaMigrations/1785148065137-AddHotQueryIndexesSecondPass.ts +61 -0
  32. package/Server/Infrastructure/Postgres/SchemaMigrations/1785240000000-RepairCrossProjectMonitorStatusReferences.ts +280 -0
  33. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  34. package/Server/Services/DatabaseService.ts +11 -3
  35. package/Server/Services/MonitorGroupService.ts +145 -0
  36. package/Server/Services/MonitorService.ts +231 -30
  37. package/Server/Services/ProbeService.ts +319 -16
  38. package/Server/Services/StatusPageService.ts +72 -42
  39. package/Server/Services/WorkspaceNotificationRuleService.ts +232 -2
  40. package/Server/Utils/Database/PostgresErrorTranslator.ts +131 -0
  41. package/Server/Utils/Monitor/MonitorResource.ts +88 -35
  42. package/Server/Utils/Monitor/MonitorStatusTimeline.ts +21 -1
  43. package/Server/Utils/Monitor/MonitorStepsProjectValidator.ts +158 -0
  44. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +706 -6
  45. package/Tests/Models/DatabaseModels/ProbeColumnAccessControl.test.ts +103 -0
  46. package/Tests/Server/API/BaseAPIUpdateNoOp.test.ts +135 -0
  47. package/Tests/Server/API/CommonAPIAuthGuard.test.ts +317 -0
  48. package/Tests/Server/API/MicrosoftTeamsManifest.test.ts +228 -0
  49. package/Tests/Server/API/ProbeAPI.test.ts +6 -0
  50. package/Tests/Server/API/ZZVerifyChatsAuth.test.ts +167 -0
  51. package/Tests/Server/Services/AddHotQueryIndexesMigration.test.ts +255 -0
  52. package/Tests/Server/Services/AddHotQueryIndexesSecondPassMigration.test.ts +258 -0
  53. package/Tests/Server/Services/DatabaseServiceSanitizeUpdateData.test.ts +1 -1
  54. package/Tests/Server/Services/DiscoveryScanClaimHookFreeSafety.test.ts +121 -0
  55. package/Tests/Server/Services/HeartbeatWriteFastPathSafety.test.ts +200 -0
  56. package/Tests/Server/Services/HookFreeWriteSafetySecondPass.test.ts +181 -0
  57. package/Tests/Server/Services/MonitorGroupServiceBatchedStatuses.test.ts +513 -0
  58. package/Tests/Server/Services/MonitorResourceProbeAgreementReuse.test.ts +492 -0
  59. package/Tests/Server/Services/MonitorServiceProbeSelection.test.ts +401 -0
  60. package/Tests/Server/Services/MonitorStatusTimelineFastPath.test.ts +453 -0
  61. package/Tests/Server/Services/ProbeAuthKeyCacheAndHeartbeat.test.ts +679 -0
  62. package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +3 -3
  63. package/Tests/Server/Services/StatusPageDomainResolutionCache.test.ts +245 -0
  64. package/Tests/Server/Services/StatusPageOverviewTimelineWindow.test.ts +283 -0
  65. package/Tests/Server/Services/StatusPageServiceMcp.test.ts +17 -1
  66. package/Tests/Server/Services/StatusPageServiceMonitorGroupBatching.test.ts +331 -0
  67. package/Tests/Server/Services/UserOnCallLogClaimNotificationRule.test.ts +12 -12
  68. package/Tests/Server/Services/WorkspaceNotificationRuleChats.test.ts +1390 -0
  69. package/Tests/Server/Utils/AI/Insights/InsightFixRouting.test.ts +4 -4
  70. package/Tests/Server/Utils/AI/Insights/InsightScanner.test.ts +2 -2
  71. package/Tests/Server/Utils/AI/Insights/InsightStore.test.ts +1 -3
  72. package/Tests/Server/Utils/AI/Insights/InsightStoreHardening.test.ts +1 -3
  73. package/Tests/Server/Utils/AI/Insights/InsightTriage.test.ts +1 -1
  74. package/Tests/Server/Utils/AI/Insights/InsightTriageRunner.test.ts +5 -5
  75. package/Tests/Server/Utils/Alert/AlertPrivacyFilter.test.ts +187 -0
  76. package/Tests/Server/Utils/AlertEpisode/AlertEpisodePrivacyFilter.test.ts +170 -0
  77. package/Tests/Server/Utils/Database/PostgresErrorTranslator.test.ts +125 -0
  78. package/Tests/Server/Utils/Incident/IncidentPrivacyFilter.test.ts +264 -0
  79. package/Tests/Server/Utils/IncidentEpisode/IncidentEpisodePrivacyFilter.test.ts +175 -0
  80. package/Tests/Server/Utils/Monitor/MonitorStepsProjectValidator.test.ts +181 -0
  81. package/Tests/Server/Utils/Monitor/NetworkDeviceWalkUtil.test.ts +1 -1
  82. package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +2 -2
  83. package/Tests/Server/Utils/Workspace/MicrosoftTeamsChannelsList.test.ts +341 -0
  84. package/Tests/Server/Utils/Workspace/MicrosoftTeamsChats.test.ts +2175 -0
  85. package/Tests/Server/Utils/Workspace/SlackChannelsList.test.ts +585 -0
  86. package/Tests/Types/Billing/MeteredPlan.test.ts +46 -0
  87. package/Tests/Types/Workspace/NotificationRules/NotificationRuleCondition.test.ts +219 -1
  88. package/Tests/UI/Components/BulkUpdateForm.test.tsx +174 -0
  89. package/Tests/UI/Components/CardModelDetailEdit.test.tsx +315 -0
  90. package/Tests/UI/Components/ModelDetailSelect.test.tsx +229 -0
  91. package/Tests/UI/Components/ModelTable/BaseModelTableBulkSelectAll.test.tsx +1258 -0
  92. package/Tests/UI/Components/ModelTableExportFromColumns.test.ts +153 -0
  93. package/Tests/UI/Components/MonacoLoader.test.ts +67 -0
  94. package/Tests/UI/Components/MonacoRuntime.test.ts +121 -0
  95. package/Tests/UI/Components/TableBulkCsvExport.test.tsx +212 -3
  96. package/Tests/UI/Utils/Project.test.ts +809 -0
  97. package/Tests/UI/Utils/TableColumnsToCsv.test.ts +360 -0
  98. package/Tests/Utils/Alerts/AlertMetricType.test.ts +143 -0
  99. package/Tests/Utils/Dashboard/VariableUrlState.test.ts +219 -0
  100. package/Tests/Utils/Incident/IncidentMetricType.test.ts +174 -0
  101. package/Tests/Utils/Monitor/MonitorProbeSelectionUtil.test.ts +145 -0
  102. package/Tests/Utils/StatusPage/ResourceUptime.test.ts +263 -0
  103. package/Types/Workspace/NotificationRules/BaseNotificationRule.ts +4 -0
  104. package/Types/Workspace/NotificationRules/NotificationRuleCondition.ts +25 -10
  105. package/Types/Workspace/WorkspaceMessagePayload.ts +1 -0
  106. package/UI/Components/BulkUpdate/BulkUpdateForm.tsx +63 -8
  107. package/UI/Components/CodeEditor/CodeEditor.tsx +3 -0
  108. package/UI/Components/CodeEditor/MonacoLoader.ts +36 -0
  109. package/UI/Components/Forms/ModelForm.tsx +41 -18
  110. package/UI/Components/ModelDetail/CardModelDetail.tsx +23 -1
  111. package/UI/Components/ModelDetail/ModelDetail.tsx +46 -1
  112. package/UI/Components/ModelTable/BaseModelTable.tsx +300 -30
  113. package/UI/Components/ModelTable/Column.ts +12 -0
  114. package/UI/Components/ModelTable/ExportFromColumns.ts +57 -0
  115. package/UI/Components/Navbar/NavBar.tsx +66 -1
  116. package/UI/Components/Table/Table.tsx +52 -13
  117. package/UI/Components/Table/Types/Column.ts +18 -0
  118. package/UI/Utils/Project.ts +214 -18
  119. package/UI/Utils/TableColumnsToCsv.ts +86 -6
  120. package/UI/esbuild-config.js +34 -0
  121. package/Utils/Monitor/MonitorProbeSelectionUtil.ts +72 -0
  122. package/build/dist/Models/DatabaseModels/DashboardDomain.js +1 -0
  123. package/build/dist/Models/DatabaseModels/DashboardDomain.js.map +1 -1
  124. package/build/dist/Models/DatabaseModels/Incident.js +2 -0
  125. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  126. package/build/dist/Models/DatabaseModels/IncidentEpisode.js +1 -0
  127. package/build/dist/Models/DatabaseModels/IncidentEpisode.js.map +1 -1
  128. package/build/dist/Models/DatabaseModels/IncidentEpisodePublicNote.js +1 -0
  129. package/build/dist/Models/DatabaseModels/IncidentEpisodePublicNote.js.map +1 -1
  130. package/build/dist/Models/DatabaseModels/IncidentEpisodeStateTimeline.js +1 -0
  131. package/build/dist/Models/DatabaseModels/IncidentEpisodeStateTimeline.js.map +1 -1
  132. package/build/dist/Models/DatabaseModels/IncidentPublicNote.js +1 -0
  133. package/build/dist/Models/DatabaseModels/IncidentPublicNote.js.map +1 -1
  134. package/build/dist/Models/DatabaseModels/IncidentStateTimeline.js +1 -0
  135. package/build/dist/Models/DatabaseModels/IncidentStateTimeline.js.map +1 -1
  136. package/build/dist/Models/DatabaseModels/Monitor.js +1 -0
  137. package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
  138. package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js +1 -0
  139. package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js.map +1 -1
  140. package/build/dist/Models/DatabaseModels/NetworkSite.js +1 -0
  141. package/build/dist/Models/DatabaseModels/NetworkSite.js.map +1 -1
  142. package/build/dist/Models/DatabaseModels/NetworkSiteStatusTimeline.js +1 -0
  143. package/build/dist/Models/DatabaseModels/NetworkSiteStatusTimeline.js.map +1 -1
  144. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js +1 -0
  145. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js.map +1 -1
  146. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js +2 -0
  147. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js.map +1 -1
  148. package/build/dist/Models/DatabaseModels/Probe.js +13 -13
  149. package/build/dist/Models/DatabaseModels/Probe.js.map +1 -1
  150. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +1 -0
  151. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
  152. package/build/dist/Models/DatabaseModels/ScheduledMaintenancePublicNote.js +1 -0
  153. package/build/dist/Models/DatabaseModels/ScheduledMaintenancePublicNote.js.map +1 -1
  154. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.js +1 -0
  155. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.js.map +1 -1
  156. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js +1 -0
  157. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js.map +1 -1
  158. package/build/dist/Models/DatabaseModels/StatusPage.js +1 -0
  159. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  160. package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js +1 -0
  161. package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js.map +1 -1
  162. package/build/dist/Models/DatabaseModels/StatusPageDomain.js +1 -0
  163. package/build/dist/Models/DatabaseModels/StatusPageDomain.js.map +1 -1
  164. package/build/dist/Models/DatabaseModels/WorkspaceNotificationSummary.js +1 -0
  165. package/build/dist/Models/DatabaseModels/WorkspaceNotificationSummary.js.map +1 -1
  166. package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
  167. package/build/dist/Server/API/BaseAPI.js +12 -1
  168. package/build/dist/Server/API/BaseAPI.js.map +1 -1
  169. package/build/dist/Server/API/CommonAPI.js +21 -0
  170. package/build/dist/Server/API/CommonAPI.js.map +1 -1
  171. package/build/dist/Server/API/MicrosoftTeamsAPI.js +72 -2
  172. package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
  173. package/build/dist/Server/API/ProbeAPI.js +2 -0
  174. package/build/dist/Server/API/ProbeAPI.js.map +1 -1
  175. package/build/dist/Server/API/SlackAPI.js +44 -0
  176. package/build/dist/Server/API/SlackAPI.js.map +1 -1
  177. package/build/dist/Server/API/StatusPageAPI.js +98 -152
  178. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  179. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785140242697-AddHotQueryIndexes.js +36 -0
  180. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785140242697-AddHotQueryIndexes.js.map +1 -0
  181. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785148065137-AddHotQueryIndexesSecondPass.js +26 -0
  182. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785148065137-AddHotQueryIndexesSecondPass.js.map +1 -0
  183. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785240000000-RepairCrossProjectMonitorStatusReferences.js +254 -0
  184. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785240000000-RepairCrossProjectMonitorStatusReferences.js.map +1 -0
  185. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  186. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  187. package/build/dist/Server/Services/DatabaseService.js +10 -2
  188. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  189. package/build/dist/Server/Services/MonitorGroupService.js +118 -0
  190. package/build/dist/Server/Services/MonitorGroupService.js.map +1 -1
  191. package/build/dist/Server/Services/MonitorService.js +198 -24
  192. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  193. package/build/dist/Server/Services/ProbeService.js +238 -9
  194. package/build/dist/Server/Services/ProbeService.js.map +1 -1
  195. package/build/dist/Server/Services/StatusPageService.js +46 -39
  196. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  197. package/build/dist/Server/Services/WorkspaceNotificationRuleService.js +178 -3
  198. package/build/dist/Server/Services/WorkspaceNotificationRuleService.js.map +1 -1
  199. package/build/dist/Server/Utils/Database/PostgresErrorTranslator.js +80 -0
  200. package/build/dist/Server/Utils/Database/PostgresErrorTranslator.js.map +1 -0
  201. package/build/dist/Server/Utils/Monitor/MonitorResource.js +67 -29
  202. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  203. package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js +31 -14
  204. package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js.map +1 -1
  205. package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js +117 -0
  206. package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js.map +1 -0
  207. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +549 -7
  208. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  209. package/build/dist/Types/Workspace/NotificationRules/NotificationRuleCondition.js +17 -7
  210. package/build/dist/Types/Workspace/NotificationRules/NotificationRuleCondition.js.map +1 -1
  211. package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js +37 -11
  212. package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js.map +1 -1
  213. package/build/dist/UI/Components/CodeEditor/CodeEditor.js +2 -0
  214. package/build/dist/UI/Components/CodeEditor/CodeEditor.js.map +1 -1
  215. package/build/dist/UI/Components/CodeEditor/MonacoLoader.js +34 -0
  216. package/build/dist/UI/Components/CodeEditor/MonacoLoader.js.map +1 -0
  217. package/build/dist/UI/Components/Forms/ModelForm.js +37 -17
  218. package/build/dist/UI/Components/Forms/ModelForm.js.map +1 -1
  219. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js +22 -1
  220. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js.map +1 -1
  221. package/build/dist/UI/Components/ModelDetail/ModelDetail.js +31 -1
  222. package/build/dist/UI/Components/ModelDetail/ModelDetail.js.map +1 -1
  223. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +234 -21
  224. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  225. package/build/dist/UI/Components/ModelTable/ExportFromColumns.js +30 -0
  226. package/build/dist/UI/Components/ModelTable/ExportFromColumns.js.map +1 -0
  227. package/build/dist/UI/Components/Navbar/NavBar.js +39 -1
  228. package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
  229. package/build/dist/UI/Components/Table/Table.js +31 -11
  230. package/build/dist/UI/Components/Table/Table.js.map +1 -1
  231. package/build/dist/UI/Utils/Project.js +161 -15
  232. package/build/dist/UI/Utils/Project.js.map +1 -1
  233. package/build/dist/UI/Utils/TableColumnsToCsv.js +67 -6
  234. package/build/dist/UI/Utils/TableColumnsToCsv.js.map +1 -1
  235. package/build/dist/Utils/Monitor/MonitorProbeSelectionUtil.js +52 -0
  236. package/build/dist/Utils/Monitor/MonitorProbeSelectionUtil.js.map +1 -0
  237. package/package.json +4 -3
@@ -78,9 +78,19 @@ export interface ComponentProps<T extends GenericObject> {
78
78
  bulkSelectedItems?: Array<T> | undefined;
79
79
  onBulkSelectedItemAdded?: ((item: T) => void) | undefined;
80
80
  onBulkSelectedItemRemoved?: ((item: T) => void) | undefined;
81
- onBulkSelectAllItems?: (() => void) | undefined;
81
+ /*
82
+ * Resolves to whether every matching row really got selected. The table
83
+ * only claims "all items selected" when it did - otherwise a failed
84
+ * select-all would hide the Select All button while leaving just the
85
+ * current page selected, with no way to retry.
86
+ */
87
+ onBulkSelectAllItems?: (() => Promise<boolean>) | undefined;
82
88
  onBulkSelectItemsOnCurrentPage?: (() => void) | undefined;
83
89
  onBulkClearAllItems?: (() => void) | undefined;
90
+ bulkSelectionError?: string | undefined;
91
+ isBulkSelectAllLoading?: boolean | undefined;
92
+ isBulkSelectionTruncated?: boolean | undefined;
93
+ bulkSelectionTotalCount?: number | undefined;
84
94
  matchBulkSelectedItemByField?: keyof T | undefined; // which field to use to match selected items. For exmaple this could be '_id'
85
95
  onBulkActionEnd?: (() => void) | undefined;
86
96
  onBulkActionStart?: (() => void) | undefined;
@@ -156,6 +166,18 @@ const Table: TableFunction = <T extends GenericObject>(
156
166
  useEffect(() => {
157
167
  if (props.bulkSelectedItems) {
158
168
  setBulkSelectedItems(props.bulkSelectedItems);
169
+
170
+ /*
171
+ * "All items selected" is a claim about a selection that exists, so it
172
+ * cannot outlive one. The parent drops the selection whenever the query
173
+ * changes underneath it (and after a bulk action completes), and this
174
+ * state is local to the table - without this, the next single row the
175
+ * user ticked would re-open the bulk bar with the "Select All" button
176
+ * still hidden, as though everything matching were already selected.
177
+ */
178
+ if (props.bulkSelectedItems.length === 0) {
179
+ setIsAllItemsSelected(false);
180
+ }
159
181
  }
160
182
  }, [props.bulkSelectedItems]);
161
183
 
@@ -238,16 +260,20 @@ const Table: TableFunction = <T extends GenericObject>(
238
260
  if (props.matchBulkSelectedItemByField === undefined) {
239
261
  return;
240
262
  }
241
- const index: number = bulkSelectedItems.findIndex((x: T) => {
242
- return (
243
- x[props.matchBulkSelectedItemByField!]?.toString() ===
244
- item[props.matchBulkSelectedItemByField!]?.toString()
245
- );
246
- });
247
263
 
248
- if (index > -1) {
249
- bulkSelectedItems.splice(index, 1);
250
- }
264
+ /*
265
+ * Rebuild rather than splice: this array is the parent's state
266
+ * array by reference (see the sync effect above), so mutating it
267
+ * in place edits React state behind React's back.
268
+ */
269
+ setBulkSelectedItems(
270
+ bulkSelectedItems.filter((x: T) => {
271
+ return (
272
+ x[props.matchBulkSelectedItemByField!]?.toString() !==
273
+ item[props.matchBulkSelectedItemByField!]?.toString()
274
+ );
275
+ }),
276
+ );
251
277
 
252
278
  props.onBulkSelectedItemRemoved?.(item);
253
279
  }}
@@ -301,14 +327,27 @@ const Table: TableFunction = <T extends GenericObject>(
301
327
  props.onBulkClearAllItems?.();
302
328
  setIsAllItemsSelected(false);
303
329
  }}
304
- onSelectAllClick={() => {
305
- props.onBulkSelectAllItems?.();
306
- setIsAllItemsSelected(true);
330
+ onSelectAllClick={async () => {
331
+ const didSelectAllItems: boolean =
332
+ (await props.onBulkSelectAllItems?.()) ?? false;
333
+
334
+ /*
335
+ * Only on success. Otherwise the bulk bar would hide the Select
336
+ * All button and claim everything was selected while the
337
+ * selection is still just the current page.
338
+ */
339
+ if (didSelectAllItems) {
340
+ setIsAllItemsSelected(true);
341
+ }
307
342
  }}
308
343
  selectedItems={bulkSelectedItems}
309
344
  singularLabel={translatedSingularLabel}
310
345
  pluralLabel={translatedPluralLabel}
311
346
  isAllItemsSelected={isAllItemsSelected}
347
+ errorMessage={props.bulkSelectionError}
348
+ isSelectingAllItems={props.isBulkSelectAllLoading}
349
+ isSelectionTruncated={props.isBulkSelectionTruncated}
350
+ totalMatchingItemsCount={props.bulkSelectionTotalCount}
312
351
  onActionStart={props.onBulkActionStart}
313
352
  onActionEnd={() => {
314
353
  setIsAllItemsSelected(false);
@@ -15,6 +15,24 @@ export default interface Column<T extends GenericObject> {
15
15
  alignItem?: AlignItem | undefined;
16
16
  key?: keyof T | null; //can be null because actions column does not have a key.
17
17
  hideOnMobile?: boolean | undefined; // Hide column on mobile devices
18
+ /*
19
+ * Every field that backs this cell, in declaration order. A ModelTable
20
+ * column may declare more than one - e.g. the alert "Affected Resources"
21
+ * cell spans hosts / kubernetesClusters / dockerHosts / podmanHosts /
22
+ * services - while `key` only ever holds the first one, because that is the
23
+ * one used for sorting and for the default renderer. CSV export reads all
24
+ * of them so it does not silently drop the rest. Defaults to [key].
25
+ */
26
+ exportKeys?: Array<string> | undefined;
27
+ /*
28
+ * Exact text for this column's CSV cell, overriding the value read off the
29
+ * row. Use it when the rendered cell is built from something the row does
30
+ * not carry under the column's own field (data fetched alongside the table,
31
+ * a computed summary, several fields combined into one phrase).
32
+ */
33
+ getExportValue?: ((item: T) => string) | undefined;
34
+ // Leave this column out of the CSV export entirely.
35
+ disableCsvExport?: boolean | undefined;
18
36
  getElement?:
19
37
  | ((item: T, onBeforeFetchData?: T | undefined) => ReactElement)
20
38
  | undefined;
@@ -4,7 +4,7 @@ import BaseModel from "../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBas
4
4
  import SubscriptionPlan, {
5
5
  PlanType,
6
6
  } from "../../Types/Billing/SubscriptionPlan";
7
- import { JSONObject } from "../../Types/JSON";
7
+ import { JSONObject, JSONValue } from "../../Types/JSON";
8
8
  import ObjectID from "../../Types/ObjectID";
9
9
  import Project from "../../Models/DatabaseModels/Project";
10
10
  import SubscriptionStatus, {
@@ -14,29 +14,193 @@ import Navigation from "./Navigation";
14
14
  import SessionStorage from "./SessionStorage";
15
15
  import Telemetry from "./Telemetry/Telemetry";
16
16
 
17
+ /**
18
+ * Session storage key that pins a browser tab to the project it is showing.
19
+ * Session storage (and not local storage) so two tabs can show two different
20
+ * projects at the same time.
21
+ */
22
+ export const CURRENT_PROJECT_ID_STORAGE_KEY: string = "current_project_id";
23
+
24
+ /**
25
+ * Local storage key that remembers the last project the user opened on this
26
+ * browser. Unlike {@link CURRENT_PROJECT_ID_STORAGE_KEY} this survives closing
27
+ * the tab, which is what makes the dashboard reopen on the project the user
28
+ * left off on instead of always falling back to the first project.
29
+ */
30
+ export const LAST_ACCESSED_PROJECT_ID_STORAGE_KEY: string =
31
+ "last_accessed_project_id";
32
+
17
33
  export default class ProjectUtil {
34
+ /**
35
+ * The project the user is currently looking at, resolved in priority order:
36
+ *
37
+ * 1. `/dashboard/:projectId/...` in the URL - the page the user actually
38
+ * opened, so a deep link always wins over anything we have cached.
39
+ * 2. `current_project_id` in session storage - keeps this tab on the project
40
+ * it was already showing while the user moves around routes that do not
41
+ * carry a project id (e.g. `/dashboard`).
42
+ * 3. `last_accessed_project_id` in local storage - the project this user last
43
+ * opened on this browser, so closing the tab and coming back to
44
+ * `/dashboard` reopens that project rather than the first one.
45
+ */
18
46
  public static getCurrentProjectId(): ObjectID | null {
19
- // if this is not available in the url, check the session storage
20
- const currentProjectId: string | undefined = SessionStorage.getItem(
21
- `current_project_id`,
22
- ) as string;
47
+ const projectIdInUrl: ObjectID | null = this.getProjectIdFromUrl();
23
48
 
24
- if (currentProjectId && ObjectID.isValidUUID(currentProjectId)) {
25
- return new ObjectID(currentProjectId);
49
+ if (projectIdInUrl) {
50
+ return projectIdInUrl;
26
51
  }
27
52
 
28
- let projectId: string | undefined = Navigation.getFirstParam(2);
53
+ const projectIdInSession: ObjectID | null = this.toProjectId(
54
+ SessionStorage.getItem(CURRENT_PROJECT_ID_STORAGE_KEY),
55
+ );
29
56
 
30
- if (projectId && projectId.includes(":projectId")) {
31
- projectId = undefined;
57
+ if (projectIdInSession) {
58
+ return projectIdInSession;
32
59
  }
33
60
 
34
- // Only return the projectId if it's a valid UUID
35
- if (projectId && ObjectID.isValidUUID(projectId)) {
36
- return new ObjectID(projectId);
61
+ return this.getLastAccessedProjectId();
62
+ }
63
+
64
+ /**
65
+ * The project id in the current URL, if the user is on a project scoped route
66
+ * like `/dashboard/:projectId/...`. Returns null on routes that carry no
67
+ * project id (`/dashboard`, `/dashboard/welcome`, ...).
68
+ */
69
+ public static getProjectIdFromUrl(): ObjectID | null {
70
+ const projectId: string | undefined = Navigation.getFirstParam(2);
71
+
72
+ if (!projectId || projectId.includes(":projectId")) {
73
+ return null;
37
74
  }
38
75
 
39
- return null;
76
+ return this.toProjectId(projectId);
77
+ }
78
+
79
+ /**
80
+ * The last project this user opened on this browser. Persisted in local
81
+ * storage so it outlives the tab. It is cleared on logout (which clears all
82
+ * of local storage) and when the project is deleted.
83
+ */
84
+ public static getLastAccessedProjectId(): ObjectID | null {
85
+ return this.toProjectId(
86
+ LocalStorage.getItem(LAST_ACCESSED_PROJECT_ID_STORAGE_KEY),
87
+ );
88
+ }
89
+
90
+ public static setLastAccessedProjectId(
91
+ projectId: ObjectID | string | null | undefined,
92
+ ): void {
93
+ const id: ObjectID | null = this.toProjectId(projectId);
94
+
95
+ if (!id) {
96
+ // Never persist junk - an unusable value would only shadow the fallbacks.
97
+ this.clearLastAccessedProjectId();
98
+ return;
99
+ }
100
+
101
+ LocalStorage.setItem(LAST_ACCESSED_PROJECT_ID_STORAGE_KEY, id.toString());
102
+ }
103
+
104
+ public static clearLastAccessedProjectId(): void {
105
+ LocalStorage.removeItem(LAST_ACCESSED_PROJECT_ID_STORAGE_KEY);
106
+ }
107
+
108
+ /**
109
+ * Picks the project the dashboard should load out of the projects the user
110
+ * has access to.
111
+ *
112
+ * Candidates are tried in order - current project (URL / this tab), then the
113
+ * last project opened on this browser, then the first project the user has
114
+ * access to. A candidate that is not in `projects` is skipped, because the
115
+ * user may have left, lost access to, or deleted it since.
116
+ */
117
+ public static getProjectToSelect(data: {
118
+ projects: Array<Project>;
119
+ currentProjectId?: ObjectID | null | undefined;
120
+ lastAccessedProjectId?: ObjectID | null | undefined;
121
+ }): Project | null {
122
+ const projects: Array<Project> = this.getSelectableProjects(data.projects);
123
+
124
+ const candidateIds: Array<ObjectID | null | undefined> = [
125
+ data.currentProjectId,
126
+ data.lastAccessedProjectId,
127
+ ];
128
+
129
+ for (const candidateId of candidateIds) {
130
+ if (!candidateId) {
131
+ continue;
132
+ }
133
+
134
+ const project: Project | undefined = projects.find((project: Project) => {
135
+ return project._id?.toString() === candidateId.toString();
136
+ });
137
+
138
+ if (project) {
139
+ return project;
140
+ }
141
+ }
142
+
143
+ return projects[0] || null;
144
+ }
145
+
146
+ /**
147
+ * {@link getProjectToSelect} with the candidates read off storage / the URL.
148
+ */
149
+ public static getProjectToLoad(projects: Array<Project>): Project | null {
150
+ return this.getProjectToSelect({
151
+ projects: projects,
152
+ currentProjectId: this.getCurrentProjectId(),
153
+ lastAccessedProjectId: this.getLastAccessedProjectId(),
154
+ });
155
+ }
156
+
157
+ /**
158
+ * The project the dashboard should switch to now that the list of projects
159
+ * the user has access to has (re)loaded, or null when whatever is already
160
+ * selected should stand.
161
+ *
162
+ * A selection is left alone as long as the user still has access to it -
163
+ * otherwise a project the user just picked (or just created) would be
164
+ * overruled by whatever we happened to have remembered.
165
+ */
166
+ public static getProjectToSelectOnProjectsLoaded(data: {
167
+ projects: Array<Project>;
168
+ selectedProject?: Project | null | undefined;
169
+ }): Project | null {
170
+ const projects: Array<Project> = this.getSelectableProjects(data.projects);
171
+
172
+ if (projects.length === 0) {
173
+ // Nothing to select yet - the list is still loading, or the user has no projects.
174
+ return null;
175
+ }
176
+
177
+ const selectedProjectId: string | undefined =
178
+ data.selectedProject?._id?.toString();
179
+
180
+ const isSelectionStillAccessible: boolean = Boolean(
181
+ selectedProjectId &&
182
+ projects.some((project: Project) => {
183
+ return project._id?.toString() === selectedProjectId;
184
+ }),
185
+ );
186
+
187
+ if (isSelectionStillAccessible) {
188
+ return null;
189
+ }
190
+
191
+ return this.getProjectToLoad(projects);
192
+ }
193
+
194
+ /**
195
+ * The projects that can actually be opened - a project without an id cannot
196
+ * be navigated to (`/dashboard/undefined`), so it is never a candidate.
197
+ */
198
+ private static getSelectableProjects(
199
+ projects: Array<Project> | null | undefined,
200
+ ): Array<Project> {
201
+ return (projects || []).filter((project: Project) => {
202
+ return Boolean(project?._id);
203
+ });
40
204
  }
41
205
 
42
206
  public static setIsSubscriptionInactiveOrOverdue(data: {
@@ -91,12 +255,19 @@ export default class ProjectUtil {
91
255
  public static getCurrentProject(): Project | null {
92
256
  const currentProjectId: string | undefined =
93
257
  this.getCurrentProjectId()?.toString();
94
- if (!LocalStorage.getItem(`project_${currentProjectId}`)) {
258
+
259
+ if (!currentProjectId) {
95
260
  return null;
96
261
  }
262
+
97
263
  const projectJson: JSONObject = LocalStorage.getItem(
98
264
  `project_${currentProjectId}`,
99
265
  ) as JSONObject;
266
+
267
+ if (!projectJson) {
268
+ return null;
269
+ }
270
+
100
271
  return BaseModel.fromJSON(projectJson, Project) as Project;
101
272
  }
102
273
 
@@ -107,7 +278,10 @@ export default class ProjectUtil {
107
278
  project = BaseModel.toJSON(project, Project);
108
279
  }
109
280
  LocalStorage.setItem(`project_${currentProjectId}`, project);
110
- SessionStorage.setItem(`current_project_id`, currentProjectId);
281
+ SessionStorage.setItem(CURRENT_PROJECT_ID_STORAGE_KEY, currentProjectId);
282
+
283
+ // Remember this project for the next time the user opens the dashboard.
284
+ this.setLastAccessedProjectId(currentProjectId);
111
285
 
112
286
  // Keep RUM span context in sync with the project being viewed.
113
287
  if (currentProjectId) {
@@ -118,8 +292,18 @@ export default class ProjectUtil {
118
292
  public static clearCurrentProject(): void {
119
293
  const currentProjectId: string | undefined =
120
294
  this.getCurrentProjectId()?.toString();
121
- LocalStorage.setItem(`project_${currentProjectId}`, null);
122
- SessionStorage.setItem(`current_project_id`, null);
295
+
296
+ if (currentProjectId) {
297
+ LocalStorage.removeItem(`project_${currentProjectId}`);
298
+ }
299
+
300
+ SessionStorage.removeItem(CURRENT_PROJECT_ID_STORAGE_KEY);
301
+
302
+ /*
303
+ * The project is gone (or the user is leaving it), so it must not be
304
+ * reopened the next time the dashboard loads.
305
+ */
306
+ this.clearLastAccessedProjectId();
123
307
  }
124
308
 
125
309
  public static getCurrentPlan(): PlanType | null {
@@ -137,4 +321,16 @@ export default class ProjectUtil {
137
321
  getAllEnvVars(),
138
322
  );
139
323
  }
324
+
325
+ private static toProjectId(
326
+ value: JSONValue | ObjectID | string | null | undefined,
327
+ ): ObjectID | null {
328
+ const id: string | undefined = value?.toString();
329
+
330
+ if (id && ObjectID.isValidUUID(id)) {
331
+ return new ObjectID(id);
332
+ }
333
+
334
+ return null;
335
+ }
140
336
  }
@@ -262,9 +262,58 @@ export default class TableColumnsToCsv {
262
262
  return null;
263
263
  }
264
264
 
265
+ /*
266
+ * The fields a column contributes to the export. A ModelTable column can
267
+ * declare several (a cell that renders hosts + clusters + services in one
268
+ * place), and BaseModelTable forwards all of them as exportKeys; `key`
269
+ * alone is only the first. Falls back to the single key for tables that
270
+ * build their Table columns by hand.
271
+ */
272
+ public static getExportKeys<T extends GenericObject>(
273
+ column: Column<T>,
274
+ ): Array<string> {
275
+ if (column.exportKeys && column.exportKeys.length > 0) {
276
+ return column.exportKeys;
277
+ }
278
+
279
+ if (column.key === null || column.key === undefined) {
280
+ return [];
281
+ }
282
+
283
+ return [String(column.key)];
284
+ }
285
+
286
+ /*
287
+ * A column that renders entirely through getElement still has to declare a
288
+ * field so the row gets fetched at all, and the convention for that is
289
+ * `field: { _id: true }` - the "Owners" column on alerts, incidents,
290
+ * monitors and friends. The id is plumbing, not the cell's value, so
291
+ * exporting it writes a raw UUID under a header like "Owners". Treat such a
292
+ * column as presentational and leave it out, the same way a column with no
293
+ * key at all is left out. A column that genuinely shows the id says so with
294
+ * FieldType.ObjectID, and one that wants its rendered content in the file
295
+ * says so with getExportValue.
296
+ */
297
+ public static isPlaceholderIdColumn<T extends GenericObject>(
298
+ column: Column<T>,
299
+ ): boolean {
300
+ if (!column.getElement) {
301
+ return false;
302
+ }
303
+
304
+ if (column.type === FieldType.ObjectID) {
305
+ return false;
306
+ }
307
+
308
+ const keys: Array<string> = this.getExportKeys(column);
309
+
310
+ return keys.length === 1 && keys[0] === "_id";
311
+ }
312
+
265
313
  /*
266
314
  * Columns that can be exported: everything except the Actions column (which
267
- * only holds buttons) and columns without a data key (purely presentational).
315
+ * only holds buttons), columns that opted out, columns without a data key
316
+ * (purely presentational), and placeholder id columns.
268
317
  */
269
318
  public static getExportableColumns<T extends GenericObject>(
270
319
  columns: Columns<T>,
@@ -274,7 +323,20 @@ export default class TableColumnsToCsv {
274
323
  return false;
275
324
  }
276
325
 
277
- if (column.key === null || column.key === undefined) {
326
+ if (column.disableCsvExport) {
327
+ return false;
328
+ }
329
+
330
+ // An explicit export value makes even a keyless column exportable.
331
+ if (column.getExportValue) {
332
+ return true;
333
+ }
334
+
335
+ if (this.getExportKeys(column).length === 0) {
336
+ return false;
337
+ }
338
+
339
+ if (this.isPlaceholderIdColumn(column)) {
278
340
  return false;
279
341
  }
280
342
 
@@ -286,13 +348,31 @@ export default class TableColumnsToCsv {
286
348
  item: T,
287
349
  column: Column<T>,
288
350
  ): string {
289
- if (column.key === null || column.key === undefined) {
290
- return "";
351
+ if (column.getExportValue) {
352
+ return column.getExportValue(item) || "";
291
353
  }
292
354
 
293
- const rawValue: unknown = this.getRawValueByPath(item, String(column.key));
355
+ /*
356
+ * Every declared field goes into the one cell, joined the same way a
357
+ * multi-value field is, so a cell that renders several relations exports
358
+ * all of them. Blanks are dropped (only one of the relations is usually
359
+ * set) and repeats are collapsed (the same entity reachable through two
360
+ * of them should not be listed twice).
361
+ */
362
+ const values: Array<string> = [];
363
+
364
+ for (const key of this.getExportKeys(column)) {
365
+ const formatted: string = this.formatValueForCsv(
366
+ this.getRawValueByPath(item, key),
367
+ column.type,
368
+ );
369
+
370
+ if (formatted.length > 0 && !values.includes(formatted)) {
371
+ values.push(formatted);
372
+ }
373
+ }
294
374
 
295
- return this.formatValueForCsv(rawValue, column.type);
375
+ return values.join("; ");
296
376
  }
297
377
 
298
378
  /*
@@ -182,6 +182,25 @@ function createFileLoaderPlugin() {
182
182
  };
183
183
  }
184
184
 
185
+ // Copy Monaco's runtime next to the bundle so the editor loads from this
186
+ // install instead of cdn.jsdelivr.net, which is unreachable when self-hosted
187
+ // offline. Only min/vs is copied - the rest of the package is sources and
188
+ // type definitions the browser never asks for.
189
+ //
190
+ // Every frontend gets a copy. Forms/Fields/FormField.tsx imports CodeEditor
191
+ // directly, so every service that renders a form pulls the editor into its
192
+ // bundle - there is no service to skip. Gating this on the built output only
193
+ // looked selective; it matched all five services and risked a silent offline
194
+ // 404 the moment a code field showed up somewhere unexpected.
195
+ function copyMonacoAssets(outdir) {
196
+ const source = path.join(resolvePackageRoot("monaco-editor"), "min", "vs");
197
+ const destination = path.resolve(path.dirname(outdir), "assets/monaco/vs");
198
+
199
+ fs.rmSync(destination, { recursive: true, force: true });
200
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
201
+ fs.cpSync(source, destination, { recursive: true });
202
+ }
203
+
185
204
  // Read environment variables from .env file
186
205
  function readEnvFile(pathToFile) {
187
206
  if (!fs.existsSync(pathToFile)) {
@@ -265,6 +284,11 @@ function createConfig(options) {
265
284
  splitting: true,
266
285
  publicPath,
267
286
  define: {
287
+ // Monaco resolves its runtime against this at load time. import.meta is
288
+ // empty at the es2017 target, so the path has to come from the build.
289
+ "process.env.MONACO_ASSET_PATH": JSON.stringify(
290
+ `${publicPath.replace(/dist\/$/, "")}assets/monaco/vs`,
291
+ ),
268
292
  "process.env.NODE_ENV": JSON.stringify(
269
293
  isDev ? "development" : "production",
270
294
  ),
@@ -319,6 +343,9 @@ async function build(config, serviceName) {
319
343
  try {
320
344
  const result = await esbuild.build(config);
321
345
 
346
+ copyMonacoAssets(config.outdir);
347
+ console.log(`šŸ“¦ Copied Monaco assets for ${serviceName}`);
348
+
322
349
  if (isAnalyze && result.metafile) {
323
350
  const analyzeText = await esbuild.analyzeMetafile(result.metafile);
324
351
  console.log(`\nšŸ“Š Bundle analysis for ${serviceName}:`);
@@ -345,6 +372,13 @@ async function build(config, serviceName) {
345
372
  async function watch(config, serviceName) {
346
373
  try {
347
374
  const context = await esbuild.context(config);
375
+
376
+ // The copy no longer reads the built output, so it can run before the
377
+ // first build instead of forcing an extra one just to have something to
378
+ // inspect. context.watch() does the initial build on its own.
379
+ copyMonacoAssets(config.outdir);
380
+ console.log(`šŸ“¦ Copied Monaco assets for ${serviceName}`);
381
+
348
382
  await context.watch();
349
383
  console.log(`šŸ‘€ Watching ${serviceName} for changes...`);
350
384
  } catch (error) {
@@ -0,0 +1,72 @@
1
+ /*
2
+ * Which probes a brand-new monitor starts out with.
3
+ *
4
+ * The server applies this same rule in
5
+ * MonitorService.addDefaultProbesToMonitor (as a database query rather than a
6
+ * filter over a list). The create form uses it to pre-select the probe picker,
7
+ * so what the user sees before pressing "Create Monitor" is what they would
8
+ * have got anyway - and can now change.
9
+ */
10
+
11
+ export interface ProbeSelectionCandidate {
12
+ id: string;
13
+ isGlobalProbe: boolean;
14
+ shouldAutoEnableProbeOnNewMonitors: boolean;
15
+ }
16
+
17
+ export default class MonitorProbeSelectionUtil {
18
+ /*
19
+ * A probe is auto-enabled on new monitors when it opts in with
20
+ * shouldAutoEnableProbeOnNewMonitors - except that global probes are skipped
21
+ * entirely for projects that turned off "add global probes by default".
22
+ */
23
+ public static getDefaultSelectedProbeIds(data: {
24
+ probes: Array<ProbeSelectionCandidate>;
25
+ doNotAddGlobalProbesByDefaultOnNewMonitors?: boolean | undefined;
26
+ }): Array<string> {
27
+ const skipGlobalProbes: boolean =
28
+ data.doNotAddGlobalProbesByDefaultOnNewMonitors === true;
29
+
30
+ const selected: Array<string> = [];
31
+
32
+ for (const probe of data.probes || []) {
33
+ if (!probe || !probe.id) {
34
+ continue;
35
+ }
36
+
37
+ if (!probe.shouldAutoEnableProbeOnNewMonitors) {
38
+ continue;
39
+ }
40
+
41
+ if (probe.isGlobalProbe && skipGlobalProbes) {
42
+ continue;
43
+ }
44
+
45
+ selected.push(probe.id);
46
+ }
47
+
48
+ return MonitorProbeSelectionUtil.dedupeProbeIds(selected);
49
+ }
50
+
51
+ /*
52
+ * Probe ids reach the server as plain strings and the same probe can be
53
+ * offered by more than one source, so duplicates have to go: MonitorProbe
54
+ * has no unique index and MonitorProbeService rejects the second insert with
55
+ * "Probe is already added to this monitor."
56
+ */
57
+ public static dedupeProbeIds(probeIds: Array<string>): Array<string> {
58
+ const seen: Set<string> = new Set<string>();
59
+ const deduped: Array<string> = [];
60
+
61
+ for (const probeId of probeIds || []) {
62
+ if (!probeId || seen.has(probeId)) {
63
+ continue;
64
+ }
65
+
66
+ seen.add(probeId);
67
+ deduped.push(probeId);
68
+ }
69
+
70
+ return deduped;
71
+ }
72
+ }
@@ -351,6 +351,7 @@ __decorate([
351
351
  description: "Full domain of your dashboard (like dashboard.acmeinc.com). This is autogenerated and is derived from subdomain and domain.",
352
352
  example: "dashboard.acmeinc.com",
353
353
  }),
354
+ Index(),
354
355
  Column({
355
356
  nullable: false,
356
357
  type: ColumnType.ShortText,