@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,1168 @@
1
+ import "@testing-library/jest-dom";
2
+ import {
3
+ act,
4
+ cleanup,
5
+ fireEvent,
6
+ render,
7
+ screen,
8
+ } from "@testing-library/react";
9
+ import * as React from "react";
10
+ import {
11
+ afterEach,
12
+ beforeEach,
13
+ describe,
14
+ expect,
15
+ jest,
16
+ test,
17
+ } from "@jest/globals";
18
+ import getJestMockFunction, { MockFunction } from "../../MockType";
19
+
20
+ /*
21
+ * The Kubernetes resource pages, rendered for real with their data fetches
22
+ * stubbed: every list table (Nodes, Pods, Deployments, ...) and every
23
+ * resource's detail Overview. Each asserts that the (i) beside a column
24
+ * header or summary field opens the text written for THAT page - "CPU" on
25
+ * the Nodes list must explain a node's own usage, on the Deployments list a
26
+ * sum over pods - and that identity columns and fields (name, namespace,
27
+ * status, age) carry none.
28
+ *
29
+ * App/Tests/Dashboard/KubernetesResourceTooltipsWiring.test.ts pins the
30
+ * same wiring in the source; this proves it reaches the DOM.
31
+ */
32
+
33
+ const mockGetItem: MockFunction = getJestMockFunction();
34
+ const mockGetList: MockFunction = getJestMockFunction();
35
+ const mockFetchInventory: MockFunction = getJestMockFunction();
36
+ const mockFetchLatestObject: MockFunction = getJestMockFunction();
37
+
38
+ /*
39
+ * The arrow wrappers are load bearing: jest.mock is hoisted above the
40
+ * compiled requires, so the mock variables above are still unassigned when
41
+ * the factories run.
42
+ */
43
+ jest.mock("../../../UI/Utils/ModelAPI/ModelAPI", () => {
44
+ return {
45
+ __esModule: true,
46
+ default: {
47
+ getItem: (...args: Array<any>) => {
48
+ return mockGetItem(...args);
49
+ },
50
+ getList: (...args: Array<any>) => {
51
+ return mockGetList(...args);
52
+ },
53
+ },
54
+ };
55
+ });
56
+
57
+ jest.mock("../../../UI/Utils/API/API", () => {
58
+ return {
59
+ __esModule: true,
60
+ default: {
61
+ getFriendlyMessage: (error: unknown) => {
62
+ return (
63
+ ((error as { message?: unknown } | null)?.message as string) ||
64
+ "Could not load"
65
+ );
66
+ },
67
+ },
68
+ };
69
+ });
70
+
71
+ jest.mock("../../../UI/Utils/Navigation", () => {
72
+ return {
73
+ __esModule: true,
74
+ default: {
75
+ getLastParamAsObjectID: () => {
76
+ const { default: ObjectIDType } = jest.requireActual(
77
+ "../../../Types/ObjectID",
78
+ ) as { default: new (id: string) => unknown };
79
+ return new ObjectIDType("84858d6c-1111-4aaa-8bbb-000000000001");
80
+ },
81
+ getLastParamAsString: () => {
82
+ return "web";
83
+ },
84
+ navigate: () => {},
85
+ },
86
+ };
87
+ });
88
+
89
+ jest.mock("../../../UI/Utils/Project", () => {
90
+ return {
91
+ __esModule: true,
92
+ default: {
93
+ getCurrentProjectId: () => {
94
+ const { default: ObjectIDType } = jest.requireActual(
95
+ "../../../Types/ObjectID",
96
+ ) as { default: new (id: string) => unknown };
97
+ return new ObjectIDType("10000000-0000-4000-8000-000000000001");
98
+ },
99
+ },
100
+ };
101
+ });
102
+
103
+ jest.mock(
104
+ "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesResourceUtils",
105
+ () => {
106
+ return {
107
+ __esModule: true,
108
+ default: {
109
+ fetchInventoryResources: (...args: Array<any>) => {
110
+ return mockFetchInventory(...args);
111
+ },
112
+ fetchPodMetricsByOwner: () => {
113
+ return Promise.resolve(new Map());
114
+ },
115
+ fetchPodMetricsByNamespace: () => {
116
+ return Promise.resolve(new Map());
117
+ },
118
+ fetchNodeAllocatableMemory: () => {
119
+ return Promise.resolve(new Map());
120
+ },
121
+ applyAggregateMetrics: () => {},
122
+ parseK8sMemoryToBytes: () => {
123
+ return 0;
124
+ },
125
+ formatAge: () => {
126
+ return "3d";
127
+ },
128
+ formatBytesForChart: (value: number) => {
129
+ return `${value} B`;
130
+ },
131
+ },
132
+ };
133
+ },
134
+ );
135
+
136
+ jest.mock(
137
+ "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesObjectFetcher",
138
+ () => {
139
+ return {
140
+ __esModule: true,
141
+ fetchLatestK8sObject: (...args: Array<any>) => {
142
+ return mockFetchLatestObject(...args);
143
+ },
144
+ };
145
+ },
146
+ );
147
+
148
+ jest.mock(
149
+ "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/useNodeAllocatableCpu",
150
+ () => {
151
+ return {
152
+ __esModule: true,
153
+ default: () => {
154
+ return null;
155
+ },
156
+ };
157
+ },
158
+ );
159
+
160
+ /*
161
+ * The other tabs are not what these tests are about; stubbing them keeps the
162
+ * metric explorer, the events feed and the YAML viewer out of the render.
163
+ * The Metrics tab stub still draws renderExtraCharts, which is where the
164
+ * node's Network Throughput header lives.
165
+ */
166
+ jest.mock(
167
+ "../../../../App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesMetricsTab",
168
+ () => {
169
+ return {
170
+ __esModule: true,
171
+ default: (props: {
172
+ renderExtraCharts?:
173
+ | ((range: { startValue: Date; endValue: Date }) => React.ReactNode)
174
+ | undefined;
175
+ }) => {
176
+ return (
177
+ <div data-testid="metrics-tab">
178
+ {props.renderExtraCharts
179
+ ? props.renderExtraCharts({
180
+ startValue: new Date("2026-09-24T10:00:00Z"),
181
+ endValue: new Date("2026-09-24T11:00:00Z"),
182
+ })
183
+ : null}
184
+ </div>
185
+ );
186
+ },
187
+ };
188
+ },
189
+ );
190
+
191
+ jest.mock(
192
+ "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/KubernetesNetworkThroughputChart",
193
+ () => {
194
+ return {
195
+ __esModule: true,
196
+ default: () => {
197
+ return <div data-testid="network-throughput-chart" />;
198
+ },
199
+ };
200
+ },
201
+ );
202
+
203
+ jest.mock(
204
+ "../../../../App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesEventsTab",
205
+ () => {
206
+ return {
207
+ __esModule: true,
208
+ default: () => {
209
+ return <div data-testid="stub-KubernetesEventsTab" />;
210
+ },
211
+ };
212
+ },
213
+ );
214
+
215
+ jest.mock(
216
+ "../../../../App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesYamlTab",
217
+ () => {
218
+ return {
219
+ __esModule: true,
220
+ default: () => {
221
+ return <div data-testid="stub-KubernetesYamlTab" />;
222
+ },
223
+ };
224
+ },
225
+ );
226
+
227
+ jest.mock(
228
+ "../../../../App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesLogsTab",
229
+ () => {
230
+ return {
231
+ __esModule: true,
232
+ default: () => {
233
+ return <div data-testid="stub-KubernetesLogsTab" />;
234
+ },
235
+ };
236
+ },
237
+ );
238
+
239
+ jest.mock(
240
+ "../../../../App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesEnvVarsTab",
241
+ () => {
242
+ return {
243
+ __esModule: true,
244
+ default: () => {
245
+ return <div data-testid="stub-KubernetesEnvVarsTab" />;
246
+ },
247
+ };
248
+ },
249
+ );
250
+
251
+ jest.mock(
252
+ "../../../../App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesVolumeMountsTab",
253
+ () => {
254
+ return {
255
+ __esModule: true,
256
+ default: () => {
257
+ return <div data-testid="stub-KubernetesVolumeMountsTab" />;
258
+ },
259
+ };
260
+ },
261
+ );
262
+
263
+ jest.mock(
264
+ "../../../../App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesImageReferenceView",
265
+ () => {
266
+ return {
267
+ __esModule: true,
268
+ default: () => {
269
+ return <div data-testid="stub-KubernetesImageReferenceView" />;
270
+ },
271
+ };
272
+ },
273
+ );
274
+
275
+ jest.mock(
276
+ "../../../../App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesResourceLink",
277
+ () => {
278
+ return {
279
+ __esModule: true,
280
+ default: (props: { resourceName: string }) => {
281
+ return <span>{props.resourceName}</span>;
282
+ },
283
+ };
284
+ },
285
+ );
286
+
287
+ import NodesPage from "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Nodes";
288
+ import PodsPage from "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Pods";
289
+ import ContainersPage from "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Containers";
290
+ import DeploymentsPage from "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Deployments";
291
+ import StatefulSetsPage from "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/StatefulSets";
292
+ import DaemonSetsPage from "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DaemonSets";
293
+ import JobsPage from "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Jobs";
294
+ import CronJobsPage from "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/CronJobs";
295
+ import NamespacesPage from "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Namespaces";
296
+ import HPAsPage from "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/HPAs";
297
+ import VPAsPage from "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/VPAs";
298
+ import PersistentVolumeClaimsPage from "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/PersistentVolumeClaims";
299
+ import PersistentVolumesPage from "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/PersistentVolumes";
300
+ import NodeDetailPage from "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NodeDetail";
301
+ import PodDetailPage from "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/PodDetail";
302
+ import DeploymentDetailPage from "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DeploymentDetail";
303
+ import StatefulSetDetailPage from "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/StatefulSetDetail";
304
+ import DaemonSetDetailPage from "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DaemonSetDetail";
305
+ import JobDetailPage from "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/JobDetail";
306
+ import CronJobDetailPage from "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/CronJobDetail";
307
+ import PVCDetailPage from "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/PVCDetail";
308
+ import PVDetailPage from "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/PVDetail";
309
+ import HPADetailPage from "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/HPADetail";
310
+ import VPADetailPage from "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/VPADetail";
311
+ import NamespaceDetailPage from "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NamespaceDetail";
312
+ import ContainerDetailPage from "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/ContainerDetail";
313
+ import { KUBERNETES_RESOURCE_METRIC_DESCRIPTIONS } from "../../../../App/FeatureSet/Dashboard/src/Components/MetricDescriptions/KubernetesResourceMetricDescriptions";
314
+
315
+ const PAGES: Record<string, React.FunctionComponent<any>> = {
316
+ Nodes: NodesPage,
317
+ Pods: PodsPage,
318
+ Containers: ContainersPage,
319
+ Deployments: DeploymentsPage,
320
+ StatefulSets: StatefulSetsPage,
321
+ DaemonSets: DaemonSetsPage,
322
+ Jobs: JobsPage,
323
+ CronJobs: CronJobsPage,
324
+ Namespaces: NamespacesPage,
325
+ HPAs: HPAsPage,
326
+ VPAs: VPAsPage,
327
+ PersistentVolumeClaims: PersistentVolumeClaimsPage,
328
+ PersistentVolumes: PersistentVolumesPage,
329
+ NodeDetail: NodeDetailPage,
330
+ PodDetail: PodDetailPage,
331
+ DeploymentDetail: DeploymentDetailPage,
332
+ StatefulSetDetail: StatefulSetDetailPage,
333
+ DaemonSetDetail: DaemonSetDetailPage,
334
+ JobDetail: JobDetailPage,
335
+ CronJobDetail: CronJobDetailPage,
336
+ PVCDetail: PVCDetailPage,
337
+ PVDetail: PVDetailPage,
338
+ HPADetail: HPADetailPage,
339
+ VPADetail: VPADetailPage,
340
+ NamespaceDetail: NamespaceDetailPage,
341
+ ContainerDetail: ContainerDetailPage,
342
+ };
343
+
344
+ const D: Record<string, string> = KUBERNETES_RESOURCE_METRIC_DESCRIPTIONS;
345
+
346
+ const PAGE_PROPS: Record<string, unknown> = {
347
+ pageRoute: undefined,
348
+ currentProject: null,
349
+ hasPaymentMethod: false,
350
+ };
351
+
352
+ function inventoryRow(): Record<string, unknown> {
353
+ return {
354
+ name: "web",
355
+ namespace: "default",
356
+ cpuUtilization: 12.5,
357
+ memoryUsageBytes: 64 * 1024 * 1024,
358
+ memoryLimitBytes: 256 * 1024 * 1024,
359
+ status: "Running",
360
+ age: "3d",
361
+ additionalAttributes: {
362
+ containers: "2",
363
+ ready: "2/3",
364
+ "resource.k8s.node.name": "node-a",
365
+ target: "Deployment/web",
366
+ minReplicas: "1",
367
+ maxReplicas: "5",
368
+ currentReplicas: "2",
369
+ desiredReplicas: "3",
370
+ updateMode: "Auto",
371
+ capacity: "10Gi",
372
+ storageClass: "standard",
373
+ volumeName: "pv-1",
374
+ accessModes: "ReadWriteOnce",
375
+ reclaimPolicy: "Delete",
376
+ claimRef: "default/data",
377
+ schedule: "*/5 * * * *",
378
+ },
379
+ };
380
+ }
381
+
382
+ function metadata(): Record<string, unknown> {
383
+ return {
384
+ name: "web",
385
+ namespace: "default",
386
+ uid: "uid-1",
387
+ creationTimestamp: "2026-09-20T10:00:00Z",
388
+ labels: {},
389
+ annotations: {},
390
+ ownerReferences: [],
391
+ };
392
+ }
393
+
394
+ const OBJECTS: Record<string, Record<string, unknown>> = {
395
+ nodes: {
396
+ metadata: metadata(),
397
+ status: {
398
+ conditions: [
399
+ {
400
+ type: "Ready",
401
+ status: "True",
402
+ reason: "",
403
+ message: "",
404
+ lastTransitionTime: "",
405
+ },
406
+ ],
407
+ addresses: [{ type: "InternalIP", address: "10.0.0.4" }],
408
+ capacity: { cpu: "4", memory: "16336412Ki", pods: "110" },
409
+ allocatable: { cpu: "3920m", memory: "15672348Ki" },
410
+ nodeInfo: {
411
+ osImage: "Ubuntu",
412
+ containerRuntimeVersion: "containerd://1.7",
413
+ kubeletVersion: "v1.30.0",
414
+ operatingSystem: "linux",
415
+ architecture: "amd64",
416
+ kernelVersion: "6.8.0",
417
+ },
418
+ },
419
+ },
420
+ pods: {
421
+ metadata: metadata(),
422
+ spec: {
423
+ containers: [
424
+ {
425
+ name: "app",
426
+ image: "nginx:1.27",
427
+ command: [],
428
+ args: [],
429
+ env: [],
430
+ ports: [],
431
+ resources: {
432
+ requests: { cpu: "250m", memory: "256Mi" },
433
+ limits: { memory: "512Mi" },
434
+ },
435
+ volumeMounts: [],
436
+ },
437
+ ],
438
+ initContainers: [],
439
+ serviceAccountName: "default",
440
+ nodeName: "node-a",
441
+ nodeSelector: {},
442
+ tolerations: [],
443
+ volumes: [],
444
+ },
445
+ status: {
446
+ phase: "Running",
447
+ podIP: "10.1.0.5",
448
+ hostIP: "10.0.0.4",
449
+ qosClass: "Burstable",
450
+ conditions: [],
451
+ containerStatuses: [
452
+ {
453
+ name: "app",
454
+ ready: true,
455
+ restartCount: 2,
456
+ state: "running",
457
+ reason: "",
458
+ image: "nginx:1.27",
459
+ },
460
+ ],
461
+ initContainerStatuses: [],
462
+ },
463
+ },
464
+ deployments: {
465
+ metadata: metadata(),
466
+ spec: { replicas: 3, strategy: "RollingUpdate", selector: {} },
467
+ status: {
468
+ replicas: 3,
469
+ readyReplicas: 2,
470
+ availableReplicas: 2,
471
+ unavailableReplicas: 1,
472
+ conditions: [],
473
+ },
474
+ },
475
+ statefulsets: {
476
+ metadata: metadata(),
477
+ spec: {
478
+ replicas: 3,
479
+ serviceName: "web",
480
+ podManagementPolicy: "OrderedReady",
481
+ updateStrategy: "RollingUpdate",
482
+ },
483
+ status: { replicas: 3, readyReplicas: 3, currentReplicas: 3 },
484
+ },
485
+ daemonsets: {
486
+ metadata: metadata(),
487
+ spec: { updateStrategy: "RollingUpdate" },
488
+ status: {
489
+ desiredNumberScheduled: 3,
490
+ currentNumberScheduled: 3,
491
+ numberReady: 2,
492
+ numberMisscheduled: 0,
493
+ numberAvailable: 2,
494
+ },
495
+ },
496
+ jobs: {
497
+ metadata: metadata(),
498
+ spec: { completions: 1, parallelism: 1, backoffLimit: 6 },
499
+ status: {
500
+ active: 1,
501
+ succeeded: 0,
502
+ failed: 1,
503
+ startTime: "2026-09-24T10:00:00Z",
504
+ completionTime: "",
505
+ conditions: [],
506
+ },
507
+ },
508
+ cronjobs: {
509
+ metadata: metadata(),
510
+ spec: {
511
+ schedule: "*/5 * * * *",
512
+ suspend: false,
513
+ concurrencyPolicy: "Forbid",
514
+ successfulJobsHistoryLimit: 3,
515
+ failedJobsHistoryLimit: 1,
516
+ },
517
+ status: { lastScheduleTime: "2026-09-24T10:55:00Z", activeCount: 0 },
518
+ },
519
+ persistentvolumeclaims: {
520
+ metadata: metadata(),
521
+ spec: {
522
+ accessModes: ["ReadWriteOnce"],
523
+ storageClassName: "standard",
524
+ volumeName: "pv-1",
525
+ resources: { requests: { storage: "10Gi" } },
526
+ },
527
+ status: { phase: "Bound", capacity: { storage: "10Gi" } },
528
+ },
529
+ persistentvolumes: {
530
+ metadata: metadata(),
531
+ spec: {
532
+ capacity: { storage: "10Gi" },
533
+ accessModes: ["ReadWriteOnce"],
534
+ storageClassName: "standard",
535
+ persistentVolumeReclaimPolicy: "Delete",
536
+ claimRef: { name: "data", namespace: "default" },
537
+ },
538
+ status: { phase: "Bound" },
539
+ },
540
+ horizontalpodautoscalers: {
541
+ metadata: metadata(),
542
+ spec: {
543
+ minReplicas: 1,
544
+ maxReplicas: 5,
545
+ scaleTargetRef: { kind: "Deployment", name: "web" },
546
+ metrics: [
547
+ {
548
+ type: "Resource",
549
+ resourceName: "cpu",
550
+ targetType: "Utilization",
551
+ targetValue: "70",
552
+ },
553
+ ],
554
+ },
555
+ status: { currentReplicas: 2, desiredReplicas: 3, conditions: [] },
556
+ },
557
+ verticalpodautoscalers: {
558
+ metadata: metadata(),
559
+ spec: {
560
+ targetRef: { kind: "Deployment", name: "web" },
561
+ updatePolicy: { updateMode: "Auto" },
562
+ resourcePolicy: "",
563
+ },
564
+ status: {
565
+ recommendation: {
566
+ containerRecommendations: [
567
+ {
568
+ containerName: "app",
569
+ target: { cpu: "250m", memory: "262144k" },
570
+ lowerBound: {},
571
+ upperBound: {},
572
+ },
573
+ ],
574
+ },
575
+ },
576
+ },
577
+ };
578
+
579
+ async function settle(): Promise<void> {
580
+ for (let i: number = 0; i < 5; i++) {
581
+ await act(async () => {
582
+ await Promise.resolve();
583
+ jest.advanceTimersByTime(10);
584
+ });
585
+ }
586
+ }
587
+
588
+ async function tooltipFor(label: string): Promise<string> {
589
+ const button: HTMLElement = screen.getByRole("button", {
590
+ name: `About ${label}`,
591
+ });
592
+
593
+ fireEvent.mouseEnter(button);
594
+ await act(async () => {
595
+ jest.advanceTimersByTime(200);
596
+ });
597
+
598
+ const tooltips: Array<HTMLElement> = screen.getAllByRole("tooltip");
599
+ const text: string = tooltips[tooltips.length - 1]?.textContent || "";
600
+
601
+ fireEvent.mouseLeave(button);
602
+ await act(async () => {
603
+ jest.advanceTimersByTime(500);
604
+ });
605
+
606
+ return text;
607
+ }
608
+
609
+ function infoLabels(): Array<string> {
610
+ return screen
611
+ .queryAllByRole("button", { name: /^About / })
612
+ .map((button: HTMLElement): string => {
613
+ return (button.getAttribute("aria-label") || "").replace("About ", "");
614
+ })
615
+ .sort();
616
+ }
617
+
618
+ beforeEach(() => {
619
+ jest.useFakeTimers();
620
+ mockGetItem.mockReset();
621
+ mockGetList.mockReset();
622
+ mockFetchInventory.mockReset();
623
+ mockFetchLatestObject.mockReset();
624
+
625
+ mockGetItem.mockImplementation(() => {
626
+ return Promise.resolve({ clusterIdentifier: "prod-cluster", name: "Prod" });
627
+ });
628
+ mockFetchInventory.mockImplementation(() => {
629
+ return Promise.resolve([inventoryRow()]);
630
+ });
631
+ mockGetList.mockImplementation(() => {
632
+ return Promise.resolve({
633
+ data: [
634
+ {
635
+ name: "app",
636
+ podName: "web-1",
637
+ podNamespaceKey: "default",
638
+ state: "running",
639
+ reason: "",
640
+ isReady: true,
641
+ restartCount: 4,
642
+ memoryLimitBytes: 512 * 1024 * 1024,
643
+ latestCpuPercent: 3.2,
644
+ latestMemoryBytes: 128 * 1024 * 1024,
645
+ metricsUpdatedAt: new Date(),
646
+ lastSeenAt: new Date(),
647
+ },
648
+ ],
649
+ count: 1,
650
+ });
651
+ });
652
+ mockFetchLatestObject.mockImplementation(
653
+ (options: { resourceType: string }) => {
654
+ return Promise.resolve(OBJECTS[options.resourceType] || null);
655
+ },
656
+ );
657
+ });
658
+
659
+ afterEach(() => {
660
+ cleanup();
661
+ jest.useRealTimers();
662
+ });
663
+
664
+ /*
665
+ * ---------------------------------------------------------------------------
666
+ * List pages
667
+ * ---------------------------------------------------------------------------
668
+ */
669
+
670
+ interface ListCase {
671
+ page: string;
672
+ // Header label -> description key.
673
+ explained: Array<[string, string]>;
674
+ // Headers the table renders that must stay without an (i).
675
+ plain: Array<string>;
676
+ }
677
+
678
+ const LIST_CASES: Array<ListCase> = [
679
+ {
680
+ page: "Nodes",
681
+ explained: [
682
+ ["CPU", "nodeCpu"],
683
+ ["Memory", "nodeMemory"],
684
+ ],
685
+ plain: ["Name", "Status", "Age"],
686
+ },
687
+ {
688
+ page: "Pods",
689
+ explained: [
690
+ ["CPU", "podCpu"],
691
+ ["Memory", "podMemory"],
692
+ ["Containers", "podContainers"],
693
+ ],
694
+ plain: ["Name", "Namespace", "Status", "Node", "Age"],
695
+ },
696
+ {
697
+ page: "Containers",
698
+ explained: [
699
+ ["CPU", "containerCpu"],
700
+ ["Memory", "containerMemory"],
701
+ ["Restarts", "containerRestarts"],
702
+ ],
703
+ plain: ["Name", "Namespace", "Status", "Pod", "Age"],
704
+ },
705
+ {
706
+ page: "Deployments",
707
+ explained: [
708
+ ["CPU", "deploymentCpu"],
709
+ ["Memory", "deploymentMemory"],
710
+ ["Ready", "deploymentReady"],
711
+ ],
712
+ plain: ["Name", "Namespace", "Status", "Age"],
713
+ },
714
+ {
715
+ page: "StatefulSets",
716
+ explained: [
717
+ ["CPU", "statefulSetCpu"],
718
+ ["Memory", "statefulSetMemory"],
719
+ ["Ready", "statefulSetReady"],
720
+ ],
721
+ plain: ["Name", "Namespace", "Status", "Age"],
722
+ },
723
+ {
724
+ page: "DaemonSets",
725
+ explained: [
726
+ ["CPU", "daemonSetCpu"],
727
+ ["Memory", "daemonSetMemory"],
728
+ ["Ready", "daemonSetReady"],
729
+ ],
730
+ plain: ["Name", "Namespace", "Status", "Age"],
731
+ },
732
+ {
733
+ page: "Jobs",
734
+ explained: [
735
+ ["CPU", "jobCpu"],
736
+ ["Memory", "jobMemory"],
737
+ ],
738
+ plain: ["Name", "Namespace", "Status", "Age"],
739
+ },
740
+ {
741
+ page: "CronJobs",
742
+ explained: [
743
+ ["CPU", "cronJobCpu"],
744
+ ["Memory", "cronJobMemory"],
745
+ ],
746
+ plain: ["Name", "Namespace", "Status", "Schedule", "Age"],
747
+ },
748
+ {
749
+ page: "Namespaces",
750
+ explained: [
751
+ ["CPU", "namespaceCpu"],
752
+ ["Memory", "namespaceMemory"],
753
+ ],
754
+ plain: ["Name", "Status", "Age"],
755
+ },
756
+ {
757
+ page: "HPAs",
758
+ explained: [
759
+ ["Min Replicas", "hpaMinReplicas"],
760
+ ["Max Replicas", "hpaMaxReplicas"],
761
+ ["Current", "hpaCurrentReplicas"],
762
+ ["Desired", "hpaDesiredReplicas"],
763
+ ],
764
+ plain: ["Name", "Namespace", "Status", "Target", "Age"],
765
+ },
766
+ {
767
+ page: "VPAs",
768
+ explained: [],
769
+ plain: ["Name", "Namespace", "Status", "Target", "Update Mode", "Age"],
770
+ },
771
+ {
772
+ page: "PersistentVolumeClaims",
773
+ explained: [["Capacity", "pvcCapacity"]],
774
+ plain: [
775
+ "Name",
776
+ "Namespace",
777
+ "Status",
778
+ "Storage Class",
779
+ "Volume",
780
+ "Access Modes",
781
+ "Age",
782
+ ],
783
+ },
784
+ {
785
+ page: "PersistentVolumes",
786
+ explained: [["Capacity", "pvCapacity"]],
787
+ plain: [
788
+ "Name",
789
+ "Status",
790
+ "Storage Class",
791
+ "Reclaim Policy",
792
+ "Claim",
793
+ "Age",
794
+ ],
795
+ },
796
+ ];
797
+
798
+ async function renderPage(page: string): Promise<void> {
799
+ const Page: React.FunctionComponent<any> | undefined = PAGES[page];
800
+
801
+ if (!Page) {
802
+ throw new Error(`No page named ${page}`);
803
+ }
804
+
805
+ render(<Page {...PAGE_PROPS} />);
806
+ await settle();
807
+ }
808
+
809
+ describe("Kubernetes list pages: each metric header opens the text for that list", () => {
810
+ test.each(LIST_CASES)(
811
+ "$page shows an (i) on exactly its metric headers",
812
+ async (listCase: ListCase) => {
813
+ await renderPage(listCase.page);
814
+
815
+ // The table rendered its headers (and the stubbed row).
816
+ expect(screen.getAllByRole("columnheader").length).toBeGreaterThanOrEqual(
817
+ listCase.plain.length,
818
+ );
819
+
820
+ expect(infoLabels()).toEqual(
821
+ listCase.explained
822
+ .map((entry: [string, string]): string => {
823
+ return entry[0];
824
+ })
825
+ .sort(),
826
+ );
827
+ },
828
+ );
829
+
830
+ const hoverCases: Array<[string, string, string]> = [];
831
+
832
+ for (const listCase of LIST_CASES) {
833
+ for (const [label, key] of listCase.explained) {
834
+ hoverCases.push([listCase.page, label, key]);
835
+ }
836
+ }
837
+
838
+ test.each(hoverCases)(
839
+ "%s: hovering the %s header shows %s",
840
+ async (page: string, label: string, key: string) => {
841
+ await renderPage(page);
842
+
843
+ expect(D[key]).toBeTruthy();
844
+ expect(await tooltipFor(label)).toBe(D[key]);
845
+ },
846
+ );
847
+
848
+ test.each(LIST_CASES)(
849
+ "$page leaves its identity and status headers plain",
850
+ async (listCase: ListCase) => {
851
+ await renderPage(listCase.page);
852
+
853
+ for (const label of listCase.plain) {
854
+ expect(
855
+ screen.queryByRole("button", { name: `About ${label}` }),
856
+ ).not.toBeInTheDocument();
857
+ }
858
+ },
859
+ );
860
+
861
+ test("the header (i) is beside the sort button, never inside it", async () => {
862
+ await renderPage("Nodes");
863
+
864
+ for (const label of ["CPU", "Memory"]) {
865
+ const info: HTMLElement = screen.getByRole("button", {
866
+ name: `About ${label}`,
867
+ });
868
+
869
+ expect(info.parentElement?.closest("button, a")).toBeNull();
870
+ expect(info.closest("th")).not.toBeNull();
871
+ }
872
+ });
873
+
874
+ test("the same title opens a different explanation on different lists", async () => {
875
+ await renderPage("Nodes");
876
+ const nodeCpu: string = await tooltipFor("CPU");
877
+ cleanup();
878
+
879
+ await renderPage("Deployments");
880
+ const deploymentCpu: string = await tooltipFor("CPU");
881
+
882
+ expect(nodeCpu).not.toBe(deploymentCpu);
883
+ expect(nodeCpu).toContain("latest sample, not an average");
884
+ expect(deploymentCpu).toContain("can pass 100%");
885
+ });
886
+ });
887
+
888
+ /*
889
+ * ---------------------------------------------------------------------------
890
+ * Detail pages
891
+ * ---------------------------------------------------------------------------
892
+ */
893
+
894
+ interface DetailCase {
895
+ page: string;
896
+ explained: Array<[string, string]>;
897
+ plain: Array<string>;
898
+ }
899
+
900
+ const DETAIL_CASES: Array<DetailCase> = [
901
+ {
902
+ page: "NodeDetail",
903
+ explained: [
904
+ ["CPU (Capacity / Allocatable)", "nodeCpuCapacity"],
905
+ ["Memory (Capacity / Allocatable)", "nodeMemoryCapacity"],
906
+ ["Pods (Capacity)", "nodePodCapacity"],
907
+ ],
908
+ plain: ["Node Name", "Cluster", "Status", "Internal IP", "Created"],
909
+ },
910
+ {
911
+ page: "PodDetail",
912
+ explained: [["Restarts", "podRestarts"]],
913
+ plain: ["Pod Name", "Cluster", "Namespace", "Status", "Node", "Created"],
914
+ },
915
+ {
916
+ page: "DeploymentDetail",
917
+ explained: [
918
+ ["Rollout Status", "deploymentRolloutStatus"],
919
+ ["Desired Replicas", "deploymentDesiredReplicas"],
920
+ ["Ready Replicas", "deploymentReadyReplicas"],
921
+ ["Available", "deploymentAvailableReplicas"],
922
+ ["Unavailable", "deploymentUnavailableReplicas"],
923
+ ],
924
+ plain: ["Name", "Cluster", "Namespace", "Strategy", "Created"],
925
+ },
926
+ {
927
+ page: "StatefulSetDetail",
928
+ explained: [
929
+ ["Replicas", "statefulSetReplicas"],
930
+ ["Ready Replicas", "statefulSetReadyReplicas"],
931
+ ],
932
+ plain: ["Name", "Cluster", "Namespace", "Service Name", "Created"],
933
+ },
934
+ {
935
+ page: "DaemonSetDetail",
936
+ explained: [
937
+ ["Desired Scheduled", "daemonSetDesiredScheduled"],
938
+ ["Current Scheduled", "daemonSetCurrentScheduled"],
939
+ ["Number Ready", "daemonSetNumberReady"],
940
+ ["Number Available", "daemonSetNumberAvailable"],
941
+ ],
942
+ plain: ["Name", "Cluster", "Namespace", "Update Strategy", "Created"],
943
+ },
944
+ {
945
+ page: "JobDetail",
946
+ explained: [
947
+ ["Completions", "jobCompletions"],
948
+ ["Parallelism", "jobParallelism"],
949
+ ["Backoff Limit", "jobBackoffLimit"],
950
+ ["Active", "jobActive"],
951
+ ["Succeeded", "jobSucceeded"],
952
+ ["Failed", "jobFailed"],
953
+ ],
954
+ plain: ["Job Name", "Cluster", "Namespace", "Start Time", "Created"],
955
+ },
956
+ {
957
+ page: "CronJobDetail",
958
+ explained: [
959
+ ["Successful Jobs History Limit", "cronJobSuccessfulJobsHistoryLimit"],
960
+ ["Failed Jobs History Limit", "cronJobFailedJobsHistoryLimit"],
961
+ ["Active Jobs", "cronJobActiveJobs"],
962
+ ],
963
+ plain: [
964
+ "CronJob Name",
965
+ "Cluster",
966
+ "Namespace",
967
+ "Schedule",
968
+ "Concurrency Policy",
969
+ "Last Schedule Time",
970
+ "Created",
971
+ ],
972
+ },
973
+ {
974
+ page: "PVCDetail",
975
+ explained: [
976
+ ["Capacity", "pvcCapacity"],
977
+ ["Requested Storage", "pvcRequestedStorage"],
978
+ ],
979
+ plain: ["PVC Name", "Cluster", "Namespace", "Status", "Created"],
980
+ },
981
+ {
982
+ page: "PVDetail",
983
+ explained: [["Capacity", "pvCapacity"]],
984
+ plain: ["PV Name", "Cluster", "Status", "Reclaim Policy", "Created"],
985
+ },
986
+ {
987
+ page: "HPADetail",
988
+ explained: [
989
+ ["Min Replicas", "hpaMinReplicas"],
990
+ ["Max Replicas", "hpaMaxReplicas"],
991
+ ["Current Replicas", "hpaCurrentReplicas"],
992
+ ["Desired Replicas", "hpaDesiredReplicas"],
993
+ ["Metrics", "hpaMetrics"],
994
+ ],
995
+ plain: ["Name", "Cluster", "Namespace", "Scaling Status", "Created"],
996
+ },
997
+ {
998
+ page: "VPADetail",
999
+ explained: [["Recommendation (app)", "vpaRecommendation"]],
1000
+ plain: ["Name", "Cluster", "Namespace", "Update Mode", "Status"],
1001
+ },
1002
+ ];
1003
+
1004
+ describe("Kubernetes detail pages: each metric-like summary field is explained", () => {
1005
+ test.each(DETAIL_CASES)(
1006
+ "$page shows an (i) on exactly its metric fields",
1007
+ async (detailCase: DetailCase) => {
1008
+ await renderPage(detailCase.page);
1009
+
1010
+ expect(infoLabels()).toEqual(
1011
+ detailCase.explained
1012
+ .map((entry: [string, string]): string => {
1013
+ return entry[0];
1014
+ })
1015
+ .sort(),
1016
+ );
1017
+ },
1018
+ );
1019
+
1020
+ const hoverCases: Array<[string, string, string]> = [];
1021
+
1022
+ for (const detailCase of DETAIL_CASES) {
1023
+ for (const [label, key] of detailCase.explained) {
1024
+ hoverCases.push([detailCase.page, label, key]);
1025
+ }
1026
+ }
1027
+
1028
+ test.each(hoverCases)(
1029
+ "%s: hovering %s shows %s",
1030
+ async (page: string, label: string, key: string) => {
1031
+ await renderPage(page);
1032
+
1033
+ expect(D[key]).toBeTruthy();
1034
+ expect(await tooltipFor(label)).toBe(D[key]);
1035
+ },
1036
+ );
1037
+
1038
+ test.each(DETAIL_CASES)(
1039
+ "$page leaves its identity and status fields plain",
1040
+ async (detailCase: DetailCase) => {
1041
+ await renderPage(detailCase.page);
1042
+
1043
+ // The field is on the page ...
1044
+ for (const label of detailCase.plain) {
1045
+ expect(screen.getAllByText(label).length).toBeGreaterThan(0);
1046
+ // ... without an (i).
1047
+ expect(
1048
+ screen.queryByRole("button", { name: `About ${label}` }),
1049
+ ).not.toBeInTheDocument();
1050
+ }
1051
+ },
1052
+ );
1053
+
1054
+ test("the Unavailable field, and its (i), only appear when pods are unavailable", async () => {
1055
+ mockFetchLatestObject.mockImplementation(
1056
+ (options: { resourceType: string }) => {
1057
+ const deployment: Record<string, unknown> = OBJECTS["deployments"]!;
1058
+ return Promise.resolve(
1059
+ options.resourceType === "deployments"
1060
+ ? {
1061
+ ...deployment,
1062
+ status: {
1063
+ replicas: 3,
1064
+ readyReplicas: 3,
1065
+ availableReplicas: 3,
1066
+ unavailableReplicas: 0,
1067
+ conditions: [],
1068
+ },
1069
+ }
1070
+ : null,
1071
+ );
1072
+ },
1073
+ );
1074
+
1075
+ await renderPage("DeploymentDetail");
1076
+
1077
+ expect(
1078
+ screen.getByRole("button", { name: "About Ready Replicas" }),
1079
+ ).toBeInTheDocument();
1080
+ expect(
1081
+ screen.queryByRole("button", { name: "About Unavailable" }),
1082
+ ).not.toBeInTheDocument();
1083
+ });
1084
+
1085
+ test("a VPA explains each container's recommendation", async () => {
1086
+ mockFetchLatestObject.mockImplementation(() => {
1087
+ const vpa: Record<string, unknown> = OBJECTS["verticalpodautoscalers"]!;
1088
+ return Promise.resolve({
1089
+ ...vpa,
1090
+ status: {
1091
+ recommendation: {
1092
+ containerRecommendations: [
1093
+ {
1094
+ containerName: "app",
1095
+ target: { cpu: "250m", memory: "256Mi" },
1096
+ lowerBound: {},
1097
+ upperBound: {},
1098
+ },
1099
+ {
1100
+ containerName: "sidecar",
1101
+ target: { cpu: "50m", memory: "64Mi" },
1102
+ lowerBound: {},
1103
+ upperBound: {},
1104
+ },
1105
+ ],
1106
+ },
1107
+ },
1108
+ });
1109
+ });
1110
+
1111
+ await renderPage("VPADetail");
1112
+
1113
+ expect(await tooltipFor("Recommendation (app)")).toBe(
1114
+ D["vpaRecommendation"],
1115
+ );
1116
+ expect(await tooltipFor("Recommendation (sidecar)")).toBe(
1117
+ D["vpaRecommendation"],
1118
+ );
1119
+ });
1120
+
1121
+ test("the node's Network Throughput chart header explains the chart", async () => {
1122
+ await renderPage("NodeDetail");
1123
+
1124
+ fireEvent.click(screen.getByRole("tab", { name: "Metrics" }));
1125
+ await settle();
1126
+
1127
+ expect(screen.getByTestId("network-throughput-chart")).toBeInTheDocument();
1128
+ expect(screen.getByText("Network Throughput")).toBeInTheDocument();
1129
+ expect(await tooltipFor("Network Throughput")).toBe(
1130
+ D["nodeNetworkThroughput"],
1131
+ );
1132
+
1133
+ const info: HTMLElement = screen.getByRole("button", {
1134
+ name: "About Network Throughput",
1135
+ });
1136
+
1137
+ expect(info.parentElement?.closest("button, a")).toBeNull();
1138
+ });
1139
+
1140
+ test("a pod's Containers tab explains its container cards", async () => {
1141
+ await renderPage("PodDetail");
1142
+
1143
+ fireEvent.click(screen.getByRole("tab", { name: "Containers" }));
1144
+ await settle();
1145
+
1146
+ expect(await tooltipFor("State")).toBe(D["containerState"]);
1147
+ expect(await tooltipFor("Ready")).toBe(D["containerReady"]);
1148
+ expect(await tooltipFor("Requests")).toBe(D["containerRequests"]);
1149
+ expect(await tooltipFor("Limits")).toBe(D["containerLimits"]);
1150
+ expect(await tooltipFor("Restarts")).toBe(D["containerRestarts"]);
1151
+ });
1152
+
1153
+ test("NamespaceDetail and ContainerDetail have no metric fields to explain", async () => {
1154
+ mockFetchLatestObject.mockImplementation(() => {
1155
+ return Promise.resolve({
1156
+ metadata: metadata(),
1157
+ status: { phase: "Active" },
1158
+ });
1159
+ });
1160
+
1161
+ await renderPage("NamespaceDetail");
1162
+ expect(infoLabels()).toEqual([]);
1163
+ cleanup();
1164
+
1165
+ await renderPage("ContainerDetail");
1166
+ expect(infoLabels()).toEqual([]);
1167
+ });
1168
+ });