@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
@@ -9,6 +9,7 @@ import {
9
9
  import React from "react";
10
10
  import { MemoryRouter } from "react-router-dom";
11
11
  import DiscoveryPage from "../../../../App/FeatureSet/Dashboard/src/Pages/NetworkDevice/Discovery";
12
+ import { NETWORK_DEVICE_METRIC_DESCRIPTIONS } from "../../../../App/FeatureSet/Dashboard/src/Components/MetricDescriptions/NetworkDeviceMetricDescriptions";
12
13
  import ProbeUtil from "../../../../App/FeatureSet/Dashboard/src/Utils/Probe";
13
14
  import NetworkDeviceDiscoveryScan, {
14
15
  DiscoveredNetworkDevice,
@@ -361,6 +362,44 @@ describe("Discovery page live progress wiring", () => {
361
362
  expect(screen.getByText("Not started")).toBeInTheDocument();
362
363
  });
363
364
 
365
+ test("only the Responded Hosts column carries a header (i), saying what it counts", async () => {
366
+ await renderPage(
367
+ scan({
368
+ status: "Completed",
369
+ scannedHostCount: 15360,
370
+ respondedHostCount: 1,
371
+ discoveredDevices: [
372
+ { ipAddress: "10.240.0.221", snmpReachable: true },
373
+ HOST,
374
+ ],
375
+ }),
376
+ );
377
+
378
+ const explained: Array<[string, string | undefined]> =
379
+ capturedTable!.columns
380
+ .filter((item: Column<NetworkDeviceDiscoveryScan>) => {
381
+ return Boolean(item.headerTooltip);
382
+ })
383
+ .map(
384
+ (
385
+ item: Column<NetworkDeviceDiscoveryScan>,
386
+ ): [string, string | undefined] => {
387
+ return [item.title, item.headerTooltip];
388
+ },
389
+ );
390
+
391
+ expect(explained).toEqual([
392
+ [
393
+ "Responded Hosts",
394
+ NETWORK_DEVICE_METRIC_DESCRIPTIONS.discoveryRespondedHosts,
395
+ ],
396
+ ]);
397
+
398
+ // The cell the (i) explains: SNMP responders, then the ping-only line.
399
+ expect(screen.getByText("1 of 15360 hosts")).toBeInTheDocument();
400
+ expect(screen.getByText("+ 1 alive without SNMP")).toBeInTheDocument();
401
+ });
402
+
364
403
  test("reviewing a live result explains that importing is safe while discovery continues", async () => {
365
404
  await renderPage(scan({ discoveredDevices: [HOST] }));
366
405
  await act(async () => {
@@ -0,0 +1,414 @@
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
+ act,
12
+ cleanup,
13
+ fireEvent,
14
+ render,
15
+ RenderResult,
16
+ screen,
17
+ } from "@testing-library/react";
18
+ import * as React from "react";
19
+ import getJestMockFunction, { MockFunction } from "../../MockType";
20
+
21
+ /*
22
+ * The Docker Swarm Clusters list (Pages/DockerSwarm/Clusters.tsx) is a
23
+ * ModelTable. It is rendered with the table replaced by a recorder, so the
24
+ * columns the page builds - header tooltip and cell renderer - can be read
25
+ * and run; the recorded count columns are then drawn by the shared Table,
26
+ * the way BaseModelTable spreads them, to check the (i) a customer hovers.
27
+ *
28
+ * Nodes, Services and Tasks read the counts cached on the cluster row
29
+ * (nodeCount / readyNodeCount, serviceCount, taskCount / runningTaskCount),
30
+ * which ingest rewrites from each inventory snapshot. The accuracy of the
31
+ * words against the ingest code is pinned in DockerSwarmMetricTooltips.test.ts.
32
+ */
33
+
34
+ const modelTableMock: MockFunction = getJestMockFunction();
35
+ const countMock: MockFunction = getJestMockFunction();
36
+
37
+ jest.mock("react-i18next", () => {
38
+ return {
39
+ useTranslation: () => {
40
+ return {
41
+ t: (key: string, opts?: { defaultValue?: string }): string => {
42
+ return opts?.defaultValue ?? key;
43
+ },
44
+ };
45
+ },
46
+ };
47
+ });
48
+
49
+ jest.mock("../../../UI/Utils/ModelAPI/ModelAPI", () => {
50
+ return {
51
+ __esModule: true,
52
+ default: {
53
+ count: (...args: Array<unknown>): unknown => {
54
+ return countMock(...args);
55
+ },
56
+ },
57
+ };
58
+ });
59
+
60
+ jest.mock("../../../UI/Utils/Navigation", () => {
61
+ return {
62
+ __esModule: true,
63
+ default: {
64
+ getFirstParam: (): undefined => {
65
+ return undefined;
66
+ },
67
+ navigate: (): void => {},
68
+ isOnThisPage: (): boolean => {
69
+ return false;
70
+ },
71
+ },
72
+ };
73
+ });
74
+
75
+ jest.mock("../../../UI/Components/ModelTable/ModelTable", () => {
76
+ return {
77
+ __esModule: true,
78
+ default: (props: unknown) => {
79
+ modelTableMock(props);
80
+ return <div data-testid="model-table" />;
81
+ },
82
+ };
83
+ });
84
+
85
+ jest.mock(
86
+ "../../../../App/FeatureSet/Dashboard/src/Components/ResourceOwners/useResourceOwners",
87
+ () => {
88
+ const actual: Record<string, unknown> = jest.requireActual(
89
+ "../../../../App/FeatureSet/Dashboard/src/Components/ResourceOwners/useResourceOwners",
90
+ ) as Record<string, unknown>;
91
+ return {
92
+ ...actual,
93
+ __esModule: true,
94
+ default: () => {
95
+ return {
96
+ getOwnersForResource: () => {
97
+ return [];
98
+ },
99
+ isLoadingOwners: false,
100
+ onResourcesFetched: () => {},
101
+ filterBar: null,
102
+ mergeFiltersIntoQuery: (query: unknown) => {
103
+ return query;
104
+ },
105
+ facetSaveState: undefined,
106
+ restoreFacetState: () => {},
107
+ };
108
+ },
109
+ };
110
+ },
111
+ );
112
+
113
+ jest.mock("../../../UI/Components/BulkUpdate/BulkLabelActions", () => {
114
+ return {
115
+ __esModule: true,
116
+ default: () => {
117
+ return { bulkActions: [], modals: null };
118
+ },
119
+ };
120
+ });
121
+
122
+ jest.mock("../../../UI/Components/BulkUpdate/BulkOwnerActions", () => {
123
+ return {
124
+ __esModule: true,
125
+ default: () => {
126
+ return { bulkActions: [], modals: null };
127
+ },
128
+ };
129
+ });
130
+
131
+ jest.mock("../../../UI/Components/BulkUpdate/BulkArchiveActions", () => {
132
+ return {
133
+ __esModule: true,
134
+ default: () => {
135
+ return { archiveBulkActions: [] };
136
+ },
137
+ };
138
+ });
139
+
140
+ import DockerSwarmClusters from "../../../../App/FeatureSet/Dashboard/src/Pages/DockerSwarm/Clusters";
141
+ import PageComponentProps from "../../../../App/FeatureSet/Dashboard/src/Pages/PageComponentProps";
142
+ import { DOCKER_SWARM_METRIC_DESCRIPTIONS } from "../../../../App/FeatureSet/Dashboard/src/Components/MetricDescriptions/DockerSwarmMetricDescriptions";
143
+ import Table from "../../../UI/Components/Table/Table";
144
+ import Column from "../../../UI/Components/Table/Types/Column";
145
+ import FieldType from "../../../UI/Components/Types/FieldType";
146
+ import SortOrder from "../../../Types/BaseDatabase/SortOrder";
147
+ import GenericObject from "../../../Types/GenericObject";
148
+
149
+ const T: typeof DOCKER_SWARM_METRIC_DESCRIPTIONS =
150
+ DOCKER_SWARM_METRIC_DESCRIPTIONS;
151
+
152
+ const PAGE_PROPS: PageComponentProps = {} as PageComponentProps;
153
+
154
+ const COUNT_COLUMNS: Array<[string, string]> = [
155
+ ["Nodes", T.clusterListNodes],
156
+ ["Services", T.clusterListServices],
157
+ ["Tasks", T.clusterListTasks],
158
+ ];
159
+
160
+ type CapturedColumn = {
161
+ field: Record<string, unknown>;
162
+ title: string;
163
+ type: FieldType;
164
+ headerTooltip?: string | undefined;
165
+ hideOnMobile?: boolean | undefined;
166
+ getElement?: ((item: unknown) => React.ReactElement) | undefined;
167
+ };
168
+
169
+ async function flush(): Promise<void> {
170
+ for (let i: number = 0; i < 8; i++) {
171
+ await act(async () => {
172
+ await Promise.resolve();
173
+ });
174
+ }
175
+ }
176
+
177
+ async function capturedColumns(): Promise<Array<CapturedColumn>> {
178
+ render(<DockerSwarmClusters {...PAGE_PROPS} />);
179
+ await flush();
180
+
181
+ expect(modelTableMock).toHaveBeenCalled();
182
+
183
+ const props: { columns: Array<CapturedColumn> } = modelTableMock.mock.calls[
184
+ modelTableMock.mock.calls.length - 1
185
+ ]![0] as { columns: Array<CapturedColumn> };
186
+
187
+ cleanup();
188
+
189
+ return props.columns;
190
+ }
191
+
192
+ function column(columns: Array<CapturedColumn>, title: string): CapturedColumn {
193
+ const found: Array<CapturedColumn> = columns.filter(
194
+ (c: CapturedColumn): boolean => {
195
+ return c.title === title;
196
+ },
197
+ );
198
+
199
+ expect(found).toHaveLength(1);
200
+
201
+ return found[0]!;
202
+ }
203
+
204
+ function cellText(
205
+ columnDef: CapturedColumn,
206
+ item: Record<string, unknown>,
207
+ ): { text: string; className: string } {
208
+ const result: RenderResult = render(columnDef.getElement!(item));
209
+ const root: HTMLElement = result.container.firstElementChild as HTMLElement;
210
+ const out: { text: string; className: string } = {
211
+ text: (root.textContent || "").trim(),
212
+ className: root.className,
213
+ };
214
+
215
+ result.unmount();
216
+
217
+ return out;
218
+ }
219
+
220
+ async function hover(trigger: HTMLElement): Promise<void> {
221
+ fireEvent.mouseEnter(trigger);
222
+ await act(async () => {
223
+ jest.advanceTimersByTime(200);
224
+ });
225
+ }
226
+
227
+ beforeEach(() => {
228
+ jest.useFakeTimers();
229
+ modelTableMock.mockReset();
230
+ countMock.mockReset();
231
+ // A project that already has a cluster, so the list (not the guide) shows.
232
+ countMock.mockResolvedValue(1);
233
+ });
234
+
235
+ afterEach(() => {
236
+ cleanup();
237
+ jest.useRealTimers();
238
+ });
239
+
240
+ describe("Docker Swarm Clusters list columns", () => {
241
+ test("Nodes, Services and Tasks carry their header tooltips; the rest carry none", async () => {
242
+ const tooltips: Record<string, string | undefined> = {};
243
+
244
+ for (const c of await capturedColumns()) {
245
+ tooltips[c.title] = c.headerTooltip;
246
+ }
247
+
248
+ expect(tooltips).toEqual({
249
+ Name: undefined,
250
+ Status: undefined,
251
+ Nodes: T.clusterListNodes,
252
+ Services: T.clusterListServices,
253
+ Tasks: T.clusterListTasks,
254
+ "Docker Version": undefined,
255
+ "Last Seen": undefined,
256
+ Labels: undefined,
257
+ Owners: undefined,
258
+ });
259
+ });
260
+
261
+ test.each(COUNT_COLUMNS)(
262
+ "%s reads the counts cached on the cluster row",
263
+ async (title: string) => {
264
+ const fields: Record<string, Array<string>> = {
265
+ Nodes: ["nodeCount", "readyNodeCount"],
266
+ Services: ["serviceCount"],
267
+ Tasks: ["taskCount", "runningTaskCount"],
268
+ };
269
+
270
+ expect(
271
+ Object.keys(column(await capturedColumns(), title).field).sort(),
272
+ ).toEqual([...fields[title]!].sort());
273
+ },
274
+ );
275
+
276
+ test("Nodes is ready out of total, and red unless every node is ready, as its text says", async () => {
277
+ const nodes: CapturedColumn = column(await capturedColumns(), "Nodes");
278
+
279
+ const allReady: { text: string; className: string } = cellText(nodes, {
280
+ nodeCount: 3,
281
+ readyNodeCount: 3,
282
+ });
283
+ expect(allReady.text).toBe("3/3 ready");
284
+ expect(allReady.className).not.toContain("text-red-700");
285
+
286
+ const oneDown: { text: string; className: string } = cellText(nodes, {
287
+ nodeCount: 3,
288
+ readyNodeCount: 2,
289
+ });
290
+ expect(oneDown.text).toBe("2/3 ready");
291
+ expect(oneDown.className).toContain("text-red-700");
292
+
293
+ expect(cellText(nodes, {}).text).toBe("—");
294
+ expect(T.clusterListNodes).toContain("shown as ready out of total");
295
+ expect(T.clusterListNodes).toContain(
296
+ "turns red when any node is not ready",
297
+ );
298
+ });
299
+
300
+ test("Services is the plain count, whatever the services' state", async () => {
301
+ const services: CapturedColumn = column(
302
+ await capturedColumns(),
303
+ "Services",
304
+ );
305
+
306
+ expect(cellText(services, { serviceCount: 7 }).text).toBe("7");
307
+ expect(cellText(services, {}).text).toBe("0");
308
+ });
309
+
310
+ test("Tasks is running out of the tasks Swarm wants running", async () => {
311
+ const tasks: CapturedColumn = column(await capturedColumns(), "Tasks");
312
+
313
+ expect(cellText(tasks, { taskCount: 12, runningTaskCount: 10 }).text).toBe(
314
+ "10/12 running",
315
+ );
316
+ expect(cellText(tasks, {}).text).toBe("—");
317
+ expect(T.clusterListTasks).toContain(
318
+ "shown as how many are actually running out of that total",
319
+ );
320
+ });
321
+ });
322
+
323
+ describe("the count (i)s as a customer meets them", () => {
324
+ async function renderCounts(onSortChanged: () => void): Promise<void> {
325
+ const columns: Array<CapturedColumn> = (await capturedColumns()).filter(
326
+ (c: CapturedColumn): boolean => {
327
+ return COUNT_COLUMNS.some((entry: [string, string]): boolean => {
328
+ return entry[0] === c.title;
329
+ });
330
+ },
331
+ );
332
+ const tableColumns: Array<Column<GenericObject>> = columns.map(
333
+ (c: CapturedColumn): Column<GenericObject> => {
334
+ return {
335
+ title: c.title,
336
+ type: c.type,
337
+ key: Object.keys(c.field)[0] as keyof GenericObject,
338
+ headerTooltip: c.headerTooltip,
339
+ hideOnMobile: c.hideOnMobile,
340
+ getElement: c.getElement as
341
+ | ((item: GenericObject) => React.ReactElement)
342
+ | undefined,
343
+ };
344
+ },
345
+ );
346
+ const rows: Array<GenericObject> = [
347
+ {
348
+ _id: "c1",
349
+ nodeCount: 3,
350
+ readyNodeCount: 2,
351
+ serviceCount: 7,
352
+ taskCount: 12,
353
+ runningTaskCount: 10,
354
+ },
355
+ ];
356
+
357
+ render(
358
+ <Table<GenericObject>
359
+ id="docker-swarm-clusters-table"
360
+ data={rows}
361
+ columns={tableColumns}
362
+ currentPageNumber={1}
363
+ totalItemsCount={rows.length}
364
+ itemsOnPage={10}
365
+ error=""
366
+ isLoading={false}
367
+ singularLabel="Cluster"
368
+ pluralLabel="Clusters"
369
+ sortOrder={SortOrder.Ascending}
370
+ sortBy={null}
371
+ onSortChanged={onSortChanged}
372
+ onNavigateToPage={() => {}}
373
+ />,
374
+ );
375
+ }
376
+
377
+ test.each(COUNT_COLUMNS)(
378
+ "hovering the %s (i) explains the count",
379
+ async (title: string, text: string) => {
380
+ await renderCounts(() => {});
381
+
382
+ const info: HTMLElement = screen.getByRole("button", {
383
+ name: `About ${title}`,
384
+ });
385
+
386
+ expect(info.closest("thead")).not.toBeNull();
387
+ expect(info.parentElement!.closest("button, a")).toBeNull();
388
+
389
+ await hover(info);
390
+ expect(screen.getByRole("tooltip")).toHaveTextContent(text);
391
+ },
392
+ );
393
+
394
+ test("asking what a count means does not sort the list", async () => {
395
+ const onSortChanged: MockFunction = getJestMockFunction();
396
+
397
+ await renderCounts(() => {
398
+ onSortChanged();
399
+ });
400
+
401
+ for (const [title] of COUNT_COLUMNS) {
402
+ const info: HTMLElement = screen.getByRole("button", {
403
+ name: `About ${title}`,
404
+ });
405
+
406
+ fireEvent.click(info);
407
+ fireEvent.keyDown(info, { key: "Enter" });
408
+ }
409
+
410
+ expect(onSortChanged).not.toHaveBeenCalled();
411
+ expect(screen.getByText("2/3 ready")).toBeInTheDocument();
412
+ expect(screen.getByText("10/12 running")).toBeInTheDocument();
413
+ });
414
+ });