@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,177 @@
1
+ import { normalizeMac } from "./EndpointAttachmentUtil";
2
+
3
+ /*
4
+ * Pure planner for learning a managed device's MAC address from a walked
5
+ * router's ARP table.
6
+ *
7
+ * WHY. A ping-only device is put on its switch port by the switch's
8
+ * forwarding table, which knows MACs and nothing else - and a ping-only
9
+ * device is exactly the device nothing ever asked for its MAC. A router's
10
+ * ARP table is the bridge: it binds the address the device is registered at
11
+ * to the MAC the switches learned. Writing that MAC onto the device once is
12
+ * what keeps the device on its port after the ARP entry ages out, and after
13
+ * the address changes.
14
+ *
15
+ * Deterministic and side-effect free, like EndpointAttachmentUtil: the
16
+ * server util (NetworkDeviceMacLearningUtil) fetches the candidate devices,
17
+ * hands them here, and applies whatever comes back.
18
+ */
19
+
20
+ export interface MacLearningIpBinding {
21
+ macAddress: string;
22
+ ipAddress: string;
23
+ }
24
+
25
+ // One NetworkDevice row, reduced to what the plan needs.
26
+ export interface MacLearningCandidateDevice {
27
+ id: string;
28
+ hostname?: string | undefined;
29
+ // A string, never an ObjectID: the plan keys on it.
30
+ siteId?: string | undefined;
31
+ macAddress?: string | undefined;
32
+ /*
33
+ * True when the stored MAC was written by an earlier pass rather than
34
+ * typed. Only such a value may be corrected; a typed one is never touched.
35
+ */
36
+ isMacAddressLearned?: boolean | undefined;
37
+ }
38
+
39
+ export interface MacLearningPlanEntry {
40
+ deviceId: string;
41
+ macAddress: string;
42
+ ipAddress: string;
43
+ /*
44
+ * What the column held when the plan was made: null for an empty column,
45
+ * the previously learned MAC for a correction. The writer compares-and-
46
+ * sets against it, so a value that changed under the plan is left alone.
47
+ */
48
+ previousMacAddress: string | null;
49
+ }
50
+
51
+ /*
52
+ * null, undefined, "" and whitespace all read as the same "no site" key -
53
+ * the convention NetworkDeviceLinkRuleUtil and the topology builder use.
54
+ */
55
+ export function siteKeyOf(siteId: string | null | undefined): string {
56
+ return (siteId || "").trim();
57
+ }
58
+
59
+ export default class DeviceMacLearningUtil {
60
+ /*
61
+ * Which devices get which MAC. The rules, every one of them a refusal:
62
+ *
63
+ * - Only a device in the SAME site as the router that reported the
64
+ * binding. Every branch has a 10.0.0.5; a project-wide match would
65
+ * stamp one store's register with another store's MAC.
66
+ * - Only a device whose MAC column is EMPTY, or holds a MAC an earlier
67
+ * pass LEARNED that this table now contradicts. What an operator typed
68
+ * is theirs and is never touched. A learned value is the pass's own
69
+ * answer, and an ARP table is exactly the evidence that can be stale
70
+ * (an address re-leased the day before the register was plugged in
71
+ * stamps the previous holder's MAC), so the pass corrects itself when
72
+ * the address is later bound to a different MAC.
73
+ * - Only an address exactly one candidate device answers at. Two
74
+ * devices registered at one address is a contradiction, and a wrong
75
+ * MAC drawn as a cable is worse than none.
76
+ * - Only an address the ARP table binds to exactly one MAC. Proxy ARP
77
+ * and stale duplicates put two MACs behind one address; neither can
78
+ * be trusted.
79
+ *
80
+ * Output is sorted by device id, so a given input always plans the same
81
+ * writes in the same order.
82
+ */
83
+ public static planMacLearning(data: {
84
+ ipBindings: Array<MacLearningIpBinding>;
85
+ devices: Array<MacLearningCandidateDevice>;
86
+ observingSiteId?: string | null | undefined;
87
+ }): Array<MacLearningPlanEntry> {
88
+ const macsByIp: Map<string, Set<string>> = new Map<string, Set<string>>();
89
+ for (const binding of data.ipBindings) {
90
+ const mac: string | undefined = normalizeMac(binding.macAddress);
91
+ const ip: string = (binding.ipAddress || "").trim();
92
+ if (!mac || !ip) {
93
+ continue;
94
+ }
95
+ const macs: Set<string> | undefined = macsByIp.get(ip);
96
+ if (macs) {
97
+ macs.add(mac);
98
+ } else {
99
+ macsByIp.set(ip, new Set<string>([mac]));
100
+ }
101
+ }
102
+
103
+ const siteKey: string = siteKeyOf(data.observingSiteId);
104
+
105
+ const devicesByIp: Map<string, Array<MacLearningCandidateDevice>> = new Map<
106
+ string,
107
+ Array<MacLearningCandidateDevice>
108
+ >();
109
+ for (const device of data.devices) {
110
+ const hostname: string = (device.hostname || "").trim();
111
+ if (!hostname || !macsByIp.has(hostname)) {
112
+ continue;
113
+ }
114
+ if (siteKeyOf(device.siteId) !== siteKey) {
115
+ continue;
116
+ }
117
+ const bucket: Array<MacLearningCandidateDevice> | undefined =
118
+ devicesByIp.get(hostname);
119
+ if (bucket) {
120
+ bucket.push(device);
121
+ } else {
122
+ devicesByIp.set(hostname, [device]);
123
+ }
124
+ }
125
+
126
+ const plan: Array<MacLearningPlanEntry> = [];
127
+
128
+ for (const [ip, candidates] of devicesByIp) {
129
+ // Two devices at one address: neither can be the one.
130
+ if (candidates.length !== 1) {
131
+ continue;
132
+ }
133
+ const macs: Set<string> = macsByIp.get(ip)!;
134
+ // Two MACs behind one address: neither can be trusted.
135
+ if (macs.size !== 1) {
136
+ continue;
137
+ }
138
+ const device: MacLearningCandidateDevice = candidates[0]!;
139
+ const learnedMac: string = Array.from(macs)[0]!;
140
+ const storedMac: string | undefined = normalizeMac(device.macAddress);
141
+ const hasStoredValue: boolean =
142
+ (device.macAddress || "").trim().length > 0;
143
+
144
+ if (!hasStoredValue) {
145
+ plan.push({
146
+ deviceId: device.id,
147
+ macAddress: learnedMac,
148
+ ipAddress: ip,
149
+ previousMacAddress: null,
150
+ });
151
+ continue;
152
+ }
153
+
154
+ // Typed by somebody, or already what the table says: leave it.
155
+ if (device.isMacAddressLearned !== true || storedMac === learnedMac) {
156
+ continue;
157
+ }
158
+
159
+ plan.push({
160
+ deviceId: device.id,
161
+ macAddress: learnedMac,
162
+ ipAddress: ip,
163
+ /*
164
+ * The raw stored value, not the normalised one: the writer's
165
+ * compare-and-set has to match what the column actually holds.
166
+ */
167
+ previousMacAddress: device.macAddress || null,
168
+ });
169
+ }
170
+
171
+ plan.sort((a: MacLearningPlanEntry, b: MacLearningPlanEntry) => {
172
+ return a.deviceId < b.deviceId ? -1 : a.deviceId > b.deviceId ? 1 : 0;
173
+ });
174
+
175
+ return plan;
176
+ }
177
+ }
@@ -0,0 +1,342 @@
1
+ import MetricUnitUtil from "../MetricUnitUtil";
2
+ import ValueFormatter from "../ValueFormatter";
3
+
4
+ /*
5
+ * How a metric sample is written into alert / incident notification text.
6
+ *
7
+ * The dashboard has always scaled metric values before showing them —
8
+ * `ValueFormatter` turns 1073741824 bytes into "1.07 GB" for every chart,
9
+ * gauge and value tile. The notification path never did. The same memory
10
+ * breach that reads "1.07 GB" on the monitor page arrived in the on-call
11
+ * engineer's inbox as the bare digits "1073741824 By", and in the
12
+ * "Affected Resources" tables as a Value column with no unit at all — a
13
+ * column of ten-digit numbers the reader has to divide by 2^30 in their
14
+ * head at 3am to find out whether the pod is anywhere near its limit.
15
+ *
16
+ * This module is the single place that decides how a (value, unit) pair
17
+ * is rendered for a notification, so the breaching-samples table, the
18
+ * "Filter Conditions Met" sentence, the observation summary and the
19
+ * per-platform resource tables cannot drift apart inside one email.
20
+ *
21
+ * WHY IT WRAPS ValueFormatter RATHER THAN BEING A CALL TO IT.
22
+ *
23
+ * Three things differ between a chart axis and an alert body:
24
+ *
25
+ * 1. ABBREVIATION. `ValueFormatter` shortens any number over 1000 to
26
+ * "1.5K" — right for a tick label, which has no horizontal room, and
27
+ * wrong for an alert, where "restarted 1.5K times" hides the
28
+ * difference between 1500 and 1549 that the reader opened the table
29
+ * to find. Here, only a unit that names an actual SCALE LADDER
30
+ * (bytes → KB → MB, seconds → min → hours) may rescale a number.
31
+ * Everything else keeps its digits and simply gets its unit spelled
32
+ * out: "1234567 cores", not "1.23M cores".
33
+ *
34
+ * 2. PREFIXED UNITS. A user who typed a threshold in "GB" makes every
35
+ * sample arrive carrying the unit "GB", and ValueFormatter has a
36
+ * ladder for "bytes" but not for "GB" — so 2500 of them rendered
37
+ * "2.5K GB" instead of "2.5 TB". Anything in a MetricUnitUtil family
38
+ * is converted to that family's base before the ladder runs.
39
+ *
40
+ * 3. UNITS WORTH HIDING. OTel's dimensionless "1" and UCUM's
41
+ * annotation-only units ("{restarts}", "{packets}") are not
42
+ * dimensions, and printing them produces the "0.06 1" that
43
+ * CompareCriteria and MonitorCriteriaObservationBuilder each grew
44
+ * their own guard against. That rule lives here now.
45
+ */
46
+ export default class MetricValueFormatter {
47
+ /*
48
+ * UCUM annotation-only units — "{thread}", "{packets}", "{errors}".
49
+ * The braces describe what is being counted; the value is a plain
50
+ * count and the text inside them is not a unit a reader should see.
51
+ */
52
+ private static readonly annotationOnlyUnitPattern: RegExp = /^\{[^{}]*\}$/;
53
+
54
+ /*
55
+ * The per-platform metric catalogs (Kubernetes, Ceph, Proxmox, Docker
56
+ * Swarm) were written for humans reading a metric picker, so they spell
57
+ * two dimensionless concepts in English rather than in UCUM. "count"
58
+ * appears on 43 catalog entries and means exactly what UCUM's "1" means
59
+ * on a counter — printing it gives "15 count", which reads as a typo.
60
+ * "ratio" means what UCUM's "1" means on a fraction metric: a value in
61
+ * [0, 1] that a reader wants as a percentage.
62
+ *
63
+ * Rewriting them here rather than in each catalog keeps the catalogs
64
+ * legible to the people who edit them and still gives every consumer of
65
+ * this formatter one vocabulary.
66
+ */
67
+ private static readonly informalUnitAliases: Record<string, string> = {
68
+ count: "",
69
+ counts: "",
70
+ ratio: "1",
71
+ fraction: "1",
72
+ };
73
+
74
+ /*
75
+ * Metrics whose unit says "seconds" but whose VALUE is a POINT in time
76
+ * rather than a length of one.
77
+ *
78
+ * Proxmox has three: pve_replication_last_sync_timestamp_seconds,
79
+ * _last_try_ and _next_sync_ — all carrying `unit: "seconds"`, all
80
+ * holding a Unix epoch. Sent through the duration ladder, an epoch of
81
+ * 1750000000 renders "20.25K days", which is not merely unhelpful: it
82
+ * READS as a duration, so a replication job that synced a minute ago
83
+ * looks 55 years stale. The bare number at least cannot be misread as
84
+ * something it is not.
85
+ *
86
+ * Same shape as ValueFormatter's kubeletstats exclusion: a narrow,
87
+ * name-anchored carve-out for a unit the exporter declared wrongly.
88
+ */
89
+ private static readonly absoluteTimestampMetricPattern: RegExp =
90
+ /(^|[._])timestamps?([._]seconds?)?$/i;
91
+
92
+ /**
93
+ * Whether `unit` names a real dimension for `metricName` — i.e. whether
94
+ * it is worth attaching to a number at all. False for the empty unit,
95
+ * for the dimensionless "1" on a non-fraction metric, and for
96
+ * annotation-only units.
97
+ */
98
+ public static hasDisplayableUnit(
99
+ unit: string | null | undefined,
100
+ metricName?: string | null | undefined,
101
+ ): boolean {
102
+ const trimmed: string = MetricValueFormatter.canonicalizeUnit(unit);
103
+
104
+ if (!trimmed) {
105
+ return false;
106
+ }
107
+
108
+ /*
109
+ * "1" is OTel's dimensionless marker. On a `.utilization` / `.ratio`
110
+ * metric it means "this is a [0, 1] fraction", which IS a scale (× 100
111
+ * into a percent) and is why the fraction case is excluded here rather
112
+ * than folded in. On anything else it is noise: "0.06 1" is not just
113
+ * ugly, it reads as 1% when the value is 6%.
114
+ */
115
+ if (trimmed === "1") {
116
+ return ValueFormatter.isFractionMetric(metricName || undefined);
117
+ }
118
+
119
+ return !MetricValueFormatter.annotationOnlyUnitPattern.test(trimmed);
120
+ }
121
+
122
+ /**
123
+ * Format one metric sample for a notification.
124
+ *
125
+ * Units with a scale ladder are rescaled, exactly as the dashboard does:
126
+ * format({ value: 1073741824, unit: "By" }) → "1.07 GB"
127
+ * format({ value: 2500, unit: "GB" }) → "2.5 TB"
128
+ * format({ value: 1500, unit: "ms" }) → "1.5 sec"
129
+ * format({ value: 3661, unit: "seconds" }) → "1.02 hours"
130
+ * format({ value: 1500000, unit: "By/s" }) → "1.5 MB/s"
131
+ * format({ value: 0.2534, unit: "1",
132
+ * metricName: "system.cpu.utilization" }) → "25.34%"
133
+ * format({ value: 87.5, unit: "%" }) → "87.50%"
134
+ *
135
+ * Units without one keep every digit and are merely labelled:
136
+ * format({ value: 1234567, unit: "cores" }) → "1234567 cores"
137
+ * format({ value: 95, unit: "Cel" }) → "95 Celsius"
138
+ *
139
+ * A value with no dimension to report is left bare:
140
+ * format({ value: 1500 }) → "1500"
141
+ * format({ value: 1500, unit: "{restarts}" }) → "1500"
142
+ * format({ value: 3.14159 }) → "3.14"
143
+ */
144
+ public static format(input: {
145
+ value: number;
146
+ unit?: string | null | undefined;
147
+ metricName?: string | null | undefined;
148
+ }): string {
149
+ if (!Number.isFinite(input.value)) {
150
+ return String(input.value);
151
+ }
152
+
153
+ const unit: string = MetricValueFormatter.canonicalizeUnit(input.unit);
154
+ const metricName: string | undefined = input.metricName || undefined;
155
+
156
+ if (!MetricValueFormatter.hasDisplayableUnit(unit, metricName)) {
157
+ return MetricValueFormatter.formatBareNumber(input.value);
158
+ }
159
+
160
+ const formatOptions: { metricName?: string } = metricName
161
+ ? { metricName: metricName }
162
+ : {};
163
+
164
+ /*
165
+ * Percent first. ValueFormatter renders "%" and the fraction-metric
166
+ * "1" as a two-decimal percentage, and neither is reachable through
167
+ * the ladder path below (there is no percent threshold table, and the
168
+ * percent family's base unit "%" has no ladder either).
169
+ */
170
+ if (
171
+ ValueFormatter.isPercentUnit(unit) ||
172
+ (unit === "1" && ValueFormatter.isFractionMetric(metricName))
173
+ ) {
174
+ return ValueFormatter.formatValue(input.value, unit, formatOptions);
175
+ }
176
+
177
+ const scalable: { value: number; unit: string } | null =
178
+ MetricValueFormatter.isAbsoluteTimestampMetric(metricName)
179
+ ? null
180
+ : MetricValueFormatter.toScalableUnit(input.value, unit);
181
+
182
+ if (scalable) {
183
+ return ValueFormatter.formatValue(
184
+ scalable.value,
185
+ scalable.unit,
186
+ formatOptions,
187
+ );
188
+ }
189
+
190
+ /*
191
+ * No ladder for this unit: label the number, leave its digits alone.
192
+ *
193
+ * The SYMBOL rather than the spelled-out name, because this suffix
194
+ * sits directly against a number, where the ladder path also emits
195
+ * symbols ("1.07 GB", "1.5 sec"). getReadableUnit is for the "Unit:"
196
+ * label line, where a name reads better than a code — and it is
197
+ * actively wrong here for compound units, which it rewrites: "m/s2"
198
+ * becomes "m per s2" and "10*3/uL" becomes "10*3 per uL", both longer
199
+ * and less recognisable than what the exporter wrote. getCompactUnit
200
+ * keeps the slash and still expands what is worth expanding
201
+ * ("Cel" → "°C", "KiBy" → "KiB").
202
+ */
203
+ const unitSymbol: string =
204
+ ValueFormatter.getCompactUnit(unit, formatOptions) || unit;
205
+
206
+ return `${MetricValueFormatter.formatBareNumber(input.value)} ${unitSymbol}`;
207
+ }
208
+
209
+ /**
210
+ * The spelled-out name of a unit, for the "Unit:" line of a root cause —
211
+ * "Bytes" rather than the UCUM "By" the exporter wrote. Returns null when
212
+ * the unit names no dimension, so the caller can drop the line entirely
213
+ * instead of printing the noise "Unit: 1".
214
+ */
215
+ public static getReadableUnit(
216
+ unit: string | null | undefined,
217
+ metricName?: string | null | undefined,
218
+ ): string | null {
219
+ if (!MetricValueFormatter.hasDisplayableUnit(unit, metricName)) {
220
+ return null;
221
+ }
222
+
223
+ const readable: string = ValueFormatter.getReadableUnit(
224
+ MetricValueFormatter.canonicalizeUnit(unit),
225
+ metricName ? { metricName: metricName } : {},
226
+ );
227
+
228
+ return readable.trim() ? readable : null;
229
+ }
230
+
231
+ /*
232
+ * Trim the unit and rewrite the catalogs' English spellings of "no
233
+ * dimension" into the UCUM ones the rest of the pipeline understands.
234
+ * Anything else is returned untouched — including its casing, which
235
+ * matters because an unrecognised unit is echoed verbatim next to the
236
+ * value ("5 widgets", not "5 Widgets").
237
+ */
238
+ private static canonicalizeUnit(unit: string | null | undefined): string {
239
+ const trimmed: string = (unit || "").trim();
240
+
241
+ if (!trimmed) {
242
+ return "";
243
+ }
244
+
245
+ const alias: string | undefined =
246
+ MetricValueFormatter.informalUnitAliases[trimmed.toLowerCase()];
247
+
248
+ return alias === undefined ? trimmed : alias;
249
+ }
250
+
251
+ /**
252
+ * Whether this metric's value is a point in time rather than a quantity,
253
+ * and so must never be run through a scale ladder. See
254
+ * absoluteTimestampMetricPattern.
255
+ */
256
+ public static isAbsoluteTimestampMetric(
257
+ metricName: string | null | undefined,
258
+ ): boolean {
259
+ if (!metricName) {
260
+ return false;
261
+ }
262
+
263
+ return MetricValueFormatter.absoluteTimestampMetricPattern.test(
264
+ metricName.trim(),
265
+ );
266
+ }
267
+
268
+ /*
269
+ * Restate (value, unit) in a unit ValueFormatter has a scale ladder for,
270
+ * or return null when no ladder can apply.
271
+ *
272
+ * Three ways a unit gets there:
273
+ * - it already has one ("bytes", "ms", "s", "ns");
274
+ * - it is a rate whose NUMERATOR does ("By/s" → scale the bytes, keep
275
+ * the "/s"), which is how 1500000 By/s reads "1.5 MB/s";
276
+ * - it is a prefixed member of a MetricUnitUtil family ("GB", "hours"),
277
+ * in which case the value is converted into that family's base first.
278
+ */
279
+ private static toScalableUnit(
280
+ value: number,
281
+ unit: string,
282
+ ): { value: number; unit: string } | null {
283
+ if (!unit) {
284
+ return null;
285
+ }
286
+
287
+ if (ValueFormatter.isScalableUnit(unit)) {
288
+ return { value: value, unit: unit };
289
+ }
290
+
291
+ if (unit.includes("/")) {
292
+ const [numerator, ...denominatorParts] = unit.split("/");
293
+ const denominator: string = denominatorParts.join("/").trim();
294
+
295
+ if (!numerator || !numerator.trim() || !denominator) {
296
+ return null;
297
+ }
298
+
299
+ const scaledNumerator: { value: number; unit: string } | null =
300
+ MetricValueFormatter.toScalableUnit(value, numerator.trim());
301
+
302
+ if (!scaledNumerator) {
303
+ return null;
304
+ }
305
+
306
+ return {
307
+ value: scaledNumerator.value,
308
+ unit: `${scaledNumerator.unit}/${denominator}`,
309
+ };
310
+ }
311
+
312
+ const baseUnit: string | null = MetricUnitUtil.getFamilyBaseUnit(unit);
313
+
314
+ if (!baseUnit || !ValueFormatter.isScalableUnit(baseUnit)) {
315
+ return null;
316
+ }
317
+
318
+ return {
319
+ value: MetricUnitUtil.convertToMetricUnit({
320
+ value: value,
321
+ fromUnit: unit,
322
+ metricUnit: baseUnit,
323
+ }),
324
+ unit: baseUnit,
325
+ };
326
+ }
327
+
328
+ /*
329
+ * The unscaled rendering: integers verbatim, everything else rounded to
330
+ * two decimals with trailing zeros dropped (1.50 → "1.5"). This is the
331
+ * behaviour the root-cause tables had before units arrived, and every
332
+ * value without a scale ladder keeps it exactly, so a counter's digits
333
+ * survive the trip to the inbox.
334
+ */
335
+ private static formatBareNumber(value: number): string {
336
+ if (Number.isInteger(value)) {
337
+ return value.toString();
338
+ }
339
+
340
+ return Number(value.toFixed(2)).toString();
341
+ }
342
+ }
@@ -0,0 +1,124 @@
1
+ import { JSONObject, JSONValue } from "../../Types/JSON";
2
+ import JSONFunctions from "../../Types/JSONFunctions";
3
+
4
+ /*
5
+ * WHAT A MONITOR TEMPLATE'S CUSTOM FIELDS MEAN, in one place.
6
+ *
7
+ * A template's `customFields` bag is a set of DEFAULTS for the monitors made
8
+ * from it — the answer to issue #3548, where an auto-import rule turned a
9
+ * thousand discovered devices into a thousand monitors whose Vendor, Services
10
+ * and Configuration Item fields were all empty and could only be filled in one
11
+ * monitor at a time.
12
+ *
13
+ * Two callers need the same answer and would drift apart if each wrote its
14
+ * own:
15
+ *
16
+ * - PROVISIONING (NetworkDeviceMonitorTemplateUtil.buildMonitor, reached
17
+ * from the auto-import engine and the alert-policy engine) starts from a
18
+ * monitor with no values at all, so it takes the whole bag.
19
+ * - SYNC (MonitorTemplateService) pushes the template onto monitors that
20
+ * already exist and may already carry values an operator typed in, so it
21
+ * OVERLAYS instead: every field the template actually gives a default for
22
+ * wins, and every other field on the monitor is left exactly as it is.
23
+ *
24
+ * Both go through a deep clone, because a jsonb bag handed to several monitors
25
+ * by reference is one nested edit away from leaking across the fleet — and
26
+ * back onto the template itself.
27
+ */
28
+ export default class MonitorTemplateCustomFieldUtil {
29
+ /*
30
+ * A defaults bag is only worth writing when there is a value in it, so
31
+ * "template has no defaults" and "template has an empty bag" are the same
32
+ * answer to every caller here. This is what keeps a sync scoped to custom
33
+ * fields alone from reporting that it wrote to a fleet it did not touch.
34
+ */
35
+ public static hasDefaults(
36
+ customFields: JSONObject | undefined | null,
37
+ ): boolean {
38
+ return Object.keys(this.getDefaults(customFields)).length > 0;
39
+ }
40
+
41
+ /*
42
+ * The template's defaults, deep-cloned, with the fields it does not actually
43
+ * default DROPPED.
44
+ *
45
+ * A template carries a key for every custom field the project has defined
46
+ * the moment somebody saves the edit form — the form submits its whole
47
+ * values object — so most keys in a freshly saved bag hold nothing. Left in,
48
+ * they would make a sync overwrite real per-monitor values with blanks: a
49
+ * template that only defaults Vendor would silently clear Configuration Item
50
+ * on every monitor in the fleet.
51
+ *
52
+ * `false` and `0` are values, not blanks — a Boolean custom field defaulting
53
+ * to false and a Number defaulting to 0 are both things an operator means.
54
+ */
55
+ public static getDefaults(
56
+ customFields: JSONObject | undefined | null,
57
+ ): JSONObject {
58
+ if (!customFields) {
59
+ return {};
60
+ }
61
+
62
+ const defaults: JSONObject = {};
63
+
64
+ for (const key of Object.keys(customFields)) {
65
+ const value: JSONValue = customFields[key] as JSONValue;
66
+
67
+ if (this.isBlank(value)) {
68
+ continue;
69
+ }
70
+
71
+ defaults[key] = value;
72
+ }
73
+
74
+ return this.clone(defaults);
75
+ }
76
+
77
+ /*
78
+ * Overlay the template's defaults onto the values a monitor already has.
79
+ * Returns a new bag; neither input is mutated.
80
+ */
81
+ public static applyDefaults(data: {
82
+ templateCustomFields: JSONObject | undefined | null;
83
+ monitorCustomFields: JSONObject | undefined | null;
84
+ }): JSONObject {
85
+ return {
86
+ ...this.clone(data.monitorCustomFields || {}),
87
+ ...this.getDefaults(data.templateCustomFields),
88
+ };
89
+ }
90
+
91
+ /*
92
+ * The whole bag, verbatim apart from being deep-cloned — blanks included.
93
+ *
94
+ * Provisioning uses this rather than getDefaults because there is nothing
95
+ * underneath to protect: a monitor being created has no values of its own,
96
+ * so a blank default and an absent one look identical on the monitor, and
97
+ * copying the bag as written keeps "what the template says" and "what the
98
+ * new monitor got" literally the same object.
99
+ */
100
+ public static clone(customFields: JSONObject): JSONObject {
101
+ return JSONFunctions.deserialize(JSONFunctions.serialize(customFields));
102
+ }
103
+
104
+ /*
105
+ * Nothing an operator would call a default. An empty multi-select arrives as
106
+ * [], a cleared text field as "" (or as spaces, from a form nobody trimmed),
107
+ * and a field never touched as null or undefined.
108
+ */
109
+ private static isBlank(value: JSONValue): boolean {
110
+ if (value === undefined || value === null) {
111
+ return true;
112
+ }
113
+
114
+ if (typeof value === "string") {
115
+ return value.trim() === "";
116
+ }
117
+
118
+ if (Array.isArray(value)) {
119
+ return value.length === 0;
120
+ }
121
+
122
+ return false;
123
+ }
124
+ }
@@ -3,10 +3,10 @@ import MonitorTemplate from "../../Models/DatabaseModels/MonitorTemplate";
3
3
  import NetworkDevice from "../../Models/DatabaseModels/NetworkDevice";
4
4
  import BadDataException from "../../Types/Exception/BadDataException";
5
5
  import { JSONObject } from "../../Types/JSON";
6
- import JSONFunctions from "../../Types/JSONFunctions";
7
6
  import MonitorSteps from "../../Types/Monitor/MonitorSteps";
8
7
  import MonitorType from "../../Types/Monitor/MonitorType";
9
8
  import ObjectID from "../../Types/ObjectID";
9
+ import MonitorTemplateCustomFieldUtil from "./MonitorTemplateCustomFieldUtil";
10
10
 
11
11
  /*
12
12
  * Materialises a project MonitorTemplate for one discovered NetworkDevice.
@@ -379,7 +379,12 @@ export default class NetworkDeviceMonitorTemplateUtil {
379
379
  return truncated;
380
380
  }
381
381
 
382
+ /*
383
+ * Delegated so "what a template's custom fields mean" is written down once
384
+ * — provisioning takes the bag whole, sync overlays it. See
385
+ * MonitorTemplateCustomFieldUtil.
386
+ */
382
387
  private static cloneCustomFields(customFields: JSONObject): JSONObject {
383
- return JSONFunctions.deserialize(JSONFunctions.serialize(customFields));
388
+ return MonitorTemplateCustomFieldUtil.clone(customFields);
384
389
  }
385
390
  }