@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,798 @@
1
+ import "@testing-library/jest-dom";
2
+ import {
3
+ act,
4
+ cleanup,
5
+ fireEvent,
6
+ render,
7
+ screen,
8
+ waitFor,
9
+ within,
10
+ } from "@testing-library/react";
11
+ import React, { ReactElement } from "react";
12
+ import { BrowserRouter, MemoryRouter } from "react-router-dom";
13
+ import {
14
+ afterEach,
15
+ beforeEach,
16
+ describe,
17
+ expect,
18
+ jest,
19
+ test,
20
+ } from "@jest/globals";
21
+ import InventoryTable, {
22
+ ComponentProps,
23
+ } from "../../../../App/FeatureSet/Dashboard/src/Components/Inventory/InventoryTable";
24
+ import {
25
+ INVENTORY_ARCHIVED_TABLE_ID,
26
+ INVENTORY_ITEMS_TABLE_ID,
27
+ } from "../../../../App/FeatureSet/Dashboard/src/Components/Inventory/InventoryFacets";
28
+ import InventoryItems from "../../../../App/FeatureSet/Dashboard/src/Pages/Inventory/Items";
29
+ import useResourceOwners, {
30
+ ResourceFacet,
31
+ UseResourceOwnersResult,
32
+ } from "../../../../App/FeatureSet/Dashboard/src/Components/ResourceOwners/useResourceOwners";
33
+ import InventoryItem from "../../../Models/DatabaseModels/InventoryItem";
34
+ import Route from "../../../Types/API/Route";
35
+ import Includes from "../../../Types/BaseDatabase/Includes";
36
+ import IncludesNone from "../../../Types/BaseDatabase/IncludesNone";
37
+ import IsNull from "../../../Types/BaseDatabase/IsNull";
38
+ import LessThan from "../../../Types/BaseDatabase/LessThan";
39
+ import { JSONObject } from "../../../Types/JSON";
40
+ import ObjectID from "../../../Types/ObjectID";
41
+ import EntitySource from "../../../Types/Telemetry/EntitySource";
42
+ import EntityType from "../../../Types/Telemetry/EntityType";
43
+ import Navigation from "../../../UI/Utils/Navigation";
44
+ import ProjectUtil from "../../../UI/Utils/Project";
45
+ import TableFilterUrlState from "../../../UI/Utils/TableFilterUrlState";
46
+
47
+ /*
48
+ * Render the production inventory table and facet hook together. ModelTable is
49
+ * the fetch boundary: capture its query and render its actual topContent so
50
+ * every selection below travels through the real chip, state and query merge.
51
+ * This catches a visible chip that fails to filter, a hidden Type/Status chip,
52
+ * and restored selections that are lost before the table's first request.
53
+ */
54
+ interface CapturedTableProps {
55
+ query: Record<string, unknown>;
56
+ topContent: ReactElement;
57
+ currentFacetState: JSONObject;
58
+ onFacetStateRestored: (state: JSONObject | null) => void;
59
+ noItemsMessage: string | ReactElement;
60
+ id: string;
61
+ }
62
+
63
+ let capturedTableProps: CapturedTableProps | null = null;
64
+ let capturedQueries: Array<Record<string, unknown>> = [];
65
+
66
+ jest.mock("../../../UI/Components/ModelTable/ModelTable", () => {
67
+ return {
68
+ __esModule: true,
69
+ default: (props: CapturedTableProps): ReactElement => {
70
+ capturedTableProps = props;
71
+ capturedQueries.push(props.query);
72
+ return <div>{props.topContent}</div>;
73
+ },
74
+ };
75
+ });
76
+
77
+ jest.mock("../../../UI/Components/Icon/Icon", () => {
78
+ return {
79
+ __esModule: true,
80
+ default: () => {
81
+ return null;
82
+ },
83
+ };
84
+ });
85
+
86
+ jest.mock("react-i18next", () => {
87
+ return {
88
+ useTranslation: () => {
89
+ return {
90
+ t: (key: string): string => {
91
+ return key;
92
+ },
93
+ };
94
+ },
95
+ };
96
+ });
97
+
98
+ jest.mock("../../../UI/Utils/Permission", () => {
99
+ return {
100
+ __esModule: true,
101
+ default: {
102
+ getAllPermissions: () => {
103
+ return [];
104
+ },
105
+ getProjectPermissions: () => {
106
+ return [];
107
+ },
108
+ getGlobalPermissions: () => {
109
+ return [];
110
+ },
111
+ },
112
+ };
113
+ });
114
+
115
+ jest.mock("../../../UI/Utils/User", () => {
116
+ return {
117
+ __esModule: true,
118
+ default: {
119
+ isMasterAdmin: () => {
120
+ return false;
121
+ },
122
+ getUserId: () => {
123
+ return null;
124
+ },
125
+ },
126
+ };
127
+ });
128
+
129
+ jest.mock(
130
+ "../../../../App/FeatureSet/Dashboard/src/Components/CustomFields/useCustomFieldFacets",
131
+ () => {
132
+ return {
133
+ __esModule: true,
134
+ default: () => {
135
+ return { facets: [], isLoading: false };
136
+ },
137
+ };
138
+ },
139
+ );
140
+
141
+ jest.mock("../../../UI/Components/BulkUpdate/BulkArchiveActions", () => {
142
+ return {
143
+ __esModule: true,
144
+ default: () => {
145
+ return { archiveBulkActions: [], unarchiveBulkActions: [] };
146
+ },
147
+ };
148
+ });
149
+
150
+ const PROJECT_ID: ObjectID = ObjectID.generate();
151
+
152
+ type RenderInventoryFunction = (props?: ComponentProps) => void;
153
+
154
+ const renderInventory: RenderInventoryFunction = (
155
+ props: ComponentProps = {},
156
+ ): void => {
157
+ render(
158
+ <MemoryRouter>
159
+ <InventoryTable {...props} />
160
+ </MemoryRouter>,
161
+ );
162
+ };
163
+
164
+ type GetTableFunction = () => CapturedTableProps;
165
+
166
+ const getTable: GetTableFunction = (): CapturedTableProps => {
167
+ expect(capturedTableProps).not.toBeNull();
168
+ return capturedTableProps!;
169
+ };
170
+
171
+ type OpenFacetFunction = (label: string) => HTMLElement;
172
+
173
+ const openFacet: OpenFacetFunction = (label: string): HTMLElement => {
174
+ const chip: HTMLElement = screen.getByRole("button", {
175
+ name: new RegExp(`^${label}(?:$| ·)`),
176
+ });
177
+
178
+ if (chip.getAttribute("aria-expanded") !== "true") {
179
+ fireEvent.click(chip);
180
+ }
181
+
182
+ return chip.parentElement!.querySelector('[role="dialog"]') as HTMLElement;
183
+ };
184
+
185
+ type SelectOptionFunction = (label: string, option: string) => void;
186
+
187
+ const selectOption: SelectOptionFunction = (
188
+ label: string,
189
+ option: string,
190
+ ): void => {
191
+ fireEvent.click(
192
+ within(openFacet(label))
193
+ .getByText(option, { exact: true })
194
+ .closest('[role="option"]')!,
195
+ );
196
+ };
197
+
198
+ type SetOperatorFunction = (label: string, operator: string) => void;
199
+
200
+ const setOperator: SetOperatorFunction = (
201
+ label: string,
202
+ operator: string,
203
+ ): void => {
204
+ fireEvent.change(
205
+ within(openFacet(label)).getByRole("combobox", {
206
+ name: `${label} operator`,
207
+ }),
208
+ { target: { value: operator } },
209
+ );
210
+ };
211
+
212
+ type ExpectIncludedFunction = (
213
+ field: string,
214
+ values: Array<string>,
215
+ excluded?: boolean,
216
+ ) => void;
217
+
218
+ const expectIncluded: ExpectIncludedFunction = (
219
+ field: string,
220
+ values: Array<string>,
221
+ excluded: boolean = false,
222
+ ): void => {
223
+ const value: unknown = getTable().query[field];
224
+ expect(value).toBeInstanceOf(excluded ? IncludesNone : Includes);
225
+ expect((value as Includes | IncludesNone).values).toEqual(values);
226
+ };
227
+
228
+ beforeEach(() => {
229
+ capturedTableProps = null;
230
+ capturedQueries = [];
231
+ jest.spyOn(ProjectUtil, "getCurrentProjectId").mockReturnValue(PROJECT_ID);
232
+ jest.spyOn(TableFilterUrlState, "read").mockReturnValue(null);
233
+ jest.spyOn(TableFilterUrlState, "write").mockImplementation(() => {});
234
+ Navigation.setLocation({
235
+ pathname: `/dashboard/${PROJECT_ID.toString()}/inventory/items`,
236
+ } as unknown as Parameters<typeof Navigation.setLocation>[0]);
237
+ });
238
+
239
+ afterEach(() => {
240
+ cleanup();
241
+ jest.restoreAllMocks();
242
+ window.history.replaceState(null, "", "/");
243
+ });
244
+
245
+ describe("inventory Items navigation with real URL persistence", () => {
246
+ type RenderItemsPageFunction = () => void;
247
+
248
+ const renderItemsPage: RenderItemsPageFunction = (): void => {
249
+ render(
250
+ <BrowserRouter>
251
+ <InventoryItems
252
+ pageRoute={
253
+ new Route(`/dashboard/${PROJECT_ID.toString()}/inventory/items`)
254
+ }
255
+ currentProject={null}
256
+ hasPaymentMethod={false}
257
+ />
258
+ </BrowserRouter>,
259
+ );
260
+ };
261
+
262
+ beforeEach(() => {
263
+ // These route tests exercise the browser URL and real persistence layer.
264
+ jest.spyOn(TableFilterUrlState, "read").mockRestore();
265
+ jest.spyOn(TableFilterUrlState, "write").mockRestore();
266
+ });
267
+
268
+ test("converts an Overview scope into visible facets before the first table request", async () => {
269
+ window.history.replaceState(
270
+ null,
271
+ "",
272
+ `/dashboard/${PROJECT_ID.toString()}/inventory/items?type=service&source=discovered&stale=true&tab=all#items`,
273
+ );
274
+ renderItemsPage();
275
+
276
+ await waitFor(() => {
277
+ expect(capturedTableProps).not.toBeNull();
278
+ });
279
+
280
+ for (const query of capturedQueries) {
281
+ expect(query["entityType"]).toEqual(new Includes([EntityType.Service]));
282
+ expect(query["source"]).toEqual(new Includes([EntitySource.Discovered]));
283
+ expect(query["inventoryStatus"]).toEqual(new Includes(["stale"]));
284
+ }
285
+
286
+ expect(
287
+ screen.getByRole("button", { name: /^Type · Service/ }),
288
+ ).toBeVisible();
289
+ expect(
290
+ screen.getByRole("button", { name: /^Status · Stale/ }),
291
+ ).toBeVisible();
292
+ const params: URLSearchParams = new URLSearchParams(window.location.search);
293
+ expect(params.has("type")).toBe(false);
294
+ expect(params.has("source")).toBe(false);
295
+ expect(params.has("stale")).toBe(false);
296
+ expect(params.get("tab")).toBe("all");
297
+ expect(params.has(`${INVENTORY_ITEMS_TABLE_ID}-facets`)).toBe(true);
298
+ expect(window.location.hash).toBe("#items");
299
+ });
300
+
301
+ test("keeps edits and clearing after remount instead of reapplying the legacy type", async () => {
302
+ window.history.replaceState(
303
+ null,
304
+ "",
305
+ `/dashboard/${PROJECT_ID.toString()}/inventory/items?type=service`,
306
+ );
307
+ renderItemsPage();
308
+
309
+ await waitFor(() => {
310
+ expect(
311
+ screen.getByRole("button", { name: /^Type · Service/ }),
312
+ ).toBeVisible();
313
+ });
314
+
315
+ selectOption("Type", "Host");
316
+ selectOption("Type", "Service");
317
+ selectOption("Status", "Live");
318
+ expectIncluded("entityType", [EntityType.Host]);
319
+
320
+ cleanup();
321
+ capturedQueries = [];
322
+ renderItemsPage();
323
+
324
+ expectIncluded("entityType", [EntityType.Host]);
325
+ expectIncluded("inventoryStatus", ["live"]);
326
+ expect(capturedQueries[0]!["entityType"]).toEqual(
327
+ new Includes([EntityType.Host]),
328
+ );
329
+ expect(new URLSearchParams(window.location.search).has("type")).toBe(false);
330
+
331
+ fireEvent.click(screen.getByRole("button", { name: "Clear all" }));
332
+ cleanup();
333
+ capturedQueries = [];
334
+ renderItemsPage();
335
+
336
+ expect(capturedQueries[0]).toEqual({
337
+ projectId: PROJECT_ID,
338
+ isArchived: false,
339
+ });
340
+ expect(screen.getByRole("button", { name: "Type" })).toBeVisible();
341
+ expect(screen.getByRole("button", { name: "Status" })).toBeVisible();
342
+ expect(
343
+ new URLSearchParams(window.location.search).has(
344
+ `${INVENTORY_ITEMS_TABLE_ID}-facets`,
345
+ ),
346
+ ).toBe(false);
347
+ });
348
+ });
349
+
350
+ describe("inventory Type and Status facet controls", () => {
351
+ test("renders Type, Source, Status and Last Seen on the inventory list", () => {
352
+ renderInventory();
353
+
354
+ for (const label of ["Type", "Source", "Status", "Last Seen"]) {
355
+ expect(screen.getByRole("button", { name: label })).toBeVisible();
356
+ }
357
+
358
+ expect(screen.queryByRole("button", { name: "Owner" })).toBeNull();
359
+ expect(screen.queryByRole("button", { name: "Labels" })).toBeNull();
360
+ expect(getTable().query).toEqual({
361
+ projectId: PROJECT_ID,
362
+ isArchived: false,
363
+ });
364
+ });
365
+
366
+ test("filters a single type using its stored value and its readable label", () => {
367
+ renderInventory();
368
+ selectOption("Type", "Kubernetes Pod");
369
+
370
+ expectIncluded("entityType", [EntityType.KubernetesPod]);
371
+ expect(
372
+ screen.getByRole("button", { name: /^Type · Kubernetes Pod/ }),
373
+ ).toBeVisible();
374
+ expect(getTable().currentFacetState["facetSelections"]).toEqual({
375
+ inventoryType: [EntityType.KubernetesPod],
376
+ });
377
+ expect(getTable().noItemsMessage).toBe(
378
+ "No inventory item matches the facets above.",
379
+ );
380
+ });
381
+
382
+ test("keeps multiple selected types and can deselect one without clearing the other", () => {
383
+ renderInventory();
384
+ selectOption("Type", "Kubernetes Pod");
385
+ selectOption("Type", "Host");
386
+
387
+ expectIncluded("entityType", [EntityType.KubernetesPod, EntityType.Host]);
388
+ expect(within(openFacet("Type")).getByRole("listbox")).toHaveAttribute(
389
+ "aria-multiselectable",
390
+ "true",
391
+ );
392
+
393
+ selectOption("Type", "Kubernetes Pod");
394
+ expectIncluded("entityType", [EntityType.Host]);
395
+ });
396
+
397
+ test("excludes multiple types with is not", () => {
398
+ renderInventory();
399
+ selectOption("Type", "Kubernetes Pod");
400
+ selectOption("Type", "Host");
401
+ setOperator("Type", "is_not");
402
+
403
+ expectIncluded(
404
+ "entityType",
405
+ [EntityType.KubernetesPod, EntityType.Host],
406
+ true,
407
+ );
408
+ expect(getTable().currentFacetState["facetOperators"]).toEqual({
409
+ inventoryType: "is_not",
410
+ });
411
+ });
412
+
413
+ test("searches the type catalog without changing the current selection", () => {
414
+ renderInventory();
415
+ selectOption("Type", "Host");
416
+ const dialog: HTMLElement = openFacet("Type");
417
+
418
+ fireEvent.change(
419
+ within(dialog).getByPlaceholderText("Search inventory types..."),
420
+ { target: { value: "Kubernetes Pod" } },
421
+ );
422
+
423
+ expect(
424
+ within(within(dialog).getByRole("listbox")).getAllByRole("option"),
425
+ ).toHaveLength(1);
426
+ expect(
427
+ within(dialog).getByRole("option", { name: /^Kubernetes Pod / }),
428
+ ).toBeVisible();
429
+ expectIncluded("entityType", [EntityType.Host]);
430
+ });
431
+
432
+ test.each([
433
+ ["Live", "live"],
434
+ ["Recent", "recent"],
435
+ ["Stale", "stale"],
436
+ ["Never seen", "never"],
437
+ ["Not tracked", "not-tracked"],
438
+ ])(
439
+ "filters %s using the matching status value",
440
+ (label: string, value: string) => {
441
+ renderInventory();
442
+ selectOption("Status", label);
443
+
444
+ expectIncluded("inventoryStatus", [value]);
445
+ expect(
446
+ screen.getByRole("button", { name: new RegExp(`^Status · ${label}`) }),
447
+ ).toBeVisible();
448
+ expect(getTable().currentFacetState["facetSelections"]).toEqual({
449
+ inventoryStatus: [value],
450
+ });
451
+ },
452
+ );
453
+
454
+ test("selects several statuses and switches between inclusion and exclusion", () => {
455
+ renderInventory();
456
+ selectOption("Status", "Live");
457
+ selectOption("Status", "Recent");
458
+ selectOption("Status", "Never seen");
459
+
460
+ expectIncluded("inventoryStatus", ["live", "recent", "never"]);
461
+ setOperator("Status", "is_not");
462
+ expectIncluded("inventoryStatus", ["live", "recent", "never"], true);
463
+ setOperator("Status", "is");
464
+ expectIncluded("inventoryStatus", ["live", "recent", "never"]);
465
+ });
466
+
467
+ test("offers only inclusion and exclusion operators for Status", () => {
468
+ renderInventory();
469
+ const dialog: HTMLElement = openFacet("Status");
470
+ const operator: HTMLElement = within(dialog).getByRole("combobox", {
471
+ name: "Status operator",
472
+ });
473
+
474
+ expect(
475
+ within(operator)
476
+ .getAllByRole("option")
477
+ .map((option: HTMLElement): string => {
478
+ return (option as HTMLOptionElement).value;
479
+ }),
480
+ ).toEqual(["is", "is_not"]);
481
+ expect(within(dialog).getByRole("listbox")).toHaveAttribute(
482
+ "aria-multiselectable",
483
+ "true",
484
+ );
485
+ });
486
+
487
+ test("combines Type, Status, Source and Last Seen without overwriting any dimension", () => {
488
+ renderInventory();
489
+ selectOption("Type", "Kubernetes Pod");
490
+ selectOption("Status", "Never seen");
491
+ selectOption("Source", "Discovered");
492
+ setOperator("Last Seen", "is_empty");
493
+
494
+ expectIncluded("entityType", [EntityType.KubernetesPod]);
495
+ expectIncluded("inventoryStatus", ["never"]);
496
+ expectIncluded("source", [EntitySource.Discovered]);
497
+ expect(getTable().query["lastSeenAt"]).toBeInstanceOf(IsNull);
498
+ expect(getTable().query["projectId"]).toEqual(PROJECT_ID);
499
+ expect(getTable().query["isArchived"]).toBe(false);
500
+ });
501
+
502
+ test.each([
503
+ ["Status", "Stale", "inventoryStatus", "Live", "live"],
504
+ ["Type", "Host", "entityType", "Kubernetes Pod", EntityType.KubernetesPod],
505
+ ["Source", "Discovered", "source", "Added by you", EntitySource.Manual],
506
+ ])(
507
+ "clears an excluded %s and resets its operator while preserving other facets",
508
+ (
509
+ label: string,
510
+ selected: string,
511
+ field: string,
512
+ nextLabel: string,
513
+ nextValue: string,
514
+ ) => {
515
+ renderInventory();
516
+ const preservesType: boolean = label !== "Type";
517
+ selectOption(
518
+ preservesType ? "Type" : "Status",
519
+ preservesType ? "Host" : "Live",
520
+ );
521
+ selectOption(label, selected);
522
+ setOperator(label, "is_not");
523
+ fireEvent.click(
524
+ screen.getByRole("button", { name: `Clear ${label} filter` }),
525
+ );
526
+
527
+ expect(getTable().query).not.toHaveProperty(field);
528
+ expectIncluded(
529
+ preservesType ? "entityType" : "inventoryStatus",
530
+ preservesType ? [EntityType.Host] : ["live"],
531
+ );
532
+ selectOption(label, nextLabel);
533
+ expectIncluded(field, [nextValue]);
534
+ },
535
+ );
536
+
537
+ test.each(["Enter", " "])(
538
+ "clears a selected Type with the %p key while preserving Status",
539
+ (key: string) => {
540
+ renderInventory();
541
+ selectOption("Type", "Host");
542
+ selectOption("Status", "Live");
543
+ fireEvent.keyDown(
544
+ screen.getByRole("button", { name: "Clear Type filter" }),
545
+ {
546
+ key: key,
547
+ },
548
+ );
549
+
550
+ expect(getTable().query).not.toHaveProperty("entityType");
551
+ expectIncluded("inventoryStatus", ["live"]);
552
+ expect(screen.getByRole("button", { name: "Type" })).toBeVisible();
553
+ },
554
+ );
555
+
556
+ test("clears every facet and removes the persisted state", () => {
557
+ renderInventory();
558
+ selectOption("Type", "Host");
559
+ selectOption("Status", "Stale");
560
+ setOperator("Status", "is_not");
561
+ selectOption("Source", "Discovered");
562
+ setOperator("Last Seen", "is_empty");
563
+ fireEvent.click(screen.getByRole("button", { name: "Clear all" }));
564
+
565
+ expect(getTable().query).toEqual({
566
+ projectId: PROJECT_ID,
567
+ isArchived: false,
568
+ });
569
+ expect(getTable().currentFacetState["facetSelections"]).toEqual({});
570
+ expect(getTable().currentFacetState["facetOperators"]).toEqual({});
571
+ expect(TableFilterUrlState.write).toHaveBeenLastCalledWith(
572
+ INVENTORY_ITEMS_TABLE_ID,
573
+ "facets",
574
+ null,
575
+ );
576
+ expect(screen.queryByRole("button", { name: "Clear all" })).toBeNull();
577
+ expect(typeof getTable().noItemsMessage).not.toBe("string");
578
+ });
579
+ });
580
+
581
+ describe("inventory facet persistence and embedded scopes", () => {
582
+ test("restores Type and Status from a shared URL before the first table query", () => {
583
+ jest.spyOn(TableFilterUrlState, "read").mockReturnValue({
584
+ facetSelections: {
585
+ inventoryType: [EntityType.Host, EntityType.KubernetesPod],
586
+ inventoryStatus: ["live", "recent"],
587
+ },
588
+ facetOperators: { inventoryStatus: "is_not" },
589
+ });
590
+ renderInventory();
591
+
592
+ expect(capturedQueries.length).toBeGreaterThan(0);
593
+ for (const query of capturedQueries) {
594
+ expect(query["entityType"]).toEqual(
595
+ new Includes([EntityType.Host, EntityType.KubernetesPod]),
596
+ );
597
+ expect(query["inventoryStatus"]).toEqual(
598
+ new IncludesNone(["live", "recent"]),
599
+ );
600
+ }
601
+ expect(
602
+ screen.getByRole("button", { name: "Clear Type filter" }),
603
+ ).toBeVisible();
604
+ expect(
605
+ screen.getByRole("button", { name: "Clear Status filter" }),
606
+ ).toBeVisible();
607
+ expect(TableFilterUrlState.read).toHaveBeenCalledWith(
608
+ INVENTORY_ITEMS_TABLE_ID,
609
+ "facets",
610
+ );
611
+ });
612
+
613
+ test("restores a saved view with all four facets and resets it through ModelTable", () => {
614
+ renderInventory();
615
+ act(() => {
616
+ getTable().onFacetStateRestored({
617
+ facetSelections: {
618
+ inventoryType: [EntityType.Host],
619
+ inventorySource: [EntitySource.Discovered],
620
+ inventoryStatus: ["stale", "recent"],
621
+ inventoryLastSeen: ["2026-09-01T00:00:00.000Z"],
622
+ },
623
+ facetOperators: {
624
+ inventoryType: "is_not",
625
+ inventoryLastSeen: "before",
626
+ },
627
+ });
628
+ });
629
+
630
+ expectIncluded("entityType", [EntityType.Host], true);
631
+ expectIncluded("source", [EntitySource.Discovered]);
632
+ expectIncluded("inventoryStatus", ["stale", "recent"]);
633
+ expect(getTable().query["lastSeenAt"]).toBeInstanceOf(LessThan);
634
+ expect(
635
+ screen.getByRole("button", { name: "Clear Last Seen filter" }),
636
+ ).toBeVisible();
637
+
638
+ act(() => {
639
+ getTable().onFacetStateRestored(null);
640
+ });
641
+
642
+ expect(getTable().query).toEqual({
643
+ projectId: PROJECT_ID,
644
+ isArchived: false,
645
+ });
646
+ expect(screen.queryByRole("button", { name: "Clear all" })).toBeNull();
647
+ });
648
+
649
+ test("preserves a locked type scope while leaving Status available", () => {
650
+ renderInventory({ query: { entityType: EntityType.Service } });
651
+
652
+ expect(screen.queryByRole("button", { name: "Type" })).toBeNull();
653
+ selectOption("Status", "Live");
654
+
655
+ expect(getTable().query["entityType"]).toBe(EntityType.Service);
656
+ expectIncluded("inventoryStatus", ["live"]);
657
+ fireEvent.click(screen.getByRole("button", { name: "Clear all" }));
658
+ expect(getTable().query["entityType"]).toBe(EntityType.Service);
659
+ });
660
+
661
+ test("preserves a locked status scope while leaving Type available", () => {
662
+ renderInventory({
663
+ query: { inventoryStatus: new Includes(["recent"]) },
664
+ });
665
+
666
+ expect(screen.queryByRole("button", { name: "Status" })).toBeNull();
667
+ selectOption("Type", "Host");
668
+
669
+ expectIncluded("inventoryStatus", ["recent"]);
670
+ expectIncluded("entityType", [EntityType.Host]);
671
+ });
672
+
673
+ test("provides the same facets on archived items using their own persistence key", () => {
674
+ renderInventory({ archivedOnly: true });
675
+ selectOption("Type", "Host");
676
+ selectOption("Status", "Stale");
677
+
678
+ expect(getTable().id).toBe(INVENTORY_ARCHIVED_TABLE_ID);
679
+ expect(getTable().query["isArchived"]).toBe(true);
680
+ expectIncluded("entityType", [EntityType.Host]);
681
+ expectIncluded("inventoryStatus", ["stale"]);
682
+ expect(TableFilterUrlState.write).toHaveBeenLastCalledWith(
683
+ INVENTORY_ARCHIVED_TABLE_ID,
684
+ "facets",
685
+ getTable().currentFacetState,
686
+ );
687
+ });
688
+ });
689
+
690
+ describe("clearing nullable extra option facets", () => {
691
+ const nullableFacets: Array<ResourceFacet> = [
692
+ {
693
+ key: "nullableSource",
694
+ queryField: "source",
695
+ label: "Nullable source",
696
+ isMultiSelect: true,
697
+ options: [{ value: EntitySource.Discovered, label: "Discovered" }],
698
+ supportedOperators: ["is", "is_not", "is_empty", "is_not_empty"],
699
+ exclusiveWith: ["explicitSource"],
700
+ },
701
+ {
702
+ key: "explicitSource",
703
+ queryField: "source",
704
+ label: "Specified source",
705
+ isMultiSelect: true,
706
+ options: [{ value: EntitySource.Discovered, label: "Discovered" }],
707
+ supportedOperators: ["is", "is_not"],
708
+ },
709
+ ];
710
+
711
+ let nullableHook: UseResourceOwnersResult<InventoryItem> | null = null;
712
+
713
+ type NullableFacetHarnessFunction = () => ReactElement;
714
+
715
+ const NullableFacetHarness: NullableFacetHarnessFunction =
716
+ (): ReactElement => {
717
+ const result: UseResourceOwnersResult<InventoryItem> =
718
+ useResourceOwners<InventoryItem>({
719
+ showOwnerFacet: false,
720
+ showLabelsFacet: false,
721
+ extraFacets: nullableFacets,
722
+ });
723
+ nullableHook = result;
724
+ return result.filterBar;
725
+ };
726
+
727
+ type GetNullableHookFunction = () => UseResourceOwnersResult<InventoryItem>;
728
+
729
+ const getNullableHook: GetNullableHookFunction =
730
+ (): UseResourceOwnersResult<InventoryItem> => {
731
+ expect(nullableHook).not.toBeNull();
732
+ return nullableHook!;
733
+ };
734
+
735
+ beforeEach(() => {
736
+ nullableHook = null;
737
+ });
738
+
739
+ test("clearing is empty removes its query and active state without needing a selected value", () => {
740
+ render(<NullableFacetHarness />);
741
+ setOperator("Nullable source", "is_empty");
742
+
743
+ expect(getNullableHook().facetSelections["nullableSource"]).toEqual([]);
744
+ expect(getNullableHook().mergeFiltersIntoQuery({}).source).toBeInstanceOf(
745
+ IsNull,
746
+ );
747
+ expect(getNullableHook().hasActiveFilters).toBe(true);
748
+
749
+ fireEvent.click(
750
+ screen.getByRole("button", { name: "Clear Nullable source filter" }),
751
+ );
752
+
753
+ expect(getNullableHook().mergeFiltersIntoQuery({})).toEqual({});
754
+ expect(getNullableHook().hasActiveFilters).toBe(false);
755
+ expect(getNullableHook().facetOperators["nullableSource"]).toBe("is");
756
+ expect(
757
+ screen.getByRole("button", { name: "Nullable source" }),
758
+ ).toBeVisible();
759
+ expect(screen.queryByRole("button", { name: "Clear all" })).toBeNull();
760
+ });
761
+
762
+ test("clearing a restored empty facet preserves a currently active conflicting facet", () => {
763
+ render(<NullableFacetHarness />);
764
+
765
+ // A saved view may predate a conflict rule and restore both chips as active.
766
+ act(() => {
767
+ getNullableHook().restoreFacetState({
768
+ facetSelections: { explicitSource: [EntitySource.Discovered] },
769
+ facetOperators: { nullableSource: "is_empty" },
770
+ });
771
+ });
772
+
773
+ expect(
774
+ screen.getByRole("button", { name: "Clear Nullable source filter" }),
775
+ ).toBeVisible();
776
+ expect(
777
+ screen.getByRole("button", { name: "Clear Specified source filter" }),
778
+ ).toBeVisible();
779
+ fireEvent.click(
780
+ screen.getByRole("button", { name: "Clear Nullable source filter" }),
781
+ );
782
+
783
+ expect(getNullableHook().mergeFiltersIntoQuery({}).source).toEqual(
784
+ new Includes([EntitySource.Discovered]),
785
+ );
786
+ expect(getNullableHook().facetSelections["explicitSource"]).toEqual([
787
+ EntitySource.Discovered,
788
+ ]);
789
+ expect(getNullableHook().facetOperators["nullableSource"]).toBe("is");
790
+ expect(getNullableHook().hasActiveFilters).toBe(true);
791
+ expect(
792
+ screen.getByRole("button", { name: "Nullable source" }),
793
+ ).toBeVisible();
794
+ expect(
795
+ screen.getByRole("button", { name: "Clear Specified source filter" }),
796
+ ).toBeVisible();
797
+ });
798
+ });