@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,1456 @@
1
+ /**
2
+ * @timezone UTC
3
+ */
4
+ import "@testing-library/jest-dom";
5
+ import {
6
+ afterEach,
7
+ beforeEach,
8
+ describe,
9
+ expect,
10
+ jest,
11
+ test,
12
+ } from "@jest/globals";
13
+ import {
14
+ act,
15
+ cleanup,
16
+ fireEvent,
17
+ render,
18
+ screen,
19
+ within,
20
+ } from "@testing-library/react";
21
+ import * as React from "react";
22
+ import getJestMockFunction, { MockFunction } from "../../MockType";
23
+
24
+ /*
25
+ * Issue #4105 review, the Ceph rate charts (CephRateChart) on the cluster
26
+ * Overview, Insights and a pool's Metrics tab.
27
+ *
28
+ * - A drag or a double-click on a rate chart reloads that very chart. It
29
+ * used to drop to a 192px skeleton for the length of the load, so the
30
+ * chart vanished from under the pointer and the cards below jumped (a
31
+ * 300px chart on Insights, 220px on the Overview), and every 30s
32
+ * auto-refresh tick on the Overview unmounted it, drag and all. Now only
33
+ * the first load is a skeleton, at the chart's height; a reload keeps the
34
+ * last chart on screen, over the window it was fetched for, dimmed and
35
+ * marked "Refreshing", and a failed reload keeps it with the error above.
36
+ * - Every zoom is a Custom window, which the card's Refresh re-resolves to
37
+ * the same instants, so Refresh never reached the rate charts and a
38
+ * failed one could not be retried while zoomed. They now reload on the
39
+ * card's Refresh count (useEmbeddedMetricCardRefreshNonce).
40
+ * - Nothing named the drag on these charts (the Insights Client I/O card
41
+ * holds only them). Each heading now carries the hover-revealed hint.
42
+ *
43
+ * The network is mocked; the pages, EmbeddedMetricCard, ResourceMetricsTab
44
+ * and CephRateChart are the production code. The line chart, the card's
45
+ * MetricView and its picker are the zoom page harness's stand-ins.
46
+ */
47
+
48
+ const CLUSTER_ID: string = "0193c0de-6666-4aaa-8bbb-000000000006";
49
+ const NOW: Date = new Date("2026-09-28T12:00:00.000Z");
50
+ const MINUTE: number = 60 * 1000;
51
+ const GIB: number = 1024 * 1024 * 1024;
52
+ const RATE_ERROR: string = "The metrics service is unavailable.";
53
+
54
+ function at(time: string): Date {
55
+ return new Date(`2026-09-28T${time}:00.000Z`);
56
+ }
57
+
58
+ const modelGetItemMock: MockFunction = getJestMockFunction();
59
+ const modelGetListMock: MockFunction = getJestMockFunction();
60
+ const analyticsGetListMock: MockFunction = getJestMockFunction();
61
+ const analyticsAggregateMock: MockFunction = getJestMockFunction();
62
+ let mockLastParam: string = "1";
63
+
64
+ jest.mock("react-i18next", () => {
65
+ return {
66
+ useTranslation: () => {
67
+ return {
68
+ t: (key: string, opts?: { defaultValue?: string }): string => {
69
+ return opts?.defaultValue ?? key;
70
+ },
71
+ };
72
+ },
73
+ };
74
+ });
75
+
76
+ jest.mock("../../../UI/Utils/ModelAPI/ModelAPI", () => {
77
+ return {
78
+ __esModule: true,
79
+ default: {
80
+ getItem: (...args: Array<unknown>): unknown => {
81
+ return modelGetItemMock(...args);
82
+ },
83
+ getList: (...args: Array<unknown>): unknown => {
84
+ return modelGetListMock(...args);
85
+ },
86
+ count: (): Promise<number> => {
87
+ return Promise.resolve(1);
88
+ },
89
+ getCommonHeaders: (): Record<string, string> => {
90
+ return {};
91
+ },
92
+ },
93
+ };
94
+ });
95
+
96
+ jest.mock("../../../UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI", () => {
97
+ return {
98
+ __esModule: true,
99
+ default: {
100
+ getList: (...args: Array<unknown>): unknown => {
101
+ return analyticsGetListMock(...args);
102
+ },
103
+ aggregate: (...args: Array<unknown>): unknown => {
104
+ return analyticsAggregateMock(...args);
105
+ },
106
+ },
107
+ };
108
+ });
109
+
110
+ jest.mock("../../../UI/Utils/API/API", () => {
111
+ return {
112
+ __esModule: true,
113
+ default: {
114
+ getFriendlyMessage: (error: unknown): string => {
115
+ return String((error as Error)?.message || error);
116
+ },
117
+ },
118
+ };
119
+ });
120
+
121
+ jest.mock("../../../UI/Utils/Navigation", () => {
122
+ return {
123
+ __esModule: true,
124
+ default: {
125
+ getLastParamAsObjectID: (): unknown => {
126
+ const { default: ObjectIDType } = jest.requireActual(
127
+ "../../../Types/ObjectID",
128
+ ) as { default: new (id: string) => unknown };
129
+ return new ObjectIDType("0193c0de-6666-4aaa-8bbb-000000000006");
130
+ },
131
+ getLastParamAsString: (): string => {
132
+ return mockLastParam;
133
+ },
134
+ navigate: (): void => {},
135
+ isOnThisPage: (): boolean => {
136
+ return false;
137
+ },
138
+ },
139
+ };
140
+ });
141
+
142
+ jest.mock("../../../UI/Utils/Project", () => {
143
+ return {
144
+ __esModule: true,
145
+ default: {
146
+ getCurrentProjectId: (): unknown => {
147
+ const { default: ObjectIDType } = jest.requireActual(
148
+ "../../../Types/ObjectID",
149
+ ) as { default: new (id: string) => unknown };
150
+ return new ObjectIDType("10000000-0000-4000-8000-000000000001");
151
+ },
152
+ },
153
+ };
154
+ });
155
+
156
+ jest.mock("../../../UI/Components/ModelDetail/CardModelDetail", () => {
157
+ return {
158
+ __esModule: true,
159
+ default: (): React.ReactElement => {
160
+ return <div data-testid="card-model-detail" />;
161
+ },
162
+ };
163
+ });
164
+
165
+ jest.mock("../../../UI/Components/Charts/Line/LineChart", () => {
166
+ const harness: { StandInLineChart: unknown } = jest.requireActual(
167
+ "./TimeRangeZoomPageHarness",
168
+ ) as { StandInLineChart: unknown };
169
+ return { __esModule: true, default: harness.StandInLineChart };
170
+ });
171
+
172
+ jest.mock(
173
+ "../../../../App/FeatureSet/Dashboard/src/Components/Metrics/MetricView",
174
+ () => {
175
+ const harness: { StandInMetricView: unknown } = jest.requireActual(
176
+ "./TimeRangeZoomPageHarness",
177
+ ) as { StandInMetricView: unknown };
178
+ return { __esModule: true, default: harness.StandInMetricView };
179
+ },
180
+ );
181
+
182
+ jest.mock("../../../UI/Components/Date/RangeStartAndEndDateView", () => {
183
+ const harness: { StandInRangeStartAndEndDateView: unknown } =
184
+ jest.requireActual("./TimeRangeZoomPageHarness") as {
185
+ StandInRangeStartAndEndDateView: unknown;
186
+ };
187
+ return {
188
+ __esModule: true,
189
+ default: harness.StandInRangeStartAndEndDateView,
190
+ };
191
+ });
192
+
193
+ jest.mock(
194
+ "../../../../App/FeatureSet/Dashboard/src/Components/Metrics/Utils/UseEventTimeReferenceLines",
195
+ () => {
196
+ return {
197
+ __esModule: true,
198
+ default: () => {
199
+ return { lines: [], markerCount: 0 };
200
+ },
201
+ };
202
+ },
203
+ );
204
+
205
+ // The hero's refresh cluster: "Refresh now" is fetchAll(false), as a tick is.
206
+ jest.mock(
207
+ "../../../../App/FeatureSet/Dashboard/src/Components/TelemetryResource/AutoRefreshControl",
208
+ () => {
209
+ const harness: { StandInAutoRefreshControl: unknown } = jest.requireActual(
210
+ "./TimeRangeZoomPageHarness",
211
+ ) as { StandInAutoRefreshControl: unknown };
212
+ return { __esModule: true, default: harness.StandInAutoRefreshControl };
213
+ },
214
+ );
215
+
216
+ import CephClusterOverview from "../../../../App/FeatureSet/Dashboard/src/Pages/Ceph/View/Index";
217
+ import CephClusterInsights from "../../../../App/FeatureSet/Dashboard/src/Pages/Ceph/View/Insights";
218
+ import CephClusterPoolDetail from "../../../../App/FeatureSet/Dashboard/src/Pages/Ceph/View/PoolDetail";
219
+ import CephRateChart from "../../../../App/FeatureSet/Dashboard/src/Components/Ceph/CephRateChart";
220
+ import EmbeddedMetricCard from "../../../../App/FeatureSet/Dashboard/src/Components/Metrics/EmbeddedMetricCard";
221
+ import {
222
+ CHART_LOADING_SKELETON_TEST_ID,
223
+ CHART_REFETCHING_TEST_ID,
224
+ } from "../../../../App/FeatureSet/Dashboard/src/Components/Metrics/ChartRefetchFrame";
225
+ import PageComponentProps from "../../../../App/FeatureSet/Dashboard/src/Pages/PageComponentProps";
226
+ import InBetween from "../../../Types/BaseDatabase/InBetween";
227
+ import TimeRange from "../../../Types/Time/TimeRange";
228
+ import {
229
+ TIME_RANGE_ZOOM_HINT_RESET_TEXT,
230
+ TIME_RANGE_ZOOM_HINT_TEXT,
231
+ } from "../../../UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint";
232
+ import {
233
+ ZOOM_CHART_TEST_ID,
234
+ cardPickers,
235
+ chartWindows,
236
+ doubleClick,
237
+ dragAcross,
238
+ expectRevealedOnHoverOf,
239
+ flush,
240
+ metricViews,
241
+ resetZoomButtons,
242
+ windowOf,
243
+ zoomCharts,
244
+ zoomHints,
245
+ } from "./TimeRangeZoomPageHarness";
246
+
247
+ const PAGE_PROPS: PageComponentProps = {} as PageComponentProps;
248
+
249
+ // ---------------------------------------------------------------- fixtures
250
+
251
+ function minutesAgo(minutes: number): Date {
252
+ return new Date(NOW.getTime() - minutes * MINUTE);
253
+ }
254
+
255
+ const CLUSTER: Record<string, unknown> = {
256
+ _id: CLUSTER_ID,
257
+ name: "ceph-prod",
258
+ fsid: "3b0c1f7e-aaaa-bbbb-cccc-000000000001",
259
+ otelCollectorStatus: "connected",
260
+ lastSeenAt: minutesAgo(1),
261
+ cephVersion: "18.2.4",
262
+ healthStatus: 0,
263
+ monCount: 3,
264
+ osdCount: 2,
265
+ osdUpCount: 2,
266
+ osdInCount: 2,
267
+ poolCount: 1,
268
+ capacityUsedPercent: 12.34,
269
+ };
270
+
271
+ type Row = Record<string, unknown>;
272
+
273
+ const ROWS_BY_KIND: Record<string, Array<Row>> = {
274
+ Osd: [],
275
+ Pool: [
276
+ {
277
+ kind: "Pool",
278
+ externalId: "1",
279
+ name: "rbd",
280
+ storedBytes: 300 * GIB,
281
+ maxAvailBytes: 700 * GIB,
282
+ objects: 76800,
283
+ metricsUpdatedAt: minutesAgo(1),
284
+ lastSeenAt: minutesAgo(1),
285
+ createdAt: minutesAgo(60 * 24 * 3),
286
+ },
287
+ ],
288
+ Mon: [],
289
+ Mgr: [],
290
+ Mds: [],
291
+ Rgw: [],
292
+ };
293
+
294
+ const RATE_METRICS: Array<string> = [
295
+ "ceph_pool_rd",
296
+ "ceph_pool_wr",
297
+ "ceph_pool_rd_bytes",
298
+ "ceph_pool_wr_bytes",
299
+ ];
300
+
301
+ interface AggregateCall {
302
+ name: string;
303
+ start: Date;
304
+ end: Date;
305
+ attributes: Record<string, unknown>;
306
+ }
307
+
308
+ function toCall(args: unknown): AggregateCall {
309
+ const aggregateBy: {
310
+ query: { name: string; attributes?: Record<string, unknown> };
311
+ startTimestamp: Date;
312
+ endTimestamp: Date;
313
+ } = (args as { aggregateBy: never }).aggregateBy;
314
+ return {
315
+ name: aggregateBy.query.name,
316
+ start: aggregateBy.startTimestamp,
317
+ end: aggregateBy.endTimestamp,
318
+ attributes: aggregateBy.query.attributes || {},
319
+ };
320
+ }
321
+
322
+ // A cumulative counter climbing through the window it is asked for.
323
+ function counterRows(start: Date, end: Date): Array<Row> {
324
+ const step: number = (end.getTime() - start.getTime()) / 4;
325
+ return [1, 2, 3].map((i: number): Row => {
326
+ return {
327
+ timestamp: new Date(start.getTime() + i * step),
328
+ value: 1000 + 10 * ((i * step) / 1000),
329
+ attributes: { pool_id: "1" },
330
+ };
331
+ });
332
+ }
333
+
334
+ interface HeldRateLoad {
335
+ call: AggregateCall;
336
+ release: () => void;
337
+ }
338
+
339
+ // Rate loads are held until released while this is set.
340
+ let holdRates: boolean = false;
341
+ let heldRates: Array<HeldRateLoad> = [];
342
+ // Rate loads over a window ending at one of these fail, or find nothing.
343
+ let failingRateEnds: Array<number> = [];
344
+ let quietRateEnds: Array<number> = [];
345
+
346
+ function answerRates(call: AggregateCall): Promise<unknown> {
347
+ if (failingRateEnds.includes(call.end.getTime())) {
348
+ return Promise.reject(new Error(RATE_ERROR));
349
+ }
350
+ if (quietRateEnds.includes(call.end.getTime())) {
351
+ return Promise.resolve({ data: [] });
352
+ }
353
+ return Promise.resolve({ data: counterRows(call.start, call.end) });
354
+ }
355
+
356
+ function arrange(): void {
357
+ modelGetItemMock.mockImplementation(async () => {
358
+ return CLUSTER;
359
+ });
360
+
361
+ modelGetListMock.mockImplementation(async (args: unknown) => {
362
+ const query: Record<string, unknown> = (
363
+ args as { query: Record<string, unknown> }
364
+ ).query;
365
+ const rows: Array<Row> = ROWS_BY_KIND[String(query["kind"])] || [];
366
+ const filtered: Array<Row> = query["externalId"]
367
+ ? rows.filter((row: Row): boolean => {
368
+ return row["externalId"] === query["externalId"];
369
+ })
370
+ : rows;
371
+ return { data: filtered, count: filtered.length, skip: 0, limit: 100 };
372
+ });
373
+
374
+ analyticsGetListMock.mockImplementation(async () => {
375
+ return { data: [], count: 0 };
376
+ });
377
+
378
+ analyticsAggregateMock.mockImplementation((args: unknown) => {
379
+ const call: AggregateCall = toCall(args);
380
+ if (RATE_METRICS.includes(call.name)) {
381
+ if (!holdRates) {
382
+ return answerRates(call);
383
+ }
384
+ return new Promise(
385
+ (resolve: (value: unknown) => void, reject: (error: Error) => void) => {
386
+ heldRates.push({
387
+ call: call,
388
+ release: (): void => {
389
+ answerRates(call).then(resolve, reject);
390
+ },
391
+ });
392
+ },
393
+ );
394
+ }
395
+ if (call.name === "ceph_cluster_total_used_bytes") {
396
+ return Promise.resolve({
397
+ data: [
398
+ { timestamp: minutesAgo(120), value: 100 * GIB },
399
+ { timestamp: minutesAgo(0), value: 300 * GIB },
400
+ ],
401
+ });
402
+ }
403
+ return Promise.resolve({ data: [] });
404
+ });
405
+ }
406
+
407
+ // ----------------------------------------------------------------- helpers
408
+
409
+ function rateCalls(): Array<AggregateCall> {
410
+ return analyticsAggregateMock.mock.calls
411
+ .map((args: Array<unknown>): AggregateCall => {
412
+ return toCall(args[0]);
413
+ })
414
+ .filter((call: AggregateCall): boolean => {
415
+ return RATE_METRICS.includes(call.name);
416
+ });
417
+ }
418
+
419
+ // How many rate loads asked for exactly [start, end].
420
+ function rateCallsOver(start: Date, end: Date): number {
421
+ return rateCalls().filter((call: AggregateCall): boolean => {
422
+ return (
423
+ call.start.getTime() === start.getTime() &&
424
+ call.end.getTime() === end.getTime()
425
+ );
426
+ }).length;
427
+ }
428
+
429
+ async function releaseRatesEndingAt(end: Date): Promise<void> {
430
+ const releasing: Array<HeldRateLoad> = heldRates.filter(
431
+ (held: HeldRateLoad): boolean => {
432
+ return held.call.end.getTime() === end.getTime();
433
+ },
434
+ );
435
+ heldRates = heldRates.filter((held: HeldRateLoad): boolean => {
436
+ return !releasing.includes(held);
437
+ });
438
+ expect(releasing.length).toBeGreaterThan(0);
439
+ for (const held of releasing) {
440
+ held.release();
441
+ }
442
+ await flush();
443
+ }
444
+
445
+ async function releaseAllRates(): Promise<void> {
446
+ const releasing: Array<HeldRateLoad> = heldRates;
447
+ heldRates = [];
448
+ for (const held of releasing) {
449
+ held.release();
450
+ }
451
+ await flush();
452
+ }
453
+
454
+ function skeletons(): Array<HTMLElement> {
455
+ return screen.queryAllByTestId(CHART_LOADING_SKELETON_TEST_ID);
456
+ }
457
+
458
+ function refreshingMarkers(): Array<HTMLElement> {
459
+ return screen.queryAllByTestId(CHART_REFETCHING_TEST_ID);
460
+ }
461
+
462
+ // The named hint group around each rate chart: its heading and the chart.
463
+ function rateSections(): Array<HTMLElement> {
464
+ return Array.from(
465
+ document.querySelectorAll<HTMLElement>('[class~="group/zoomhint"]'),
466
+ );
467
+ }
468
+
469
+ function rateAlerts(): Array<HTMLElement> {
470
+ return rateSections().flatMap((section: HTMLElement): Array<HTMLElement> => {
471
+ return within(section).queryAllByRole("alert");
472
+ });
473
+ }
474
+
475
+ function same<T>(value: T, count: number): Array<T> {
476
+ return Array.from({ length: count }, (): T => {
477
+ return value;
478
+ });
479
+ }
480
+
481
+ function pressCardRefresh(index: number = 0): void {
482
+ fireEvent.click(screen.getAllByRole("button", { name: "Refresh" })[index]!);
483
+ }
484
+
485
+ // A 30-second auto-refresh tick (the Overview's default interval).
486
+ async function autoRefreshTick(): Promise<void> {
487
+ await act(async () => {
488
+ jest.advanceTimersByTime(30_000);
489
+ });
490
+ await flush();
491
+ }
492
+
493
+ const HOUR: [string, string] = windowOf(at("11:00"), NOW);
494
+ const ZOOM: [string, string] = windowOf(at("11:20"), at("11:40"));
495
+
496
+ beforeEach(() => {
497
+ jest.useFakeTimers();
498
+ jest.setSystemTime(NOW);
499
+ window.localStorage.clear();
500
+ mockLastParam = "1";
501
+ holdRates = false;
502
+ heldRates = [];
503
+ failingRateEnds = [];
504
+ quietRateEnds = [];
505
+ for (const mock of [
506
+ modelGetItemMock,
507
+ modelGetListMock,
508
+ analyticsGetListMock,
509
+ analyticsAggregateMock,
510
+ ]) {
511
+ mock.mockReset();
512
+ }
513
+ arrange();
514
+ });
515
+
516
+ afterEach(() => {
517
+ cleanup();
518
+ jest.useRealTimers();
519
+ });
520
+
521
+ // --------------------------------------------------------------- Overview
522
+
523
+ async function renderOverview(): Promise<void> {
524
+ render(<CephClusterOverview {...PAGE_PROPS} />);
525
+ await flush();
526
+ }
527
+
528
+ async function renderLoadedOverview(): Promise<Array<HTMLElement>> {
529
+ await renderOverview();
530
+ const charts: Array<HTMLElement> = zoomCharts();
531
+ expect(charts).toHaveLength(2);
532
+ expect(chartWindows()).toEqual(same(HOUR, 2));
533
+ return charts;
534
+ }
535
+
536
+ function expectSameCharts(charts: Array<HTMLElement>): void {
537
+ const now: Array<HTMLElement> = zoomCharts();
538
+ expect(now).toHaveLength(charts.length);
539
+ charts.forEach((chart: HTMLElement, index: number) => {
540
+ // The very same node: React kept the chart mounted.
541
+ expect(now[index]).toBe(chart);
542
+ });
543
+ }
544
+
545
+ describe("Ceph Overview: the Golden Signals rate charts stay on screen while they reload", () => {
546
+ test("before the first load, each rate chart's skeleton holds the chart's 220px", async () => {
547
+ holdRates = true;
548
+ await renderOverview();
549
+
550
+ expect(zoomCharts()).toHaveLength(0);
551
+ expect(skeletons()).toHaveLength(2);
552
+ for (const skeleton of skeletons()) {
553
+ expect(skeleton).toHaveStyle({ height: "220px" });
554
+ }
555
+
556
+ await releaseAllRates();
557
+
558
+ expect(skeletons()).toHaveLength(0);
559
+ expect(zoomCharts()).toHaveLength(2);
560
+ expect(refreshingMarkers()).toHaveLength(0);
561
+ });
562
+
563
+ test("a drag on Client IOPS keeps both charts, over the hour they were fetched for, until the zoom lands", async () => {
564
+ const charts: Array<HTMLElement> = await renderLoadedOverview();
565
+ holdRates = true;
566
+
567
+ await dragAcross(charts[0]!, at("11:20"), at("11:40"));
568
+
569
+ // The zoomed window is asked for...
570
+ expect(rateCallsOver(at("11:20"), at("11:40"))).toBe(4);
571
+ // ...and meanwhile the same two charts stay, still on THEIR hour.
572
+ expectSameCharts(charts);
573
+ expect(chartWindows()).toEqual(same(HOUR, 2));
574
+ expect(refreshingMarkers()).toHaveLength(2);
575
+ expect(skeletons()).toHaveLength(0);
576
+
577
+ await releaseRatesEndingAt(at("11:40"));
578
+
579
+ expectSameCharts(charts);
580
+ expect(chartWindows()).toEqual(same(ZOOM, 2));
581
+ expect(refreshingMarkers()).toHaveLength(0);
582
+ });
583
+
584
+ test("a double-click reset keeps them on screen too, then draws the hour again", async () => {
585
+ await renderLoadedOverview();
586
+ await dragAcross(zoomCharts()[0]!, at("11:20"), at("11:40"));
587
+ const charts: Array<HTMLElement> = zoomCharts();
588
+ expect(chartWindows()).toEqual(same(ZOOM, 2));
589
+ holdRates = true;
590
+
591
+ await doubleClick(charts[1]!);
592
+
593
+ expect(cardPickers()[0]).toHaveTextContent(TimeRange.PAST_ONE_HOUR);
594
+ expectSameCharts(charts);
595
+ expect(chartWindows()).toEqual(same(ZOOM, 2));
596
+ expect(refreshingMarkers()).toHaveLength(2);
597
+
598
+ await releaseRatesEndingAt(NOW);
599
+
600
+ expectSameCharts(charts);
601
+ expect(chartWindows()).toEqual(same(HOUR, 2));
602
+ expect(refreshingMarkers()).toHaveLength(0);
603
+ });
604
+
605
+ test("an auto-refresh tick slides the hour without unmounting the charts (a drag in progress survives it)", async () => {
606
+ const charts: Array<HTMLElement> = await renderLoadedOverview();
607
+ const keys: Array<string | null> = charts.map(
608
+ (chart: HTMLElement): string | null => {
609
+ return chart.getAttribute("data-chart-key");
610
+ },
611
+ );
612
+ holdRates = true;
613
+
614
+ await autoRefreshTick();
615
+
616
+ const tick: Date = new Date(NOW.getTime() + 30_000);
617
+ const tickHourStart: Date = new Date(tick.getTime() - 60 * MINUTE);
618
+ expect(rateCallsOver(tickHourStart, tick)).toBe(4);
619
+ expectSameCharts(charts);
620
+ expect(chartWindows()).toEqual(same(HOUR, 2));
621
+ expect(refreshingMarkers()).toHaveLength(2);
622
+ expect(skeletons()).toHaveLength(0);
623
+
624
+ await releaseRatesEndingAt(tick);
625
+
626
+ expectSameCharts(charts);
627
+ expect(
628
+ zoomCharts().map((chart: HTMLElement): string | null => {
629
+ return chart.getAttribute("data-chart-key");
630
+ }),
631
+ ).toEqual(keys);
632
+ expect(chartWindows()).toEqual(same(windowOf(tickHourStart, tick), 2));
633
+ expect(refreshingMarkers()).toHaveLength(0);
634
+ });
635
+
636
+ test("while zoomed, a tick neither restarts nor repeats the load for the pinned window", async () => {
637
+ await renderLoadedOverview();
638
+ holdRates = true;
639
+ await dragAcross(zoomCharts()[0]!, at("11:20"), at("11:40"));
640
+ const calls: number = rateCalls().length;
641
+
642
+ await autoRefreshTick();
643
+ fireEvent.click(screen.getByTestId("manual-refresh"));
644
+ await flush();
645
+
646
+ expect(rateCalls()).toHaveLength(calls);
647
+ // The load already in flight still lands.
648
+ await releaseRatesEndingAt(at("11:40"));
649
+ expect(chartWindows()).toEqual(same(ZOOM, 2));
650
+ expect(refreshingMarkers()).toHaveLength(0);
651
+ });
652
+
653
+ test("a reload that fails keeps the last chart, with the error above it, and the chart still resets the zoom", async () => {
654
+ await renderLoadedOverview();
655
+ failingRateEnds = [at("11:40").getTime()];
656
+
657
+ await dragAcross(zoomCharts()[0]!, at("11:20"), at("11:40"));
658
+
659
+ expect(zoomCharts()).toHaveLength(2);
660
+ expect(chartWindows()).toEqual(same(HOUR, 2));
661
+ const alerts: Array<HTMLElement> = rateAlerts();
662
+ expect(alerts).toHaveLength(2);
663
+ for (const alert of alerts) {
664
+ expect(alert).toHaveTextContent(
665
+ `Couldn't refresh — showing previously loaded data. ${RATE_ERROR}`,
666
+ );
667
+ }
668
+ expect(resetZoomButtons()).toHaveLength(1);
669
+
670
+ await doubleClick(zoomCharts()[0]!);
671
+
672
+ expect(cardPickers()[0]).toHaveTextContent(TimeRange.PAST_ONE_HOUR);
673
+ expect(chartWindows()).toEqual(same(HOUR, 2));
674
+ expect(rateAlerts()).toHaveLength(0);
675
+ });
676
+
677
+ test("a slow load for a window the reader has left cannot repaint the charts", async () => {
678
+ await renderLoadedOverview();
679
+ holdRates = true;
680
+
681
+ await dragAcross(zoomCharts()[0]!, at("11:20"), at("11:40"));
682
+ await doubleClick(zoomCharts()[1]!);
683
+ await releaseRatesEndingAt(NOW);
684
+ expect(chartWindows()).toEqual(same(HOUR, 2));
685
+
686
+ await releaseRatesEndingAt(at("11:40"));
687
+
688
+ expect(chartWindows()).toEqual(same(HOUR, 2));
689
+ expect(refreshingMarkers()).toHaveLength(0);
690
+ });
691
+
692
+ test("a zoom into a quiet stretch: the empty state keeps the chart's height, selects no text, and reloads in place", async () => {
693
+ await renderLoadedOverview();
694
+ quietRateEnds = [at("11:40").getTime()];
695
+
696
+ await dragAcross(zoomCharts()[0]!, at("11:20"), at("11:40"));
697
+
698
+ expect(zoomCharts()).toHaveLength(0);
699
+ const empty: HTMLElement = screen.getByText(
700
+ "No client I/O reported in the selected time range.",
701
+ );
702
+ expect(empty).toHaveStyle({ height: "220px" });
703
+ expect(empty).toHaveClass("select-none");
704
+ expect(empty.className).not.toMatch(/\bh-48\b/);
705
+
706
+ holdRates = true;
707
+ fireEvent.doubleClick(empty);
708
+ await flush();
709
+
710
+ // The way back reloads the hour; the empty state stays until it lands.
711
+ expect(cardPickers()[0]).toHaveTextContent(TimeRange.PAST_ONE_HOUR);
712
+ expect(
713
+ screen.getByText("No client I/O reported in the selected time range."),
714
+ ).toBe(empty);
715
+ expect(refreshingMarkers()).toHaveLength(2);
716
+ expect(skeletons()).toHaveLength(0);
717
+
718
+ await releaseRatesEndingAt(NOW);
719
+
720
+ expect(zoomCharts()).toHaveLength(2);
721
+ expect(chartWindows()).toEqual(same(HOUR, 2));
722
+ });
723
+ });
724
+
725
+ describe("Ceph Overview: the Golden Signals card's Refresh reaches its rate charts", () => {
726
+ test("zoomed, a failed rate chart is retried by the card's Refresh over the pinned window", async () => {
727
+ await renderLoadedOverview();
728
+ failingRateEnds = [at("11:40").getTime()];
729
+ await dragAcross(zoomCharts()[0]!, at("11:20"), at("11:40"));
730
+ expect(rateCallsOver(at("11:20"), at("11:40"))).toBe(4);
731
+ expect(rateAlerts()).toHaveLength(2);
732
+
733
+ failingRateEnds = [];
734
+ pressCardRefresh();
735
+ await flush();
736
+
737
+ expect(rateCallsOver(at("11:20"), at("11:40"))).toBe(8);
738
+ expect(chartWindows()).toEqual(same(ZOOM, 2));
739
+ expect(rateAlerts()).toHaveLength(0);
740
+ // Still zoomed: Refresh re-sends the pinned window.
741
+ expect(cardPickers()[0]).toHaveTextContent(TimeRange.CUSTOM);
742
+ expect(resetZoomButtons()).toHaveLength(1);
743
+ });
744
+
745
+ test("zoomed and healthy, the card's Refresh reloads both rate charts once each", async () => {
746
+ await renderLoadedOverview();
747
+ await dragAcross(zoomCharts()[0]!, at("11:20"), at("11:40"));
748
+ expect(rateCallsOver(at("11:20"), at("11:40"))).toBe(4);
749
+
750
+ pressCardRefresh();
751
+ await flush();
752
+
753
+ expect(rateCallsOver(at("11:20"), at("11:40"))).toBe(8);
754
+ expect(chartWindows()).toEqual(same(ZOOM, 2));
755
+ });
756
+
757
+ test("not zoomed, the card's Refresh loads each rate metric once, over the re-resolved hour", async () => {
758
+ await renderLoadedOverview();
759
+ const later: Date = new Date(NOW.getTime() + MINUTE);
760
+ jest.setSystemTime(later);
761
+ const before: number = rateCalls().length;
762
+
763
+ pressCardRefresh();
764
+ await flush();
765
+
766
+ // The nonce and the new window land together: one load, not two.
767
+ expect(rateCalls().length - before).toBe(4);
768
+ expect(rateCallsOver(new Date(later.getTime() - 60 * MINUTE), later)).toBe(
769
+ 4,
770
+ );
771
+ });
772
+
773
+ test("the page's own Refresh now leaves a pinned zoom's rate charts alone", async () => {
774
+ await renderLoadedOverview();
775
+ await dragAcross(zoomCharts()[0]!, at("11:20"), at("11:40"));
776
+ const before: number = rateCalls().length;
777
+
778
+ fireEvent.click(screen.getByTestId("manual-refresh"));
779
+ await flush();
780
+
781
+ expect(rateCalls()).toHaveLength(before);
782
+ expect(chartWindows()).toEqual(same(ZOOM, 2));
783
+ });
784
+ });
785
+
786
+ describe("Ceph Overview: each rate chart's heading names the drag", () => {
787
+ test("one hover-revealed hint per rate chart, in a group around that chart alone", async () => {
788
+ await renderLoadedOverview();
789
+
790
+ const hints: Array<HTMLElement> = zoomHints();
791
+ expect(hints).toHaveLength(2);
792
+ const titles: Array<string> = ["Client IOPS", "Client Throughput"];
793
+ hints.forEach((hint: HTMLElement, index: number) => {
794
+ const group: HTMLElement = expectRevealedOnHoverOf(hint);
795
+ expect(group).toHaveTextContent(titles[index]!);
796
+ const charts: NodeListOf<Element> = group.querySelectorAll(
797
+ `[data-testid="${ZOOM_CHART_TEST_ID}"]`,
798
+ );
799
+ expect(charts).toHaveLength(1);
800
+ expect(charts[0]).toBe(zoomCharts()[index]);
801
+ expect(hint).toHaveTextContent(TIME_RANGE_ZOOM_HINT_TEXT);
802
+ // The heading's own weight stays off the hint.
803
+ expect(hint).toHaveClass("ml-auto", "font-normal");
804
+ });
805
+ });
806
+
807
+ test("zoomed, the hints name the double-click back; reset, they name the drag again", async () => {
808
+ await renderLoadedOverview();
809
+
810
+ await dragAcross(zoomCharts()[1]!, at("11:20"), at("11:40"));
811
+ expect(zoomHints()).toHaveLength(2);
812
+ for (const hint of zoomHints()) {
813
+ expect(hint).toHaveTextContent(TIME_RANGE_ZOOM_HINT_RESET_TEXT);
814
+ }
815
+
816
+ await doubleClick(zoomCharts()[0]!);
817
+ expect(zoomHints()).toHaveLength(2);
818
+ for (const hint of zoomHints()) {
819
+ expect(hint).toHaveTextContent(TIME_RANGE_ZOOM_HINT_TEXT);
820
+ }
821
+ });
822
+
823
+ test("the hint stays in the heading while a chart reloads", async () => {
824
+ await renderLoadedOverview();
825
+ holdRates = true;
826
+
827
+ await dragAcross(zoomCharts()[0]!, at("11:20"), at("11:40"));
828
+
829
+ expect(zoomHints()).toHaveLength(2);
830
+ expect(refreshingMarkers()).toHaveLength(2);
831
+ });
832
+ });
833
+
834
+ // --------------------------------------------------------------- Insights
835
+
836
+ async function renderInsights(): Promise<void> {
837
+ render(<CephClusterInsights {...PAGE_PROPS} />);
838
+ await flush();
839
+ }
840
+
841
+ async function renderLoadedInsights(): Promise<Array<HTMLElement>> {
842
+ await renderInsights();
843
+ // Capacity, Latency and Data Health have a MetricView; Client I/O does not.
844
+ expect(metricViews()).toHaveLength(3);
845
+ const charts: Array<HTMLElement> = zoomCharts();
846
+ expect(charts).toHaveLength(2);
847
+ expect(chartWindows()).toEqual(same(HOUR, 2));
848
+ return charts;
849
+ }
850
+
851
+ // The four cards' Refresh buttons, in page order.
852
+ const CLIENT_IO_CARD: number = 1;
853
+
854
+ describe("Ceph Insights: the Client I/O rate charts stay on screen while they reload", () => {
855
+ test("before the first load, each skeleton holds the chart's default 300px", async () => {
856
+ holdRates = true;
857
+ await renderInsights();
858
+
859
+ expect(skeletons()).toHaveLength(2);
860
+ for (const skeleton of skeletons()) {
861
+ expect(skeleton).toHaveStyle({ height: "300px" });
862
+ }
863
+
864
+ await releaseAllRates();
865
+
866
+ expect(skeletons()).toHaveLength(0);
867
+ expect(zoomCharts()).toHaveLength(2);
868
+ });
869
+
870
+ test("a drag on Client Throughput keeps both charts while every card retimes", async () => {
871
+ const charts: Array<HTMLElement> = await renderLoadedInsights();
872
+ holdRates = true;
873
+
874
+ await dragAcross(charts[1]!, at("11:20"), at("11:40"));
875
+
876
+ // The three metric cards move to the zoom at once...
877
+ expect(chartWindows(metricViews())).toEqual(same(ZOOM, 3));
878
+ // ...and the rate charts stay, on their hour, until their load lands.
879
+ expectSameCharts(charts);
880
+ expect(chartWindows()).toEqual(same(HOUR, 2));
881
+ expect(refreshingMarkers()).toHaveLength(2);
882
+
883
+ await releaseRatesEndingAt(at("11:40"));
884
+
885
+ expectSameCharts(charts);
886
+ expect(chartWindows()).toEqual(same(ZOOM, 2));
887
+ });
888
+
889
+ test("a double-click in another card keeps them on screen while they reload the hour", async () => {
890
+ await renderLoadedInsights();
891
+ await dragAcross(metricViews()[0]!, at("11:20"), at("11:40"));
892
+ const charts: Array<HTMLElement> = zoomCharts();
893
+ holdRates = true;
894
+
895
+ await doubleClick(metricViews()[2]!);
896
+
897
+ expectSameCharts(charts);
898
+ expect(chartWindows()).toEqual(same(ZOOM, 2));
899
+ await releaseRatesEndingAt(NOW);
900
+ expect(chartWindows()).toEqual(same(HOUR, 2));
901
+ });
902
+
903
+ test("zoomed, the Client I/O card's Refresh retries its failed rate charts", async () => {
904
+ await renderLoadedInsights();
905
+ failingRateEnds = [at("11:40").getTime()];
906
+ await dragAcross(zoomCharts()[0]!, at("11:20"), at("11:40"));
907
+ expect(rateAlerts()).toHaveLength(2);
908
+ expect(rateCallsOver(at("11:20"), at("11:40"))).toBe(4);
909
+
910
+ failingRateEnds = [];
911
+ // The card holds only the rate charts: its Refresh has nothing else to do.
912
+ pressCardRefresh(CLIENT_IO_CARD);
913
+ await flush();
914
+
915
+ expect(rateCallsOver(at("11:20"), at("11:40"))).toBe(8);
916
+ expect(rateAlerts()).toHaveLength(0);
917
+ expect(chartWindows()).toEqual(same(ZOOM, 2));
918
+ expect(resetZoomButtons()).toHaveLength(4);
919
+ });
920
+
921
+ test("each rate chart's heading names the drag, revealed over that chart", async () => {
922
+ await renderLoadedInsights();
923
+
924
+ const hints: Array<HTMLElement> = zoomHints();
925
+ expect(hints).toHaveLength(2);
926
+ hints.forEach((hint: HTMLElement, index: number) => {
927
+ const group: HTMLElement = expectRevealedOnHoverOf(hint);
928
+ expect(
929
+ group.querySelectorAll(`[data-testid="${ZOOM_CHART_TEST_ID}"]`),
930
+ ).toHaveLength(1);
931
+ expect(group).toContainElement(zoomCharts()[index]!);
932
+ });
933
+ expect(hints[0]!.closest('[class~="group/zoomhint"]')).toHaveTextContent(
934
+ "Client IOPS",
935
+ );
936
+ expect(hints[1]!.closest('[class~="group/zoomhint"]')).toHaveTextContent(
937
+ "Client Throughput",
938
+ );
939
+
940
+ await dragAcross(metricViews()[2]!, at("11:20"), at("11:40"));
941
+ for (const hint of zoomHints()) {
942
+ expect(hint).toHaveTextContent(TIME_RANGE_ZOOM_HINT_RESET_TEXT);
943
+ }
944
+ });
945
+ });
946
+
947
+ // ------------------------------------------------ pool detail, Metrics tab
948
+
949
+ async function renderPoolMetrics(): Promise<void> {
950
+ mockLastParam = "1";
951
+ render(<CephClusterPoolDetail {...PAGE_PROPS} />);
952
+ await flush();
953
+ fireEvent.click(await screen.findByTestId("tab-Metrics"));
954
+ await flush();
955
+ }
956
+
957
+ describe("Ceph pool detail, Metrics tab: the pool's rate charts stay on screen while they reload", () => {
958
+ test("before the first load, each skeleton holds the chart's default 300px", async () => {
959
+ holdRates = true;
960
+ await renderPoolMetrics();
961
+
962
+ expect(skeletons()).toHaveLength(2);
963
+ for (const skeleton of skeletons()) {
964
+ expect(skeleton).toHaveStyle({ height: "300px" });
965
+ }
966
+ await releaseAllRates();
967
+ expect(zoomCharts()).toHaveLength(2);
968
+ });
969
+
970
+ test("a drag keeps both charts on their hour until the zoom lands, still for this pool", async () => {
971
+ await renderPoolMetrics();
972
+ const charts: Array<HTMLElement> = zoomCharts();
973
+ expect(charts).toHaveLength(2);
974
+ holdRates = true;
975
+
976
+ await dragAcross(charts[0]!, at("11:20"), at("11:40"));
977
+
978
+ expectSameCharts(charts);
979
+ expect(chartWindows()).toEqual(same(HOUR, 2));
980
+ expect(refreshingMarkers()).toHaveLength(2);
981
+ for (const held of heldRates) {
982
+ expect(held.call.attributes["pool_id"]).toBe("1");
983
+ }
984
+
985
+ await releaseRatesEndingAt(at("11:40"));
986
+
987
+ expectSameCharts(charts);
988
+ expect(chartWindows()).toEqual(same(ZOOM, 2));
989
+ });
990
+
991
+ test("zoomed, the tab's Refresh retries a failed rate chart for this pool", async () => {
992
+ await renderPoolMetrics();
993
+ failingRateEnds = [at("11:40").getTime()];
994
+ await dragAcross(zoomCharts()[1]!, at("11:20"), at("11:40"));
995
+ expect(rateAlerts()).toHaveLength(2);
996
+
997
+ failingRateEnds = [];
998
+ pressCardRefresh();
999
+ await flush();
1000
+
1001
+ const retried: Array<AggregateCall> = rateCalls().filter(
1002
+ (call: AggregateCall): boolean => {
1003
+ return (
1004
+ call.start.getTime() === at("11:20").getTime() &&
1005
+ call.end.getTime() === at("11:40").getTime()
1006
+ );
1007
+ },
1008
+ );
1009
+ expect(retried).toHaveLength(8);
1010
+ for (const call of retried) {
1011
+ expect(call.attributes["pool_id"]).toBe("1");
1012
+ }
1013
+ expect(rateAlerts()).toHaveLength(0);
1014
+ expect(chartWindows()).toEqual(same(ZOOM, 2));
1015
+ expect(cardPickers()[0]).toHaveTextContent(TimeRange.CUSTOM);
1016
+ });
1017
+
1018
+ test("each rate chart's heading names the drag, revealed over that chart", async () => {
1019
+ await renderPoolMetrics();
1020
+
1021
+ const hints: Array<HTMLElement> = zoomHints();
1022
+ expect(hints).toHaveLength(2);
1023
+ hints.forEach((hint: HTMLElement, index: number) => {
1024
+ const group: HTMLElement = expectRevealedOnHoverOf(hint);
1025
+ expect(group).toContainElement(zoomCharts()[index]!);
1026
+ expect(hint).toHaveTextContent(TIME_RANGE_ZOOM_HINT_TEXT);
1027
+ });
1028
+ });
1029
+ });
1030
+
1031
+ // ---------------------------------- CephRateChart in a card of its own
1032
+
1033
+ describe("CephRateChart in a card on a Custom window", () => {
1034
+ function renderCard(): void {
1035
+ render(
1036
+ <EmbeddedMetricCard
1037
+ title="Client I/O"
1038
+ defaultTimeRange={{
1039
+ range: TimeRange.CUSTOM,
1040
+ startAndEndDate: new InBetween<Date>(at("11:20"), at("11:40")),
1041
+ }}
1042
+ renderExtraCharts={(dateRange: InBetween<Date>): React.ReactElement => {
1043
+ return (
1044
+ <CephRateChart
1045
+ clusterName="ceph-prod"
1046
+ series={[
1047
+ { metricName: "ceph_pool_rd", label: "Read" },
1048
+ { metricName: "ceph_pool_wr", label: "Write" },
1049
+ ]}
1050
+ seriesKeyAttributes={["pool_id"]}
1051
+ startDate={dateRange.startValue}
1052
+ endDate={dateRange.endValue}
1053
+ />
1054
+ );
1055
+ }}
1056
+ />,
1057
+ );
1058
+ }
1059
+
1060
+ test("a failed first load is retried by the card's Refresh: a skeleton while it runs, then the chart", async () => {
1061
+ failingRateEnds = [at("11:40").getTime()];
1062
+ renderCard();
1063
+ await flush();
1064
+
1065
+ expect(screen.getByText(RATE_ERROR)).toBeInTheDocument();
1066
+ expect(zoomCharts()).toHaveLength(0);
1067
+ expect(rateCallsOver(at("11:20"), at("11:40"))).toBe(2);
1068
+
1069
+ failingRateEnds = [];
1070
+ holdRates = true;
1071
+ pressCardRefresh();
1072
+ await flush();
1073
+
1074
+ expect(rateCallsOver(at("11:20"), at("11:40"))).toBe(4);
1075
+ // Nothing was ever drawn, so the retry is a skeleton at the chart's height.
1076
+ expect(skeletons()).toHaveLength(1);
1077
+ expect(skeletons()[0]).toHaveStyle({ height: "300px" });
1078
+ expect(screen.queryByText(RATE_ERROR)).toBeNull();
1079
+
1080
+ await releaseAllRates();
1081
+
1082
+ expect(zoomCharts()).toHaveLength(1);
1083
+ expect(chartWindows()).toEqual([ZOOM]);
1084
+ });
1085
+
1086
+ test("a failed retry shows the error again", async () => {
1087
+ failingRateEnds = [at("11:40").getTime()];
1088
+ renderCard();
1089
+ await flush();
1090
+
1091
+ pressCardRefresh();
1092
+ await flush();
1093
+
1094
+ expect(rateCallsOver(at("11:20"), at("11:40"))).toBe(4);
1095
+ expect(screen.getByText(RATE_ERROR)).toBeInTheDocument();
1096
+ expect(skeletons()).toHaveLength(0);
1097
+ });
1098
+
1099
+ test("a retry never moves the page: the error, the retry's skeleton and the error again all hold the chart's height", async () => {
1100
+ failingRateEnds = [at("11:40").getTime()];
1101
+ renderCard();
1102
+ await flush();
1103
+ expect(errorBoxes()).toHaveLength(1);
1104
+ expect(errorBoxes()[0]).toHaveStyle({ minHeight: "300px" });
1105
+
1106
+ holdRates = true;
1107
+ pressCardRefresh();
1108
+ await flush();
1109
+
1110
+ expect(errorBoxes()).toHaveLength(0);
1111
+ expect(skeletons()[0]).toHaveStyle({ height: "300px" });
1112
+
1113
+ await releaseAllRates();
1114
+
1115
+ expect(errorBoxes()).toHaveLength(1);
1116
+ expect(errorBoxes()[0]).toHaveStyle({ minHeight: "300px" });
1117
+ });
1118
+ });
1119
+
1120
+ // --------------------------------- a rate chart with nothing drawn, failed
1121
+
1122
+ // The box around each rate chart's error when it has nothing to draw.
1123
+ function errorBoxes(): Array<HTMLElement> {
1124
+ return screen.queryAllByText(RATE_ERROR).map((error: HTMLElement) => {
1125
+ return error.parentElement as HTMLElement;
1126
+ });
1127
+ }
1128
+
1129
+ describe("A Ceph rate chart that failed with nothing drawn keeps its place and takes the way back", () => {
1130
+ test("on Insights, the error holds the chart's 300px and selects no text; unzoomed, a double-click does nothing", async () => {
1131
+ failingRateEnds = [NOW.getTime()];
1132
+ await renderInsights();
1133
+
1134
+ expect(zoomCharts()).toHaveLength(0);
1135
+ const boxes: Array<HTMLElement> = errorBoxes();
1136
+ expect(boxes).toHaveLength(2);
1137
+ for (const box of boxes) {
1138
+ expect(box).toHaveStyle({ minHeight: "300px" });
1139
+ expect(box).toHaveClass("select-none");
1140
+ }
1141
+ const calls: number = rateCalls().length;
1142
+
1143
+ fireEvent.doubleClick(screen.getAllByText(RATE_ERROR)[0]!);
1144
+ await flush();
1145
+
1146
+ // Not zoomed, so there is nothing to go back to.
1147
+ expect(rateCalls()).toHaveLength(calls);
1148
+ expect(cardPickers()[0]).toHaveTextContent(TimeRange.PAST_ONE_HOUR);
1149
+ expect(errorBoxes()).toHaveLength(2);
1150
+ });
1151
+
1152
+ test("on the Overview, the error holds the chart's 220px", async () => {
1153
+ failingRateEnds = [NOW.getTime()];
1154
+ await renderOverview();
1155
+
1156
+ const boxes: Array<HTMLElement> = errorBoxes();
1157
+ expect(boxes).toHaveLength(2);
1158
+ for (const box of boxes) {
1159
+ expect(box).toHaveStyle({ minHeight: "220px" });
1160
+ }
1161
+ });
1162
+
1163
+ test("zoomed from another card, a double-click on the error resets every card and the charts draw the hour", async () => {
1164
+ failingRateEnds = [NOW.getTime(), at("11:40").getTime()];
1165
+ await renderInsights();
1166
+ await dragAcross(metricViews()[0]!, at("11:20"), at("11:40"));
1167
+
1168
+ expect(chartWindows(metricViews())).toEqual(same(ZOOM, 3));
1169
+ expect(rateCallsOver(at("11:20"), at("11:40"))).toBe(4);
1170
+ expect(errorBoxes()).toHaveLength(2);
1171
+ // The heading names the way back...
1172
+ expect(zoomHints()).toHaveLength(2);
1173
+ for (const hint of zoomHints()) {
1174
+ expect(hint).toHaveTextContent(TIME_RANGE_ZOOM_HINT_RESET_TEXT);
1175
+ }
1176
+
1177
+ failingRateEnds = [];
1178
+ // ...and the error, where the pointer already is, takes it.
1179
+ fireEvent.doubleClick(screen.getAllByText(RATE_ERROR)[1]!);
1180
+ await flush();
1181
+
1182
+ expect(cardPickers()[0]).toHaveTextContent(TimeRange.PAST_ONE_HOUR);
1183
+ expect(chartWindows(metricViews())).toEqual(same(HOUR, 3));
1184
+ expect(resetZoomButtons()).toHaveLength(0);
1185
+ expect(errorBoxes()).toHaveLength(0);
1186
+ expect(zoomCharts()).toHaveLength(2);
1187
+ expect(chartWindows()).toEqual(same(HOUR, 2));
1188
+ });
1189
+ });
1190
+
1191
+ // ------------------------------- CephRateChart rerendered for other counters
1192
+
1193
+ /*
1194
+ * Runs its callback in the layout phase of every commit that rerenders it:
1195
+ * after that commit's DOM changes and before any effect can follow them up,
1196
+ * so it sees what the reader could see for a frame.
1197
+ */
1198
+ function CommitProbe(props: { onCommit: () => void }): null {
1199
+ React.useLayoutEffect((): void => {
1200
+ props.onCommit();
1201
+ });
1202
+ return null;
1203
+ }
1204
+
1205
+ describe("CephRateChart keeps its last chart, and its error, only for the counters they came from", () => {
1206
+ interface PoolChartOptions {
1207
+ poolId: string;
1208
+ start: Date;
1209
+ end: Date;
1210
+ clusterName?: string | undefined;
1211
+ }
1212
+
1213
+ // The page's text after each commit of the tree around the chart.
1214
+ let commits: Array<string> = [];
1215
+
1216
+ // A pool's rate chart as PoolDetail draws it, beside a commit probe.
1217
+ function poolChart(options: PoolChartOptions): React.ReactElement {
1218
+ return (
1219
+ <>
1220
+ <CephRateChart
1221
+ clusterName={options.clusterName ?? "ceph-prod"}
1222
+ series={[
1223
+ { metricName: "ceph_pool_rd", label: "Read" },
1224
+ { metricName: "ceph_pool_wr", label: "Write" },
1225
+ ]}
1226
+ seriesKeyAttributes={["pool_id"]}
1227
+ extraAttributes={{ pool_id: options.poolId }}
1228
+ startDate={options.start}
1229
+ endDate={options.end}
1230
+ heightInPx={220}
1231
+ />
1232
+ <CommitProbe
1233
+ onCommit={(): void => {
1234
+ commits.push(document.body.textContent || "");
1235
+ }}
1236
+ />
1237
+ </>
1238
+ );
1239
+ }
1240
+
1241
+ function heldAttribute(name: string): Array<unknown> {
1242
+ return heldRates.map((held: HeldRateLoad): unknown => {
1243
+ return held.call.attributes[name];
1244
+ });
1245
+ }
1246
+
1247
+ async function releaseRatesForPool(poolId: string): Promise<void> {
1248
+ const releasing: Array<HeldRateLoad> = heldRates.filter(
1249
+ (held: HeldRateLoad): boolean => {
1250
+ return held.call.attributes["pool_id"] === poolId;
1251
+ },
1252
+ );
1253
+ heldRates = heldRates.filter((held: HeldRateLoad): boolean => {
1254
+ return !releasing.includes(held);
1255
+ });
1256
+ expect(releasing.length).toBeGreaterThan(0);
1257
+ for (const held of releasing) {
1258
+ held.release();
1259
+ }
1260
+ await flush();
1261
+ }
1262
+
1263
+ beforeEach(() => {
1264
+ commits = [];
1265
+ });
1266
+
1267
+ test("a new window for the same pool keeps the chart, on its last window, until the load lands", async () => {
1268
+ const { rerender } = render(
1269
+ poolChart({ poolId: "1", start: at("11:00"), end: NOW }),
1270
+ );
1271
+ await flush();
1272
+ const charts: Array<HTMLElement> = zoomCharts();
1273
+ expect(charts).toHaveLength(1);
1274
+ expect(chartWindows()).toEqual([HOUR]);
1275
+ holdRates = true;
1276
+
1277
+ rerender(poolChart({ poolId: "1", start: at("11:20"), end: at("11:40") }));
1278
+ await flush();
1279
+
1280
+ expectSameCharts(charts);
1281
+ expect(chartWindows()).toEqual([HOUR]);
1282
+ expect(refreshingMarkers()).toHaveLength(1);
1283
+ expect(skeletons()).toHaveLength(0);
1284
+ expect(heldAttribute("pool_id")).toEqual(["1", "1"]);
1285
+
1286
+ await releaseAllRates();
1287
+
1288
+ expectSameCharts(charts);
1289
+ expect(chartWindows()).toEqual([ZOOM]);
1290
+ expect(refreshingMarkers()).toHaveLength(0);
1291
+ });
1292
+
1293
+ test("another pool starts over from a skeleton at the chart's height, never drawing the last pool's rates", async () => {
1294
+ const { rerender } = render(
1295
+ poolChart({ poolId: "1", start: at("11:00"), end: NOW }),
1296
+ );
1297
+ await flush();
1298
+ expect(zoomCharts()).toHaveLength(1);
1299
+ holdRates = true;
1300
+ commits = [];
1301
+
1302
+ rerender(poolChart({ poolId: "2", start: at("11:00"), end: NOW }));
1303
+
1304
+ // Already the switch's own commit draws no chart under pool 2.
1305
+ expect(commits.length).toBeGreaterThan(0);
1306
+ for (const text of commits) {
1307
+ expect(text).not.toContain("Drag across the chart");
1308
+ }
1309
+ expect(zoomCharts()).toHaveLength(0);
1310
+ expect(skeletons()).toHaveLength(1);
1311
+ expect(skeletons()[0]).toHaveStyle({ height: "220px" });
1312
+ expect(refreshingMarkers()).toHaveLength(0);
1313
+ expect(heldAttribute("pool_id")).toEqual(["2", "2"]);
1314
+
1315
+ await releaseAllRates();
1316
+
1317
+ expect(skeletons()).toHaveLength(0);
1318
+ expect(zoomCharts()).toHaveLength(1);
1319
+ expect(chartWindows()).toEqual([HOUR]);
1320
+ });
1321
+
1322
+ test("another cluster starts over from the skeleton too", async () => {
1323
+ const { rerender } = render(
1324
+ poolChart({ poolId: "1", start: at("11:00"), end: NOW }),
1325
+ );
1326
+ await flush();
1327
+ expect(zoomCharts()).toHaveLength(1);
1328
+ holdRates = true;
1329
+
1330
+ rerender(
1331
+ poolChart({
1332
+ poolId: "1",
1333
+ start: at("11:00"),
1334
+ end: NOW,
1335
+ clusterName: "ceph-dr",
1336
+ }),
1337
+ );
1338
+ await flush();
1339
+
1340
+ expect(zoomCharts()).toHaveLength(0);
1341
+ expect(skeletons()).toHaveLength(1);
1342
+ expect(heldAttribute("resource.ceph.cluster.name")).toEqual([
1343
+ "ceph-dr",
1344
+ "ceph-dr",
1345
+ ]);
1346
+
1347
+ await releaseAllRates();
1348
+
1349
+ expect(zoomCharts()).toHaveLength(1);
1350
+ });
1351
+
1352
+ test("a slow load for the last pool cannot paint over the next one", async () => {
1353
+ holdRates = true;
1354
+ const { rerender } = render(
1355
+ poolChart({ poolId: "1", start: at("11:00"), end: NOW }),
1356
+ );
1357
+ await flush();
1358
+ rerender(poolChart({ poolId: "2", start: at("11:00"), end: NOW }));
1359
+ await flush();
1360
+
1361
+ await releaseRatesForPool("1");
1362
+
1363
+ expect(zoomCharts()).toHaveLength(0);
1364
+ expect(skeletons()).toHaveLength(1);
1365
+
1366
+ await releaseRatesForPool("2");
1367
+
1368
+ expect(zoomCharts()).toHaveLength(1);
1369
+ expect(skeletons()).toHaveLength(0);
1370
+ });
1371
+
1372
+ test("for the same pool, a failed reload's error stays up while its retry runs, and clears when it lands", async () => {
1373
+ const { rerender } = render(
1374
+ poolChart({ poolId: "1", start: at("11:00"), end: NOW }),
1375
+ );
1376
+ await flush();
1377
+ failingRateEnds = [at("11:40").getTime()];
1378
+ rerender(poolChart({ poolId: "1", start: at("11:20"), end: at("11:40") }));
1379
+ await flush();
1380
+ expect(zoomCharts()).toHaveLength(1);
1381
+ expect(screen.getByRole("alert")).toHaveTextContent(RATE_ERROR);
1382
+
1383
+ failingRateEnds = [];
1384
+ holdRates = true;
1385
+ rerender(poolChart({ poolId: "1", start: at("11:25"), end: at("11:40") }));
1386
+ await flush();
1387
+
1388
+ // The retry is in flight: the chart, its error and the marker stay.
1389
+ expect(zoomCharts()).toHaveLength(1);
1390
+ expect(screen.getByRole("alert")).toHaveTextContent(RATE_ERROR);
1391
+ expect(refreshingMarkers()).toHaveLength(1);
1392
+
1393
+ await releaseAllRates();
1394
+
1395
+ expect(screen.queryByRole("alert")).toBeNull();
1396
+ expect(chartWindows()).toEqual([windowOf(at("11:25"), at("11:40"))]);
1397
+ });
1398
+
1399
+ test("a failed first load for one pool is never shown under another, not even for a frame", async () => {
1400
+ failingRateEnds = [NOW.getTime()];
1401
+ const { rerender } = render(
1402
+ poolChart({ poolId: "1", start: at("11:00"), end: NOW }),
1403
+ );
1404
+ await flush();
1405
+ expect(screen.getByText(RATE_ERROR)).toBeInTheDocument();
1406
+ holdRates = true;
1407
+ commits = [];
1408
+
1409
+ rerender(poolChart({ poolId: "2", start: at("11:00"), end: NOW }));
1410
+
1411
+ expect(commits.length).toBeGreaterThan(0);
1412
+ for (const text of commits) {
1413
+ expect(text).not.toContain(RATE_ERROR);
1414
+ }
1415
+ expect(skeletons()).toHaveLength(1);
1416
+ expect(screen.queryByText(RATE_ERROR)).toBeNull();
1417
+ });
1418
+
1419
+ test("a failed reload's error stays with its pool: another pool's first frame is only its skeleton", async () => {
1420
+ const { rerender } = render(
1421
+ poolChart({ poolId: "1", start: at("11:00"), end: NOW }),
1422
+ );
1423
+ await flush();
1424
+ failingRateEnds = [at("11:40").getTime()];
1425
+ rerender(poolChart({ poolId: "1", start: at("11:20"), end: at("11:40") }));
1426
+ await flush();
1427
+ expect(screen.getByRole("alert")).toHaveTextContent(RATE_ERROR);
1428
+ holdRates = true;
1429
+ commits = [];
1430
+
1431
+ rerender(poolChart({ poolId: "2", start: at("11:20"), end: at("11:40") }));
1432
+
1433
+ expect(commits.length).toBeGreaterThan(0);
1434
+ for (const text of commits) {
1435
+ expect(text).not.toContain(RATE_ERROR);
1436
+ }
1437
+ expect(zoomCharts()).toHaveLength(0);
1438
+ expect(skeletons()).toHaveLength(1);
1439
+ expect(screen.queryByRole("alert")).toBeNull();
1440
+ });
1441
+
1442
+ test("a pool's own failure shows once its load for that pool fails", async () => {
1443
+ const { rerender } = render(
1444
+ poolChart({ poolId: "1", start: at("11:00"), end: NOW }),
1445
+ );
1446
+ await flush();
1447
+ failingRateEnds = [NOW.getTime()];
1448
+
1449
+ rerender(poolChart({ poolId: "2", start: at("11:00"), end: NOW }));
1450
+ await flush();
1451
+
1452
+ expect(zoomCharts()).toHaveLength(0);
1453
+ expect(skeletons()).toHaveLength(0);
1454
+ expect(screen.getByText(RATE_ERROR)).toBeInTheDocument();
1455
+ });
1456
+ });