@oneuptime/common 14.0.2 → 14.0.3

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 (166) hide show
  1. package/Models/DatabaseModels/AlertFeed.ts +2 -0
  2. package/Models/DatabaseModels/IncidentAlert.ts +540 -0
  3. package/Models/DatabaseModels/IncidentFeed.ts +2 -0
  4. package/Models/DatabaseModels/Index.ts +2 -0
  5. package/Models/DatabaseModels/Project.ts +60 -0
  6. package/Models/DatabaseModels/RumApplication.ts +52 -1
  7. package/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.ts +28 -0
  8. package/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.ts +89 -0
  9. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  10. package/Server/Services/IncidentAlertService.ts +1811 -0
  11. package/Server/Services/IncidentService.ts +210 -10
  12. package/Server/Services/IncidentStateTimelineService.ts +24 -0
  13. package/Server/Services/Index.ts +2 -0
  14. package/Server/Utils/SessionReplay/SessionReplayGateCache.ts +24 -2
  15. package/Tests/App/Dashboard/BulkIncidentLinkActions.test.tsx +869 -0
  16. package/Tests/App/Dashboard/CephMetricTooltips.test.tsx +1777 -0
  17. package/Tests/App/Dashboard/CloudFleetSummary.test.ts +8 -0
  18. package/Tests/App/Dashboard/CloudMetricTooltips.test.tsx +887 -0
  19. package/Tests/App/Dashboard/ContainerHostMetricTooltips.test.tsx +1309 -0
  20. package/Tests/App/Dashboard/DiscoveryScanLivePage.test.tsx +39 -0
  21. package/Tests/App/Dashboard/DockerSwarmClustersListTooltips.test.tsx +414 -0
  22. package/Tests/App/Dashboard/DockerSwarmMetricTooltips.test.ts +796 -0
  23. package/Tests/App/Dashboard/DockerSwarmOverviewTooltips.test.tsx +747 -0
  24. package/Tests/App/Dashboard/ExceptionMonitorEnvironmentFilter.test.tsx +38 -18
  25. package/Tests/App/Dashboard/HostDetailViewTooltips.test.tsx +725 -0
  26. package/Tests/App/Dashboard/HostListColumnTooltips.test.tsx +472 -0
  27. package/Tests/App/Dashboard/HostMetricTooltips.test.ts +709 -0
  28. package/Tests/App/Dashboard/HostOverviewTooltips.test.tsx +698 -0
  29. package/Tests/App/Dashboard/HostTooltipHarness.ts +124 -0
  30. package/Tests/App/Dashboard/HostsListTooltips.test.tsx +395 -0
  31. package/Tests/App/Dashboard/IncidentAlertLinkDialog.test.tsx +691 -0
  32. package/Tests/App/Dashboard/IncidentAlertLinkHelpers.test.ts +458 -0
  33. package/Tests/App/Dashboard/IncidentAlertLinkPages.test.tsx +828 -0
  34. package/Tests/App/Dashboard/IncidentAlertLinkSideMenus.test.tsx +231 -0
  35. package/Tests/App/Dashboard/IncidentCreateFromAlerts.test.tsx +689 -0
  36. package/Tests/App/Dashboard/IoTMetricTooltips.test.tsx +1123 -0
  37. package/Tests/App/Dashboard/KubernetesClusterMetricTooltips.test.ts +837 -0
  38. package/Tests/App/Dashboard/KubernetesClusterOverviewTooltips.test.tsx +699 -0
  39. package/Tests/App/Dashboard/KubernetesContainersTabTooltips.test.tsx +320 -0
  40. package/Tests/App/Dashboard/KubernetesCostTooltips.test.tsx +502 -0
  41. package/Tests/App/Dashboard/KubernetesInsightsTooltips.test.tsx +222 -0
  42. package/Tests/App/Dashboard/KubernetesResourceMetricTooltips.test.ts +561 -0
  43. package/Tests/App/Dashboard/KubernetesResourcePagesTooltips.test.tsx +1168 -0
  44. package/Tests/App/Dashboard/LinkIncidentAlertModal.test.tsx +154 -0
  45. package/Tests/App/Dashboard/MetricDescriptionRules.ts +170 -0
  46. package/Tests/App/Dashboard/MetricDescriptionsCatalog.test.ts +206 -0
  47. package/Tests/App/Dashboard/MetricDescriptionsConsistency.test.ts +275 -0
  48. package/Tests/App/Dashboard/NetworkDeviceTooltips.test.tsx +852 -0
  49. package/Tests/App/Dashboard/NetworkDiagnosticsTooltips.test.tsx +556 -0
  50. package/Tests/App/Dashboard/NetworkMetricTooltips.test.ts +1348 -0
  51. package/Tests/App/Dashboard/NetworkSiteTooltips.test.tsx +679 -0
  52. package/Tests/App/Dashboard/ProxmoxMetricTooltips.test.ts +592 -0
  53. package/Tests/App/Dashboard/ProxmoxOverviewTooltips.test.tsx +1002 -0
  54. package/Tests/App/Dashboard/ProxmoxResourcePageTooltips.test.tsx +923 -0
  55. package/Tests/App/Dashboard/ResourceOverviewTileTooltips.test.tsx +1066 -0
  56. package/Tests/App/Dashboard/RumOverviewPage.test.tsx +1195 -0
  57. package/Tests/App/Dashboard/ServerlessMetricTooltips.test.tsx +474 -0
  58. package/Tests/App/Dashboard/ServiceMetricTooltips.test.tsx +772 -0
  59. package/Tests/App/Dashboard/SessionReplayAuditTable.test.tsx +56 -1
  60. package/Tests/App/Dashboard/TelemetryMetricsSignals.test.ts +1207 -0
  61. package/Tests/App/Dashboard/TelemetryResourceTabTooltips.test.tsx +437 -0
  62. package/Tests/App/Dashboard/VMwareMetricTooltips.test.ts +801 -0
  63. package/Tests/App/Dashboard/VMwareOverviewTooltips.test.tsx +601 -0
  64. package/Tests/App/Dashboard/VMwareResourcePagesTooltips.test.tsx +646 -0
  65. package/Tests/App/Dashboard/VMwareTooltipHarness.ts +145 -0
  66. package/Tests/Models/DatabaseModels/IncidentAlertModel.test.ts +554 -0
  67. package/Tests/Models/DatabaseModels/SessionReplayModels.test.ts +117 -0
  68. package/Tests/Server/Infrastructure/Postgres/AddIncidentAlertMigration.test.ts +341 -0
  69. package/Tests/Server/Infrastructure/Postgres/AddSessionReplaySameOriginTracePropagationMigration.test.ts +247 -0
  70. package/Tests/Server/Services/IncidentAlertPostgres.test.ts +903 -0
  71. package/Tests/Server/Services/IncidentAlertRegistration.test.ts +96 -0
  72. package/Tests/Server/Services/IncidentAlertService.test.ts +2225 -0
  73. package/Tests/Server/Services/IncidentAlertStateCascade.test.ts +970 -0
  74. package/Tests/Server/Services/IncidentCreateFromAlerts.test.ts +1284 -0
  75. package/Tests/Server/Utils/SessionReplay/SessionReplayGateCachePolicy.test.ts +103 -0
  76. package/Tests/Types/Rum/WebVitals.test.ts +173 -0
  77. package/Tests/UI/Components/FieldLabelInfoCardTooltip.test.tsx +413 -0
  78. package/Tests/UI/Components/InfoTooltip.test.tsx +242 -0
  79. package/Tests/UI/Components/ModelTable/BaseModelTableBulkDelete.test.tsx +207 -3
  80. package/Tests/UI/Components/ModelTable/BaseModelTableBulkDeleteVerb.test.tsx +367 -0
  81. package/Tests/UI/Components/ModelTable/BaseModelTableCreateButtonTitle.test.tsx +275 -0
  82. package/Tests/UI/Components/TableHeaderTooltip.test.tsx +662 -0
  83. package/Tests/UI/Rum/PrivacySummaryCard.test.tsx +166 -9
  84. package/Tests/UI/Rum/RecordingHealthTooltips.test.tsx +447 -0
  85. package/Tests/UI/Rum/ReplayCorrelationPanel.test.tsx +164 -0
  86. package/Tests/UI/Rum/ReplayFrameCapture.test.ts +6750 -0
  87. package/Tests/UI/Rum/ReplayRail.test.tsx +723 -12
  88. package/Tests/UI/Rum/ReplayRailDetail.test.tsx +86 -0
  89. package/Tests/UI/Rum/ReplayScreenshot.test.ts +2124 -0
  90. package/Tests/UI/Rum/ReplayScreenshotActions.test.tsx +3288 -0
  91. package/Tests/UI/Rum/ReplayStage.test.tsx +62 -0
  92. package/Tests/UI/Rum/ReplayStageOverlays.test.tsx +983 -2
  93. package/Tests/UI/Rum/SessionReplaySettingsPolicyLoading.test.tsx +149 -0
  94. package/Tests/UI/Rum/SessionReplaySetupGuide.test.tsx +107 -6
  95. package/Tests/UI/Rum/SessionReplayUsersTableTooltips.test.tsx +330 -0
  96. package/Tests/UI/Rum/UserFlowTooltips.test.tsx +886 -0
  97. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +10 -0
  98. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +2 -0
  99. package/Tests/UI/Utils/PermissionGate.test.ts +76 -0
  100. package/Tests/Utils/Incident/IncidentFromAlerts.test.ts +696 -0
  101. package/Tests/Utils/Rum/SessionTraceState.test.ts +239 -0
  102. package/Types/Incident/IncidentAlertLink.ts +38 -0
  103. package/Types/Permission.ts +48 -0
  104. package/Types/Rum/SessionReplay.ts +25 -6
  105. package/Types/Rum/WebVitals.ts +52 -0
  106. package/UI/Components/Detail/FieldLabel.tsx +8 -0
  107. package/UI/Components/InfoCard/InfoCard.tsx +47 -1
  108. package/UI/Components/ModelTable/BaseModelTable.tsx +95 -17
  109. package/UI/Components/ModelTable/Column.ts +5 -0
  110. package/UI/Components/Table/TableHeader.tsx +47 -2
  111. package/UI/Components/Table/Types/Column.ts +7 -0
  112. package/UI/Components/Tooltip/InfoTooltip.tsx +78 -0
  113. package/UI/Utils/PermissionGate.ts +11 -2
  114. package/Utils/Incident/IncidentFromAlerts.ts +516 -0
  115. package/Utils/Rum/SessionTraceState.ts +212 -0
  116. package/build/dist/Models/DatabaseModels/AlertFeed.js +2 -0
  117. package/build/dist/Models/DatabaseModels/AlertFeed.js.map +1 -1
  118. package/build/dist/Models/DatabaseModels/IncidentAlert.js +553 -0
  119. package/build/dist/Models/DatabaseModels/IncidentAlert.js.map +1 -0
  120. package/build/dist/Models/DatabaseModels/IncidentFeed.js +2 -0
  121. package/build/dist/Models/DatabaseModels/IncidentFeed.js.map +1 -1
  122. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  123. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  124. package/build/dist/Models/DatabaseModels/Project.js +62 -0
  125. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  126. package/build/dist/Models/DatabaseModels/RumApplication.js +53 -1
  127. package/build/dist/Models/DatabaseModels/RumApplication.js.map +1 -1
  128. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.js +20 -0
  129. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.js.map +1 -0
  130. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.js +42 -0
  131. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.js.map +1 -0
  132. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  133. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  134. package/build/dist/Server/Services/IncidentAlertService.js +1370 -0
  135. package/build/dist/Server/Services/IncidentAlertService.js.map +1 -0
  136. package/build/dist/Server/Services/IncidentService.js +164 -12
  137. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  138. package/build/dist/Server/Services/IncidentStateTimelineService.js +21 -0
  139. package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
  140. package/build/dist/Server/Services/Index.js +2 -0
  141. package/build/dist/Server/Services/Index.js.map +1 -1
  142. package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js +8 -0
  143. package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js.map +1 -1
  144. package/build/dist/Types/Incident/IncidentAlertLink.js +34 -0
  145. package/build/dist/Types/Incident/IncidentAlertLink.js.map +1 -0
  146. package/build/dist/Types/Permission.js +42 -0
  147. package/build/dist/Types/Permission.js.map +1 -1
  148. package/build/dist/Types/Rum/WebVitals.js +25 -0
  149. package/build/dist/Types/Rum/WebVitals.js.map +1 -1
  150. package/build/dist/UI/Components/Detail/FieldLabel.js +2 -0
  151. package/build/dist/UI/Components/Detail/FieldLabel.js.map +1 -1
  152. package/build/dist/UI/Components/InfoCard/InfoCard.js +22 -1
  153. package/build/dist/UI/Components/InfoCard/InfoCard.js.map +1 -1
  154. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +66 -14
  155. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  156. package/build/dist/UI/Components/Table/TableHeader.js +18 -2
  157. package/build/dist/UI/Components/Table/TableHeader.js.map +1 -1
  158. package/build/dist/UI/Components/Tooltip/InfoTooltip.js +28 -0
  159. package/build/dist/UI/Components/Tooltip/InfoTooltip.js.map +1 -0
  160. package/build/dist/UI/Utils/PermissionGate.js +4 -2
  161. package/build/dist/UI/Utils/PermissionGate.js.map +1 -1
  162. package/build/dist/Utils/Incident/IncidentFromAlerts.js +306 -0
  163. package/build/dist/Utils/Incident/IncidentFromAlerts.js.map +1 -0
  164. package/build/dist/Utils/Rum/SessionTraceState.js +154 -0
  165. package/build/dist/Utils/Rum/SessionTraceState.js.map +1 -0
  166. package/package.json +1 -1
@@ -0,0 +1,646 @@
1
+ /**
2
+ * @timezone UTC
3
+ */
4
+ import {
5
+ afterEach,
6
+ beforeEach,
7
+ describe,
8
+ expect,
9
+ jest,
10
+ test,
11
+ } from "@jest/globals";
12
+ import "@testing-library/jest-dom";
13
+ import {
14
+ cleanup,
15
+ fireEvent,
16
+ render,
17
+ screen,
18
+ waitFor,
19
+ } from "@testing-library/react";
20
+ import React from "react";
21
+
22
+ /*
23
+ * The VMware detail pages (host, VM, datastore, cluster) and list pages
24
+ * (hosts, VMs, datastores, clusters, resource pools), rendered for real
25
+ * against a stubbed inventory. Every metric summary field and every metric
26
+ * column header must carry an (i) with its own explanation; identity
27
+ * fields (names, IDs, timestamps) and plain metadata columns must not.
28
+ */
29
+
30
+ interface ListRequest {
31
+ query: Record<string, unknown>;
32
+ }
33
+
34
+ const mockGetListCalls: Array<ListRequest> = [];
35
+ let mockVCenter: unknown = null;
36
+ let mockRowsByKind: Record<string, Array<unknown>> = {};
37
+ let mockAggregateData: Array<Record<string, unknown>> = [];
38
+
39
+ jest.mock("../../../UI/Utils/ModelAPI/ModelAPI", () => {
40
+ return {
41
+ __esModule: true,
42
+ default: {
43
+ getItem: (): Promise<unknown> => {
44
+ return Promise.resolve(mockVCenter);
45
+ },
46
+ getList: (request: ListRequest): Promise<unknown> => {
47
+ mockGetListCalls.push(request);
48
+ const rows: Array<unknown> =
49
+ mockRowsByKind[String(request.query["kind"])] || [];
50
+ return Promise.resolve({ data: rows, count: rows.length });
51
+ },
52
+ },
53
+ };
54
+ });
55
+
56
+ jest.mock("../../../UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI", () => {
57
+ return {
58
+ __esModule: true,
59
+ default: {
60
+ aggregate: (): Promise<unknown> => {
61
+ return Promise.resolve({ data: mockAggregateData });
62
+ },
63
+ },
64
+ };
65
+ });
66
+
67
+ // The Metrics tab is not the subject here, and it is not the open tab.
68
+ jest.mock(
69
+ "../../../../App/FeatureSet/Dashboard/src/Components/Infrastructure/ResourceMetricsTab",
70
+ () => {
71
+ return {
72
+ __esModule: true,
73
+ default: (): React.ReactElement => {
74
+ return React.createElement("div", { "data-testid": "metrics-tab" });
75
+ },
76
+ };
77
+ },
78
+ );
79
+
80
+ import HostDetail from "../../../../App/FeatureSet/Dashboard/src/Pages/VMware/View/HostDetail";
81
+ import VirtualMachineDetail from "../../../../App/FeatureSet/Dashboard/src/Pages/VMware/View/VirtualMachineDetail";
82
+ import DatastoreDetail from "../../../../App/FeatureSet/Dashboard/src/Pages/VMware/View/DatastoreDetail";
83
+ import ClusterDetail from "../../../../App/FeatureSet/Dashboard/src/Pages/VMware/View/ClusterDetail";
84
+ import HostsPage from "../../../../App/FeatureSet/Dashboard/src/Pages/VMware/View/Hosts";
85
+ import VirtualMachinesPage from "../../../../App/FeatureSet/Dashboard/src/Pages/VMware/View/VirtualMachines";
86
+ import DatastoresPage from "../../../../App/FeatureSet/Dashboard/src/Pages/VMware/View/Datastores";
87
+ import ClustersPage from "../../../../App/FeatureSet/Dashboard/src/Pages/VMware/View/Clusters";
88
+ import ResourcePoolsPage from "../../../../App/FeatureSet/Dashboard/src/Pages/VMware/View/ResourcePools";
89
+ import PageComponentProps from "../../../../App/FeatureSet/Dashboard/src/Pages/PageComponentProps";
90
+ import {
91
+ VMWARE_METRIC_DESCRIPTIONS,
92
+ VMwareMetric,
93
+ } from "../../../../App/FeatureSet/Dashboard/src/Components/MetricDescriptions/VMwareMetricDescriptions";
94
+ import Route from "../../../Types/API/Route";
95
+ import ObjectID from "../../../Types/ObjectID";
96
+ import ProjectUtil from "../../../UI/Utils/Project";
97
+ import {
98
+ GIB,
99
+ explainedLabels,
100
+ explanationOnFocus,
101
+ expectNotNested,
102
+ infoButtons,
103
+ infoButtonsFor,
104
+ infoLabels,
105
+ inventoryRow,
106
+ vcenterModel,
107
+ } from "./VMwareTooltipHarness";
108
+
109
+ const PROJECT_ID: string = "22222222-2222-4222-8222-222222222222";
110
+ const VCENTER_ID: string = "11111111-1111-4111-8111-111111111111";
111
+ const BASE: string = `/dashboard/${PROJECT_ID}/vmware/${VCENTER_ID}`;
112
+
113
+ const D: Record<VMwareMetric, string> = VMWARE_METRIC_DESCRIPTIONS;
114
+ const MIB: number = 1024 * 1024;
115
+
116
+ const PAGE_PROPS: PageComponentProps = {
117
+ pageRoute: new Route(BASE),
118
+ currentProject: null,
119
+ hasPaymentMethod: false,
120
+ };
121
+
122
+ function openAt(pathname: string): void {
123
+ window.history.pushState({}, "", pathname);
124
+ }
125
+
126
+ function detailUrl(segment: string, externalId: string): string {
127
+ return `${BASE}/${segment}/${encodeURIComponent(externalId)}`;
128
+ }
129
+
130
+ beforeEach(() => {
131
+ jest.useFakeTimers();
132
+ mockGetListCalls.length = 0;
133
+ mockVCenter = vcenterModel();
134
+ mockRowsByKind = {};
135
+ mockAggregateData = [];
136
+ jest
137
+ .spyOn(ProjectUtil, "getCurrentProjectId")
138
+ .mockReturnValue(new ObjectID(PROJECT_ID));
139
+ });
140
+
141
+ afterEach(() => {
142
+ cleanup();
143
+ jest.restoreAllMocks();
144
+ jest.useRealTimers();
145
+ window.history.pushState({}, "", "/");
146
+ });
147
+
148
+ /*
149
+ * The labels of a detail page's summary cards that carry NO (i): identity
150
+ * and timestamp fields, which need no explanation.
151
+ */
152
+ function expectNoInfoOn(titles: Array<string>): void {
153
+ for (const title of titles) {
154
+ expect({ title, buttons: infoButtonsFor(title).length }).toEqual({
155
+ title,
156
+ buttons: 0,
157
+ });
158
+ }
159
+ }
160
+
161
+ async function waitForInfo(label: string): Promise<void> {
162
+ await waitFor(() => {
163
+ expect(infoButtonsFor(label).length).toBeGreaterThan(0);
164
+ });
165
+ }
166
+
167
+ describe("VMware ESXi host detail", () => {
168
+ const EXTERNAL_ID: string = "host/dc1/esx01";
169
+
170
+ function host(fields: Record<string, unknown>): unknown {
171
+ return inventoryRow({
172
+ kind: "Host",
173
+ externalId: EXTERNAL_ID,
174
+ name: "esx01",
175
+ datacenterName: "dc1",
176
+ clusterName: "cl1",
177
+ metricsUpdatedAt: new Date(),
178
+ lastSeenAt: new Date(),
179
+ ...fields,
180
+ });
181
+ }
182
+
183
+ test("CPU and memory carry their explanations; identity fields do not", async () => {
184
+ mockRowsByKind["Host"] = [
185
+ host({
186
+ latestCpuPercent: 37.5,
187
+ latestCpuMhz: 12000,
188
+ cpuCapacityMhz: 48000,
189
+ latestMemoryBytes: 64 * GIB,
190
+ maxMemoryBytes: 256 * GIB,
191
+ latestMemoryPercent: 25,
192
+ }),
193
+ ];
194
+ openAt(detailUrl("hosts", EXTERNAL_ID));
195
+
196
+ render(<HostDetail {...PAGE_PROPS} />);
197
+ await waitForInfo("CPU");
198
+
199
+ expect(await explainedLabels()).toEqual([
200
+ ["CPU", D.hostCpu],
201
+ ["Memory (Used / Capacity)", D.hostMemory],
202
+ ]);
203
+ expect(
204
+ screen.getByText("37.5% (12.00 GHz of 48.00 GHz)"),
205
+ ).toBeInTheDocument();
206
+ expectNoInfoOn([
207
+ "Host Name",
208
+ "vCenter",
209
+ "Datacenter",
210
+ "Cluster",
211
+ "External ID",
212
+ "Metrics Updated",
213
+ "Last Seen",
214
+ ]);
215
+ });
216
+
217
+ test("a host that reports capacity but no usage explains its CPU Capacity instead", async () => {
218
+ mockRowsByKind["Host"] = [host({ cpuCapacityMhz: 48000 })];
219
+ openAt(detailUrl("hosts", EXTERNAL_ID));
220
+
221
+ render(<HostDetail {...PAGE_PROPS} />);
222
+ await waitForInfo("CPU Capacity");
223
+
224
+ expect(await explainedLabels()).toEqual([
225
+ ["CPU Capacity", D.hostCpuCapacity],
226
+ ]);
227
+ });
228
+ });
229
+
230
+ describe("VMware virtual machine detail", () => {
231
+ const EXTERNAL_ID: string = "vm/4210aaaa-bbbb-cccc-dddd-eeeeffff0001";
232
+
233
+ function vm(fields: Record<string, unknown>): unknown {
234
+ return inventoryRow({
235
+ kind: "VirtualMachine",
236
+ externalId: EXTERNAL_ID,
237
+ name: "web-01",
238
+ vmInstanceUuid: "4210aaaa-bbbb-cccc-dddd-eeeeffff0001",
239
+ hostName: "esx01",
240
+ clusterName: "cl1",
241
+ datacenterName: "dc1",
242
+ resourcePoolName: "Resources",
243
+ isTemplate: false,
244
+ metricsUpdatedAt: new Date(),
245
+ lastSeenAt: new Date(),
246
+ ...fields,
247
+ });
248
+ }
249
+
250
+ test("a running VM under memory pressure explains every number it shows", async () => {
251
+ mockRowsByKind["VirtualMachine"] = [
252
+ vm({
253
+ isPoweredOn: true,
254
+ latestCpuPercent: 64,
255
+ latestCpuMhz: 2400,
256
+ cpuReadinessPercent: 12.5,
257
+ latestMemoryBytes: 3 * GIB,
258
+ maxMemoryBytes: 8 * GIB,
259
+ latestMemoryPercent: 37.5,
260
+ memoryBalloonedBytes: 512 * MIB,
261
+ memorySwappedBytes: 128 * MIB,
262
+ latestDiskBytes: 40 * GIB,
263
+ maxDiskBytes: 100 * GIB,
264
+ latestDiskPercent: 40,
265
+ }),
266
+ ];
267
+ openAt(detailUrl("virtual-machines", EXTERNAL_ID));
268
+
269
+ render(<VirtualMachineDetail {...PAGE_PROPS} />);
270
+ await waitForInfo("Power State");
271
+
272
+ expect(await explainedLabels()).toEqual([
273
+ ["Power State", D.vmPowerState],
274
+ ["CPU", D.vmCpu],
275
+ ["CPU Ready", D.vmCpuReady],
276
+ ["Memory (Used / Configured)", D.vmMemory],
277
+ ["Memory Ballooned", D.vmMemoryBallooned],
278
+ ["Memory Swapped", D.vmMemorySwapped],
279
+ ["Disk (Used / Provisioned)", D.vmDisk],
280
+ ]);
281
+ // Above 10% CPU Ready the value itself is flagged.
282
+ expect(screen.getByText("12.5% — CPU contention")).toBeInTheDocument();
283
+ expectNoInfoOn([
284
+ "VM Name",
285
+ "vCenter",
286
+ "Instance UUID",
287
+ "Host",
288
+ "Cluster",
289
+ "Datacenter",
290
+ "Resource Pool",
291
+ "External ID",
292
+ "Metrics Updated",
293
+ "Last Seen",
294
+ ]);
295
+ });
296
+
297
+ test("a powered-off VM with no CPU reading still explains its N/A", async () => {
298
+ mockRowsByKind["VirtualMachine"] = [
299
+ vm({
300
+ isPoweredOn: false,
301
+ latestDiskBytes: 40 * GIB,
302
+ maxDiskBytes: 100 * GIB,
303
+ }),
304
+ ];
305
+ openAt(detailUrl("virtual-machines", EXTERNAL_ID));
306
+
307
+ render(<VirtualMachineDetail {...PAGE_PROPS} />);
308
+ await waitForInfo("CPU");
309
+
310
+ expect(
311
+ screen.getByText("N/A — CPU metrics are only reported while powered on"),
312
+ ).toBeInTheDocument();
313
+ expect(await explainedLabels()).toEqual([
314
+ ["Power State", D.vmPowerState],
315
+ ["CPU", D.vmCpu],
316
+ ["Disk (Used / Provisioned)", D.vmDisk],
317
+ ]);
318
+ // Ballooned / swapped are only shown above zero.
319
+ expectNoInfoOn(["Memory Ballooned", "Memory Swapped", "CPU Ready"]);
320
+ });
321
+
322
+ test("a template's Type row reuses the power-state explanation, which names templates", async () => {
323
+ mockRowsByKind["VirtualMachine"] = [
324
+ vm({
325
+ isTemplate: true,
326
+ latestDiskBytes: 20 * GIB,
327
+ maxDiskBytes: 60 * GIB,
328
+ }),
329
+ ];
330
+ openAt(detailUrl("virtual-machines", EXTERNAL_ID));
331
+
332
+ render(<VirtualMachineDetail {...PAGE_PROPS} />);
333
+ await waitForInfo("Type");
334
+
335
+ expect(await explainedLabels()).toEqual([
336
+ ["Type", D.vmPowerState],
337
+ ["Disk (Used / Provisioned)", D.vmDisk],
338
+ ]);
339
+ expect(D.vmPowerState).toContain("Templates never run");
340
+ });
341
+ });
342
+
343
+ describe("VMware datastore detail", () => {
344
+ const EXTERNAL_ID: string = "datastore/dc1/ds1";
345
+
346
+ test("capacity fields and the growth forecast are explained", async () => {
347
+ mockRowsByKind["Datastore"] = [
348
+ inventoryRow({
349
+ kind: "Datastore",
350
+ externalId: EXTERNAL_ID,
351
+ name: "ds1",
352
+ datacenterName: "dc1",
353
+ latestDiskBytes: 85 * GIB,
354
+ maxDiskBytes: 100 * GIB,
355
+ latestDiskPercent: 85,
356
+ metricsUpdatedAt: new Date(),
357
+ }),
358
+ ];
359
+ // Growing 1 GiB an hour over the last 24 hours: full in about 15 hours.
360
+ const now: number = Date.now();
361
+ mockAggregateData = [0, 1, 2, 3, 4, 5].map(
362
+ (step: number): Record<string, unknown> => {
363
+ return {
364
+ timestamp: new Date(now - (5 - step) * 60 * 60 * 1000),
365
+ value: (80 + step) * GIB,
366
+ };
367
+ },
368
+ );
369
+ openAt(detailUrl("datastores", EXTERNAL_ID));
370
+
371
+ render(<DatastoreDetail {...PAGE_PROPS} />);
372
+ await waitForInfo("Growth Forecast");
373
+
374
+ expect(await explainedLabels()).toEqual([
375
+ ["Used", D.datastoreUsed],
376
+ ["Capacity", D.datastoreCapacity],
377
+ ["Free", D.datastoreFree],
378
+ ["Used %", D.datastoreUsedPercent],
379
+ ["Growth Forecast", D.datastoreGrowthForecast],
380
+ ]);
381
+ expectNoInfoOn([
382
+ "Datastore Name",
383
+ "vCenter",
384
+ "Datacenter",
385
+ "External ID",
386
+ "Metrics Updated",
387
+ ]);
388
+ });
389
+
390
+ test("without a forecast the other fields keep their (i)s", async () => {
391
+ mockRowsByKind["Datastore"] = [
392
+ inventoryRow({
393
+ kind: "Datastore",
394
+ externalId: EXTERNAL_ID,
395
+ name: "ds1",
396
+ latestDiskBytes: 10 * GIB,
397
+ maxDiskBytes: 100 * GIB,
398
+ }),
399
+ ];
400
+ openAt(detailUrl("datastores", EXTERNAL_ID));
401
+
402
+ render(<DatastoreDetail {...PAGE_PROPS} />);
403
+ await waitForInfo("Used %");
404
+
405
+ expect(infoLabels()).toEqual(["Used", "Capacity", "Free", "Used %"]);
406
+ });
407
+ });
408
+
409
+ describe("VMware cluster detail", () => {
410
+ const EXTERNAL_ID: string = "cluster/dc1/cl1";
411
+
412
+ test("host, VM and capacity counts are explained", async () => {
413
+ mockRowsByKind["Cluster"] = [
414
+ inventoryRow({
415
+ kind: "Cluster",
416
+ externalId: EXTERNAL_ID,
417
+ name: "cl1",
418
+ datacenterName: "dc1",
419
+ hostCount: 4,
420
+ effectiveHostCount: 3,
421
+ vmCount: 20,
422
+ poweredOnVmCount: 18,
423
+ vmTemplateCount: 2,
424
+ cpuEffectiveMhz: 90000,
425
+ cpuCapacityMhz: 120000,
426
+ memoryEffectiveBytes: 768 * GIB,
427
+ maxMemoryBytes: 1024 * GIB,
428
+ metricsUpdatedAt: new Date(),
429
+ }),
430
+ ];
431
+ openAt(detailUrl("clusters", EXTERNAL_ID));
432
+
433
+ render(<ClusterDetail {...PAGE_PROPS} />);
434
+ await waitForInfo("Hosts");
435
+
436
+ expect(await explainedLabels()).toEqual([
437
+ ["Hosts", D.clusterHosts],
438
+ ["Virtual Machines", D.clusterVirtualMachines],
439
+ ["VM Templates", D.clusterTemplates],
440
+ ["CPU (Effective / Total)", D.clusterCpu],
441
+ ["Memory (Effective / Total)", D.clusterMemory],
442
+ ]);
443
+ // The not-effective host is flagged in the value the (i) explains.
444
+ expect(
445
+ screen.getByText(
446
+ "3 of 4 effective — 1 in maintenance mode or unresponsive",
447
+ ),
448
+ ).toBeInTheDocument();
449
+ expectNoInfoOn([
450
+ "Cluster Name",
451
+ "vCenter",
452
+ "Datacenter",
453
+ "External ID",
454
+ "Metrics Updated",
455
+ ]);
456
+ });
457
+ });
458
+
459
+ describe("VMware list pages: metric column headers", () => {
460
+ const now: Date = new Date();
461
+
462
+ async function renderList(
463
+ Page: React.FunctionComponent<PageComponentProps>,
464
+ segment: string,
465
+ waitLabel: string,
466
+ ): Promise<void> {
467
+ openAt(`${BASE}/${segment}`);
468
+ render(<Page {...PAGE_PROPS} />);
469
+ await waitForInfo(waitLabel);
470
+ }
471
+
472
+ test("Hosts: CPU Capacity, CPU and Memory", async () => {
473
+ mockRowsByKind["Host"] = [
474
+ inventoryRow({
475
+ kind: "Host",
476
+ externalId: "host/dc1/esx01",
477
+ name: "esx01",
478
+ datacenterName: "dc1",
479
+ clusterName: "cl1",
480
+ latestCpuPercent: 20,
481
+ cpuCapacityMhz: 48000,
482
+ latestMemoryBytes: 64 * GIB,
483
+ maxMemoryBytes: 256 * GIB,
484
+ metricsUpdatedAt: now,
485
+ }),
486
+ ];
487
+
488
+ await renderList(HostsPage, "hosts", "CPU Capacity");
489
+
490
+ expect(await explainedLabels()).toEqual([
491
+ ["CPU Capacity", D.hostListCpuCapacity],
492
+ ["CPU", D.hostListCpu],
493
+ ["Memory", D.hostListMemory],
494
+ ]);
495
+ expectNoInfoOn(["Name", "Datacenter", "Cluster"]);
496
+ });
497
+
498
+ test("Virtual Machines: status, power state, CPU Ready, CPU, memory and age", async () => {
499
+ mockRowsByKind["VirtualMachine"] = [
500
+ inventoryRow({
501
+ kind: "VirtualMachine",
502
+ externalId: "vm/uuid-1",
503
+ name: "web-01",
504
+ hostName: "esx01",
505
+ isTemplate: false,
506
+ isPoweredOn: true,
507
+ latestCpuPercent: 30,
508
+ cpuReadinessPercent: 2,
509
+ latestMemoryBytes: 2 * GIB,
510
+ maxMemoryBytes: 8 * GIB,
511
+ metricsUpdatedAt: now,
512
+ }),
513
+ ];
514
+
515
+ await renderList(VirtualMachinesPage, "virtual-machines", "Power State");
516
+
517
+ expect(await explainedLabels()).toEqual([
518
+ ["Status", D.vmListStatus],
519
+ ["Power State", D.vmListPowerState],
520
+ ["CPU Ready", D.vmListCpuReady],
521
+ ["CPU", D.vmListCpu],
522
+ ["Memory", D.vmListMemory],
523
+ ["Age", D.vmListAge],
524
+ ]);
525
+ expectNoInfoOn(["Name", "Host", "Cluster", "Resource Pool"]);
526
+ });
527
+
528
+ test("Datastores: Used / Capacity", async () => {
529
+ mockRowsByKind["Datastore"] = [
530
+ inventoryRow({
531
+ kind: "Datastore",
532
+ externalId: "datastore/dc1/ds1",
533
+ name: "ds1",
534
+ datacenterName: "dc1",
535
+ latestDiskBytes: 40 * GIB,
536
+ maxDiskBytes: 100 * GIB,
537
+ }),
538
+ ];
539
+
540
+ await renderList(DatastoresPage, "datastores", "Used / Capacity");
541
+
542
+ expect(await explainedLabels()).toEqual([
543
+ ["Used / Capacity", D.datastoreListUsedCapacity],
544
+ ]);
545
+ });
546
+
547
+ test("Clusters: every count and capacity column", async () => {
548
+ mockRowsByKind["Cluster"] = [
549
+ inventoryRow({
550
+ kind: "Cluster",
551
+ externalId: "cluster/dc1/cl1",
552
+ name: "cl1",
553
+ datacenterName: "dc1",
554
+ hostCount: 4,
555
+ effectiveHostCount: 4,
556
+ vmCount: 10,
557
+ poweredOnVmCount: 9,
558
+ vmTemplateCount: 1,
559
+ cpuEffectiveMhz: 90000,
560
+ cpuCapacityMhz: 120000,
561
+ memoryEffectiveBytes: 768 * GIB,
562
+ maxMemoryBytes: 1024 * GIB,
563
+ }),
564
+ ];
565
+
566
+ await renderList(ClustersPage, "clusters", "Effective CPU");
567
+
568
+ expect(await explainedLabels()).toEqual([
569
+ ["Hosts (effective / total)", D.clusterListHosts],
570
+ ["VMs (powered on / total)", D.clusterListVirtualMachines],
571
+ ["Templates", D.clusterListTemplates],
572
+ ["Effective CPU", D.clusterListEffectiveCpu],
573
+ ["Effective Memory", D.clusterListEffectiveMemory],
574
+ ]);
575
+ });
576
+
577
+ test("Resource Pools: CPU, memory and ballooned / swapped", async () => {
578
+ mockRowsByKind["ResourcePool"] = [
579
+ inventoryRow({
580
+ kind: "ResourcePool",
581
+ externalId: "resourcepool/dc1/cl1/Resources",
582
+ name: "Resources",
583
+ clusterName: "cl1",
584
+ resourcePoolPath: "/dc1/host/cl1/Resources",
585
+ latestCpuMhz: 1500,
586
+ latestMemoryBytes: 4 * GIB,
587
+ memoryBalloonedBytes: 0,
588
+ memorySwappedBytes: 0,
589
+ metricsUpdatedAt: now,
590
+ }),
591
+ ];
592
+
593
+ await renderList(ResourcePoolsPage, "resource-pools", "CPU Usage");
594
+
595
+ expect(await explainedLabels()).toEqual([
596
+ ["CPU Usage", D.resourcePoolListCpu],
597
+ ["Memory Usage", D.resourcePoolListMemory],
598
+ ["Ballooned / Swapped", D.resourcePoolListBalloonedSwapped],
599
+ ]);
600
+ expectNoInfoOn(["Inventory Path", "Cluster / Host"]);
601
+ });
602
+
603
+ test("a header (i) sits beside the sort button, and explaining does not sort", async () => {
604
+ mockRowsByKind["VirtualMachine"] = [
605
+ inventoryRow({
606
+ kind: "VirtualMachine",
607
+ externalId: "vm/uuid-a",
608
+ name: "alpha",
609
+ isPoweredOn: true,
610
+ latestCpuPercent: 90,
611
+ metricsUpdatedAt: now,
612
+ }),
613
+ inventoryRow({
614
+ kind: "VirtualMachine",
615
+ externalId: "vm/uuid-b",
616
+ name: "bravo",
617
+ isPoweredOn: true,
618
+ latestCpuPercent: 10,
619
+ metricsUpdatedAt: now,
620
+ }),
621
+ ];
622
+
623
+ await renderList(VirtualMachinesPage, "virtual-machines", "CPU");
624
+
625
+ for (const button of infoButtons()) {
626
+ expectNotNested(button);
627
+ }
628
+
629
+ const cpuInfo: HTMLElement = infoButtonsFor("CPU")[0]!;
630
+ const header: HTMLElement = cpuInfo.closest("th") as HTMLElement;
631
+
632
+ expect(header).not.toBeNull();
633
+ expect(header.getAttribute("aria-sort")).toBe("none");
634
+
635
+ fireEvent.click(cpuInfo);
636
+ // Pressing the (i) did not sort the column.
637
+ expect(header.getAttribute("aria-sort")).toBe("none");
638
+ expect(await explanationOnFocus(cpuInfo)).toBe(D.vmListCpu);
639
+
640
+ // The sort button next to it still sorts.
641
+ fireEvent.click(
642
+ header.querySelector("button:not([aria-label])") as HTMLElement,
643
+ );
644
+ expect(header.getAttribute("aria-sort")).not.toBe("none");
645
+ });
646
+ });