@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.
- package/Models/DatabaseModels/Index.ts +2 -0
- package/Models/DatabaseModels/KubernetesAiAgent.ts +364 -0
- package/Models/DatabaseModels/KubernetesCluster.ts +3 -3
- package/Models/DatabaseModels/Project.ts +55 -5
- package/Models/DatabaseModels/ProxmoxResource.ts +49 -1
- package/Models/DatabaseModels/RunnerJob.ts +75 -0
- package/Models/DatabaseModels/TeamComplianceSetting.ts +143 -2
- package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +17 -0
- package/Server/API/AIChatAPI.ts +6 -3
- package/Server/API/AIInvestigationAPI.ts +17 -10
- package/Server/API/AutoRemediationAPI.ts +207 -25
- package/Server/API/KubernetesClusterAiAccessAPI.ts +623 -21
- package/Server/API/MicrosoftTeamsAPI.ts +93 -14
- package/Server/API/ProxmoxResourceAPI.ts +244 -14
- package/Server/Infrastructure/GlobalCache.ts +32 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1796000000000-AddTeamComplianceRuleScope.ts +99 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1796100000000-AddKubernetesAiAgentAndAiDefaults.ts +137 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1796200000000-AddProxmoxResourceNativePushColumns.ts +42 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
- package/Server/Middleware/TelemetryIngest.ts +41 -18
- package/Server/Services/AIService.ts +70 -1
- package/Server/Services/AlertService.ts +352 -11
- package/Server/Services/AlertSeverityService.ts +65 -15
- package/Server/Services/AutoRemediationRuleService.ts +1 -1
- package/Server/Services/IncidentService.ts +280 -104
- package/Server/Services/IncidentSeverityService.ts +66 -15
- package/Server/Services/KubernetesAiAgentJobService.ts +185 -0
- package/Server/Services/KubernetesAiAgentService.ts +1634 -0
- package/Server/Services/KubernetesClusterAiAccessService.ts +1118 -451
- package/Server/Services/KubernetesClusterService.ts +129 -21
- package/Server/Services/ProjectService.ts +32 -0
- package/Server/Services/ProxmoxResourceService.ts +322 -5
- package/Server/Services/RunnerJobService.ts +241 -104
- package/Server/Services/RunnerService.ts +1 -1
- package/Server/Services/ScheduledMaintenanceService.ts +737 -1
- package/Server/Services/ScheduledMaintenanceStateTimelineService.ts +414 -56
- package/Server/Services/TeamComplianceSettingService.ts +1125 -9
- package/Server/Utils/AI/ClusterAccess/ClusterAccessContext.ts +67 -9
- package/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.ts +39 -10
- package/Server/Utils/AI/ClusterAccess/KubectlJobRunner.ts +14 -3
- package/Server/Utils/AI/Remediation/RemediationCommandTools.ts +37 -15
- package/Server/Utils/AI/Remediation/RemediationExecutionRunner.ts +67 -24
- package/Server/Utils/AI/SRE/AIInvestigationEngine.ts +48 -5
- package/Server/Utils/AI/SRE/IncidentPostmortemRunner.ts +40 -8
- package/Server/Utils/AI/SRE/InvestigationEligibility.ts +13 -2
- package/Server/Utils/AI/SRE/InvestigationGrader.ts +1 -1
- package/Server/Utils/AutoRemediation/CommandPlanExecutor.ts +6 -1
- package/Server/Utils/Telemetry/InventoryEntityRegistry.ts +133 -7
- package/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.ts +58 -4
- package/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.ts +447 -0
- package/Server/Utils/Telemetry/ProxmoxNativePush.ts +903 -0
- package/Server/Utils/Telemetry/TelemetryEntity.ts +87 -27
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +684 -47
- package/Tests/App/AdminDashboard/RowActionPlacementOverrides.test.tsx +278 -0
- package/Tests/App/Dashboard/AIChatChartWidgetZoom.test.tsx +738 -0
- package/Tests/App/Dashboard/AIFeaturesSettingsPointers.test.tsx +147 -0
- package/Tests/App/Dashboard/AIInsightsKubernetesClusterPage.test.tsx +1523 -0
- package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +463 -3
- package/Tests/App/Dashboard/AffectedResourcesPickerDropdownPlacement.test.tsx +958 -0
- package/Tests/App/Dashboard/AffectedResourcesPickerFocusLeave.test.tsx +595 -0
- package/Tests/App/Dashboard/AskAiAvailability.test.ts +20 -3
- package/Tests/App/Dashboard/AskAiDisabled.test.tsx +8 -3
- package/Tests/App/Dashboard/CephRateChartReload.test.tsx +1456 -0
- package/Tests/App/Dashboard/CephTimeRangeZoom.test.tsx +1229 -0
- package/Tests/App/Dashboard/ChartCardTimeRangeZoom.test.tsx +151 -0
- package/Tests/App/Dashboard/ChartCardZoomAffordances.test.tsx +257 -0
- package/Tests/App/Dashboard/ChartRefetchFrame.test.tsx +141 -0
- package/Tests/App/Dashboard/ChartZoomHarness.tsx +249 -0
- package/Tests/App/Dashboard/ChartZoomStandIn.tsx +131 -0
- package/Tests/App/Dashboard/CloudServerlessSlowLoads.test.tsx +628 -0
- package/Tests/App/Dashboard/CloudServerlessTimeRangeZoom.test.tsx +607 -0
- package/Tests/App/Dashboard/ClusterAccessNotice.test.tsx +91 -19
- package/Tests/App/Dashboard/CompanionMetricsTabZoom.test.tsx +632 -0
- package/Tests/App/Dashboard/ContainerChartZoomStandIn.tsx +195 -0
- package/Tests/App/Dashboard/ContainerHostOverviewSlowLoads.test.tsx +542 -0
- package/Tests/App/Dashboard/ContainerHostOverviewTimeRangeZoom.test.tsx +848 -0
- package/Tests/App/Dashboard/ContainerMetricsTimeRangeZoom.test.tsx +561 -0
- package/Tests/App/Dashboard/DashboardBoardWideZoomShell.test.tsx +1147 -0
- package/Tests/App/Dashboard/DashboardEditPermissions.test.tsx +3 -2
- package/Tests/App/Dashboard/DashboardHistogramWidgetDragCursor.test.tsx +399 -0
- package/Tests/App/Dashboard/DashboardHistogramWidgetZoom.test.tsx +985 -0
- package/Tests/App/Dashboard/DashboardHistogramWidgetZoomHint.test.tsx +803 -0
- package/Tests/App/Dashboard/DashboardLazyPages.test.tsx +1 -0
- package/Tests/App/Dashboard/DashboardSettingsPreviewZoom.test.tsx +679 -0
- package/Tests/App/Dashboard/DashboardSloAndDataSourceChartZoom.test.tsx +881 -0
- package/Tests/App/Dashboard/DashboardTraceStatusWidgets.test.tsx +878 -0
- package/Tests/App/Dashboard/DatabaseMetricChartModalTimeRangeZoom.test.tsx +442 -0
- package/Tests/App/Dashboard/DatabaseOverviewTimeRangeZoom.test.tsx +831 -0
- package/Tests/App/Dashboard/DatabaseServerComponents.test.tsx +43 -0
- package/Tests/App/Dashboard/DockerSwarmInsightsTimeRangeZoom.test.tsx +524 -0
- package/Tests/App/Dashboard/DockerSwarmRateChartRefresh.test.tsx +259 -0
- package/Tests/App/Dashboard/EdgeDetailPanelHistory.test.tsx +763 -0
- package/Tests/App/Dashboard/EdgeDetailPanelTimeRangeZoom.test.tsx +547 -0
- package/Tests/App/Dashboard/EdgeHistoryBucketsCases.tsx +565 -0
- package/Tests/App/Dashboard/EdgeHistoryBucketsKathmandu.test.tsx +73 -0
- package/Tests/App/Dashboard/EdgeHistoryBucketsKolkata.test.tsx +74 -0
- package/Tests/App/Dashboard/EdgeHistoryBucketsNewYork.test.tsx +72 -0
- package/Tests/App/Dashboard/EdgeHistoryBucketsUtc.test.tsx +72 -0
- package/Tests/App/Dashboard/EmbeddedMetricCardRefresh.test.tsx +310 -0
- package/Tests/App/Dashboard/EmbeddedMetricCardTimeRangeZoom.test.tsx +536 -0
- package/Tests/App/Dashboard/EscalationRuleReadiness.test.tsx +3 -1
- package/Tests/App/Dashboard/EscalationRuleRowActions.test.tsx +660 -0
- package/Tests/App/Dashboard/EventNotes.test.tsx +2 -2
- package/Tests/App/Dashboard/EventOverviewPages.test.tsx +587 -13
- package/Tests/App/Dashboard/EventOverviewSnapshotExplorerZoom.test.tsx +1139 -0
- package/Tests/App/Dashboard/EventOverviewSnapshotZoomAcrossTabs.test.tsx +1057 -0
- package/Tests/App/Dashboard/EventOverviewTelemetrySnapshotZoom.test.tsx +757 -0
- package/Tests/App/Dashboard/ExceptionOccurrenceTrendZoom.test.tsx +675 -0
- package/Tests/App/Dashboard/ExceptionOccurrencesZoomSurvivesViewSwitch.test.tsx +405 -0
- package/Tests/App/Dashboard/ExceptionSignalPages.test.tsx +67 -3
- package/Tests/App/Dashboard/ExceptionTablesSpanStatus.test.tsx +515 -0
- package/Tests/App/Dashboard/ExplorerOwnZoomRegression.test.tsx +739 -0
- package/Tests/App/Dashboard/FlowBandwidthAxisCases.tsx +461 -0
- package/Tests/App/Dashboard/FlowBandwidthAxisKathmandu.test.tsx +25 -0
- package/Tests/App/Dashboard/FlowBandwidthAxisKolkata.test.tsx +25 -0
- package/Tests/App/Dashboard/FlowBandwidthAxisNewYork.test.tsx +25 -0
- package/Tests/App/Dashboard/FlowBandwidthAxisUtc.test.tsx +24 -0
- package/Tests/App/Dashboard/FlowTopTalkersZoomRefetch.test.tsx +722 -0
- package/Tests/App/Dashboard/FlowViewportGuard.test.tsx +2526 -0
- package/Tests/App/Dashboard/HostPagesSlowLoads.test.tsx +872 -0
- package/Tests/App/Dashboard/HostPagesTimeRangeZoom.test.tsx +1208 -0
- package/Tests/App/Dashboard/IncidentRootCauseTimeRangeZoom.test.tsx +358 -0
- package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +100 -9
- package/Tests/App/Dashboard/InfrastructureMetricsTabTimeRangeZoom.test.tsx +485 -0
- package/Tests/App/Dashboard/InvestigationDrawerCompanionZoom.test.tsx +655 -0
- package/Tests/App/Dashboard/InvestigationDrawerZoom.test.tsx +830 -0
- package/Tests/App/Dashboard/InvestigationNotStarted.test.tsx +338 -2
- package/Tests/App/Dashboard/InvoicesPayInvoice.test.tsx +454 -16
- package/Tests/App/Dashboard/KubernetesClusterAiAccessSettings.test.ts +799 -819
- package/Tests/App/Dashboard/KubernetesClusterAiAgentOverviewCard.test.tsx +367 -0
- package/Tests/App/Dashboard/KubernetesClusterAiAgentStatus.test.ts +1202 -0
- package/Tests/App/Dashboard/KubernetesClusterAiDocumentationMarkdown.test.ts +106 -0
- package/Tests/App/Dashboard/KubernetesClusterAiNavigation.test.tsx +354 -0
- package/Tests/App/Dashboard/KubernetesClusterAiPage.test.tsx +1884 -2054
- package/Tests/App/Dashboard/KubernetesClusterAiPageServerParity.test.ts +478 -179
- package/Tests/App/Dashboard/KubernetesClusterMetricTooltips.test.ts +50 -0
- package/Tests/App/Dashboard/KubernetesClusterOverviewTooltips.test.tsx +2 -1
- package/Tests/App/Dashboard/KubernetesClusterOverviewZoom.test.tsx +1217 -0
- package/Tests/App/Dashboard/KubernetesClusterPagesZoom.test.tsx +731 -0
- package/Tests/App/Dashboard/KubernetesCostsLoadRecovery.test.tsx +869 -0
- package/Tests/App/Dashboard/KubernetesCostsZoom.test.tsx +1214 -0
- package/Tests/App/Dashboard/KubernetesCostsZoomExactHours.test.tsx +692 -0
- package/Tests/App/Dashboard/KubernetesNetworkThroughputReload.test.tsx +902 -0
- package/Tests/App/Dashboard/KubernetesNetworkZoomBucketEdges.test.tsx +571 -0
- package/Tests/App/Dashboard/KubernetesOverviewZoomBucketEdges.test.tsx +697 -0
- package/Tests/App/Dashboard/KubernetesResourceMetricsTabZoom.test.tsx +633 -0
- package/Tests/App/Dashboard/LogsExplorerTimeRangeZoom.test.tsx +470 -0
- package/Tests/App/Dashboard/LogsInsightsTimeRangeZoom.test.tsx +948 -0
- package/Tests/App/Dashboard/LogsViewerPinnedWindowZoom.test.tsx +486 -0
- package/Tests/App/Dashboard/MetricChartsBucketInspectorDoubleClick.test.tsx +1001 -0
- package/Tests/App/Dashboard/MetricExplorerInvestigationAutoRefresh.test.tsx +514 -0
- package/Tests/App/Dashboard/MetricExplorerSavedViewZoomRoundTrip.test.tsx +581 -0
- package/Tests/App/Dashboard/MetricExplorerTimeRangeZoom.test.tsx +1162 -0
- package/Tests/App/Dashboard/MetricMonitorPreviewZoomLabel.test.ts +72 -0
- package/Tests/App/Dashboard/MetricRowSparklineTimeRangeZoom.test.tsx +204 -0
- package/Tests/App/Dashboard/MetricViewFirstResultLands.test.tsx +408 -0
- package/Tests/App/Dashboard/MetricViewOwnZoomResetPlacement.test.tsx +952 -0
- package/Tests/App/Dashboard/MetricViewTimeRange.test.ts +139 -0
- package/Tests/App/Dashboard/MetricViewTimeRangeZoom.test.tsx +556 -0
- package/Tests/App/Dashboard/MonitorMetricsChartWrappersZoom.test.tsx +552 -0
- package/Tests/App/Dashboard/MonitorMetricsTabsTimeRangeZoom.test.tsx +740 -0
- package/Tests/App/Dashboard/MonitorOverviewChartsTimeRangeZoom.test.tsx +735 -0
- package/Tests/App/Dashboard/MonitorStepPreviewsTimeRangeZoom.test.tsx +835 -0
- package/Tests/App/Dashboard/NetworkDeviceTimeRangeZoom.test.tsx +1115 -0
- package/Tests/App/Dashboard/OnCallPreventionGuards.test.tsx +186 -17
- package/Tests/App/Dashboard/OnCallRulesDeleteGuard.test.tsx +155 -46
- package/Tests/App/Dashboard/PasskeySettings.test.tsx +87 -2
- package/Tests/App/Dashboard/ProjectAISettingsPages.test.tsx +884 -0
- package/Tests/App/Dashboard/ProxmoxNodeRemove.test.tsx +621 -0
- package/Tests/App/Dashboard/ProxmoxOverviewSlowLoads.test.tsx +584 -0
- package/Tests/App/Dashboard/ProxmoxPagesTimeRangeZoom.test.tsx +823 -0
- package/Tests/App/Dashboard/ProxmoxRateChartReload.test.tsx +1253 -0
- package/Tests/App/Dashboard/PublicDashboardBoardWideZoom.test.tsx +605 -0
- package/Tests/App/Dashboard/RealRowsLineChart.tsx +250 -0
- package/Tests/App/Dashboard/RechartsHostsPlotCursor.test.tsx +513 -0
- package/Tests/App/Dashboard/RowActionPlacementOverrides.test.tsx +865 -0
- package/Tests/App/Dashboard/RumOverviewTimeRangeZoom.test.tsx +877 -0
- package/Tests/App/Dashboard/RunnerKubernetesAgentForm.test.tsx +20 -4
- package/Tests/App/Dashboard/RunnerSupersededByAiAgentNote.test.tsx +296 -0
- package/Tests/App/Dashboard/SecurityEventConnectionsTable.test.tsx +146 -36
- package/Tests/App/Dashboard/ServiceDatabaseSlowLoads.test.tsx +1166 -0
- package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +1243 -104
- package/Tests/App/Dashboard/ServiceOverviewTimeRangeZoom.test.tsx +792 -0
- package/Tests/App/Dashboard/SloBudgetBurnDownZoom.test.tsx +771 -0
- package/Tests/App/Dashboard/SloBudgetBurnDownZoomAffordances.test.tsx +380 -0
- package/Tests/App/Dashboard/SloEventMetricsBarChartZoom.test.tsx +369 -0
- package/Tests/App/Dashboard/SloHistoryChartsTimeRangeZoom.test.tsx +694 -0
- package/Tests/App/Dashboard/SloHistoryChartsZoomAffordances.test.tsx +497 -0
- package/Tests/App/Dashboard/SloMetricsTimeRangeZoom.test.tsx +541 -0
- package/Tests/App/Dashboard/SlowLoadHarness.ts +123 -0
- package/Tests/App/Dashboard/SnapshotExplorerPrimaryZoom.test.tsx +1194 -0
- package/Tests/App/Dashboard/SnapshotViewerPinnedWindow.test.tsx +620 -0
- package/Tests/App/Dashboard/TelemetrySnapshotPanelTimeRangeZoom.test.tsx +765 -0
- package/Tests/App/Dashboard/TelemetrySnapshotZoom.test.tsx +516 -0
- package/Tests/App/Dashboard/TelemetrySnapshotZoomAcrossTabs.test.tsx +827 -0
- package/Tests/App/Dashboard/TimeRangeZoomPageHarness.tsx +463 -0
- package/Tests/App/Dashboard/TraceChartEditorStatusSplit.test.tsx +543 -0
- package/Tests/App/Dashboard/TraceStatusPresentation.test.tsx +967 -0
- package/Tests/App/Dashboard/TraceTimelineMinimapZoom.test.tsx +315 -0
- package/Tests/App/Dashboard/TracesAnalyticsStatusSplit.test.tsx +778 -0
- package/Tests/App/Dashboard/TracesAnalyticsViewZoom.test.tsx +349 -0
- package/Tests/App/Dashboard/TracesExplorerTimeRangeZoom.test.tsx +551 -0
- package/Tests/App/Dashboard/TracesStatusChartAndFacet.test.tsx +687 -0
- package/Tests/App/Dashboard/TracesViewerStaleResponses.test.tsx +458 -0
- package/Tests/App/Dashboard/UptimeStripsExcludedFromZoom.test.tsx +406 -0
- package/Tests/App/Dashboard/VMwareOverviewSlowLoads.test.tsx +535 -0
- package/Tests/App/Dashboard/VMwarePagesTimeRangeZoom.test.tsx +606 -0
- package/Tests/App/Dashboard/ValueWidgetSparklineStaleHover.test.tsx +1292 -0
- package/Tests/App/Dashboard/ValueWidgetSparklineZoom.test.tsx +765 -0
- package/Tests/App/StatusPage/StatusPageResourceExplorer.test.tsx +98 -0
- package/Tests/Models/DatabaseModels/KubernetesAiAgentModel.test.ts +295 -0
- package/Tests/Models/DatabaseModels/TeamComplianceSettingModel.test.ts +347 -0
- package/Tests/Server/API/AIChatRouteAuthorization.test.ts +58 -0
- package/Tests/Server/API/AIGenerationProjectAIToggle.test.ts +1 -1
- package/Tests/Server/API/AIInvestigationAPI.test.ts +123 -45
- package/Tests/Server/API/AutoRemediationApproveClusterRound.test.ts +1329 -0
- package/Tests/Server/API/AutoRemediationApproveKubectlRecheck.test.ts +25 -2
- package/Tests/Server/API/KubernetesClusterAiAccessAPI.test.ts +2 -0
- package/Tests/Server/API/KubernetesClusterAiAccessAPIAgent.test.ts +903 -0
- package/Tests/Server/API/MicrosoftTeamsChatsRefreshRoute.test.ts +667 -0
- package/Tests/Server/API/MicrosoftTeamsManifest.test.ts +294 -6
- package/Tests/Server/API/ProxmoxResourceAPIRemoveNode.test.ts +1932 -0
- package/Tests/Server/Infrastructure/GlobalCache.test.ts +195 -0
- package/Tests/Server/Infrastructure/Postgres/AddKubernetesAiAgentAndAiDefaultsMigration.test.ts +623 -0
- package/Tests/Server/Infrastructure/Postgres/AddKubernetesClusterAiAccessMigration.test.ts +87 -10
- package/Tests/Server/Infrastructure/Postgres/AddTeamComplianceRuleScopeMigration.test.ts +622 -0
- package/Tests/Server/Middleware/TelemetryIngestBrowserKey.test.ts +61 -4
- package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerPinnedKey.test.ts +98 -2
- package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerRateLimit.test.ts +105 -2
- package/Tests/Server/Services/AIServiceBalanceBlocker.test.ts +282 -0
- package/Tests/Server/Services/AlertMonitorClearWritesNull.test.ts +409 -0
- package/Tests/Server/Services/AlertMonitorEditFeed.test.ts +474 -0
- package/Tests/Server/Services/AlertMonitorEditGuard.test.ts +610 -0
- package/Tests/Server/Services/AutoRemediationClusterRuleEngine.test.ts +2 -0
- package/Tests/Server/Services/AutoRemediationRuleCommandRunners.test.ts +2 -1
- package/Tests/Server/Services/IncidentAffectedMonitorsEdit.test.ts +1360 -0
- package/Tests/Server/Services/IncidentAlertSloLinkSurvivesEdit.test.ts +3 -1
- package/Tests/Server/Services/IncidentSeverityDeletePausesComplianceRules.test.ts +488 -0
- package/Tests/Server/Services/KubernetesAiAgentJobService.test.ts +329 -0
- package/Tests/Server/Services/KubernetesAiAgentServiceHelpers.test.ts +618 -0
- package/Tests/Server/Services/KubernetesAiAgentServiceLifecycle.test.ts +1444 -0
- package/Tests/Server/Services/KubernetesAiAgentServiceRegister.test.ts +1666 -0
- package/Tests/Server/Services/KubernetesClusterAiAccessDeletedRunnerRemedy.test.ts +140 -111
- package/Tests/Server/Services/KubernetesClusterAiAccessGapNextSteps.test.ts +267 -105
- package/Tests/Server/Services/KubernetesClusterAiAccessLegacySuperseded.test.ts +440 -0
- package/Tests/Server/Services/KubernetesClusterAiAccessService.test.ts +567 -411
- package/Tests/Server/Services/KubernetesClusterAiAccessTarget.test.ts +1168 -0
- package/Tests/Server/Services/KubernetesClusterAiSettingsPermission.test.ts +410 -12
- package/Tests/Server/Services/KubernetesClusterServiceAiAccessFeed.test.ts +55 -0
- package/Tests/Server/Services/ProjectServiceNewProjectAiDefaults.test.ts +288 -0
- package/Tests/Server/Services/ProxmoxResourcePostgres.test.ts +5633 -0
- package/Tests/Server/Services/ProxmoxResourceService.test.ts +6458 -5
- package/Tests/Server/Services/RunnerJobEnqueueKubectl.test.ts +48 -15
- package/Tests/Server/Services/RunnerJobEnqueueKubectlAiAgent.test.ts +620 -0
- package/Tests/Server/Services/RunnerJobKubectlWriteScope.test.ts +170 -3
- package/Tests/Server/Services/RunnerJobTimeoutReason.test.ts +72 -0
- package/Tests/Server/Services/RunnerServiceAgentName.test.ts +6 -4
- package/Tests/Server/Services/ScheduledMaintenanceAttachmentEdit.test.ts +2113 -0
- package/Tests/Server/Services/ScheduledMaintenanceMonitorRelease.test.ts +972 -0
- package/Tests/Server/Services/TeamComplianceSettingPostgres.test.ts +2237 -0
- package/Tests/Server/Services/TeamComplianceSettingService.test.ts +3988 -138
- package/Tests/Server/Utils/AI/ClusterAccessContext.test.ts +107 -1
- package/Tests/Server/Utils/AI/IncidentPostmortemRunner.test.ts +351 -0
- package/Tests/Server/Utils/AI/InvestigationClusterAccessWiring.test.ts +2 -0
- package/Tests/Server/Utils/AI/InvestigationEligibility.test.ts +310 -0
- package/Tests/Server/Utils/AI/KubectlInvestigationToolkit.test.ts +70 -0
- package/Tests/Server/Utils/AI/KubectlJobRunnerFinishedJob.test.ts +58 -0
- package/Tests/Server/Utils/AI/RemediationCommandToolsKubectl.test.ts +2 -0
- package/Tests/Server/Utils/AI/RemediationCommandToolsKubectlOutcome.test.ts +2 -0
- package/Tests/Server/Utils/AI/RemediationCommandToolsLiveCluster.test.ts +2 -0
- package/Tests/Server/Utils/AI/RemediationCommandToolsRunnerScope.test.ts +125 -1
- package/Tests/Server/Utils/AI/RemediationExecutionRunner.test.ts +252 -3
- package/Tests/Server/Utils/AI/RemediationExecutionRunnerClusterMode.test.ts +2 -0
- package/Tests/Server/Utils/AI/RemediationExecutionRunnerProjectGates.test.ts +240 -0
- package/Tests/Server/Utils/AI/RemediationKubectlCopyParity.test.ts +2 -0
- package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutor.test.ts +2 -0
- package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutorUnknownRun.test.ts +2 -0
- package/Tests/Server/Utils/AutoRemediation/CommandPlanVerification.test.ts +2 -0
- package/Tests/Server/Utils/Telemetry/HostAssetAttributesFromOtlp.test.ts +68 -0
- package/Tests/Server/Utils/Telemetry/InventoryMirrorPostgres.test.ts +370 -0
- package/Tests/Server/Utils/Telemetry/NetworkDeviceInventoryAttributes.test.ts +522 -0
- package/Tests/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.test.ts +155 -0
- package/Tests/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.test.ts +8631 -0
- package/Tests/Server/Utils/Telemetry/ProxmoxNativePush.test.ts +2852 -0
- package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +367 -1
- package/Tests/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.test.ts +45 -3
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsChats.test.ts +25 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsGroupChatNames.test.ts +3119 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsGroupChatNamesPostgres.test.ts +1721 -0
- package/Tests/Types/Kubernetes/KubernetesAgentChartAiAccess.test.ts +581 -113
- package/Tests/Types/Kubernetes/KubernetesAiAgentAccessContract.test.ts +510 -0
- package/Tests/Types/Team/ComplianceRule.test.ts +1029 -0
- package/Tests/Types/Team/TeamComplianceStatus.test.ts +176 -0
- package/Tests/UI/Components/ActionButton/RowActions.test.tsx +641 -0
- package/Tests/UI/Components/ActionButton/RowActionsAccessibility.test.tsx +414 -0
- package/Tests/UI/Components/ActionButton/SplitActionButtons.test.ts +565 -0
- package/Tests/UI/Components/Charts/BarChartDragToSelect.test.tsx +386 -0
- package/Tests/UI/Components/Charts/ChartDoubleClickResetWithoutDblclick.test.tsx +1025 -0
- package/Tests/UI/Components/Charts/ChartGroupPageZoomHint.test.tsx +196 -0
- package/Tests/UI/Components/Charts/ChartPlaceholdersTakeDoubleClick.test.tsx +1861 -0
- package/Tests/UI/Components/Charts/ChartPlotClicksWhileResetArmed.test.tsx +382 -0
- package/Tests/UI/Components/Charts/ChartRangeSelection.test.tsx +704 -0
- package/Tests/UI/Components/Charts/ChartRangeSelectionDoubleClickReset.test.tsx +1481 -0
- package/Tests/UI/Components/Charts/ChartRangeSelectionRealRecharts.test.tsx +918 -0
- package/Tests/UI/Components/Charts/ChartRowBucketStart.test.ts +350 -0
- package/Tests/UI/Components/Charts/DoubleClickReset.test.tsx +996 -0
- package/Tests/UI/Components/Charts/HistogramDoubleClickResetRealRecharts.test.tsx +965 -0
- package/Tests/UI/Components/Charts/HistogramDriftThenRefetch.test.tsx +277 -0
- package/Tests/UI/Components/Charts/HistogramRangeSelectionDataChange.test.tsx +1757 -0
- package/Tests/UI/Components/Charts/HistogramRangeSelectionRealRecharts.test.tsx +560 -0
- package/Tests/UI/Components/Charts/TimeRangeZoom/ChartPageTimeRangeZoom.test.tsx +513 -0
- package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.test.tsx +845 -0
- package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.test.tsx +119 -0
- package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHintTailwind.test.tsx +274 -0
- package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.test.ts +433 -0
- package/Tests/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.test.tsx +863 -0
- package/Tests/UI/Components/Charts/TimeRangeZoom/ZoomDocsPromises.test.tsx +1147 -0
- package/Tests/UI/Components/DisabledButtonTooltip.test.tsx +14 -3
- package/Tests/UI/Components/EntityDropdownEscape.test.tsx +454 -0
- package/Tests/UI/Components/EntityDropdownFocusLeave.test.tsx +420 -0
- package/Tests/UI/Components/Forms/FormFieldDropdownDisabled.test.tsx +233 -0
- package/Tests/UI/Components/Input.test.tsx +80 -1
- package/Tests/UI/Components/LogsAnalyticsViewZoom.test.tsx +947 -0
- package/Tests/UI/Components/LogsHistogramDragTooltip.test.tsx +17 -5
- package/Tests/UI/Components/LogsViewerAnalyticsZoom.test.tsx +501 -0
- package/Tests/UI/Components/LogsViewerHistogramZoom.test.tsx +1 -1
- package/Tests/UI/Components/ModelTable/BaseModelTableBulkDelete.test.tsx +6 -4
- package/Tests/UI/Components/ModelTable/BaseModelTablePermissionGating.test.tsx +60 -8
- package/Tests/UI/Components/ModelTable/BaseModelTableRowActions.test.tsx +476 -0
- package/Tests/UI/Components/ModelTable/ModelTableWrapContent.test.tsx +7 -5
- package/Tests/UI/Components/MoreMenuPortal.test.tsx +623 -0
- package/Tests/UI/Components/StatusPage/ResourceGridActions.test.tsx +538 -0
- package/Tests/UI/Components/StatusPage/ResourceGridSelection.test.tsx +14 -4
- package/Tests/UI/Components/TableCellWrapping.test.tsx +3 -3
- package/Tests/UI/Components/TableRowActions.test.tsx +472 -0
- package/Tests/UI/Components/TelemetryHistogramDragTooltip.test.tsx +1 -1
- package/Tests/UI/Components/TelemetryViewer/TelemetryViewerHistogramClickZoom.test.tsx +1 -1
- package/Tests/UI/Components/TelemetryViewer/TelemetryViewerZoomScope.test.tsx +470 -0
- package/Tests/UI/Components/TelemetryViewer/UseViewerTimeRangeZoom.test.tsx +571 -0
- package/Tests/UI/Components/TelemetryViewer/ViewerDoubleClickResetUnderDataChange.test.tsx +928 -0
- package/Tests/UI/Components/TelemetryViewer/ViewerFollowsEnclosingZoom.test.tsx +1569 -0
- package/Tests/UI/Components/Toggle.test.tsx +66 -0
- package/Tests/UI/MicrosoftTeams/MicrosoftTeamsChatsCardRefreshNames.test.tsx +1023 -0
- package/Tests/UI/Rum/ChunkLoader.test.ts +201 -0
- package/Tests/UI/Rum/FidelityNotices.test.ts +54 -0
- package/Tests/UI/Rum/ReplayCorrelationPanel.test.tsx +72 -0
- package/Tests/UI/Rum/ReplayFrameCapture.test.ts +1009 -9
- package/Tests/UI/Rum/ReplayRecordedAssets.test.ts +1069 -0
- package/Tests/UI/Rum/ReplayStage.test.tsx +913 -2
- package/Tests/UI/Rum/SessionReplayTable.test.tsx +443 -8
- package/Tests/UI/Rum/SessionReplayUsersTable.test.tsx +439 -9
- package/Tests/UI/Rum/UserFlowTablesRowActions.test.tsx +501 -0
- package/Tests/Utils/AiRemediation/KubectlPolicyChartRbacParity.test.ts +47 -19
- package/Tests/Utils/AiRemediation/KubectlWriteScopeCallerParity.test.ts +2 -0
- package/Tests/Utils/AiRemediation/KubernetesAiAgentPolicyCopyParity.test.ts +396 -0
- package/Tests/Utils/Dashboard/DashboardTimeRangeZoom.test.ts +104 -1
- package/Types/AI/AIRunType.ts +9 -4
- package/Types/AI/InvestigationNotStartedReason.ts +6 -0
- package/Types/AutoRemediation/AiRemediationCommandPlan.ts +14 -16
- package/Types/AutoRemediation/AiRemediationCommandPolicyVerdict.ts +28 -0
- package/Types/Dashboard/DashboardComponents/DashboardTraceChartComponent.ts +4 -2
- package/Types/Kubernetes/KubernetesClusterAiAccess.ts +263 -32
- package/Types/Kubernetes/KubernetesClusterAiAccessPermissions.ts +20 -16
- package/Types/Kubernetes/KubernetesClusterAiInsights.ts +81 -0
- package/Types/Monitor/ProxmoxAlertTemplates.ts +4 -4
- package/Types/Team/ComplianceNotificationChannel.ts +22 -0
- package/Types/Team/ComplianceRule.ts +434 -0
- package/Types/Team/ComplianceRuleType.ts +21 -0
- package/Types/Team/TeamComplianceStatus.ts +81 -0
- package/Types/Telemetry/TelemetryIngestSurface.ts +28 -0
- package/UI/Components/ActionButton/ActionButtonSchema.ts +21 -0
- package/UI/Components/ActionButton/RowActions.tsx +238 -0
- package/UI/Components/ActionButton/SplitActionButtons.ts +173 -0
- package/UI/Components/Charts/Area/AreaChart.tsx +32 -2
- package/UI/Components/Charts/Bar/BarChart.tsx +40 -3
- package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +37 -11
- package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +80 -243
- package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +122 -31
- package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +95 -244
- package/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.ts +214 -0
- package/UI/Components/Charts/ChartLibrary/Utils/UseChartBucketsChange.ts +32 -0
- package/UI/Components/Charts/ChartLibrary/Utils/UseChartRangeSelection.ts +599 -0
- package/UI/Components/Charts/Line/LineChart.tsx +32 -2
- package/UI/Components/Charts/TimeRangeZoom/ResetTimeRangeZoomButton.tsx +70 -0
- package/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.tsx +226 -0
- package/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.tsx +73 -0
- package/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.ts +163 -0
- package/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.ts +215 -0
- package/UI/Components/Charts/Utils/DataPoint.ts +20 -8
- package/UI/Components/Charts/Utils/XAxis.ts +75 -0
- package/UI/Components/Charts/Utils/useHistogramRangeSelection.ts +493 -59
- package/UI/Components/EntityDropdown/EntityDropdown.tsx +107 -11
- package/UI/Components/Forms/Fields/FormField.tsx +10 -0
- package/UI/Components/Input/Input.tsx +90 -57
- package/UI/Components/List/ListRow.tsx +8 -74
- package/UI/Components/LogsViewer/LogsViewer.tsx +40 -11
- package/UI/Components/LogsViewer/components/LogTimeRangePicker.tsx +22 -6
- package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +209 -6
- package/UI/Components/LogsViewer/components/LogsHistogram.tsx +127 -84
- package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +3 -0
- package/UI/Components/ModelTable/BaseModelTable.tsx +12 -1
- package/UI/Components/MoreMenu/MoreMenu.tsx +365 -30
- package/UI/Components/MoreMenu/MoreMenuItem.tsx +70 -17
- package/UI/Components/OrderedStatesList/Item.tsx +7 -72
- package/UI/Components/StatusPage/ResourceGrid.tsx +126 -77
- package/UI/Components/Table/TableBody.tsx +8 -2
- package/UI/Components/Table/TableRow.tsx +20 -140
- package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +41 -10
- package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +47 -9
- package/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.tsx +25 -6
- package/UI/Components/TelemetryViewer/types.ts +2 -0
- package/UI/Components/TelemetryViewer/useViewerTimeRangeZoom.ts +448 -0
- package/UI/Components/Toggle/Toggle.tsx +26 -0
- package/UI/Components/Tooltip/Tooltip.tsx +8 -1
- package/UI/Styles/Theme.css +14 -2
- package/Utils/AiRemediation/CommandPolicy.ts +1 -1
- package/Utils/AiRemediation/KubectlPolicy.ts +1 -1
- package/Utils/Dashboard/DashboardTimeRangeZoom.ts +46 -2
- package/build/dist/Models/DatabaseModels/Index.js +2 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/KubernetesAiAgent.js +385 -0
- package/build/dist/Models/DatabaseModels/KubernetesAiAgent.js.map +1 -0
- package/build/dist/Models/DatabaseModels/KubernetesCluster.js +3 -3
- package/build/dist/Models/DatabaseModels/KubernetesCluster.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Project.js +56 -5
- package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ProxmoxResource.js +51 -1
- package/build/dist/Models/DatabaseModels/ProxmoxResource.js.map +1 -1
- package/build/dist/Models/DatabaseModels/RunnerJob.js +74 -0
- package/build/dist/Models/DatabaseModels/RunnerJob.js.map +1 -1
- package/build/dist/Models/DatabaseModels/TeamComplianceSetting.js +134 -3
- package/build/dist/Models/DatabaseModels/TeamComplianceSetting.js.map +1 -1
- package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
- package/build/dist/Server/API/AIChatAPI.js +6 -1
- package/build/dist/Server/API/AIChatAPI.js.map +1 -1
- package/build/dist/Server/API/AIInvestigationAPI.js +17 -10
- package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -1
- package/build/dist/Server/API/AutoRemediationAPI.js +144 -24
- package/build/dist/Server/API/AutoRemediationAPI.js.map +1 -1
- package/build/dist/Server/API/KubernetesClusterAiAccessAPI.js +436 -22
- package/build/dist/Server/API/KubernetesClusterAiAccessAPI.js.map +1 -1
- package/build/dist/Server/API/MicrosoftTeamsAPI.js +69 -13
- package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
- package/build/dist/Server/API/ProxmoxResourceAPI.js +187 -14
- package/build/dist/Server/API/ProxmoxResourceAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/GlobalCache.js +27 -0
- package/build/dist/Server/Infrastructure/GlobalCache.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796000000000-AddTeamComplianceRuleScope.js +50 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796000000000-AddTeamComplianceRuleScope.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796100000000-AddKubernetesAiAgentAndAiDefaults.js +84 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796100000000-AddKubernetesAiAgentAndAiDefaults.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796200000000-AddProxmoxResourceNativePushColumns.js +31 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796200000000-AddProxmoxResourceNativePushColumns.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/TelemetryIngest.js +34 -19
- package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
- package/build/dist/Server/Services/AIService.js +59 -1
- package/build/dist/Server/Services/AIService.js.map +1 -1
- package/build/dist/Server/Services/AlertService.js +248 -13
- package/build/dist/Server/Services/AlertService.js.map +1 -1
- package/build/dist/Server/Services/AlertSeverityService.js +51 -15
- package/build/dist/Server/Services/AlertSeverityService.js.map +1 -1
- package/build/dist/Server/Services/AutoRemediationRuleService.js +1 -1
- package/build/dist/Server/Services/AutoRemediationRuleService.js.map +1 -1
- package/build/dist/Server/Services/IncidentService.js +199 -72
- package/build/dist/Server/Services/IncidentService.js.map +1 -1
- package/build/dist/Server/Services/IncidentSeverityService.js +51 -15
- package/build/dist/Server/Services/IncidentSeverityService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesAiAgentJobService.js +158 -0
- package/build/dist/Server/Services/KubernetesAiAgentJobService.js.map +1 -0
- package/build/dist/Server/Services/KubernetesAiAgentService.js +1145 -0
- package/build/dist/Server/Services/KubernetesAiAgentService.js.map +1 -0
- package/build/dist/Server/Services/KubernetesClusterAiAccessService.js +842 -371
- package/build/dist/Server/Services/KubernetesClusterAiAccessService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesClusterService.js +86 -17
- package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +22 -0
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/ProxmoxResourceService.js +258 -3
- package/build/dist/Server/Services/ProxmoxResourceService.js.map +1 -1
- package/build/dist/Server/Services/RunnerJobService.js +178 -92
- package/build/dist/Server/Services/RunnerJobService.js.map +1 -1
- package/build/dist/Server/Services/RunnerService.js +1 -1
- package/build/dist/Server/Services/RunnerService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceService.js +535 -3
- package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js +316 -46
- package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/TeamComplianceSettingService.js +833 -7
- package/build/dist/Server/Services/TeamComplianceSettingService.js.map +1 -1
- package/build/dist/Server/Utils/AI/ClusterAccess/ClusterAccessContext.js +52 -9
- package/build/dist/Server/Utils/AI/ClusterAccess/ClusterAccessContext.js.map +1 -1
- package/build/dist/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.js +13 -9
- package/build/dist/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.js.map +1 -1
- package/build/dist/Server/Utils/AI/ClusterAccess/KubectlJobRunner.js +7 -3
- package/build/dist/Server/Utils/AI/ClusterAccess/KubectlJobRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js +29 -15
- package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js.map +1 -1
- package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js +56 -20
- package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js +39 -5
- package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/IncidentPostmortemRunner.js +30 -3
- package/build/dist/Server/Utils/AI/SRE/IncidentPostmortemRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/InvestigationEligibility.js +13 -2
- package/build/dist/Server/Utils/AI/SRE/InvestigationEligibility.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/InvestigationGrader.js +1 -1
- package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js +4 -2
- package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js +111 -6
- package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js +40 -4
- package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.js +296 -0
- package/build/dist/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/ProxmoxNativePush.js +699 -0
- package/build/dist/Server/Utils/Telemetry/ProxmoxNativePush.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +77 -19
- package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +505 -42
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
- package/build/dist/Types/AI/AIRunType.js +9 -4
- package/build/dist/Types/AI/AIRunType.js.map +1 -1
- package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js +10 -16
- package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js.map +1 -1
- package/build/dist/Types/AutoRemediation/AiRemediationCommandPolicyVerdict.js +28 -0
- package/build/dist/Types/AutoRemediation/AiRemediationCommandPolicyVerdict.js.map +1 -0
- package/build/dist/Types/Kubernetes/KubernetesClusterAiAccess.js +72 -16
- package/build/dist/Types/Kubernetes/KubernetesClusterAiAccess.js.map +1 -1
- package/build/dist/Types/Kubernetes/KubernetesClusterAiAccessPermissions.js +20 -16
- package/build/dist/Types/Kubernetes/KubernetesClusterAiAccessPermissions.js.map +1 -1
- package/build/dist/Types/Kubernetes/KubernetesClusterAiInsights.js +17 -0
- package/build/dist/Types/Kubernetes/KubernetesClusterAiInsights.js.map +1 -0
- package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js +4 -4
- package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js.map +1 -1
- package/build/dist/Types/Team/ComplianceNotificationChannel.js +23 -0
- package/build/dist/Types/Team/ComplianceNotificationChannel.js.map +1 -0
- package/build/dist/Types/Team/ComplianceRule.js +304 -0
- package/build/dist/Types/Team/ComplianceRule.js.map +1 -0
- package/build/dist/Types/Team/ComplianceRuleType.js +21 -0
- package/build/dist/Types/Team/ComplianceRuleType.js.map +1 -1
- package/build/dist/Types/Team/TeamComplianceStatus.js +2 -0
- package/build/dist/Types/Team/TeamComplianceStatus.js.map +1 -0
- package/build/dist/Types/Telemetry/TelemetryIngestSurface.js +25 -0
- package/build/dist/Types/Telemetry/TelemetryIngestSurface.js.map +1 -1
- package/build/dist/UI/Components/ActionButton/ActionButtonSchema.js +20 -1
- package/build/dist/UI/Components/ActionButton/ActionButtonSchema.js.map +1 -1
- package/build/dist/UI/Components/ActionButton/RowActions.js +113 -0
- package/build/dist/UI/Components/ActionButton/RowActions.js.map +1 -0
- package/build/dist/UI/Components/ActionButton/SplitActionButtons.js +71 -0
- package/build/dist/UI/Components/ActionButton/SplitActionButtons.js.map +1 -0
- package/build/dist/UI/Components/Charts/Area/AreaChart.js +16 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js +16 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +27 -5
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +61 -194
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +84 -26
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +75 -196
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js +133 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartBucketsChange.js +26 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartBucketsChange.js.map +1 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartRangeSelection.js +371 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartRangeSelection.js.map +1 -0
- package/build/dist/UI/Components/Charts/Line/LineChart.js +16 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/TimeRangeZoom/ResetTimeRangeZoomButton.js +35 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/ResetTimeRangeZoomButton.js.map +1 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.js +107 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.js.map +1 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.js +41 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.js.map +1 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.js +117 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.js.map +1 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.js +121 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.js.map +1 -0
- package/build/dist/UI/Components/Charts/Utils/DataPoint.js +15 -7
- package/build/dist/UI/Components/Charts/Utils/DataPoint.js.map +1 -1
- package/build/dist/UI/Components/Charts/Utils/XAxis.js +70 -0
- package/build/dist/UI/Components/Charts/Utils/XAxis.js.map +1 -1
- package/build/dist/UI/Components/Charts/Utils/useHistogramRangeSelection.js +305 -38
- package/build/dist/UI/Components/Charts/Utils/useHistogramRangeSelection.js.map +1 -1
- package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js +89 -14
- package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js.map +1 -1
- package/build/dist/UI/Components/Forms/Fields/FormField.js +11 -2
- package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
- package/build/dist/UI/Components/Input/Input.js +69 -54
- package/build/dist/UI/Components/Input/Input.js.map +1 -1
- package/build/dist/UI/Components/List/ListRow.js +4 -39
- package/build/dist/UI/Components/List/ListRow.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js +31 -8
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js +10 -1
- package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +139 -21
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +30 -8
- package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +10 -0
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/MoreMenu/MoreMenu.js +253 -14
- package/build/dist/UI/Components/MoreMenu/MoreMenu.js.map +1 -1
- package/build/dist/UI/Components/MoreMenu/MoreMenuItem.js +43 -13
- package/build/dist/UI/Components/MoreMenu/MoreMenuItem.js.map +1 -1
- package/build/dist/UI/Components/OrderedStatesList/Item.js +2 -38
- package/build/dist/UI/Components/OrderedStatesList/Item.js.map +1 -1
- package/build/dist/UI/Components/StatusPage/ResourceGrid.js +53 -15
- package/build/dist/UI/Components/StatusPage/ResourceGrid.js.map +1 -1
- package/build/dist/UI/Components/Table/TableBody.js +6 -2
- package/build/dist/UI/Components/Table/TableBody.js.map +1 -1
- package/build/dist/UI/Components/Table/TableRow.js +12 -75
- package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +32 -7
- package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +27 -9
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js +11 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/useViewerTimeRangeZoom.js +272 -0
- package/build/dist/UI/Components/TelemetryViewer/useViewerTimeRangeZoom.js.map +1 -0
- package/build/dist/UI/Components/Toggle/Toggle.js +14 -1
- package/build/dist/UI/Components/Toggle/Toggle.js.map +1 -1
- package/build/dist/UI/Components/Tooltip/Tooltip.js +1 -1
- package/build/dist/UI/Components/Tooltip/Tooltip.js.map +1 -1
- package/build/dist/Utils/AiRemediation/CommandPolicy.js +1 -1
- package/build/dist/Utils/AiRemediation/CommandPolicy.js.map +1 -1
- package/build/dist/Utils/AiRemediation/KubectlPolicy.js +1 -1
- package/build/dist/Utils/AiRemediation/KubectlPolicy.js.map +1 -1
- package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js +27 -1
- package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,1861 @@
|
|
|
1
|
+
/** @timezone UTC */
|
|
2
|
+
|
|
3
|
+
import "@testing-library/jest-dom";
|
|
4
|
+
import {
|
|
5
|
+
afterEach,
|
|
6
|
+
beforeEach,
|
|
7
|
+
describe,
|
|
8
|
+
expect,
|
|
9
|
+
jest,
|
|
10
|
+
test,
|
|
11
|
+
} from "@jest/globals";
|
|
12
|
+
import {
|
|
13
|
+
RenderResult,
|
|
14
|
+
act,
|
|
15
|
+
cleanup,
|
|
16
|
+
fireEvent,
|
|
17
|
+
render,
|
|
18
|
+
screen,
|
|
19
|
+
waitFor,
|
|
20
|
+
} from "@testing-library/react";
|
|
21
|
+
import * as React from "react";
|
|
22
|
+
import getJestMockFunction, { MockFunction } from "../../../MockType";
|
|
23
|
+
|
|
24
|
+
/*
|
|
25
|
+
* Issue #4116, the placeholders (commits 7f6df1c28c and b6ec899df4). Right
|
|
26
|
+
* after a zoom a chart often has no bars to double-click: the zoomed window
|
|
27
|
+
* is still loading, or it turned out to hold nothing. That is exactly when
|
|
28
|
+
* a reader double-clicks to undo the zoom, so the loader and the empty box
|
|
29
|
+
* standing where the bars were take the double-click as the bars do - and
|
|
30
|
+
* are select-none, so the double-click does not also select a word of them.
|
|
31
|
+
*
|
|
32
|
+
* The bars can also land in the middle of that double-click - the #4116
|
|
33
|
+
* race once more - and take the placeholder's place between its second
|
|
34
|
+
* press and that press's release. The browser then sends the chart no
|
|
35
|
+
* dblclick: the node the press went down on is gone, or React kept it for
|
|
36
|
+
* the chart without the placeholder's handlers, and a dblclick sent to it
|
|
37
|
+
* bubbles away from the chart's own. So a placeholder takes the second
|
|
38
|
+
* press itself (placeholderProps): its release - over the chart that took
|
|
39
|
+
* the placeholder's place, or anywhere on the page - zooms out in the next
|
|
40
|
+
* task, once, and a dblclick that does reach the chart still zooms out only
|
|
41
|
+
* once. Only a main-button press on a zoomed chart counts.
|
|
42
|
+
*
|
|
43
|
+
* - The explorers' volume histograms (TelemetryHistogram - traces,
|
|
44
|
+
* exceptions, security events - and LogsHistogram): a zoomed chart's
|
|
45
|
+
* loader zooms out; a zoomed chart whose window is empty keeps its card,
|
|
46
|
+
* and its empty box zooms out. A zoomed chart keeps its card - title,
|
|
47
|
+
* the way back - while its window loads or reloads too, the loader in
|
|
48
|
+
* the box the bars will take, and says nothing of zooming in ("Click or
|
|
49
|
+
* drag to zoom", "Drag to zoom") while it has no bars. Unzoomed nothing
|
|
50
|
+
* changes: the loader is a bare box that ignores the double-click, and an
|
|
51
|
+
* empty histogram with no header controls renders nothing. A click on a
|
|
52
|
+
* bar still waiting out the double-click window when the bars go away
|
|
53
|
+
* does not zoom in once a double-click on the placeholder zooms out.
|
|
54
|
+
* - The log and trace analytics views swap their chart for a loader on
|
|
55
|
+
* every refetch. The timeseries' loader zooms out, through the chart's
|
|
56
|
+
* own selection, so a click on a bucket still waiting to zoom in when the
|
|
57
|
+
* loader came up zooms nowhere; the top list's and the table's loaders do
|
|
58
|
+
* not zoom out - they are no time charts, even when a host hands the view
|
|
59
|
+
* zoom handlers of its own.
|
|
60
|
+
* - An exception's Occurrence Trend clears its bars for a zoom's refetch;
|
|
61
|
+
* the loader in their place goes back to the preset window.
|
|
62
|
+
*
|
|
63
|
+
* Before commit b6ec899df4, every case in which the chart takes a
|
|
64
|
+
* placeholder's place mid-double-click stayed zoomed, a zoomed histogram
|
|
65
|
+
* with no bars offered "Click or drag to zoom", and one loading its window
|
|
66
|
+
* - its first bars, or an empty window again - stood as the bare loader
|
|
67
|
+
* box, its card gone.
|
|
68
|
+
*
|
|
69
|
+
* Everything is real, recharts included - only ResponsiveContainer is
|
|
70
|
+
* stood in for, as in HistogramDoubleClickResetRealRecharts.test.tsx, since
|
|
71
|
+
* jsdom lays nothing out - down to the data boundary: API and ModelAPI are
|
|
72
|
+
* mocked as in LogsAnalyticsViewZoom.test.tsx, TracesAnalyticsViewZoom.test.tsx
|
|
73
|
+
* and ExceptionOccurrenceTrendZoom.test.tsx, and a request can be held
|
|
74
|
+
* open to keep a loader on screen.
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
const postMock: MockFunction = getJestMockFunction();
|
|
78
|
+
|
|
79
|
+
jest.mock("../../../../UI/Utils/API/API", () => {
|
|
80
|
+
return {
|
|
81
|
+
__esModule: true,
|
|
82
|
+
default: {
|
|
83
|
+
post: (...args: Array<any>) => {
|
|
84
|
+
return postMock(...args);
|
|
85
|
+
},
|
|
86
|
+
getFriendlyMessage: (error: Error) => {
|
|
87
|
+
return error?.message || "Failed";
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
jest.mock("../../../../UI/Utils/ModelAPI/ModelAPI", () => {
|
|
94
|
+
return {
|
|
95
|
+
__esModule: true,
|
|
96
|
+
default: {
|
|
97
|
+
getCommonHeaders: () => {
|
|
98
|
+
return {};
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
jest.mock("../../../../UI/Utils/Telemetry/UseTelemetryEntityNames", () => {
|
|
105
|
+
// One map for every render: the views memoise their series on it.
|
|
106
|
+
const noNames: Map<string, string> = new Map();
|
|
107
|
+
return {
|
|
108
|
+
__esModule: true,
|
|
109
|
+
default: () => {
|
|
110
|
+
return noNames;
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
jest.mock("recharts", () => {
|
|
116
|
+
const actual: Record<string, unknown> = jest.requireActual(
|
|
117
|
+
"recharts",
|
|
118
|
+
) as Record<string, unknown>;
|
|
119
|
+
const react: typeof React = jest.requireActual("react") as typeof React;
|
|
120
|
+
return {
|
|
121
|
+
...actual,
|
|
122
|
+
ResponsiveContainer: ({ children }: { children: React.ReactElement }) => {
|
|
123
|
+
return react.cloneElement(children, { width: 600, height: 300 });
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Imported after the mocks so the components pick them up.
|
|
129
|
+
import LogsHistogram from "../../../../UI/Components/LogsViewer/components/LogsHistogram";
|
|
130
|
+
import { HistogramBucket as LogsHistogramBucket } from "../../../../UI/Components/LogsViewer/types";
|
|
131
|
+
import TelemetryHistogram from "../../../../UI/Components/TelemetryViewer/components/TelemetryHistogram";
|
|
132
|
+
import {
|
|
133
|
+
HistogramBucket as TelemetryHistogramBucket,
|
|
134
|
+
HistogramSeriesOption,
|
|
135
|
+
} from "../../../../UI/Components/TelemetryViewer/types";
|
|
136
|
+
import LogsAnalyticsView, {
|
|
137
|
+
LOGS_ANALYTICS_TIMESERIES_TEST_ID,
|
|
138
|
+
} from "../../../../UI/Components/LogsViewer/components/LogsAnalyticsView";
|
|
139
|
+
import TracesAnalyticsView, {
|
|
140
|
+
TRACES_ANALYTICS_TIMESERIES_TEST_ID,
|
|
141
|
+
} from "../../../../../App/FeatureSet/Dashboard/src/Components/Traces/TracesAnalyticsView";
|
|
142
|
+
import ExceptionOccurrenceTrend from "../../../../../App/FeatureSet/Dashboard/src/Components/Exceptions/ExceptionOccurrenceTrend";
|
|
143
|
+
import { DOUBLE_CLICK_DISAMBIGUATION_MS } from "../../../../UI/Components/Charts/ChartLibrary/Utils/DoubleClick";
|
|
144
|
+
import { RESET_TIME_RANGE_ZOOM_BUTTON_TEST_ID } from "../../../../UI/Components/Charts/TimeRangeZoom/ResetTimeRangeZoomButton";
|
|
145
|
+
import { TimeRangeZoomProvider } from "../../../../UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext";
|
|
146
|
+
import { TimeRangeZoom } from "../../../../UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom";
|
|
147
|
+
import InBetween from "../../../../Types/BaseDatabase/InBetween";
|
|
148
|
+
import { JSONObject } from "../../../../Types/JSON";
|
|
149
|
+
import ObjectID from "../../../../Types/ObjectID";
|
|
150
|
+
import RangeStartAndEndDateTime from "../../../../Types/Time/RangeStartAndEndDateTime";
|
|
151
|
+
import TimeRange from "../../../../Types/Time/TimeRange";
|
|
152
|
+
|
|
153
|
+
const NOW: Date = new Date("2026-09-28T12:00:00.000Z");
|
|
154
|
+
const MINUTE_MS: number = 60 * 1000;
|
|
155
|
+
const DAY_MS: number = 24 * 60 * MINUTE_MS;
|
|
156
|
+
// One animation frame, as fake timers run them.
|
|
157
|
+
const FRAME_MS: number = 16;
|
|
158
|
+
// Inside every plot below: under its top edge and above its x-axis.
|
|
159
|
+
const POINTER_Y: number = 120;
|
|
160
|
+
|
|
161
|
+
type SelectHandler = (startTime: Date, endTime: Date) => void;
|
|
162
|
+
|
|
163
|
+
/* Pointer and page */
|
|
164
|
+
|
|
165
|
+
interface PointerSpot {
|
|
166
|
+
clientX: number;
|
|
167
|
+
clientY: number;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// One animation frame passes: fake timers run the frame's callbacks.
|
|
171
|
+
function nextFrame(): void {
|
|
172
|
+
act(() => {
|
|
173
|
+
jest.advanceTimersByTime(FRAME_MS);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Long past the double-click window: a click still waiting to zoom in has.
|
|
178
|
+
function waitOutTheDoubleClickWindow(): void {
|
|
179
|
+
act(() => {
|
|
180
|
+
jest.advanceTimersByTime(DOUBLE_CLICK_DISAMBIGUATION_MS * 4);
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// The browser moves on to the next task: a timeout set for 0ms runs.
|
|
185
|
+
function nextTask(): void {
|
|
186
|
+
act(() => {
|
|
187
|
+
jest.advanceTimersByTime(0);
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Every timer still pending runs, however far off.
|
|
192
|
+
function runEveryPendingTimer(): void {
|
|
193
|
+
act(() => {
|
|
194
|
+
jest.runOnlyPendingTimers();
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// The pointer comes to rest at `spot` over a chart root.
|
|
199
|
+
function restAt(chartRoot: Element, spot: PointerSpot): void {
|
|
200
|
+
fireEvent.mouseMove(chartRoot, { ...spot, buttons: 0 });
|
|
201
|
+
nextFrame();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/*
|
|
205
|
+
* The main button goes down at `spot`. `clickCount` is the browser's own
|
|
206
|
+
* count (MouseEvent.detail): 2 for the second press of a double-click.
|
|
207
|
+
*/
|
|
208
|
+
function press(
|
|
209
|
+
chartRoot: Element,
|
|
210
|
+
spot: PointerSpot,
|
|
211
|
+
clickCount: number,
|
|
212
|
+
): void {
|
|
213
|
+
fireEvent.mouseDown(chartRoot, {
|
|
214
|
+
...spot,
|
|
215
|
+
button: 0,
|
|
216
|
+
buttons: 1,
|
|
217
|
+
detail: clickCount,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// The pointer moves to `spot` with the button held.
|
|
222
|
+
function moveHeld(chartRoot: Element, spot: PointerSpot): void {
|
|
223
|
+
fireEvent.mouseMove(chartRoot, { ...spot, buttons: 1 });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function release(
|
|
227
|
+
chartRoot: Element,
|
|
228
|
+
spot: PointerSpot,
|
|
229
|
+
clickCount: number,
|
|
230
|
+
): void {
|
|
231
|
+
fireEvent.mouseUp(chartRoot, {
|
|
232
|
+
...spot,
|
|
233
|
+
button: 0,
|
|
234
|
+
buttons: 0,
|
|
235
|
+
detail: clickCount,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/*
|
|
240
|
+
* What a browser delivers for a double-click on an element that stays on
|
|
241
|
+
* the page: two presses, counted, each with its release and its click,
|
|
242
|
+
* and then the dblclick.
|
|
243
|
+
*/
|
|
244
|
+
function doubleClickOn(target: Element): void {
|
|
245
|
+
for (const clickCount of [1, 2]) {
|
|
246
|
+
fireEvent.mouseDown(target, { button: 0, buttons: 1, detail: clickCount });
|
|
247
|
+
fireEvent.mouseUp(target, { button: 0, buttons: 0, detail: clickCount });
|
|
248
|
+
fireEvent.click(target, { button: 0, detail: clickCount });
|
|
249
|
+
}
|
|
250
|
+
fireEvent.doubleClick(target, { button: 0, detail: 2 });
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/*
|
|
254
|
+
* Runs `gesture` and returns whatever it threw. An error thrown by a
|
|
255
|
+
* handler does not come back out of the event's dispatch: the page reports
|
|
256
|
+
* it as uncaught, so that is where this listens too.
|
|
257
|
+
*/
|
|
258
|
+
function errorsThrownBy(gesture: () => void): Array<unknown> {
|
|
259
|
+
const errors: Array<unknown> = [];
|
|
260
|
+
const onError: (event: ErrorEvent) => void = (event: ErrorEvent): void => {
|
|
261
|
+
errors.push(event.error);
|
|
262
|
+
};
|
|
263
|
+
window.addEventListener("error", onError);
|
|
264
|
+
try {
|
|
265
|
+
gesture();
|
|
266
|
+
} catch (error: unknown) {
|
|
267
|
+
errors.push(error);
|
|
268
|
+
} finally {
|
|
269
|
+
window.removeEventListener("error", onError);
|
|
270
|
+
}
|
|
271
|
+
return errors;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/*
|
|
275
|
+
* The loader's box: what stands where the chart will be while it loads,
|
|
276
|
+
* around the spinner.
|
|
277
|
+
*/
|
|
278
|
+
function loaderBox(): HTMLElement {
|
|
279
|
+
const box: HTMLElement | null =
|
|
280
|
+
screen.getByTestId("component-loader").parentElement;
|
|
281
|
+
if (!box) {
|
|
282
|
+
throw new Error("the loader stands in no box");
|
|
283
|
+
}
|
|
284
|
+
return box;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// The chart root recharts listens on for the pointer, inside `scope`.
|
|
288
|
+
function chartRootIn(scope: ParentNode): Element {
|
|
289
|
+
const wrapper: Element | null = scope.querySelector(".recharts-wrapper");
|
|
290
|
+
if (!wrapper) {
|
|
291
|
+
throw new Error("recharts drew no chart");
|
|
292
|
+
}
|
|
293
|
+
return wrapper;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
interface DrawnTick {
|
|
297
|
+
label: string;
|
|
298
|
+
x: number;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// The x-axis tick labels recharts drew inside `scope`, left to right.
|
|
302
|
+
function drawnTicks(scope: ParentNode): Array<DrawnTick> {
|
|
303
|
+
return Array.from(
|
|
304
|
+
scope.querySelectorAll(
|
|
305
|
+
".recharts-xAxis-tick-labels text.recharts-cartesian-axis-tick-value",
|
|
306
|
+
),
|
|
307
|
+
).map((tick: Element): DrawnTick => {
|
|
308
|
+
return {
|
|
309
|
+
label: tick.textContent || "",
|
|
310
|
+
x: Number(tick.getAttribute("x")),
|
|
311
|
+
};
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/*
|
|
316
|
+
* Where the pointer lands on the bucket whose tick reads `label` ("10:04";
|
|
317
|
+
* the labels are local times, "10:04 AM" on a 12-hour clock).
|
|
318
|
+
*/
|
|
319
|
+
function spotOnTick(scope: ParentNode, label: string): PointerSpot {
|
|
320
|
+
const tick: DrawnTick | undefined = drawnTicks(scope).find(
|
|
321
|
+
(drawn: DrawnTick): boolean => {
|
|
322
|
+
return drawn.label.startsWith(label);
|
|
323
|
+
},
|
|
324
|
+
);
|
|
325
|
+
if (!tick) {
|
|
326
|
+
throw new Error(`recharts drew no tick label "${label}"`);
|
|
327
|
+
}
|
|
328
|
+
return { clientX: tick.x, clientY: POINTER_Y };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// A single click on a chart: its press, its release, the browser's click.
|
|
332
|
+
function clickChartAt(chartRoot: Element, spot: PointerSpot): void {
|
|
333
|
+
restAt(chartRoot, spot);
|
|
334
|
+
press(chartRoot, spot, 1);
|
|
335
|
+
release(chartRoot, spot, 1);
|
|
336
|
+
fireEvent.click(chartRoot, { ...spot, button: 0, detail: 1 });
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/* The data boundary */
|
|
340
|
+
|
|
341
|
+
interface PostRequest {
|
|
342
|
+
url: { toString: () => string };
|
|
343
|
+
data: Record<string, unknown>;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
interface HeldRequest {
|
|
347
|
+
request: PostRequest;
|
|
348
|
+
answer: () => void;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const LOGS_ANALYTICS_ROUTE: string = "/telemetry/logs/analytics";
|
|
352
|
+
const TRACES_ANALYTICS_ROUTE: string = "/telemetry/traces/analytics";
|
|
353
|
+
const EXCEPTIONS_HISTOGRAM_ROUTE: string = "/telemetry/exceptions/histogram";
|
|
354
|
+
|
|
355
|
+
// ClickHouse's bucket labels for the analytics views: UTC, no zone marker.
|
|
356
|
+
const ANALYTICS_BUCKETS: Array<string> = [
|
|
357
|
+
"2026-09-28 11:10:00",
|
|
358
|
+
"2026-09-28 11:11:00",
|
|
359
|
+
"2026-09-28 11:12:00",
|
|
360
|
+
];
|
|
361
|
+
|
|
362
|
+
// While set, a request waits for answerHeldRequests().
|
|
363
|
+
let isHoldingRequests: boolean = false;
|
|
364
|
+
const heldRequests: Array<HeldRequest> = [];
|
|
365
|
+
|
|
366
|
+
function logsAnalyticsAnswer(body: Record<string, unknown>): unknown {
|
|
367
|
+
if (body["chartType"] === "toplist") {
|
|
368
|
+
return { data: { data: [{ value: "Error", count: 4 }] } };
|
|
369
|
+
}
|
|
370
|
+
if (body["chartType"] === "table") {
|
|
371
|
+
return {
|
|
372
|
+
data: { data: [{ groupValues: { severityText: "Error" }, count: 4 }] },
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
return {
|
|
376
|
+
data: {
|
|
377
|
+
data: ANALYTICS_BUCKETS.map(
|
|
378
|
+
(time: string, index: number): Record<string, unknown> => {
|
|
379
|
+
return { time: time, count: index + 1, groupValues: {} };
|
|
380
|
+
},
|
|
381
|
+
),
|
|
382
|
+
},
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function tracesAnalyticsAnswer(body: Record<string, unknown>): unknown {
|
|
387
|
+
if (body["chartType"] === "toplist") {
|
|
388
|
+
return { data: { data: [{ value: "GET /", metricValue: 4, count: 4 }] } };
|
|
389
|
+
}
|
|
390
|
+
if (body["chartType"] === "table") {
|
|
391
|
+
return {
|
|
392
|
+
data: {
|
|
393
|
+
data: [
|
|
394
|
+
{
|
|
395
|
+
groupValues: { name: "GET /" },
|
|
396
|
+
count: 4,
|
|
397
|
+
errorCount: 0,
|
|
398
|
+
avgDurationMs: 1,
|
|
399
|
+
p50DurationMs: 1,
|
|
400
|
+
p90DurationMs: 1,
|
|
401
|
+
p95DurationMs: 1,
|
|
402
|
+
p99DurationMs: 1,
|
|
403
|
+
minDurationMs: 1,
|
|
404
|
+
maxDurationMs: 1,
|
|
405
|
+
},
|
|
406
|
+
],
|
|
407
|
+
},
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
return {
|
|
411
|
+
data: {
|
|
412
|
+
data: ANALYTICS_BUCKETS.map((time: string): Record<string, unknown> => {
|
|
413
|
+
return { time: time, value: 3, groupValues: { name: "GET /" } };
|
|
414
|
+
}),
|
|
415
|
+
},
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/*
|
|
420
|
+
* Occurrences in every bucket the exception histogram is asked for, so a
|
|
421
|
+
* zoomed window always has bars to draw.
|
|
422
|
+
*/
|
|
423
|
+
function exceptionHistogramAnswer(body: Record<string, unknown>): unknown {
|
|
424
|
+
const startMs: number = new Date(String(body["startTime"])).getTime();
|
|
425
|
+
const endMs: number = new Date(String(body["endTime"])).getTime();
|
|
426
|
+
const bucketMs: number = Number(body["bucketSizeInMinutes"]) * MINUTE_MS;
|
|
427
|
+
const buckets: Array<Record<string, unknown>> = [];
|
|
428
|
+
|
|
429
|
+
for (
|
|
430
|
+
let timeMs: number = Math.floor(startMs / bucketMs) * bucketMs;
|
|
431
|
+
timeMs < endMs;
|
|
432
|
+
timeMs += bucketMs
|
|
433
|
+
) {
|
|
434
|
+
buckets.push({
|
|
435
|
+
time: new Date(timeMs).toISOString(),
|
|
436
|
+
series: "unhandled",
|
|
437
|
+
count: 2,
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return { data: { buckets: buckets } };
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function answerFor(request: PostRequest): unknown {
|
|
445
|
+
const url: string = request.url.toString();
|
|
446
|
+
|
|
447
|
+
if (url.includes(LOGS_ANALYTICS_ROUTE)) {
|
|
448
|
+
return logsAnalyticsAnswer(request.data);
|
|
449
|
+
}
|
|
450
|
+
if (url.includes(TRACES_ANALYTICS_ROUTE)) {
|
|
451
|
+
return tracesAnalyticsAnswer(request.data);
|
|
452
|
+
}
|
|
453
|
+
if (url.includes(EXCEPTIONS_HISTOGRAM_ROUTE)) {
|
|
454
|
+
return exceptionHistogramAnswer(request.data);
|
|
455
|
+
}
|
|
456
|
+
throw new Error(`nothing answers ${url}`);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function holdRequests(): void {
|
|
460
|
+
isHoldingRequests = true;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Every held request gets its answer, and requests are answered at once again.
|
|
464
|
+
async function answerHeldRequests(): Promise<void> {
|
|
465
|
+
isHoldingRequests = false;
|
|
466
|
+
await act(async () => {
|
|
467
|
+
for (const held of heldRequests.splice(0)) {
|
|
468
|
+
held.answer();
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// The bodies of every request made to `route`, oldest first.
|
|
474
|
+
function requestsTo(route: string): Array<Record<string, unknown>> {
|
|
475
|
+
return postMock.mock.calls
|
|
476
|
+
.map((call: Array<unknown>): PostRequest => {
|
|
477
|
+
return call[0] as PostRequest;
|
|
478
|
+
})
|
|
479
|
+
.filter((request: PostRequest): boolean => {
|
|
480
|
+
return request.url.toString().includes(route);
|
|
481
|
+
})
|
|
482
|
+
.map((request: PostRequest): Record<string, unknown> => {
|
|
483
|
+
return request.data;
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// The bodies of the requests to `route` still waiting for an answer.
|
|
488
|
+
function heldBodies(route: string): Array<Record<string, unknown>> {
|
|
489
|
+
return heldRequests
|
|
490
|
+
.filter((held: HeldRequest): boolean => {
|
|
491
|
+
return held.request.url.toString().includes(route);
|
|
492
|
+
})
|
|
493
|
+
.map((held: HeldRequest): Record<string, unknown> => {
|
|
494
|
+
return held.request.data;
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
let histogramOnScreen: HistogramOnScreen | null = null;
|
|
499
|
+
|
|
500
|
+
beforeEach(() => {
|
|
501
|
+
/*
|
|
502
|
+
* Installed AT the fixed time, not moved there with setSystemTime: fake
|
|
503
|
+
* timers count animation frames from the moment they are installed.
|
|
504
|
+
*/
|
|
505
|
+
jest.useFakeTimers({ now: NOW });
|
|
506
|
+
isHoldingRequests = false;
|
|
507
|
+
heldRequests.length = 0;
|
|
508
|
+
postMock.mockReset();
|
|
509
|
+
postMock.mockImplementation((request: PostRequest): Promise<unknown> => {
|
|
510
|
+
if (!isHoldingRequests) {
|
|
511
|
+
return Promise.resolve(answerFor(request));
|
|
512
|
+
}
|
|
513
|
+
return new Promise((resolve: (value: unknown) => void): void => {
|
|
514
|
+
heldRequests.push({
|
|
515
|
+
request: request,
|
|
516
|
+
answer: (): void => {
|
|
517
|
+
resolve(answerFor(request));
|
|
518
|
+
},
|
|
519
|
+
});
|
|
520
|
+
});
|
|
521
|
+
});
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
afterEach(() => {
|
|
525
|
+
cleanup();
|
|
526
|
+
histogramOnScreen = null;
|
|
527
|
+
heldRequests.length = 0;
|
|
528
|
+
jest.useRealTimers();
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
/* The explorers' volume histograms */
|
|
532
|
+
|
|
533
|
+
interface Bar {
|
|
534
|
+
time: string;
|
|
535
|
+
count: number;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// What the zoomed chart shows: ten one-minute bars, 10:00 to 10:09.
|
|
539
|
+
const SHOWN_BARS: Array<Bar> = Array.from(
|
|
540
|
+
{ length: 10 },
|
|
541
|
+
(_: unknown, index: number): Bar => {
|
|
542
|
+
return {
|
|
543
|
+
time: new Date(
|
|
544
|
+
Date.parse("2026-09-28T10:00:00.000Z") + index * MINUTE_MS,
|
|
545
|
+
).toISOString(),
|
|
546
|
+
count: 5 + index,
|
|
547
|
+
};
|
|
548
|
+
},
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
const TELEMETRY_SERIES: Array<HistogramSeriesOption> = [
|
|
552
|
+
{ key: "error", label: "Error", color: "#dc2626" },
|
|
553
|
+
];
|
|
554
|
+
|
|
555
|
+
// The traces explorer's header control: which metric the histogram charts.
|
|
556
|
+
const CHART_METRIC_PICKER: React.ReactElement = (
|
|
557
|
+
<select title="Chart metric" defaultValue="count">
|
|
558
|
+
<option value="count">Count</option>
|
|
559
|
+
<option value="p95">P95 response time</option>
|
|
560
|
+
</select>
|
|
561
|
+
);
|
|
562
|
+
|
|
563
|
+
// Every message an empty histogram card can show.
|
|
564
|
+
const EMPTY_TEXTS: Array<string> = [
|
|
565
|
+
"No data in the selected range",
|
|
566
|
+
"No data for this metric in the selected range",
|
|
567
|
+
"No logs in the selected range",
|
|
568
|
+
];
|
|
569
|
+
|
|
570
|
+
interface HistogramProps {
|
|
571
|
+
bars: Array<Bar>;
|
|
572
|
+
isLoading: boolean;
|
|
573
|
+
onTimeRangeSelect?: SelectHandler | undefined;
|
|
574
|
+
// Set only while the chart shows a window dragged out of it.
|
|
575
|
+
onZoomOut?: (() => void) | undefined;
|
|
576
|
+
/*
|
|
577
|
+
* Handed no bucket interval, the chart can only zoom by a drag ("Drag
|
|
578
|
+
* to zoom"); with one (as by default here), by a click too.
|
|
579
|
+
*/
|
|
580
|
+
withoutBucketInterval?: boolean | undefined;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function bucketIntervalFor(props: HistogramProps): number | undefined {
|
|
584
|
+
return props.withoutBucketInterval ? undefined : MINUTE_MS;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
interface HistogramUnderTest {
|
|
588
|
+
name: string;
|
|
589
|
+
title: string;
|
|
590
|
+
// What its empty box says.
|
|
591
|
+
emptyText: string;
|
|
592
|
+
/*
|
|
593
|
+
* Header controls (the traces explorer's metric picker) keep the card on
|
|
594
|
+
* screen through any empty window, zoomed or not.
|
|
595
|
+
*/
|
|
596
|
+
hasHeaderActions: boolean;
|
|
597
|
+
render: (props: HistogramProps) => React.ReactElement;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function telemetryBuckets(bars: Array<Bar>): Array<TelemetryHistogramBucket> {
|
|
601
|
+
return bars.map((bar: Bar): TelemetryHistogramBucket => {
|
|
602
|
+
return { time: bar.time, series: "error", count: bar.count };
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function logsBuckets(bars: Array<Bar>): Array<LogsHistogramBucket> {
|
|
607
|
+
return bars.map((bar: Bar): LogsHistogramBucket => {
|
|
608
|
+
return { time: bar.time, severity: "Error", count: bar.count };
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const HISTOGRAMS: Array<HistogramUnderTest> = [
|
|
613
|
+
{
|
|
614
|
+
name: "telemetry histogram (the exceptions explorer's)",
|
|
615
|
+
title: "Exceptions over time",
|
|
616
|
+
emptyText: "No data in the selected range",
|
|
617
|
+
hasHeaderActions: false,
|
|
618
|
+
render: (props: HistogramProps): React.ReactElement => {
|
|
619
|
+
return (
|
|
620
|
+
<TelemetryHistogram
|
|
621
|
+
buckets={telemetryBuckets(props.bars)}
|
|
622
|
+
isLoading={props.isLoading}
|
|
623
|
+
series={TELEMETRY_SERIES}
|
|
624
|
+
title="Exceptions over time"
|
|
625
|
+
onTimeRangeSelect={props.onTimeRangeSelect}
|
|
626
|
+
onZoomOut={props.onZoomOut}
|
|
627
|
+
bucketIntervalMs={bucketIntervalFor(props)}
|
|
628
|
+
/>
|
|
629
|
+
);
|
|
630
|
+
},
|
|
631
|
+
},
|
|
632
|
+
{
|
|
633
|
+
name: "telemetry histogram with a metric picker (the traces explorer's)",
|
|
634
|
+
title: "Traces over time",
|
|
635
|
+
emptyText: "No data for this metric in the selected range",
|
|
636
|
+
hasHeaderActions: true,
|
|
637
|
+
render: (props: HistogramProps): React.ReactElement => {
|
|
638
|
+
return (
|
|
639
|
+
<TelemetryHistogram
|
|
640
|
+
buckets={telemetryBuckets(props.bars)}
|
|
641
|
+
isLoading={props.isLoading}
|
|
642
|
+
series={TELEMETRY_SERIES}
|
|
643
|
+
title="Traces over time"
|
|
644
|
+
onTimeRangeSelect={props.onTimeRangeSelect}
|
|
645
|
+
onZoomOut={props.onZoomOut}
|
|
646
|
+
bucketIntervalMs={bucketIntervalFor(props)}
|
|
647
|
+
headerActions={CHART_METRIC_PICKER}
|
|
648
|
+
/>
|
|
649
|
+
);
|
|
650
|
+
},
|
|
651
|
+
},
|
|
652
|
+
{
|
|
653
|
+
name: "logs histogram",
|
|
654
|
+
title: "Log Volume",
|
|
655
|
+
emptyText: "No logs in the selected range",
|
|
656
|
+
hasHeaderActions: false,
|
|
657
|
+
render: (props: HistogramProps): React.ReactElement => {
|
|
658
|
+
return (
|
|
659
|
+
<LogsHistogram
|
|
660
|
+
buckets={logsBuckets(props.bars)}
|
|
661
|
+
isLoading={props.isLoading}
|
|
662
|
+
onTimeRangeSelect={props.onTimeRangeSelect}
|
|
663
|
+
onZoomOut={props.onZoomOut}
|
|
664
|
+
bucketIntervalMs={bucketIntervalFor(props)}
|
|
665
|
+
/>
|
|
666
|
+
);
|
|
667
|
+
},
|
|
668
|
+
},
|
|
669
|
+
];
|
|
670
|
+
|
|
671
|
+
interface HistogramOnScreen {
|
|
672
|
+
histogram: HistogramUnderTest;
|
|
673
|
+
props: HistogramProps;
|
|
674
|
+
rendered: RenderResult;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function showHistogram(
|
|
678
|
+
histogram: HistogramUnderTest,
|
|
679
|
+
props: HistogramProps,
|
|
680
|
+
): HTMLElement {
|
|
681
|
+
const rendered: RenderResult = render(histogram.render(props));
|
|
682
|
+
histogramOnScreen = {
|
|
683
|
+
histogram: histogram,
|
|
684
|
+
props: props,
|
|
685
|
+
rendered: rendered,
|
|
686
|
+
};
|
|
687
|
+
return rendered.container;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function shownHistogram(): HistogramOnScreen {
|
|
691
|
+
if (!histogramOnScreen) {
|
|
692
|
+
throw new Error("no histogram rendered");
|
|
693
|
+
}
|
|
694
|
+
return histogramOnScreen;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// The host re-renders the chart: new bars, or a new loading state.
|
|
698
|
+
function rerenderHistogram(changes: Partial<HistogramProps>): void {
|
|
699
|
+
const chart: HistogramOnScreen = shownHistogram();
|
|
700
|
+
chart.props = { ...chart.props, ...changes };
|
|
701
|
+
chart.rendered.rerender(chart.histogram.render(chart.props));
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// The band a click on one bar paints while it waits to zoom in, if drawn.
|
|
705
|
+
function drawnBand(): Element | null {
|
|
706
|
+
return shownHistogram().rendered.container.querySelector(
|
|
707
|
+
".recharts-reference-area-rect",
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// A single click on the bar whose tick reads `label`.
|
|
712
|
+
function clickBar(label: string): void {
|
|
713
|
+
const container: HTMLElement = shownHistogram().rendered.container;
|
|
714
|
+
clickChartAt(chartRootIn(container), spotOnTick(container, label));
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/*
|
|
718
|
+
* Where the pointer rests through a whole double-click on a placeholder:
|
|
719
|
+
* over the box, and over the plot of the chart that takes its place.
|
|
720
|
+
*/
|
|
721
|
+
const STILL_POINTER: PointerSpot = { clientX: 300, clientY: 60 };
|
|
722
|
+
|
|
723
|
+
/*
|
|
724
|
+
* The first click of a double-click on `target` - press, release, click -
|
|
725
|
+
* and the second press: what the browser sends before the chart comes.
|
|
726
|
+
*/
|
|
727
|
+
function clickThenPressAgain(target: Element): void {
|
|
728
|
+
press(target, STILL_POINTER, 1);
|
|
729
|
+
release(target, STILL_POINTER, 1);
|
|
730
|
+
fireEvent.click(target, { ...STILL_POINTER, button: 0, detail: 1 });
|
|
731
|
+
press(target, STILL_POINTER, 2);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/*
|
|
735
|
+
* The same with the right button, which the browser counts too, but which
|
|
736
|
+
* opens a context menu rather than clicking.
|
|
737
|
+
*/
|
|
738
|
+
function rightClickThenPressAgain(target: Element): void {
|
|
739
|
+
const rightButton: PointerSpot & { button: number } = {
|
|
740
|
+
...STILL_POINTER,
|
|
741
|
+
button: 2,
|
|
742
|
+
};
|
|
743
|
+
fireEvent.mouseDown(target, { ...rightButton, buttons: 2, detail: 1 });
|
|
744
|
+
fireEvent.mouseUp(target, { ...rightButton, buttons: 0, detail: 1 });
|
|
745
|
+
fireEvent.contextMenu(target, rightButton);
|
|
746
|
+
fireEvent.mouseDown(target, { ...rightButton, buttons: 2, detail: 2 });
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/*
|
|
750
|
+
* A second press of `button` let go over the chart drawn inside
|
|
751
|
+
* `chartArea`: returns the node the release lands on.
|
|
752
|
+
*/
|
|
753
|
+
function releaseOverTheChart(chartArea: ParentNode, button: number): Element {
|
|
754
|
+
const chartRoot: Element = chartRootIn(chartArea);
|
|
755
|
+
fireEvent.mouseUp(chartRoot, {
|
|
756
|
+
...STILL_POINTER,
|
|
757
|
+
button: button,
|
|
758
|
+
buttons: 0,
|
|
759
|
+
detail: 2,
|
|
760
|
+
});
|
|
761
|
+
return chartRoot;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// The same, let go off the chart, where only the page hears it.
|
|
765
|
+
function releaseOffTheChart(button: number): Element {
|
|
766
|
+
fireEvent.mouseUp(document.body, {
|
|
767
|
+
...STILL_POINTER,
|
|
768
|
+
button: button,
|
|
769
|
+
buttons: 0,
|
|
770
|
+
detail: 2,
|
|
771
|
+
});
|
|
772
|
+
return document.body;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/*
|
|
776
|
+
* What the browser sends after the release of a double-click's second
|
|
777
|
+
* press: its click and the dblclick, to the nearest node the press and the
|
|
778
|
+
* release share - or nothing at all once the node the press went down on
|
|
779
|
+
* has left the page.
|
|
780
|
+
*/
|
|
781
|
+
function browserSendsTheClicks(pressed: Element, released: Element): void {
|
|
782
|
+
if (!pressed.isConnected) {
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
let shared: Element | null = pressed;
|
|
786
|
+
while (shared && !shared.contains(released)) {
|
|
787
|
+
shared = shared.parentElement;
|
|
788
|
+
}
|
|
789
|
+
if (!shared) {
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
fireEvent.click(shared, { ...STILL_POINTER, button: 0, detail: 2 });
|
|
793
|
+
fireEvent.doubleClick(shared, { ...STILL_POINTER, button: 0, detail: 2 });
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/*
|
|
797
|
+
* Each way a double-click on a loader can end once the chart has taken its
|
|
798
|
+
* place: where its second press went down, and where it is let go. The
|
|
799
|
+
* spinner goes with the loader. The box around it goes too, or React keeps
|
|
800
|
+
* it for the chart - the analytics views' and the Occurrence Trend's charts
|
|
801
|
+
* render into the very div their loader did - without the loader's
|
|
802
|
+
* handlers; the click and the dblclick the browser then sends that box
|
|
803
|
+
* bubble away from the chart's own dblclick handler.
|
|
804
|
+
*/
|
|
805
|
+
interface LoaderRaceEnding {
|
|
806
|
+
name: string;
|
|
807
|
+
// What the second press goes down on.
|
|
808
|
+
pressTarget: () => HTMLElement;
|
|
809
|
+
// Its release, of `button`: returns the node it lands on.
|
|
810
|
+
release: (chartArea: HTMLElement, button: number) => Element;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function spinner(): HTMLElement {
|
|
814
|
+
return screen.getByTestId("component-loader");
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
const LOADER_RACE_ENDINGS: Array<LoaderRaceEnding> = [
|
|
818
|
+
{
|
|
819
|
+
name: "pressed on its spinner and let go over the chart",
|
|
820
|
+
pressTarget: spinner,
|
|
821
|
+
release: releaseOverTheChart,
|
|
822
|
+
},
|
|
823
|
+
{
|
|
824
|
+
name: "pressed on its spinner and let go off the chart, where only the page hears it",
|
|
825
|
+
pressTarget: spinner,
|
|
826
|
+
release: (_chartArea: HTMLElement, button: number): Element => {
|
|
827
|
+
return releaseOffTheChart(button);
|
|
828
|
+
},
|
|
829
|
+
},
|
|
830
|
+
{
|
|
831
|
+
name: "pressed on the box around its spinner and let go over the chart",
|
|
832
|
+
pressTarget: loaderBox,
|
|
833
|
+
release: releaseOverTheChart,
|
|
834
|
+
},
|
|
835
|
+
];
|
|
836
|
+
|
|
837
|
+
// The chart took the loader's place: the spinner is gone, the chart drawn.
|
|
838
|
+
function expectChartInTheLoadersPlace(chartArea: HTMLElement): void {
|
|
839
|
+
expect(screen.queryByTestId("component-loader")).toBeNull();
|
|
840
|
+
expect(chartRootIn(chartArea)).toBeInTheDocument();
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// The zoom hints a histogram's header can show.
|
|
844
|
+
const ZOOM_IN_HINTS: Array<string> = ["Click or drag to zoom", "Drag to zoom"];
|
|
845
|
+
|
|
846
|
+
function zoomInHintsShown(): Array<string> {
|
|
847
|
+
return ZOOM_IN_HINTS.filter((hint: string): boolean => {
|
|
848
|
+
return screen.queryByText(hint) !== null;
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
interface Placeholder {
|
|
853
|
+
name: string;
|
|
854
|
+
// Whether the chart's next window is still loading when its bars go.
|
|
855
|
+
isLoading: boolean;
|
|
856
|
+
find: (histogram: HistogramUnderTest) => HTMLElement;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
const PLACEHOLDERS: Array<Placeholder> = [
|
|
860
|
+
{
|
|
861
|
+
name: "empty box",
|
|
862
|
+
isLoading: false,
|
|
863
|
+
find: (histogram: HistogramUnderTest): HTMLElement => {
|
|
864
|
+
return screen.getByText(histogram.emptyText);
|
|
865
|
+
},
|
|
866
|
+
},
|
|
867
|
+
{
|
|
868
|
+
name: "loader",
|
|
869
|
+
isLoading: true,
|
|
870
|
+
find: (): HTMLElement => {
|
|
871
|
+
return loaderBox();
|
|
872
|
+
},
|
|
873
|
+
},
|
|
874
|
+
];
|
|
875
|
+
|
|
876
|
+
for (const histogram of HISTOGRAMS) {
|
|
877
|
+
describe(`the ${histogram.name}`, () => {
|
|
878
|
+
describe("loading its first bars", () => {
|
|
879
|
+
test("zoomed: the loader is not selectable, and a double-click on it zooms out once", () => {
|
|
880
|
+
const onTimeRangeSelect: MockFunction = getJestMockFunction();
|
|
881
|
+
const onZoomOut: MockFunction = getJestMockFunction();
|
|
882
|
+
showHistogram(histogram, {
|
|
883
|
+
bars: [],
|
|
884
|
+
isLoading: true,
|
|
885
|
+
onTimeRangeSelect: onTimeRangeSelect,
|
|
886
|
+
onZoomOut: onZoomOut,
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
const box: HTMLElement = loaderBox();
|
|
890
|
+
// The double-click must not also select anything around the spinner.
|
|
891
|
+
expect(box).toHaveClass("select-none");
|
|
892
|
+
/*
|
|
893
|
+
* A zoomed chart keeps its card while its window loads - its title
|
|
894
|
+
* and the way back - with the loader in the box the bars will take,
|
|
895
|
+
* so the double-click stays where the reader's pointer already is.
|
|
896
|
+
*/
|
|
897
|
+
expect(screen.getByText(histogram.title)).toBeInTheDocument();
|
|
898
|
+
expect(screen.getByText("Double-click to reset")).toBeInTheDocument();
|
|
899
|
+
|
|
900
|
+
doubleClickOn(box);
|
|
901
|
+
expect(onZoomOut).toHaveBeenCalledTimes(1);
|
|
902
|
+
|
|
903
|
+
waitOutTheDoubleClickWindow();
|
|
904
|
+
expect(onZoomOut).toHaveBeenCalledTimes(1);
|
|
905
|
+
expect(onTimeRangeSelect).not.toHaveBeenCalled();
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
test("not zoomed: a double-click on the loader does nothing, and throws nothing", () => {
|
|
909
|
+
const onTimeRangeSelect: MockFunction = getJestMockFunction();
|
|
910
|
+
showHistogram(histogram, {
|
|
911
|
+
bars: [],
|
|
912
|
+
isLoading: true,
|
|
913
|
+
onTimeRangeSelect: onTimeRangeSelect,
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
expect(
|
|
917
|
+
errorsThrownBy((): void => {
|
|
918
|
+
doubleClickOn(loaderBox());
|
|
919
|
+
}),
|
|
920
|
+
).toEqual([]);
|
|
921
|
+
|
|
922
|
+
waitOutTheDoubleClickWindow();
|
|
923
|
+
expect(onTimeRangeSelect).not.toHaveBeenCalled();
|
|
924
|
+
expect(screen.getByTestId("component-loader")).toBeInTheDocument();
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
test("with no zoom offered at all: a double-click on the loader throws nothing", () => {
|
|
928
|
+
showHistogram(histogram, { bars: [], isLoading: true });
|
|
929
|
+
|
|
930
|
+
expect(
|
|
931
|
+
errorsThrownBy((): void => {
|
|
932
|
+
doubleClickOn(loaderBox());
|
|
933
|
+
waitOutTheDoubleClickWindow();
|
|
934
|
+
}),
|
|
935
|
+
).toEqual([]);
|
|
936
|
+
expect(screen.getByTestId("component-loader")).toBeInTheDocument();
|
|
937
|
+
});
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
describe("with an empty window", () => {
|
|
941
|
+
test("zoomed: the card stays - its title and the reset hint - and its empty box takes the double-click", () => {
|
|
942
|
+
const onTimeRangeSelect: MockFunction = getJestMockFunction();
|
|
943
|
+
const onZoomOut: MockFunction = getJestMockFunction();
|
|
944
|
+
const container: HTMLElement = showHistogram(histogram, {
|
|
945
|
+
bars: [],
|
|
946
|
+
isLoading: false,
|
|
947
|
+
onTimeRangeSelect: onTimeRangeSelect,
|
|
948
|
+
onZoomOut: onZoomOut,
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
expect(screen.getByText(histogram.title)).toBeInTheDocument();
|
|
952
|
+
expect(screen.getByText("Double-click to reset")).toBeInTheDocument();
|
|
953
|
+
if (histogram.hasHeaderActions) {
|
|
954
|
+
expect(screen.getByTitle("Chart metric")).toBeInTheDocument();
|
|
955
|
+
}
|
|
956
|
+
// Its own message, and only that one; no chart over an empty window.
|
|
957
|
+
expect(
|
|
958
|
+
EMPTY_TEXTS.filter((text: string): boolean => {
|
|
959
|
+
return screen.queryByText(text) !== null;
|
|
960
|
+
}),
|
|
961
|
+
).toEqual([histogram.emptyText]);
|
|
962
|
+
expect(container.querySelector(".recharts-wrapper")).toBeNull();
|
|
963
|
+
|
|
964
|
+
const emptyBox: HTMLElement = screen.getByText(histogram.emptyText);
|
|
965
|
+
// The double-click must not also select a word of the message.
|
|
966
|
+
expect(emptyBox).toHaveClass("select-none");
|
|
967
|
+
|
|
968
|
+
doubleClickOn(emptyBox);
|
|
969
|
+
expect(onZoomOut).toHaveBeenCalledTimes(1);
|
|
970
|
+
|
|
971
|
+
waitOutTheDoubleClickWindow();
|
|
972
|
+
expect(onZoomOut).toHaveBeenCalledTimes(1);
|
|
973
|
+
expect(onTimeRangeSelect).not.toHaveBeenCalled();
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
if (histogram.hasHeaderActions) {
|
|
977
|
+
test("not zoomed: the card stays for its metric picker, and a double-click on its empty box does nothing", () => {
|
|
978
|
+
const onTimeRangeSelect: MockFunction = getJestMockFunction();
|
|
979
|
+
showHistogram(histogram, {
|
|
980
|
+
bars: [],
|
|
981
|
+
isLoading: false,
|
|
982
|
+
onTimeRangeSelect: onTimeRangeSelect,
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
expect(screen.getByTitle("Chart metric")).toBeInTheDocument();
|
|
986
|
+
expect(screen.queryByText("Double-click to reset")).toBeNull();
|
|
987
|
+
|
|
988
|
+
expect(
|
|
989
|
+
errorsThrownBy((): void => {
|
|
990
|
+
doubleClickOn(screen.getByText(histogram.emptyText));
|
|
991
|
+
waitOutTheDoubleClickWindow();
|
|
992
|
+
}),
|
|
993
|
+
).toEqual([]);
|
|
994
|
+
expect(onTimeRangeSelect).not.toHaveBeenCalled();
|
|
995
|
+
});
|
|
996
|
+
} else {
|
|
997
|
+
test("not zoomed: it still renders nothing at all", () => {
|
|
998
|
+
const container: HTMLElement = showHistogram(histogram, {
|
|
999
|
+
bars: [],
|
|
1000
|
+
isLoading: false,
|
|
1001
|
+
onTimeRangeSelect: getJestMockFunction(),
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
expect(container).toBeEmptyDOMElement();
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
describe("a click on a bar still waiting to zoom in when the bars go away", () => {
|
|
1010
|
+
for (const placeholder of PLACEHOLDERS) {
|
|
1011
|
+
test(`a double-click on the ${placeholder.name} that takes their place zooms out once, and the click never zooms in`, () => {
|
|
1012
|
+
const onTimeRangeSelect: MockFunction = getJestMockFunction();
|
|
1013
|
+
const onZoomOut: MockFunction = getJestMockFunction();
|
|
1014
|
+
showHistogram(histogram, {
|
|
1015
|
+
bars: SHOWN_BARS,
|
|
1016
|
+
isLoading: false,
|
|
1017
|
+
onTimeRangeSelect: onTimeRangeSelect,
|
|
1018
|
+
onZoomOut: onZoomOut,
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
clickBar("10:04");
|
|
1022
|
+
// The click waits out the double-click window, its bar banded.
|
|
1023
|
+
expect(drawnBand()).not.toBeNull();
|
|
1024
|
+
expect(onTimeRangeSelect).not.toHaveBeenCalled();
|
|
1025
|
+
|
|
1026
|
+
// The chart's next window lands (or starts loading) with no bars.
|
|
1027
|
+
rerenderHistogram({ bars: [], isLoading: placeholder.isLoading });
|
|
1028
|
+
expect(
|
|
1029
|
+
shownHistogram().rendered.container.querySelector(
|
|
1030
|
+
".recharts-wrapper",
|
|
1031
|
+
),
|
|
1032
|
+
).toBeNull();
|
|
1033
|
+
|
|
1034
|
+
const box: HTMLElement = placeholder.find(histogram);
|
|
1035
|
+
expect(box).toHaveClass("select-none");
|
|
1036
|
+
doubleClickOn(box);
|
|
1037
|
+
expect(onZoomOut).toHaveBeenCalledTimes(1);
|
|
1038
|
+
|
|
1039
|
+
waitOutTheDoubleClickWindow();
|
|
1040
|
+
expect(onTimeRangeSelect).not.toHaveBeenCalled();
|
|
1041
|
+
expect(onZoomOut).toHaveBeenCalledTimes(1);
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
/*
|
|
1047
|
+
* The zoomed window lands in the middle of the double-click: its bars
|
|
1048
|
+
* take the loader's place between the second press and its release.
|
|
1049
|
+
* No dblclick reaches the chart - the node the press went down on is
|
|
1050
|
+
* gone, or kept without a dblclick handler - and before commit
|
|
1051
|
+
* b6ec899df4 the chart stayed zoomed.
|
|
1052
|
+
*/
|
|
1053
|
+
describe("a double-click on the loader whose second press the bars land under", () => {
|
|
1054
|
+
for (const ending of LOADER_RACE_ENDINGS) {
|
|
1055
|
+
test(`zoomed: ${ending.name}, it zooms out once, in the next task`, () => {
|
|
1056
|
+
const onTimeRangeSelect: MockFunction = getJestMockFunction();
|
|
1057
|
+
const onZoomOut: MockFunction = getJestMockFunction();
|
|
1058
|
+
const container: HTMLElement = showHistogram(histogram, {
|
|
1059
|
+
bars: [],
|
|
1060
|
+
isLoading: true,
|
|
1061
|
+
onTimeRangeSelect: onTimeRangeSelect,
|
|
1062
|
+
onZoomOut: onZoomOut,
|
|
1063
|
+
});
|
|
1064
|
+
const pressed: HTMLElement = ending.pressTarget();
|
|
1065
|
+
|
|
1066
|
+
clickThenPressAgain(pressed);
|
|
1067
|
+
rerenderHistogram({ bars: SHOWN_BARS, isLoading: false });
|
|
1068
|
+
expectChartInTheLoadersPlace(container);
|
|
1069
|
+
|
|
1070
|
+
browserSendsTheClicks(pressed, ending.release(container, 0));
|
|
1071
|
+
|
|
1072
|
+
// No dblclick reached the chart: the zoom-out waits for the next task.
|
|
1073
|
+
expect(onZoomOut).not.toHaveBeenCalled();
|
|
1074
|
+
|
|
1075
|
+
nextTask();
|
|
1076
|
+
expect(onZoomOut).toHaveBeenCalledTimes(1);
|
|
1077
|
+
|
|
1078
|
+
waitOutTheDoubleClickWindow();
|
|
1079
|
+
runEveryPendingTimer();
|
|
1080
|
+
expect(onZoomOut).toHaveBeenCalledTimes(1);
|
|
1081
|
+
expect(onTimeRangeSelect).not.toHaveBeenCalled();
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
test("zoomed: when the browser does send the click and the dblclick, to the box around the new chart, it zooms out exactly once", () => {
|
|
1086
|
+
const onTimeRangeSelect: MockFunction = getJestMockFunction();
|
|
1087
|
+
const onZoomOut: MockFunction = getJestMockFunction();
|
|
1088
|
+
const container: HTMLElement = showHistogram(histogram, {
|
|
1089
|
+
bars: [],
|
|
1090
|
+
isLoading: true,
|
|
1091
|
+
onTimeRangeSelect: onTimeRangeSelect,
|
|
1092
|
+
onZoomOut: onZoomOut,
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
clickThenPressAgain(loaderBox());
|
|
1096
|
+
rerenderHistogram({ bars: SHOWN_BARS, isLoading: false });
|
|
1097
|
+
const chartRoot: Element = chartRootIn(container);
|
|
1098
|
+
release(chartRoot, STILL_POINTER, 2);
|
|
1099
|
+
const chartBox: HTMLElement | null = chartRoot.parentElement;
|
|
1100
|
+
if (!chartBox) {
|
|
1101
|
+
throw new Error("the chart has no box around it");
|
|
1102
|
+
}
|
|
1103
|
+
fireEvent.click(chartBox, { ...STILL_POINTER, button: 0, detail: 2 });
|
|
1104
|
+
fireEvent.doubleClick(chartBox, {
|
|
1105
|
+
...STILL_POINTER,
|
|
1106
|
+
button: 0,
|
|
1107
|
+
detail: 2,
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
expect(onZoomOut).toHaveBeenCalledTimes(1);
|
|
1111
|
+
|
|
1112
|
+
nextTask();
|
|
1113
|
+
waitOutTheDoubleClickWindow();
|
|
1114
|
+
runEveryPendingTimer();
|
|
1115
|
+
expect(onZoomOut).toHaveBeenCalledTimes(1);
|
|
1116
|
+
expect(onTimeRangeSelect).not.toHaveBeenCalled();
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
for (const ending of LOADER_RACE_ENDINGS) {
|
|
1120
|
+
test(`not zoomed: ${ending.name}, it zooms nothing in and throws nothing, and a click on a bar then zooms in as ever`, () => {
|
|
1121
|
+
const onTimeRangeSelect: MockFunction = getJestMockFunction();
|
|
1122
|
+
const container: HTMLElement = showHistogram(histogram, {
|
|
1123
|
+
bars: [],
|
|
1124
|
+
isLoading: true,
|
|
1125
|
+
onTimeRangeSelect: onTimeRangeSelect,
|
|
1126
|
+
});
|
|
1127
|
+
const pressed: HTMLElement = ending.pressTarget();
|
|
1128
|
+
|
|
1129
|
+
expect(
|
|
1130
|
+
errorsThrownBy((): void => {
|
|
1131
|
+
clickThenPressAgain(pressed);
|
|
1132
|
+
rerenderHistogram({ bars: SHOWN_BARS, isLoading: false });
|
|
1133
|
+
browserSendsTheClicks(pressed, ending.release(container, 0));
|
|
1134
|
+
nextTask();
|
|
1135
|
+
waitOutTheDoubleClickWindow();
|
|
1136
|
+
}),
|
|
1137
|
+
).toEqual([]);
|
|
1138
|
+
expect(onTimeRangeSelect).not.toHaveBeenCalled();
|
|
1139
|
+
|
|
1140
|
+
clickBar("10:04");
|
|
1141
|
+
expect(onTimeRangeSelect).toHaveBeenCalledTimes(1);
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
for (const ending of LOADER_RACE_ENDINGS) {
|
|
1146
|
+
test(`zoomed: the same with the right button - ${ending.name} - zooms nothing out`, () => {
|
|
1147
|
+
const onTimeRangeSelect: MockFunction = getJestMockFunction();
|
|
1148
|
+
const onZoomOut: MockFunction = getJestMockFunction();
|
|
1149
|
+
const container: HTMLElement = showHistogram(histogram, {
|
|
1150
|
+
bars: [],
|
|
1151
|
+
isLoading: true,
|
|
1152
|
+
onTimeRangeSelect: onTimeRangeSelect,
|
|
1153
|
+
onZoomOut: onZoomOut,
|
|
1154
|
+
});
|
|
1155
|
+
|
|
1156
|
+
rightClickThenPressAgain(ending.pressTarget());
|
|
1157
|
+
rerenderHistogram({ bars: SHOWN_BARS, isLoading: false });
|
|
1158
|
+
// A right-button release: a context menu, no click.
|
|
1159
|
+
ending.release(container, 2);
|
|
1160
|
+
nextTask();
|
|
1161
|
+
waitOutTheDoubleClickWindow();
|
|
1162
|
+
runEveryPendingTimer();
|
|
1163
|
+
|
|
1164
|
+
expect(onZoomOut).not.toHaveBeenCalled();
|
|
1165
|
+
expect(onTimeRangeSelect).not.toHaveBeenCalled();
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1170
|
+
/*
|
|
1171
|
+
* The header says how to zoom in only when there are bars to click or
|
|
1172
|
+
* drag; with none, "Click or drag to zoom" pointed at nothing, and only
|
|
1173
|
+
* the way back applies (commit b6ec899df4).
|
|
1174
|
+
*/
|
|
1175
|
+
describe("its header's zoom hints", () => {
|
|
1176
|
+
const INTERVALS: Array<{
|
|
1177
|
+
name: string;
|
|
1178
|
+
withoutBucketInterval: boolean;
|
|
1179
|
+
zoomInHint: string;
|
|
1180
|
+
}> = [
|
|
1181
|
+
{
|
|
1182
|
+
name: "a bar can be clicked",
|
|
1183
|
+
withoutBucketInterval: false,
|
|
1184
|
+
zoomInHint: "Click or drag to zoom",
|
|
1185
|
+
},
|
|
1186
|
+
{
|
|
1187
|
+
name: "only a drag zooms (no bucket interval)",
|
|
1188
|
+
withoutBucketInterval: true,
|
|
1189
|
+
zoomInHint: "Drag to zoom",
|
|
1190
|
+
},
|
|
1191
|
+
];
|
|
1192
|
+
|
|
1193
|
+
for (const interval of INTERVALS) {
|
|
1194
|
+
test(`where ${interval.name}: zoomed with no bars, loaded or loading, only "Double-click to reset"; the zoom-in hint comes back with the bars`, () => {
|
|
1195
|
+
showHistogram(histogram, {
|
|
1196
|
+
bars: [],
|
|
1197
|
+
isLoading: false,
|
|
1198
|
+
onTimeRangeSelect: getJestMockFunction(),
|
|
1199
|
+
onZoomOut: getJestMockFunction(),
|
|
1200
|
+
withoutBucketInterval: interval.withoutBucketInterval,
|
|
1201
|
+
});
|
|
1202
|
+
|
|
1203
|
+
expect(zoomInHintsShown()).toEqual([]);
|
|
1204
|
+
expect(screen.getByText("Double-click to reset")).toBeInTheDocument();
|
|
1205
|
+
|
|
1206
|
+
// The empty window loads again.
|
|
1207
|
+
rerenderHistogram({ isLoading: true });
|
|
1208
|
+
|
|
1209
|
+
expect(zoomInHintsShown()).toEqual([]);
|
|
1210
|
+
expect(screen.getByText("Double-click to reset")).toBeInTheDocument();
|
|
1211
|
+
|
|
1212
|
+
// Its bars land.
|
|
1213
|
+
rerenderHistogram({ bars: SHOWN_BARS, isLoading: false });
|
|
1214
|
+
|
|
1215
|
+
expect(zoomInHintsShown()).toEqual([interval.zoomInHint]);
|
|
1216
|
+
expect(screen.getByText("Double-click to reset")).toBeInTheDocument();
|
|
1217
|
+
});
|
|
1218
|
+
|
|
1219
|
+
test(`where ${interval.name}: not zoomed, with bars, the zoom-in hint alone`, () => {
|
|
1220
|
+
showHistogram(histogram, {
|
|
1221
|
+
bars: SHOWN_BARS,
|
|
1222
|
+
isLoading: false,
|
|
1223
|
+
onTimeRangeSelect: getJestMockFunction(),
|
|
1224
|
+
withoutBucketInterval: interval.withoutBucketInterval,
|
|
1225
|
+
});
|
|
1226
|
+
|
|
1227
|
+
expect(zoomInHintsShown()).toEqual([interval.zoomInHint]);
|
|
1228
|
+
expect(screen.queryByText("Double-click to reset")).toBeNull();
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
});
|
|
1232
|
+
|
|
1233
|
+
describe("reloading an empty window", () => {
|
|
1234
|
+
/*
|
|
1235
|
+
* Before commit b6ec899df4 a zoomed chart with no bars swapped its
|
|
1236
|
+
* whole card for the bare loader box whenever it loaded: the title
|
|
1237
|
+
* and the way back went, and the box under the pointer changed
|
|
1238
|
+
* height (h-32 for h-[120px]) - just as the reader double-clicked.
|
|
1239
|
+
*/
|
|
1240
|
+
test("zoomed: the card stays - its title and the way back - and the loader comes up in its empty box, which takes the double-click", () => {
|
|
1241
|
+
const onTimeRangeSelect: MockFunction = getJestMockFunction();
|
|
1242
|
+
const onZoomOut: MockFunction = getJestMockFunction();
|
|
1243
|
+
const container: HTMLElement = showHistogram(histogram, {
|
|
1244
|
+
bars: [],
|
|
1245
|
+
isLoading: false,
|
|
1246
|
+
onTimeRangeSelect: onTimeRangeSelect,
|
|
1247
|
+
onZoomOut: onZoomOut,
|
|
1248
|
+
});
|
|
1249
|
+
const card: HTMLElement | null =
|
|
1250
|
+
container.firstElementChild as HTMLElement | null;
|
|
1251
|
+
const title: HTMLElement = screen.getByText(histogram.title);
|
|
1252
|
+
const emptyBox: HTMLElement = screen.getByText(histogram.emptyText);
|
|
1253
|
+
|
|
1254
|
+
// A filter changes, say: the zoomed window loads again.
|
|
1255
|
+
rerenderHistogram({ isLoading: true });
|
|
1256
|
+
|
|
1257
|
+
// The very same card and box, not a bare loader in their place.
|
|
1258
|
+
expect(container.firstElementChild).toBe(card);
|
|
1259
|
+
expect(title.isConnected).toBe(true);
|
|
1260
|
+
expect(screen.getByText("Double-click to reset")).toBeInTheDocument();
|
|
1261
|
+
expect(screen.queryByText(histogram.emptyText)).toBeNull();
|
|
1262
|
+
|
|
1263
|
+
const box: HTMLElement = loaderBox();
|
|
1264
|
+
expect(box).toBe(emptyBox);
|
|
1265
|
+
expect(card).toContainElement(box);
|
|
1266
|
+
expect(box).toHaveClass("h-[120px]", "select-none");
|
|
1267
|
+
expect(box).not.toHaveClass("h-32");
|
|
1268
|
+
|
|
1269
|
+
doubleClickOn(box);
|
|
1270
|
+
expect(onZoomOut).toHaveBeenCalledTimes(1);
|
|
1271
|
+
|
|
1272
|
+
waitOutTheDoubleClickWindow();
|
|
1273
|
+
expect(onZoomOut).toHaveBeenCalledTimes(1);
|
|
1274
|
+
expect(onTimeRangeSelect).not.toHaveBeenCalled();
|
|
1275
|
+
});
|
|
1276
|
+
|
|
1277
|
+
test("not zoomed: loading with no bars is still the bare loader box, with no card around it", () => {
|
|
1278
|
+
const container: HTMLElement = showHistogram(histogram, {
|
|
1279
|
+
bars: [],
|
|
1280
|
+
isLoading: true,
|
|
1281
|
+
onTimeRangeSelect: getJestMockFunction(),
|
|
1282
|
+
});
|
|
1283
|
+
|
|
1284
|
+
const box: HTMLElement = loaderBox();
|
|
1285
|
+
expect(container.firstElementChild).toBe(box);
|
|
1286
|
+
expect(box).toHaveClass("h-32");
|
|
1287
|
+
expect(screen.queryByText(histogram.title)).toBeNull();
|
|
1288
|
+
expect(screen.queryByText("Double-click to reset")).toBeNull();
|
|
1289
|
+
});
|
|
1290
|
+
});
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
/* The log and trace analytics views */
|
|
1295
|
+
|
|
1296
|
+
type WindowName = "wide" | "zoomed";
|
|
1297
|
+
|
|
1298
|
+
// The viewer's window before the zoom (six hours), and the zoomed hour.
|
|
1299
|
+
const WINDOWS: Record<WindowName, { startTime: string; endTime: string }> = {
|
|
1300
|
+
wide: {
|
|
1301
|
+
startTime: "2026-09-28T06:00:00.000Z",
|
|
1302
|
+
endTime: "2026-09-28T12:00:00.000Z",
|
|
1303
|
+
},
|
|
1304
|
+
zoomed: {
|
|
1305
|
+
startTime: "2026-09-28T11:00:00.000Z",
|
|
1306
|
+
endTime: "2026-09-28T12:00:00.000Z",
|
|
1307
|
+
},
|
|
1308
|
+
};
|
|
1309
|
+
|
|
1310
|
+
/*
|
|
1311
|
+
* Each window's props, built once: the views refetch whenever the object
|
|
1312
|
+
* they are handed changes, as a viewer's does when its window does.
|
|
1313
|
+
*/
|
|
1314
|
+
const LOGS_TIME_RANGES: Record<WindowName, RangeStartAndEndDateTime> = {
|
|
1315
|
+
wide: {
|
|
1316
|
+
range: TimeRange.CUSTOM,
|
|
1317
|
+
startAndEndDate: new InBetween<Date>(
|
|
1318
|
+
new Date(WINDOWS.wide.startTime),
|
|
1319
|
+
new Date(WINDOWS.wide.endTime),
|
|
1320
|
+
),
|
|
1321
|
+
},
|
|
1322
|
+
zoomed: {
|
|
1323
|
+
range: TimeRange.CUSTOM,
|
|
1324
|
+
startAndEndDate: new InBetween<Date>(
|
|
1325
|
+
new Date(WINDOWS.zoomed.startTime),
|
|
1326
|
+
new Date(WINDOWS.zoomed.endTime),
|
|
1327
|
+
),
|
|
1328
|
+
},
|
|
1329
|
+
};
|
|
1330
|
+
|
|
1331
|
+
const TRACES_BASE_FILTERS: Record<WindowName, JSONObject> = {
|
|
1332
|
+
wide: { ...WINDOWS.wide },
|
|
1333
|
+
zoomed: { ...WINDOWS.zoomed },
|
|
1334
|
+
};
|
|
1335
|
+
|
|
1336
|
+
const NO_FACET_FILTERS: Map<string, Set<string>> = new Map();
|
|
1337
|
+
// The logs viewer's severity filter, narrowed to errors.
|
|
1338
|
+
const ERRORS_ONLY: Map<string, Set<string>> = new Map([
|
|
1339
|
+
["severityText", new Set(["Error"])],
|
|
1340
|
+
]);
|
|
1341
|
+
const NO_ATTRIBUTES: Array<string> = [];
|
|
1342
|
+
const NO_SERVICE_NAMES: Record<string, string> = {};
|
|
1343
|
+
|
|
1344
|
+
interface HostZoomHandlers {
|
|
1345
|
+
onTimeRangeSelect?: SelectHandler | undefined;
|
|
1346
|
+
onTimeRangeReset?: (() => void) | undefined;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
interface AnalyticsViewUnderTest {
|
|
1350
|
+
name: string;
|
|
1351
|
+
route: string;
|
|
1352
|
+
timeseriesTestId: string;
|
|
1353
|
+
// What the top list and the table show once they land.
|
|
1354
|
+
groupText: string;
|
|
1355
|
+
/*
|
|
1356
|
+
* The view on `window`. `refetched` asks it for its window again without
|
|
1357
|
+
* moving it: the logs viewer's filter changes, the traces explorer's
|
|
1358
|
+
* Refresh button bumps its tick.
|
|
1359
|
+
*/
|
|
1360
|
+
render: (
|
|
1361
|
+
window: WindowName,
|
|
1362
|
+
handlers: HostZoomHandlers,
|
|
1363
|
+
refetched?: boolean,
|
|
1364
|
+
) => React.ReactElement;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
const ANALYTICS_VIEWS: Array<AnalyticsViewUnderTest> = [
|
|
1368
|
+
{
|
|
1369
|
+
name: "LogsAnalyticsView",
|
|
1370
|
+
route: LOGS_ANALYTICS_ROUTE,
|
|
1371
|
+
timeseriesTestId: LOGS_ANALYTICS_TIMESERIES_TEST_ID,
|
|
1372
|
+
groupText: "Error",
|
|
1373
|
+
render: (
|
|
1374
|
+
window: WindowName,
|
|
1375
|
+
handlers: HostZoomHandlers,
|
|
1376
|
+
refetched?: boolean,
|
|
1377
|
+
): React.ReactElement => {
|
|
1378
|
+
return (
|
|
1379
|
+
<LogsAnalyticsView
|
|
1380
|
+
timeRange={LOGS_TIME_RANGES[window]}
|
|
1381
|
+
appliedFacetFilters={refetched ? ERRORS_ONLY : NO_FACET_FILTERS}
|
|
1382
|
+
logAttributes={NO_ATTRIBUTES}
|
|
1383
|
+
onTimeRangeSelect={handlers.onTimeRangeSelect}
|
|
1384
|
+
onTimeRangeReset={handlers.onTimeRangeReset}
|
|
1385
|
+
/>
|
|
1386
|
+
);
|
|
1387
|
+
},
|
|
1388
|
+
},
|
|
1389
|
+
{
|
|
1390
|
+
name: "TracesAnalyticsView",
|
|
1391
|
+
route: TRACES_ANALYTICS_ROUTE,
|
|
1392
|
+
timeseriesTestId: TRACES_ANALYTICS_TIMESERIES_TEST_ID,
|
|
1393
|
+
groupText: "GET /",
|
|
1394
|
+
render: (
|
|
1395
|
+
window: WindowName,
|
|
1396
|
+
handlers: HostZoomHandlers,
|
|
1397
|
+
refetched?: boolean,
|
|
1398
|
+
): React.ReactElement => {
|
|
1399
|
+
return (
|
|
1400
|
+
<TracesAnalyticsView
|
|
1401
|
+
baseFilters={TRACES_BASE_FILTERS[window]}
|
|
1402
|
+
attributeKeys={NO_ATTRIBUTES}
|
|
1403
|
+
serviceNameMap={NO_SERVICE_NAMES}
|
|
1404
|
+
refreshTick={refetched ? 1 : 0}
|
|
1405
|
+
onTimeRangeSelect={handlers.onTimeRangeSelect}
|
|
1406
|
+
onTimeRangeReset={handlers.onTimeRangeReset}
|
|
1407
|
+
/>
|
|
1408
|
+
);
|
|
1409
|
+
},
|
|
1410
|
+
},
|
|
1411
|
+
];
|
|
1412
|
+
|
|
1413
|
+
interface SpyZoom {
|
|
1414
|
+
zoom: TimeRangeZoom;
|
|
1415
|
+
zoomToTimeRange: MockFunction;
|
|
1416
|
+
resetZoom: MockFunction;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
// The zoom a viewer offers the charts inside it, with its two gestures spied on.
|
|
1420
|
+
function spyZoom(isZoomed: boolean): SpyZoom {
|
|
1421
|
+
const zoomToTimeRange: MockFunction = getJestMockFunction();
|
|
1422
|
+
const resetZoom: MockFunction = getJestMockFunction();
|
|
1423
|
+
return {
|
|
1424
|
+
zoomToTimeRange: zoomToTimeRange,
|
|
1425
|
+
resetZoom: resetZoom,
|
|
1426
|
+
zoom: {
|
|
1427
|
+
isZoomed: isZoomed,
|
|
1428
|
+
rangeBeforeZoom: isZoomed ? LOGS_TIME_RANGES.wide : null,
|
|
1429
|
+
zoomToTimeRange: (startTime: Date, endTime: Date): void => {
|
|
1430
|
+
zoomToTimeRange(startTime, endTime);
|
|
1431
|
+
},
|
|
1432
|
+
resetZoom: (): void => {
|
|
1433
|
+
resetZoom();
|
|
1434
|
+
},
|
|
1435
|
+
},
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
function inViewer(
|
|
1440
|
+
zoom: TimeRangeZoom,
|
|
1441
|
+
view: React.ReactElement,
|
|
1442
|
+
): React.ReactElement {
|
|
1443
|
+
return <TimeRangeZoomProvider zoom={zoom}>{view}</TimeRangeZoomProvider>;
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
async function waitForTimeseries(view: AnalyticsViewUnderTest): Promise<void> {
|
|
1447
|
+
await waitFor(() => {
|
|
1448
|
+
expect(
|
|
1449
|
+
chartRootIn(screen.getByTestId(view.timeseriesTestId)),
|
|
1450
|
+
).toBeInTheDocument();
|
|
1451
|
+
});
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
// The reader picks another chart in the view's "Chart" control.
|
|
1455
|
+
function switchChartTo(chartType: string): void {
|
|
1456
|
+
fireEvent.change(screen.getByDisplayValue("Timeseries"), {
|
|
1457
|
+
target: { value: chartType },
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
const NOT_TIME_CHARTS: Array<[string, string]> = [
|
|
1462
|
+
["top list", "toplist"],
|
|
1463
|
+
["table", "table"],
|
|
1464
|
+
];
|
|
1465
|
+
|
|
1466
|
+
for (const view of ANALYTICS_VIEWS) {
|
|
1467
|
+
describe(`${view.name}: the loader standing in for the chart while it refetches`, () => {
|
|
1468
|
+
test("timeseries, zoomed by the viewer around it: a double-click on the loader of the zoom's refetch zooms out once", async () => {
|
|
1469
|
+
const beforeTheZoom: SpyZoom = spyZoom(false);
|
|
1470
|
+
const zoomed: SpyZoom = spyZoom(true);
|
|
1471
|
+
const { rerender } = render(
|
|
1472
|
+
inViewer(beforeTheZoom.zoom, view.render("wide", {})),
|
|
1473
|
+
);
|
|
1474
|
+
await waitForTimeseries(view);
|
|
1475
|
+
|
|
1476
|
+
// The reader zoomed: the viewer is on the zoomed hour, and refetches.
|
|
1477
|
+
holdRequests();
|
|
1478
|
+
rerender(inViewer(zoomed.zoom, view.render("zoomed", {})));
|
|
1479
|
+
expect(heldBodies(view.route)).toEqual([
|
|
1480
|
+
expect.objectContaining({
|
|
1481
|
+
chartType: "timeseries",
|
|
1482
|
+
startTime: WINDOWS.zoomed.startTime,
|
|
1483
|
+
endTime: WINDOWS.zoomed.endTime,
|
|
1484
|
+
}),
|
|
1485
|
+
]);
|
|
1486
|
+
expect(screen.queryByTestId(view.timeseriesTestId)).toBeNull();
|
|
1487
|
+
|
|
1488
|
+
const box: HTMLElement = loaderBox();
|
|
1489
|
+
expect(box).toHaveClass("select-none");
|
|
1490
|
+
doubleClickOn(box);
|
|
1491
|
+
expect(zoomed.resetZoom).toHaveBeenCalledTimes(1);
|
|
1492
|
+
|
|
1493
|
+
waitOutTheDoubleClickWindow();
|
|
1494
|
+
expect(zoomed.resetZoom).toHaveBeenCalledTimes(1);
|
|
1495
|
+
expect(zoomed.zoomToTimeRange).not.toHaveBeenCalled();
|
|
1496
|
+
expect(beforeTheZoom.resetZoom).not.toHaveBeenCalled();
|
|
1497
|
+
expect(beforeTheZoom.zoomToTimeRange).not.toHaveBeenCalled();
|
|
1498
|
+
|
|
1499
|
+
await answerHeldRequests();
|
|
1500
|
+
await waitForTimeseries(view);
|
|
1501
|
+
});
|
|
1502
|
+
|
|
1503
|
+
test("timeseries, zoomed by its host: a double-click on the loader zooms out through the host's reset, once", async () => {
|
|
1504
|
+
const hostSelect: MockFunction = getJestMockFunction();
|
|
1505
|
+
const hostReset: MockFunction = getJestMockFunction();
|
|
1506
|
+
const { rerender } = render(
|
|
1507
|
+
view.render("wide", { onTimeRangeSelect: hostSelect }),
|
|
1508
|
+
);
|
|
1509
|
+
await waitForTimeseries(view);
|
|
1510
|
+
|
|
1511
|
+
holdRequests();
|
|
1512
|
+
rerender(
|
|
1513
|
+
view.render("zoomed", {
|
|
1514
|
+
onTimeRangeSelect: hostSelect,
|
|
1515
|
+
onTimeRangeReset: hostReset,
|
|
1516
|
+
}),
|
|
1517
|
+
);
|
|
1518
|
+
expect(heldBodies(view.route)).toEqual([
|
|
1519
|
+
expect.objectContaining({
|
|
1520
|
+
chartType: "timeseries",
|
|
1521
|
+
startTime: WINDOWS.zoomed.startTime,
|
|
1522
|
+
}),
|
|
1523
|
+
]);
|
|
1524
|
+
|
|
1525
|
+
doubleClickOn(loaderBox());
|
|
1526
|
+
expect(hostReset).toHaveBeenCalledTimes(1);
|
|
1527
|
+
|
|
1528
|
+
waitOutTheDoubleClickWindow();
|
|
1529
|
+
expect(hostReset).toHaveBeenCalledTimes(1);
|
|
1530
|
+
expect(hostSelect).not.toHaveBeenCalled();
|
|
1531
|
+
|
|
1532
|
+
await answerHeldRequests();
|
|
1533
|
+
await waitForTimeseries(view);
|
|
1534
|
+
});
|
|
1535
|
+
|
|
1536
|
+
/*
|
|
1537
|
+
* The loader takes the double-click as the chart does - through the
|
|
1538
|
+
* chart's own selection, which also drops the first click's zoom-in -
|
|
1539
|
+
* not by resetting the zoom behind the selection's back.
|
|
1540
|
+
*/
|
|
1541
|
+
test("timeseries: a click on a bucket still waiting to zoom in when a refetch swaps the chart for the loader - a double-click on the loader drops it, and zooms out once", async () => {
|
|
1542
|
+
const zoomed: SpyZoom = spyZoom(true);
|
|
1543
|
+
const { rerender } = render(
|
|
1544
|
+
inViewer(zoomed.zoom, view.render("zoomed", {})),
|
|
1545
|
+
);
|
|
1546
|
+
await waitForTimeseries(view);
|
|
1547
|
+
|
|
1548
|
+
const plot: HTMLElement = screen.getByTestId(view.timeseriesTestId);
|
|
1549
|
+
expect(plot.querySelector(".recharts-reference-area")).toBeNull();
|
|
1550
|
+
clickChartAt(chartRootIn(plot), spotOnTick(plot, "11:11"));
|
|
1551
|
+
/*
|
|
1552
|
+
* The click waits out the double-click window, its bucket banded. (On
|
|
1553
|
+
* the logs view's area chart a one-bucket band has no width, so
|
|
1554
|
+
* recharts draws the band's layer and no rectangle in it.)
|
|
1555
|
+
*/
|
|
1556
|
+
expect(plot.querySelector(".recharts-reference-area")).not.toBeNull();
|
|
1557
|
+
expect(zoomed.zoomToTimeRange).not.toHaveBeenCalled();
|
|
1558
|
+
|
|
1559
|
+
holdRequests();
|
|
1560
|
+
rerender(inViewer(zoomed.zoom, view.render("zoomed", {}, true)));
|
|
1561
|
+
expect(heldBodies(view.route)).toEqual([
|
|
1562
|
+
expect.objectContaining({
|
|
1563
|
+
chartType: "timeseries",
|
|
1564
|
+
startTime: WINDOWS.zoomed.startTime,
|
|
1565
|
+
}),
|
|
1566
|
+
]);
|
|
1567
|
+
|
|
1568
|
+
doubleClickOn(loaderBox());
|
|
1569
|
+
expect(zoomed.resetZoom).toHaveBeenCalledTimes(1);
|
|
1570
|
+
|
|
1571
|
+
waitOutTheDoubleClickWindow();
|
|
1572
|
+
expect(zoomed.zoomToTimeRange).not.toHaveBeenCalled();
|
|
1573
|
+
expect(zoomed.resetZoom).toHaveBeenCalledTimes(1);
|
|
1574
|
+
|
|
1575
|
+
await answerHeldRequests();
|
|
1576
|
+
await waitForTimeseries(view);
|
|
1577
|
+
});
|
|
1578
|
+
|
|
1579
|
+
/*
|
|
1580
|
+
* The zoom's refetch lands in the middle of the double-click on its
|
|
1581
|
+
* loader: the chart takes the loader's place between the second press
|
|
1582
|
+
* and its release, and no dblclick reaches the chart - the view keeps
|
|
1583
|
+
* the loader's box for the chart, without its handlers. Before commit
|
|
1584
|
+
* b6ec899df4 the loader took only the dblclick, and the view stayed
|
|
1585
|
+
* zoomed.
|
|
1586
|
+
*/
|
|
1587
|
+
for (const ending of LOADER_RACE_ENDINGS) {
|
|
1588
|
+
test(`timeseries, zoomed by the viewer: a double-click on the loader whose second press the chart lands under - ${ending.name} - zooms out once, in the next task`, async () => {
|
|
1589
|
+
const beforeTheZoom: SpyZoom = spyZoom(false);
|
|
1590
|
+
const zoomed: SpyZoom = spyZoom(true);
|
|
1591
|
+
const { rerender } = render(
|
|
1592
|
+
inViewer(beforeTheZoom.zoom, view.render("wide", {})),
|
|
1593
|
+
);
|
|
1594
|
+
await waitForTimeseries(view);
|
|
1595
|
+
|
|
1596
|
+
holdRequests();
|
|
1597
|
+
rerender(inViewer(zoomed.zoom, view.render("zoomed", {})));
|
|
1598
|
+
const pressed: HTMLElement = ending.pressTarget();
|
|
1599
|
+
|
|
1600
|
+
clickThenPressAgain(pressed);
|
|
1601
|
+
await answerHeldRequests();
|
|
1602
|
+
await waitForTimeseries(view);
|
|
1603
|
+
|
|
1604
|
+
const chartArea: HTMLElement = screen.getByTestId(
|
|
1605
|
+
view.timeseriesTestId,
|
|
1606
|
+
);
|
|
1607
|
+
expectChartInTheLoadersPlace(chartArea);
|
|
1608
|
+
expect(zoomed.resetZoom).not.toHaveBeenCalled();
|
|
1609
|
+
|
|
1610
|
+
browserSendsTheClicks(pressed, ending.release(chartArea, 0));
|
|
1611
|
+
|
|
1612
|
+
// No dblclick reached the chart: the reset waits for the next task.
|
|
1613
|
+
expect(zoomed.resetZoom).not.toHaveBeenCalled();
|
|
1614
|
+
|
|
1615
|
+
nextTask();
|
|
1616
|
+
expect(zoomed.resetZoom).toHaveBeenCalledTimes(1);
|
|
1617
|
+
|
|
1618
|
+
waitOutTheDoubleClickWindow();
|
|
1619
|
+
runEveryPendingTimer();
|
|
1620
|
+
expect(zoomed.resetZoom).toHaveBeenCalledTimes(1);
|
|
1621
|
+
expect(zoomed.zoomToTimeRange).not.toHaveBeenCalled();
|
|
1622
|
+
expect(beforeTheZoom.resetZoom).not.toHaveBeenCalled();
|
|
1623
|
+
});
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
for (const [chartName, chartType] of NOT_TIME_CHARTS) {
|
|
1627
|
+
test(`the ${chartName} is no time chart: zoomed by the viewer, a double-click on its loader zooms nothing out`, async () => {
|
|
1628
|
+
const zoomed: SpyZoom = spyZoom(true);
|
|
1629
|
+
render(inViewer(zoomed.zoom, view.render("zoomed", {})));
|
|
1630
|
+
await waitForTimeseries(view);
|
|
1631
|
+
|
|
1632
|
+
holdRequests();
|
|
1633
|
+
switchChartTo(chartType);
|
|
1634
|
+
expect(heldBodies(view.route)).toEqual([
|
|
1635
|
+
expect.objectContaining({ chartType: chartType }),
|
|
1636
|
+
]);
|
|
1637
|
+
|
|
1638
|
+
const box: HTMLElement = loaderBox();
|
|
1639
|
+
expect(
|
|
1640
|
+
errorsThrownBy((): void => {
|
|
1641
|
+
doubleClickOn(box);
|
|
1642
|
+
waitOutTheDoubleClickWindow();
|
|
1643
|
+
}),
|
|
1644
|
+
).toEqual([]);
|
|
1645
|
+
expect(zoomed.resetZoom).not.toHaveBeenCalled();
|
|
1646
|
+
expect(zoomed.zoomToTimeRange).not.toHaveBeenCalled();
|
|
1647
|
+
|
|
1648
|
+
await answerHeldRequests();
|
|
1649
|
+
await waitFor(() => {
|
|
1650
|
+
expect(screen.getByText(view.groupText)).toBeInTheDocument();
|
|
1651
|
+
});
|
|
1652
|
+
});
|
|
1653
|
+
|
|
1654
|
+
/*
|
|
1655
|
+
* A host's handlers reach the view whatever chart it shows (see
|
|
1656
|
+
* resolveChartTimeRangeZoom), so here only the loader itself keeps the
|
|
1657
|
+
* double-click from the host's reset.
|
|
1658
|
+
*/
|
|
1659
|
+
test(`the ${chartName} is no time chart: zoomed by its host, a double-click on its loader does not reach the host's reset`, async () => {
|
|
1660
|
+
const hostSelect: MockFunction = getJestMockFunction();
|
|
1661
|
+
const hostReset: MockFunction = getJestMockFunction();
|
|
1662
|
+
render(
|
|
1663
|
+
view.render("zoomed", {
|
|
1664
|
+
onTimeRangeSelect: hostSelect,
|
|
1665
|
+
onTimeRangeReset: hostReset,
|
|
1666
|
+
}),
|
|
1667
|
+
);
|
|
1668
|
+
await waitForTimeseries(view);
|
|
1669
|
+
|
|
1670
|
+
holdRequests();
|
|
1671
|
+
switchChartTo(chartType);
|
|
1672
|
+
expect(heldBodies(view.route)).toEqual([
|
|
1673
|
+
expect.objectContaining({ chartType: chartType }),
|
|
1674
|
+
]);
|
|
1675
|
+
|
|
1676
|
+
doubleClickOn(loaderBox());
|
|
1677
|
+
waitOutTheDoubleClickWindow();
|
|
1678
|
+
expect(hostReset).not.toHaveBeenCalled();
|
|
1679
|
+
expect(hostSelect).not.toHaveBeenCalled();
|
|
1680
|
+
|
|
1681
|
+
await answerHeldRequests();
|
|
1682
|
+
await waitFor(() => {
|
|
1683
|
+
expect(screen.getByText(view.groupText)).toBeInTheDocument();
|
|
1684
|
+
});
|
|
1685
|
+
});
|
|
1686
|
+
}
|
|
1687
|
+
});
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
/* An exception's Occurrence Trend */
|
|
1691
|
+
|
|
1692
|
+
const FINGERPRINT: string = "9f86d081884c7d659a2feaa0c55ad015";
|
|
1693
|
+
const SERVICE_ID: string = "60000000-0000-4000-8000-000000000001";
|
|
1694
|
+
|
|
1695
|
+
function trendRequests(): Array<Record<string, unknown>> {
|
|
1696
|
+
return requestsTo(EXCEPTIONS_HISTOGRAM_ROUTE);
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
function spanOf(body: Record<string, unknown> | undefined): number {
|
|
1700
|
+
if (!body) {
|
|
1701
|
+
throw new Error("no such request");
|
|
1702
|
+
}
|
|
1703
|
+
return (
|
|
1704
|
+
new Date(String(body["endTime"])).getTime() -
|
|
1705
|
+
new Date(String(body["startTime"])).getTime()
|
|
1706
|
+
);
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
function trendResetButton(): HTMLElement | null {
|
|
1710
|
+
return screen.queryByTestId(RESET_TIME_RANGE_ZOOM_BUTTON_TEST_ID);
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
function renderTrend(): void {
|
|
1714
|
+
render(
|
|
1715
|
+
<ExceptionOccurrenceTrend
|
|
1716
|
+
fingerprint={FINGERPRINT}
|
|
1717
|
+
primaryEntityId={new ObjectID(SERVICE_ID)}
|
|
1718
|
+
/>,
|
|
1719
|
+
);
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
async function waitForTrendBars(): Promise<void> {
|
|
1723
|
+
await waitFor(() => {
|
|
1724
|
+
expect(
|
|
1725
|
+
chartRootIn(screen.getByTestId("exception-trend-plot")),
|
|
1726
|
+
).toBeInTheDocument();
|
|
1727
|
+
});
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
/*
|
|
1731
|
+
* A drag on the real chart, from the bar under one drawn tick label to the
|
|
1732
|
+
* bar under the next: the card zooms into those two bars.
|
|
1733
|
+
*/
|
|
1734
|
+
function dragAcrossTwoBars(): void {
|
|
1735
|
+
const plot: HTMLElement = screen.getByTestId("exception-trend-plot");
|
|
1736
|
+
const chartRoot: Element = chartRootIn(plot);
|
|
1737
|
+
const ticks: Array<DrawnTick> = drawnTicks(plot);
|
|
1738
|
+
expect(ticks.length).toBeGreaterThanOrEqual(2);
|
|
1739
|
+
const from: PointerSpot = { clientX: ticks[0]!.x, clientY: POINTER_Y };
|
|
1740
|
+
const to: PointerSpot = { clientX: ticks[1]!.x, clientY: POINTER_Y };
|
|
1741
|
+
|
|
1742
|
+
restAt(chartRoot, from);
|
|
1743
|
+
press(chartRoot, from, 1);
|
|
1744
|
+
nextFrame();
|
|
1745
|
+
moveHeld(chartRoot, to);
|
|
1746
|
+
nextFrame();
|
|
1747
|
+
release(chartRoot, to, 1);
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
describe("an exception's Occurrence Trend: the loader standing in for its bars", () => {
|
|
1751
|
+
test("after a zoom, a double-click on the loader of the zoomed window goes back to the preset, once", async () => {
|
|
1752
|
+
renderTrend();
|
|
1753
|
+
await waitForTrendBars();
|
|
1754
|
+
expect(trendRequests()).toHaveLength(1);
|
|
1755
|
+
expect(spanOf(trendRequests()[0])).toBe(DAY_MS);
|
|
1756
|
+
|
|
1757
|
+
// The zoom clears the bars and asks for the dragged window.
|
|
1758
|
+
holdRequests();
|
|
1759
|
+
dragAcrossTwoBars();
|
|
1760
|
+
expect(trendRequests()).toHaveLength(2);
|
|
1761
|
+
expect(spanOf(trendRequests()[1])).toBeLessThan(DAY_MS);
|
|
1762
|
+
expect(heldBodies(EXCEPTIONS_HISTOGRAM_ROUTE)).toEqual([
|
|
1763
|
+
trendRequests()[1],
|
|
1764
|
+
]);
|
|
1765
|
+
expect(trendResetButton()).toBeInTheDocument();
|
|
1766
|
+
expect(
|
|
1767
|
+
screen.getByText("Loading occurrences for the selected window…"),
|
|
1768
|
+
).toBeInTheDocument();
|
|
1769
|
+
expect(screen.queryByTestId("exception-trend-plot")).toBeNull();
|
|
1770
|
+
|
|
1771
|
+
const box: HTMLElement = loaderBox();
|
|
1772
|
+
expect(box).toHaveClass("select-none");
|
|
1773
|
+
doubleClickOn(box);
|
|
1774
|
+
|
|
1775
|
+
// Back on the whole preset: one request for the last 24 hours.
|
|
1776
|
+
expect(trendRequests()).toHaveLength(3);
|
|
1777
|
+
expect(spanOf(trendRequests()[2])).toBe(DAY_MS);
|
|
1778
|
+
expect(trendResetButton()).toBeNull();
|
|
1779
|
+
expect(
|
|
1780
|
+
screen.getByText("Loading occurrences for the last 24 hours…"),
|
|
1781
|
+
).toBeInTheDocument();
|
|
1782
|
+
|
|
1783
|
+
waitOutTheDoubleClickWindow();
|
|
1784
|
+
expect(trendRequests()).toHaveLength(3);
|
|
1785
|
+
|
|
1786
|
+
await answerHeldRequests();
|
|
1787
|
+
await waitForTrendBars();
|
|
1788
|
+
expect(
|
|
1789
|
+
screen.getByText(/occurrences in the last 24 hours$/),
|
|
1790
|
+
).toBeInTheDocument();
|
|
1791
|
+
expect(trendRequests()).toHaveLength(3);
|
|
1792
|
+
});
|
|
1793
|
+
|
|
1794
|
+
/*
|
|
1795
|
+
* The zoomed window's bars land in the middle of the double-click on
|
|
1796
|
+
* the loader: they take its place between the second press and its
|
|
1797
|
+
* release, and no dblclick reaches them - the card keeps the loader's
|
|
1798
|
+
* box for them, without its handlers. Before commit b6ec899df4 the
|
|
1799
|
+
* loader took only the dblclick, and the card stayed on the zoomed
|
|
1800
|
+
* window.
|
|
1801
|
+
*/
|
|
1802
|
+
for (const ending of LOADER_RACE_ENDINGS) {
|
|
1803
|
+
test(`after a zoom, a double-click on the loader whose second press the bars land under - ${ending.name} - goes back to the preset once, in the next task`, async () => {
|
|
1804
|
+
renderTrend();
|
|
1805
|
+
await waitForTrendBars();
|
|
1806
|
+
|
|
1807
|
+
holdRequests();
|
|
1808
|
+
dragAcrossTwoBars();
|
|
1809
|
+
expect(trendRequests()).toHaveLength(2);
|
|
1810
|
+
const pressed: HTMLElement = ending.pressTarget();
|
|
1811
|
+
|
|
1812
|
+
clickThenPressAgain(pressed);
|
|
1813
|
+
await answerHeldRequests();
|
|
1814
|
+
await waitForTrendBars();
|
|
1815
|
+
|
|
1816
|
+
const chartArea: HTMLElement = screen.getByTestId("exception-trend-plot");
|
|
1817
|
+
expectChartInTheLoadersPlace(chartArea);
|
|
1818
|
+
expect(trendResetButton()).toBeInTheDocument();
|
|
1819
|
+
|
|
1820
|
+
browserSendsTheClicks(pressed, ending.release(chartArea, 0));
|
|
1821
|
+
|
|
1822
|
+
// No dblclick reached the bars: the reset waits for the next task.
|
|
1823
|
+
expect(trendRequests()).toHaveLength(2);
|
|
1824
|
+
|
|
1825
|
+
nextTask();
|
|
1826
|
+
|
|
1827
|
+
// Back on the whole preset: one request for the last 24 hours.
|
|
1828
|
+
expect(trendRequests()).toHaveLength(3);
|
|
1829
|
+
expect(spanOf(trendRequests()[2])).toBe(DAY_MS);
|
|
1830
|
+
expect(trendResetButton()).toBeNull();
|
|
1831
|
+
|
|
1832
|
+
waitOutTheDoubleClickWindow();
|
|
1833
|
+
runEveryPendingTimer();
|
|
1834
|
+
await waitForTrendBars();
|
|
1835
|
+
expect(trendRequests()).toHaveLength(3);
|
|
1836
|
+
expect(
|
|
1837
|
+
screen.getByText(/occurrences in the last 24 hours$/),
|
|
1838
|
+
).toBeInTheDocument();
|
|
1839
|
+
});
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
test("before any zoom, a double-click on the first load's loader asks for nothing and throws nothing", async () => {
|
|
1843
|
+
holdRequests();
|
|
1844
|
+
renderTrend();
|
|
1845
|
+
expect(trendRequests()).toHaveLength(1);
|
|
1846
|
+
|
|
1847
|
+
const box: HTMLElement = loaderBox();
|
|
1848
|
+
expect(
|
|
1849
|
+
errorsThrownBy((): void => {
|
|
1850
|
+
doubleClickOn(box);
|
|
1851
|
+
waitOutTheDoubleClickWindow();
|
|
1852
|
+
}),
|
|
1853
|
+
).toEqual([]);
|
|
1854
|
+
expect(trendRequests()).toHaveLength(1);
|
|
1855
|
+
expect(trendResetButton()).toBeNull();
|
|
1856
|
+
|
|
1857
|
+
await answerHeldRequests();
|
|
1858
|
+
await waitForTrendBars();
|
|
1859
|
+
expect(trendRequests()).toHaveLength(1);
|
|
1860
|
+
});
|
|
1861
|
+
});
|