@skyhook-io/radar-app 1.16.3 → 1.17.1
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/README.md +1 -1
- package/package.json +3 -3
- package/src/App.tsx +138 -93
- package/src/RadarApp.tsx +27 -17
- package/src/api/actions.errorCode.test.ts +40 -0
- package/src/api/actions.test.ts +48 -0
- package/src/api/actions.ts +79 -0
- package/src/api/apiResources.navigation.test.tsx +232 -0
- package/src/api/apiResources.ts +46 -3
- package/src/api/client.goneResource.test.ts +49 -0
- package/src/api/client.helmSource.test.ts +39 -0
- package/src/api/client.namespaceSwitch.hook.test.tsx +54 -0
- package/src/api/client.namespaceSwitch.test.ts +107 -0
- package/src/api/client.ts +381 -47
- package/src/api/cnpg-ha.live.test.ts +140 -0
- package/src/api/cnpg-ha.test.ts +78 -0
- package/src/api/cnpg-ha.ts +19 -0
- package/src/api/cnpg-history.ts +123 -0
- package/src/api/cnpg-inspect.ts +102 -0
- package/src/api/cnpg-protection.ts +112 -0
- package/src/api/cnpg-recovery.test.tsx +60 -0
- package/src/api/cnpg-recovery.ts +262 -0
- package/src/api/cnpg-sessions.ts +175 -0
- package/src/api/cnpg-storage.ts +138 -0
- package/src/api/cnpg.legacy-reads.test.ts +43 -0
- package/src/api/cnpg.pooler-runtime.test.ts +15 -0
- package/src/api/cnpg.ts +428 -10
- package/src/api/diagnose.test.ts +40 -0
- package/src/api/diagnose.ts +12 -1
- package/src/api/radarFeatures.render.test.tsx +65 -3
- package/src/api/radarFeatures.ts +6 -0
- package/src/api/reviewedActionMutation.test.tsx +164 -0
- package/src/api/reviewedActionMutation.ts +66 -0
- package/src/api/timelineSource.test.ts +9 -0
- package/src/api/timelineSource.ts +8 -0
- package/src/api/traffic.sources.test.ts +55 -0
- package/src/api/traffic.ts +173 -14
- package/src/components/applications/ApplicationsView.tsx +3 -0
- package/src/components/audit/AuditView.tsx +9 -0
- package/src/components/capacity/CapacityActivity.tsx +3 -31
- package/src/components/capacity/CapacityDemand.tsx +4 -36
- package/src/components/capacity/CapacityOverview.tsx +217 -56
- package/src/components/capacity/CapacityPoolDetail.tsx +24 -62
- package/src/components/capacity/CapacityView.test.tsx +354 -1
- package/src/components/capacity/ClusterSchedulingCard.tsx +2 -10
- package/src/components/capacity/certaintyGlyph.test.tsx +2 -9
- package/src/components/capacity/routes.ts +42 -0
- package/src/components/capacity/shared.tsx +23 -150
- package/src/components/capacity/workspaceRoute.ts +18 -0
- package/src/components/cnpg/CNPGActionHost.compatibility.test.tsx +30 -0
- package/src/components/cnpg/CNPGArchivingRepair.tsx +191 -0
- package/src/components/cnpg/CNPGBlockingSessions.test.tsx +50 -0
- package/src/components/cnpg/CNPGBlockingSessions.tsx +333 -0
- package/src/components/cnpg/CNPGClusterActivity.test.tsx +11 -0
- package/src/components/cnpg/CNPGClusterActivity.tsx +18 -9
- package/src/components/cnpg/CNPGClusterLogs.compatibility.test.tsx +130 -0
- package/src/components/cnpg/CNPGClusterLogs.empty.test.tsx +29 -0
- package/src/components/cnpg/CNPGClusterLogs.test.ts +9 -0
- package/src/components/cnpg/CNPGClusterLogs.tsx +81 -24
- package/src/components/cnpg/CNPGClusterTabs.test.tsx +152 -0
- package/src/components/cnpg/CNPGClusterTabs.tsx +187 -0
- package/src/components/cnpg/CNPGClustersKindView.tsx +46 -0
- package/src/components/cnpg/CNPGConfiguration.test.tsx +223 -0
- package/src/components/cnpg/CNPGConfiguration.tsx +190 -0
- package/src/components/cnpg/CNPGCreateClusterDialog.test.tsx +113 -0
- package/src/components/cnpg/CNPGCreateClusterDialog.tsx +74 -0
- package/src/components/cnpg/CNPGDeclarations.test.ts +107 -0
- package/src/components/cnpg/CNPGDeclarations.tsx +236 -78
- package/src/components/cnpg/CNPGDetailPage.test.tsx +62 -0
- package/src/components/cnpg/CNPGDetailPage.tsx +159 -98
- package/src/components/cnpg/CNPGDrawerTrail.tsx +2 -2
- package/src/components/cnpg/CNPGInvestigationAction.test.tsx +30 -0
- package/src/components/cnpg/CNPGOperator.test.tsx +34 -0
- package/src/components/cnpg/CNPGOperator.tsx +78 -30
- package/src/components/cnpg/CNPGOperatorBanner.tsx +39 -0
- package/src/components/cnpg/CNPGOperatorConditions.test.tsx +45 -0
- package/src/components/cnpg/CNPGOperatorConditions.tsx +16 -0
- package/src/components/cnpg/CNPGOperatorConfig.test.tsx +27 -0
- package/src/components/cnpg/CNPGOperatorDiagnosis.tsx +247 -0
- package/src/components/cnpg/CNPGOverview.test.tsx +171 -0
- package/src/components/cnpg/CNPGOverview.tsx +238 -63
- package/src/components/cnpg/CNPGParametersInEffect.test.tsx +51 -0
- package/src/components/cnpg/CNPGParametersInEffect.tsx +120 -0
- package/src/components/cnpg/CNPGPerformance.prerequisite.test.tsx +48 -0
- package/src/components/cnpg/CNPGPerformance.test.tsx +73 -0
- package/src/components/cnpg/CNPGPerformance.tsx +477 -0
- package/src/components/cnpg/CNPGPooling.test.tsx +89 -0
- package/src/components/cnpg/CNPGPooling.tsx +97 -40
- package/src/components/cnpg/CNPGProtection.test.tsx +145 -0
- package/src/components/cnpg/CNPGProtection.tsx +230 -124
- package/src/components/cnpg/CNPGReplicationTab.test.tsx +87 -0
- package/src/components/cnpg/CNPGReplicationTab.tsx +212 -0
- package/src/components/cnpg/CNPGReplicationView.tsx +264 -0
- package/src/components/cnpg/CNPGStorage.test.tsx +140 -0
- package/src/components/cnpg/CNPGStorage.tsx +619 -0
- package/src/components/cnpg/CNPGSummaryHost.overview.test.tsx +44 -0
- package/src/components/cnpg/CNPGSummaryHost.test.tsx +23 -0
- package/src/components/cnpg/CNPGSummaryHost.tsx +228 -54
- package/src/components/cnpg/CNPGTrends.denied.test.tsx +33 -0
- package/src/components/cnpg/CNPGTrends.missing.test.tsx +69 -0
- package/src/components/cnpg/CNPGTrends.test.ts +38 -0
- package/src/components/cnpg/CNPGTrends.tsx +702 -0
- package/src/components/cnpg/CNPGView.tsx +34 -21
- package/src/components/cnpg/CNPGWorkspace.compatibility.test.tsx +90 -0
- package/src/components/cnpg/actions/CNPGClusterActions.dialog.test.tsx +79 -0
- package/src/components/cnpg/actions/CNPGClusterActions.test.tsx +142 -0
- package/src/components/cnpg/actions/CNPGClusterActions.tsx +853 -0
- package/src/components/cnpg/actions/CNPGConnectButton.read.test.tsx +40 -0
- package/src/components/cnpg/actions/CNPGConnectButton.test.ts +25 -0
- package/src/components/cnpg/actions/CNPGConnectButton.tsx +131 -0
- package/src/components/cnpg/actions/CNPGDestroyInstanceDialog.tsx +185 -0
- package/src/components/cnpg/actions/CNPGInstanceActions.test.tsx +38 -0
- package/src/components/cnpg/actions/CNPGInstanceActions.tsx +74 -0
- package/src/components/cnpg/actions/CNPGMaintenanceBanner.tsx +53 -0
- package/src/components/cnpg/actions/CNPGPoolerActions.test.tsx +35 -0
- package/src/components/cnpg/actions/CNPGPoolerActions.tsx +79 -0
- package/src/components/cnpg/actions/CNPGReportDialog.test.tsx +12 -0
- package/src/components/cnpg/actions/CNPGReportDialog.tsx +91 -0
- package/src/components/cnpg/actions/CNPGScheduleActions.run.test.tsx +58 -0
- package/src/components/cnpg/actions/CNPGScheduleActions.test.tsx +54 -0
- package/src/components/cnpg/actions/CNPGScheduleActions.tsx +208 -0
- package/src/components/cnpg/actions/actionModel.test.ts +111 -0
- package/src/components/cnpg/actions/actionModel.ts +127 -0
- package/src/components/cnpg/actions/renderCNPGHeaderActions.tsx +60 -0
- package/src/components/cnpg/actions/useCNPGWriteGuard.tsx +58 -0
- package/src/components/cnpg/actions/useOpenCNPGPsql.ts +22 -0
- package/src/components/cnpg/archivingRepair.test.ts +104 -0
- package/src/components/cnpg/archivingRepair.ts +145 -0
- package/src/components/cnpg/baseBackup.test.ts +36 -0
- package/src/components/cnpg/baseBackup.ts +36 -0
- package/src/components/cnpg/blocking.test.ts +59 -0
- package/src/components/cnpg/blocking.ts +131 -0
- package/src/components/cnpg/coverageText.test.ts +27 -0
- package/src/components/cnpg/creation/CNPGTargetFields.tsx +110 -0
- package/src/components/cnpg/creation/targetModel.test.ts +137 -0
- package/src/components/cnpg/creation/targetModel.ts +149 -0
- package/src/components/cnpg/fleetStatus.test.ts +46 -0
- package/src/components/cnpg/fleetStatus.ts +43 -0
- package/src/components/cnpg/formFields.ts +2 -0
- package/src/components/cnpg/grantText.test.tsx +11 -0
- package/src/components/cnpg/host.test.tsx +24 -0
- package/src/components/cnpg/host.tsx +101 -0
- package/src/components/cnpg/inspectModel.test.ts +120 -0
- package/src/components/cnpg/inspectModel.ts +140 -0
- package/src/components/cnpg/logicalSlots.test.ts +30 -0
- package/src/components/cnpg/logicalSlots.ts +29 -0
- package/src/components/cnpg/lsn.test.ts +40 -0
- package/src/components/cnpg/lsn.ts +43 -0
- package/src/components/cnpg/operations/CNPGFleetOperation.tsx +30 -0
- package/src/components/cnpg/operations/CNPGOperationTracker.test.tsx +247 -0
- package/src/components/cnpg/operations/CNPGOperationTracker.tsx +255 -0
- package/src/components/cnpg/operations/model.test.ts +510 -0
- package/src/components/cnpg/operations/model.ts +499 -0
- package/src/components/cnpg/operations/presentation.test.ts +80 -0
- package/src/components/cnpg/operations/presentation.ts +84 -0
- package/src/components/cnpg/operations/store.ts +98 -0
- package/src/components/cnpg/operatorStatus.test.ts +134 -0
- package/src/components/cnpg/operatorStatus.ts +194 -0
- package/src/components/cnpg/paths.test.ts +54 -0
- package/src/components/cnpg/paths.ts +47 -4
- package/src/components/cnpg/protection/CNPGAttachArchiveDialog.tsx +185 -0
- package/src/components/cnpg/protection/CNPGCreateScheduleDialog.tsx +196 -0
- package/src/components/cnpg/protection/CNPGProtectionSetup.test.tsx +121 -0
- package/src/components/cnpg/protection/CNPGProtectionSetup.tsx +452 -0
- package/src/components/cnpg/protection/CNPGScheduleInput.tsx +115 -0
- package/src/components/cnpg/protection/CNPGScheduleRepairButton.tsx +120 -0
- package/src/components/cnpg/protection/scheduleModel.test.ts +19 -0
- package/src/components/cnpg/protection/scheduleModel.ts +16 -0
- package/src/components/cnpg/recovery/CNPGRecoveryPoint.tsx +220 -0
- package/src/components/cnpg/recovery/CNPGRestoreButton.test.tsx +29 -0
- package/src/components/cnpg/recovery/CNPGRestoreButton.tsx +26 -0
- package/src/components/cnpg/recovery/CNPGRestoreChecks.test.tsx +23 -0
- package/src/components/cnpg/recovery/CNPGRestoreChecks.tsx +153 -0
- package/src/components/cnpg/recovery/CNPGRestoreDialog.back.test.tsx +149 -0
- package/src/components/cnpg/recovery/CNPGRestoreDialog.test.tsx +46 -0
- package/src/components/cnpg/recovery/CNPGRestoreDialog.tsx +333 -0
- package/src/components/cnpg/recovery/CNPGRestoreProgress.tsx +182 -0
- package/src/components/cnpg/recovery/CNPGRestoreValidation.tsx +216 -0
- package/src/components/cnpg/recovery/restoreModel.test.ts +709 -0
- package/src/components/cnpg/recovery/restoreModel.ts +685 -0
- package/src/components/cnpg/recovery/restoreOperation.ts +56 -0
- package/src/components/cnpg/refresh.test.ts +38 -0
- package/src/components/cnpg/refresh.ts +17 -0
- package/src/components/cnpg/refreshNotice.test.tsx +29 -0
- package/src/components/cnpg/routes.test.ts +34 -3
- package/src/components/cnpg/routes.ts +60 -48
- package/src/components/cnpg/runtimeAssessment.ts +198 -0
- package/src/components/cnpg/runtimeModel.test.ts +175 -0
- package/src/components/cnpg/runtimeModel.ts +196 -0
- package/src/components/cnpg/runtimeParts.test.tsx +17 -0
- package/src/components/cnpg/runtimeParts.tsx +79 -0
- package/src/components/cnpg/shared.test.tsx +59 -0
- package/src/components/cnpg/shared.tsx +100 -182
- package/src/components/cnpg/storageModel.test.ts +107 -0
- package/src/components/cnpg/storageModel.ts +101 -0
- package/src/components/cnpg/trendSamples.test.ts +148 -0
- package/src/components/cnpg/trendSamples.ts +222 -0
- package/src/components/cnpg/useCNPGClusterAssessment.ts +30 -0
- package/src/components/cnpg/useCNPGKubectlContext.test.tsx +45 -0
- package/src/components/cnpg/useCNPGKubectlContext.ts +13 -0
- package/src/components/cnpg/useCNPGNavigate.test.tsx +58 -0
- package/src/components/cnpg/useCNPGNavigate.ts +21 -0
- package/src/components/cnpg/useCNPGPoolerLive.ts +46 -0
- package/src/components/cnpg/useCNPGScreenParams.ts +24 -0
- package/src/components/cnpg/useCNPGSidebarWorkspace.ts +66 -16
- package/src/components/cnpg/workspaceRoute.ts +23 -0
- package/src/components/cost/ApplicationCostTab.test.ts +7 -0
- package/src/components/cost/ApplicationCostTab.tsx +7 -7
- package/src/components/cost/CostView.test.ts +2 -1
- package/src/components/cost/CostView.tsx +0 -2
- package/src/components/cost/WorkloadCostTab.tsx +2 -4
- package/src/components/cost/errors.ts +3 -1
- package/src/components/cost/source.test.ts +10 -0
- package/src/components/cost/source.ts +5 -1
- package/src/components/diagnose/AgentSetupNotice.test.tsx +58 -0
- package/src/components/diagnose/AgentSetupNotice.tsx +142 -51
- package/src/components/diagnose/ApplyDialog.ack.test.tsx +47 -0
- package/src/components/diagnose/ApplyDialog.test.tsx +59 -6
- package/src/components/diagnose/ApplyDialog.tsx +26 -28
- package/src/components/diagnose/DiagnoseContext.test.ts +43 -0
- package/src/components/diagnose/DiagnoseContext.tsx +136 -54
- package/src/components/diagnose/DiagnoseSurface.tsx +16 -3
- package/src/components/diagnose/InvestigationView.tsx +39 -2
- package/src/components/diagnose/LocalDiagnoseAction.tsx +40 -20
- package/src/components/diagnose/agentCatalog.ts +2 -2
- package/src/components/diagnose/diagnoseEvidenceTypes.ts +2 -0
- package/src/components/diagnose/investigationEvidence/adapters/issues.ts +8 -0
- package/src/components/diagnose/investigationEvidence/projection.test.ts +16 -0
- package/src/components/dock/BottomDock.tsx +2 -0
- package/src/components/dock/TerminalTab.test.tsx +17 -0
- package/src/components/dock/TerminalTab.tsx +7 -3
- package/src/components/dock/TrafficFlowListTab.tsx +27 -6
- package/src/components/execution/BatchExecutionView.tsx +11 -11
- package/src/components/gitops/ArgoResourceDiffLoader.tsx +4 -2
- package/src/components/gitops/GitOpsView.tsx +16 -23
- package/src/components/helm/ChartBrowser.tsx +3 -3
- package/src/components/helm/HelmCompareRoute.test.tsx +59 -0
- package/src/components/helm/HelmCompareRoute.tsx +57 -26
- package/src/components/helm/HelmReleaseDrawer.test.ts +55 -3
- package/src/components/helm/HelmReleaseDrawer.tsx +50 -12
- package/src/components/helm/RevisionHistory.tsx +3 -2
- package/src/components/helm/TrackChartSourceDialog.render.test.tsx +120 -0
- package/src/components/helm/TrackChartSourceDialog.test.ts +50 -0
- package/src/components/helm/TrackChartSourceDialog.tsx +153 -176
- package/src/components/helm/ValuesViewer.test.tsx +301 -0
- package/src/components/helm/ValuesViewer.tsx +169 -78
- package/src/components/home/ActivitySummary.tsx +2 -1
- package/src/components/home/ClusterHealthCard.tsx +7 -0
- package/src/components/home/HomeView.tsx +20 -9
- package/src/components/home/ProblemsPanel.test.tsx +81 -0
- package/src/components/home/TopologyPreview.tsx +2 -2
- package/src/components/home/mcpToolCatalog.ts +1 -1
- package/src/components/issues/IssuesPane.render.test.tsx +122 -0
- package/src/components/issues/IssuesPane.test.ts +65 -1
- package/src/components/issues/IssuesPane.tsx +188 -25
- package/src/components/issues/uncheckedSources.ts +21 -0
- package/src/components/resource/PVCUsageBar.render.test.tsx +32 -0
- package/src/components/resource/PVCUsageBar.test.tsx +24 -0
- package/src/components/resource/PVCUsageBar.tsx +15 -6
- package/src/components/resource/WorkloadMetricsHelpDialog.test.tsx +3 -1
- package/src/components/resource/WorkloadMetricsHelpDialog.tsx +1 -1
- package/src/components/resources/CompositeRenderer.tsx +4 -4
- package/src/components/resources/ResourceDetailDrawer.tsx +2 -2
- package/src/components/resources/ResourcesView.tsx +26 -12
- package/src/components/resources/renderers/KyvernoPolicyQueued.tsx +2 -1
- package/src/components/resources/renderers/NodeRenderer.tsx +7 -1
- package/src/components/resources/renderers/ServiceRenderer.test.tsx +81 -0
- package/src/components/resources/renderers/ServiceRenderer.tsx +19 -14
- package/src/components/settings/LocalIntegrationStatus.test.tsx +52 -0
- package/src/components/settings/LocalIntegrationStatus.tsx +3 -4
- package/src/components/settings/SettingsDialog.tsx +150 -34
- package/src/components/shared/LargeClusterNamespacePicker.tsx +11 -5
- package/src/components/timeline/TimelineList.tsx +25 -24
- package/src/components/timeline/TimelineLoadError.test.tsx +62 -0
- package/src/components/timeline/TimelineLoadError.tsx +96 -0
- package/src/components/timeline/TimelineRetentionNote.test.tsx +49 -0
- package/src/components/timeline/TimelineRetentionNote.tsx +20 -0
- package/src/components/timeline/TimelineView.tsx +25 -45
- package/src/components/timeline/ViewModeToggle.tsx +20 -0
- package/src/components/timeline/useStickyError.test.tsx +47 -0
- package/src/components/timeline/useStickyError.ts +15 -0
- package/src/components/traffic/TrafficConnectionsTable.tsx +144 -0
- package/src/components/traffic/TrafficFilterSidebar.tsx +22 -1
- package/src/components/traffic/TrafficFlowListContext.tsx +12 -4
- package/src/components/traffic/TrafficFocusSearch.tsx +172 -0
- package/src/components/traffic/TrafficGraph.tsx +109 -26
- package/src/components/traffic/TrafficGraphTooLarge.tsx +150 -0
- package/src/components/traffic/TrafficView.tsx +582 -149
- package/src/components/traffic/trafficFilters.test.ts +398 -1
- package/src/components/traffic/trafficFilters.ts +408 -0
- package/src/components/ui/DiagnosticsOverlay.tsx +34 -2
- package/src/components/ui/Markdown.test.tsx +65 -0
- package/src/components/ui/Markdown.tsx +144 -127
- package/src/components/whats-new/releaseNotes.ts +63 -1
- package/src/components/workload/WorkloadView.test.ts +11 -1
- package/src/components/workload/WorkloadView.tsx +162 -529
- package/src/components/workspace/layout.test.tsx +29 -0
- package/src/components/workspace/layout.tsx +259 -0
- package/src/components/workspace/table.ts +7 -0
- package/src/context/TimelineCustomization.tsx +15 -0
- package/src/contexts/CapabilitiesContext.tsx +1 -1
- package/src/hooks/useGitOpsWriteGuard.test.tsx +457 -0
- package/src/hooks/useGitOpsWriteGuard.ts +193 -0
- package/src/hooks/useResolvedGitOpsOwner.ts +256 -0
- package/src/integrations/resourceHost.test.tsx +55 -0
- package/src/integrations/resourceHost.ts +99 -0
- package/src/integrations/resourceHosts.test.tsx +141 -0
- package/src/integrations/resourceHosts.tsx +119 -0
- package/src/integrations/workspaceRoute.ts +20 -0
- package/src/integrations/workspaceRoutes.test.ts +40 -0
- package/src/integrations/workspaceRoutes.ts +22 -0
- package/src/integrations/workspaceScreens.tsx +14 -0
- package/src/utils/drawer-trail.ts +35 -0
- package/src/utils/kindFilterStore.test.ts +147 -0
- package/src/utils/kindFilterStore.ts +91 -0
- package/src/utils/navigation.test.ts +13 -1
- package/src/utils/navigation.ts +39 -2
- package/src/utils/page-links.ts +21 -0
- package/src/utils/tabStorage.ts +40 -0
- package/src/utils/viewMemory.test.ts +78 -0
- package/src/utils/viewMemory.ts +54 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { useState, type ReactNode } from 'react'
|
|
2
|
+
import { useQueries } from '@tanstack/react-query'
|
|
3
|
+
import { clsx } from 'clsx'
|
|
4
|
+
import { ActionConfirmDialog, PaneLoader, Tooltip, formatAge, toneFillClass, toneTextClass, formatGrant } from '@skyhook-io/k8s-ui'
|
|
5
|
+
import { formatCPUString, formatMemoryString, parseCPUToNanocores, parseMemoryToBytes } from '@skyhook-io/k8s-ui/utils/format'
|
|
6
|
+
import { podMetricsQuery, usePodMetrics } from '../../api/client'
|
|
7
|
+
import { useCNPGAction, useCNPGClusterCapabilities } from '../../api/cnpg'
|
|
8
|
+
import { actionDialogResult } from '../../api/actions'
|
|
9
|
+
import { useCNPGSessions, type CNPGBackend, type CNPGSessionInstance, type CNPGSessionsResponse } from '../../api/cnpg-sessions'
|
|
10
|
+
import { useToast } from '../ui/Toast'
|
|
11
|
+
import { buildBlockingTree, cnpgConnectionFigure, cnpgNoMetricsReadings, countVictims, type BlockingNode } from './blocking'
|
|
12
|
+
import { RefreshFailedNotice } from '../workspace/layout'
|
|
13
|
+
import { Denied } from './runtimeParts'
|
|
14
|
+
|
|
15
|
+
function age(s?: number): string {
|
|
16
|
+
if (s === undefined || s === null) return '—'
|
|
17
|
+
if (s < 1) return `${Math.round(s * 1000)} ms`
|
|
18
|
+
if (s < 90) return `${s.toFixed(0)} s`
|
|
19
|
+
if (s < 5400) return `${Math.round(s / 60)} min`
|
|
20
|
+
return `${(s / 3600).toFixed(1)} h`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Who blocks whom, on one instance, read from pg_stat_activity with
|
|
25
|
+
* pg_blocking_pids over the caller's pods/exec — plus connection headroom and
|
|
26
|
+
* each instance's CPU and memory, so lock waits and resource pressure can be
|
|
27
|
+
* told apart. The instance is chosen by the Sessions section's picker.
|
|
28
|
+
*/
|
|
29
|
+
export function CNPGBlockingSessions({
|
|
30
|
+
namespace,
|
|
31
|
+
cluster,
|
|
32
|
+
pod,
|
|
33
|
+
headroom = true,
|
|
34
|
+
}: {
|
|
35
|
+
namespace: string
|
|
36
|
+
cluster: string
|
|
37
|
+
/** The instance to read; undefined reads the primary. */
|
|
38
|
+
pod?: string
|
|
39
|
+
/** false when the host already shows the connection figure. */
|
|
40
|
+
headroom?: boolean
|
|
41
|
+
}) {
|
|
42
|
+
const q = useCNPGSessions(namespace, cluster, pod)
|
|
43
|
+
const data = q.data
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<section className="overflow-hidden rounded-xl border border-theme-border bg-theme-surface shadow-theme-sm">
|
|
47
|
+
<div className="flex flex-wrap items-center gap-3 border-b border-theme-border px-4 py-2.5">
|
|
48
|
+
<div className="text-sm font-semibold text-theme-text-primary">Blocking{data?.pod ? <> on <span className="font-mono">{data.pod}</span></> : null}</div>
|
|
49
|
+
{data?.capturedAt && <span className="text-xs text-theme-text-tertiary">read {formatAge(data.capturedAt)} ago</span>}
|
|
50
|
+
</div>
|
|
51
|
+
<div className="space-y-4 p-4">
|
|
52
|
+
{!data && q.isLoading && <PaneLoader label="Reading pg_stat_activity…" className="h-20" />}
|
|
53
|
+
{!data && !q.isLoading && <div className="text-sm text-theme-text-tertiary">Sessions could not be read: {q.error instanceof Error ? q.error.message : 'unknown error'}</div>}
|
|
54
|
+
<RefreshFailedNotice queries={[q]} />
|
|
55
|
+
{data && <Body namespace={namespace} cluster={cluster} data={data} headroom={headroom} />}
|
|
56
|
+
</div>
|
|
57
|
+
<div className="border-t border-theme-border px-4 py-2 text-xs text-theme-text-tertiary">
|
|
58
|
+
Source: pg_stat_activity and pg_blocking_pids over pods/exec.
|
|
59
|
+
{data?.state === 'ok' && !!data.sessions?.length && <> Query text is cut at 200 characters.</>}
|
|
60
|
+
</div>
|
|
61
|
+
</section>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function Body({
|
|
66
|
+
namespace,
|
|
67
|
+
cluster,
|
|
68
|
+
data,
|
|
69
|
+
headroom,
|
|
70
|
+
}: {
|
|
71
|
+
namespace: string
|
|
72
|
+
cluster: string
|
|
73
|
+
data: CNPGSessionsResponse
|
|
74
|
+
headroom: boolean
|
|
75
|
+
}) {
|
|
76
|
+
if (data.state === 'denied') return <Denied title="No access to blocking detail" grant={formatGrant(data.permission.grant) ?? 'create pods/exec'}>
|
|
77
|
+
Who blocks whom is read inside PostgreSQL, which needs exec into the instance.
|
|
78
|
+
</Denied>
|
|
79
|
+
if (data.state === 'unavailable') return <div className="text-sm text-theme-text-tertiary">Blocking detail not measured.</div>
|
|
80
|
+
if (data.state !== 'ok') {
|
|
81
|
+
return (
|
|
82
|
+
<>
|
|
83
|
+
<div className="text-sm text-theme-text-tertiary">
|
|
84
|
+
Sessions on {data.pod || 'the primary'} could not be read ({data.state}){data.error ? `: ${data.error}` : ''}.
|
|
85
|
+
</div>
|
|
86
|
+
<Resources namespace={namespace} instances={data.instances} />
|
|
87
|
+
</>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
const sessions = data.sessions ?? []
|
|
91
|
+
const roots = buildBlockingTree(sessions)
|
|
92
|
+
return (
|
|
93
|
+
<>
|
|
94
|
+
{headroom && <Headroom data={data} />}
|
|
95
|
+
{roots.length === 0 ? (
|
|
96
|
+
<div className="text-sm text-theme-text-secondary">No session is waiting on another session’s lock on {data.pod}.</div>
|
|
97
|
+
) : (
|
|
98
|
+
<div className="space-y-2">
|
|
99
|
+
{data.truncated && (
|
|
100
|
+
<div className={clsx('text-xs', toneTextClass('degraded'))}>
|
|
101
|
+
Showing {sessions.length} of {data.involvedTotal} sessions involved in blocking (oldest transactions first).
|
|
102
|
+
</div>
|
|
103
|
+
)}
|
|
104
|
+
{roots.map((r) => (
|
|
105
|
+
<Tree key={`${r.session.pid}`} node={r} depth={0} namespace={namespace} cluster={cluster} data={data} />
|
|
106
|
+
))}
|
|
107
|
+
</div>
|
|
108
|
+
)}
|
|
109
|
+
<Resources namespace={namespace} instances={data.instances} />
|
|
110
|
+
</>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function Headroom({ data }: { data: CNPGSessionsResponse }) {
|
|
115
|
+
const figure = cnpgConnectionFigure(data)
|
|
116
|
+
if (!figure) return null
|
|
117
|
+
return (
|
|
118
|
+
<div>
|
|
119
|
+
<div className="flex flex-wrap items-baseline gap-x-3 text-sm">
|
|
120
|
+
<span className="text-xs text-theme-text-tertiary">Connections</span>
|
|
121
|
+
<span className={clsx('font-mono', figure.tone && toneTextClass(figure.tone))}>{figure.value}</span>
|
|
122
|
+
<Tooltip content={figure.detail}>
|
|
123
|
+
<span className="text-xs text-theme-text-secondary">{figure.limit}</span>
|
|
124
|
+
</Tooltip>
|
|
125
|
+
</div>
|
|
126
|
+
<div className="mt-1 h-1.5 overflow-hidden rounded bg-theme-elevated">
|
|
127
|
+
<div className={clsx('h-full', figure.tone ? toneFillClass(figure.tone) : 'bg-accent')} style={{ width: `${Math.min(100, (figure.ratio ?? 0) * 100)}%` }} />
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function Resources({ namespace, instances }: { namespace: string; instances: CNPGSessionInstance[] }) {
|
|
134
|
+
const metrics = useQueries({ queries: instances.map((i) => podMetricsQuery(namespace, i.pod)) })
|
|
135
|
+
if (instances.length === 0) return null
|
|
136
|
+
// No readings for any Pod is said once rather than on every card; it may be
|
|
137
|
+
// a missing metrics API or a cluster too new to have been sampled.
|
|
138
|
+
if (cnpgNoMetricsReadings(metrics.map((m) => m.data))) {
|
|
139
|
+
return <div><h3 className="mb-1 text-sm font-semibold text-theme-text-primary">Resource usage</h3><div className="text-xs text-theme-text-tertiary">CPU and memory not measured: the metrics API has no readings for these Pods (metrics-server may be missing, or has not sampled them yet).</div></div>
|
|
140
|
+
}
|
|
141
|
+
return (
|
|
142
|
+
<div>
|
|
143
|
+
<h3 className="mb-1 text-sm font-semibold text-theme-text-primary">Resource usage</h3>
|
|
144
|
+
<div className="mb-1 text-xs text-theme-text-tertiary">CPU and memory of each instance’s postgres container (metrics-server)</div>
|
|
145
|
+
<div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
|
|
146
|
+
{instances.map((i) => (
|
|
147
|
+
<InstanceResources key={i.pod} namespace={namespace} inst={i} />
|
|
148
|
+
))}
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function usage(value: number, limit: number | undefined): { pct?: number; tone?: 'degraded' | 'unhealthy' } {
|
|
155
|
+
if (!limit) return {}
|
|
156
|
+
const pct = value / limit
|
|
157
|
+
return { pct, tone: pct >= 0.9 ? 'unhealthy' : pct >= 0.75 ? 'degraded' : undefined }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function InstanceResources({ namespace, inst }: { namespace: string; inst: CNPGSessionInstance }) {
|
|
161
|
+
const m = usePodMetrics(namespace, inst.pod)
|
|
162
|
+
const c = m.data?.containers.find((x) => x.name === 'postgres')
|
|
163
|
+
let body: ReactNode
|
|
164
|
+
if (m.isLoading) body = <span className="text-theme-text-tertiary">reading…</span>
|
|
165
|
+
else if (m.data === null) body = <span className="text-theme-text-tertiary">not measured yet: no metrics for this Pod</span>
|
|
166
|
+
else if (!c) body = <span className="text-theme-text-tertiary">not measured{m.error instanceof Error ? `: ${m.error.message}` : ''}</span>
|
|
167
|
+
else {
|
|
168
|
+
const cpu = usage(parseCPUToNanocores(c.usage.cpu), inst.cpuLimit ? parseCPUToNanocores(inst.cpuLimit) : undefined)
|
|
169
|
+
const mem = usage(parseMemoryToBytes(c.usage.memory), inst.memoryLimit ? parseMemoryToBytes(inst.memoryLimit) : undefined)
|
|
170
|
+
body = (
|
|
171
|
+
<div className="space-y-0.5 font-mono text-xs">
|
|
172
|
+
<div className={cpu.tone && toneTextClass(cpu.tone)}>
|
|
173
|
+
CPU <span className="whitespace-nowrap">{formatCPUString(c.usage.cpu)}</span>
|
|
174
|
+
· {inst.cpuLimit ? <>limit <span className="whitespace-nowrap">{formatCPUString(inst.cpuLimit)}</span></> : 'no limit'}
|
|
175
|
+
</div>
|
|
176
|
+
<div className={mem.tone && toneTextClass(mem.tone)}>
|
|
177
|
+
Memory <span className="whitespace-nowrap">{formatMemoryString(c.usage.memory)}</span>
|
|
178
|
+
· {inst.memoryLimit ? <>limit <span className="whitespace-nowrap">{formatMemoryString(inst.memoryLimit)}</span></> : 'no limit'}
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
return (
|
|
184
|
+
<div className="rounded-lg border border-theme-border bg-theme-base px-3 py-2 text-sm">
|
|
185
|
+
<div className="font-mono text-xs font-semibold">
|
|
186
|
+
{inst.pod} <span className="font-sans font-normal text-theme-text-tertiary">{inst.role}</span>
|
|
187
|
+
</div>
|
|
188
|
+
<div className="mt-0.5">{body}</div>
|
|
189
|
+
</div>
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function Tree({ node, depth, namespace, cluster, data }: { node: BlockingNode; depth: number; namespace: string; cluster: string; data: CNPGSessionsResponse }) {
|
|
194
|
+
return (
|
|
195
|
+
<div className={clsx(depth > 0 && 'ml-5 border-l border-theme-border pl-3')}>
|
|
196
|
+
<SessionRow node={node} depth={depth} namespace={namespace} cluster={cluster} data={data} />
|
|
197
|
+
{node.children.map((c) => (
|
|
198
|
+
<Tree key={`${node.session.pid}-${c.session.pid}`} node={c} depth={depth + 1} namespace={namespace} cluster={cluster} data={data} />
|
|
199
|
+
))}
|
|
200
|
+
</div>
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function SessionRow({ node, depth, namespace, cluster, data }: { node: BlockingNode; depth: number; namespace: string; cluster: string; data: CNPGSessionsResponse }) {
|
|
205
|
+
const s = node.session
|
|
206
|
+
const [signal, setSignal] = useState<'cancelBackend' | 'terminateBackend' | null>(null)
|
|
207
|
+
const victims = countVictims(node)
|
|
208
|
+
const waiting = s.waitEventType === 'Lock'
|
|
209
|
+
return (
|
|
210
|
+
<div className="my-1 rounded-lg border border-theme-border bg-theme-base p-3">
|
|
211
|
+
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-sm">
|
|
212
|
+
<span className="font-mono font-semibold">pid {s.pid}</span>
|
|
213
|
+
{depth === 0 && victims > 0 && <span className={clsx('text-xs font-medium', toneTextClass('unhealthy'))}>blocks {victims}</span>}
|
|
214
|
+
{waiting && <span className={clsx('text-xs', toneTextClass('degraded'))}>waiting on {s.waitEvent ?? 'a lock'}</span>}
|
|
215
|
+
{node.cycle && <span className={clsx('text-xs', toneTextClass('unhealthy'))}>lock cycle</span>}
|
|
216
|
+
{node.alsoWaitsOn.length > 0 && <span className="text-xs text-theme-text-tertiary">also waits on {node.alsoWaitsOn.join(', ')}</span>}
|
|
217
|
+
<span className={clsx('text-xs', s.state?.startsWith('idle in transaction') ? toneTextClass('degraded') : 'text-theme-text-secondary')}>{s.state ?? '—'}</span>
|
|
218
|
+
<span className="ml-auto flex gap-3 text-xs">
|
|
219
|
+
<Tooltip content="pg_cancel_backend: stops the statement it is running now. An idle-in-transaction session is running nothing, so it keeps its locks.">
|
|
220
|
+
<button type="button" className="text-accent-text hover:underline" onClick={() => setSignal('cancelBackend')}>
|
|
221
|
+
Stop query
|
|
222
|
+
</button>
|
|
223
|
+
</Tooltip>
|
|
224
|
+
<Tooltip content="pg_terminate_backend: ends the session and rolls back its open transaction.">
|
|
225
|
+
<button type="button" className="text-accent-text hover:underline" onClick={() => setSignal('terminateBackend')}>
|
|
226
|
+
Terminate…
|
|
227
|
+
</button>
|
|
228
|
+
</Tooltip>
|
|
229
|
+
</span>
|
|
230
|
+
</div>
|
|
231
|
+
<div className="mt-1 flex flex-wrap gap-x-3 text-xs text-theme-text-secondary">
|
|
232
|
+
<span className="font-mono">
|
|
233
|
+
{s.user ?? '—'}@{s.database ?? '—'}
|
|
234
|
+
</span>
|
|
235
|
+
{s.application && <span>{s.application}</span>}
|
|
236
|
+
{s.clientAddr && <span className="font-mono">{s.clientAddr}</span>}
|
|
237
|
+
{s.waitEventType && !waiting && (
|
|
238
|
+
<span>
|
|
239
|
+
wait {s.waitEventType}/{s.waitEvent}
|
|
240
|
+
</span>
|
|
241
|
+
)}
|
|
242
|
+
<span>transaction {age(s.xactAgeSeconds)}</span>
|
|
243
|
+
<span>query {age(s.queryAgeSeconds)}</span>
|
|
244
|
+
<span>connected {age(s.backendAgeSeconds)}</span>
|
|
245
|
+
</div>
|
|
246
|
+
{s.query && (
|
|
247
|
+
<pre className="mt-1.5 whitespace-pre-wrap break-all rounded bg-theme-elevated px-2 py-1 font-mono text-[11.5px] text-theme-text-primary">
|
|
248
|
+
{s.query}
|
|
249
|
+
{s.queryTruncated ? ' …' : ''}
|
|
250
|
+
</pre>
|
|
251
|
+
)}
|
|
252
|
+
{signal && <SignalDialog kind={signal} session={s} namespace={namespace} cluster={cluster} data={data} onClose={() => setSignal(null)} />}
|
|
253
|
+
</div>
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function SignalDialog({
|
|
258
|
+
kind,
|
|
259
|
+
session,
|
|
260
|
+
namespace,
|
|
261
|
+
cluster,
|
|
262
|
+
data,
|
|
263
|
+
onClose,
|
|
264
|
+
}: {
|
|
265
|
+
kind: 'cancelBackend' | 'terminateBackend'
|
|
266
|
+
session: CNPGBackend
|
|
267
|
+
namespace: string
|
|
268
|
+
cluster: string
|
|
269
|
+
data: CNPGSessionsResponse
|
|
270
|
+
onClose: () => void
|
|
271
|
+
}) {
|
|
272
|
+
const caps = useCNPGClusterCapabilities(namespace, cluster)
|
|
273
|
+
const mutation = useCNPGAction('clusters', namespace, cluster, caps.data?.context)
|
|
274
|
+
const { showSuccess } = useToast()
|
|
275
|
+
const terminate = kind === 'terminateBackend'
|
|
276
|
+
const fn = terminate ? 'pg_terminate_backend' : 'pg_cancel_backend'
|
|
277
|
+
const idle = session.state?.startsWith('idle in transaction')
|
|
278
|
+
const ctx = caps.data?.context
|
|
279
|
+
return (
|
|
280
|
+
<ActionConfirmDialog
|
|
281
|
+
open
|
|
282
|
+
onClose={onClose}
|
|
283
|
+
onConfirm={() => {
|
|
284
|
+
if (!caps.data) return
|
|
285
|
+
mutation.mutate(
|
|
286
|
+
{
|
|
287
|
+
action: kind,
|
|
288
|
+
request: {
|
|
289
|
+
reviewedContext: caps.data.context,
|
|
290
|
+
uid: caps.data.uid,
|
|
291
|
+
facts: {},
|
|
292
|
+
params: { pod: data.pod, podUID: data.podUID, pid: session.pid, backendStart: session.backendStart },
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
onSuccess: (r) => {
|
|
297
|
+
showSuccess(r.message)
|
|
298
|
+
onClose()
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
)
|
|
302
|
+
}}
|
|
303
|
+
title={terminate ? `Terminate backend ${session.pid}?` : `Cancel the query of backend ${session.pid}?`}
|
|
304
|
+
subject={{ kind: 'Cluster', namespace, name: cluster }}
|
|
305
|
+
context={ctx}
|
|
306
|
+
effect={
|
|
307
|
+
terminate
|
|
308
|
+
? `Ends the session of ${session.user ?? 'this user'} on ${data.pod}. Its open transaction is rolled back — work it did since the transaction began is lost — and its locks are released. The client sees its connection close.`
|
|
309
|
+
: `Stops the statement backend ${session.pid} is running on ${data.pod}. The client gets an error for that statement; the session and its transaction stay open.`
|
|
310
|
+
}
|
|
311
|
+
warnings={[
|
|
312
|
+
!terminate && idle ? 'This session is idle in a transaction: it is running no statement, so cancelling does nothing and it keeps its locks. Terminate it to release them.' : null,
|
|
313
|
+
terminate && session.xactAgeSeconds !== undefined ? `The transaction has been open for ${age(session.xactAgeSeconds)}.` : null,
|
|
314
|
+
].filter(Boolean) as string[]}
|
|
315
|
+
notes={[
|
|
316
|
+
`Sent only if backend ${session.pid} still has the start time you reviewed (${session.backendStart}); a reused pid is a different session and is left alone.`,
|
|
317
|
+
]}
|
|
318
|
+
writes={[
|
|
319
|
+
{
|
|
320
|
+
summary: `exec psql in Pod ${namespace}/${data.pod} (container postgres)`,
|
|
321
|
+
detail: `SELECT ${fn}(pid) FROM pg_stat_activity\nWHERE pid = ${session.pid} AND backend_start = '${session.backendStart}'\n AND backend_type = 'client backend'`,
|
|
322
|
+
},
|
|
323
|
+
]}
|
|
324
|
+
typedConfirmation={terminate ? String(session.pid) : undefined}
|
|
325
|
+
confirmLabel={terminate ? 'Terminate backend' : 'Stop query'}
|
|
326
|
+
disruptive={terminate}
|
|
327
|
+
disabledReason={caps.data && data.permission.exec === 'denied' ? `Needs ${formatGrant(data.permission.grant) ?? 'create pods/exec'}` : undefined}
|
|
328
|
+
incompleteReason={!caps.data ? 'Reading the cluster…' : undefined}
|
|
329
|
+
isLoading={mutation.isPending}
|
|
330
|
+
result={actionDialogResult(mutation.outcomeError ?? mutation.error, { unknown: "An earlier write to this Cluster may have taken effect" })}
|
|
331
|
+
/>
|
|
332
|
+
)
|
|
333
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
2
|
+
import { expect, it, vi } from 'vitest'
|
|
3
|
+
import { CNPGClusterActivity } from './CNPGClusterActivity'
|
|
4
|
+
vi.mock('../../api/cnpg', () => ({ useCNPGClusterActivity: () => ({ data: { events: [], oldest: null, attributionSince: null }, isRefetchError: true, error: new Error('Activity timeout'), dataUpdatedAt: Date.now() - 180_000 }) }))
|
|
5
|
+
vi.mock('../../api/cnpg-history', () => ({ useCNPGClusterActivityWindow: () => ({}) }))
|
|
6
|
+
vi.mock('./CNPGTrends', () => ({ useCNPGIntervalParams: () => null }))
|
|
7
|
+
it('labels retained activity after a failed refresh', () => {
|
|
8
|
+
const html = renderToStaticMarkup(<CNPGClusterActivity namespace="db" name="pg" />)
|
|
9
|
+
expect(html).toContain('Last refresh failed: Activity timeout')
|
|
10
|
+
expect(html).toContain('3m ago')
|
|
11
|
+
})
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { useState } from 'react'
|
|
2
2
|
import { PaneLoader, TimelineList, formatAge, type NavigateToResource } from '@skyhook-io/k8s-ui'
|
|
3
3
|
import { useCNPGClusterActivity } from '../../api/cnpg'
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { useCNPGClusterActivityWindow } from '../../api/cnpg-history'
|
|
5
|
+
import { CNPGIntervalBanner, useCNPGIntervalParams } from './CNPGTrends'
|
|
6
|
+
import { Notice, RefreshFailedNotice, Segments } from '../workspace/layout'
|
|
6
7
|
|
|
7
8
|
const RANGES = [
|
|
8
9
|
{ id: '6', label: '6 h' },
|
|
@@ -18,22 +19,30 @@ const RANGES = [
|
|
|
18
19
|
*/
|
|
19
20
|
export function CNPGClusterActivity({ namespace, name, onNavigate }: { namespace: string; name: string; onNavigate?: NavigateToResource }) {
|
|
20
21
|
const [hours, setHours] = useState<string>('24')
|
|
21
|
-
const
|
|
22
|
+
const interval = useCNPGIntervalParams()
|
|
23
|
+
const ranged = useCNPGClusterActivity(namespace, name, Number(hours))
|
|
24
|
+
const windowed = useCNPGClusterActivityWindow(namespace, name, interval?.since ?? '', interval?.until ?? '', !!interval)
|
|
25
|
+
const q = interval ? windowed : ranged
|
|
22
26
|
|
|
23
27
|
return (
|
|
24
28
|
<div className="flex h-full min-h-0 flex-col gap-3 p-4">
|
|
25
|
-
|
|
26
|
-
<
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
{interval ? (
|
|
30
|
+
<CNPGIntervalBanner since={interval.since} until={interval.until} note="Selected on a History chart: events and changes inside this interval only." onClear={interval.clear} />
|
|
31
|
+
) : (
|
|
32
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
33
|
+
<Segments label="Range" value={hours} onChange={setHours} options={RANGES.map((r) => ({ id: r.id, label: r.label }))} />
|
|
34
|
+
<span className="text-xs text-theme-text-tertiary">
|
|
35
|
+
Kubernetes events and changes for the Cluster, its instances, Backups, Poolers and declarations.
|
|
36
|
+
</span>
|
|
37
|
+
</div>
|
|
38
|
+
)}
|
|
31
39
|
<div className="text-xs text-theme-text-tertiary">
|
|
32
40
|
{q.data?.attributionSince
|
|
33
41
|
? `The earliest recorded event linking a Backup, Pooler or declaration to this cluster is ${formatAge(q.data.attributionSince)} old. Deleted child objects from before Radar recorded that link are not shown.`
|
|
34
42
|
: 'Radar has not recorded any Backup, Pooler or declaration events linked to this cluster yet, so deleted child objects may be missing.'}
|
|
35
43
|
{' '}Events for kinds you cannot list are omitted.
|
|
36
44
|
</div>
|
|
45
|
+
<RefreshFailedNotice queries={[q]} />
|
|
37
46
|
{q.data?.truncated && <Notice>Showing the most recent events only; narrow the range to see all of them.</Notice>}
|
|
38
47
|
{q.error && !q.data ? (
|
|
39
48
|
<Notice>Activity could not be loaded: {q.error instanceof Error ? q.error.message : 'unknown error'}</Notice>
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
2
|
+
import { MemoryRouter } from 'react-router-dom'
|
|
3
|
+
import { beforeEach, expect, it, vi } from 'vitest'
|
|
4
|
+
import { CNPGClusterLogs } from './CNPGClusterLogs'
|
|
5
|
+
import { guardRadarFeature, radarFeatureSupport } from '../../api/radarFeatures'
|
|
6
|
+
import { fetchJSON } from '../../api/client'
|
|
7
|
+
|
|
8
|
+
const state = vi.hoisted(() => ({ props: undefined as any, capabilities: { features: { resourceIssues: true } } as any, capabilitiesPending: false, fetchStatus: 'idle' as 'idle' | 'fetching' | 'paused', interval: null as null | { since: string; until: string; clear: () => void } }))
|
|
9
|
+
vi.mock('@skyhook-io/k8s-ui', async (original) => ({ ...await original<typeof import('@skyhook-io/k8s-ui')>(), WorkloadLogsViewer: (props: any) => { state.props = props; return <div>logs</div> } }))
|
|
10
|
+
vi.mock('../../api/client', () => ({ fetchJSON: vi.fn(async () => ({ pods: [], logs: [] })), useCapabilities: () => ({ isPending: state.capabilitiesPending, isFetching: state.fetchStatus === 'fetching', fetchStatus: state.fetchStatus, refetch: vi.fn() }), useRadarFeature: () => {
|
|
11
|
+
const support = radarFeatureSupport('cnpgWorkspace', state.capabilities, 'v1.16.0')
|
|
12
|
+
return { support, guard: (fn: () => Promise<unknown>) => guardRadarFeature('cnpgWorkspace', support, {}, fn) }
|
|
13
|
+
} }))
|
|
14
|
+
vi.mock('../../hooks/useDesktopDownload', () => ({ useDesktopDownload: () => undefined }))
|
|
15
|
+
vi.mock('../../context/ThemeContext', () => ({ useTheme: () => ({ theme: 'light' }) }))
|
|
16
|
+
vi.mock('./useCNPGNavigate', () => ({ useCNPGNavigate: () => vi.fn() }))
|
|
17
|
+
vi.mock('./useCNPGSidebarWorkspace', () => ({ useCNPGFleet: () => ({ fleet: null }) }))
|
|
18
|
+
vi.mock('./CNPGTrends', () => ({ useCNPGIntervalParams: () => state.interval, CNPGIntervalBanner: ({ note }: { note: string }) => <div>{note}</div> }))
|
|
19
|
+
beforeEach(() => { state.props = undefined; state.capabilities = { features: { resourceIssues: true } }; state.capabilitiesPending = false; state.fetchStatus = 'idle'; state.interval = null; vi.clearAllMocks(); vi.unstubAllGlobals() })
|
|
20
|
+
function mount(search = '') { return renderToStaticMarkup(<MemoryRouter initialEntries={[{ pathname: '/cnpg/clusters/db/pg', search }]}><CNPGClusterLogs namespace="db" name="pg" /></MemoryRouter>) }
|
|
21
|
+
|
|
22
|
+
it('retains ordinary snapshot selectors and follows on the actual missing-flag capability state', async () => {
|
|
23
|
+
mount()
|
|
24
|
+
await state.props.fetchAll({ container: 'postgres', tailLines: 40, sinceSeconds: 60 })
|
|
25
|
+
expect(fetchJSON).toHaveBeenCalledWith('/cnpg/clusters/db/pg/logs?container=postgres&tailLines=40&sinceSeconds=60', { signal: undefined })
|
|
26
|
+
const eventSource = vi.fn(function (url: string) { return { url } })
|
|
27
|
+
vi.stubGlobal('EventSource', eventSource)
|
|
28
|
+
state.props.createStream({ container: 'postgres' })
|
|
29
|
+
expect(eventSource).toHaveBeenCalledWith('/api/cnpg/clusters/db/pg/logs/stream?container=postgres&tailLines=50', { withCredentials: false })
|
|
30
|
+
})
|
|
31
|
+
it('uses the old postgres default for empty and all selectors in both snapshot and SSE requests', async () => {
|
|
32
|
+
mount()
|
|
33
|
+
expect(state.props.initialContainer).toBe('postgres')
|
|
34
|
+
expect(state.props.allowAllContainers).toBe(false)
|
|
35
|
+
mount('?container=all')
|
|
36
|
+
expect(state.props.initialContainer).toBe('postgres')
|
|
37
|
+
expect(state.props.allowAllContainers).toBe(false)
|
|
38
|
+
const eventSource = vi.fn(function (url: string) { return { url } })
|
|
39
|
+
vi.stubGlobal('EventSource', eventSource)
|
|
40
|
+
for (const container of [undefined, 'all']) {
|
|
41
|
+
await state.props.fetchAll({ container, tailLines: 40, sinceSeconds: 60 })
|
|
42
|
+
const path = vi.mocked(fetchJSON).mock.lastCall![0]
|
|
43
|
+
expect(Object.fromEntries(new URL(path, 'http://radar.test').searchParams)).toEqual({ tailLines: '40', sinceSeconds: '60' })
|
|
44
|
+
state.props.createStream({ container, sinceSeconds: 60 })
|
|
45
|
+
const streamPath = eventSource.mock.lastCall![0]
|
|
46
|
+
expect(Object.fromEntries(new URL(streamPath, 'http://radar.test').searchParams)).toEqual({ tailLines: '50', sinceSeconds: '60' })
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
it('retains exact plugin-container selection supported by the old producer', async () => {
|
|
50
|
+
mount('?container=plugin-barman-cloud')
|
|
51
|
+
expect(state.props.initialContainer).toBe('plugin-barman-cloud')
|
|
52
|
+
await state.props.fetchAll({ container: 'plugin-barman-cloud' })
|
|
53
|
+
expect(fetchJSON).toHaveBeenCalledWith('/cnpg/clusters/db/pg/logs?container=plugin-barman-cloud', { signal: undefined })
|
|
54
|
+
const eventSource = vi.fn(function (url: string) { return { url } })
|
|
55
|
+
vi.stubGlobal('EventSource', eventSource)
|
|
56
|
+
state.props.createStream({ container: 'plugin-barman-cloud' })
|
|
57
|
+
expect(eventSource).toHaveBeenCalledWith('/api/cnpg/clusters/db/pg/logs/stream?container=plugin-barman-cloud&tailLines=50', { withCredentials: false })
|
|
58
|
+
})
|
|
59
|
+
it('only requests all-container snapshots and SSE after confirmed support', async () => {
|
|
60
|
+
const eventSource = vi.fn(function (url: string) { return { url } })
|
|
61
|
+
vi.stubGlobal('EventSource', eventSource)
|
|
62
|
+
state.capabilities = undefined
|
|
63
|
+
mount()
|
|
64
|
+
expect(state.props.initialContainer).toBe('postgres')
|
|
65
|
+
expect(state.props.allowAllContainers).toBe(false)
|
|
66
|
+
await state.props.fetchAll({})
|
|
67
|
+
expect(fetchJSON).toHaveBeenLastCalledWith('/cnpg/clusters/db/pg/logs', { signal: undefined })
|
|
68
|
+
state.props.createStream({})
|
|
69
|
+
expect(eventSource).toHaveBeenLastCalledWith('/api/cnpg/clusters/db/pg/logs/stream?tailLines=50', { withCredentials: false })
|
|
70
|
+
state.capabilities = { features: { cnpgWorkspace: true } }
|
|
71
|
+
mount()
|
|
72
|
+
expect(state.props.initialContainer).toBeUndefined()
|
|
73
|
+
expect(state.props.allowAllContainers).toBe(true)
|
|
74
|
+
mount('?container=all')
|
|
75
|
+
expect(state.props.initialContainer).toBeUndefined()
|
|
76
|
+
await state.props.fetchAll({})
|
|
77
|
+
expect(fetchJSON).toHaveBeenLastCalledWith('/cnpg/clusters/db/pg/logs?container=all', { signal: undefined })
|
|
78
|
+
state.props.createStream({})
|
|
79
|
+
expect(eventSource).toHaveBeenLastCalledWith('/api/cnpg/clusters/db/pg/logs/stream?container=all&tailLines=50', { withCredentials: false })
|
|
80
|
+
})
|
|
81
|
+
it('refuses both-bound interval semantics on that old server without an unbounded request', async () => {
|
|
82
|
+
state.interval = { since: '2026-10-13T00:00:00Z', until: '2026-10-13T01:00:00Z', clear: vi.fn() }
|
|
83
|
+
expect(mount()).toContain('Clear it to read ordinary logs')
|
|
84
|
+
await expect(state.props.fetchAll({ tailLines: 40 })).rejects.toMatchObject({ feature: 'cnpgWorkspace' })
|
|
85
|
+
expect(fetchJSON).not.toHaveBeenCalled()
|
|
86
|
+
expect(state.props.createStream).toBeUndefined()
|
|
87
|
+
})
|
|
88
|
+
it('uses the advertised interval contract without tailing or streaming', async () => {
|
|
89
|
+
state.capabilities = { features: { cnpgWorkspace: true } }
|
|
90
|
+
state.interval = { since: '2026-10-13T00:00:00Z', until: '2026-10-13T01:00:00Z', clear: vi.fn() }
|
|
91
|
+
mount()
|
|
92
|
+
await state.props.fetchAll({ tailLines: 40, sinceSeconds: 60 })
|
|
93
|
+
const path = vi.mocked(fetchJSON).mock.calls[0][0]
|
|
94
|
+
const params = new URL(path, 'http://radar.test').searchParams
|
|
95
|
+
expect(Object.fromEntries(params)).toEqual({ sinceTime: state.interval.since, untilTime: state.interval.until, container: 'all' })
|
|
96
|
+
expect(state.props.createStream).toBeUndefined()
|
|
97
|
+
})
|
|
98
|
+
it('waits for capabilities before offering interval semantics, while ordinary logs remain immediate', () => {
|
|
99
|
+
state.capabilities = undefined
|
|
100
|
+
state.capabilitiesPending = true
|
|
101
|
+
state.interval = { since: '2026-10-13T00:00:00Z', until: '2026-10-13T01:00:00Z', clear: vi.fn() }
|
|
102
|
+
expect(mount()).toContain('Checking interval support')
|
|
103
|
+
expect(state.props).toBeUndefined()
|
|
104
|
+
state.interval = null
|
|
105
|
+
mount()
|
|
106
|
+
expect(state.props.createStream).toBeTypeOf('function')
|
|
107
|
+
})
|
|
108
|
+
it('settles a failed capabilities read without loading forever or issuing an interval request', () => {
|
|
109
|
+
state.capabilities = undefined
|
|
110
|
+
state.interval = { since: '2026-10-13T00:00:00Z', until: '2026-10-13T01:00:00Z', clear: vi.fn() }
|
|
111
|
+
const html = mount()
|
|
112
|
+
expect(html).toContain('Interval log support could not be checked')
|
|
113
|
+
expect(html).toContain('Clear this interval to read ordinary logs')
|
|
114
|
+
expect(html).toContain('Retry')
|
|
115
|
+
expect(html).not.toContain('Checking interval support')
|
|
116
|
+
expect(state.props).toBeUndefined()
|
|
117
|
+
expect(fetchJSON).not.toHaveBeenCalled()
|
|
118
|
+
})
|
|
119
|
+
it('keeps the clear-interval fallback available while an offline request is paused', () => {
|
|
120
|
+
state.capabilities = undefined
|
|
121
|
+
state.capabilitiesPending = true
|
|
122
|
+
state.fetchStatus = 'paused'
|
|
123
|
+
state.interval = { since: '2026-10-13T00:00:00Z', until: '2026-10-13T01:00:00Z', clear: vi.fn() }
|
|
124
|
+
const html = mount()
|
|
125
|
+
expect(html).toContain('Clear this interval to read ordinary logs')
|
|
126
|
+
expect(html).toContain('Waiting for connection')
|
|
127
|
+
expect(html).not.toContain('Checking interval support')
|
|
128
|
+
expect(state.props).toBeUndefined()
|
|
129
|
+
expect(fetchJSON).not.toHaveBeenCalled()
|
|
130
|
+
})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
2
|
+
import { MemoryRouter } from 'react-router-dom'
|
|
3
|
+
import { expect, it, vi } from 'vitest'
|
|
4
|
+
import { CNPGClusterLogs } from './CNPGClusterLogs'
|
|
5
|
+
const state = vi.hoisted(() => ({ props: {} as any, navigate: vi.fn(), problems: [{ job: 'initdb', severity: 'critical', title: 'First instance cannot be scheduled' }] as any[] }))
|
|
6
|
+
vi.mock('@skyhook-io/k8s-ui', async (original) => ({ ...await original<typeof import('@skyhook-io/k8s-ui')>(), WorkloadLogsViewer: (props: any) => { state.props = props; return <div>{props.emptySourceState}</div> } }))
|
|
7
|
+
vi.mock('../../api/client', () => ({ useCapabilities: () => ({ isPending: false }), useRadarFeature: () => ({ guard: (fn: () => unknown) => fn(), support: 'supported' }), fetchJSON: vi.fn() }))
|
|
8
|
+
vi.mock('../../hooks/useDesktopDownload', () => ({ useDesktopDownload: () => undefined }))
|
|
9
|
+
vi.mock('../../context/ThemeContext', () => ({ useTheme: () => ({ theme: 'light' }) }))
|
|
10
|
+
vi.mock('./useCNPGNavigate', () => ({ useCNPGNavigate: () => state.navigate }))
|
|
11
|
+
vi.mock('./useCNPGSidebarWorkspace', () => ({ useCNPGFleet: () => ({ fleet: { rows: [{ namespace: 'db', name: 'analytics', problems: state.problems }] } }) }))
|
|
12
|
+
vi.mock('./CNPGTrends', () => ({ useCNPGIntervalParams: () => null }))
|
|
13
|
+
it('supplies the first-instance problem and Overview link from the CNPG host', () => {
|
|
14
|
+
const html = renderToStaticMarkup(<MemoryRouter initialEntries={[{ pathname: '/cnpg/clusters/db/analytics', search: '?ctx=test&tab=logs&drawer=x', state: { returnLabel: 'Clusters' } }]}><CNPGClusterLogs namespace="db" name="analytics" /></MemoryRouter>)
|
|
15
|
+
expect(html).toContain('First instance cannot be scheduled')
|
|
16
|
+
expect(html).toContain('See Overview’s problem')
|
|
17
|
+
state.props.emptySourceState.props.children[1].props.onClick()
|
|
18
|
+
expect(state.navigate).toHaveBeenCalledWith('/cnpg/clusters/db/analytics?ctx=test&tab=overview&drawer=x&problems=all', { replace: true, state: { returnLabel: 'Clusters' } })
|
|
19
|
+
expect(state.props.disableSourceControlsWithoutSource).toBe(true)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('does not invent a first-instance cause or use an unrelated problem', () => {
|
|
23
|
+
state.problems = [{ severity: 'critical', title: 'Backup failed' }]
|
|
24
|
+
const html = renderToStaticMarkup(<MemoryRouter><CNPGClusterLogs namespace="db" name="analytics" /></MemoryRouter>)
|
|
25
|
+
expect(html).toContain('No instance log source yet')
|
|
26
|
+
expect(html).toContain('Open Overview')
|
|
27
|
+
expect(html).not.toContain('first instance has not started')
|
|
28
|
+
expect(html).not.toContain('Backup failed')
|
|
29
|
+
})
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { expect, it } from 'vitest'
|
|
2
|
+
import { query, intervalQuery } from './CNPGClusterLogs'
|
|
3
|
+
it('requests all containers explicitly for snapshots, streams and bounded intervals', () => {
|
|
4
|
+
expect(new URLSearchParams(query({}).slice(1)).get('container')).toBe('all')
|
|
5
|
+
expect(new URLSearchParams(query({}, 50).slice(1)).get('container')).toBe('all')
|
|
6
|
+
expect(new URLSearchParams(intervalQuery({}, 'from', 'to').slice(1)).get('container')).toBe('all')
|
|
7
|
+
expect(new URLSearchParams(query({ container: 'plugin-barman-cloud' }).slice(1)).get('container')).toBe('plugin-barman-cloud')
|
|
8
|
+
expect(new URLSearchParams(intervalQuery({ container: 'postgres' }, 'from', 'to').slice(1)).get('container')).toBe('postgres')
|
|
9
|
+
})
|