@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,472 @@
1
+ import "@testing-library/jest-dom";
2
+ import {
3
+ afterEach,
4
+ beforeEach,
5
+ describe,
6
+ expect,
7
+ jest,
8
+ test,
9
+ } from "@jest/globals";
10
+ import {
11
+ cleanup,
12
+ fireEvent,
13
+ render,
14
+ screen,
15
+ within,
16
+ } from "@testing-library/react";
17
+ import * as React from "react";
18
+ import getJestMockFunction, { MockFunction } from "../../MockType";
19
+ import {
20
+ explanationOnFocus,
21
+ explanationOnHover,
22
+ expectNotNestedInControl,
23
+ infoButtonsFor,
24
+ infoLabels,
25
+ settle,
26
+ } from "./HostTooltipHarness";
27
+
28
+ /*
29
+ * The Host Processes, Services and Systemd Units lists, rendered for real
30
+ * with their data layer mocked. The metric columns carry an (i) in their
31
+ * header through the shared Table's headerTooltip; on a sortable column the
32
+ * (i) sits beside the sort button, never inside it, and asking what a
33
+ * column means does not re-sort the table.
34
+ */
35
+
36
+ const MODEL_ID: string = "84858d6c-1111-4aaa-8bbb-000000000001";
37
+ const GIB: number = 1024 * 1024 * 1024;
38
+ const SORTED_PATTERN: RegExp = /^(ascending|descending)$/;
39
+
40
+ const getItemMock: MockFunction = getJestMockFunction();
41
+ const getListMock: MockFunction = getJestMockFunction();
42
+
43
+ jest.mock("../../../UI/Utils/ModelAPI/ModelAPI", () => {
44
+ return {
45
+ __esModule: true,
46
+ default: {
47
+ getItem: (...args: Array<unknown>) => {
48
+ return getItemMock(...args);
49
+ },
50
+ },
51
+ };
52
+ });
53
+
54
+ jest.mock("../../../UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI", () => {
55
+ return {
56
+ __esModule: true,
57
+ default: {
58
+ getList: (...args: Array<unknown>) => {
59
+ return getListMock(...args);
60
+ },
61
+ },
62
+ };
63
+ });
64
+
65
+ jest.mock("../../../UI/Utils/API/API", () => {
66
+ return {
67
+ __esModule: true,
68
+ default: {
69
+ getFriendlyMessage: (error: unknown) => {
70
+ return String((error as Error)?.message || error);
71
+ },
72
+ },
73
+ };
74
+ });
75
+
76
+ jest.mock("../../../UI/Utils/Navigation", () => {
77
+ return {
78
+ __esModule: true,
79
+ default: {
80
+ getLastParamAsObjectID: () => {
81
+ const { default: ObjectIDType } = jest.requireActual(
82
+ "../../../Types/ObjectID",
83
+ ) as { default: new (id: string) => unknown };
84
+ return new ObjectIDType("84858d6c-1111-4aaa-8bbb-000000000001");
85
+ },
86
+ navigate: () => {},
87
+ },
88
+ };
89
+ });
90
+
91
+ jest.mock("../../../UI/Utils/Project", () => {
92
+ return {
93
+ __esModule: true,
94
+ default: {
95
+ getCurrentProjectId: () => {
96
+ const { default: ObjectIDType } = jest.requireActual(
97
+ "../../../Types/ObjectID",
98
+ ) as { default: new (id: string) => unknown };
99
+ return new ObjectIDType("10000000-0000-4000-8000-000000000001");
100
+ },
101
+ },
102
+ };
103
+ });
104
+
105
+ import HostProcesses from "../../../../App/FeatureSet/Dashboard/src/Pages/Host/View/Processes";
106
+ import HostServices from "../../../../App/FeatureSet/Dashboard/src/Pages/Host/View/Services";
107
+ import HostSystemdUnits from "../../../../App/FeatureSet/Dashboard/src/Pages/Host/View/SystemdUnits";
108
+ import PageComponentProps from "../../../../App/FeatureSet/Dashboard/src/Pages/PageComponentProps";
109
+ import {
110
+ HOST_METRIC_DESCRIPTIONS,
111
+ HostMetric,
112
+ } from "../../../../App/FeatureSet/Dashboard/src/Components/MetricDescriptions/HostMetricDescriptions";
113
+
114
+ const D: Record<HostMetric, string> = HOST_METRIC_DESCRIPTIONS;
115
+
116
+ // The Host pages read their ids from the route, not from these props.
117
+ const PAGE_PROPS: PageComponentProps = {} as PageComponentProps;
118
+
119
+ interface ListCall {
120
+ query: { name: string };
121
+ }
122
+
123
+ function secondsAgo(seconds: number): Date {
124
+ return new Date(Date.now() - seconds * 1000);
125
+ }
126
+
127
+ function headerCell(label: string): HTMLElement {
128
+ return infoButtonsFor(label)[0]!.closest("th") as HTMLElement;
129
+ }
130
+
131
+ beforeEach(() => {
132
+ jest.useFakeTimers();
133
+ getItemMock.mockReset();
134
+ getListMock.mockReset();
135
+ getItemMock.mockResolvedValue({
136
+ _id: MODEL_ID,
137
+ name: "web-01",
138
+ hostIdentifier: "web-01",
139
+ totalMemoryBytes: 16 * GIB,
140
+ osType: "linux",
141
+ });
142
+ });
143
+
144
+ afterEach(() => {
145
+ cleanup();
146
+ jest.useRealTimers();
147
+ });
148
+
149
+ // -------------------------------------------------------------- processes
150
+
151
+ function processAttributes(pid: string, exe: string): Record<string, string> {
152
+ return {
153
+ "resource.process.pid": pid,
154
+ "resource.process.executable.name": exe,
155
+ "resource.host.name": "web-01",
156
+ };
157
+ }
158
+
159
+ /*
160
+ * Newest first, as the API sorts them. postgres's newest scrape holds its
161
+ * three CPU readings at one timestamp: 30% user, 6% system, 0% wait.
162
+ */
163
+ function processRows(call: ListCall): Array<Record<string, unknown>> {
164
+ const at: Date = secondsAgo(10);
165
+
166
+ if (call.query.name === "process.cpu.utilization") {
167
+ return [
168
+ {
169
+ time: at,
170
+ value: 0.3,
171
+ attributes: { ...processAttributes("4321", "postgres"), state: "user" },
172
+ },
173
+ {
174
+ time: at,
175
+ value: 0.06,
176
+ attributes: {
177
+ ...processAttributes("4321", "postgres"),
178
+ state: "system",
179
+ },
180
+ },
181
+ {
182
+ time: at,
183
+ value: 0,
184
+ attributes: { ...processAttributes("4321", "postgres"), state: "wait" },
185
+ },
186
+ {
187
+ time: at,
188
+ value: 0.05,
189
+ attributes: { ...processAttributes("99", "nginx"), state: "user" },
190
+ },
191
+ ];
192
+ }
193
+
194
+ return [
195
+ {
196
+ time: at,
197
+ value: 2 * GIB,
198
+ attributes: processAttributes("4321", "postgres"),
199
+ },
200
+ {
201
+ time: at,
202
+ value: 100 * 1024 * 1024,
203
+ attributes: processAttributes("99", "nginx"),
204
+ },
205
+ ];
206
+ }
207
+
208
+ describe("Host Processes list", () => {
209
+ beforeEach(() => {
210
+ getListMock.mockImplementation((call: unknown) => {
211
+ return Promise.resolve({ data: processRows(call as ListCall) });
212
+ });
213
+ });
214
+
215
+ test("the CPU and Memory headers carry an (i); Process and User do not", async () => {
216
+ render(<HostProcesses {...PAGE_PROPS} />);
217
+ await settle();
218
+
219
+ expect(infoLabels()).toEqual(["CPU", "Memory"]);
220
+ expect(
221
+ screen.getByRole("columnheader", { name: "Process" }),
222
+ ).toBeInTheDocument();
223
+ });
224
+
225
+ test.each([
226
+ ["CPU", "processListCpu"],
227
+ ["Memory", "processListMemory"],
228
+ ])(
229
+ "the %s header explains it with %s",
230
+ async (label: string, key: string) => {
231
+ render(<HostProcesses {...PAGE_PROPS} />);
232
+ await settle();
233
+
234
+ expect(await explanationOnHover(infoButtonsFor(label)[0]!)).toBe(
235
+ D[key as HostMetric],
236
+ );
237
+ expectNotNestedInControl(infoButtonsFor(label)[0]!);
238
+ expect(headerCell(label)).toHaveTextContent(label);
239
+ },
240
+ );
241
+
242
+ test("the CPU column shows one of the three readings, not their sum, as its text warns", async () => {
243
+ render(<HostProcesses {...PAGE_PROPS} />);
244
+ await settle();
245
+
246
+ const row: HTMLElement = screen
247
+ .getByText("postgres")
248
+ .closest("tr") as HTMLElement;
249
+
250
+ expect(within(row).getByText("30.0%")).toBeInTheDocument();
251
+ expect(within(row).queryByText("36.0%")).not.toBeInTheDocument();
252
+ expect(D.processListCpu).toContain("only one of them");
253
+ });
254
+
255
+ test("the Memory column compares RSS with the host's total RAM", async () => {
256
+ render(<HostProcesses {...PAGE_PROPS} />);
257
+ await settle();
258
+
259
+ const row: HTMLElement = screen
260
+ .getByText("postgres")
261
+ .closest("tr") as HTMLElement;
262
+
263
+ expect(within(row).getByText("2.0 GiB")).toBeInTheDocument();
264
+ expect(within(row).getByText("12.5%")).toBeInTheDocument();
265
+ });
266
+
267
+ test("the list reads the last 15 minutes, whatever the page shows elsewhere", async () => {
268
+ render(<HostProcesses {...PAGE_PROPS} />);
269
+ await settle();
270
+
271
+ for (const call of getListMock.mock.calls) {
272
+ const time: { startValue: Date; endValue: Date } = (
273
+ call[0] as { query: { time: { startValue: Date; endValue: Date } } }
274
+ ).query.time;
275
+
276
+ expect(time.endValue.getTime() - time.startValue.getTime()).toBe(
277
+ 15 * 60_000,
278
+ );
279
+ }
280
+ expect(D.processListCpu).toContain("last 15 minutes");
281
+ expect(D.processListMemory).toContain("last 15 minutes");
282
+ });
283
+ });
284
+
285
+ // --------------------------------------------------------------- services
286
+
287
+ function serviceRow(
288
+ name: string,
289
+ code: number,
290
+ startupMode: string,
291
+ ): Record<string, unknown> {
292
+ return {
293
+ time: secondsAgo(15),
294
+ value: code,
295
+ attributes: {
296
+ name,
297
+ startup_mode: startupMode,
298
+ "resource.host.name": "web-01",
299
+ },
300
+ };
301
+ }
302
+
303
+ describe("Host Services list", () => {
304
+ beforeEach(() => {
305
+ getListMock.mockResolvedValue({
306
+ data: [
307
+ serviceRow("Spooler", 4, "auto_start"),
308
+ serviceRow("wuauserv", 1, "demand_start"),
309
+ serviceRow("BootDriver", 4, "boot_start"),
310
+ ],
311
+ });
312
+ });
313
+
314
+ test("the Startup and Status headers carry an (i); Service does not", async () => {
315
+ render(<HostServices {...PAGE_PROPS} />);
316
+ await settle();
317
+
318
+ expect(infoLabels()).toEqual(["Startup", "Status"]);
319
+ });
320
+
321
+ test.each([
322
+ ["Startup", "serviceListStartup"],
323
+ ["Status", "serviceListStatus"],
324
+ ])(
325
+ "the %s header explains it with %s",
326
+ async (label: string, key: string) => {
327
+ render(<HostServices {...PAGE_PROPS} />);
328
+ await settle();
329
+
330
+ expect(await explanationOnHover(infoButtonsFor(label)[0]!)).toBe(
331
+ D[key as HostMetric],
332
+ );
333
+ },
334
+ );
335
+
336
+ test("the header (i) opens from the keyboard too", async () => {
337
+ render(<HostServices {...PAGE_PROPS} />);
338
+ await settle();
339
+
340
+ expect(await explanationOnFocus(infoButtonsFor("Status")[0]!)).toBe(
341
+ D.serviceListStatus,
342
+ );
343
+ });
344
+
345
+ test("on a sortable column the (i) sits beside the sort button, not in it", async () => {
346
+ render(<HostServices {...PAGE_PROPS} />);
347
+ await settle();
348
+
349
+ for (const label of ["Startup", "Status"]) {
350
+ const button: HTMLElement = infoButtonsFor(label)[0]!;
351
+ const sortButton: HTMLElement = within(headerCell(label)).getByRole(
352
+ "button",
353
+ { name: label },
354
+ );
355
+
356
+ expectNotNestedInControl(button);
357
+ expect(sortButton).not.toContainElement(button);
358
+ }
359
+ });
360
+
361
+ test("asking what Status means does not sort; the sort button still does", async () => {
362
+ render(<HostServices {...PAGE_PROPS} />);
363
+ await settle();
364
+
365
+ expect(headerCell("Status")).toHaveAttribute("aria-sort", "none");
366
+
367
+ fireEvent.click(infoButtonsFor("Status")[0]!);
368
+ await settle();
369
+
370
+ expect(headerCell("Status")).toHaveAttribute("aria-sort", "none");
371
+
372
+ fireEvent.click(
373
+ within(headerCell("Status")).getByRole("button", { name: "Status" }),
374
+ );
375
+ await settle();
376
+
377
+ // Sorted by Status now (the shared header flips the current direction).
378
+ expect(headerCell("Status").getAttribute("aria-sort")).not.toBe("none");
379
+ expect(headerCell("Status").getAttribute("aria-sort")).toMatch(
380
+ SORTED_PATTERN,
381
+ );
382
+ // The (i) survives the re-render the sort causes.
383
+ expect(infoLabels()).toEqual(["Startup", "Status"]);
384
+ });
385
+
386
+ test("the rows use the labels the texts list", async () => {
387
+ render(<HostServices {...PAGE_PROPS} />);
388
+ await settle();
389
+
390
+ for (const label of ["Automatic", "Manual", "Boot", "Running", "Stopped"]) {
391
+ expect(screen.getAllByText(label).length).toBeGreaterThan(0);
392
+ }
393
+ expect(D.serviceListStartup).toContain("Automatic");
394
+ expect(D.serviceListStartup).toContain("Manual");
395
+ expect(D.serviceListStartup).toContain("Boot");
396
+ expect(D.serviceListStatus).toContain("Running");
397
+ expect(D.serviceListStatus).toContain("Stopped");
398
+ });
399
+ });
400
+
401
+ // ---------------------------------------------------------- systemd units
402
+
403
+ function unitRow(name: string, state: string): Record<string, unknown> {
404
+ return {
405
+ time: secondsAgo(15),
406
+ value: 1,
407
+ attributes: {
408
+ "resource.systemd.unit.name": name,
409
+ "systemd.unit.active_state": state,
410
+ "resource.host.name": "web-01",
411
+ },
412
+ };
413
+ }
414
+
415
+ describe("Host Systemd Units list", () => {
416
+ beforeEach(() => {
417
+ getListMock.mockResolvedValue({
418
+ data: [
419
+ unitRow("nginx.service", "active"),
420
+ unitRow("backup.service", "failed"),
421
+ unitRow("logrotate.timer", "active"),
422
+ ],
423
+ });
424
+ });
425
+
426
+ test("the Type and State headers carry an (i); Unit does not", async () => {
427
+ render(<HostSystemdUnits {...PAGE_PROPS} />);
428
+ await settle();
429
+
430
+ expect(infoLabels()).toEqual(["Type", "State"]);
431
+ });
432
+
433
+ test.each([
434
+ ["Type", "unitListType"],
435
+ ["State", "unitListState"],
436
+ ])(
437
+ "the %s header explains it with %s",
438
+ async (label: string, key: string) => {
439
+ render(<HostSystemdUnits {...PAGE_PROPS} />);
440
+ await settle();
441
+
442
+ expect(await explanationOnHover(infoButtonsFor(label)[0]!)).toBe(
443
+ D[key as HostMetric],
444
+ );
445
+ expectNotNestedInControl(infoButtonsFor(label)[0]!);
446
+ },
447
+ );
448
+
449
+ test("asking what State means does not sort the table", async () => {
450
+ render(<HostSystemdUnits {...PAGE_PROPS} />);
451
+ await settle();
452
+
453
+ const before: string | null = headerCell("State").getAttribute("aria-sort");
454
+
455
+ fireEvent.click(infoButtonsFor("State")[0]!);
456
+ await settle();
457
+
458
+ expect(headerCell("State").getAttribute("aria-sort")).toBe(before);
459
+ });
460
+
461
+ test("the rows use the labels the texts quote", async () => {
462
+ render(<HostSystemdUnits {...PAGE_PROPS} />);
463
+ await settle();
464
+
465
+ expect(screen.getAllByText("Active").length).toBeGreaterThan(0);
466
+ expect(screen.getByText("Failed")).toBeInTheDocument();
467
+ expect(screen.getByText("Timer")).toBeInTheDocument();
468
+ expect(D.unitListState).toContain("Active");
469
+ expect(D.unitListState).toContain("Failed");
470
+ expect(D.unitListType).toContain("Timer");
471
+ });
472
+ });