@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,147 @@
1
+ import Entities from "../../Models/DatabaseModels/Index";
2
+ import InventoryItem from "../../Models/DatabaseModels/InventoryItem";
3
+ import QueryUtil from "../../Server/Types/Database/QueryUtil";
4
+ import Query from "../../Types/BaseDatabase/Query";
5
+ import Includes from "../../Types/BaseDatabase/Includes";
6
+ import IncludesNone from "../../Types/BaseDatabase/IncludesNone";
7
+ import LessThan from "../../Types/BaseDatabase/LessThan";
8
+ import { getColumnAccessControlForAllColumns } from "../../Types/Database/AccessControl/ColumnAccessControl";
9
+ import { ColumnAccessControl } from "../../Types/BaseDatabase/AccessControl";
10
+ import TableColumnType from "../../Types/Database/TableColumnType";
11
+ import { JSONObject } from "../../Types/JSON";
12
+ import JSONFunctions from "../../Types/JSONFunctions";
13
+ import ObjectID from "../../Types/ObjectID";
14
+ import EntitySource from "../../Types/Telemetry/EntitySource";
15
+ import EntityType from "../../Types/Telemetry/EntityType";
16
+ import { InventoryLiveness } from "../../Types/Telemetry/InventoryLiveness";
17
+ import { DataSource, FindOptionsWhere } from "typeorm";
18
+ import { ColumnMetadata } from "typeorm/metadata/ColumnMetadata";
19
+
20
+ describe("inventory status is a read-only computed query column", () => {
21
+ let database: DataSource;
22
+
23
+ beforeAll(async () => {
24
+ database = new DataSource({
25
+ type: "postgres",
26
+ entities: Entities,
27
+ synchronize: false,
28
+ });
29
+ await (
30
+ database as unknown as { buildMetadatas: () => Promise<void> }
31
+ ).buildMetadatas();
32
+ });
33
+
34
+ function buildQuery(query: Query<InventoryItem>): [string, Array<unknown>] {
35
+ return database
36
+ .getRepository(InventoryItem)
37
+ .createQueryBuilder("inventory")
38
+ .setFindOptions({
39
+ select: { _id: true, inventoryStatus: true },
40
+ where: QueryUtil.serializeQuery(
41
+ InventoryItem,
42
+ query,
43
+ ) as unknown as FindOptionsWhere<InventoryItem>,
44
+ })
45
+ .getQueryAndParameters();
46
+ }
47
+
48
+ test("TypeORM excludes status from inserts, updates and physical schema columns", () => {
49
+ const column: ColumnMetadata = database
50
+ .getMetadata(InventoryItem)
51
+ .findColumnWithPropertyName("inventoryStatus")!;
52
+ expect(column.isVirtualProperty).toBe(true);
53
+ expect(column.isInsert).toBe(false);
54
+ expect(column.isUpdate).toBe(false);
55
+ expect(column.query).toBeDefined();
56
+ });
57
+
58
+ test("model metadata allows reads and filtering but rejects client writes", () => {
59
+ const model: InventoryItem = new InventoryItem();
60
+ expect(model.getTableColumnMetadata("inventoryStatus").type).toBe(
61
+ TableColumnType.ShortText,
62
+ );
63
+ const control: ColumnAccessControl =
64
+ getColumnAccessControlForAllColumns(model)["inventoryStatus"]!;
65
+ expect(control.read.length).toBeGreaterThan(0);
66
+ expect(control.create).toEqual([]);
67
+ expect(control.update).toEqual([]);
68
+ });
69
+
70
+ test.each([false, true])(
71
+ "the real query compiler uses CASE for a status selection (negated: %s)",
72
+ (negated: boolean) => {
73
+ const values: Array<string> = [
74
+ InventoryLiveness.Live,
75
+ InventoryLiveness.Never,
76
+ ];
77
+ const [sql, parameters]: [string, Array<unknown>] = buildQuery({
78
+ inventoryStatus: negated
79
+ ? new IncludesNone(values)
80
+ : new Includes(values),
81
+ });
82
+ const where: string = sql.slice(sql.indexOf("WHERE"));
83
+ expect(where).toContain("CASE");
84
+ expect(where).toContain('"inventory"."source"');
85
+ expect(where).toContain('"inventory"."lastSeenAt"');
86
+ expect(where).not.toContain('"inventory"."inventoryStatus"');
87
+ expect(where).toContain(negated ? "NOT IN" : " IN");
88
+ expect(parameters).toEqual(values);
89
+ expect(sql).toContain('AS "inventory_inventoryStatus"');
90
+ },
91
+ );
92
+
93
+ test("status composes with type, source, dates, project and archive filters", () => {
94
+ const projectId: ObjectID = ObjectID.generate();
95
+ const before: Date = new Date("2026-09-06T12:00:00.000Z");
96
+ const [sql, parameters]: [string, Array<unknown>] = buildQuery({
97
+ projectId,
98
+ entityType: new Includes([EntityType.Service]),
99
+ source: EntitySource.Discovered,
100
+ lastSeenAt: new LessThan(before.toISOString()),
101
+ isArchived: false,
102
+ inventoryStatus: new Includes([InventoryLiveness.Stale]),
103
+ });
104
+ for (const column of [
105
+ "projectId",
106
+ "entityType",
107
+ "source",
108
+ "lastSeenAt",
109
+ "isArchived",
110
+ ]) {
111
+ expect(sql).toContain(`"inventory"."${column}"`);
112
+ }
113
+ expect(parameters).toEqual(
114
+ expect.arrayContaining([
115
+ projectId.toString(),
116
+ EntityType.Service,
117
+ EntitySource.Discovered,
118
+ before.toISOString(),
119
+ false,
120
+ InventoryLiveness.Stale,
121
+ ]),
122
+ );
123
+ });
124
+
125
+ test("API serialization preserves status negation and multiple values", () => {
126
+ const payload: JSONObject = JSON.parse(
127
+ JSON.stringify(
128
+ JSONFunctions.serialize({
129
+ inventoryStatus: new IncludesNone([
130
+ InventoryLiveness.Stale,
131
+ InventoryLiveness.Never,
132
+ ]),
133
+ }),
134
+ ),
135
+ );
136
+ const query: Query<InventoryItem> = JSONFunctions.deserialize(
137
+ payload,
138
+ ) as Query<InventoryItem>;
139
+ expect(query.inventoryStatus).toBeInstanceOf(IncludesNone);
140
+ const [sql, parameters]: [string, Array<unknown>] = buildQuery(query);
141
+ expect(sql).toContain("NOT IN");
142
+ expect(parameters).toEqual([
143
+ InventoryLiveness.Stale,
144
+ InventoryLiveness.Never,
145
+ ]);
146
+ });
147
+ });
@@ -0,0 +1,294 @@
1
+ /**
2
+ * NetworkDevice.macAddress column contract (issue #3489).
3
+ *
4
+ * A ping-only device — a POS register, a handset, a kiosk — speaks neither
5
+ * LLDP nor CDP, so nothing it says places it on a switch port. The switches
6
+ * already know: every walked switch reports its forwarding table, and the
7
+ * server keeps one NetworkEndpoint row per MAC it learned. This column is
8
+ * what lets a device be recognised in that inventory, and everything below
9
+ * pins a property that, quietly changed, either refuses a write the device
10
+ * form is about to make or leaves the column unreadable by the one page that
11
+ * needs it.
12
+ *
13
+ * Three properties matter more than the rest:
14
+ *
15
+ * - NOT REQUIRED. The MAC is optional twice over — a device whose hostname
16
+ * is an IP address that a router's ARP table resolves is matched by
17
+ * address without it — and every existing create path (the form, the
18
+ * CRUD API, discovery import, seeding) omits it. A required column would
19
+ * break all of them at once.
20
+ * - NULLABLE WITH NO DEFAULT. NULL means "not declared, and not learned
21
+ * yet", which is what every row already on disk should say; a default
22
+ * would give the fleet a MAC it does not have, and the ARP learner only
23
+ * ever fills an EMPTY value, so a default would also stop it filling
24
+ * anything.
25
+ * - READABLE ON A RELATION QUERY, and governed exactly as `hostname` is.
26
+ * The topology API selects it off the device list, and an operator who
27
+ * may set a device's address may set its MAC — the two are the layer-3
28
+ * and layer-2 spellings of the same fact.
29
+ */
30
+
31
+ import NetworkDevice from "../../Models/DatabaseModels/NetworkDevice";
32
+ import { ColumnAccessControl } from "../../Types/BaseDatabase/AccessControl";
33
+ import { TableColumnMetadata } from "../../Types/Database/TableColumn";
34
+ import TableColumnType from "../../Types/Database/TableColumnType";
35
+ import ColumnType from "../../Types/Database/ColumnType";
36
+ import ColumnLength from "../../Types/Database/ColumnLength";
37
+ import Columns from "../../Types/Database/Columns";
38
+ import Permission from "../../Types/Permission";
39
+ import { normalizeMac } from "../../Utils/Monitor/EndpointAttachmentUtil";
40
+ import { describe, expect, test } from "@jest/globals";
41
+ import { getMetadataArgsStorage } from "typeorm";
42
+ import { ColumnMetadataArgs } from "typeorm/metadata-args/ColumnMetadataArgs";
43
+
44
+ const COLUMN: string = "macAddress";
45
+
46
+ /*
47
+ * The column this one is measured against. `hostname` is the device's
48
+ * layer-3 address and this is its layer-2 one; whoever may state one may
49
+ * state the other. Compared against rather than written out as a permission
50
+ * list, so the two are revised together or not at all.
51
+ */
52
+ const PEER_ADDRESS_COLUMN: string = "hostname";
53
+
54
+ function metadata(): TableColumnMetadata {
55
+ return new NetworkDevice().getTableColumnMetadata(COLUMN);
56
+ }
57
+
58
+ function typeOrmColumn(): ColumnMetadataArgs | undefined {
59
+ return getMetadataArgsStorage().columns.find((column: ColumnMetadataArgs) => {
60
+ return column.target === NetworkDevice && column.propertyName === COLUMN;
61
+ });
62
+ }
63
+
64
+ function accessControlFor(column: string): ColumnAccessControl | null {
65
+ return new NetworkDevice().getColumnAccessControlFor(column);
66
+ }
67
+
68
+ describe("NetworkDevice.macAddress", () => {
69
+ test("exists as a short text column", () => {
70
+ expect(metadata()).toBeDefined();
71
+ expect(metadata().type).toBe(TableColumnType.ShortText);
72
+ expect(typeOrmColumn()).toBeDefined();
73
+ expect(typeOrmColumn()?.options.type).toBe(ColumnType.ShortText);
74
+ });
75
+
76
+ /*
77
+ * The title is the label on the device form and the heading in the
78
+ * generated API docs; the form field module reads it from here.
79
+ */
80
+ test("is titled MAC Address", () => {
81
+ expect(metadata().title).toBe("MAC Address");
82
+ });
83
+
84
+ /*
85
+ * The API documentation is the only explanation an integrator gets. It
86
+ * has to say what the column is FOR — the switch port, found from the
87
+ * forwarding tables — and that it is optional because ARP can stand in.
88
+ */
89
+ test("its description says what the column is for and that it is optional", () => {
90
+ const description: string = metadata().description ?? "";
91
+
92
+ expect(description).toContain("switch port");
93
+ expect(description).toContain("forwarding tables");
94
+ expect(description).toContain("LLDP");
95
+ expect(description).toContain("CDP");
96
+ expect(description).toContain("ping");
97
+ expect(description).toContain("ARP");
98
+ expect(description).toContain("Optional");
99
+ });
100
+
101
+ /*
102
+ * The example is what the generated docs show and what a form placeholder
103
+ * is modelled on. It has to be in the STORED form — lowercase colon — so
104
+ * that what an integrator copies from the docs is what they read back.
105
+ */
106
+ test("its example is already in the stored form", () => {
107
+ const example: string = String(metadata().example);
108
+
109
+ expect(normalizeMac(example)).toBe(example);
110
+ expect(example).toBe(example.toLowerCase());
111
+ expect(example.split(":")).toHaveLength(6);
112
+ });
113
+
114
+ /*
115
+ * THE assertion of the column. A required column makes the API reject a
116
+ * create that omits it — and every create path in the product omits it.
117
+ */
118
+ test("is not required, so a create that omits it is still accepted", () => {
119
+ expect(metadata().required).toBeFalsy();
120
+
121
+ const requiredColumns: Columns = new NetworkDevice().getRequiredColumns();
122
+
123
+ expect(requiredColumns.columns).not.toContain(COLUMN);
124
+ // ...and the check is not vacuous: the peer address genuinely is required.
125
+ expect(requiredColumns.columns).toContain(PEER_ADDRESS_COLUMN);
126
+ });
127
+
128
+ /*
129
+ * NULL is a meaning here: "not declared, not learned yet". A default
130
+ * would restate every existing device as having a MAC it does not have,
131
+ * and the ARP learner — which only ever fills an EMPTY value — would then
132
+ * never fill anything.
133
+ */
134
+ test("is nullable with no default", () => {
135
+ expect(typeOrmColumn()?.options.nullable).toBe(true);
136
+ expect(typeOrmColumn()?.options.default).toBeUndefined();
137
+ expect(metadata().defaultValue).toBeUndefined();
138
+ expect(new NetworkDevice().isDefaultValueColumn(COLUMN)).toBe(false);
139
+ });
140
+
141
+ /*
142
+ * character varying(100): the shared ShortText length, so the migration's
143
+ * SQL and the entity agree without either restating the number.
144
+ */
145
+ test("is a ShortText-length varchar", () => {
146
+ expect(typeOrmColumn()?.options.length).toBe(ColumnLength.ShortText);
147
+ expect(ColumnLength.ShortText).toBe(100);
148
+ });
149
+
150
+ /*
151
+ * The topology API reads devices with a nested select, and the builder
152
+ * needs the MAC on every one of them to match endpoints against. A column
153
+ * that cannot be read through a relation would be silently dropped from
154
+ * that select, and adoption would never fire.
155
+ */
156
+ test("is readable from a relation query, like the address beside it", () => {
157
+ expect(metadata().canReadOnRelationQuery).toBe(true);
158
+ expect(
159
+ new NetworkDevice().getTableColumnMetadata(PEER_ADDRESS_COLUMN)
160
+ .canReadOnRelationQuery,
161
+ ).toBe(true);
162
+ });
163
+ });
164
+
165
+ describe("NetworkDevice.macAddress access control", () => {
166
+ /*
167
+ * The named grants the feature leans on. A project member fills the form
168
+ * in; the fine-grained device permissions are what an API key carries;
169
+ * and the topology page is readable by a Viewer, so the column it draws
170
+ * from has to be too.
171
+ */
172
+ test("lets a project member and the device permissions set it", () => {
173
+ const column: ColumnAccessControl | null = accessControlFor(COLUMN);
174
+
175
+ expect(column?.create).toContain(Permission.ProjectMember);
176
+ expect(column?.create).toContain(Permission.CreateNetworkDevice);
177
+ expect(column?.update).toContain(Permission.ProjectMember);
178
+ expect(column?.update).toContain(Permission.EditNetworkDevice);
179
+ });
180
+
181
+ test("lets a viewer read it, since the topology map is viewer-readable", () => {
182
+ const column: ColumnAccessControl | null = accessControlFor(COLUMN);
183
+
184
+ expect(column?.read).toContain(Permission.Viewer);
185
+ expect(column?.read).toContain(Permission.ReadNetworkDevice);
186
+ });
187
+
188
+ /*
189
+ * The whole claim, in one assertion: this column is governed exactly as
190
+ * the device's hostname is. Written as a comparison rather than a
191
+ * permission list on purpose — the two are the layer-3 and layer-2
192
+ * address of the same device and must be revised together, so a change
193
+ * that widens both keeps this green while one drifting from the other
194
+ * fails here, which is the case worth catching.
195
+ */
196
+ test("is governed exactly as the hostname is", () => {
197
+ expect(accessControlFor(COLUMN)).toEqual(
198
+ accessControlFor(PEER_ADDRESS_COLUMN),
199
+ );
200
+ });
201
+
202
+ // Broken out per operation so a failure says WHICH of the three drifted.
203
+ test("grants the same create, read and update permissions as the hostname", () => {
204
+ const column: ColumnAccessControl | null = accessControlFor(COLUMN);
205
+ const peer: ColumnAccessControl | null =
206
+ accessControlFor(PEER_ADDRESS_COLUMN);
207
+
208
+ expect(column?.create).toEqual(peer?.create);
209
+ expect(column?.read).toEqual(peer?.read);
210
+ expect(column?.update).toEqual(peer?.update);
211
+ });
212
+
213
+ /*
214
+ * ...and the comparison above is not two empty objects agreeing with each
215
+ * other. A column with no create permission at all is silently dropped
216
+ * from every write, which would leave the form field doing nothing.
217
+ */
218
+ test("actually grants somebody the right to set, read and change it", () => {
219
+ const column: ColumnAccessControl | null = accessControlFor(COLUMN);
220
+
221
+ expect(column).not.toBeNull();
222
+ expect(column?.create?.length).toBeGreaterThan(0);
223
+ expect(column?.read?.length).toBeGreaterThan(0);
224
+ expect(column?.update?.length).toBeGreaterThan(0);
225
+ });
226
+
227
+ /*
228
+ * Everyone who may create a device may declare its MAC, everyone who may
229
+ * read a device may see it, and everyone who may edit a device may change
230
+ * it. Anything narrower would produce a device whose MAC its creator
231
+ * could not state, or a map that cannot place a device its viewer can see.
232
+ */
233
+ test("its setters, readers and editors are those of the table", () => {
234
+ const device: NetworkDevice = new NetworkDevice();
235
+ const column: ColumnAccessControl | null = accessControlFor(COLUMN);
236
+
237
+ expect(column?.create).toEqual(device.getCreatePermissions());
238
+ expect(column?.read).toEqual(device.getReadPermissions());
239
+ expect(column?.update).toEqual(device.getUpdatePermissions());
240
+ });
241
+ });
242
+
243
+ /*
244
+ * The create contract, re-derived the way NetworkDeviceCreateContract.test.ts
245
+ * derives it. Adding a column must not change what a caller has to supply,
246
+ * and this column in particular is optional by design — so the set of
247
+ * caller-supplied required columns is exactly what it was before it existed.
248
+ */
249
+ describe("NetworkDevice.macAddress leaves the create contract alone", () => {
250
+ const device: NetworkDevice = new NetworkDevice();
251
+
252
+ function callerSuppliedRequiredColumns(): Array<string> {
253
+ return device.getRequiredColumns().columns.filter((column: string) => {
254
+ return !device.isDefaultValueColumn(column);
255
+ });
256
+ }
257
+
258
+ test("the caller is still asked for exactly the four identity columns", () => {
259
+ expect(callerSuppliedRequiredColumns().sort()).toEqual([
260
+ "hostname",
261
+ "name",
262
+ "projectId",
263
+ "slug",
264
+ ]);
265
+ });
266
+
267
+ test("and never for the MAC", () => {
268
+ expect(callerSuppliedRequiredColumns()).not.toContain(COLUMN);
269
+ });
270
+ });
271
+
272
+ describe("the isMacAddressLearned column", () => {
273
+ const FLAG: string = "isMacAddressLearned";
274
+
275
+ test("is an optional boolean that defaults to false", () => {
276
+ const metadata: TableColumnMetadata =
277
+ new NetworkDevice().getTableColumnMetadata(FLAG);
278
+
279
+ expect(metadata.type).toBe(TableColumnType.Boolean);
280
+ expect(metadata.required).toBeFalsy();
281
+ expect(metadata.defaultValue).toBe(false);
282
+ expect(metadata.title).toBe("MAC Address Learned");
283
+ });
284
+
285
+ test("is writable by exactly the roles that may write the MAC beside it", () => {
286
+ const device: NetworkDevice = new NetworkDevice();
287
+ const flagAccess: ColumnAccessControl =
288
+ device.getColumnAccessControlFor(FLAG)!;
289
+ const macAccess: ColumnAccessControl =
290
+ device.getColumnAccessControlFor("macAddress")!;
291
+
292
+ expect(flagAccess).toEqual(macAccess);
293
+ });
294
+ });
@@ -28,6 +28,7 @@ import {
28
28
  } from "../../../Server/Utils/Express";
29
29
  import InBetween from "../../../Types/BaseDatabase/InBetween";
30
30
  import Includes from "../../../Types/BaseDatabase/Includes";
31
+ import IncludesAnyOfGroups from "../../../Types/BaseDatabase/IncludesAnyOfGroups";
31
32
  import SortOrder from "../../../Types/BaseDatabase/SortOrder";
32
33
  import DashboardComponentType from "../../../Types/Dashboard/DashboardComponentType";
33
34
  import DashboardViewConfig from "../../../Types/Dashboard/DashboardViewConfig";
@@ -880,6 +881,134 @@ describe("DashboardAPI public resource-list", () => {
880
881
  expect(query["kind"]).toBe("Pod");
881
882
  });
882
883
 
884
+ describe("published monitor label variable selections", () => {
885
+ const firstLabelId: string = ObjectID.generate().toString();
886
+ const secondLabelId: string = ObjectID.generate().toString();
887
+ const fixedLabelId: string = ObjectID.generate().toString();
888
+ const labelVariable: JSONObject = {
889
+ id: "unit",
890
+ name: "UNIT",
891
+ type: DashboardVariableType.ProjectLabel,
892
+ labelOptions: [
893
+ { label: "Payments", value: firstLabelId },
894
+ { label: "Support", value: secondLabelId },
895
+ ],
896
+ defaultValue: firstLabelId,
897
+ };
898
+
899
+ it("applies the chosen label while preserving stored monitor policy and project", async () => {
900
+ const monitor: BuiltWidget = buildWidget({
901
+ componentType: DashboardComponentType.MonitorList,
902
+ argumentsObject: {
903
+ labelVariableId: "unit",
904
+ labelIds: [fixedLabelId],
905
+ statusFilter: "operational",
906
+ monitorTypes: ["Ping"],
907
+ maxRows: 6,
908
+ },
909
+ });
910
+ setDashboardWidgets([monitor.widget], [labelVariable]);
911
+
912
+ await callRoute({
913
+ resourceType: "monitor",
914
+ body: {
915
+ componentId: monitor.componentId.toString(),
916
+ variables: [{ id: "unit", selectedValue: secondLabelId }],
917
+ },
918
+ });
919
+
920
+ expect(nextFunction).not.toHaveBeenCalled();
921
+ const findBy: JSONObject = getFindByArgs(MonitorService);
922
+ expect(findBy["query"]).toEqual({
923
+ projectId,
924
+ currentMonitorStatus: { isOperationalState: true },
925
+ monitorType: new Includes(["Ping"]),
926
+ labels: new IncludesAnyOfGroups([[fixedLabelId], [secondLabelId]]),
927
+ });
928
+ expect(findBy["limit"]).toBe(6);
929
+ expect(findBy["sort"]).toEqual({ name: SortOrder.Ascending });
930
+ expect(findBy["select"]).toEqual({
931
+ _id: true,
932
+ name: true,
933
+ monitorType: true,
934
+ currentMonitorStatus: { name: true, color: true },
935
+ });
936
+ });
937
+
938
+ it("retains fixed labels when the public viewer chooses All", async () => {
939
+ const monitor: BuiltWidget = buildWidget({
940
+ componentType: DashboardComponentType.MonitorList,
941
+ argumentsObject: {
942
+ labelVariableId: "unit",
943
+ labelIds: [fixedLabelId],
944
+ },
945
+ });
946
+ setDashboardWidgets([monitor.widget], [labelVariable]);
947
+
948
+ await callRoute({
949
+ resourceType: "monitor",
950
+ body: {
951
+ componentId: monitor.componentId.toString(),
952
+ variables: [{ id: "unit", selectedValue: "" }],
953
+ },
954
+ });
955
+
956
+ expect(nextFunction).not.toHaveBeenCalled();
957
+ expect(getFindByArgs(MonitorService)["query"]).toEqual({
958
+ projectId,
959
+ labels: new Includes([fixedLabelId]),
960
+ });
961
+ });
962
+
963
+ it("accepts several published label IDs for a saved multi-select", async () => {
964
+ const monitor: BuiltWidget = buildWidget({
965
+ componentType: DashboardComponentType.MonitorList,
966
+ argumentsObject: { labelVariableId: "unit" },
967
+ });
968
+ setDashboardWidgets(
969
+ [monitor.widget],
970
+ [{ ...labelVariable, isMultiSelect: true }],
971
+ );
972
+
973
+ await callRoute({
974
+ resourceType: "monitor",
975
+ body: {
976
+ componentId: monitor.componentId.toString(),
977
+ variables: [
978
+ { id: "unit", selectedValues: [firstLabelId, secondLabelId] },
979
+ ],
980
+ },
981
+ });
982
+
983
+ expect(nextFunction).not.toHaveBeenCalled();
984
+ expect(getFindByArgs(MonitorService)["query"]).toEqual({
985
+ projectId,
986
+ labels: new Includes([firstLabelId, secondLabelId]),
987
+ });
988
+ });
989
+
990
+ it("reports an invalid selection before reading monitors when a choice was removed", async () => {
991
+ const monitor: BuiltWidget = buildWidget({
992
+ componentType: DashboardComponentType.MonitorList,
993
+ argumentsObject: { labelVariableId: "unit" },
994
+ });
995
+ setDashboardWidgets([monitor.widget], [labelVariable]);
996
+
997
+ await callRoute({
998
+ resourceType: "monitor",
999
+ body: {
1000
+ componentId: monitor.componentId.toString(),
1001
+ variables: [
1002
+ { id: "unit", selectedValue: ObjectID.generate().toString() },
1003
+ ],
1004
+ },
1005
+ });
1006
+
1007
+ expect(getThrownError()).toBeInstanceOf(BadDataException);
1008
+ expectNothingListed();
1009
+ });
1010
+ });
1011
+
883
1012
  it("passes only dynamic time and bounded variable selections into policy", async () => {
884
1013
  const buildSpy: ReturnType<typeof jest.spyOn> = jest.spyOn(
885
1014
  PublicDashboardResourceListPolicy,