@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,140 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import type { CNPGFleetRow } from '@skyhook-io/k8s-ui'
|
|
3
|
+
import type { CNPGRuntimeResponse } from './cnpg'
|
|
4
|
+
import { cnpgInstanceLive, cnpgLiveStandbyGaps, cnpgReplicationLive, withLiveReplication } from '../components/cnpg/runtimeAssessment'
|
|
5
|
+
|
|
6
|
+
const rt = (status: any): CNPGRuntimeResponse => ({
|
|
7
|
+
cluster: { namespace: 'db', name: 'pg', uid: 'u' },
|
|
8
|
+
sampledAt: '2026-09-30T10:00:00Z',
|
|
9
|
+
permission: { proxy: 'allowed' },
|
|
10
|
+
instances: [
|
|
11
|
+
{ pod: 'pg-1', role: 'primary', status, metrics: { state: 'ok' } },
|
|
12
|
+
{ pod: 'pg-2', role: 'replica', status: { state: 'ok', replication: [] }, metrics: { state: 'ok' } },
|
|
13
|
+
],
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
describe('live replication from an incomplete primary report', () => {
|
|
17
|
+
it('is unknown, never "0 streaming"', () => {
|
|
18
|
+
expect(cnpgReplicationLive(rt({ state: 'partial', incomplete: true, replication: null }))).toBeUndefined()
|
|
19
|
+
expect(cnpgReplicationLive(rt({ state: 'ok', replication: [] }))?.streaming).toBe(0)
|
|
20
|
+
})
|
|
21
|
+
it('carries the incomplete flag to the instance list', () => {
|
|
22
|
+
expect(cnpgInstanceLive(rt({ state: 'partial', incomplete: true }))?.[0].incomplete).toBe(true)
|
|
23
|
+
})
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
describe('standbys and slots from the live read', () => {
|
|
27
|
+
const row = (over: Partial<CNPGFleetRow> = {}): CNPGFleetRow =>
|
|
28
|
+
({
|
|
29
|
+
key: 'db/pg',
|
|
30
|
+
namespace: 'db',
|
|
31
|
+
name: 'pg',
|
|
32
|
+
cluster: { metadata: { annotations: {} }, status: { currentPrimary: 'pg-6' } },
|
|
33
|
+
instances: { ready: 3, desired: 3 },
|
|
34
|
+
pods: [],
|
|
35
|
+
replication: { text: '2/2 Pods ready · lag unknown', tone: 'unknown' },
|
|
36
|
+
problems: [],
|
|
37
|
+
attention: false,
|
|
38
|
+
categories: new Set(),
|
|
39
|
+
hibernated: false,
|
|
40
|
+
...over,
|
|
41
|
+
}) as unknown as CNPGFleetRow
|
|
42
|
+
const live = (standby1: any): CNPGRuntimeResponse => ({
|
|
43
|
+
cluster: { namespace: 'db', name: 'pg', uid: 'u' },
|
|
44
|
+
sampledAt: '2026-10-04T10:00:00Z',
|
|
45
|
+
permission: { proxy: 'allowed' },
|
|
46
|
+
instances: [
|
|
47
|
+
{
|
|
48
|
+
pod: 'pg-6',
|
|
49
|
+
role: 'primary',
|
|
50
|
+
status: {
|
|
51
|
+
state: 'ok',
|
|
52
|
+
timeline: 7,
|
|
53
|
+
currentLsn: '3/BC06F9E8',
|
|
54
|
+
replication: [{ applicationName: 'pg-2', state: 'streaming', replayLsn: '3/BC06F9E8' }],
|
|
55
|
+
slots: [
|
|
56
|
+
{ name: '_cnpg_pg_1', type: 'physical', active: false, retainedBytes: 4.7e9 },
|
|
57
|
+
{ name: '_cnpg_pg_2', type: 'physical', active: true, retainedBytes: 0 },
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
metrics: { state: 'ok' },
|
|
61
|
+
},
|
|
62
|
+
{ pod: 'pg-1', role: 'replica', status: standby1, metrics: { state: 'ok' } },
|
|
63
|
+
{ pod: 'pg-2', role: 'replica', status: { state: 'ok', timeline: 7, roleDetail: 'streaming' }, metrics: { state: 'ok' } },
|
|
64
|
+
],
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('a standby with no row on the primary is a gap, with what it reports about itself', () => {
|
|
68
|
+
const gaps = cnpgLiveStandbyGaps(row(), live({ state: 'ok', timeline: 4, replayPaused: true, roleDetail: 'replayPaused' }))
|
|
69
|
+
expect(gaps.map((g) => g.pod)).toEqual(['pg-1'])
|
|
70
|
+
expect(gaps[0].evidence).toEqual([
|
|
71
|
+
'it has no row in the primary’s pg_stat_replication',
|
|
72
|
+
'its WAL replay is paused',
|
|
73
|
+
'it is on timeline 4 while the primary is on 7',
|
|
74
|
+
])
|
|
75
|
+
})
|
|
76
|
+
it('a fenced standby, or one running pg_rewind, is not a gap', () => {
|
|
77
|
+
const fenced = row({ cluster: { metadata: { annotations: { 'cnpg.io/fencedInstances': '["pg-1"]' } }, status: {} } } as any)
|
|
78
|
+
expect(cnpgLiveStandbyGaps(fenced, live({ state: 'ok', timeline: 7 }))).toEqual([])
|
|
79
|
+
expect(cnpgLiveStandbyGaps(row(), live({ state: 'ok', timeline: 7, roleDetail: 'pgRewind' }))).toEqual([])
|
|
80
|
+
})
|
|
81
|
+
it('raises the gap and the inactive slot as problems, and names the standby in the replication fact', () => {
|
|
82
|
+
const r = withLiveReplication(row(), live({ state: 'ok', timeline: 4, replayPaused: true }))
|
|
83
|
+
expect(r.replication.text).toBe('1 of 2 expected standbys streaming · pg-1 not connected')
|
|
84
|
+
expect(r.attention).toBe(true)
|
|
85
|
+
expect(r.problems.map((p) => p.id).sort()).toEqual(['slot:db/pg:_cnpg_pg_1', 'standby:db/pg:pg-1'])
|
|
86
|
+
expect(r.problems.find((p) => p.id === 'slot:db/pg:_cnpg_pg_1')!.title).toBe('Inactive slot _cnpg_pg_1 holds 4.4 GiB of WAL on pg-6 for pg-1')
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
describe('a live read replaces what it disproves', () => {
|
|
91
|
+
const base = (problems: any[]): CNPGFleetRow =>
|
|
92
|
+
({
|
|
93
|
+
key: 'db/pg',
|
|
94
|
+
namespace: 'db',
|
|
95
|
+
name: 'pg',
|
|
96
|
+
cluster: { metadata: {}, status: {} },
|
|
97
|
+
instances: { ready: 2, desired: 2 },
|
|
98
|
+
pods: [],
|
|
99
|
+
replication: { text: '2/2 Pods ready · pg-2 not receiving WAL', tone: 'degraded' },
|
|
100
|
+
problems,
|
|
101
|
+
attention: true,
|
|
102
|
+
categories: new Set(['availability']),
|
|
103
|
+
hibernated: false,
|
|
104
|
+
}) as unknown as CNPGFleetRow
|
|
105
|
+
const fleetStandby = { id: 'standby:db/pg:pg-2', reason: 'CNPGStandbyNotReceiving', severity: 'warning', category: 'availability', title: 'pg-2 is not receiving WAL from the primary', subject: { kind: 'Pod', group: '', namespace: 'db', name: 'pg-2' }, source: 'measurement' }
|
|
106
|
+
const fleetSlot = { id: 'slot:db/pg:_cnpg_pg_2', reason: 'CNPGInactiveSlot', slot: '_cnpg_pg_2', severity: 'warning', category: 'availability', title: 'Inactive slot _cnpg_pg_2 holds 2 GiB', subject: { kind: 'Cluster', group: 'postgresql.cnpg.io', namespace: 'db', name: 'pg' }, source: 'measurement' }
|
|
107
|
+
const rt = (primaryStatus: any): CNPGRuntimeResponse => ({
|
|
108
|
+
cluster: { namespace: 'db', name: 'pg', uid: 'u' },
|
|
109
|
+
sampledAt: '2026-10-04T10:00:00Z',
|
|
110
|
+
permission: { proxy: 'allowed' },
|
|
111
|
+
instances: [
|
|
112
|
+
{ pod: 'pg-1', role: 'primary', status: primaryStatus, metrics: { state: 'ok' } },
|
|
113
|
+
{ pod: 'pg-2', role: 'replica', status: { state: 'ok', timeline: 1 }, metrics: { state: 'ok' } },
|
|
114
|
+
],
|
|
115
|
+
})
|
|
116
|
+
it('clears the fleet’s standby and slot problems when the primary streams to it and the slot is active', () => {
|
|
117
|
+
const r = withLiveReplication(
|
|
118
|
+
base([fleetStandby, fleetSlot]),
|
|
119
|
+
rt({ state: 'ok', timeline: 1, replication: [{ applicationName: 'pg-2', state: 'streaming' }], slots: [{ name: '_cnpg_pg_2', type: 'physical', active: true, retainedBytes: 0 }] }),
|
|
120
|
+
)
|
|
121
|
+
expect(r.problems).toEqual([])
|
|
122
|
+
expect(r.attention).toBe(false)
|
|
123
|
+
expect(r.replication.text).toBe('1/1 streaming')
|
|
124
|
+
})
|
|
125
|
+
it('keeps a fleet slot warning the live read cannot disprove', () => {
|
|
126
|
+
// Unmeasured: inactive, retained bytes not reported.
|
|
127
|
+
const unmeasured = withLiveReplication(base([fleetSlot]), rt({ state: 'ok', timeline: 1, replication: [{ applicationName: 'pg-2', state: 'streaming' }], slots: [{ name: '_cnpg_pg_2', type: 'physical', active: false }] }))
|
|
128
|
+
expect(unmeasured.problems.map((p) => p.id)).toEqual(['slot:db/pg:_cnpg_pg_2'])
|
|
129
|
+
// Absent from a capped, partial list.
|
|
130
|
+
const partial = withLiveReplication(base([fleetSlot]), rt({ state: 'partial', replication: null, slots: [] }))
|
|
131
|
+
expect(partial.problems.map((p) => p.id)).toEqual(['slot:db/pg:_cnpg_pg_2'])
|
|
132
|
+
// Absent from a complete list: the slot is gone.
|
|
133
|
+
const gone = withLiveReplication(base([fleetSlot]), rt({ state: 'ok', timeline: 1, replication: [{ applicationName: 'pg-2', state: 'streaming' }], slots: [] }))
|
|
134
|
+
expect(gone.problems).toEqual([])
|
|
135
|
+
})
|
|
136
|
+
it('judges slots from a partial primary report the replication rows cannot use', () => {
|
|
137
|
+
const r = withLiveReplication(base([]), rt({ state: 'partial', incomplete: true, replication: null, slots: [{ name: '_cnpg_pg_2', type: 'physical', active: false, retainedBytes: 3 * 1024 ** 3 }] }))
|
|
138
|
+
expect(r.problems.map((p) => p.id)).toEqual(['slot:db/pg:_cnpg_pg_2'])
|
|
139
|
+
})
|
|
140
|
+
})
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import type { CNPGFleetRow, Grant } from '@skyhook-io/k8s-ui'
|
|
3
|
+
import type { CNPGRuntimeResponse } from './cnpg'
|
|
4
|
+
import { cnpgInstanceLiveUnavailable, cnpgReplicationGap, withLiveReplication } from '../components/cnpg/runtimeAssessment'
|
|
5
|
+
|
|
6
|
+
function row(desired: number | null): CNPGFleetRow {
|
|
7
|
+
return {
|
|
8
|
+
instances: { ready: 1, desired },
|
|
9
|
+
pods: [{ name: 'pg-1', role: 'primary', ready: true }],
|
|
10
|
+
replication: { text: 'Lag unknown', tone: 'unknown' },
|
|
11
|
+
key: 'db/pg',
|
|
12
|
+
problems: [],
|
|
13
|
+
categories: new Set(),
|
|
14
|
+
} as unknown as CNPGFleetRow
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function runtime(replication: { state: string; replayLag?: number }[]): CNPGRuntimeResponse {
|
|
18
|
+
return {
|
|
19
|
+
instances: [{ pod: 'pg-1', role: 'primary', status: { state: 'ok', capturedAt: '2026-09-30T12:00:00Z', replication } }],
|
|
20
|
+
} as unknown as CNPGRuntimeResponse
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('withLiveReplication', () => {
|
|
24
|
+
it('counts expected standbys from spec.instances, so missing standby Pods are not healthy', () => {
|
|
25
|
+
const r = withLiveReplication(row(3), runtime([]))
|
|
26
|
+
expect(r.replication).toMatchObject({ tone: 'degraded', text: '0 of 2 expected standbys streaming' })
|
|
27
|
+
const one = withLiveReplication(row(3), runtime([{ state: 'streaming', replayLag: 0 }]))
|
|
28
|
+
expect(one.replication).toMatchObject({ tone: 'degraded', text: expect.stringContaining('1 of 2 expected standbys streaming') })
|
|
29
|
+
})
|
|
30
|
+
it('says the replay delay covers only the standbys that are connected', () => {
|
|
31
|
+
const one = withLiveReplication(row(3), runtime([{ state: 'streaming', replayLag: 0 }]))
|
|
32
|
+
expect(one.replication.text).toBe('1 of 2 expected standbys streaming · max replay delay 0 s (connected standbys only)')
|
|
33
|
+
const all = withLiveReplication(row(2), runtime([{ state: 'streaming', replayLag: 0 }]))
|
|
34
|
+
expect(all.replication.text).toBe('1/1 streaming · max replay delay 0 s')
|
|
35
|
+
})
|
|
36
|
+
it('is healthy only when every expected standby streams', () => {
|
|
37
|
+
const r = withLiveReplication(row(2), runtime([{ state: 'streaming', replayLag: 0.1 }]))
|
|
38
|
+
expect(r.replication.tone).toBe('healthy')
|
|
39
|
+
})
|
|
40
|
+
it('is unknown when spec.instances is not reported', () => {
|
|
41
|
+
expect(withLiveReplication(row(null), runtime([])).replication.tone).toBe('unknown')
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
describe('cnpgInstanceLiveUnavailable', () => {
|
|
46
|
+
const rt = (proxy: 'allowed' | 'denied', grant?: Grant) => ({ permission: { proxy, grant }, instances: [] }) as unknown as CNPGRuntimeResponse
|
|
47
|
+
it('names the grant only when the proxy is denied', () => {
|
|
48
|
+
expect(cnpgInstanceLiveUnavailable(rt('denied', { verb: 'get', resource: 'pods', subresource: 'proxy', namespace: 'db' }), undefined)).toBe('needs get pods/proxy in namespace db')
|
|
49
|
+
expect(cnpgInstanceLiveUnavailable(rt('allowed'), undefined)).toBeUndefined()
|
|
50
|
+
expect(cnpgInstanceLiveUnavailable(undefined, undefined)).toBe('instance managers not read yet')
|
|
51
|
+
expect(cnpgInstanceLiveUnavailable(undefined, new Error('boom'))).toBe('the runtime read failed: boom')
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
describe('cnpgReplicationGap', () => {
|
|
56
|
+
it('says why the primary’s replication rows are missing', () => {
|
|
57
|
+
const rt = (instances: unknown[]) => ({ permission: { proxy: 'allowed' }, instances }) as unknown as CNPGRuntimeResponse
|
|
58
|
+
expect(cnpgReplicationGap(rt([{ pod: 'pg-1', role: 'primary', status: { state: 'unreachable', error: 'PostgreSQL is not running on this instance' } }]), undefined)).toBe(
|
|
59
|
+
'pg-1 did not report (PostgreSQL is not running on this instance)',
|
|
60
|
+
)
|
|
61
|
+
expect(cnpgReplicationGap(rt([{ pod: 'pg-1', role: 'primary', status: { state: 'ok', replication: [] } }]), undefined)).toBeUndefined()
|
|
62
|
+
expect(cnpgReplicationGap({ permission: { proxy: 'denied', grant: { verb: 'get', resource: 'pods', subresource: 'proxy', namespace: 'db' } }, instances: [] } as unknown as CNPGRuntimeResponse, undefined)).toBe(
|
|
63
|
+
'needs get pods/proxy in namespace db',
|
|
64
|
+
)
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
describe('withLiveReplication with a missing standby', () => {
|
|
69
|
+
it('takes the worse of the missing standby and the lag', () => {
|
|
70
|
+
const rt = {
|
|
71
|
+
permission: { proxy: 'allowed' },
|
|
72
|
+
instances: [{ pod: 'pg-1', role: 'primary', status: { state: 'ok', capturedAt: 't', replication: [{ applicationName: 'pg-2', state: 'streaming', replayLag: 72 }] } }],
|
|
73
|
+
} as unknown as CNPGRuntimeResponse
|
|
74
|
+
const out = withLiveReplication(row(3), rt)
|
|
75
|
+
expect(out.replication.tone).toBe('unhealthy')
|
|
76
|
+
expect(out.replication.text).toContain('1 of 2 expected standbys streaming')
|
|
77
|
+
})
|
|
78
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useQuery } from '@tanstack/react-query'
|
|
2
|
+
import type { CNPGClusterHA } from '@skyhook-io/k8s-ui'
|
|
3
|
+
import { fetchJSON, useRadarFeature } from './client'
|
|
4
|
+
|
|
5
|
+
// /api/cnpg/clusters/{ns}/{name}/ha — quorum, disruption budgets, Leases,
|
|
6
|
+
// failure domains, Jobs, image drift and certificate renewal ownership. Each
|
|
7
|
+
// part is authorized on its own and says when it could not be read.
|
|
8
|
+
export function useCNPGClusterHA(namespace: string, name: string, options?: { enabled?: boolean; refetchInterval?: number | false }) {
|
|
9
|
+
const { guard, gatedKey } = useRadarFeature('cnpgWorkspace')
|
|
10
|
+
return useQuery<CNPGClusterHA>({
|
|
11
|
+
queryKey: ['cnpg', 'ha', namespace, name, ...gatedKey],
|
|
12
|
+
queryFn: ({ signal }) => guard(() => fetchJSON<CNPGClusterHA>(`/cnpg/clusters/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}/ha`, signal)),
|
|
13
|
+
enabled: (options?.enabled ?? true) && !!name,
|
|
14
|
+
staleTime: 10_000,
|
|
15
|
+
refetchInterval: options?.refetchInterval ?? 30_000,
|
|
16
|
+
refetchIntervalInBackground: false,
|
|
17
|
+
retry: false,
|
|
18
|
+
})
|
|
19
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { useQuery } from '@tanstack/react-query'
|
|
2
|
+
import type { CNPGFleetMetricsReading, Grant } from '@skyhook-io/k8s-ui'
|
|
3
|
+
import { fetchJSON, useRadarFeature } from './client'
|
|
4
|
+
import { shouldRetryRadarQuery } from './radarFeatures'
|
|
5
|
+
import type { CNPGClusterActivityResponse } from './cnpg'
|
|
6
|
+
|
|
7
|
+
export type CNPGHistoryRange = '15m' | '1h' | '6h' | '24h'
|
|
8
|
+
|
|
9
|
+
export const CNPG_HISTORY_RANGES: { id: CNPGHistoryRange; label: string }[] = [
|
|
10
|
+
{ id: '15m', label: '15 min' },
|
|
11
|
+
{ id: '1h', label: '1 h' },
|
|
12
|
+
{ id: '6h', label: '6 h' },
|
|
13
|
+
{ id: '24h', label: '24 h' },
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
export interface CNPGHistoryPoint {
|
|
17
|
+
timestamp: number
|
|
18
|
+
value: number | null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface CNPGHistorySeries {
|
|
22
|
+
labels: Record<string, string>
|
|
23
|
+
dataPoints: CNPGHistoryPoint[]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface CNPGHistoryChart {
|
|
27
|
+
id: string
|
|
28
|
+
title: string
|
|
29
|
+
unit: string
|
|
30
|
+
/** The PromQL families the chart is built from. */
|
|
31
|
+
source: string
|
|
32
|
+
/** The label naming each series (pod, state, datname, persistentvolumeclaim or series). */
|
|
33
|
+
seriesBy: string
|
|
34
|
+
/** ok | empty (scraped, nothing to plot) | noSeries (not scraped) | denied | ambiguous | scopeMismatch | error | notRead */
|
|
35
|
+
state: string
|
|
36
|
+
reason?: string
|
|
37
|
+
grant?: Grant
|
|
38
|
+
thresholds?: { value: number; label: string }[]
|
|
39
|
+
series: CNPGHistorySeries[]
|
|
40
|
+
omitted?: number
|
|
41
|
+
steps: number
|
|
42
|
+
covered: number
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface CNPGClusterHistoryResponse {
|
|
46
|
+
cluster: { namespace: string; name: string; uid: string }
|
|
47
|
+
/** prometheus | none (Radar has no Prometheus; `reason` says why) */
|
|
48
|
+
source: 'prometheus' | 'none'
|
|
49
|
+
/** ok | ambiguous | scopeMismatch | error, for the query as a whole */
|
|
50
|
+
state?: string
|
|
51
|
+
reason?: string
|
|
52
|
+
range: CNPGHistoryRange
|
|
53
|
+
start?: string
|
|
54
|
+
end?: string
|
|
55
|
+
stepSeconds?: number
|
|
56
|
+
selector?: string
|
|
57
|
+
isolation?: { mode: 'configured' | 'verified' | 'unverified'; labels?: Record<string, string>; note: string }
|
|
58
|
+
/** The volume chart's own match: claims are tied to the cluster apart from Pods. */
|
|
59
|
+
pvcIsolation?: { mode: 'configured' | 'verified' | 'unverified'; labels?: Record<string, string>; note: string }
|
|
60
|
+
sampledAt: string
|
|
61
|
+
charts: CNPGHistoryChart[]
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// /api/cnpg/clusters/{ns}/{name}/history — server-built Prometheus range
|
|
65
|
+
// queries for the Runtime Trends section.
|
|
66
|
+
export function useCNPGClusterHistory(namespace: string, name: string, range: CNPGHistoryRange, enabled = true) {
|
|
67
|
+
const { guard, gatedKey } = useRadarFeature('cnpgWorkspace')
|
|
68
|
+
return useQuery<CNPGClusterHistoryResponse>({
|
|
69
|
+
queryKey: ['cnpg', 'history', namespace, name, range, ...gatedKey],
|
|
70
|
+
queryFn: ({ signal }) => guard(() =>
|
|
71
|
+
fetchJSON<CNPGClusterHistoryResponse>(
|
|
72
|
+
`/cnpg/clusters/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}/history?range=${range}`,
|
|
73
|
+
signal,
|
|
74
|
+
)),
|
|
75
|
+
enabled: enabled && !!name,
|
|
76
|
+
refetchInterval: range === '15m' ? 30_000 : 60_000,
|
|
77
|
+
refetchIntervalInBackground: false,
|
|
78
|
+
staleTime: 10_000,
|
|
79
|
+
retry: false,
|
|
80
|
+
placeholderData: (prev) => (prev?.range === range ? prev : undefined),
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface CNPGFleetMetricsResponse {
|
|
85
|
+
sampledAt: string
|
|
86
|
+
source: 'prometheus' | 'none'
|
|
87
|
+
reason?: string
|
|
88
|
+
lagSource: string
|
|
89
|
+
growthSource: string
|
|
90
|
+
clusters: CNPGFleetMetricsReading[]
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// /api/cnpg/fleet-metrics — each visible Cluster's largest standby replay lag
|
|
94
|
+
// and volume growth, read with the workspace's namespace set.
|
|
95
|
+
export function useCNPGFleetMetrics(namespaces: string[], enabled = true) {
|
|
96
|
+
const { guard, gatedKey } = useRadarFeature('cnpgWorkspace')
|
|
97
|
+
const ns = [...namespaces].sort().join(',')
|
|
98
|
+
return useQuery<CNPGFleetMetricsResponse>({
|
|
99
|
+
queryKey: ['cnpg', 'fleet-metrics', ns, ...gatedKey],
|
|
100
|
+
queryFn: ({ signal }) => guard(() => fetchJSON<CNPGFleetMetricsResponse>(`/cnpg/fleet-metrics${ns ? `?namespaces=${encodeURIComponent(ns)}` : ''}`, signal)),
|
|
101
|
+
enabled,
|
|
102
|
+
staleTime: 20_000,
|
|
103
|
+
refetchInterval: 60_000,
|
|
104
|
+
retry: false,
|
|
105
|
+
placeholderData: (prev) => prev,
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// The Cluster's activity inside one interval, both bounds applied server-side.
|
|
110
|
+
export function useCNPGClusterActivityWindow(namespace: string, name: string, since: string, until: string, enabled = true) {
|
|
111
|
+
const { guard, gatedKey } = useRadarFeature('cnpgWorkspace')
|
|
112
|
+
return useQuery<CNPGClusterActivityResponse>({
|
|
113
|
+
queryKey: ['cnpg', 'activity-window', namespace, name, since, until, ...gatedKey],
|
|
114
|
+
queryFn: ({ signal }) => guard(() =>
|
|
115
|
+
fetchJSON<CNPGClusterActivityResponse>(
|
|
116
|
+
`/cnpg/clusters/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}/activity?since=${encodeURIComponent(since)}&until=${encodeURIComponent(until)}&limit=500`,
|
|
117
|
+
signal,
|
|
118
|
+
)),
|
|
119
|
+
enabled: enabled && !!name,
|
|
120
|
+
staleTime: 30_000,
|
|
121
|
+
retry: shouldRetryRadarQuery,
|
|
122
|
+
})
|
|
123
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { useQuery } from '@tanstack/react-query'
|
|
2
|
+
import type { Grant } from '@skyhook-io/k8s-ui'
|
|
3
|
+
import { fetchJSON, useRadarFeature } from './client'
|
|
4
|
+
import type { CNPGRuntimeSourceState } from './cnpg'
|
|
5
|
+
|
|
6
|
+
const enc = encodeURIComponent
|
|
7
|
+
|
|
8
|
+
interface CNPGInspectSource {
|
|
9
|
+
state: CNPGRuntimeSourceState
|
|
10
|
+
error?: string
|
|
11
|
+
capturedAt?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface CNPGTimelineSwitch {
|
|
15
|
+
from: number
|
|
16
|
+
to: number
|
|
17
|
+
switchLsn: string
|
|
18
|
+
/** PostgreSQL's own text, e.g. "before 2026-10-01 12:00:00+00" or "no recovery target specified". */
|
|
19
|
+
reason: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface CNPGDatabaseContents {
|
|
23
|
+
tables: number
|
|
24
|
+
/** The planner's estimate (pg_class.reltuples); it can be stale. */
|
|
25
|
+
estimatedRows: number
|
|
26
|
+
/** Tables without an estimate (never analyzed; before PostgreSQL 14 also an empty one), not counted as zero. */
|
|
27
|
+
noEstimate: number
|
|
28
|
+
largest: { name: string; estimatedRows?: number; bytes: number }[]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface CNPGRestoreChecksResponse extends CNPGInspectSource {
|
|
32
|
+
cluster: { namespace: string; name: string; uid: string }
|
|
33
|
+
pod: string
|
|
34
|
+
sampledAt: string
|
|
35
|
+
permission: { exec: 'allowed' | 'denied' | 'unknown'; grant?: Grant }
|
|
36
|
+
/** spec.bootstrap.recovery.recoveryTarget as declared. */
|
|
37
|
+
target?: Record<string, unknown>
|
|
38
|
+
database: string
|
|
39
|
+
inRecovery?: boolean
|
|
40
|
+
timeline?: number
|
|
41
|
+
history?: CNPGTimelineSwitch[]
|
|
42
|
+
historyMissing?: boolean
|
|
43
|
+
databaseCount?: number
|
|
44
|
+
databases?: { name: string; bytes: number }[]
|
|
45
|
+
roleCount?: number
|
|
46
|
+
roles?: { name: string; canLogin: boolean }[]
|
|
47
|
+
contents?: CNPGDatabaseContents
|
|
48
|
+
contentsSource?: CNPGInspectSource
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// /api/cnpg/clusters/{ns}/{name}/restore-checks — what Radar reads in a
|
|
52
|
+
// restored cluster's primary over the caller's pods/exec. Denied is a state.
|
|
53
|
+
export function useCNPGRestoreChecks(namespace: string, name: string, enabled = true) {
|
|
54
|
+
const { guard, gatedKey } = useRadarFeature('cnpgWorkspace')
|
|
55
|
+
return useQuery<CNPGRestoreChecksResponse>({
|
|
56
|
+
queryKey: ['cnpg', 'restore-checks', namespace, name, ...gatedKey],
|
|
57
|
+
queryFn: ({ signal }) => guard(() => fetchJSON<CNPGRestoreChecksResponse>(`/cnpg/clusters/${enc(namespace)}/${enc(name)}/restore-checks`, signal)),
|
|
58
|
+
enabled: enabled && !!name,
|
|
59
|
+
staleTime: 60_000,
|
|
60
|
+
retry: false,
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface CNPGParameterSetting {
|
|
65
|
+
name: string
|
|
66
|
+
/** PostgreSQL's display; null when the connection itself sets the parameter. */
|
|
67
|
+
value: string | null
|
|
68
|
+
setByClient?: boolean
|
|
69
|
+
source: string
|
|
70
|
+
/** pg_settings.context: when a change takes effect. */
|
|
71
|
+
context: string
|
|
72
|
+
pendingRestart: boolean
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface CNPGInstanceSettings extends CNPGInspectSource {
|
|
76
|
+
pod: string
|
|
77
|
+
role: 'primary' | 'replica' | 'unknown'
|
|
78
|
+
settings?: CNPGParameterSetting[]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface CNPGParametersResponse extends CNPGInspectSource {
|
|
82
|
+
cluster: { namespace: string; name: string; uid: string }
|
|
83
|
+
sampledAt: string
|
|
84
|
+
permission: { exec: 'allowed' | 'denied' | 'unknown'; grant?: Grant }
|
|
85
|
+
declared: { name: string; value: string }[]
|
|
86
|
+
omitted?: number
|
|
87
|
+
skipped?: string[]
|
|
88
|
+
instances: CNPGInstanceSettings[]
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// /api/cnpg/clusters/{ns}/{name}/parameters — the declared parameters as each
|
|
92
|
+
// instance's PostgreSQL reports them. Observation only.
|
|
93
|
+
export function useCNPGParameters(namespace: string, name: string, enabled = true) {
|
|
94
|
+
const { guard, gatedKey } = useRadarFeature('cnpgWorkspace')
|
|
95
|
+
return useQuery<CNPGParametersResponse>({
|
|
96
|
+
queryKey: ['cnpg', 'parameters', namespace, name, ...gatedKey],
|
|
97
|
+
queryFn: ({ signal }) => guard(() => fetchJSON<CNPGParametersResponse>(`/cnpg/clusters/${enc(namespace)}/${enc(name)}/parameters`, signal)),
|
|
98
|
+
enabled: enabled && !!name,
|
|
99
|
+
staleTime: 30_000,
|
|
100
|
+
retry: false,
|
|
101
|
+
})
|
|
102
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
|
2
|
+
import { apiUrl } from './config'
|
|
3
|
+
import { useReviewedActionMutation } from './reviewedActionMutation'
|
|
4
|
+
import type { CNPGSchedulePreview } from '@skyhook-io/k8s-ui'
|
|
5
|
+
import { fetchJSON, useRadarFeature } from './client'
|
|
6
|
+
import type { ActionRequest } from './actions'
|
|
7
|
+
import { cnpgPath, type CNPGActionResult } from './cnpg'
|
|
8
|
+
|
|
9
|
+
export interface CNPGArchivingParams {
|
|
10
|
+
objectStore: string
|
|
11
|
+
serverName: string
|
|
12
|
+
acknowledgeArchive: boolean
|
|
13
|
+
}
|
|
14
|
+
export interface CNPGArchivingFacts {
|
|
15
|
+
clusterConfig: string
|
|
16
|
+
objectStoreUID: string
|
|
17
|
+
objectStoreConfig: string
|
|
18
|
+
objectStore: string
|
|
19
|
+
serverName: string
|
|
20
|
+
}
|
|
21
|
+
export interface CNPGArchivingPreview {
|
|
22
|
+
context: string
|
|
23
|
+
uid: string
|
|
24
|
+
facts: CNPGArchivingFacts
|
|
25
|
+
destination: string
|
|
26
|
+
endpoint?: string
|
|
27
|
+
plugin: Record<string, unknown>
|
|
28
|
+
warnings: string[]
|
|
29
|
+
unchanged: boolean
|
|
30
|
+
}
|
|
31
|
+
export interface CNPGScheduleMethodFacts {
|
|
32
|
+
clusterUID: string
|
|
33
|
+
clusterConfig: string
|
|
34
|
+
scheduleConfig: string
|
|
35
|
+
}
|
|
36
|
+
export interface CNPGScheduleMethodPreview {
|
|
37
|
+
context: string
|
|
38
|
+
uid: string
|
|
39
|
+
cluster: string
|
|
40
|
+
previousMethod: string
|
|
41
|
+
facts: CNPGScheduleMethodFacts
|
|
42
|
+
unchanged: boolean
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function useCNPGArchivingPreview(namespace: string, name: string) {
|
|
46
|
+
const { guard } = useRadarFeature('cnpgProtectionSetup')
|
|
47
|
+
return useMutation<CNPGArchivingPreview, Error, { reviewedContext: string; objectStore: string; serverName: string }>(
|
|
48
|
+
{
|
|
49
|
+
mutationFn: (request) =>
|
|
50
|
+
guard(() =>
|
|
51
|
+
fetchJSON(`${cnpgPath('clusters', namespace, name)}/protection/preview`, {
|
|
52
|
+
method: 'POST',
|
|
53
|
+
headers: { 'Content-Type': 'application/json' },
|
|
54
|
+
body: JSON.stringify(request),
|
|
55
|
+
}),
|
|
56
|
+
),
|
|
57
|
+
},
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function useCNPGScheduleMethodPreview(namespace: string, name: string) {
|
|
62
|
+
const { guard, gatedKey } = useRadarFeature('cnpgProtectionSetup')
|
|
63
|
+
return useQuery<CNPGScheduleMethodPreview>({
|
|
64
|
+
queryKey: ['cnpg', 'method-preview', namespace, name, ...gatedKey],
|
|
65
|
+
queryFn: ({ signal }) =>
|
|
66
|
+
guard(() => fetchJSON(`${cnpgPath('scheduledbackups', namespace, name)}/method-preview`, signal)),
|
|
67
|
+
enabled: !!name,
|
|
68
|
+
retry: false,
|
|
69
|
+
staleTime: 5_000,
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function useCNPGDraftSchedulePreview(namespace: string, name: string, schedule: string, enabled = true) {
|
|
74
|
+
const { guard, gatedKey } = useRadarFeature('cnpgProtectionSetup')
|
|
75
|
+
return useQuery<CNPGSchedulePreview>({
|
|
76
|
+
queryKey: ['cnpg', 'draft-schedule-preview', namespace, name, schedule, ...gatedKey],
|
|
77
|
+
queryFn: ({ signal }) =>
|
|
78
|
+
guard(() =>
|
|
79
|
+
fetchJSON(
|
|
80
|
+
`${cnpgPath('clusters', namespace, name)}/schedule-preview?schedule=${encodeURIComponent(schedule)}`,
|
|
81
|
+
signal,
|
|
82
|
+
),
|
|
83
|
+
),
|
|
84
|
+
enabled: enabled && !!name,
|
|
85
|
+
retry: false,
|
|
86
|
+
staleTime: 30_000,
|
|
87
|
+
placeholderData: (previous) => previous,
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
type ProtectionAction =
|
|
92
|
+
| { kind: 'clusters'; action: 'configureArchiving'; request: ActionRequest<CNPGArchivingFacts, CNPGArchivingParams> }
|
|
93
|
+
| { kind: 'scheduledbackups'; action: 'repairMethod'; request: ActionRequest<CNPGScheduleMethodFacts> }
|
|
94
|
+
|
|
95
|
+
export function useCNPGProtectionAction<Kind extends ProtectionAction['kind']>(kind: Kind, namespace: string, name: string, reviewedContext: string | undefined) {
|
|
96
|
+
const { guard } = useRadarFeature('cnpgProtectionSetup')
|
|
97
|
+
const client = useQueryClient()
|
|
98
|
+
return useReviewedActionMutation<CNPGActionResult, Extract<ProtectionAction, { kind: Kind }>>(apiUrl(cnpgPath(kind, namespace, name)), reviewedContext, {
|
|
99
|
+
mutationFn: ({ kind, action, request }) =>
|
|
100
|
+
guard(() =>
|
|
101
|
+
fetchJSON(`${cnpgPath(kind, namespace, name)}/actions/${action}`, {
|
|
102
|
+
method: 'POST',
|
|
103
|
+
headers: { 'Content-Type': 'application/json' },
|
|
104
|
+
body: JSON.stringify(request),
|
|
105
|
+
}),
|
|
106
|
+
),
|
|
107
|
+
onSettled: () => {
|
|
108
|
+
client.invalidateQueries({ queryKey: ['cnpg'] })
|
|
109
|
+
client.invalidateQueries({ queryKey: ['resource'] })
|
|
110
|
+
},
|
|
111
|
+
})
|
|
112
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { act } from 'react'
|
|
3
|
+
import { createRoot } from 'react-dom/client'
|
|
4
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
5
|
+
import { expect, it, vi } from 'vitest'
|
|
6
|
+
import { fetchJSON } from './client'
|
|
7
|
+
import { apiUrl } from './config'
|
|
8
|
+
import { useReviewedActionMutation } from './reviewedActionMutation'
|
|
9
|
+
import { useRecordCNPGRestoreValidation, type CNPGRestoreValidationRequest } from './cnpg-recovery'
|
|
10
|
+
|
|
11
|
+
vi.mock('./client', async (importOriginal) => ({
|
|
12
|
+
...await importOriginal<typeof import('./client')>(),
|
|
13
|
+
fetchJSON: vi.fn(),
|
|
14
|
+
useRadarFeature: () => ({ guard: <T,>(read: () => T) => read() }),
|
|
15
|
+
}))
|
|
16
|
+
|
|
17
|
+
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
|
|
18
|
+
|
|
19
|
+
it('retains an unknown validation-note result through remount without changing its HTTP body', async () => {
|
|
20
|
+
const client = new QueryClient({ defaultOptions: { mutations: { retry: false } } })
|
|
21
|
+
const invalidate = vi.spyOn(client, 'invalidateQueries')
|
|
22
|
+
const host = document.body.appendChild(document.createElement('div'))
|
|
23
|
+
const root = createRoot(host)
|
|
24
|
+
const send = vi.mocked(fetchJSON).mockRejectedValue(new TypeError('Connection lost'))
|
|
25
|
+
let current!: ReturnType<typeof useRecordCNPGRestoreValidation>
|
|
26
|
+
let operational!: ReturnType<typeof useReviewedActionMutation<void, { request: { reviewedContext: string } }>>
|
|
27
|
+
const operationalWrite = vi.fn(async () => {})
|
|
28
|
+
function Dialog() {
|
|
29
|
+
current = useRecordCNPGRestoreValidation('db', 'restored', 'ctx')
|
|
30
|
+
operational = useReviewedActionMutation(apiUrl('/cnpg/clusters/db/restored'), 'ctx', { mutationFn: operationalWrite })
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
const mount = (open: boolean) => act(() => root.render(<QueryClientProvider client={client}>{open && <Dialog />}</QueryClientProvider>))
|
|
34
|
+
const request: CNPGRestoreValidationRequest = { reviewedContext: 'ctx', uid: 'restored-uid', params: { checked: 'Application query passed' } }
|
|
35
|
+
const attempt = () => act(async () => {
|
|
36
|
+
await current.mutateAsync({ request }).catch(() => {})
|
|
37
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
38
|
+
})
|
|
39
|
+
try {
|
|
40
|
+
mount(true)
|
|
41
|
+
await attempt()
|
|
42
|
+
expect(send).toHaveBeenCalledWith('/cnpg/clusters/db/restored/restore-validation', expect.objectContaining({ body: JSON.stringify(request) }))
|
|
43
|
+
expect(invalidate).toHaveBeenCalledWith({ queryKey: ['cnpg', 'recovery', 'db', 'restored'] })
|
|
44
|
+
expect(invalidate).toHaveBeenCalledWith({ queryKey: ['cnpg', 'workspace'] })
|
|
45
|
+
expect(client.getMutationCache().getAll()[0].options.meta?.errorMessage).toBeUndefined()
|
|
46
|
+
await act(async () => { await operational.mutateAsync({ request: { reviewedContext: 'ctx' } }) })
|
|
47
|
+
expect(operationalWrite).toHaveBeenCalledOnce()
|
|
48
|
+
mount(false)
|
|
49
|
+
client.clear()
|
|
50
|
+
mount(true)
|
|
51
|
+
expect(current.outcomeError?.message).toContain('Connection lost')
|
|
52
|
+
await attempt()
|
|
53
|
+
expect(send).toHaveBeenCalledTimes(1)
|
|
54
|
+
} finally {
|
|
55
|
+
act(() => root.unmount())
|
|
56
|
+
client.clear()
|
|
57
|
+
host.remove()
|
|
58
|
+
send.mockReset()
|
|
59
|
+
}
|
|
60
|
+
})
|