@oneuptime/common 12.0.34 → 13.0.0

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 (259) hide show
  1. package/Models/DatabaseModels/GlobalConfig.ts +12 -12
  2. package/Models/DatabaseModels/InventoryItem.ts +44 -1
  3. package/Models/DatabaseModels/NetworkDevice.ts +126 -0
  4. package/Server/EnvironmentConfig.ts +47 -16
  5. package/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.ts +25 -0
  6. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  7. package/Server/Infrastructure/Redis.ts +8 -8
  8. package/Server/Infrastructure/Status.ts +10 -4
  9. package/Server/Services/DatabaseService.ts +65 -13
  10. package/Server/Services/InventoryItemService.ts +31 -1
  11. package/Server/Services/MonitorTemplateService.ts +140 -19
  12. package/Server/Services/NetworkDeviceAutoImportRuleEngineService.ts +349 -41
  13. package/Server/Services/NetworkDeviceDiscoveryScanService.ts +87 -2
  14. package/Server/Services/NetworkDeviceService.ts +69 -0
  15. package/Server/Services/UserNotificationRuleService.ts +76 -25
  16. package/Server/Types/Database/Permissions/ReadPermission.ts +45 -5
  17. package/Server/Types/Database/QueryUtil.ts +56 -0
  18. package/Server/Types/Markdown.ts +93 -1
  19. package/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.ts +82 -17
  20. package/Server/Utils/EmailRollup/EmailRollupFlushRunner.ts +10 -1
  21. package/Server/Utils/EmailRollup/EmailRollupRenderer.ts +2 -2
  22. package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +82 -35
  23. package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +31 -5
  24. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +202 -37
  25. package/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.ts +82 -9
  26. package/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.ts +16 -7
  27. package/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.ts +29 -17
  28. package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +107 -48
  29. package/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.ts +158 -0
  30. package/Server/Utils/Monitor/NetworkInventoryUtil.ts +27 -0
  31. package/Server/Utils/Telemetry/EntityRegistry.ts +11 -1
  32. package/Server/Utils/Telemetry/TelemetryEntity.ts +83 -9
  33. package/Tests/App/Dashboard/AddNeighborToMonitoringModal.test.tsx +32 -0
  34. package/Tests/App/Dashboard/AdoptedDeviceDrawer.test.tsx +374 -0
  35. package/Tests/App/Dashboard/DashboardLabelVariables.test.tsx +580 -0
  36. package/Tests/App/Dashboard/DashboardMonitorLabelVariable.test.tsx +438 -0
  37. package/Tests/App/Dashboard/DashboardVariableAllContract.test.tsx +5 -2
  38. package/Tests/App/Dashboard/DeviceAttachmentCard.test.tsx +273 -0
  39. package/Tests/App/Dashboard/DiscoveryReviewInventoryRefresh.test.tsx +505 -0
  40. package/Tests/App/Dashboard/EntityDetailPanel.test.tsx +163 -0
  41. package/Tests/App/Dashboard/InfrastructureExplorer.test.tsx +401 -0
  42. package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +232 -0
  43. package/Tests/App/Dashboard/InventoryItems.test.tsx +201 -0
  44. package/Tests/App/Dashboard/InventoryTypeAndStatusFacets.test.tsx +798 -0
  45. package/Tests/App/Dashboard/NetworkTopologyLiveView.test.tsx +320 -0
  46. package/Tests/App/Dashboard/NetworkTopologyToolbar.test.tsx +275 -0
  47. package/Tests/App/Dashboard/OnCallCalendarFeedEmptyState.test.tsx +975 -0
  48. package/Tests/App/Dashboard/OnCallCalendarFeedPlanGate.test.tsx +63 -0
  49. package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +626 -0
  50. package/Tests/App/Dashboard/TopologyDataLoading.test.tsx +348 -0
  51. package/Tests/App/Dashboard/TopologyPageNavigation.test.tsx +331 -0
  52. package/Tests/App/Dashboard/UserSettingsEmailPreferences.test.tsx +603 -0
  53. package/Tests/App/Dashboard/UserSettingsNotificationSettings.test.tsx +324 -0
  54. package/Tests/App/PublicDashboard/DashboardVariableSelector.test.tsx +13 -4
  55. package/Tests/App/StatusPage/StatusPageOidcOrigin.test.tsx +223 -0
  56. package/Tests/Models/InventoryItemStatus.test.ts +147 -0
  57. package/Tests/Models/NetworkDeviceMacAddressColumn.test.ts +294 -0
  58. package/Tests/Server/API/DashboardPublicResourceListAPI.test.ts +129 -0
  59. package/Tests/Server/EnvironmentConfigValkey.test.ts +227 -0
  60. package/Tests/Server/Infrastructure/Postgres/AddMacAddressToNetworkDeviceMigration.test.ts +365 -0
  61. package/Tests/Server/Infrastructure/Postgres/CustomFieldValueMappingMigration.test.ts +62 -23
  62. package/Tests/Server/Infrastructure/ValkeyDeploymentConfig.test.ts +265 -0
  63. package/Tests/Server/Middleware/HttpMetricsMiddleware.test.ts +382 -0
  64. package/Tests/Server/Middleware/SCIMAuthorization.test.ts +525 -0
  65. package/Tests/Server/Services/DatabaseServiceAggregateBy.test.ts +291 -56
  66. package/Tests/Server/Services/DatabaseServiceSortTiebreaker.test.ts +151 -0
  67. package/Tests/Server/Services/InventoryItemManualCreate.test.ts +105 -0
  68. package/Tests/Server/Services/InventoryItemPromotionGate.test.ts +178 -0
  69. package/Tests/Server/Services/MonitorTemplateServiceCustomFieldSync.test.ts +502 -0
  70. package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +1133 -81
  71. package/Tests/Server/Services/NetworkDeviceDiscoveryScanRegistration.test.ts +520 -0
  72. package/Tests/Server/Services/NetworkDeviceMacAddressNormalization.test.ts +572 -0
  73. package/Tests/Server/Types/Database/InventoryStatusPostgres.test.ts +368 -0
  74. package/Tests/Server/Types/Database/Permissions/ReadBlockPermission.test.ts +261 -14
  75. package/Tests/Server/Types/Database/QueryUtilIncludesAnyOfGroups.test.ts +169 -0
  76. package/Tests/Server/Types/Database/QueryUtilIncludesAnyOfGroupsPostgres.test.ts +297 -0
  77. package/Tests/Server/Types/Markdown.test.ts +194 -1
  78. package/Tests/Server/Types/Workflow/Components/IncomingWebhookUtils.test.ts +279 -0
  79. package/Tests/Server/Types/Workflow/Components/TextToJson.test.ts +212 -0
  80. package/Tests/Server/Utils/CustomField/CustomFieldMappingRegistry.test.ts +436 -0
  81. package/Tests/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.test.ts +214 -0
  82. package/Tests/Server/Utils/EmailRollup/EmailRollupFlushRunnerBehaviour.test.ts +1 -1
  83. package/Tests/Server/Utils/EmailRollup/EmailRollupRenderer.test.ts +10 -3
  84. package/Tests/Server/Utils/EventLoop.test.ts +359 -0
  85. package/Tests/Server/Utils/JsonWebToken.test.ts +30 -0
  86. package/Tests/Server/Utils/Monitor/Criteria/CompareCriteria.test.ts +242 -0
  87. package/Tests/Server/Utils/Monitor/Criteria/CompareCriteriaAggregation.test.ts +13 -7
  88. package/Tests/Server/Utils/Monitor/Criteria/DatabaseMonitorCriteria.test.ts +29 -0
  89. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.ts +30 -6
  90. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaAnomalyUnits.test.ts +246 -0
  91. package/Tests/Server/Utils/Monitor/MonitorCriteriaEmailEvalLogAgreement.test.ts +280 -0
  92. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +224 -10
  93. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorMetricUnits.test.ts +570 -0
  94. package/Tests/Server/Utils/Monitor/MonitorCriteriaExpectationBuilderUnits.test.ts +30 -5
  95. package/Tests/Server/Utils/Monitor/MonitorCriteriaMessageBuilderUnits.test.ts +68 -21
  96. package/Tests/Server/Utils/Monitor/MonitorCriteriaObservationBuilderUnits.test.ts +36 -19
  97. package/Tests/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.test.ts +573 -0
  98. package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +317 -0
  99. package/Tests/Server/Utils/Telemetry/ContextSpanProcessor.test.ts +281 -0
  100. package/Tests/Server/Utils/Telemetry/InventoryDuplicateIdentity.test.ts +316 -0
  101. package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +111 -8
  102. package/Tests/Types/Database/IncludesAnyOfGroups.test.ts +106 -0
  103. package/Tests/Types/JSONFunctions.test.ts +40 -0
  104. package/Tests/Types/NetworkAutomation/AutoImportRunMerge.test.ts +235 -0
  105. package/Tests/Types/NetworkAutomation/RuleRunResult.test.ts +66 -0
  106. package/Tests/Types/NetworkAutomation/RuleRunResultDescribe.test.ts +309 -1
  107. package/Tests/Types/Telemetry/InventoryLiveness.test.ts +99 -0
  108. package/Tests/UI/Components/CustomFields/CustomFieldsDetail.test.tsx +160 -0
  109. package/Tests/UI/ReactRouterSingletonBuild.test.ts +133 -0
  110. package/Tests/UI/Rum/ReplayHeader.test.tsx +10 -2
  111. package/Tests/UI/Rum/ReplayPlayerChrome.test.tsx +514 -0
  112. package/Tests/UI/Rum/ReplayTimeFormat.test.ts +94 -0
  113. package/Tests/UI/Rum/ReplayUi.test.tsx +559 -0
  114. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +5 -0
  115. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +1 -0
  116. package/Tests/Utils/Dashboard/Components/DashboardMonitorListComponent.test.ts +11 -1
  117. package/Tests/Utils/Dashboard/LabelVariable.test.ts +257 -0
  118. package/Tests/Utils/Dashboard/VariableUrlState.test.ts +144 -14
  119. package/Tests/Utils/MetricUnitUtil.test.ts +81 -0
  120. package/Tests/Utils/Monitor/DeviceMacLearningUtil.test.ts +497 -0
  121. package/Tests/Utils/Monitor/MetricValueFormatter.test.ts +546 -0
  122. package/Tests/Utils/Monitor/MonitorTemplateCustomFieldUtil.test.ts +218 -0
  123. package/Tests/Utils/Monitor/NetworkTopologyEndpointAdoption.test.ts +1755 -0
  124. package/Tests/Utils/NetworkAutomation/AutoImportRunChain.test.ts +320 -0
  125. package/Types/BaseDatabase/IncludesAnyOfGroups.ts +77 -0
  126. package/Types/Dashboard/DashboardComponents/ComponentArgument.ts +1 -0
  127. package/Types/Dashboard/DashboardComponents/DashboardMonitorListComponent.ts +1 -0
  128. package/Types/Dashboard/DashboardVariable.ts +8 -0
  129. package/Types/JSON.ts +4 -0
  130. package/Types/JSONFunctions.ts +16 -0
  131. package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +13 -0
  132. package/Types/NetworkAutomation/RuleRunResult.ts +402 -13
  133. package/Types/SerializableObjectDictionary.ts +4 -0
  134. package/Types/Telemetry/InventoryLiveness.ts +56 -0
  135. package/UI/Components/CSVFileUpload/CSVFileUpload.tsx +9 -2
  136. package/UI/Components/CustomFields/CustomFieldsDetail.tsx +46 -21
  137. package/UI/Components/Dashboard/DashboardVariableControl.tsx +285 -0
  138. package/UI/esbuild-config.js +6 -0
  139. package/Utils/Dashboard/Components/DashboardMonitorListComponent.ts +12 -1
  140. package/Utils/Dashboard/LabelVariable.ts +152 -0
  141. package/Utils/Dashboard/VariableUrlState.ts +27 -5
  142. package/Utils/MetricUnitUtil.ts +34 -0
  143. package/Utils/Monitor/DeviceMacLearningUtil.ts +177 -0
  144. package/Utils/Monitor/MetricValueFormatter.ts +342 -0
  145. package/Utils/Monitor/MonitorTemplateCustomFieldUtil.ts +124 -0
  146. package/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.ts +7 -2
  147. package/Utils/Monitor/NetworkTopologyUtil.ts +693 -0
  148. package/Utils/NetworkAutomation/AutoImportRunChain.ts +138 -0
  149. package/build/dist/Models/DatabaseModels/GlobalConfig.js +12 -12
  150. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  151. package/build/dist/Models/DatabaseModels/InventoryItem.js +34 -1
  152. package/build/dist/Models/DatabaseModels/InventoryItem.js.map +1 -1
  153. package/build/dist/Models/DatabaseModels/NetworkDevice.js +128 -0
  154. package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
  155. package/build/dist/Server/EnvironmentConfig.js +44 -12
  156. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  157. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.js +14 -0
  158. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.js.map +1 -0
  159. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  160. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  161. package/build/dist/Server/Infrastructure/Redis.js +8 -8
  162. package/build/dist/Server/Infrastructure/Redis.js.map +1 -1
  163. package/build/dist/Server/Infrastructure/Status.js +10 -4
  164. package/build/dist/Server/Infrastructure/Status.js.map +1 -1
  165. package/build/dist/Server/Services/DatabaseService.js +58 -13
  166. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  167. package/build/dist/Server/Services/InventoryItemService.js +28 -1
  168. package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
  169. package/build/dist/Server/Services/MonitorTemplateService.js +116 -17
  170. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  171. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js +270 -37
  172. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js.map +1 -1
  173. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js +65 -0
  174. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js.map +1 -1
  175. package/build/dist/Server/Services/NetworkDeviceService.js +54 -0
  176. package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
  177. package/build/dist/Server/Services/UserNotificationRuleService.js +60 -25
  178. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  179. package/build/dist/Server/Types/Database/Permissions/ReadPermission.js +29 -4
  180. package/build/dist/Server/Types/Database/Permissions/ReadPermission.js.map +1 -1
  181. package/build/dist/Server/Types/Database/QueryUtil.js +36 -0
  182. package/build/dist/Server/Types/Database/QueryUtil.js.map +1 -1
  183. package/build/dist/Server/Types/Markdown.js +75 -0
  184. package/build/dist/Server/Types/Markdown.js.map +1 -1
  185. package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js +53 -18
  186. package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js.map +1 -1
  187. package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js +10 -1
  188. package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js.map +1 -1
  189. package/build/dist/Server/Utils/EmailRollup/EmailRollupRenderer.js +2 -2
  190. package/build/dist/Server/Utils/EmailRollup/EmailRollupRenderer.js.map +1 -1
  191. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +58 -35
  192. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
  193. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +29 -5
  194. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
  195. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +166 -35
  196. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  197. package/build/dist/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.js +58 -9
  198. package/build/dist/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.js.map +1 -1
  199. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.js +9 -3
  200. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.js.map +1 -1
  201. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.js +26 -12
  202. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.js.map +1 -1
  203. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +74 -33
  204. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -1
  205. package/build/dist/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.js +114 -0
  206. package/build/dist/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.js.map +1 -0
  207. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js +25 -0
  208. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js.map +1 -1
  209. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +9 -1
  210. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
  211. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +70 -13
  212. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
  213. package/build/dist/Types/BaseDatabase/IncludesAnyOfGroups.js +54 -0
  214. package/build/dist/Types/BaseDatabase/IncludesAnyOfGroups.js.map +1 -0
  215. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js +1 -0
  216. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js.map +1 -1
  217. package/build/dist/Types/Dashboard/DashboardVariable.js +1 -0
  218. package/build/dist/Types/Dashboard/DashboardVariable.js.map +1 -1
  219. package/build/dist/Types/JSON.js +1 -0
  220. package/build/dist/Types/JSON.js.map +1 -1
  221. package/build/dist/Types/JSONFunctions.js +13 -0
  222. package/build/dist/Types/JSONFunctions.js.map +1 -1
  223. package/build/dist/Types/NetworkAutomation/RuleRunResult.js +291 -6
  224. package/build/dist/Types/NetworkAutomation/RuleRunResult.js.map +1 -1
  225. package/build/dist/Types/SerializableObjectDictionary.js +4 -0
  226. package/build/dist/Types/SerializableObjectDictionary.js.map +1 -1
  227. package/build/dist/Types/Telemetry/InventoryLiveness.js +37 -0
  228. package/build/dist/Types/Telemetry/InventoryLiveness.js.map +1 -0
  229. package/build/dist/UI/Components/CSVFileUpload/CSVFileUpload.js +7 -0
  230. package/build/dist/UI/Components/CSVFileUpload/CSVFileUpload.js.map +1 -1
  231. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +9 -2
  232. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
  233. package/build/dist/UI/Components/Dashboard/DashboardVariableControl.js +137 -0
  234. package/build/dist/UI/Components/Dashboard/DashboardVariableControl.js.map +1 -0
  235. package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js +9 -1
  236. package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js.map +1 -1
  237. package/build/dist/Utils/Dashboard/LabelVariable.js +96 -0
  238. package/build/dist/Utils/Dashboard/LabelVariable.js.map +1 -0
  239. package/build/dist/Utils/Dashboard/VariableUrlState.js +19 -4
  240. package/build/dist/Utils/Dashboard/VariableUrlState.js.map +1 -1
  241. package/build/dist/Utils/MetricUnitUtil.js +26 -0
  242. package/build/dist/Utils/MetricUnitUtil.js.map +1 -1
  243. package/build/dist/Utils/Monitor/DeviceMacLearningUtil.js +112 -0
  244. package/build/dist/Utils/Monitor/DeviceMacLearningUtil.js.map +1 -0
  245. package/build/dist/Utils/Monitor/MetricValueFormatter.js +276 -0
  246. package/build/dist/Utils/Monitor/MetricValueFormatter.js.map +1 -0
  247. package/build/dist/Utils/Monitor/MonitorTemplateCustomFieldUtil.js +101 -0
  248. package/build/dist/Utils/Monitor/MonitorTemplateCustomFieldUtil.js.map +1 -0
  249. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js +7 -2
  250. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js.map +1 -1
  251. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +484 -0
  252. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
  253. package/build/dist/Utils/NetworkAutomation/AutoImportRunChain.js +59 -0
  254. package/build/dist/Utils/NetworkAutomation/AutoImportRunChain.js.map +1 -0
  255. package/jest.config.json +2 -0
  256. package/package.json +1 -1
  257. package/test-setup.sh +9 -9
  258. package/tsconfig.json +51 -11
  259. package/Tests/App/Dashboard/NotificationEmailPreferences.test.tsx +0 -399
@@ -0,0 +1,436 @@
1
+ import { describe, expect, test } from "@jest/globals";
2
+ import CustomFieldMappingSourceResource from "../../../../Types/CustomField/CustomFieldMappingSourceResource";
3
+ import { CustomFieldMappingSourceInfo } from "../../../../Types/CustomField/CustomFieldMappingCatalog";
4
+ import { JSONObject } from "../../../../Types/JSON";
5
+ import ObjectID from "../../../../Types/ObjectID";
6
+ import Monitor from "../../../../Models/DatabaseModels/Monitor";
7
+ import AlertCustomField from "../../../../Models/DatabaseModels/AlertCustomField";
8
+ import IncidentCustomField from "../../../../Models/DatabaseModels/IncidentCustomField";
9
+ import ScheduledMaintenanceCustomField from "../../../../Models/DatabaseModels/ScheduledMaintenanceCustomField";
10
+ import {
11
+ CustomFieldMappingSourceEntry,
12
+ CustomFieldMappingTargetEntry,
13
+ getCustomFieldMappingTarget,
14
+ getCustomFieldMappingTargets,
15
+ getCustomFieldMappingTargetsForSource,
16
+ } from "../../../../Server/Utils/CustomField/CustomFieldMappingRegistry";
17
+
18
+ /*
19
+ * The registry is the server half of the custom-field mapping catalog, and
20
+ * three separate behaviours in it are load bearing in ways nothing else
21
+ * checks:
22
+ *
23
+ * - it is MEMOISED, and CustomFieldMappingService.propagateFromSourceRecord
24
+ * matches source entries by REFERENCE across two accessor calls. Handing
25
+ * out fresh objects would leave that filter matching nothing, and field
26
+ * propagation would stop working with no error anywhere.
27
+ *
28
+ * - readSourceIdsFromPayload distinguishes null ("the payload says nothing
29
+ * about the relation") from [] ("the relation was cleared"). Collapsing
30
+ * the two would make every unrelated update look like a detach.
31
+ *
32
+ * - readHydratedSourcesFromPayload must refuse an id STUB. A monitor read
33
+ * without its customFields column is not a monitor whose bag is empty;
34
+ * treating it as empty skips the lookup and silently inherits nothing.
35
+ */
36
+
37
+ const ALERT_TABLE: string = new AlertCustomField().tableName!;
38
+ const INCIDENT_TABLE: string = new IncidentCustomField().tableName!;
39
+ const SCHEDULED_MAINTENANCE_TABLE: string =
40
+ new ScheduledMaintenanceCustomField().tableName!;
41
+
42
+ const MONITOR_ID: ObjectID = new ObjectID(
43
+ "22222222-2222-4222-8222-222222222222",
44
+ );
45
+
46
+ type GetSourceFunction = (
47
+ definitionTableName: string,
48
+ ) => CustomFieldMappingSourceEntry;
49
+
50
+ const getSource: GetSourceFunction = (
51
+ definitionTableName: string,
52
+ ): CustomFieldMappingSourceEntry => {
53
+ const target: CustomFieldMappingTargetEntry | undefined =
54
+ getCustomFieldMappingTarget(definitionTableName);
55
+
56
+ if (!target || !target.sources[0]) {
57
+ throw new Error(`No mapping source registered for ${definitionTableName}`);
58
+ }
59
+
60
+ return target.sources[0];
61
+ };
62
+
63
+ type BuildMonitorFunction = (data: {
64
+ id: ObjectID;
65
+ customFields?: JSONObject | null | undefined;
66
+ hydrated?: boolean;
67
+ }) => Monitor;
68
+
69
+ const buildMonitor: BuildMonitorFunction = (data: {
70
+ id: ObjectID;
71
+ customFields?: JSONObject | null | undefined;
72
+ hydrated?: boolean;
73
+ }): Monitor => {
74
+ const monitor: Monitor = new Monitor();
75
+ monitor.id = data.id;
76
+
77
+ if (data.hydrated !== false) {
78
+ monitor.customFields = (data.customFields ?? null) as JSONObject;
79
+ }
80
+
81
+ return monitor;
82
+ };
83
+
84
+ describe("the registry mirrors the catalog, and only the catalog", () => {
85
+ test("registers exactly the three targets the catalog lists", () => {
86
+ const names: Array<string> = getCustomFieldMappingTargets().map(
87
+ (target: CustomFieldMappingTargetEntry) => {
88
+ return target.targetName;
89
+ },
90
+ );
91
+
92
+ expect(names).toEqual(["Alert", "Incident", "Scheduled Maintenance"]);
93
+ });
94
+
95
+ test("keys every target by its definition table name", () => {
96
+ expect(getCustomFieldMappingTarget(ALERT_TABLE)?.targetName).toBe("Alert");
97
+ expect(getCustomFieldMappingTarget(INCIDENT_TABLE)?.targetName).toBe(
98
+ "Incident",
99
+ );
100
+ expect(
101
+ getCustomFieldMappingTarget(SCHEDULED_MAINTENANCE_TABLE)?.targetName,
102
+ ).toBe("Scheduled Maintenance");
103
+ });
104
+
105
+ test("returns undefined for a definition table with no mapping", () => {
106
+ expect(getCustomFieldMappingTarget("TeamCustomField")).toBeUndefined();
107
+ });
108
+
109
+ test("returns undefined rather than throwing when handed no table name", () => {
110
+ expect(getCustomFieldMappingTarget(undefined)).toBeUndefined();
111
+ expect(getCustomFieldMappingTarget("")).toBeUndefined();
112
+ });
113
+
114
+ test("every registered source is a Monitor source today", () => {
115
+ for (const target of getCustomFieldMappingTargets()) {
116
+ expect(target.sources.length).toBe(1);
117
+ expect(target.sources[0]!.info.resource).toBe(
118
+ CustomFieldMappingSourceResource.Monitor,
119
+ );
120
+ }
121
+ });
122
+
123
+ test("Alert takes one monitor; Incident and Scheduled Maintenance take many", () => {
124
+ expect(getSource(ALERT_TABLE).info.isManySources).toBe(false);
125
+ expect(getSource(INCIDENT_TABLE).info.isManySources).toBe(true);
126
+ expect(getSource(SCHEDULED_MAINTENANCE_TABLE).info.isManySources).toBe(
127
+ true,
128
+ );
129
+ });
130
+ });
131
+
132
+ describe("memoisation - propagation matches source entries by reference", () => {
133
+ test("hands back the identical target array on every call", () => {
134
+ expect(getCustomFieldMappingTargets()).toBe(getCustomFieldMappingTargets());
135
+ });
136
+
137
+ test("hands back the identical source object on every call", () => {
138
+ expect(getSource(ALERT_TABLE)).toBe(getSource(ALERT_TABLE));
139
+ });
140
+
141
+ test("the reverse lookup yields the same source object the forward lookup does", () => {
142
+ const reverse: Array<{
143
+ target: CustomFieldMappingTargetEntry;
144
+ source: CustomFieldMappingSourceEntry;
145
+ }> = getCustomFieldMappingTargetsForSource(
146
+ CustomFieldMappingSourceResource.Monitor,
147
+ );
148
+
149
+ const alertPair: {
150
+ target: CustomFieldMappingTargetEntry;
151
+ source: CustomFieldMappingSourceEntry;
152
+ } = reverse.find(
153
+ (pair: {
154
+ target: CustomFieldMappingTargetEntry;
155
+ source: CustomFieldMappingSourceEntry;
156
+ }) => {
157
+ return pair.target.definitionTableName === ALERT_TABLE;
158
+ },
159
+ )!;
160
+
161
+ expect(alertPair.source).toBe(getSource(ALERT_TABLE));
162
+ });
163
+ });
164
+
165
+ describe("the reverse lookup used when a monitor's fields change", () => {
166
+ test("finds all three targets for the Monitor resource", () => {
167
+ const matches: Array<{
168
+ target: CustomFieldMappingTargetEntry;
169
+ source: CustomFieldMappingSourceEntry;
170
+ }> = getCustomFieldMappingTargetsForSource(
171
+ CustomFieldMappingSourceResource.Monitor,
172
+ );
173
+
174
+ expect(
175
+ matches.map(
176
+ (pair: {
177
+ target: CustomFieldMappingTargetEntry;
178
+ source: CustomFieldMappingSourceEntry;
179
+ }) => {
180
+ return pair.target.targetName;
181
+ },
182
+ ),
183
+ ).toEqual(["Alert", "Incident", "Scheduled Maintenance"]);
184
+ });
185
+
186
+ test("finds nothing for a resource that maps to no target", () => {
187
+ expect(
188
+ getCustomFieldMappingTargetsForSource(
189
+ "SomeResourceNobodyRegistered" as CustomFieldMappingSourceResource,
190
+ ),
191
+ ).toEqual([]);
192
+ });
193
+ });
194
+
195
+ describe("the select clause pulled off a persisted target record", () => {
196
+ test("a single source selects the FK column itself", () => {
197
+ expect(getSource(ALERT_TABLE).targetRelationSelect).toEqual({
198
+ monitorId: true,
199
+ });
200
+ });
201
+
202
+ test("a many source selects only the ids of the relation array", () => {
203
+ expect(getSource(INCIDENT_TABLE).targetRelationSelect).toEqual({
204
+ monitors: { _id: true },
205
+ });
206
+ });
207
+ });
208
+
209
+ describe("reading source ids off a persisted target record", () => {
210
+ test("reads the FK column of a single-source target", () => {
211
+ expect(
212
+ getSource(ALERT_TABLE)
213
+ .readSourceIdsFromRecord({ monitorId: MONITOR_ID })
214
+ .map((id: ObjectID) => {
215
+ return id.toString();
216
+ }),
217
+ ).toEqual([MONITOR_ID.toString()]);
218
+ });
219
+
220
+ test("reads the relation-object spelling of a single-source target too", () => {
221
+ expect(
222
+ getSource(ALERT_TABLE)
223
+ .readSourceIdsFromRecord({ monitor: { _id: MONITOR_ID.toString() } })
224
+ .map((id: ObjectID) => {
225
+ return id.toString();
226
+ }),
227
+ ).toEqual([MONITOR_ID.toString()]);
228
+ });
229
+
230
+ test("reads an empty list from a single-source target with no monitor", () => {
231
+ expect(getSource(ALERT_TABLE).readSourceIdsFromRecord({})).toEqual([]);
232
+ expect(
233
+ getSource(ALERT_TABLE).readSourceIdsFromRecord({ monitorId: null }),
234
+ ).toEqual([]);
235
+ });
236
+
237
+ test("reads every id off a many-source target", () => {
238
+ const second: ObjectID = new ObjectID(
239
+ "33333333-3333-4333-8333-333333333333",
240
+ );
241
+
242
+ expect(
243
+ getSource(INCIDENT_TABLE)
244
+ .readSourceIdsFromRecord({
245
+ monitors: [
246
+ buildMonitor({ id: MONITOR_ID }),
247
+ buildMonitor({ id: second }),
248
+ ],
249
+ })
250
+ .map((id: ObjectID) => {
251
+ return id.toString();
252
+ }),
253
+ ).toEqual([MONITOR_ID.toString(), second.toString()]);
254
+ });
255
+
256
+ test("drops entries of a many-source relation that carry no id", () => {
257
+ expect(
258
+ getSource(INCIDENT_TABLE).readSourceIdsFromRecord({
259
+ monitors: [new Monitor(), buildMonitor({ id: MONITOR_ID })],
260
+ }).length,
261
+ ).toBe(1);
262
+ });
263
+
264
+ test("reads an empty list from a many-source target with no relation at all", () => {
265
+ expect(getSource(INCIDENT_TABLE).readSourceIdsFromRecord({})).toEqual([]);
266
+ });
267
+ });
268
+
269
+ describe("null means silence, [] means cleared", () => {
270
+ test("a single-source payload that never names the relation reads null", () => {
271
+ expect(
272
+ getSource(ALERT_TABLE).readSourceIdsFromPayload({ title: "Unrelated" }),
273
+ ).toBeNull();
274
+ });
275
+
276
+ test("a single-source payload that clears the relation reads an empty list", () => {
277
+ expect(
278
+ getSource(ALERT_TABLE).readSourceIdsFromPayload({ monitorId: null }),
279
+ ).toEqual([]);
280
+ });
281
+
282
+ test("a single-source payload reads the FK spelling", () => {
283
+ expect(
284
+ getSource(ALERT_TABLE)
285
+ .readSourceIdsFromPayload({ monitorId: MONITOR_ID })!
286
+ .map((id: ObjectID) => {
287
+ return id.toString();
288
+ }),
289
+ ).toEqual([MONITOR_ID.toString()]);
290
+ });
291
+
292
+ test("a single-source payload reads the dashboard's relation spelling", () => {
293
+ expect(
294
+ getSource(ALERT_TABLE)
295
+ .readSourceIdsFromPayload({ monitor: { _id: MONITOR_ID.toString() } })!
296
+ .map((id: ObjectID) => {
297
+ return id.toString();
298
+ }),
299
+ ).toEqual([MONITOR_ID.toString()]);
300
+ });
301
+
302
+ test("a many-source payload that never names the relation reads null", () => {
303
+ expect(
304
+ getSource(INCIDENT_TABLE).readSourceIdsFromPayload({
305
+ title: "Unrelated",
306
+ }),
307
+ ).toBeNull();
308
+ });
309
+
310
+ test("a many-source payload that empties the relation reads an empty list", () => {
311
+ expect(
312
+ getSource(INCIDENT_TABLE).readSourceIdsFromPayload({ monitors: [] }),
313
+ ).toEqual([]);
314
+ });
315
+
316
+ test("a many-source payload reads every attached id", () => {
317
+ expect(
318
+ getSource(INCIDENT_TABLE)
319
+ .readSourceIdsFromPayload({
320
+ monitors: [buildMonitor({ id: MONITOR_ID })],
321
+ })!
322
+ .map((id: ObjectID) => {
323
+ return id.toString();
324
+ }),
325
+ ).toEqual([MONITOR_ID.toString()]);
326
+ });
327
+
328
+ test("an absent payload reads null rather than throwing", () => {
329
+ expect(getSource(ALERT_TABLE).readSourceIdsFromPayload(null)).toBeNull();
330
+ expect(getSource(INCIDENT_TABLE).readSourceIdsFromPayload(null)).toBeNull();
331
+ });
332
+ });
333
+
334
+ describe("a hydrated monitor is taken from the payload; a stub is not", () => {
335
+ test("takes the bag off a single-source payload's relation object", () => {
336
+ expect(
337
+ getSource(ALERT_TABLE).readHydratedSourcesFromPayload({
338
+ monitor: buildMonitor({
339
+ id: MONITOR_ID,
340
+ customFields: { severity: "High" },
341
+ }),
342
+ }),
343
+ ).toEqual({ [MONITOR_ID.toString()]: { severity: "High" } });
344
+ });
345
+
346
+ test("treats a monitor read WITH a null customFields column as hydrated and empty", () => {
347
+ expect(
348
+ getSource(ALERT_TABLE).readHydratedSourcesFromPayload({
349
+ monitor: buildMonitor({ id: MONITOR_ID, customFields: null }),
350
+ }),
351
+ ).toEqual({ [MONITOR_ID.toString()]: {} });
352
+ });
353
+
354
+ test("refuses an id stub whose customFields were never read", () => {
355
+ expect(
356
+ getSource(ALERT_TABLE).readHydratedSourcesFromPayload({
357
+ monitor: buildMonitor({ id: MONITOR_ID, hydrated: false }),
358
+ }),
359
+ ).toEqual({});
360
+ });
361
+
362
+ test("takes nothing from the FK-only spelling - there is no bag to take", () => {
363
+ expect(
364
+ getSource(ALERT_TABLE).readHydratedSourcesFromPayload({
365
+ monitorId: MONITOR_ID,
366
+ }),
367
+ ).toEqual({});
368
+ });
369
+
370
+ test("takes the bag off every hydrated monitor of a many-source payload", () => {
371
+ const second: ObjectID = new ObjectID(
372
+ "44444444-4444-4444-8444-444444444444",
373
+ );
374
+
375
+ expect(
376
+ getSource(INCIDENT_TABLE).readHydratedSourcesFromPayload({
377
+ monitors: [
378
+ buildMonitor({ id: MONITOR_ID, customFields: { team: "Core" } }),
379
+ buildMonitor({ id: second, customFields: { team: "Edge" } }),
380
+ ],
381
+ }),
382
+ ).toEqual({
383
+ [MONITOR_ID.toString()]: { team: "Core" },
384
+ [second.toString()]: { team: "Edge" },
385
+ });
386
+ });
387
+
388
+ test("skips the stubs of a many-source payload and keeps the rest", () => {
389
+ const stubbed: ObjectID = new ObjectID(
390
+ "55555555-5555-4555-8555-555555555555",
391
+ );
392
+
393
+ expect(
394
+ getSource(INCIDENT_TABLE).readHydratedSourcesFromPayload({
395
+ monitors: [
396
+ buildMonitor({ id: MONITOR_ID, customFields: { team: "Core" } }),
397
+ buildMonitor({ id: stubbed, hydrated: false }),
398
+ ],
399
+ }),
400
+ ).toEqual({ [MONITOR_ID.toString()]: { team: "Core" } });
401
+ });
402
+
403
+ test("takes nothing from an absent payload", () => {
404
+ expect(getSource(ALERT_TABLE).readHydratedSourcesFromPayload(null)).toEqual(
405
+ {},
406
+ );
407
+ expect(
408
+ getSource(INCIDENT_TABLE).readHydratedSourcesFromPayload(null),
409
+ ).toEqual({});
410
+ });
411
+ });
412
+
413
+ describe("the reverse query that finds every target attached to a source", () => {
414
+ test("a single source matches on the FK column directly", () => {
415
+ expect(
416
+ getSource(ALERT_TABLE).buildTargetQueryForSourceId(MONITOR_ID),
417
+ ).toEqual({ monitorId: MONITOR_ID });
418
+ });
419
+
420
+ test("a many source matches through the relation array", () => {
421
+ expect(
422
+ getSource(INCIDENT_TABLE).buildTargetQueryForSourceId(MONITOR_ID),
423
+ ).toEqual({ monitors: [MONITOR_ID] });
424
+ });
425
+
426
+ test("the query names the property the catalog says holds the relation", () => {
427
+ for (const target of getCustomFieldMappingTargets()) {
428
+ const source: CustomFieldMappingSourceEntry = target.sources[0]!;
429
+ const info: CustomFieldMappingSourceInfo = source.info;
430
+
431
+ expect(
432
+ Object.keys(source.buildTargetQueryForSourceId(MONITOR_ID)),
433
+ ).toEqual([info.targetRelationProperty]);
434
+ }
435
+ });
436
+ });
@@ -3,6 +3,7 @@ import PublicDashboardResourceListPolicy, {
3
3
  } from "../../../../Server/Utils/Dashboard/PublicDashboardResourceListPolicy";
4
4
  import InBetween from "../../../../Types/BaseDatabase/InBetween";
5
5
  import Includes from "../../../../Types/BaseDatabase/Includes";
6
+ import IncludesAnyOfGroups from "../../../../Types/BaseDatabase/IncludesAnyOfGroups";
6
7
  import Search from "../../../../Types/BaseDatabase/Search";
7
8
  import SortOrder from "../../../../Types/BaseDatabase/SortOrder";
8
9
  import { LIMIT_PER_PROJECT } from "../../../../Types/Database/LimitMax";
@@ -1038,6 +1039,219 @@ describe("PublicDashboardResourceListPolicy", () => {
1038
1039
  });
1039
1040
  });
1040
1041
 
1042
+ describe("monitor project label variables", () => {
1043
+ const firstLabelId: string = ObjectID.generate().toString();
1044
+ const secondLabelId: string = ObjectID.generate().toString();
1045
+ const fixedLabelId: string = ObjectID.generate().toString();
1046
+ const labelVariable: JSONObject = {
1047
+ id: "unit",
1048
+ name: "UNIT",
1049
+ type: DashboardVariableType.ProjectLabel,
1050
+ labelOptions: [
1051
+ { label: "Payments", value: firstLabelId },
1052
+ { label: "Support", value: secondLabelId },
1053
+ ],
1054
+ defaultValue: firstLabelId,
1055
+ };
1056
+
1057
+ it("filters by the saved variable's default when no selection was sent", () => {
1058
+ const result: PublicDashboardResourceListPolicyResult = build({
1059
+ componentType: DashboardComponentType.MonitorList,
1060
+ argumentsObject: { labelVariableId: "unit" },
1061
+ storedVariables: [labelVariable],
1062
+ });
1063
+
1064
+ expect(result.query).toEqual({ labels: new Includes([firstLabelId]) });
1065
+ });
1066
+
1067
+ it("uses the selected label ID while retaining the saved widget filters", () => {
1068
+ const result: PublicDashboardResourceListPolicyResult = build({
1069
+ componentType: DashboardComponentType.MonitorList,
1070
+ argumentsObject: {
1071
+ labelVariableId: "unit",
1072
+ statusFilter: "operational",
1073
+ monitorTypes: ["Ping"],
1074
+ monitorStatusIds: ["status"],
1075
+ maxRows: 7,
1076
+ },
1077
+ storedVariables: [labelVariable],
1078
+ requestedVariables: [{ id: "unit", selectedValue: secondLabelId }],
1079
+ });
1080
+
1081
+ expect(result.query).toEqual({
1082
+ labels: new Includes([secondLabelId]),
1083
+ currentMonitorStatus: { isOperationalState: true },
1084
+ monitorType: new Includes(["Ping"]),
1085
+ currentMonitorStatusId: new Includes(["status"]),
1086
+ });
1087
+ expect(result.limit).toBe(7);
1088
+ expect(result.sort).toEqual({ name: SortOrder.Ascending });
1089
+ expect(result.select).toEqual({
1090
+ _id: true,
1091
+ name: true,
1092
+ monitorType: true,
1093
+ currentMonitorStatus: { name: true, color: true },
1094
+ });
1095
+ });
1096
+
1097
+ it("requires both a fixed label group and the selected variable group", () => {
1098
+ const result: PublicDashboardResourceListPolicyResult = build({
1099
+ componentType: DashboardComponentType.MonitorList,
1100
+ argumentsObject: {
1101
+ labelVariableId: "unit",
1102
+ labelIds: [fixedLabelId],
1103
+ },
1104
+ storedVariables: [labelVariable],
1105
+ requestedVariables: [{ id: "unit", selectedValue: secondLabelId }],
1106
+ });
1107
+
1108
+ expect(result.query["labels"]).toEqual(
1109
+ new IncludesAnyOfGroups([[fixedLabelId], [secondLabelId]]),
1110
+ );
1111
+ });
1112
+
1113
+ it("lets All clear only the variable filter and preserves fixed labels", () => {
1114
+ const result: PublicDashboardResourceListPolicyResult = build({
1115
+ componentType: DashboardComponentType.MonitorList,
1116
+ argumentsObject: {
1117
+ labelVariableId: "unit",
1118
+ labelIds: [fixedLabelId],
1119
+ },
1120
+ storedVariables: [labelVariable],
1121
+ requestedVariables: [{ id: "unit", selectedValue: "" }],
1122
+ });
1123
+
1124
+ expect(result.query["labels"]).toEqual(new Includes([fixedLabelId]));
1125
+ });
1126
+
1127
+ it("leaves the label filter absent when All is selected without fixed labels", () => {
1128
+ const result: PublicDashboardResourceListPolicyResult = build({
1129
+ componentType: DashboardComponentType.MonitorList,
1130
+ argumentsObject: { labelVariableId: "unit" },
1131
+ storedVariables: [labelVariable],
1132
+ requestedVariables: [{ id: "unit", selectedValue: "" }],
1133
+ });
1134
+
1135
+ expect(result.query["labels"]).toBeUndefined();
1136
+ });
1137
+
1138
+ it("matches either selected label for a multi-select", () => {
1139
+ const result: PublicDashboardResourceListPolicyResult = build({
1140
+ componentType: DashboardComponentType.MonitorList,
1141
+ argumentsObject: { labelVariableId: "unit" },
1142
+ storedVariables: [{ ...labelVariable, isMultiSelect: true }],
1143
+ requestedVariables: [
1144
+ { id: "unit", selectedValues: [firstLabelId, secondLabelId] },
1145
+ ],
1146
+ });
1147
+
1148
+ expect(result.query["labels"]).toEqual(
1149
+ new Includes([firstLabelId, secondLabelId]),
1150
+ );
1151
+ });
1152
+
1153
+ it("treats an empty multi-select as All even when a default or stale scalar exists", () => {
1154
+ for (const requestedVariables of [
1155
+ [],
1156
+ [{ id: "unit", selectedValue: firstLabelId, selectedValues: [] }],
1157
+ ]) {
1158
+ const result: PublicDashboardResourceListPolicyResult = build({
1159
+ componentType: DashboardComponentType.MonitorList,
1160
+ argumentsObject: {
1161
+ labelVariableId: "unit",
1162
+ labelIds: [fixedLabelId],
1163
+ },
1164
+ storedVariables: [{ ...labelVariable, isMultiSelect: true }],
1165
+ requestedVariables,
1166
+ });
1167
+
1168
+ expect(result.query["labels"]).toEqual(new Includes([fixedLabelId]));
1169
+ }
1170
+ });
1171
+
1172
+ it("ignores stale multi-select values for a saved single-select", () => {
1173
+ const result: PublicDashboardResourceListPolicyResult = build({
1174
+ componentType: DashboardComponentType.MonitorList,
1175
+ argumentsObject: { labelVariableId: "unit" },
1176
+ storedVariables: [labelVariable],
1177
+ requestedVariables: [
1178
+ {
1179
+ id: "unit",
1180
+ selectedValue: firstLabelId,
1181
+ selectedValues: [secondLabelId],
1182
+ },
1183
+ ],
1184
+ });
1185
+
1186
+ expect(result.query["labels"]).toEqual(new Includes([firstLabelId]));
1187
+ });
1188
+
1189
+ it("does not apply a label variable to an unbound widget", () => {
1190
+ const result: PublicDashboardResourceListPolicyResult = build({
1191
+ componentType: DashboardComponentType.MonitorList,
1192
+ argumentsObject: { labelIds: [fixedLabelId] },
1193
+ storedVariables: [labelVariable],
1194
+ requestedVariables: [{ id: "unit", selectedValue: secondLabelId }],
1195
+ });
1196
+
1197
+ expect(result.query["labels"]).toEqual(new Includes([fixedLabelId]));
1198
+ });
1199
+
1200
+ it("rejects a selection that is no longer among the published choices", () => {
1201
+ expect(() => {
1202
+ return build({
1203
+ componentType: DashboardComponentType.MonitorList,
1204
+ argumentsObject: { labelVariableId: "unit" },
1205
+ storedVariables: [labelVariable],
1206
+ requestedVariables: [
1207
+ { id: "unit", selectedValue: ObjectID.generate().toString() },
1208
+ ],
1209
+ });
1210
+ }).toThrow(BadDataException);
1211
+ });
1212
+
1213
+ it("rejects a missing or incompatible saved variable binding", () => {
1214
+ for (const storedVariables of [
1215
+ [],
1216
+ [
1217
+ {
1218
+ id: "unit",
1219
+ name: "UNIT",
1220
+ type: DashboardVariableType.TelemetryAttribute,
1221
+ attributeKey: "unit",
1222
+ },
1223
+ ],
1224
+ ]) {
1225
+ expect(() => {
1226
+ return build({
1227
+ componentType: DashboardComponentType.MonitorList,
1228
+ argumentsObject: { labelVariableId: "unit" },
1229
+ storedVariables,
1230
+ });
1231
+ }).toThrow(BadDataException);
1232
+ }
1233
+ });
1234
+
1235
+ it("rejects malformed saved label choices", () => {
1236
+ for (const labelOptions of [
1237
+ null,
1238
+ "Payments",
1239
+ [null],
1240
+ [{ value: firstLabelId }],
1241
+ [{ label: "Payments", value: 42 }],
1242
+ [{ label: "", value: firstLabelId }],
1243
+ ]) {
1244
+ expect(() => {
1245
+ return build({
1246
+ componentType: DashboardComponentType.MonitorList,
1247
+ argumentsObject: { labelVariableId: "unit" },
1248
+ storedVariables: [{ ...labelVariable, labelOptions }],
1249
+ });
1250
+ }).toThrow(BadDataException);
1251
+ }
1252
+ });
1253
+ });
1254
+
1041
1255
  describe("fixed limits and malformed widgets", () => {
1042
1256
  it("clamps stored list and map limits to the project ceiling", () => {
1043
1257
  expect(
@@ -182,7 +182,7 @@ describe("EmailRollupFlushRunner - one flush", () => {
182
182
  `${HARNESS_DASHBOARD_URL}/${PROJECT_ID.toString()}/home`,
183
183
  );
184
184
  expect(vars["preferencesLink"]).toBe(
185
- `${HARNESS_DASHBOARD_URL}/${PROJECT_ID.toString()}/user-settings/notification-settings`,
185
+ `${HARNESS_DASHBOARD_URL}/${PROJECT_ID.toString()}/user-settings/email-preferences`,
186
186
  );
187
187
  });
188
188