@oneuptime/common 14.0.7 → 14.0.9

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 (1064) hide show
  1. package/Models/DatabaseModels/Incident.ts +267 -1
  2. package/Models/DatabaseModels/IncidentCustomField.ts +202 -0
  3. package/Models/DatabaseModels/IncidentEpisode.ts +30 -0
  4. package/Models/DatabaseModels/IncidentTemplate.ts +126 -1
  5. package/Models/DatabaseModels/Index.ts +2 -0
  6. package/Models/DatabaseModels/KubernetesAiAgent.ts +364 -0
  7. package/Models/DatabaseModels/KubernetesCluster.ts +3 -3
  8. package/Models/DatabaseModels/Project.ts +55 -5
  9. package/Models/DatabaseModels/ProxmoxResource.ts +49 -1
  10. package/Models/DatabaseModels/RunnerJob.ts +75 -0
  11. package/Models/DatabaseModels/StatusPage.ts +55 -0
  12. package/Models/DatabaseModels/StatusPageSubscriber.ts +128 -0
  13. package/Models/DatabaseModels/TeamComplianceSetting.ts +143 -2
  14. package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +17 -0
  15. package/Server/API/AIChatAPI.ts +6 -3
  16. package/Server/API/AIInvestigationAPI.ts +17 -10
  17. package/Server/API/AutoRemediationAPI.ts +207 -25
  18. package/Server/API/IncidentAPI.ts +162 -0
  19. package/Server/API/KubernetesClusterAiAccessAPI.ts +623 -21
  20. package/Server/API/MicrosoftTeamsAPI.ts +93 -14
  21. package/Server/API/ProxmoxResourceAPI.ts +244 -14
  22. package/Server/API/StatusPageAPI.ts +487 -171
  23. package/Server/API/StatusPageSubscriberAPI.ts +63 -15
  24. package/Server/Infrastructure/GlobalCache.ts +32 -0
  25. package/Server/Infrastructure/Postgres/SchemaMigrations/1795300000000-SkipHiddenIncidentCreatedNotifications.ts +49 -0
  26. package/Server/Infrastructure/Postgres/SchemaMigrations/1795400000000-AddIncidentStatusPageScope.ts +108 -0
  27. package/Server/Infrastructure/Postgres/SchemaMigrations/1795500000000-AddIncidentTemplateStatusPageScopeFlag.ts +33 -0
  28. package/Server/Infrastructure/Postgres/SchemaMigrations/1795600000000-AddStatusPageSubscriberUnsubscribeToken.ts +51 -0
  29. package/Server/Infrastructure/Postgres/SchemaMigrations/1795700000000-AddStatusPageSubscriberIsAddedByTeam.ts +33 -0
  30. package/Server/Infrastructure/Postgres/SchemaMigrations/1795800000000-AddIncidentCustomFieldCreateAndNotificationSettings.ts +168 -0
  31. package/Server/Infrastructure/Postgres/SchemaMigrations/1795900000000-AddSubscriberNotificationClaimedAt.ts +44 -0
  32. package/Server/Infrastructure/Postgres/SchemaMigrations/1796000000000-AddTeamComplianceRuleScope.ts +99 -0
  33. package/Server/Infrastructure/Postgres/SchemaMigrations/1796100000000-AddKubernetesAiAgentAndAiDefaults.ts +137 -0
  34. package/Server/Infrastructure/Postgres/SchemaMigrations/1796200000000-AddProxmoxResourceNativePushColumns.ts +42 -0
  35. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +20 -0
  36. package/Server/Infrastructure/Semaphore.ts +10 -0
  37. package/Server/Middleware/IdentityRateLimit.ts +108 -3
  38. package/Server/Middleware/SubscriberNotificationPreviewRateLimit.ts +237 -0
  39. package/Server/Middleware/SubscriberNotificationTestSendRateLimit.ts +304 -0
  40. package/Server/Middleware/TelemetryIngest.ts +41 -18
  41. package/Server/Services/AIService.ts +70 -1
  42. package/Server/Services/AlertService.ts +352 -11
  43. package/Server/Services/AlertSeverityService.ts +65 -15
  44. package/Server/Services/AutoRemediationRuleService.ts +1 -1
  45. package/Server/Services/DatabaseService.ts +93 -10
  46. package/Server/Services/IncidentCustomFieldService.ts +276 -1
  47. package/Server/Services/IncidentEpisodePublicNoteService.ts +28 -0
  48. package/Server/Services/IncidentEpisodeService.ts +15 -0
  49. package/Server/Services/IncidentEpisodeStateTimelineService.ts +27 -1
  50. package/Server/Services/IncidentPublicNoteService.ts +30 -1
  51. package/Server/Services/IncidentService.ts +1788 -142
  52. package/Server/Services/IncidentSeverityService.ts +66 -15
  53. package/Server/Services/IncidentStateTimelineService.ts +13 -0
  54. package/Server/Services/IncidentTemplateService.ts +109 -0
  55. package/Server/Services/KubernetesAiAgentJobService.ts +185 -0
  56. package/Server/Services/KubernetesAiAgentService.ts +1634 -0
  57. package/Server/Services/KubernetesClusterAiAccessService.ts +1118 -451
  58. package/Server/Services/KubernetesClusterService.ts +129 -21
  59. package/Server/Services/ProjectService.ts +32 -0
  60. package/Server/Services/ProxmoxResourceService.ts +322 -5
  61. package/Server/Services/RunnerJobService.ts +241 -104
  62. package/Server/Services/RunnerService.ts +1 -1
  63. package/Server/Services/ScheduledMaintenancePublicNoteService.ts +30 -1
  64. package/Server/Services/ScheduledMaintenanceService.ts +805 -19
  65. package/Server/Services/ScheduledMaintenanceStateTimelineService.ts +414 -56
  66. package/Server/Services/SmsService.ts +10 -0
  67. package/Server/Services/StatusPageService.ts +65 -8
  68. package/Server/Services/StatusPageSubscriberNotificationTemplateService.ts +299 -37
  69. package/Server/Services/StatusPageSubscriberNotificationTemplateStatusPageService.ts +129 -0
  70. package/Server/Services/StatusPageSubscriberService.ts +1161 -64
  71. package/Server/Services/TeamComplianceSettingService.ts +1125 -9
  72. package/Server/Types/Markdown.ts +153 -6
  73. package/Server/Types/Workflow/Components/Email.ts +61 -2
  74. package/Server/Utils/AI/ClusterAccess/ClusterAccessContext.ts +67 -9
  75. package/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.ts +39 -10
  76. package/Server/Utils/AI/ClusterAccess/KubectlJobRunner.ts +14 -3
  77. package/Server/Utils/AI/Remediation/RemediationCommandTools.ts +37 -15
  78. package/Server/Utils/AI/Remediation/RemediationExecutionRunner.ts +67 -24
  79. package/Server/Utils/AI/SRE/AIInvestigationEngine.ts +48 -5
  80. package/Server/Utils/AI/SRE/IncidentPostmortemRunner.ts +40 -8
  81. package/Server/Utils/AI/SRE/InvestigationEligibility.ts +13 -2
  82. package/Server/Utils/AI/SRE/InvestigationGrader.ts +1 -1
  83. package/Server/Utils/AutoRemediation/CommandPlanExecutor.ts +6 -1
  84. package/Server/Utils/CustomField/CustomFieldRename.ts +378 -0
  85. package/Server/Utils/DataSource/EgressGuard.ts +66 -21
  86. package/Server/Utils/Mail/PinnedSmtpSocket.ts +130 -0
  87. package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +10 -0
  88. package/Server/Utils/SSRFProtection.ts +76 -16
  89. package/Server/Utils/StatusPage/IncidentStatusPageScope.ts +948 -0
  90. package/Server/Utils/StatusPage/IncidentSubscriberAudienceBuilder.ts +656 -0
  91. package/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.ts +805 -0
  92. package/Server/Utils/StatusPage/StatusPageExclusion.ts +18 -0
  93. package/Server/Utils/StatusPage/StatusPageReadAccess.ts +125 -0
  94. package/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.ts +151 -0
  95. package/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.ts +70 -0
  96. package/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.ts +421 -0
  97. package/Server/Utils/StatusPage/SubscriberNotificationClaim.ts +107 -0
  98. package/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.ts +1049 -0
  99. package/Server/Utils/StatusPage/SubscriberNotificationFanOut.ts +180 -0
  100. package/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.ts +839 -0
  101. package/Server/Utils/StatusPage/SubscriberNotificationResendAccess.ts +393 -0
  102. package/Server/Utils/StatusPage/SubscriberNotificationRunLimit.ts +246 -0
  103. package/Server/Utils/StatusPage/SubscriberNotificationTiming.ts +133 -0
  104. package/Server/Utils/StatusPage/SubscriberTemplateIncidentRecordAccess.ts +215 -0
  105. package/Server/Utils/StatusPageResource.ts +51 -20
  106. package/Server/Utils/Telemetry/InventoryEntityRegistry.ts +133 -7
  107. package/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.ts +58 -4
  108. package/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.ts +447 -0
  109. package/Server/Utils/Telemetry/ProxmoxNativePush.ts +903 -0
  110. package/Server/Utils/Telemetry/TelemetryEntity.ts +87 -27
  111. package/Server/Utils/VM/VMAPI.ts +1 -0
  112. package/Server/Utils/VM/VMRunner.ts +10 -0
  113. package/Server/Utils/Workflow/OAuth2TokenClient.ts +10 -0
  114. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +684 -47
  115. package/Server/Views/Partials/SensitiveUrlToken.ejs +8 -5
  116. package/Tests/App/Accounts/RegisterVerifyEmail.test.tsx +748 -0
  117. package/Tests/App/Accounts/ResendVerificationEmail.test.tsx +1222 -0
  118. package/Tests/App/Accounts/VerificationEmailResendUtils.test.ts +797 -0
  119. package/Tests/App/Accounts/VerifyEmailPending.test.tsx +538 -0
  120. package/Tests/App/Accounts/VerifyEmailResend.test.tsx +779 -0
  121. package/Tests/App/AdminDashboard/RowActionPlacementOverrides.test.tsx +278 -0
  122. package/Tests/App/Dashboard/AIChatChartWidgetZoom.test.tsx +738 -0
  123. package/Tests/App/Dashboard/AIFeaturesSettingsPointers.test.tsx +147 -0
  124. package/Tests/App/Dashboard/AIInsightsKubernetesClusterPage.test.tsx +1523 -0
  125. package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +463 -3
  126. package/Tests/App/Dashboard/AffectedResourcesPickerDropdownPlacement.test.tsx +958 -0
  127. package/Tests/App/Dashboard/AffectedResourcesPickerFocusLeave.test.tsx +595 -0
  128. package/Tests/App/Dashboard/AskAiAvailability.test.ts +20 -3
  129. package/Tests/App/Dashboard/AskAiDisabled.test.tsx +8 -3
  130. package/Tests/App/Dashboard/CephRateChartReload.test.tsx +1456 -0
  131. package/Tests/App/Dashboard/CephTimeRangeZoom.test.tsx +1229 -0
  132. package/Tests/App/Dashboard/ChartCardTimeRangeZoom.test.tsx +151 -0
  133. package/Tests/App/Dashboard/ChartCardZoomAffordances.test.tsx +257 -0
  134. package/Tests/App/Dashboard/ChartRefetchFrame.test.tsx +141 -0
  135. package/Tests/App/Dashboard/ChartZoomHarness.tsx +249 -0
  136. package/Tests/App/Dashboard/ChartZoomStandIn.tsx +131 -0
  137. package/Tests/App/Dashboard/CloudServerlessSlowLoads.test.tsx +628 -0
  138. package/Tests/App/Dashboard/CloudServerlessTimeRangeZoom.test.tsx +607 -0
  139. package/Tests/App/Dashboard/ClusterAccessNotice.test.tsx +91 -19
  140. package/Tests/App/Dashboard/CompanionMetricsTabZoom.test.tsx +632 -0
  141. package/Tests/App/Dashboard/ContainerChartZoomStandIn.tsx +195 -0
  142. package/Tests/App/Dashboard/ContainerHostOverviewSlowLoads.test.tsx +542 -0
  143. package/Tests/App/Dashboard/ContainerHostOverviewTimeRangeZoom.test.tsx +848 -0
  144. package/Tests/App/Dashboard/ContainerMetricsTimeRangeZoom.test.tsx +561 -0
  145. package/Tests/App/Dashboard/CustomFieldsSettingsIncidentOptions.test.tsx +357 -0
  146. package/Tests/App/Dashboard/DashboardBoardWideZoomShell.test.tsx +1147 -0
  147. package/Tests/App/Dashboard/DashboardEditPermissions.test.tsx +3 -2
  148. package/Tests/App/Dashboard/DashboardHistogramWidgetDragCursor.test.tsx +399 -0
  149. package/Tests/App/Dashboard/DashboardHistogramWidgetZoom.test.tsx +985 -0
  150. package/Tests/App/Dashboard/DashboardHistogramWidgetZoomHint.test.tsx +803 -0
  151. package/Tests/App/Dashboard/DashboardLazyPages.test.tsx +1 -0
  152. package/Tests/App/Dashboard/DashboardSettingsPreviewZoom.test.tsx +679 -0
  153. package/Tests/App/Dashboard/DashboardSloAndDataSourceChartZoom.test.tsx +881 -0
  154. package/Tests/App/Dashboard/DashboardTraceStatusWidgets.test.tsx +878 -0
  155. package/Tests/App/Dashboard/DatabaseMetricChartModalTimeRangeZoom.test.tsx +442 -0
  156. package/Tests/App/Dashboard/DatabaseOverviewTimeRangeZoom.test.tsx +831 -0
  157. package/Tests/App/Dashboard/DatabaseServerComponents.test.tsx +43 -0
  158. package/Tests/App/Dashboard/DockerSwarmInsightsTimeRangeZoom.test.tsx +524 -0
  159. package/Tests/App/Dashboard/DockerSwarmRateChartRefresh.test.tsx +259 -0
  160. package/Tests/App/Dashboard/EdgeDetailPanelHistory.test.tsx +763 -0
  161. package/Tests/App/Dashboard/EdgeDetailPanelTimeRangeZoom.test.tsx +547 -0
  162. package/Tests/App/Dashboard/EdgeHistoryBucketsCases.tsx +565 -0
  163. package/Tests/App/Dashboard/EdgeHistoryBucketsKathmandu.test.tsx +73 -0
  164. package/Tests/App/Dashboard/EdgeHistoryBucketsKolkata.test.tsx +74 -0
  165. package/Tests/App/Dashboard/EdgeHistoryBucketsNewYork.test.tsx +72 -0
  166. package/Tests/App/Dashboard/EdgeHistoryBucketsUtc.test.tsx +72 -0
  167. package/Tests/App/Dashboard/EmbeddedMetricCardRefresh.test.tsx +310 -0
  168. package/Tests/App/Dashboard/EmbeddedMetricCardTimeRangeZoom.test.tsx +536 -0
  169. package/Tests/App/Dashboard/EscalationRuleReadiness.test.tsx +3 -1
  170. package/Tests/App/Dashboard/EscalationRuleRowActions.test.tsx +660 -0
  171. package/Tests/App/Dashboard/EventNotes.test.tsx +778 -2
  172. package/Tests/App/Dashboard/EventNotesPages.test.tsx +119 -0
  173. package/Tests/App/Dashboard/EventNotesUtil.test.ts +116 -17
  174. package/Tests/App/Dashboard/EventOverviewPages.test.tsx +828 -17
  175. package/Tests/App/Dashboard/EventOverviewSnapshotExplorerZoom.test.tsx +1139 -0
  176. package/Tests/App/Dashboard/EventOverviewSnapshotZoomAcrossTabs.test.tsx +1057 -0
  177. package/Tests/App/Dashboard/EventOverviewTelemetrySnapshotZoom.test.tsx +757 -0
  178. package/Tests/App/Dashboard/ExceptionOccurrenceTrendZoom.test.tsx +675 -0
  179. package/Tests/App/Dashboard/ExceptionOccurrencesZoomSurvivesViewSwitch.test.tsx +405 -0
  180. package/Tests/App/Dashboard/ExceptionSignalPages.test.tsx +67 -3
  181. package/Tests/App/Dashboard/ExceptionTablesSpanStatus.test.tsx +515 -0
  182. package/Tests/App/Dashboard/ExplorerOwnZoomRegression.test.tsx +739 -0
  183. package/Tests/App/Dashboard/FlowBandwidthAxisCases.tsx +461 -0
  184. package/Tests/App/Dashboard/FlowBandwidthAxisKathmandu.test.tsx +25 -0
  185. package/Tests/App/Dashboard/FlowBandwidthAxisKolkata.test.tsx +25 -0
  186. package/Tests/App/Dashboard/FlowBandwidthAxisNewYork.test.tsx +25 -0
  187. package/Tests/App/Dashboard/FlowBandwidthAxisUtc.test.tsx +24 -0
  188. package/Tests/App/Dashboard/FlowTopTalkersZoomRefetch.test.tsx +722 -0
  189. package/Tests/App/Dashboard/FlowViewportGuard.test.tsx +2526 -0
  190. package/Tests/App/Dashboard/HostPagesSlowLoads.test.tsx +872 -0
  191. package/Tests/App/Dashboard/HostPagesTimeRangeZoom.test.tsx +1208 -0
  192. package/Tests/App/Dashboard/IncidentChangeStateNotifyDefault.test.tsx +91 -1
  193. package/Tests/App/Dashboard/IncidentCreateDetailsStep.test.tsx +643 -0
  194. package/Tests/App/Dashboard/IncidentCreateFromAlerts.test.tsx +8 -1
  195. package/Tests/App/Dashboard/IncidentCreateStatusPageScope.test.tsx +601 -0
  196. package/Tests/App/Dashboard/IncidentCustomFieldTemplateVariables.test.tsx +386 -0
  197. package/Tests/App/Dashboard/IncidentNoteTemplatePlaceholders.test.tsx +124 -0
  198. package/Tests/App/Dashboard/IncidentNoteTemplateVariablesLoader.test.ts +305 -0
  199. package/Tests/App/Dashboard/IncidentOverviewNotifyDefault.test.tsx +112 -0
  200. package/Tests/App/Dashboard/IncidentRootCauseTimeRangeZoom.test.tsx +358 -0
  201. package/Tests/App/Dashboard/IncidentSettingsRenotifyOnPublish.test.tsx +674 -0
  202. package/Tests/App/Dashboard/IncidentSettingsStatusPageScope.test.tsx +1051 -0
  203. package/Tests/App/Dashboard/IncidentTemplateCustomFields.test.tsx +384 -0
  204. package/Tests/App/Dashboard/IncidentTemplateStatusPageScopeCard.test.tsx +215 -0
  205. package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +100 -9
  206. package/Tests/App/Dashboard/InfrastructureMetricsTabTimeRangeZoom.test.tsx +485 -0
  207. package/Tests/App/Dashboard/InvestigationDrawerCompanionZoom.test.tsx +655 -0
  208. package/Tests/App/Dashboard/InvestigationDrawerZoom.test.tsx +830 -0
  209. package/Tests/App/Dashboard/InvestigationNotStarted.test.tsx +338 -2
  210. package/Tests/App/Dashboard/InvoicesPayInvoice.test.tsx +454 -16
  211. package/Tests/App/Dashboard/KubernetesClusterAiAccessSettings.test.ts +799 -819
  212. package/Tests/App/Dashboard/KubernetesClusterAiAgentOverviewCard.test.tsx +367 -0
  213. package/Tests/App/Dashboard/KubernetesClusterAiAgentStatus.test.ts +1202 -0
  214. package/Tests/App/Dashboard/KubernetesClusterAiDocumentationMarkdown.test.ts +106 -0
  215. package/Tests/App/Dashboard/KubernetesClusterAiNavigation.test.tsx +354 -0
  216. package/Tests/App/Dashboard/KubernetesClusterAiPage.test.tsx +1884 -2054
  217. package/Tests/App/Dashboard/KubernetesClusterAiPageServerParity.test.ts +478 -179
  218. package/Tests/App/Dashboard/KubernetesClusterMetricTooltips.test.ts +50 -0
  219. package/Tests/App/Dashboard/KubernetesClusterOverviewTooltips.test.tsx +2 -1
  220. package/Tests/App/Dashboard/KubernetesClusterOverviewZoom.test.tsx +1217 -0
  221. package/Tests/App/Dashboard/KubernetesClusterPagesZoom.test.tsx +731 -0
  222. package/Tests/App/Dashboard/KubernetesCostsLoadRecovery.test.tsx +869 -0
  223. package/Tests/App/Dashboard/KubernetesCostsZoom.test.tsx +1214 -0
  224. package/Tests/App/Dashboard/KubernetesCostsZoomExactHours.test.tsx +692 -0
  225. package/Tests/App/Dashboard/KubernetesNetworkThroughputReload.test.tsx +902 -0
  226. package/Tests/App/Dashboard/KubernetesNetworkZoomBucketEdges.test.tsx +571 -0
  227. package/Tests/App/Dashboard/KubernetesOverviewZoomBucketEdges.test.tsx +697 -0
  228. package/Tests/App/Dashboard/KubernetesResourceMetricsTabZoom.test.tsx +633 -0
  229. package/Tests/App/Dashboard/LogsExplorerTimeRangeZoom.test.tsx +470 -0
  230. package/Tests/App/Dashboard/LogsInsightsTimeRangeZoom.test.tsx +948 -0
  231. package/Tests/App/Dashboard/LogsViewerPinnedWindowZoom.test.tsx +486 -0
  232. package/Tests/App/Dashboard/MetricChartsBucketInspectorDoubleClick.test.tsx +1001 -0
  233. package/Tests/App/Dashboard/MetricExplorerInvestigationAutoRefresh.test.tsx +514 -0
  234. package/Tests/App/Dashboard/MetricExplorerSavedViewZoomRoundTrip.test.tsx +581 -0
  235. package/Tests/App/Dashboard/MetricExplorerTimeRangeZoom.test.tsx +1162 -0
  236. package/Tests/App/Dashboard/MetricMonitorPreviewZoomLabel.test.ts +72 -0
  237. package/Tests/App/Dashboard/MetricRowSparklineTimeRangeZoom.test.tsx +204 -0
  238. package/Tests/App/Dashboard/MetricViewFirstResultLands.test.tsx +408 -0
  239. package/Tests/App/Dashboard/MetricViewOwnZoomResetPlacement.test.tsx +952 -0
  240. package/Tests/App/Dashboard/MetricViewTimeRange.test.ts +139 -0
  241. package/Tests/App/Dashboard/MetricViewTimeRangeZoom.test.tsx +556 -0
  242. package/Tests/App/Dashboard/MonitorMetricsChartWrappersZoom.test.tsx +552 -0
  243. package/Tests/App/Dashboard/MonitorMetricsTabsTimeRangeZoom.test.tsx +740 -0
  244. package/Tests/App/Dashboard/MonitorOverviewChartsTimeRangeZoom.test.tsx +735 -0
  245. package/Tests/App/Dashboard/MonitorStepPreviewsTimeRangeZoom.test.tsx +835 -0
  246. package/Tests/App/Dashboard/NetworkDeviceTimeRangeZoom.test.tsx +1115 -0
  247. package/Tests/App/Dashboard/OnCallPreventionGuards.test.tsx +186 -17
  248. package/Tests/App/Dashboard/OnCallRulesDeleteGuard.test.tsx +155 -46
  249. package/Tests/App/Dashboard/PasskeySettings.test.tsx +87 -2
  250. package/Tests/App/Dashboard/ProjectAISettingsPages.test.tsx +884 -0
  251. package/Tests/App/Dashboard/ProxmoxNodeRemove.test.tsx +621 -0
  252. package/Tests/App/Dashboard/ProxmoxOverviewSlowLoads.test.tsx +584 -0
  253. package/Tests/App/Dashboard/ProxmoxPagesTimeRangeZoom.test.tsx +823 -0
  254. package/Tests/App/Dashboard/ProxmoxRateChartReload.test.tsx +1253 -0
  255. package/Tests/App/Dashboard/PublicDashboardBoardWideZoom.test.tsx +605 -0
  256. package/Tests/App/Dashboard/PublicNotePagesNotifyDefault.test.tsx +113 -1
  257. package/Tests/App/Dashboard/RealRowsLineChart.tsx +250 -0
  258. package/Tests/App/Dashboard/RechartsHostsPlotCursor.test.tsx +513 -0
  259. package/Tests/App/Dashboard/RowActionPlacementOverrides.test.tsx +865 -0
  260. package/Tests/App/Dashboard/RumOverviewTimeRangeZoom.test.tsx +877 -0
  261. package/Tests/App/Dashboard/RunnerKubernetesAgentForm.test.tsx +20 -4
  262. package/Tests/App/Dashboard/RunnerSupersededByAiAgentNote.test.tsx +296 -0
  263. package/Tests/App/Dashboard/SecurityEventConnectionsTable.test.tsx +146 -36
  264. package/Tests/App/Dashboard/ServiceDatabaseSlowLoads.test.tsx +1166 -0
  265. package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +1243 -104
  266. package/Tests/App/Dashboard/ServiceOverviewTimeRangeZoom.test.tsx +792 -0
  267. package/Tests/App/Dashboard/SloBudgetBurnDownZoom.test.tsx +771 -0
  268. package/Tests/App/Dashboard/SloBudgetBurnDownZoomAffordances.test.tsx +380 -0
  269. package/Tests/App/Dashboard/SloEventMetricsBarChartZoom.test.tsx +369 -0
  270. package/Tests/App/Dashboard/SloHistoryChartsTimeRangeZoom.test.tsx +694 -0
  271. package/Tests/App/Dashboard/SloHistoryChartsZoomAffordances.test.tsx +497 -0
  272. package/Tests/App/Dashboard/SloMetricsTimeRangeZoom.test.tsx +541 -0
  273. package/Tests/App/Dashboard/SlowLoadHarness.ts +123 -0
  274. package/Tests/App/Dashboard/SnapshotExplorerPrimaryZoom.test.tsx +1194 -0
  275. package/Tests/App/Dashboard/SnapshotViewerPinnedWindow.test.tsx +620 -0
  276. package/Tests/App/Dashboard/SubscriberAudienceSummary.test.tsx +512 -0
  277. package/Tests/App/Dashboard/SubscriberNotificationPreviewModal.test.tsx +837 -0
  278. package/Tests/App/Dashboard/SubscriberNotificationStatusLabel.test.tsx +88 -0
  279. package/Tests/App/Dashboard/SubscriberNotificationStatusResend.test.tsx +473 -0
  280. package/Tests/App/Dashboard/SubscriberTemplateLivePreview.test.tsx +196 -0
  281. package/Tests/App/Dashboard/TeamAddedSubscribersUnsubscribedNotice.test.tsx +248 -0
  282. package/Tests/App/Dashboard/TelemetrySnapshotPanelTimeRangeZoom.test.tsx +765 -0
  283. package/Tests/App/Dashboard/TelemetrySnapshotZoom.test.tsx +516 -0
  284. package/Tests/App/Dashboard/TelemetrySnapshotZoomAcrossTabs.test.tsx +827 -0
  285. package/Tests/App/Dashboard/TimeRangeZoomPageHarness.tsx +463 -0
  286. package/Tests/App/Dashboard/TraceChartEditorStatusSplit.test.tsx +543 -0
  287. package/Tests/App/Dashboard/TraceStatusPresentation.test.tsx +967 -0
  288. package/Tests/App/Dashboard/TraceTimelineMinimapZoom.test.tsx +315 -0
  289. package/Tests/App/Dashboard/TracesAnalyticsStatusSplit.test.tsx +778 -0
  290. package/Tests/App/Dashboard/TracesAnalyticsViewZoom.test.tsx +349 -0
  291. package/Tests/App/Dashboard/TracesExplorerTimeRangeZoom.test.tsx +551 -0
  292. package/Tests/App/Dashboard/TracesStatusChartAndFacet.test.tsx +687 -0
  293. package/Tests/App/Dashboard/TracesViewerStaleResponses.test.tsx +458 -0
  294. package/Tests/App/Dashboard/UptimeStripsExcludedFromZoom.test.tsx +406 -0
  295. package/Tests/App/Dashboard/VMwareOverviewSlowLoads.test.tsx +535 -0
  296. package/Tests/App/Dashboard/VMwarePagesTimeRangeZoom.test.tsx +606 -0
  297. package/Tests/App/Dashboard/ValueWidgetSparklineStaleHover.test.tsx +1292 -0
  298. package/Tests/App/Dashboard/ValueWidgetSparklineZoom.test.tsx +765 -0
  299. package/Tests/App/SensitiveUrlTokenBootstrap.test.ts +64 -3
  300. package/Tests/App/StatusPage/StatusPageResourceExplorer.test.tsx +98 -0
  301. package/Tests/App/StatusPage/StatusPageUnsubscribePage.test.tsx +412 -0
  302. package/Tests/Models/DatabaseModels/KubernetesAiAgentModel.test.ts +295 -0
  303. package/Tests/Models/DatabaseModels/TeamComplianceSettingModel.test.ts +347 -0
  304. package/Tests/Models/IncidentCustomFieldApiSchemaContract.test.ts +217 -0
  305. package/Tests/Models/IncidentCustomFieldCreateSettingsColumns.test.ts +252 -0
  306. package/Tests/Models/StatusPageSubscriberUnsubscribeColumns.test.ts +218 -0
  307. package/Tests/Server/API/AIChatRouteAuthorization.test.ts +58 -0
  308. package/Tests/Server/API/AIGenerationProjectAIToggle.test.ts +1 -1
  309. package/Tests/Server/API/AIInvestigationAPI.test.ts +123 -45
  310. package/Tests/Server/API/AutoRemediationApproveClusterRound.test.ts +1329 -0
  311. package/Tests/Server/API/AutoRemediationApproveKubectlRecheck.test.ts +25 -2
  312. package/Tests/Server/API/ExpiredSessionRouteGuards.test.ts +7 -0
  313. package/Tests/Server/API/IncidentSubscriberAudienceAPI.test.ts +1408 -0
  314. package/Tests/Server/API/KubernetesClusterAiAccessAPI.test.ts +2 -0
  315. package/Tests/Server/API/KubernetesClusterAiAccessAPIAgent.test.ts +903 -0
  316. package/Tests/Server/API/MicrosoftTeamsChatsRefreshRoute.test.ts +667 -0
  317. package/Tests/Server/API/MicrosoftTeamsManifest.test.ts +294 -6
  318. package/Tests/Server/API/ProxmoxResourceAPIRemoveNode.test.ts +1932 -0
  319. package/Tests/Server/API/StatusPageIncidentScope.test.ts +1868 -0
  320. package/Tests/Server/API/StatusPageIncidentScopePostgres.test.ts +790 -0
  321. package/Tests/Server/API/StatusPageManageSubscriptionRecipient.test.ts +10 -1
  322. package/Tests/Server/API/StatusPageManageSubscriptionTemplateVariables.test.ts +206 -18
  323. package/Tests/Server/API/StatusPagePostmortemVisibility.test.ts +14 -2
  324. package/Tests/Server/API/StatusPageSubscriberCreateResponse.test.ts +217 -0
  325. package/Tests/Server/API/StatusPageSubscriberOwnership.test.ts +103 -2
  326. package/Tests/Server/API/StatusPageUnsubscribeAPI.test.ts +396 -0
  327. package/Tests/Server/Infrastructure/GlobalCache.test.ts +195 -0
  328. package/Tests/Server/Infrastructure/Postgres/AddIncidentStatusPageScopeMigration.test.ts +395 -0
  329. package/Tests/Server/Infrastructure/Postgres/AddIncidentTemplateStatusPageScopeFlagMigration.test.ts +150 -0
  330. package/Tests/Server/Infrastructure/Postgres/AddKubernetesAiAgentAndAiDefaultsMigration.test.ts +623 -0
  331. package/Tests/Server/Infrastructure/Postgres/AddKubernetesClusterAiAccessMigration.test.ts +87 -10
  332. package/Tests/Server/Infrastructure/Postgres/AddSubscriberNotificationClaimedAtMigration.test.ts +156 -0
  333. package/Tests/Server/Infrastructure/Postgres/AddTeamComplianceRuleScopeMigration.test.ts +622 -0
  334. package/Tests/Server/Infrastructure/Postgres/SkipHiddenIncidentCreatedNotificationsMigration.test.ts +410 -0
  335. package/Tests/Server/Middleware/IdentityRateLimit.test.ts +551 -1
  336. package/Tests/Server/Middleware/SubscriberNotificationPreviewRateLimit.test.ts +238 -0
  337. package/Tests/Server/Middleware/SubscriberNotificationTestSendRateLimit.test.ts +245 -0
  338. package/Tests/Server/Middleware/TelemetryIngestBrowserKey.test.ts +61 -4
  339. package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerPinnedKey.test.ts +98 -2
  340. package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerRateLimit.test.ts +105 -2
  341. package/Tests/Server/Services/AIServiceBalanceBlocker.test.ts +282 -0
  342. package/Tests/Server/Services/AddStatusPageSubscriberUnsubscribeTokenMigration.test.ts +152 -0
  343. package/Tests/Server/Services/AlertMonitorClearWritesNull.test.ts +409 -0
  344. package/Tests/Server/Services/AlertMonitorEditFeed.test.ts +474 -0
  345. package/Tests/Server/Services/AlertMonitorEditGuard.test.ts +610 -0
  346. package/Tests/Server/Services/AutoRemediationClusterRuleEngine.test.ts +2 -0
  347. package/Tests/Server/Services/AutoRemediationRuleCommandRunners.test.ts +2 -1
  348. package/Tests/Server/Services/CrossProjectRelationWriteGuard.test.ts +221 -36
  349. package/Tests/Server/Services/DatabaseServiceCompareAndSetColumns.test.ts +191 -0
  350. package/Tests/Server/Services/IncidentAffectedMonitorsEdit.test.ts +1360 -0
  351. package/Tests/Server/Services/IncidentAlertSloLinkSurvivesEdit.test.ts +3 -1
  352. package/Tests/Server/Services/IncidentCreatedRenotifyOnPublish.test.ts +681 -0
  353. package/Tests/Server/Services/IncidentCreatedResend.test.ts +775 -0
  354. package/Tests/Server/Services/IncidentCustomFieldRenamePostgres.test.ts +713 -0
  355. package/Tests/Server/Services/IncidentCustomFieldService.test.ts +751 -0
  356. package/Tests/Server/Services/IncidentCustomFieldValueValidation.test.ts +556 -0
  357. package/Tests/Server/Services/IncidentSeverityDeletePausesComplianceRules.test.ts +488 -0
  358. package/Tests/Server/Services/IncidentStatusPageScope.test.ts +2971 -0
  359. package/Tests/Server/Services/IncidentStatusPageScopeFeed.test.ts +131 -0
  360. package/Tests/Server/Services/IncidentStatusPageScopePostgres.test.ts +625 -0
  361. package/Tests/Server/Services/IncidentTemplateCustomFieldsCopy.test.ts +311 -0
  362. package/Tests/Server/Services/IncidentTemplateStatusPageScope.test.ts +363 -0
  363. package/Tests/Server/Services/KubernetesAiAgentJobService.test.ts +329 -0
  364. package/Tests/Server/Services/KubernetesAiAgentServiceHelpers.test.ts +618 -0
  365. package/Tests/Server/Services/KubernetesAiAgentServiceLifecycle.test.ts +1444 -0
  366. package/Tests/Server/Services/KubernetesAiAgentServiceRegister.test.ts +1666 -0
  367. package/Tests/Server/Services/KubernetesClusterAiAccessDeletedRunnerRemedy.test.ts +140 -111
  368. package/Tests/Server/Services/KubernetesClusterAiAccessGapNextSteps.test.ts +267 -105
  369. package/Tests/Server/Services/KubernetesClusterAiAccessLegacySuperseded.test.ts +440 -0
  370. package/Tests/Server/Services/KubernetesClusterAiAccessService.test.ts +567 -411
  371. package/Tests/Server/Services/KubernetesClusterAiAccessTarget.test.ts +1168 -0
  372. package/Tests/Server/Services/KubernetesClusterAiSettingsPermission.test.ts +410 -12
  373. package/Tests/Server/Services/KubernetesClusterServiceAiAccessFeed.test.ts +55 -0
  374. package/Tests/Server/Services/ProjectServiceNewProjectAiDefaults.test.ts +288 -0
  375. package/Tests/Server/Services/ProxmoxResourcePostgres.test.ts +5633 -0
  376. package/Tests/Server/Services/ProxmoxResourceService.test.ts +6458 -5
  377. package/Tests/Server/Services/PublicNoteEditFeedEventType.test.ts +213 -0
  378. package/Tests/Server/Services/PublicNoteResendAccess.test.ts +994 -0
  379. package/Tests/Server/Services/RunnerJobEnqueueKubectl.test.ts +48 -15
  380. package/Tests/Server/Services/RunnerJobEnqueueKubectlAiAgent.test.ts +620 -0
  381. package/Tests/Server/Services/RunnerJobKubectlWriteScope.test.ts +170 -3
  382. package/Tests/Server/Services/RunnerJobTimeoutReason.test.ts +72 -0
  383. package/Tests/Server/Services/RunnerServiceAgentName.test.ts +6 -4
  384. package/Tests/Server/Services/ScheduledMaintenanceAttachmentEdit.test.ts +2113 -0
  385. package/Tests/Server/Services/ScheduledMaintenanceMonitorRelease.test.ts +972 -0
  386. package/Tests/Server/Services/ScheduledMaintenanceSubscriberTemplateFormats.test.ts +289 -9
  387. package/Tests/Server/Services/SmsServiceFailIfNotSent.test.ts +67 -0
  388. package/Tests/Server/Services/StatusPageReportIncidentCounts.test.ts +610 -0
  389. package/Tests/Server/Services/StatusPageSubscriberAudienceCounts.test.ts +201 -0
  390. package/Tests/Server/Services/StatusPageSubscriberAudienceCountsPostgres.test.ts +271 -0
  391. package/Tests/Server/Services/StatusPageSubscriberConfirmationTemplateEscaping.test.ts +200 -0
  392. package/Tests/Server/Services/StatusPageSubscriberNotificationLogRedaction.test.ts +54 -0
  393. package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateCompile.test.ts +37 -0
  394. package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateIncidentRecordAccess.test.ts +772 -0
  395. package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateLookup.test.ts +506 -0
  396. package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateLookupPostgres.test.ts +261 -0
  397. package/Tests/Server/Services/StatusPageSubscriberReport.test.ts +21 -5
  398. package/Tests/Server/Services/StatusPageSubscriberUnsubscribe.test.ts +1618 -0
  399. package/Tests/Server/Services/StatusPageSubscriberUnsubscribeLinkSenders.test.ts +373 -0
  400. package/Tests/Server/Services/StatusPageSubscriberUnsubscribePostgres.test.ts +388 -0
  401. package/Tests/Server/Services/SubscriberNotificationInFlightRequeue.test.ts +500 -0
  402. package/Tests/Server/Services/TeamComplianceSettingPostgres.test.ts +2237 -0
  403. package/Tests/Server/Services/TeamComplianceSettingService.test.ts +3988 -138
  404. package/Tests/Server/Types/MarkdownEmailLinks.test.ts +320 -0
  405. package/Tests/Server/Types/Workflow/Components/Email.test.ts +491 -1
  406. package/Tests/Server/Types/Workflow/Components/EmailSmtpPinning.test.ts +398 -0
  407. package/Tests/Server/Utils/AI/ClusterAccessContext.test.ts +107 -1
  408. package/Tests/Server/Utils/AI/IncidentPostmortemRunner.test.ts +351 -0
  409. package/Tests/Server/Utils/AI/InvestigationClusterAccessWiring.test.ts +2 -0
  410. package/Tests/Server/Utils/AI/InvestigationEligibility.test.ts +310 -0
  411. package/Tests/Server/Utils/AI/KubectlInvestigationToolkit.test.ts +70 -0
  412. package/Tests/Server/Utils/AI/KubectlJobRunnerFinishedJob.test.ts +58 -0
  413. package/Tests/Server/Utils/AI/RemediationCommandToolsKubectl.test.ts +2 -0
  414. package/Tests/Server/Utils/AI/RemediationCommandToolsKubectlOutcome.test.ts +2 -0
  415. package/Tests/Server/Utils/AI/RemediationCommandToolsLiveCluster.test.ts +2 -0
  416. package/Tests/Server/Utils/AI/RemediationCommandToolsRunnerScope.test.ts +125 -1
  417. package/Tests/Server/Utils/AI/RemediationExecutionRunner.test.ts +252 -3
  418. package/Tests/Server/Utils/AI/RemediationExecutionRunnerClusterMode.test.ts +2 -0
  419. package/Tests/Server/Utils/AI/RemediationExecutionRunnerProjectGates.test.ts +240 -0
  420. package/Tests/Server/Utils/AI/RemediationKubectlCopyParity.test.ts +2 -0
  421. package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutor.test.ts +2 -0
  422. package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutorUnknownRun.test.ts +2 -0
  423. package/Tests/Server/Utils/AutoRemediation/CommandPlanVerification.test.ts +2 -0
  424. package/Tests/Server/Utils/CustomField/CustomFieldRename.test.ts +586 -0
  425. package/Tests/Server/Utils/DataSource/EgressGuard.test.ts +246 -10
  426. package/Tests/Server/Utils/DataSource/EgressGuardDeploymentConfig.test.ts +226 -0
  427. package/Tests/Server/Utils/DataSource/EgressGuardPinning.test.ts +131 -0
  428. package/Tests/Server/Utils/DataSource/HttpFetch.test.ts +4 -0
  429. package/Tests/Server/Utils/EgressPolicyEnvironment.ts +45 -0
  430. package/Tests/Server/Utils/Mail/PinnedSmtpSocket.test.ts +409 -0
  431. package/Tests/Server/Utils/Monitor/MonitorCriteriaObservationBuilderUnits.test.ts +23 -0
  432. package/Tests/Server/Utils/SSRFProtection.test.ts +4 -0
  433. package/Tests/Server/Utils/SSRFProtectionCloudServiceAddresses.test.ts +4 -0
  434. package/Tests/Server/Utils/SSRFProtectionPrivateNetwork.test.ts +4 -0
  435. package/Tests/Server/Utils/SSRFProtectionResolutionDetail.test.ts +314 -0
  436. package/Tests/Server/Utils/StatusPage/IncidentStatusPageScope.test.ts +1699 -0
  437. package/Tests/Server/Utils/StatusPage/IncidentStatusPageScopeQueriesPostgres.test.ts +508 -0
  438. package/Tests/Server/Utils/StatusPage/IncidentSubscriberAudienceCallerPermission.test.ts +246 -0
  439. package/Tests/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.test.ts +1348 -0
  440. package/Tests/Server/Utils/StatusPage/StatusPageReadAccess.test.ts +221 -0
  441. package/Tests/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.test.ts +132 -0
  442. package/Tests/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.test.ts +155 -0
  443. package/Tests/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.test.ts +708 -0
  444. package/Tests/Server/Utils/StatusPage/SubscriberNotificationClaim.test.ts +255 -0
  445. package/Tests/Server/Utils/StatusPage/SubscriberNotificationClaimPostgres.test.ts +528 -0
  446. package/Tests/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.test.ts +1238 -0
  447. package/Tests/Server/Utils/StatusPage/SubscriberNotificationFanOut.test.ts +472 -0
  448. package/Tests/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.test.ts +1168 -0
  449. package/Tests/Server/Utils/StatusPage/SubscriberNotificationRunLimit.test.ts +275 -0
  450. package/Tests/Server/Utils/StatusPage/SubscriberNotificationTiming.test.ts +131 -0
  451. package/Tests/Server/Utils/StatusPageResource.test.ts +98 -0
  452. package/Tests/Server/Utils/Telemetry/HostAssetAttributesFromOtlp.test.ts +68 -0
  453. package/Tests/Server/Utils/Telemetry/InventoryMirrorPostgres.test.ts +370 -0
  454. package/Tests/Server/Utils/Telemetry/NetworkDeviceInventoryAttributes.test.ts +522 -0
  455. package/Tests/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.test.ts +155 -0
  456. package/Tests/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.test.ts +8631 -0
  457. package/Tests/Server/Utils/Telemetry/ProxmoxNativePush.test.ts +2852 -0
  458. package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +367 -1
  459. package/Tests/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.test.ts +45 -3
  460. package/Tests/Server/Utils/VM/VMRunnerPrivateNetworkWiring.test.ts +17 -0
  461. package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +75 -0
  462. package/Tests/Server/Utils/Workspace/MicrosoftTeamsChats.test.ts +25 -0
  463. package/Tests/Server/Utils/Workspace/MicrosoftTeamsGroupChatNames.test.ts +3119 -0
  464. package/Tests/Server/Utils/Workspace/MicrosoftTeamsGroupChatNamesPostgres.test.ts +1721 -0
  465. package/Tests/Types/CustomField/CustomFieldSavedViews.test.ts +247 -0
  466. package/Tests/Types/CustomField/CustomFieldTemplateValues.test.ts +186 -0
  467. package/Tests/Types/CustomField/CustomFieldTypeCorrespondence.test.ts +48 -0
  468. package/Tests/Types/CustomField/CustomFieldValueFormat.test.ts +215 -0
  469. package/Tests/Types/CustomField/CustomFieldValueValidator.test.ts +343 -0
  470. package/Tests/Types/CustomField/CustomFieldVariableKey.test.ts +364 -0
  471. package/Tests/Types/Kubernetes/KubernetesAgentChartAiAccess.test.ts +581 -113
  472. package/Tests/Types/Kubernetes/KubernetesAiAgentAccessContract.test.ts +510 -0
  473. package/Tests/Types/SafeHtml.test.ts +102 -0
  474. package/Tests/Types/StatusPage/IncidentCreatedRenotify.test.ts +419 -0
  475. package/Tests/Types/StatusPage/IncidentCreatedResend.test.ts +118 -0
  476. package/Tests/Types/StatusPage/IncidentScopeAddedPagesNotification.test.ts +558 -0
  477. package/Tests/Types/StatusPage/IncidentSubscriberAudience.test.ts +284 -0
  478. package/Tests/Types/StatusPage/StatusPageSubscriberUnsubscribe.test.ts +341 -0
  479. package/Tests/Types/StatusPage/SubscriberNotificationPreview.test.ts +196 -0
  480. package/Tests/Types/StatusPage/SubscriberNotificationResend.test.ts +201 -0
  481. package/Tests/Types/StatusPage/SubscriberNotificationTemplateCompiler.test.ts +277 -0
  482. package/Tests/Types/StatusPage/SubscriberNotificationTemplatePreview.test.ts +179 -0
  483. package/Tests/Types/StatusPage/SubscriberNotificationTemplateVariables.test.ts +324 -0
  484. package/Tests/Types/StatusPage/SubscriberUpdateNotification.test.ts +60 -13
  485. package/Tests/Types/Team/ComplianceRule.test.ts +1029 -0
  486. package/Tests/Types/Team/TeamComplianceStatus.test.ts +176 -0
  487. package/Tests/UI/Components/ActionButton/RowActions.test.tsx +641 -0
  488. package/Tests/UI/Components/ActionButton/RowActionsAccessibility.test.tsx +414 -0
  489. package/Tests/UI/Components/ActionButton/SplitActionButtons.test.ts +565 -0
  490. package/Tests/UI/Components/Charts/BarChartDragToSelect.test.tsx +386 -0
  491. package/Tests/UI/Components/Charts/ChartDoubleClickResetWithoutDblclick.test.tsx +1025 -0
  492. package/Tests/UI/Components/Charts/ChartGroupPageZoomHint.test.tsx +196 -0
  493. package/Tests/UI/Components/Charts/ChartPlaceholdersTakeDoubleClick.test.tsx +1861 -0
  494. package/Tests/UI/Components/Charts/ChartPlotClicksWhileResetArmed.test.tsx +382 -0
  495. package/Tests/UI/Components/Charts/ChartRangeSelection.test.tsx +704 -0
  496. package/Tests/UI/Components/Charts/ChartRangeSelectionDoubleClickReset.test.tsx +1481 -0
  497. package/Tests/UI/Components/Charts/ChartRangeSelectionRealRecharts.test.tsx +918 -0
  498. package/Tests/UI/Components/Charts/ChartRowBucketStart.test.ts +350 -0
  499. package/Tests/UI/Components/Charts/DoubleClickReset.test.tsx +996 -0
  500. package/Tests/UI/Components/Charts/HistogramDoubleClickResetRealRecharts.test.tsx +965 -0
  501. package/Tests/UI/Components/Charts/HistogramDriftThenRefetch.test.tsx +277 -0
  502. package/Tests/UI/Components/Charts/HistogramRangeSelectionDataChange.test.tsx +1757 -0
  503. package/Tests/UI/Components/Charts/HistogramRangeSelectionRealRecharts.test.tsx +560 -0
  504. package/Tests/UI/Components/Charts/TimeRangeZoom/ChartPageTimeRangeZoom.test.tsx +513 -0
  505. package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.test.tsx +845 -0
  506. package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.test.tsx +119 -0
  507. package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHintTailwind.test.tsx +274 -0
  508. package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.test.ts +433 -0
  509. package/Tests/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.test.tsx +863 -0
  510. package/Tests/UI/Components/Charts/TimeRangeZoom/ZoomDocsPromises.test.tsx +1147 -0
  511. package/Tests/UI/Components/CustomFields/CustomFieldFormFields.test.ts +432 -0
  512. package/Tests/UI/Components/CustomFields/CustomFieldModelFormFields.test.tsx +685 -0
  513. package/Tests/UI/Components/CustomFields/CustomFieldsDetailOrder.test.tsx +469 -0
  514. package/Tests/UI/Components/DisabledButtonTooltip.test.tsx +14 -3
  515. package/Tests/UI/Components/EntityDropdownEscape.test.tsx +454 -0
  516. package/Tests/UI/Components/EntityDropdownFocusLeave.test.tsx +420 -0
  517. package/Tests/UI/Components/Forms/FormFieldDropdownDisabled.test.tsx +233 -0
  518. package/Tests/UI/Components/Forms/ModelFormCustomFieldInputs.test.tsx +325 -0
  519. package/Tests/UI/Components/Forms/ModelFormDropdownOptionsRefused.test.tsx +358 -0
  520. package/Tests/UI/Components/Input.test.tsx +80 -1
  521. package/Tests/UI/Components/LogsAnalyticsViewZoom.test.tsx +947 -0
  522. package/Tests/UI/Components/LogsHistogramDragTooltip.test.tsx +17 -5
  523. package/Tests/UI/Components/LogsViewerAnalyticsZoom.test.tsx +501 -0
  524. package/Tests/UI/Components/LogsViewerHistogramZoom.test.tsx +1 -1
  525. package/Tests/UI/Components/ModelTable/BaseModelTableBulkDelete.test.tsx +6 -4
  526. package/Tests/UI/Components/ModelTable/BaseModelTablePermissionGating.test.tsx +60 -8
  527. package/Tests/UI/Components/ModelTable/BaseModelTableRowActions.test.tsx +476 -0
  528. package/Tests/UI/Components/ModelTable/CustomFieldColumns.test.tsx +22 -0
  529. package/Tests/UI/Components/ModelTable/ModelTableWrapContent.test.tsx +7 -5
  530. package/Tests/UI/Components/MoreMenuPortal.test.tsx +623 -0
  531. package/Tests/UI/Components/StatusPage/ResourceGridActions.test.tsx +538 -0
  532. package/Tests/UI/Components/StatusPage/ResourceGridSelection.test.tsx +14 -4
  533. package/Tests/UI/Components/TableCellWrapping.test.tsx +3 -3
  534. package/Tests/UI/Components/TableRowActions.test.tsx +472 -0
  535. package/Tests/UI/Components/TelemetryHistogramDragTooltip.test.tsx +1 -1
  536. package/Tests/UI/Components/TelemetryViewer/TelemetryViewerHistogramClickZoom.test.tsx +1 -1
  537. package/Tests/UI/Components/TelemetryViewer/TelemetryViewerZoomScope.test.tsx +470 -0
  538. package/Tests/UI/Components/TelemetryViewer/UseViewerTimeRangeZoom.test.tsx +571 -0
  539. package/Tests/UI/Components/TelemetryViewer/ViewerDoubleClickResetUnderDataChange.test.tsx +928 -0
  540. package/Tests/UI/Components/TelemetryViewer/ViewerFollowsEnclosingZoom.test.tsx +1569 -0
  541. package/Tests/UI/Components/Toggle.test.tsx +66 -0
  542. package/Tests/UI/MicrosoftTeams/MicrosoftTeamsChatsCardRefreshNames.test.tsx +1023 -0
  543. package/Tests/UI/Monitor/NetworkPathView.test.tsx +289 -0
  544. package/Tests/UI/Rum/ChunkLoader.test.ts +201 -0
  545. package/Tests/UI/Rum/FidelityNotices.test.ts +54 -0
  546. package/Tests/UI/Rum/ReplayCorrelationPanel.test.tsx +72 -0
  547. package/Tests/UI/Rum/ReplayFrameCapture.test.ts +1009 -9
  548. package/Tests/UI/Rum/ReplayRecordedAssets.test.ts +1069 -0
  549. package/Tests/UI/Rum/ReplayStage.test.tsx +913 -2
  550. package/Tests/UI/Rum/SessionReplayTable.test.tsx +443 -8
  551. package/Tests/UI/Rum/SessionReplayUsersTable.test.tsx +439 -9
  552. package/Tests/UI/Rum/UserFlowTablesRowActions.test.tsx +501 -0
  553. package/Tests/Utils/AiRemediation/KubectlPolicyChartRbacParity.test.ts +47 -19
  554. package/Tests/Utils/AiRemediation/KubectlWriteScopeCallerParity.test.ts +2 -0
  555. package/Tests/Utils/AiRemediation/KubernetesAiAgentPolicyCopyParity.test.ts +396 -0
  556. package/Tests/Utils/Dashboard/DashboardTimeRangeZoom.test.ts +104 -1
  557. package/Tests/Utils/Incident/IncidentNoteTemplateVariables.test.ts +623 -0
  558. package/Tests/Utils/Markdown/MarkdownEscape.test.ts +89 -0
  559. package/Tests/Utils/ProbeNetworkFailureUtil.test.ts +992 -0
  560. package/Tests/Utils/RequestFailedDetails.test.ts +575 -0
  561. package/Types/AI/AIRunType.ts +9 -4
  562. package/Types/AI/InvestigationNotStartedReason.ts +6 -0
  563. package/Types/AutoRemediation/AiRemediationCommandPlan.ts +14 -16
  564. package/Types/AutoRemediation/AiRemediationCommandPolicyVerdict.ts +28 -0
  565. package/Types/CustomField/CustomFieldOrder.ts +55 -0
  566. package/Types/CustomField/CustomFieldSavedViews.ts +225 -0
  567. package/Types/CustomField/CustomFieldTemplateMerge.ts +71 -0
  568. package/Types/CustomField/CustomFieldType.ts +14 -0
  569. package/Types/CustomField/CustomFieldValueFormat.ts +161 -0
  570. package/Types/CustomField/CustomFieldValueValidator.ts +488 -0
  571. package/Types/CustomField/CustomFieldVariableKey.ts +211 -0
  572. package/Types/Dashboard/DashboardComponents/DashboardTraceChartComponent.ts +4 -2
  573. package/Types/Kubernetes/KubernetesClusterAiAccess.ts +263 -32
  574. package/Types/Kubernetes/KubernetesClusterAiAccessPermissions.ts +20 -16
  575. package/Types/Kubernetes/KubernetesClusterAiInsights.ts +81 -0
  576. package/Types/Monitor/ProxmoxAlertTemplates.ts +4 -4
  577. package/Types/SafeHtml.ts +93 -0
  578. package/Types/StatusPage/IncidentCreatedRenotify.ts +224 -0
  579. package/Types/StatusPage/IncidentCreatedResend.ts +108 -0
  580. package/Types/StatusPage/IncidentScopeAddedPagesNotification.ts +343 -0
  581. package/Types/StatusPage/IncidentSubscriberAudience.ts +332 -0
  582. package/Types/StatusPage/StatusPageSubscriberUnsubscribe.ts +405 -0
  583. package/Types/StatusPage/SubscriberNotificationInterruption.ts +32 -0
  584. package/Types/StatusPage/SubscriberNotificationPreview.ts +322 -0
  585. package/Types/StatusPage/SubscriberNotificationResend.ts +132 -0
  586. package/Types/StatusPage/SubscriberNotificationTemplateCompiler.ts +180 -0
  587. package/Types/StatusPage/SubscriberNotificationTemplatePreview.ts +301 -0
  588. package/Types/StatusPage/SubscriberNotificationTemplateVariables.ts +217 -4
  589. package/Types/StatusPage/SubscriberUpdateNotification.ts +32 -11
  590. package/Types/Team/ComplianceNotificationChannel.ts +22 -0
  591. package/Types/Team/ComplianceRule.ts +434 -0
  592. package/Types/Team/ComplianceRuleType.ts +21 -0
  593. package/Types/Team/TeamComplianceStatus.ts +81 -0
  594. package/Types/Telemetry/TelemetryIngestSurface.ts +28 -0
  595. package/UI/Components/ActionButton/ActionButtonSchema.ts +21 -0
  596. package/UI/Components/ActionButton/RowActions.tsx +238 -0
  597. package/UI/Components/ActionButton/SplitActionButtons.ts +173 -0
  598. package/UI/Components/Charts/Area/AreaChart.tsx +32 -2
  599. package/UI/Components/Charts/Bar/BarChart.tsx +40 -3
  600. package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +37 -11
  601. package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +80 -243
  602. package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +122 -31
  603. package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +95 -244
  604. package/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.ts +214 -0
  605. package/UI/Components/Charts/ChartLibrary/Utils/UseChartBucketsChange.ts +32 -0
  606. package/UI/Components/Charts/ChartLibrary/Utils/UseChartRangeSelection.ts +599 -0
  607. package/UI/Components/Charts/Line/LineChart.tsx +32 -2
  608. package/UI/Components/Charts/TimeRangeZoom/ResetTimeRangeZoomButton.tsx +70 -0
  609. package/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.tsx +226 -0
  610. package/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.tsx +73 -0
  611. package/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.ts +163 -0
  612. package/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.ts +215 -0
  613. package/UI/Components/Charts/Utils/DataPoint.ts +20 -8
  614. package/UI/Components/Charts/Utils/XAxis.ts +75 -0
  615. package/UI/Components/Charts/Utils/useHistogramRangeSelection.ts +493 -59
  616. package/UI/Components/CustomFields/CustomFieldFormFields.ts +317 -0
  617. package/UI/Components/CustomFields/CustomFieldModelFormFields.tsx +395 -0
  618. package/UI/Components/CustomFields/CustomFieldsDetail.tsx +95 -55
  619. package/UI/Components/EntityDropdown/EntityDropdown.tsx +107 -11
  620. package/UI/Components/Forms/Fields/FormField.tsx +10 -0
  621. package/UI/Components/Forms/ModelForm.tsx +70 -8
  622. package/UI/Components/Forms/Validation.ts +1 -1
  623. package/UI/Components/Input/Input.tsx +90 -57
  624. package/UI/Components/List/ListRow.tsx +8 -74
  625. package/UI/Components/LogsViewer/LogsViewer.tsx +40 -11
  626. package/UI/Components/LogsViewer/components/LogTimeRangePicker.tsx +22 -6
  627. package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +209 -6
  628. package/UI/Components/LogsViewer/components/LogsHistogram.tsx +127 -84
  629. package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +3 -0
  630. package/UI/Components/ModelFormModal/ModelFormModal.tsx +2 -4
  631. package/UI/Components/ModelTable/AnalyticsModelTable.tsx +2 -6
  632. package/UI/Components/ModelTable/BaseModelTable.tsx +21 -9
  633. package/UI/Components/ModelTable/CustomFieldColumns.tsx +14 -1
  634. package/UI/Components/ModelTable/ModelTable.tsx +6 -7
  635. package/UI/Components/MoreMenu/MoreMenu.tsx +365 -30
  636. package/UI/Components/MoreMenu/MoreMenuItem.tsx +70 -17
  637. package/UI/Components/OrderedStatesList/Item.tsx +7 -72
  638. package/UI/Components/StatusPage/ResourceGrid.tsx +126 -77
  639. package/UI/Components/Table/TableBody.tsx +8 -2
  640. package/UI/Components/Table/TableRow.tsx +20 -140
  641. package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +41 -10
  642. package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +47 -9
  643. package/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.tsx +25 -6
  644. package/UI/Components/TelemetryViewer/types.ts +2 -0
  645. package/UI/Components/TelemetryViewer/useViewerTimeRangeZoom.ts +448 -0
  646. package/UI/Components/Toggle/Toggle.tsx +26 -0
  647. package/UI/Components/Tooltip/Tooltip.tsx +8 -1
  648. package/UI/Styles/Theme.css +14 -2
  649. package/UI/Utils/SensitiveUrlToken.ts +10 -5
  650. package/Utils/API.ts +247 -2
  651. package/Utils/AiRemediation/CommandPolicy.ts +1 -1
  652. package/Utils/AiRemediation/KubectlPolicy.ts +1 -1
  653. package/Utils/Dashboard/DashboardTimeRangeZoom.ts +46 -2
  654. package/Utils/Incident/IncidentNoteTemplateVariables.ts +304 -0
  655. package/Utils/Markdown/MarkdownEscape.ts +65 -0
  656. package/Utils/ProbeNetworkFailureUtil.ts +673 -0
  657. package/build/dist/Models/DatabaseModels/Incident.js +267 -1
  658. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  659. package/build/dist/Models/DatabaseModels/IncidentCustomField.js +207 -1
  660. package/build/dist/Models/DatabaseModels/IncidentCustomField.js.map +1 -1
  661. package/build/dist/Models/DatabaseModels/IncidentEpisode.js +31 -0
  662. package/build/dist/Models/DatabaseModels/IncidentEpisode.js.map +1 -1
  663. package/build/dist/Models/DatabaseModels/IncidentTemplate.js +124 -1
  664. package/build/dist/Models/DatabaseModels/IncidentTemplate.js.map +1 -1
  665. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  666. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  667. package/build/dist/Models/DatabaseModels/KubernetesAiAgent.js +385 -0
  668. package/build/dist/Models/DatabaseModels/KubernetesAiAgent.js.map +1 -0
  669. package/build/dist/Models/DatabaseModels/KubernetesCluster.js +3 -3
  670. package/build/dist/Models/DatabaseModels/KubernetesCluster.js.map +1 -1
  671. package/build/dist/Models/DatabaseModels/Project.js +56 -5
  672. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  673. package/build/dist/Models/DatabaseModels/ProxmoxResource.js +51 -1
  674. package/build/dist/Models/DatabaseModels/ProxmoxResource.js.map +1 -1
  675. package/build/dist/Models/DatabaseModels/RunnerJob.js +74 -0
  676. package/build/dist/Models/DatabaseModels/RunnerJob.js.map +1 -1
  677. package/build/dist/Models/DatabaseModels/StatusPage.js +56 -0
  678. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  679. package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js +131 -0
  680. package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js.map +1 -1
  681. package/build/dist/Models/DatabaseModels/TeamComplianceSetting.js +134 -3
  682. package/build/dist/Models/DatabaseModels/TeamComplianceSetting.js.map +1 -1
  683. package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
  684. package/build/dist/Server/API/AIChatAPI.js +6 -1
  685. package/build/dist/Server/API/AIChatAPI.js.map +1 -1
  686. package/build/dist/Server/API/AIInvestigationAPI.js +17 -10
  687. package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -1
  688. package/build/dist/Server/API/AutoRemediationAPI.js +144 -24
  689. package/build/dist/Server/API/AutoRemediationAPI.js.map +1 -1
  690. package/build/dist/Server/API/IncidentAPI.js +100 -1
  691. package/build/dist/Server/API/IncidentAPI.js.map +1 -1
  692. package/build/dist/Server/API/KubernetesClusterAiAccessAPI.js +436 -22
  693. package/build/dist/Server/API/KubernetesClusterAiAccessAPI.js.map +1 -1
  694. package/build/dist/Server/API/MicrosoftTeamsAPI.js +69 -13
  695. package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
  696. package/build/dist/Server/API/ProxmoxResourceAPI.js +187 -14
  697. package/build/dist/Server/API/ProxmoxResourceAPI.js.map +1 -1
  698. package/build/dist/Server/API/StatusPageAPI.js +417 -176
  699. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  700. package/build/dist/Server/API/StatusPageSubscriberAPI.js +52 -13
  701. package/build/dist/Server/API/StatusPageSubscriberAPI.js.map +1 -1
  702. package/build/dist/Server/Infrastructure/GlobalCache.js +27 -0
  703. package/build/dist/Server/Infrastructure/GlobalCache.js.map +1 -1
  704. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795300000000-SkipHiddenIncidentCreatedNotifications.js +41 -0
  705. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795300000000-SkipHiddenIncidentCreatedNotifications.js.map +1 -0
  706. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795400000000-AddIncidentStatusPageScope.js +57 -0
  707. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795400000000-AddIncidentStatusPageScope.js.map +1 -0
  708. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795500000000-AddIncidentTemplateStatusPageScopeFlag.js +23 -0
  709. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795500000000-AddIncidentTemplateStatusPageScopeFlag.js.map +1 -0
  710. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795600000000-AddStatusPageSubscriberUnsubscribeToken.js +40 -0
  711. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795600000000-AddStatusPageSubscriberUnsubscribeToken.js.map +1 -0
  712. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795700000000-AddStatusPageSubscriberIsAddedByTeam.js +26 -0
  713. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795700000000-AddStatusPageSubscriberIsAddedByTeam.js.map +1 -0
  714. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795800000000-AddIncidentCustomFieldCreateAndNotificationSettings.js +89 -0
  715. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795800000000-AddIncidentCustomFieldCreateAndNotificationSettings.js.map +1 -0
  716. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795900000000-AddSubscriberNotificationClaimedAt.js +29 -0
  717. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795900000000-AddSubscriberNotificationClaimedAt.js.map +1 -0
  718. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796000000000-AddTeamComplianceRuleScope.js +50 -0
  719. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796000000000-AddTeamComplianceRuleScope.js.map +1 -0
  720. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796100000000-AddKubernetesAiAgentAndAiDefaults.js +84 -0
  721. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796100000000-AddKubernetesAiAgentAndAiDefaults.js.map +1 -0
  722. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796200000000-AddProxmoxResourceNativePushColumns.js +31 -0
  723. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796200000000-AddProxmoxResourceNativePushColumns.js.map +1 -0
  724. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +20 -0
  725. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  726. package/build/dist/Server/Infrastructure/Semaphore.js +3 -0
  727. package/build/dist/Server/Infrastructure/Semaphore.js.map +1 -1
  728. package/build/dist/Server/Middleware/IdentityRateLimit.js +89 -3
  729. package/build/dist/Server/Middleware/IdentityRateLimit.js.map +1 -1
  730. package/build/dist/Server/Middleware/SubscriberNotificationPreviewRateLimit.js +149 -0
  731. package/build/dist/Server/Middleware/SubscriberNotificationPreviewRateLimit.js.map +1 -0
  732. package/build/dist/Server/Middleware/SubscriberNotificationTestSendRateLimit.js +190 -0
  733. package/build/dist/Server/Middleware/SubscriberNotificationTestSendRateLimit.js.map +1 -0
  734. package/build/dist/Server/Middleware/TelemetryIngest.js +34 -19
  735. package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
  736. package/build/dist/Server/Services/AIService.js +59 -1
  737. package/build/dist/Server/Services/AIService.js.map +1 -1
  738. package/build/dist/Server/Services/AlertService.js +248 -13
  739. package/build/dist/Server/Services/AlertService.js.map +1 -1
  740. package/build/dist/Server/Services/AlertSeverityService.js +51 -15
  741. package/build/dist/Server/Services/AlertSeverityService.js.map +1 -1
  742. package/build/dist/Server/Services/AutoRemediationRuleService.js +1 -1
  743. package/build/dist/Server/Services/AutoRemediationRuleService.js.map +1 -1
  744. package/build/dist/Server/Services/DatabaseService.js +56 -7
  745. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  746. package/build/dist/Server/Services/IncidentCustomFieldService.js +204 -8
  747. package/build/dist/Server/Services/IncidentCustomFieldService.js.map +1 -1
  748. package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js +24 -0
  749. package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js.map +1 -1
  750. package/build/dist/Server/Services/IncidentEpisodeService.js +14 -0
  751. package/build/dist/Server/Services/IncidentEpisodeService.js.map +1 -1
  752. package/build/dist/Server/Services/IncidentEpisodeStateTimelineService.js +26 -0
  753. package/build/dist/Server/Services/IncidentEpisodeStateTimelineService.js.map +1 -1
  754. package/build/dist/Server/Services/IncidentPublicNoteService.js +26 -1
  755. package/build/dist/Server/Services/IncidentPublicNoteService.js.map +1 -1
  756. package/build/dist/Server/Services/IncidentService.js +1251 -72
  757. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  758. package/build/dist/Server/Services/IncidentSeverityService.js +51 -15
  759. package/build/dist/Server/Services/IncidentSeverityService.js.map +1 -1
  760. package/build/dist/Server/Services/IncidentStateTimelineService.js +12 -0
  761. package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
  762. package/build/dist/Server/Services/IncidentTemplateService.js +83 -0
  763. package/build/dist/Server/Services/IncidentTemplateService.js.map +1 -1
  764. package/build/dist/Server/Services/KubernetesAiAgentJobService.js +158 -0
  765. package/build/dist/Server/Services/KubernetesAiAgentJobService.js.map +1 -0
  766. package/build/dist/Server/Services/KubernetesAiAgentService.js +1145 -0
  767. package/build/dist/Server/Services/KubernetesAiAgentService.js.map +1 -0
  768. package/build/dist/Server/Services/KubernetesClusterAiAccessService.js +842 -371
  769. package/build/dist/Server/Services/KubernetesClusterAiAccessService.js.map +1 -1
  770. package/build/dist/Server/Services/KubernetesClusterService.js +86 -17
  771. package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
  772. package/build/dist/Server/Services/ProjectService.js +22 -0
  773. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  774. package/build/dist/Server/Services/ProxmoxResourceService.js +258 -3
  775. package/build/dist/Server/Services/ProxmoxResourceService.js.map +1 -1
  776. package/build/dist/Server/Services/RunnerJobService.js +178 -92
  777. package/build/dist/Server/Services/RunnerJobService.js.map +1 -1
  778. package/build/dist/Server/Services/RunnerService.js +1 -1
  779. package/build/dist/Server/Services/RunnerService.js.map +1 -1
  780. package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js +26 -1
  781. package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js.map +1 -1
  782. package/build/dist/Server/Services/ScheduledMaintenanceService.js +582 -20
  783. package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
  784. package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js +316 -46
  785. package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js.map +1 -1
  786. package/build/dist/Server/Services/SmsService.js +2 -22
  787. package/build/dist/Server/Services/SmsService.js.map +1 -1
  788. package/build/dist/Server/Services/StatusPageService.js +47 -9
  789. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  790. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js +237 -32
  791. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js.map +1 -1
  792. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateStatusPageService.js +109 -0
  793. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateStatusPageService.js.map +1 -1
  794. package/build/dist/Server/Services/StatusPageSubscriberService.js +925 -99
  795. package/build/dist/Server/Services/StatusPageSubscriberService.js.map +1 -1
  796. package/build/dist/Server/Services/TeamComplianceSettingService.js +833 -7
  797. package/build/dist/Server/Services/TeamComplianceSettingService.js.map +1 -1
  798. package/build/dist/Server/Types/Markdown.js +110 -6
  799. package/build/dist/Server/Types/Markdown.js.map +1 -1
  800. package/build/dist/Server/Types/Workflow/Components/Email.js +55 -6
  801. package/build/dist/Server/Types/Workflow/Components/Email.js.map +1 -1
  802. package/build/dist/Server/Utils/AI/ClusterAccess/ClusterAccessContext.js +52 -9
  803. package/build/dist/Server/Utils/AI/ClusterAccess/ClusterAccessContext.js.map +1 -1
  804. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.js +13 -9
  805. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.js.map +1 -1
  806. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlJobRunner.js +7 -3
  807. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlJobRunner.js.map +1 -1
  808. package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js +29 -15
  809. package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js.map +1 -1
  810. package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js +56 -20
  811. package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js.map +1 -1
  812. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js +39 -5
  813. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js.map +1 -1
  814. package/build/dist/Server/Utils/AI/SRE/IncidentPostmortemRunner.js +30 -3
  815. package/build/dist/Server/Utils/AI/SRE/IncidentPostmortemRunner.js.map +1 -1
  816. package/build/dist/Server/Utils/AI/SRE/InvestigationEligibility.js +13 -2
  817. package/build/dist/Server/Utils/AI/SRE/InvestigationEligibility.js.map +1 -1
  818. package/build/dist/Server/Utils/AI/SRE/InvestigationGrader.js +1 -1
  819. package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js +4 -2
  820. package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js.map +1 -1
  821. package/build/dist/Server/Utils/CustomField/CustomFieldRename.js +211 -0
  822. package/build/dist/Server/Utils/CustomField/CustomFieldRename.js.map +1 -0
  823. package/build/dist/Server/Utils/DataSource/EgressGuard.js +41 -12
  824. package/build/dist/Server/Utils/DataSource/EgressGuard.js.map +1 -1
  825. package/build/dist/Server/Utils/Mail/PinnedSmtpSocket.js +101 -0
  826. package/build/dist/Server/Utils/Mail/PinnedSmtpSocket.js.map +1 -0
  827. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +9 -0
  828. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -1
  829. package/build/dist/Server/Utils/SSRFProtection.js +48 -6
  830. package/build/dist/Server/Utils/SSRFProtection.js.map +1 -1
  831. package/build/dist/Server/Utils/StatusPage/IncidentStatusPageScope.js +631 -0
  832. package/build/dist/Server/Utils/StatusPage/IncidentStatusPageScope.js.map +1 -0
  833. package/build/dist/Server/Utils/StatusPage/IncidentSubscriberAudienceBuilder.js +403 -0
  834. package/build/dist/Server/Utils/StatusPage/IncidentSubscriberAudienceBuilder.js.map +1 -0
  835. package/build/dist/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.js +451 -0
  836. package/build/dist/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.js.map +1 -0
  837. package/build/dist/Server/Utils/StatusPage/StatusPageExclusion.js +13 -0
  838. package/build/dist/Server/Utils/StatusPage/StatusPageExclusion.js.map +1 -0
  839. package/build/dist/Server/Utils/StatusPage/StatusPageReadAccess.js +91 -0
  840. package/build/dist/Server/Utils/StatusPage/StatusPageReadAccess.js.map +1 -0
  841. package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.js +101 -0
  842. package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.js.map +1 -0
  843. package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.js +47 -0
  844. package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.js.map +1 -0
  845. package/build/dist/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.js +273 -0
  846. package/build/dist/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.js.map +1 -0
  847. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationClaim.js +73 -0
  848. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationClaim.js.map +1 -0
  849. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.js +722 -0
  850. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.js.map +1 -0
  851. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationFanOut.js +102 -0
  852. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationFanOut.js.map +1 -0
  853. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.js +527 -0
  854. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.js.map +1 -0
  855. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationResendAccess.js +278 -0
  856. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationResendAccess.js.map +1 -0
  857. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationRunLimit.js +137 -0
  858. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationRunLimit.js.map +1 -0
  859. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationTiming.js +103 -0
  860. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationTiming.js.map +1 -0
  861. package/build/dist/Server/Utils/StatusPage/SubscriberTemplateIncidentRecordAccess.js +114 -0
  862. package/build/dist/Server/Utils/StatusPage/SubscriberTemplateIncidentRecordAccess.js.map +1 -0
  863. package/build/dist/Server/Utils/StatusPageResource.js +39 -7
  864. package/build/dist/Server/Utils/StatusPageResource.js.map +1 -1
  865. package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js +111 -6
  866. package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js.map +1 -1
  867. package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js +40 -4
  868. package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js.map +1 -1
  869. package/build/dist/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.js +296 -0
  870. package/build/dist/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.js.map +1 -0
  871. package/build/dist/Server/Utils/Telemetry/ProxmoxNativePush.js +699 -0
  872. package/build/dist/Server/Utils/Telemetry/ProxmoxNativePush.js.map +1 -0
  873. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +77 -19
  874. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
  875. package/build/dist/Server/Utils/VM/VMAPI.js.map +1 -1
  876. package/build/dist/Server/Utils/VM/VMRunner.js +1 -0
  877. package/build/dist/Server/Utils/VM/VMRunner.js.map +1 -1
  878. package/build/dist/Server/Utils/Workflow/OAuth2TokenClient.js +3 -0
  879. package/build/dist/Server/Utils/Workflow/OAuth2TokenClient.js.map +1 -1
  880. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +505 -42
  881. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  882. package/build/dist/Types/AI/AIRunType.js +9 -4
  883. package/build/dist/Types/AI/AIRunType.js.map +1 -1
  884. package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js +10 -16
  885. package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js.map +1 -1
  886. package/build/dist/Types/AutoRemediation/AiRemediationCommandPolicyVerdict.js +28 -0
  887. package/build/dist/Types/AutoRemediation/AiRemediationCommandPolicyVerdict.js.map +1 -0
  888. package/build/dist/Types/CustomField/CustomFieldOrder.js +36 -0
  889. package/build/dist/Types/CustomField/CustomFieldOrder.js.map +1 -0
  890. package/build/dist/Types/CustomField/CustomFieldSavedViews.js +135 -0
  891. package/build/dist/Types/CustomField/CustomFieldSavedViews.js.map +1 -0
  892. package/build/dist/Types/CustomField/CustomFieldTemplateMerge.js +27 -0
  893. package/build/dist/Types/CustomField/CustomFieldTemplateMerge.js.map +1 -0
  894. package/build/dist/Types/CustomField/CustomFieldType.js +12 -0
  895. package/build/dist/Types/CustomField/CustomFieldType.js.map +1 -1
  896. package/build/dist/Types/CustomField/CustomFieldValueFormat.js +126 -0
  897. package/build/dist/Types/CustomField/CustomFieldValueFormat.js.map +1 -0
  898. package/build/dist/Types/CustomField/CustomFieldValueValidator.js +280 -0
  899. package/build/dist/Types/CustomField/CustomFieldValueValidator.js.map +1 -0
  900. package/build/dist/Types/CustomField/CustomFieldVariableKey.js +150 -0
  901. package/build/dist/Types/CustomField/CustomFieldVariableKey.js.map +1 -0
  902. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccess.js +72 -16
  903. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccess.js.map +1 -1
  904. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccessPermissions.js +20 -16
  905. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccessPermissions.js.map +1 -1
  906. package/build/dist/Types/Kubernetes/KubernetesClusterAiInsights.js +17 -0
  907. package/build/dist/Types/Kubernetes/KubernetesClusterAiInsights.js.map +1 -0
  908. package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js +4 -4
  909. package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js.map +1 -1
  910. package/build/dist/Types/SafeHtml.js +84 -0
  911. package/build/dist/Types/SafeHtml.js.map +1 -0
  912. package/build/dist/Types/StatusPage/IncidentCreatedRenotify.js +117 -0
  913. package/build/dist/Types/StatusPage/IncidentCreatedRenotify.js.map +1 -0
  914. package/build/dist/Types/StatusPage/IncidentCreatedResend.js +86 -0
  915. package/build/dist/Types/StatusPage/IncidentCreatedResend.js.map +1 -0
  916. package/build/dist/Types/StatusPage/IncidentScopeAddedPagesNotification.js +246 -0
  917. package/build/dist/Types/StatusPage/IncidentScopeAddedPagesNotification.js.map +1 -0
  918. package/build/dist/Types/StatusPage/IncidentSubscriberAudience.js +183 -0
  919. package/build/dist/Types/StatusPage/IncidentSubscriberAudience.js.map +1 -0
  920. package/build/dist/Types/StatusPage/StatusPageSubscriberUnsubscribe.js +277 -0
  921. package/build/dist/Types/StatusPage/StatusPageSubscriberUnsubscribe.js.map +1 -0
  922. package/build/dist/Types/StatusPage/SubscriberNotificationInterruption.js +27 -0
  923. package/build/dist/Types/StatusPage/SubscriberNotificationInterruption.js.map +1 -0
  924. package/build/dist/Types/StatusPage/SubscriberNotificationPreview.js +190 -0
  925. package/build/dist/Types/StatusPage/SubscriberNotificationPreview.js.map +1 -0
  926. package/build/dist/Types/StatusPage/SubscriberNotificationResend.js +102 -0
  927. package/build/dist/Types/StatusPage/SubscriberNotificationResend.js.map +1 -0
  928. package/build/dist/Types/StatusPage/SubscriberNotificationTemplateCompiler.js +97 -0
  929. package/build/dist/Types/StatusPage/SubscriberNotificationTemplateCompiler.js.map +1 -0
  930. package/build/dist/Types/StatusPage/SubscriberNotificationTemplatePreview.js +175 -0
  931. package/build/dist/Types/StatusPage/SubscriberNotificationTemplatePreview.js.map +1 -0
  932. package/build/dist/Types/StatusPage/SubscriberNotificationTemplateVariables.js +144 -4
  933. package/build/dist/Types/StatusPage/SubscriberNotificationTemplateVariables.js.map +1 -1
  934. package/build/dist/Types/StatusPage/SubscriberUpdateNotification.js +29 -9
  935. package/build/dist/Types/StatusPage/SubscriberUpdateNotification.js.map +1 -1
  936. package/build/dist/Types/Team/ComplianceNotificationChannel.js +23 -0
  937. package/build/dist/Types/Team/ComplianceNotificationChannel.js.map +1 -0
  938. package/build/dist/Types/Team/ComplianceRule.js +304 -0
  939. package/build/dist/Types/Team/ComplianceRule.js.map +1 -0
  940. package/build/dist/Types/Team/ComplianceRuleType.js +21 -0
  941. package/build/dist/Types/Team/ComplianceRuleType.js.map +1 -1
  942. package/build/dist/Types/Team/TeamComplianceStatus.js +2 -0
  943. package/build/dist/Types/Team/TeamComplianceStatus.js.map +1 -0
  944. package/build/dist/Types/Telemetry/TelemetryIngestSurface.js +25 -0
  945. package/build/dist/Types/Telemetry/TelemetryIngestSurface.js.map +1 -1
  946. package/build/dist/UI/Components/ActionButton/ActionButtonSchema.js +20 -1
  947. package/build/dist/UI/Components/ActionButton/ActionButtonSchema.js.map +1 -1
  948. package/build/dist/UI/Components/ActionButton/RowActions.js +113 -0
  949. package/build/dist/UI/Components/ActionButton/RowActions.js.map +1 -0
  950. package/build/dist/UI/Components/ActionButton/SplitActionButtons.js +71 -0
  951. package/build/dist/UI/Components/ActionButton/SplitActionButtons.js.map +1 -0
  952. package/build/dist/UI/Components/Charts/Area/AreaChart.js +16 -1
  953. package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
  954. package/build/dist/UI/Components/Charts/Bar/BarChart.js +16 -1
  955. package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
  956. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +27 -5
  957. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
  958. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +61 -194
  959. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
  960. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +84 -26
  961. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
  962. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +75 -196
  963. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
  964. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js +133 -0
  965. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js.map +1 -1
  966. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartBucketsChange.js +26 -0
  967. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartBucketsChange.js.map +1 -0
  968. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartRangeSelection.js +371 -0
  969. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartRangeSelection.js.map +1 -0
  970. package/build/dist/UI/Components/Charts/Line/LineChart.js +16 -1
  971. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  972. package/build/dist/UI/Components/Charts/TimeRangeZoom/ResetTimeRangeZoomButton.js +35 -0
  973. package/build/dist/UI/Components/Charts/TimeRangeZoom/ResetTimeRangeZoomButton.js.map +1 -0
  974. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.js +107 -0
  975. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.js.map +1 -0
  976. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.js +41 -0
  977. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.js.map +1 -0
  978. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.js +117 -0
  979. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.js.map +1 -0
  980. package/build/dist/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.js +121 -0
  981. package/build/dist/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.js.map +1 -0
  982. package/build/dist/UI/Components/Charts/Utils/DataPoint.js +15 -7
  983. package/build/dist/UI/Components/Charts/Utils/DataPoint.js.map +1 -1
  984. package/build/dist/UI/Components/Charts/Utils/XAxis.js +70 -0
  985. package/build/dist/UI/Components/Charts/Utils/XAxis.js.map +1 -1
  986. package/build/dist/UI/Components/Charts/Utils/useHistogramRangeSelection.js +305 -38
  987. package/build/dist/UI/Components/Charts/Utils/useHistogramRangeSelection.js.map +1 -1
  988. package/build/dist/UI/Components/CustomFields/CustomFieldFormFields.js +169 -0
  989. package/build/dist/UI/Components/CustomFields/CustomFieldFormFields.js.map +1 -0
  990. package/build/dist/UI/Components/CustomFields/CustomFieldModelFormFields.js +212 -0
  991. package/build/dist/UI/Components/CustomFields/CustomFieldModelFormFields.js.map +1 -0
  992. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +64 -49
  993. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
  994. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js +89 -14
  995. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js.map +1 -1
  996. package/build/dist/UI/Components/Forms/Fields/FormField.js +11 -2
  997. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  998. package/build/dist/UI/Components/Forms/ModelForm.js +39 -5
  999. package/build/dist/UI/Components/Forms/ModelForm.js.map +1 -1
  1000. package/build/dist/UI/Components/Forms/Validation.js +1 -1
  1001. package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
  1002. package/build/dist/UI/Components/Input/Input.js +69 -54
  1003. package/build/dist/UI/Components/Input/Input.js.map +1 -1
  1004. package/build/dist/UI/Components/List/ListRow.js +4 -39
  1005. package/build/dist/UI/Components/List/ListRow.js.map +1 -1
  1006. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +31 -8
  1007. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  1008. package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js +10 -1
  1009. package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js.map +1 -1
  1010. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +139 -21
  1011. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
  1012. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +30 -8
  1013. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -1
  1014. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +1 -1
  1015. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -1
  1016. package/build/dist/UI/Components/ModelFormModal/ModelFormModal.js.map +1 -1
  1017. package/build/dist/UI/Components/ModelTable/AnalyticsModelTable.js.map +1 -1
  1018. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +12 -2
  1019. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  1020. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js +13 -1
  1021. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
  1022. package/build/dist/UI/Components/ModelTable/ModelTable.js +1 -1
  1023. package/build/dist/UI/Components/ModelTable/ModelTable.js.map +1 -1
  1024. package/build/dist/UI/Components/MoreMenu/MoreMenu.js +253 -14
  1025. package/build/dist/UI/Components/MoreMenu/MoreMenu.js.map +1 -1
  1026. package/build/dist/UI/Components/MoreMenu/MoreMenuItem.js +43 -13
  1027. package/build/dist/UI/Components/MoreMenu/MoreMenuItem.js.map +1 -1
  1028. package/build/dist/UI/Components/OrderedStatesList/Item.js +2 -38
  1029. package/build/dist/UI/Components/OrderedStatesList/Item.js.map +1 -1
  1030. package/build/dist/UI/Components/StatusPage/ResourceGrid.js +53 -15
  1031. package/build/dist/UI/Components/StatusPage/ResourceGrid.js.map +1 -1
  1032. package/build/dist/UI/Components/Table/TableBody.js +6 -2
  1033. package/build/dist/UI/Components/Table/TableBody.js.map +1 -1
  1034. package/build/dist/UI/Components/Table/TableRow.js +12 -75
  1035. package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
  1036. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +32 -7
  1037. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
  1038. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +27 -9
  1039. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
  1040. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js +11 -1
  1041. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js.map +1 -1
  1042. package/build/dist/UI/Components/TelemetryViewer/useViewerTimeRangeZoom.js +272 -0
  1043. package/build/dist/UI/Components/TelemetryViewer/useViewerTimeRangeZoom.js.map +1 -0
  1044. package/build/dist/UI/Components/Toggle/Toggle.js +14 -1
  1045. package/build/dist/UI/Components/Toggle/Toggle.js.map +1 -1
  1046. package/build/dist/UI/Components/Tooltip/Tooltip.js +1 -1
  1047. package/build/dist/UI/Components/Tooltip/Tooltip.js.map +1 -1
  1048. package/build/dist/UI/Utils/SensitiveUrlToken.js +10 -5
  1049. package/build/dist/UI/Utils/SensitiveUrlToken.js.map +1 -1
  1050. package/build/dist/Utils/API.js +177 -1
  1051. package/build/dist/Utils/API.js.map +1 -1
  1052. package/build/dist/Utils/AiRemediation/CommandPolicy.js +1 -1
  1053. package/build/dist/Utils/AiRemediation/CommandPolicy.js.map +1 -1
  1054. package/build/dist/Utils/AiRemediation/KubectlPolicy.js +1 -1
  1055. package/build/dist/Utils/AiRemediation/KubectlPolicy.js.map +1 -1
  1056. package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js +27 -1
  1057. package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js.map +1 -1
  1058. package/build/dist/Utils/Incident/IncidentNoteTemplateVariables.js +155 -0
  1059. package/build/dist/Utils/Incident/IncidentNoteTemplateVariables.js.map +1 -0
  1060. package/build/dist/Utils/Markdown/MarkdownEscape.js +48 -0
  1061. package/build/dist/Utils/Markdown/MarkdownEscape.js.map +1 -1
  1062. package/build/dist/Utils/ProbeNetworkFailureUtil.js +460 -0
  1063. package/build/dist/Utils/ProbeNetworkFailureUtil.js.map +1 -0
  1064. package/package.json +1 -1
@@ -0,0 +1,2526 @@
1
+ import "@testing-library/jest-dom";
2
+ import {
3
+ afterEach,
4
+ beforeEach,
5
+ describe,
6
+ expect,
7
+ jest,
8
+ test,
9
+ } from "@jest/globals";
10
+ import { RenderResult, act, cleanup, render } from "@testing-library/react";
11
+ import type { SpyInstance } from "jest-mock";
12
+ import React, {
13
+ FunctionComponent,
14
+ MutableRefObject,
15
+ ReactElement,
16
+ } from "react";
17
+ import {
18
+ FitViewOptions,
19
+ Node,
20
+ ReactFlowState,
21
+ Viewport,
22
+ XYPosition,
23
+ } from "reactflow";
24
+ import FlowViewportGuard, {
25
+ ComponentProps,
26
+ isKeyboardFocus,
27
+ measuredRectOf,
28
+ renderedPaneSize,
29
+ selectDrawingInView,
30
+ } from "../../../../App/FeatureSet/Dashboard/src/Components/Topology/FlowViewportGuard";
31
+ import {
32
+ FlowExtent,
33
+ FlowRect,
34
+ FlowTransform,
35
+ UNBOUNDED_FLOW_EXTENT,
36
+ } from "../../../../App/FeatureSet/Dashboard/src/Components/Topology/FlowViewport";
37
+ import getJestMockFunction, { MockFunction } from "../../MockType";
38
+
39
+ /*
40
+ * FlowViewportGuard keeps the Service Map on its canvas (issue #4117).
41
+ * Rendered inside <ReactFlow>, it decides from React Flow's own store:
42
+ *
43
+ * - it fits the view only once React Flow holds the drawing the map asked
44
+ * for, every card measured, in a pane that has a size - the size the page
45
+ * gives it, not one React Flow has yet to catch up with - and a viewport
46
+ * that is ready; a fit React Flow refuses is owed until one lands (a
47
+ * re-fit owed from a resize only until the user takes the view), and the
48
+ * same drawing is never fitted twice in the same pane;
49
+ * - it fits again whenever the drawing changes, and when the pane is resized
50
+ * while the view is still the automatic framing (autoFrame), a resize being
51
+ * judged against the last pane size it saw; once the user has the view, a
52
+ * resize re-applies the pan extent instead;
53
+ * - it follows keyboard focus: a card or connection focused off the canvas
54
+ * is panned the least that brings it 24 px inside, through React Flow's
55
+ * panBy, which keeps the view inside the pan extent, and drawn there at
56
+ * once, before the page is scrolled to where it now is; the scroll Chromium
57
+ * makes of React Flow's element before it tells of the focus is dropped
58
+ * first, and a scroll of that element the browser makes anyway is undone
59
+ * and made the same pan. Focus a pointer gives, and focus the browser hands
60
+ * back as the window regains focus, are left alone;
61
+ * - it reports the measured drawing plus a margin as the pan extent, whether
62
+ * any card is in view, and the live instance, and withdraws all three when
63
+ * it unmounts.
64
+ *
65
+ * React Flow is replaced by a fake of the three hooks the guard calls.
66
+ * useStore reads a small mutable state - the nodes React Flow holds, the
67
+ * viewport transform, the pane's size, React Flow's element and its d3 zoom -
68
+ * through useSyncExternalStore, so a store change re-renders the guard the
69
+ * way zustand's does; useStoreApi hands out the same state for the reads the
70
+ * guard makes inside its effects, React Flow's panBy among them, which moves
71
+ * the view the way React Flow's does unless a test has the pan extent refuse
72
+ * the pan. useReactFlow returns an instance that, like React Flow's, becomes
73
+ * a new object when the viewport is initialized, and what is on screen moves
74
+ * only once React renders the view, as React Flow's viewport does (see
75
+ * DrawnViewport). jsdom lays nothing out, so where the canvas and what React
76
+ * Flow draws in it are on screen is the test's to say (see flowElement); nor
77
+ * does it tell keyboard focus from a pointer's, or let a test say when an
78
+ * event happened, so the focus tests do both (see focusCameBy and
79
+ * pageClockMs). The guard and its maths run for real.
80
+ */
81
+
82
+ /* d3-zoom's behaviour as React Flow keeps it: what the guard asks of it. */
83
+ interface FakeD3Zoom {
84
+ translateBy: MockFunction;
85
+ }
86
+
87
+ /* Stands in for d3's selection of React Flow's zoom pane. */
88
+ interface FakeD3Selection {
89
+ selects: string;
90
+ }
91
+
92
+ interface FakeFlowState {
93
+ nodeInternals: Map<string, Node>;
94
+ transform: FlowTransform;
95
+ width: number;
96
+ height: number;
97
+ /*
98
+ * React Flow's own element, `.react-flow`, which React Flow records when
99
+ * its zoom pane mounts. Null unless a test lays one out.
100
+ */
101
+ domNode: HTMLDivElement | null;
102
+ d3Zoom: FakeD3Zoom | null;
103
+ d3Selection: FakeD3Selection | null;
104
+ /* React Flow's store.panBy (see panViewBy). */
105
+ panBy: MockFunction;
106
+ }
107
+
108
+ interface FakeFlowInstance {
109
+ viewportInitialized: boolean;
110
+ fitView: MockFunction;
111
+ getViewport: () => Viewport;
112
+ setViewport: MockFunction;
113
+ }
114
+
115
+ /* What useStoreApi returns: the store, for reads made inside effects. */
116
+ interface FakeStoreApi {
117
+ getState: () => FakeFlowState;
118
+ }
119
+
120
+ interface FakeFlowStore {
121
+ state: FakeFlowState;
122
+ instance: FakeFlowInstance;
123
+ listeners: Set<() => void>;
124
+ subscribe: (listener: () => void) => () => void;
125
+ /* Replaces some of the state, then tells every subscriber, as zustand does. */
126
+ set: (partial: Partial<FakeFlowState>) => void;
127
+ /*
128
+ * React Flow's d3 zoom becoming ready (or not). useReactFlow memoizes its
129
+ * instance on it, so the instance is a new object only when this flips.
130
+ */
131
+ setViewportInitialized: (viewportInitialized: boolean) => void;
132
+ }
133
+
134
+ const mockFitView: MockFunction = getJestMockFunction();
135
+ const mockSetViewport: MockFunction = getJestMockFunction();
136
+ const mockTranslateBy: MockFunction = getJestMockFunction();
137
+ const mockPanBy: MockFunction = getJestMockFunction();
138
+
139
+ /*
140
+ * Whether the pan extent lets panBy move the view. A test sets it to false
141
+ * for a view already at the edge of the extent, which refuses a pan outright.
142
+ */
143
+ let panByMoves: boolean = true;
144
+
145
+ /*
146
+ * React Flow's store.panBy, which mockPanBy runs (see beforeEach): it moves
147
+ * the view by a delta in screen pixels, positive moving the drawing right and
148
+ * down, and says whether the view moved. React Flow keeps the view inside the
149
+ * pan extent as it goes; here the extent lets a pan through whole unless
150
+ * panByMoves says it refuses it. Like React Flow's, it does nothing for a zero
151
+ * delta.
152
+ */
153
+ function panViewBy(delta: XYPosition): boolean {
154
+ if (!panByMoves || (!delta.x && !delta.y)) {
155
+ return false;
156
+ }
157
+ const [x, y, zoom] = mockFlowStore.state.transform;
158
+ mockFlowStore.set({ transform: [x + delta.x, y + delta.y, zoom] });
159
+ return true;
160
+ }
161
+
162
+ /* d3's selection of the zoom pane: the guard only ever hands it back to d3. */
163
+ const D3_SELECTION: FakeD3Selection = { selects: ".react-flow__renderer" };
164
+
165
+ function emptyFlowState(): FakeFlowState {
166
+ return {
167
+ nodeInternals: new Map<string, Node>(),
168
+ transform: [0, 0, 1],
169
+ width: 0,
170
+ height: 0,
171
+ domNode: null,
172
+ d3Zoom: { translateBy: mockTranslateBy },
173
+ d3Selection: D3_SELECTION,
174
+ panBy: mockPanBy,
175
+ };
176
+ }
177
+
178
+ /*
179
+ * The instance useReactFlow returns. Until its viewport is ready React
180
+ * Flow's reports the identity viewport; once it is, the view is the store's
181
+ * transform. setViewport is a mock that moves the view the way React Flow's
182
+ * d3 zoom handler does, by writing the store's transform (see beforeEach).
183
+ */
184
+ function flowInstance(viewportInitialized: boolean): FakeFlowInstance {
185
+ return {
186
+ viewportInitialized,
187
+ fitView: mockFitView,
188
+ getViewport: (): Viewport => {
189
+ if (!viewportInitialized) {
190
+ return { x: 0, y: 0, zoom: 1 };
191
+ }
192
+ const transform: FlowTransform = mockFlowStore.state.transform;
193
+ return { x: transform[0], y: transform[1], zoom: transform[2] };
194
+ },
195
+ setViewport: mockSetViewport,
196
+ };
197
+ }
198
+
199
+ function notifyFlowSubscribers(): void {
200
+ for (const listener of Array.from(mockFlowStore.listeners)) {
201
+ listener();
202
+ }
203
+ }
204
+
205
+ const mockFlowStore: FakeFlowStore = {
206
+ state: emptyFlowState(),
207
+ instance: flowInstance(false),
208
+ listeners: new Set<() => void>(),
209
+ subscribe: (listener: () => void): (() => void) => {
210
+ mockFlowStore.listeners.add(listener);
211
+ return (): void => {
212
+ mockFlowStore.listeners.delete(listener);
213
+ };
214
+ },
215
+ set: (partial: Partial<FakeFlowState>): void => {
216
+ mockFlowStore.state = { ...mockFlowStore.state, ...partial };
217
+ notifyFlowSubscribers();
218
+ },
219
+ setViewportInitialized: (viewportInitialized: boolean): void => {
220
+ if (mockFlowStore.instance.viewportInitialized === viewportInitialized) {
221
+ return;
222
+ }
223
+ mockFlowStore.instance = flowInstance(viewportInitialized);
224
+ notifyFlowSubscribers();
225
+ },
226
+ };
227
+
228
+ /* One object for the life of the store, as React Flow's context gives out. */
229
+ const mockStoreApi: FakeStoreApi = {
230
+ getState: (): FakeFlowState => {
231
+ return mockFlowStore.state;
232
+ },
233
+ };
234
+
235
+ /*
236
+ * The arrow wrappers are load bearing: jest.mock is hoisted above the
237
+ * compiled requires, so the store is only reached once a hook runs.
238
+ */
239
+ jest.mock("reactflow", () => {
240
+ return {
241
+ __esModule: true,
242
+ useStore: (selector: (state: ReactFlowState) => unknown): unknown => {
243
+ return React.useSyncExternalStore(
244
+ mockFlowStore.subscribe,
245
+ (): unknown => {
246
+ return selector(mockFlowStore.state as unknown as ReactFlowState);
247
+ },
248
+ );
249
+ },
250
+ useStoreApi: (): FakeStoreApi => {
251
+ return mockStoreApi;
252
+ },
253
+ useReactFlow: (): FakeFlowInstance => {
254
+ return React.useSyncExternalStore(
255
+ mockFlowStore.subscribe,
256
+ (): FakeFlowInstance => {
257
+ return mockFlowStore.instance;
258
+ },
259
+ );
260
+ },
261
+ };
262
+ });
263
+
264
+ const CARD_WIDTH: number = 220;
265
+ const CARD_HEIGHT: number = 80;
266
+ const PANE_WIDTH: number = 800;
267
+ const PANE_HEIGHT: number = 600;
268
+ const PAN_MARGIN: number = 50;
269
+ /* Not the map's own options, so a fit is seen to use the ones it was given. */
270
+ const FIT_VIEW_OPTIONS: FitViewOptions = {
271
+ padding: 0.25,
272
+ minZoom: 0.2,
273
+ maxZoom: 1.2,
274
+ };
275
+
276
+ /* A card React Flow has measured, placed at (x, y) on the drawing. */
277
+ function measuredCard(
278
+ id: string,
279
+ x: number,
280
+ y: number,
281
+ width: number = CARD_WIDTH,
282
+ height: number = CARD_HEIGHT,
283
+ ): Node {
284
+ return {
285
+ id,
286
+ position: { x, y },
287
+ positionAbsolute: { x, y },
288
+ width,
289
+ height,
290
+ data: {},
291
+ };
292
+ }
293
+
294
+ /* A card React Flow has been handed but not measured: it has no size yet. */
295
+ function unmeasuredCard(id: string, x: number, y: number): Node {
296
+ return {
297
+ id,
298
+ position: { x, y },
299
+ positionAbsolute: { x, y },
300
+ data: {},
301
+ };
302
+ }
303
+
304
+ /* React Flow's nodeInternals: the cards it holds, by id, in drawing order. */
305
+ function holding(...cards: Array<Node>): Map<string, Node> {
306
+ return new Map<string, Node>(
307
+ cards.map((card: Node): [string, Node] => {
308
+ return [card.id, card];
309
+ }),
310
+ );
311
+ }
312
+
313
+ /* The drawing the map asks for unless a test says otherwise: api calls db. */
314
+ const DRAWING_KEY: string = "api|db";
315
+
316
+ function apiAndDb(): Map<string, Node> {
317
+ return holding(measuredCard("api", 0, 0), measuredCard("db", 400, 200));
318
+ }
319
+
320
+ /* The measured boxes of api and db, grown by the pan margin. */
321
+ const API_AND_DB_EXTENT: FlowExtent = [
322
+ [-50, -50],
323
+ [670, 330],
324
+ ];
325
+
326
+ /* A change to React Flow's store, inside act() as React Flow's would be. */
327
+ function flowChanges(partial: Partial<FakeFlowState>): void {
328
+ act(() => {
329
+ mockFlowStore.set(partial);
330
+ });
331
+ }
332
+
333
+ function viewportReady(): void {
334
+ act(() => {
335
+ mockFlowStore.setViewportInitialized(true);
336
+ });
337
+ }
338
+
339
+ /*
340
+ * React Flow holding the drawing, measured, in a pane of its full size, with
341
+ * its viewport ready: everything a fit waits for.
342
+ */
343
+ function drawnAndReady(nodeInternals: Map<string, Node> = apiAndDb()): void {
344
+ flowChanges({ nodeInternals, width: PANE_WIDTH, height: PANE_HEIGHT });
345
+ viewportReady();
346
+ }
347
+
348
+ /*
349
+ * The same, already so before the guard mounts: nothing is rendered yet, so
350
+ * there is nobody to tell.
351
+ */
352
+ function drawnAndReadyBeforeMount(): void {
353
+ mockFlowStore.state = {
354
+ ...emptyFlowState(),
355
+ nodeInternals: apiAndDb(),
356
+ width: PANE_WIDTH,
357
+ height: PANE_HEIGHT,
358
+ };
359
+ mockFlowStore.instance = flowInstance(true);
360
+ }
361
+
362
+ /* A plain store state for the selector, the pane sized unless overridden. */
363
+ function flowState(partial: Partial<FakeFlowState>): ReactFlowState {
364
+ return {
365
+ ...emptyFlowState(),
366
+ width: PANE_WIDTH,
367
+ height: PANE_HEIGHT,
368
+ ...partial,
369
+ } as unknown as ReactFlowState;
370
+ }
371
+
372
+ /*
373
+ * Where the page puts the canvas: its top-left corner in the browser's
374
+ * viewport, in screen pixels.
375
+ */
376
+ const CANVAS_LEFT: number = 40;
377
+ const CANVAS_TOP: number = 120;
378
+ /*
379
+ * React Flow's controls, as Chromium lays them out: 26 x 27 px buttons in a
380
+ * panel 15 px in from the corner.
381
+ */
382
+ const CONTROLS_MARGIN_PX: number = 15;
383
+ const CONTROLS_BUTTON_WIDTH_PX: number = 26;
384
+ const CONTROLS_BUTTON_HEIGHT_PX: number = 27;
385
+
386
+ /* A box on screen, as getBoundingClientRect reports it. */
387
+ function screenRect(
388
+ left: number,
389
+ top: number,
390
+ width: number,
391
+ height: number,
392
+ ): DOMRect {
393
+ const box: Omit<DOMRect, "toJSON"> = {
394
+ x: left,
395
+ y: top,
396
+ left,
397
+ top,
398
+ width,
399
+ height,
400
+ right: left + width,
401
+ bottom: top + height,
402
+ };
403
+ return {
404
+ ...box,
405
+ toJSON: (): Omit<DOMRect, "toJSON"> => {
406
+ return box;
407
+ },
408
+ };
409
+ }
410
+
411
+ /*
412
+ * The view as React Flow has drawn it: the transform its viewport was last
413
+ * rendered with. A pan moves React Flow's store at once, but what is on
414
+ * screen only once React renders the change - straight away inside
415
+ * flushSync, otherwise after the event that asked for it - and it is what is
416
+ * on screen that the browser measures (see onScreen).
417
+ */
418
+ let drawnTransform: FlowTransform = [0, 0, 1];
419
+
420
+ /*
421
+ * React Flow's viewport, down to the transform it draws the map with. It is
422
+ * rendered beside the guard (see renderGuard), as <ReactFlow> renders both.
423
+ */
424
+ const DrawnViewport: FunctionComponent = (): ReactElement => {
425
+ const transform: FlowTransform = React.useSyncExternalStore(
426
+ mockFlowStore.subscribe,
427
+ (): FlowTransform => {
428
+ return mockFlowStore.state.transform;
429
+ },
430
+ );
431
+ React.useLayoutEffect(() => {
432
+ drawnTransform = transform;
433
+ }, [transform]);
434
+ return <></>;
435
+ };
436
+
437
+ /*
438
+ * Where React Flow draws a box of the drawing (in flow units) on screen:
439
+ * through the view it has drawn (see drawnTransform), from the canvas's
440
+ * top-left corner. Read when asked, so a pan moves it once React has rendered
441
+ * the pan, the way it moves what React Flow draws.
442
+ */
443
+ function onScreen(box: FlowRect): DOMRect {
444
+ const [x, y, zoom] = drawnTransform;
445
+ return screenRect(
446
+ CANVAS_LEFT + x + box.x * zoom,
447
+ CANVAS_TOP + y + box.y * zoom,
448
+ box.width * zoom,
449
+ box.height * zoom,
450
+ );
451
+ }
452
+
453
+ /*
454
+ * React Flow's element as the page lays it out: `.react-flow`, the store's
455
+ * domNode, around `.react-flow__renderer`, the zoom pane React Flow measures
456
+ * the canvas from and draws every card and connection in, and around the
457
+ * panel of its controls, which React Flow puts beside the renderer rather
458
+ * than in it. jsdom lays nothing out, so the renderer's size and where things
459
+ * are on screen are the test's to set: React Flow's element, the renderer and
460
+ * the pane inside it all cover the canvas, at (CANVAS_LEFT, CANVAS_TOP), and
461
+ * everything in React Flow's element moves with its scroll, as a browser
462
+ * moves it.
463
+ */
464
+ interface FlowElement {
465
+ domNode: HTMLDivElement;
466
+ /* The page lays the canvas out at another size, unseen by React Flow. */
467
+ layOut: (width: number, height: number) => void;
468
+ /* A card React Flow draws for a box of the drawing, focusable as it makes them. */
469
+ drawCard: (box: FlowRect) => HTMLDivElement;
470
+ /* A connection React Flow draws: a focusable SVG group, over its box. */
471
+ drawConnection: (box: FlowRect) => SVGGElement;
472
+ /*
473
+ * The controls' Fit to screen button, the lowest of three: in the canvas's
474
+ * bottom-left corner, 15 px in, wherever the view is.
475
+ */
476
+ fitViewButton: HTMLButtonElement;
477
+ }
478
+
479
+ function flowElement(width: number, height: number): FlowElement {
480
+ const domNode: HTMLDivElement = document.createElement("div");
481
+ domNode.className = "react-flow";
482
+ const renderer: HTMLDivElement = document.createElement("div");
483
+ renderer.className = "react-flow__renderer";
484
+ domNode.appendChild(renderer);
485
+ const size: { width: number; height: number } = { width, height };
486
+ Object.defineProperty(renderer, "offsetWidth", {
487
+ configurable: true,
488
+ get: (): number => {
489
+ return size.width;
490
+ },
491
+ });
492
+ Object.defineProperty(renderer, "offsetHeight", {
493
+ configurable: true,
494
+ get: (): number => {
495
+ return size.height;
496
+ },
497
+ });
498
+ const canvas: () => DOMRect = (): DOMRect => {
499
+ return screenRect(CANVAS_LEFT, CANVAS_TOP, size.width, size.height);
500
+ };
501
+ /*
502
+ * Where a box inside React Flow's element is on screen. A scroll of the
503
+ * element moves all it holds - the renderer, what React Flow draws, the
504
+ * controls - the other way, while the element stays where the page put it.
505
+ */
506
+ const inScroll: (box: DOMRect) => DOMRect = (box: DOMRect): DOMRect => {
507
+ return screenRect(
508
+ box.left - domNode.scrollLeft,
509
+ box.top - domNode.scrollTop,
510
+ box.width,
511
+ box.height,
512
+ );
513
+ };
514
+ const scrolledCanvas: () => DOMRect = (): DOMRect => {
515
+ return inScroll(canvas());
516
+ };
517
+ domNode.getBoundingClientRect = canvas;
518
+ renderer.getBoundingClientRect = scrolledCanvas;
519
+
520
+ // The pane, then the viewport React Flow transforms, then what it draws.
521
+ const pane: HTMLDivElement = document.createElement("div");
522
+ pane.className = "react-flow__pane";
523
+ pane.getBoundingClientRect = scrolledCanvas;
524
+ renderer.appendChild(pane);
525
+ const viewport: HTMLDivElement = document.createElement("div");
526
+ viewport.className = "react-flow__viewport";
527
+ pane.appendChild(viewport);
528
+ const edges: SVGSVGElement = document.createElementNS(
529
+ "http://www.w3.org/2000/svg",
530
+ "svg",
531
+ );
532
+ edges.setAttribute("class", "react-flow__edges");
533
+ viewport.appendChild(edges);
534
+ const nodes: HTMLDivElement = document.createElement("div");
535
+ nodes.className = "react-flow__nodes";
536
+ viewport.appendChild(nodes);
537
+
538
+ const controls: HTMLDivElement = document.createElement("div");
539
+ controls.className = "react-flow__panel react-flow__controls bottom left";
540
+ domNode.appendChild(controls);
541
+ const fitViewButton: HTMLButtonElement = document.createElement("button");
542
+ fitViewButton.className = "react-flow__controls-button";
543
+ fitViewButton.getBoundingClientRect = (): DOMRect => {
544
+ return inScroll(
545
+ screenRect(
546
+ CANVAS_LEFT + CONTROLS_MARGIN_PX,
547
+ CANVAS_TOP +
548
+ size.height -
549
+ CONTROLS_MARGIN_PX -
550
+ CONTROLS_BUTTON_HEIGHT_PX,
551
+ CONTROLS_BUTTON_WIDTH_PX,
552
+ CONTROLS_BUTTON_HEIGHT_PX,
553
+ ),
554
+ );
555
+ };
556
+ controls.appendChild(fitViewButton);
557
+
558
+ return {
559
+ domNode,
560
+ layOut: (nextWidth: number, nextHeight: number): void => {
561
+ size.width = nextWidth;
562
+ size.height = nextHeight;
563
+ },
564
+ drawCard: (box: FlowRect): HTMLDivElement => {
565
+ const card: HTMLDivElement = document.createElement("div");
566
+ card.className = "react-flow__node";
567
+ card.tabIndex = 0;
568
+ card.getBoundingClientRect = (): DOMRect => {
569
+ return inScroll(onScreen(box));
570
+ };
571
+ nodes.appendChild(card);
572
+ return card;
573
+ },
574
+ drawConnection: (box: FlowRect): SVGGElement => {
575
+ const connection: SVGGElement = document.createElementNS(
576
+ "http://www.w3.org/2000/svg",
577
+ "g",
578
+ );
579
+ connection.setAttribute("class", "react-flow__edge");
580
+ connection.setAttribute("tabindex", "0");
581
+ connection.getBoundingClientRect = (): DOMRect => {
582
+ return inScroll(onScreen(box));
583
+ };
584
+ edges.appendChild(connection);
585
+ return connection;
586
+ },
587
+ fitViewButton,
588
+ };
589
+ }
590
+
591
+ /* The page resizes the canvas, then React Flow's ResizeObserver reports it. */
592
+ function canvasResized(flow: FlowElement, width: number, height: number): void {
593
+ flow.layOut(width, height);
594
+ flowChanges({ width, height });
595
+ }
596
+
597
+ /* Elements a test put on the page; afterEach takes them off again. */
598
+ const onPage: Array<Element> = [];
599
+
600
+ function putOnPage(element: Element): void {
601
+ document.body.appendChild(element);
602
+ onPage.push(element);
603
+ }
604
+
605
+ /* db's box on the drawing, as apiAndDb measures it. */
606
+ const DB_BOX: FlowRect = {
607
+ x: 400,
608
+ y: 200,
609
+ width: CARD_WIDTH,
610
+ height: CARD_HEIGHT,
611
+ };
612
+
613
+ /* api's box on the drawing, as apiAndDb measures it. */
614
+ const API_BOX: FlowRect = {
615
+ x: 0,
616
+ y: 0,
617
+ width: CARD_WIDTH,
618
+ height: CARD_HEIGHT,
619
+ };
620
+
621
+ /*
622
+ * The view mapOnPage leaves, standing for where the fit put the drawing: half
623
+ * size, db's card on screen at (340, 270)-(450, 310), well inside the canvas
624
+ * at (40, 120)-(840, 720).
625
+ */
626
+ const FITTED_TRANSFORM: FlowTransform = [100, 50, 0.5];
627
+
628
+ interface MapOnPage extends FlowElement {
629
+ /* The card React Flow draws for db. */
630
+ db: HTMLDivElement;
631
+ }
632
+
633
+ /*
634
+ * The map on a page: React Flow's element in the document, where its cards
635
+ * can take focus, and in the store, holding api and db measured in a canvas
636
+ * of the pane's size, with the viewport ready and the drawing fitted.
637
+ */
638
+ function mapOnPage(): MapOnPage {
639
+ const flow: FlowElement = flowElement(PANE_WIDTH, PANE_HEIGHT);
640
+ putOnPage(flow.domNode);
641
+ flowChanges({ domNode: flow.domNode });
642
+ drawnAndReady();
643
+ flowChanges({ transform: FITTED_TRANSFORM });
644
+ return { ...flow, db: flow.drawCard(DB_BOX) };
645
+ }
646
+
647
+ /*
648
+ * How focus last came to an element: by a key (Tab, or a script focusing
649
+ * after one), or by a pointer pressing it. A browser draws a focus ring for
650
+ * the first and not the second, and :focus-visible matches the focused
651
+ * element only for the first. jsdom parses :focus-visible but never matches
652
+ * it, so the focus tests answer it from this (see answerFocusVisible).
653
+ */
654
+ type FocusCause = "keyboard" | "pointer";
655
+ let focusCameBy: FocusCause = "keyboard";
656
+
657
+ /*
658
+ * Element.matches answering :focus-visible the way a browser does: for the
659
+ * focused element, when focus came to it by a key. Any other selector is
660
+ * jsdom's to match.
661
+ */
662
+ function answerFocusVisible(): SpyInstance<(selectors: string) => boolean> {
663
+ const jsdomMatches: (selectors: string) => boolean =
664
+ Element.prototype.matches;
665
+ return jest.spyOn(Element.prototype, "matches").mockImplementation(function (
666
+ this: Element,
667
+ selectors: string,
668
+ ): boolean {
669
+ if (selectors === ":focus-visible") {
670
+ return focusCameBy === "keyboard" && this === document.activeElement;
671
+ }
672
+ return jsdomMatches.call(this, selectors);
673
+ });
674
+ }
675
+
676
+ /*
677
+ * Keyboard focus moving onto an element, as Tab moves it: jsdom makes it the
678
+ * document's active element and fires focusin, which bubbles up from it.
679
+ */
680
+ function tabOnto(element: HTMLElement | SVGElement): void {
681
+ focusCameBy = "keyboard";
682
+ act(() => {
683
+ element.focus();
684
+ });
685
+ // jsdom focuses only what is on the page and focusable: check it took.
686
+ expect(document.activeElement).toBe(element);
687
+ }
688
+
689
+ /*
690
+ * A pointer pressing an element: the browser focuses it, firing focusin as
691
+ * for Tab, but draws no focus ring.
692
+ */
693
+ function pointerFocuses(element: HTMLElement | SVGElement): void {
694
+ focusCameBy = "pointer";
695
+ act(() => {
696
+ element.focus();
697
+ });
698
+ expect(document.activeElement).toBe(element);
699
+ }
700
+
701
+ /*
702
+ * The window regaining focus, as it does when the user comes back to the
703
+ * tab. The browser then hands focus back to the element that had it (see
704
+ * focusHandedBackTo).
705
+ */
706
+ function windowRegainsFocus(): void {
707
+ act(() => {
708
+ window.dispatchEvent(new FocusEvent("focus"));
709
+ });
710
+ }
711
+
712
+ /*
713
+ * The browser handing focus back to the element that had it before the
714
+ * window lost focus: the element is still the document's active element, and
715
+ * focusin fires on it again.
716
+ */
717
+ function focusHandedBackTo(element: HTMLElement | SVGElement): void {
718
+ expect(document.activeElement).toBe(element);
719
+ act(() => {
720
+ element.dispatchEvent(new FocusEvent("focusin", { bubbles: true }));
721
+ });
722
+ }
723
+
724
+ /*
725
+ * The page's clock, in ms: when each event happened, as the browser stamps
726
+ * it (event.timeStamp). jsdom stamps an event with the wall clock; the focus
727
+ * tests read every event's time off this clock instead (see
728
+ * readEventTimesOffPageClock), so events a test fires one after another
729
+ * happen at the same moment, and a test that means time to pass moves the
730
+ * clock on.
731
+ */
732
+ let pageClockMs: number = 0;
733
+
734
+ function readEventTimesOffPageClock(): SpyInstance<() => number> {
735
+ return jest
736
+ .spyOn(Event.prototype, "timeStamp", "get")
737
+ .mockImplementation((): number => {
738
+ return pageClockMs;
739
+ });
740
+ }
741
+
742
+ /*
743
+ * The browser scrolling React Flow's overflow:hidden element, as it does to
744
+ * bring a focused card or connection into view or to show a match of find
745
+ * in page, and firing its scroll event.
746
+ */
747
+ function browserScrolls(domNode: HTMLElement, left: number, top: number): void {
748
+ act(() => {
749
+ domNode.scrollLeft = left;
750
+ domNode.scrollTop = top;
751
+ domNode.dispatchEvent(new Event("scroll"));
752
+ });
753
+ }
754
+
755
+ /*
756
+ * The browser scrolling React Flow's element without saying so yet: its
757
+ * scroll event comes a frame later (see scrollEventArrives). Chromium makes
758
+ * such a scroll as Tab moves focus onto an element outside the view, before
759
+ * it fires focusin.
760
+ */
761
+ function browserHasScrolled(domNode: HTMLElement, scroll: XYPosition): void {
762
+ domNode.scrollLeft = scroll.x;
763
+ domNode.scrollTop = scroll.y;
764
+ }
765
+
766
+ /* The scroll event, a frame after the browser scrolled React Flow's element. */
767
+ function scrollEventArrives(domNode: HTMLElement): void {
768
+ act(() => {
769
+ domNode.dispatchEvent(new Event("scroll"));
770
+ });
771
+ }
772
+
773
+ /* The page scrolled to show an element, with Element.scrollIntoView. */
774
+ interface PageScroll {
775
+ element: Element;
776
+ options: boolean | ScrollIntoViewOptions | undefined;
777
+ /* Where the element was on screen as the page was scrolled to it. */
778
+ box: DOMRect;
779
+ }
780
+
781
+ /*
782
+ * Gives the page a scrollIntoView that records each call. jsdom has none;
783
+ * the focus tests take this one away again after each test.
784
+ */
785
+ function pageScrollsRecorded(): Array<PageScroll> {
786
+ const scrolls: Array<PageScroll> = [];
787
+ Element.prototype.scrollIntoView = function (
788
+ this: Element,
789
+ options?: boolean | ScrollIntoViewOptions,
790
+ ): void {
791
+ scrolls.push({ element: this, options, box: this.getBoundingClientRect() });
792
+ };
793
+ return scrolls;
794
+ }
795
+
796
+ /*
797
+ * What the guard's event listeners threw. jsdom reports a throw in a
798
+ * listener to the window, as an error event, rather than to whoever fired
799
+ * the event, so it would otherwise go unnoticed.
800
+ */
801
+ const thrownInListeners: Array<unknown> = [];
802
+
803
+ function noteThrown(event: ErrorEvent): void {
804
+ thrownInListeners.push(event.error);
805
+ }
806
+
807
+ /*
808
+ * Every pan the guard asked React Flow for, in screen pixels. React Flow's
809
+ * panBy does nothing for a zero delta, so asking for one is no pan and is
810
+ * left out; and it adds a delta to the view, so -0 reads as 0.
811
+ */
812
+ function pansAskedFor(): Array<XYPosition> {
813
+ const pans: Array<XYPosition> = [];
814
+ for (const call of mockPanBy.mock.calls) {
815
+ const delta: XYPosition = call[0] as XYPosition;
816
+ if (delta.x || delta.y) {
817
+ pans.push({ x: delta.x + 0, y: delta.y + 0 });
818
+ }
819
+ }
820
+ return pans;
821
+ }
822
+
823
+ interface RenderedGuard {
824
+ autoFrame: MutableRefObject<boolean>;
825
+ onInstance: MockFunction;
826
+ onExtentChange: MockFunction;
827
+ onDrawingInViewChange: MockFunction;
828
+ /* Renders the guard again with some props changed, as the map does. */
829
+ rerender: (changes: Partial<ComponentProps>) => void;
830
+ unmount: () => void;
831
+ }
832
+
833
+ interface GuardRenderOptions {
834
+ /*
835
+ * Inside <React.StrictMode>, which mounts the guard's effects, unmounts
836
+ * them and mounts them again, keeping its refs.
837
+ */
838
+ strictMode?: boolean;
839
+ }
840
+
841
+ function renderGuard(
842
+ changes: Partial<ComponentProps> = {},
843
+ options: GuardRenderOptions = {},
844
+ ): RenderedGuard {
845
+ const onInstance: MockFunction = getJestMockFunction();
846
+ const onExtentChange: MockFunction = getJestMockFunction();
847
+ const onDrawingInViewChange: MockFunction = getJestMockFunction();
848
+ /* The map starts on the automatic framing. */
849
+ const autoFrame: MutableRefObject<boolean> = { current: true };
850
+ let props: ComponentProps = {
851
+ drawingKey: DRAWING_KEY,
852
+ fitViewOptions: FIT_VIEW_OPTIONS,
853
+ autoFrame,
854
+ panMargin: PAN_MARGIN,
855
+ onInstance,
856
+ onExtentChange,
857
+ onDrawingInViewChange,
858
+ ...changes,
859
+ };
860
+ const element: (current: ComponentProps) => ReactElement = (
861
+ current: ComponentProps,
862
+ ): ReactElement => {
863
+ const guard: ReactElement = (
864
+ <>
865
+ <FlowViewportGuard {...current} />
866
+ <DrawnViewport />
867
+ </>
868
+ );
869
+ return options.strictMode ? (
870
+ <React.StrictMode>{guard}</React.StrictMode>
871
+ ) : (
872
+ guard
873
+ );
874
+ };
875
+ const rendered: RenderResult = render(element(props));
876
+ return {
877
+ autoFrame: props.autoFrame,
878
+ onInstance,
879
+ onExtentChange,
880
+ onDrawingInViewChange,
881
+ rerender: (next: Partial<ComponentProps>): void => {
882
+ props = { ...props, ...next };
883
+ rendered.rerender(element(props));
884
+ },
885
+ unmount: (): void => {
886
+ rendered.unmount();
887
+ },
888
+ };
889
+ }
890
+
891
+ /* Every answer the guard gave to "is any of the drawing in view?", in order. */
892
+ function inViewReports(guard: RenderedGuard): Array<boolean> {
893
+ return guard.onDrawingInViewChange.mock.calls.map(
894
+ (call: Array<unknown>): boolean => {
895
+ return call[0] as boolean;
896
+ },
897
+ );
898
+ }
899
+
900
+ beforeEach(() => {
901
+ mockFlowStore.state = emptyFlowState();
902
+ mockFlowStore.instance = flowInstance(false);
903
+ mockFlowStore.listeners.clear();
904
+ drawnTransform = mockFlowStore.state.transform;
905
+ mockFitView.mockReset();
906
+ mockFitView.mockReturnValue(true);
907
+ mockTranslateBy.mockReset();
908
+ mockSetViewport.mockReset();
909
+ mockSetViewport.mockImplementation((viewport: Viewport): void => {
910
+ mockFlowStore.set({ transform: [viewport.x, viewport.y, viewport.zoom] });
911
+ });
912
+ panByMoves = true;
913
+ mockPanBy.mockReset();
914
+ mockPanBy.mockImplementation(panViewBy);
915
+ });
916
+
917
+ afterEach(() => {
918
+ cleanup();
919
+ for (const element of onPage.splice(0)) {
920
+ element.remove();
921
+ }
922
+ /*
923
+ * Whatever a test did, the guard never moved the view with setViewport,
924
+ * which does not keep it inside the pan extent: the next drag would jump.
925
+ */
926
+ expect(mockSetViewport).not.toHaveBeenCalled();
927
+ });
928
+
929
+ describe("framing the drawing", () => {
930
+ interface Holdup {
931
+ name: string;
932
+ /* Brings React Flow to everything a fit needs but one thing. */
933
+ arrange: () => void;
934
+ /* Supplies that thing, the way React Flow would. */
935
+ lift: () => void;
936
+ }
937
+
938
+ const HOLDUPS: Array<Holdup> = [
939
+ {
940
+ name: "React Flow still holds the previous drawing",
941
+ arrange: (): void => {
942
+ drawnAndReady(
943
+ holding(measuredCard("web", -400, 0), measuredCard("api", 0, 0)),
944
+ );
945
+ },
946
+ lift: (): void => {
947
+ flowChanges({ nodeInternals: apiAndDb() });
948
+ },
949
+ },
950
+ {
951
+ name: "a card is not measured yet",
952
+ arrange: (): void => {
953
+ drawnAndReady(
954
+ holding(measuredCard("api", 0, 0), unmeasuredCard("db", 400, 200)),
955
+ );
956
+ },
957
+ lift: (): void => {
958
+ flowChanges({ nodeInternals: apiAndDb() });
959
+ },
960
+ },
961
+ {
962
+ name: "the pane has no size yet",
963
+ arrange: (): void => {
964
+ flowChanges({ nodeInternals: apiAndDb() });
965
+ viewportReady();
966
+ },
967
+ lift: (): void => {
968
+ flowChanges({ width: PANE_WIDTH, height: PANE_HEIGHT });
969
+ },
970
+ },
971
+ {
972
+ name: "the pane has no height yet",
973
+ arrange: (): void => {
974
+ flowChanges({ nodeInternals: apiAndDb(), width: PANE_WIDTH });
975
+ viewportReady();
976
+ },
977
+ lift: (): void => {
978
+ flowChanges({ height: PANE_HEIGHT });
979
+ },
980
+ },
981
+ {
982
+ name: "the viewport is not initialized",
983
+ arrange: (): void => {
984
+ flowChanges({
985
+ nodeInternals: apiAndDb(),
986
+ width: PANE_WIDTH,
987
+ height: PANE_HEIGHT,
988
+ });
989
+ },
990
+ lift: (): void => {
991
+ viewportReady();
992
+ },
993
+ },
994
+ ];
995
+
996
+ test.each(HOLDUPS)(
997
+ "does not fit while $name, then fits once, with the options it was given",
998
+ (holdup: Holdup) => {
999
+ const guard: RenderedGuard = renderGuard();
1000
+ guard.autoFrame.current = false;
1001
+ holdup.arrange();
1002
+ expect(mockFitView).not.toHaveBeenCalled();
1003
+ expect(guard.autoFrame.current).toBe(false);
1004
+
1005
+ holdup.lift();
1006
+ expect(mockFitView).toHaveBeenCalledTimes(1);
1007
+ expect(mockFitView).toHaveBeenCalledWith(FIT_VIEW_OPTIONS);
1008
+ // A fit makes the view the automatic framing.
1009
+ expect(guard.autoFrame.current).toBe(true);
1010
+ },
1011
+ );
1012
+
1013
+ test("never fits a drawing with no cards in it", () => {
1014
+ renderGuard({ drawingKey: "" });
1015
+ drawnAndReady(holding());
1016
+ flowChanges({ width: 1000 });
1017
+ expect(mockFitView).not.toHaveBeenCalled();
1018
+ });
1019
+
1020
+ test("fits a drawing once: panning, re-measuring and re-rendering leave the view alone", () => {
1021
+ const guard: RenderedGuard = renderGuard();
1022
+ drawnAndReady();
1023
+ expect(mockFitView).toHaveBeenCalledTimes(1);
1024
+
1025
+ // The user pans and zooms.
1026
+ flowChanges({ transform: [-120, 40, 0.8] });
1027
+ // React Flow takes the map's rebuilt cards, their sizes carried over.
1028
+ flowChanges({ nodeInternals: apiAndDb() });
1029
+ // React Flow measures a card at a new size.
1030
+ flowChanges({
1031
+ nodeInternals: holding(
1032
+ measuredCard("api", 0, 0, 240, 96),
1033
+ measuredCard("db", 400, 200),
1034
+ ),
1035
+ });
1036
+ /*
1037
+ * The #4117 path: the same drawing sent back to unmeasured by a new nodes
1038
+ * array, then measured again. It is still the drawing on screen.
1039
+ */
1040
+ flowChanges({
1041
+ nodeInternals: holding(
1042
+ unmeasuredCard("api", 0, 0),
1043
+ unmeasuredCard("db", 400, 200),
1044
+ ),
1045
+ });
1046
+ flowChanges({ nodeInternals: apiAndDb() });
1047
+ // The map renders the guard again with the same drawing.
1048
+ guard.rerender({});
1049
+
1050
+ expect(mockFitView).toHaveBeenCalledTimes(1);
1051
+ expect(guard.autoFrame.current).toBe(true);
1052
+ // Nor is the view nudged back inside the pan extent: nothing was resized.
1053
+ expect(mockTranslateBy).not.toHaveBeenCalled();
1054
+ expect(mockSetViewport).not.toHaveBeenCalled();
1055
+ });
1056
+
1057
+ test("a fit React Flow refuses counts for nothing: it is tried again on the next change, until one lands", () => {
1058
+ mockFitView.mockReturnValue(false);
1059
+ const guard: RenderedGuard = renderGuard();
1060
+ guard.autoFrame.current = false;
1061
+ drawnAndReady();
1062
+ expect(mockFitView).toHaveBeenCalledTimes(1);
1063
+ // Nothing recorded: the view has not become the automatic framing...
1064
+ expect(guard.autoFrame.current).toBe(false);
1065
+
1066
+ /*
1067
+ * ...and the drawing still counts as never framed, so a resized pane
1068
+ * tries again even though the view is not the automatic framing.
1069
+ */
1070
+ flowChanges({ width: 1000 });
1071
+ expect(mockFitView).toHaveBeenCalledTimes(2);
1072
+ expect(guard.autoFrame.current).toBe(false);
1073
+
1074
+ // React Flow re-measuring the drawing tries again, and this time it lands.
1075
+ mockFitView.mockReturnValue(true);
1076
+ flowChanges({
1077
+ nodeInternals: holding(
1078
+ measuredCard("api", 0, 0),
1079
+ unmeasuredCard("db", 400, 200),
1080
+ ),
1081
+ });
1082
+ expect(mockFitView).toHaveBeenCalledTimes(2);
1083
+ flowChanges({ nodeInternals: apiAndDb() });
1084
+ expect(mockFitView).toHaveBeenCalledTimes(3);
1085
+ expect(mockFitView).toHaveBeenLastCalledWith(FIT_VIEW_OPTIONS);
1086
+ expect(guard.autoFrame.current).toBe(true);
1087
+
1088
+ // That one is recorded: the drawing measured again in the same pane is left alone.
1089
+ flowChanges({
1090
+ nodeInternals: holding(
1091
+ measuredCard("api", 0, 0),
1092
+ unmeasuredCard("db", 400, 200),
1093
+ ),
1094
+ });
1095
+ flowChanges({ nodeInternals: apiAndDb() });
1096
+ expect(mockFitView).toHaveBeenCalledTimes(3);
1097
+ });
1098
+
1099
+ test("a refused fit is owed: a re-measure that leaves every card measured tries it again, and nothing is recorded until it lands", () => {
1100
+ mockFitView.mockReturnValue(false);
1101
+ const guard: RenderedGuard = renderGuard();
1102
+ guard.autoFrame.current = false;
1103
+ drawnAndReady();
1104
+ expect(mockFitView).toHaveBeenCalledTimes(1);
1105
+
1106
+ // React Flow measures a card at a new size: the drawing is never unmeasured.
1107
+ flowChanges({
1108
+ nodeInternals: holding(
1109
+ measuredCard("api", 0, 0, 240, 96),
1110
+ measuredCard("db", 400, 200),
1111
+ ),
1112
+ });
1113
+ expect(mockFitView).toHaveBeenCalledTimes(2);
1114
+ // React Flow takes the map's rebuilt cards, their sizes carried over.
1115
+ flowChanges({
1116
+ nodeInternals: holding(
1117
+ measuredCard("api", 0, 0, 240, 96),
1118
+ measuredCard("db", 400, 200),
1119
+ ),
1120
+ });
1121
+ expect(mockFitView).toHaveBeenCalledTimes(3);
1122
+ expect(mockFitView).toHaveBeenLastCalledWith(FIT_VIEW_OPTIONS);
1123
+
1124
+ /*
1125
+ * Nothing is recorded: the view is not the automatic framing, and the
1126
+ * owed fit still comes before the user's view - a resize tries it
1127
+ * rather than keeping the view inside the pan extent.
1128
+ */
1129
+ expect(guard.autoFrame.current).toBe(false);
1130
+ flowChanges({ width: 1000 });
1131
+ expect(mockFitView).toHaveBeenCalledTimes(4);
1132
+ expect(mockTranslateBy).not.toHaveBeenCalled();
1133
+ expect(guard.autoFrame.current).toBe(false);
1134
+
1135
+ // The next re-measure tries it again, and it lands.
1136
+ mockFitView.mockReturnValue(true);
1137
+ flowChanges({ nodeInternals: apiAndDb() });
1138
+ expect(mockFitView).toHaveBeenCalledTimes(5);
1139
+ expect(mockFitView).toHaveBeenLastCalledWith(FIT_VIEW_OPTIONS);
1140
+ expect(guard.autoFrame.current).toBe(true);
1141
+
1142
+ // Recorded: nothing more is owed.
1143
+ flowChanges({ nodeInternals: apiAndDb() });
1144
+ flowChanges({ transform: [-40, 0, 1] });
1145
+ guard.rerender({});
1146
+ expect(mockFitView).toHaveBeenCalledTimes(5);
1147
+ });
1148
+
1149
+ test("a re-fit React Flow refuses after a resize is still owed once the size has settled: the next re-measure lands it", () => {
1150
+ const guard: RenderedGuard = renderGuard();
1151
+ drawnAndReady();
1152
+ expect(mockFitView).toHaveBeenCalledTimes(1);
1153
+
1154
+ // A narrower window while the view is the automatic framing: refused.
1155
+ mockFitView.mockReturnValue(false);
1156
+ flowChanges({ width: 640 });
1157
+ expect(mockFitView).toHaveBeenCalledTimes(2);
1158
+
1159
+ /*
1160
+ * The pane has not changed size since, and the drawing was fitted
1161
+ * before; the fit is still owed all the same.
1162
+ */
1163
+ mockFitView.mockReturnValue(true);
1164
+ flowChanges({ nodeInternals: apiAndDb() });
1165
+ expect(mockFitView).toHaveBeenCalledTimes(3);
1166
+ expect(mockFitView).toHaveBeenLastCalledWith(FIT_VIEW_OPTIONS);
1167
+ expect(guard.autoFrame.current).toBe(true);
1168
+
1169
+ // Landed, so nothing more is owed.
1170
+ flowChanges({ nodeInternals: apiAndDb() });
1171
+ expect(mockFitView).toHaveBeenCalledTimes(3);
1172
+ });
1173
+
1174
+ test("a re-fit still owed from a resize is dropped once the user has taken the view: it is theirs to keep", () => {
1175
+ const guard: RenderedGuard = renderGuard();
1176
+ drawnAndReady();
1177
+ expect(mockFitView).toHaveBeenCalledTimes(1);
1178
+
1179
+ // A narrower window while the view is the automatic framing: the re-fit is refused.
1180
+ mockFitView.mockReturnValue(false);
1181
+ flowChanges({ width: 640 });
1182
+ expect(mockFitView).toHaveBeenCalledTimes(2);
1183
+ mockFitView.mockReturnValue(true);
1184
+
1185
+ // Before it lands, the user pans and the map clears autoFrame; then React Flow re-measures the drawing.
1186
+ guard.autoFrame.current = false;
1187
+ flowChanges({ transform: [-120, 0, 1] });
1188
+ flowChanges({ nodeInternals: apiAndDb() });
1189
+ expect(mockFitView).toHaveBeenCalledTimes(2);
1190
+ expect(guard.autoFrame.current).toBe(false);
1191
+
1192
+ // Their view is kept through the next resize too, inside the pan extent.
1193
+ flowChanges({ width: 560 });
1194
+ expect(mockTranslateBy).toHaveBeenCalledTimes(1);
1195
+ expect(mockFitView).toHaveBeenCalledTimes(2);
1196
+
1197
+ /*
1198
+ * Dropped, not put off: once Fit to screen (the map's own fit) makes the
1199
+ * view the automatic framing again, a re-measure fits nothing.
1200
+ */
1201
+ guard.autoFrame.current = true;
1202
+ flowChanges({ nodeInternals: apiAndDb() });
1203
+ expect(mockFitView).toHaveBeenCalledTimes(2);
1204
+ expect(guard.autoFrame.current).toBe(true);
1205
+ });
1206
+
1207
+ test("a new drawing's fit, refused, is still owed after the user takes the view: the next re-measure lands it", () => {
1208
+ const guard: RenderedGuard = renderGuard();
1209
+ drawnAndReady();
1210
+ expect(mockFitView).toHaveBeenCalledTimes(1);
1211
+
1212
+ // The map asks for another drawing, and React Flow refuses its fit.
1213
+ mockFitView.mockReturnValue(false);
1214
+ guard.rerender({ drawingKey: "api|cache" });
1215
+ flowChanges({
1216
+ nodeInternals: holding(
1217
+ measuredCard("api", 0, 0),
1218
+ measuredCard("cache", 400, 0),
1219
+ ),
1220
+ });
1221
+ expect(mockFitView).toHaveBeenCalledTimes(2);
1222
+ mockFitView.mockReturnValue(true);
1223
+
1224
+ // The user pans before it lands, and the map clears autoFrame.
1225
+ guard.autoFrame.current = false;
1226
+ flowChanges({ transform: [-120, 0, 1] });
1227
+ expect(mockFitView).toHaveBeenCalledTimes(2);
1228
+
1229
+ // A new drawing is framed whoever has the view: React Flow re-measuring it tries again.
1230
+ flowChanges({
1231
+ nodeInternals: holding(
1232
+ measuredCard("api", 0, 0),
1233
+ measuredCard("cache", 400, 0),
1234
+ ),
1235
+ });
1236
+ expect(mockFitView).toHaveBeenCalledTimes(3);
1237
+ expect(mockFitView).toHaveBeenLastCalledWith(FIT_VIEW_OPTIONS);
1238
+ expect(guard.autoFrame.current).toBe(true);
1239
+ });
1240
+
1241
+ test("a new drawing is fitted even after the user moved the view, which is the automatic framing again", () => {
1242
+ const guard: RenderedGuard = renderGuard();
1243
+ drawnAndReady();
1244
+ expect(mockFitView).toHaveBeenCalledTimes(1);
1245
+
1246
+ // The user pans and zooms; the map clears autoFrame.
1247
+ guard.autoFrame.current = false;
1248
+ flowChanges({ transform: [-300, -120, 0.6] });
1249
+
1250
+ // The map asks for another drawing. React Flow still holds the old one.
1251
+ guard.rerender({ drawingKey: "api|cache" });
1252
+ expect(mockFitView).toHaveBeenCalledTimes(1);
1253
+
1254
+ // React Flow takes it; the new card is measured a frame later.
1255
+ flowChanges({
1256
+ nodeInternals: holding(
1257
+ measuredCard("api", 0, 0),
1258
+ unmeasuredCard("cache", 400, 0),
1259
+ ),
1260
+ });
1261
+ expect(mockFitView).toHaveBeenCalledTimes(1);
1262
+ flowChanges({
1263
+ nodeInternals: holding(
1264
+ measuredCard("api", 0, 0),
1265
+ measuredCard("cache", 400, 0),
1266
+ ),
1267
+ });
1268
+ expect(mockFitView).toHaveBeenCalledTimes(2);
1269
+ expect(mockFitView).toHaveBeenLastCalledWith(FIT_VIEW_OPTIONS);
1270
+ expect(guard.autoFrame.current).toBe(true);
1271
+
1272
+ // Going back to the first drawing is a change too.
1273
+ guard.autoFrame.current = false;
1274
+ guard.rerender({ drawingKey: DRAWING_KEY });
1275
+ flowChanges({ nodeInternals: apiAndDb() });
1276
+ expect(mockFitView).toHaveBeenCalledTimes(3);
1277
+ expect(guard.autoFrame.current).toBe(true);
1278
+ });
1279
+
1280
+ test("a resized pane is fitted again while the view is the automatic framing, and left alone once the user moved it", () => {
1281
+ const guard: RenderedGuard = renderGuard();
1282
+ drawnAndReady();
1283
+ expect(mockFitView).toHaveBeenCalledTimes(1);
1284
+
1285
+ // A narrower window: the drawing is framed again.
1286
+ flowChanges({ width: 640 });
1287
+ expect(mockFitView).toHaveBeenCalledTimes(2);
1288
+ // So is a taller canvas.
1289
+ flowChanges({ height: 700 });
1290
+ expect(mockFitView).toHaveBeenCalledTimes(3);
1291
+
1292
+ // The user zooms in; a resize now keeps their view.
1293
+ guard.autoFrame.current = false;
1294
+ flowChanges({ width: 720, height: 540 });
1295
+ expect(mockFitView).toHaveBeenCalledTimes(3);
1296
+ expect(guard.autoFrame.current).toBe(false);
1297
+
1298
+ // Fit to screen makes the view the automatic framing again: a later resize fits.
1299
+ guard.autoFrame.current = true;
1300
+ flowChanges({ width: 900, height: 500 });
1301
+ expect(mockFitView).toHaveBeenCalledTimes(4);
1302
+ expect(mockFitView).toHaveBeenLastCalledWith(FIT_VIEW_OPTIONS);
1303
+ expect(guard.autoFrame.current).toBe(true);
1304
+ });
1305
+
1306
+ test("a resize is judged against the last pane size the guard saw: after Fit to screen, going back to the size of its own last fit is fitted", () => {
1307
+ const guard: RenderedGuard = renderGuard();
1308
+ drawnAndReady();
1309
+ // Fitted 800 wide.
1310
+ expect(mockFitView).toHaveBeenCalledTimes(1);
1311
+
1312
+ // The user pans, and the map clears autoFrame. A wider window keeps their view.
1313
+ guard.autoFrame.current = false;
1314
+ flowChanges({ transform: [-200, 0, 1] });
1315
+ flowChanges({ width: 1000 });
1316
+ expect(mockFitView).toHaveBeenCalledTimes(1);
1317
+ expect(mockTranslateBy).toHaveBeenCalledTimes(1);
1318
+
1319
+ // Fit to screen: the map fits the view itself, and sets autoFrame.
1320
+ guard.autoFrame.current = true;
1321
+ flowChanges({ transform: [100, 120, 1] });
1322
+
1323
+ // Back to 800 wide, the width of the guard's last fit: a resize all the same.
1324
+ flowChanges({ width: PANE_WIDTH });
1325
+ expect(mockFitView).toHaveBeenCalledTimes(2);
1326
+ expect(mockFitView).toHaveBeenLastCalledWith(FIT_VIEW_OPTIONS);
1327
+ expect(guard.autoFrame.current).toBe(true);
1328
+ // A fit, not the user's view kept.
1329
+ expect(mockTranslateBy).toHaveBeenCalledTimes(1);
1330
+ });
1331
+
1332
+ test("a resize while the user has the view re-applies the pan extent then and there, and fits nothing", () => {
1333
+ const guard: RenderedGuard = renderGuard();
1334
+ drawnAndReady();
1335
+ expect(mockFitView).toHaveBeenCalledTimes(1);
1336
+
1337
+ // While the view is the automatic framing, a resize fits the drawing again instead.
1338
+ flowChanges({ width: 640 });
1339
+ expect(mockFitView).toHaveBeenCalledTimes(2);
1340
+ expect(mockTranslateBy).not.toHaveBeenCalled();
1341
+
1342
+ // The user zooms in and pans; then the window narrows and the canvas shortens.
1343
+ guard.autoFrame.current = false;
1344
+ flowChanges({ transform: [-300, -100, 1.1] });
1345
+ flowChanges({ width: 560, height: 540 });
1346
+ // d3-zoom applies the extent now, moving nothing the extent allows.
1347
+ expect(mockTranslateBy.mock.calls).toEqual([[D3_SELECTION, 0, 0]]);
1348
+ expect(mockTranslateBy.mock.calls[0]?.[0]).toBe(D3_SELECTION);
1349
+ expect(mockFitView).toHaveBeenCalledTimes(2);
1350
+ expect(guard.autoFrame.current).toBe(false);
1351
+
1352
+ // Only a resize does: re-measured cards, a pan, a re-render and the same size again do not.
1353
+ flowChanges({ nodeInternals: apiAndDb() });
1354
+ flowChanges({ transform: [-320, -100, 1.1] });
1355
+ guard.rerender({});
1356
+ flowChanges({ width: 560, height: 540 });
1357
+ expect(mockTranslateBy).toHaveBeenCalledTimes(1);
1358
+ expect(mockFitView).toHaveBeenCalledTimes(2);
1359
+ });
1360
+
1361
+ test("does not fit while React Flow's record of the pane size is not the size the page gives it, then fits once", () => {
1362
+ const guard: RenderedGuard = renderGuard();
1363
+ guard.autoFrame.current = false;
1364
+ // The page has laid the canvas out 400 high; React Flow still has it at 600.
1365
+ const flow: FlowElement = flowElement(1000, 400);
1366
+ flowChanges({
1367
+ domNode: flow.domNode,
1368
+ nodeInternals: apiAndDb(),
1369
+ width: 1000,
1370
+ height: 600,
1371
+ });
1372
+ viewportReady();
1373
+ expect(mockFitView).not.toHaveBeenCalled();
1374
+ // Other changes do not get a fit in first.
1375
+ flowChanges({ nodeInternals: apiAndDb() });
1376
+ expect(mockFitView).not.toHaveBeenCalled();
1377
+ expect(guard.autoFrame.current).toBe(false);
1378
+
1379
+ // React Flow's ResizeObserver catches up: one fit, in the canvas on the page.
1380
+ flowChanges({ height: 400 });
1381
+ expect(mockFitView).toHaveBeenCalledTimes(1);
1382
+ expect(mockFitView).toHaveBeenCalledWith(FIT_VIEW_OPTIONS);
1383
+ expect(guard.autoFrame.current).toBe(true);
1384
+
1385
+ // Exactly one: React Flow learning the size is not a resize to fit again for.
1386
+ flowChanges({ nodeInternals: apiAndDb() });
1387
+ guard.rerender({});
1388
+ expect(mockFitView).toHaveBeenCalledTimes(1);
1389
+ });
1390
+
1391
+ test("a canvas resized again before React Flow has seen the first resize is fitted once, for the size it ends at", () => {
1392
+ const flow: FlowElement = flowElement(PANE_WIDTH, PANE_HEIGHT);
1393
+ const guard: RenderedGuard = renderGuard();
1394
+ flowChanges({ domNode: flow.domNode });
1395
+ drawnAndReady();
1396
+ expect(mockFitView).toHaveBeenCalledTimes(1);
1397
+
1398
+ /*
1399
+ * The window narrows twice in a frame: React Flow records the first
1400
+ * width while the page already has the canvas at the second.
1401
+ */
1402
+ flow.layOut(640, PANE_HEIGHT);
1403
+ flowChanges({ width: 700 });
1404
+ expect(mockFitView).toHaveBeenCalledTimes(1);
1405
+ flowChanges({ width: 640 });
1406
+ expect(mockFitView).toHaveBeenCalledTimes(2);
1407
+ expect(mockFitView).toHaveBeenLastCalledWith(FIT_VIEW_OPTIONS);
1408
+
1409
+ // Once the user has the view, the extent waits for the size the same way.
1410
+ guard.autoFrame.current = false;
1411
+ flow.layOut(560, 500);
1412
+ flowChanges({ width: 600, height: 500 });
1413
+ expect(mockTranslateBy).not.toHaveBeenCalled();
1414
+ flowChanges({ width: 560 });
1415
+ expect(mockTranslateBy).toHaveBeenCalledTimes(1);
1416
+ expect(mockFitView).toHaveBeenCalledTimes(2);
1417
+ });
1418
+
1419
+ test("a canvas hidden and shown again at its size is not a resize: React Flow's 500 x 500 meanwhile is never taken for one", () => {
1420
+ const flow: FlowElement = flowElement(PANE_WIDTH, PANE_HEIGHT);
1421
+ const guard: RenderedGuard = renderGuard();
1422
+ flowChanges({ domNode: flow.domNode });
1423
+ drawnAndReady();
1424
+ expect(mockFitView).toHaveBeenCalledTimes(1);
1425
+
1426
+ // Hidden: the page gives the canvas no size, and React Flow falls back to 500 x 500.
1427
+ flow.layOut(0, 0);
1428
+ flowChanges({ width: 500, height: 500 });
1429
+ // Shown again as it was: the fitted view still fits it.
1430
+ canvasResized(flow, PANE_WIDTH, PANE_HEIGHT);
1431
+ expect(mockFitView).toHaveBeenCalledTimes(1);
1432
+
1433
+ // Nor, once the user has the view, is there an extent to re-apply.
1434
+ guard.autoFrame.current = false;
1435
+ flow.layOut(0, 0);
1436
+ flowChanges({ width: 500, height: 500 });
1437
+ canvasResized(flow, PANE_WIDTH, PANE_HEIGHT);
1438
+ expect(mockTranslateBy).not.toHaveBeenCalled();
1439
+ expect(mockFitView).toHaveBeenCalledTimes(1);
1440
+ expect(guard.autoFrame.current).toBe(false);
1441
+ });
1442
+
1443
+ test("a new drawing that changes the canvas height is fitted once, in the canvas it ends up in", () => {
1444
+ const flow: FlowElement = flowElement(PANE_WIDTH, PANE_HEIGHT);
1445
+ const guard: RenderedGuard = renderGuard();
1446
+ flowChanges({ domNode: flow.domNode });
1447
+ drawnAndReady();
1448
+ expect(mockFitView).toHaveBeenCalledTimes(1);
1449
+
1450
+ // The map asks for a smaller drawing, and the page gives it a shorter canvas.
1451
+ guard.rerender({ drawingKey: "api" });
1452
+ flow.layOut(PANE_WIDTH, 400);
1453
+ // React Flow takes the drawing and measures it before it has seen the new size.
1454
+ flowChanges({ nodeInternals: holding(measuredCard("api", 0, 0)) });
1455
+ expect(mockFitView).toHaveBeenCalledTimes(1);
1456
+
1457
+ // Then it has: the one fit, for the new drawing in the new canvas.
1458
+ flowChanges({ height: 400 });
1459
+ expect(mockFitView).toHaveBeenCalledTimes(2);
1460
+ expect(mockFitView).toHaveBeenLastCalledWith(FIT_VIEW_OPTIONS);
1461
+
1462
+ flowChanges({ nodeInternals: holding(measuredCard("api", 0, 0)) });
1463
+ expect(mockFitView).toHaveBeenCalledTimes(2);
1464
+ });
1465
+
1466
+ test("does not fit a canvas the page has not laid out, for all React Flow's fallback says it is 500 x 500", () => {
1467
+ const guard: RenderedGuard = renderGuard();
1468
+ guard.autoFrame.current = false;
1469
+ // A canvas with no size on the page: React Flow records 500 x 500.
1470
+ const flow: FlowElement = flowElement(0, 0);
1471
+ flowChanges({
1472
+ domNode: flow.domNode,
1473
+ nodeInternals: apiAndDb(),
1474
+ width: 500,
1475
+ height: 500,
1476
+ });
1477
+ viewportReady();
1478
+ flowChanges({ nodeInternals: apiAndDb() });
1479
+ expect(mockFitView).not.toHaveBeenCalled();
1480
+ expect(guard.autoFrame.current).toBe(false);
1481
+
1482
+ // Laid out, and measured by React Flow: now there is a canvas to fit.
1483
+ canvasResized(flow, PANE_WIDTH, PANE_HEIGHT);
1484
+ expect(mockFitView).toHaveBeenCalledTimes(1);
1485
+ expect(guard.autoFrame.current).toBe(true);
1486
+ });
1487
+ });
1488
+
1489
+ describe("following keyboard focus", () => {
1490
+ let matches: SpyInstance<(selectors: string) => boolean>;
1491
+ let eventTime: SpyInstance<() => number>;
1492
+
1493
+ beforeEach(() => {
1494
+ focusCameBy = "keyboard";
1495
+ matches = answerFocusVisible();
1496
+ pageClockMs = 0;
1497
+ eventTime = readEventTimesOffPageClock();
1498
+ thrownInListeners.splice(0);
1499
+ window.addEventListener("error", noteThrown);
1500
+ });
1501
+
1502
+ afterEach(() => {
1503
+ matches.mockRestore();
1504
+ eventTime.mockRestore();
1505
+ Reflect.deleteProperty(Element.prototype, "scrollIntoView");
1506
+ window.removeEventListener("error", noteThrown);
1507
+ // Whatever a test did, nothing the guard does on focus or scroll threw.
1508
+ expect(thrownInListeners).toEqual([]);
1509
+ });
1510
+
1511
+ /*
1512
+ * At half size db's card is 110 x 40 on screen, its top-left corner at
1513
+ * (240 + x, 220 + y) for a view at [x, y, 0.5]. The canvas spans
1514
+ * (40, 120)-(840, 720), and a focused card is brought 24 px inside it:
1515
+ * within (64, 144)-(816, 696).
1516
+ */
1517
+ interface OffCanvas {
1518
+ name: string;
1519
+ /* The view when db takes focus: the automatic framing of a larger map. */
1520
+ transform: FlowTransform;
1521
+ /* The least pan that brings db 24 px inside, in screen pixels. */
1522
+ pan: XYPosition;
1523
+ }
1524
+
1525
+ const OFF_CANVAS: Array<OffCanvas> = [
1526
+ {
1527
+ // db at (340, 800)-(450, 840): its bottom comes up to 696.
1528
+ name: "below the canvas",
1529
+ transform: [100, 580, 0.5],
1530
+ pan: { x: 0, y: -144 },
1531
+ },
1532
+ {
1533
+ // db at (900, 270)-(1010, 310): its right comes in to 816.
1534
+ name: "right of the canvas",
1535
+ transform: [660, 50, 0.5],
1536
+ pan: { x: -194, y: 0 },
1537
+ },
1538
+ {
1539
+ // db at (340, 20)-(450, 60): its top comes down to 144.
1540
+ name: "above the canvas",
1541
+ transform: [100, -200, 0.5],
1542
+ pan: { x: 0, y: 124 },
1543
+ },
1544
+ {
1545
+ // db at (-150, 270)-(-40, 310): its left comes in to 64.
1546
+ name: "left of the canvas",
1547
+ transform: [-390, 50, 0.5],
1548
+ pan: { x: 214, y: 0 },
1549
+ },
1550
+ {
1551
+ // db at (340, 700)-(450, 740).
1552
+ name: "cut off by the canvas's bottom edge",
1553
+ transform: [100, 480, 0.5],
1554
+ pan: { x: 0, y: -44 },
1555
+ },
1556
+ {
1557
+ // db at (900, 800)-(1010, 840): both axes in one pan.
1558
+ name: "past the canvas's bottom-right corner",
1559
+ transform: [660, 580, 0.5],
1560
+ pan: { x: -194, y: -144 },
1561
+ },
1562
+ {
1563
+ // db at (707, 657)-(817, 697).
1564
+ name: "a pixel short of 24 px inside the bottom-right corner",
1565
+ transform: [467, 437, 0.5],
1566
+ pan: { x: -1, y: -1 },
1567
+ },
1568
+ ];
1569
+
1570
+ test.each(OFF_CANVAS)(
1571
+ "focus on a card $name pans the least that brings it 24 px inside the canvas, and the view is the user's from then on",
1572
+ (offCanvas: OffCanvas) => {
1573
+ const guard: RenderedGuard = renderGuard();
1574
+ const map: MapOnPage = mapOnPage();
1575
+ flowChanges({ transform: offCanvas.transform });
1576
+ expect(guard.autoFrame.current).toBe(true);
1577
+
1578
+ tabOnto(map.db);
1579
+ // One pan, in screen pixels whatever the zoom.
1580
+ expect(pansAskedFor()).toEqual([offCanvas.pan]);
1581
+ const [x, y, zoom] = offCanvas.transform;
1582
+ expect(mockFlowStore.state.transform).toEqual([
1583
+ x + offCanvas.pan.x,
1584
+ y + offCanvas.pan.y,
1585
+ zoom,
1586
+ ]);
1587
+ // The view moved: it is no longer the automatic framing.
1588
+ expect(guard.autoFrame.current).toBe(false);
1589
+ expect(mockFitView).toHaveBeenCalledTimes(1);
1590
+ },
1591
+ );
1592
+
1593
+ interface InView {
1594
+ name: string;
1595
+ transform: FlowTransform;
1596
+ }
1597
+
1598
+ const IN_VIEW: Array<InView> = [
1599
+ // db at (340, 270)-(450, 310).
1600
+ { name: "in the middle of the canvas", transform: FITTED_TRANSFORM },
1601
+ // db at (64, 144)-(174, 184).
1602
+ {
1603
+ name: "exactly 24 px inside the top-left corner",
1604
+ transform: [-176, -76, 0.5],
1605
+ },
1606
+ // db at (706, 656)-(816, 696).
1607
+ {
1608
+ name: "exactly 24 px inside the bottom-right corner",
1609
+ transform: [466, 436, 0.5],
1610
+ },
1611
+ ];
1612
+
1613
+ test.each(IN_VIEW)(
1614
+ "focus on a card $name moves nothing, and the view stays the automatic framing",
1615
+ (inView: InView) => {
1616
+ const guard: RenderedGuard = renderGuard();
1617
+ const map: MapOnPage = mapOnPage();
1618
+ flowChanges({ transform: inView.transform });
1619
+
1620
+ tabOnto(map.db);
1621
+ expect(pansAskedFor()).toEqual([]);
1622
+ expect(mockFlowStore.state.transform).toEqual(inView.transform);
1623
+ expect(guard.autoFrame.current).toBe(true);
1624
+ },
1625
+ );
1626
+
1627
+ test("focus on a connection is followed too, and one longer than the canvas only has to show in part", () => {
1628
+ const guard: RenderedGuard = renderGuard();
1629
+ const map: MapOnPage = mapOnPage();
1630
+ /*
1631
+ * A connection across the whole drawing and more, below the canvas: on
1632
+ * screen at (-360, 820)-(1140, 850), wider than the canvas.
1633
+ */
1634
+ const connection: SVGGElement = map.drawConnection({
1635
+ x: -1000,
1636
+ y: 1300,
1637
+ width: 3000,
1638
+ height: 60,
1639
+ });
1640
+
1641
+ tabOnto(connection);
1642
+ // Up, to 24 px inside the bottom edge; across, it already shows.
1643
+ expect(pansAskedFor()).toEqual([{ x: 0, y: -154 }]);
1644
+ expect(mockFlowStore.state.transform).toEqual([100, -104, 0.5]);
1645
+ expect(guard.autoFrame.current).toBe(false);
1646
+ });
1647
+
1648
+ test("focus on the map's controls moves nothing: React Flow keeps them on the canvas, outside its renderer", () => {
1649
+ const guard: RenderedGuard = renderGuard();
1650
+ const map: MapOnPage = mapOnPage();
1651
+
1652
+ // The Fit to screen button, at (55, 678)-(81, 705): nearer the edges than a focused card is left.
1653
+ tabOnto(map.fitViewButton);
1654
+ expect(pansAskedFor()).toEqual([]);
1655
+ expect(mockFlowStore.state.transform).toEqual(FITTED_TRANSFORM);
1656
+ expect(guard.autoFrame.current).toBe(true);
1657
+ });
1658
+
1659
+ /*
1660
+ * Tab onto a card outside the view the way Chromium goes about it: it
1661
+ * scrolls React Flow's element to show the card, centring it, and only
1662
+ * then moves focus, firing focusin; the scroll's own event comes a frame
1663
+ * later.
1664
+ */
1665
+ interface ChromiumTab {
1666
+ name: string;
1667
+ /* The view when db takes focus: the automatic framing of a larger map. */
1668
+ transform: FlowTransform;
1669
+ /* How far Chromium scrolls React Flow's element to centre db. */
1670
+ scroll: XYPosition;
1671
+ /* db's top-left corner on screen through that scroll. */
1672
+ shows: XYPosition;
1673
+ /* The one pan: the least that brings db 24 px inside. */
1674
+ pan: XYPosition;
1675
+ /* db's top-left corner on screen once it is followed. */
1676
+ lands: XYPosition;
1677
+ }
1678
+
1679
+ const CHROMIUM_TABS: Array<ChromiumTab> = [
1680
+ {
1681
+ // db at (340, 3768)-(450, 3808): its bottom comes up to 696.
1682
+ name: "far below the canvas",
1683
+ transform: [100, 3548, 0.5],
1684
+ scroll: { x: 0, y: 3368 },
1685
+ shows: { x: 340, y: 400 },
1686
+ pan: { x: 0, y: -3112 },
1687
+ lands: { x: 340, y: 656 },
1688
+ },
1689
+ {
1690
+ // db at (2340, 3768)-(2450, 3808): its bottom-right corner comes in to (816, 696).
1691
+ name: "far past the canvas's bottom-right corner",
1692
+ transform: [2100, 3548, 0.5],
1693
+ scroll: { x: 1955, y: 3368 },
1694
+ shows: { x: 385, y: 400 },
1695
+ pan: { x: -1634, y: -3112 },
1696
+ lands: { x: 706, y: 656 },
1697
+ },
1698
+ ];
1699
+
1700
+ test.each(CHROMIUM_TABS)(
1701
+ "Tab onto a card $name, which Chromium scrolls React Flow's element to before focusin, is followed with one pan, and the element is left unscrolled",
1702
+ (tab: ChromiumTab) => {
1703
+ const guard: RenderedGuard = renderGuard();
1704
+ const map: MapOnPage = mapOnPage();
1705
+ flowChanges({ transform: tab.transform });
1706
+
1707
+ browserHasScrolled(map.domNode, tab.scroll);
1708
+ // Through the scroll, db shows in the middle of the canvas.
1709
+ expect(map.db.getBoundingClientRect()).toMatchObject({
1710
+ left: tab.shows.x,
1711
+ top: tab.shows.y,
1712
+ });
1713
+ tabOnto(map.db);
1714
+ /*
1715
+ * The pan shows db instead of the scroll, so the scroll is dropped
1716
+ * first, and the pan is the least that brings db 24 px inside.
1717
+ */
1718
+ expect(map.domNode.scrollLeft).toBe(0);
1719
+ expect(map.domNode.scrollTop).toBe(0);
1720
+ expect(pansAskedFor()).toEqual([tab.pan]);
1721
+
1722
+ // The scroll's event, a frame later, finds nothing scrolled: no second pan.
1723
+ scrollEventArrives(map.domNode);
1724
+ expect(pansAskedFor()).toEqual([tab.pan]);
1725
+ const [x, y, zoom] = tab.transform;
1726
+ expect(mockFlowStore.state.transform).toEqual([
1727
+ x + tab.pan.x,
1728
+ y + tab.pan.y,
1729
+ zoom,
1730
+ ]);
1731
+ // db is on the canvas, 24 px inside its edge.
1732
+ expect(map.db.getBoundingClientRect()).toMatchObject({
1733
+ left: tab.lands.x,
1734
+ top: tab.lands.y,
1735
+ right: tab.lands.x + 110,
1736
+ bottom: tab.lands.y + 40,
1737
+ });
1738
+ expect(guard.autoFrame.current).toBe(false);
1739
+ },
1740
+ );
1741
+
1742
+ test("the page is then scrolled to the focused card where the pan has put it, as little as shows it", () => {
1743
+ renderGuard();
1744
+ const map: MapOnPage = mapOnPage();
1745
+ const pageScrolls: Array<PageScroll> = pageScrollsRecorded();
1746
+ // db far below the canvas: Chromium scrolls React Flow's element to centre it, then fires focusin.
1747
+ flowChanges({ transform: [100, 3548, 0.5] });
1748
+ browserHasScrolled(map.domNode, { x: 0, y: 3368 });
1749
+
1750
+ tabOnto(map.db);
1751
+ expect(pansAskedFor()).toEqual([{ x: 0, y: -3112 }]);
1752
+ /*
1753
+ * The browser may have scrolled the page towards where db was. The page
1754
+ * is scrolled to db once, the nearest way on both axes...
1755
+ */
1756
+ expect(pageScrolls).toHaveLength(1);
1757
+ expect(pageScrolls[0]?.element).toBe(map.db);
1758
+ expect(pageScrolls[0]?.options).toEqual({
1759
+ block: "nearest",
1760
+ inline: "nearest",
1761
+ });
1762
+ // ...to where db is drawn by then: where the pan put it, 24 px inside the canvas's bottom edge.
1763
+ expect(pageScrolls[0]?.box).toMatchObject({
1764
+ left: 340,
1765
+ top: 656,
1766
+ right: 450,
1767
+ bottom: 696,
1768
+ });
1769
+ });
1770
+
1771
+ test("focus is followed all the same in a DOM without scrollIntoView, and nothing throws for the want of it", () => {
1772
+ const guard: RenderedGuard = renderGuard();
1773
+ const map: MapOnPage = mapOnPage();
1774
+ // jsdom has none.
1775
+ expect("scrollIntoView" in map.db).toBe(false);
1776
+ // db below the canvas.
1777
+ flowChanges({ transform: [100, 580, 0.5] });
1778
+
1779
+ tabOnto(map.db);
1780
+ expect(pansAskedFor()).toEqual([{ x: 0, y: -144 }]);
1781
+ expect(guard.autoFrame.current).toBe(false);
1782
+ expect(thrownInListeners).toEqual([]);
1783
+ });
1784
+
1785
+ test("focus a pointer gives a card is not followed: the card is under the pointer, and moving the map between press and release would lose the click", () => {
1786
+ const guard: RenderedGuard = renderGuard();
1787
+ const map: MapOnPage = mapOnPage();
1788
+ const pageScrolls: Array<PageScroll> = pageScrollsRecorded();
1789
+ // db cut off by the canvas's bottom edge, at (340, 700)-(450, 740): Tab would bring it up 44 px.
1790
+ flowChanges({ transform: [100, 480, 0.5] });
1791
+
1792
+ /*
1793
+ * A press on db's top focuses it. Should the browser scroll React Flow's
1794
+ * element to show the rest of db, that scroll is not dropped either:
1795
+ * dropping it would move db under the pointer just the same.
1796
+ */
1797
+ browserHasScrolled(map.domNode, { x: 0, y: 20 });
1798
+ pointerFocuses(map.db);
1799
+ expect(map.db.matches(":focus-visible")).toBe(false);
1800
+
1801
+ expect(pansAskedFor()).toEqual([]);
1802
+ expect(map.domNode.scrollTop).toBe(20);
1803
+ expect(pageScrolls).toEqual([]);
1804
+ expect(mockFlowStore.state.transform).toEqual([100, 480, 0.5]);
1805
+ expect(guard.autoFrame.current).toBe(true);
1806
+ });
1807
+
1808
+ test("focus the browser hands back as the window regains focus is not followed, whatever the view did meanwhile; 100 ms on, focus is the user's again", () => {
1809
+ const guard: RenderedGuard = renderGuard();
1810
+ const map: MapOnPage = mapOnPage();
1811
+ const api: HTMLDivElement = map.drawCard(API_BOX);
1812
+ // Tab onto db, where it shows: nothing moves.
1813
+ tabOnto(map.db);
1814
+ expect(pansAskedFor()).toEqual([]);
1815
+
1816
+ /*
1817
+ * The user goes to another tab, and the map is framed afresh meanwhile
1818
+ * (new data, say), leaving db below the canvas at (340, 800)-(450, 840)
1819
+ * and api cut off by its bottom edge at (140, 700)-(250, 740).
1820
+ */
1821
+ flowChanges({ transform: [100, 580, 0.5] });
1822
+
1823
+ /*
1824
+ * A minute on, the user comes back: the window regains focus, and 99 ms
1825
+ * later the browser hands focus back to db, focus ring and all.
1826
+ */
1827
+ pageClockMs = 60000;
1828
+ windowRegainsFocus();
1829
+ pageClockMs = 60099;
1830
+ focusHandedBackTo(map.db);
1831
+ expect(map.db.matches(":focus-visible")).toBe(true);
1832
+ // The view is left as it is, and it is still the automatic framing.
1833
+ expect(pansAskedFor()).toEqual([]);
1834
+ expect(mockFlowStore.state.transform).toEqual([100, 580, 0.5]);
1835
+ expect(guard.autoFrame.current).toBe(true);
1836
+
1837
+ // 100 ms after the window regained focus, Tab onto api is the user's, and is followed.
1838
+ pageClockMs = 60100;
1839
+ tabOnto(api);
1840
+ expect(pansAskedFor()).toEqual([{ x: 0, y: -44 }]);
1841
+ expect(guard.autoFrame.current).toBe(false);
1842
+ });
1843
+
1844
+ test("a pan the pan extent refuses leaves the view the automatic framing, whether it follows focus or undoes a scroll", () => {
1845
+ panByMoves = false;
1846
+ const guard: RenderedGuard = renderGuard();
1847
+ const map: MapOnPage = mapOnPage();
1848
+ /*
1849
+ * The automatic framing of a larger map, the view at the edge of the pan
1850
+ * extent, with db at (340, 670)-(450, 710): on the canvas, but nearer its
1851
+ * bottom edge than a focused card is left.
1852
+ */
1853
+ flowChanges({ transform: [100, 450, 0.5] });
1854
+
1855
+ tabOnto(map.db);
1856
+ expect(pansAskedFor()).toEqual([{ x: 0, y: -14 }]);
1857
+ expect(mockFlowStore.state.transform).toEqual([100, 450, 0.5]);
1858
+ expect(guard.autoFrame.current).toBe(true);
1859
+
1860
+ /*
1861
+ * Find in page scrolls React Flow's element to a match lower down. The
1862
+ * scroll is undone all the same, so the map stays on its canvas; the pan
1863
+ * it becomes is refused, and so is the one to show db, still focused.
1864
+ */
1865
+ browserScrolls(map.domNode, 0, 120);
1866
+ expect(map.domNode.scrollLeft).toBe(0);
1867
+ expect(map.domNode.scrollTop).toBe(0);
1868
+ expect(pansAskedFor()).toEqual([
1869
+ { x: 0, y: -14 },
1870
+ { x: 0, y: -120 },
1871
+ { x: 0, y: -14 },
1872
+ ]);
1873
+ expect(mockFlowStore.state.transform).toEqual([100, 450, 0.5]);
1874
+ expect(guard.autoFrame.current).toBe(true);
1875
+ });
1876
+
1877
+ test("a scroll of React Flow's element is undone and made the same pan, which takes the view from the automatic framing", () => {
1878
+ const guard: RenderedGuard = renderGuard();
1879
+ const map: MapOnPage = mapOnPage();
1880
+ expect(guard.autoFrame.current).toBe(true);
1881
+
1882
+ // Find in page scrolls React Flow's element to a match past the canvas's bottom-right. Nothing of the map has focus.
1883
+ browserScrolls(map.domNode, 40, 300);
1884
+ // The element is back where it was, and all React Flow draws in it...
1885
+ expect(map.domNode.scrollLeft).toBe(0);
1886
+ expect(map.domNode.scrollTop).toBe(0);
1887
+ // ...and the view moves by as much instead, in screen pixels whatever the zoom.
1888
+ expect(pansAskedFor()).toEqual([{ x: -40, y: -300 }]);
1889
+ expect(mockFlowStore.state.transform).toEqual([60, -250, 0.5]);
1890
+ expect(guard.autoFrame.current).toBe(false);
1891
+ expect(mockFitView).toHaveBeenCalledTimes(1);
1892
+
1893
+ // A scroll along one axis pans along that axis, from where the view now is.
1894
+ browserScrolls(map.domNode, 0, 120);
1895
+ expect(map.domNode.scrollTop).toBe(0);
1896
+ expect(pansAskedFor()).toEqual([
1897
+ { x: -40, y: -300 },
1898
+ { x: 0, y: -120 },
1899
+ ]);
1900
+ expect(mockFlowStore.state.transform).toEqual([60, -370, 0.5]);
1901
+
1902
+ // So a resize now keeps the user's view, inside the pan extent, rather than fitting.
1903
+ canvasResized(map, 640, PANE_HEIGHT);
1904
+ expect(mockFitView).toHaveBeenCalledTimes(1);
1905
+ expect(mockTranslateBy).toHaveBeenCalledTimes(1);
1906
+ });
1907
+
1908
+ interface ScrolledTo {
1909
+ name: string;
1910
+ /* Where the user dragged the view, with db off the canvas. */
1911
+ transform: FlowTransform;
1912
+ /* How far the browser scrolls React Flow's element to show db. */
1913
+ scroll: XYPosition;
1914
+ pans: Array<XYPosition>;
1915
+ /* The view the pans leave. */
1916
+ after: FlowTransform;
1917
+ }
1918
+
1919
+ const SCROLLED_TO: Array<ScrolledTo> = [
1920
+ {
1921
+ /*
1922
+ * db at (770, 980)-(880, 1020). The least scroll that shows it leaves
1923
+ * it at (730, 680)-(840, 720), flush with the canvas's bottom-right
1924
+ * corner; a second pan brings it to (706, 656)-(816, 696).
1925
+ */
1926
+ name: "a card the scroll leaves at the canvas's edge is then brought 24 px inside",
1927
+ transform: [530, 760, 0.5],
1928
+ scroll: { x: 40, y: 300 },
1929
+ pans: [
1930
+ { x: -40, y: -300 },
1931
+ { x: -24, y: -24 },
1932
+ ],
1933
+ after: [466, 436, 0.5],
1934
+ },
1935
+ {
1936
+ /*
1937
+ * db at (340, 800)-(450, 840), centred as find in page centres a
1938
+ * match: the pan leaves it at (340, 400)-(450, 440).
1939
+ */
1940
+ name: "a card the scroll centres needs nothing more",
1941
+ transform: [100, 580, 0.5],
1942
+ scroll: { x: 0, y: 400 },
1943
+ pans: [{ x: 0, y: -400 }],
1944
+ after: [100, 180, 0.5],
1945
+ },
1946
+ ];
1947
+
1948
+ test.each(SCROLLED_TO)(
1949
+ "a scroll to show the focused card is undone and made the same pan, and $name",
1950
+ (scrolledTo: ScrolledTo) => {
1951
+ const guard: RenderedGuard = renderGuard();
1952
+ const map: MapOnPage = mapOnPage();
1953
+ // db takes focus where it shows: nothing moves.
1954
+ tabOnto(map.db);
1955
+ expect(pansAskedFor()).toEqual([]);
1956
+
1957
+ // The user drags db off the canvas, and the map clears autoFrame.
1958
+ guard.autoFrame.current = false;
1959
+ flowChanges({ transform: scrolledTo.transform });
1960
+
1961
+ browserScrolls(map.domNode, scrolledTo.scroll.x, scrolledTo.scroll.y);
1962
+ expect(map.domNode.scrollLeft).toBe(0);
1963
+ expect(map.domNode.scrollTop).toBe(0);
1964
+ expect(pansAskedFor()).toEqual(scrolledTo.pans);
1965
+ expect(mockFlowStore.state.transform).toEqual(scrolledTo.after);
1966
+ expect(guard.autoFrame.current).toBe(false);
1967
+ },
1968
+ );
1969
+
1970
+ test("a scroll while a card a pointer focused has focus is undone and made the same pan, and the card is left where the scroll put it", () => {
1971
+ const guard: RenderedGuard = renderGuard();
1972
+ const map: MapOnPage = mapOnPage();
1973
+ // A press on db, where it shows, focuses it: nothing moves.
1974
+ pointerFocuses(map.db);
1975
+ expect(pansAskedFor()).toEqual([]);
1976
+
1977
+ // The user drags db off the canvas, and the map clears autoFrame.
1978
+ guard.autoFrame.current = false;
1979
+ flowChanges({ transform: [530, 760, 0.5] });
1980
+
1981
+ /*
1982
+ * Find in page scrolls React Flow's element to a match in db, leaving db
1983
+ * flush with the canvas's bottom-right corner. The scroll is undone and
1984
+ * made the same pan, and that is all: had the keyboard focused db, it
1985
+ * would be brought 24 px inside as well (see above).
1986
+ */
1987
+ browserScrolls(map.domNode, 40, 300);
1988
+ expect(map.domNode.scrollLeft).toBe(0);
1989
+ expect(map.domNode.scrollTop).toBe(0);
1990
+ expect(pansAskedFor()).toEqual([{ x: -40, y: -300 }]);
1991
+ expect(mockFlowStore.state.transform).toEqual([490, 460, 0.5]);
1992
+ expect(map.db.getBoundingClientRect()).toMatchObject({
1993
+ left: 730,
1994
+ top: 680,
1995
+ right: 840,
1996
+ bottom: 720,
1997
+ });
1998
+ });
1999
+
2000
+ test("a scroll event that finds nothing scrolled, as undoing a scroll fires, changes nothing, even with the focused card off the canvas", () => {
2001
+ const guard: RenderedGuard = renderGuard();
2002
+ const map: MapOnPage = mapOnPage();
2003
+ tabOnto(map.db);
2004
+ // The view moves on without the user (a re-fit, say), leaving db, still focused, below the canvas.
2005
+ flowChanges({ transform: [100, 580, 0.5] });
2006
+
2007
+ browserScrolls(map.domNode, 0, 0);
2008
+ expect(pansAskedFor()).toEqual([]);
2009
+ expect(mockFlowStore.state.transform).toEqual([100, 580, 0.5]);
2010
+ // Nothing moved, so the view is still the automatic framing.
2011
+ expect(guard.autoFrame.current).toBe(true);
2012
+ });
2013
+
2014
+ test("stops following once the guard is gone: focus and scrolls are left to the browser", () => {
2015
+ const guard: RenderedGuard = renderGuard();
2016
+ const map: MapOnPage = mapOnPage();
2017
+ // db below the canvas.
2018
+ flowChanges({ transform: [100, 580, 0.5] });
2019
+ guard.unmount();
2020
+
2021
+ tabOnto(map.db);
2022
+ browserScrolls(map.domNode, 40, 300);
2023
+ // The element is left as the browser scrolled it, and nothing is panned.
2024
+ expect(map.domNode.scrollLeft).toBe(40);
2025
+ expect(map.domNode.scrollTop).toBe(300);
2026
+ expect(pansAskedFor()).toEqual([]);
2027
+ expect(mockFlowStore.state.transform).toEqual([100, 580, 0.5]);
2028
+ });
2029
+
2030
+ test("leaves nothing listening on the window once it is gone", () => {
2031
+ const page: Window = window;
2032
+ const added: SpyInstance<
2033
+ (...args: Parameters<Window["addEventListener"]>) => void
2034
+ > = jest.spyOn(page, "addEventListener");
2035
+ const removed: SpyInstance<
2036
+ (...args: Parameters<Window["removeEventListener"]>) => void
2037
+ > = jest.spyOn(page, "removeEventListener");
2038
+ const forFocus: (
2039
+ calls: Array<Parameters<Window["removeEventListener"]>>,
2040
+ ) => Array<Parameters<Window["removeEventListener"]>> = (
2041
+ calls: Array<Parameters<Window["removeEventListener"]>>,
2042
+ ): Array<Parameters<Window["removeEventListener"]>> => {
2043
+ return calls.filter(
2044
+ (call: Parameters<Window["removeEventListener"]>): boolean => {
2045
+ return call[0] === "focus";
2046
+ },
2047
+ );
2048
+ };
2049
+ try {
2050
+ const guard: RenderedGuard = renderGuard();
2051
+ mapOnPage();
2052
+ // It listens for the window regaining focus...
2053
+ const listening: Array<Parameters<Window["removeEventListener"]>> =
2054
+ forFocus(added.mock.calls);
2055
+ expect(listening).toHaveLength(1);
2056
+
2057
+ guard.unmount();
2058
+ // ...and once it is gone, it takes that same listener off, the same way.
2059
+ expect(forFocus(removed.mock.calls)).toEqual(listening);
2060
+ } finally {
2061
+ added.mockRestore();
2062
+ removed.mockRestore();
2063
+ }
2064
+ });
2065
+ });
2066
+
2067
+ describe("isKeyboardFocus", () => {
2068
+ test("is whether the element matches :focus-visible, the focus a browser draws a ring for", () => {
2069
+ const card: HTMLDivElement = document.createElement("div");
2070
+ const matches: SpyInstance<(selectors: string) => boolean> = jest.spyOn(
2071
+ card,
2072
+ "matches",
2073
+ );
2074
+ matches.mockReturnValue(true);
2075
+ expect(isKeyboardFocus(card)).toBe(true);
2076
+ matches.mockReturnValue(false);
2077
+ expect(isKeyboardFocus(card)).toBe(false);
2078
+ expect(matches.mock.calls).toEqual([
2079
+ [":focus-visible"],
2080
+ [":focus-visible"],
2081
+ ]);
2082
+ });
2083
+
2084
+ test("counts all focus as keyboard focus in a browser that cannot say: one that does not know :focus-visible throws", () => {
2085
+ const card: HTMLDivElement = document.createElement("div");
2086
+ jest.spyOn(card, "matches").mockImplementation((selectors: string) => {
2087
+ throw new DOMException(
2088
+ `'${selectors}' is not a valid selector`,
2089
+ "SyntaxError",
2090
+ );
2091
+ });
2092
+ expect(isKeyboardFocus(card)).toBe(true);
2093
+ });
2094
+ });
2095
+
2096
+ describe("the pan extent", () => {
2097
+ test("is the measured drawing plus the pan margin, from where React Flow placed each card", () => {
2098
+ const guard: RenderedGuard = renderGuard();
2099
+ flowChanges({
2100
+ nodeInternals: holding(
2101
+ { ...measuredCard("api", 0, 0), positionAbsolute: { x: 100, y: 50 } },
2102
+ {
2103
+ ...measuredCard("db", 400, 200, 300, 120),
2104
+ positionAbsolute: { x: 500, y: 350 },
2105
+ },
2106
+ ),
2107
+ });
2108
+ // Measured boxes (100, 50)-(320, 130) and (500, 350)-(800, 470), grown by 50.
2109
+ expect(guard.onExtentChange).toHaveBeenCalledTimes(1);
2110
+ expect(guard.onExtentChange).toHaveBeenLastCalledWith([
2111
+ [50, 0],
2112
+ [850, 520],
2113
+ ]);
2114
+ });
2115
+
2116
+ test("is reported once per distinct extent", () => {
2117
+ const guard: RenderedGuard = renderGuard();
2118
+ drawnAndReady();
2119
+ expect(guard.onExtentChange).toHaveBeenCalledTimes(1);
2120
+ expect(guard.onExtentChange).toHaveBeenLastCalledWith([
2121
+ [-50, -50],
2122
+ [670, 330],
2123
+ ]);
2124
+
2125
+ // The same boxes again: rebuilt cards, a pan, a resize, a re-render.
2126
+ flowChanges({ nodeInternals: apiAndDb() });
2127
+ flowChanges({ transform: [-40, 10, 0.9], width: 700 });
2128
+ guard.rerender({});
2129
+ // Nor is rounding noise a new extent.
2130
+ flowChanges({
2131
+ nodeInternals: holding(
2132
+ measuredCard("api", 1e-9, 0),
2133
+ measuredCard("db", 400, 200),
2134
+ ),
2135
+ });
2136
+ expect(guard.onExtentChange).toHaveBeenCalledTimes(1);
2137
+
2138
+ // A card measured at another size moves the edge.
2139
+ flowChanges({
2140
+ nodeInternals: holding(
2141
+ measuredCard("api", 0, 0),
2142
+ measuredCard("db", 400, 200, 300, 120),
2143
+ ),
2144
+ });
2145
+ expect(guard.onExtentChange).toHaveBeenCalledTimes(2);
2146
+ expect(guard.onExtentChange).toHaveBeenLastCalledWith([
2147
+ [-50, -50],
2148
+ [750, 370],
2149
+ ]);
2150
+
2151
+ // So does another margin.
2152
+ guard.rerender({ panMargin: 80 });
2153
+ expect(guard.onExtentChange).toHaveBeenCalledTimes(3);
2154
+ expect(guard.onExtentChange).toHaveBeenLastCalledWith([
2155
+ [-80, -80],
2156
+ [780, 400],
2157
+ ]);
2158
+ });
2159
+
2160
+ test("is not reported while a card is unmeasured or React Flow holds another drawing", () => {
2161
+ const guard: RenderedGuard = renderGuard();
2162
+ // React Flow still holds the drawing before this one...
2163
+ flowChanges({
2164
+ nodeInternals: holding(
2165
+ measuredCard("web", -600, 0),
2166
+ measuredCard("api", 0, 0),
2167
+ ),
2168
+ width: PANE_WIDTH,
2169
+ height: PANE_HEIGHT,
2170
+ });
2171
+ // ...then this one, before db is measured.
2172
+ flowChanges({
2173
+ nodeInternals: holding(
2174
+ measuredCard("api", 0, 0),
2175
+ unmeasuredCard("db", 400, 200),
2176
+ ),
2177
+ });
2178
+ expect(guard.onExtentChange).not.toHaveBeenCalled();
2179
+ flowChanges({ nodeInternals: apiAndDb() });
2180
+ expect(guard.onExtentChange).toHaveBeenCalledTimes(1);
2181
+
2182
+ /*
2183
+ * The map moves on to a drawing React Flow has not taken yet: the old
2184
+ * extent stands until the new one is measured.
2185
+ */
2186
+ guard.rerender({ drawingKey: "api|cache" });
2187
+ flowChanges({
2188
+ nodeInternals: holding(
2189
+ measuredCard("api", 0, 0),
2190
+ unmeasuredCard("cache", 0, 300),
2191
+ ),
2192
+ });
2193
+ expect(guard.onExtentChange).toHaveBeenCalledTimes(1);
2194
+ flowChanges({
2195
+ nodeInternals: holding(
2196
+ measuredCard("api", 0, 0),
2197
+ measuredCard("cache", 0, 300),
2198
+ ),
2199
+ });
2200
+ expect(guard.onExtentChange).toHaveBeenCalledTimes(2);
2201
+ expect(guard.onExtentChange).toHaveBeenLastCalledWith([
2202
+ [-50, -50],
2203
+ [270, 430],
2204
+ ]);
2205
+ });
2206
+
2207
+ test("is unbounded again once the guard is gone", () => {
2208
+ const guard: RenderedGuard = renderGuard();
2209
+ drawnAndReady();
2210
+ expect(guard.onExtentChange).toHaveBeenCalledTimes(1);
2211
+ guard.unmount();
2212
+ expect(guard.onExtentChange).toHaveBeenCalledTimes(2);
2213
+ expect(guard.onExtentChange).toHaveBeenLastCalledWith(
2214
+ UNBOUNDED_FLOW_EXTENT,
2215
+ );
2216
+ });
2217
+ });
2218
+
2219
+ describe("mounted a second time with its refs kept, as StrictMode does", () => {
2220
+ test("the pan extent is reported again after the unmount withdrew it, rather than left unbounded", () => {
2221
+ drawnAndReadyBeforeMount();
2222
+ const guard: RenderedGuard = renderGuard({}, { strictMode: true });
2223
+ // Mounted, unmounted and mounted again: reported, withdrawn, reported.
2224
+ expect(guard.onExtentChange.mock.calls).toEqual([
2225
+ [API_AND_DB_EXTENT],
2226
+ [UNBOUNDED_FLOW_EXTENT],
2227
+ [API_AND_DB_EXTENT],
2228
+ ]);
2229
+
2230
+ // And from then on once per distinct extent, as before.
2231
+ flowChanges({ nodeInternals: apiAndDb() });
2232
+ expect(guard.onExtentChange).toHaveBeenCalledTimes(3);
2233
+ });
2234
+
2235
+ test("the instance and the in-view answer are reported again too", () => {
2236
+ drawnAndReadyBeforeMount();
2237
+ const guard: RenderedGuard = renderGuard({}, { strictMode: true });
2238
+ const reported: Array<unknown> = guard.onInstance.mock.calls.map(
2239
+ (call: Array<unknown>): unknown => {
2240
+ return call[0];
2241
+ },
2242
+ );
2243
+ expect(reported).toHaveLength(3);
2244
+ expect(reported[0]).toBe(mockFlowStore.instance);
2245
+ expect(reported[1]).toBeNull();
2246
+ expect(reported[2]).toBe(mockFlowStore.instance);
2247
+ expect(inViewReports(guard)).toEqual([true, true, true]);
2248
+ });
2249
+
2250
+ test("the drawing is fitted once, not once per mount", () => {
2251
+ drawnAndReadyBeforeMount();
2252
+ const guard: RenderedGuard = renderGuard({}, { strictMode: true });
2253
+ expect(mockFitView).toHaveBeenCalledTimes(1);
2254
+ expect(mockFitView).toHaveBeenCalledWith(FIT_VIEW_OPTIONS);
2255
+ expect(guard.autoFrame.current).toBe(true);
2256
+ });
2257
+ });
2258
+
2259
+ describe("whether the drawing is in view", () => {
2260
+ test("is in view while a measured card shows at least 8 px of itself on the pane", () => {
2261
+ const guard: RenderedGuard = renderGuard();
2262
+ drawnAndReady();
2263
+ // Panned so that only the right-hand 8 px of db (x 400-620) are left.
2264
+ flowChanges({ transform: [8 - 620, 0, 1] });
2265
+ expect(inViewReports(guard)).toEqual([true]);
2266
+ // One pixel further, a 7 px sliver is not a map.
2267
+ flowChanges({ transform: [7 - 620, 0, 1] });
2268
+ expect(inViewReports(guard)).toEqual([true, false]);
2269
+ flowChanges({ transform: [0, 0, 1] });
2270
+ expect(inViewReports(guard)).toEqual([true, false, true]);
2271
+ });
2272
+
2273
+ test("is out of view once every card has left the pane", () => {
2274
+ const guard: RenderedGuard = renderGuard();
2275
+ drawnAndReady();
2276
+ flowChanges({ transform: [5000, 5000, 1] });
2277
+ expect(inViewReports(guard)).toEqual([true, false]);
2278
+ // Zoomed far out the cards are small, but they are back on the pane.
2279
+ flowChanges({ transform: [0, 0, 0.1] });
2280
+ expect(inViewReports(guard)).toEqual([true, false, true]);
2281
+ });
2282
+
2283
+ test("is out of view while React Flow holds cards but has measured none", () => {
2284
+ const guard: RenderedGuard = renderGuard();
2285
+ flowChanges({
2286
+ nodeInternals: holding(
2287
+ unmeasuredCard("api", 0, 0),
2288
+ unmeasuredCard("db", 400, 200),
2289
+ ),
2290
+ width: PANE_WIDTH,
2291
+ height: PANE_HEIGHT,
2292
+ });
2293
+ expect(inViewReports(guard)).toEqual([true, false]);
2294
+ flowChanges({ nodeInternals: apiAndDb() });
2295
+ expect(inViewReports(guard)).toEqual([true, false, true]);
2296
+ });
2297
+
2298
+ test("is in view with no cards at all, wherever the view is", () => {
2299
+ const guard: RenderedGuard = renderGuard({ drawingKey: "" });
2300
+ flowChanges({
2301
+ width: PANE_WIDTH,
2302
+ height: PANE_HEIGHT,
2303
+ transform: [5000, 5000, 1],
2304
+ });
2305
+ viewportReady();
2306
+ expect(inViewReports(guard)).toEqual([true]);
2307
+ });
2308
+
2309
+ test("reads as in view once the guard is gone", () => {
2310
+ const guard: RenderedGuard = renderGuard();
2311
+ drawnAndReady();
2312
+ flowChanges({ transform: [5000, 5000, 1] });
2313
+ expect(inViewReports(guard)).toEqual([true, false]);
2314
+ guard.unmount();
2315
+ expect(inViewReports(guard)).toEqual([true, false, true]);
2316
+ });
2317
+ });
2318
+
2319
+ describe("the instance", () => {
2320
+ test("is reported once the viewport is ready, as the object React Flow returns, and withdrawn on unmount", () => {
2321
+ const guard: RenderedGuard = renderGuard();
2322
+ expect(guard.onInstance.mock.calls).toEqual([[null]]);
2323
+
2324
+ viewportReady();
2325
+ expect(guard.onInstance).toHaveBeenCalledTimes(2);
2326
+ expect(guard.onInstance.mock.calls[1]?.[0]).toBe(mockFlowStore.instance);
2327
+
2328
+ // The drawing, the pane and the view changing do not report it again.
2329
+ flowChanges({
2330
+ nodeInternals: apiAndDb(),
2331
+ width: PANE_WIDTH,
2332
+ height: PANE_HEIGHT,
2333
+ });
2334
+ flowChanges({ transform: [-50, -50, 0.5] });
2335
+ expect(guard.onInstance).toHaveBeenCalledTimes(2);
2336
+
2337
+ guard.unmount();
2338
+ expect(guard.onInstance).toHaveBeenCalledTimes(3);
2339
+ expect(guard.onInstance).toHaveBeenLastCalledWith(null);
2340
+ });
2341
+
2342
+ test("new callbacks from the map are the ones called, without anything being reported again", () => {
2343
+ const guard: RenderedGuard = renderGuard();
2344
+ viewportReady();
2345
+ const onInstance: MockFunction = getJestMockFunction();
2346
+ const onExtentChange: MockFunction = getJestMockFunction();
2347
+ const onDrawingInViewChange: MockFunction = getJestMockFunction();
2348
+ guard.rerender({ onInstance, onExtentChange, onDrawingInViewChange });
2349
+ expect(onInstance).not.toHaveBeenCalled();
2350
+ expect(onDrawingInViewChange).not.toHaveBeenCalled();
2351
+
2352
+ drawnAndReady();
2353
+ expect(onExtentChange.mock.calls).toEqual([
2354
+ [
2355
+ [
2356
+ [-50, -50],
2357
+ [670, 330],
2358
+ ],
2359
+ ],
2360
+ ]);
2361
+
2362
+ guard.unmount();
2363
+ expect(onInstance.mock.calls).toEqual([[null]]);
2364
+ expect(onDrawingInViewChange.mock.calls).toEqual([[true]]);
2365
+ expect(onExtentChange).toHaveBeenLastCalledWith(UNBOUNDED_FLOW_EXTENT);
2366
+ // The first callbacks heard nothing after they were replaced.
2367
+ expect(guard.onInstance).toHaveBeenCalledTimes(2);
2368
+ expect(guard.onExtentChange).not.toHaveBeenCalled();
2369
+ expect(inViewReports(guard)).toEqual([true]);
2370
+ });
2371
+ });
2372
+
2373
+ describe("measuredRectOf", () => {
2374
+ test("is a measured card's box, where React Flow placed it", () => {
2375
+ expect(
2376
+ measuredRectOf({
2377
+ ...measuredCard("api", 10, 20),
2378
+ positionAbsolute: { x: 110, y: 220 },
2379
+ }),
2380
+ ).toEqual({ x: 110, y: 220, width: CARD_WIDTH, height: CARD_HEIGHT });
2381
+ });
2382
+
2383
+ test("falls back to the card's own position without an absolute one", () => {
2384
+ expect(
2385
+ measuredRectOf({
2386
+ id: "api",
2387
+ position: { x: 10, y: 20 },
2388
+ width: CARD_WIDTH,
2389
+ height: CARD_HEIGHT,
2390
+ data: {},
2391
+ }),
2392
+ ).toEqual({ x: 10, y: 20, width: CARD_WIDTH, height: CARD_HEIGHT });
2393
+ });
2394
+
2395
+ test("is null until React Flow has measured both sides", () => {
2396
+ expect(measuredRectOf(unmeasuredCard("api", 0, 0))).toBeNull();
2397
+ expect(
2398
+ measuredRectOf({ ...unmeasuredCard("api", 0, 0), width: CARD_WIDTH }),
2399
+ ).toBeNull();
2400
+ expect(
2401
+ measuredRectOf({ ...unmeasuredCard("api", 0, 0), height: CARD_HEIGHT }),
2402
+ ).toBeNull();
2403
+ expect(
2404
+ measuredRectOf({
2405
+ ...unmeasuredCard("api", 0, 0),
2406
+ width: 0,
2407
+ height: CARD_HEIGHT,
2408
+ }),
2409
+ ).toBeNull();
2410
+ expect(
2411
+ measuredRectOf({
2412
+ ...unmeasuredCard("api", 0, 0),
2413
+ width: null,
2414
+ height: null,
2415
+ }),
2416
+ ).toBeNull();
2417
+ });
2418
+ });
2419
+
2420
+ describe("renderedPaneSize", () => {
2421
+ test("is the size the page gives React Flow's zoom pane, as it is now", () => {
2422
+ const flow: FlowElement = flowElement(1000, 400);
2423
+ expect(renderedPaneSize(flow.domNode)).toEqual({
2424
+ width: 1000,
2425
+ height: 400,
2426
+ });
2427
+ flow.layOut(640, 480);
2428
+ expect(renderedPaneSize(flow.domNode)).toEqual({ width: 640, height: 480 });
2429
+ });
2430
+
2431
+ test("is a zero size, not nothing, for a zoom pane the page has not laid out", () => {
2432
+ expect(renderedPaneSize(flowElement(0, 0).domNode)).toEqual({
2433
+ width: 0,
2434
+ height: 0,
2435
+ });
2436
+ });
2437
+
2438
+ test("is null without React Flow's element, or before its zoom pane is in it", () => {
2439
+ expect(renderedPaneSize(null)).toBeNull();
2440
+ expect(renderedPaneSize(undefined)).toBeNull();
2441
+ const domNode: HTMLDivElement = document.createElement("div");
2442
+ domNode.className = "react-flow";
2443
+ expect(renderedPaneSize(domNode)).toBeNull();
2444
+ });
2445
+ });
2446
+
2447
+ describe("selectDrawingInView", () => {
2448
+ test("nothing drawn is nothing lost", () => {
2449
+ expect(selectDrawingInView(flowState({ transform: [9000, 9000, 1] }))).toBe(
2450
+ true,
2451
+ );
2452
+ });
2453
+
2454
+ test("a pane that has no size yet cannot be judged, so counts as in view", () => {
2455
+ expect(
2456
+ selectDrawingInView(
2457
+ flowState({
2458
+ nodeInternals: apiAndDb(),
2459
+ transform: [9000, 9000, 1],
2460
+ width: 0,
2461
+ height: 0,
2462
+ }),
2463
+ ),
2464
+ ).toBe(true);
2465
+ });
2466
+
2467
+ test("needs at least 8 px of a measured card on the pane on each axis", () => {
2468
+ const inViewAt: (transform: FlowTransform) => boolean = (
2469
+ transform: FlowTransform,
2470
+ ): boolean => {
2471
+ return selectDrawingInView(
2472
+ flowState({ nodeInternals: apiAndDb(), transform }),
2473
+ );
2474
+ };
2475
+ expect(inViewAt([0, 0, 1])).toBe(true);
2476
+ // db spans x 400-620 and y 200-280; api is further up and left.
2477
+ expect(inViewAt([8 - 620, 0, 1])).toBe(true);
2478
+ expect(inViewAt([7 - 620, 0, 1])).toBe(false);
2479
+ expect(inViewAt([0, 8 - 280, 1])).toBe(true);
2480
+ expect(inViewAt([0, 7 - 280, 1])).toBe(false);
2481
+ // Coming in from the right: api's left 8 px at the pane's right edge.
2482
+ expect(inViewAt([PANE_WIDTH - 8, 0, 1])).toBe(true);
2483
+ expect(inViewAt([PANE_WIDTH - 7, 0, 1])).toBe(false);
2484
+ });
2485
+
2486
+ test("is false when every card is off the pane", () => {
2487
+ expect(
2488
+ selectDrawingInView(
2489
+ flowState({ nodeInternals: apiAndDb(), transform: [-5000, 0, 1] }),
2490
+ ),
2491
+ ).toBe(false);
2492
+ });
2493
+
2494
+ test("does not count cards React Flow has not measured, even on the pane", () => {
2495
+ expect(
2496
+ selectDrawingInView(
2497
+ flowState({
2498
+ nodeInternals: holding(
2499
+ unmeasuredCard("api", 0, 0),
2500
+ unmeasuredCard("db", 400, 200),
2501
+ ),
2502
+ }),
2503
+ ),
2504
+ ).toBe(false);
2505
+ expect(
2506
+ selectDrawingInView(
2507
+ flowState({
2508
+ nodeInternals: holding(
2509
+ unmeasuredCard("api", 0, 0),
2510
+ measuredCard("db", 5000, 5000),
2511
+ ),
2512
+ }),
2513
+ ),
2514
+ ).toBe(false);
2515
+ expect(
2516
+ selectDrawingInView(
2517
+ flowState({
2518
+ nodeInternals: holding(
2519
+ unmeasuredCard("api", 5000, 5000),
2520
+ measuredCard("db", 400, 200),
2521
+ ),
2522
+ }),
2523
+ ),
2524
+ ).toBe(true);
2525
+ });
2526
+ });