@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
package/src/api/cnpg.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
import { useQuery } from '@tanstack/react-query'
|
|
2
|
-
import type { CNPGWorkspaceResponse, TimelineEvent } from '@skyhook-io/k8s-ui'
|
|
3
|
-
import { fetchJSON } from './client'
|
|
1
|
+
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
|
2
|
+
import type { CNPGKindCoverage, CNPGSchedulePreview, CNPGWorkspaceResponse, Grant, TimelineEvent } from '@skyhook-io/k8s-ui'
|
|
3
|
+
import { fetchJSON, useRadarFeature } from './client'
|
|
4
|
+
import { apiUrl } from './config'
|
|
5
|
+
import { useReviewedActionMutation } from './reviewedActionMutation'
|
|
6
|
+
import { shouldRetryRadarQuery } from './radarFeatures'
|
|
7
|
+
import type { CNPGOperatorDiagnosis } from './cnpg-recovery'
|
|
8
|
+
import type { CNPGPoolerFacts } from './cnpg-sessions'
|
|
9
|
+
import { actionErrorCode, type ActionCapability, type ActionErrorCode, type ActionRequest } from './actions'
|
|
4
10
|
|
|
5
11
|
// /api/cnpg/workspace
|
|
6
12
|
//
|
|
@@ -11,19 +17,18 @@ import { fetchJSON } from './client'
|
|
|
11
17
|
export function useCNPGWorkspace(namespaces: string[], options?: { enabled?: boolean }) {
|
|
12
18
|
const ns = [...namespaces].sort().join(',')
|
|
13
19
|
return useQuery<CNPGWorkspaceResponse>({
|
|
14
|
-
queryKey: ['cnpg', 'workspace', ns],
|
|
20
|
+
queryKey: ['cnpg', 'workspace', ns ],
|
|
15
21
|
queryFn: ({ signal }) => fetchJSON<CNPGWorkspaceResponse>(`/cnpg/workspace${ns ? `?namespaces=${encodeURIComponent(ns)}` : ''}`, signal),
|
|
16
22
|
enabled: options?.enabled ?? true,
|
|
17
23
|
staleTime: 10_000,
|
|
18
24
|
refetchInterval: 30_000,
|
|
19
25
|
placeholderData: (prev) => prev,
|
|
26
|
+
retry: shouldRetryRadarQuery,
|
|
20
27
|
})
|
|
21
28
|
}
|
|
22
29
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
deniedNamespaces?: string[]
|
|
26
|
-
}
|
|
30
|
+
/** The operator's Deployments and Services are read with the same per-kind coverage as the workspace. */
|
|
31
|
+
export type CNPGOperatorCoverage = CNPGKindCoverage
|
|
27
32
|
|
|
28
33
|
export interface CNPGOperatorComponent {
|
|
29
34
|
role: 'operator' | 'plugin'
|
|
@@ -34,6 +39,26 @@ export interface CNPGOperatorComponent {
|
|
|
34
39
|
version?: string
|
|
35
40
|
readyReplicas: number | null
|
|
36
41
|
replicas: number | null
|
|
42
|
+
/** The Deployment's Pods; null when they could not be read (see podCoverage). */
|
|
43
|
+
pods?: CNPGOperatorComponentPod[] | null
|
|
44
|
+
podCoverage?: { state: string; grant?: Grant; reason?: string }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface CNPGContainerTermination {
|
|
48
|
+
container: string
|
|
49
|
+
reason: string
|
|
50
|
+
exitCode: number
|
|
51
|
+
finishedAt?: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface CNPGOperatorComponentPod {
|
|
55
|
+
name: string
|
|
56
|
+
ready: boolean
|
|
57
|
+
/** Restarts across the Pod's containers since the Pod was created. */
|
|
58
|
+
restarts: number
|
|
59
|
+
/** When the current container started. */
|
|
60
|
+
startedAt?: string
|
|
61
|
+
lastTermination?: CNPGContainerTermination
|
|
37
62
|
}
|
|
38
63
|
|
|
39
64
|
export interface CNPGOperatorConfig {
|
|
@@ -51,6 +76,7 @@ export interface CNPGOperatorResponse {
|
|
|
51
76
|
coverage: { deployments: CNPGOperatorCoverage; services: CNPGOperatorCoverage }
|
|
52
77
|
components: CNPGOperatorComponent[]
|
|
53
78
|
config: CNPGOperatorConfig[]
|
|
79
|
+
diagnosis?: CNPGOperatorDiagnosis[]
|
|
54
80
|
}
|
|
55
81
|
|
|
56
82
|
// /api/cnpg/operator
|
|
@@ -60,11 +86,43 @@ export interface CNPGOperatorResponse {
|
|
|
60
86
|
// its own namespace, which users rarely have selected.
|
|
61
87
|
export function useCNPGOperator(options?: { enabled?: boolean }) {
|
|
62
88
|
return useQuery<CNPGOperatorResponse>({
|
|
63
|
-
queryKey: ['cnpg', 'operator'],
|
|
89
|
+
queryKey: ['cnpg', 'operator' ],
|
|
64
90
|
queryFn: ({ signal }) => fetchJSON<CNPGOperatorResponse>('/cnpg/operator', signal),
|
|
65
91
|
enabled: options?.enabled ?? true,
|
|
66
92
|
staleTime: 30_000,
|
|
67
93
|
refetchInterval: 60_000,
|
|
94
|
+
retry: shouldRetryRadarQuery,
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Whether the operator watching a namespace is acting on it. While it is not,
|
|
100
|
+
* CNPG status there is whatever the operator last wrote. webhookRejects is
|
|
101
|
+
* null when the webhook state could not be read.
|
|
102
|
+
*/
|
|
103
|
+
export interface CNPGOperatorVerdict {
|
|
104
|
+
state: 'reconciling' | 'notReconciling' | 'unknown' | 'notWatched'
|
|
105
|
+
reasons?: string[]
|
|
106
|
+
unknown?: string
|
|
107
|
+
webhookRejects: boolean | null
|
|
108
|
+
webhookReason?: string
|
|
109
|
+
operator?: string
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// /api/cnpg/operator/status?namespaces=
|
|
113
|
+
//
|
|
114
|
+
// The cheap operator verdict for the fleet and cluster pages, keyed by namespace.
|
|
115
|
+
export function useCNPGOperatorStatus(namespaces: string[], options?: { enabled?: boolean }) {
|
|
116
|
+
const { guard, gatedKey } = useRadarFeature('cnpgWorkspace')
|
|
117
|
+
const ns = [...new Set(namespaces)].sort().join(',')
|
|
118
|
+
return useQuery<{ namespaces: Record<string, CNPGOperatorVerdict> }>({
|
|
119
|
+
queryKey: ['cnpg', 'operator-status', ns, ...gatedKey],
|
|
120
|
+
queryFn: ({ signal }) => guard(() => fetchJSON<{ namespaces: Record<string, CNPGOperatorVerdict> }>(`/cnpg/operator/status?namespaces=${encodeURIComponent(ns)}`, signal)),
|
|
121
|
+
enabled: (options?.enabled ?? true) && ns !== '',
|
|
122
|
+
staleTime: 10_000,
|
|
123
|
+
refetchInterval: 30_000,
|
|
124
|
+
placeholderData: (prev) => prev,
|
|
125
|
+
retry: shouldRetryRadarQuery,
|
|
68
126
|
})
|
|
69
127
|
}
|
|
70
128
|
|
|
@@ -81,7 +139,7 @@ export interface CNPGClusterActivityResponse {
|
|
|
81
139
|
// attributed to it, including ones since deleted.
|
|
82
140
|
export function useCNPGClusterActivity(namespace: string, name: string, sinceHours = 24) {
|
|
83
141
|
return useQuery<CNPGClusterActivityResponse>({
|
|
84
|
-
queryKey: ['cnpg', 'activity', namespace, name, sinceHours],
|
|
142
|
+
queryKey: ['cnpg', 'activity', namespace, name, sinceHours ],
|
|
85
143
|
queryFn: ({ signal }) => {
|
|
86
144
|
const since = new Date(Date.now() - sinceHours * 3600_000).toISOString()
|
|
87
145
|
return fetchJSON<CNPGClusterActivityResponse>(
|
|
@@ -91,5 +149,365 @@ export function useCNPGClusterActivity(namespace: string, name: string, sinceHou
|
|
|
91
149
|
},
|
|
92
150
|
staleTime: 10_000,
|
|
93
151
|
refetchInterval: 30_000,
|
|
152
|
+
retry: shouldRetryRadarQuery,
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface CNPGInstanceFact {
|
|
157
|
+
pod: string
|
|
158
|
+
podUID: string
|
|
159
|
+
role: 'primary' | 'standby'
|
|
160
|
+
ready: boolean
|
|
161
|
+
healthy: boolean
|
|
162
|
+
fenced: boolean
|
|
163
|
+
podReadable: boolean
|
|
164
|
+
podExists: boolean
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export interface CNPGBackupMethod {
|
|
168
|
+
method: 'plugin' | 'volumeSnapshot' | 'barmanObjectStore'
|
|
169
|
+
pluginName?: string
|
|
170
|
+
capability: 'backup' | 'unknown' | 'none'
|
|
171
|
+
reason?: string
|
|
172
|
+
deprecated?: boolean
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface CNPGClusterFacts {
|
|
176
|
+
generation: number
|
|
177
|
+
currentPrimary?: string
|
|
178
|
+
targetPrimary?: string
|
|
179
|
+
phase?: string
|
|
180
|
+
phaseReason?: string
|
|
181
|
+
hibernation: string
|
|
182
|
+
hibernated: boolean
|
|
183
|
+
fencedInstances: { raw: string; all: boolean; instances: string[]; malformed?: boolean }
|
|
184
|
+
instances: CNPGInstanceFact[]
|
|
185
|
+
backupMethods: CNPGBackupMethod[]
|
|
186
|
+
backupTarget?: string
|
|
187
|
+
/** The ContinuousArchiving condition is False. */
|
|
188
|
+
archivingFailing: boolean
|
|
189
|
+
isReplicaCluster: boolean
|
|
190
|
+
maintenance: { declared: boolean; inProgress: boolean; reusePVC: boolean }
|
|
191
|
+
terminating: boolean
|
|
192
|
+
/** A running offline volume-snapshot Backup: the operator fenced its instance and lifts the fence itself. */
|
|
193
|
+
coldSnapshotBackup?: string
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export type CNPGClusterActionName =
|
|
197
|
+
| 'backup'
|
|
198
|
+
| 'switchover'
|
|
199
|
+
| 'restart'
|
|
200
|
+
| 'restartInstance'
|
|
201
|
+
| 'reload'
|
|
202
|
+
| 'fence'
|
|
203
|
+
| 'unfence'
|
|
204
|
+
| 'hibernate'
|
|
205
|
+
| 'rehydrate'
|
|
206
|
+
| 'setMaintenance'
|
|
207
|
+
| 'unsetMaintenance'
|
|
208
|
+
|
|
209
|
+
export interface CNPGClusterCapabilities {
|
|
210
|
+
uid: string
|
|
211
|
+
resourceVersion: string
|
|
212
|
+
context: string
|
|
213
|
+
facts: CNPGClusterFacts
|
|
214
|
+
/** `restore` is creating a new Cluster in this namespace from this one's backups (`create clusters`). */
|
|
215
|
+
actions: Record<CNPGClusterActionName, ActionCapability> & { psql: ActionCapability; destroyInstance: ActionCapability; restore: ActionCapability }
|
|
216
|
+
instanceActions: Record<string, { restart: ActionCapability; switchoverTarget: ActionCapability; fence: ActionCapability; unfence: ActionCapability; psql: ActionCapability; destroy: ActionCapability }>
|
|
217
|
+
restartPlan?: {
|
|
218
|
+
primaryUpdateStrategy?: string
|
|
219
|
+
primaryUpdateMethod?: string
|
|
220
|
+
steps: { instance: string; role: string; effect: 'recreate' | 'skipped_fenced' | 'switchover' | 'restart' | 'wait_for_user' | 'restart_only_instance' }[]
|
|
221
|
+
}
|
|
222
|
+
hibernateEffects?: {
|
|
223
|
+
poolers: CNPGEffectList
|
|
224
|
+
unsuspendedScheduledBackups: CNPGEffectList
|
|
225
|
+
databases: CNPGEffectList
|
|
226
|
+
publications: CNPGEffectList
|
|
227
|
+
subscriptions: CNPGEffectList
|
|
228
|
+
volumes: { available: boolean; reason?: string; items: { name: string; instance: string; role: string; capacity?: string; requested?: string }[] }
|
|
229
|
+
}
|
|
230
|
+
operator?: CNPGOperatorVerdict
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export interface CNPGEffectList {
|
|
234
|
+
available: boolean
|
|
235
|
+
reason?: string
|
|
236
|
+
names: string[]
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export type CNPGScheduleActionName = 'suspend' | 'resume' | 'run' | 'setSchedule'
|
|
240
|
+
|
|
241
|
+
export interface CNPGScheduleCapabilities {
|
|
242
|
+
uid: string
|
|
243
|
+
resourceVersion: string
|
|
244
|
+
context: string
|
|
245
|
+
facts: {
|
|
246
|
+
generation: number
|
|
247
|
+
cluster: string
|
|
248
|
+
clusterUID: string
|
|
249
|
+
clusterGeneration: number
|
|
250
|
+
suspended: boolean
|
|
251
|
+
nextScheduleTime?: string
|
|
252
|
+
method?: string
|
|
253
|
+
pluginName?: string
|
|
254
|
+
target?: string
|
|
255
|
+
clusterState: 'ok' | 'missing' | 'hibernated' | 'unreadable'
|
|
256
|
+
backupBlockedReason?: string
|
|
257
|
+
terminating: boolean
|
|
258
|
+
catchUp: boolean
|
|
259
|
+
/** spec.schedule verbatim; setSchedule binds it. */
|
|
260
|
+
schedule: string
|
|
261
|
+
preview: CNPGSchedulePreview
|
|
262
|
+
}
|
|
263
|
+
actions: Record<CNPGScheduleActionName, ActionCapability>
|
|
264
|
+
operator?: CNPGOperatorVerdict
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function cnpgPath(kind: 'clusters' | 'scheduledbackups' | 'poolers', namespace: string, name: string) {
|
|
268
|
+
return `/cnpg/${kind}/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}`
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function useCNPGClusterCapabilities(namespace: string, name: string, enabled = true) {
|
|
272
|
+
const { guard, gatedKey } = useRadarFeature('cnpgWorkspace')
|
|
273
|
+
return useQuery<CNPGClusterCapabilities>({
|
|
274
|
+
queryKey: ['cnpg', 'capabilities', 'clusters', namespace, name, ...gatedKey],
|
|
275
|
+
queryFn: ({ signal }) => guard(() => fetchJSON<CNPGClusterCapabilities>(`${cnpgPath('clusters', namespace, name)}/capabilities`, signal)),
|
|
276
|
+
enabled: enabled && !!name,
|
|
277
|
+
staleTime: 5_000,
|
|
278
|
+
retry: false,
|
|
279
|
+
})
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Parsed server-side with the operator's cron semantics; runs counted from the
|
|
283
|
+
// schedule's own lastCheckTime. Pure computation: writes nothing.
|
|
284
|
+
export function useCNPGSchedulePreview(namespace: string, name: string, schedule: string, enabled = true) {
|
|
285
|
+
const { guard, gatedKey } = useRadarFeature('cnpgWorkspace')
|
|
286
|
+
return useQuery<CNPGSchedulePreview>({
|
|
287
|
+
queryKey: ['cnpg', 'schedule-preview', namespace, name, schedule, ...gatedKey],
|
|
288
|
+
queryFn: ({ signal }) => guard(() =>
|
|
289
|
+
fetchJSON<CNPGSchedulePreview>(`${cnpgPath('scheduledbackups', namespace, name)}/schedule-preview?schedule=${encodeURIComponent(schedule)}`, signal)),
|
|
290
|
+
enabled: enabled && !!name,
|
|
291
|
+
staleTime: 30_000,
|
|
292
|
+
retry: false,
|
|
293
|
+
placeholderData: (prev) => prev,
|
|
294
|
+
})
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function useCNPGScheduleCapabilities(namespace: string, name: string, enabled = true) {
|
|
298
|
+
const { guard, gatedKey } = useRadarFeature('cnpgWorkspace')
|
|
299
|
+
return useQuery<CNPGScheduleCapabilities>({
|
|
300
|
+
queryKey: ['cnpg', 'capabilities', 'scheduledbackups', namespace, name, ...gatedKey],
|
|
301
|
+
queryFn: ({ signal }) => guard(() => fetchJSON<CNPGScheduleCapabilities>(`${cnpgPath('scheduledbackups', namespace, name)}/capabilities`, signal)),
|
|
302
|
+
enabled: enabled && !!name,
|
|
303
|
+
staleTime: 5_000,
|
|
304
|
+
retry: false,
|
|
305
|
+
})
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export interface CNPGActionResult {
|
|
309
|
+
action: string
|
|
310
|
+
message: string
|
|
311
|
+
backup?: string
|
|
312
|
+
resolvedAfterTimeout?: boolean
|
|
313
|
+
catchUp?: boolean
|
|
314
|
+
/** What the action acted on, for following its outcome. */
|
|
315
|
+
target?: { pod?: string; podUID?: string; pid?: number; backendStart?: string; keepPVC?: boolean; pvcs?: { name: string; uid: string }[]; jobs?: string[]; paused?: boolean; generation?: number }
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// No mutation meta: errors stay with the dialog (shown inline so the user can
|
|
319
|
+
// adjust and retry), and the caller toasts success worded from the result.
|
|
320
|
+
type CNPGActionMutation = {
|
|
321
|
+
clusters:
|
|
322
|
+
| { action: CNPGClusterActionName | 'destroyInstance'; request: ActionRequest<CNPGClusterFacts> }
|
|
323
|
+
| { action: 'cancelBackend' | 'terminateBackend'; request: ActionRequest<Record<string, never>, { pod: string; podUID?: string; pid: number; backendStart: string }> }
|
|
324
|
+
scheduledbackups: { action: CNPGScheduleActionName; request: ActionRequest<CNPGScheduleCapabilities['facts']> }
|
|
325
|
+
poolers: { action: 'pause' | 'resume'; request: ActionRequest<Pick<CNPGPoolerFacts, 'paused' | 'generation'>> }
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function useCNPGAction<Kind extends keyof CNPGActionMutation>(kind: Kind, namespace: string, name: string, reviewedContext: string | undefined) {
|
|
329
|
+
const { guard } = useRadarFeature('cnpgWorkspace')
|
|
330
|
+
const queryClient = useQueryClient()
|
|
331
|
+
return useReviewedActionMutation<CNPGActionResult, CNPGActionMutation[Kind]>(apiUrl(cnpgPath(kind, namespace, name)), reviewedContext, {
|
|
332
|
+
mutationFn: ({ action, request }) => guard(() =>
|
|
333
|
+
fetchJSON<CNPGActionResult>(`${cnpgPath(kind, namespace, name)}/actions/${action}`, {
|
|
334
|
+
method: 'POST',
|
|
335
|
+
headers: { 'Content-Type': 'application/json' },
|
|
336
|
+
body: JSON.stringify(request),
|
|
337
|
+
})),
|
|
338
|
+
onSuccess: () => {
|
|
339
|
+
queryClient.invalidateQueries({ queryKey: ['cnpg'] })
|
|
340
|
+
queryClient.invalidateQueries({ queryKey: ['resource'] })
|
|
341
|
+
},
|
|
342
|
+
// A refusal over changed facts re-reads them, so the dialog shows what
|
|
343
|
+
// the user would now be confirming.
|
|
344
|
+
onError: (err) => {
|
|
345
|
+
if (cnpgActionErrorCode(err) !== undefined) queryClient.invalidateQueries({ queryKey: ['cnpg'] })
|
|
346
|
+
},
|
|
347
|
+
})
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** The action refusals the CloudNativePG handlers add to the shared ones. */
|
|
351
|
+
export type CNPGActionErrorCode = ActionErrorCode | 'all_fenced' | 'operator_webhook_unavailable' | 'invalid_schedule'
|
|
352
|
+
|
|
353
|
+
export function cnpgActionErrorCode(err: unknown): CNPGActionErrorCode | undefined {
|
|
354
|
+
return actionErrorCode<CNPGActionErrorCode>(err)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export type CNPGRuntimeSourceState = 'ok' | 'denied' | 'unreachable' | 'error' | 'partial'
|
|
358
|
+
|
|
359
|
+
export interface CNPGRuntimeReplication {
|
|
360
|
+
applicationName: string
|
|
361
|
+
state?: string
|
|
362
|
+
syncState?: string
|
|
363
|
+
syncPriority?: number
|
|
364
|
+
writeLag?: number
|
|
365
|
+
flushLag?: number
|
|
366
|
+
replayLag?: number
|
|
367
|
+
sentLsn?: string
|
|
368
|
+
writeLsn?: string
|
|
369
|
+
flushLsn?: string
|
|
370
|
+
replayLsn?: string
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export interface CNPGRuntimeBaseBackup {
|
|
374
|
+
applicationName: string
|
|
375
|
+
instance?: string
|
|
376
|
+
phase: string
|
|
377
|
+
startedAt?: string
|
|
378
|
+
/** Absent while PostgreSQL has no estimate (waiting for a checkpoint, or estimation disabled). */
|
|
379
|
+
totalBytes?: number
|
|
380
|
+
streamedBytes: number
|
|
381
|
+
tablespacesTotal: number
|
|
382
|
+
tablespacesStreamed: number
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export interface CNPGRuntimeInstance {
|
|
386
|
+
pod: string
|
|
387
|
+
podUID?: string
|
|
388
|
+
role: 'primary' | 'replica' | 'unknown'
|
|
389
|
+
status: {
|
|
390
|
+
state: CNPGRuntimeSourceState
|
|
391
|
+
error?: string
|
|
392
|
+
reason?: string
|
|
393
|
+
capturedAt?: string
|
|
394
|
+
isPrimary?: boolean
|
|
395
|
+
currentLsn?: string
|
|
396
|
+
receivedLsn?: string
|
|
397
|
+
replayLsn?: string
|
|
398
|
+
timeline?: number
|
|
399
|
+
replayPaused?: boolean
|
|
400
|
+
pendingRestart?: boolean
|
|
401
|
+
pendingRestartForDecrease?: boolean
|
|
402
|
+
isPgRewindRunning?: boolean
|
|
403
|
+
instanceManagerVersion?: string
|
|
404
|
+
roleDetail?: 'primary' | 'pgRewind' | 'replayPaused' | 'streaming' | 'fileBased'
|
|
405
|
+
isWalReceiverActive?: boolean
|
|
406
|
+
archiving?: { lastArchivedWal?: string; lastArchivedAt?: string; lastFailedWal?: string; lastFailedAt?: string; readyWalFiles?: number }
|
|
407
|
+
replication?: CNPGRuntimeReplication[] | null
|
|
408
|
+
slots?: null | { name: string; type?: string; plugin?: string; active?: boolean; database?: string; restartLsn?: string; walStatus?: string; retainedBytes?: number }[]
|
|
409
|
+
slotsTruncated?: boolean
|
|
410
|
+
/** pg_basebackup streams to joining instances; [] when the report says none run. */
|
|
411
|
+
baseBackups?: CNPGRuntimeBaseBackup[] | null
|
|
412
|
+
/**
|
|
413
|
+
* The instance manager answered without finishing its reads (a masked
|
|
414
|
+
* error, or pg_rewind running): replication, slots, baseBackups and
|
|
415
|
+
* archiving are null when unread, and pendingRestart is not established.
|
|
416
|
+
*/
|
|
417
|
+
incomplete?: boolean
|
|
418
|
+
maskedError?: string
|
|
419
|
+
}
|
|
420
|
+
metrics: {
|
|
421
|
+
state: CNPGRuntimeSourceState
|
|
422
|
+
error?: string
|
|
423
|
+
reason?: string
|
|
424
|
+
capturedAt?: string
|
|
425
|
+
missing?: string[]
|
|
426
|
+
maxConnections?: number
|
|
427
|
+
sessions?: { state: string; database: string; user: string; application: string; count: number }[]
|
|
428
|
+
sessionsTotal?: number
|
|
429
|
+
waitingBackends?: number
|
|
430
|
+
oldestXactSeconds?: number
|
|
431
|
+
xidAge?: { database: string; age: number }[]
|
|
432
|
+
mxidAge?: { database: string; age: number }[]
|
|
433
|
+
/** null when cnpg_pg_extensions_update_available was not exported (see missing); [] when all are current. */
|
|
434
|
+
extensionUpdates?: { database: string; extension: string; installedVersion: string; defaultVersion: string }[] | null
|
|
435
|
+
databaseSizes?: { database: string; bytes: number }[]
|
|
436
|
+
archiver?: { archivedCount?: number; failedCount?: number; secondsSinceLastArchival?: number; secondsSinceLastFailure?: number }
|
|
437
|
+
xactCommitTotal?: number
|
|
438
|
+
xactRollbackTotal?: number
|
|
439
|
+
blksHit?: number
|
|
440
|
+
blksRead?: number
|
|
441
|
+
deadlocksTotal?: number
|
|
442
|
+
tempBytesTotal?: number
|
|
443
|
+
walBytes?: number
|
|
444
|
+
/** cnpg_last_update_timestamp: when the exporter last ran its queries (epoch seconds). */
|
|
445
|
+
lastUpdateTimestamp?: number
|
|
446
|
+
/** Client sessions per state over every group (absent when there are none). */
|
|
447
|
+
sessionsByState?: Record<string, number>
|
|
448
|
+
walSegments?: number
|
|
449
|
+
postmasterStartTime?: number
|
|
450
|
+
/** pg_stat_database counters per database, cumulative since the last stats reset. */
|
|
451
|
+
databases?: { database: string; xactCommit?: number; xactRollback?: number; tempFiles?: number; tempBytes?: number; deadlocks?: number; blksHit?: number; blksRead?: number }[]
|
|
452
|
+
/** Absent when neither pg_stat_checkpointer (17+) nor pg_stat_bgwriter was exported. */
|
|
453
|
+
checkpoints?: {
|
|
454
|
+
source: 'pg_stat_checkpointer' | 'pg_stat_bgwriter'
|
|
455
|
+
timed?: number
|
|
456
|
+
requested?: number
|
|
457
|
+
restartpointsTimed?: number
|
|
458
|
+
restartpointsRequested?: number
|
|
459
|
+
restartpointsDone?: number
|
|
460
|
+
buffersWritten?: number
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export interface CNPGRuntimeResponse {
|
|
466
|
+
cluster: { namespace: string; name: string; uid: string }
|
|
467
|
+
sampledAt: string
|
|
468
|
+
permission: { proxy: 'allowed' | 'denied'; grant?: Grant }
|
|
469
|
+
instances: CNPGRuntimeInstance[]
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// /api/cnpg/clusters/{ns}/{name}/runtime — live instance-manager status and
|
|
473
|
+
// exporter metrics read through pods/proxy. Polled only while visible.
|
|
474
|
+
export function useCNPGRuntime(namespace: string, name: string, enabled = true) {
|
|
475
|
+
const { guard, gatedKey } = useRadarFeature('cnpgWorkspace')
|
|
476
|
+
return useQuery<CNPGRuntimeResponse>({
|
|
477
|
+
queryKey: ['cnpg', 'runtime', namespace, name, ...gatedKey],
|
|
478
|
+
queryFn: ({ signal }) => guard(() => fetchJSON<CNPGRuntimeResponse>(`/cnpg/clusters/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}/runtime`, signal)),
|
|
479
|
+
enabled: enabled && !!name,
|
|
480
|
+
refetchInterval: (q) => (q.state.data?.permission.proxy === 'denied' ? false : 5_000),
|
|
481
|
+
refetchIntervalInBackground: false,
|
|
482
|
+
staleTime: 4_000,
|
|
483
|
+
retry: false,
|
|
484
|
+
})
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
export interface CNPGPoolerRuntimeResponse {
|
|
488
|
+
pooler: { namespace: string; name: string; uid: string }
|
|
489
|
+
sampledAt: string
|
|
490
|
+
permission: { proxy: 'allowed' | 'denied'; grant?: Grant }
|
|
491
|
+
pods: {
|
|
492
|
+
pod: string
|
|
493
|
+
schedulingReason?: string
|
|
494
|
+
state: CNPGRuntimeSourceState
|
|
495
|
+
error?: string
|
|
496
|
+
reason?: string
|
|
497
|
+
missing?: string[]
|
|
498
|
+
pools?: { database: string; user: string; clActive?: number; clWaiting?: number; svActive?: number; svIdle?: number; svUsed?: number; maxwaitSeconds?: number; poolMode?: string }[]
|
|
499
|
+
}[]
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
export function useCNPGPoolerRuntime(namespace: string, name: string, enabled = true) {
|
|
503
|
+
const { guard, gatedKey } = useRadarFeature('cnpgWorkspace')
|
|
504
|
+
return useQuery<CNPGPoolerRuntimeResponse>({
|
|
505
|
+
queryKey: ['cnpg', 'pooler-runtime', namespace, name, ...gatedKey],
|
|
506
|
+
queryFn: ({ signal }) => guard(() => fetchJSON<CNPGPoolerRuntimeResponse>(`/cnpg/poolers/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}/runtime`, signal)),
|
|
507
|
+
enabled: enabled && !!name,
|
|
508
|
+
refetchInterval: 30_000,
|
|
509
|
+
refetchIntervalInBackground: false,
|
|
510
|
+
staleTime: 25_000,
|
|
511
|
+
retry: false,
|
|
94
512
|
})
|
|
95
513
|
}
|
package/src/api/diagnose.test.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
addTurn,
|
|
4
4
|
createRun,
|
|
5
5
|
DiagnoseError,
|
|
6
|
+
fetchAgents,
|
|
6
7
|
subscribeRun,
|
|
7
8
|
type DiagnoseStreamEvent,
|
|
8
9
|
} from "./diagnose";
|
|
@@ -367,3 +368,42 @@ describe("refused requests", () => {
|
|
|
367
368
|
expect((await startError()).message).toBe(message);
|
|
368
369
|
});
|
|
369
370
|
});
|
|
371
|
+
|
|
372
|
+
describe("fetchAgents agents normalisation", () => {
|
|
373
|
+
const withFetch = async (body: unknown) => {
|
|
374
|
+
const original = globalThis.fetch;
|
|
375
|
+
globalThis.fetch = (async () =>
|
|
376
|
+
new Response(JSON.stringify(body), {
|
|
377
|
+
status: 200,
|
|
378
|
+
headers: { "content-type": "application/json" },
|
|
379
|
+
})) as typeof fetch;
|
|
380
|
+
try {
|
|
381
|
+
return await fetchAgents();
|
|
382
|
+
} finally {
|
|
383
|
+
globalThis.fetch = original;
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
it("turns the server's null agent list into an empty array", async () => {
|
|
388
|
+
// Older servers send null for a machine with no CLI installed, the case
|
|
389
|
+
// callers most need to survive.
|
|
390
|
+
const r = await withFetch({
|
|
391
|
+
agents: null,
|
|
392
|
+
enabled: false,
|
|
393
|
+
eligible: true,
|
|
394
|
+
consented: {},
|
|
395
|
+
});
|
|
396
|
+
expect(r.agents).toEqual([]);
|
|
397
|
+
expect(r.eligible).toBe(true);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it("leaves a populated list alone", async () => {
|
|
401
|
+
const r = await withFetch({
|
|
402
|
+
agents: [{ name: "claude", label: "Claude Code", supported: true }],
|
|
403
|
+
enabled: true,
|
|
404
|
+
eligible: true,
|
|
405
|
+
consented: {},
|
|
406
|
+
});
|
|
407
|
+
expect(r.agents.map((a) => a.name)).toEqual(["claude"]);
|
|
408
|
+
});
|
|
409
|
+
});
|
package/src/api/diagnose.ts
CHANGED
|
@@ -29,6 +29,10 @@ export interface AgentsResponse {
|
|
|
29
29
|
// "install an agent to enable this" (eligible && !enabled) apart from "not
|
|
30
30
|
// available here" (auth/cloud/--no-mcp). Absent on older servers / embed hosts.
|
|
31
31
|
eligible?: boolean;
|
|
32
|
+
// cliOverride: RADAR_AI_CLI_BIN is set, pinning the engine to one CLI. With
|
|
33
|
+
// CLIs detected and investigations still off, that variable is what's broken.
|
|
34
|
+
// Absent on older servers and embed hosts.
|
|
35
|
+
cliOverride?: boolean;
|
|
32
36
|
// Machine-scoped consent per disclosure surface, recorded server-side
|
|
33
37
|
// (~/.radar) — one acknowledgment covers the web panel and the CLI.
|
|
34
38
|
consented?: Record<string, boolean>;
|
|
@@ -257,7 +261,14 @@ export async function fetchAgents(
|
|
|
257
261
|
signal,
|
|
258
262
|
});
|
|
259
263
|
if (!res.ok) throw new Error(`agents: ${res.status}`);
|
|
260
|
-
|
|
264
|
+
const body = (await res.json()) as AgentsResponse & {
|
|
265
|
+
agents: AgentInfo[] | null;
|
|
266
|
+
};
|
|
267
|
+
// Older Radar servers send "no agent found" as JSON null, not []. This client
|
|
268
|
+
// also runs against servers of other versions, and that is the single most
|
|
269
|
+
// common response on a machine with no CLI, so normalise here rather than in
|
|
270
|
+
// every caller that reaches for .filter.
|
|
271
|
+
return { ...body, agents: body.agents ?? [] };
|
|
261
272
|
}
|
|
262
273
|
|
|
263
274
|
/**
|
|
@@ -4,7 +4,7 @@ import { createRoot } from 'react-dom/client'
|
|
|
4
4
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
5
5
|
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
|
|
6
6
|
import { getRadarUpgradeRequirement } from '@skyhook-io/k8s-ui'
|
|
7
|
-
import { ApiError, isNotFoundError, useApplications, useCapacityPoolDetail, useDrainPlan, usePodEnvironment, useWorkloadHistory } from './client'
|
|
7
|
+
import { ApiError, isNotFoundError, useApplications, useCapacityPoolDetail, useDrainPlan, useNodeRemovalPlan, usePodEnvironment, useSubjectIssues, useWorkloadHistory } from './client'
|
|
8
8
|
import { getApiBase } from './config'
|
|
9
9
|
import { usePolicyResource } from './policy'
|
|
10
10
|
import { isRadarFeatureUnsupported } from './radarFeatures'
|
|
@@ -35,6 +35,7 @@ beforeEach(() => {
|
|
|
35
35
|
if (url.includes('/capacity/pools/')) return Promise.resolve(Response.json({ pool: { name: 'default' } }))
|
|
36
36
|
if (url.endsWith('/environment')) return Promise.resolve(Response.json({ containers: [], coverage: {} }))
|
|
37
37
|
if (url.includes('/policy/resource/')) return Promise.resolve(Response.json({ evaluated: true, status: 'ready' }))
|
|
38
|
+
if (url.includes('/issues/resource/')) return Promise.resolve(Response.json({ issues: [], coverage: 'ok' }))
|
|
38
39
|
return new Promise<Response>(() => {})
|
|
39
40
|
}))
|
|
40
41
|
element = document.createElement('div')
|
|
@@ -73,6 +74,49 @@ function Policy() {
|
|
|
73
74
|
return <span>{data?.status ?? (getRadarUpgradeRequirement(error) ? 'upgrade' : 'pending')}</span>
|
|
74
75
|
}
|
|
75
76
|
|
|
77
|
+
function SubjectIssues() {
|
|
78
|
+
const { data, error } = useSubjectIssues({ kind: 'Pod', group: '', namespace: 'shop', name: 'web' }, true)
|
|
79
|
+
return <span>{data ? 'data' : getRadarUpgradeRequirement(error) ? 'upgrade' : error ? 'unconfirmed' : 'pending'}</span>
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
it('waits for coverage capabilities instead of probing an older array-shaped issues route', async () => {
|
|
83
|
+
await render(<SubjectIssues />)
|
|
84
|
+
expect(element.textContent).toBe('pending')
|
|
85
|
+
expect(asked('/issues/resource/')).toBe(false)
|
|
86
|
+
|
|
87
|
+
await act(async () => { client.setQueryData(['capabilities'], { features: { resourceIssues: true } }) })
|
|
88
|
+
await settle()
|
|
89
|
+
await settle()
|
|
90
|
+
expect(element.textContent).toBe('upgrade')
|
|
91
|
+
expect(asked('/issues/resource/')).toBe(false)
|
|
92
|
+
|
|
93
|
+
await act(async () => { client.setQueryData(['capabilities'], { features: { resourceIssueCoverage: true } }) })
|
|
94
|
+
await settle()
|
|
95
|
+
await settle()
|
|
96
|
+
expect(element.textContent).toBe('data')
|
|
97
|
+
expect(asked('/issues/resource/')).toBe(true)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('leaves issue coverage unconfirmed after a failed capability check and recovers when it succeeds', async () => {
|
|
101
|
+
const answer = vi.mocked(fetch).getMockImplementation()!
|
|
102
|
+
vi.mocked(fetch).mockImplementation((input, init) =>
|
|
103
|
+
String(input).endsWith('/capabilities')
|
|
104
|
+
? Promise.resolve(Response.json({ error: 'capabilities unavailable' }, { status: 503 }))
|
|
105
|
+
: answer(input, init),
|
|
106
|
+
)
|
|
107
|
+
await render(<SubjectIssues />)
|
|
108
|
+
await settle()
|
|
109
|
+
await settle()
|
|
110
|
+
expect(element.textContent).toBe('unconfirmed')
|
|
111
|
+
expect(asked('/issues/resource/')).toBe(false)
|
|
112
|
+
|
|
113
|
+
await act(async () => { client.setQueryData(['capabilities'], { features: { resourceIssueCoverage: true } }) })
|
|
114
|
+
await settle()
|
|
115
|
+
await settle()
|
|
116
|
+
expect(element.textContent).toBe('data')
|
|
117
|
+
expect(asked('/issues/resource/')).toBe(true)
|
|
118
|
+
})
|
|
119
|
+
|
|
76
120
|
it('fetches for real once the agent turns out to advertise the feature', async () => {
|
|
77
121
|
await render(<Environment />)
|
|
78
122
|
expect(element.textContent).toBe('upgrade')
|
|
@@ -90,14 +134,14 @@ it('turns an unsupported policy answer into not installed once discovery shows n
|
|
|
90
134
|
// No upgrade note yet: discovery may still withdraw it.
|
|
91
135
|
expect(element.textContent).toBe('loading')
|
|
92
136
|
|
|
93
|
-
await act(async () => { client.setQueryData(['api-resources'], [{ group: '', name: 'pods' }]) })
|
|
137
|
+
await act(async () => { client.setQueryData(['api-resources'], [{ group: '', version: 'v1', kind: 'Pod', name: 'pods', namespaced: true, isCrd: false, verbs: ['get', 'list', 'watch'] }]) })
|
|
94
138
|
await settle()
|
|
95
139
|
expect(element.textContent).toBe('not_installed')
|
|
96
140
|
expect(asked('/policy/resource/')).toBe(false)
|
|
97
141
|
})
|
|
98
142
|
|
|
99
143
|
it('keeps the upgrade note when discovery shows an OpenReports policy engine', async () => {
|
|
100
|
-
client.setQueryData(['api-resources'], [{ group: 'openreports.io', name: 'reports' }])
|
|
144
|
+
client.setQueryData(['api-resources'], [{ group: 'openreports.io', version: 'v1alpha1', kind: 'Report', name: 'reports', namespaced: true, isCrd: true, verbs: ['get', 'list', 'watch'] }])
|
|
101
145
|
await render(<Policy />)
|
|
102
146
|
expect(element.textContent).toBe('upgrade')
|
|
103
147
|
})
|
|
@@ -153,6 +197,24 @@ function DrainPlanProbe() {
|
|
|
153
197
|
return null
|
|
154
198
|
}
|
|
155
199
|
|
|
200
|
+
function RemovalPlanProbe() {
|
|
201
|
+
const { error } = useNodeRemovalPlan('worker-1', true)
|
|
202
|
+
return <span>{getRadarUpgradeRequirement(error) ? 'upgrade' : 'pending'}</span>
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
it('does not poll an old Radar for removal details', async () => {
|
|
206
|
+
host.radarVersion = 'v1.13.1'
|
|
207
|
+
await render(<RemovalPlanProbe />)
|
|
208
|
+
expect(element.textContent).toBe('upgrade')
|
|
209
|
+
expect(asked('/drain-plan')).toBe(false)
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('requests removal details once the drain-plan feature is supported', async () => {
|
|
213
|
+
client.setQueryData(['capabilities'], { features: { resourceIssues: true } })
|
|
214
|
+
await render(<RemovalPlanProbe />)
|
|
215
|
+
expect(asked('/drain-plan')).toBe(true)
|
|
216
|
+
})
|
|
217
|
+
|
|
156
218
|
it('never asks a Radar older than v1.14 for a drain plan', async () => {
|
|
157
219
|
host.radarVersion = 'v1.13.1'
|
|
158
220
|
await render(<DrainPlanProbe />)
|