@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
@@ -5,7 +5,7 @@ import PermissionGate, {
5
5
  PermissionGateResult,
6
6
  } from "../../Utils/PermissionGate";
7
7
  import { ButtonStyleType } from "../Button/Button";
8
- import Card from "../Card/Card";
8
+ import Card, { CardButtonSchema } from "../Card/Card";
9
9
  import ComponentLoader from "../ComponentLoader/ComponentLoader";
10
10
  import Detail from "../Detail/Detail";
11
11
  import { DropdownOption } from "../Dropdown/Dropdown";
@@ -79,6 +79,22 @@ export interface ComponentProps {
79
79
  * real reason.
80
80
  */
81
81
  hideIfEmpty?: boolean | undefined;
82
+ /*
83
+ * Card buttons that belong beside "Edit Fields" rather than on a card of
84
+ * their own — the Monitor Template's "Sync Custom Fields to Linked Monitors"
85
+ * being the one that exists. They render after the edit button and are not
86
+ * touched by the gating above: whether a push to other records is offered is
87
+ * the caller's decision, not this card's.
88
+ */
89
+ additionalButtons?: Array<CardButtonSchema> | undefined;
90
+ /*
91
+ * The record's stored bag, handed back after every load — including the
92
+ * reload that follows a save — so a caller whose extra button acts on those
93
+ * values can enable and word it from what is actually stored, rather than
94
+ * re-reading the record behind this card's back and going stale the moment
95
+ * somebody edits it here.
96
+ */
97
+ onValuesLoaded?: ((customFields: JSONObject) => void) | undefined;
82
98
  }
83
99
 
84
100
  const CustomFieldsDetail: FunctionComponent<ComponentProps> = (
@@ -219,6 +235,12 @@ const CustomFieldsDetail: FunctionComponent<ComponentProps> = (
219
235
  setSchemaList(schemaList.data);
220
236
  setModel(item);
221
237
 
238
+ if (props.onValuesLoaded) {
239
+ props.onValuesLoaded(
240
+ ((item as any)?.["customFields"] as JSONObject) || {},
241
+ );
242
+ }
243
+
222
244
  setIsLoading(false);
223
245
  } catch (err) {
224
246
  setIsLoading(false);
@@ -313,30 +335,33 @@ const CustomFieldsDetail: FunctionComponent<ComponentProps> = (
313
335
  const showEditButton: boolean =
314
336
  canEdit && (updateGate.isAllowed || Boolean(updateGate.disabledReason));
315
337
 
338
+ const cardButtons: Array<CardButtonSchema> = [
339
+ ...(showEditButton
340
+ ? [
341
+ {
342
+ title: "Edit Fields",
343
+ buttonStyle: ButtonStyleType.NORMAL,
344
+ disabled: !updateGate.isAllowed,
345
+ tooltip: updateGate.disabledReason,
346
+ onClick: () => {
347
+ if (!updateGate.isAllowed) {
348
+ return;
349
+ }
350
+
351
+ setShowModelForm(true);
352
+ },
353
+ icon: IconProp.Edit,
354
+ } as CardButtonSchema,
355
+ ]
356
+ : []),
357
+ ...(props.additionalButtons || []),
358
+ ];
359
+
316
360
  return (
317
361
  <Card
318
362
  title={props.title}
319
363
  description={props.description}
320
- buttons={
321
- showEditButton
322
- ? [
323
- {
324
- title: "Edit Fields",
325
- buttonStyle: ButtonStyleType.NORMAL,
326
- disabled: !updateGate.isAllowed,
327
- tooltip: updateGate.disabledReason,
328
- onClick: () => {
329
- if (!updateGate.isAllowed) {
330
- return;
331
- }
332
-
333
- setShowModelForm(true);
334
- },
335
- icon: IconProp.Edit,
336
- },
337
- ]
338
- : []
339
- }
364
+ buttons={cardButtons}
340
365
  >
341
366
  <div className="border-t border-gray-200 px-4 py-5 sm:px-6 -m-6 -mt-2">
342
367
  {isLoading && !loadError && <ComponentLoader />}
@@ -0,0 +1,285 @@
1
+ import React, {
2
+ FunctionComponent,
3
+ ReactElement,
4
+ useEffect,
5
+ useRef,
6
+ useState,
7
+ } from "react";
8
+ import DashboardVariable, {
9
+ DashboardVariableType,
10
+ DashboardVariableOption,
11
+ } from "../../../Types/Dashboard/DashboardVariable";
12
+
13
+ export interface VariableValueChange {
14
+ selectedValue?: string | undefined;
15
+ selectedValues?: Array<string> | undefined;
16
+ }
17
+
18
+ export interface ComponentProps {
19
+ variable: DashboardVariable;
20
+ dynamicOptions?: Array<string> | undefined;
21
+ isLoadingOptions?: boolean | undefined;
22
+ onVariableValueChange: (
23
+ variableId: string,
24
+ change: VariableValueChange,
25
+ ) => void;
26
+ }
27
+
28
+ const MultiSelectPopover: FunctionComponent<{
29
+ options: Array<DashboardVariableOption>;
30
+ selected: Array<string>;
31
+ label: string;
32
+ isLoading: boolean;
33
+ maxSelections?: number | undefined;
34
+ unavailableLabel?: string | undefined;
35
+ onChange: (next: Array<string>) => void;
36
+ }> = ({
37
+ options,
38
+ selected,
39
+ label,
40
+ isLoading,
41
+ maxSelections,
42
+ unavailableLabel,
43
+ onChange,
44
+ }: {
45
+ options: Array<DashboardVariableOption>;
46
+ selected: Array<string>;
47
+ label: string;
48
+ isLoading: boolean;
49
+ maxSelections?: number | undefined;
50
+ unavailableLabel?: string | undefined;
51
+ onChange: (next: Array<string>) => void;
52
+ }): ReactElement => {
53
+ const [open, setOpen] = useState<boolean>(false);
54
+ const wrapRef: React.RefObject<HTMLDivElement> = useRef<HTMLDivElement>(null);
55
+
56
+ useEffect(() => {
57
+ if (!open) {
58
+ return undefined;
59
+ }
60
+ const onDocClick: (e: MouseEvent) => void = (e: MouseEvent): void => {
61
+ if (wrapRef.current && !wrapRef.current.contains(e.target as Node)) {
62
+ setOpen(false);
63
+ }
64
+ };
65
+ document.addEventListener("mousedown", onDocClick);
66
+ return () => {
67
+ document.removeEventListener("mousedown", onDocClick);
68
+ };
69
+ }, [open]);
70
+
71
+ const buttonText: string =
72
+ selected.length === 0
73
+ ? isLoading
74
+ ? "Loading…"
75
+ : "All"
76
+ : selected.length === 1
77
+ ? options.find((option: DashboardVariableOption) => {
78
+ return option.value === selected[0];
79
+ })?.label ||
80
+ unavailableLabel ||
81
+ (selected[0] as string)
82
+ : `${selected.length} selected`;
83
+
84
+ const toggle: (value: string) => void = (value: string): void => {
85
+ if (selected.includes(value)) {
86
+ onChange(
87
+ selected.filter((v: string) => {
88
+ return v !== value;
89
+ }),
90
+ );
91
+ } else {
92
+ onChange([...selected, value]);
93
+ }
94
+ };
95
+
96
+ return (
97
+ <div className="relative" ref={wrapRef}>
98
+ <button
99
+ type="button"
100
+ className="text-xs border border-gray-200 rounded-md px-2.5 py-1.5 bg-white text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-100 focus:border-blue-300 transition-colors flex items-center gap-1"
101
+ disabled={isLoading}
102
+ onClick={() => {
103
+ setOpen(!open);
104
+ }}
105
+ title={label}
106
+ aria-label={`${label}: ${buttonText}`}
107
+ aria-expanded={open}
108
+ >
109
+ <span className="truncate max-w-[10rem]">{buttonText}</span>
110
+ <span className="text-gray-400">▾</span>
111
+ </button>
112
+ {open && (
113
+ <div className="absolute z-30 mt-1 right-0 w-56 max-h-72 overflow-auto rounded-md border border-gray-200 bg-white shadow-lg py-1">
114
+ <div className="flex items-center justify-between px-3 py-1.5 border-b border-gray-100 text-[11px] text-gray-500">
115
+ <span>{selected.length} selected</span>
116
+ <button
117
+ type="button"
118
+ className="text-blue-600 hover:underline disabled:text-gray-300"
119
+ disabled={selected.length === 0}
120
+ onClick={() => {
121
+ onChange([]);
122
+ }}
123
+ >
124
+ Clear
125
+ </button>
126
+ </div>
127
+ {maxSelections !== undefined && selected.length >= maxSelections && (
128
+ <p className="px-3 py-1 text-xs text-gray-500">
129
+ Choose up to {maxSelections} labels.
130
+ </p>
131
+ )}
132
+ {options.length === 0 ? (
133
+ <div className="px-3 py-2 text-xs text-gray-400">
134
+ {isLoading ? "Loading options…" : "No options available"}
135
+ </div>
136
+ ) : (
137
+ options.map((option: DashboardVariableOption) => {
138
+ const checked: boolean = selected.includes(option.value);
139
+ return (
140
+ <label
141
+ key={option.value}
142
+ className="flex items-center gap-2 px-3 py-1.5 text-xs text-gray-700 hover:bg-gray-50 cursor-pointer"
143
+ >
144
+ <input
145
+ type="checkbox"
146
+ className="h-3.5 w-3.5"
147
+ checked={checked}
148
+ disabled={
149
+ !checked &&
150
+ maxSelections !== undefined &&
151
+ selected.length >= maxSelections
152
+ }
153
+ onChange={() => {
154
+ toggle(option.value);
155
+ }}
156
+ />
157
+ <span className="truncate">{option.label}</span>
158
+ </label>
159
+ );
160
+ })
161
+ )}
162
+ </div>
163
+ )}
164
+ </div>
165
+ );
166
+ };
167
+
168
+ const DashboardVariableControl: FunctionComponent<ComponentProps> = (
169
+ props: ComponentProps,
170
+ ): ReactElement => {
171
+ const { variable } = props;
172
+ const isTelemetryAttribute: boolean =
173
+ variable.type === DashboardVariableType.TelemetryAttribute;
174
+ const isCustomList: boolean =
175
+ variable.type === DashboardVariableType.CustomList ||
176
+ Boolean(variable.customListValues);
177
+
178
+ const customListOptions: Array<string> = variable.customListValues
179
+ ? variable.customListValues.split(",").map((v: string) => {
180
+ return v.trim();
181
+ })
182
+ : [];
183
+
184
+ const isProjectLabel: boolean =
185
+ variable.type === DashboardVariableType.ProjectLabel;
186
+ const options: Array<DashboardVariableOption> = isProjectLabel
187
+ ? variable.labelOptions || []
188
+ : (isTelemetryAttribute
189
+ ? props.dynamicOptions || []
190
+ : customListOptions
191
+ ).map((value: string) => {
192
+ return { label: value, value };
193
+ });
194
+ const selectedValue: string =
195
+ variable.selectedValue ?? variable.defaultValue ?? "";
196
+ const isUnavailableSelection: boolean = Boolean(
197
+ isProjectLabel &&
198
+ selectedValue &&
199
+ !options.some((option: DashboardVariableOption) => {
200
+ return option.value === selectedValue;
201
+ }),
202
+ );
203
+
204
+ const useSelect: boolean =
205
+ isTelemetryAttribute || isCustomList || isProjectLabel;
206
+ const label: string = variable.label || variable.name;
207
+
208
+ return (
209
+ <div className="flex items-center gap-1.5">
210
+ <label className="text-xs font-medium text-gray-400 uppercase tracking-wide">
211
+ {label}
212
+ </label>
213
+ {useSelect && variable.isMultiSelect ? (
214
+ /*
215
+ * `selectedValues` only — never defaultValue. The popover renders
216
+ * "All" for an empty list, and DashboardVariableInterpolation
217
+ * resolves an empty list to no predicate, so the two agree. Seeding
218
+ * this from defaultValue would put a filter behind a control that
219
+ * reads "All".
220
+ */
221
+ <MultiSelectPopover
222
+ options={options}
223
+ selected={variable.selectedValues || []}
224
+ label={label}
225
+ isLoading={Boolean(props.isLoadingOptions)}
226
+ maxSelections={isProjectLabel ? 100 : undefined}
227
+ unavailableLabel={isProjectLabel ? "Unavailable label" : undefined}
228
+ onChange={(next: Array<string>) => {
229
+ props.onVariableValueChange(variable.id, { selectedValues: next });
230
+ }}
231
+ />
232
+ ) : useSelect ? (
233
+ <select
234
+ aria-label={label}
235
+ className="text-xs border border-gray-200 rounded-md px-2.5 py-1.5 bg-white text-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-100 focus:border-blue-300 transition-colors"
236
+ /*
237
+ * `??`, not `||`: picking "All" stores selectedValue as "", which is
238
+ * a real selection and must not collapse back to defaultValue. Only
239
+ * an unset (undefined) selection falls through to the default — the
240
+ * same rule the query side applies in
241
+ * Common/Utils/Dashboard/VariableInterpolation.ts, so what the
242
+ * toolbar shows and what the widgets are filtered by stay in step.
243
+ */
244
+ value={variable.selectedValue ?? variable.defaultValue ?? ""}
245
+ disabled={props.isLoadingOptions}
246
+ onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
247
+ props.onVariableValueChange(variable.id, {
248
+ selectedValue: e.target.value,
249
+ });
250
+ }}
251
+ >
252
+ <option value="">
253
+ {props.isLoadingOptions ? "Loading…" : "All"}
254
+ </option>
255
+ {isUnavailableSelection && (
256
+ <option value={selectedValue}>Unavailable label</option>
257
+ )}
258
+ {options.map((option: DashboardVariableOption) => {
259
+ return (
260
+ <option key={option.value} value={option.value}>
261
+ {option.label}
262
+ </option>
263
+ );
264
+ })}
265
+ </select>
266
+ ) : (
267
+ <input
268
+ type="text"
269
+ aria-label={label}
270
+ className="text-xs border border-gray-200 rounded-md px-2.5 py-1.5 bg-white text-gray-700 w-28 focus:outline-none focus:ring-2 focus:ring-blue-100 focus:border-blue-300 transition-colors"
271
+ // `??` so clearing the box stays cleared. See the select above.
272
+ value={variable.selectedValue ?? variable.defaultValue ?? ""}
273
+ placeholder={variable.name}
274
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
275
+ props.onVariableValueChange(variable.id, {
276
+ selectedValue: e.target.value,
277
+ });
278
+ }}
279
+ />
280
+ )}
281
+ </div>
282
+ );
283
+ };
284
+
285
+ export default DashboardVariableControl;
@@ -304,6 +304,8 @@ function createConfig(options) {
304
304
  const isDev = process.env.NODE_ENV !== "production";
305
305
  const isAnalyze = process.env.analyze === "true";
306
306
  const reactRoot = resolvePackageRoot("react");
307
+ const reactRouterRoot = resolvePackageRoot("react-router");
308
+ const reactRouterDomRoot = resolvePackageRoot("react-router-dom");
307
309
  const reactI18nextRoot = resolvePackageRoot("react-i18next");
308
310
  const i18nextRoot = resolvePackageRoot("i18next");
309
311
 
@@ -356,6 +358,10 @@ function createConfig(options) {
356
358
  react: reactRoot,
357
359
  "react/jsx-runtime": path.join(reactRoot, "jsx-runtime.js"),
358
360
  "react/jsx-dev-runtime": path.join(reactRoot, "jsx-dev-runtime.js"),
361
+ // Public dashboards reuse widgets from the private dashboard package.
362
+ // Their links must share the router context installed by the entry app.
363
+ "react-router": reactRouterRoot,
364
+ "react-router-dom": reactRouterDomRoot,
359
365
  // Force a single instance of i18next/react-i18next so that translations
360
366
  // initialized in the service entry are visible to Common UI components.
361
367
  // Without this, Common's own node_modules copy gets a separate, never-
@@ -74,7 +74,8 @@ export default class DashboardMonitorListComponentUtil extends DashboardBaseComp
74
74
 
75
75
  componentArguments.push({
76
76
  name: "Title",
77
- description: "Header shown above the monitor list",
77
+ description:
78
+ "Header shown above the monitor list. Use {{UNIT}} to show a dashboard variable’s selected label; configure Label Variable separately to filter monitors.",
78
79
  required: false,
79
80
  type: ComponentInputType.Text,
80
81
  id: "title",
@@ -142,6 +143,16 @@ export default class DashboardMonitorListComponentUtil extends DashboardBaseComp
142
143
  entityFilterModelType: EntityFilterModelType.Label,
143
144
  });
144
145
 
146
+ componentArguments.push({
147
+ name: "Label Variable",
148
+ description:
149
+ "Filter monitors using a Project Labels dashboard variable. This is combined with the fixed filters above. All removes only the variable filter.",
150
+ required: false,
151
+ type: ComponentInputType.ProjectLabelVariable,
152
+ id: "labelVariableId",
153
+ section: FiltersSection,
154
+ });
155
+
145
156
  return componentArguments;
146
157
  }
147
158
  }
@@ -0,0 +1,152 @@
1
+ import Includes from "../../Types/BaseDatabase/Includes";
2
+ import IncludesAnyOfGroups from "../../Types/BaseDatabase/IncludesAnyOfGroups";
3
+ import DashboardVariable, {
4
+ DashboardVariableOption,
5
+ DashboardVariableType,
6
+ } from "../../Types/Dashboard/DashboardVariable";
7
+ import BadDataException from "../../Types/Exception/BadDataException";
8
+ import ObjectID from "../../Types/ObjectID";
9
+ import DashboardVariableInterpolation, {
10
+ ResolvedVariableValue,
11
+ } from "./VariableInterpolation";
12
+
13
+ export interface DashboardLabelFilterData {
14
+ labelIds?: Array<string> | undefined;
15
+ labelVariableId?: string | undefined;
16
+ variables?: Array<DashboardVariable> | undefined;
17
+ }
18
+
19
+ export default class DashboardLabelVariable {
20
+ public static getOptions(
21
+ variable: DashboardVariable,
22
+ ): Array<DashboardVariableOption> {
23
+ const options: Array<DashboardVariableOption> | undefined =
24
+ variable.labelOptions;
25
+ if (
26
+ !Array.isArray(options) ||
27
+ options.length === 0 ||
28
+ options.length > 1000
29
+ ) {
30
+ throw new BadDataException(
31
+ "A project label variable must offer between 1 and 1000 labels.",
32
+ );
33
+ }
34
+
35
+ const seen: Set<string> = new Set<string>();
36
+ for (const option of options) {
37
+ if (
38
+ !option ||
39
+ typeof option.value !== "string" ||
40
+ !ObjectID.isValidUUID(option.value) ||
41
+ typeof option.label !== "string" ||
42
+ option.label.trim().length === 0 ||
43
+ option.label.length > 1024 ||
44
+ seen.has(option.value)
45
+ ) {
46
+ throw new BadDataException(
47
+ "Project label variable choices must have unique label IDs and nonempty display names.",
48
+ );
49
+ }
50
+ seen.add(option.value);
51
+ }
52
+ return options;
53
+ }
54
+
55
+ public static getFilter(
56
+ data: DashboardLabelFilterData,
57
+ ): Includes | IncludesAnyOfGroups | undefined {
58
+ const fixed: Array<string> = [...new Set(data.labelIds || [])];
59
+ if (!data.labelVariableId) {
60
+ return fixed.length > 0 ? new Includes(fixed) : undefined;
61
+ }
62
+
63
+ const matching: Array<DashboardVariable> = (data.variables || []).filter(
64
+ (variable: DashboardVariable) => {
65
+ return variable.id === data.labelVariableId;
66
+ },
67
+ );
68
+ const variable: DashboardVariable | undefined = matching[0];
69
+ if (
70
+ matching.length !== 1 ||
71
+ !variable ||
72
+ variable.type !== DashboardVariableType.ProjectLabel
73
+ ) {
74
+ throw new BadDataException(
75
+ "The monitor widget's label variable is missing or is not a project label variable. Edit the widget to select a label variable.",
76
+ );
77
+ }
78
+
79
+ const allowed: Set<string> = new Set(
80
+ DashboardLabelVariable.getOptions(variable).map(
81
+ (option: DashboardVariableOption) => {
82
+ return option.value;
83
+ },
84
+ ),
85
+ );
86
+ const resolved: ResolvedVariableValue | undefined =
87
+ DashboardVariableInterpolation.resolveValue(variable);
88
+ const selected: Array<string> =
89
+ resolved?.multi ||
90
+ (resolved?.scalar !== undefined ? [resolved.scalar] : []);
91
+ if (
92
+ selected.length > 100 ||
93
+ selected.some((value: string) => {
94
+ return !allowed.has(value);
95
+ })
96
+ ) {
97
+ throw new BadDataException(
98
+ "Choose up to 100 labels offered by this dashboard variable, or select All.",
99
+ );
100
+ }
101
+
102
+ if (selected.length === 0) {
103
+ return fixed.length > 0 ? new Includes(fixed) : undefined;
104
+ }
105
+ const dynamic: Array<string> = [...new Set(selected)];
106
+ return fixed.length > 0
107
+ ? new IncludesAnyOfGroups([fixed, dynamic])
108
+ : new Includes(dynamic);
109
+ }
110
+
111
+ public static interpolateTitle(
112
+ title: string | undefined,
113
+ variables: Array<DashboardVariable> | undefined,
114
+ ): string | undefined {
115
+ if (!title || !variables?.length) {
116
+ return title;
117
+ }
118
+ return title.replace(
119
+ /\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}/g,
120
+ (placeholder: string, name: string): string => {
121
+ const variable: DashboardVariable | undefined = variables.find(
122
+ (item: DashboardVariable) => {
123
+ return item.name === name;
124
+ },
125
+ );
126
+ if (!variable) {
127
+ return placeholder;
128
+ }
129
+ const resolved: ResolvedVariableValue | undefined =
130
+ DashboardVariableInterpolation.resolveValue(variable);
131
+ if (!resolved) {
132
+ return "All";
133
+ }
134
+ const values: Array<string> = resolved.multi || [
135
+ resolved.scalar as string,
136
+ ];
137
+ return values
138
+ .map((value: string): string => {
139
+ if (variable.type !== DashboardVariableType.ProjectLabel) {
140
+ return value;
141
+ }
142
+ return (
143
+ variable.labelOptions?.find((option: DashboardVariableOption) => {
144
+ return option.value === value;
145
+ })?.label || "Unavailable label"
146
+ );
147
+ })
148
+ .join(", ");
149
+ },
150
+ );
151
+ }
152
+ }
@@ -65,10 +65,25 @@ export default class DashboardVariableUrlState {
65
65
  if (!fromName) {
66
66
  return variable;
67
67
  }
68
+
69
+ /*
70
+ * The URL's comma syntax cannot distinguish one multi-select choice
71
+ * from a scalar. Resolve that ambiguity using the saved definition.
72
+ */
73
+ if (variable.isMultiSelect) {
74
+ return {
75
+ ...variable,
76
+ selectedValue: undefined,
77
+ selectedValues:
78
+ fromName.selectedValues ||
79
+ (fromName.selectedValue ? [fromName.selectedValue] : []),
80
+ };
81
+ }
68
82
  return {
69
83
  ...variable,
70
- selectedValue: fromName.selectedValue,
71
- selectedValues: fromName.selectedValues,
84
+ selectedValue:
85
+ fromName.selectedValue ?? fromName.selectedValues?.join(","),
86
+ selectedValues: undefined,
72
87
  };
73
88
  });
74
89
  }
@@ -110,11 +125,18 @@ export default class DashboardVariableUrlState {
110
125
  continue;
111
126
  }
112
127
  const paramKey: string = `${VAR_PREFIX}${name}`;
113
- if (variable.selectedValues && variable.selectedValues.length > 0) {
114
- params.set(paramKey, variable.selectedValues.join(","));
128
+ if (variable.isMultiSelect) {
129
+ if (Array.isArray(variable.selectedValues)) {
130
+ params.set(paramKey, variable.selectedValues.join(","));
131
+ }
115
132
  continue;
116
133
  }
117
- if (variable.selectedValue && variable.selectedValue.length > 0) {
134
+
135
+ /*
136
+ * Keep an explicit All as `var-name=`. Omitting it would restore the
137
+ * saved selection or default when this URL is reopened.
138
+ */
139
+ if (typeof variable.selectedValue === "string") {
118
140
  params.set(paramKey, variable.selectedValue);
119
141
  }
120
142
  }
@@ -297,6 +297,40 @@ export default class MetricUnitUtil {
297
297
  return (value * fromDef.toCanonical) / metricDef.toCanonical;
298
298
  }
299
299
 
300
+ /*
301
+ * The canonical base of the family this unit belongs to — "B" for any
302
+ * data unit, "sec" for any time unit, "%" for percent, "bit" for bits.
303
+ * Null when the unit belongs to no known family.
304
+ *
305
+ * Distinct from getCanonicalUnitValue, which answers "what should the
306
+ * threshold dropdown default to for this metric" and returns the unit
307
+ * itself for a known family member. This answers "what is the one unit
308
+ * every member of this family can be converted into", which is what a
309
+ * formatter needs before it can pick a human-readable scale: a value
310
+ * carrying the unit "GB" has to become bytes before a bytes ladder can
311
+ * decide that 2500 of them read best as "2.5 TB".
312
+ */
313
+ public static getFamilyBaseUnit(
314
+ metricUnit: string | undefined,
315
+ ): string | null {
316
+ if (!metricUnit || !metricUnit.trim()) {
317
+ return null;
318
+ }
319
+
320
+ const family: Array<UnitDefinition> | null = findFamily(metricUnit);
321
+ if (!family) {
322
+ return null;
323
+ }
324
+
325
+ const base: UnitDefinition | undefined = family.find(
326
+ (u: UnitDefinition) => {
327
+ return u.toCanonical === 1;
328
+ },
329
+ );
330
+
331
+ return base?.value || null;
332
+ }
333
+
300
334
  /*
301
335
  * Convenience: does the metric unit belong to a family we can offer
302
336
  * conversions for? When false, the UI should still render a dropdown,