@oneuptime/common 12.0.28 → 12.0.29

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 (182) hide show
  1. package/Models/DatabaseModels/DashboardDomain.ts +43 -0
  2. package/Models/DatabaseModels/MonitorTemplate.ts +15 -3
  3. package/Models/DatabaseModels/StatusPageDomain.ts +43 -0
  4. package/Server/API/DashboardDomainAPI.ts +74 -0
  5. package/Server/API/MicrosoftTeamsAPI.ts +81 -180
  6. package/Server/API/StatusPageDomainAPI.ts +74 -0
  7. package/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.ts +43 -0
  8. package/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.ts +40 -0
  9. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  10. package/Server/Middleware/ProjectAuthorization.ts +6 -1
  11. package/Server/Services/DashboardDomainService.ts +131 -0
  12. package/Server/Services/LogPipelineProcessorService.ts +88 -0
  13. package/Server/Services/MonitorTemplateService.ts +70 -11
  14. package/Server/Services/StatusPageDomainService.ts +163 -3
  15. package/Server/Services/StatusPagePrivateUserSessionService.ts +197 -56
  16. package/Server/Types/Domain.ts +17 -0
  17. package/Server/Utils/APIKey/AccessPermission.ts +24 -0
  18. package/Server/Utils/Greenlock/Greenlock.ts +156 -58
  19. package/Server/Utils/LogPipelineProcessorValidation.ts +112 -0
  20. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.ts +1094 -45
  21. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.ts +153 -17
  22. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.ts +23 -11
  23. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.ts +0 -170
  24. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.ts +158 -0
  25. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.ts +23 -11
  26. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +35 -13
  27. package/Tests/App/Dashboard/MonitorTemplateColumnAndFacet.test.tsx +505 -0
  28. package/Tests/App/StatusPage/StatusPageLoginCodeBootstrap.test.ts +106 -0
  29. package/Tests/App/StatusPage/StatusPageLoginCodeUtil.test.ts +84 -0
  30. package/Tests/App/StatusPage/StatusPageRedirectSafety.test.ts +119 -0
  31. package/Tests/Models/MonitorTemplateMonitorNameColumn.test.ts +258 -0
  32. package/Tests/Server/API/CustomDomainReissueSslAPI.test.ts +469 -0
  33. package/Tests/Server/API/MicrosoftTeamsBotMessagesEndpoint.test.ts +729 -0
  34. package/Tests/Server/API/MicrosoftTeamsBotTestEndpoint.test.ts +26 -0
  35. package/Tests/Server/API/MicrosoftTeamsLegacyWebhookRemoval.test.ts +402 -0
  36. package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +4 -0
  37. package/Tests/Server/Services/AddCertificateReissueRequestedAtToDomainsMigration.test.ts +287 -0
  38. package/Tests/Server/Services/CustomDomainCertificateReissue.test.ts +477 -0
  39. package/Tests/Server/Services/LogPipelineProcessorSaveValidation.test.ts +249 -0
  40. package/Tests/Server/Services/MonitorTemplateServiceBulkSync.test.ts +549 -0
  41. package/Tests/Server/Services/MonitorTemplateServiceNetworkDeviceSync.test.ts +16 -3
  42. package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +57 -0
  43. package/Tests/Server/Services/SecurityEventLastErrorColumnWidth.test.ts +151 -15
  44. package/Tests/Server/Services/StatusPagePrivateUserSessionService.test.ts +587 -0
  45. package/Tests/Server/Types/Database/Permissions/ApiKeyFileAccess.test.ts +286 -0
  46. package/Tests/Server/Types/Database/Permissions/MonitorTemplateColumnAccess.test.ts +328 -0
  47. package/Tests/Server/Types/Database/QueryHelperFindWithSameTextAndIsNull.test.ts +83 -0
  48. package/Tests/Server/Utils/AI/SRE/ConfidenceSignal.test.ts +377 -0
  49. package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +16 -1
  50. package/Tests/Server/Utils/Greenlock/CertificateRenewalSchedule.test.ts +250 -0
  51. package/Tests/Server/Utils/LogPipelineProcessorValidation.test.ts +198 -0
  52. package/Tests/Server/Utils/SecurityEvent/ConnectorErrorMessage.test.ts +48 -9
  53. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsAuth.test.ts +726 -0
  54. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsClient.test.ts +74 -11
  55. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsErrorHandling.test.ts +1107 -0
  56. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPoller.test.ts +20 -3
  57. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerFailureTaxonomy.test.ts +51 -11
  58. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerHardening.test.ts +1246 -0
  59. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsRequestContract.test.ts +961 -0
  60. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsResponseParsing.test.ts +653 -0
  61. package/Tests/Server/Utils/Workspace/MicrosoftTeamsActionAuthorization.test.ts +880 -0
  62. package/Tests/Types/API/HTTPResponse.test.ts +175 -0
  63. package/Tests/Types/API/Route.test.ts +36 -4
  64. package/Tests/Types/Database/DatabaseProperty.test.ts +171 -0
  65. package/Tests/Types/DateLocalShortDateTimeString.test.ts +518 -0
  66. package/Tests/Types/DateLocalTimeString.test.ts +259 -0
  67. package/Tests/Types/DateUserPrefers12HourFormat.test.ts +316 -0
  68. package/Tests/UI/Components/LogsHistogram.test.tsx +41 -2
  69. package/Tests/UI/Components/LogsViewerEmptyStateTimeRange.test.tsx +212 -0
  70. package/Tests/UI/Components/TelemetryChartClockFormat.test.tsx +359 -0
  71. package/Tests/UI/Components/TimeRangePickerClockFormat.test.tsx +422 -0
  72. package/Tests/UI/Components/TimeRangePickerDropdown.test.tsx +146 -8
  73. package/Tests/UI/MicrosoftTeams/MicrosoftTeamsMessagingEndpointGuidance.test.tsx +520 -0
  74. package/Tests/UI/Utils/Navigation.test.ts +82 -0
  75. package/Tests/Utils/Array.test.ts +100 -0
  76. package/Tests/Utils/CertificateReissue.test.ts +225 -0
  77. package/Tests/Utils/Grok/Grok.test.ts +492 -0
  78. package/Tests/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.test.ts +181 -13
  79. package/Types/API/Route.ts +20 -5
  80. package/Types/Date.ts +143 -13
  81. package/Types/Rum/SessionReplay.ts +62 -1
  82. package/UI/Components/Date/TimeRangePickerDropdown.tsx +9 -5
  83. package/UI/Components/LogsViewer/LogsViewer.tsx +12 -7
  84. package/UI/Components/LogsViewer/components/HistogramTooltip.tsx +18 -17
  85. package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +21 -28
  86. package/UI/Components/LogsViewer/components/LogsHistogram.tsx +2 -4
  87. package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +2 -4
  88. package/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.tsx +17 -16
  89. package/UI/Utils/Navigation.ts +42 -0
  90. package/Utils/Array.ts +37 -0
  91. package/Utils/CertificateReissue.ts +128 -0
  92. package/Utils/Grok/Grok.ts +461 -0
  93. package/Utils/Grok/GrokPatterns.ts +118 -0
  94. package/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.ts +58 -6
  95. package/build/dist/Models/DatabaseModels/DashboardDomain.js +44 -0
  96. package/build/dist/Models/DatabaseModels/DashboardDomain.js.map +1 -1
  97. package/build/dist/Models/DatabaseModels/MonitorTemplate.js +17 -4
  98. package/build/dist/Models/DatabaseModels/MonitorTemplate.js.map +1 -1
  99. package/build/dist/Models/DatabaseModels/StatusPageDomain.js +44 -0
  100. package/build/dist/Models/DatabaseModels/StatusPageDomain.js.map +1 -1
  101. package/build/dist/Server/API/DashboardDomainAPI.js +43 -1
  102. package/build/dist/Server/API/DashboardDomainAPI.js.map +1 -1
  103. package/build/dist/Server/API/MicrosoftTeamsAPI.js +74 -118
  104. package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
  105. package/build/dist/Server/API/StatusPageDomainAPI.js +43 -1
  106. package/build/dist/Server/API/StatusPageDomainAPI.js.map +1 -1
  107. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.js +34 -0
  108. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.js.map +1 -0
  109. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.js +28 -0
  110. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.js.map +1 -0
  111. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  112. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  113. package/build/dist/Server/Middleware/ProjectAuthorization.js +6 -1
  114. package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
  115. package/build/dist/Server/Services/DashboardDomainService.js +104 -0
  116. package/build/dist/Server/Services/DashboardDomainService.js.map +1 -1
  117. package/build/dist/Server/Services/LogPipelineProcessorService.js +65 -0
  118. package/build/dist/Server/Services/LogPipelineProcessorService.js.map +1 -1
  119. package/build/dist/Server/Services/MonitorTemplateService.js +57 -11
  120. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  121. package/build/dist/Server/Services/StatusPageDomainService.js +132 -3
  122. package/build/dist/Server/Services/StatusPageDomainService.js.map +1 -1
  123. package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js +112 -32
  124. package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js.map +1 -1
  125. package/build/dist/Server/Types/Domain.js +16 -0
  126. package/build/dist/Server/Types/Domain.js.map +1 -1
  127. package/build/dist/Server/Utils/APIKey/AccessPermission.js +24 -0
  128. package/build/dist/Server/Utils/APIKey/AccessPermission.js.map +1 -1
  129. package/build/dist/Server/Utils/Greenlock/Greenlock.js +110 -35
  130. package/build/dist/Server/Utils/Greenlock/Greenlock.js.map +1 -1
  131. package/build/dist/Server/Utils/LogPipelineProcessorValidation.js +77 -0
  132. package/build/dist/Server/Utils/LogPipelineProcessorValidation.js.map +1 -0
  133. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js +777 -36
  134. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js.map +1 -1
  135. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js +108 -17
  136. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js.map +1 -1
  137. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js +17 -6
  138. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js.map +1 -1
  139. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js +0 -159
  140. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js.map +1 -1
  141. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.js +128 -0
  142. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.js.map +1 -0
  143. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js +17 -6
  144. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js.map +1 -1
  145. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +25 -13
  146. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  147. package/build/dist/Types/API/Route.js +16 -5
  148. package/build/dist/Types/API/Route.js.map +1 -1
  149. package/build/dist/Types/Date.js +115 -13
  150. package/build/dist/Types/Date.js.map +1 -1
  151. package/build/dist/Types/Rum/SessionReplay.js +38 -0
  152. package/build/dist/Types/Rum/SessionReplay.js.map +1 -1
  153. package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js +9 -5
  154. package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js.map +1 -1
  155. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +12 -7
  156. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  157. package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js +15 -14
  158. package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js.map +1 -1
  159. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +17 -22
  160. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
  161. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +2 -4
  162. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -1
  163. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +2 -4
  164. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
  165. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js +15 -14
  166. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js.map +1 -1
  167. package/build/dist/UI/Utils/Navigation.js +30 -0
  168. package/build/dist/UI/Utils/Navigation.js.map +1 -1
  169. package/build/dist/Utils/Array.js +25 -0
  170. package/build/dist/Utils/Array.js.map +1 -1
  171. package/build/dist/Utils/CertificateReissue.js +97 -0
  172. package/build/dist/Utils/CertificateReissue.js.map +1 -0
  173. package/build/dist/Utils/Grok/Grok.js +324 -0
  174. package/build/dist/Utils/Grok/Grok.js.map +1 -0
  175. package/build/dist/Utils/Grok/GrokPatterns.js +112 -0
  176. package/build/dist/Utils/Grok/GrokPatterns.js.map +1 -0
  177. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js +63 -3
  178. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js.map +1 -1
  179. package/package.json +1 -1
  180. package/UI/Utils/JsonWebToken.ts +0 -14
  181. package/build/dist/UI/Utils/JsonWebToken.js +0 -10
  182. package/build/dist/UI/Utils/JsonWebToken.js.map +0 -1
@@ -31,6 +31,8 @@ import SortOrder from "../../../../../Types/BaseDatabase/SortOrder";
31
31
  import { LIMIT_PER_PROJECT } from "../../../../../Types/Database/LimitMax";
32
32
  import BadDataException from "../../../../../Types/Exception/BadDataException";
33
33
  import URL from "../../../../../Types/API/URL";
34
+ import DatabaseCommonInteractionProps from "../../../../../Types/BaseDatabase/DatabaseCommonInteractionProps";
35
+ import MicrosoftTeamsActionAuthorization from "./Authorization";
34
36
 
35
37
  export default class MicrosoftTeamsIncidentActions {
36
38
  @CaptureSpan()
@@ -296,6 +298,7 @@ export default class MicrosoftTeamsIncidentActions {
296
298
  value: JSONObject;
297
299
  projectId: ObjectID;
298
300
  oneUptimeUserId: ObjectID;
301
+ databaseProps: DatabaseCommonInteractionProps;
299
302
  turnContext: TurnContext;
300
303
  }): Promise<void> {
301
304
  const {
@@ -304,6 +307,7 @@ export default class MicrosoftTeamsIncidentActions {
304
307
  value,
305
308
  projectId,
306
309
  oneUptimeUserId,
310
+ databaseProps,
307
311
  turnContext,
308
312
  } = data;
309
313
 
@@ -315,10 +319,15 @@ export default class MicrosoftTeamsIncidentActions {
315
319
  return;
316
320
  }
317
321
 
318
- await IncidentService.acknowledgeIncident(
319
- new ObjectID(actionValue),
320
- oneUptimeUserId,
321
- );
322
+ const incidentId: ObjectID = new ObjectID(actionValue);
323
+
324
+ await MicrosoftTeamsActionAuthorization.assertCanUpdateIncident({
325
+ incidentId: incidentId,
326
+ projectId: projectId,
327
+ props: databaseProps,
328
+ });
329
+
330
+ await IncidentService.acknowledgeIncident(incidentId, oneUptimeUserId);
322
331
  await turnContext.sendActivity("✅ Incident acknowledged.");
323
332
  return;
324
333
  }
@@ -331,10 +340,15 @@ export default class MicrosoftTeamsIncidentActions {
331
340
  return;
332
341
  }
333
342
 
334
- await IncidentService.resolveIncident(
335
- new ObjectID(actionValue),
336
- oneUptimeUserId,
337
- );
343
+ const incidentId: ObjectID = new ObjectID(actionValue);
344
+
345
+ await MicrosoftTeamsActionAuthorization.assertCanUpdateIncident({
346
+ incidentId: incidentId,
347
+ projectId: projectId,
348
+ props: databaseProps,
349
+ });
350
+
351
+ await IncidentService.resolveIncident(incidentId, oneUptimeUserId);
338
352
  await turnContext.sendActivity("✅ Incident resolved.");
339
353
  return;
340
354
  }
@@ -574,9 +588,7 @@ export default class MicrosoftTeamsIncidentActions {
574
588
  data: {
575
589
  currentIncidentStateId: new ObjectID(incidentStateId.toString()),
576
590
  },
577
- props: {
578
- isRoot: true,
579
- },
591
+ props: databaseProps,
580
592
  });
581
593
 
582
594
  await turnContext.sendActivity(
@@ -111,6 +111,7 @@ import AccessTokenService from "../../../Services/AccessTokenService";
111
111
  import AIService, { AI_DISABLED_MESSAGE } from "../../../Services/AIService";
112
112
  import DatabaseCommonInteractionProps from "../../../../Types/BaseDatabase/DatabaseCommonInteractionProps";
113
113
  import { AIChatCitation } from "../../../../Types/AI/AIChatTypes";
114
+ import TeamMemberService from "../../../Services/TeamMemberService";
114
115
 
115
116
  // Microsoft Teams apps should always be single-tenant
116
117
  const MICROSOFT_TEAMS_APP_TYPE: string = "SingleTenant";
@@ -3999,27 +4000,48 @@ All monitoring checks are passing normally.`;
3999
4000
  userLookupParamsRes,
4000
4001
  );
4001
4002
 
4002
- // Handle incident actions
4003
- if (MicrosoftTeamsIncidentActions.isIncidentAction({ actionType })) {
4004
- await MicrosoftTeamsIncidentActions.handleBotIncidentAction({
4005
- actionType,
4006
- actionValue,
4007
- value,
4008
- projectId,
4003
+ const isIncidentAction: boolean =
4004
+ MicrosoftTeamsIncidentActions.isIncidentAction({ actionType });
4005
+ const isAlertAction: boolean = MicrosoftTeamsAlertActions.isAlertAction({
4006
+ actionType,
4007
+ });
4008
+
4009
+ if (isIncidentAction || isAlertAction) {
4010
+ const databaseProps: DatabaseCommonInteractionProps =
4011
+ await AccessTokenService.getDatabaseCommonInteractionPropsByUserAndProject(
4012
+ {
4013
+ userId: oneUptimeUserId,
4014
+ projectId: projectId,
4015
+ },
4016
+ );
4017
+
4018
+ databaseProps.userTeamIds = await TeamMemberService.getTeamIdsForUser(
4009
4019
  oneUptimeUserId,
4010
- turnContext: data.turnContext,
4011
- });
4012
- return;
4013
- }
4020
+ projectId,
4021
+ );
4022
+
4023
+ // Handle incident actions
4024
+ if (isIncidentAction) {
4025
+ await MicrosoftTeamsIncidentActions.handleBotIncidentAction({
4026
+ actionType,
4027
+ actionValue,
4028
+ value,
4029
+ projectId,
4030
+ oneUptimeUserId,
4031
+ databaseProps,
4032
+ turnContext: data.turnContext,
4033
+ });
4034
+ return;
4035
+ }
4014
4036
 
4015
- // Handle alert actions
4016
- if (MicrosoftTeamsAlertActions.isAlertAction({ actionType })) {
4037
+ // Handle alert actions
4017
4038
  await MicrosoftTeamsAlertActions.handleBotAlertAction({
4018
4039
  actionType,
4019
4040
  actionValue,
4020
4041
  value,
4021
4042
  projectId,
4022
4043
  oneUptimeUserId,
4044
+ databaseProps,
4023
4045
  turnContext: data.turnContext,
4024
4046
  });
4025
4047
  return;
@@ -0,0 +1,505 @@
1
+ import "@testing-library/jest-dom";
2
+ import { cleanup, render, screen, waitFor } from "@testing-library/react";
3
+ import React, { ReactElement } from "react";
4
+ import { MemoryRouter } from "react-router-dom";
5
+ import {
6
+ afterEach,
7
+ beforeEach,
8
+ describe,
9
+ expect,
10
+ jest,
11
+ test,
12
+ } from "@jest/globals";
13
+
14
+ /*
15
+ * Issue #3491: the monitor list had no way to see or filter which template a
16
+ * monitor came from, so "which monitors did that template edit just change?"
17
+ * could only be answered by opening monitors one at a time — which does not
18
+ * survive a fleet of thousands sharing one template.
19
+ *
20
+ * The fix is a Template column and a Template chip on MonitorTable, and both
21
+ * fail invisibly:
22
+ *
23
+ * - a column whose `field` stops asking for the relation renders a blank cell
24
+ * on every row, exactly like a project with no templates;
25
+ * - a chip whose `queryField` stops naming the foreign key lights up over a
26
+ * list it is not filtering;
27
+ * - and on the template page's own Linked Monitors table — the same component,
28
+ * scoped by `monitorTemplateId` — a chip would OVERWRITE that scope rather
29
+ * than narrow it, because the facet bar merges everything into one query
30
+ * object, so the page would list another template's monitors under this
31
+ * template's heading.
32
+ *
33
+ * These render the real MonitorTable and read the column set and facet list it
34
+ * actually hands downstream, rather than reading its source.
35
+ */
36
+
37
+ jest.mock("react-i18next", () => {
38
+ return {
39
+ useTranslation: () => {
40
+ return {
41
+ t: (key: string): string => {
42
+ return key;
43
+ },
44
+ };
45
+ },
46
+ };
47
+ });
48
+
49
+ jest.mock("../../../UI/Utils/Permission", () => {
50
+ return {
51
+ __esModule: true,
52
+ default: {
53
+ getAllPermissions: () => {
54
+ return [];
55
+ },
56
+ getProjectPermissions: () => {
57
+ return [];
58
+ },
59
+ getGlobalPermissions: () => {
60
+ return [];
61
+ },
62
+ },
63
+ };
64
+ });
65
+
66
+ jest.mock("../../../UI/Utils/User", () => {
67
+ return {
68
+ __esModule: true,
69
+ default: {
70
+ isMasterAdmin: () => {
71
+ return false;
72
+ },
73
+ getUserId: () => {
74
+ return null;
75
+ },
76
+ },
77
+ };
78
+ });
79
+
80
+ type CapturedTableProps = {
81
+ columns?: Array<CapturedColumn> | undefined;
82
+ query?: Record<string, unknown> | undefined;
83
+ };
84
+
85
+ type CapturedColumn = {
86
+ title: string;
87
+ field?: Record<string, unknown> | undefined;
88
+ getElement?: ((item: Monitor) => ReactElement) | undefined;
89
+ getExportValue?: ((item: Monitor) => string) | undefined;
90
+ };
91
+
92
+ let capturedTableProps: CapturedTableProps | null = null;
93
+
94
+ jest.mock("../../../UI/Components/ModelTable/ModelTable", () => {
95
+ return {
96
+ __esModule: true,
97
+ default: (props: CapturedTableProps) => {
98
+ capturedTableProps = props;
99
+ return null;
100
+ },
101
+ };
102
+ });
103
+
104
+ type CapturedHookOptions = {
105
+ extraFacets?: Array<ResourceFacet> | undefined;
106
+ };
107
+
108
+ let capturedHookOptions: CapturedHookOptions | null = null;
109
+
110
+ /*
111
+ * The hook is replaced rather than run: the real one fetches owners, labels and
112
+ * every chip's options over the network on mount. Its named exports stay real,
113
+ * because MonitorTable builds its other chips out of them.
114
+ */
115
+ jest.mock(
116
+ "../../../../App/FeatureSet/Dashboard/src/Components/ResourceOwners/useResourceOwners",
117
+ () => {
118
+ const actual: Record<string, unknown> = jest.requireActual(
119
+ "../../../../App/FeatureSet/Dashboard/src/Components/ResourceOwners/useResourceOwners",
120
+ ) as Record<string, unknown>;
121
+
122
+ return {
123
+ ...actual,
124
+ __esModule: true,
125
+ default: (options: CapturedHookOptions) => {
126
+ capturedHookOptions = options;
127
+
128
+ return {
129
+ getOwnersForResource: () => {
130
+ return [];
131
+ },
132
+ isLoadingOwners: false,
133
+ onResourcesFetched: () => {
134
+ // no-op
135
+ },
136
+ filterBar: null,
137
+ mergeFiltersIntoQuery: (
138
+ base: Record<string, unknown> | undefined,
139
+ ) => {
140
+ return base || {};
141
+ },
142
+ facetSaveState: {},
143
+ restoreFacetState: () => {
144
+ // no-op
145
+ },
146
+ };
147
+ },
148
+ };
149
+ },
150
+ );
151
+
152
+ jest.mock(
153
+ "../../../../App/FeatureSet/Dashboard/src/Components/CustomFields/useCustomFieldFacets",
154
+ () => {
155
+ return {
156
+ __esModule: true,
157
+ default: () => {
158
+ return { facets: [], isLoading: false };
159
+ },
160
+ };
161
+ },
162
+ );
163
+
164
+ jest.mock("../../../UI/Components/BulkUpdate/BulkLabelActions", () => {
165
+ return {
166
+ __esModule: true,
167
+ default: () => {
168
+ return { bulkActions: [], modals: null };
169
+ },
170
+ };
171
+ });
172
+
173
+ jest.mock("../../../UI/Components/BulkUpdate/BulkOwnerActions", () => {
174
+ return {
175
+ __esModule: true,
176
+ default: () => {
177
+ return { bulkActions: [], modals: null };
178
+ },
179
+ };
180
+ });
181
+
182
+ jest.mock("../../../../App/FeatureSet/Dashboard/src/Utils/Probe", () => {
183
+ return {
184
+ __esModule: true,
185
+ default: {
186
+ getAllProbes: async () => {
187
+ return [];
188
+ },
189
+ },
190
+ };
191
+ });
192
+
193
+ import MonitorTable from "../../../../App/FeatureSet/Dashboard/src/Components/Monitor/MonitorTable";
194
+ import {
195
+ MONITOR_TEMPLATE_FACET_KEY,
196
+ MONITOR_TEMPLATE_FACET_QUERY_FIELD,
197
+ } from "../../../../App/FeatureSet/Dashboard/src/Components/Monitor/MonitorFacets";
198
+ import { ResourceFacet } from "../../../../App/FeatureSet/Dashboard/src/Components/ResourceOwners/ResourceFacet";
199
+ import Monitor from "../../../Models/DatabaseModels/Monitor";
200
+ import MonitorTemplate from "../../../Models/DatabaseModels/MonitorTemplate";
201
+ import Includes from "../../../Types/BaseDatabase/Includes";
202
+ import IsNull from "../../../Types/BaseDatabase/IsNull";
203
+ import ObjectID from "../../../Types/ObjectID";
204
+ import Navigation from "../../../UI/Utils/Navigation";
205
+ import ProjectUtil from "../../../UI/Utils/Project";
206
+
207
+ const PROJECT_ID: ObjectID = ObjectID.generate();
208
+ const TEMPLATE_ID: ObjectID = ObjectID.generate();
209
+
210
+ type RenderTableFunction = (
211
+ query: Record<string, unknown>,
212
+ ) => Promise<CapturedTableProps>;
213
+
214
+ const renderTable: RenderTableFunction = async (
215
+ query: Record<string, unknown>,
216
+ ): Promise<CapturedTableProps> => {
217
+ render(
218
+ <MemoryRouter>
219
+ <MonitorTable query={query as never} />
220
+ </MemoryRouter>,
221
+ );
222
+
223
+ await waitFor(() => {
224
+ expect(capturedTableProps).not.toBeNull();
225
+ });
226
+
227
+ return capturedTableProps!;
228
+ };
229
+
230
+ type ColumnTitledFunction = (title: string) => CapturedColumn | undefined;
231
+
232
+ const columnTitled: ColumnTitledFunction = (
233
+ title: string,
234
+ ): CapturedColumn | undefined => {
235
+ return (capturedTableProps?.columns || []).find(
236
+ (column: CapturedColumn): boolean => {
237
+ return column.title === title;
238
+ },
239
+ );
240
+ };
241
+
242
+ type FacetKeyedFunction = (key: string) => ResourceFacet | undefined;
243
+
244
+ const facetKeyed: FacetKeyedFunction = (
245
+ key: string,
246
+ ): ResourceFacet | undefined => {
247
+ return (capturedHookOptions?.extraFacets || []).find(
248
+ (facet: ResourceFacet): boolean => {
249
+ return facet.key === key;
250
+ },
251
+ );
252
+ };
253
+
254
+ type MonitorWithTemplateFunction = (
255
+ templateName: string | undefined,
256
+ ) => Monitor;
257
+
258
+ const monitorWithTemplate: MonitorWithTemplateFunction = (
259
+ templateName: string | undefined,
260
+ ): Monitor => {
261
+ const monitor: Monitor = new Monitor();
262
+ monitor.name = "core-switch-01";
263
+
264
+ if (templateName) {
265
+ const template: MonitorTemplate = new MonitorTemplate();
266
+ template._id = TEMPLATE_ID.toString();
267
+ template.templateName = templateName;
268
+ monitor.monitorTemplate = template;
269
+ }
270
+
271
+ return monitor;
272
+ };
273
+
274
+ beforeEach(() => {
275
+ capturedTableProps = null;
276
+ capturedHookOptions = null;
277
+
278
+ jest.spyOn(ProjectUtil, "getCurrentProjectId").mockReturnValue(PROJECT_ID);
279
+
280
+ /*
281
+ * The table builds its row link off the current route. In the app the router
282
+ * pushes this in; under test nothing does.
283
+ */
284
+ Navigation.setLocation({
285
+ pathname: `/dashboard/${PROJECT_ID.toString()}/monitors`,
286
+ } as unknown as Parameters<typeof Navigation.setLocation>[0]);
287
+ });
288
+
289
+ afterEach(() => {
290
+ cleanup();
291
+ jest.restoreAllMocks();
292
+ });
293
+
294
+ describe("the Template column on the monitor list", () => {
295
+ test("is one of the table's columns", async () => {
296
+ await renderTable({ projectId: PROJECT_ID });
297
+
298
+ expect(columnTitled("Template")).toBeDefined();
299
+ });
300
+
301
+ /*
302
+ * Both keys, and through the relation. `templateName` is the label the cell
303
+ * prints; `_id` is what makes it a link to the template — which is the reason
304
+ * to look at the column at all, since the next thing a user does is go and
305
+ * read what the template now says.
306
+ */
307
+ test("asks for the template's name and id through the relation", async () => {
308
+ await renderTable({ projectId: PROJECT_ID });
309
+
310
+ expect(columnTitled("Template")!.field).toEqual({
311
+ monitorTemplate: {
312
+ _id: true,
313
+ templateName: true,
314
+ },
315
+ });
316
+ });
317
+
318
+ test("renders the template's name, linked to the template", async () => {
319
+ await renderTable({ projectId: PROJECT_ID });
320
+
321
+ render(
322
+ <MemoryRouter>
323
+ {columnTitled("Template")!.getElement!(
324
+ monitorWithTemplate("Production API Health"),
325
+ )}
326
+ </MemoryRouter>,
327
+ );
328
+
329
+ const link: HTMLElement = screen.getByText("Production API Health");
330
+
331
+ expect(link).toBeInTheDocument();
332
+ expect(link.closest("a")?.getAttribute("href")).toContain(
333
+ TEMPLATE_ID.toString(),
334
+ );
335
+ expect(link.closest("a")?.getAttribute("href")).toContain(
336
+ "settings/templates",
337
+ );
338
+ });
339
+
340
+ /*
341
+ * Most monitors are not created from a template, so the empty case is the
342
+ * common one and has to read as "no template" rather than as a value that
343
+ * failed to load.
344
+ */
345
+ test("renders a dash when the monitor came from no template", async () => {
346
+ await renderTable({ projectId: PROJECT_ID });
347
+
348
+ render(
349
+ <MemoryRouter>
350
+ {columnTitled("Template")!.getElement!(monitorWithTemplate(undefined))}
351
+ </MemoryRouter>,
352
+ );
353
+
354
+ expect(screen.getByText("—")).toBeInTheDocument();
355
+ expect(screen.queryByRole("link")).not.toBeInTheDocument();
356
+ });
357
+
358
+ /*
359
+ * "Export the list and diff it" is exactly what an admin does after a
360
+ * template change, and a cell rendered through getElement contributes nothing
361
+ * usable to the CSV on its own.
362
+ */
363
+ test("exports the template name, and an empty cell when there is none", async () => {
364
+ await renderTable({ projectId: PROJECT_ID });
365
+
366
+ const column: CapturedColumn = columnTitled("Template")!;
367
+
368
+ expect(column.getExportValue!(monitorWithTemplate("Edge Ping"))).toBe(
369
+ "Edge Ping",
370
+ );
371
+ expect(column.getExportValue!(monitorWithTemplate(undefined))).toBe("");
372
+ });
373
+ });
374
+
375
+ describe("the Template chip in the monitor list's filter bar", () => {
376
+ test("is one of the chips the bar is given", async () => {
377
+ await renderTable({ projectId: PROJECT_ID });
378
+
379
+ expect(facetKeyed(MONITOR_TEMPLATE_FACET_KEY)).toBeDefined();
380
+ expect(facetKeyed(MONITOR_TEMPLATE_FACET_KEY)!.label).toBe("Template");
381
+ });
382
+
383
+ /*
384
+ * The foreign key, not the relation. "is empty" is only answerable of the
385
+ * column — a monitor from no template has no template row to join against —
386
+ * and the same field is what the template page scopes its own table by.
387
+ */
388
+ test("filters the monitor's template foreign key", async () => {
389
+ await renderTable({ projectId: PROJECT_ID });
390
+
391
+ expect(facetKeyed(MONITOR_TEMPLATE_FACET_KEY)!.queryField).toBe(
392
+ MONITOR_TEMPLATE_FACET_QUERY_FIELD,
393
+ );
394
+ expect(MONITOR_TEMPLATE_FACET_QUERY_FIELD).toBe("monitorTemplateId");
395
+ });
396
+
397
+ test("turns a picked template into an id filter", async () => {
398
+ await renderTable({ projectId: PROJECT_ID });
399
+
400
+ const value: unknown = facetKeyed(MONITOR_TEMPLATE_FACET_KEY)!
401
+ .toQueryValue!([TEMPLATE_ID.toString()], "is");
402
+
403
+ expect(value).toBeInstanceOf(Includes);
404
+ expect(((value as Includes).values as Array<ObjectID>)[0]!.toString()).toBe(
405
+ TEMPLATE_ID.toString(),
406
+ );
407
+ });
408
+
409
+ test("turns 'is empty' into the monitors that came from no template", async () => {
410
+ await renderTable({ projectId: PROJECT_ID });
411
+
412
+ expect(
413
+ facetKeyed(MONITOR_TEMPLATE_FACET_KEY)!.toQueryValue!([], "is_empty"),
414
+ ).toBeInstanceOf(IsNull);
415
+ });
416
+
417
+ test("offers the empty operators, which is how that is asked for", async () => {
418
+ await renderTable({ projectId: PROJECT_ID });
419
+
420
+ expect(facetKeyed(MONITOR_TEMPLATE_FACET_KEY)!.supportedOperators).toEqual([
421
+ "is",
422
+ "is_not",
423
+ "is_empty",
424
+ "is_not_empty",
425
+ ]);
426
+ });
427
+
428
+ /*
429
+ * A project can hold any number of templates, so the options have to be
430
+ * searched server-side, and a selection restored from a shared link has to be
431
+ * resolvable to a label even when it is not on the first page of results.
432
+ */
433
+ test("searches its options on the server and resolves restored ones", async () => {
434
+ await renderTable({ projectId: PROJECT_ID });
435
+
436
+ expect(typeof facetKeyed(MONITOR_TEMPLATE_FACET_KEY)!.loadOptions).toBe(
437
+ "function",
438
+ );
439
+ expect(typeof facetKeyed(MONITOR_TEMPLATE_FACET_KEY)!.resolveOptions).toBe(
440
+ "function",
441
+ );
442
+ });
443
+
444
+ /*
445
+ * BaseModelTable builds its request as `{...props.query, ...columnFilterQuery}`,
446
+ * so a column-filter popup over the chip's field would replace the chip's
447
+ * constraint outright, silently, while the chip carried on claiming to apply.
448
+ */
449
+ test("no column filter competes with it for the same field", async () => {
450
+ const props: CapturedTableProps & {
451
+ filters?: Array<{ field?: Record<string, unknown> }>;
452
+ } = (await renderTable({ projectId: PROJECT_ID })) as CapturedTableProps & {
453
+ filters?: Array<{ field?: Record<string, unknown> }>;
454
+ };
455
+
456
+ for (const filter of props.filters || []) {
457
+ expect(Object.keys(filter.field || {})).not.toContain(
458
+ MONITOR_TEMPLATE_FACET_QUERY_FIELD,
459
+ );
460
+ expect(Object.keys(filter.field || {})).not.toContain("monitorTemplate");
461
+ }
462
+ });
463
+ });
464
+
465
+ describe("the same table scoped to one template", () => {
466
+ /*
467
+ * The template page's Linked Monitors card. A chip here would overwrite the
468
+ * page's own scope rather than narrow it, and the column would repeat one
469
+ * value down every row.
470
+ */
471
+ test("shows no Template chip", async () => {
472
+ await renderTable({
473
+ projectId: PROJECT_ID,
474
+ monitorTemplateId: TEMPLATE_ID,
475
+ });
476
+
477
+ expect(facetKeyed(MONITOR_TEMPLATE_FACET_KEY)).toBeUndefined();
478
+ });
479
+
480
+ test("shows no Template column", async () => {
481
+ await renderTable({
482
+ projectId: PROJECT_ID,
483
+ monitorTemplateId: TEMPLATE_ID,
484
+ });
485
+
486
+ expect(columnTitled("Template")).toBeUndefined();
487
+ });
488
+
489
+ /*
490
+ * Guards the two above from passing because the table rendered nothing at
491
+ * all: the rest of its columns and chips have to still be there.
492
+ */
493
+ test("keeps every other column and chip", async () => {
494
+ await renderTable({
495
+ projectId: PROJECT_ID,
496
+ monitorTemplateId: TEMPLATE_ID,
497
+ });
498
+
499
+ expect(columnTitled("Name")).toBeDefined();
500
+ expect(columnTitled("Monitor Status")).toBeDefined();
501
+ expect(columnTitled("Labels")).toBeDefined();
502
+ expect(facetKeyed("currentMonitorStatus")).toBeDefined();
503
+ expect(facetKeyed("monitorType")).toBeDefined();
504
+ });
505
+ });