@oneuptime/common 14.0.1 → 14.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 (1090) hide show
  1. package/Models/DatabaseModels/AlertEpisodeMember.ts +8 -1
  2. package/Models/DatabaseModels/AutoRemediationSuggestion.ts +63 -0
  3. package/Models/DatabaseModels/IncidentEpisodeMember.ts +8 -1
  4. package/Models/DatabaseModels/IncidentEpisodePublicNote.ts +9 -2
  5. package/Models/DatabaseModels/IncidentPublicNote.ts +9 -2
  6. package/Models/DatabaseModels/KubernetesCluster.ts +429 -0
  7. package/Models/DatabaseModels/Monitor.ts +48 -0
  8. package/Models/DatabaseModels/Runner.ts +5 -4
  9. package/Models/DatabaseModels/RunnerJob.ts +66 -0
  10. package/Models/DatabaseModels/ScheduledMaintenancePublicNote.ts +10 -2
  11. package/Models/DatabaseModels/WorkflowVariable.ts +479 -2
  12. package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +79 -25
  13. package/Server/API/AIInvestigationAPI.ts +341 -0
  14. package/Server/API/AutoRemediationAPI.ts +184 -2
  15. package/Server/API/CommonAPI.ts +70 -0
  16. package/Server/API/KubernetesClusterAiAccessAPI.ts +793 -0
  17. package/Server/API/MicrosoftTeamsAPI.ts +770 -315
  18. package/Server/API/MonitorAPI.ts +140 -0
  19. package/Server/API/OnCallScheduleTimelineAPI.ts +5 -21
  20. package/Server/API/SlackAPI.ts +447 -99
  21. package/Server/API/StatusPageAPI.ts +146 -26
  22. package/Server/API/TelemetryAPI.ts +216 -5
  23. package/Server/API/UserTwoFactorBackupCodeAPI.ts +1 -0
  24. package/Server/API/WorkflowVariableAPI.ts +156 -0
  25. package/Server/Infrastructure/GlobalCache.ts +35 -0
  26. package/Server/Infrastructure/Postgres/SchemaMigrations/1794300000000-AddOAuth2WorkflowVariables.ts +97 -0
  27. package/Server/Infrastructure/Postgres/SchemaMigrations/1794400000000-AddKubernetesClusterAiAccess.ts +129 -0
  28. package/Server/Infrastructure/Postgres/SchemaMigrations/1794500000000-AddIncomingEmailCustomAddressToMonitor.ts +25 -0
  29. package/Server/Infrastructure/Postgres/SchemaMigrations/1794600000000-AddAuthTokenExpiresAtToWorkspaceProjectAuthToken.ts +20 -0
  30. package/Server/Infrastructure/Postgres/SchemaMigrations/1794700000000-RemoveAppAccessTokenFromWorkspaceProjectAuthTokenMiscData.ts +38 -0
  31. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +10 -0
  32. package/Server/Middleware/MasterAdminAuthorization.ts +28 -0
  33. package/Server/Middleware/TelemetryIngest.ts +47 -2
  34. package/Server/Middleware/UserAuthorization.ts +56 -2
  35. package/Server/Services/AIAgentService.ts +1 -0
  36. package/Server/Services/AccessTokenService.ts +121 -48
  37. package/Server/Services/AlertEpisodeMemberService.ts +9 -0
  38. package/Server/Services/AlertEpisodeService.ts +14 -1
  39. package/Server/Services/AlertService.ts +134 -22
  40. package/Server/Services/AutoRemediationRuleEngineService.ts +968 -39
  41. package/Server/Services/AutoRemediationRuleService.ts +118 -0
  42. package/Server/Services/BillingService.ts +1 -0
  43. package/Server/Services/InboundEmail/Providers/SendGridInboundProvider.ts +16 -22
  44. package/Server/Services/IncidentEpisodeMemberService.ts +9 -0
  45. package/Server/Services/IncidentEpisodePublicNoteService.ts +66 -0
  46. package/Server/Services/IncidentEpisodeRoleMemberService.ts +27 -0
  47. package/Server/Services/IncidentEpisodeService.ts +14 -1
  48. package/Server/Services/IncidentGroupingEngineService.ts +11 -0
  49. package/Server/Services/IncidentMemberService.ts +21 -0
  50. package/Server/Services/IncidentPublicNoteService.ts +63 -0
  51. package/Server/Services/IncidentService.ts +120 -20
  52. package/Server/Services/IncidentStateTimelineService.ts +15 -0
  53. package/Server/Services/IncidentTemplateService.ts +83 -3
  54. package/Server/Services/KubernetesClusterAiAccessService.ts +1943 -0
  55. package/Server/Services/KubernetesClusterService.ts +1570 -1
  56. package/Server/Services/LogAggregationService.ts +14 -6
  57. package/Server/Services/MonitorService.ts +651 -52
  58. package/Server/Services/MonitorStatusTimelineService.ts +639 -2
  59. package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +2 -0
  60. package/Server/Services/OnCallDutyPolicyEscalationRuleTeamService.ts +2 -0
  61. package/Server/Services/OnCallDutyPolicyEscalationRuleUserService.ts +2 -0
  62. package/Server/Services/OnCallDutyPolicyScheduleService.ts +3 -0
  63. package/Server/Services/OnCallNotificationAlertingService.ts +36 -12
  64. package/Server/Services/OnCallSetupReminderService.ts +1 -0
  65. package/Server/Services/ProbeService.ts +7 -1
  66. package/Server/Services/ProjectService.ts +11 -1
  67. package/Server/Services/RunbookCredentialService.ts +26 -1
  68. package/Server/Services/RunbookSecretService.ts +36 -0
  69. package/Server/Services/RunnerJobService.ts +941 -23
  70. package/Server/Services/RunnerService.ts +439 -6
  71. package/Server/Services/ScheduledMaintenancePublicNoteService.ts +66 -0
  72. package/Server/Services/ScheduledMaintenanceService.ts +124 -19
  73. package/Server/Services/ScheduledMaintenanceTemplateService.ts +82 -3
  74. package/Server/Services/ServiceLevelObjectiveService.ts +14 -1
  75. package/Server/Services/StatusPagePrivateUserService.ts +1 -0
  76. package/Server/Services/StatusPageService.ts +301 -74
  77. package/Server/Services/StatusPageSubscriberService.ts +4 -0
  78. package/Server/Services/TeamMemberService.ts +309 -7
  79. package/Server/Services/TraceAggregationService.ts +10 -6
  80. package/Server/Services/UserEmailService.ts +1 -0
  81. package/Server/Services/UserNotificationMethodAdminService.ts +1 -0
  82. package/Server/Services/UserNotificationRuleAdminService.ts +1 -0
  83. package/Server/Services/UserNotificationRuleService.ts +4 -0
  84. package/Server/Services/UserService.ts +122 -1
  85. package/Server/Services/WorkflowVariableService.ts +593 -8
  86. package/Server/Services/WorkspaceNotificationRuleService.ts +588 -8
  87. package/Server/Services/WorkspaceProjectAuthTokenService.ts +170 -1
  88. package/Server/Types/Database/QueryHelper.ts +22 -0
  89. package/Server/Types/Markdown.ts +502 -27
  90. package/Server/Utils/AI/ClusterAccess/ClusterAccessContext.ts +182 -0
  91. package/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.ts +525 -0
  92. package/Server/Utils/AI/ClusterAccess/KubectlJobRunner.ts +854 -0
  93. package/Server/Utils/AI/CodeFix/CodeFixAgentAuth.ts +22 -2
  94. package/Server/Utils/AI/Remediation/RemediationCommandTools.ts +1916 -307
  95. package/Server/Utils/AI/Remediation/RemediationExecutionRunner.ts +1601 -74
  96. package/Server/Utils/AI/SRE/AIInvestigationEngine.ts +139 -7
  97. package/Server/Utils/AI/SRE/AlertInvestigationRunner.ts +58 -1
  98. package/Server/Utils/AI/SRE/IncidentInvestigationRunner.ts +54 -1
  99. package/Server/Utils/AI/Toolbox/AIActionTools.ts +14 -6
  100. package/Server/Utils/AI/Toolbox/ResourceTools.ts +4 -2
  101. package/Server/Utils/AutoRemediation/CommandPlanExecutor.ts +1341 -150
  102. package/Server/Utils/AutoRemediation/RemediationVerifier.ts +375 -4
  103. package/Server/Utils/Database/AffectedResourceRelations.ts +127 -0
  104. package/Server/Utils/Database/ProjectScopedReferenceValidator.ts +240 -1
  105. package/Server/Utils/EmailRollup/EmailRollupFlushRunner.ts +1 -0
  106. package/Server/Utils/Monitor/MonitorAlert.ts +83 -9
  107. package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +5 -1
  108. package/Server/Utils/Monitor/MonitorIncident.ts +104 -9
  109. package/Server/Utils/Monitor/MonitorPayloadRedaction.ts +124 -7
  110. package/Server/Utils/Monitor/ProbeMonitorsNotification.ts +407 -0
  111. package/Server/Utils/OnCall/OnCallShiftReminderRunner.ts +1 -0
  112. package/Server/Utils/PushNotificationUtil.ts +45 -0
  113. package/Server/Utils/Rules/OwnerRuleAssignment.ts +40 -3
  114. package/Server/Utils/Runbook/RunbookExecutePermission.ts +56 -6
  115. package/Server/Utils/SessionReplay/SessionReplayReadService.ts +223 -33
  116. package/Server/Utils/SessionReplay/SessionReplayUserFlowReadService.ts +559 -0
  117. package/Server/Utils/StartServer.ts +4 -3
  118. package/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.ts +528 -0
  119. package/Server/Utils/TwoFactorBackupCodeNotification.ts +1 -0
  120. package/Server/Utils/UserPermission/UserPermission.ts +16 -0
  121. package/Server/Utils/Workflow/OAuth2TokenClient.ts +704 -0
  122. package/Server/Utils/Workflow/WorkflowVariableOAuthToken.ts +318 -0
  123. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.ts +47 -7
  124. package/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.ts +57 -16
  125. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.ts +193 -214
  126. package/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.ts +57 -13
  127. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Monitor.ts +54 -17
  128. package/Server/Utils/Workspace/MicrosoftTeams/Actions/ScheduledMaintenance.ts +179 -161
  129. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +281 -167
  130. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsServiceUrl.ts +114 -0
  131. package/Server/Utils/Workspace/MicrosoftTeams/ReactionNoteSync.ts +895 -0
  132. package/Server/Utils/Workspace/Slack/Actions/ActionTypes.ts +7 -9
  133. package/Server/Utils/Workspace/Slack/Actions/Alert.ts +96 -227
  134. package/Server/Utils/Workspace/Slack/Actions/AlertEpisode.ts +93 -214
  135. package/Server/Utils/Workspace/Slack/Actions/Auth.ts +30 -3
  136. package/Server/Utils/Workspace/Slack/Actions/Authorization.ts +99 -0
  137. package/Server/Utils/Workspace/Slack/Actions/Incident.ts +117 -283
  138. package/Server/Utils/Workspace/Slack/Actions/IncidentEpisode.ts +101 -196
  139. package/Server/Utils/Workspace/Slack/Actions/ReactionNote.ts +329 -0
  140. package/Server/Utils/Workspace/Slack/Actions/ScheduledMaintenance.ts +111 -287
  141. package/Server/Utils/Workspace/Slack/Slack.ts +86 -17
  142. package/Server/Utils/Workspace/Slack/app-manifest.json +2 -0
  143. package/Server/Utils/Workspace/Workspace.ts +171 -54
  144. package/Server/Utils/Workspace/WorkspaceActionAuthorization.ts +252 -0
  145. package/Server/Utils/Workspace/WorkspaceBase.ts +39 -0
  146. package/Server/Utils/Workspace/WorkspaceOAuthState.ts +280 -0
  147. package/Server/Utils/Workspace/WorkspaceProjectReferenceValidator.ts +96 -0
  148. package/Server/Utils/Workspace/WorkspaceReactionNote.ts +620 -0
  149. package/Tests/App/Dashboard/AttributesJSONDashboardSurfaces.test.tsx +496 -0
  150. package/Tests/App/Dashboard/AutoRemediationRulesTableCommandRunners.test.tsx +152 -0
  151. package/Tests/App/Dashboard/BreadcrumbCoverage.test.tsx +32 -0
  152. package/Tests/App/Dashboard/ClusterAccessNotice.test.tsx +794 -0
  153. package/Tests/App/Dashboard/CreateOAuthWorkflowVariableModal.test.tsx +840 -0
  154. package/Tests/App/Dashboard/CreateWorkflowModal.test.tsx +653 -0
  155. package/Tests/App/Dashboard/DatabaseMonitorStepForm.test.tsx +197 -0
  156. package/Tests/App/Dashboard/DatabaseMonitorView.test.tsx +376 -0
  157. package/Tests/App/Dashboard/DisabledWarningRobustness.test.tsx +262 -0
  158. package/Tests/App/Dashboard/EventNotes.test.tsx +2080 -0
  159. package/Tests/App/Dashboard/EventNotesPages.test.tsx +486 -0
  160. package/Tests/App/Dashboard/EventNotesUtil.test.ts +933 -0
  161. package/Tests/App/Dashboard/ExceptionMonitorEnvironmentFilter.test.tsx +218 -0
  162. package/Tests/App/Dashboard/IncidentChangeStateNotifyDefault.test.tsx +665 -0
  163. package/Tests/App/Dashboard/IncidentEpisodeFeedPublicNoteNotifyDefault.test.tsx +494 -0
  164. package/Tests/App/Dashboard/IncidentEpisodeOverviewNotifyDefault.test.tsx +706 -0
  165. package/Tests/App/Dashboard/IncidentFeedPublicNoteNotifyDefault.test.tsx +472 -0
  166. package/Tests/App/Dashboard/IncidentOverviewNotifyDefault.test.tsx +858 -0
  167. package/Tests/App/Dashboard/IncomingEmailAddressSettings.test.tsx +567 -0
  168. package/Tests/App/Dashboard/InvestigationClusterToolFormat.test.ts +241 -0
  169. package/Tests/App/Dashboard/InvestigationEvidenceKubectl.test.tsx +322 -0
  170. package/Tests/App/Dashboard/InvestigationPanel.test.tsx +12 -0
  171. package/Tests/App/Dashboard/InvestigationPanelClusterAccess.test.tsx +476 -0
  172. package/Tests/App/Dashboard/InvestigationRunDetailsKubectl.test.tsx +383 -0
  173. package/Tests/App/Dashboard/KubectlActivitySummary.test.tsx +366 -0
  174. package/Tests/App/Dashboard/KubernetesClusterAiAccessSettings.test.ts +2886 -0
  175. package/Tests/App/Dashboard/KubernetesClusterAiPage.test.tsx +2688 -0
  176. package/Tests/App/Dashboard/KubernetesClusterAiPageServerParity.test.ts +915 -0
  177. package/Tests/App/Dashboard/LayerDateTimeFieldTimezoneName.test.tsx +66 -0
  178. package/Tests/App/Dashboard/LogMonitorPreview.test.tsx +174 -0
  179. package/Tests/App/Dashboard/MonitorFeedRefresh.test.tsx +279 -0
  180. package/Tests/App/Dashboard/MonitorMetricQueryConfig.test.ts +244 -0
  181. package/Tests/App/Dashboard/MonitorOpenWorkCard.test.tsx +381 -0
  182. package/Tests/App/Dashboard/MonitorOpenWorkHook.test.tsx +784 -0
  183. package/Tests/App/Dashboard/MonitorOverviewData.test.tsx +2199 -0
  184. package/Tests/App/Dashboard/MonitorOverviewHero.test.tsx +1169 -0
  185. package/Tests/App/Dashboard/MonitorOverviewInput.test.ts +1127 -0
  186. package/Tests/App/Dashboard/MonitorOverviewPage.test.tsx +2136 -0
  187. package/Tests/App/Dashboard/MonitorOverviewSelectReadable.test.ts +368 -0
  188. package/Tests/App/Dashboard/MonitorOverviewStatBar.test.tsx +526 -0
  189. package/Tests/App/Dashboard/MonitorOverviewSupportingCards.test.tsx +542 -0
  190. package/Tests/App/Dashboard/MonitorOwnersCombine.test.ts +299 -0
  191. package/Tests/App/Dashboard/MonitorProbesCard.test.tsx +602 -0
  192. package/Tests/App/Dashboard/MonitorResponseTimeCard.test.tsx +386 -0
  193. package/Tests/App/Dashboard/MonitorSetupAndConnectionCards.test.tsx +612 -0
  194. package/Tests/App/Dashboard/MonitorStatusChangesCard.test.tsx +257 -0
  195. package/Tests/App/Dashboard/MonitorSummaryTestMonitorButton.test.tsx +360 -1
  196. package/Tests/App/Dashboard/MonitorUptimeHistoryCard.test.tsx +835 -0
  197. package/Tests/App/Dashboard/MonitorUptimeSummaryHook.test.tsx +971 -0
  198. package/Tests/App/Dashboard/MonitorViewLayout.test.tsx +580 -0
  199. package/Tests/App/Dashboard/OnCallLayerRestrictionTimesPreserved.test.tsx +441 -0
  200. package/Tests/App/Dashboard/OnCallNavbar.test.tsx +214 -0
  201. package/Tests/App/Dashboard/OnCallSchedulePreviewCalendarDisplay.test.tsx +1389 -0
  202. package/Tests/App/Dashboard/OnCallSchedulePreviewOverrides.test.tsx +205 -1
  203. package/Tests/App/Dashboard/OnCallScheduleTimeInputsSettingsZone.test.tsx +198 -0
  204. package/Tests/App/Dashboard/PublicNotePagesNotifyDefault.test.tsx +524 -0
  205. package/Tests/App/Dashboard/RemediationSuggestionCardApproval.test.tsx +702 -0
  206. package/Tests/App/Dashboard/RemoveUserFromProject.test.tsx +427 -0
  207. package/Tests/App/Dashboard/ResourceArchiveSettings.test.tsx +4 -0
  208. package/Tests/App/Dashboard/RumSessionReplaySideMenu.test.tsx +3 -1
  209. package/Tests/App/Dashboard/RunbookStepTypeMaps.test.ts +166 -0
  210. package/Tests/App/Dashboard/RunnerKubernetesAgentForm.test.tsx +622 -0
  211. package/Tests/App/Dashboard/ScheduledMaintenanceChangeStateNotifyDefault.test.tsx +1491 -0
  212. package/Tests/App/Dashboard/ScheduledMaintenanceFeedPublicNoteNotifyDefault.test.tsx +489 -0
  213. package/Tests/App/Dashboard/ScheduledMaintenanceOverviewNotifyDefault.test.tsx +909 -0
  214. package/Tests/App/Dashboard/ScheduledMaintenanceOverviewPage.test.tsx +14 -0
  215. package/Tests/App/Dashboard/ScheduledMaintenanceWindowStatsTranslation.test.tsx +307 -0
  216. package/Tests/App/Dashboard/SecurityEventAttributeColumnPicker.test.tsx +542 -0
  217. package/Tests/App/Dashboard/SecurityEventConnectionFormModal.test.tsx +174 -0
  218. package/Tests/App/Dashboard/SecurityEventConnectionsEmptyState.test.tsx +973 -212
  219. package/Tests/App/Dashboard/SecurityEventConnectionsTable.test.tsx +491 -52
  220. package/Tests/App/Dashboard/SecurityEventDetailPanel.test.tsx +301 -0
  221. package/Tests/App/Dashboard/SecurityEventListRow.test.tsx +158 -1
  222. package/Tests/App/Dashboard/SecurityEventProviderTile.test.tsx +869 -0
  223. package/Tests/App/Dashboard/SecurityEventsEmptyState.test.tsx +959 -94
  224. package/Tests/App/Dashboard/SecurityEventsNoResults.test.tsx +2 -0
  225. package/Tests/App/Dashboard/SecurityEventsViewer.test.tsx +274 -1
  226. package/Tests/App/Dashboard/TeamMembershipRemoval.test.ts +900 -0
  227. package/Tests/App/Dashboard/TeamMembershipRemovalPages.test.tsx +460 -0
  228. package/Tests/App/Dashboard/TimezoneElements.test.tsx +321 -0
  229. package/Tests/App/Dashboard/TimezoneSelectButton.test.tsx +634 -0
  230. package/Tests/App/Dashboard/TraceExplorer.test.tsx +260 -0
  231. package/Tests/App/Dashboard/TraceMonitorPreview.test.tsx +182 -0
  232. package/Tests/App/Dashboard/UserTimezoneInit.test.tsx +544 -0
  233. package/Tests/App/Dashboard/WorkflowVariableModals.test.tsx +1630 -0
  234. package/Tests/App/Dashboard/WorkflowVariableRoutes.test.tsx +603 -0
  235. package/Tests/App/Dashboard/WorkflowVariableTokenStatus.test.tsx +589 -0
  236. package/Tests/App/Dashboard/WorkflowVariableUtil.test.ts +1543 -0
  237. package/Tests/App/Dashboard/WorkflowVariableView.test.tsx +3227 -0
  238. package/Tests/App/Dashboard/WorkflowVariablesTable.test.tsx +1277 -395
  239. package/Tests/App/StatusPage/StatusPageOverviewMonitorGroupUptime.test.tsx +714 -0
  240. package/Tests/App/StatusPage/StatusPageOverviewUptimeBars.NewYork.test.tsx +1009 -0
  241. package/Tests/App/StatusPage/StatusPageOverviewUptimeBars.test.tsx +1092 -0
  242. package/Tests/Models/DatabaseModels/DomainRoleTierCoverage.test.ts +5 -0
  243. package/Tests/Models/DatabaseModels/KubernetesClusterAiAccessColumns.test.ts +520 -0
  244. package/Tests/Models/DatabaseModels/MonitorSecretKeyColumnAccessControl.test.ts +66 -2
  245. package/Tests/Models/DatabaseModels/WorkspaceProjectAuthTokenAccessControl.test.ts +236 -0
  246. package/Tests/Models/IncidentEpisodePublicNoteNotifyColumnContract.test.ts +103 -0
  247. package/Tests/Models/IncidentPublicNoteNotifyColumnContract.test.ts +96 -0
  248. package/Tests/Models/ScheduledMaintenancePublicNoteNotifyColumnContract.test.ts +111 -0
  249. package/Tests/Server/API/AIInvestigationAPI.test.ts +976 -0
  250. package/Tests/Server/API/AutoRemediationApproveKubectlRecheck.test.ts +836 -0
  251. package/Tests/Server/API/CommonAPIAssertCanCreateTable.test.ts +565 -0
  252. package/Tests/Server/API/CommonAPIAssertCanReadTable.test.ts +282 -0
  253. package/Tests/Server/API/KubernetesClusterAiAccessAPI.test.ts +1744 -0
  254. package/Tests/Server/API/LogHistogramAPI.test.ts +371 -0
  255. package/Tests/Server/API/MicrosoftTeamsLegacyWebhookRemoval.test.ts +125 -30
  256. package/Tests/Server/API/MicrosoftTeamsOAuthState.test.ts +1121 -0
  257. package/Tests/Server/API/MonitorUptimeSummaryAPI.test.ts +685 -0
  258. package/Tests/Server/API/SessionReplayAPI.test.ts +602 -1
  259. package/Tests/Server/API/SlackChannelCacheAPI.test.ts +358 -0
  260. package/Tests/Server/API/SlackOAuthState.test.ts +688 -0
  261. package/Tests/Server/API/StatusPageOverviewMonitorGroupUptime.test.ts +527 -0
  262. package/Tests/Server/API/StatusPageTestEmailReportAuth.test.ts +2 -0
  263. package/Tests/Server/API/StatusPageUptimeAPI.test.ts +427 -0
  264. package/Tests/Server/API/UserProjectsAPI.test.ts +4 -0
  265. package/Tests/Server/API/WorkflowVariableAPI.test.ts +404 -0
  266. package/Tests/Server/API/WorkspaceOAuthTestHelpers.ts +289 -0
  267. package/Tests/Server/API/WorkspaceSendTestNotificationRoutes.test.ts +2064 -0
  268. package/Tests/Server/Infrastructure/GlobalCache.test.ts +86 -0
  269. package/Tests/Server/Infrastructure/Postgres/AddKubernetesClusterAiAccessMigration.test.ts +589 -0
  270. package/Tests/Server/Infrastructure/Postgres/AddOAuth2WorkflowVariablesMigration.test.ts +254 -0
  271. package/Tests/Server/Middleware/BlockedUserMiddleware.test.ts +403 -0
  272. package/Tests/Server/Middleware/MasterAdminAuthorization.test.ts +13 -0
  273. package/Tests/Server/Middleware/PublicRouteUserMiddleware.test.ts +2 -0
  274. package/Tests/Server/Middleware/RemovedProjectMemberAuthorization.test.ts +469 -0
  275. package/Tests/Server/Middleware/TelemetryIngestBrowserKey.test.ts +77 -1
  276. package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerPinnedKey.test.ts +211 -0
  277. package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerRateLimit.test.ts +358 -0
  278. package/Tests/Server/Middleware/UserAuthorization.test.ts +97 -0
  279. package/Tests/Server/Middleware/UserAuthorizationEditionEnforcement.test.ts +2 -0
  280. package/Tests/Server/Services/AccessTokenProjectUserGrant.test.ts +14 -2
  281. package/Tests/Server/Services/AccessTokenRemovedMemberAccess.test.ts +1044 -0
  282. package/Tests/Server/Services/AddOwnersIdempotency.test.ts +5 -0
  283. package/Tests/Server/Services/AlertEpisodeMemberCreatePermission.test.ts +139 -0
  284. package/Tests/Server/Services/AutoRemediationClusterRuleEngine.test.ts +1361 -0
  285. package/Tests/Server/Services/AutoRemediationRuleCommandRunners.test.ts +375 -0
  286. package/Tests/Server/Services/CrossProjectRelationWriteGuard.test.ts +2384 -0
  287. package/Tests/Server/Services/FindOwnersProjectMembership.test.ts +267 -0
  288. package/Tests/Server/Services/InboundEmail/SendGridInboundProviderAddress.test.ts +90 -0
  289. package/Tests/Server/Services/IncidentEpisodeMemberCreatePermission.test.ts +143 -0
  290. package/Tests/Server/Services/IncidentEpisodePublicNoteNotifyDefault.test.ts +805 -0
  291. package/Tests/Server/Services/IncidentPublicNoteNotifyDefault.test.ts +724 -0
  292. package/Tests/Server/Services/IncidentRoleProjectMembership.test.ts +233 -0
  293. package/Tests/Server/Services/KubernetesClusterAiAccessAgentRunnerAssignments.test.ts +372 -0
  294. package/Tests/Server/Services/KubernetesClusterAiAccessBindingGuard.test.ts +1876 -0
  295. package/Tests/Server/Services/KubernetesClusterAiAccessDeletedRunnerRemedy.test.ts +620 -0
  296. package/Tests/Server/Services/KubernetesClusterAiAccessGapNextSteps.test.ts +421 -0
  297. package/Tests/Server/Services/KubernetesClusterAiAccessService.test.ts +3662 -0
  298. package/Tests/Server/Services/KubernetesClusterAiAccessSubjectClusters.test.ts +405 -0
  299. package/Tests/Server/Services/KubernetesClusterAiSettingsPermission.test.ts +975 -0
  300. package/Tests/Server/Services/KubernetesClusterServiceAiAccessFeed.test.ts +497 -0
  301. package/Tests/Server/Services/KubernetesClusterServiceDeleteSettlesRemediation.test.ts +328 -0
  302. package/Tests/Server/Services/LogAggregationService.test.ts +99 -0
  303. package/Tests/Server/Services/MonitorServiceIncomingEmailCustomAddress.test.ts +447 -0
  304. package/Tests/Server/Services/MonitorServiceProbeFlagSync.test.ts +1334 -0
  305. package/Tests/Server/Services/MonitorServiceProbeStatusGrouping.test.ts +1640 -0
  306. package/Tests/Server/Services/MonitorServiceRefreshCurrentStatus.test.ts +225 -0
  307. package/Tests/Server/Services/MonitorStatusBridgeProbePath.test.ts +8 -8
  308. package/Tests/Server/Services/MonitorStatusTimelineDailyAggregates.test.ts +264 -1
  309. package/Tests/Server/Services/MonitorStatusTimelineMergedDowntime.test.ts +284 -0
  310. package/Tests/Server/Services/MonitorUptimeSummaryService.test.ts +1043 -0
  311. package/Tests/Server/Services/NetworkDeviceRuleEngines.test.ts +5 -0
  312. package/Tests/Server/Services/OnCallUndeliverablePageOwnerMembership.test.ts +359 -0
  313. package/Tests/Server/Services/ProbeServiceDashboardLink.test.ts +191 -0
  314. package/Tests/Server/Services/ProjectOwnersProjectMembership.test.ts +221 -0
  315. package/Tests/Server/Services/RuleRun/IncidentAlertMaintenanceRuleEnginesRun.test.ts +5 -0
  316. package/Tests/Server/Services/RuleRun/OwnerRuleEnginesGroupARun.test.ts +28 -0
  317. package/Tests/Server/Services/RuleRun/OwnerRuleEnginesGroupBRun.test.ts +9 -1
  318. package/Tests/Server/Services/RunnerJobEnqueueAiCommandAgentRunner.test.ts +230 -0
  319. package/Tests/Server/Services/RunnerJobEnqueueKubectl.test.ts +978 -0
  320. package/Tests/Server/Services/RunnerJobKubectlWriteScope.test.ts +866 -0
  321. package/Tests/Server/Services/RunnerJobSubmitResultRedaction.test.ts +322 -0
  322. package/Tests/Server/Services/RunnerJobSubmitResultUnstorableText.test.ts +357 -0
  323. package/Tests/Server/Services/RunnerJobTimeoutReason.test.ts +227 -0
  324. package/Tests/Server/Services/RunnerServiceAgentName.test.ts +581 -0
  325. package/Tests/Server/Services/ScheduledMaintenancePublicNoteNotifyDefault.test.ts +802 -0
  326. package/Tests/Server/Services/ScheduledMaintenanceSubscriberTemplateFormats.test.ts +53 -0
  327. package/Tests/Server/Services/ServiceLevelObjectiveLabelAndOwnerRuleEngines.test.ts +5 -0
  328. package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +12 -0
  329. package/Tests/Server/Services/StatusPageSubscriberReport.test.ts +1176 -1
  330. package/Tests/Server/Services/TeamMemberBillingSeatSync.test.ts +9 -0
  331. package/Tests/Server/Services/TeamMemberOnCallCleanup.test.ts +18 -0
  332. package/Tests/Server/Services/TeamMemberProjectMembership.test.ts +255 -0
  333. package/Tests/Server/Services/TeamMemberRemovalRevokesProjectAccess.test.ts +600 -0
  334. package/Tests/Server/Services/TeamMemberResourceCleanup.test.ts +731 -0
  335. package/Tests/Server/Services/TeamMemberWorkspaceAccountCleanup.test.ts +164 -0
  336. package/Tests/Server/Services/TraceAggregationService.test.ts +64 -0
  337. package/Tests/Server/Services/UserServiceBlockedUser.test.ts +293 -0
  338. package/Tests/Server/Services/WorkflowVariableOAuthHooks.test.ts +1016 -0
  339. package/Tests/Server/Services/WorkflowVariableOAuthPostgres.test.ts +543 -0
  340. package/Tests/Server/Services/WorkflowVariableRenameUniqueness.test.ts +16 -5
  341. package/Tests/Server/Services/WorkspaceNotificationRuleNoteDelivery.test.ts +473 -0
  342. package/Tests/Server/Services/WorkspaceNotificationRuleSendTestNotification.test.ts +3690 -0
  343. package/Tests/Server/Services/WorkspaceProjectAuthTokenMiscDataSecrets.test.ts +374 -0
  344. package/Tests/Server/Services/WorkspaceProjectAuthTokenSlackChannelCache.test.ts +143 -0
  345. package/Tests/Server/Types/Database/Permissions/WorkflowVariableOAuthColumnPermission.test.ts +250 -0
  346. package/Tests/Server/Types/Database/QueryHelperJsonContains.test.ts +96 -0
  347. package/Tests/Server/Types/Markdown.test.ts +1033 -0
  348. package/Tests/Server/Utils/AI/ClusterAccessContext.test.ts +285 -0
  349. package/Tests/Server/Utils/AI/CodeFix/CodeFixAgentAuth.test.ts +159 -1
  350. package/Tests/Server/Utils/AI/InvestigationClusterAccessWiring.test.ts +752 -0
  351. package/Tests/Server/Utils/AI/KubectlInvestigationToolkit.test.ts +1840 -0
  352. package/Tests/Server/Utils/AI/KubectlJobRunner.test.ts +1067 -0
  353. package/Tests/Server/Utils/AI/KubectlJobRunnerFinishedJob.test.ts +355 -0
  354. package/Tests/Server/Utils/AI/PostIncidentStatusUpdateTool.test.ts +496 -0
  355. package/Tests/Server/Utils/AI/RemediationCommandTools.test.ts +305 -0
  356. package/Tests/Server/Utils/AI/RemediationCommandToolsKubectl.test.ts +1365 -0
  357. package/Tests/Server/Utils/AI/RemediationCommandToolsKubectlOutcome.test.ts +639 -0
  358. package/Tests/Server/Utils/AI/RemediationCommandToolsLiveCluster.test.ts +1585 -0
  359. package/Tests/Server/Utils/AI/RemediationCommandToolsRunnerScope.test.ts +1065 -0
  360. package/Tests/Server/Utils/AI/RemediationExecutionRunner.test.ts +2138 -1
  361. package/Tests/Server/Utils/AI/RemediationExecutionRunnerClusterMode.test.ts +1035 -0
  362. package/Tests/Server/Utils/AI/RemediationKubectlCopyParity.test.ts +517 -0
  363. package/Tests/Server/Utils/AI/ResourceTools.test.ts +27 -1
  364. package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutor.test.ts +1371 -24
  365. package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutorRollbackRace.test.ts +962 -0
  366. package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutorUnknownRun.test.ts +518 -0
  367. package/Tests/Server/Utils/AutoRemediation/CommandPlanVerification.test.ts +939 -2
  368. package/Tests/Server/Utils/Database/ProjectScopedRelationReferences.test.ts +371 -0
  369. package/Tests/Server/Utils/Monitor/AffectedResourceList.test.ts +64 -0
  370. package/Tests/Server/Utils/Monitor/DatabaseMetricObservationRemediation.test.ts +102 -0
  371. package/Tests/Server/Utils/Monitor/MonitorAlertCriteriaReferences.test.ts +362 -0
  372. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorBreachingSamples.test.ts +55 -0
  373. package/Tests/Server/Utils/Monitor/MonitorEmailAddressRedaction.test.ts +255 -0
  374. package/Tests/Server/Utils/Monitor/MonitorIncidentCriteriaReferences.test.ts +347 -0
  375. package/Tests/Server/Utils/Monitor/MonitorIncidentMemberRoleMembership.test.ts +203 -0
  376. package/Tests/Server/Utils/Monitor/ProbeMonitorsNotification.test.ts +1147 -0
  377. package/Tests/Server/Utils/PushNotificationUtilCreators.test.ts +79 -0
  378. package/Tests/Server/Utils/ResourceFeed/ResourceFeedUtil.test.ts +21 -1
  379. package/Tests/Server/Utils/Rules/OwnerRuleAssignment.test.ts +155 -1
  380. package/Tests/Server/Utils/SessionReplay/SessionReplayReadServiceQueries.test.ts +288 -0
  381. package/Tests/Server/Utils/SessionReplay/SessionReplayUserFlowReadService.test.ts +304 -0
  382. package/Tests/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.test.ts +40 -3
  383. package/Tests/Server/Utils/Workflow/OAuth2TokenClient.test.ts +702 -0
  384. package/Tests/Server/Utils/Workflow/OAuth2TokenClientHttpIntegration.test.ts +507 -0
  385. package/Tests/Server/Utils/Workflow/WorkflowVariableOAuthToken.test.ts +831 -0
  386. package/Tests/Server/Utils/Workspace/MicrosoftTeamsActionAuthorization.test.ts +63 -1
  387. package/Tests/Server/Utils/Workspace/MicrosoftTeamsAppAccessToken.test.ts +256 -0
  388. package/Tests/Server/Utils/Workspace/MicrosoftTeamsChannelSend.test.ts +60 -0
  389. package/Tests/Server/Utils/Workspace/MicrosoftTeamsChatActionAuthorization.test.ts +442 -0
  390. package/Tests/Server/Utils/Workspace/MicrosoftTeamsChats.test.ts +44 -2
  391. package/Tests/Server/Utils/Workspace/MicrosoftTeamsDirectMessage.test.ts +55 -0
  392. package/Tests/Server/Utils/Workspace/MicrosoftTeamsReactionNoteSync.test.ts +1522 -0
  393. package/Tests/Server/Utils/Workspace/MicrosoftTeamsServiceUrl.test.ts +130 -0
  394. package/Tests/Server/Utils/Workspace/MicrosoftTeamsTeamInstalls.test.ts +4 -4
  395. package/Tests/Server/Utils/Workspace/Slack/SlackActionAuthorization.test.ts +691 -0
  396. package/Tests/Server/Utils/Workspace/Slack/SlackMessageByTimestamp.test.ts +253 -0
  397. package/Tests/Server/Utils/Workspace/Slack/SlackReactionNote.test.ts +574 -0
  398. package/Tests/Server/Utils/Workspace/WorkspaceActionAuthorization.test.ts +488 -0
  399. package/Tests/Server/Utils/Workspace/WorkspaceCreateProjectReferences.test.ts +861 -0
  400. package/Tests/Server/Utils/Workspace/WorkspaceMessageAuthor.test.ts +635 -0
  401. package/Tests/Server/Utils/Workspace/WorkspaceOAuthState.test.ts +548 -0
  402. package/Tests/Server/Utils/Workspace/WorkspaceReactionNote.test.ts +839 -0
  403. package/Tests/Server/Utils/Workspace/WorkspaceSendErrorMessage.test.ts +725 -0
  404. package/Tests/Types/AutoRemediation/AiRemediationCommandPlan.test.ts +283 -9
  405. package/Tests/Types/DateBrowserLocalWallClock.test.ts +280 -0
  406. package/Tests/Types/DateLocalShortMonthNameFallback.test.ts +389 -0
  407. package/Tests/Types/DateUserTimezone.test.ts +177 -4
  408. package/Tests/Types/Kubernetes/KubernetesAgentChartAiAccess.test.ts +306 -0
  409. package/Tests/Types/Kubernetes/KubernetesClusterAiAccess.test.ts +283 -0
  410. package/Tests/Types/Kubernetes/KubernetesRunnerPostureWriteScope.test.ts +68 -0
  411. package/Tests/Types/Monitor/DatabaseMonitor/SqlServerPlatform.test.ts +144 -0
  412. package/Tests/Types/Monitor/MonitorStepExceptionMonitor.test.ts +160 -0
  413. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCoverage.test.ts +53 -0
  414. package/Tests/Types/Runbook/RunbookStepType.test.ts +91 -16
  415. package/Tests/Types/Runner/RunnerLiveStatus.test.ts +14 -3
  416. package/Tests/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.test.ts +15 -0
  417. package/Tests/Types/StatusPage/PublicNoteSubscriberNotificationDefault.test.ts +1868 -0
  418. package/Tests/Types/Timezone.test.ts +5 -0
  419. package/Tests/Types/TimezoneAlias.test.ts +1034 -0
  420. package/Tests/Types/Workflow/JiraTemplateFixtures.ts +833 -0
  421. package/Tests/Types/Workflow/JiraTemplateScripts.test.ts +5414 -0
  422. package/Tests/Types/Workflow/JiraTemplateSimulation.test.ts +4803 -0
  423. package/Tests/Types/Workflow/JiraTemplates.test.ts +5429 -0
  424. package/Tests/Types/Workflow/Templates.test.ts +79 -4
  425. package/Tests/Types/Workflow/WorkflowVariableOAuth.test.ts +520 -0
  426. package/Tests/Types/Workspace/WorkspaceNoteReaction.test.ts +177 -0
  427. package/Tests/UI/Components/AttributesJSON/AttributesJSONView.test.tsx +390 -0
  428. package/Tests/UI/Components/AttributesJSON/CopyAttributesAsJSONButton.test.tsx +566 -0
  429. package/Tests/UI/Components/AttributesJSON/JSONHighlight.test.ts +131 -0
  430. package/Tests/UI/Components/BasicFormDropdownNormalization.test.tsx +276 -1
  431. package/Tests/UI/Components/Calendar/Calendar.test.tsx +1611 -0
  432. package/Tests/UI/Components/Calendar/CalendarDayLayout.test.ts +1486 -0
  433. package/Tests/UI/Components/Calendar/CalendarDayLayoutDst.test.ts +632 -0
  434. package/Tests/UI/Components/Calendar/CalendarDst.test.tsx +388 -0
  435. package/Tests/UI/Components/Charts/ChartGroupPlotFloor.test.tsx +170 -0
  436. package/Tests/UI/Components/Charts/HistogramSelection.test.ts +282 -0
  437. package/Tests/UI/Components/CustomTimeRangeModal.test.tsx +6 -0
  438. package/Tests/UI/Components/CustomTimeRangeModalTimezoneTranslation.test.tsx +285 -0
  439. package/Tests/UI/Components/Dropdown.test.tsx +999 -2
  440. package/Tests/UI/Components/Forms/FormFieldCheckboxTranslation.test.tsx +216 -0
  441. package/Tests/UI/Components/Forms/FormFieldTimezoneDescriptionTranslation.test.tsx +461 -0
  442. package/Tests/UI/Components/Forms/IncidentEpisodePublicNoteNotifyPayload.test.tsx +641 -0
  443. package/Tests/UI/Components/Forms/IncidentPublicNoteNotifyPayload.test.tsx +346 -0
  444. package/Tests/UI/Components/Forms/ScheduledMaintenancePublicNoteNotifyPayload.test.tsx +493 -0
  445. package/Tests/UI/Components/Graphs/DayUptimeGraph.test.tsx +911 -2
  446. package/Tests/UI/Components/Graphs/DayUptimeGraphTimezone.NewYork.test.tsx +730 -0
  447. package/Tests/UI/Components/Graphs/DayUptimeGraphTimezone.Tokyo.test.tsx +506 -0
  448. package/Tests/UI/Components/HistogramClickToZoom.test.tsx +619 -0
  449. package/Tests/UI/Components/LogDetailsPanelAttributesJSON.test.tsx +248 -0
  450. package/Tests/UI/Components/LogsViewerHistogramZoom.test.tsx +47 -1
  451. package/Tests/UI/Components/LogsViewerSmallScreens.test.tsx +620 -0
  452. package/Tests/UI/Components/ModelPageModelChange.test.tsx +242 -3
  453. package/Tests/UI/Components/ModelTable/BaseModelTableDeleteConfirmation.test.tsx +521 -0
  454. package/Tests/UI/Components/MonitorGraphs/Uptime.test.tsx +406 -2
  455. package/Tests/UI/Components/MonitorGraphs/UptimeBarDayModal.test.tsx +169 -2
  456. package/Tests/UI/Components/TelemetryViewer/TelemetryViewerHistogramClickZoom.test.tsx +266 -0
  457. package/Tests/UI/Components/TelemetryViewer/TelemetryViewerSmallScreens.test.tsx +439 -0
  458. package/Tests/UI/Components/TimezoneDropdownLegacyValues.test.tsx +704 -0
  459. package/Tests/UI/Components/UseHistogramZoom.test.tsx +89 -1
  460. package/Tests/UI/Components/UseLogsHistogram.test.tsx +175 -0
  461. package/Tests/UI/Monitor/MonitorSummarySnapshotRender.test.tsx +5 -5
  462. package/Tests/UI/Telemetry/TelemetryDetailPanel.test.tsx +28 -0
  463. package/Tests/UI/Utils/ProjectUsersModelAPI.test.ts +51 -0
  464. package/Tests/UI/Utils/Timezone.test.ts +747 -33
  465. package/Tests/UI/Workspace/SendTestNotificationButton.test.tsx +1854 -0
  466. package/Tests/UI/Workspace/WorkspaceCardsSendTestNotification.test.tsx +1864 -0
  467. package/Tests/Utils/AI/InvestigationReport.test.ts +435 -7
  468. package/Tests/Utils/AiRemediation/KubectlAllowlistValidatorParity.test.ts +1084 -0
  469. package/Tests/Utils/AiRemediation/KubectlOutputRedactor.test.ts +2531 -0
  470. package/Tests/Utils/AiRemediation/KubectlPolicy.test.ts +1895 -0
  471. package/Tests/Utils/AiRemediation/KubectlPolicyAllowlist.test.ts +428 -0
  472. package/Tests/Utils/AiRemediation/KubectlPolicyChartRbacParity.test.ts +828 -0
  473. package/Tests/Utils/AiRemediation/KubectlPolicyCustomResourceScope.test.ts +573 -0
  474. package/Tests/Utils/AiRemediation/KubectlPolicyDenials.test.ts +1525 -0
  475. package/Tests/Utils/AiRemediation/KubectlPolicyExposeOverrides.test.ts +275 -0
  476. package/Tests/Utils/AiRemediation/KubectlPolicyNodePatch.test.ts +428 -0
  477. package/Tests/Utils/AiRemediation/KubectlPolicyObjectWords.test.ts +195 -0
  478. package/Tests/Utils/AiRemediation/KubectlPolicyPatchParents.test.ts +637 -0
  479. package/Tests/Utils/AiRemediation/KubectlPolicyPodSecurityBaseline.test.ts +322 -0
  480. package/Tests/Utils/AiRemediation/KubectlPolicySafeWriteScope.test.ts +1026 -0
  481. package/Tests/Utils/AiRemediation/KubectlWaitBudget.test.ts +267 -0
  482. package/Tests/Utils/AiRemediation/KubectlWriteScope.test.ts +390 -0
  483. package/Tests/Utils/AiRemediation/KubectlWriteScopeCallerParity.test.ts +777 -0
  484. package/Tests/Utils/AiRemediation/KubectlWriteScopeClusterScoped.test.ts +613 -0
  485. package/Tests/Utils/AiRemediation/KubectlWriteScopeEntryPoint.test.ts +576 -0
  486. package/Tests/Utils/AiRemediation/KubectlWriteScopeKubectlSpellings.test.ts +752 -0
  487. package/Tests/Utils/AiRemediation/KubectlWriteScopeObjectWords.test.ts +358 -0
  488. package/Tests/Utils/AiRemediation/KubectlWriteScopeOverrides.test.ts +293 -0
  489. package/Tests/Utils/AiRemediation/KubectlWriteScopeParityCases.ts +598 -0
  490. package/Tests/Utils/AiRemediation/KubectlWriteScopePolicyParity.test.ts +403 -0
  491. package/Tests/Utils/Dashboard/DashboardClockComponent.test.ts +154 -5
  492. package/Tests/Utils/Monitor/IncomingEmailMonitorAddress.test.ts +478 -0
  493. package/Tests/Utils/Monitor/MonitorCheckScheduleUtil.test.ts +646 -0
  494. package/Tests/Utils/Monitor/MonitorOverviewCriteriaUtil.test.ts +292 -0
  495. package/Tests/Utils/Monitor/MonitorOverviewFamily.test.ts +274 -0
  496. package/Tests/Utils/Monitor/MonitorOverviewPresentationUtil.test.ts +3100 -0
  497. package/Tests/Utils/Monitor/MonitorOverviewProbeUtil.test.ts +1412 -0
  498. package/Tests/Utils/Monitor/MonitorOverviewTargetUtil.test.ts +374 -0
  499. package/Tests/Utils/Monitor/MonitorStatusHistoryUtil.test.ts +332 -0
  500. package/Tests/Utils/Monitor/MonitorUptimeSummaryUtil.test.ts +946 -0
  501. package/Tests/Utils/Rum/SessionIdentity.test.ts +94 -0
  502. package/Tests/Utils/Rum/SessionReplayWafCompatibility.test.ts +840 -0
  503. package/Tests/Utils/Rum/SessionReplayWireEncoding.test.ts +208 -0
  504. package/Tests/Utils/Rum/UserFlow.test.ts +1010 -0
  505. package/Tests/Utils/StatusPage/MonitorGroupMergedDowntimeUtil.test.ts +217 -0
  506. package/Tests/Utils/StatusPage/ResourceUptime.test.ts +1272 -1
  507. package/Tests/Utils/StatusPage/UptimeDailyAggregateTotals.test.ts +298 -0
  508. package/Tests/Utils/StatusPage/UptimeDailyAggregateUtil.test.ts +649 -1
  509. package/Tests/Utils/StatusPage/UptimeDailyAggregateWindow.test.ts +314 -0
  510. package/Tests/Utils/Telemetry/AttributesJSON.test.ts +454 -0
  511. package/Tests/Utils/Uptime/DayUptimeGraphUtil.test.ts +418 -2
  512. package/Tests/Utils/Uptime/DayUptimeGraphUtilMidnightDst.test.ts +160 -0
  513. package/Tests/Utils/Uptime/DayUptimeGraphUtilTimezone.Auckland.test.ts +45 -0
  514. package/Tests/Utils/Uptime/DayUptimeGraphUtilTimezone.Kolkata.test.ts +40 -0
  515. package/Tests/Utils/Uptime/DayUptimeGraphUtilTimezone.London.test.ts +147 -0
  516. package/Tests/Utils/Uptime/DayUptimeGraphUtilTimezone.NewYork.test.ts +48 -0
  517. package/Tests/Utils/Uptime/DayUptimeGraphUtilTimezone.Tokyo.test.ts +39 -0
  518. package/Tests/Utils/Uptime/DayUptimeGraphUtilTimezoneCases.ts +1015 -0
  519. package/Tests/Utils/Uptime/UptimeUtil.test.ts +75 -0
  520. package/Tests/Utils/Uptime/UptimeUtilChronologicalOrder.test.ts +1279 -0
  521. package/Types/AutoRemediation/AiRemediationCommandPlan.ts +203 -5
  522. package/Types/Date.ts +121 -7
  523. package/Types/Email/EmailMessage.ts +8 -0
  524. package/Types/Email/EmailTemplateType.ts +1 -0
  525. package/Types/Exception/ExceptionMessages.ts +1 -0
  526. package/Types/Kubernetes/KubernetesClusterAiAccess.ts +488 -0
  527. package/Types/Kubernetes/KubernetesClusterAiAccessPermissions.ts +54 -0
  528. package/Types/Kubernetes/KubernetesClusterAiAccessToolNames.ts +17 -0
  529. package/Types/Monitor/DatabaseMonitor/DatabaseMonitorResponse.ts +9 -0
  530. package/Types/Monitor/DatabaseMonitor/SqlServerPlatform.ts +117 -0
  531. package/Types/Monitor/KubernetesAlertTemplates.ts +6 -5
  532. package/Types/Monitor/MonitorStepExceptionMonitor.ts +56 -0
  533. package/Types/Monitor/MonitorUptimeSummary.ts +113 -0
  534. package/Types/Monitor/Recommendation/MonitorRecommendationUtil.ts +11 -0
  535. package/Types/Rum/SessionReplay.ts +55 -2
  536. package/Types/Rum/SessionReplayApi.ts +44 -1
  537. package/Types/Rum/UserFlow.ts +187 -0
  538. package/Types/Runbook/RunbookStepType.ts +10 -0
  539. package/Types/Runbook/RunnerJobOrigin.ts +15 -0
  540. package/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.ts +6 -2
  541. package/Types/StatusPage/MergedDowntimeTotals.ts +17 -0
  542. package/Types/StatusPage/PublicNoteSubscriberNotificationDefault.ts +148 -0
  543. package/Types/StatusPage/UptimeDailyAggregate.ts +15 -0
  544. package/Types/Telemetry/TelemetryIngestSurface.ts +9 -0
  545. package/Types/Timezone.ts +5 -0
  546. package/Types/TimezoneAlias.ts +385 -0
  547. package/Types/Workflow/Templates.ts +3041 -2
  548. package/Types/Workflow/WorkflowVariableOAuth.ts +476 -0
  549. package/Types/Workspace/WorkspaceNoteReaction.ts +132 -0
  550. package/Typings/Index.d.ts +24 -0
  551. package/UI/Components/AttributesJSON/AttributesJSONPreferences.ts +175 -0
  552. package/UI/Components/AttributesJSON/AttributesJSONView.tsx +138 -0
  553. package/UI/Components/AttributesJSON/AttributesViewToggle.tsx +79 -0
  554. package/UI/Components/AttributesJSON/BracesGlyph.tsx +30 -0
  555. package/UI/Components/AttributesJSON/CopyAttributesAsJSONButton.tsx +491 -0
  556. package/UI/Components/AttributesJSON/JSONHighlight.ts +77 -0
  557. package/UI/Components/Calendar/Calendar.css +83 -0
  558. package/UI/Components/Calendar/Calendar.tsx +136 -7
  559. package/UI/Components/Calendar/CalendarDayLayout.ts +236 -0
  560. package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +23 -1
  561. package/UI/Components/Charts/Utils/HistogramSelection.ts +117 -0
  562. package/UI/Components/Charts/Utils/useHistogramRangeSelection.ts +269 -0
  563. package/UI/Components/Charts/Utils/useHistogramZoom.ts +26 -2
  564. package/UI/Components/Date/CustomTimeRangeModal.tsx +26 -3
  565. package/UI/Components/Dropdown/Dropdown.tsx +58 -9
  566. package/UI/Components/Forms/Fields/FormField.tsx +49 -14
  567. package/UI/Components/Graphs/DayUptimeGraph.tsx +166 -25
  568. package/UI/Components/Graphs/UptimeBarTooltip.tsx +10 -2
  569. package/UI/Components/Graphs/UptimeDaySummary.tsx +8 -0
  570. package/UI/Components/LogsViewer/LogsViewer.tsx +52 -5
  571. package/UI/Components/LogsViewer/components/ColumnSelector.tsx +13 -2
  572. package/UI/Components/LogsViewer/components/KeyboardShortcutsHelp.tsx +13 -1
  573. package/UI/Components/LogsViewer/components/LogDetailsPanel.tsx +91 -76
  574. package/UI/Components/LogsViewer/components/LogsFacetSidebar.tsx +15 -1
  575. package/UI/Components/LogsViewer/components/LogsHistogram.tsx +28 -106
  576. package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +54 -6
  577. package/UI/Components/LogsViewer/components/SavedViewsDropdown.tsx +8 -2
  578. package/UI/Components/LogsViewer/useLogsHistogram.ts +39 -6
  579. package/UI/Components/ModelTable/BaseModelTable.tsx +66 -14
  580. package/UI/Components/MonitorGraphs/Uptime.tsx +12 -0
  581. package/UI/Components/MonitorGraphs/UptimeBarDayModal.tsx +10 -2
  582. package/UI/Components/Page/ModelPage.tsx +59 -3
  583. package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +69 -5
  584. package/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.tsx +12 -1
  585. package/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.tsx +15 -1
  586. package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +36 -104
  587. package/UI/Components/TelemetryViewer/components/TelemetrySearchBar.tsx +9 -1
  588. package/UI/Utils/ModelAPI/ProjectUsersModelAPI.ts +19 -1
  589. package/UI/Utils/Timezone.ts +69 -28
  590. package/Utils/AI/InvestigationReport.ts +195 -25
  591. package/Utils/AiRemediation/KubectlOutputRedactor.ts +1724 -0
  592. package/Utils/AiRemediation/KubectlPolicy.ts +4136 -0
  593. package/Utils/AiRemediation/KubectlWaitBudget.ts +156 -0
  594. package/Utils/AiRemediation/KubectlWriteScope.ts +1964 -0
  595. package/Utils/Dashboard/Components/DashboardClockComponent.ts +7 -6
  596. package/Utils/Monitor/IncomingEmailMonitorAddress.ts +292 -0
  597. package/Utils/Monitor/MonitorCheckScheduleUtil.ts +654 -0
  598. package/Utils/Monitor/MonitorOverviewCriteriaUtil.ts +188 -0
  599. package/Utils/Monitor/MonitorOverviewFamily.ts +263 -0
  600. package/Utils/Monitor/MonitorOverviewPresentationUtil.ts +1958 -0
  601. package/Utils/Monitor/MonitorOverviewProbeUtil.ts +979 -0
  602. package/Utils/Monitor/MonitorOverviewTargetUtil.ts +299 -0
  603. package/Utils/Monitor/MonitorStatusHistoryUtil.ts +201 -0
  604. package/Utils/Monitor/MonitorUptimeSummaryUtil.ts +638 -0
  605. package/Utils/Rum/SessionIdentity.ts +44 -0
  606. package/Utils/Rum/SessionReplayWireEncoding.ts +137 -0
  607. package/Utils/Rum/UserFlow.ts +1527 -0
  608. package/Utils/StatusPage/MonitorGroupMergedDowntimeUtil.ts +107 -0
  609. package/Utils/StatusPage/ResourceUptime.ts +99 -0
  610. package/Utils/StatusPage/UptimeDailyAggregateUtil.ts +197 -0
  611. package/Utils/Telemetry/AttributesJSON.ts +367 -0
  612. package/Utils/Uptime/DayUptimeGraphUtil.ts +165 -0
  613. package/Utils/Uptime/UptimeUtil.ts +122 -38
  614. package/build/dist/Models/DatabaseModels/AlertEpisodeMember.js +8 -1
  615. package/build/dist/Models/DatabaseModels/AlertEpisodeMember.js.map +1 -1
  616. package/build/dist/Models/DatabaseModels/AutoRemediationSuggestion.js +62 -0
  617. package/build/dist/Models/DatabaseModels/AutoRemediationSuggestion.js.map +1 -1
  618. package/build/dist/Models/DatabaseModels/IncidentEpisodeMember.js +8 -1
  619. package/build/dist/Models/DatabaseModels/IncidentEpisodeMember.js.map +1 -1
  620. package/build/dist/Models/DatabaseModels/IncidentEpisodePublicNote.js +10 -3
  621. package/build/dist/Models/DatabaseModels/IncidentEpisodePublicNote.js.map +1 -1
  622. package/build/dist/Models/DatabaseModels/IncidentPublicNote.js +10 -3
  623. package/build/dist/Models/DatabaseModels/IncidentPublicNote.js.map +1 -1
  624. package/build/dist/Models/DatabaseModels/KubernetesCluster.js +434 -0
  625. package/build/dist/Models/DatabaseModels/KubernetesCluster.js.map +1 -1
  626. package/build/dist/Models/DatabaseModels/Monitor.js +49 -0
  627. package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
  628. package/build/dist/Models/DatabaseModels/Runner.js +4 -4
  629. package/build/dist/Models/DatabaseModels/Runner.js.map +1 -1
  630. package/build/dist/Models/DatabaseModels/RunnerJob.js +66 -0
  631. package/build/dist/Models/DatabaseModels/RunnerJob.js.map +1 -1
  632. package/build/dist/Models/DatabaseModels/ScheduledMaintenancePublicNote.js +11 -3
  633. package/build/dist/Models/DatabaseModels/ScheduledMaintenancePublicNote.js.map +1 -1
  634. package/build/dist/Models/DatabaseModels/WorkflowVariable.js +487 -3
  635. package/build/dist/Models/DatabaseModels/WorkflowVariable.js.map +1 -1
  636. package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js +64 -16
  637. package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
  638. package/build/dist/Server/API/AIInvestigationAPI.js +205 -0
  639. package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -1
  640. package/build/dist/Server/API/AutoRemediationAPI.js +115 -2
  641. package/build/dist/Server/API/AutoRemediationAPI.js.map +1 -1
  642. package/build/dist/Server/API/CommonAPI.js +49 -0
  643. package/build/dist/Server/API/CommonAPI.js.map +1 -1
  644. package/build/dist/Server/API/KubernetesClusterAiAccessAPI.js +564 -0
  645. package/build/dist/Server/API/KubernetesClusterAiAccessAPI.js.map +1 -0
  646. package/build/dist/Server/API/MicrosoftTeamsAPI.js +465 -176
  647. package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
  648. package/build/dist/Server/API/MonitorAPI.js +107 -1
  649. package/build/dist/Server/API/MonitorAPI.js.map +1 -1
  650. package/build/dist/Server/API/OnCallScheduleTimelineAPI.js +5 -16
  651. package/build/dist/Server/API/OnCallScheduleTimelineAPI.js.map +1 -1
  652. package/build/dist/Server/API/SlackAPI.js +322 -78
  653. package/build/dist/Server/API/SlackAPI.js.map +1 -1
  654. package/build/dist/Server/API/StatusPageAPI.js +123 -17
  655. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  656. package/build/dist/Server/API/TelemetryAPI.js +144 -5
  657. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  658. package/build/dist/Server/API/UserTwoFactorBackupCodeAPI.js +1 -0
  659. package/build/dist/Server/API/UserTwoFactorBackupCodeAPI.js.map +1 -1
  660. package/build/dist/Server/API/WorkflowVariableAPI.js +109 -0
  661. package/build/dist/Server/API/WorkflowVariableAPI.js.map +1 -0
  662. package/build/dist/Server/Infrastructure/GlobalCache.js +29 -0
  663. package/build/dist/Server/Infrastructure/GlobalCache.js.map +1 -1
  664. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794300000000-AddOAuth2WorkflowVariables.js +38 -0
  665. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794300000000-AddOAuth2WorkflowVariables.js.map +1 -0
  666. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794400000000-AddKubernetesClusterAiAccess.js +54 -0
  667. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794400000000-AddKubernetesClusterAiAccess.js.map +1 -0
  668. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794500000000-AddIncomingEmailCustomAddressToMonitor.js +14 -0
  669. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794500000000-AddIncomingEmailCustomAddressToMonitor.js.map +1 -0
  670. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794600000000-AddAuthTokenExpiresAtToWorkspaceProjectAuthToken.js +12 -0
  671. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794600000000-AddAuthTokenExpiresAtToWorkspaceProjectAuthToken.js.map +1 -0
  672. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794700000000-RemoveAppAccessTokenFromWorkspaceProjectAuthTokenMiscData.js +32 -0
  673. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794700000000-RemoveAppAccessTokenFromWorkspaceProjectAuthTokenMiscData.js.map +1 -0
  674. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +10 -0
  675. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  676. package/build/dist/Server/Middleware/MasterAdminAuthorization.js +21 -0
  677. package/build/dist/Server/Middleware/MasterAdminAuthorization.js.map +1 -1
  678. package/build/dist/Server/Middleware/TelemetryIngest.js +32 -2
  679. package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
  680. package/build/dist/Server/Middleware/UserAuthorization.js +39 -6
  681. package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
  682. package/build/dist/Server/Services/AIAgentService.js +1 -0
  683. package/build/dist/Server/Services/AIAgentService.js.map +1 -1
  684. package/build/dist/Server/Services/AccessTokenService.js +87 -42
  685. package/build/dist/Server/Services/AccessTokenService.js.map +1 -1
  686. package/build/dist/Server/Services/AlertEpisodeMemberService.js +4 -0
  687. package/build/dist/Server/Services/AlertEpisodeMemberService.js.map +1 -1
  688. package/build/dist/Server/Services/AlertEpisodeService.js +12 -1
  689. package/build/dist/Server/Services/AlertEpisodeService.js.map +1 -1
  690. package/build/dist/Server/Services/AlertService.js +110 -23
  691. package/build/dist/Server/Services/AlertService.js.map +1 -1
  692. package/build/dist/Server/Services/AutoRemediationRuleEngineService.js +716 -30
  693. package/build/dist/Server/Services/AutoRemediationRuleEngineService.js.map +1 -1
  694. package/build/dist/Server/Services/AutoRemediationRuleService.js +100 -0
  695. package/build/dist/Server/Services/AutoRemediationRuleService.js.map +1 -1
  696. package/build/dist/Server/Services/BillingService.js +1 -0
  697. package/build/dist/Server/Services/BillingService.js.map +1 -1
  698. package/build/dist/Server/Services/InboundEmail/Providers/SendGridInboundProvider.js +11 -16
  699. package/build/dist/Server/Services/InboundEmail/Providers/SendGridInboundProvider.js.map +1 -1
  700. package/build/dist/Server/Services/IncidentEpisodeMemberService.js +4 -0
  701. package/build/dist/Server/Services/IncidentEpisodeMemberService.js.map +1 -1
  702. package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js +48 -0
  703. package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js.map +1 -1
  704. package/build/dist/Server/Services/IncidentEpisodeRoleMemberService.js +21 -0
  705. package/build/dist/Server/Services/IncidentEpisodeRoleMemberService.js.map +1 -1
  706. package/build/dist/Server/Services/IncidentEpisodeService.js +12 -1
  707. package/build/dist/Server/Services/IncidentEpisodeService.js.map +1 -1
  708. package/build/dist/Server/Services/IncidentGroupingEngineService.js +8 -0
  709. package/build/dist/Server/Services/IncidentGroupingEngineService.js.map +1 -1
  710. package/build/dist/Server/Services/IncidentMemberService.js +14 -0
  711. package/build/dist/Server/Services/IncidentMemberService.js.map +1 -1
  712. package/build/dist/Server/Services/IncidentPublicNoteService.js +48 -0
  713. package/build/dist/Server/Services/IncidentPublicNoteService.js.map +1 -1
  714. package/build/dist/Server/Services/IncidentService.js +99 -21
  715. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  716. package/build/dist/Server/Services/IncidentStateTimelineService.js +12 -0
  717. package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
  718. package/build/dist/Server/Services/IncidentTemplateService.js +68 -4
  719. package/build/dist/Server/Services/IncidentTemplateService.js.map +1 -1
  720. package/build/dist/Server/Services/KubernetesClusterAiAccessService.js +1470 -0
  721. package/build/dist/Server/Services/KubernetesClusterAiAccessService.js.map +1 -0
  722. package/build/dist/Server/Services/KubernetesClusterService.js +1097 -0
  723. package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
  724. package/build/dist/Server/Services/LogAggregationService.js +14 -6
  725. package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
  726. package/build/dist/Server/Services/MonitorService.js +487 -48
  727. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  728. package/build/dist/Server/Services/MonitorStatusTimelineService.js +540 -3
  729. package/build/dist/Server/Services/MonitorStatusTimelineService.js.map +1 -1
  730. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +2 -0
  731. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
  732. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleTeamService.js +2 -0
  733. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleTeamService.js.map +1 -1
  734. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleUserService.js +2 -0
  735. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleUserService.js.map +1 -1
  736. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js +3 -0
  737. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js.map +1 -1
  738. package/build/dist/Server/Services/OnCallNotificationAlertingService.js +29 -7
  739. package/build/dist/Server/Services/OnCallNotificationAlertingService.js.map +1 -1
  740. package/build/dist/Server/Services/OnCallSetupReminderService.js +1 -0
  741. package/build/dist/Server/Services/OnCallSetupReminderService.js.map +1 -1
  742. package/build/dist/Server/Services/ProbeService.js +7 -1
  743. package/build/dist/Server/Services/ProbeService.js.map +1 -1
  744. package/build/dist/Server/Services/ProjectService.js +11 -1
  745. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  746. package/build/dist/Server/Services/RunbookCredentialService.js +24 -0
  747. package/build/dist/Server/Services/RunbookCredentialService.js.map +1 -1
  748. package/build/dist/Server/Services/RunbookSecretService.js +44 -0
  749. package/build/dist/Server/Services/RunbookSecretService.js.map +1 -1
  750. package/build/dist/Server/Services/RunnerJobService.js +641 -21
  751. package/build/dist/Server/Services/RunnerJobService.js.map +1 -1
  752. package/build/dist/Server/Services/RunnerService.js +344 -5
  753. package/build/dist/Server/Services/RunnerService.js.map +1 -1
  754. package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js +48 -0
  755. package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js.map +1 -1
  756. package/build/dist/Server/Services/ScheduledMaintenanceService.js +102 -20
  757. package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
  758. package/build/dist/Server/Services/ScheduledMaintenanceTemplateService.js +67 -4
  759. package/build/dist/Server/Services/ScheduledMaintenanceTemplateService.js.map +1 -1
  760. package/build/dist/Server/Services/ServiceLevelObjectiveService.js +12 -1
  761. package/build/dist/Server/Services/ServiceLevelObjectiveService.js.map +1 -1
  762. package/build/dist/Server/Services/StatusPagePrivateUserService.js +1 -0
  763. package/build/dist/Server/Services/StatusPagePrivateUserService.js.map +1 -1
  764. package/build/dist/Server/Services/StatusPageService.js +230 -32
  765. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  766. package/build/dist/Server/Services/StatusPageSubscriberService.js +4 -0
  767. package/build/dist/Server/Services/StatusPageSubscriberService.js.map +1 -1
  768. package/build/dist/Server/Services/TeamMemberService.js +270 -11
  769. package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
  770. package/build/dist/Server/Services/TraceAggregationService.js +10 -6
  771. package/build/dist/Server/Services/TraceAggregationService.js.map +1 -1
  772. package/build/dist/Server/Services/UserEmailService.js +1 -0
  773. package/build/dist/Server/Services/UserEmailService.js.map +1 -1
  774. package/build/dist/Server/Services/UserNotificationMethodAdminService.js +1 -0
  775. package/build/dist/Server/Services/UserNotificationMethodAdminService.js.map +1 -1
  776. package/build/dist/Server/Services/UserNotificationRuleAdminService.js +1 -0
  777. package/build/dist/Server/Services/UserNotificationRuleAdminService.js.map +1 -1
  778. package/build/dist/Server/Services/UserNotificationRuleService.js +4 -0
  779. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  780. package/build/dist/Server/Services/UserService.js +107 -1
  781. package/build/dist/Server/Services/UserService.js.map +1 -1
  782. package/build/dist/Server/Services/WorkflowVariableService.js +396 -7
  783. package/build/dist/Server/Services/WorkflowVariableService.js.map +1 -1
  784. package/build/dist/Server/Services/WorkspaceNotificationRuleService.js +463 -7
  785. package/build/dist/Server/Services/WorkspaceNotificationRuleService.js.map +1 -1
  786. package/build/dist/Server/Services/WorkspaceProjectAuthTokenService.js +144 -2
  787. package/build/dist/Server/Services/WorkspaceProjectAuthTokenService.js.map +1 -1
  788. package/build/dist/Server/Types/Database/QueryHelper.js +20 -0
  789. package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
  790. package/build/dist/Server/Types/Markdown.js +394 -27
  791. package/build/dist/Server/Types/Markdown.js.map +1 -1
  792. package/build/dist/Server/Utils/AI/ClusterAccess/ClusterAccessContext.js +123 -0
  793. package/build/dist/Server/Utils/AI/ClusterAccess/ClusterAccessContext.js.map +1 -0
  794. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.js +344 -0
  795. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.js.map +1 -0
  796. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlJobRunner.js +587 -0
  797. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlJobRunner.js.map +1 -0
  798. package/build/dist/Server/Utils/AI/CodeFix/CodeFixAgentAuth.js +18 -2
  799. package/build/dist/Server/Utils/AI/CodeFix/CodeFixAgentAuth.js.map +1 -1
  800. package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js +1380 -239
  801. package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js.map +1 -1
  802. package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js +1099 -68
  803. package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js.map +1 -1
  804. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js +88 -6
  805. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js.map +1 -1
  806. package/build/dist/Server/Utils/AI/SRE/AlertInvestigationRunner.js +45 -11
  807. package/build/dist/Server/Utils/AI/SRE/AlertInvestigationRunner.js.map +1 -1
  808. package/build/dist/Server/Utils/AI/SRE/IncidentInvestigationRunner.js +41 -11
  809. package/build/dist/Server/Utils/AI/SRE/IncidentInvestigationRunner.js.map +1 -1
  810. package/build/dist/Server/Utils/AI/Toolbox/AIActionTools.js +11 -4
  811. package/build/dist/Server/Utils/AI/Toolbox/AIActionTools.js.map +1 -1
  812. package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js +4 -2
  813. package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js.map +1 -1
  814. package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js +997 -141
  815. package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js.map +1 -1
  816. package/build/dist/Server/Utils/AutoRemediation/RemediationVerifier.js +299 -4
  817. package/build/dist/Server/Utils/AutoRemediation/RemediationVerifier.js.map +1 -1
  818. package/build/dist/Server/Utils/Database/AffectedResourceRelations.js +120 -0
  819. package/build/dist/Server/Utils/Database/AffectedResourceRelations.js.map +1 -0
  820. package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js +160 -1
  821. package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js.map +1 -1
  822. package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js +1 -0
  823. package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js.map +1 -1
  824. package/build/dist/Server/Utils/Monitor/MonitorAlert.js +59 -12
  825. package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
  826. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +5 -1
  827. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -1
  828. package/build/dist/Server/Utils/Monitor/MonitorIncident.js +84 -22
  829. package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
  830. package/build/dist/Server/Utils/Monitor/MonitorPayloadRedaction.js +70 -5
  831. package/build/dist/Server/Utils/Monitor/MonitorPayloadRedaction.js.map +1 -1
  832. package/build/dist/Server/Utils/Monitor/ProbeMonitorsNotification.js +249 -0
  833. package/build/dist/Server/Utils/Monitor/ProbeMonitorsNotification.js.map +1 -0
  834. package/build/dist/Server/Utils/OnCall/OnCallShiftReminderRunner.js +1 -0
  835. package/build/dist/Server/Utils/OnCall/OnCallShiftReminderRunner.js.map +1 -1
  836. package/build/dist/Server/Utils/PushNotificationUtil.js +27 -0
  837. package/build/dist/Server/Utils/PushNotificationUtil.js.map +1 -1
  838. package/build/dist/Server/Utils/Rules/OwnerRuleAssignment.js +26 -3
  839. package/build/dist/Server/Utils/Rules/OwnerRuleAssignment.js.map +1 -1
  840. package/build/dist/Server/Utils/Runbook/RunbookExecutePermission.js +38 -3
  841. package/build/dist/Server/Utils/Runbook/RunbookExecutePermission.js.map +1 -1
  842. package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js +143 -23
  843. package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js.map +1 -1
  844. package/build/dist/Server/Utils/SessionReplay/SessionReplayUserFlowReadService.js +406 -0
  845. package/build/dist/Server/Utils/SessionReplay/SessionReplayUserFlowReadService.js.map +1 -0
  846. package/build/dist/Server/Utils/StartServer.js +4 -3
  847. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  848. package/build/dist/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.js +382 -0
  849. package/build/dist/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.js.map +1 -0
  850. package/build/dist/Server/Utils/TwoFactorBackupCodeNotification.js +1 -0
  851. package/build/dist/Server/Utils/TwoFactorBackupCodeNotification.js.map +1 -1
  852. package/build/dist/Server/Utils/UserPermission/UserPermission.js +10 -0
  853. package/build/dist/Server/Utils/UserPermission/UserPermission.js.map +1 -1
  854. package/build/dist/Server/Utils/Workflow/OAuth2TokenClient.js +447 -0
  855. package/build/dist/Server/Utils/Workflow/OAuth2TokenClient.js.map +1 -0
  856. package/build/dist/Server/Utils/Workflow/WorkflowVariableOAuthToken.js +202 -0
  857. package/build/dist/Server/Utils/Workflow/WorkflowVariableOAuthToken.js.map +1 -0
  858. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js +39 -1
  859. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js.map +1 -1
  860. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.js +44 -4
  861. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.js.map +1 -1
  862. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js +150 -191
  863. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js.map +1 -1
  864. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.js +44 -4
  865. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.js.map +1 -1
  866. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Monitor.js +39 -17
  867. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Monitor.js.map +1 -1
  868. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/ScheduledMaintenance.js +151 -145
  869. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/ScheduledMaintenance.js.map +1 -1
  870. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +221 -127
  871. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  872. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsServiceUrl.js +91 -0
  873. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsServiceUrl.js.map +1 -0
  874. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/ReactionNoteSync.js +673 -0
  875. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/ReactionNoteSync.js.map +1 -0
  876. package/build/dist/Server/Utils/Workspace/Slack/Actions/ActionTypes.js +3 -9
  877. package/build/dist/Server/Utils/Workspace/Slack/Actions/ActionTypes.js.map +1 -1
  878. package/build/dist/Server/Utils/Workspace/Slack/Actions/Alert.js +65 -166
  879. package/build/dist/Server/Utils/Workspace/Slack/Actions/Alert.js.map +1 -1
  880. package/build/dist/Server/Utils/Workspace/Slack/Actions/AlertEpisode.js +63 -157
  881. package/build/dist/Server/Utils/Workspace/Slack/Actions/AlertEpisode.js.map +1 -1
  882. package/build/dist/Server/Utils/Workspace/Slack/Actions/Auth.js +23 -3
  883. package/build/dist/Server/Utils/Workspace/Slack/Actions/Auth.js.map +1 -1
  884. package/build/dist/Server/Utils/Workspace/Slack/Actions/Authorization.js +92 -0
  885. package/build/dist/Server/Utils/Workspace/Slack/Actions/Authorization.js.map +1 -0
  886. package/build/dist/Server/Utils/Workspace/Slack/Actions/Incident.js +81 -210
  887. package/build/dist/Server/Utils/Workspace/Slack/Actions/Incident.js.map +1 -1
  888. package/build/dist/Server/Utils/Workspace/Slack/Actions/IncidentEpisode.js +68 -138
  889. package/build/dist/Server/Utils/Workspace/Slack/Actions/IncidentEpisode.js.map +1 -1
  890. package/build/dist/Server/Utils/Workspace/Slack/Actions/ReactionNote.js +240 -0
  891. package/build/dist/Server/Utils/Workspace/Slack/Actions/ReactionNote.js.map +1 -0
  892. package/build/dist/Server/Utils/Workspace/Slack/Actions/ScheduledMaintenance.js +85 -212
  893. package/build/dist/Server/Utils/Workspace/Slack/Actions/ScheduledMaintenance.js.map +1 -1
  894. package/build/dist/Server/Utils/Workspace/Slack/Slack.js +60 -17
  895. package/build/dist/Server/Utils/Workspace/Slack/Slack.js.map +1 -1
  896. package/build/dist/Server/Utils/Workspace/Slack/app-manifest.json +2 -0
  897. package/build/dist/Server/Utils/Workspace/Workspace.js +134 -41
  898. package/build/dist/Server/Utils/Workspace/Workspace.js.map +1 -1
  899. package/build/dist/Server/Utils/Workspace/WorkspaceActionAuthorization.js +197 -0
  900. package/build/dist/Server/Utils/Workspace/WorkspaceActionAuthorization.js.map +1 -0
  901. package/build/dist/Server/Utils/Workspace/WorkspaceBase.js +32 -0
  902. package/build/dist/Server/Utils/Workspace/WorkspaceBase.js.map +1 -1
  903. package/build/dist/Server/Utils/Workspace/WorkspaceOAuthState.js +199 -0
  904. package/build/dist/Server/Utils/Workspace/WorkspaceOAuthState.js.map +1 -0
  905. package/build/dist/Server/Utils/Workspace/WorkspaceProjectReferenceValidator.js +91 -0
  906. package/build/dist/Server/Utils/Workspace/WorkspaceProjectReferenceValidator.js.map +1 -0
  907. package/build/dist/Server/Utils/Workspace/WorkspaceReactionNote.js +450 -0
  908. package/build/dist/Server/Utils/Workspace/WorkspaceReactionNote.js.map +1 -0
  909. package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js +103 -5
  910. package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js.map +1 -1
  911. package/build/dist/Types/Date.js +97 -7
  912. package/build/dist/Types/Date.js.map +1 -1
  913. package/build/dist/Types/Email/EmailTemplateType.js +1 -0
  914. package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
  915. package/build/dist/Types/Exception/ExceptionMessages.js +1 -0
  916. package/build/dist/Types/Exception/ExceptionMessages.js.map +1 -1
  917. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccess.js +245 -0
  918. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccess.js.map +1 -0
  919. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccessPermissions.js +49 -0
  920. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccessPermissions.js.map +1 -0
  921. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccessToolNames.js +16 -0
  922. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccessToolNames.js.map +1 -0
  923. package/build/dist/Types/Monitor/DatabaseMonitor/SqlServerPlatform.js +104 -0
  924. package/build/dist/Types/Monitor/DatabaseMonitor/SqlServerPlatform.js.map +1 -0
  925. package/build/dist/Types/Monitor/KubernetesAlertTemplates.js +6 -5
  926. package/build/dist/Types/Monitor/KubernetesAlertTemplates.js.map +1 -1
  927. package/build/dist/Types/Monitor/MonitorStepExceptionMonitor.js +32 -0
  928. package/build/dist/Types/Monitor/MonitorStepExceptionMonitor.js.map +1 -1
  929. package/build/dist/Types/Monitor/MonitorUptimeSummary.js +52 -0
  930. package/build/dist/Types/Monitor/MonitorUptimeSummary.js.map +1 -0
  931. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationUtil.js +9 -1
  932. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationUtil.js.map +1 -1
  933. package/build/dist/Types/Rum/SessionReplay.js +52 -2
  934. package/build/dist/Types/Rum/SessionReplay.js.map +1 -1
  935. package/build/dist/Types/Rum/SessionReplayApi.js +11 -0
  936. package/build/dist/Types/Rum/SessionReplayApi.js.map +1 -1
  937. package/build/dist/Types/Rum/UserFlow.js +115 -0
  938. package/build/dist/Types/Rum/UserFlow.js.map +1 -0
  939. package/build/dist/Types/Runbook/RunbookStepType.js +10 -0
  940. package/build/dist/Types/Runbook/RunbookStepType.js.map +1 -1
  941. package/build/dist/Types/Runbook/RunnerJobOrigin.js +14 -0
  942. package/build/dist/Types/Runbook/RunnerJobOrigin.js.map +1 -1
  943. package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.js +2 -2
  944. package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.js.map +1 -1
  945. package/build/dist/Types/StatusPage/MergedDowntimeTotals.js +2 -0
  946. package/build/dist/Types/StatusPage/MergedDowntimeTotals.js.map +1 -0
  947. package/build/dist/Types/StatusPage/PublicNoteSubscriberNotificationDefault.js +65 -0
  948. package/build/dist/Types/StatusPage/PublicNoteSubscriberNotificationDefault.js.map +1 -0
  949. package/build/dist/Types/Telemetry/TelemetryIngestSurface.js +8 -0
  950. package/build/dist/Types/Telemetry/TelemetryIngestSurface.js.map +1 -1
  951. package/build/dist/Types/Timezone.js +5 -0
  952. package/build/dist/Types/Timezone.js.map +1 -1
  953. package/build/dist/Types/TimezoneAlias.js +340 -0
  954. package/build/dist/Types/TimezoneAlias.js.map +1 -0
  955. package/build/dist/Types/Workflow/Templates.js +2694 -2
  956. package/build/dist/Types/Workflow/Templates.js.map +1 -1
  957. package/build/dist/Types/Workflow/WorkflowVariableOAuth.js +314 -0
  958. package/build/dist/Types/Workflow/WorkflowVariableOAuth.js.map +1 -0
  959. package/build/dist/Types/Workspace/WorkspaceNoteReaction.js +98 -0
  960. package/build/dist/Types/Workspace/WorkspaceNoteReaction.js.map +1 -0
  961. package/build/dist/UI/Components/AttributesJSON/AttributesJSONPreferences.js +95 -0
  962. package/build/dist/UI/Components/AttributesJSON/AttributesJSONPreferences.js.map +1 -0
  963. package/build/dist/UI/Components/AttributesJSON/AttributesJSONView.js +44 -0
  964. package/build/dist/UI/Components/AttributesJSON/AttributesJSONView.js.map +1 -0
  965. package/build/dist/UI/Components/AttributesJSON/AttributesViewToggle.js +27 -0
  966. package/build/dist/UI/Components/AttributesJSON/AttributesViewToggle.js.map +1 -0
  967. package/build/dist/UI/Components/AttributesJSON/BracesGlyph.js +9 -0
  968. package/build/dist/UI/Components/AttributesJSON/BracesGlyph.js.map +1 -0
  969. package/build/dist/UI/Components/AttributesJSON/CopyAttributesAsJSONButton.js +260 -0
  970. package/build/dist/UI/Components/AttributesJSON/CopyAttributesAsJSONButton.js.map +1 -0
  971. package/build/dist/UI/Components/AttributesJSON/JSONHighlight.js +56 -0
  972. package/build/dist/UI/Components/AttributesJSON/JSONHighlight.js.map +1 -0
  973. package/build/dist/UI/Components/Calendar/Calendar.js +94 -7
  974. package/build/dist/UI/Components/Calendar/Calendar.js.map +1 -1
  975. package/build/dist/UI/Components/Calendar/CalendarDayLayout.js +136 -0
  976. package/build/dist/UI/Components/Calendar/CalendarDayLayout.js.map +1 -0
  977. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +1 -1
  978. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
  979. package/build/dist/UI/Components/Charts/Utils/HistogramSelection.js +72 -0
  980. package/build/dist/UI/Components/Charts/Utils/HistogramSelection.js.map +1 -0
  981. package/build/dist/UI/Components/Charts/Utils/useHistogramRangeSelection.js +178 -0
  982. package/build/dist/UI/Components/Charts/Utils/useHistogramRangeSelection.js.map +1 -0
  983. package/build/dist/UI/Components/Charts/Utils/useHistogramZoom.js +13 -1
  984. package/build/dist/UI/Components/Charts/Utils/useHistogramZoom.js.map +1 -1
  985. package/build/dist/UI/Components/Date/CustomTimeRangeModal.js +20 -4
  986. package/build/dist/UI/Components/Date/CustomTimeRangeModal.js.map +1 -1
  987. package/build/dist/UI/Components/Dropdown/Dropdown.js +33 -9
  988. package/build/dist/UI/Components/Dropdown/Dropdown.js.map +1 -1
  989. package/build/dist/UI/Components/Forms/Fields/FormField.js +36 -13
  990. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  991. package/build/dist/UI/Components/Graphs/DayUptimeGraph.js +117 -11
  992. package/build/dist/UI/Components/Graphs/DayUptimeGraph.js.map +1 -1
  993. package/build/dist/UI/Components/Graphs/UptimeBarTooltip.js +5 -1
  994. package/build/dist/UI/Components/Graphs/UptimeBarTooltip.js.map +1 -1
  995. package/build/dist/UI/Components/Graphs/UptimeDaySummary.js.map +1 -1
  996. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +36 -8
  997. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  998. package/build/dist/UI/Components/LogsViewer/components/ColumnSelector.js +10 -2
  999. package/build/dist/UI/Components/LogsViewer/components/ColumnSelector.js.map +1 -1
  1000. package/build/dist/UI/Components/LogsViewer/components/KeyboardShortcutsHelp.js +9 -1
  1001. package/build/dist/UI/Components/LogsViewer/components/KeyboardShortcutsHelp.js.map +1 -1
  1002. package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js +16 -15
  1003. package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js.map +1 -1
  1004. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js +1 -1
  1005. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js.map +1 -1
  1006. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +14 -74
  1007. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -1
  1008. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +22 -5
  1009. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -1
  1010. package/build/dist/UI/Components/LogsViewer/components/SavedViewsDropdown.js +9 -2
  1011. package/build/dist/UI/Components/LogsViewer/components/SavedViewsDropdown.js.map +1 -1
  1012. package/build/dist/UI/Components/LogsViewer/useLogsHistogram.js +16 -5
  1013. package/build/dist/UI/Components/LogsViewer/useLogsHistogram.js.map +1 -1
  1014. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +23 -8
  1015. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  1016. package/build/dist/UI/Components/MonitorGraphs/Uptime.js +7 -1
  1017. package/build/dist/UI/Components/MonitorGraphs/Uptime.js.map +1 -1
  1018. package/build/dist/UI/Components/MonitorGraphs/UptimeBarDayModal.js +5 -1
  1019. package/build/dist/UI/Components/MonitorGraphs/UptimeBarDayModal.js.map +1 -1
  1020. package/build/dist/UI/Components/Page/ModelPage.js +37 -3
  1021. package/build/dist/UI/Components/Page/ModelPage.js.map +1 -1
  1022. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +27 -6
  1023. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
  1024. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.js +7 -1
  1025. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.js.map +1 -1
  1026. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js +1 -1
  1027. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js.map +1 -1
  1028. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +17 -77
  1029. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
  1030. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchBar.js +10 -1
  1031. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchBar.js.map +1 -1
  1032. package/build/dist/UI/Utils/ModelAPI/ProjectUsersModelAPI.js +15 -1
  1033. package/build/dist/UI/Utils/ModelAPI/ProjectUsersModelAPI.js.map +1 -1
  1034. package/build/dist/UI/Utils/Timezone.js +52 -17
  1035. package/build/dist/UI/Utils/Timezone.js.map +1 -1
  1036. package/build/dist/Utils/AI/InvestigationReport.js +116 -21
  1037. package/build/dist/Utils/AI/InvestigationReport.js.map +1 -1
  1038. package/build/dist/Utils/AiRemediation/KubectlOutputRedactor.js +1209 -0
  1039. package/build/dist/Utils/AiRemediation/KubectlOutputRedactor.js.map +1 -0
  1040. package/build/dist/Utils/AiRemediation/KubectlPolicy.js +2973 -0
  1041. package/build/dist/Utils/AiRemediation/KubectlPolicy.js.map +1 -0
  1042. package/build/dist/Utils/AiRemediation/KubectlWaitBudget.js +99 -0
  1043. package/build/dist/Utils/AiRemediation/KubectlWaitBudget.js.map +1 -0
  1044. package/build/dist/Utils/AiRemediation/KubectlWriteScope.js +1429 -0
  1045. package/build/dist/Utils/AiRemediation/KubectlWriteScope.js.map +1 -0
  1046. package/build/dist/Utils/Dashboard/Components/DashboardClockComponent.js +7 -6
  1047. package/build/dist/Utils/Dashboard/Components/DashboardClockComponent.js.map +1 -1
  1048. package/build/dist/Utils/Monitor/IncomingEmailMonitorAddress.js +214 -0
  1049. package/build/dist/Utils/Monitor/IncomingEmailMonitorAddress.js.map +1 -0
  1050. package/build/dist/Utils/Monitor/MonitorCheckScheduleUtil.js +471 -0
  1051. package/build/dist/Utils/Monitor/MonitorCheckScheduleUtil.js.map +1 -0
  1052. package/build/dist/Utils/Monitor/MonitorOverviewCriteriaUtil.js +107 -0
  1053. package/build/dist/Utils/Monitor/MonitorOverviewCriteriaUtil.js.map +1 -0
  1054. package/build/dist/Utils/Monitor/MonitorOverviewFamily.js +191 -0
  1055. package/build/dist/Utils/Monitor/MonitorOverviewFamily.js.map +1 -0
  1056. package/build/dist/Utils/Monitor/MonitorOverviewPresentationUtil.js +1259 -0
  1057. package/build/dist/Utils/Monitor/MonitorOverviewPresentationUtil.js.map +1 -0
  1058. package/build/dist/Utils/Monitor/MonitorOverviewProbeUtil.js +593 -0
  1059. package/build/dist/Utils/Monitor/MonitorOverviewProbeUtil.js.map +1 -0
  1060. package/build/dist/Utils/Monitor/MonitorOverviewTargetUtil.js +195 -0
  1061. package/build/dist/Utils/Monitor/MonitorOverviewTargetUtil.js.map +1 -0
  1062. package/build/dist/Utils/Monitor/MonitorStatusHistoryUtil.js +127 -0
  1063. package/build/dist/Utils/Monitor/MonitorStatusHistoryUtil.js.map +1 -0
  1064. package/build/dist/Utils/Monitor/MonitorUptimeSummaryUtil.js +464 -0
  1065. package/build/dist/Utils/Monitor/MonitorUptimeSummaryUtil.js.map +1 -0
  1066. package/build/dist/Utils/Rum/SessionIdentity.js +31 -1
  1067. package/build/dist/Utils/Rum/SessionIdentity.js.map +1 -1
  1068. package/build/dist/Utils/Rum/SessionReplayWireEncoding.js +128 -0
  1069. package/build/dist/Utils/Rum/SessionReplayWireEncoding.js.map +1 -0
  1070. package/build/dist/Utils/Rum/UserFlow.js +941 -0
  1071. package/build/dist/Utils/Rum/UserFlow.js.map +1 -0
  1072. package/build/dist/Utils/StatusPage/MonitorGroupMergedDowntimeUtil.js +82 -0
  1073. package/build/dist/Utils/StatusPage/MonitorGroupMergedDowntimeUtil.js.map +1 -0
  1074. package/build/dist/Utils/StatusPage/ResourceUptime.js +46 -0
  1075. package/build/dist/Utils/StatusPage/ResourceUptime.js.map +1 -1
  1076. package/build/dist/Utils/StatusPage/UptimeDailyAggregateUtil.js +148 -0
  1077. package/build/dist/Utils/StatusPage/UptimeDailyAggregateUtil.js.map +1 -1
  1078. package/build/dist/Utils/Telemetry/AttributesJSON.js +245 -0
  1079. package/build/dist/Utils/Telemetry/AttributesJSON.js.map +1 -0
  1080. package/build/dist/Utils/Uptime/DayUptimeGraphUtil.js +103 -0
  1081. package/build/dist/Utils/Uptime/DayUptimeGraphUtil.js.map +1 -1
  1082. package/build/dist/Utils/Uptime/UptimeUtil.js +91 -27
  1083. package/build/dist/Utils/Uptime/UptimeUtil.js.map +1 -1
  1084. package/jest.config.json +2 -1
  1085. package/package.json +2 -1
  1086. package/tsconfig.tests.json +36 -0
  1087. package/Tests/UI/Components/EmptyState/EmptyStateGuideLinks.test.tsx +0 -337
  1088. package/UI/Components/EmptyState/EmptyStateGuideLinks.tsx +0 -124
  1089. package/build/dist/UI/Components/EmptyState/EmptyStateGuideLinks.js +0 -47
  1090. package/build/dist/UI/Components/EmptyState/EmptyStateGuideLinks.js.map +0 -1
@@ -0,0 +1,3227 @@
1
+ import "@testing-library/jest-dom";
2
+ import {
3
+ act,
4
+ cleanup,
5
+ fireEvent,
6
+ render,
7
+ screen,
8
+ waitFor,
9
+ within,
10
+ } from "@testing-library/react";
11
+ import React, { ReactElement, ReactNode } from "react";
12
+ import {
13
+ afterEach,
14
+ beforeEach,
15
+ describe,
16
+ expect,
17
+ jest,
18
+ test,
19
+ } from "@jest/globals";
20
+
21
+ /*
22
+ * A workflow variable's own page (Workflows > Global Variables > View, and
23
+ * Workflow > Workflow Variables > View).
24
+ *
25
+ * The variables list used to do everything in its row actions - Show ID,
26
+ * Update Content, Update Credentials, Edit Details, Delete - and a Token
27
+ * column with its own Refresh now button. The list now only lists and
28
+ * creates; its single View action opens this page, and everything done to one
29
+ * variable happens here. So what is asserted here is that every one of those
30
+ * doors exists on the page, for the right kind of variable, on both the global
31
+ * and the local page, gated the way the list gated them.
32
+ *
33
+ * The cards, the modals, the token status and the delete card are replaced by
34
+ * prop recorders: what is under test is what the page hands them and what it
35
+ * does when their callbacks fire. The real CardModelDetail and ModelDelete
36
+ * fetch on mount.
37
+ */
38
+
39
+ let permissionsForTest: Array<unknown> = [];
40
+ let isMasterAdminForTest: boolean = false;
41
+
42
+ jest.mock("../../../UI/Utils/Permission", () => {
43
+ return {
44
+ __esModule: true,
45
+ default: {
46
+ getAllPermissions: (): Array<unknown> => {
47
+ return permissionsForTest;
48
+ },
49
+ getProjectPermissions: (): null => {
50
+ return null;
51
+ },
52
+ getGlobalPermissions: (): null => {
53
+ return null;
54
+ },
55
+ },
56
+ };
57
+ });
58
+
59
+ jest.mock("../../../UI/Utils/User", () => {
60
+ return {
61
+ __esModule: true,
62
+ default: {
63
+ isMasterAdmin: (): boolean => {
64
+ return isMasterAdminForTest;
65
+ },
66
+ getUserId: (): null => {
67
+ return null;
68
+ },
69
+ },
70
+ };
71
+ });
72
+
73
+ jest.mock("../../../UI/Utils/Project", () => {
74
+ return {
75
+ __esModule: true,
76
+ default: {
77
+ getCurrentProjectId: (): unknown => {
78
+ return PROJECT_ID;
79
+ },
80
+ },
81
+ };
82
+ });
83
+
84
+ const navigate: ReturnType<typeof jest.fn<(to: unknown) => void>> =
85
+ jest.fn<(to: unknown) => void>();
86
+ const getLastParamAsObjectID: ReturnType<
87
+ typeof jest.fn<(index?: number) => unknown>
88
+ > = jest.fn<(index?: number) => unknown>();
89
+
90
+ jest.mock("../../../UI/Utils/Navigation", () => {
91
+ return {
92
+ __esModule: true,
93
+ default: {
94
+ navigate: (to: unknown): void => {
95
+ navigate(to);
96
+ },
97
+ getLastParamAsObjectID: (index?: number): unknown => {
98
+ return getLastParamAsObjectID(index);
99
+ },
100
+ },
101
+ };
102
+ });
103
+
104
+ const getItem: ReturnType<typeof jest.fn<(data: unknown) => Promise<unknown>>> =
105
+ jest.fn<(data: unknown) => Promise<unknown>>();
106
+ const updateById: ReturnType<
107
+ typeof jest.fn<(data: unknown) => Promise<unknown>>
108
+ > = jest.fn<(data: unknown) => Promise<unknown>>();
109
+
110
+ jest.mock("../../../UI/Utils/ModelAPI/ModelAPI", () => {
111
+ return {
112
+ __esModule: true,
113
+ default: {
114
+ getItem: (data: unknown): Promise<unknown> => {
115
+ return getItem(data);
116
+ },
117
+ updateById: (data: unknown): Promise<unknown> => {
118
+ return updateById(data);
119
+ },
120
+ getCommonHeaders: (): Record<string, string> => {
121
+ return { tenantid: "project-header" };
122
+ },
123
+ },
124
+ };
125
+ });
126
+
127
+ const apiPost: ReturnType<typeof jest.fn<(data: unknown) => Promise<unknown>>> =
128
+ jest.fn<(data: unknown) => Promise<unknown>>();
129
+
130
+ jest.mock("../../../UI/Utils/API/API", () => {
131
+ return {
132
+ __esModule: true,
133
+ default: {
134
+ post: (data: unknown): Promise<unknown> => {
135
+ return apiPost(data);
136
+ },
137
+ getFriendlyMessage: (error: unknown): string => {
138
+ return error instanceof Error ? error.message : "Request failed.";
139
+ },
140
+ },
141
+ };
142
+ });
143
+
144
+ /*
145
+ * Every render of a visible loader, so a test can prove the loader never
146
+ * appeared at all - a background reload that flashed it for one render and
147
+ * took it down again would still blank the page for that moment.
148
+ */
149
+ let pageLoaderRenderCount: number = 0;
150
+
151
+ jest.mock("../../../UI/Components/Loader/PageLoader", () => {
152
+ return {
153
+ __esModule: true,
154
+ default: (props: { isVisible: boolean }): ReactElement | null => {
155
+ if (props.isVisible) {
156
+ pageLoaderRenderCount++;
157
+ }
158
+
159
+ return props.isVisible ? <div data-testid="page-loader" /> : null;
160
+ },
161
+ };
162
+ });
163
+
164
+ type CardButtonEntry = {
165
+ title: string;
166
+ disabled?: boolean | undefined;
167
+ tooltip?: string | undefined;
168
+ onClick: () => void;
169
+ };
170
+
171
+ type CapturedCardProps = {
172
+ title?: string | undefined;
173
+ description?: string | undefined;
174
+ buttons?: Array<CardButtonEntry> | undefined;
175
+ children?: ReactNode;
176
+ };
177
+
178
+ let capturedCards: Record<string, CapturedCardProps> = {};
179
+
180
+ /*
181
+ * The card header the mocked Card and CardModelDetail both render: a heading
182
+ * and the card's buttons as real buttons, so a click goes through the same
183
+ * disabled attribute a person would meet.
184
+ */
185
+ function renderMockCard(
186
+ testId: string,
187
+ cardProps: CapturedCardProps,
188
+ ): ReactElement {
189
+ return (
190
+ <section data-testid={testId}>
191
+ <h2>{cardProps.title}</h2>
192
+ {(cardProps.buttons || []).map((button: CardButtonEntry) => {
193
+ return (
194
+ <button
195
+ key={button.title}
196
+ type="button"
197
+ data-testid={`card-button:${button.title}`}
198
+ disabled={Boolean(button.disabled)}
199
+ title={button.tooltip}
200
+ onClick={button.onClick}
201
+ >
202
+ {button.title}
203
+ </button>
204
+ );
205
+ })}
206
+ {cardProps.children}
207
+ </section>
208
+ );
209
+ }
210
+
211
+ jest.mock("../../../UI/Components/Card/Card", () => {
212
+ return {
213
+ __esModule: true,
214
+ default: (props: CapturedCardProps): ReactElement => {
215
+ capturedCards[props.title || ""] = props;
216
+ return renderMockCard(`card:${props.title}`, props);
217
+ },
218
+ };
219
+ });
220
+
221
+ type DetailField = {
222
+ field?: Record<string, unknown> | undefined;
223
+ title?: string | undefined;
224
+ description?: string | undefined;
225
+ getElement?: ((item: unknown) => ReactElement) | undefined;
226
+ };
227
+
228
+ type FormFieldEntry = {
229
+ field?: Record<string, unknown> | undefined;
230
+ title?: string | undefined;
231
+ description?: string | undefined;
232
+ fieldType?: string | undefined;
233
+ required?: unknown;
234
+ };
235
+
236
+ type CapturedDetailProps = {
237
+ name: string;
238
+ cardProps: CapturedCardProps;
239
+ isEditable?: boolean | undefined;
240
+ editButtonText?: string | undefined;
241
+ onSaveSuccess?: ((item: unknown) => void) | undefined;
242
+ formFields?: Array<FormFieldEntry> | undefined;
243
+ modelDetailProps: {
244
+ modelType: unknown;
245
+ modelId: { toString: () => string };
246
+ fields: Array<DetailField>;
247
+ };
248
+ };
249
+
250
+ let capturedDetails: Record<string, CapturedDetailProps> = {};
251
+
252
+ jest.mock("../../../UI/Components/ModelDetail/CardModelDetail", () => {
253
+ return {
254
+ __esModule: true,
255
+ default: (props: CapturedDetailProps): ReactElement => {
256
+ capturedDetails[props.name] = props;
257
+ return renderMockCard(`detail:${props.name}`, props.cardProps);
258
+ },
259
+ };
260
+ });
261
+
262
+ type CapturedDeleteProps = {
263
+ modelType: unknown;
264
+ modelId: { toString: () => string };
265
+ onDeleteSuccess: () => void;
266
+ };
267
+
268
+ let capturedDeleteProps: CapturedDeleteProps | null = null;
269
+
270
+ jest.mock("../../../UI/Components/ModelDelete/ModelDelete", () => {
271
+ return {
272
+ __esModule: true,
273
+ default: (props: CapturedDeleteProps): ReactElement => {
274
+ capturedDeleteProps = props;
275
+ return <div data-testid="model-delete" />;
276
+ },
277
+ };
278
+ });
279
+
280
+ type CapturedContentModalProps = {
281
+ variable: { name?: string | undefined; id?: unknown };
282
+ onClose: () => void;
283
+ onSuccess: () => void;
284
+ };
285
+
286
+ let capturedContentModalProps: CapturedContentModalProps | null = null;
287
+
288
+ jest.mock(
289
+ "../../../../App/FeatureSet/Dashboard/src/Components/Workflow/UpdateWorkflowVariableContentModal",
290
+ () => {
291
+ return {
292
+ __esModule: true,
293
+ default: (props: CapturedContentModalProps): ReactElement => {
294
+ capturedContentModalProps = props;
295
+ return <div data-testid="update-content-modal" />;
296
+ },
297
+ };
298
+ },
299
+ );
300
+
301
+ type CapturedCredentialsModalProps = {
302
+ variable: { name?: string | undefined; id?: unknown };
303
+ onClose: () => void;
304
+ onSaved: (savedWhat: string) => void;
305
+ };
306
+
307
+ let capturedCredentialsModalProps: CapturedCredentialsModalProps | null = null;
308
+
309
+ jest.mock(
310
+ "../../../../App/FeatureSet/Dashboard/src/Components/Workflow/UpdateWorkflowVariableCredentialsModal",
311
+ () => {
312
+ return {
313
+ __esModule: true,
314
+ default: (props: CapturedCredentialsModalProps): ReactElement => {
315
+ capturedCredentialsModalProps = props;
316
+ return <div data-testid="update-credentials-modal" />;
317
+ },
318
+ };
319
+ },
320
+ );
321
+
322
+ type CapturedTokenRefreshModalProps = {
323
+ variableName: string;
324
+ outcome: {
325
+ variableName: string;
326
+ savedWhat?: string | undefined;
327
+ expiresAt?: Date | null | undefined;
328
+ error?: string | undefined;
329
+ } | null;
330
+ onClose: () => void;
331
+ };
332
+
333
+ let capturedTokenRefreshModalProps: CapturedTokenRefreshModalProps | null =
334
+ null;
335
+
336
+ jest.mock(
337
+ "../../../../App/FeatureSet/Dashboard/src/Components/Workflow/WorkflowVariableTokenRefreshModal",
338
+ () => {
339
+ return {
340
+ __esModule: true,
341
+ default: (props: CapturedTokenRefreshModalProps): ReactElement => {
342
+ capturedTokenRefreshModalProps = props;
343
+ return (
344
+ <div
345
+ data-testid="token-refresh-modal"
346
+ data-pending={props.outcome ? "false" : "true"}
347
+ />
348
+ );
349
+ },
350
+ };
351
+ },
352
+ );
353
+
354
+ type RefreshActionEntry = {
355
+ onClick: () => void;
356
+ isLoading?: boolean | undefined;
357
+ disabled?: boolean | undefined;
358
+ tooltip?: string | undefined;
359
+ };
360
+
361
+ type CapturedTokenStatusProps = {
362
+ variable: {
363
+ name?: string | undefined;
364
+ id?: unknown;
365
+ oauthLastRefreshError?: string | undefined;
366
+ };
367
+ refreshAction?: RefreshActionEntry | undefined;
368
+ };
369
+
370
+ let capturedTokenStatusProps: CapturedTokenStatusProps | null = null;
371
+
372
+ jest.mock(
373
+ "../../../../App/FeatureSet/Dashboard/src/Components/Workflow/WorkflowVariableTokenStatus",
374
+ () => {
375
+ return {
376
+ __esModule: true,
377
+ // Disabled exactly as the real one is: while loading, or when asked to.
378
+ default: (props: CapturedTokenStatusProps): ReactElement => {
379
+ capturedTokenStatusProps = props;
380
+ return (
381
+ <div data-testid="token-status">
382
+ {props.refreshAction ? (
383
+ <button
384
+ type="button"
385
+ data-testid="refresh-now"
386
+ disabled={Boolean(
387
+ props.refreshAction.disabled || props.refreshAction.isLoading,
388
+ )}
389
+ title={props.refreshAction.tooltip}
390
+ onClick={props.refreshAction.onClick}
391
+ >
392
+ Refresh now
393
+ </button>
394
+ ) : null}
395
+ </div>
396
+ );
397
+ },
398
+ };
399
+ },
400
+ );
401
+
402
+ import WorkflowVariableView, {
403
+ WORKFLOW_VARIABLE_VIEW_SELECT,
404
+ getVariableScopeError,
405
+ } from "../../../../App/FeatureSet/Dashboard/src/Components/Workflow/WorkflowVariableView";
406
+ import GlobalWorkflowVariableViewPage from "../../../../App/FeatureSet/Dashboard/src/Pages/Workflow/VariableView";
407
+ import LocalWorkflowVariableViewPage from "../../../../App/FeatureSet/Dashboard/src/Pages/Workflow/View/VariableView";
408
+ import {
409
+ TokenRefreshOutcome,
410
+ getTokenRefreshDescription,
411
+ getTokenRefreshTitle,
412
+ } from "../../../../App/FeatureSet/Dashboard/src/Utils/Workflow/WorkflowVariableUtil";
413
+ import WorkflowVariable from "../../../Models/DatabaseModels/WorkflowVariable";
414
+ import ObjectID from "../../../Types/ObjectID";
415
+ import Permission from "../../../Types/Permission";
416
+ import {
417
+ OAuth2ClientAuthenticationMethod,
418
+ OAuth2GrantType,
419
+ WorkflowVariableType,
420
+ } from "../../../Types/Workflow/WorkflowVariableOAuth";
421
+ import PermissionGate, { ModelAction } from "../../../UI/Utils/PermissionGate";
422
+
423
+ const PROJECT_ID: ObjectID = new ObjectID(
424
+ "11111111-1111-4111-8111-111111111111",
425
+ );
426
+ const WORKFLOW_ID: ObjectID = new ObjectID(
427
+ "22222222-2222-4222-8222-222222222222",
428
+ );
429
+ const VARIABLE_ID: ObjectID = new ObjectID(
430
+ "33333333-3333-4333-8333-333333333333",
431
+ );
432
+ const OTHER_WORKFLOW_ID: ObjectID = new ObjectID(
433
+ "44444444-4444-4444-8444-444444444444",
434
+ );
435
+ const OTHER_VARIABLE_ID: ObjectID = new ObjectID(
436
+ "55555555-5555-4555-8555-555555555555",
437
+ );
438
+ // What Navigation hands back for a route segment a page should not be reading.
439
+ const UNEXPECTED_PARAM_ID: ObjectID = new ObjectID(
440
+ "99999999-9999-4999-8999-999999999999",
441
+ );
442
+
443
+ const DETAILS_CARD: string = "Workflow > Variable Details";
444
+ const OAUTH_SETTINGS_CARD: string = "Workflow > OAuth 2.0 Settings";
445
+
446
+ // Columns nobody may read back: they must never be selected, shown or edited.
447
+ const WRITE_ONLY_COLUMNS: Array<string> = [
448
+ "content",
449
+ "oauthClientSecret",
450
+ "oauthRefreshToken",
451
+ "oauthAccessToken",
452
+ ];
453
+
454
+ const TOKEN_EXPIRES_AT: string = "2030-01-01T10:00:00.000Z";
455
+
456
+ let lastParams: Record<number, ObjectID> = {};
457
+
458
+ type Deferred<T> = {
459
+ promise: Promise<T>;
460
+ resolve: (value: T) => void;
461
+ reject: (error: unknown) => void;
462
+ };
463
+
464
+ function deferred<T>(): Deferred<T> {
465
+ let resolve!: (value: T) => void;
466
+ let reject!: (error: unknown) => void;
467
+
468
+ const promise: Promise<T> = new Promise<T>(
469
+ (
470
+ promiseResolve: (value: T) => void,
471
+ promiseReject: (error: unknown) => void,
472
+ ) => {
473
+ resolve = promiseResolve;
474
+ reject = promiseReject;
475
+ },
476
+ );
477
+
478
+ return { promise, resolve, reject };
479
+ }
480
+
481
+ function applyOverrides(
482
+ variable: WorkflowVariable,
483
+ overrides?: Partial<Record<string, unknown>>,
484
+ ): WorkflowVariable {
485
+ for (const [key, value] of Object.entries(overrides || {})) {
486
+ (variable as unknown as Record<string, unknown>)[key] = value;
487
+ }
488
+
489
+ return variable;
490
+ }
491
+
492
+ function staticVariable(
493
+ overrides?: Partial<Record<string, unknown>>,
494
+ ): WorkflowVariable {
495
+ const variable: WorkflowVariable = new WorkflowVariable();
496
+ variable._id = VARIABLE_ID.toString();
497
+ variable.name = "API_KEY";
498
+ variable.variableType = WorkflowVariableType.Static;
499
+
500
+ // The model types isSecret as a string, but the column is a boolean.
501
+ return applyOverrides(variable, { isSecret: false, ...(overrides || {}) });
502
+ }
503
+
504
+ function oauthVariable(
505
+ overrides?: Partial<Record<string, unknown>>,
506
+ ): WorkflowVariable {
507
+ const variable: WorkflowVariable = new WorkflowVariable();
508
+ variable._id = VARIABLE_ID.toString();
509
+ variable.name = "API_TOKEN";
510
+ variable.variableType = WorkflowVariableType.OAuth2;
511
+ variable.oauthGrantType = OAuth2GrantType.ClientCredentials;
512
+
513
+ return applyOverrides(variable, { isSecret: true, ...(overrides || {}) });
514
+ }
515
+
516
+ // A variable local to WORKFLOW_ID.
517
+ function localStaticVariable(
518
+ overrides?: Partial<Record<string, unknown>>,
519
+ ): WorkflowVariable {
520
+ return staticVariable({ workflowId: WORKFLOW_ID, ...(overrides || {}) });
521
+ }
522
+
523
+ function localOAuthVariable(
524
+ overrides?: Partial<Record<string, unknown>>,
525
+ ): WorkflowVariable {
526
+ return oauthVariable({ workflowId: WORKFLOW_ID, ...(overrides || {}) });
527
+ }
528
+
529
+ function renderView(data?: {
530
+ workflowId?: ObjectID | undefined;
531
+ variableId?: ObjectID | undefined;
532
+ }): ReturnType<typeof render> {
533
+ return render(
534
+ <WorkflowVariableView
535
+ variableId={data?.variableId || VARIABLE_ID}
536
+ workflowId={data?.workflowId}
537
+ />,
538
+ );
539
+ }
540
+
541
+ async function waitForPage(): Promise<void> {
542
+ await screen.findByTestId("model-delete");
543
+ }
544
+
545
+ // Loads `variable` and renders the global page (or a workflow's, with workflowId).
546
+ async function renderLoaded(
547
+ variable: WorkflowVariable,
548
+ workflowId?: ObjectID | undefined,
549
+ ): Promise<ReturnType<typeof render>> {
550
+ getItem.mockResolvedValue(variable);
551
+
552
+ const result: ReturnType<typeof render> = renderView({ workflowId });
553
+
554
+ await waitForPage();
555
+
556
+ // From here on, any loader render is one a reload caused.
557
+ pageLoaderRenderCount = 0;
558
+
559
+ return result;
560
+ }
561
+
562
+ function expectNoLoaderSinceLoad(): void {
563
+ expect(pageLoaderRenderCount).toBe(0);
564
+ }
565
+
566
+ function detail(name: string): CapturedDetailProps {
567
+ const props: CapturedDetailProps | undefined = capturedDetails[name];
568
+
569
+ if (!props) {
570
+ throw new Error(`The page rendered no "${name}" card`);
571
+ }
572
+
573
+ return props;
574
+ }
575
+
576
+ function card(title: string): CapturedCardProps {
577
+ const props: CapturedCardProps | undefined = capturedCards[title];
578
+
579
+ if (!props) {
580
+ throw new Error(`The page rendered no "${title}" card`);
581
+ }
582
+
583
+ return props;
584
+ }
585
+
586
+ function cardButton(
587
+ props: CapturedCardProps,
588
+ title: string,
589
+ ): CardButtonEntry | undefined {
590
+ return (props.buttons || []).find((button: CardButtonEntry) => {
591
+ return button.title === title;
592
+ });
593
+ }
594
+
595
+ type NamedField = { field?: Record<string, unknown> | undefined };
596
+
597
+ function fieldNames(fields: Array<NamedField>): Array<string> {
598
+ return fields.map((entry: NamedField) => {
599
+ return Object.keys(entry.field || {})[0] || "";
600
+ });
601
+ }
602
+
603
+ function detailFormFieldNames(name: string): Array<string> {
604
+ return fieldNames(detail(name).formFields || []);
605
+ }
606
+
607
+ function detailViewField(name: string, title: string): DetailField {
608
+ const field: DetailField | undefined = detail(
609
+ name,
610
+ ).modelDetailProps.fields.find((entry: DetailField) => {
611
+ return entry.title === title;
612
+ });
613
+
614
+ if (!field) {
615
+ throw new Error(`The "${name}" card shows no "${title}" field`);
616
+ }
617
+
618
+ return field;
619
+ }
620
+
621
+ function renderDetailElement(
622
+ name: string,
623
+ title: string,
624
+ item: WorkflowVariable,
625
+ ): HTMLElement {
626
+ const element: ReactElement = detailViewField(name, title).getElement!(item);
627
+
628
+ return render(element).container;
629
+ }
630
+
631
+ function getItemCall(index: number = 0): {
632
+ modelType: unknown;
633
+ id: ObjectID;
634
+ select: Record<string, unknown>;
635
+ } {
636
+ return getItem.mock.calls[index]![0] as {
637
+ modelType: unknown;
638
+ id: ObjectID;
639
+ select: Record<string, unknown>;
640
+ };
641
+ }
642
+
643
+ function refreshCall(index: number = 0): {
644
+ url: unknown;
645
+ headers: Record<string, string>;
646
+ data: unknown;
647
+ } {
648
+ return apiPost.mock.calls[index]![0] as {
649
+ url: unknown;
650
+ headers: Record<string, string>;
651
+ data: unknown;
652
+ };
653
+ }
654
+
655
+ function tokenModal(): CapturedTokenRefreshModalProps {
656
+ if (!capturedTokenRefreshModalProps) {
657
+ throw new Error("No token refresh modal is open");
658
+ }
659
+
660
+ return capturedTokenRefreshModalProps;
661
+ }
662
+
663
+ function tokenStatus(): CapturedTokenStatusProps {
664
+ if (!capturedTokenStatusProps) {
665
+ throw new Error("The page rendered no token status");
666
+ }
667
+
668
+ return capturedTokenStatusProps;
669
+ }
670
+
671
+ function missingUpdatePermissionMessage(): string {
672
+ return PermissionGate.getMissingPermissionMessage(
673
+ new WorkflowVariable(),
674
+ ModelAction.Update,
675
+ );
676
+ }
677
+
678
+ async function clickRefreshNow(): Promise<void> {
679
+ await act(async () => {
680
+ fireEvent.click(screen.getByTestId("refresh-now"));
681
+ });
682
+ }
683
+
684
+ async function waitForOutcome(): Promise<void> {
685
+ await waitFor(() => {
686
+ expect(capturedTokenRefreshModalProps?.outcome).toBeTruthy();
687
+ });
688
+ }
689
+
690
+ const NOT_FOUND_MESSAGE: RegExp = /This variable could not be found/;
691
+
692
+ /*
693
+ * What the API really answers for a missing, deleted or out-of-tenant id: an
694
+ * empty object, which ModelAPI turns into a WorkflowVariable with nothing set.
695
+ */
696
+ function missingVariable(): WorkflowVariable {
697
+ return new WorkflowVariable();
698
+ }
699
+
700
+ /*
701
+ * Makes the next read (a background reload after a save or a token refresh)
702
+ * wait until the test settles it.
703
+ */
704
+ function holdNextRead(): Deferred<unknown> {
705
+ const pending: Deferred<unknown> = deferred<unknown>();
706
+ getItem.mockReturnValueOnce(pending.promise);
707
+ return pending;
708
+ }
709
+
710
+ async function waitForReads(count: number): Promise<void> {
711
+ await waitFor(() => {
712
+ expect(getItem).toHaveBeenCalledTimes(count);
713
+ });
714
+ }
715
+
716
+ // The variable's page, whole: its cards and its delete card, and no error.
717
+ function expectOAuthPageShown(): void {
718
+ expect(screen.getByTestId(`detail:${DETAILS_CARD}`)).toBeInTheDocument();
719
+ expect(screen.getByTestId("card:Access Token")).toBeInTheDocument();
720
+ expect(
721
+ screen.getByTestId(`detail:${OAUTH_SETTINGS_CARD}`),
722
+ ).toBeInTheDocument();
723
+ expect(screen.getByTestId("model-delete")).toBeInTheDocument();
724
+ expect(screen.queryByTestId("page-loader")).not.toBeInTheDocument();
725
+ expect(screen.queryByTestId("refresh-button")).not.toBeInTheDocument();
726
+ }
727
+
728
+ beforeEach(() => {
729
+ capturedCards = {};
730
+ capturedDetails = {};
731
+ capturedDeleteProps = null;
732
+ capturedContentModalProps = null;
733
+ capturedCredentialsModalProps = null;
734
+ capturedTokenRefreshModalProps = null;
735
+ capturedTokenStatusProps = null;
736
+ pageLoaderRenderCount = 0;
737
+
738
+ getItem.mockReset();
739
+ getItem.mockResolvedValue(staticVariable());
740
+ updateById.mockReset();
741
+ updateById.mockResolvedValue(undefined);
742
+ apiPost.mockReset();
743
+ apiPost.mockResolvedValue({
744
+ data: { oauthAccessTokenExpiresAt: TOKEN_EXPIRES_AT },
745
+ });
746
+ navigate.mockReset();
747
+
748
+ lastParams = {};
749
+ getLastParamAsObjectID.mockReset();
750
+ getLastParamAsObjectID.mockImplementation((index?: number): unknown => {
751
+ return lastParams[index ?? 0] || UNEXPECTED_PARAM_ID;
752
+ });
753
+
754
+ permissionsForTest = [Permission.ProjectAdmin];
755
+ isMasterAdminForTest = false;
756
+ PermissionGate.clearPermissionPropsCache();
757
+ });
758
+
759
+ afterEach(() => {
760
+ cleanup();
761
+ });
762
+
763
+ describe("getVariableScopeError", () => {
764
+ test("a global variable on the global page is fine", () => {
765
+ expect(getVariableScopeError({ variable: staticVariable() })).toBeNull();
766
+ });
767
+
768
+ test("a local variable on the global page is refused, pointing at its workflow", () => {
769
+ const error: string | null = getVariableScopeError({
770
+ variable: localStaticVariable(),
771
+ });
772
+
773
+ expect(error).toContain("belongs to a workflow");
774
+ expect(error).toContain("Workflow Variables page");
775
+ });
776
+
777
+ test("a variable of this workflow on this workflow's page is fine", () => {
778
+ expect(
779
+ getVariableScopeError({
780
+ variable: localStaticVariable(),
781
+ workflowId: WORKFLOW_ID,
782
+ }),
783
+ ).toBeNull();
784
+ });
785
+
786
+ test("compares workflow ids by value, not by object", () => {
787
+ expect(
788
+ getVariableScopeError({
789
+ variable: localStaticVariable(),
790
+ workflowId: new ObjectID(WORKFLOW_ID.toString()),
791
+ }),
792
+ ).toBeNull();
793
+ });
794
+
795
+ test("another workflow's variable on this workflow's page is refused", () => {
796
+ expect(
797
+ getVariableScopeError({
798
+ variable: staticVariable({ workflowId: OTHER_WORKFLOW_ID }),
799
+ workflowId: WORKFLOW_ID,
800
+ }),
801
+ ).toBe("This variable does not belong to this workflow.");
802
+ });
803
+
804
+ test("a global variable on a workflow's page is refused", () => {
805
+ expect(
806
+ getVariableScopeError({
807
+ variable: staticVariable(),
808
+ workflowId: WORKFLOW_ID,
809
+ }),
810
+ ).toBe("This variable does not belong to this workflow.");
811
+ });
812
+ });
813
+
814
+ describe("loading the variable", () => {
815
+ test("reads the variable by its id", async () => {
816
+ await renderLoaded(staticVariable());
817
+
818
+ expect(getItem).toHaveBeenCalledTimes(1);
819
+ expect(getItemCall().modelType).toBe(WorkflowVariable);
820
+ expect(getItemCall().id.toString()).toBe(VARIABLE_ID.toString());
821
+ expect(getItemCall().select).toBe(WORKFLOW_VARIABLE_VIEW_SELECT);
822
+ });
823
+
824
+ test("selects what the layout and the token status need", async () => {
825
+ await renderLoaded(oauthVariable());
826
+
827
+ expect(Object.keys(getItemCall().select)).toEqual(
828
+ expect.arrayContaining([
829
+ "_id",
830
+ "name",
831
+ "variableType",
832
+ "workflowId",
833
+ "isSecret",
834
+ "oauthGrantType",
835
+ "oauthAccessTokenExpiresAt",
836
+ "oauthLastRefreshedAt",
837
+ "oauthLastRefreshError",
838
+ "oauthLastRefreshErrorAt",
839
+ ]),
840
+ );
841
+ });
842
+
843
+ /*
844
+ * These columns have an empty read list. Selecting any one of them makes the
845
+ * server refuse the whole request, so the page would never load at all - and
846
+ * it would be the wrong place to show a secret if it did.
847
+ */
848
+ test("never selects content, a credential or a token", async () => {
849
+ await renderLoaded(oauthVariable());
850
+
851
+ for (const column of WRITE_ONLY_COLUMNS) {
852
+ expect(getItemCall().select).not.toHaveProperty(column);
853
+ expect(WORKFLOW_VARIABLE_VIEW_SELECT).not.toHaveProperty(column);
854
+ }
855
+ });
856
+
857
+ test("shows a loader until the variable arrives, and nothing else", async () => {
858
+ const pending: Deferred<unknown> = deferred<unknown>();
859
+ getItem.mockReturnValue(pending.promise);
860
+
861
+ renderView();
862
+
863
+ expect(screen.getByTestId("page-loader")).toBeInTheDocument();
864
+ expect(screen.queryByTestId("model-delete")).not.toBeInTheDocument();
865
+ expect(capturedDetails[DETAILS_CARD]).toBeUndefined();
866
+
867
+ await act(async () => {
868
+ pending.resolve(staticVariable());
869
+ });
870
+
871
+ await waitForPage();
872
+
873
+ expect(screen.queryByTestId("page-loader")).not.toBeInTheDocument();
874
+ expect(screen.getByTestId(`detail:${DETAILS_CARD}`)).toBeInTheDocument();
875
+ });
876
+
877
+ /*
878
+ * The API answers a missing id with an empty object, not null. Read as a
879
+ * variable, that has no type - so without the id check it would be drawn
880
+ * as an empty static variable with an Update Content button.
881
+ */
882
+ test("says so when the variable is not found", async () => {
883
+ getItem.mockResolvedValue(missingVariable());
884
+
885
+ renderView();
886
+
887
+ expect(await screen.findByText(NOT_FOUND_MESSAGE)).toBeInTheDocument();
888
+ expect(screen.queryByTestId("page-loader")).not.toBeInTheDocument();
889
+ // Nothing to edit or delete.
890
+ expect(screen.queryByTestId("model-delete")).not.toBeInTheDocument();
891
+ expect(capturedDetails[DETAILS_CARD]).toBeUndefined();
892
+ expect(screen.queryByTestId("card:Content")).not.toBeInTheDocument();
893
+ expect(
894
+ screen.queryByTestId("card-button:Update Content"),
895
+ ).not.toBeInTheDocument();
896
+ });
897
+
898
+ test("says so when the read answers with nothing at all", async () => {
899
+ getItem.mockResolvedValue(null);
900
+
901
+ renderView();
902
+
903
+ expect(await screen.findByText(NOT_FOUND_MESSAGE)).toBeInTheDocument();
904
+ expect(screen.queryByTestId("page-loader")).not.toBeInTheDocument();
905
+ expect(screen.queryByTestId("model-delete")).not.toBeInTheDocument();
906
+ expect(capturedDetails[DETAILS_CARD]).toBeUndefined();
907
+ });
908
+
909
+ /*
910
+ * An empty variable has no workflowId either, so a scope check run first
911
+ * would call a missing variable "another workflow's".
912
+ */
913
+ test("a workflow's page says a missing variable is missing, not another workflow's", async () => {
914
+ getItem.mockResolvedValue(missingVariable());
915
+
916
+ renderView({ workflowId: WORKFLOW_ID });
917
+
918
+ expect(await screen.findByText(NOT_FOUND_MESSAGE)).toBeInTheDocument();
919
+ expect(screen.queryByText(/does not belong/)).not.toBeInTheDocument();
920
+ expect(screen.queryByTestId("model-delete")).not.toBeInTheDocument();
921
+ expect(capturedDetails[DETAILS_CARD]).toBeUndefined();
922
+ });
923
+
924
+ test("a workflow's page says so for a null read too", async () => {
925
+ getItem.mockResolvedValue(null);
926
+
927
+ renderView({ workflowId: WORKFLOW_ID });
928
+
929
+ expect(await screen.findByText(NOT_FOUND_MESSAGE)).toBeInTheDocument();
930
+ expect(screen.queryByText(/does not belong/)).not.toBeInTheDocument();
931
+ });
932
+
933
+ // A variable with values but no id is still not one the page can act on.
934
+ test("a read with fields but no id is not found either", async () => {
935
+ const variable: WorkflowVariable = new WorkflowVariable();
936
+ variable.name = "API_KEY";
937
+ variable.variableType = WorkflowVariableType.Static;
938
+
939
+ getItem.mockResolvedValue(variable);
940
+
941
+ renderView();
942
+
943
+ expect(await screen.findByText(NOT_FOUND_MESSAGE)).toBeInTheDocument();
944
+ expect(screen.queryByTestId("model-delete")).not.toBeInTheDocument();
945
+ });
946
+
947
+ test("shows the friendly message when the request fails", async () => {
948
+ getItem.mockRejectedValue(new Error("The API is unreachable."));
949
+
950
+ renderView();
951
+
952
+ expect(
953
+ await screen.findByText("The API is unreachable."),
954
+ ).toBeInTheDocument();
955
+ expect(screen.queryByTestId("model-delete")).not.toBeInTheDocument();
956
+ expect(screen.queryByTestId("page-loader")).not.toBeInTheDocument();
957
+ });
958
+
959
+ test("Refresh? on the error reads the variable again", async () => {
960
+ getItem.mockRejectedValueOnce(new Error("The API is unreachable."));
961
+ getItem.mockResolvedValueOnce(staticVariable());
962
+
963
+ renderView();
964
+
965
+ await screen.findByText("The API is unreachable.");
966
+
967
+ await act(async () => {
968
+ fireEvent.click(screen.getByTestId("refresh-button"));
969
+ });
970
+
971
+ await waitForPage();
972
+
973
+ expect(getItem).toHaveBeenCalledTimes(2);
974
+ expect(
975
+ screen.queryByText("The API is unreachable."),
976
+ ).not.toBeInTheDocument();
977
+ });
978
+
979
+ // The error owns the page, so its retry is a foreground load: loader, then page.
980
+ test("Refresh? on the error retries in the foreground and shows the variable", async () => {
981
+ const retry: Deferred<unknown> = deferred<unknown>();
982
+ getItem.mockRejectedValueOnce(new Error("The API is unreachable."));
983
+ getItem.mockReturnValueOnce(retry.promise);
984
+
985
+ renderView();
986
+
987
+ await screen.findByText("The API is unreachable.");
988
+
989
+ await act(async () => {
990
+ fireEvent.click(screen.getByTestId("refresh-button"));
991
+ });
992
+
993
+ expect(screen.getByTestId("page-loader")).toBeInTheDocument();
994
+ expect(
995
+ screen.queryByText("The API is unreachable."),
996
+ ).not.toBeInTheDocument();
997
+
998
+ await act(async () => {
999
+ retry.resolve(oauthVariable());
1000
+ });
1001
+
1002
+ await waitForPage();
1003
+
1004
+ expectOAuthPageShown();
1005
+ expect(tokenStatus().variable.name).toBe("API_TOKEN");
1006
+ expect(getItemCall(1).id.toString()).toBe(VARIABLE_ID.toString());
1007
+ });
1008
+
1009
+ test("Refresh? after a not-found shows the variable once it is there", async () => {
1010
+ getItem.mockResolvedValueOnce(missingVariable());
1011
+ getItem.mockResolvedValueOnce(staticVariable());
1012
+
1013
+ renderView();
1014
+
1015
+ await screen.findByText(NOT_FOUND_MESSAGE);
1016
+
1017
+ await act(async () => {
1018
+ fireEvent.click(screen.getByTestId("refresh-button"));
1019
+ });
1020
+
1021
+ await waitForPage();
1022
+
1023
+ expect(screen.queryByText(NOT_FOUND_MESSAGE)).not.toBeInTheDocument();
1024
+ expect(screen.getByTestId("card:Content")).toBeInTheDocument();
1025
+ });
1026
+
1027
+ test("Refresh? that fails again shows the new error", async () => {
1028
+ getItem.mockRejectedValueOnce(new Error("The API is unreachable."));
1029
+ getItem.mockRejectedValueOnce(new Error("Still unreachable."));
1030
+
1031
+ renderView();
1032
+
1033
+ await screen.findByText("The API is unreachable.");
1034
+
1035
+ await act(async () => {
1036
+ fireEvent.click(screen.getByTestId("refresh-button"));
1037
+ });
1038
+
1039
+ expect(await screen.findByText("Still unreachable.")).toBeInTheDocument();
1040
+ expect(screen.queryByTestId("page-loader")).not.toBeInTheDocument();
1041
+ expect(screen.queryByTestId("model-delete")).not.toBeInTheDocument();
1042
+ });
1043
+
1044
+ test("reads the new variable when the page moves to another one", async () => {
1045
+ const result: ReturnType<typeof render> =
1046
+ await renderLoaded(staticVariable());
1047
+
1048
+ getItem.mockResolvedValue(
1049
+ staticVariable({ _id: OTHER_VARIABLE_ID.toString(), name: "OTHER" }),
1050
+ );
1051
+
1052
+ result.rerender(<WorkflowVariableView variableId={OTHER_VARIABLE_ID} />);
1053
+
1054
+ await waitFor(() => {
1055
+ expect(getItem).toHaveBeenCalledTimes(2);
1056
+ });
1057
+
1058
+ expect(getItemCall(1).id.toString()).toBe(OTHER_VARIABLE_ID.toString());
1059
+
1060
+ await waitFor(() => {
1061
+ expect(detail(DETAILS_CARD).modelDetailProps.modelId.toString()).toBe(
1062
+ OTHER_VARIABLE_ID.toString(),
1063
+ );
1064
+ });
1065
+ expect(capturedDeleteProps?.modelId.toString()).toBe(
1066
+ OTHER_VARIABLE_ID.toString(),
1067
+ );
1068
+ });
1069
+ });
1070
+
1071
+ /*
1072
+ * Moving from one variable's page to another's reuses the component, so the
1073
+ * first variable's read can still be in flight when the second one starts.
1074
+ * Whichever answers last, the page must show the variable it is now on.
1075
+ *
1076
+ * The two variables differ in type, so the layout alone says which one
1077
+ * landed: the cards' modelId comes from the prop, not from the read.
1078
+ */
1079
+ describe("a slow read never overwrites a newer one", () => {
1080
+ function otherStaticVariable(): WorkflowVariable {
1081
+ return staticVariable({ _id: OTHER_VARIABLE_ID.toString(), name: "OTHER" });
1082
+ }
1083
+
1084
+ function expectOtherVariableShown(): void {
1085
+ expect(screen.getByTestId("card:Content")).toBeInTheDocument();
1086
+ expect(screen.queryByTestId("card:Access Token")).not.toBeInTheDocument();
1087
+ expect(capturedDetails[OAUTH_SETTINGS_CARD]).toBeUndefined();
1088
+ expect(screen.queryByTestId("page-loader")).not.toBeInTheDocument();
1089
+
1090
+ // The variable the page acts on is the one it read for OTHER.
1091
+ fireEvent.click(screen.getByTestId("card-button:Update Content"));
1092
+ expect(capturedContentModalProps?.variable.name).toBe("OTHER");
1093
+ }
1094
+
1095
+ test("the previous variable's read answering last is ignored", async () => {
1096
+ const first: Deferred<unknown> = deferred<unknown>();
1097
+ const second: Deferred<unknown> = deferred<unknown>();
1098
+ getItem.mockReturnValueOnce(first.promise);
1099
+ getItem.mockReturnValueOnce(second.promise);
1100
+
1101
+ const result: ReturnType<typeof render> = renderView();
1102
+
1103
+ result.rerender(<WorkflowVariableView variableId={OTHER_VARIABLE_ID} />);
1104
+
1105
+ await waitForReads(2);
1106
+ expect(getItemCall(0).id.toString()).toBe(VARIABLE_ID.toString());
1107
+ expect(getItemCall(1).id.toString()).toBe(OTHER_VARIABLE_ID.toString());
1108
+
1109
+ await act(async () => {
1110
+ second.resolve(otherStaticVariable());
1111
+ });
1112
+
1113
+ await waitForPage();
1114
+
1115
+ await act(async () => {
1116
+ first.resolve(oauthVariable());
1117
+ });
1118
+
1119
+ expectOtherVariableShown();
1120
+ });
1121
+
1122
+ test("the previous variable's read answering first does not land either", async () => {
1123
+ const first: Deferred<unknown> = deferred<unknown>();
1124
+ const second: Deferred<unknown> = deferred<unknown>();
1125
+ getItem.mockReturnValueOnce(first.promise);
1126
+ getItem.mockReturnValueOnce(second.promise);
1127
+
1128
+ const result: ReturnType<typeof render> = renderView();
1129
+
1130
+ result.rerender(<WorkflowVariableView variableId={OTHER_VARIABLE_ID} />);
1131
+
1132
+ await waitForReads(2);
1133
+
1134
+ await act(async () => {
1135
+ first.resolve(oauthVariable());
1136
+ });
1137
+
1138
+ // Still waiting for the variable the page is on.
1139
+ expect(screen.getByTestId("page-loader")).toBeInTheDocument();
1140
+ expect(screen.queryByTestId("model-delete")).not.toBeInTheDocument();
1141
+
1142
+ await act(async () => {
1143
+ second.resolve(otherStaticVariable());
1144
+ });
1145
+
1146
+ await waitForPage();
1147
+
1148
+ expectOtherVariableShown();
1149
+ });
1150
+
1151
+ test("the previous variable's late failure does not replace the page with an error", async () => {
1152
+ const first: Deferred<unknown> = deferred<unknown>();
1153
+ const second: Deferred<unknown> = deferred<unknown>();
1154
+ getItem.mockReturnValueOnce(first.promise);
1155
+ getItem.mockReturnValueOnce(second.promise);
1156
+
1157
+ const result: ReturnType<typeof render> = renderView();
1158
+
1159
+ result.rerender(<WorkflowVariableView variableId={OTHER_VARIABLE_ID} />);
1160
+
1161
+ await waitForReads(2);
1162
+
1163
+ await act(async () => {
1164
+ second.resolve(otherStaticVariable());
1165
+ });
1166
+
1167
+ await waitForPage();
1168
+
1169
+ await act(async () => {
1170
+ first.reject(new Error("The API is unreachable."));
1171
+ });
1172
+
1173
+ expect(
1174
+ screen.queryByText("The API is unreachable."),
1175
+ ).not.toBeInTheDocument();
1176
+ expectOtherVariableShown();
1177
+ });
1178
+
1179
+ // Two saves in a row: the first save's reload must not undo the second's.
1180
+ test("an older background reload answering last is ignored", async () => {
1181
+ await renderLoaded(staticVariable({ isSecret: false }));
1182
+
1183
+ const olderReload: Deferred<unknown> = holdNextRead();
1184
+ const newerReload: Deferred<unknown> = holdNextRead();
1185
+
1186
+ await act(async () => {
1187
+ detail(DETAILS_CARD).onSaveSuccess?.(staticVariable());
1188
+ });
1189
+ await act(async () => {
1190
+ detail(DETAILS_CARD).onSaveSuccess?.(staticVariable({ isSecret: true }));
1191
+ });
1192
+
1193
+ await waitForReads(3);
1194
+
1195
+ await act(async () => {
1196
+ newerReload.resolve(staticVariable({ isSecret: true }));
1197
+ });
1198
+
1199
+ await waitFor(() => {
1200
+ expect(detailFormFieldNames(DETAILS_CARD)).toEqual([
1201
+ "name",
1202
+ "description",
1203
+ ]);
1204
+ });
1205
+
1206
+ await act(async () => {
1207
+ olderReload.resolve(staticVariable({ isSecret: false }));
1208
+ });
1209
+
1210
+ // The secret toggle would come back if the stale read landed.
1211
+ expect(detailFormFieldNames(DETAILS_CARD)).toEqual(["name", "description"]);
1212
+ });
1213
+
1214
+ test("a read that answers after the page is gone does nothing", async () => {
1215
+ const pending: Deferred<unknown> = deferred<unknown>();
1216
+ getItem.mockReturnValueOnce(pending.promise);
1217
+
1218
+ const errorSpy: ReturnType<typeof jest.spyOn> = jest
1219
+ .spyOn(console, "error")
1220
+ .mockImplementation((): void => {});
1221
+
1222
+ try {
1223
+ const result: ReturnType<typeof render> = renderView();
1224
+
1225
+ result.unmount();
1226
+
1227
+ await act(async () => {
1228
+ pending.resolve(oauthVariable());
1229
+ });
1230
+
1231
+ expect(errorSpy).not.toHaveBeenCalled();
1232
+ expect(capturedDetails[DETAILS_CARD]).toBeUndefined();
1233
+ expect(capturedTokenStatusProps).toBeNull();
1234
+ } finally {
1235
+ errorSpy.mockRestore();
1236
+ }
1237
+ });
1238
+
1239
+ test("a read that fails after the page is gone does nothing", async () => {
1240
+ const pending: Deferred<unknown> = deferred<unknown>();
1241
+ getItem.mockReturnValueOnce(pending.promise);
1242
+
1243
+ const errorSpy: ReturnType<typeof jest.spyOn> = jest
1244
+ .spyOn(console, "error")
1245
+ .mockImplementation((): void => {});
1246
+
1247
+ try {
1248
+ const result: ReturnType<typeof render> = renderView();
1249
+
1250
+ result.unmount();
1251
+
1252
+ await act(async () => {
1253
+ pending.reject(new Error("The API is unreachable."));
1254
+ });
1255
+
1256
+ expect(errorSpy).not.toHaveBeenCalled();
1257
+ } finally {
1258
+ errorSpy.mockRestore();
1259
+ }
1260
+ });
1261
+ });
1262
+
1263
+ /*
1264
+ * After a save or a token refresh the page reads the variable again to pick
1265
+ * up what was written. The page already shows a variable that exists, and the
1266
+ * token refresh result on top of it is the one thing somebody needs to read
1267
+ * right then - so a reload never shows the loader, and a failed one leaves
1268
+ * everything where it was.
1269
+ */
1270
+ describe("reloads after a save or a token refresh", () => {
1271
+ test("a failed reload after Refresh now leaves the page and the outcome on screen", async () => {
1272
+ await renderLoaded(oauthVariable());
1273
+
1274
+ const reload: Deferred<unknown> = holdNextRead();
1275
+
1276
+ await clickRefreshNow();
1277
+ await waitForOutcome();
1278
+ await waitForReads(2);
1279
+
1280
+ await act(async () => {
1281
+ reload.reject(new Error("The API is unreachable."));
1282
+ });
1283
+
1284
+ expectOAuthPageShown();
1285
+ expect(
1286
+ screen.queryByText("The API is unreachable."),
1287
+ ).not.toBeInTheDocument();
1288
+ expect(screen.getByTestId("token-refresh-modal")).toHaveAttribute(
1289
+ "data-pending",
1290
+ "false",
1291
+ );
1292
+ expect(tokenModal().outcome?.expiresAt?.toISOString()).toBe(
1293
+ TOKEN_EXPIRES_AT,
1294
+ );
1295
+ // The token status still shows the variable it had.
1296
+ expect(tokenStatus().variable.name).toBe("API_TOKEN");
1297
+ expectNoLoaderSinceLoad();
1298
+ });
1299
+
1300
+ test("a failed reload after a refused Refresh now keeps the refusal on screen", async () => {
1301
+ apiPost.mockRejectedValue(new Error("invalid_client"));
1302
+
1303
+ await renderLoaded(oauthVariable());
1304
+
1305
+ const reload: Deferred<unknown> = holdNextRead();
1306
+
1307
+ await clickRefreshNow();
1308
+ await waitForOutcome();
1309
+ await waitForReads(2);
1310
+
1311
+ await act(async () => {
1312
+ reload.reject(new Error("The API is unreachable."));
1313
+ });
1314
+
1315
+ expectOAuthPageShown();
1316
+ expect(screen.getByTestId("token-refresh-modal")).toBeInTheDocument();
1317
+ expect(tokenModal().outcome?.error).toBe("invalid_client");
1318
+ });
1319
+
1320
+ test("a failed reload after a credentials save leaves the page and the outcome on screen", async () => {
1321
+ await renderLoaded(oauthVariable());
1322
+
1323
+ const reload: Deferred<unknown> = holdNextRead();
1324
+
1325
+ fireEvent.click(screen.getByTestId("card-button:Update Credentials"));
1326
+
1327
+ await act(async () => {
1328
+ capturedCredentialsModalProps?.onSaved("Client secret");
1329
+ });
1330
+
1331
+ await waitForOutcome();
1332
+ await waitForReads(2);
1333
+
1334
+ await act(async () => {
1335
+ reload.reject(new Error("The API is unreachable."));
1336
+ });
1337
+
1338
+ expectOAuthPageShown();
1339
+ expect(
1340
+ screen.queryByText("The API is unreachable."),
1341
+ ).not.toBeInTheDocument();
1342
+ expect(screen.getByTestId("token-refresh-modal")).toHaveAttribute(
1343
+ "data-pending",
1344
+ "false",
1345
+ );
1346
+ expect(tokenModal().outcome?.savedWhat).toBe("Client secret");
1347
+ expect(tokenModal().variableName).toBe("API_TOKEN");
1348
+ expectNoLoaderSinceLoad();
1349
+ });
1350
+
1351
+ test("a failed reload after saving the details leaves the page", async () => {
1352
+ await renderLoaded(oauthVariable());
1353
+
1354
+ const reload: Deferred<unknown> = holdNextRead();
1355
+
1356
+ await act(async () => {
1357
+ detail(DETAILS_CARD).onSaveSuccess?.(oauthVariable());
1358
+ });
1359
+
1360
+ await waitForReads(2);
1361
+
1362
+ await act(async () => {
1363
+ reload.reject(new Error("The API is unreachable."));
1364
+ });
1365
+
1366
+ expectOAuthPageShown();
1367
+ expect(
1368
+ screen.queryByText("The API is unreachable."),
1369
+ ).not.toBeInTheDocument();
1370
+ expect(tokenStatus().variable.name).toBe("API_TOKEN");
1371
+ expectNoLoaderSinceLoad();
1372
+ });
1373
+
1374
+ test("a failed reload after saving the OAuth 2.0 settings leaves the page", async () => {
1375
+ await renderLoaded(oauthVariable());
1376
+
1377
+ const reload: Deferred<unknown> = holdNextRead();
1378
+
1379
+ await act(async () => {
1380
+ detail(OAUTH_SETTINGS_CARD).onSaveSuccess?.(oauthVariable());
1381
+ });
1382
+
1383
+ await waitForReads(2);
1384
+
1385
+ await act(async () => {
1386
+ reload.reject(new Error("The API is unreachable."));
1387
+ });
1388
+
1389
+ expectOAuthPageShown();
1390
+ expect(
1391
+ screen.queryByText("The API is unreachable."),
1392
+ ).not.toBeInTheDocument();
1393
+ expectNoLoaderSinceLoad();
1394
+ });
1395
+
1396
+ test("a failed reload after saving a static variable's details leaves the page", async () => {
1397
+ await renderLoaded(staticVariable());
1398
+
1399
+ const reload: Deferred<unknown> = holdNextRead();
1400
+
1401
+ await act(async () => {
1402
+ detail(DETAILS_CARD).onSaveSuccess?.(staticVariable());
1403
+ });
1404
+
1405
+ await waitForReads(2);
1406
+
1407
+ await act(async () => {
1408
+ reload.reject(new Error("The API is unreachable."));
1409
+ });
1410
+
1411
+ expect(screen.getByTestId(`detail:${DETAILS_CARD}`)).toBeInTheDocument();
1412
+ expect(screen.getByTestId("card:Content")).toBeInTheDocument();
1413
+ expect(screen.getByTestId("model-delete")).toBeInTheDocument();
1414
+ expect(
1415
+ screen.queryByText("The API is unreachable."),
1416
+ ).not.toBeInTheDocument();
1417
+ });
1418
+
1419
+ // A reload that reads back nothing is a failed reload too, not a not-found page.
1420
+ test("a reload that finds no variable leaves the page and the outcome on screen", async () => {
1421
+ await renderLoaded(oauthVariable());
1422
+
1423
+ const reload: Deferred<unknown> = holdNextRead();
1424
+
1425
+ await clickRefreshNow();
1426
+ await waitForOutcome();
1427
+ await waitForReads(2);
1428
+
1429
+ await act(async () => {
1430
+ reload.resolve(missingVariable());
1431
+ });
1432
+
1433
+ expectOAuthPageShown();
1434
+ expect(screen.queryByText(NOT_FOUND_MESSAGE)).not.toBeInTheDocument();
1435
+ expect(screen.getByTestId("token-refresh-modal")).toBeInTheDocument();
1436
+ });
1437
+
1438
+ test("a successful reload after Refresh now updates the token status without the loader", async () => {
1439
+ await renderLoaded(oauthVariable());
1440
+
1441
+ const reload: Deferred<unknown> = holdNextRead();
1442
+
1443
+ await clickRefreshNow();
1444
+ await waitForOutcome();
1445
+ await waitForReads(2);
1446
+
1447
+ // The reload is out, and the page is still the page.
1448
+ expectOAuthPageShown();
1449
+ expect(tokenStatus().variable.oauthLastRefreshError).toBeUndefined();
1450
+
1451
+ await act(async () => {
1452
+ reload.resolve(
1453
+ oauthVariable({ oauthLastRefreshError: "invalid_client" }),
1454
+ );
1455
+ });
1456
+
1457
+ await waitFor(() => {
1458
+ expect(tokenStatus().variable.oauthLastRefreshError).toBe(
1459
+ "invalid_client",
1460
+ );
1461
+ });
1462
+
1463
+ expectOAuthPageShown();
1464
+ expect(screen.getByTestId("token-refresh-modal")).toBeInTheDocument();
1465
+ // Only the first load, before the page was up, ever showed it.
1466
+ expectNoLoaderSinceLoad();
1467
+ });
1468
+
1469
+ test("a successful reload after a settings save updates the page without the loader", async () => {
1470
+ await renderLoaded(oauthVariable());
1471
+
1472
+ const reload: Deferred<unknown> = holdNextRead();
1473
+
1474
+ await act(async () => {
1475
+ detail(OAUTH_SETTINGS_CARD).onSaveSuccess?.(oauthVariable());
1476
+ });
1477
+
1478
+ await waitForReads(2);
1479
+
1480
+ expectOAuthPageShown();
1481
+
1482
+ await act(async () => {
1483
+ reload.resolve(oauthVariable({ oauthLastRefreshError: "invalid_scope" }));
1484
+ });
1485
+
1486
+ await waitFor(() => {
1487
+ expect(tokenStatus().variable.oauthLastRefreshError).toBe(
1488
+ "invalid_scope",
1489
+ );
1490
+ });
1491
+
1492
+ expectOAuthPageShown();
1493
+ expectNoLoaderSinceLoad();
1494
+ });
1495
+
1496
+ test("a failed reload does not stop the next one from landing", async () => {
1497
+ await renderLoaded(oauthVariable());
1498
+
1499
+ getItem.mockRejectedValueOnce(new Error("The API is unreachable."));
1500
+
1501
+ await act(async () => {
1502
+ detail(DETAILS_CARD).onSaveSuccess?.(oauthVariable());
1503
+ });
1504
+
1505
+ await waitForReads(2);
1506
+
1507
+ getItem.mockResolvedValueOnce(
1508
+ oauthVariable({ oauthLastRefreshError: "invalid_client" }),
1509
+ );
1510
+
1511
+ await clickRefreshNow();
1512
+ await waitForOutcome();
1513
+
1514
+ await waitFor(() => {
1515
+ expect(tokenStatus().variable.oauthLastRefreshError).toBe(
1516
+ "invalid_client",
1517
+ );
1518
+ });
1519
+
1520
+ expectOAuthPageShown();
1521
+ expectNoLoaderSinceLoad();
1522
+ });
1523
+ });
1524
+
1525
+ describe("the variable's scope", () => {
1526
+ test("the global page refuses a variable that belongs to a workflow", async () => {
1527
+ getItem.mockResolvedValue(localStaticVariable());
1528
+
1529
+ renderView();
1530
+
1531
+ expect(
1532
+ await screen.findByText(/This variable belongs to a workflow/),
1533
+ ).toBeInTheDocument();
1534
+ expect(screen.queryByTestId("model-delete")).not.toBeInTheDocument();
1535
+ expect(capturedDetails[DETAILS_CARD]).toBeUndefined();
1536
+ });
1537
+
1538
+ test("a workflow's page refuses another workflow's variable", async () => {
1539
+ getItem.mockResolvedValue(
1540
+ staticVariable({ workflowId: OTHER_WORKFLOW_ID }),
1541
+ );
1542
+
1543
+ renderView({ workflowId: WORKFLOW_ID });
1544
+
1545
+ expect(
1546
+ await screen.findByText(
1547
+ "This variable does not belong to this workflow.",
1548
+ ),
1549
+ ).toBeInTheDocument();
1550
+ expect(screen.queryByTestId("model-delete")).not.toBeInTheDocument();
1551
+ expect(capturedDetails[DETAILS_CARD]).toBeUndefined();
1552
+ });
1553
+
1554
+ test("a workflow's page refuses a global variable", async () => {
1555
+ getItem.mockResolvedValue(staticVariable());
1556
+
1557
+ renderView({ workflowId: WORKFLOW_ID });
1558
+
1559
+ expect(
1560
+ await screen.findByText(
1561
+ "This variable does not belong to this workflow.",
1562
+ ),
1563
+ ).toBeInTheDocument();
1564
+ expect(screen.queryByTestId("model-delete")).not.toBeInTheDocument();
1565
+ });
1566
+
1567
+ test("the global page shows a global variable", async () => {
1568
+ await renderLoaded(staticVariable());
1569
+
1570
+ expect(screen.queryByText(/does not belong/)).not.toBeInTheDocument();
1571
+ expect(screen.queryByText(/belongs to a workflow/)).not.toBeInTheDocument();
1572
+ expect(screen.getByTestId(`detail:${DETAILS_CARD}`)).toBeInTheDocument();
1573
+ });
1574
+
1575
+ test("a workflow's page shows that workflow's variable", async () => {
1576
+ await renderLoaded(localStaticVariable(), WORKFLOW_ID);
1577
+
1578
+ expect(screen.queryByText(/does not belong/)).not.toBeInTheDocument();
1579
+ expect(screen.getByTestId(`detail:${DETAILS_CARD}`)).toBeInTheDocument();
1580
+ });
1581
+ });
1582
+
1583
+ describe("a static variable", () => {
1584
+ test("has an editable Variable Details card for this variable", async () => {
1585
+ await renderLoaded(staticVariable());
1586
+
1587
+ const details: CapturedDetailProps = detail(DETAILS_CARD);
1588
+
1589
+ expect(details.cardProps.title).toBe("Variable Details");
1590
+ expect(details.isEditable).toBe(true);
1591
+ expect(details.editButtonText).toBe("Edit Variable");
1592
+ expect(details.modelDetailProps.modelType).toBe(WorkflowVariable);
1593
+ expect(details.modelDetailProps.modelId.toString()).toBe(
1594
+ VARIABLE_ID.toString(),
1595
+ );
1596
+ });
1597
+
1598
+ test("its edit form offers the name, the description and the secret toggle", async () => {
1599
+ await renderLoaded(staticVariable({ isSecret: false }));
1600
+
1601
+ expect(detailFormFieldNames(DETAILS_CARD)).toEqual([
1602
+ "name",
1603
+ "description",
1604
+ "isSecret",
1605
+ ]);
1606
+ });
1607
+
1608
+ /*
1609
+ * The server refuses to turn the flag off again, so a toggle that can only
1610
+ * fail is not offered once the variable is secret.
1611
+ */
1612
+ test("its edit form has no secret toggle once the variable is secret", async () => {
1613
+ await renderLoaded(staticVariable({ isSecret: true }));
1614
+
1615
+ expect(detailFormFieldNames(DETAILS_CARD)).toEqual(["name", "description"]);
1616
+ });
1617
+
1618
+ test("never shows or edits the content on the details card", async () => {
1619
+ await renderLoaded(staticVariable());
1620
+
1621
+ const details: CapturedDetailProps = detail(DETAILS_CARD);
1622
+
1623
+ for (const column of WRITE_ONLY_COLUMNS) {
1624
+ expect(detailFormFieldNames(DETAILS_CARD)).not.toContain(column);
1625
+ expect(fieldNames(details.modelDetailProps.fields)).not.toContain(column);
1626
+ }
1627
+
1628
+ // The type is fixed once saved.
1629
+ expect(detailFormFieldNames(DETAILS_CARD)).not.toContain("variableType");
1630
+ });
1631
+
1632
+ test("saving the details reads the variable again, and drops the toggle once it is secret", async () => {
1633
+ await renderLoaded(staticVariable({ isSecret: false }));
1634
+
1635
+ getItem.mockResolvedValue(staticVariable({ isSecret: true }));
1636
+
1637
+ await act(async () => {
1638
+ detail(DETAILS_CARD).onSaveSuccess?.(staticVariable({ isSecret: true }));
1639
+ });
1640
+
1641
+ await waitFor(() => {
1642
+ expect(detailFormFieldNames(DETAILS_CARD)).toEqual([
1643
+ "name",
1644
+ "description",
1645
+ ]);
1646
+ });
1647
+ expect(getItem).toHaveBeenCalledTimes(2);
1648
+ });
1649
+
1650
+ test("a reload after a save keeps the page on screen instead of the loader", async () => {
1651
+ await renderLoaded(staticVariable());
1652
+
1653
+ const pending: Deferred<unknown> = deferred<unknown>();
1654
+ getItem.mockReturnValue(pending.promise);
1655
+
1656
+ await act(async () => {
1657
+ detail(DETAILS_CARD).onSaveSuccess?.(staticVariable());
1658
+ });
1659
+
1660
+ expect(screen.queryByTestId("page-loader")).not.toBeInTheDocument();
1661
+ expect(screen.getByTestId(`detail:${DETAILS_CARD}`)).toBeInTheDocument();
1662
+
1663
+ await act(async () => {
1664
+ pending.resolve(staticVariable());
1665
+ });
1666
+
1667
+ expectNoLoaderSinceLoad();
1668
+ });
1669
+
1670
+ test("shows its type as Static", async () => {
1671
+ await renderLoaded(staticVariable());
1672
+
1673
+ const element: HTMLElement = renderDetailElement(
1674
+ DETAILS_CARD,
1675
+ "Type",
1676
+ staticVariable(),
1677
+ );
1678
+
1679
+ expect(
1680
+ within(element).getByTestId("workflow-variable-type"),
1681
+ ).toHaveTextContent("Static");
1682
+ });
1683
+
1684
+ test("shows whether it is secret", async () => {
1685
+ await renderLoaded(staticVariable());
1686
+
1687
+ expect(
1688
+ renderDetailElement(
1689
+ DETAILS_CARD,
1690
+ "Secret",
1691
+ staticVariable({ isSecret: true }),
1692
+ ),
1693
+ ).toHaveTextContent("Secret");
1694
+ expect(
1695
+ renderDetailElement(
1696
+ DETAILS_CARD,
1697
+ "Secret",
1698
+ staticVariable({ isSecret: false }),
1699
+ ),
1700
+ ).toHaveTextContent("Not secret");
1701
+ expect(detailViewField(DETAILS_CARD, "Secret").description).toContain(
1702
+ "[REDACTED]",
1703
+ );
1704
+ });
1705
+
1706
+ test("has a Content card with an Update Content button", async () => {
1707
+ await renderLoaded(staticVariable());
1708
+
1709
+ expect(screen.getByTestId("card:Content")).toBeInTheDocument();
1710
+ expect(
1711
+ screen.getByTestId("card-button:Update Content"),
1712
+ ).toBeInTheDocument();
1713
+ expect(
1714
+ screen.getByTestId("workflow-variable-content-note"),
1715
+ ).toHaveTextContent("Hidden. The content cannot be viewed once saved.");
1716
+ });
1717
+
1718
+ test("never renders the content, even if the API were to return it", async () => {
1719
+ await renderLoaded(staticVariable({ content: "super-secret-value" }));
1720
+
1721
+ expect(screen.queryByText(/super-secret-value/)).not.toBeInTheDocument();
1722
+ });
1723
+
1724
+ test("Update Content opens the content modal for this variable", async () => {
1725
+ await renderLoaded(staticVariable());
1726
+
1727
+ expect(
1728
+ screen.queryByTestId("update-content-modal"),
1729
+ ).not.toBeInTheDocument();
1730
+
1731
+ fireEvent.click(screen.getByTestId("card-button:Update Content"));
1732
+
1733
+ expect(screen.getByTestId("update-content-modal")).toBeInTheDocument();
1734
+ expect(capturedContentModalProps?.variable.name).toBe("API_KEY");
1735
+ expect(String(capturedContentModalProps?.variable.id)).toBe(
1736
+ VARIABLE_ID.toString(),
1737
+ );
1738
+ });
1739
+
1740
+ test("a saved content closes the modal and says the content was updated", async () => {
1741
+ await renderLoaded(staticVariable());
1742
+
1743
+ fireEvent.click(screen.getByTestId("card-button:Update Content"));
1744
+
1745
+ act(() => {
1746
+ capturedContentModalProps?.onSuccess();
1747
+ });
1748
+
1749
+ expect(
1750
+ screen.queryByTestId("update-content-modal"),
1751
+ ).not.toBeInTheDocument();
1752
+ expect(
1753
+ screen.getByTestId("workflow-variable-content-note"),
1754
+ ).toHaveTextContent("Content updated.");
1755
+ });
1756
+
1757
+ test("a closed content modal changes nothing", async () => {
1758
+ await renderLoaded(staticVariable());
1759
+
1760
+ fireEvent.click(screen.getByTestId("card-button:Update Content"));
1761
+
1762
+ act(() => {
1763
+ capturedContentModalProps?.onClose();
1764
+ });
1765
+
1766
+ expect(
1767
+ screen.queryByTestId("update-content-modal"),
1768
+ ).not.toBeInTheDocument();
1769
+ expect(
1770
+ screen.getByTestId("workflow-variable-content-note"),
1771
+ ).toHaveTextContent("Hidden.");
1772
+ });
1773
+
1774
+ test("opening the content modal again clears the updated note", async () => {
1775
+ await renderLoaded(staticVariable());
1776
+
1777
+ fireEvent.click(screen.getByTestId("card-button:Update Content"));
1778
+
1779
+ act(() => {
1780
+ capturedContentModalProps?.onSuccess();
1781
+ });
1782
+
1783
+ fireEvent.click(screen.getByTestId("card-button:Update Content"));
1784
+
1785
+ expect(
1786
+ screen.getByTestId("workflow-variable-content-note"),
1787
+ ).toHaveTextContent("Hidden.");
1788
+ });
1789
+
1790
+ test("has no Access Token card, no OAuth 2.0 Settings and no Update Credentials", async () => {
1791
+ await renderLoaded(staticVariable());
1792
+
1793
+ expect(screen.queryByTestId("card:Access Token")).not.toBeInTheDocument();
1794
+ expect(screen.queryByTestId("token-status")).not.toBeInTheDocument();
1795
+ expect(
1796
+ screen.queryByTestId(`detail:${OAUTH_SETTINGS_CARD}`),
1797
+ ).not.toBeInTheDocument();
1798
+ expect(capturedDetails[OAUTH_SETTINGS_CARD]).toBeUndefined();
1799
+ expect(
1800
+ screen.queryByTestId("card-button:Update Credentials"),
1801
+ ).not.toBeInTheDocument();
1802
+ });
1803
+ });
1804
+
1805
+ describe("an OAuth 2.0 variable", () => {
1806
+ test("has an Access Token card showing the token's status with Refresh now", async () => {
1807
+ await renderLoaded(oauthVariable());
1808
+
1809
+ expect(screen.getByTestId("card:Access Token")).toBeInTheDocument();
1810
+ expect(
1811
+ within(screen.getByTestId("card:Access Token")).getByTestId(
1812
+ "token-status",
1813
+ ),
1814
+ ).toBeInTheDocument();
1815
+ expect(tokenStatus().variable.name).toBe("API_TOKEN");
1816
+ expect(String(tokenStatus().variable.id)).toBe(VARIABLE_ID.toString());
1817
+ expect(tokenStatus().refreshAction).toBeDefined();
1818
+ expect(screen.getByTestId("refresh-now")).toBeEnabled();
1819
+ });
1820
+
1821
+ test("has an editable OAuth 2.0 Settings card for this variable", async () => {
1822
+ await renderLoaded(oauthVariable());
1823
+
1824
+ const settings: CapturedDetailProps = detail(OAUTH_SETTINGS_CARD);
1825
+
1826
+ expect(settings.cardProps.title).toBe("OAuth 2.0 Settings");
1827
+ expect(settings.isEditable).toBe(true);
1828
+ expect(settings.editButtonText).toBe("Edit Settings");
1829
+ expect(settings.modelDetailProps.modelType).toBe(WorkflowVariable);
1830
+ expect(settings.modelDetailProps.modelId.toString()).toBe(
1831
+ VARIABLE_ID.toString(),
1832
+ );
1833
+ });
1834
+
1835
+ test("its settings form is exactly the readable settings", async () => {
1836
+ await renderLoaded(oauthVariable());
1837
+
1838
+ expect(detailFormFieldNames(OAUTH_SETTINGS_CARD)).toEqual([
1839
+ "oauthTokenUrl",
1840
+ "oauthClientId",
1841
+ "oauthScope",
1842
+ "oauthAdditionalParameters",
1843
+ "oauthClientAuthenticationMethod",
1844
+ ]);
1845
+ });
1846
+
1847
+ /*
1848
+ * The secret and refresh token are write-only (their own modal replaces
1849
+ * them), and the grant and the type are fixed once saved.
1850
+ */
1851
+ test("its settings form never carries a credential, the grant or the type", async () => {
1852
+ await renderLoaded(oauthVariable());
1853
+
1854
+ for (const column of [
1855
+ "oauthClientSecret",
1856
+ "oauthRefreshToken",
1857
+ "oauthAccessToken",
1858
+ "oauthGrantType",
1859
+ "variableType",
1860
+ "content",
1861
+ ]) {
1862
+ expect(detailFormFieldNames(OAUTH_SETTINGS_CARD)).not.toContain(column);
1863
+ }
1864
+ });
1865
+
1866
+ test("its settings card shows the grant but never a credential or a token", async () => {
1867
+ await renderLoaded(oauthVariable());
1868
+
1869
+ const shown: Array<string> = fieldNames(
1870
+ detail(OAUTH_SETTINGS_CARD).modelDetailProps.fields,
1871
+ );
1872
+
1873
+ expect(shown).toEqual(
1874
+ expect.arrayContaining([
1875
+ "oauthGrantType",
1876
+ "oauthTokenUrl",
1877
+ "oauthClientId",
1878
+ "oauthScope",
1879
+ "oauthClientAuthenticationMethod",
1880
+ "oauthAdditionalParameters",
1881
+ ]),
1882
+ );
1883
+
1884
+ for (const column of WRITE_ONLY_COLUMNS) {
1885
+ expect(shown).not.toContain(column);
1886
+ }
1887
+ });
1888
+
1889
+ test("shows the client authentication method in words", async () => {
1890
+ await renderLoaded(oauthVariable());
1891
+
1892
+ expect(
1893
+ renderDetailElement(
1894
+ OAUTH_SETTINGS_CARD,
1895
+ "Client Authentication",
1896
+ oauthVariable({
1897
+ oauthClientAuthenticationMethod:
1898
+ OAuth2ClientAuthenticationMethod.RequestBody,
1899
+ }),
1900
+ ),
1901
+ ).toHaveTextContent("Request body (client_secret_post)");
1902
+
1903
+ // Nothing saved means the default, HTTP Basic.
1904
+ expect(
1905
+ renderDetailElement(
1906
+ OAUTH_SETTINGS_CARD,
1907
+ "Client Authentication",
1908
+ oauthVariable(),
1909
+ ),
1910
+ ).toHaveTextContent("HTTP Basic header (client_secret_basic)");
1911
+ });
1912
+
1913
+ test("its details form has no secret toggle", async () => {
1914
+ await renderLoaded(oauthVariable({ isSecret: true }));
1915
+
1916
+ expect(detailFormFieldNames(DETAILS_CARD)).toEqual(["name", "description"]);
1917
+ });
1918
+
1919
+ test("its details form has no secret toggle even if the flag reads false", async () => {
1920
+ await renderLoaded(oauthVariable({ isSecret: false }));
1921
+
1922
+ expect(detailFormFieldNames(DETAILS_CARD)).toEqual(["name", "description"]);
1923
+ });
1924
+
1925
+ test("shows its type as OAuth 2.0, and that it is always secret", async () => {
1926
+ await renderLoaded(oauthVariable());
1927
+
1928
+ expect(
1929
+ renderDetailElement(DETAILS_CARD, "Type", oauthVariable()),
1930
+ ).toHaveTextContent("OAuth 2.0");
1931
+ expect(detailViewField(DETAILS_CARD, "Secret").description).toBe(
1932
+ "OAuth 2.0 variables are always secret.",
1933
+ );
1934
+ });
1935
+
1936
+ test("has an Update Credentials button that opens the credentials modal for this variable", async () => {
1937
+ await renderLoaded(oauthVariable());
1938
+
1939
+ expect(
1940
+ cardButton(detail(OAUTH_SETTINGS_CARD).cardProps, "Update Credentials"),
1941
+ ).toBeDefined();
1942
+ expect(
1943
+ screen.queryByTestId("update-credentials-modal"),
1944
+ ).not.toBeInTheDocument();
1945
+
1946
+ fireEvent.click(screen.getByTestId("card-button:Update Credentials"));
1947
+
1948
+ expect(screen.getByTestId("update-credentials-modal")).toBeInTheDocument();
1949
+ expect(capturedCredentialsModalProps?.variable.name).toBe("API_TOKEN");
1950
+ expect(String(capturedCredentialsModalProps?.variable.id)).toBe(
1951
+ VARIABLE_ID.toString(),
1952
+ );
1953
+ });
1954
+
1955
+ test("has no Content card and no Update Content", async () => {
1956
+ await renderLoaded(oauthVariable());
1957
+
1958
+ expect(screen.queryByTestId("card:Content")).not.toBeInTheDocument();
1959
+ expect(
1960
+ screen.queryByTestId("card-button:Update Content"),
1961
+ ).not.toBeInTheDocument();
1962
+ expect(
1963
+ screen.queryByTestId("workflow-variable-content-note"),
1964
+ ).not.toBeInTheDocument();
1965
+ });
1966
+
1967
+ // A settings change discards the cached token, so the status is stale.
1968
+ test("saving the settings reads the variable again", async () => {
1969
+ await renderLoaded(oauthVariable());
1970
+
1971
+ getItem.mockResolvedValue(
1972
+ oauthVariable({ oauthLastRefreshError: "invalid_scope" }),
1973
+ );
1974
+
1975
+ await act(async () => {
1976
+ detail(OAUTH_SETTINGS_CARD).onSaveSuccess?.(oauthVariable());
1977
+ });
1978
+
1979
+ await waitFor(() => {
1980
+ expect(tokenStatus().variable.oauthLastRefreshError).toBe(
1981
+ "invalid_scope",
1982
+ );
1983
+ });
1984
+ expect(getItem).toHaveBeenCalledTimes(2);
1985
+ });
1986
+ });
1987
+
1988
+ describe("how workflows refer to the variable", () => {
1989
+ test("the global page shows {{global.variables.NAME}}", async () => {
1990
+ await renderLoaded(staticVariable());
1991
+
1992
+ const element: HTMLElement = renderDetailElement(
1993
+ DETAILS_CARD,
1994
+ "Use in Workflows",
1995
+ staticVariable(),
1996
+ );
1997
+
1998
+ expect(
1999
+ within(element).getByTestId("workflow-variable-reference"),
2000
+ ).toHaveTextContent("{{global.variables.API_KEY}}");
2001
+ });
2002
+
2003
+ test("a workflow's page shows {{local.variables.NAME}}", async () => {
2004
+ await renderLoaded(localStaticVariable(), WORKFLOW_ID);
2005
+
2006
+ const element: HTMLElement = renderDetailElement(
2007
+ DETAILS_CARD,
2008
+ "Use in Workflows",
2009
+ localStaticVariable(),
2010
+ );
2011
+
2012
+ expect(
2013
+ within(element).getByTestId("workflow-variable-reference"),
2014
+ ).toHaveTextContent("{{local.variables.API_KEY}}");
2015
+ });
2016
+
2017
+ test("the reference follows the name the card renders", async () => {
2018
+ await renderLoaded(oauthVariable());
2019
+
2020
+ const element: HTMLElement = renderDetailElement(
2021
+ DETAILS_CARD,
2022
+ "Use in Workflows",
2023
+ oauthVariable({ name: "RENAMED_TOKEN" }),
2024
+ );
2025
+
2026
+ expect(
2027
+ within(element).getByTestId("workflow-variable-reference"),
2028
+ ).toHaveTextContent("{{global.variables.RENAMED_TOKEN}}");
2029
+ });
2030
+
2031
+ test("the rename warning on the name field quotes the right scope", async () => {
2032
+ await renderLoaded(staticVariable());
2033
+
2034
+ const globalName: FormFieldEntry | undefined = (detail(DETAILS_CARD)
2035
+ .formFields || [])[0];
2036
+
2037
+ expect(globalName?.description).toContain("{{global.variables.THIS_NAME}}");
2038
+ expect(globalName?.description).toContain("Renaming it does not update");
2039
+
2040
+ cleanup();
2041
+ capturedDetails = {};
2042
+
2043
+ await renderLoaded(localStaticVariable(), WORKFLOW_ID);
2044
+
2045
+ const localName: FormFieldEntry | undefined = (detail(DETAILS_CARD)
2046
+ .formFields || [])[0];
2047
+
2048
+ expect(localName?.description).toContain("{{local.variables.THIS_NAME}}");
2049
+ });
2050
+ });
2051
+
2052
+ describe("Refresh now", () => {
2053
+ test("posts to the variable's refresh route with the project header", async () => {
2054
+ await renderLoaded(oauthVariable());
2055
+
2056
+ await clickRefreshNow();
2057
+
2058
+ await waitFor(() => {
2059
+ expect(apiPost).toHaveBeenCalledTimes(1);
2060
+ });
2061
+ expect(String(refreshCall().url)).toContain(
2062
+ `/workflow-variable/${VARIABLE_ID.toString()}/refresh-oauth-token`,
2063
+ );
2064
+ expect(refreshCall().headers).toEqual({ tenantid: "project-header" });
2065
+ expect(refreshCall().data).toEqual({});
2066
+ });
2067
+
2068
+ test("shows the token that was fetched and when it expires", async () => {
2069
+ await renderLoaded(oauthVariable());
2070
+
2071
+ await clickRefreshNow();
2072
+ await waitForOutcome();
2073
+
2074
+ expect(screen.getByTestId("token-refresh-modal")).toHaveAttribute(
2075
+ "data-pending",
2076
+ "false",
2077
+ );
2078
+ expect(tokenModal().variableName).toBe("API_TOKEN");
2079
+
2080
+ const outcome: TokenRefreshOutcome = tokenModal()
2081
+ .outcome as TokenRefreshOutcome;
2082
+
2083
+ expect(outcome.variableName).toBe("API_TOKEN");
2084
+ expect(outcome.error).toBeUndefined();
2085
+ expect(outcome.savedWhat).toBeUndefined();
2086
+ expect(outcome.expiresAt?.toISOString()).toBe(TOKEN_EXPIRES_AT);
2087
+ expect(getTokenRefreshTitle(outcome)).toBe("Access Token Fetched");
2088
+ });
2089
+
2090
+ test("shows the identity provider's refusal", async () => {
2091
+ apiPost.mockRejectedValue(
2092
+ new Error(
2093
+ "The token endpoint refused the request (HTTP 401): invalid_client.",
2094
+ ),
2095
+ );
2096
+
2097
+ await renderLoaded(oauthVariable());
2098
+
2099
+ await clickRefreshNow();
2100
+ await waitForOutcome();
2101
+
2102
+ const outcome: TokenRefreshOutcome = tokenModal()
2103
+ .outcome as TokenRefreshOutcome;
2104
+
2105
+ expect(outcome.error).toBe(
2106
+ "The token endpoint refused the request (HTTP 401): invalid_client.",
2107
+ );
2108
+ expect(getTokenRefreshTitle(outcome)).toBe(
2109
+ "Could Not Fetch an Access Token",
2110
+ );
2111
+ expect(getTokenRefreshDescription(outcome)).toContain("invalid_client");
2112
+ });
2113
+
2114
+ // Refresh now has its own spinner, so no pending modal opens over it.
2115
+ test("spins the button while the request is out, without a pending modal", async () => {
2116
+ const pending: Deferred<unknown> = deferred<unknown>();
2117
+ apiPost.mockReturnValue(pending.promise);
2118
+
2119
+ await renderLoaded(oauthVariable());
2120
+
2121
+ await clickRefreshNow();
2122
+
2123
+ expect(tokenStatus().refreshAction?.isLoading).toBe(true);
2124
+ expect(screen.getByTestId("refresh-now")).toBeDisabled();
2125
+ expect(screen.queryByTestId("token-refresh-modal")).not.toBeInTheDocument();
2126
+
2127
+ await act(async () => {
2128
+ pending.resolve({
2129
+ data: { oauthAccessTokenExpiresAt: TOKEN_EXPIRES_AT },
2130
+ });
2131
+ });
2132
+
2133
+ await waitForOutcome();
2134
+
2135
+ expect(tokenStatus().refreshAction?.isLoading).toBe(false);
2136
+ expect(screen.getByTestId("refresh-now")).toBeEnabled();
2137
+ });
2138
+
2139
+ // The refresh wrote the expiry, or the failure, to the variable.
2140
+ test("reads the variable again afterwards", async () => {
2141
+ await renderLoaded(oauthVariable());
2142
+
2143
+ getItem.mockResolvedValue(
2144
+ oauthVariable({ oauthLastRefreshError: "invalid_client" }),
2145
+ );
2146
+
2147
+ await clickRefreshNow();
2148
+ await waitForOutcome();
2149
+
2150
+ await waitFor(() => {
2151
+ expect(getItem).toHaveBeenCalledTimes(2);
2152
+ });
2153
+ expect(getItemCall(1).id.toString()).toBe(VARIABLE_ID.toString());
2154
+ await waitFor(() => {
2155
+ expect(tokenStatus().variable.oauthLastRefreshError).toBe(
2156
+ "invalid_client",
2157
+ );
2158
+ });
2159
+ expect(screen.queryByTestId("page-loader")).not.toBeInTheDocument();
2160
+ });
2161
+
2162
+ test("reads the variable again after a failure too", async () => {
2163
+ apiPost.mockRejectedValue(new Error("invalid_client"));
2164
+
2165
+ await renderLoaded(oauthVariable());
2166
+
2167
+ await clickRefreshNow();
2168
+ await waitForOutcome();
2169
+
2170
+ await waitFor(() => {
2171
+ expect(getItem).toHaveBeenCalledTimes(2);
2172
+ });
2173
+ });
2174
+
2175
+ test("the outcome modal closes", async () => {
2176
+ await renderLoaded(oauthVariable());
2177
+
2178
+ await clickRefreshNow();
2179
+ await waitForOutcome();
2180
+
2181
+ act(() => {
2182
+ tokenModal().onClose();
2183
+ });
2184
+
2185
+ expect(screen.queryByTestId("token-refresh-modal")).not.toBeInTheDocument();
2186
+ });
2187
+ });
2188
+
2189
+ describe("Update Credentials", () => {
2190
+ test("closing the modal saves nothing and fetches nothing", async () => {
2191
+ await renderLoaded(oauthVariable());
2192
+
2193
+ fireEvent.click(screen.getByTestId("card-button:Update Credentials"));
2194
+
2195
+ act(() => {
2196
+ capturedCredentialsModalProps?.onClose();
2197
+ });
2198
+
2199
+ expect(
2200
+ screen.queryByTestId("update-credentials-modal"),
2201
+ ).not.toBeInTheDocument();
2202
+ expect(apiPost).not.toHaveBeenCalled();
2203
+ expect(screen.queryByTestId("token-refresh-modal")).not.toBeInTheDocument();
2204
+ });
2205
+
2206
+ /*
2207
+ * A token request can take a while. The credentials modal has just closed,
2208
+ * so a pending modal says at once that a token is on its way.
2209
+ */
2210
+ test("a save closes the modal and shows a pending token modal at once", async () => {
2211
+ const pending: Deferred<unknown> = deferred<unknown>();
2212
+ apiPost.mockReturnValue(pending.promise);
2213
+
2214
+ await renderLoaded(oauthVariable());
2215
+
2216
+ fireEvent.click(screen.getByTestId("card-button:Update Credentials"));
2217
+
2218
+ await act(async () => {
2219
+ capturedCredentialsModalProps?.onSaved("Client secret");
2220
+ });
2221
+
2222
+ expect(
2223
+ screen.queryByTestId("update-credentials-modal"),
2224
+ ).not.toBeInTheDocument();
2225
+ expect(screen.getByTestId("token-refresh-modal")).toHaveAttribute(
2226
+ "data-pending",
2227
+ "true",
2228
+ );
2229
+ expect(tokenModal().outcome).toBeNull();
2230
+ expect(tokenModal().variableName).toBe("API_TOKEN");
2231
+ expect(apiPost).toHaveBeenCalledTimes(1);
2232
+ expect(String(refreshCall().url)).toContain(
2233
+ `/workflow-variable/${VARIABLE_ID.toString()}/refresh-oauth-token`,
2234
+ );
2235
+
2236
+ await act(async () => {
2237
+ pending.resolve({
2238
+ data: { oauthAccessTokenExpiresAt: TOKEN_EXPIRES_AT },
2239
+ });
2240
+ });
2241
+
2242
+ await waitForOutcome();
2243
+
2244
+ expect(screen.getByTestId("token-refresh-modal")).toHaveAttribute(
2245
+ "data-pending",
2246
+ "false",
2247
+ );
2248
+ });
2249
+
2250
+ test("the outcome says what was saved and that a token was fetched", async () => {
2251
+ await renderLoaded(oauthVariable());
2252
+
2253
+ fireEvent.click(screen.getByTestId("card-button:Update Credentials"));
2254
+
2255
+ await act(async () => {
2256
+ capturedCredentialsModalProps?.onSaved("Client secret");
2257
+ });
2258
+
2259
+ await waitForOutcome();
2260
+
2261
+ const outcome: TokenRefreshOutcome = tokenModal()
2262
+ .outcome as TokenRefreshOutcome;
2263
+
2264
+ expect(outcome.savedWhat).toBe("Client secret");
2265
+ expect(outcome.variableName).toBe("API_TOKEN");
2266
+ expect(outcome.error).toBeUndefined();
2267
+ expect(outcome.expiresAt?.toISOString()).toBe(TOKEN_EXPIRES_AT);
2268
+ expect(getTokenRefreshDescription(outcome)).toMatch(
2269
+ /^Client secret saved\. OneUptime fetched a new access token/,
2270
+ );
2271
+ });
2272
+
2273
+ test("a saved secret the provider then refused says both", async () => {
2274
+ apiPost.mockRejectedValue(new Error("invalid_client"));
2275
+
2276
+ await renderLoaded(oauthVariable());
2277
+
2278
+ fireEvent.click(screen.getByTestId("card-button:Update Credentials"));
2279
+
2280
+ await act(async () => {
2281
+ capturedCredentialsModalProps?.onSaved("Client secret");
2282
+ });
2283
+
2284
+ await waitForOutcome();
2285
+
2286
+ const outcome: TokenRefreshOutcome = tokenModal()
2287
+ .outcome as TokenRefreshOutcome;
2288
+
2289
+ expect(outcome.savedWhat).toBe("Client secret");
2290
+ expect(outcome.error).toBe("invalid_client");
2291
+ expect(getTokenRefreshTitle(outcome)).toBe(
2292
+ "Could Not Fetch an Access Token",
2293
+ );
2294
+ expect(getTokenRefreshDescription(outcome)).toMatch(
2295
+ /^Client secret saved\. /,
2296
+ );
2297
+ });
2298
+
2299
+ test("reads the variable again after the token request", async () => {
2300
+ await renderLoaded(oauthVariable());
2301
+
2302
+ fireEvent.click(screen.getByTestId("card-button:Update Credentials"));
2303
+
2304
+ await act(async () => {
2305
+ capturedCredentialsModalProps?.onSaved("Client secret and refresh token");
2306
+ });
2307
+
2308
+ await waitForOutcome();
2309
+
2310
+ await waitFor(() => {
2311
+ expect(getItem).toHaveBeenCalledTimes(2);
2312
+ });
2313
+ expect(tokenModal().outcome?.savedWhat).toBe(
2314
+ "Client secret and refresh token",
2315
+ );
2316
+ });
2317
+ });
2318
+
2319
+ describe("permissions", () => {
2320
+ describe("with permission to update variables", () => {
2321
+ test("Update Content is enabled and says what it does", async () => {
2322
+ await renderLoaded(staticVariable());
2323
+
2324
+ const button: HTMLElement = screen.getByTestId(
2325
+ "card-button:Update Content",
2326
+ );
2327
+
2328
+ expect(button).toBeEnabled();
2329
+ expect(button.getAttribute("title")).toContain(
2330
+ "Replace this variable's content",
2331
+ );
2332
+ });
2333
+
2334
+ test("Update Credentials and Refresh now are enabled", async () => {
2335
+ await renderLoaded(oauthVariable());
2336
+
2337
+ expect(
2338
+ screen.getByTestId("card-button:Update Credentials"),
2339
+ ).toBeEnabled();
2340
+ expect(screen.getByTestId("refresh-now")).toBeEnabled();
2341
+ expect(tokenStatus().refreshAction?.disabled).toBe(false);
2342
+ expect(tokenStatus().refreshAction?.tooltip).toContain(
2343
+ "Fetch a new access token",
2344
+ );
2345
+ });
2346
+
2347
+ test("the details and settings cards are editable", async () => {
2348
+ await renderLoaded(oauthVariable());
2349
+
2350
+ expect(detail(DETAILS_CARD).isEditable).toBe(true);
2351
+ expect(detail(OAUTH_SETTINGS_CARD).isEditable).toBe(true);
2352
+ });
2353
+ });
2354
+
2355
+ describe("without permission to update variables", () => {
2356
+ beforeEach(() => {
2357
+ permissionsForTest = [Permission.Viewer];
2358
+ });
2359
+
2360
+ test("Update Content is shown disabled, naming the permission it needs", async () => {
2361
+ await renderLoaded(staticVariable());
2362
+
2363
+ const button: HTMLElement = screen.getByTestId(
2364
+ "card-button:Update Content",
2365
+ );
2366
+
2367
+ expect(button).toBeDisabled();
2368
+ expect(button).toHaveAttribute("title", missingUpdatePermissionMessage());
2369
+ expect(button.getAttribute("title")).toContain(
2370
+ "You do not have permission to update",
2371
+ );
2372
+ expect(button.getAttribute("title")).toContain("Project Admin");
2373
+ });
2374
+
2375
+ test("Update Content opens nothing even if its click fires", async () => {
2376
+ await renderLoaded(staticVariable());
2377
+
2378
+ act(() => {
2379
+ cardButton(card("Content"), "Update Content")!.onClick();
2380
+ });
2381
+
2382
+ expect(
2383
+ screen.queryByTestId("update-content-modal"),
2384
+ ).not.toBeInTheDocument();
2385
+ });
2386
+
2387
+ test("Update Credentials is shown disabled, naming the permission it needs", async () => {
2388
+ await renderLoaded(oauthVariable());
2389
+
2390
+ const button: HTMLElement = screen.getByTestId(
2391
+ "card-button:Update Credentials",
2392
+ );
2393
+
2394
+ expect(button).toBeDisabled();
2395
+ expect(button).toHaveAttribute("title", missingUpdatePermissionMessage());
2396
+ });
2397
+
2398
+ test("Update Credentials opens nothing even if its click fires", async () => {
2399
+ await renderLoaded(oauthVariable());
2400
+
2401
+ act(() => {
2402
+ cardButton(
2403
+ detail(OAUTH_SETTINGS_CARD).cardProps,
2404
+ "Update Credentials",
2405
+ )!.onClick();
2406
+ });
2407
+
2408
+ expect(
2409
+ screen.queryByTestId("update-credentials-modal"),
2410
+ ).not.toBeInTheDocument();
2411
+ });
2412
+
2413
+ test("Refresh now is shown disabled, naming the permission it needs", async () => {
2414
+ await renderLoaded(oauthVariable());
2415
+
2416
+ expect(screen.getByTestId("refresh-now")).toBeDisabled();
2417
+ expect(tokenStatus().refreshAction?.disabled).toBe(true);
2418
+ expect(tokenStatus().refreshAction?.tooltip).toBe(
2419
+ missingUpdatePermissionMessage(),
2420
+ );
2421
+ });
2422
+ });
2423
+
2424
+ /*
2425
+ * The snapshot arrives on a response header, so it is empty for a moment
2426
+ * after login. A disabled button then would blame somebody who may well
2427
+ * hold the permission, so the affordances stay hidden instead.
2428
+ */
2429
+ describe("while the permission snapshot is still empty", () => {
2430
+ beforeEach(() => {
2431
+ permissionsForTest = [];
2432
+ });
2433
+
2434
+ test("Update Content is hidden, but the Content card stays", async () => {
2435
+ await renderLoaded(staticVariable());
2436
+
2437
+ expect(screen.getByTestId("card:Content")).toBeInTheDocument();
2438
+ expect(
2439
+ screen.queryByTestId("card-button:Update Content"),
2440
+ ).not.toBeInTheDocument();
2441
+ expect(card("Content").buttons).toEqual([]);
2442
+ });
2443
+
2444
+ test("Update Credentials and Refresh now are hidden, but the cards stay", async () => {
2445
+ await renderLoaded(oauthVariable());
2446
+
2447
+ expect(screen.getByTestId("card:Access Token")).toBeInTheDocument();
2448
+ expect(
2449
+ screen.getByTestId(`detail:${OAUTH_SETTINGS_CARD}`),
2450
+ ).toBeInTheDocument();
2451
+ expect(
2452
+ screen.queryByTestId("card-button:Update Credentials"),
2453
+ ).not.toBeInTheDocument();
2454
+ expect(detail(OAUTH_SETTINGS_CARD).cardProps.buttons).toEqual([]);
2455
+ expect(screen.queryByTestId("refresh-now")).not.toBeInTheDocument();
2456
+ expect(tokenStatus().refreshAction).toBeUndefined();
2457
+ });
2458
+ });
2459
+
2460
+ test("a master admin may update without any project permission", async () => {
2461
+ permissionsForTest = [];
2462
+ isMasterAdminForTest = true;
2463
+
2464
+ await renderLoaded(oauthVariable());
2465
+
2466
+ expect(screen.getByTestId("card-button:Update Credentials")).toBeEnabled();
2467
+ expect(screen.getByTestId("refresh-now")).toBeEnabled();
2468
+ });
2469
+ });
2470
+
2471
+ describe("deleting the variable", () => {
2472
+ test("the delete card deletes this workflow variable", async () => {
2473
+ await renderLoaded(staticVariable());
2474
+
2475
+ expect(capturedDeleteProps?.modelType).toBe(WorkflowVariable);
2476
+ expect(capturedDeleteProps?.modelId.toString()).toBe(
2477
+ VARIABLE_ID.toString(),
2478
+ );
2479
+ });
2480
+
2481
+ test("an OAuth 2.0 variable can be deleted too", async () => {
2482
+ await renderLoaded(oauthVariable());
2483
+
2484
+ expect(screen.getByTestId("model-delete")).toBeInTheDocument();
2485
+ expect(capturedDeleteProps?.modelId.toString()).toBe(
2486
+ VARIABLE_ID.toString(),
2487
+ );
2488
+ });
2489
+
2490
+ test("after a delete the global page goes back to the global variables", async () => {
2491
+ await renderLoaded(staticVariable());
2492
+
2493
+ act(() => {
2494
+ capturedDeleteProps?.onDeleteSuccess();
2495
+ });
2496
+
2497
+ expect(navigate).toHaveBeenCalledTimes(1);
2498
+ expect(String(navigate.mock.calls[0]![0])).toBe(
2499
+ `/dashboard/${PROJECT_ID.toString()}/workflows/variables`,
2500
+ );
2501
+ });
2502
+
2503
+ test("after a delete a workflow's page goes back to that workflow's variables", async () => {
2504
+ await renderLoaded(localStaticVariable(), WORKFLOW_ID);
2505
+
2506
+ act(() => {
2507
+ capturedDeleteProps?.onDeleteSuccess();
2508
+ });
2509
+
2510
+ expect(navigate).toHaveBeenCalledTimes(1);
2511
+ expect(String(navigate.mock.calls[0]![0])).toBe(
2512
+ `/dashboard/${PROJECT_ID.toString()}/workflows/${WORKFLOW_ID.toString()}/variables`,
2513
+ );
2514
+ });
2515
+ });
2516
+
2517
+ /*
2518
+ * The list's row actions (Show ID, Update Content, Update Credentials, Edit
2519
+ * Details, Delete) and its Token column's Refresh now all moved here. If one
2520
+ * goes missing from the page, it is gone from the product.
2521
+ */
2522
+ describe("everything the list's row actions used to do", () => {
2523
+ test("a static variable's page can show its id, edit it, replace its content and delete it", async () => {
2524
+ await renderLoaded(staticVariable());
2525
+
2526
+ expect(fieldNames(detail(DETAILS_CARD).modelDetailProps.fields)).toContain(
2527
+ "_id",
2528
+ );
2529
+ expect(detail(DETAILS_CARD).isEditable).toBe(true);
2530
+ expect(
2531
+ screen.getByTestId("card-button:Update Content"),
2532
+ ).toBeInTheDocument();
2533
+ expect(screen.getByTestId("model-delete")).toBeInTheDocument();
2534
+ });
2535
+
2536
+ test("an OAuth 2.0 variable's page can show its id, edit it, replace its credentials, refresh its token and delete it", async () => {
2537
+ await renderLoaded(oauthVariable());
2538
+
2539
+ expect(fieldNames(detail(DETAILS_CARD).modelDetailProps.fields)).toContain(
2540
+ "_id",
2541
+ );
2542
+ expect(detail(DETAILS_CARD).isEditable).toBe(true);
2543
+ expect(detail(OAUTH_SETTINGS_CARD).isEditable).toBe(true);
2544
+ expect(
2545
+ screen.getByTestId("card-button:Update Credentials"),
2546
+ ).toBeInTheDocument();
2547
+ expect(screen.getByTestId("refresh-now")).toBeInTheDocument();
2548
+ expect(screen.getByTestId("model-delete")).toBeInTheDocument();
2549
+ });
2550
+
2551
+ test("the same doors are there on a workflow's page", async () => {
2552
+ await renderLoaded(localOAuthVariable(), WORKFLOW_ID);
2553
+
2554
+ expect(detail(DETAILS_CARD).isEditable).toBe(true);
2555
+ expect(detail(OAUTH_SETTINGS_CARD).isEditable).toBe(true);
2556
+ expect(
2557
+ screen.getByTestId("card-button:Update Credentials"),
2558
+ ).toBeInTheDocument();
2559
+ expect(screen.getByTestId("refresh-now")).toBeInTheDocument();
2560
+ expect(screen.getByTestId("model-delete")).toBeInTheDocument();
2561
+ });
2562
+ });
2563
+
2564
+ /*
2565
+ * The page wrappers key WorkflowVariableView by the ids they read from the
2566
+ * route. React Router keeps the page mounted when only a param changes, so
2567
+ * without the key, going straight from one variable's page to another's would
2568
+ * hand the new variable to the old instance - with the old one's modals, notes
2569
+ * and requests still attached to it.
2570
+ */
2571
+ describe("the page wrappers", () => {
2572
+ /*
2573
+ * The variable a wrapper moves on to: another one, of the other kind, so
2574
+ * the layout alone says which variable is on screen.
2575
+ */
2576
+ function otherStaticVariable(
2577
+ overrides?: Partial<Record<string, unknown>>,
2578
+ ): WorkflowVariable {
2579
+ return staticVariable({
2580
+ _id: OTHER_VARIABLE_ID.toString(),
2581
+ name: "OTHER",
2582
+ ...(overrides || {}),
2583
+ });
2584
+ }
2585
+
2586
+ function contentNote(): HTMLElement {
2587
+ return screen.getByTestId("workflow-variable-content-note");
2588
+ }
2589
+
2590
+ // Sets the page's "Content updated" note, which lives only in the page.
2591
+ function markContentUpdated(): void {
2592
+ fireEvent.click(screen.getByTestId("card-button:Update Content"));
2593
+
2594
+ act(() => {
2595
+ capturedContentModalProps?.onSuccess();
2596
+ });
2597
+
2598
+ expect(contentNote()).toHaveTextContent("Content updated.");
2599
+ }
2600
+
2601
+ // The other variable's page, whole, and nothing of the previous one's.
2602
+ function expectOtherStaticVariableShown(): void {
2603
+ expect(screen.queryByTestId("page-loader")).not.toBeInTheDocument();
2604
+ expect(screen.getByTestId("card:Content")).toBeInTheDocument();
2605
+ expect(screen.queryByTestId("card:Access Token")).not.toBeInTheDocument();
2606
+ expect(screen.queryByTestId("token-status")).not.toBeInTheDocument();
2607
+ expect(
2608
+ screen.queryByTestId(`detail:${OAUTH_SETTINGS_CARD}`),
2609
+ ).not.toBeInTheDocument();
2610
+ expect(screen.queryByTestId("token-refresh-modal")).not.toBeInTheDocument();
2611
+ expect(
2612
+ screen.queryByTestId("update-content-modal"),
2613
+ ).not.toBeInTheDocument();
2614
+ expect(detail(DETAILS_CARD).modelDetailProps.modelId.toString()).toBe(
2615
+ OTHER_VARIABLE_ID.toString(),
2616
+ );
2617
+ expect(capturedDeleteProps?.modelId.toString()).toBe(
2618
+ OTHER_VARIABLE_ID.toString(),
2619
+ );
2620
+
2621
+ // The variable the page acts on is the one it read for OTHER.
2622
+ fireEvent.click(screen.getByTestId("card-button:Update Content"));
2623
+ expect(capturedContentModalProps?.variable.name).toBe("OTHER");
2624
+
2625
+ // Closed again, so the check can run again later in the same test.
2626
+ act(() => {
2627
+ capturedContentModalProps?.onClose();
2628
+ });
2629
+ }
2630
+
2631
+ type WrapperUnderTest = {
2632
+ renderPage: () => ReturnType<typeof render>;
2633
+ moveToOtherVariable: (result: ReturnType<typeof render>) => void;
2634
+ // An OAuth 2.0 variable, so it has a Refresh now.
2635
+ previousVariable: () => WorkflowVariable;
2636
+ };
2637
+
2638
+ type HeldRefreshRace = {
2639
+ refresh: Deferred<unknown>;
2640
+ otherRead: Deferred<unknown>;
2641
+ previousFollowUpRead: Deferred<unknown>;
2642
+ };
2643
+
2644
+ /*
2645
+ * The race the key closes: Refresh now on an OAuth 2.0 variable, its request
2646
+ * still out, and the page moves straight to another variable. The refresh,
2647
+ * the other variable's read and the read the refresh makes afterwards are
2648
+ * all held, so each test picks the order they land in.
2649
+ */
2650
+ async function startRefreshThenMove(
2651
+ wrapper: WrapperUnderTest,
2652
+ ): Promise<HeldRefreshRace> {
2653
+ const refresh: Deferred<unknown> = deferred<unknown>();
2654
+ apiPost.mockReturnValue(refresh.promise);
2655
+
2656
+ getItem.mockResolvedValueOnce(wrapper.previousVariable());
2657
+ const otherRead: Deferred<unknown> = holdNextRead();
2658
+ const previousFollowUpRead: Deferred<unknown> = holdNextRead();
2659
+
2660
+ const result: ReturnType<typeof render> = wrapper.renderPage();
2661
+
2662
+ await waitForPage();
2663
+ expectOAuthPageShown();
2664
+
2665
+ await clickRefreshNow();
2666
+
2667
+ expect(apiPost).toHaveBeenCalledTimes(1);
2668
+ expect(screen.getByTestId("refresh-now")).toBeDisabled();
2669
+
2670
+ wrapper.moveToOtherVariable(result);
2671
+
2672
+ await waitForReads(2);
2673
+ expect(getItemCall(1).id.toString()).toBe(OTHER_VARIABLE_ID.toString());
2674
+ expect(screen.getByTestId("page-loader")).toBeInTheDocument();
2675
+ expect(screen.queryByTestId("model-delete")).not.toBeInTheDocument();
2676
+
2677
+ return { refresh, otherRead, previousFollowUpRead };
2678
+ }
2679
+
2680
+ /*
2681
+ * The previous variable's page is gone, but its refresh still finishes -
2682
+ * and reads its own variable again, as it always does afterwards.
2683
+ */
2684
+ async function finishPreviousRefresh(race: HeldRefreshRace): Promise<void> {
2685
+ await act(async () => {
2686
+ race.refresh.resolve({
2687
+ data: { oauthAccessTokenExpiresAt: TOKEN_EXPIRES_AT },
2688
+ });
2689
+ });
2690
+
2691
+ await waitForReads(3);
2692
+ expect(getItemCall(2).id.toString()).toBe(VARIABLE_ID.toString());
2693
+ }
2694
+
2695
+ describe("Workflows > Global Variables > View Variable", () => {
2696
+ function globalPage(): ReactElement {
2697
+ return (
2698
+ <GlobalWorkflowVariableViewPage
2699
+ {...({} as unknown as React.ComponentProps<
2700
+ typeof GlobalWorkflowVariableViewPage
2701
+ >)}
2702
+ />
2703
+ );
2704
+ }
2705
+
2706
+ function renderGlobalPage(): ReturnType<typeof render> {
2707
+ return render(globalPage());
2708
+ }
2709
+
2710
+ // What following a link to another variable's page does to the wrapper.
2711
+ function moveGlobalPageTo(
2712
+ result: ReturnType<typeof render>,
2713
+ variableId: ObjectID,
2714
+ ): void {
2715
+ lastParams = { 0: variableId };
2716
+ result.rerender(globalPage());
2717
+ }
2718
+
2719
+ const globalWrapper: WrapperUnderTest = {
2720
+ renderPage: renderGlobalPage,
2721
+ moveToOtherVariable: (result: ReturnType<typeof render>): void => {
2722
+ moveGlobalPageTo(result, OTHER_VARIABLE_ID);
2723
+ },
2724
+ previousVariable: (): WorkflowVariable => {
2725
+ return oauthVariable();
2726
+ },
2727
+ };
2728
+
2729
+ beforeEach(() => {
2730
+ // /dashboard/:projectId/workflows/variables/:id
2731
+ lastParams = { 0: VARIABLE_ID };
2732
+ });
2733
+
2734
+ test("reads the variable named by the last route segment", async () => {
2735
+ getItem.mockResolvedValue(staticVariable());
2736
+
2737
+ renderGlobalPage();
2738
+
2739
+ await waitForPage();
2740
+
2741
+ expect(getLastParamAsObjectID).toHaveBeenCalledWith(0);
2742
+ expect(getItemCall().id.toString()).toBe(VARIABLE_ID.toString());
2743
+ expect(capturedDeleteProps?.modelId.toString()).toBe(
2744
+ VARIABLE_ID.toString(),
2745
+ );
2746
+ });
2747
+
2748
+ test("is the global page: a global reference, and back to the global list after a delete", async () => {
2749
+ getItem.mockResolvedValue(staticVariable());
2750
+
2751
+ renderGlobalPage();
2752
+
2753
+ await waitForPage();
2754
+
2755
+ expect(
2756
+ within(
2757
+ renderDetailElement(
2758
+ DETAILS_CARD,
2759
+ "Use in Workflows",
2760
+ staticVariable(),
2761
+ ),
2762
+ ).getByTestId("workflow-variable-reference"),
2763
+ ).toHaveTextContent("{{global.variables.API_KEY}}");
2764
+
2765
+ act(() => {
2766
+ capturedDeleteProps?.onDeleteSuccess();
2767
+ });
2768
+
2769
+ expect(String(navigate.mock.calls[0]![0])).toBe(
2770
+ `/dashboard/${PROJECT_ID.toString()}/workflows/variables`,
2771
+ );
2772
+ });
2773
+
2774
+ test("refuses a variable that belongs to a workflow", async () => {
2775
+ getItem.mockResolvedValue(localStaticVariable());
2776
+
2777
+ renderGlobalPage();
2778
+
2779
+ expect(
2780
+ await screen.findByText(/This variable belongs to a workflow/),
2781
+ ).toBeInTheDocument();
2782
+ });
2783
+
2784
+ describe("moving straight to another variable's page", () => {
2785
+ test("mounts a fresh page: the loader again, a read of the new variable, and no note from the previous one", async () => {
2786
+ getItem.mockResolvedValueOnce(staticVariable());
2787
+ const otherRead: Deferred<unknown> = holdNextRead();
2788
+
2789
+ const result: ReturnType<typeof render> = renderGlobalPage();
2790
+
2791
+ await waitForPage();
2792
+
2793
+ markContentUpdated();
2794
+
2795
+ moveGlobalPageTo(result, OTHER_VARIABLE_ID);
2796
+
2797
+ await waitForReads(2);
2798
+ expect(getItemCall(1).id.toString()).toBe(OTHER_VARIABLE_ID.toString());
2799
+ expect(screen.getByTestId("page-loader")).toBeInTheDocument();
2800
+ expect(screen.queryByTestId("model-delete")).not.toBeInTheDocument();
2801
+
2802
+ await act(async () => {
2803
+ otherRead.resolve(otherStaticVariable());
2804
+ });
2805
+
2806
+ await waitForPage();
2807
+
2808
+ expect(contentNote()).toHaveTextContent("Hidden.");
2809
+ expectOtherStaticVariableShown();
2810
+ });
2811
+
2812
+ // The content modal was for the previous variable's content.
2813
+ test("an open content modal does not follow onto the new variable's page", async () => {
2814
+ getItem.mockResolvedValueOnce(staticVariable());
2815
+ const otherRead: Deferred<unknown> = holdNextRead();
2816
+
2817
+ const result: ReturnType<typeof render> = renderGlobalPage();
2818
+
2819
+ await waitForPage();
2820
+
2821
+ fireEvent.click(screen.getByTestId("card-button:Update Content"));
2822
+ expect(screen.getByTestId("update-content-modal")).toBeInTheDocument();
2823
+
2824
+ moveGlobalPageTo(result, OTHER_VARIABLE_ID);
2825
+
2826
+ await waitForReads(2);
2827
+
2828
+ await act(async () => {
2829
+ otherRead.resolve(otherStaticVariable());
2830
+ });
2831
+
2832
+ await waitForPage();
2833
+
2834
+ expectOtherStaticVariableShown();
2835
+ });
2836
+
2837
+ test("the previous variable's token outcome does not follow onto the new variable's page", async () => {
2838
+ getItem.mockResolvedValue(oauthVariable());
2839
+
2840
+ const result: ReturnType<typeof render> = renderGlobalPage();
2841
+
2842
+ await waitForPage();
2843
+
2844
+ await clickRefreshNow();
2845
+ await waitForOutcome();
2846
+ // The read the refresh makes afterwards.
2847
+ await waitForReads(2);
2848
+
2849
+ expect(screen.getByTestId("token-refresh-modal")).toBeInTheDocument();
2850
+
2851
+ const otherRead: Deferred<unknown> = holdNextRead();
2852
+
2853
+ moveGlobalPageTo(result, OTHER_VARIABLE_ID);
2854
+
2855
+ await waitForReads(3);
2856
+ expect(getItemCall(2).id.toString()).toBe(OTHER_VARIABLE_ID.toString());
2857
+ expect(screen.getByTestId("page-loader")).toBeInTheDocument();
2858
+
2859
+ await act(async () => {
2860
+ otherRead.resolve(otherStaticVariable());
2861
+ });
2862
+
2863
+ await waitForPage();
2864
+
2865
+ expectOtherStaticVariableShown();
2866
+ });
2867
+
2868
+ /*
2869
+ * One instance for both variables would show the previous variable's
2870
+ * outcome modal over the new page, and its follow-up read would put the
2871
+ * previous variable back on screen.
2872
+ */
2873
+ test("a token refresh still out for the previous variable never lands on the new variable's page", async () => {
2874
+ const race: HeldRefreshRace = await startRefreshThenMove(globalWrapper);
2875
+
2876
+ await act(async () => {
2877
+ race.otherRead.resolve(otherStaticVariable());
2878
+ });
2879
+
2880
+ await waitForPage();
2881
+ expectOtherStaticVariableShown();
2882
+
2883
+ await finishPreviousRefresh(race);
2884
+
2885
+ await act(async () => {
2886
+ race.previousFollowUpRead.resolve(oauthVariable());
2887
+ });
2888
+
2889
+ expectOtherStaticVariableShown();
2890
+ });
2891
+
2892
+ /*
2893
+ * One instance for both variables would let the refresh's follow-up read
2894
+ * outdate the new variable's read, and when that follow-up failed the
2895
+ * page would sit on the loader for good.
2896
+ */
2897
+ test("a token refresh that settles while the new variable loads leaves that load alone", async () => {
2898
+ const race: HeldRefreshRace = await startRefreshThenMove(globalWrapper);
2899
+
2900
+ await finishPreviousRefresh(race);
2901
+
2902
+ await act(async () => {
2903
+ race.previousFollowUpRead.reject(
2904
+ new Error("The API is unreachable."),
2905
+ );
2906
+ });
2907
+
2908
+ // Still waiting for the variable the page is on.
2909
+ expect(screen.getByTestId("page-loader")).toBeInTheDocument();
2910
+ expect(
2911
+ screen.queryByTestId("token-refresh-modal"),
2912
+ ).not.toBeInTheDocument();
2913
+
2914
+ await act(async () => {
2915
+ race.otherRead.resolve(otherStaticVariable());
2916
+ });
2917
+
2918
+ await waitForPage();
2919
+
2920
+ expect(
2921
+ screen.queryByText("The API is unreachable."),
2922
+ ).not.toBeInTheDocument();
2923
+ expectOtherStaticVariableShown();
2924
+ });
2925
+
2926
+ // A re-render of the same route is not a new page.
2927
+ test("a re-render for the same variable keeps the page as it is", async () => {
2928
+ getItem.mockResolvedValueOnce(staticVariable());
2929
+
2930
+ const result: ReturnType<typeof render> = renderGlobalPage();
2931
+
2932
+ await waitForPage();
2933
+ pageLoaderRenderCount = 0;
2934
+
2935
+ markContentUpdated();
2936
+
2937
+ // The same id, as a new ObjectID - as Navigation hands it back.
2938
+ moveGlobalPageTo(result, new ObjectID(VARIABLE_ID.toString()));
2939
+
2940
+ await act(async () => {
2941
+ await Promise.resolve();
2942
+ });
2943
+
2944
+ expect(getItem).toHaveBeenCalledTimes(1);
2945
+ expectNoLoaderSinceLoad();
2946
+ expect(contentNote()).toHaveTextContent("Content updated.");
2947
+ expect(screen.getByTestId("model-delete")).toBeInTheDocument();
2948
+ });
2949
+ });
2950
+ });
2951
+
2952
+ describe("Workflow > Workflow Variables > View Variable", () => {
2953
+ function localPage(): ReactElement {
2954
+ return (
2955
+ <LocalWorkflowVariableViewPage
2956
+ {...({} as unknown as React.ComponentProps<
2957
+ typeof LocalWorkflowVariableViewPage
2958
+ >)}
2959
+ />
2960
+ );
2961
+ }
2962
+
2963
+ function renderLocalPage(): ReturnType<typeof render> {
2964
+ return render(localPage());
2965
+ }
2966
+
2967
+ // What following a link to another variable's page does to the wrapper.
2968
+ function moveLocalPageTo(
2969
+ result: ReturnType<typeof render>,
2970
+ params: { variableId: ObjectID; workflowId: ObjectID },
2971
+ ): void {
2972
+ lastParams = { 0: params.variableId, 2: params.workflowId };
2973
+ result.rerender(localPage());
2974
+ }
2975
+
2976
+ const localWrapper: WrapperUnderTest = {
2977
+ renderPage: renderLocalPage,
2978
+ moveToOtherVariable: (result: ReturnType<typeof render>): void => {
2979
+ moveLocalPageTo(result, {
2980
+ variableId: OTHER_VARIABLE_ID,
2981
+ workflowId: WORKFLOW_ID,
2982
+ });
2983
+ },
2984
+ previousVariable: (): WorkflowVariable => {
2985
+ return localOAuthVariable();
2986
+ },
2987
+ };
2988
+
2989
+ beforeEach(() => {
2990
+ // /dashboard/:projectId/workflows/:id/variables/:subModelId
2991
+ lastParams = { 0: VARIABLE_ID, 2: WORKFLOW_ID };
2992
+ });
2993
+
2994
+ test("reads the variable from the last segment and the workflow from two before it", async () => {
2995
+ getItem.mockResolvedValue(localStaticVariable());
2996
+
2997
+ renderLocalPage();
2998
+
2999
+ await waitForPage();
3000
+
3001
+ expect(getLastParamAsObjectID).toHaveBeenCalledWith(0);
3002
+ expect(getLastParamAsObjectID).toHaveBeenCalledWith(2);
3003
+ expect(getItemCall().id.toString()).toBe(VARIABLE_ID.toString());
3004
+ expect(capturedDeleteProps?.modelId.toString()).toBe(
3005
+ VARIABLE_ID.toString(),
3006
+ );
3007
+ });
3008
+
3009
+ test("is the workflow's page: a local reference, and back to its variables after a delete", async () => {
3010
+ getItem.mockResolvedValue(localStaticVariable());
3011
+
3012
+ renderLocalPage();
3013
+
3014
+ await waitForPage();
3015
+
3016
+ expect(
3017
+ within(
3018
+ renderDetailElement(
3019
+ DETAILS_CARD,
3020
+ "Use in Workflows",
3021
+ localStaticVariable(),
3022
+ ),
3023
+ ).getByTestId("workflow-variable-reference"),
3024
+ ).toHaveTextContent("{{local.variables.API_KEY}}");
3025
+
3026
+ act(() => {
3027
+ capturedDeleteProps?.onDeleteSuccess();
3028
+ });
3029
+
3030
+ expect(String(navigate.mock.calls[0]![0])).toBe(
3031
+ `/dashboard/${PROJECT_ID.toString()}/workflows/${WORKFLOW_ID.toString()}/variables`,
3032
+ );
3033
+ });
3034
+
3035
+ test("refuses another workflow's variable", async () => {
3036
+ getItem.mockResolvedValue(
3037
+ staticVariable({ workflowId: OTHER_WORKFLOW_ID }),
3038
+ );
3039
+
3040
+ renderLocalPage();
3041
+
3042
+ expect(
3043
+ await screen.findByText(
3044
+ "This variable does not belong to this workflow.",
3045
+ ),
3046
+ ).toBeInTheDocument();
3047
+ expect(screen.queryByTestId("model-delete")).not.toBeInTheDocument();
3048
+ });
3049
+
3050
+ test("refuses a global variable", async () => {
3051
+ getItem.mockResolvedValue(staticVariable());
3052
+
3053
+ renderLocalPage();
3054
+
3055
+ expect(
3056
+ await screen.findByText(
3057
+ "This variable does not belong to this workflow.",
3058
+ ),
3059
+ ).toBeInTheDocument();
3060
+ });
3061
+
3062
+ // A deleted variable's link: it is gone, not someone else's.
3063
+ test("says a missing variable could not be found, not that it belongs elsewhere", async () => {
3064
+ getItem.mockResolvedValue(missingVariable());
3065
+
3066
+ renderLocalPage();
3067
+
3068
+ expect(await screen.findByText(NOT_FOUND_MESSAGE)).toBeInTheDocument();
3069
+ expect(
3070
+ screen.queryByText("This variable does not belong to this workflow."),
3071
+ ).not.toBeInTheDocument();
3072
+ expect(screen.queryByTestId("model-delete")).not.toBeInTheDocument();
3073
+ });
3074
+
3075
+ describe("moving straight to another variable's page", () => {
3076
+ test("another variable of the workflow mounts a fresh page, without the previous one's open modal", async () => {
3077
+ getItem.mockResolvedValueOnce(localStaticVariable());
3078
+ const otherRead: Deferred<unknown> = holdNextRead();
3079
+
3080
+ const result: ReturnType<typeof render> = renderLocalPage();
3081
+
3082
+ await waitForPage();
3083
+
3084
+ fireEvent.click(screen.getByTestId("card-button:Update Content"));
3085
+ expect(screen.getByTestId("update-content-modal")).toBeInTheDocument();
3086
+
3087
+ moveLocalPageTo(result, {
3088
+ variableId: OTHER_VARIABLE_ID,
3089
+ workflowId: WORKFLOW_ID,
3090
+ });
3091
+
3092
+ await waitForReads(2);
3093
+ expect(getItemCall(1).id.toString()).toBe(OTHER_VARIABLE_ID.toString());
3094
+ expect(screen.getByTestId("page-loader")).toBeInTheDocument();
3095
+ expect(screen.queryByTestId("model-delete")).not.toBeInTheDocument();
3096
+
3097
+ await act(async () => {
3098
+ otherRead.resolve(otherStaticVariable({ workflowId: WORKFLOW_ID }));
3099
+ });
3100
+
3101
+ await waitForPage();
3102
+
3103
+ expect(contentNote()).toHaveTextContent("Hidden.");
3104
+ expectOtherStaticVariableShown();
3105
+ });
3106
+
3107
+ /*
3108
+ * The same variable id under another workflow is another page: the scope
3109
+ * check, the reference and the way back after a delete all hang on the
3110
+ * workflow.
3111
+ */
3112
+ test("another workflow's page mounts a fresh page too", async () => {
3113
+ getItem.mockResolvedValueOnce(localStaticVariable());
3114
+ const nextRead: Deferred<unknown> = holdNextRead();
3115
+
3116
+ const result: ReturnType<typeof render> = renderLocalPage();
3117
+
3118
+ await waitForPage();
3119
+
3120
+ fireEvent.click(screen.getByTestId("card-button:Update Content"));
3121
+ expect(screen.getByTestId("update-content-modal")).toBeInTheDocument();
3122
+
3123
+ moveLocalPageTo(result, {
3124
+ variableId: VARIABLE_ID,
3125
+ workflowId: OTHER_WORKFLOW_ID,
3126
+ });
3127
+
3128
+ await waitForReads(2);
3129
+ expect(getItemCall(1).id.toString()).toBe(VARIABLE_ID.toString());
3130
+ expect(screen.getByTestId("page-loader")).toBeInTheDocument();
3131
+ expect(screen.queryByTestId("model-delete")).not.toBeInTheDocument();
3132
+
3133
+ await act(async () => {
3134
+ nextRead.resolve(staticVariable({ workflowId: OTHER_WORKFLOW_ID }));
3135
+ });
3136
+
3137
+ await waitForPage();
3138
+
3139
+ expect(
3140
+ screen.queryByTestId("update-content-modal"),
3141
+ ).not.toBeInTheDocument();
3142
+ expect(contentNote()).toHaveTextContent("Hidden.");
3143
+
3144
+ act(() => {
3145
+ capturedDeleteProps?.onDeleteSuccess();
3146
+ });
3147
+
3148
+ expect(String(navigate.mock.calls[0]![0])).toBe(
3149
+ `/dashboard/${PROJECT_ID.toString()}/workflows/${OTHER_WORKFLOW_ID.toString()}/variables`,
3150
+ );
3151
+ });
3152
+
3153
+ test("a token refresh still out for the previous variable never lands on the new variable's page", async () => {
3154
+ const race: HeldRefreshRace = await startRefreshThenMove(localWrapper);
3155
+
3156
+ await act(async () => {
3157
+ race.otherRead.resolve(
3158
+ otherStaticVariable({ workflowId: WORKFLOW_ID }),
3159
+ );
3160
+ });
3161
+
3162
+ await waitForPage();
3163
+ expectOtherStaticVariableShown();
3164
+
3165
+ await finishPreviousRefresh(race);
3166
+
3167
+ await act(async () => {
3168
+ race.previousFollowUpRead.resolve(localOAuthVariable());
3169
+ });
3170
+
3171
+ expectOtherStaticVariableShown();
3172
+ });
3173
+
3174
+ test("a token refresh that settles while the new variable loads leaves that load alone", async () => {
3175
+ const race: HeldRefreshRace = await startRefreshThenMove(localWrapper);
3176
+
3177
+ await finishPreviousRefresh(race);
3178
+
3179
+ await act(async () => {
3180
+ race.previousFollowUpRead.reject(
3181
+ new Error("The API is unreachable."),
3182
+ );
3183
+ });
3184
+
3185
+ expect(screen.getByTestId("page-loader")).toBeInTheDocument();
3186
+
3187
+ await act(async () => {
3188
+ race.otherRead.resolve(
3189
+ otherStaticVariable({ workflowId: WORKFLOW_ID }),
3190
+ );
3191
+ });
3192
+
3193
+ await waitForPage();
3194
+
3195
+ expectOtherStaticVariableShown();
3196
+ });
3197
+
3198
+ // A re-render of the same route is not a new page.
3199
+ test("a re-render for the same workflow and variable keeps the page as it is", async () => {
3200
+ getItem.mockResolvedValueOnce(localStaticVariable());
3201
+
3202
+ const result: ReturnType<typeof render> = renderLocalPage();
3203
+
3204
+ await waitForPage();
3205
+ pageLoaderRenderCount = 0;
3206
+
3207
+ fireEvent.click(screen.getByTestId("card-button:Update Content"));
3208
+ expect(screen.getByTestId("update-content-modal")).toBeInTheDocument();
3209
+
3210
+ // The same ids, as new ObjectIDs - as Navigation hands them back.
3211
+ moveLocalPageTo(result, {
3212
+ variableId: new ObjectID(VARIABLE_ID.toString()),
3213
+ workflowId: new ObjectID(WORKFLOW_ID.toString()),
3214
+ });
3215
+
3216
+ await act(async () => {
3217
+ await Promise.resolve();
3218
+ });
3219
+
3220
+ expect(getItem).toHaveBeenCalledTimes(1);
3221
+ expectNoLoaderSinceLoad();
3222
+ expect(screen.getByTestId("update-content-modal")).toBeInTheDocument();
3223
+ expect(screen.getByTestId("model-delete")).toBeInTheDocument();
3224
+ });
3225
+ });
3226
+ });
3227
+ });