@oneuptime/common 14.0.8 → 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 (639) hide show
  1. package/Models/DatabaseModels/Index.ts +2 -0
  2. package/Models/DatabaseModels/KubernetesAiAgent.ts +364 -0
  3. package/Models/DatabaseModels/KubernetesCluster.ts +3 -3
  4. package/Models/DatabaseModels/Project.ts +55 -5
  5. package/Models/DatabaseModels/ProxmoxResource.ts +49 -1
  6. package/Models/DatabaseModels/RunnerJob.ts +75 -0
  7. package/Models/DatabaseModels/TeamComplianceSetting.ts +143 -2
  8. package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +17 -0
  9. package/Server/API/AIChatAPI.ts +6 -3
  10. package/Server/API/AIInvestigationAPI.ts +17 -10
  11. package/Server/API/AutoRemediationAPI.ts +207 -25
  12. package/Server/API/KubernetesClusterAiAccessAPI.ts +623 -21
  13. package/Server/API/MicrosoftTeamsAPI.ts +93 -14
  14. package/Server/API/ProxmoxResourceAPI.ts +244 -14
  15. package/Server/Infrastructure/GlobalCache.ts +32 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1796000000000-AddTeamComplianceRuleScope.ts +99 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/1796100000000-AddKubernetesAiAgentAndAiDefaults.ts +137 -0
  18. package/Server/Infrastructure/Postgres/SchemaMigrations/1796200000000-AddProxmoxResourceNativePushColumns.ts +42 -0
  19. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  20. package/Server/Middleware/TelemetryIngest.ts +41 -18
  21. package/Server/Services/AIService.ts +70 -1
  22. package/Server/Services/AlertService.ts +352 -11
  23. package/Server/Services/AlertSeverityService.ts +65 -15
  24. package/Server/Services/AutoRemediationRuleService.ts +1 -1
  25. package/Server/Services/IncidentService.ts +280 -104
  26. package/Server/Services/IncidentSeverityService.ts +66 -15
  27. package/Server/Services/KubernetesAiAgentJobService.ts +185 -0
  28. package/Server/Services/KubernetesAiAgentService.ts +1634 -0
  29. package/Server/Services/KubernetesClusterAiAccessService.ts +1118 -451
  30. package/Server/Services/KubernetesClusterService.ts +129 -21
  31. package/Server/Services/ProjectService.ts +32 -0
  32. package/Server/Services/ProxmoxResourceService.ts +322 -5
  33. package/Server/Services/RunnerJobService.ts +241 -104
  34. package/Server/Services/RunnerService.ts +1 -1
  35. package/Server/Services/ScheduledMaintenanceService.ts +737 -1
  36. package/Server/Services/ScheduledMaintenanceStateTimelineService.ts +414 -56
  37. package/Server/Services/TeamComplianceSettingService.ts +1125 -9
  38. package/Server/Utils/AI/ClusterAccess/ClusterAccessContext.ts +67 -9
  39. package/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.ts +39 -10
  40. package/Server/Utils/AI/ClusterAccess/KubectlJobRunner.ts +14 -3
  41. package/Server/Utils/AI/Remediation/RemediationCommandTools.ts +37 -15
  42. package/Server/Utils/AI/Remediation/RemediationExecutionRunner.ts +67 -24
  43. package/Server/Utils/AI/SRE/AIInvestigationEngine.ts +48 -5
  44. package/Server/Utils/AI/SRE/IncidentPostmortemRunner.ts +40 -8
  45. package/Server/Utils/AI/SRE/InvestigationEligibility.ts +13 -2
  46. package/Server/Utils/AI/SRE/InvestigationGrader.ts +1 -1
  47. package/Server/Utils/AutoRemediation/CommandPlanExecutor.ts +6 -1
  48. package/Server/Utils/Telemetry/InventoryEntityRegistry.ts +133 -7
  49. package/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.ts +58 -4
  50. package/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.ts +447 -0
  51. package/Server/Utils/Telemetry/ProxmoxNativePush.ts +903 -0
  52. package/Server/Utils/Telemetry/TelemetryEntity.ts +87 -27
  53. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +684 -47
  54. package/Tests/App/AdminDashboard/RowActionPlacementOverrides.test.tsx +278 -0
  55. package/Tests/App/Dashboard/AIChatChartWidgetZoom.test.tsx +738 -0
  56. package/Tests/App/Dashboard/AIFeaturesSettingsPointers.test.tsx +147 -0
  57. package/Tests/App/Dashboard/AIInsightsKubernetesClusterPage.test.tsx +1523 -0
  58. package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +463 -3
  59. package/Tests/App/Dashboard/AffectedResourcesPickerDropdownPlacement.test.tsx +958 -0
  60. package/Tests/App/Dashboard/AffectedResourcesPickerFocusLeave.test.tsx +595 -0
  61. package/Tests/App/Dashboard/AskAiAvailability.test.ts +20 -3
  62. package/Tests/App/Dashboard/AskAiDisabled.test.tsx +8 -3
  63. package/Tests/App/Dashboard/CephRateChartReload.test.tsx +1456 -0
  64. package/Tests/App/Dashboard/CephTimeRangeZoom.test.tsx +1229 -0
  65. package/Tests/App/Dashboard/ChartCardTimeRangeZoom.test.tsx +151 -0
  66. package/Tests/App/Dashboard/ChartCardZoomAffordances.test.tsx +257 -0
  67. package/Tests/App/Dashboard/ChartRefetchFrame.test.tsx +141 -0
  68. package/Tests/App/Dashboard/ChartZoomHarness.tsx +249 -0
  69. package/Tests/App/Dashboard/ChartZoomStandIn.tsx +131 -0
  70. package/Tests/App/Dashboard/CloudServerlessSlowLoads.test.tsx +628 -0
  71. package/Tests/App/Dashboard/CloudServerlessTimeRangeZoom.test.tsx +607 -0
  72. package/Tests/App/Dashboard/ClusterAccessNotice.test.tsx +91 -19
  73. package/Tests/App/Dashboard/CompanionMetricsTabZoom.test.tsx +632 -0
  74. package/Tests/App/Dashboard/ContainerChartZoomStandIn.tsx +195 -0
  75. package/Tests/App/Dashboard/ContainerHostOverviewSlowLoads.test.tsx +542 -0
  76. package/Tests/App/Dashboard/ContainerHostOverviewTimeRangeZoom.test.tsx +848 -0
  77. package/Tests/App/Dashboard/ContainerMetricsTimeRangeZoom.test.tsx +561 -0
  78. package/Tests/App/Dashboard/DashboardBoardWideZoomShell.test.tsx +1147 -0
  79. package/Tests/App/Dashboard/DashboardEditPermissions.test.tsx +3 -2
  80. package/Tests/App/Dashboard/DashboardHistogramWidgetDragCursor.test.tsx +399 -0
  81. package/Tests/App/Dashboard/DashboardHistogramWidgetZoom.test.tsx +985 -0
  82. package/Tests/App/Dashboard/DashboardHistogramWidgetZoomHint.test.tsx +803 -0
  83. package/Tests/App/Dashboard/DashboardLazyPages.test.tsx +1 -0
  84. package/Tests/App/Dashboard/DashboardSettingsPreviewZoom.test.tsx +679 -0
  85. package/Tests/App/Dashboard/DashboardSloAndDataSourceChartZoom.test.tsx +881 -0
  86. package/Tests/App/Dashboard/DashboardTraceStatusWidgets.test.tsx +878 -0
  87. package/Tests/App/Dashboard/DatabaseMetricChartModalTimeRangeZoom.test.tsx +442 -0
  88. package/Tests/App/Dashboard/DatabaseOverviewTimeRangeZoom.test.tsx +831 -0
  89. package/Tests/App/Dashboard/DatabaseServerComponents.test.tsx +43 -0
  90. package/Tests/App/Dashboard/DockerSwarmInsightsTimeRangeZoom.test.tsx +524 -0
  91. package/Tests/App/Dashboard/DockerSwarmRateChartRefresh.test.tsx +259 -0
  92. package/Tests/App/Dashboard/EdgeDetailPanelHistory.test.tsx +763 -0
  93. package/Tests/App/Dashboard/EdgeDetailPanelTimeRangeZoom.test.tsx +547 -0
  94. package/Tests/App/Dashboard/EdgeHistoryBucketsCases.tsx +565 -0
  95. package/Tests/App/Dashboard/EdgeHistoryBucketsKathmandu.test.tsx +73 -0
  96. package/Tests/App/Dashboard/EdgeHistoryBucketsKolkata.test.tsx +74 -0
  97. package/Tests/App/Dashboard/EdgeHistoryBucketsNewYork.test.tsx +72 -0
  98. package/Tests/App/Dashboard/EdgeHistoryBucketsUtc.test.tsx +72 -0
  99. package/Tests/App/Dashboard/EmbeddedMetricCardRefresh.test.tsx +310 -0
  100. package/Tests/App/Dashboard/EmbeddedMetricCardTimeRangeZoom.test.tsx +536 -0
  101. package/Tests/App/Dashboard/EscalationRuleReadiness.test.tsx +3 -1
  102. package/Tests/App/Dashboard/EscalationRuleRowActions.test.tsx +660 -0
  103. package/Tests/App/Dashboard/EventNotes.test.tsx +2 -2
  104. package/Tests/App/Dashboard/EventOverviewPages.test.tsx +587 -13
  105. package/Tests/App/Dashboard/EventOverviewSnapshotExplorerZoom.test.tsx +1139 -0
  106. package/Tests/App/Dashboard/EventOverviewSnapshotZoomAcrossTabs.test.tsx +1057 -0
  107. package/Tests/App/Dashboard/EventOverviewTelemetrySnapshotZoom.test.tsx +757 -0
  108. package/Tests/App/Dashboard/ExceptionOccurrenceTrendZoom.test.tsx +675 -0
  109. package/Tests/App/Dashboard/ExceptionOccurrencesZoomSurvivesViewSwitch.test.tsx +405 -0
  110. package/Tests/App/Dashboard/ExceptionSignalPages.test.tsx +67 -3
  111. package/Tests/App/Dashboard/ExceptionTablesSpanStatus.test.tsx +515 -0
  112. package/Tests/App/Dashboard/ExplorerOwnZoomRegression.test.tsx +739 -0
  113. package/Tests/App/Dashboard/FlowBandwidthAxisCases.tsx +461 -0
  114. package/Tests/App/Dashboard/FlowBandwidthAxisKathmandu.test.tsx +25 -0
  115. package/Tests/App/Dashboard/FlowBandwidthAxisKolkata.test.tsx +25 -0
  116. package/Tests/App/Dashboard/FlowBandwidthAxisNewYork.test.tsx +25 -0
  117. package/Tests/App/Dashboard/FlowBandwidthAxisUtc.test.tsx +24 -0
  118. package/Tests/App/Dashboard/FlowTopTalkersZoomRefetch.test.tsx +722 -0
  119. package/Tests/App/Dashboard/FlowViewportGuard.test.tsx +2526 -0
  120. package/Tests/App/Dashboard/HostPagesSlowLoads.test.tsx +872 -0
  121. package/Tests/App/Dashboard/HostPagesTimeRangeZoom.test.tsx +1208 -0
  122. package/Tests/App/Dashboard/IncidentRootCauseTimeRangeZoom.test.tsx +358 -0
  123. package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +100 -9
  124. package/Tests/App/Dashboard/InfrastructureMetricsTabTimeRangeZoom.test.tsx +485 -0
  125. package/Tests/App/Dashboard/InvestigationDrawerCompanionZoom.test.tsx +655 -0
  126. package/Tests/App/Dashboard/InvestigationDrawerZoom.test.tsx +830 -0
  127. package/Tests/App/Dashboard/InvestigationNotStarted.test.tsx +338 -2
  128. package/Tests/App/Dashboard/InvoicesPayInvoice.test.tsx +454 -16
  129. package/Tests/App/Dashboard/KubernetesClusterAiAccessSettings.test.ts +799 -819
  130. package/Tests/App/Dashboard/KubernetesClusterAiAgentOverviewCard.test.tsx +367 -0
  131. package/Tests/App/Dashboard/KubernetesClusterAiAgentStatus.test.ts +1202 -0
  132. package/Tests/App/Dashboard/KubernetesClusterAiDocumentationMarkdown.test.ts +106 -0
  133. package/Tests/App/Dashboard/KubernetesClusterAiNavigation.test.tsx +354 -0
  134. package/Tests/App/Dashboard/KubernetesClusterAiPage.test.tsx +1884 -2054
  135. package/Tests/App/Dashboard/KubernetesClusterAiPageServerParity.test.ts +478 -179
  136. package/Tests/App/Dashboard/KubernetesClusterMetricTooltips.test.ts +50 -0
  137. package/Tests/App/Dashboard/KubernetesClusterOverviewTooltips.test.tsx +2 -1
  138. package/Tests/App/Dashboard/KubernetesClusterOverviewZoom.test.tsx +1217 -0
  139. package/Tests/App/Dashboard/KubernetesClusterPagesZoom.test.tsx +731 -0
  140. package/Tests/App/Dashboard/KubernetesCostsLoadRecovery.test.tsx +869 -0
  141. package/Tests/App/Dashboard/KubernetesCostsZoom.test.tsx +1214 -0
  142. package/Tests/App/Dashboard/KubernetesCostsZoomExactHours.test.tsx +692 -0
  143. package/Tests/App/Dashboard/KubernetesNetworkThroughputReload.test.tsx +902 -0
  144. package/Tests/App/Dashboard/KubernetesNetworkZoomBucketEdges.test.tsx +571 -0
  145. package/Tests/App/Dashboard/KubernetesOverviewZoomBucketEdges.test.tsx +697 -0
  146. package/Tests/App/Dashboard/KubernetesResourceMetricsTabZoom.test.tsx +633 -0
  147. package/Tests/App/Dashboard/LogsExplorerTimeRangeZoom.test.tsx +470 -0
  148. package/Tests/App/Dashboard/LogsInsightsTimeRangeZoom.test.tsx +948 -0
  149. package/Tests/App/Dashboard/LogsViewerPinnedWindowZoom.test.tsx +486 -0
  150. package/Tests/App/Dashboard/MetricChartsBucketInspectorDoubleClick.test.tsx +1001 -0
  151. package/Tests/App/Dashboard/MetricExplorerInvestigationAutoRefresh.test.tsx +514 -0
  152. package/Tests/App/Dashboard/MetricExplorerSavedViewZoomRoundTrip.test.tsx +581 -0
  153. package/Tests/App/Dashboard/MetricExplorerTimeRangeZoom.test.tsx +1162 -0
  154. package/Tests/App/Dashboard/MetricMonitorPreviewZoomLabel.test.ts +72 -0
  155. package/Tests/App/Dashboard/MetricRowSparklineTimeRangeZoom.test.tsx +204 -0
  156. package/Tests/App/Dashboard/MetricViewFirstResultLands.test.tsx +408 -0
  157. package/Tests/App/Dashboard/MetricViewOwnZoomResetPlacement.test.tsx +952 -0
  158. package/Tests/App/Dashboard/MetricViewTimeRange.test.ts +139 -0
  159. package/Tests/App/Dashboard/MetricViewTimeRangeZoom.test.tsx +556 -0
  160. package/Tests/App/Dashboard/MonitorMetricsChartWrappersZoom.test.tsx +552 -0
  161. package/Tests/App/Dashboard/MonitorMetricsTabsTimeRangeZoom.test.tsx +740 -0
  162. package/Tests/App/Dashboard/MonitorOverviewChartsTimeRangeZoom.test.tsx +735 -0
  163. package/Tests/App/Dashboard/MonitorStepPreviewsTimeRangeZoom.test.tsx +835 -0
  164. package/Tests/App/Dashboard/NetworkDeviceTimeRangeZoom.test.tsx +1115 -0
  165. package/Tests/App/Dashboard/OnCallPreventionGuards.test.tsx +186 -17
  166. package/Tests/App/Dashboard/OnCallRulesDeleteGuard.test.tsx +155 -46
  167. package/Tests/App/Dashboard/PasskeySettings.test.tsx +87 -2
  168. package/Tests/App/Dashboard/ProjectAISettingsPages.test.tsx +884 -0
  169. package/Tests/App/Dashboard/ProxmoxNodeRemove.test.tsx +621 -0
  170. package/Tests/App/Dashboard/ProxmoxOverviewSlowLoads.test.tsx +584 -0
  171. package/Tests/App/Dashboard/ProxmoxPagesTimeRangeZoom.test.tsx +823 -0
  172. package/Tests/App/Dashboard/ProxmoxRateChartReload.test.tsx +1253 -0
  173. package/Tests/App/Dashboard/PublicDashboardBoardWideZoom.test.tsx +605 -0
  174. package/Tests/App/Dashboard/RealRowsLineChart.tsx +250 -0
  175. package/Tests/App/Dashboard/RechartsHostsPlotCursor.test.tsx +513 -0
  176. package/Tests/App/Dashboard/RowActionPlacementOverrides.test.tsx +865 -0
  177. package/Tests/App/Dashboard/RumOverviewTimeRangeZoom.test.tsx +877 -0
  178. package/Tests/App/Dashboard/RunnerKubernetesAgentForm.test.tsx +20 -4
  179. package/Tests/App/Dashboard/RunnerSupersededByAiAgentNote.test.tsx +296 -0
  180. package/Tests/App/Dashboard/SecurityEventConnectionsTable.test.tsx +146 -36
  181. package/Tests/App/Dashboard/ServiceDatabaseSlowLoads.test.tsx +1166 -0
  182. package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +1243 -104
  183. package/Tests/App/Dashboard/ServiceOverviewTimeRangeZoom.test.tsx +792 -0
  184. package/Tests/App/Dashboard/SloBudgetBurnDownZoom.test.tsx +771 -0
  185. package/Tests/App/Dashboard/SloBudgetBurnDownZoomAffordances.test.tsx +380 -0
  186. package/Tests/App/Dashboard/SloEventMetricsBarChartZoom.test.tsx +369 -0
  187. package/Tests/App/Dashboard/SloHistoryChartsTimeRangeZoom.test.tsx +694 -0
  188. package/Tests/App/Dashboard/SloHistoryChartsZoomAffordances.test.tsx +497 -0
  189. package/Tests/App/Dashboard/SloMetricsTimeRangeZoom.test.tsx +541 -0
  190. package/Tests/App/Dashboard/SlowLoadHarness.ts +123 -0
  191. package/Tests/App/Dashboard/SnapshotExplorerPrimaryZoom.test.tsx +1194 -0
  192. package/Tests/App/Dashboard/SnapshotViewerPinnedWindow.test.tsx +620 -0
  193. package/Tests/App/Dashboard/TelemetrySnapshotPanelTimeRangeZoom.test.tsx +765 -0
  194. package/Tests/App/Dashboard/TelemetrySnapshotZoom.test.tsx +516 -0
  195. package/Tests/App/Dashboard/TelemetrySnapshotZoomAcrossTabs.test.tsx +827 -0
  196. package/Tests/App/Dashboard/TimeRangeZoomPageHarness.tsx +463 -0
  197. package/Tests/App/Dashboard/TraceChartEditorStatusSplit.test.tsx +543 -0
  198. package/Tests/App/Dashboard/TraceStatusPresentation.test.tsx +967 -0
  199. package/Tests/App/Dashboard/TraceTimelineMinimapZoom.test.tsx +315 -0
  200. package/Tests/App/Dashboard/TracesAnalyticsStatusSplit.test.tsx +778 -0
  201. package/Tests/App/Dashboard/TracesAnalyticsViewZoom.test.tsx +349 -0
  202. package/Tests/App/Dashboard/TracesExplorerTimeRangeZoom.test.tsx +551 -0
  203. package/Tests/App/Dashboard/TracesStatusChartAndFacet.test.tsx +687 -0
  204. package/Tests/App/Dashboard/TracesViewerStaleResponses.test.tsx +458 -0
  205. package/Tests/App/Dashboard/UptimeStripsExcludedFromZoom.test.tsx +406 -0
  206. package/Tests/App/Dashboard/VMwareOverviewSlowLoads.test.tsx +535 -0
  207. package/Tests/App/Dashboard/VMwarePagesTimeRangeZoom.test.tsx +606 -0
  208. package/Tests/App/Dashboard/ValueWidgetSparklineStaleHover.test.tsx +1292 -0
  209. package/Tests/App/Dashboard/ValueWidgetSparklineZoom.test.tsx +765 -0
  210. package/Tests/App/StatusPage/StatusPageResourceExplorer.test.tsx +98 -0
  211. package/Tests/Models/DatabaseModels/KubernetesAiAgentModel.test.ts +295 -0
  212. package/Tests/Models/DatabaseModels/TeamComplianceSettingModel.test.ts +347 -0
  213. package/Tests/Server/API/AIChatRouteAuthorization.test.ts +58 -0
  214. package/Tests/Server/API/AIGenerationProjectAIToggle.test.ts +1 -1
  215. package/Tests/Server/API/AIInvestigationAPI.test.ts +123 -45
  216. package/Tests/Server/API/AutoRemediationApproveClusterRound.test.ts +1329 -0
  217. package/Tests/Server/API/AutoRemediationApproveKubectlRecheck.test.ts +25 -2
  218. package/Tests/Server/API/KubernetesClusterAiAccessAPI.test.ts +2 -0
  219. package/Tests/Server/API/KubernetesClusterAiAccessAPIAgent.test.ts +903 -0
  220. package/Tests/Server/API/MicrosoftTeamsChatsRefreshRoute.test.ts +667 -0
  221. package/Tests/Server/API/MicrosoftTeamsManifest.test.ts +294 -6
  222. package/Tests/Server/API/ProxmoxResourceAPIRemoveNode.test.ts +1932 -0
  223. package/Tests/Server/Infrastructure/GlobalCache.test.ts +195 -0
  224. package/Tests/Server/Infrastructure/Postgres/AddKubernetesAiAgentAndAiDefaultsMigration.test.ts +623 -0
  225. package/Tests/Server/Infrastructure/Postgres/AddKubernetesClusterAiAccessMigration.test.ts +87 -10
  226. package/Tests/Server/Infrastructure/Postgres/AddTeamComplianceRuleScopeMigration.test.ts +622 -0
  227. package/Tests/Server/Middleware/TelemetryIngestBrowserKey.test.ts +61 -4
  228. package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerPinnedKey.test.ts +98 -2
  229. package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerRateLimit.test.ts +105 -2
  230. package/Tests/Server/Services/AIServiceBalanceBlocker.test.ts +282 -0
  231. package/Tests/Server/Services/AlertMonitorClearWritesNull.test.ts +409 -0
  232. package/Tests/Server/Services/AlertMonitorEditFeed.test.ts +474 -0
  233. package/Tests/Server/Services/AlertMonitorEditGuard.test.ts +610 -0
  234. package/Tests/Server/Services/AutoRemediationClusterRuleEngine.test.ts +2 -0
  235. package/Tests/Server/Services/AutoRemediationRuleCommandRunners.test.ts +2 -1
  236. package/Tests/Server/Services/IncidentAffectedMonitorsEdit.test.ts +1360 -0
  237. package/Tests/Server/Services/IncidentAlertSloLinkSurvivesEdit.test.ts +3 -1
  238. package/Tests/Server/Services/IncidentSeverityDeletePausesComplianceRules.test.ts +488 -0
  239. package/Tests/Server/Services/KubernetesAiAgentJobService.test.ts +329 -0
  240. package/Tests/Server/Services/KubernetesAiAgentServiceHelpers.test.ts +618 -0
  241. package/Tests/Server/Services/KubernetesAiAgentServiceLifecycle.test.ts +1444 -0
  242. package/Tests/Server/Services/KubernetesAiAgentServiceRegister.test.ts +1666 -0
  243. package/Tests/Server/Services/KubernetesClusterAiAccessDeletedRunnerRemedy.test.ts +140 -111
  244. package/Tests/Server/Services/KubernetesClusterAiAccessGapNextSteps.test.ts +267 -105
  245. package/Tests/Server/Services/KubernetesClusterAiAccessLegacySuperseded.test.ts +440 -0
  246. package/Tests/Server/Services/KubernetesClusterAiAccessService.test.ts +567 -411
  247. package/Tests/Server/Services/KubernetesClusterAiAccessTarget.test.ts +1168 -0
  248. package/Tests/Server/Services/KubernetesClusterAiSettingsPermission.test.ts +410 -12
  249. package/Tests/Server/Services/KubernetesClusterServiceAiAccessFeed.test.ts +55 -0
  250. package/Tests/Server/Services/ProjectServiceNewProjectAiDefaults.test.ts +288 -0
  251. package/Tests/Server/Services/ProxmoxResourcePostgres.test.ts +5633 -0
  252. package/Tests/Server/Services/ProxmoxResourceService.test.ts +6458 -5
  253. package/Tests/Server/Services/RunnerJobEnqueueKubectl.test.ts +48 -15
  254. package/Tests/Server/Services/RunnerJobEnqueueKubectlAiAgent.test.ts +620 -0
  255. package/Tests/Server/Services/RunnerJobKubectlWriteScope.test.ts +170 -3
  256. package/Tests/Server/Services/RunnerJobTimeoutReason.test.ts +72 -0
  257. package/Tests/Server/Services/RunnerServiceAgentName.test.ts +6 -4
  258. package/Tests/Server/Services/ScheduledMaintenanceAttachmentEdit.test.ts +2113 -0
  259. package/Tests/Server/Services/ScheduledMaintenanceMonitorRelease.test.ts +972 -0
  260. package/Tests/Server/Services/TeamComplianceSettingPostgres.test.ts +2237 -0
  261. package/Tests/Server/Services/TeamComplianceSettingService.test.ts +3988 -138
  262. package/Tests/Server/Utils/AI/ClusterAccessContext.test.ts +107 -1
  263. package/Tests/Server/Utils/AI/IncidentPostmortemRunner.test.ts +351 -0
  264. package/Tests/Server/Utils/AI/InvestigationClusterAccessWiring.test.ts +2 -0
  265. package/Tests/Server/Utils/AI/InvestigationEligibility.test.ts +310 -0
  266. package/Tests/Server/Utils/AI/KubectlInvestigationToolkit.test.ts +70 -0
  267. package/Tests/Server/Utils/AI/KubectlJobRunnerFinishedJob.test.ts +58 -0
  268. package/Tests/Server/Utils/AI/RemediationCommandToolsKubectl.test.ts +2 -0
  269. package/Tests/Server/Utils/AI/RemediationCommandToolsKubectlOutcome.test.ts +2 -0
  270. package/Tests/Server/Utils/AI/RemediationCommandToolsLiveCluster.test.ts +2 -0
  271. package/Tests/Server/Utils/AI/RemediationCommandToolsRunnerScope.test.ts +125 -1
  272. package/Tests/Server/Utils/AI/RemediationExecutionRunner.test.ts +252 -3
  273. package/Tests/Server/Utils/AI/RemediationExecutionRunnerClusterMode.test.ts +2 -0
  274. package/Tests/Server/Utils/AI/RemediationExecutionRunnerProjectGates.test.ts +240 -0
  275. package/Tests/Server/Utils/AI/RemediationKubectlCopyParity.test.ts +2 -0
  276. package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutor.test.ts +2 -0
  277. package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutorUnknownRun.test.ts +2 -0
  278. package/Tests/Server/Utils/AutoRemediation/CommandPlanVerification.test.ts +2 -0
  279. package/Tests/Server/Utils/Telemetry/HostAssetAttributesFromOtlp.test.ts +68 -0
  280. package/Tests/Server/Utils/Telemetry/InventoryMirrorPostgres.test.ts +370 -0
  281. package/Tests/Server/Utils/Telemetry/NetworkDeviceInventoryAttributes.test.ts +522 -0
  282. package/Tests/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.test.ts +155 -0
  283. package/Tests/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.test.ts +8631 -0
  284. package/Tests/Server/Utils/Telemetry/ProxmoxNativePush.test.ts +2852 -0
  285. package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +367 -1
  286. package/Tests/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.test.ts +45 -3
  287. package/Tests/Server/Utils/Workspace/MicrosoftTeamsChats.test.ts +25 -0
  288. package/Tests/Server/Utils/Workspace/MicrosoftTeamsGroupChatNames.test.ts +3119 -0
  289. package/Tests/Server/Utils/Workspace/MicrosoftTeamsGroupChatNamesPostgres.test.ts +1721 -0
  290. package/Tests/Types/Kubernetes/KubernetesAgentChartAiAccess.test.ts +581 -113
  291. package/Tests/Types/Kubernetes/KubernetesAiAgentAccessContract.test.ts +510 -0
  292. package/Tests/Types/Team/ComplianceRule.test.ts +1029 -0
  293. package/Tests/Types/Team/TeamComplianceStatus.test.ts +176 -0
  294. package/Tests/UI/Components/ActionButton/RowActions.test.tsx +641 -0
  295. package/Tests/UI/Components/ActionButton/RowActionsAccessibility.test.tsx +414 -0
  296. package/Tests/UI/Components/ActionButton/SplitActionButtons.test.ts +565 -0
  297. package/Tests/UI/Components/Charts/BarChartDragToSelect.test.tsx +386 -0
  298. package/Tests/UI/Components/Charts/ChartDoubleClickResetWithoutDblclick.test.tsx +1025 -0
  299. package/Tests/UI/Components/Charts/ChartGroupPageZoomHint.test.tsx +196 -0
  300. package/Tests/UI/Components/Charts/ChartPlaceholdersTakeDoubleClick.test.tsx +1861 -0
  301. package/Tests/UI/Components/Charts/ChartPlotClicksWhileResetArmed.test.tsx +382 -0
  302. package/Tests/UI/Components/Charts/ChartRangeSelection.test.tsx +704 -0
  303. package/Tests/UI/Components/Charts/ChartRangeSelectionDoubleClickReset.test.tsx +1481 -0
  304. package/Tests/UI/Components/Charts/ChartRangeSelectionRealRecharts.test.tsx +918 -0
  305. package/Tests/UI/Components/Charts/ChartRowBucketStart.test.ts +350 -0
  306. package/Tests/UI/Components/Charts/DoubleClickReset.test.tsx +996 -0
  307. package/Tests/UI/Components/Charts/HistogramDoubleClickResetRealRecharts.test.tsx +965 -0
  308. package/Tests/UI/Components/Charts/HistogramDriftThenRefetch.test.tsx +277 -0
  309. package/Tests/UI/Components/Charts/HistogramRangeSelectionDataChange.test.tsx +1757 -0
  310. package/Tests/UI/Components/Charts/HistogramRangeSelectionRealRecharts.test.tsx +560 -0
  311. package/Tests/UI/Components/Charts/TimeRangeZoom/ChartPageTimeRangeZoom.test.tsx +513 -0
  312. package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.test.tsx +845 -0
  313. package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.test.tsx +119 -0
  314. package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHintTailwind.test.tsx +274 -0
  315. package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.test.ts +433 -0
  316. package/Tests/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.test.tsx +863 -0
  317. package/Tests/UI/Components/Charts/TimeRangeZoom/ZoomDocsPromises.test.tsx +1147 -0
  318. package/Tests/UI/Components/DisabledButtonTooltip.test.tsx +14 -3
  319. package/Tests/UI/Components/EntityDropdownEscape.test.tsx +454 -0
  320. package/Tests/UI/Components/EntityDropdownFocusLeave.test.tsx +420 -0
  321. package/Tests/UI/Components/Forms/FormFieldDropdownDisabled.test.tsx +233 -0
  322. package/Tests/UI/Components/Input.test.tsx +80 -1
  323. package/Tests/UI/Components/LogsAnalyticsViewZoom.test.tsx +947 -0
  324. package/Tests/UI/Components/LogsHistogramDragTooltip.test.tsx +17 -5
  325. package/Tests/UI/Components/LogsViewerAnalyticsZoom.test.tsx +501 -0
  326. package/Tests/UI/Components/LogsViewerHistogramZoom.test.tsx +1 -1
  327. package/Tests/UI/Components/ModelTable/BaseModelTableBulkDelete.test.tsx +6 -4
  328. package/Tests/UI/Components/ModelTable/BaseModelTablePermissionGating.test.tsx +60 -8
  329. package/Tests/UI/Components/ModelTable/BaseModelTableRowActions.test.tsx +476 -0
  330. package/Tests/UI/Components/ModelTable/ModelTableWrapContent.test.tsx +7 -5
  331. package/Tests/UI/Components/MoreMenuPortal.test.tsx +623 -0
  332. package/Tests/UI/Components/StatusPage/ResourceGridActions.test.tsx +538 -0
  333. package/Tests/UI/Components/StatusPage/ResourceGridSelection.test.tsx +14 -4
  334. package/Tests/UI/Components/TableCellWrapping.test.tsx +3 -3
  335. package/Tests/UI/Components/TableRowActions.test.tsx +472 -0
  336. package/Tests/UI/Components/TelemetryHistogramDragTooltip.test.tsx +1 -1
  337. package/Tests/UI/Components/TelemetryViewer/TelemetryViewerHistogramClickZoom.test.tsx +1 -1
  338. package/Tests/UI/Components/TelemetryViewer/TelemetryViewerZoomScope.test.tsx +470 -0
  339. package/Tests/UI/Components/TelemetryViewer/UseViewerTimeRangeZoom.test.tsx +571 -0
  340. package/Tests/UI/Components/TelemetryViewer/ViewerDoubleClickResetUnderDataChange.test.tsx +928 -0
  341. package/Tests/UI/Components/TelemetryViewer/ViewerFollowsEnclosingZoom.test.tsx +1569 -0
  342. package/Tests/UI/Components/Toggle.test.tsx +66 -0
  343. package/Tests/UI/MicrosoftTeams/MicrosoftTeamsChatsCardRefreshNames.test.tsx +1023 -0
  344. package/Tests/UI/Rum/ChunkLoader.test.ts +201 -0
  345. package/Tests/UI/Rum/FidelityNotices.test.ts +54 -0
  346. package/Tests/UI/Rum/ReplayCorrelationPanel.test.tsx +72 -0
  347. package/Tests/UI/Rum/ReplayFrameCapture.test.ts +1009 -9
  348. package/Tests/UI/Rum/ReplayRecordedAssets.test.ts +1069 -0
  349. package/Tests/UI/Rum/ReplayStage.test.tsx +913 -2
  350. package/Tests/UI/Rum/SessionReplayTable.test.tsx +443 -8
  351. package/Tests/UI/Rum/SessionReplayUsersTable.test.tsx +439 -9
  352. package/Tests/UI/Rum/UserFlowTablesRowActions.test.tsx +501 -0
  353. package/Tests/Utils/AiRemediation/KubectlPolicyChartRbacParity.test.ts +47 -19
  354. package/Tests/Utils/AiRemediation/KubectlWriteScopeCallerParity.test.ts +2 -0
  355. package/Tests/Utils/AiRemediation/KubernetesAiAgentPolicyCopyParity.test.ts +396 -0
  356. package/Tests/Utils/Dashboard/DashboardTimeRangeZoom.test.ts +104 -1
  357. package/Types/AI/AIRunType.ts +9 -4
  358. package/Types/AI/InvestigationNotStartedReason.ts +6 -0
  359. package/Types/AutoRemediation/AiRemediationCommandPlan.ts +14 -16
  360. package/Types/AutoRemediation/AiRemediationCommandPolicyVerdict.ts +28 -0
  361. package/Types/Dashboard/DashboardComponents/DashboardTraceChartComponent.ts +4 -2
  362. package/Types/Kubernetes/KubernetesClusterAiAccess.ts +263 -32
  363. package/Types/Kubernetes/KubernetesClusterAiAccessPermissions.ts +20 -16
  364. package/Types/Kubernetes/KubernetesClusterAiInsights.ts +81 -0
  365. package/Types/Monitor/ProxmoxAlertTemplates.ts +4 -4
  366. package/Types/Team/ComplianceNotificationChannel.ts +22 -0
  367. package/Types/Team/ComplianceRule.ts +434 -0
  368. package/Types/Team/ComplianceRuleType.ts +21 -0
  369. package/Types/Team/TeamComplianceStatus.ts +81 -0
  370. package/Types/Telemetry/TelemetryIngestSurface.ts +28 -0
  371. package/UI/Components/ActionButton/ActionButtonSchema.ts +21 -0
  372. package/UI/Components/ActionButton/RowActions.tsx +238 -0
  373. package/UI/Components/ActionButton/SplitActionButtons.ts +173 -0
  374. package/UI/Components/Charts/Area/AreaChart.tsx +32 -2
  375. package/UI/Components/Charts/Bar/BarChart.tsx +40 -3
  376. package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +37 -11
  377. package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +80 -243
  378. package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +122 -31
  379. package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +95 -244
  380. package/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.ts +214 -0
  381. package/UI/Components/Charts/ChartLibrary/Utils/UseChartBucketsChange.ts +32 -0
  382. package/UI/Components/Charts/ChartLibrary/Utils/UseChartRangeSelection.ts +599 -0
  383. package/UI/Components/Charts/Line/LineChart.tsx +32 -2
  384. package/UI/Components/Charts/TimeRangeZoom/ResetTimeRangeZoomButton.tsx +70 -0
  385. package/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.tsx +226 -0
  386. package/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.tsx +73 -0
  387. package/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.ts +163 -0
  388. package/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.ts +215 -0
  389. package/UI/Components/Charts/Utils/DataPoint.ts +20 -8
  390. package/UI/Components/Charts/Utils/XAxis.ts +75 -0
  391. package/UI/Components/Charts/Utils/useHistogramRangeSelection.ts +493 -59
  392. package/UI/Components/EntityDropdown/EntityDropdown.tsx +107 -11
  393. package/UI/Components/Forms/Fields/FormField.tsx +10 -0
  394. package/UI/Components/Input/Input.tsx +90 -57
  395. package/UI/Components/List/ListRow.tsx +8 -74
  396. package/UI/Components/LogsViewer/LogsViewer.tsx +40 -11
  397. package/UI/Components/LogsViewer/components/LogTimeRangePicker.tsx +22 -6
  398. package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +209 -6
  399. package/UI/Components/LogsViewer/components/LogsHistogram.tsx +127 -84
  400. package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +3 -0
  401. package/UI/Components/ModelTable/BaseModelTable.tsx +12 -1
  402. package/UI/Components/MoreMenu/MoreMenu.tsx +365 -30
  403. package/UI/Components/MoreMenu/MoreMenuItem.tsx +70 -17
  404. package/UI/Components/OrderedStatesList/Item.tsx +7 -72
  405. package/UI/Components/StatusPage/ResourceGrid.tsx +126 -77
  406. package/UI/Components/Table/TableBody.tsx +8 -2
  407. package/UI/Components/Table/TableRow.tsx +20 -140
  408. package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +41 -10
  409. package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +47 -9
  410. package/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.tsx +25 -6
  411. package/UI/Components/TelemetryViewer/types.ts +2 -0
  412. package/UI/Components/TelemetryViewer/useViewerTimeRangeZoom.ts +448 -0
  413. package/UI/Components/Toggle/Toggle.tsx +26 -0
  414. package/UI/Components/Tooltip/Tooltip.tsx +8 -1
  415. package/UI/Styles/Theme.css +14 -2
  416. package/Utils/AiRemediation/CommandPolicy.ts +1 -1
  417. package/Utils/AiRemediation/KubectlPolicy.ts +1 -1
  418. package/Utils/Dashboard/DashboardTimeRangeZoom.ts +46 -2
  419. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  420. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  421. package/build/dist/Models/DatabaseModels/KubernetesAiAgent.js +385 -0
  422. package/build/dist/Models/DatabaseModels/KubernetesAiAgent.js.map +1 -0
  423. package/build/dist/Models/DatabaseModels/KubernetesCluster.js +3 -3
  424. package/build/dist/Models/DatabaseModels/KubernetesCluster.js.map +1 -1
  425. package/build/dist/Models/DatabaseModels/Project.js +56 -5
  426. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  427. package/build/dist/Models/DatabaseModels/ProxmoxResource.js +51 -1
  428. package/build/dist/Models/DatabaseModels/ProxmoxResource.js.map +1 -1
  429. package/build/dist/Models/DatabaseModels/RunnerJob.js +74 -0
  430. package/build/dist/Models/DatabaseModels/RunnerJob.js.map +1 -1
  431. package/build/dist/Models/DatabaseModels/TeamComplianceSetting.js +134 -3
  432. package/build/dist/Models/DatabaseModels/TeamComplianceSetting.js.map +1 -1
  433. package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
  434. package/build/dist/Server/API/AIChatAPI.js +6 -1
  435. package/build/dist/Server/API/AIChatAPI.js.map +1 -1
  436. package/build/dist/Server/API/AIInvestigationAPI.js +17 -10
  437. package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -1
  438. package/build/dist/Server/API/AutoRemediationAPI.js +144 -24
  439. package/build/dist/Server/API/AutoRemediationAPI.js.map +1 -1
  440. package/build/dist/Server/API/KubernetesClusterAiAccessAPI.js +436 -22
  441. package/build/dist/Server/API/KubernetesClusterAiAccessAPI.js.map +1 -1
  442. package/build/dist/Server/API/MicrosoftTeamsAPI.js +69 -13
  443. package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
  444. package/build/dist/Server/API/ProxmoxResourceAPI.js +187 -14
  445. package/build/dist/Server/API/ProxmoxResourceAPI.js.map +1 -1
  446. package/build/dist/Server/Infrastructure/GlobalCache.js +27 -0
  447. package/build/dist/Server/Infrastructure/GlobalCache.js.map +1 -1
  448. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796000000000-AddTeamComplianceRuleScope.js +50 -0
  449. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796000000000-AddTeamComplianceRuleScope.js.map +1 -0
  450. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796100000000-AddKubernetesAiAgentAndAiDefaults.js +84 -0
  451. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796100000000-AddKubernetesAiAgentAndAiDefaults.js.map +1 -0
  452. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796200000000-AddProxmoxResourceNativePushColumns.js +31 -0
  453. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796200000000-AddProxmoxResourceNativePushColumns.js.map +1 -0
  454. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  455. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  456. package/build/dist/Server/Middleware/TelemetryIngest.js +34 -19
  457. package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
  458. package/build/dist/Server/Services/AIService.js +59 -1
  459. package/build/dist/Server/Services/AIService.js.map +1 -1
  460. package/build/dist/Server/Services/AlertService.js +248 -13
  461. package/build/dist/Server/Services/AlertService.js.map +1 -1
  462. package/build/dist/Server/Services/AlertSeverityService.js +51 -15
  463. package/build/dist/Server/Services/AlertSeverityService.js.map +1 -1
  464. package/build/dist/Server/Services/AutoRemediationRuleService.js +1 -1
  465. package/build/dist/Server/Services/AutoRemediationRuleService.js.map +1 -1
  466. package/build/dist/Server/Services/IncidentService.js +199 -72
  467. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  468. package/build/dist/Server/Services/IncidentSeverityService.js +51 -15
  469. package/build/dist/Server/Services/IncidentSeverityService.js.map +1 -1
  470. package/build/dist/Server/Services/KubernetesAiAgentJobService.js +158 -0
  471. package/build/dist/Server/Services/KubernetesAiAgentJobService.js.map +1 -0
  472. package/build/dist/Server/Services/KubernetesAiAgentService.js +1145 -0
  473. package/build/dist/Server/Services/KubernetesAiAgentService.js.map +1 -0
  474. package/build/dist/Server/Services/KubernetesClusterAiAccessService.js +842 -371
  475. package/build/dist/Server/Services/KubernetesClusterAiAccessService.js.map +1 -1
  476. package/build/dist/Server/Services/KubernetesClusterService.js +86 -17
  477. package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
  478. package/build/dist/Server/Services/ProjectService.js +22 -0
  479. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  480. package/build/dist/Server/Services/ProxmoxResourceService.js +258 -3
  481. package/build/dist/Server/Services/ProxmoxResourceService.js.map +1 -1
  482. package/build/dist/Server/Services/RunnerJobService.js +178 -92
  483. package/build/dist/Server/Services/RunnerJobService.js.map +1 -1
  484. package/build/dist/Server/Services/RunnerService.js +1 -1
  485. package/build/dist/Server/Services/RunnerService.js.map +1 -1
  486. package/build/dist/Server/Services/ScheduledMaintenanceService.js +535 -3
  487. package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
  488. package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js +316 -46
  489. package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js.map +1 -1
  490. package/build/dist/Server/Services/TeamComplianceSettingService.js +833 -7
  491. package/build/dist/Server/Services/TeamComplianceSettingService.js.map +1 -1
  492. package/build/dist/Server/Utils/AI/ClusterAccess/ClusterAccessContext.js +52 -9
  493. package/build/dist/Server/Utils/AI/ClusterAccess/ClusterAccessContext.js.map +1 -1
  494. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.js +13 -9
  495. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.js.map +1 -1
  496. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlJobRunner.js +7 -3
  497. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlJobRunner.js.map +1 -1
  498. package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js +29 -15
  499. package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js.map +1 -1
  500. package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js +56 -20
  501. package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js.map +1 -1
  502. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js +39 -5
  503. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js.map +1 -1
  504. package/build/dist/Server/Utils/AI/SRE/IncidentPostmortemRunner.js +30 -3
  505. package/build/dist/Server/Utils/AI/SRE/IncidentPostmortemRunner.js.map +1 -1
  506. package/build/dist/Server/Utils/AI/SRE/InvestigationEligibility.js +13 -2
  507. package/build/dist/Server/Utils/AI/SRE/InvestigationEligibility.js.map +1 -1
  508. package/build/dist/Server/Utils/AI/SRE/InvestigationGrader.js +1 -1
  509. package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js +4 -2
  510. package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js.map +1 -1
  511. package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js +111 -6
  512. package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js.map +1 -1
  513. package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js +40 -4
  514. package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js.map +1 -1
  515. package/build/dist/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.js +296 -0
  516. package/build/dist/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.js.map +1 -0
  517. package/build/dist/Server/Utils/Telemetry/ProxmoxNativePush.js +699 -0
  518. package/build/dist/Server/Utils/Telemetry/ProxmoxNativePush.js.map +1 -0
  519. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +77 -19
  520. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
  521. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +505 -42
  522. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  523. package/build/dist/Types/AI/AIRunType.js +9 -4
  524. package/build/dist/Types/AI/AIRunType.js.map +1 -1
  525. package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js +10 -16
  526. package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js.map +1 -1
  527. package/build/dist/Types/AutoRemediation/AiRemediationCommandPolicyVerdict.js +28 -0
  528. package/build/dist/Types/AutoRemediation/AiRemediationCommandPolicyVerdict.js.map +1 -0
  529. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccess.js +72 -16
  530. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccess.js.map +1 -1
  531. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccessPermissions.js +20 -16
  532. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccessPermissions.js.map +1 -1
  533. package/build/dist/Types/Kubernetes/KubernetesClusterAiInsights.js +17 -0
  534. package/build/dist/Types/Kubernetes/KubernetesClusterAiInsights.js.map +1 -0
  535. package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js +4 -4
  536. package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js.map +1 -1
  537. package/build/dist/Types/Team/ComplianceNotificationChannel.js +23 -0
  538. package/build/dist/Types/Team/ComplianceNotificationChannel.js.map +1 -0
  539. package/build/dist/Types/Team/ComplianceRule.js +304 -0
  540. package/build/dist/Types/Team/ComplianceRule.js.map +1 -0
  541. package/build/dist/Types/Team/ComplianceRuleType.js +21 -0
  542. package/build/dist/Types/Team/ComplianceRuleType.js.map +1 -1
  543. package/build/dist/Types/Team/TeamComplianceStatus.js +2 -0
  544. package/build/dist/Types/Team/TeamComplianceStatus.js.map +1 -0
  545. package/build/dist/Types/Telemetry/TelemetryIngestSurface.js +25 -0
  546. package/build/dist/Types/Telemetry/TelemetryIngestSurface.js.map +1 -1
  547. package/build/dist/UI/Components/ActionButton/ActionButtonSchema.js +20 -1
  548. package/build/dist/UI/Components/ActionButton/ActionButtonSchema.js.map +1 -1
  549. package/build/dist/UI/Components/ActionButton/RowActions.js +113 -0
  550. package/build/dist/UI/Components/ActionButton/RowActions.js.map +1 -0
  551. package/build/dist/UI/Components/ActionButton/SplitActionButtons.js +71 -0
  552. package/build/dist/UI/Components/ActionButton/SplitActionButtons.js.map +1 -0
  553. package/build/dist/UI/Components/Charts/Area/AreaChart.js +16 -1
  554. package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
  555. package/build/dist/UI/Components/Charts/Bar/BarChart.js +16 -1
  556. package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
  557. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +27 -5
  558. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
  559. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +61 -194
  560. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
  561. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +84 -26
  562. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
  563. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +75 -196
  564. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
  565. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js +133 -0
  566. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js.map +1 -1
  567. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartBucketsChange.js +26 -0
  568. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartBucketsChange.js.map +1 -0
  569. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartRangeSelection.js +371 -0
  570. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartRangeSelection.js.map +1 -0
  571. package/build/dist/UI/Components/Charts/Line/LineChart.js +16 -1
  572. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  573. package/build/dist/UI/Components/Charts/TimeRangeZoom/ResetTimeRangeZoomButton.js +35 -0
  574. package/build/dist/UI/Components/Charts/TimeRangeZoom/ResetTimeRangeZoomButton.js.map +1 -0
  575. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.js +107 -0
  576. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.js.map +1 -0
  577. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.js +41 -0
  578. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.js.map +1 -0
  579. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.js +117 -0
  580. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.js.map +1 -0
  581. package/build/dist/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.js +121 -0
  582. package/build/dist/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.js.map +1 -0
  583. package/build/dist/UI/Components/Charts/Utils/DataPoint.js +15 -7
  584. package/build/dist/UI/Components/Charts/Utils/DataPoint.js.map +1 -1
  585. package/build/dist/UI/Components/Charts/Utils/XAxis.js +70 -0
  586. package/build/dist/UI/Components/Charts/Utils/XAxis.js.map +1 -1
  587. package/build/dist/UI/Components/Charts/Utils/useHistogramRangeSelection.js +305 -38
  588. package/build/dist/UI/Components/Charts/Utils/useHistogramRangeSelection.js.map +1 -1
  589. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js +89 -14
  590. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js.map +1 -1
  591. package/build/dist/UI/Components/Forms/Fields/FormField.js +11 -2
  592. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  593. package/build/dist/UI/Components/Input/Input.js +69 -54
  594. package/build/dist/UI/Components/Input/Input.js.map +1 -1
  595. package/build/dist/UI/Components/List/ListRow.js +4 -39
  596. package/build/dist/UI/Components/List/ListRow.js.map +1 -1
  597. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +31 -8
  598. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  599. package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js +10 -1
  600. package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js.map +1 -1
  601. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +139 -21
  602. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
  603. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +30 -8
  604. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -1
  605. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +1 -1
  606. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -1
  607. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +10 -0
  608. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  609. package/build/dist/UI/Components/MoreMenu/MoreMenu.js +253 -14
  610. package/build/dist/UI/Components/MoreMenu/MoreMenu.js.map +1 -1
  611. package/build/dist/UI/Components/MoreMenu/MoreMenuItem.js +43 -13
  612. package/build/dist/UI/Components/MoreMenu/MoreMenuItem.js.map +1 -1
  613. package/build/dist/UI/Components/OrderedStatesList/Item.js +2 -38
  614. package/build/dist/UI/Components/OrderedStatesList/Item.js.map +1 -1
  615. package/build/dist/UI/Components/StatusPage/ResourceGrid.js +53 -15
  616. package/build/dist/UI/Components/StatusPage/ResourceGrid.js.map +1 -1
  617. package/build/dist/UI/Components/Table/TableBody.js +6 -2
  618. package/build/dist/UI/Components/Table/TableBody.js.map +1 -1
  619. package/build/dist/UI/Components/Table/TableRow.js +12 -75
  620. package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
  621. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +32 -7
  622. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
  623. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +27 -9
  624. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
  625. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js +11 -1
  626. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js.map +1 -1
  627. package/build/dist/UI/Components/TelemetryViewer/useViewerTimeRangeZoom.js +272 -0
  628. package/build/dist/UI/Components/TelemetryViewer/useViewerTimeRangeZoom.js.map +1 -0
  629. package/build/dist/UI/Components/Toggle/Toggle.js +14 -1
  630. package/build/dist/UI/Components/Toggle/Toggle.js.map +1 -1
  631. package/build/dist/UI/Components/Tooltip/Tooltip.js +1 -1
  632. package/build/dist/UI/Components/Tooltip/Tooltip.js.map +1 -1
  633. package/build/dist/Utils/AiRemediation/CommandPolicy.js +1 -1
  634. package/build/dist/Utils/AiRemediation/CommandPolicy.js.map +1 -1
  635. package/build/dist/Utils/AiRemediation/KubectlPolicy.js +1 -1
  636. package/build/dist/Utils/AiRemediation/KubectlPolicy.js.map +1 -1
  637. package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js +27 -1
  638. package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js.map +1 -1
  639. package/package.json +1 -1
@@ -0,0 +1,1569 @@
1
+ /** @timezone UTC */
2
+
3
+ import "@testing-library/jest-dom";
4
+ import {
5
+ afterEach,
6
+ beforeEach,
7
+ describe,
8
+ expect,
9
+ jest,
10
+ test,
11
+ } from "@jest/globals";
12
+ import {
13
+ act,
14
+ cleanup,
15
+ fireEvent,
16
+ render,
17
+ screen,
18
+ waitFor,
19
+ within,
20
+ } from "@testing-library/react";
21
+ import React, {
22
+ FunctionComponent,
23
+ ReactElement,
24
+ useEffect,
25
+ useRef,
26
+ useState,
27
+ } from "react";
28
+ import getJestMockFunction, { MockFunction } from "../../../MockType";
29
+
30
+ /*
31
+ * Issue #4105: an explorer (the logs viewer, and the traces, exceptions and
32
+ * security events viewers on the telemetry shell) keeps a zoom of its own
33
+ * over the window its host owns, and shadows any zoom offered around it. A
34
+ * telemetry snapshot pins its primary explorer to the window the snapshot
35
+ * shows and offers the snapshot's zoom around it: there the explorer must
36
+ * FOLLOW that zoom instead, so a drag on its histogram retimes the whole
37
+ * snapshot and one reset undoes it all.
38
+ *
39
+ * The rule (useViewerTimeRangeZoom, shouldFollowEnclosingZoom): an explorer
40
+ * follows the zoom around it only while that zoom says it is over the
41
+ * explorer's very window. Then a drag on any of its charts goes to that
42
+ * zoom, a double-click and "Reset zoom" go to that zoom's reset (offered
43
+ * only while it is zoomed), and the explorer keeps no zoom of its own, so
44
+ * its picker offers no second "Reset zoom". Anywhere else - no zoom around,
45
+ * or a zoom over another window - it zooms on its own exactly as before.
46
+ *
47
+ * Driven here with the hook in a page that zooms, and with the real
48
+ * telemetry shell and logs viewer in one. Recharts is stood in for so the
49
+ * histograms and the analytics chart can be dragged (see
50
+ * LogsHistogramDragTooltip.test.tsx).
51
+ */
52
+
53
+ const getListMock: MockFunction = getJestMockFunction();
54
+ const postMock: MockFunction = getJestMockFunction();
55
+
56
+ // The logs viewer loads services and log attributes on mount.
57
+ jest.mock("../../../../UI/Utils/ModelAPI/ModelAPI", () => {
58
+ return {
59
+ __esModule: true,
60
+ default: {
61
+ getList: (...args: Array<unknown>) => {
62
+ return getListMock(...args);
63
+ },
64
+ getCommonHeaders: () => {
65
+ return {};
66
+ },
67
+ },
68
+ };
69
+ });
70
+
71
+ jest.mock("../../../../UI/Utils/API/API", () => {
72
+ return {
73
+ __esModule: true,
74
+ default: {
75
+ post: (...args: Array<unknown>) => {
76
+ return postMock(...args);
77
+ },
78
+ getFriendlyErrorMessage: (error: Error) => {
79
+ return error.message;
80
+ },
81
+ getFriendlyMessage: (error: Error) => {
82
+ return error.message;
83
+ },
84
+ },
85
+ };
86
+ });
87
+
88
+ jest.mock("recharts", () => {
89
+ const react: typeof React = jest.requireActual("react") as typeof React;
90
+
91
+ interface StubRow {
92
+ time: string;
93
+ }
94
+
95
+ interface StubChartProps {
96
+ data: Array<StubRow>;
97
+ children?: React.ReactNode;
98
+ onMouseDown?: (state: { activeLabel: string }) => void;
99
+ onMouseMove?: (state: { activeLabel: string }) => void;
100
+ onMouseUp?: (state: { activeLabel: string }) => void;
101
+ }
102
+
103
+ const chart: (props: StubChartProps) => React.ReactElement = (
104
+ props: StubChartProps,
105
+ ): React.ReactElement => {
106
+ return react.createElement(
107
+ "div",
108
+ null,
109
+ props.data.map((row: StubRow) => {
110
+ return react.createElement("div", {
111
+ key: row.time,
112
+ "data-testid": `bucket-${row.time}`,
113
+ onMouseDown: () => {
114
+ props.onMouseDown?.({ activeLabel: row.time });
115
+ },
116
+ onMouseMove: () => {
117
+ props.onMouseMove?.({ activeLabel: row.time });
118
+ },
119
+ onMouseUp: () => {
120
+ props.onMouseUp?.({ activeLabel: row.time });
121
+ },
122
+ });
123
+ }),
124
+ props.children,
125
+ );
126
+ };
127
+
128
+ const nothing: () => null = (): null => {
129
+ return null;
130
+ };
131
+
132
+ return {
133
+ __esModule: true,
134
+ ResponsiveContainer: (props: { children: React.ReactNode }) => {
135
+ return react.createElement("div", null, props.children);
136
+ },
137
+ AreaChart: chart,
138
+ BarChart: chart,
139
+ Area: nothing,
140
+ Bar: nothing,
141
+ XAxis: nothing,
142
+ YAxis: nothing,
143
+ CartesianGrid: nothing,
144
+ Tooltip: nothing,
145
+ ReferenceArea: nothing,
146
+ };
147
+ });
148
+
149
+ // Imported after the mocks so the viewers pick the stand-ins up.
150
+ import useViewerTimeRangeZoom, {
151
+ shouldFollowEnclosingZoom,
152
+ ViewerTimeRangeZoom,
153
+ } from "../../../../UI/Components/TelemetryViewer/useViewerTimeRangeZoom";
154
+ import TelemetryViewer from "../../../../UI/Components/TelemetryViewer/TelemetryViewer";
155
+ import LogsViewer from "../../../../UI/Components/LogsViewer/LogsViewer";
156
+ import { LOGS_ANALYTICS_TIMESERIES_TEST_ID } from "../../../../UI/Components/LogsViewer/components/LogsAnalyticsView";
157
+ import {
158
+ ChartTimeRangeZoomContextValue,
159
+ TimeRangeZoomProvider,
160
+ useChartTimeRangeZoom,
161
+ } from "../../../../UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext";
162
+ import useTimeRangeZoom, {
163
+ TimeRangeZoom,
164
+ } from "../../../../UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom";
165
+ import TimeRangeZoomUtil from "../../../../UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil";
166
+ import ResetTimeRangeZoomButton, {
167
+ RESET_TIME_RANGE_ZOOM_BUTTON_TEST_ID,
168
+ } from "../../../../UI/Components/Charts/TimeRangeZoom/ResetTimeRangeZoomButton";
169
+ import { getTimeRangeButtonLabel } from "../../../../UI/Components/Date/TimeRangePickerDropdown";
170
+ import { HistogramBucket as LogsHistogramBucket } from "../../../../UI/Components/LogsViewer/types";
171
+ import {
172
+ HistogramBucket,
173
+ HistogramSeriesOption,
174
+ } from "../../../../UI/Components/TelemetryViewer/types";
175
+ import InBetween from "../../../../Types/BaseDatabase/InBetween";
176
+ import LogSeverity from "../../../../Types/Log/LogSeverity";
177
+ import RangeStartAndEndDateTime from "../../../../Types/Time/RangeStartAndEndDateTime";
178
+ import TimeRange from "../../../../Types/Time/TimeRange";
179
+
180
+ const NOW: Date = new Date("2026-09-28T12:00:00.000Z");
181
+ const MINUTE_MS: number = 60 * 1000;
182
+
183
+ function custom(startIso: string, endIso: string): RangeStartAndEndDateTime {
184
+ return {
185
+ range: TimeRange.CUSTOM,
186
+ startAndEndDate: new InBetween<Date>(new Date(startIso), new Date(endIso)),
187
+ };
188
+ }
189
+
190
+ // The window the explorer is pinned to, and the zoom around it is over.
191
+ const WINDOW_TEXT: string =
192
+ "2026-09-28T10:00:00.000Z..2026-09-28T10:15:00.000Z";
193
+ const WINDOW: RangeStartAndEndDateTime = custom(
194
+ "2026-09-28T10:00:00.000Z",
195
+ "2026-09-28T10:15:00.000Z",
196
+ );
197
+ // A window of the page's that the explorer does not show.
198
+ const OTHER_TEXT: string = "2026-09-28T08:00:00.000Z..2026-09-28T09:00:00.000Z";
199
+ const OTHER: RangeStartAndEndDateTime = custom(
200
+ "2026-09-28T08:00:00.000Z",
201
+ "2026-09-28T09:00:00.000Z",
202
+ );
203
+
204
+ // A drag inside WINDOW, and a second one inside that.
205
+ const SLICE_START: string = "2026-09-28T10:03:00.000Z";
206
+ const SLICE_END: string = "2026-09-28T10:05:00.000Z";
207
+ const SLICE_TEXT: string = `${SLICE_START}..${SLICE_END}`;
208
+ const NESTED_START: string = "2026-09-28T10:03:30.000Z";
209
+ const NESTED_END: string = "2026-09-28T10:04:00.000Z";
210
+
211
+ // Volume histogram bars, one minute each; a drag from A to B covers both.
212
+ const BAR_A: string = "2026-09-28 10:03:00";
213
+ const BAR_B: string = "2026-09-28 10:04:00";
214
+ const BAR_C: string = "2026-09-28 10:05:00";
215
+
216
+ // The logs viewer's Analytics timeseries buckets, from the API.
217
+ const ANALYTICS_A: string = "2026-09-28 10:07:00";
218
+ const ANALYTICS_B: string = "2026-09-28 10:08:00";
219
+
220
+ const SERIES: Array<HistogramSeriesOption> = [
221
+ { key: "ok", label: "OK", color: "#34d399" },
222
+ ];
223
+
224
+ const TELEMETRY_BUCKETS: Array<HistogramBucket> = [BAR_A, BAR_B, BAR_C].map(
225
+ (time: string): HistogramBucket => {
226
+ return { time, series: "ok", count: 3 };
227
+ },
228
+ );
229
+
230
+ const LOGS_BUCKETS: Array<LogsHistogramBucket> = [BAR_A, BAR_B, BAR_C].map(
231
+ (time: string): LogsHistogramBucket => {
232
+ return { time, severity: LogSeverity.Error, count: 3 };
233
+ },
234
+ );
235
+
236
+ // Dates restored from a URL can arrive as strings; print both the same way.
237
+ function iso(date: unknown): string {
238
+ return new Date(date as Date | string).toISOString();
239
+ }
240
+
241
+ function windowOf(range: RangeStartAndEndDateTime | null | undefined): string {
242
+ if (!range) {
243
+ return "none";
244
+ }
245
+
246
+ if (range.range !== TimeRange.CUSTOM) {
247
+ return range.range;
248
+ }
249
+
250
+ return `${iso(range.startAndEndDate!.startValue)}..${iso(
251
+ range.startAndEndDate!.endValue,
252
+ )}`;
253
+ }
254
+
255
+ function read(testId: string): string {
256
+ return screen.getByTestId(testId).textContent || "";
257
+ }
258
+
259
+ // What a page offers the charts below it (see TimeRangeZoomProvider).
260
+ function offered(
261
+ timeRange: RangeStartAndEndDateTime | null,
262
+ isZoomed: boolean = false,
263
+ ): ChartTimeRangeZoomContextValue {
264
+ return {
265
+ onTimeRangeSelect: (): void => {},
266
+ onTimeRangeReset: isZoomed ? (): void => {} : undefined,
267
+ isZoomed: isZoomed,
268
+ rangeBeforeZoom: isZoomed ? OTHER : null,
269
+ timeRange: timeRange,
270
+ };
271
+ }
272
+
273
+ type UsePinFollowFunction = (
274
+ pinnedTo: RangeStartAndEndDateTime | null,
275
+ apply: (range: RangeStartAndEndDateTime) => void,
276
+ ) => void;
277
+
278
+ /*
279
+ * How the explorers' hosts follow a window they are pinned to: a new one,
280
+ * by value, is applied; the same one handed over again is not (see the
281
+ * pin-follow effects of DashboardLogsViewer, TracesViewer and
282
+ * ExceptionsViewer). Like theirs, it lands a render after the pin moves.
283
+ */
284
+ const usePinFollow: UsePinFollowFunction = (
285
+ pinnedTo: RangeStartAndEndDateTime | null,
286
+ apply: (range: RangeStartAndEndDateTime) => void,
287
+ ): void => {
288
+ const pinKey: string = TimeRangeZoomUtil.getRangeKey(pinnedTo);
289
+ const lastPinKey: React.MutableRefObject<string> = useRef<string>(pinKey);
290
+
291
+ useEffect(() => {
292
+ if (lastPinKey.current === pinKey) {
293
+ return;
294
+ }
295
+
296
+ lastPinKey.current = pinKey;
297
+
298
+ if (pinnedTo) {
299
+ apply(pinnedTo);
300
+ }
301
+ }, [pinKey]);
302
+ };
303
+
304
+ const pageChangeSpy: MockFunction = getJestMockFunction();
305
+ const explorerSelectSpy: MockFunction = getJestMockFunction();
306
+ const explorerChangeSpy: MockFunction = getJestMockFunction();
307
+
308
+ let latestPage: TimeRangeZoom | null = null;
309
+ let setPageRangeFromOutside:
310
+ | ((range: RangeStartAndEndDateTime) => void)
311
+ | null = null;
312
+
313
+ function page(): TimeRangeZoom {
314
+ if (!latestPage) {
315
+ throw new Error("the page has not rendered");
316
+ }
317
+ return latestPage;
318
+ }
319
+
320
+ function pageMovesTo(range: RangeStartAndEndDateTime): void {
321
+ act(() => {
322
+ setPageRangeFromOutside!(range);
323
+ });
324
+ }
325
+
326
+ interface ZoomingPageProps {
327
+ initialRange: RangeStartAndEndDateTime;
328
+ // Whether the explorer is pinned to the page's range, as a snapshot's is.
329
+ pinExplorer: boolean;
330
+ // Whether the page offers its zoom around the explorer at all.
331
+ offersZoom?: boolean | undefined;
332
+ explorer: (pinnedTo: RangeStartAndEndDateTime | null) => ReactElement;
333
+ }
334
+
335
+ /*
336
+ * A page that owns a range and zooms it, with its own "Reset zoom" (the way
337
+ * the telemetry snapshot offers one beside its badge), around an explorer.
338
+ */
339
+ const ZoomingPage: FunctionComponent<ZoomingPageProps> = (
340
+ props: ZoomingPageProps,
341
+ ): ReactElement => {
342
+ const [pageRange, setPageRange] = useState<RangeStartAndEndDateTime>(
343
+ props.initialRange,
344
+ );
345
+
346
+ const zoom: TimeRangeZoom = useTimeRangeZoom({
347
+ timeRange: pageRange,
348
+ onTimeRangeChange: (next: RangeStartAndEndDateTime): void => {
349
+ pageChangeSpy(next);
350
+ setPageRange(next);
351
+ },
352
+ });
353
+
354
+ latestPage = zoom;
355
+ setPageRangeFromOutside = setPageRange;
356
+
357
+ const explorer: ReactElement = (
358
+ <div data-testid="explorer">
359
+ {props.explorer(props.pinExplorer ? pageRange : null)}
360
+ </div>
361
+ );
362
+
363
+ return (
364
+ <div>
365
+ <span data-testid="page-window">{windowOf(pageRange)}</span>
366
+ <span data-testid="page-zoomed">{String(zoom.isZoomed)}</span>
367
+ {props.offersZoom === false ? (
368
+ explorer
369
+ ) : (
370
+ <TimeRangeZoomProvider zoom={zoom}>
371
+ <div data-testid="page-reset">
372
+ <ResetTimeRangeZoomButton />
373
+ </div>
374
+ {explorer}
375
+ </TimeRangeZoomProvider>
376
+ )}
377
+ </div>
378
+ );
379
+ };
380
+
381
+ beforeEach(() => {
382
+ jest.useFakeTimers();
383
+ jest.setSystemTime(NOW);
384
+ latestPage = null;
385
+ setPageRangeFromOutside = null;
386
+ pageChangeSpy.mockReset();
387
+ explorerSelectSpy.mockReset();
388
+ explorerChangeSpy.mockReset();
389
+ getListMock.mockReset();
390
+ postMock.mockReset();
391
+ getListMock.mockImplementation(async () => {
392
+ return { data: [], count: 0, skip: 0, limit: 0 };
393
+ });
394
+ postMock.mockImplementation(
395
+ async (args: { url: { toString: () => string } }) => {
396
+ if (args.url.toString().includes("/telemetry/logs/analytics")) {
397
+ return {
398
+ data: {
399
+ data: [ANALYTICS_A, ANALYTICS_B].map((time: string) => {
400
+ return { time, count: 4, groupValues: {} };
401
+ }),
402
+ },
403
+ };
404
+ }
405
+ return { data: {} };
406
+ },
407
+ );
408
+ });
409
+
410
+ afterEach(() => {
411
+ cleanup();
412
+ jest.useRealTimers();
413
+ });
414
+
415
+ describe("shouldFollowEnclosingZoom: which zoom around an explorer it follows", () => {
416
+ test("no zoom around the explorer: nothing to follow", () => {
417
+ expect(shouldFollowEnclosingZoom(null, WINDOW)).toBe(false);
418
+ });
419
+
420
+ test("a zoom over another window is not followed", () => {
421
+ expect(shouldFollowEnclosingZoom(offered(OTHER), WINDOW)).toBe(false);
422
+ // Zoomed or not.
423
+ expect(shouldFollowEnclosingZoom(offered(OTHER, true), WINDOW)).toBe(false);
424
+ });
425
+
426
+ test("a window that shares one edge is another window", () => {
427
+ expect(
428
+ shouldFollowEnclosingZoom(
429
+ offered(custom("2026-09-28T10:00:00.000Z", "2026-09-28T10:14:59.000Z")),
430
+ WINDOW,
431
+ ),
432
+ ).toBe(false);
433
+ expect(
434
+ shouldFollowEnclosingZoom(
435
+ offered(custom("2026-09-28T10:00:01.000Z", "2026-09-28T10:15:00.000Z")),
436
+ WINDOW,
437
+ ),
438
+ ).toBe(false);
439
+ });
440
+
441
+ test("a relative range and a custom one are different windows", () => {
442
+ expect(
443
+ shouldFollowEnclosingZoom(
444
+ offered({ range: TimeRange.PAST_ONE_HOUR }),
445
+ WINDOW,
446
+ ),
447
+ ).toBe(false);
448
+ expect(
449
+ shouldFollowEnclosingZoom(offered(WINDOW), {
450
+ range: TimeRange.PAST_ONE_HOUR,
451
+ }),
452
+ ).toBe(false);
453
+ });
454
+
455
+ test("a zoom over the explorer's very window is followed, compared by value", () => {
456
+ expect(shouldFollowEnclosingZoom(offered(WINDOW), WINDOW)).toBe(true);
457
+ // New objects, the same instants.
458
+ expect(
459
+ shouldFollowEnclosingZoom(
460
+ offered(custom("2026-09-28T10:00:00.000Z", "2026-09-28T10:15:00.000Z")),
461
+ custom("2026-09-28T10:00:00.000Z", "2026-09-28T10:15:00.000Z"),
462
+ ),
463
+ ).toBe(true);
464
+ // Zoomed or not: the zoom's range is where it is now.
465
+ expect(shouldFollowEnclosingZoom(offered(WINDOW, true), WINDOW)).toBe(true);
466
+ });
467
+
468
+ test("a window restored with string dates is still the same window", () => {
469
+ expect(
470
+ shouldFollowEnclosingZoom(offered(WINDOW), {
471
+ range: TimeRange.CUSTOM,
472
+ startAndEndDate: new InBetween<Date>(
473
+ "2026-09-28T10:00:00.000Z" as unknown as Date,
474
+ "2026-09-28T10:15:00.000Z" as unknown as Date,
475
+ ),
476
+ }),
477
+ ).toBe(true);
478
+ });
479
+
480
+ test("the same relative range is the same window, whatever it resolved to", () => {
481
+ expect(
482
+ shouldFollowEnclosingZoom(offered({ range: TimeRange.PAST_ONE_DAY }), {
483
+ range: TimeRange.PAST_ONE_DAY,
484
+ }),
485
+ ).toBe(true);
486
+ });
487
+
488
+ /*
489
+ * The investigation drawer offers a zoom that names no range, and so can
490
+ * any hand-built one: nothing ties it to the explorer's window, so the
491
+ * explorer keeps shadowing it with its own, as it always has.
492
+ */
493
+ test("a zoom that does not say which range it is over is not followed", () => {
494
+ expect(shouldFollowEnclosingZoom(offered(null), WINDOW)).toBe(false);
495
+ expect(shouldFollowEnclosingZoom(offered(null, true), WINDOW)).toBe(false);
496
+ });
497
+
498
+ test("an explorer that has no window of its own follows nothing", () => {
499
+ expect(shouldFollowEnclosingZoom(offered(WINDOW), undefined)).toBe(false);
500
+ });
501
+ });
502
+
503
+ /*
504
+ * The hook in a real page: the page owns a range and zooms it; the explorer
505
+ * owns its window the way the Dashboard explorers do (a drag becomes a
506
+ * custom window, a pick is applied as it comes) and, when pinned, follows
507
+ * the page's range the way a snapshot's primary explorer follows the
508
+ * snapshot's window.
509
+ */
510
+ let latestExplorer: ViewerTimeRangeZoom | null = null;
511
+
512
+ function explorer(): ViewerTimeRangeZoom {
513
+ if (!latestExplorer) {
514
+ throw new Error("the explorer has not rendered");
515
+ }
516
+ return latestExplorer;
517
+ }
518
+
519
+ interface ProbeExplorerProps {
520
+ pinnedTo: RangeStartAndEndDateTime | null;
521
+ initial: RangeStartAndEndDateTime;
522
+ // False for a host that cannot apply a zoom (no select handler).
523
+ canZoom: boolean;
524
+ }
525
+
526
+ const ProbeExplorer: FunctionComponent<ProbeExplorerProps> = (
527
+ props: ProbeExplorerProps,
528
+ ): ReactElement => {
529
+ const [range, setRange] = useState<RangeStartAndEndDateTime>(props.initial);
530
+
531
+ usePinFollow(props.pinnedTo, setRange);
532
+
533
+ const zoom: ViewerTimeRangeZoom = useViewerTimeRangeZoom({
534
+ timeRange: range,
535
+ onTimeRangeSelect: props.canZoom
536
+ ? (startTime: Date, endTime: Date): void => {
537
+ explorerSelectSpy(startTime, endTime);
538
+ setRange({
539
+ range: TimeRange.CUSTOM,
540
+ startAndEndDate: new InBetween<Date>(startTime, endTime),
541
+ });
542
+ }
543
+ : undefined,
544
+ onTimeRangeChange: (next: RangeStartAndEndDateTime): void => {
545
+ explorerChangeSpy(next);
546
+ setRange(next);
547
+ },
548
+ });
549
+
550
+ latestExplorer = zoom;
551
+
552
+ return (
553
+ <div>
554
+ <span data-testid="explorer-window">{windowOf(range)}</span>
555
+ <span data-testid="follows">{String(zoom.followsEnclosingZoom)}</span>
556
+ <span data-testid="explorer-zoomed">
557
+ {String(Boolean(zoom.zoom?.isZoomed))}
558
+ </span>
559
+ <span data-testid="before-zoom">
560
+ {windowOf(zoom.zoom?.rangeBeforeZoom || null)}
561
+ </span>
562
+ <span data-testid="can-zoom-out">{String(Boolean(zoom.onZoomOut))}</span>
563
+ </div>
564
+ );
565
+ };
566
+
567
+ interface ProbePageOptions {
568
+ pageRange: RangeStartAndEndDateTime;
569
+ explorerRange: RangeStartAndEndDateTime;
570
+ pinExplorer: boolean;
571
+ offersZoom?: boolean | undefined;
572
+ canZoom?: boolean | undefined;
573
+ }
574
+
575
+ function renderProbePage(options: ProbePageOptions): void {
576
+ render(
577
+ <ZoomingPage
578
+ initialRange={options.pageRange}
579
+ pinExplorer={options.pinExplorer}
580
+ offersZoom={options.offersZoom}
581
+ explorer={(pinnedTo: RangeStartAndEndDateTime | null): ReactElement => {
582
+ return (
583
+ <ProbeExplorer
584
+ pinnedTo={pinnedTo}
585
+ initial={options.explorerRange}
586
+ canZoom={options.canZoom !== false}
587
+ />
588
+ );
589
+ }}
590
+ />,
591
+ );
592
+ }
593
+
594
+ function drag(startIso: string, endIso: string): void {
595
+ act(() => {
596
+ explorer().onTimeRangeSelect!(new Date(startIso), new Date(endIso));
597
+ });
598
+ }
599
+
600
+ function zoomOut(): void {
601
+ act(() => {
602
+ explorer().onZoomOut!();
603
+ });
604
+ }
605
+
606
+ // A snapshot pinning its explorer to the snapshot's window.
607
+ function renderFollowingExplorer(canZoom: boolean = true): void {
608
+ renderProbePage({
609
+ pageRange: WINDOW,
610
+ explorerRange: WINDOW,
611
+ pinExplorer: true,
612
+ canZoom: canZoom,
613
+ });
614
+ }
615
+
616
+ describe("the hook: with no zoom around it, or one over another window, the explorer zooms on its own", () => {
617
+ test("no zoom around: a drag reaches the explorer's host and the explorer is zoomed", () => {
618
+ renderProbePage({
619
+ pageRange: WINDOW,
620
+ explorerRange: WINDOW,
621
+ pinExplorer: false,
622
+ offersZoom: false,
623
+ });
624
+
625
+ drag(SLICE_START, SLICE_END);
626
+
627
+ expect(explorerSelectSpy).toHaveBeenCalledTimes(1);
628
+ expect(read("explorer-window")).toBe(SLICE_TEXT);
629
+ expect(read("explorer-zoomed")).toBe("true");
630
+ expect(read("before-zoom")).toBe(WINDOW_TEXT);
631
+ expect(pageChangeSpy).not.toHaveBeenCalled();
632
+ expect(read("page-window")).toBe(WINDOW_TEXT);
633
+
634
+ zoomOut();
635
+
636
+ expect(explorerChangeSpy).toHaveBeenCalledTimes(1);
637
+ expect(windowOf(explorerChangeSpy.mock.calls[0]![0] as never)).toBe(
638
+ WINDOW_TEXT,
639
+ );
640
+ expect(read("explorer-window")).toBe(WINDOW_TEXT);
641
+ expect(read("can-zoom-out")).toBe("false");
642
+ expect(pageChangeSpy).not.toHaveBeenCalled();
643
+ });
644
+
645
+ test("a zoom around over another window: the explorer's own zoom, and the page is never touched", () => {
646
+ renderProbePage({
647
+ pageRange: OTHER,
648
+ explorerRange: WINDOW,
649
+ pinExplorer: false,
650
+ });
651
+
652
+ drag(SLICE_START, SLICE_END);
653
+
654
+ expect(explorerSelectSpy).toHaveBeenCalledTimes(1);
655
+ expect(read("explorer-zoomed")).toBe("true");
656
+ expect(read("page-window")).toBe(OTHER_TEXT);
657
+
658
+ zoomOut();
659
+
660
+ expect(read("explorer-window")).toBe(WINDOW_TEXT);
661
+ expect(pageChangeSpy).not.toHaveBeenCalled();
662
+ expect(read("page-window")).toBe(OTHER_TEXT);
663
+ });
664
+
665
+ test("a zoomed page over another window offers the explorer no way back of the page's", () => {
666
+ renderProbePage({
667
+ pageRange: OTHER,
668
+ explorerRange: WINDOW,
669
+ pinExplorer: false,
670
+ });
671
+
672
+ act(() => {
673
+ page().zoomToTimeRange(
674
+ new Date("2026-09-28T08:10:00.000Z"),
675
+ new Date("2026-09-28T08:20:00.000Z"),
676
+ );
677
+ });
678
+
679
+ expect(read("page-zoomed")).toBe("true");
680
+ expect(read("explorer-zoomed")).toBe("false");
681
+ expect(read("can-zoom-out")).toBe("false");
682
+
683
+ // A stray double-click in the explorer leaves the page zoomed.
684
+ act(() => {
685
+ explorer().zoom!.resetZoom();
686
+ });
687
+ expect(read("page-zoomed")).toBe("true");
688
+ });
689
+
690
+ test("a zoom around that names no range is shadowed, as before", () => {
691
+ const aroundSelect: MockFunction = getJestMockFunction();
692
+ const aroundReset: MockFunction = getJestMockFunction();
693
+ const handBuilt: TimeRangeZoom = {
694
+ isZoomed: true,
695
+ rangeBeforeZoom: OTHER,
696
+ zoomToTimeRange: (startTime: Date, endTime: Date): void => {
697
+ aroundSelect(startTime, endTime);
698
+ },
699
+ resetZoom: (): void => {
700
+ aroundReset();
701
+ },
702
+ };
703
+
704
+ render(
705
+ <TimeRangeZoomProvider zoom={handBuilt}>
706
+ <ProbeExplorer pinnedTo={null} initial={WINDOW} canZoom={true} />
707
+ </TimeRangeZoomProvider>,
708
+ );
709
+
710
+ expect(read("explorer-zoomed")).toBe("false");
711
+
712
+ drag(SLICE_START, SLICE_END);
713
+ zoomOut();
714
+
715
+ expect(aroundSelect).not.toHaveBeenCalled();
716
+ expect(aroundReset).not.toHaveBeenCalled();
717
+ expect(explorerSelectSpy).toHaveBeenCalledTimes(1);
718
+ expect(explorerChangeSpy).toHaveBeenCalledTimes(1);
719
+ });
720
+
721
+ test("a host that cannot zoom, under a zoom over another window, offers no drag (as before)", () => {
722
+ renderProbePage({
723
+ pageRange: OTHER,
724
+ explorerRange: WINDOW,
725
+ pinExplorer: false,
726
+ canZoom: false,
727
+ });
728
+
729
+ expect(explorer().zoom).toBeNull();
730
+ expect(explorer().onTimeRangeSelect).toBeUndefined();
731
+ expect(explorer().onZoomOut).toBeUndefined();
732
+ });
733
+ });
734
+
735
+ describe("the hook: a zoom around over the explorer's very window is followed", () => {
736
+ test("it is adopted: the explorer says so, and is not zoomed while the page is not", () => {
737
+ renderFollowingExplorer();
738
+
739
+ expect(read("follows")).toBe("true");
740
+ expect(read("explorer-zoomed")).toBe("false");
741
+ expect(read("can-zoom-out")).toBe("false");
742
+ expect(explorer().onTimeRangeSelect).toBeInstanceOf(Function);
743
+ });
744
+
745
+ test("a drag zooms the page, not the explorer's host; the explorer follows the page there", () => {
746
+ renderFollowingExplorer();
747
+
748
+ drag(SLICE_START, SLICE_END);
749
+
750
+ expect(explorerSelectSpy).not.toHaveBeenCalled();
751
+ expect(pageChangeSpy).toHaveBeenCalledTimes(1);
752
+ expect(windowOf(pageChangeSpy.mock.calls[0]![0] as never)).toBe(SLICE_TEXT);
753
+ expect(read("page-window")).toBe(SLICE_TEXT);
754
+ expect(read("page-zoomed")).toBe("true");
755
+ // Pinned to the page's range, the explorer is on the slice, still following.
756
+ expect(read("explorer-window")).toBe(SLICE_TEXT);
757
+ expect(read("follows")).toBe("true");
758
+ });
759
+
760
+ test("while the page is zoomed the explorer is too, with the page's way back", () => {
761
+ renderFollowingExplorer();
762
+
763
+ drag(SLICE_START, SLICE_END);
764
+
765
+ expect(read("explorer-zoomed")).toBe("true");
766
+ expect(read("can-zoom-out")).toBe("true");
767
+ expect(read("before-zoom")).toBe(WINDOW_TEXT);
768
+ // The zoom it hands on is over the page's range.
769
+ expect(windowOf(explorer().zoom!.timeRange)).toBe(SLICE_TEXT);
770
+ });
771
+
772
+ test("a double-click resets the page, and the explorer follows it back", () => {
773
+ renderFollowingExplorer();
774
+ drag(SLICE_START, SLICE_END);
775
+
776
+ zoomOut();
777
+
778
+ expect(explorerChangeSpy).not.toHaveBeenCalled();
779
+ expect(pageChangeSpy).toHaveBeenCalledTimes(2);
780
+ expect(windowOf(pageChangeSpy.mock.calls[1]![0] as never)).toBe(
781
+ WINDOW_TEXT,
782
+ );
783
+ expect(read("page-window")).toBe(WINDOW_TEXT);
784
+ expect(read("page-zoomed")).toBe("false");
785
+ expect(read("explorer-window")).toBe(WINDOW_TEXT);
786
+ expect(read("explorer-zoomed")).toBe("false");
787
+ expect(read("can-zoom-out")).toBe("false");
788
+ });
789
+
790
+ test("the zoom it hands its charts resets the page too", () => {
791
+ renderFollowingExplorer();
792
+ drag(SLICE_START, SLICE_END);
793
+ // The page's zoom, not one of the explorer's own.
794
+ expect(read("page-zoomed")).toBe("true");
795
+ expect(explorer().zoom!.isZoomed).toBe(true);
796
+
797
+ act(() => {
798
+ explorer().zoom!.resetZoom();
799
+ });
800
+
801
+ expect(explorerChangeSpy).not.toHaveBeenCalled();
802
+ expect(read("page-zoomed")).toBe("false");
803
+ expect(read("page-window")).toBe(WINDOW_TEXT);
804
+ expect(read("explorer-window")).toBe(WINDOW_TEXT);
805
+ });
806
+
807
+ test("the page's own reset (the snapshot badge's) takes the explorer back with it", () => {
808
+ renderFollowingExplorer();
809
+ drag(SLICE_START, SLICE_END);
810
+
811
+ act(() => {
812
+ page().resetZoom();
813
+ });
814
+
815
+ expect(read("page-window")).toBe(WINDOW_TEXT);
816
+ expect(read("explorer-window")).toBe(WINDOW_TEXT);
817
+ expect(read("explorer-zoomed")).toBe("false");
818
+ });
819
+
820
+ test("it keeps no zoom of its own: once the page is reset there is nothing left to undo", () => {
821
+ renderFollowingExplorer();
822
+ drag(SLICE_START, SLICE_END);
823
+ act(() => {
824
+ page().resetZoom();
825
+ });
826
+ const pageChanges: number = pageChangeSpy.mock.calls.length;
827
+
828
+ act(() => {
829
+ explorer().zoom!.resetZoom();
830
+ });
831
+
832
+ expect(pageChangeSpy.mock.calls.length).toBe(pageChanges);
833
+ expect(explorerChangeSpy).not.toHaveBeenCalled();
834
+ expect(read("explorer-window")).toBe(WINDOW_TEXT);
835
+ });
836
+
837
+ test("before any zoom a double-click has nothing to do: none is offered, and a stray one changes nothing", () => {
838
+ renderFollowingExplorer();
839
+ expect(read("follows")).toBe("true");
840
+
841
+ expect(explorer().onZoomOut).toBeUndefined();
842
+
843
+ act(() => {
844
+ explorer().zoom!.resetZoom();
845
+ });
846
+
847
+ expect(pageChangeSpy).not.toHaveBeenCalled();
848
+ expect(explorerChangeSpy).not.toHaveBeenCalled();
849
+ });
850
+
851
+ test("a second drag zooms within the first; one reset returns to the window before both", () => {
852
+ renderFollowingExplorer();
853
+
854
+ drag(SLICE_START, SLICE_END);
855
+ drag(NESTED_START, NESTED_END);
856
+
857
+ expect(read("page-window")).toBe(`${NESTED_START}..${NESTED_END}`);
858
+ expect(read("explorer-window")).toBe(`${NESTED_START}..${NESTED_END}`);
859
+ expect(read("before-zoom")).toBe(WINDOW_TEXT);
860
+
861
+ zoomOut();
862
+
863
+ expect(read("page-window")).toBe(WINDOW_TEXT);
864
+ expect(read("explorer-window")).toBe(WINDOW_TEXT);
865
+ });
866
+
867
+ test("two drags before anything re-renders both reach the page", () => {
868
+ renderFollowingExplorer();
869
+
870
+ act(() => {
871
+ explorer().onTimeRangeSelect!(new Date(SLICE_START), new Date(SLICE_END));
872
+ explorer().onTimeRangeSelect!(
873
+ new Date(NESTED_START),
874
+ new Date(NESTED_END),
875
+ );
876
+ });
877
+
878
+ expect(explorerSelectSpy).not.toHaveBeenCalled();
879
+ expect(read("page-window")).toBe(`${NESTED_START}..${NESTED_END}`);
880
+
881
+ zoomOut();
882
+
883
+ expect(read("page-window")).toBe(WINDOW_TEXT);
884
+ });
885
+
886
+ test("the drag is handed over as it came: the page cuts it at the end of its own range", () => {
887
+ renderFollowingExplorer();
888
+
889
+ drag("2026-09-28T10:14:00.000Z", "2026-09-28T10:16:00.000Z");
890
+
891
+ expect(read("page-window")).toBe(
892
+ "2026-09-28T10:14:00.000Z..2026-09-28T10:15:00.000Z",
893
+ );
894
+ });
895
+
896
+ test("a host that cannot zoom on its own still hands a drag to the zoom it follows", () => {
897
+ renderFollowingExplorer(false);
898
+
899
+ expect(read("follows")).toBe("true");
900
+ // Nothing of its own to offer its charts; the page's reaches them as is.
901
+ expect(explorer().zoom).toBeNull();
902
+
903
+ drag(SLICE_START, SLICE_END);
904
+
905
+ expect(read("page-window")).toBe(SLICE_TEXT);
906
+ expect(read("explorer-window")).toBe(SLICE_TEXT);
907
+ expect(explorer().onZoomOut).toBeInstanceOf(Function);
908
+
909
+ zoomOut();
910
+
911
+ expect(read("page-window")).toBe(WINDOW_TEXT);
912
+ });
913
+
914
+ test("its callbacks keep their identity as it starts and stops following", () => {
915
+ renderProbePage({
916
+ pageRange: WINDOW,
917
+ explorerRange: WINDOW,
918
+ pinExplorer: false,
919
+ });
920
+ const select: unknown = explorer().onTimeRangeSelect;
921
+ const pick: unknown = explorer().onTimeRangeChange;
922
+ const zoomTo: unknown = explorer().zoom!.zoomToTimeRange;
923
+ const reset: unknown = explorer().zoom!.resetZoom;
924
+ expect(read("follows")).toBe("true");
925
+
926
+ pageMovesTo(OTHER);
927
+ expect(read("follows")).toBe("false");
928
+
929
+ pageMovesTo(WINDOW);
930
+ expect(read("follows")).toBe("true");
931
+
932
+ expect(explorer().onTimeRangeSelect).toBe(select);
933
+ expect(explorer().onTimeRangeChange).toBe(pick);
934
+ expect(explorer().zoom!.zoomToTimeRange).toBe(zoomTo);
935
+ expect(explorer().zoom!.resetZoom).toBe(reset);
936
+ });
937
+ });
938
+
939
+ describe("the hook: an explorer stops following once the zoom around is over another window", () => {
940
+ test("the page moving off the explorer's window: the explorer is on its own again, with no zoom", () => {
941
+ renderProbePage({
942
+ pageRange: WINDOW,
943
+ explorerRange: WINDOW,
944
+ pinExplorer: false,
945
+ });
946
+ expect(read("follows")).toBe("true");
947
+
948
+ pageMovesTo(OTHER);
949
+
950
+ expect(read("follows")).toBe("false");
951
+ expect(read("explorer-window")).toBe(WINDOW_TEXT);
952
+ expect(read("explorer-zoomed")).toBe("false");
953
+ expect(read("can-zoom-out")).toBe("false");
954
+
955
+ // A drag now zooms the explorer alone.
956
+ drag(SLICE_START, SLICE_END);
957
+
958
+ expect(explorerSelectSpy).toHaveBeenCalledTimes(1);
959
+ expect(pageChangeSpy).not.toHaveBeenCalled();
960
+ expect(read("page-window")).toBe(OTHER_TEXT);
961
+ expect(read("explorer-zoomed")).toBe("true");
962
+ expect(read("before-zoom")).toBe(WINDOW_TEXT);
963
+ });
964
+
965
+ test("a page zoom it followed is not the explorer's to undo once it stops following", () => {
966
+ renderProbePage({
967
+ pageRange: WINDOW,
968
+ explorerRange: WINDOW,
969
+ pinExplorer: false,
970
+ });
971
+
972
+ // The explorer follows; the drag zooms the page (the explorer is not pinned).
973
+ drag(SLICE_START, SLICE_END);
974
+ expect(read("page-window")).toBe(SLICE_TEXT);
975
+ expect(read("page-zoomed")).toBe("true");
976
+
977
+ // Now the explorer is on a window the page's zoom is not over.
978
+ expect(read("explorer-window")).toBe(WINDOW_TEXT);
979
+ expect(read("follows")).toBe("false");
980
+ expect(read("explorer-zoomed")).toBe("false");
981
+ expect(read("can-zoom-out")).toBe("false");
982
+
983
+ act(() => {
984
+ explorer().zoom!.resetZoom();
985
+ });
986
+ expect(read("page-zoomed")).toBe("true");
987
+ expect(read("page-window")).toBe(SLICE_TEXT);
988
+ });
989
+
990
+ test("the explorer's own picker moving it off the window: it stops following, and the page keeps its zoom", () => {
991
+ renderFollowingExplorer();
992
+ drag(SLICE_START, SLICE_END);
993
+ expect(read("page-zoomed")).toBe("true");
994
+
995
+ act(() => {
996
+ explorer().onTimeRangeChange!({ range: TimeRange.PAST_ONE_DAY });
997
+ });
998
+
999
+ expect(explorerChangeSpy).toHaveBeenCalledWith({
1000
+ range: TimeRange.PAST_ONE_DAY,
1001
+ });
1002
+ expect(read("explorer-window")).toBe(TimeRange.PAST_ONE_DAY);
1003
+ expect(read("follows")).toBe("false");
1004
+ expect(read("explorer-zoomed")).toBe("false");
1005
+ // The page's zoom is the page's: a pick in the explorer does not end it.
1006
+ expect(read("page-window")).toBe(SLICE_TEXT);
1007
+ expect(read("page-zoomed")).toBe("true");
1008
+ });
1009
+
1010
+ test("a pinned explorer that stopped following starts again when the page's reset re-pins it", () => {
1011
+ renderFollowingExplorer();
1012
+ drag(SLICE_START, SLICE_END);
1013
+ act(() => {
1014
+ explorer().onTimeRangeChange!({ range: TimeRange.PAST_ONE_DAY });
1015
+ });
1016
+ expect(read("follows")).toBe("false");
1017
+
1018
+ act(() => {
1019
+ page().resetZoom();
1020
+ });
1021
+
1022
+ expect(read("explorer-window")).toBe(WINDOW_TEXT);
1023
+ expect(read("follows")).toBe("true");
1024
+
1025
+ drag(SLICE_START, SLICE_END);
1026
+ expect(read("page-window")).toBe(SLICE_TEXT);
1027
+ expect(explorerSelectSpy).not.toHaveBeenCalled();
1028
+ });
1029
+
1030
+ test("a zoom of the explorer's own is dropped when it starts following, and does not come back when it stops", () => {
1031
+ renderProbePage({
1032
+ pageRange: OTHER,
1033
+ explorerRange: WINDOW,
1034
+ pinExplorer: false,
1035
+ });
1036
+
1037
+ // The explorer zooms itself to the slice.
1038
+ drag(SLICE_START, SLICE_END);
1039
+ expect(read("explorer-zoomed")).toBe("true");
1040
+
1041
+ // The page's range comes onto the explorer's window: it follows now.
1042
+ pageMovesTo(custom(SLICE_START, SLICE_END));
1043
+ expect(read("follows")).toBe("true");
1044
+ // The page is not zoomed, so neither is the explorer: its own zoom is gone.
1045
+ expect(read("explorer-zoomed")).toBe("false");
1046
+ expect(read("can-zoom-out")).toBe("false");
1047
+
1048
+ // And the page moving away again does not bring it back.
1049
+ pageMovesTo(OTHER);
1050
+ expect(read("follows")).toBe("false");
1051
+ expect(read("explorer-zoomed")).toBe("false");
1052
+ expect(read("can-zoom-out")).toBe("false");
1053
+
1054
+ act(() => {
1055
+ explorer().zoom!.resetZoom();
1056
+ });
1057
+ expect(explorerChangeSpy).not.toHaveBeenCalled();
1058
+ expect(read("explorer-window")).toBe(SLICE_TEXT);
1059
+ });
1060
+ });
1061
+
1062
+ /*
1063
+ * The telemetry shell (the traces, exceptions and security events
1064
+ * explorers) inside a page that zooms. Its host owns the window the way
1065
+ * TracesViewer does and, when pinned, follows the page's range the way a
1066
+ * snapshot's primary explorer follows the snapshot's window.
1067
+ */
1068
+ interface Item {
1069
+ id: string;
1070
+ }
1071
+
1072
+ // An analytics chart a host renders in the histogram's place: it zooms whatever it finds in context.
1073
+ const StandInAnalyticsChart: FunctionComponent = (): ReactElement => {
1074
+ const zoom: ChartTimeRangeZoomContextValue | null = useChartTimeRangeZoom();
1075
+
1076
+ return (
1077
+ <div data-testid="analytics-chart">
1078
+ <button
1079
+ type="button"
1080
+ onClick={() => {
1081
+ zoom?.onTimeRangeSelect(new Date(SLICE_START), new Date(SLICE_END));
1082
+ }}
1083
+ >
1084
+ Drag on analytics
1085
+ </button>
1086
+ <button
1087
+ type="button"
1088
+ onClick={() => {
1089
+ zoom?.onTimeRangeReset?.();
1090
+ }}
1091
+ >
1092
+ Double-click analytics
1093
+ </button>
1094
+ </div>
1095
+ );
1096
+ };
1097
+
1098
+ interface ShellExplorerProps {
1099
+ pinnedTo: RangeStartAndEndDateTime | null;
1100
+ initial: RangeStartAndEndDateTime;
1101
+ analytics: boolean;
1102
+ }
1103
+
1104
+ const TelemetryShellExplorer: FunctionComponent<ShellExplorerProps> = (
1105
+ props: ShellExplorerProps,
1106
+ ): ReactElement => {
1107
+ const [timeRange, setTimeRange] = useState<RangeStartAndEndDateTime>(
1108
+ props.initial,
1109
+ );
1110
+
1111
+ usePinFollow(props.pinnedTo, setTimeRange);
1112
+
1113
+ return (
1114
+ <div>
1115
+ <span data-testid="explorer-window">{windowOf(timeRange)}</span>
1116
+ <TelemetryViewer<Item>
1117
+ items={[]}
1118
+ isLoading={false}
1119
+ renderRow={(item: Item): ReactElement => {
1120
+ return <span>{item.id}</span>;
1121
+ }}
1122
+ getRowKey={(item: Item): string => {
1123
+ return item.id;
1124
+ }}
1125
+ searchValue=""
1126
+ onSearchChange={(): void => {}}
1127
+ onSearchSubmit={(): void => {}}
1128
+ timeRange={timeRange}
1129
+ onTimeRangeChange={(next: RangeStartAndEndDateTime): void => {
1130
+ explorerChangeSpy(next);
1131
+ setTimeRange(next);
1132
+ }}
1133
+ page={1}
1134
+ pageSize={50}
1135
+ totalCount={0}
1136
+ onPageChange={(): void => {}}
1137
+ onPageSizeChange={(): void => {}}
1138
+ showHistogram={true}
1139
+ histogramBuckets={TELEMETRY_BUCKETS}
1140
+ histogramSeries={SERIES}
1141
+ histogramBucketIntervalMs={MINUTE_MS}
1142
+ onHistogramTimeRangeSelect={(startTime: Date, endTime: Date): void => {
1143
+ explorerSelectSpy(startTime, endTime);
1144
+ setTimeRange({
1145
+ range: TimeRange.CUSTOM,
1146
+ startAndEndDate: new InBetween<Date>(startTime, endTime),
1147
+ });
1148
+ }}
1149
+ mainContentOverride={
1150
+ props.analytics ? <StandInAnalyticsChart /> : undefined
1151
+ }
1152
+ />
1153
+ </div>
1154
+ );
1155
+ };
1156
+
1157
+ // The logs viewer, hosted the way DashboardLogsViewer hosts it.
1158
+ const LogsShellExplorer: FunctionComponent<ShellExplorerProps> = (
1159
+ props: ShellExplorerProps,
1160
+ ): ReactElement => {
1161
+ const [timeRange, setTimeRange] = useState<RangeStartAndEndDateTime>(
1162
+ props.initial,
1163
+ );
1164
+
1165
+ usePinFollow(props.pinnedTo, setTimeRange);
1166
+
1167
+ return (
1168
+ <div>
1169
+ <span data-testid="explorer-window">{windowOf(timeRange)}</span>
1170
+ <LogsViewer
1171
+ logs={[]}
1172
+ isLoading={false}
1173
+ filterData={{}}
1174
+ onFilterChanged={(): void => {}}
1175
+ showFilters={true}
1176
+ histogramBuckets={LOGS_BUCKETS}
1177
+ histogramLoading={false}
1178
+ histogramBucketIntervalMs={MINUTE_MS}
1179
+ onHistogramTimeRangeSelect={(startTime: Date, endTime: Date): void => {
1180
+ explorerSelectSpy(startTime, endTime);
1181
+ setTimeRange({
1182
+ range: TimeRange.CUSTOM,
1183
+ startAndEndDate: new InBetween<Date>(startTime, endTime),
1184
+ });
1185
+ }}
1186
+ timeRange={timeRange}
1187
+ onTimeRangeChange={(next: RangeStartAndEndDateTime): void => {
1188
+ explorerChangeSpy(next);
1189
+ setTimeRange(next);
1190
+ }}
1191
+ viewMode={props.analytics ? "analytics" : "list"}
1192
+ onViewModeChange={(): void => {}}
1193
+ />
1194
+ </div>
1195
+ );
1196
+ };
1197
+
1198
+ interface ShellCase {
1199
+ name: string;
1200
+ pickerTestId: string;
1201
+ element: (props: ShellExplorerProps) => ReactElement;
1202
+ }
1203
+
1204
+ const SHELLS: Array<ShellCase> = [
1205
+ {
1206
+ name: "the telemetry shell",
1207
+ pickerTestId: "telemetry-time-range-picker-button",
1208
+ element: (props: ShellExplorerProps): ReactElement => {
1209
+ return <TelemetryShellExplorer {...props} />;
1210
+ },
1211
+ },
1212
+ {
1213
+ name: "the logs viewer",
1214
+ pickerTestId: "log-time-range-picker-button",
1215
+ element: (props: ShellExplorerProps): ReactElement => {
1216
+ return <LogsShellExplorer {...props} />;
1217
+ },
1218
+ },
1219
+ ];
1220
+
1221
+ interface ShellPageOptions {
1222
+ pageRange: RangeStartAndEndDateTime;
1223
+ explorerRange: RangeStartAndEndDateTime;
1224
+ pinExplorer: boolean;
1225
+ analytics?: boolean | undefined;
1226
+ }
1227
+
1228
+ async function renderShellPage(
1229
+ shell: ShellCase,
1230
+ options: ShellPageOptions,
1231
+ ): Promise<void> {
1232
+ await act(async () => {
1233
+ render(
1234
+ <ZoomingPage
1235
+ initialRange={options.pageRange}
1236
+ pinExplorer={options.pinExplorer}
1237
+ explorer={(pinnedTo: RangeStartAndEndDateTime | null): ReactElement => {
1238
+ return shell.element({
1239
+ pinnedTo: pinnedTo,
1240
+ initial: options.explorerRange,
1241
+ analytics: Boolean(options.analytics),
1242
+ });
1243
+ }}
1244
+ />,
1245
+ );
1246
+ });
1247
+
1248
+ await waitFor(() => {
1249
+ expect(screen.getByTestId(`bucket-${BAR_A}`)).toBeInTheDocument();
1250
+ });
1251
+ }
1252
+
1253
+ function dragHistogram(): void {
1254
+ fireEvent.mouseDown(screen.getByTestId(`bucket-${BAR_A}`));
1255
+ fireEvent.mouseMove(screen.getByTestId(`bucket-${BAR_B}`));
1256
+ fireEvent.mouseUp(screen.getByTestId(`bucket-${BAR_B}`));
1257
+ }
1258
+
1259
+ // The element a volume histogram takes its double-click on.
1260
+ function histogramPlot(): HTMLElement {
1261
+ return screen.getByTestId(`bucket-${BAR_A}`).parentElement!.parentElement!
1262
+ .parentElement!;
1263
+ }
1264
+
1265
+ function pickerLabel(shell: ShellCase): string {
1266
+ return (screen.getByTestId(shell.pickerTestId).textContent || "").trim();
1267
+ }
1268
+
1269
+ function resetButtons(): Array<HTMLElement> {
1270
+ return screen.queryAllByTestId(RESET_TIME_RANGE_ZOOM_BUTTON_TEST_ID);
1271
+ }
1272
+
1273
+ function explorerResetButtons(): Array<HTMLElement> {
1274
+ return within(screen.getByTestId("explorer")).queryAllByTestId(
1275
+ RESET_TIME_RANGE_ZOOM_BUTTON_TEST_ID,
1276
+ );
1277
+ }
1278
+
1279
+ function pageResetButton(): HTMLElement {
1280
+ return within(screen.getByTestId("page-reset")).getByTestId(
1281
+ RESET_TIME_RANGE_ZOOM_BUTTON_TEST_ID,
1282
+ );
1283
+ }
1284
+
1285
+ const SLICE_LABEL: string = getTimeRangeButtonLabel(
1286
+ custom(SLICE_START, SLICE_END),
1287
+ );
1288
+
1289
+ describe.each(SHELLS)(
1290
+ "$name pinned to the window of the zoom around it follows that zoom",
1291
+ (shell: ShellCase) => {
1292
+ async function renderFollowing(analytics: boolean = false): Promise<void> {
1293
+ await renderShellPage(shell, {
1294
+ pageRange: WINDOW,
1295
+ explorerRange: WINDOW,
1296
+ pinExplorer: true,
1297
+ analytics: analytics,
1298
+ });
1299
+ }
1300
+
1301
+ test("a drag on the histogram retimes the page, never the explorer's host, and the explorer follows", async () => {
1302
+ await renderFollowing();
1303
+
1304
+ dragHistogram();
1305
+
1306
+ expect(explorerSelectSpy).not.toHaveBeenCalled();
1307
+ expect(pageChangeSpy).toHaveBeenCalledTimes(1);
1308
+ expect(read("page-window")).toBe(SLICE_TEXT);
1309
+ await waitFor(() => {
1310
+ expect(read("explorer-window")).toBe(SLICE_TEXT);
1311
+ });
1312
+ expect(pickerLabel(shell)).toBe(SLICE_LABEL);
1313
+ });
1314
+
1315
+ test("the page's Reset zoom is the only one: the explorer's picker offers none", async () => {
1316
+ await renderFollowing();
1317
+
1318
+ dragHistogram();
1319
+ await waitFor(() => {
1320
+ expect(read("explorer-window")).toBe(SLICE_TEXT);
1321
+ });
1322
+
1323
+ expect(resetButtons()).toHaveLength(1);
1324
+ expect(pageResetButton()).toBeVisible();
1325
+ expect(explorerResetButtons()).toHaveLength(0);
1326
+ });
1327
+
1328
+ test("the histogram offers the page's double-click once the page is zoomed", async () => {
1329
+ await renderFollowing();
1330
+
1331
+ expect(screen.queryByText("Double-click to reset")).toBeNull();
1332
+
1333
+ dragHistogram();
1334
+
1335
+ // The page's zoom, not one of the explorer's own.
1336
+ expect(read("page-zoomed")).toBe("true");
1337
+ await waitFor(() => {
1338
+ expect(screen.getByText("Double-click to reset")).toBeInTheDocument();
1339
+ });
1340
+ });
1341
+
1342
+ test("a double-click on the histogram resets the page, and the explorer with it", async () => {
1343
+ await renderFollowing();
1344
+ dragHistogram();
1345
+ expect(read("page-zoomed")).toBe("true");
1346
+ await waitFor(() => {
1347
+ expect(screen.getByText("Double-click to reset")).toBeInTheDocument();
1348
+ });
1349
+
1350
+ fireEvent.doubleClick(histogramPlot());
1351
+
1352
+ expect(read("page-window")).toBe(WINDOW_TEXT);
1353
+ expect(read("page-zoomed")).toBe("false");
1354
+ await waitFor(() => {
1355
+ expect(read("explorer-window")).toBe(WINDOW_TEXT);
1356
+ });
1357
+ expect(explorerChangeSpy).not.toHaveBeenCalled();
1358
+ expect(resetButtons()).toHaveLength(0);
1359
+ expect(screen.queryByText("Double-click to reset")).toBeNull();
1360
+ });
1361
+
1362
+ test("the page's Reset zoom takes the explorer back too", async () => {
1363
+ await renderFollowing();
1364
+ dragHistogram();
1365
+ await waitFor(() => {
1366
+ expect(read("explorer-window")).toBe(SLICE_TEXT);
1367
+ });
1368
+
1369
+ fireEvent.click(pageResetButton());
1370
+
1371
+ expect(read("page-window")).toBe(WINDOW_TEXT);
1372
+ await waitFor(() => {
1373
+ expect(read("explorer-window")).toBe(WINDOW_TEXT);
1374
+ });
1375
+ expect(resetButtons()).toHaveLength(0);
1376
+ });
1377
+
1378
+ test("a second drag, then one double-click: back on the window from before both", async () => {
1379
+ await renderFollowing();
1380
+ dragHistogram();
1381
+ await waitFor(() => {
1382
+ expect(read("explorer-window")).toBe(SLICE_TEXT);
1383
+ });
1384
+
1385
+ // From 10:04 into the 10:05 bar: cut at the end of the slice.
1386
+ fireEvent.mouseDown(screen.getByTestId(`bucket-${BAR_B}`));
1387
+ fireEvent.mouseMove(screen.getByTestId(`bucket-${BAR_C}`));
1388
+ fireEvent.mouseUp(screen.getByTestId(`bucket-${BAR_C}`));
1389
+
1390
+ expect(read("page-window")).toBe(
1391
+ "2026-09-28T10:04:00.000Z..2026-09-28T10:05:00.000Z",
1392
+ );
1393
+ await waitFor(() => {
1394
+ expect(read("explorer-window")).toBe(
1395
+ "2026-09-28T10:04:00.000Z..2026-09-28T10:05:00.000Z",
1396
+ );
1397
+ });
1398
+
1399
+ fireEvent.doubleClick(histogramPlot());
1400
+
1401
+ expect(read("page-window")).toBe(WINDOW_TEXT);
1402
+ await waitFor(() => {
1403
+ expect(read("explorer-window")).toBe(WINDOW_TEXT);
1404
+ });
1405
+ expect(explorerSelectSpy).not.toHaveBeenCalled();
1406
+ });
1407
+ },
1408
+ );
1409
+
1410
+ describe.each(SHELLS)(
1411
+ "$name under a zoom over another window keeps its own zoom, as before",
1412
+ (shell: ShellCase) => {
1413
+ async function renderOwn(): Promise<void> {
1414
+ await renderShellPage(shell, {
1415
+ pageRange: OTHER,
1416
+ explorerRange: WINDOW,
1417
+ pinExplorer: false,
1418
+ });
1419
+ }
1420
+
1421
+ test("a drag on the histogram reaches the explorer's host alone", async () => {
1422
+ await renderOwn();
1423
+
1424
+ dragHistogram();
1425
+
1426
+ expect(explorerSelectSpy).toHaveBeenCalledTimes(1);
1427
+ expect(read("explorer-window")).toBe(SLICE_TEXT);
1428
+ expect(pageChangeSpy).not.toHaveBeenCalled();
1429
+ expect(read("page-window")).toBe(OTHER_TEXT);
1430
+ });
1431
+
1432
+ test("its picker offers its own Reset zoom, which puts its window back", async () => {
1433
+ await renderOwn();
1434
+ dragHistogram();
1435
+
1436
+ expect(explorerResetButtons()).toHaveLength(1);
1437
+ // The page is not zoomed: its button shows nothing.
1438
+ expect(resetButtons()).toHaveLength(1);
1439
+
1440
+ fireEvent.click(explorerResetButtons()[0]!);
1441
+
1442
+ expect(explorerChangeSpy).toHaveBeenCalledTimes(1);
1443
+ expect(read("explorer-window")).toBe(WINDOW_TEXT);
1444
+ expect(pageChangeSpy).not.toHaveBeenCalled();
1445
+ expect(resetButtons()).toHaveLength(0);
1446
+ });
1447
+
1448
+ test("a double-click on the histogram undoes its own zoom, not the page's", async () => {
1449
+ await renderOwn();
1450
+ act(() => {
1451
+ page().zoomToTimeRange(
1452
+ new Date("2026-09-28T08:10:00.000Z"),
1453
+ new Date("2026-09-28T08:20:00.000Z"),
1454
+ );
1455
+ });
1456
+ dragHistogram();
1457
+
1458
+ fireEvent.doubleClick(histogramPlot());
1459
+
1460
+ expect(read("explorer-window")).toBe(WINDOW_TEXT);
1461
+ expect(read("page-zoomed")).toBe("true");
1462
+ expect(read("page-window")).toBe(
1463
+ "2026-09-28T08:10:00.000Z..2026-09-28T08:20:00.000Z",
1464
+ );
1465
+ });
1466
+
1467
+ test("a zoomed page's Reset zoom is not offered in the explorer's picker", async () => {
1468
+ await renderOwn();
1469
+
1470
+ act(() => {
1471
+ page().zoomToTimeRange(
1472
+ new Date("2026-09-28T08:10:00.000Z"),
1473
+ new Date("2026-09-28T08:20:00.000Z"),
1474
+ );
1475
+ });
1476
+
1477
+ expect(pageResetButton()).toBeVisible();
1478
+ expect(explorerResetButtons()).toHaveLength(0);
1479
+ });
1480
+ },
1481
+ );
1482
+
1483
+ describe("the charts a follower renders in the histogram's place share the zoom it follows", () => {
1484
+ test("the telemetry shell's analytics content: a drag retimes the page, a double-click resets it", async () => {
1485
+ await act(async () => {
1486
+ render(
1487
+ <ZoomingPage
1488
+ initialRange={WINDOW}
1489
+ pinExplorer={true}
1490
+ explorer={(
1491
+ pinnedTo: RangeStartAndEndDateTime | null,
1492
+ ): ReactElement => {
1493
+ return (
1494
+ <TelemetryShellExplorer
1495
+ pinnedTo={pinnedTo}
1496
+ initial={WINDOW}
1497
+ analytics={true}
1498
+ />
1499
+ );
1500
+ }}
1501
+ />,
1502
+ );
1503
+ });
1504
+
1505
+ fireEvent.click(screen.getByText("Drag on analytics"));
1506
+
1507
+ expect(explorerSelectSpy).not.toHaveBeenCalled();
1508
+ expect(read("page-window")).toBe(SLICE_TEXT);
1509
+ await waitFor(() => {
1510
+ expect(read("explorer-window")).toBe(SLICE_TEXT);
1511
+ });
1512
+ // One way back, the page's.
1513
+ expect(resetButtons()).toHaveLength(1);
1514
+ expect(explorerResetButtons()).toHaveLength(0);
1515
+
1516
+ fireEvent.click(screen.getByText("Double-click analytics"));
1517
+
1518
+ expect(read("page-window")).toBe(WINDOW_TEXT);
1519
+ await waitFor(() => {
1520
+ expect(read("explorer-window")).toBe(WINDOW_TEXT);
1521
+ });
1522
+ expect(explorerChangeSpy).not.toHaveBeenCalled();
1523
+ });
1524
+
1525
+ test("the logs viewer's Analytics chart: a drag retimes the page, a double-click resets it", async () => {
1526
+ await renderShellPage(SHELLS[1]!, {
1527
+ pageRange: WINDOW,
1528
+ explorerRange: WINDOW,
1529
+ pinExplorer: true,
1530
+ analytics: true,
1531
+ });
1532
+ await waitFor(() => {
1533
+ expect(screen.getByTestId(`bucket-${ANALYTICS_A}`)).toBeInTheDocument();
1534
+ });
1535
+
1536
+ fireEvent.mouseDown(screen.getByTestId(`bucket-${ANALYTICS_A}`));
1537
+ fireEvent.mouseMove(screen.getByTestId(`bucket-${ANALYTICS_B}`));
1538
+ fireEvent.mouseUp(screen.getByTestId(`bucket-${ANALYTICS_B}`));
1539
+
1540
+ expect(explorerSelectSpy).not.toHaveBeenCalled();
1541
+ expect(pageChangeSpy).toHaveBeenCalledTimes(1);
1542
+ expect(read("page-window")).toBe(
1543
+ "2026-09-28T10:07:00.000Z..2026-09-28T10:09:00.000Z",
1544
+ );
1545
+ await waitFor(() => {
1546
+ expect(read("explorer-window")).toBe(
1547
+ "2026-09-28T10:07:00.000Z..2026-09-28T10:09:00.000Z",
1548
+ );
1549
+ });
1550
+ // One way back, the page's.
1551
+ expect(resetButtons()).toHaveLength(1);
1552
+ expect(explorerResetButtons()).toHaveLength(0);
1553
+
1554
+ // The chart reloads for the slice before it can be double-clicked.
1555
+ await waitFor(() => {
1556
+ expect(
1557
+ screen.getByTestId(LOGS_ANALYTICS_TIMESERIES_TEST_ID),
1558
+ ).toBeInTheDocument();
1559
+ });
1560
+ fireEvent.doubleClick(
1561
+ screen.getByTestId(LOGS_ANALYTICS_TIMESERIES_TEST_ID),
1562
+ );
1563
+
1564
+ expect(read("page-window")).toBe(WINDOW_TEXT);
1565
+ await waitFor(() => {
1566
+ expect(read("explorer-window")).toBe(WINDOW_TEXT);
1567
+ });
1568
+ });
1569
+ });