@oneuptime/common 12.0.1 → 12.0.2

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 (205) hide show
  1. package/Models/DatabaseModels/Host.ts +12 -4
  2. package/Models/DatabaseModels/KubernetesContainer.ts +5 -4
  3. package/Models/DatabaseModels/KubernetesResource.ts +8 -7
  4. package/Models/DatabaseModels/PodmanResource.ts +6 -6
  5. package/Models/DatabaseModels/ServiceLevelObjective.ts +91 -0
  6. package/Models/DatabaseModels/StatusPage.ts +95 -0
  7. package/Server/API/TeamMemberAPI.ts +85 -0
  8. package/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.ts +84 -0
  9. package/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.ts +40 -0
  10. package/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.ts +131 -0
  11. package/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.ts +25 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
  13. package/Server/Middleware/ProjectAuthorization.ts +66 -4
  14. package/Server/Services/BillingService.ts +600 -51
  15. package/Server/Services/CephClusterService.ts +47 -5
  16. package/Server/Services/CephResourceService.ts +43 -4
  17. package/Server/Services/CloudResourceService.ts +47 -5
  18. package/Server/Services/DatabaseService.ts +87 -6
  19. package/Server/Services/DockerHostService.ts +47 -5
  20. package/Server/Services/DockerResourceService.ts +62 -8
  21. package/Server/Services/DockerSwarmClusterService.ts +48 -5
  22. package/Server/Services/DockerSwarmResourceService.ts +53 -4
  23. package/Server/Services/HostService.ts +48 -5
  24. package/Server/Services/IoTDeviceService.ts +1 -17
  25. package/Server/Services/IoTFleetService.ts +47 -5
  26. package/Server/Services/KubernetesClusterService.ts +48 -5
  27. package/Server/Services/KubernetesContainerService.ts +50 -9
  28. package/Server/Services/KubernetesResourceService.ts +53 -7
  29. package/Server/Services/LabelService.ts +106 -1
  30. package/Server/Services/MonitorService.ts +57 -0
  31. package/Server/Services/PodmanHostService.ts +47 -5
  32. package/Server/Services/PodmanResourceService.ts +62 -8
  33. package/Server/Services/ProjectService.ts +165 -35
  34. package/Server/Services/ProxmoxClusterService.ts +47 -5
  35. package/Server/Services/ProxmoxResourceService.ts +43 -4
  36. package/Server/Services/RumApplicationService.ts +48 -5
  37. package/Server/Services/ServerlessFunctionService.ts +48 -5
  38. package/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.ts +426 -0
  39. package/Server/Services/ServiceLevelObjectiveService.ts +39 -0
  40. package/Server/Services/StatusPageService.ts +95 -36
  41. package/Server/Services/StatusPageSubscriberNotificationTemplateService.ts +18 -1
  42. package/Server/Services/StatusPageSubscriberService.ts +3 -0
  43. package/Server/Types/Database/JSONColumnQuery.ts +621 -0
  44. package/Server/Types/Database/Permissions/PublicPermission.ts +9 -2
  45. package/Server/Types/Database/QueryHelper.ts +17 -32
  46. package/Server/Utils/Database/TruncateColumnValue.ts +84 -0
  47. package/Tests/App/Dashboard/UsersTableGroupsByPerson.test.tsx +448 -0
  48. package/Tests/Server/API/BaseAPIApiKeyAuth.test.ts +704 -0
  49. package/Tests/Server/API/TeamMemberRemoveUserFromProjectAPI.test.ts +303 -0
  50. package/Tests/Server/Middleware/MasterAdminAuthorization.test.ts +350 -0
  51. package/Tests/Server/Middleware/ProjectAuthorizationApiKeyHeader.test.ts +362 -0
  52. package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +687 -0
  53. package/Tests/Server/Services/BillingService.test.ts +78 -3
  54. package/Tests/Server/Services/BillingServiceChangePlanCancel.test.ts +645 -0
  55. package/Tests/Server/Services/BillingServiceTrialPlanChange.test.ts +1005 -0
  56. package/Tests/Server/Services/HookFreeWriteLengthClamp.test.ts +503 -0
  57. package/Tests/Server/Services/HostIpAddressesColumnWidth.test.ts +198 -0
  58. package/Tests/Server/Services/HostServiceUpdateLastSeen.test.ts +510 -0
  59. package/Tests/Server/Services/InventoryResourceNameColumnWidth.test.ts +500 -0
  60. package/Tests/Server/Services/InventoryUpsertSchemaParity.test.ts +586 -0
  61. package/Tests/Server/Services/InventoryUpsertTruncationGuard.test.ts +1242 -0
  62. package/Tests/Server/Services/ProjectServiceChangePlan.test.ts +650 -0
  63. package/Tests/Server/Services/ProjectServiceExtendTrial.test.ts +1 -0
  64. package/Tests/Server/Services/ResourceUpdateLastSeenLivenessFallback.test.ts +387 -0
  65. package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.test.ts +770 -0
  66. package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +130 -0
  67. package/Tests/Server/Services/SloMonitorLabelRuleHooks.test.ts +352 -0
  68. package/Tests/Server/Services/StatusPageSubscriberReport.test.ts +29 -13
  69. package/Tests/Server/TestingUtils/Services/BillingServiceHelper.ts +69 -8
  70. package/Tests/Server/Types/Database/JSONColumnQuery.test.ts +593 -0
  71. package/Tests/Server/Types/Database/Permissions/PublicPermission.test.ts +495 -0
  72. package/Tests/Server/Types/Database/QueryUtil.test.ts +113 -0
  73. package/Tests/Server/Utils/Database/TruncateColumnValue.test.ts +179 -0
  74. package/Tests/Types/Events/Recurring.test.ts +157 -0
  75. package/Tests/UI/Components/ModelTable/ModelTableGroupsProjectUsers.test.tsx +375 -0
  76. package/Tests/UI/Components/ModelTable/useCustomFieldColumns.test.tsx +196 -0
  77. package/Tests/UI/Utils/ProjectUsersModelAPI.test.ts +739 -0
  78. package/Tests/UI/Utils/TeamMembersByUser.test.ts +633 -0
  79. package/Tests/Utils/StatusPage/ReportPeriod.test.ts +218 -0
  80. package/Tests/Utils/Telemetry/HostIpAddresses.test.ts +291 -0
  81. package/Types/CustomField/CustomFieldDefinition.ts +24 -0
  82. package/Types/Date.ts +112 -0
  83. package/Types/Events/Recurring.ts +99 -1
  84. package/Types/StatusPage/StatusPageReport.ts +11 -0
  85. package/Types/StatusPage/StatusPageReportPeriodType.ts +21 -0
  86. package/UI/Components/ModelTable/CustomFieldColumns.tsx +2 -6
  87. package/UI/Components/ModelTable/useCustomFieldColumns.ts +21 -2
  88. package/UI/Utils/ModelAPI/ProjectUsersModelAPI.ts +305 -0
  89. package/UI/Utils/TeamMembersByUser.ts +302 -0
  90. package/Utils/StatusPage/ReportPeriod.ts +236 -0
  91. package/Utils/Telemetry/HostIpAddresses.ts +98 -0
  92. package/build/dist/Models/DatabaseModels/Host.js +12 -4
  93. package/build/dist/Models/DatabaseModels/Host.js.map +1 -1
  94. package/build/dist/Models/DatabaseModels/KubernetesContainer.js +4 -4
  95. package/build/dist/Models/DatabaseModels/KubernetesContainer.js.map +1 -1
  96. package/build/dist/Models/DatabaseModels/KubernetesResource.js +7 -7
  97. package/build/dist/Models/DatabaseModels/KubernetesResource.js.map +1 -1
  98. package/build/dist/Models/DatabaseModels/PodmanResource.js +6 -6
  99. package/build/dist/Models/DatabaseModels/PodmanResource.js.map +1 -1
  100. package/build/dist/Models/DatabaseModels/ServiceLevelObjective.js +87 -0
  101. package/build/dist/Models/DatabaseModels/ServiceLevelObjective.js.map +1 -1
  102. package/build/dist/Models/DatabaseModels/StatusPage.js +98 -0
  103. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  104. package/build/dist/Server/API/TeamMemberAPI.js +57 -2
  105. package/build/dist/Server/API/TeamMemberAPI.js.map +1 -1
  106. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.js +45 -0
  107. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.js.map +1 -0
  108. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.js +31 -0
  109. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.js.map +1 -0
  110. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.js +90 -0
  111. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.js.map +1 -0
  112. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.js +14 -0
  113. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.js.map +1 -0
  114. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
  115. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  116. package/build/dist/Server/Middleware/ProjectAuthorization.js +59 -4
  117. package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
  118. package/build/dist/Server/Services/BillingService.js +466 -36
  119. package/build/dist/Server/Services/BillingService.js.map +1 -1
  120. package/build/dist/Server/Services/CephClusterService.js +42 -5
  121. package/build/dist/Server/Services/CephClusterService.js.map +1 -1
  122. package/build/dist/Server/Services/CephResourceService.js +28 -3
  123. package/build/dist/Server/Services/CephResourceService.js.map +1 -1
  124. package/build/dist/Server/Services/CloudResourceService.js +42 -5
  125. package/build/dist/Server/Services/CloudResourceService.js.map +1 -1
  126. package/build/dist/Server/Services/DatabaseService.js +69 -2
  127. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  128. package/build/dist/Server/Services/DockerHostService.js +42 -5
  129. package/build/dist/Server/Services/DockerHostService.js.map +1 -1
  130. package/build/dist/Server/Services/DockerResourceService.js +34 -4
  131. package/build/dist/Server/Services/DockerResourceService.js.map +1 -1
  132. package/build/dist/Server/Services/DockerSwarmClusterService.js +43 -5
  133. package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
  134. package/build/dist/Server/Services/DockerSwarmResourceService.js +28 -3
  135. package/build/dist/Server/Services/DockerSwarmResourceService.js.map +1 -1
  136. package/build/dist/Server/Services/HostService.js +43 -5
  137. package/build/dist/Server/Services/HostService.js.map +1 -1
  138. package/build/dist/Server/Services/IoTDeviceService.js +1 -8
  139. package/build/dist/Server/Services/IoTDeviceService.js.map +1 -1
  140. package/build/dist/Server/Services/IoTFleetService.js +42 -5
  141. package/build/dist/Server/Services/IoTFleetService.js.map +1 -1
  142. package/build/dist/Server/Services/KubernetesClusterService.js +43 -5
  143. package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
  144. package/build/dist/Server/Services/KubernetesContainerService.js +29 -3
  145. package/build/dist/Server/Services/KubernetesContainerService.js.map +1 -1
  146. package/build/dist/Server/Services/KubernetesResourceService.js +32 -4
  147. package/build/dist/Server/Services/KubernetesResourceService.js.map +1 -1
  148. package/build/dist/Server/Services/LabelService.js +92 -0
  149. package/build/dist/Server/Services/LabelService.js.map +1 -1
  150. package/build/dist/Server/Services/MonitorService.js +47 -0
  151. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  152. package/build/dist/Server/Services/PodmanHostService.js +42 -5
  153. package/build/dist/Server/Services/PodmanHostService.js.map +1 -1
  154. package/build/dist/Server/Services/PodmanResourceService.js +34 -4
  155. package/build/dist/Server/Services/PodmanResourceService.js.map +1 -1
  156. package/build/dist/Server/Services/ProjectService.js +135 -21
  157. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  158. package/build/dist/Server/Services/ProxmoxClusterService.js +42 -5
  159. package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -1
  160. package/build/dist/Server/Services/ProxmoxResourceService.js +28 -3
  161. package/build/dist/Server/Services/ProxmoxResourceService.js.map +1 -1
  162. package/build/dist/Server/Services/RumApplicationService.js +43 -5
  163. package/build/dist/Server/Services/RumApplicationService.js.map +1 -1
  164. package/build/dist/Server/Services/ServerlessFunctionService.js +43 -5
  165. package/build/dist/Server/Services/ServerlessFunctionService.js.map +1 -1
  166. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js +347 -0
  167. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js.map +1 -0
  168. package/build/dist/Server/Services/ServiceLevelObjectiveService.js +33 -1
  169. package/build/dist/Server/Services/ServiceLevelObjectiveService.js.map +1 -1
  170. package/build/dist/Server/Services/StatusPageService.js +69 -46
  171. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  172. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js +17 -1
  173. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js.map +1 -1
  174. package/build/dist/Server/Services/StatusPageSubscriberService.js +3 -0
  175. package/build/dist/Server/Services/StatusPageSubscriberService.js.map +1 -1
  176. package/build/dist/Server/Types/Database/JSONColumnQuery.js +402 -0
  177. package/build/dist/Server/Types/Database/JSONColumnQuery.js.map +1 -0
  178. package/build/dist/Server/Types/Database/Permissions/PublicPermission.js +9 -2
  179. package/build/dist/Server/Types/Database/Permissions/PublicPermission.js.map +1 -1
  180. package/build/dist/Server/Types/Database/QueryHelper.js +17 -27
  181. package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
  182. package/build/dist/Server/Utils/Database/TruncateColumnValue.js +32 -0
  183. package/build/dist/Server/Utils/Database/TruncateColumnValue.js.map +1 -0
  184. package/build/dist/Types/CustomField/CustomFieldDefinition.js +2 -0
  185. package/build/dist/Types/CustomField/CustomFieldDefinition.js.map +1 -0
  186. package/build/dist/Types/Date.js +68 -0
  187. package/build/dist/Types/Date.js.map +1 -1
  188. package/build/dist/Types/Events/Recurring.js +63 -0
  189. package/build/dist/Types/Events/Recurring.js.map +1 -1
  190. package/build/dist/Types/StatusPage/StatusPageReportPeriodType.js +22 -0
  191. package/build/dist/Types/StatusPage/StatusPageReportPeriodType.js.map +1 -0
  192. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
  193. package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js +18 -2
  194. package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js.map +1 -1
  195. package/build/dist/UI/Utils/ModelAPI/ProjectUsersModelAPI.js +213 -0
  196. package/build/dist/UI/Utils/ModelAPI/ProjectUsersModelAPI.js.map +1 -0
  197. package/build/dist/UI/Utils/TeamMembersByUser.js +195 -0
  198. package/build/dist/UI/Utils/TeamMembersByUser.js.map +1 -0
  199. package/build/dist/Utils/StatusPage/ReportPeriod.js +131 -0
  200. package/build/dist/Utils/StatusPage/ReportPeriod.js.map +1 -0
  201. package/build/dist/Utils/Telemetry/HostIpAddresses.js +82 -0
  202. package/build/dist/Utils/Telemetry/HostIpAddresses.js.map +1 -0
  203. package/jest.config.json +2 -0
  204. package/package.json +1 -1
  205. package/tsconfig.json +12 -1
@@ -0,0 +1,375 @@
1
+ import "@testing-library/jest-dom";
2
+ import { render, screen, waitFor } from "@testing-library/react";
3
+ import React, { ReactElement } from "react";
4
+ import {
5
+ afterEach,
6
+ beforeEach,
7
+ describe,
8
+ expect,
9
+ jest,
10
+ test,
11
+ } from "@jest/globals";
12
+
13
+ /*
14
+ * The end-to-end shape of the fix, through the real table rather than around
15
+ * it: memberships in, one row per person out.
16
+ *
17
+ * The unit suites prove ProjectUsersModelAPI groups and pages correctly, and
18
+ * the page suite proves the page hands the table that API. Neither proves the
19
+ * two compose - that BaseModelTable renders what the override returns as rows,
20
+ * pages over people, and keeps the injected per-row fields intact all the way
21
+ * to the cell renderers. That composition is where the duplicate rows would
22
+ * come back, so it is asserted here on the rendered DOM.
23
+ */
24
+
25
+ jest.mock("../../../../UI/Utils/Permission", () => {
26
+ return {
27
+ __esModule: true,
28
+ default: {
29
+ getAllPermissions: () => {
30
+ return [];
31
+ },
32
+ getProjectPermissions: () => {
33
+ return [];
34
+ },
35
+ getGlobalPermissions: () => {
36
+ return [];
37
+ },
38
+ },
39
+ };
40
+ });
41
+
42
+ jest.mock("../../../../UI/Utils/User", () => {
43
+ return {
44
+ __esModule: true,
45
+ default: {
46
+ isMasterAdmin: () => {
47
+ return true;
48
+ },
49
+ getUserId: () => {
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) => {
62
+ return value;
63
+ },
64
+ translateValue: (value: unknown) => {
65
+ return value;
66
+ },
67
+ };
68
+ },
69
+ };
70
+ });
71
+
72
+ import ModelTable from "../../../../UI/Components/ModelTable/ModelTable";
73
+ import ModelAPI from "../../../../UI/Utils/ModelAPI/ModelAPI";
74
+ import ProjectUsersModelAPI from "../../../../UI/Utils/ModelAPI/ProjectUsersModelAPI";
75
+ import { ProjectUserRow } from "../../../../UI/Utils/TeamMembersByUser";
76
+ import FieldType from "../../../../UI/Components/Types/FieldType";
77
+ import Team from "../../../../Models/DatabaseModels/Team";
78
+ import TeamMember from "../../../../Models/DatabaseModels/TeamMember";
79
+ import User from "../../../../Models/DatabaseModels/User";
80
+ import Query from "../../../../Types/BaseDatabase/Query";
81
+ import Name from "../../../../Types/Name";
82
+ import ObjectID from "../../../../Types/ObjectID";
83
+
84
+ const PROJECT_ID: ObjectID = new ObjectID(
85
+ "00000000-0000-4000-8000-000000000001",
86
+ );
87
+
88
+ type MembershipSpec = {
89
+ id: string;
90
+ userId: string;
91
+ userName: string;
92
+ teamId: string;
93
+ teamName: string;
94
+ };
95
+
96
+ const buildMembership: (spec: MembershipSpec) => TeamMember = (
97
+ spec: MembershipSpec,
98
+ ): TeamMember => {
99
+ const teamMember: TeamMember = new TeamMember();
100
+
101
+ teamMember._id = spec.id;
102
+ teamMember.userId = new ObjectID(spec.userId);
103
+ teamMember.projectId = PROJECT_ID;
104
+ teamMember.hasAcceptedInvitation = true;
105
+
106
+ const user: User = new User();
107
+ user._id = spec.userId;
108
+ user.name = new Name(spec.userName);
109
+ teamMember.user = user;
110
+
111
+ teamMember.teamId = new ObjectID(spec.teamId);
112
+
113
+ const team: Team = new Team();
114
+ team._id = spec.teamId;
115
+ team.name = spec.teamName;
116
+ teamMember.team = team;
117
+
118
+ return teamMember;
119
+ };
120
+
121
+ /*
122
+ * The list from the bug report: Thomas Stock and Jason Apel each on two teams,
123
+ * so six membership rows for four people.
124
+ */
125
+ const REPORTED_MEMBERSHIPS: Array<MembershipSpec> = [
126
+ {
127
+ id: "m1",
128
+ userId: "thomas",
129
+ userName: "Thomas Stock",
130
+ teamId: "online",
131
+ teamName: "NTW_Online Operations",
132
+ },
133
+ {
134
+ id: "m2",
135
+ userId: "thomas",
136
+ userName: "Thomas Stock",
137
+ teamId: "owners",
138
+ teamName: "Owners",
139
+ },
140
+ {
141
+ id: "m3",
142
+ userId: "jason",
143
+ userName: "Jason Apel",
144
+ teamId: "owners",
145
+ teamName: "Owners",
146
+ },
147
+ {
148
+ id: "m4",
149
+ userId: "colton",
150
+ userName: "Colton Hawkins",
151
+ teamId: "online",
152
+ teamName: "NTW_Online Operations",
153
+ },
154
+ {
155
+ id: "m5",
156
+ userId: "karsten",
157
+ userName: "Karsten Huster",
158
+ teamId: "it",
159
+ teamName: "NTW_IT Operations",
160
+ },
161
+ {
162
+ id: "m6",
163
+ userId: "jason",
164
+ userName: "Jason Apel",
165
+ teamId: "online",
166
+ teamName: "NTW_Online Operations",
167
+ },
168
+ ];
169
+
170
+ type GetListCall = { limit: number; skip: number };
171
+
172
+ let getListCalls: Array<GetListCall> = [];
173
+
174
+ type RenderTableFunction = (memberships: Array<MembershipSpec>) => void;
175
+
176
+ const renderTable: RenderTableFunction = (
177
+ memberships: Array<MembershipSpec>,
178
+ ): void => {
179
+ jest
180
+ .spyOn(ModelAPI, "getList")
181
+ .mockImplementation(async (data: unknown): Promise<never> => {
182
+ const call: GetListCall = data as GetListCall;
183
+ getListCalls.push({ limit: call.limit, skip: call.skip });
184
+
185
+ return {
186
+ data: memberships.map(buildMembership),
187
+ count: memberships.length,
188
+ skip: 0,
189
+ limit: memberships.length,
190
+ } as unknown as never;
191
+ });
192
+
193
+ render(
194
+ <ModelTable<TeamMember>
195
+ modelType={TeamMember}
196
+ modelAPI={ProjectUsersModelAPI}
197
+ id="users-table"
198
+ userPreferencesKey="users-table-test"
199
+ name="Settings > Users"
200
+ singularName="User"
201
+ pluralName="Users"
202
+ isDeleteable={false}
203
+ isEditable={false}
204
+ isCreateable={false}
205
+ isViewable={false}
206
+ query={{ projectId: PROJECT_ID } as Query<TeamMember>}
207
+ cardProps={{ title: "Users", description: "Everyone in this project." }}
208
+ filters={[]}
209
+ columns={[
210
+ {
211
+ field: { user: { name: true } },
212
+ title: "User",
213
+ type: FieldType.Element,
214
+ getElement: (item: TeamMember): ReactElement => {
215
+ return <span>{item.user?.name?.toString() || ""}</span>;
216
+ },
217
+ },
218
+ {
219
+ field: { team: { name: true, _id: true } },
220
+ title: "Teams",
221
+ type: FieldType.Element,
222
+ getElement: (item: TeamMember): ReactElement => {
223
+ return (
224
+ <span>
225
+ {((item as ProjectUserRow).teamsForUser || [])
226
+ .map((team: Team) => {
227
+ return team.name?.toString() || "";
228
+ })
229
+ .join(", ")}
230
+ </span>
231
+ );
232
+ },
233
+ },
234
+ ]}
235
+ />,
236
+ );
237
+ };
238
+
239
+ type RowTextsFunction = () => Array<string>;
240
+
241
+ const rowTexts: RowTextsFunction = (): Array<string> => {
242
+ return screen.getAllByRole("row").map((row: HTMLElement) => {
243
+ return row.textContent || "";
244
+ });
245
+ };
246
+
247
+ describe("ModelTable driven by ProjectUsersModelAPI", () => {
248
+ beforeEach(() => {
249
+ getListCalls = [];
250
+ });
251
+
252
+ afterEach(() => {
253
+ jest.restoreAllMocks();
254
+ });
255
+
256
+ /*
257
+ * The regression that started all this. If this fails, Thomas Stock is on
258
+ * screen twice again.
259
+ */
260
+ test("renders one row per person, not one per membership", async () => {
261
+ renderTable(REPORTED_MEMBERSHIPS);
262
+
263
+ await waitFor(() => {
264
+ expect(screen.getByText("Thomas Stock")).toBeInTheDocument();
265
+ });
266
+
267
+ expect(screen.getAllByText("Thomas Stock")).toHaveLength(1);
268
+ expect(screen.getAllByText("Jason Apel")).toHaveLength(1);
269
+ expect(screen.getAllByText("Colton Hawkins")).toHaveLength(1);
270
+ expect(screen.getAllByText("Karsten Huster")).toHaveLength(1);
271
+ });
272
+
273
+ test("six memberships become four body rows", async () => {
274
+ renderTable(REPORTED_MEMBERSHIPS);
275
+
276
+ await waitFor(() => {
277
+ expect(screen.getByText("Thomas Stock")).toBeInTheDocument();
278
+ });
279
+
280
+ // The header row plus one row per person - not one per membership.
281
+ expect(rowTexts()).toHaveLength(5);
282
+ });
283
+
284
+ test("each person's row carries every team they belong to", async () => {
285
+ renderTable(REPORTED_MEMBERSHIPS);
286
+
287
+ await waitFor(() => {
288
+ expect(screen.getByText("Thomas Stock")).toBeInTheDocument();
289
+ });
290
+
291
+ const thomasRow: string =
292
+ rowTexts().find((text: string) => {
293
+ return text.includes("Thomas Stock");
294
+ }) || "";
295
+
296
+ expect(thomasRow).toContain("NTW_Online Operations");
297
+ expect(thomasRow).toContain("Owners");
298
+
299
+ const coltonRow: string =
300
+ rowTexts().find((text: string) => {
301
+ return text.includes("Colton Hawkins");
302
+ }) || "";
303
+
304
+ expect(coltonRow).toContain("NTW_Online Operations");
305
+ expect(coltonRow).not.toContain("Owners");
306
+ });
307
+
308
+ test("the rows are sorted by name, so paging is stable", async () => {
309
+ renderTable(REPORTED_MEMBERSHIPS);
310
+
311
+ await waitFor(() => {
312
+ expect(screen.getByText("Thomas Stock")).toBeInTheDocument();
313
+ });
314
+
315
+ const names: Array<string> = [
316
+ "Colton Hawkins",
317
+ "Jason Apel",
318
+ "Karsten Huster",
319
+ "Thomas Stock",
320
+ ];
321
+
322
+ const rendered: Array<string> = rowTexts()
323
+ .map((text: string) => {
324
+ return (
325
+ names.find((name: string) => {
326
+ return text.includes(name);
327
+ }) || ""
328
+ );
329
+ })
330
+ .filter(Boolean);
331
+
332
+ expect(rendered).toEqual(names);
333
+ });
334
+
335
+ test("the table reads the membership list itself rather than asking for a page of it", async () => {
336
+ renderTable(REPORTED_MEMBERSHIPS);
337
+
338
+ await waitFor(() => {
339
+ expect(screen.getByText("Thomas Stock")).toBeInTheDocument();
340
+ });
341
+
342
+ /*
343
+ * The table's own page size never reaches the server: the override has to
344
+ * see every membership before it can know which people are on page 1.
345
+ */
346
+ expect(getListCalls[0]!.skip).toBe(0);
347
+ expect(getListCalls[0]!.limit).toBeGreaterThan(REPORTED_MEMBERSHIPS.length);
348
+ });
349
+
350
+ test("a project where nobody is on two teams renders unchanged", async () => {
351
+ renderTable([
352
+ {
353
+ id: "m1",
354
+ userId: "a",
355
+ userName: "Aaron Falzon",
356
+ teamId: "t1",
357
+ teamName: "Ops",
358
+ },
359
+ {
360
+ id: "m2",
361
+ userId: "b",
362
+ userName: "Zoe Baker",
363
+ teamId: "t1",
364
+ teamName: "Ops",
365
+ },
366
+ ]);
367
+
368
+ await waitFor(() => {
369
+ expect(screen.getByText("Aaron Falzon")).toBeInTheDocument();
370
+ });
371
+
372
+ expect(screen.getAllByText("Aaron Falzon")).toHaveLength(1);
373
+ expect(screen.getAllByText("Zoe Baker")).toHaveLength(1);
374
+ });
375
+ });
@@ -0,0 +1,196 @@
1
+ import { afterEach, describe, expect, jest, test } from "@jest/globals";
2
+ import "@testing-library/jest-dom";
3
+ import { cleanup, render, waitFor } from "@testing-library/react";
4
+ import * as React from "react";
5
+ import getJestMockFunction, { MockFunction } from "../../../MockType";
6
+
7
+ /*
8
+ * `isLoading` used to start `false` and flip true inside the effect, which was
9
+ * fine while custom field definitions only ever added optional table columns:
10
+ * a column that appears late is just a column that appears late.
11
+ *
12
+ * The facet bar cannot read it that way. It restores its chips from the URL on
13
+ * the very first render, and a chip whose definition has not arrived yet is
14
+ * indistinguishable from a chip the user cleared — the difference being that
15
+ * the second means "erase the shared link's filter". So the first render has
16
+ * to be able to say "a request is coming", and these tests pin that.
17
+ */
18
+
19
+ const getListMock: MockFunction = getJestMockFunction();
20
+
21
+ /*
22
+ * The arrow wrappers are load bearing: jest.mock is hoisted above the compiled
23
+ * requires, so naming the mock directly would capture it before its
24
+ * initializer has run.
25
+ */
26
+ jest.mock("../../../../UI/Utils/ModelAPI/ModelAPI", () => {
27
+ return {
28
+ __esModule: true,
29
+ default: {
30
+ getList: (...args: Array<any>) => {
31
+ return getListMock(...args);
32
+ },
33
+ },
34
+ };
35
+ });
36
+
37
+ const getCurrentProjectIdMock: MockFunction = getJestMockFunction();
38
+
39
+ jest.mock("../../../../UI/Utils/Project", () => {
40
+ return {
41
+ __esModule: true,
42
+ default: {
43
+ getCurrentProjectId: (...args: Array<any>) => {
44
+ return getCurrentProjectIdMock(...args);
45
+ },
46
+ },
47
+ };
48
+ });
49
+
50
+ import useCustomFieldColumns, {
51
+ CustomFieldColumnsResult,
52
+ } from "../../../../UI/Components/ModelTable/useCustomFieldColumns";
53
+ import { CustomFieldDefinition } from "../../../../UI/Components/ModelTable/CustomFieldColumns";
54
+ import BaseModel from "../../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
55
+ import IncidentCustomField from "../../../../Models/DatabaseModels/IncidentCustomField";
56
+ import CustomFieldType from "../../../../Types/CustomField/CustomFieldType";
57
+ import ObjectID from "../../../../Types/ObjectID";
58
+
59
+ const PROJECT_ID: string = "11111111-1111-4111-8111-111111111111";
60
+
61
+ interface Snapshot {
62
+ isLoading: boolean;
63
+ definitionNames: Array<string>;
64
+ columnIds: Array<string>;
65
+ }
66
+
67
+ type HarnessPropsType = {
68
+ customFieldsModelType?: { new (): BaseModel } | undefined;
69
+ onRender: (snapshot: Snapshot) => void;
70
+ };
71
+
72
+ const Harness: React.FunctionComponent<HarnessPropsType> = (
73
+ props: HarnessPropsType,
74
+ ): React.ReactElement => {
75
+ const result: CustomFieldColumnsResult<BaseModel> =
76
+ useCustomFieldColumns<BaseModel>({
77
+ customFieldsModelType: props.customFieldsModelType,
78
+ });
79
+
80
+ props.onRender({
81
+ isLoading: result.isLoading,
82
+ definitionNames: result.definitions.map((d: CustomFieldDefinition) => {
83
+ return d.name;
84
+ }),
85
+ columnIds: result.columns.map((column: { id?: string | undefined }) => {
86
+ return column.id || "";
87
+ }),
88
+ });
89
+
90
+ return <div />;
91
+ };
92
+
93
+ type RenderHarnessFunction = (
94
+ customFieldsModelType?: { new (): BaseModel } | undefined,
95
+ ) => Array<Snapshot>;
96
+
97
+ const renderHarness: RenderHarnessFunction = (
98
+ customFieldsModelType?: { new (): BaseModel } | undefined,
99
+ ): Array<Snapshot> => {
100
+ const snapshots: Array<Snapshot> = [];
101
+
102
+ render(
103
+ <Harness
104
+ customFieldsModelType={customFieldsModelType}
105
+ onRender={(snapshot: Snapshot) => {
106
+ snapshots.push(snapshot);
107
+ }}
108
+ />,
109
+ );
110
+
111
+ return snapshots;
112
+ };
113
+
114
+ afterEach(() => {
115
+ cleanup();
116
+ getListMock.mockReset();
117
+ getCurrentProjectIdMock.mockReset();
118
+ });
119
+
120
+ describe("useCustomFieldColumns — isLoading on the first render", () => {
121
+ test("is true before the request has even been sent", () => {
122
+ getCurrentProjectIdMock.mockReturnValue(new ObjectID(PROJECT_ID));
123
+ getListMock.mockReturnValue(new Promise(() => {}));
124
+
125
+ const snapshots: Array<Snapshot> = renderHarness(IncidentCustomField);
126
+
127
+ expect(snapshots[0]!.isLoading).toBe(true);
128
+ });
129
+
130
+ test("is false when there is no definition model to load", () => {
131
+ /*
132
+ * Every table without custom fields renders this hook too. Reporting
133
+ * "loading" there would make the facet bar hold its URL snapshot hostage
134
+ * on pages that will never have a chip to wait for.
135
+ */
136
+ getCurrentProjectIdMock.mockReturnValue(new ObjectID(PROJECT_ID));
137
+
138
+ expect(renderHarness(undefined)[0]!.isLoading).toBe(false);
139
+ expect(getListMock).not.toHaveBeenCalled();
140
+ });
141
+
142
+ test("is false when there is no current project", () => {
143
+ getCurrentProjectIdMock.mockReturnValue(null);
144
+
145
+ expect(renderHarness(IncidentCustomField)[0]!.isLoading).toBe(false);
146
+ expect(getListMock).not.toHaveBeenCalled();
147
+ });
148
+ });
149
+
150
+ describe("useCustomFieldColumns — settling", () => {
151
+ test("goes false once the definitions land, and reports them sorted", () => {
152
+ getCurrentProjectIdMock.mockReturnValue(new ObjectID(PROJECT_ID));
153
+ getListMock.mockReturnValue(
154
+ Promise.resolve({
155
+ data: [
156
+ { name: "Team", customFieldType: CustomFieldType.Text },
157
+ { name: "Impacted Users", customFieldType: CustomFieldType.Number },
158
+ ],
159
+ }),
160
+ );
161
+
162
+ const snapshots: Array<Snapshot> = renderHarness(IncidentCustomField);
163
+
164
+ return waitFor(() => {
165
+ const last: Snapshot = snapshots[snapshots.length - 1]!;
166
+
167
+ expect(last.isLoading).toBe(false);
168
+ // Sorted, so a viewer's column layout does not shuffle between loads.
169
+ expect(last.definitionNames).toEqual(["Impacted Users", "Team"]);
170
+ expect(last.columnIds).toEqual([
171
+ "customFields.Impacted Users",
172
+ "customFields.Team",
173
+ ]);
174
+ });
175
+ });
176
+
177
+ test("goes false and yields nothing when the request fails", () => {
178
+ /*
179
+ * Custom fields are a paid feature the viewer may not be able to read, so
180
+ * a failure here is expected in normal operation. It must settle rather
181
+ * than leave every caller waiting forever.
182
+ */
183
+ getCurrentProjectIdMock.mockReturnValue(new ObjectID(PROJECT_ID));
184
+ getListMock.mockReturnValue(Promise.reject(new Error("no permission")));
185
+
186
+ const snapshots: Array<Snapshot> = renderHarness(IncidentCustomField);
187
+
188
+ return waitFor(() => {
189
+ const last: Snapshot = snapshots[snapshots.length - 1]!;
190
+
191
+ expect(last.isLoading).toBe(false);
192
+ expect(last.definitionNames).toEqual([]);
193
+ expect(last.columnIds).toEqual([]);
194
+ });
195
+ });
196
+ });