@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,124 @@
1
+ import { expect, jest } from "@jest/globals";
2
+ import { act, fireEvent } from "@testing-library/react";
3
+
4
+ /*
5
+ * Helpers for the Host page render tests (HostOverviewTooltips,
6
+ * HostProcessViewTooltips, HostServiceUnitTooltips, HostListColumnTooltips).
7
+ * Not a test file itself (no .test. in the name), so jest does not run it.
8
+ *
9
+ * An (i) is an InfoTooltip: a <button aria-label="About <title>"> whose
10
+ * Tippy popup is created lazily on first hover and portalled to <body>.
11
+ * Under jsdom a hidden popup never finishes animating out, so a page that
12
+ * hovers several (i)s can hold several popups at once; the text is
13
+ * therefore read through the aria-describedby link Tippy puts on the
14
+ * trigger while it is shown, never through "the" tooltip on the page.
15
+ *
16
+ * Every helper here expects jest fake timers.
17
+ */
18
+
19
+ const ABOUT: string = "About ";
20
+
21
+ // The (i) buttons under `root`, in document order.
22
+ export function infoButtons(
23
+ root: ParentNode = document.body,
24
+ ): Array<HTMLElement> {
25
+ return Array.from(
26
+ root.querySelectorAll<HTMLElement>(`button[aria-label^="${ABOUT}"]`),
27
+ );
28
+ }
29
+
30
+ // What each (i) under `root` explains, in document order.
31
+ export function infoLabels(root: ParentNode = document.body): Array<string> {
32
+ return infoButtons(root).map((button: HTMLElement): string => {
33
+ return (button.getAttribute("aria-label") || "").slice(ABOUT.length);
34
+ });
35
+ }
36
+
37
+ // The (i) buttons named "About <label>", in document order.
38
+ export function infoButtonsFor(
39
+ label: string,
40
+ root: ParentNode = document.body,
41
+ ): Array<HTMLElement> {
42
+ return infoButtons(root).filter((button: HTMLElement): boolean => {
43
+ return button.getAttribute("aria-label") === `${ABOUT}${label}`;
44
+ });
45
+ }
46
+
47
+ /*
48
+ * Hover an (i), read the text its tooltip shows, then move the pointer
49
+ * away again so the next hover starts clean.
50
+ */
51
+ export async function explanationOnHover(button: HTMLElement): Promise<string> {
52
+ fireEvent.mouseEnter(button);
53
+ await act(async () => {
54
+ jest.advanceTimersByTime(250);
55
+ });
56
+
57
+ const describedBy: string | null = button.getAttribute("aria-describedby");
58
+ const popup: HTMLElement | null = describedBy
59
+ ? document.getElementById(describedBy)
60
+ : null;
61
+ const text: string = popup?.textContent || "";
62
+
63
+ fireEvent.mouseLeave(button);
64
+ await act(async () => {
65
+ jest.advanceTimersByTime(250);
66
+ });
67
+
68
+ return text;
69
+ }
70
+
71
+ // The same, reached with the keyboard instead of the mouse.
72
+ export async function explanationOnFocus(button: HTMLElement): Promise<string> {
73
+ act(() => {
74
+ button.focus();
75
+ });
76
+ await act(async () => {
77
+ jest.advanceTimersByTime(250);
78
+ });
79
+
80
+ const describedBy: string | null = button.getAttribute("aria-describedby");
81
+ const popup: HTMLElement | null = describedBy
82
+ ? document.getElementById(describedBy)
83
+ : null;
84
+ const text: string = popup?.textContent || "";
85
+
86
+ act(() => {
87
+ button.blur();
88
+ });
89
+ await act(async () => {
90
+ jest.advanceTimersByTime(250);
91
+ });
92
+
93
+ return text;
94
+ }
95
+
96
+ /*
97
+ * An (i) is itself a button: inside another button or a link it is
98
+ * invalid HTML and its click would reach the outer control.
99
+ */
100
+ export function expectNotNestedInControl(button: HTMLElement): void {
101
+ const outer: Element | null =
102
+ button.parentElement?.closest("button, a, [role='button']") || null;
103
+
104
+ expect({
105
+ label: button.getAttribute("aria-label"),
106
+ nestedIn: outer ? outer.tagName : null,
107
+ }).toEqual({ label: button.getAttribute("aria-label"), nestedIn: null });
108
+ }
109
+
110
+ // Let every pending promise (mocked fetches, state updates) settle.
111
+ export async function settle(): Promise<void> {
112
+ for (let i: number = 0; i < 12; i++) {
113
+ await act(async () => {
114
+ await Promise.resolve();
115
+ });
116
+ }
117
+ }
118
+
119
+ // A promise that never settles: keeps a page in its loading state.
120
+ export function never<T>(): Promise<T> {
121
+ return new Promise<T>(() => {
122
+ // Intentionally never resolved.
123
+ });
124
+ }
@@ -0,0 +1,395 @@
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
+ screen,
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
+ } from "./HostTooltipHarness";
26
+
27
+ /*
28
+ * The Hosts list (Pages/Host/Hosts.tsx) is a ModelTable. It is rendered with
29
+ * the table replaced by a recorder, so the columns the page builds - header
30
+ * tooltip and cell renderer - can be read and run; the recorded columns are
31
+ * then drawn by the shared Table, the way BaseModelTable spreads them, to
32
+ * check the (i) a customer actually hovers.
33
+ *
34
+ * The one metric column is Resources: CPU cores and RAM on one line, the
35
+ * process count below, all three read from the host record (Host.cpuCores,
36
+ * Host.totalMemoryBytes, Host.processCount), which ingest refreshes at most
37
+ * about once a minute. The accuracy of those words against the ingest code
38
+ * is pinned in HostMetricTooltips.test.ts.
39
+ */
40
+
41
+ const GIB: number = 1024 * 1024 * 1024;
42
+
43
+ const modelTableMock: MockFunction = getJestMockFunction();
44
+ const countMock: MockFunction = getJestMockFunction();
45
+
46
+ jest.mock("react-i18next", () => {
47
+ return {
48
+ useTranslation: () => {
49
+ return {
50
+ t: (key: string, opts?: { defaultValue?: string }): string => {
51
+ return opts?.defaultValue ?? key;
52
+ },
53
+ };
54
+ },
55
+ };
56
+ });
57
+
58
+ jest.mock("../../../UI/Utils/ModelAPI/ModelAPI", () => {
59
+ return {
60
+ __esModule: true,
61
+ default: {
62
+ count: (...args: Array<unknown>): unknown => {
63
+ return countMock(...args);
64
+ },
65
+ },
66
+ };
67
+ });
68
+
69
+ jest.mock("../../../UI/Utils/Navigation", () => {
70
+ return {
71
+ __esModule: true,
72
+ default: {
73
+ getFirstParam: (): undefined => {
74
+ return undefined;
75
+ },
76
+ navigate: (): void => {},
77
+ isOnThisPage: (): boolean => {
78
+ return false;
79
+ },
80
+ },
81
+ };
82
+ });
83
+
84
+ jest.mock("../../../UI/Components/ModelTable/ModelTable", () => {
85
+ return {
86
+ __esModule: true,
87
+ default: (props: unknown) => {
88
+ modelTableMock(props);
89
+ return <div data-testid="model-table" />;
90
+ },
91
+ };
92
+ });
93
+
94
+ jest.mock(
95
+ "../../../../App/FeatureSet/Dashboard/src/Components/ResourceOwners/useResourceOwners",
96
+ () => {
97
+ const actual: Record<string, unknown> = jest.requireActual(
98
+ "../../../../App/FeatureSet/Dashboard/src/Components/ResourceOwners/useResourceOwners",
99
+ ) as Record<string, unknown>;
100
+ return {
101
+ ...actual,
102
+ __esModule: true,
103
+ default: () => {
104
+ return {
105
+ getOwnersForResource: () => {
106
+ return [];
107
+ },
108
+ isLoadingOwners: false,
109
+ onResourcesFetched: () => {},
110
+ filterBar: null,
111
+ mergeFiltersIntoQuery: (query: unknown) => {
112
+ return query;
113
+ },
114
+ facetSaveState: undefined,
115
+ restoreFacetState: () => {},
116
+ };
117
+ },
118
+ };
119
+ },
120
+ );
121
+
122
+ jest.mock("../../../UI/Components/BulkUpdate/BulkLabelActions", () => {
123
+ return {
124
+ __esModule: true,
125
+ default: () => {
126
+ return { bulkActions: [], modals: null };
127
+ },
128
+ };
129
+ });
130
+
131
+ jest.mock("../../../UI/Components/BulkUpdate/BulkOwnerActions", () => {
132
+ return {
133
+ __esModule: true,
134
+ default: () => {
135
+ return { bulkActions: [], modals: null };
136
+ },
137
+ };
138
+ });
139
+
140
+ jest.mock("../../../UI/Components/BulkUpdate/BulkArchiveActions", () => {
141
+ return {
142
+ __esModule: true,
143
+ default: () => {
144
+ return { archiveBulkActions: [] };
145
+ },
146
+ };
147
+ });
148
+
149
+ import Hosts from "../../../../App/FeatureSet/Dashboard/src/Pages/Host/Hosts";
150
+ import PageComponentProps from "../../../../App/FeatureSet/Dashboard/src/Pages/PageComponentProps";
151
+ import { HOST_METRIC_DESCRIPTIONS } from "../../../../App/FeatureSet/Dashboard/src/Components/MetricDescriptions/HostMetricDescriptions";
152
+ import Table from "../../../UI/Components/Table/Table";
153
+ import Column from "../../../UI/Components/Table/Types/Column";
154
+ import FieldType from "../../../UI/Components/Types/FieldType";
155
+ import SortOrder from "../../../Types/BaseDatabase/SortOrder";
156
+ import GenericObject from "../../../Types/GenericObject";
157
+
158
+ const PAGE_PROPS: PageComponentProps = {} as PageComponentProps;
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(<Hosts {...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
+ // The recorded columns drawn by the shared Table, keyed as BaseModelTable does.
205
+ function renderAsTable(
206
+ columns: Array<CapturedColumn>,
207
+ rows: Array<GenericObject>,
208
+ onSortChanged: () => void = () => {},
209
+ ): void {
210
+ const tableColumns: Array<Column<GenericObject>> = columns.map(
211
+ (c: CapturedColumn): Column<GenericObject> => {
212
+ return {
213
+ title: c.title,
214
+ type: c.type,
215
+ key: Object.keys(c.field)[0] as keyof GenericObject,
216
+ headerTooltip: c.headerTooltip,
217
+ hideOnMobile: c.hideOnMobile,
218
+ getElement: c.getElement as
219
+ | ((item: GenericObject) => React.ReactElement)
220
+ | undefined,
221
+ };
222
+ },
223
+ );
224
+
225
+ render(
226
+ <Table<GenericObject>
227
+ id="hosts-table"
228
+ data={rows}
229
+ columns={tableColumns}
230
+ currentPageNumber={1}
231
+ totalItemsCount={rows.length}
232
+ itemsOnPage={10}
233
+ error=""
234
+ isLoading={false}
235
+ singularLabel="Host"
236
+ pluralLabel="Hosts"
237
+ sortOrder={SortOrder.Ascending}
238
+ sortBy={null}
239
+ onSortChanged={onSortChanged}
240
+ onNavigateToPage={() => {}}
241
+ />,
242
+ );
243
+ }
244
+
245
+ beforeEach(() => {
246
+ jest.useFakeTimers();
247
+ modelTableMock.mockReset();
248
+ countMock.mockReset();
249
+ // A project that already has hosts, so the list (not the guide) shows.
250
+ countMock.mockResolvedValue(3);
251
+ });
252
+
253
+ afterEach(() => {
254
+ cleanup();
255
+ jest.useRealTimers();
256
+ });
257
+
258
+ describe("Hosts list columns", () => {
259
+ test("Resources carries its header tooltip; names, network, status and ownership carry none", async () => {
260
+ const columns: Array<CapturedColumn> = await capturedColumns();
261
+ const tooltips: Record<string, string | undefined> = {};
262
+
263
+ for (const c of columns) {
264
+ tooltips[c.title] = c.headerTooltip;
265
+ }
266
+
267
+ expect(tooltips).toEqual({
268
+ Name: undefined,
269
+ OS: undefined,
270
+ "IP Address": undefined,
271
+ Resources: HOST_METRIC_DESCRIPTIONS.hostListResources,
272
+ Status: undefined,
273
+ "Last Seen": undefined,
274
+ Labels: undefined,
275
+ Owners: undefined,
276
+ });
277
+ });
278
+
279
+ test("Resources shows cores and RAM on one line and the process count below, as the text says", async () => {
280
+ const resources: CapturedColumn = column(
281
+ await capturedColumns(),
282
+ "Resources",
283
+ );
284
+
285
+ const full: ReturnType<typeof render> = render(
286
+ resources.getElement!({
287
+ cpuCores: 8,
288
+ totalMemoryBytes: 16 * GIB,
289
+ processCount: 312,
290
+ }),
291
+ );
292
+ expect(full.getByText("8 cores · 16 GiB")).toBeInTheDocument();
293
+ expect(full.getByText("312 processes")).toBeInTheDocument();
294
+ full.unmount();
295
+
296
+ // Without a process count (no processes scraper) the line is absent.
297
+ const noProcesses: ReturnType<typeof render> = render(
298
+ resources.getElement!({ cpuCores: 1, totalMemoryBytes: 2 * GIB }),
299
+ );
300
+ expect(noProcesses.getByText("1 core · 2.0 GiB")).toBeInTheDocument();
301
+ expect(noProcesses.queryByText(/processes/)).not.toBeInTheDocument();
302
+ noProcesses.unmount();
303
+
304
+ const nothing: ReturnType<typeof render> = render(
305
+ resources.getElement!({}),
306
+ );
307
+ expect(nothing.getByText("—")).toBeInTheDocument();
308
+ nothing.unmount();
309
+ });
310
+
311
+ test("the column is only the host record's cached fields, which the page asks for", async () => {
312
+ await capturedColumns();
313
+
314
+ const props: { selectMoreFields: Record<string, boolean> } = modelTableMock
315
+ .mock.calls[modelTableMock.mock.calls.length - 1]![0] as {
316
+ selectMoreFields: Record<string, boolean>;
317
+ };
318
+
319
+ expect(props.selectMoreFields).toEqual(
320
+ expect.objectContaining({
321
+ cpuCores: true,
322
+ totalMemoryBytes: true,
323
+ processCount: true,
324
+ }),
325
+ );
326
+ });
327
+ });
328
+
329
+ describe("the Resources (i) as a customer meets it", () => {
330
+ async function renderList(
331
+ onSortChanged: () => void = () => {},
332
+ ): Promise<void> {
333
+ const columns: Array<CapturedColumn> = (await capturedColumns()).filter(
334
+ (c: CapturedColumn): boolean => {
335
+ // Name and Owners need the router and the owners API; not metrics.
336
+ return c.title === "Resources" || c.title === "Status";
337
+ },
338
+ );
339
+
340
+ renderAsTable(
341
+ columns,
342
+ [
343
+ {
344
+ _id: "h1",
345
+ cpuCores: 4,
346
+ totalMemoryBytes: 8 * GIB,
347
+ processCount: 140,
348
+ otelCollectorStatus: "connected",
349
+ },
350
+ ],
351
+ onSortChanged,
352
+ );
353
+ }
354
+
355
+ test("the header has exactly one (i), named after the column", async () => {
356
+ await renderList();
357
+
358
+ expect(infoLabels()).toEqual(["Resources"]);
359
+
360
+ const info: HTMLElement = infoButtonsFor("Resources")[0]!;
361
+
362
+ expect(info.closest("thead")).not.toBeNull();
363
+ expectNotNestedInControl(info);
364
+ });
365
+
366
+ test("hovering or focusing the (i) explains the three numbers", async () => {
367
+ await renderList();
368
+
369
+ const info: HTMLElement = infoButtonsFor("Resources")[0]!;
370
+
371
+ expect(await explanationOnHover(info)).toBe(
372
+ HOST_METRIC_DESCRIPTIONS.hostListResources,
373
+ );
374
+ expect(await explanationOnFocus(info)).toBe(
375
+ HOST_METRIC_DESCRIPTIONS.hostListResources,
376
+ );
377
+ });
378
+
379
+ test("asking what the column means does not sort the table", async () => {
380
+ const onSortChanged: MockFunction = getJestMockFunction();
381
+
382
+ await renderList(() => {
383
+ onSortChanged();
384
+ });
385
+
386
+ const info: HTMLElement = infoButtonsFor("Resources")[0]!;
387
+
388
+ fireEvent.click(info);
389
+ fireEvent.keyDown(info, { key: "Enter" });
390
+
391
+ expect(onSortChanged).not.toHaveBeenCalled();
392
+ expect(screen.getByText("4 cores · 8.0 GiB")).toBeInTheDocument();
393
+ expect(screen.getByText("140 processes")).toBeInTheDocument();
394
+ });
395
+ });