@oneuptime/common 14.0.5 → 14.0.7

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 (192) hide show
  1. package/Models/DatabaseModels/InventoryItem.ts +23 -12
  2. package/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.ts +217 -0
  3. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  4. package/Server/Services/AlertService.ts +32 -36
  5. package/Server/Services/IncidentAlertService.ts +609 -2
  6. package/Server/Services/IncidentService.ts +134 -44
  7. package/Server/Services/InventoryItemService.ts +38 -0
  8. package/Server/Services/IoTDeviceCredentialService.ts +13 -4
  9. package/Server/Services/MetricService.ts +1 -1
  10. package/Server/Services/ScheduledMaintenanceService.ts +50 -46
  11. package/Server/Services/UserNotificationRuleService.ts +55 -53
  12. package/Server/Services/WorkspaceNotificationSummaryService.ts +38 -20
  13. package/Server/Types/Database/QueryHelper.ts +54 -1
  14. package/Server/Utils/AI/Toolbox/MetricTools.ts +13 -2
  15. package/Server/Utils/AI/Toolbox/ResourceTools.ts +12 -1
  16. package/Server/Utils/AffectedResources/LinkedAffectedResources.ts +576 -0
  17. package/Server/Utils/Alert/AlertStateChangeAuthorization.ts +129 -0
  18. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +1 -1
  19. package/Server/Utils/Inventory/InventoryOverviewAggregation.ts +111 -0
  20. package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +33 -0
  21. package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +4 -3
  22. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +3 -1
  23. package/Server/Utils/Response.ts +31 -0
  24. package/Server/Utils/Topology/TopologyConcurrencyLimiter.ts +229 -0
  25. package/Server/Utils/Topology/TopologyQueries.ts +1350 -0
  26. package/Server/Utils/Topology/TopologyRequest.ts +463 -0
  27. package/Server/Utils/Topology/TopologyResponseCache.ts +183 -0
  28. package/Server/Utils/Topology/TopologySql.ts +1024 -0
  29. package/Tests/App/Dashboard/AcknowledgeAlertsOnDeclare.test.ts +369 -0
  30. package/Tests/App/Dashboard/AffectedResourcesSlo.test.tsx +1 -1
  31. package/Tests/App/Dashboard/AffectedResourcesSummaryStep.test.tsx +510 -123
  32. package/Tests/App/Dashboard/AlertsTableAffectedResources.test.tsx +490 -0
  33. package/Tests/App/Dashboard/ChangeAlertStateDeclareIncident.test.tsx +886 -0
  34. package/Tests/App/Dashboard/CreateReviewStepRoleSummary.test.tsx +1250 -0
  35. package/Tests/App/Dashboard/CreateReviewStepSelectionSummary.test.tsx +738 -0
  36. package/Tests/App/Dashboard/DeclareIncidentFromAlert.test.ts +417 -0
  37. package/Tests/App/Dashboard/EntityDetailPanel.test.tsx +1948 -229
  38. package/Tests/App/Dashboard/EntityDetailPanelLifecycle.test.tsx +493 -0
  39. package/Tests/App/Dashboard/EntityDetailPanelNavigation.test.tsx +439 -0
  40. package/Tests/App/Dashboard/EventOverviewPages.test.tsx +292 -1
  41. package/Tests/App/Dashboard/EventStatusPanel.test.tsx +798 -2
  42. package/Tests/App/Dashboard/FetchStateAndSeverityElements.test.tsx +669 -0
  43. package/Tests/App/Dashboard/FetchUsers.test.tsx +230 -0
  44. package/Tests/App/Dashboard/IncidentCreateAcknowledgeAlerts.test.tsx +1611 -0
  45. package/Tests/App/Dashboard/InfrastructureExplorer.test.tsx +1365 -57
  46. package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +964 -23
  47. package/Tests/App/Dashboard/InventoryRelationships.test.tsx +885 -0
  48. package/Tests/App/Dashboard/NetworkTopologyExplorerSearch.test.tsx +1267 -0
  49. package/Tests/App/Dashboard/ResourceOverviewTileTooltips.test.tsx +4 -1
  50. package/Tests/App/Dashboard/RumOverviewPage.test.tsx +127 -0
  51. package/Tests/App/Dashboard/ServiceMapDatabaseLink.test.tsx +201 -15
  52. package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +508 -36
  53. package/Tests/App/Dashboard/SiteSearchBox.test.tsx +2110 -0
  54. package/Tests/App/Dashboard/TelemetryMetricsSignals.test.ts +168 -8
  55. package/Tests/App/Dashboard/TopologyDataLifecycle.test.tsx +583 -0
  56. package/Tests/App/Dashboard/TopologyDataLoading.test.tsx +982 -218
  57. package/Tests/App/Dashboard/TopologyPageNavigation.test.tsx +818 -114
  58. package/Tests/Server/Infrastructure/Postgres/RenameInventoryItemMigration.test.ts +90 -21
  59. package/Tests/Server/Services/DomainService.test.ts +525 -0
  60. package/Tests/Server/Services/IncidentAlertAcknowledgeOnDeclare.test.ts +2630 -0
  61. package/Tests/Server/Services/IncidentAlertPostgres.test.ts +1625 -1
  62. package/Tests/Server/Services/IncidentDeclareAcknowledgeAlerts.test.ts +2189 -0
  63. package/Tests/Server/Services/InventoryItemServiceOverviewCounts.test.ts +345 -0
  64. package/Tests/Server/Services/InventoryOverviewPostgres.test.ts +254 -0
  65. package/Tests/Server/Services/IoTDeviceCredentialService.test.ts +819 -0
  66. package/Tests/Server/Services/LinkedAffectedResourcesCreatedFeed.test.ts +420 -0
  67. package/Tests/Server/Services/MetricServiceAggregate.test.ts +12 -0
  68. package/Tests/Server/Services/SavedViewDefaults.test.ts +325 -0
  69. package/Tests/Server/Services/SloAffectedResourceCreatedFeed.test.ts +92 -36
  70. package/Tests/Server/Services/StatusPageCertificateService.test.ts +169 -0
  71. package/Tests/Server/Services/TeamComplianceSettingService.test.ts +191 -0
  72. package/Tests/Server/Types/Database/QueryHelperSearchAllWords.test.ts +651 -0
  73. package/Tests/Server/Utils/AI/AlertWriteTools.test.ts +256 -0
  74. package/Tests/Server/Utils/AI/IncidentWriteTools.test.ts +482 -0
  75. package/Tests/Server/Utils/AffectedResources/LinkedAffectedResources.test.ts +858 -0
  76. package/Tests/Server/Utils/Alert/AlertStateChangeAuthorization.test.ts +1172 -0
  77. package/Tests/Server/Utils/Monitor/Criteria/CompareCriteria.test.ts +54 -0
  78. package/Tests/Server/Utils/Monitor/MetricDecimalThreshold.test.ts +417 -0
  79. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorAffectedResourceEdgeCases.test.ts +96 -0
  80. package/Tests/Server/Utils/Topology/InventoryItemEntityKeyIndexMigration.test.ts +386 -0
  81. package/Tests/Server/Utils/Topology/InventoryItemEntityKeyIndexMigrationPostgres.test.ts +430 -0
  82. package/Tests/Server/Utils/Topology/TopologyActivityParityPostgres.test.ts +279 -0
  83. package/Tests/Server/Utils/Topology/TopologyConcurrencyLimiter.test.ts +534 -0
  84. package/Tests/Server/Utils/Topology/TopologyEstateGenerator.ts +624 -0
  85. package/Tests/Server/Utils/Topology/TopologyJsonResponse.test.ts +90 -0
  86. package/Tests/Server/Utils/Topology/TopologyQueries.test.ts +1634 -0
  87. package/Tests/Server/Utils/Topology/TopologyQueriesDifferentialPostgres.test.ts +683 -0
  88. package/Tests/Server/Utils/Topology/TopologyQueriesPostgres.test.ts +2624 -0
  89. package/Tests/Server/Utils/Topology/TopologyReference.ts +1018 -0
  90. package/Tests/Server/Utils/Topology/TopologyRequest.test.ts +643 -0
  91. package/Tests/Server/Utils/Topology/TopologyResponseCache.test.ts +305 -0
  92. package/Tests/Server/Utils/Topology/TopologySql.test.ts +973 -0
  93. package/Tests/Types/BaseDatabase/AggregationType.test.ts +5 -1
  94. package/Tests/Types/Dashboard/KubernetesDashboardTemplate.test.ts +184 -0
  95. package/Tests/Types/Dashboard/KubernetesDashboardTemplates.test.ts +107 -0
  96. package/Tests/Types/Monitor/KubernetesAlertTemplates.test.ts +8 -14
  97. package/Tests/Types/Monitor/KubernetesMetricCatalog.test.ts +4 -3
  98. package/Tests/Types/Monitor/Utils/KubernetesAgentEmittedMetrics.ts +27 -6
  99. package/Tests/Types/Topology/TopologyTypeRules.test.ts +176 -0
  100. package/Tests/UI/Rum/ChunkLoader.test.ts +76 -0
  101. package/Tests/UI/Rum/ReplayRailDetail.test.tsx +53 -0
  102. package/Tests/UI/Rum/ReplaySignals.test.ts +54 -0
  103. package/Tests/Utils/Dashboard/KubernetesClusterCpuTileRepair.test.ts +370 -0
  104. package/Tests/Utils/Incident/IncidentFromAlerts.test.ts +302 -0
  105. package/Tests/Utils/Rules/MonitorTypeRuleCriteria.test.ts +181 -0
  106. package/Types/BaseDatabase/AggregationType.ts +7 -0
  107. package/Types/Dashboard/DashboardTemplates.ts +20 -7
  108. package/Types/Incident/IncidentAlertLink.ts +19 -0
  109. package/Types/Rum/SessionReplayCustomEvents.ts +29 -3
  110. package/Types/Rum/WebVitals.ts +13 -0
  111. package/Types/Telemetry/InventoryLiveness.ts +26 -0
  112. package/Types/Topology/TopologyApi.ts +428 -0
  113. package/Types/Topology/TopologyTypeRules.ts +191 -0
  114. package/UI/Components/Input/Input.tsx +11 -0
  115. package/Utils/Dashboard/KubernetesClusterCpuTileRepair.ts +147 -0
  116. package/Utils/Incident/IncidentFromAlerts.ts +78 -0
  117. package/build/dist/Models/DatabaseModels/InventoryItem.js +20 -11
  118. package/build/dist/Models/DatabaseModels/InventoryItem.js.map +1 -1
  119. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.js +165 -0
  120. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.js.map +1 -0
  121. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  122. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  123. package/build/dist/Server/Services/AlertService.js +21 -33
  124. package/build/dist/Server/Services/AlertService.js.map +1 -1
  125. package/build/dist/Server/Services/IncidentAlertService.js +457 -3
  126. package/build/dist/Server/Services/IncidentAlertService.js.map +1 -1
  127. package/build/dist/Server/Services/IncidentService.js +96 -42
  128. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  129. package/build/dist/Server/Services/InventoryItemService.js +31 -0
  130. package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
  131. package/build/dist/Server/Services/IoTDeviceCredentialService.js +12 -5
  132. package/build/dist/Server/Services/IoTDeviceCredentialService.js.map +1 -1
  133. package/build/dist/Server/Services/MetricService.js +1 -1
  134. package/build/dist/Server/Services/ScheduledMaintenanceService.js +37 -40
  135. package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
  136. package/build/dist/Server/Services/UserNotificationRuleService.js +55 -51
  137. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  138. package/build/dist/Server/Services/WorkspaceNotificationSummaryService.js +42 -25
  139. package/build/dist/Server/Services/WorkspaceNotificationSummaryService.js.map +1 -1
  140. package/build/dist/Server/Types/Database/QueryHelper.js +47 -1
  141. package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
  142. package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js +13 -2
  143. package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js.map +1 -1
  144. package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js +12 -1
  145. package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js.map +1 -1
  146. package/build/dist/Server/Utils/AffectedResources/LinkedAffectedResources.js +405 -0
  147. package/build/dist/Server/Utils/AffectedResources/LinkedAffectedResources.js.map +1 -0
  148. package/build/dist/Server/Utils/Alert/AlertStateChangeAuthorization.js +112 -0
  149. package/build/dist/Server/Utils/Alert/AlertStateChangeAuthorization.js.map +1 -0
  150. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +1 -1
  151. package/build/dist/Server/Utils/Inventory/InventoryOverviewAggregation.js +81 -0
  152. package/build/dist/Server/Utils/Inventory/InventoryOverviewAggregation.js.map +1 -0
  153. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +31 -0
  154. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
  155. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +1 -1
  156. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
  157. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +1 -1
  158. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  159. package/build/dist/Server/Utils/Response.js +30 -0
  160. package/build/dist/Server/Utils/Response.js.map +1 -1
  161. package/build/dist/Server/Utils/Topology/TopologyConcurrencyLimiter.js +178 -0
  162. package/build/dist/Server/Utils/Topology/TopologyConcurrencyLimiter.js.map +1 -0
  163. package/build/dist/Server/Utils/Topology/TopologyQueries.js +884 -0
  164. package/build/dist/Server/Utils/Topology/TopologyQueries.js.map +1 -0
  165. package/build/dist/Server/Utils/Topology/TopologyRequest.js +275 -0
  166. package/build/dist/Server/Utils/Topology/TopologyRequest.js.map +1 -0
  167. package/build/dist/Server/Utils/Topology/TopologyResponseCache.js +146 -0
  168. package/build/dist/Server/Utils/Topology/TopologyResponseCache.js.map +1 -0
  169. package/build/dist/Server/Utils/Topology/TopologySql.js +688 -0
  170. package/build/dist/Server/Utils/Topology/TopologySql.js.map +1 -0
  171. package/build/dist/Types/BaseDatabase/AggregationType.js +7 -0
  172. package/build/dist/Types/BaseDatabase/AggregationType.js.map +1 -1
  173. package/build/dist/Types/Dashboard/DashboardTemplates.js +20 -7
  174. package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
  175. package/build/dist/Types/Incident/IncidentAlertLink.js +17 -0
  176. package/build/dist/Types/Incident/IncidentAlertLink.js.map +1 -1
  177. package/build/dist/Types/Rum/SessionReplayCustomEvents.js.map +1 -1
  178. package/build/dist/Types/Rum/WebVitals.js +12 -0
  179. package/build/dist/Types/Rum/WebVitals.js.map +1 -1
  180. package/build/dist/Types/Telemetry/InventoryLiveness.js +23 -0
  181. package/build/dist/Types/Telemetry/InventoryLiveness.js.map +1 -1
  182. package/build/dist/Types/Topology/TopologyApi.js +55 -0
  183. package/build/dist/Types/Topology/TopologyApi.js.map +1 -0
  184. package/build/dist/Types/Topology/TopologyTypeRules.js +165 -0
  185. package/build/dist/Types/Topology/TopologyTypeRules.js.map +1 -0
  186. package/build/dist/UI/Components/Input/Input.js +1 -1
  187. package/build/dist/UI/Components/Input/Input.js.map +1 -1
  188. package/build/dist/Utils/Dashboard/KubernetesClusterCpuTileRepair.js +110 -0
  189. package/build/dist/Utils/Dashboard/KubernetesClusterCpuTileRepair.js.map +1 -0
  190. package/build/dist/Utils/Incident/IncidentFromAlerts.js +38 -0
  191. package/build/dist/Utils/Incident/IncidentFromAlerts.js.map +1 -1
  192. package/package.json +1 -1
@@ -7,38 +7,50 @@ import {
7
7
  jest,
8
8
  test,
9
9
  } from "@jest/globals";
10
- import { cleanup, render, waitFor } from "@testing-library/react";
10
+ import {
11
+ cleanup,
12
+ render,
13
+ RenderResult,
14
+ waitFor,
15
+ within,
16
+ } from "@testing-library/react";
11
17
  import * as React from "react";
12
18
  import { MemoryRouter } from "react-router-dom";
19
+ import getJestMockFunction, { MockFunction } from "../../MockType";
13
20
 
14
21
  /*
15
- * The incident and scheduled maintenance wizards end on a summary that lists
16
- * what each step picked. The "Resources Affected" step uses one picker that
17
- * writes to every resource relation the page hands it, but its summary is
18
- * hand-written per page - so a resource type the picker offers can be left
19
- * out of it. Podman hosts were: pick only Podman hosts and the summary said
20
- * "No resources affected", although the selection saved fine.
22
+ * The incident and scheduled maintenance wizards end on a review step that
23
+ * lists what each earlier step picked. The "Resources Affected" step uses one
24
+ * picker that writes to every resource relation the page hands it, and the
25
+ * form holds what it picked as bare IDs. Its summary used to be hand-written
26
+ * per page: it named monitors but only counted every other type ("3 Podman
27
+ * hosts"), so the one place meant to confirm the choice never said which
28
+ * hosts or services were picked - and a type the summary forgot was not even
29
+ * counted. Pick only Podman hosts and it once said "No resources affected",
30
+ * although the selection saved fine.
31
+ *
32
+ * The summary now renders the same picker, read-only, with the same resource
33
+ * types as the editor; the picker looks the names of bare IDs up, so every
34
+ * picked resource is named.
21
35
  *
22
36
  * The invariant pinned here: every resource prop the editor picker is given
23
- * shows up in the summary. It drives the real pages, with ModelForm mocked to
24
- * capture the fields it is handed (as MonitorCreateFromMonitorBackedDevice
25
- * does), reads the resource props off the picker element the step renders,
26
- * and renders the step's summary once per prop with only that type selected.
27
- * A resource type added to a page's picker later is picked up without edits
28
- * here.
37
+ * is named in the summary. These tests drive the real pages, with ModelForm
38
+ * mocked to capture the fields it is handed (as
39
+ * CreateReviewStepSelectionSummary.test.tsx does), read the resource props
40
+ * off the picker element the step renders, and render the step's summary
41
+ * against a ModelAPI that knows a name for each resource under its own model
42
+ * type only. A resource type added to a page's picker later is picked up
43
+ * without edits here.
29
44
  */
30
45
 
46
+ type SummaryItem = Record<string, unknown>;
47
+
31
48
  type CapturedField = {
32
49
  stepId?: string | undefined;
33
50
  getCustomElement?:
34
- | ((
35
- values: Record<string, unknown>,
36
- elementProps: Record<string, unknown>,
37
- ) => React.ReactElement)
38
- | undefined;
39
- getSummaryElement?:
40
- | ((item: Record<string, unknown>) => React.ReactElement)
51
+ | ((values: SummaryItem, elementProps: SummaryItem) => React.ReactElement)
41
52
  | undefined;
53
+ getSummaryElement?: ((item: SummaryItem) => React.ReactElement) | undefined;
42
54
  };
43
55
 
44
56
  type CapturedFormProps = {
@@ -63,58 +75,85 @@ jest.mock("../../../UI/Components/Forms/ModelForm", () => {
63
75
  };
64
76
  });
65
77
 
66
- /*
67
- * The monitor summary looks its monitors up by id. Off the network, it
68
- * renders how many ids it was given so the count is still observable.
69
- */
70
- jest.mock(
71
- "../../../../App/FeatureSet/Dashboard/src/Components/Monitor/FetchMonitors",
72
- () => {
73
- return {
74
- __esModule: true,
75
- default: (props: { monitorIds: Array<unknown> }): React.ReactElement => {
76
- return <div>{`${props.monitorIds.length} monitor ids`}</div>;
78
+ const getListMock: MockFunction = getJestMockFunction();
79
+ const getItemMock: MockFunction = getJestMockFunction();
80
+
81
+ jest.mock("../../../UI/Utils/ModelAPI/ModelAPI", () => {
82
+ return {
83
+ __esModule: true,
84
+ default: {
85
+ getList: (...args: Array<unknown>) => {
86
+ return getListMock(...args);
77
87
  },
78
- };
79
- },
80
- );
88
+ getItem: (...args: Array<unknown>) => {
89
+ return getItemMock(...args);
90
+ },
91
+ },
92
+ };
93
+ });
81
94
 
82
95
  import AffectedResourcesPicker, {
83
96
  AffectedResourcesPayload,
97
+ NAME_LOADING_PLACEHOLDER,
84
98
  } from "../../../../App/FeatureSet/Dashboard/src/Components/AffectedResources/AffectedResourcesPicker";
85
99
  import IncidentCreate from "../../../../App/FeatureSet/Dashboard/src/Pages/Incidents/Create";
86
100
  import ScheduledMaintenanceCreate from "../../../../App/FeatureSet/Dashboard/src/Pages/ScheduledMaintenanceEvents/Create";
87
101
  import PageComponentProps from "../../../../App/FeatureSet/Dashboard/src/Pages/PageComponentProps";
102
+ import CephCluster from "../../../Models/DatabaseModels/CephCluster";
103
+ import BaseModel from "../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
104
+ import DatabaseServer from "../../../Models/DatabaseModels/DatabaseServer";
105
+ import DockerHost from "../../../Models/DatabaseModels/DockerHost";
106
+ import DockerSwarmCluster from "../../../Models/DatabaseModels/DockerSwarmCluster";
107
+ import Host from "../../../Models/DatabaseModels/Host";
108
+ import IoTFleet from "../../../Models/DatabaseModels/IoTFleet";
109
+ import KubernetesCluster from "../../../Models/DatabaseModels/KubernetesCluster";
110
+ import Monitor from "../../../Models/DatabaseModels/Monitor";
111
+ import NetworkSite from "../../../Models/DatabaseModels/NetworkSite";
112
+ import PodmanHost from "../../../Models/DatabaseModels/PodmanHost";
88
113
  import Project from "../../../Models/DatabaseModels/Project";
114
+ import ProxmoxCluster from "../../../Models/DatabaseModels/ProxmoxCluster";
115
+ import Service from "../../../Models/DatabaseModels/Service";
116
+ import VMwareVCenter from "../../../Models/DatabaseModels/VMwareVCenter";
89
117
  import Route from "../../../Types/API/Route";
118
+ import Includes from "../../../Types/BaseDatabase/Includes";
90
119
  import ObjectID from "../../../Types/ObjectID";
91
120
  import Navigation from "../../../UI/Utils/Navigation";
92
121
  import ProjectUtil from "../../../UI/Utils/Project";
122
+ import User from "../../../UI/Utils/User";
123
+
124
+ const PROJECT_ID: string = "3b1d7c2e-5f4a-4e6b-9c8d-7a6f5e4d3c2b";
93
125
 
94
126
  type ResourceProp = Exclude<
95
127
  keyof AffectedResourcesPayload,
96
128
  "__affectedResourcesPayload"
97
129
  >;
98
130
 
131
+ type ResourceModel = BaseModel & {
132
+ name?: string | undefined;
133
+ };
134
+
135
+ type ResourceModelType = { new (): BaseModel };
136
+
99
137
  /*
100
- * Every relation the picker can write. A Record keyed by the payload's own
101
- * fields, so a resource type added to the picker fails to compile here until
102
- * it is listed.
138
+ * Every relation the picker can write, and the model its names are looked up
139
+ * in. A Record keyed by the payload's own fields, so a resource type added to
140
+ * the picker fails to compile here until it is listed - and the API below
141
+ * can tell whether a lookup went to the right model.
103
142
  */
104
- const RESOURCE_PROPS: Record<ResourceProp, true> = {
105
- monitors: true,
106
- hosts: true,
107
- kubernetesClusters: true,
108
- dockerHosts: true,
109
- podmanHosts: true,
110
- proxmoxClusters: true,
111
- vmwareVCenters: true,
112
- cephClusters: true,
113
- dockerSwarmClusters: true,
114
- iotFleets: true,
115
- databaseServers: true,
116
- networkSites: true,
117
- services: true,
143
+ const RESOURCE_PROPS: Record<ResourceProp, ResourceModelType> = {
144
+ monitors: Monitor,
145
+ hosts: Host,
146
+ kubernetesClusters: KubernetesCluster,
147
+ dockerHosts: DockerHost,
148
+ podmanHosts: PodmanHost,
149
+ proxmoxClusters: ProxmoxCluster,
150
+ vmwareVCenters: VMwareVCenter,
151
+ cephClusters: CephCluster,
152
+ dockerSwarmClusters: DockerSwarmCluster,
153
+ iotFleets: IoTFleet,
154
+ databaseServers: DatabaseServer,
155
+ networkSites: NetworkSite,
156
+ services: Service,
118
157
  };
119
158
 
120
159
  const RESOURCE_PROP_NAMES: Array<ResourceProp> = Object.keys(
@@ -123,16 +162,163 @@ const RESOURCE_PROP_NAMES: Array<ResourceProp> = Object.keys(
123
162
 
124
163
  const NOTHING_SELECTED_PATTERN: RegExp = /No resources affected/;
125
164
 
165
+ type KnownResource = {
166
+ modelType: ResourceModelType;
167
+ name: string;
168
+ };
169
+
170
+ /*
171
+ * The resources the API knows, by id. Each is known under the one model type
172
+ * it was registered with: a lookup of that id against any other model finds
173
+ * nothing, as the server would.
174
+ */
175
+ const KNOWN_RESOURCES: Map<string, KnownResource> = new Map<
176
+ string,
177
+ KnownResource
178
+ >();
179
+
180
+ /*
181
+ * Fresh ids of one resource type, each registered with the API under that
182
+ * type's model and a name saying which prop and which pick it is, so a name
183
+ * on screen can only have come from looking up that very id.
184
+ */
185
+ function knownResources(prop: ResourceProp, count: number): Array<string> {
186
+ const ids: Array<string> = [];
187
+
188
+ for (let index: number = 0; index < count; index++) {
189
+ const id: string = ObjectID.generate().toString();
190
+ KNOWN_RESOURCES.set(id, {
191
+ modelType: RESOURCE_PROPS[prop],
192
+ name: `${prop} resource ${index + 1} ${id.slice(0, 8)}`,
193
+ });
194
+ ids.push(id);
195
+ }
196
+
197
+ return ids;
198
+ }
199
+
200
+ function nameOf(id: string): string {
201
+ const known: KnownResource | undefined = KNOWN_RESOURCES.get(id);
202
+
203
+ expect(known).toBeDefined();
204
+
205
+ return known!.name;
206
+ }
207
+
208
+ type ListRequest = {
209
+ modelType: unknown;
210
+ query?: Record<string, unknown> | undefined;
211
+ select?: Record<string, unknown> | undefined;
212
+ };
213
+
214
+ type ItemRequest = {
215
+ modelType: unknown;
216
+ id: ObjectID;
217
+ select?: Record<string, unknown> | undefined;
218
+ };
219
+
220
+ type ListAnswer = {
221
+ data: Array<ResourceModel>;
222
+ count: number;
223
+ skip: number;
224
+ limit: number;
225
+ };
226
+
227
+ function listOf(data: Array<ResourceModel>): ListAnswer {
228
+ return { data: data, count: data.length, skip: 0, limit: data.length };
229
+ }
230
+
231
+ async function answerList(request: ListRequest): Promise<ListAnswer> {
232
+ const idQuery: unknown = request.query?.["_id"];
233
+
234
+ /*
235
+ * What the pages ask for on mount (the first incident state and the like)
236
+ * has nothing to do with these summaries: an empty list.
237
+ */
238
+ if (!(idQuery instanceof Includes)) {
239
+ return listOf([]);
240
+ }
241
+
242
+ const found: Array<ResourceModel> = [];
243
+
244
+ for (const value of idQuery.values) {
245
+ const id: string = value.toString();
246
+ const known: KnownResource | undefined = KNOWN_RESOURCES.get(id);
247
+
248
+ if (!known || known.modelType !== request.modelType) {
249
+ continue;
250
+ }
251
+
252
+ const model: ResourceModel = new known.modelType() as ResourceModel;
253
+ model._id = id;
254
+ model.name = known.name;
255
+ found.push(model);
256
+ }
257
+
258
+ return listOf(found);
259
+ }
260
+
261
+ type Lookup = {
262
+ modelType: unknown;
263
+ ids: Array<string>;
264
+ select: unknown;
265
+ };
266
+
126
267
  /*
127
- * The selection size the per-type check uses, as a standalone number. Not \b:
128
- * textContent runs a heading into the count after it ("Monitors3 monitor ids").
268
+ * Every request made since the page finished mounting, whichever of getItem
269
+ * or getList carried it, as the model type, ids and columns it asked for.
129
270
  */
130
- const COUNT_OF_THREE_PATTERN: RegExp = /(^|\D)3(\D|$)/;
271
+ function lookupsMade(): Array<Lookup> {
272
+ const itemLookups: Array<Lookup> = getItemMock.mock.calls.map(
273
+ (call: Array<unknown>): Lookup => {
274
+ const request: ItemRequest = call[0] as ItemRequest;
275
+ return {
276
+ modelType: request.modelType,
277
+ ids: [request.id.toString()],
278
+ select: request.select,
279
+ };
280
+ },
281
+ );
282
+
283
+ const listLookups: Array<Lookup> = getListMock.mock.calls.map(
284
+ (call: Array<unknown>): Lookup => {
285
+ const request: ListRequest = call[0] as ListRequest;
286
+ const idQuery: unknown = request.query?.["_id"];
287
+ const ids: Array<string> = [];
288
+
289
+ if (idQuery instanceof Includes) {
290
+ for (const value of idQuery.values) {
291
+ ids.push(value.toString());
292
+ }
293
+ }
294
+
295
+ return {
296
+ modelType: request.modelType,
297
+ ids: ids,
298
+ select: request.select,
299
+ };
300
+ },
301
+ );
302
+
303
+ return [...itemLookups, ...listLookups];
304
+ }
305
+
306
+ // The lookup the picker should make for a batch of ids of one type.
307
+ function nameLookupOf(prop: ResourceProp, ids: Array<string>): Lookup {
308
+ return {
309
+ modelType: RESOURCE_PROPS[prop],
310
+ ids: ids,
311
+ select: expect.objectContaining({ _id: true, name: true }),
312
+ };
313
+ }
131
314
 
132
315
  type PageUnderTest = {
133
316
  name: string;
134
317
  component: React.FunctionComponent<PageComponentProps>;
135
318
  route: string;
319
+ nothingSelectedMessage: string;
320
+ // Offered on this page, so the checks below are not vacuous.
321
+ mustOffer: Array<ResourceProp>;
136
322
  };
137
323
 
138
324
  const PAGES: Array<PageUnderTest> = [
@@ -140,27 +326,30 @@ const PAGES: Array<PageUnderTest> = [
140
326
  name: "incident create",
141
327
  component: IncidentCreate,
142
328
  route: "/dashboard/incidents/create",
329
+ nothingSelectedMessage: "No resources affected by this incident.",
330
+ mustOffer: ["monitors", "podmanHosts", "databaseServers"],
143
331
  },
144
332
  {
145
333
  name: "scheduled maintenance create",
146
334
  component: ScheduledMaintenanceCreate,
147
335
  route: "/dashboard/scheduled-maintenance-events/create",
336
+ nothingSelectedMessage:
337
+ "No resources affected by this scheduled maintenance event.",
338
+ mustOffer: ["monitors", "podmanHosts", "databaseServers", "networkSites"],
148
339
  },
149
340
  ];
150
341
 
151
342
  type PickerStep = {
152
343
  resourceProps: Array<ResourceProp>;
153
- getSummaryElement: (item: Record<string, unknown>) => React.ReactElement;
344
+ getSummaryElement: (item: SummaryItem) => React.ReactElement;
154
345
  };
155
346
 
156
347
  async function openForm(page: PageUnderTest): Promise<CapturedFormProps> {
157
- const project: Project = new Project();
158
-
159
348
  render(
160
349
  <MemoryRouter>
161
350
  <page.component
162
351
  pageRoute={new Route(page.route)}
163
- currentProject={project}
352
+ currentProject={new Project()}
164
353
  hasPaymentMethod={true}
165
354
  />
166
355
  </MemoryRouter>,
@@ -170,12 +359,19 @@ async function openForm(page: PageUnderTest): Promise<CapturedFormProps> {
170
359
  expect(capturedForm).not.toBeNull();
171
360
  });
172
361
 
362
+ /*
363
+ * Whatever the page asks for on mount has been asked by now, so from here
364
+ * on every request is one a summary made.
365
+ */
366
+ getListMock.mockClear();
367
+ getItemMock.mockClear();
368
+
173
369
  return capturedForm!;
174
370
  }
175
371
 
176
372
  /** The fields whose editor is the picker and that render a summary. */
177
373
  function findPickerSteps(form: CapturedFormProps): Array<PickerStep> {
178
- const emptySelection: Record<string, unknown> = {};
374
+ const emptySelection: SummaryItem = {};
179
375
  for (const prop of RESOURCE_PROP_NAMES) {
180
376
  emptySelection[prop] = [];
181
377
  }
@@ -211,27 +407,95 @@ function findPickerSteps(form: CapturedFormProps): Array<PickerStep> {
211
407
  return steps;
212
408
  }
213
409
 
214
- function renderSummary(
410
+ async function openPickerStep(page: PageUnderTest): Promise<PickerStep> {
411
+ const steps: Array<PickerStep> = findPickerSteps(await openForm(page));
412
+
413
+ expect(steps).toHaveLength(1);
414
+
415
+ return steps[0]!;
416
+ }
417
+
418
+ function renderStepSummary(step: PickerStep, item: SummaryItem): RenderResult {
419
+ return render(<MemoryRouter>{step.getSummaryElement(item)}</MemoryRouter>);
420
+ }
421
+
422
+ /*
423
+ * Whether the summary is still waiting on a name: the picker shows a
424
+ * placeholder on a busy chip until the lookup for it lands.
425
+ */
426
+ function isLookingUpNames(container: HTMLElement): boolean {
427
+ return (
428
+ container.querySelectorAll('[aria-busy="true"]').length > 0 ||
429
+ (container.textContent || "").includes(NAME_LOADING_PLACEHOLDER)
430
+ );
431
+ }
432
+
433
+ // Renders the step's summary and waits for every lookup it started to land.
434
+ async function renderSummary(
215
435
  step: PickerStep,
216
- item: Record<string, unknown>,
217
- ): string {
218
- const { container } = render(step.getSummaryElement(item));
219
- const text: string = container.textContent || "";
220
- cleanup();
221
- return text;
436
+ item: SummaryItem,
437
+ ): Promise<RenderResult> {
438
+ const result: RenderResult = renderStepSummary(step, item);
439
+
440
+ await waitFor(() => {
441
+ expect(isLookingUpNames(result.container)).toBe(false);
442
+ });
443
+
444
+ return result;
222
445
  }
223
446
 
224
- function ids(count: number): Array<string> {
225
- return Array.from({ length: count }, () => {
226
- return ObjectID.generate().toString();
447
+ /*
448
+ * Waits for every name to show in the summary. Resolves false rather than
449
+ * throwing when one never does, so a check over every resource type can
450
+ * report all the types that fail at once.
451
+ */
452
+ async function showsEveryName(
453
+ container: HTMLElement,
454
+ names: Array<string>,
455
+ ): Promise<boolean> {
456
+ try {
457
+ await waitFor(() => {
458
+ for (const name of names) {
459
+ expect(within(container).getByText(name)).toBeInTheDocument();
460
+ }
461
+ });
462
+ return true;
463
+ } catch {
464
+ return false;
465
+ }
466
+ }
467
+
468
+ // Lets anything a summary might have started get as far as the API.
469
+ async function nextTick(): Promise<void> {
470
+ await new Promise<void>((resolve: () => void) => {
471
+ setTimeout(resolve, 0);
227
472
  });
228
473
  }
229
474
 
230
475
  describe("the Resources Affected summary step", () => {
231
476
  beforeEach(() => {
232
477
  capturedForm = null;
233
- jest.spyOn(ProjectUtil, "getCurrentProjectId").mockReturnValue(null);
478
+ KNOWN_RESOURCES.clear();
479
+ getListMock.mockReset();
480
+ getItemMock.mockReset();
481
+ getListMock.mockImplementation(answerList as never);
482
+ // Nothing these pages or summaries show is fetched one item at a time.
483
+ getItemMock.mockResolvedValue(null as never);
484
+
485
+ jest
486
+ .spyOn(ProjectUtil, "getCurrentProjectId")
487
+ .mockReturnValue(new ObjectID(PROJECT_ID));
234
488
  jest.spyOn(Navigation, "getQueryStringByName").mockReturnValue(null);
489
+
490
+ /*
491
+ * The picker shows only the types the viewer may read
492
+ * (filterTypesByReadPermission); a type it may not is left out, with a
493
+ * note that something is hidden. What these tests pin is which types the
494
+ * summary is handed, not permissions, so the viewer is one who may read
495
+ * every type - a master admin - rather than whatever the cookies and
496
+ * localStorage of the test environment happen to say.
497
+ */
498
+ jest.spyOn(User, "isMasterAdmin").mockReturnValue(true);
235
499
  });
236
500
 
237
501
  afterEach(() => {
@@ -245,82 +509,205 @@ describe("the Resources Affected summary step", () => {
245
509
 
246
510
  expect(steps).toHaveLength(1);
247
511
  expect(steps[0]!.resourceProps).toEqual(
248
- expect.arrayContaining(["monitors", "podmanHosts"]),
512
+ expect.arrayContaining(page.mustOffer),
249
513
  );
250
514
  });
251
515
 
252
- test("counts a selection of every resource type the picker offers", async () => {
253
- const steps: Array<PickerStep> = findPickerSteps(await openForm(page));
516
+ test("names a selection of every resource type the picker offers", async () => {
517
+ const step: PickerStep = await openPickerStep(page);
254
518
  const missing: Array<string> = [];
255
519
 
256
- for (const step of steps) {
257
- for (const prop of step.resourceProps) {
258
- const text: string = renderSummary(step, { [prop]: ids(3) });
259
-
260
- if (
261
- NOTHING_SELECTED_PATTERN.test(text) ||
262
- !COUNT_OF_THREE_PATTERN.test(text)
263
- ) {
264
- missing.push(`${prop}: "${text}"`);
265
- }
520
+ for (const prop of step.resourceProps) {
521
+ const selected: Array<string> = knownResources(prop, 3);
522
+ const result: RenderResult = renderStepSummary(step, {
523
+ [prop]: selected,
524
+ });
525
+
526
+ const named: boolean = await showsEveryName(
527
+ result.container,
528
+ selected.map(nameOf),
529
+ );
530
+ const text: string = result.container.textContent || "";
531
+
532
+ if (
533
+ !named ||
534
+ NOTHING_SELECTED_PATTERN.test(text) ||
535
+ isLookingUpNames(result.container) ||
536
+ within(result.container).queryByTestId(
537
+ "affected-resources-hidden-note",
538
+ )
539
+ ) {
540
+ missing.push(`${prop}: "${text}"`);
266
541
  }
542
+
543
+ result.unmount();
267
544
  }
268
545
 
269
546
  expect(missing).toEqual([]);
270
547
  });
271
548
 
272
- test("says so when nothing is selected", async () => {
273
- const [step] = findPickerSteps(await openForm(page));
549
+ test("looks each type's names up in its own model, for exactly the ids selected", async () => {
550
+ const step: PickerStep = await openPickerStep(page);
551
+ const lookupsByProp: Record<string, Array<Lookup>> = {};
552
+ const expectedByProp: Record<string, Array<Lookup>> = {};
553
+
554
+ for (const prop of step.resourceProps) {
555
+ const selected: Array<string> = knownResources(prop, 3);
556
+ getListMock.mockClear();
557
+ getItemMock.mockClear();
558
+
559
+ const result: RenderResult = await renderSummary(step, {
560
+ [prop]: selected,
561
+ });
562
+
563
+ lookupsByProp[prop] = lookupsMade();
564
+ expectedByProp[prop] = [nameLookupOf(prop, selected)];
565
+
566
+ result.unmount();
567
+ }
274
568
 
275
- expect(renderSummary(step!, {})).toMatch(NOTHING_SELECTED_PATTERN);
569
+ expect(lookupsByProp).toEqual(expectedByProp);
276
570
  });
277
571
 
278
- test("names Podman hosts, singular and plural", async () => {
279
- const [step] = findPickerSteps(await openForm(page));
572
+ test("names every resource of a mixed selection, each under its type", async () => {
573
+ const step: PickerStep = await openPickerStep(page);
574
+
575
+ const picks: Array<{ prop: ResourceProp; label: string; count: number }> =
576
+ [
577
+ { prop: "monitors", label: "Monitor", count: 1 },
578
+ { prop: "dockerHosts", label: "Docker Host", count: 2 },
579
+ { prop: "podmanHosts", label: "Podman Host", count: 4 },
580
+ { prop: "services", label: "Service", count: 1 },
581
+ { prop: "databaseServers", label: "Database", count: 2 },
582
+ ];
583
+
584
+ const item: SummaryItem = {};
585
+ const expectedLookups: Array<Lookup> = [];
586
+
587
+ for (const pick of picks) {
588
+ const selected: Array<string> = knownResources(pick.prop, pick.count);
589
+ item[pick.prop] = selected;
590
+ expectedLookups.push(nameLookupOf(pick.prop, selected));
591
+ }
280
592
 
281
- expect(renderSummary(step!, { podmanHosts: ids(1) })).toContain(
282
- "1 Podman host",
283
- );
284
- expect(renderSummary(step!, { podmanHosts: ids(1) })).not.toContain(
285
- "1 Podman hosts",
286
- );
287
- expect(renderSummary(step!, { podmanHosts: ids(2) })).toContain(
288
- "2 Podman hosts",
289
- );
593
+ const { container } = await renderSummary(step, item);
594
+
595
+ for (const pick of picks) {
596
+ for (const id of item[pick.prop] as Array<string>) {
597
+ const name: HTMLElement = within(container).getByText(nameOf(id));
598
+
599
+ // The chip reads "<Type label><name>".
600
+ expect(name.parentElement).toHaveTextContent(
601
+ `${pick.label}${nameOf(id)}`,
602
+ );
603
+ }
604
+ }
605
+
606
+ expect(container.textContent).not.toMatch(NOTHING_SELECTED_PATTERN);
607
+
608
+ // One lookup per type, however many of that type were picked.
609
+ expect(lookupsMade()).toHaveLength(expectedLookups.length);
610
+ expect(lookupsMade()).toEqual(expect.arrayContaining(expectedLookups));
290
611
  });
291
612
 
292
- test("offers databases and names them, singular and plural", async () => {
293
- const [step] = findPickerSteps(await openForm(page));
613
+ test("keeps its nothing-selected sentence and looks nothing up when nothing is selected", async () => {
614
+ const step: PickerStep = await openPickerStep(page);
294
615
 
295
- expect(step!.resourceProps).toContain("databaseServers");
296
- expect(renderSummary(step!, { databaseServers: ids(1) })).toContain(
297
- "1 database",
298
- );
299
- expect(renderSummary(step!, { databaseServers: ids(1) })).not.toContain(
300
- "1 databases",
301
- );
302
- expect(renderSummary(step!, { databaseServers: ids(2) })).toContain(
303
- "2 databases",
304
- );
305
- expect(renderSummary(step!, { databaseServers: ids(2) })).not.toMatch(
306
- NOTHING_SELECTED_PATTERN,
307
- );
616
+ const allEmpty: SummaryItem = {};
617
+ for (const prop of RESOURCE_PROP_NAMES) {
618
+ allEmpty[prop] = [];
619
+ }
620
+
621
+ const emptySelections: Array<SummaryItem> = [{}, allEmpty];
622
+
623
+ for (const item of emptySelections) {
624
+ const result: RenderResult = await renderSummary(step, item);
625
+
626
+ await nextTick();
627
+
628
+ expect(result.container).toHaveTextContent(page.nothingSelectedMessage);
629
+ result.unmount();
630
+ }
631
+
632
+ expect(lookupsMade()).toEqual([]);
308
633
  });
309
634
 
310
- test("lists Podman hosts alongside the other resource types", async () => {
311
- const [step] = findPickerSteps(await openForm(page));
635
+ test("reads a resource the API cannot find as unknown instead of dropping it", async () => {
636
+ const step: PickerStep = await openPickerStep(page);
637
+
638
+ const found: string = knownResources("podmanHosts", 1)[0]!;
639
+ // Deleted, or not readable: the lookup does not return it.
640
+ const gone: string = ObjectID.generate().toString();
641
+
642
+ const { container } = await renderSummary(step, {
643
+ podmanHosts: [found, gone],
644
+ });
645
+
646
+ expect(within(container).getByText(nameOf(found))).toBeInTheDocument();
647
+ expect(
648
+ within(container).getByText("Unknown Podman Host"),
649
+ ).toBeInTheDocument();
650
+ expect(container.textContent).not.toMatch(NOTHING_SELECTED_PATTERN);
651
+ expect(lookupsMade()).toEqual([
652
+ nameLookupOf("podmanHosts", [found, gone]),
653
+ ]);
654
+ });
312
655
 
313
- const text: string = renderSummary(step!, {
314
- monitors: ids(1),
315
- dockerHosts: ids(2),
316
- podmanHosts: ids(4),
317
- services: ids(1),
656
+ test("names resources that arrive with their name without looking them up", async () => {
657
+ /*
658
+ * The alert prefill and incident templates hand the form resources as
659
+ * { _id, name } objects rather than bare ids. Those are named as they
660
+ * come; only a bare id among them is looked up.
661
+ */
662
+ const step: PickerStep = await openPickerStep(page);
663
+
664
+ const namedMonitor: { _id: string; name: string } = {
665
+ _id: ObjectID.generate().toString(),
666
+ name: "Checkout API monitor",
667
+ };
668
+ const namedPodmanHost: { _id: string; name: string } = {
669
+ _id: ObjectID.generate().toString(),
670
+ name: "podman-edge-01",
671
+ };
672
+ const bareId: string = knownResources("podmanHosts", 1)[0]!;
673
+
674
+ const { container } = await renderSummary(step, {
675
+ monitors: [namedMonitor],
676
+ podmanHosts: [namedPodmanHost, bareId],
318
677
  });
319
678
 
320
- expect(text).toContain("1 monitor ids");
321
- expect(text).toContain("2 Docker hosts");
322
- expect(text).toContain("4 Podman hosts");
323
- expect(text).toContain("1 service");
679
+ expect(
680
+ within(container).getByText(namedMonitor.name),
681
+ ).toBeInTheDocument();
682
+ expect(
683
+ within(container).getByText(namedPodmanHost.name),
684
+ ).toBeInTheDocument();
685
+ expect(within(container).getByText(nameOf(bareId))).toBeInTheDocument();
686
+ expect(lookupsMade()).toEqual([nameLookupOf("podmanHosts", [bareId])]);
687
+ });
688
+
689
+ test("is read-only: no search input and no remove buttons", async () => {
690
+ const step: PickerStep = await openPickerStep(page);
691
+
692
+ const monitors: Array<string> = knownResources("monitors", 1);
693
+ const podmanHosts: Array<string> = knownResources("podmanHosts", 2);
694
+
695
+ const { container } = await renderSummary(step, {
696
+ monitors: monitors,
697
+ podmanHosts: podmanHosts,
698
+ });
699
+
700
+ // Not vacuous: the chips are there.
701
+ for (const id of [...monitors, ...podmanHosts]) {
702
+ expect(within(container).getByText(nameOf(id))).toBeInTheDocument();
703
+ }
704
+
705
+ expect(container.querySelectorAll("input")).toHaveLength(0);
706
+ expect(within(container).queryAllByRole("textbox")).toHaveLength(0);
707
+ expect(within(container).queryAllByRole("combobox")).toHaveLength(0);
708
+ expect(
709
+ within(container).queryAllByRole("button", { name: /^Remove\b/ }),
710
+ ).toHaveLength(0);
324
711
  });
325
712
  });
326
713
  });