@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,367 @@
1
+ import "@testing-library/jest-dom";
2
+ import { cleanup, render, waitFor } from "@testing-library/react";
3
+ import React from "react";
4
+ import {
5
+ afterEach,
6
+ beforeEach,
7
+ describe,
8
+ expect,
9
+ jest,
10
+ test,
11
+ } from "@jest/globals";
12
+
13
+ /*
14
+ * The schema BaseModelTable hands the bulk action bar for its default Delete,
15
+ * with and without `bulkActions.deleteVerb`. The rendered behaviour (menu,
16
+ * dialog, deleting) is covered end to end in BaseModelTableBulkDelete.test;
17
+ * this pins the parts that are not visible as text - the icon and the style -
18
+ * and the exact sentences for both shapes, by capturing what Table receives.
19
+ * The per-row delete action is captured too, for its icon and for the tooltip
20
+ * a viewer who cannot delete sees on both.
21
+ */
22
+
23
+ let permissionsForTest: Array<unknown> = ["ProjectAdmin"];
24
+
25
+ jest.mock("../../../../UI/Utils/Permission", () => {
26
+ return {
27
+ __esModule: true,
28
+ default: {
29
+ getAllPermissions: (): Array<unknown> => {
30
+ return permissionsForTest;
31
+ },
32
+ getProjectPermissions: (): null => {
33
+ return null;
34
+ },
35
+ getGlobalPermissions: (): null => {
36
+ return null;
37
+ },
38
+ },
39
+ };
40
+ });
41
+
42
+ jest.mock("../../../../UI/Utils/User", () => {
43
+ return {
44
+ __esModule: true,
45
+ default: {
46
+ isMasterAdmin: (): boolean => {
47
+ return false;
48
+ },
49
+ getUserId: (): null => {
50
+ return null;
51
+ },
52
+ },
53
+ };
54
+ });
55
+
56
+ jest.mock("../../../../UI/Utils/Translation", () => {
57
+ return {
58
+ __esModule: true,
59
+ default: () => {
60
+ return {
61
+ translateString: (value: string | undefined): string | undefined => {
62
+ return value;
63
+ },
64
+ translateValue: (value: unknown): unknown => {
65
+ return value;
66
+ },
67
+ };
68
+ },
69
+ };
70
+ });
71
+
72
+ type CapturedBulkActions = {
73
+ buttons: Array<Record<string, any>>;
74
+ };
75
+
76
+ let capturedBulkActions: Array<CapturedBulkActions> = [];
77
+ let capturedRowActions: Array<Array<Record<string, any>>> = [];
78
+
79
+ jest.mock("../../../../UI/Components/Table/Table", () => {
80
+ return {
81
+ __esModule: true,
82
+ default: (props: {
83
+ bulkActions?: CapturedBulkActions | undefined;
84
+ actionButtons?: Array<Record<string, any>> | undefined;
85
+ }): React.ReactElement => {
86
+ if (props.bulkActions) {
87
+ capturedBulkActions.push(props.bulkActions);
88
+ }
89
+ if (props.actionButtons) {
90
+ capturedRowActions.push(props.actionButtons);
91
+ }
92
+ return <div data-testid="table" />;
93
+ },
94
+ };
95
+ });
96
+
97
+ import BaseModelTable, {
98
+ BaseTableCallbacks,
99
+ ComponentProps as BaseModelTableProps,
100
+ ModalTableBulkDefaultActions,
101
+ } from "../../../../UI/Components/ModelTable/BaseModelTable";
102
+ import TableFilterUrlState from "../../../../UI/Utils/TableFilterUrlState";
103
+ import PermissionGate from "../../../../UI/Utils/PermissionGate";
104
+ import FieldType from "../../../../UI/Components/Types/FieldType";
105
+ import { ButtonStyleType } from "../../../../UI/Components/Button/Button";
106
+ import IncidentAlert from "../../../../Models/DatabaseModels/IncidentAlert";
107
+ import IconProp from "../../../../Types/Icon/IconProp";
108
+ import ListResult from "../../../../Types/BaseDatabase/ListResult";
109
+ import { JSONObject } from "../../../../Types/JSON";
110
+ import Permission from "../../../../Types/Permission";
111
+
112
+ const WARNING: string = "Only the links are removed.";
113
+
114
+ type TableOptions = {
115
+ deleteVerb?: string | undefined;
116
+ deleteIcon?: IconProp | undefined;
117
+ deleteConfirmationWarning?: string | undefined;
118
+ deleteButtonText?: string | undefined;
119
+ };
120
+
121
+ type MakePropsFunction = (
122
+ options: TableOptions,
123
+ ) => BaseModelTableProps<IncidentAlert>;
124
+
125
+ const makeProps: MakePropsFunction = (
126
+ options: TableOptions,
127
+ ): BaseModelTableProps<IncidentAlert> => {
128
+ const callbacks: BaseTableCallbacks<IncidentAlert> = {
129
+ deleteItem: async (): Promise<void> => {
130
+ return undefined;
131
+ },
132
+ getModelFromJSON: (item: JSONObject): IncidentAlert => {
133
+ return item as unknown as IncidentAlert;
134
+ },
135
+ getJSONFromModel: (item: IncidentAlert): JSONObject => {
136
+ return item as unknown as JSONObject;
137
+ },
138
+ addSlugToSelect: (select: unknown): unknown => {
139
+ return select;
140
+ },
141
+ getList: async (data: {
142
+ skip: number;
143
+ limit: number;
144
+ }): Promise<ListResult<IncidentAlert>> => {
145
+ return { data: [], count: 0, skip: data.skip, limit: data.limit };
146
+ },
147
+ toJSONArray: (): Array<JSONObject> => {
148
+ return [];
149
+ },
150
+ updateById: async (): Promise<void> => {
151
+ return undefined;
152
+ },
153
+ showCreateEditModal: (): React.ReactElement => {
154
+ return <div />;
155
+ },
156
+ } as unknown as BaseTableCallbacks<IncidentAlert>;
157
+
158
+ return {
159
+ modelType: IncidentAlert,
160
+ id: "links-table",
161
+ name: "Links",
162
+ userPreferencesKey: "links-table",
163
+ urlStateKey: "links-table",
164
+ singularName: "Alert",
165
+ pluralName: "Alerts",
166
+ columns: [
167
+ { field: { createdAt: true }, title: "At", type: FieldType.Text },
168
+ ],
169
+ filters: [],
170
+ cardProps: { title: "Links", description: "Links" },
171
+ isCreateable: false,
172
+ isEditable: false,
173
+ isDeleteable: true,
174
+ isViewable: false,
175
+ deleteButtonText: options.deleteButtonText,
176
+ callbacks: callbacks,
177
+ bulkActions: {
178
+ buttons: [ModalTableBulkDefaultActions.Delete],
179
+ deleteVerb: options.deleteVerb,
180
+ deleteIcon: options.deleteIcon,
181
+ deleteConfirmationWarning: options.deleteConfirmationWarning,
182
+ },
183
+ } as unknown as BaseModelTableProps<IncidentAlert>;
184
+ };
185
+
186
+ type DeleteActionFunction = (
187
+ options: TableOptions,
188
+ ) => Promise<Record<string, any>>;
189
+
190
+ const deleteAction: DeleteActionFunction = async (
191
+ options: TableOptions,
192
+ ): Promise<Record<string, any>> => {
193
+ render(<BaseModelTable<IncidentAlert> {...makeProps(options)} />);
194
+
195
+ await waitFor(() => {
196
+ expect(capturedBulkActions.length).toBeGreaterThan(0);
197
+ });
198
+
199
+ const buttons: Array<Record<string, any>> =
200
+ capturedBulkActions[capturedBulkActions.length - 1]!.buttons;
201
+
202
+ // Exactly one delete action, whatever it is called.
203
+ expect(buttons).toHaveLength(1);
204
+
205
+ return buttons[0]!;
206
+ };
207
+
208
+ type RowDeleteActionFunction = (title: string) => Promise<Record<string, any>>;
209
+
210
+ /*
211
+ * The per-row delete action from the table rendered last. Its title is
212
+ * deleteButtonText, or "Delete".
213
+ */
214
+ const rowDeleteAction: RowDeleteActionFunction = async (
215
+ title: string,
216
+ ): Promise<Record<string, any>> => {
217
+ let action: Record<string, any> | undefined = undefined;
218
+
219
+ await waitFor(() => {
220
+ const rowActions: Array<Record<string, any>> =
221
+ capturedRowActions[capturedRowActions.length - 1] || [];
222
+
223
+ action = rowActions.find((rowAction: Record<string, any>): boolean => {
224
+ return rowAction["title"] === title;
225
+ });
226
+
227
+ expect(action).toBeDefined();
228
+ });
229
+
230
+ return action!;
231
+ };
232
+
233
+ const TWO: Array<IncidentAlert> = [new IncidentAlert(), new IncidentAlert()];
234
+ const ONE: Array<IncidentAlert> = [new IncidentAlert()];
235
+
236
+ describe("BaseModelTable bulk delete verb", () => {
237
+ beforeEach(() => {
238
+ capturedBulkActions = [];
239
+ capturedRowActions = [];
240
+ permissionsForTest = ["ProjectAdmin"];
241
+ PermissionGate.clearPermissionPropsCache();
242
+ window.history.replaceState(window.history.state, "", "/dashboard/links");
243
+ TableFilterUrlState.resetClaimedKeys();
244
+ window.localStorage.clear();
245
+ });
246
+
247
+ afterEach(() => {
248
+ cleanup();
249
+ jest.restoreAllMocks();
250
+ });
251
+
252
+ test("without a verb the default Delete is unchanged", async () => {
253
+ const action: Record<string, any> = await deleteAction({
254
+ deleteConfirmationWarning: WARNING,
255
+ });
256
+
257
+ expect(action["title"]).toBe("Delete");
258
+ expect(action["icon"]).toBe(IconProp.Trash);
259
+ expect(action["buttonStyleType"]).toBe(ButtonStyleType.DANGER);
260
+ expect(action["confirmTitle"](TWO)).toBe("Delete 2 Alerts");
261
+ expect(action["confirmTitle"](ONE)).toBe("Delete 1 Alert");
262
+ expect(action["confirmMessage"](TWO)).toBe(
263
+ `Are you sure you want to delete 2 Alerts? This action cannot be undone. ${WARNING}`,
264
+ );
265
+ });
266
+
267
+ test("a deleteIcon on its own changes nothing", async () => {
268
+ const action: Record<string, any> = await deleteAction({
269
+ deleteIcon: IconProp.LinkSlash,
270
+ });
271
+
272
+ expect(action["title"]).toBe("Delete");
273
+ expect(action["icon"]).toBe(IconProp.Trash);
274
+ });
275
+
276
+ test("with a verb, the title, icon and both sentences use it", async () => {
277
+ const action: Record<string, any> = await deleteAction({
278
+ deleteVerb: "Unlink",
279
+ deleteIcon: IconProp.LinkSlash,
280
+ deleteConfirmationWarning: WARNING,
281
+ });
282
+
283
+ expect(action["title"]).toBe("Unlink");
284
+ expect(action["icon"]).toBe(IconProp.LinkSlash);
285
+ expect(action["confirmTitle"](TWO)).toBe("Unlink 2 Alerts");
286
+ expect(action["confirmTitle"](ONE)).toBe("Unlink 1 Alert");
287
+ expect(action["confirmMessage"](TWO)).toBe(
288
+ `Are you sure you want to unlink 2 alerts? ${WARNING}`,
289
+ );
290
+ expect(action["confirmMessage"](ONE)).toBe(
291
+ `Are you sure you want to unlink 1 alert? ${WARNING}`,
292
+ );
293
+ });
294
+
295
+ test("with a verb and no icon, keeps the trash can", async () => {
296
+ const action: Record<string, any> = await deleteAction({
297
+ deleteVerb: "Remove",
298
+ });
299
+
300
+ expect(action["title"]).toBe("Remove");
301
+ expect(action["icon"]).toBe(IconProp.Trash);
302
+ expect(action["confirmMessage"](TWO)).toBe(
303
+ "Are you sure you want to remove 2 alerts?",
304
+ );
305
+ });
306
+
307
+ test("with a verb and an icon, the row action wears the same icon", async () => {
308
+ await deleteAction({
309
+ deleteVerb: "Unlink",
310
+ deleteIcon: IconProp.LinkSlash,
311
+ deleteButtonText: "Unlink",
312
+ });
313
+
314
+ const row: Record<string, any> = await rowDeleteAction("Unlink");
315
+
316
+ expect(row["icon"]).toBe(IconProp.LinkSlash);
317
+ });
318
+
319
+ test("without a verb the row action keeps the trash can", async () => {
320
+ await deleteAction({ deleteIcon: IconProp.LinkSlash });
321
+
322
+ const row: Record<string, any> = await rowDeleteAction("Delete");
323
+
324
+ expect(row["icon"]).toBe(IconProp.Trash);
325
+ });
326
+
327
+ /*
328
+ * A viewer who can read the links but not remove them sees both Unlinks
329
+ * locked. The tooltip must not say the locked button deletes the Alert -
330
+ * that is the confusion the verb exists to remove.
331
+ */
332
+ test("a viewer's locked Unlink says unlink, not delete this Alert", async () => {
333
+ permissionsForTest = [Permission.Viewer];
334
+
335
+ const bulk: Record<string, any> = await deleteAction({
336
+ deleteVerb: "Unlink",
337
+ deleteIcon: IconProp.LinkSlash,
338
+ deleteButtonText: "Unlink",
339
+ });
340
+ const row: Record<string, any> = await rowDeleteAction("Unlink");
341
+
342
+ for (const action of [bulk, row]) {
343
+ expect(action["disabled"]).toBe(true);
344
+ expect(action["tooltip"]).toContain(
345
+ "You do not have permission to unlink this Alert.",
346
+ );
347
+ expect(action["tooltip"]).toContain("Delete Incident Alert");
348
+ expect(action["tooltip"]).not.toContain("delete this Alert");
349
+ }
350
+ });
351
+
352
+ test("a viewer's locked Delete without a verb still says delete", async () => {
353
+ permissionsForTest = [Permission.Viewer];
354
+
355
+ const bulk: Record<string, any> = await deleteAction({
356
+ deleteButtonText: "Remove",
357
+ });
358
+ const row: Record<string, any> = await rowDeleteAction("Remove");
359
+
360
+ for (const action of [bulk, row]) {
361
+ expect(action["disabled"]).toBe(true);
362
+ expect(action["tooltip"]).toContain(
363
+ "You do not have permission to delete this Alert.",
364
+ );
365
+ }
366
+ });
367
+ });
@@ -0,0 +1,275 @@
1
+ import "@testing-library/jest-dom";
2
+ import { cleanup, fireEvent, render, waitFor } from "@testing-library/react";
3
+ import React from "react";
4
+ import getJestMockFunction, { MockFunction } from "../../../MockType";
5
+ import {
6
+ afterEach,
7
+ beforeEach,
8
+ describe,
9
+ expect,
10
+ jest,
11
+ test,
12
+ } from "@jest/globals";
13
+
14
+ /*
15
+ * The header's create button reads "<createVerb> <singularName>". It used to
16
+ * be translated one word at a time, so a table with createVerb "Link" read
17
+ * "Link" out of context - most locales translated it as the noun ("a link"),
18
+ * and a verb-noun phrase does not keep English word order in many languages
19
+ * either. The whole phrase is now looked up first and the word-by-word join
20
+ * is only the fallback. Translation is mocked with a dictionary per test.
21
+ */
22
+
23
+ let dictionary: Record<string, string> = {};
24
+ let lookedUp: Array<string> = [];
25
+
26
+ jest.mock("../../../../UI/Utils/Permission", () => {
27
+ return {
28
+ __esModule: true,
29
+ default: {
30
+ getAllPermissions: (): Array<unknown> => {
31
+ return ["ProjectAdmin"];
32
+ },
33
+ getProjectPermissions: (): null => {
34
+ return null;
35
+ },
36
+ getGlobalPermissions: (): null => {
37
+ return null;
38
+ },
39
+ },
40
+ };
41
+ });
42
+
43
+ jest.mock("../../../../UI/Utils/User", () => {
44
+ return {
45
+ __esModule: true,
46
+ default: {
47
+ isMasterAdmin: (): boolean => {
48
+ return false;
49
+ },
50
+ getUserId: (): null => {
51
+ return null;
52
+ },
53
+ },
54
+ };
55
+ });
56
+
57
+ jest.mock("../../../../UI/Utils/Translation", () => {
58
+ const translate: (value: string | undefined) => string | undefined = (
59
+ value: string | undefined,
60
+ ): string | undefined => {
61
+ if (typeof value !== "string" || value.length === 0) {
62
+ return value;
63
+ }
64
+
65
+ lookedUp.push(value);
66
+
67
+ return dictionary[value] ?? value;
68
+ };
69
+
70
+ return {
71
+ __esModule: true,
72
+ default: () => {
73
+ return {
74
+ translateString: translate,
75
+ translateValue: (value: unknown): unknown => {
76
+ return typeof value === "string" ? translate(value) : value;
77
+ },
78
+ };
79
+ },
80
+ };
81
+ });
82
+
83
+ import BaseModelTable, {
84
+ BaseTableCallbacks,
85
+ ComponentProps as BaseModelTableProps,
86
+ } from "../../../../UI/Components/ModelTable/BaseModelTable";
87
+ import TableFilterUrlState from "../../../../UI/Utils/TableFilterUrlState";
88
+ import PermissionGate from "../../../../UI/Utils/PermissionGate";
89
+ import FieldType from "../../../../UI/Components/Types/FieldType";
90
+ import Monitor from "../../../../Models/DatabaseModels/Monitor";
91
+ import ListResult from "../../../../Types/BaseDatabase/ListResult";
92
+ import { JSONObject } from "../../../../Types/JSON";
93
+
94
+ type TableOptions = {
95
+ createVerb?: string | undefined;
96
+ singularName?: string | undefined;
97
+ onCreateClick?: (() => void) | undefined;
98
+ };
99
+
100
+ describe("BaseModelTable header create button title", () => {
101
+ type MakePropsFunction = (
102
+ options: TableOptions,
103
+ ) => BaseModelTableProps<Monitor>;
104
+
105
+ const makeProps: MakePropsFunction = (
106
+ options: TableOptions,
107
+ ): BaseModelTableProps<Monitor> => {
108
+ const callbacks: BaseTableCallbacks<Monitor> = {
109
+ deleteItem: async (): Promise<void> => {
110
+ return undefined;
111
+ },
112
+ getModelFromJSON: (item: JSONObject): Monitor => {
113
+ return item as unknown as Monitor;
114
+ },
115
+ getJSONFromModel: (item: Monitor): JSONObject => {
116
+ return item as unknown as JSONObject;
117
+ },
118
+ addSlugToSelect: (select: unknown): unknown => {
119
+ return select;
120
+ },
121
+ getList: async (data: {
122
+ skip: number;
123
+ limit: number;
124
+ }): Promise<ListResult<Monitor>> => {
125
+ return { data: [], count: 0, skip: data.skip, limit: data.limit };
126
+ },
127
+ toJSONArray: (): Array<JSONObject> => {
128
+ return [];
129
+ },
130
+ updateById: async (): Promise<void> => {
131
+ return undefined;
132
+ },
133
+ showCreateEditModal: (): React.ReactElement => {
134
+ return <div data-testid="create-edit-modal" />;
135
+ },
136
+ } as unknown as BaseTableCallbacks<Monitor>;
137
+
138
+ return {
139
+ modelType: Monitor,
140
+ id: "create-title-table",
141
+ name: "Create Title",
142
+ userPreferencesKey: "create-title-table",
143
+ urlStateKey: "create-title-table",
144
+ columns: [{ field: { name: true }, title: "Name", type: FieldType.Text }],
145
+ filters: [],
146
+ cardProps: { title: "Things", description: "All things" },
147
+ isCreateable: true,
148
+ isEditable: false,
149
+ isDeleteable: false,
150
+ isViewable: false,
151
+ callbacks: callbacks,
152
+ ...(options.createVerb ? { createVerb: options.createVerb } : {}),
153
+ ...(options.singularName ? { singularName: options.singularName } : {}),
154
+ ...(options.onCreateClick
155
+ ? { onCreateClick: options.onCreateClick }
156
+ : {}),
157
+ } as unknown as BaseModelTableProps<Monitor>;
158
+ };
159
+
160
+ type CardButtonTextsFunction = () => Array<string>;
161
+
162
+ const cardButtonTexts: CardButtonTextsFunction = (): Array<string> => {
163
+ return Array.from(
164
+ document.querySelectorAll<HTMLElement>('[data-testid="card-button"]'),
165
+ ).map((button: HTMLElement): string => {
166
+ return (button.textContent || "").trim();
167
+ });
168
+ };
169
+
170
+ type RenderedTitleFunction = (options: TableOptions) => Promise<string>;
171
+
172
+ /* The header's first card button is the create button. */
173
+ const renderedTitle: RenderedTitleFunction = async (
174
+ options: TableOptions,
175
+ ): Promise<string> => {
176
+ render(<BaseModelTable<Monitor> {...makeProps(options)} />);
177
+
178
+ await waitFor(() => {
179
+ expect(cardButtonTexts().length).toBeGreaterThan(0);
180
+ });
181
+
182
+ return cardButtonTexts()[0]!;
183
+ };
184
+
185
+ beforeEach(() => {
186
+ dictionary = {};
187
+ lookedUp = [];
188
+ PermissionGate.clearPermissionPropsCache();
189
+ window.history.replaceState(window.history.state, "", "/dashboard/things");
190
+ TableFilterUrlState.resetClaimedKeys();
191
+ window.localStorage.clear();
192
+ });
193
+
194
+ afterEach(() => {
195
+ cleanup();
196
+ jest.restoreAllMocks();
197
+ });
198
+
199
+ test("uses the whole phrase's translation when there is one", async () => {
200
+ dictionary = {
201
+ "Link Alert": "Alarm verknüpfen",
202
+ Link: "Verknüpfung",
203
+ Alert: "Alarm",
204
+ };
205
+
206
+ expect(
207
+ await renderedTitle({ createVerb: "Link", singularName: "Alert" }),
208
+ ).toBe("Alarm verknüpfen");
209
+ expect(lookedUp).toContain("Link Alert");
210
+ });
211
+
212
+ test("falls back to word by word when the phrase has no translation", async () => {
213
+ dictionary = {
214
+ Invite: "Einladen",
215
+ Member: "Mitglied",
216
+ };
217
+
218
+ expect(
219
+ await renderedTitle({ createVerb: "Invite", singularName: "Member" }),
220
+ ).toBe("Einladen Mitglied");
221
+ });
222
+
223
+ test("reads the same in English, where the phrase maps to itself", async () => {
224
+ dictionary = {
225
+ "Link Alert": "Link Alert",
226
+ Link: "Link",
227
+ Alert: "Alert",
228
+ };
229
+
230
+ expect(
231
+ await renderedTitle({ createVerb: "Link", singularName: "Alert" }),
232
+ ).toBe("Link Alert");
233
+ });
234
+
235
+ test("is unchanged with no translations at all", async () => {
236
+ expect(
237
+ await renderedTitle({ createVerb: "Link", singularName: "Alert" }),
238
+ ).toBe("Link Alert");
239
+ });
240
+
241
+ test("defaults the verb to Create and the noun to the model's name", async () => {
242
+ dictionary = {
243
+ "Create Monitor": "Monitor erstellen",
244
+ };
245
+
246
+ expect(await renderedTitle({})).toBe("Monitor erstellen");
247
+ });
248
+
249
+ test("defaults still translate word by word without a phrase entry", async () => {
250
+ dictionary = {
251
+ Create: "Erstellen",
252
+ };
253
+
254
+ expect(await renderedTitle({})).toBe("Erstellen Monitor");
255
+ });
256
+
257
+ test("still opens the page's own create flow", async () => {
258
+ const onCreateClick: MockFunction = getJestMockFunction();
259
+ dictionary = {
260
+ "Link Alert": "Alarm verknüpfen",
261
+ };
262
+
263
+ await renderedTitle({
264
+ createVerb: "Link",
265
+ singularName: "Alert",
266
+ onCreateClick: onCreateClick as unknown as () => void,
267
+ });
268
+
269
+ fireEvent.click(
270
+ document.querySelector<HTMLElement>('[data-testid="card-button"]')!,
271
+ );
272
+
273
+ expect(onCreateClick).toHaveBeenCalledTimes(1);
274
+ });
275
+ });