@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.
Files changed (321) hide show
  1. package/README.md +1 -1
  2. package/package.json +3 -3
  3. package/src/App.tsx +138 -93
  4. package/src/RadarApp.tsx +27 -17
  5. package/src/api/actions.errorCode.test.ts +40 -0
  6. package/src/api/actions.test.ts +48 -0
  7. package/src/api/actions.ts +79 -0
  8. package/src/api/apiResources.navigation.test.tsx +232 -0
  9. package/src/api/apiResources.ts +46 -3
  10. package/src/api/client.goneResource.test.ts +49 -0
  11. package/src/api/client.helmSource.test.ts +39 -0
  12. package/src/api/client.namespaceSwitch.hook.test.tsx +54 -0
  13. package/src/api/client.namespaceSwitch.test.ts +107 -0
  14. package/src/api/client.ts +381 -47
  15. package/src/api/cnpg-ha.live.test.ts +140 -0
  16. package/src/api/cnpg-ha.test.ts +78 -0
  17. package/src/api/cnpg-ha.ts +19 -0
  18. package/src/api/cnpg-history.ts +123 -0
  19. package/src/api/cnpg-inspect.ts +102 -0
  20. package/src/api/cnpg-protection.ts +112 -0
  21. package/src/api/cnpg-recovery.test.tsx +60 -0
  22. package/src/api/cnpg-recovery.ts +262 -0
  23. package/src/api/cnpg-sessions.ts +175 -0
  24. package/src/api/cnpg-storage.ts +138 -0
  25. package/src/api/cnpg.legacy-reads.test.ts +43 -0
  26. package/src/api/cnpg.pooler-runtime.test.ts +15 -0
  27. package/src/api/cnpg.ts +428 -10
  28. package/src/api/diagnose.test.ts +40 -0
  29. package/src/api/diagnose.ts +12 -1
  30. package/src/api/radarFeatures.render.test.tsx +65 -3
  31. package/src/api/radarFeatures.ts +6 -0
  32. package/src/api/reviewedActionMutation.test.tsx +164 -0
  33. package/src/api/reviewedActionMutation.ts +66 -0
  34. package/src/api/timelineSource.test.ts +9 -0
  35. package/src/api/timelineSource.ts +8 -0
  36. package/src/api/traffic.sources.test.ts +55 -0
  37. package/src/api/traffic.ts +173 -14
  38. package/src/components/applications/ApplicationsView.tsx +3 -0
  39. package/src/components/audit/AuditView.tsx +9 -0
  40. package/src/components/capacity/CapacityActivity.tsx +3 -31
  41. package/src/components/capacity/CapacityDemand.tsx +4 -36
  42. package/src/components/capacity/CapacityOverview.tsx +217 -56
  43. package/src/components/capacity/CapacityPoolDetail.tsx +24 -62
  44. package/src/components/capacity/CapacityView.test.tsx +354 -1
  45. package/src/components/capacity/ClusterSchedulingCard.tsx +2 -10
  46. package/src/components/capacity/certaintyGlyph.test.tsx +2 -9
  47. package/src/components/capacity/routes.ts +42 -0
  48. package/src/components/capacity/shared.tsx +23 -150
  49. package/src/components/capacity/workspaceRoute.ts +18 -0
  50. package/src/components/cnpg/CNPGActionHost.compatibility.test.tsx +30 -0
  51. package/src/components/cnpg/CNPGArchivingRepair.tsx +191 -0
  52. package/src/components/cnpg/CNPGBlockingSessions.test.tsx +50 -0
  53. package/src/components/cnpg/CNPGBlockingSessions.tsx +333 -0
  54. package/src/components/cnpg/CNPGClusterActivity.test.tsx +11 -0
  55. package/src/components/cnpg/CNPGClusterActivity.tsx +18 -9
  56. package/src/components/cnpg/CNPGClusterLogs.compatibility.test.tsx +130 -0
  57. package/src/components/cnpg/CNPGClusterLogs.empty.test.tsx +29 -0
  58. package/src/components/cnpg/CNPGClusterLogs.test.ts +9 -0
  59. package/src/components/cnpg/CNPGClusterLogs.tsx +81 -24
  60. package/src/components/cnpg/CNPGClusterTabs.test.tsx +152 -0
  61. package/src/components/cnpg/CNPGClusterTabs.tsx +187 -0
  62. package/src/components/cnpg/CNPGClustersKindView.tsx +46 -0
  63. package/src/components/cnpg/CNPGConfiguration.test.tsx +223 -0
  64. package/src/components/cnpg/CNPGConfiguration.tsx +190 -0
  65. package/src/components/cnpg/CNPGCreateClusterDialog.test.tsx +113 -0
  66. package/src/components/cnpg/CNPGCreateClusterDialog.tsx +74 -0
  67. package/src/components/cnpg/CNPGDeclarations.test.ts +107 -0
  68. package/src/components/cnpg/CNPGDeclarations.tsx +236 -78
  69. package/src/components/cnpg/CNPGDetailPage.test.tsx +62 -0
  70. package/src/components/cnpg/CNPGDetailPage.tsx +159 -98
  71. package/src/components/cnpg/CNPGDrawerTrail.tsx +2 -2
  72. package/src/components/cnpg/CNPGInvestigationAction.test.tsx +30 -0
  73. package/src/components/cnpg/CNPGOperator.test.tsx +34 -0
  74. package/src/components/cnpg/CNPGOperator.tsx +78 -30
  75. package/src/components/cnpg/CNPGOperatorBanner.tsx +39 -0
  76. package/src/components/cnpg/CNPGOperatorConditions.test.tsx +45 -0
  77. package/src/components/cnpg/CNPGOperatorConditions.tsx +16 -0
  78. package/src/components/cnpg/CNPGOperatorConfig.test.tsx +27 -0
  79. package/src/components/cnpg/CNPGOperatorDiagnosis.tsx +247 -0
  80. package/src/components/cnpg/CNPGOverview.test.tsx +171 -0
  81. package/src/components/cnpg/CNPGOverview.tsx +238 -63
  82. package/src/components/cnpg/CNPGParametersInEffect.test.tsx +51 -0
  83. package/src/components/cnpg/CNPGParametersInEffect.tsx +120 -0
  84. package/src/components/cnpg/CNPGPerformance.prerequisite.test.tsx +48 -0
  85. package/src/components/cnpg/CNPGPerformance.test.tsx +73 -0
  86. package/src/components/cnpg/CNPGPerformance.tsx +477 -0
  87. package/src/components/cnpg/CNPGPooling.test.tsx +89 -0
  88. package/src/components/cnpg/CNPGPooling.tsx +97 -40
  89. package/src/components/cnpg/CNPGProtection.test.tsx +145 -0
  90. package/src/components/cnpg/CNPGProtection.tsx +230 -124
  91. package/src/components/cnpg/CNPGReplicationTab.test.tsx +87 -0
  92. package/src/components/cnpg/CNPGReplicationTab.tsx +212 -0
  93. package/src/components/cnpg/CNPGReplicationView.tsx +264 -0
  94. package/src/components/cnpg/CNPGStorage.test.tsx +140 -0
  95. package/src/components/cnpg/CNPGStorage.tsx +619 -0
  96. package/src/components/cnpg/CNPGSummaryHost.overview.test.tsx +44 -0
  97. package/src/components/cnpg/CNPGSummaryHost.test.tsx +23 -0
  98. package/src/components/cnpg/CNPGSummaryHost.tsx +228 -54
  99. package/src/components/cnpg/CNPGTrends.denied.test.tsx +33 -0
  100. package/src/components/cnpg/CNPGTrends.missing.test.tsx +69 -0
  101. package/src/components/cnpg/CNPGTrends.test.ts +38 -0
  102. package/src/components/cnpg/CNPGTrends.tsx +702 -0
  103. package/src/components/cnpg/CNPGView.tsx +34 -21
  104. package/src/components/cnpg/CNPGWorkspace.compatibility.test.tsx +90 -0
  105. package/src/components/cnpg/actions/CNPGClusterActions.dialog.test.tsx +79 -0
  106. package/src/components/cnpg/actions/CNPGClusterActions.test.tsx +142 -0
  107. package/src/components/cnpg/actions/CNPGClusterActions.tsx +853 -0
  108. package/src/components/cnpg/actions/CNPGConnectButton.read.test.tsx +40 -0
  109. package/src/components/cnpg/actions/CNPGConnectButton.test.ts +25 -0
  110. package/src/components/cnpg/actions/CNPGConnectButton.tsx +131 -0
  111. package/src/components/cnpg/actions/CNPGDestroyInstanceDialog.tsx +185 -0
  112. package/src/components/cnpg/actions/CNPGInstanceActions.test.tsx +38 -0
  113. package/src/components/cnpg/actions/CNPGInstanceActions.tsx +74 -0
  114. package/src/components/cnpg/actions/CNPGMaintenanceBanner.tsx +53 -0
  115. package/src/components/cnpg/actions/CNPGPoolerActions.test.tsx +35 -0
  116. package/src/components/cnpg/actions/CNPGPoolerActions.tsx +79 -0
  117. package/src/components/cnpg/actions/CNPGReportDialog.test.tsx +12 -0
  118. package/src/components/cnpg/actions/CNPGReportDialog.tsx +91 -0
  119. package/src/components/cnpg/actions/CNPGScheduleActions.run.test.tsx +58 -0
  120. package/src/components/cnpg/actions/CNPGScheduleActions.test.tsx +54 -0
  121. package/src/components/cnpg/actions/CNPGScheduleActions.tsx +208 -0
  122. package/src/components/cnpg/actions/actionModel.test.ts +111 -0
  123. package/src/components/cnpg/actions/actionModel.ts +127 -0
  124. package/src/components/cnpg/actions/renderCNPGHeaderActions.tsx +60 -0
  125. package/src/components/cnpg/actions/useCNPGWriteGuard.tsx +58 -0
  126. package/src/components/cnpg/actions/useOpenCNPGPsql.ts +22 -0
  127. package/src/components/cnpg/archivingRepair.test.ts +104 -0
  128. package/src/components/cnpg/archivingRepair.ts +145 -0
  129. package/src/components/cnpg/baseBackup.test.ts +36 -0
  130. package/src/components/cnpg/baseBackup.ts +36 -0
  131. package/src/components/cnpg/blocking.test.ts +59 -0
  132. package/src/components/cnpg/blocking.ts +131 -0
  133. package/src/components/cnpg/coverageText.test.ts +27 -0
  134. package/src/components/cnpg/creation/CNPGTargetFields.tsx +110 -0
  135. package/src/components/cnpg/creation/targetModel.test.ts +137 -0
  136. package/src/components/cnpg/creation/targetModel.ts +149 -0
  137. package/src/components/cnpg/fleetStatus.test.ts +46 -0
  138. package/src/components/cnpg/fleetStatus.ts +43 -0
  139. package/src/components/cnpg/formFields.ts +2 -0
  140. package/src/components/cnpg/grantText.test.tsx +11 -0
  141. package/src/components/cnpg/host.test.tsx +24 -0
  142. package/src/components/cnpg/host.tsx +101 -0
  143. package/src/components/cnpg/inspectModel.test.ts +120 -0
  144. package/src/components/cnpg/inspectModel.ts +140 -0
  145. package/src/components/cnpg/logicalSlots.test.ts +30 -0
  146. package/src/components/cnpg/logicalSlots.ts +29 -0
  147. package/src/components/cnpg/lsn.test.ts +40 -0
  148. package/src/components/cnpg/lsn.ts +43 -0
  149. package/src/components/cnpg/operations/CNPGFleetOperation.tsx +30 -0
  150. package/src/components/cnpg/operations/CNPGOperationTracker.test.tsx +247 -0
  151. package/src/components/cnpg/operations/CNPGOperationTracker.tsx +255 -0
  152. package/src/components/cnpg/operations/model.test.ts +510 -0
  153. package/src/components/cnpg/operations/model.ts +499 -0
  154. package/src/components/cnpg/operations/presentation.test.ts +80 -0
  155. package/src/components/cnpg/operations/presentation.ts +84 -0
  156. package/src/components/cnpg/operations/store.ts +98 -0
  157. package/src/components/cnpg/operatorStatus.test.ts +134 -0
  158. package/src/components/cnpg/operatorStatus.ts +194 -0
  159. package/src/components/cnpg/paths.test.ts +54 -0
  160. package/src/components/cnpg/paths.ts +47 -4
  161. package/src/components/cnpg/protection/CNPGAttachArchiveDialog.tsx +185 -0
  162. package/src/components/cnpg/protection/CNPGCreateScheduleDialog.tsx +196 -0
  163. package/src/components/cnpg/protection/CNPGProtectionSetup.test.tsx +121 -0
  164. package/src/components/cnpg/protection/CNPGProtectionSetup.tsx +452 -0
  165. package/src/components/cnpg/protection/CNPGScheduleInput.tsx +115 -0
  166. package/src/components/cnpg/protection/CNPGScheduleRepairButton.tsx +120 -0
  167. package/src/components/cnpg/protection/scheduleModel.test.ts +19 -0
  168. package/src/components/cnpg/protection/scheduleModel.ts +16 -0
  169. package/src/components/cnpg/recovery/CNPGRecoveryPoint.tsx +220 -0
  170. package/src/components/cnpg/recovery/CNPGRestoreButton.test.tsx +29 -0
  171. package/src/components/cnpg/recovery/CNPGRestoreButton.tsx +26 -0
  172. package/src/components/cnpg/recovery/CNPGRestoreChecks.test.tsx +23 -0
  173. package/src/components/cnpg/recovery/CNPGRestoreChecks.tsx +153 -0
  174. package/src/components/cnpg/recovery/CNPGRestoreDialog.back.test.tsx +149 -0
  175. package/src/components/cnpg/recovery/CNPGRestoreDialog.test.tsx +46 -0
  176. package/src/components/cnpg/recovery/CNPGRestoreDialog.tsx +333 -0
  177. package/src/components/cnpg/recovery/CNPGRestoreProgress.tsx +182 -0
  178. package/src/components/cnpg/recovery/CNPGRestoreValidation.tsx +216 -0
  179. package/src/components/cnpg/recovery/restoreModel.test.ts +709 -0
  180. package/src/components/cnpg/recovery/restoreModel.ts +685 -0
  181. package/src/components/cnpg/recovery/restoreOperation.ts +56 -0
  182. package/src/components/cnpg/refresh.test.ts +38 -0
  183. package/src/components/cnpg/refresh.ts +17 -0
  184. package/src/components/cnpg/refreshNotice.test.tsx +29 -0
  185. package/src/components/cnpg/routes.test.ts +34 -3
  186. package/src/components/cnpg/routes.ts +60 -48
  187. package/src/components/cnpg/runtimeAssessment.ts +198 -0
  188. package/src/components/cnpg/runtimeModel.test.ts +175 -0
  189. package/src/components/cnpg/runtimeModel.ts +196 -0
  190. package/src/components/cnpg/runtimeParts.test.tsx +17 -0
  191. package/src/components/cnpg/runtimeParts.tsx +79 -0
  192. package/src/components/cnpg/shared.test.tsx +59 -0
  193. package/src/components/cnpg/shared.tsx +100 -182
  194. package/src/components/cnpg/storageModel.test.ts +107 -0
  195. package/src/components/cnpg/storageModel.ts +101 -0
  196. package/src/components/cnpg/trendSamples.test.ts +148 -0
  197. package/src/components/cnpg/trendSamples.ts +222 -0
  198. package/src/components/cnpg/useCNPGClusterAssessment.ts +30 -0
  199. package/src/components/cnpg/useCNPGKubectlContext.test.tsx +45 -0
  200. package/src/components/cnpg/useCNPGKubectlContext.ts +13 -0
  201. package/src/components/cnpg/useCNPGNavigate.test.tsx +58 -0
  202. package/src/components/cnpg/useCNPGNavigate.ts +21 -0
  203. package/src/components/cnpg/useCNPGPoolerLive.ts +46 -0
  204. package/src/components/cnpg/useCNPGScreenParams.ts +24 -0
  205. package/src/components/cnpg/useCNPGSidebarWorkspace.ts +66 -16
  206. package/src/components/cnpg/workspaceRoute.ts +23 -0
  207. package/src/components/cost/ApplicationCostTab.test.ts +7 -0
  208. package/src/components/cost/ApplicationCostTab.tsx +7 -7
  209. package/src/components/cost/CostView.test.ts +2 -1
  210. package/src/components/cost/CostView.tsx +0 -2
  211. package/src/components/cost/WorkloadCostTab.tsx +2 -4
  212. package/src/components/cost/errors.ts +3 -1
  213. package/src/components/cost/source.test.ts +10 -0
  214. package/src/components/cost/source.ts +5 -1
  215. package/src/components/diagnose/AgentSetupNotice.test.tsx +58 -0
  216. package/src/components/diagnose/AgentSetupNotice.tsx +142 -51
  217. package/src/components/diagnose/ApplyDialog.ack.test.tsx +47 -0
  218. package/src/components/diagnose/ApplyDialog.test.tsx +59 -6
  219. package/src/components/diagnose/ApplyDialog.tsx +26 -28
  220. package/src/components/diagnose/DiagnoseContext.test.ts +43 -0
  221. package/src/components/diagnose/DiagnoseContext.tsx +136 -54
  222. package/src/components/diagnose/DiagnoseSurface.tsx +16 -3
  223. package/src/components/diagnose/InvestigationView.tsx +39 -2
  224. package/src/components/diagnose/LocalDiagnoseAction.tsx +40 -20
  225. package/src/components/diagnose/agentCatalog.ts +2 -2
  226. package/src/components/diagnose/diagnoseEvidenceTypes.ts +2 -0
  227. package/src/components/diagnose/investigationEvidence/adapters/issues.ts +8 -0
  228. package/src/components/diagnose/investigationEvidence/projection.test.ts +16 -0
  229. package/src/components/dock/BottomDock.tsx +2 -0
  230. package/src/components/dock/TerminalTab.test.tsx +17 -0
  231. package/src/components/dock/TerminalTab.tsx +7 -3
  232. package/src/components/dock/TrafficFlowListTab.tsx +27 -6
  233. package/src/components/execution/BatchExecutionView.tsx +11 -11
  234. package/src/components/gitops/ArgoResourceDiffLoader.tsx +4 -2
  235. package/src/components/gitops/GitOpsView.tsx +16 -23
  236. package/src/components/helm/ChartBrowser.tsx +3 -3
  237. package/src/components/helm/HelmCompareRoute.test.tsx +59 -0
  238. package/src/components/helm/HelmCompareRoute.tsx +57 -26
  239. package/src/components/helm/HelmReleaseDrawer.test.ts +55 -3
  240. package/src/components/helm/HelmReleaseDrawer.tsx +50 -12
  241. package/src/components/helm/RevisionHistory.tsx +3 -2
  242. package/src/components/helm/TrackChartSourceDialog.render.test.tsx +120 -0
  243. package/src/components/helm/TrackChartSourceDialog.test.ts +50 -0
  244. package/src/components/helm/TrackChartSourceDialog.tsx +153 -176
  245. package/src/components/helm/ValuesViewer.test.tsx +301 -0
  246. package/src/components/helm/ValuesViewer.tsx +169 -78
  247. package/src/components/home/ActivitySummary.tsx +2 -1
  248. package/src/components/home/ClusterHealthCard.tsx +7 -0
  249. package/src/components/home/HomeView.tsx +20 -9
  250. package/src/components/home/ProblemsPanel.test.tsx +81 -0
  251. package/src/components/home/TopologyPreview.tsx +2 -2
  252. package/src/components/home/mcpToolCatalog.ts +1 -1
  253. package/src/components/issues/IssuesPane.render.test.tsx +122 -0
  254. package/src/components/issues/IssuesPane.test.ts +65 -1
  255. package/src/components/issues/IssuesPane.tsx +188 -25
  256. package/src/components/issues/uncheckedSources.ts +21 -0
  257. package/src/components/resource/PVCUsageBar.render.test.tsx +32 -0
  258. package/src/components/resource/PVCUsageBar.test.tsx +24 -0
  259. package/src/components/resource/PVCUsageBar.tsx +15 -6
  260. package/src/components/resource/WorkloadMetricsHelpDialog.test.tsx +3 -1
  261. package/src/components/resource/WorkloadMetricsHelpDialog.tsx +1 -1
  262. package/src/components/resources/CompositeRenderer.tsx +4 -4
  263. package/src/components/resources/ResourceDetailDrawer.tsx +2 -2
  264. package/src/components/resources/ResourcesView.tsx +26 -12
  265. package/src/components/resources/renderers/KyvernoPolicyQueued.tsx +2 -1
  266. package/src/components/resources/renderers/NodeRenderer.tsx +7 -1
  267. package/src/components/resources/renderers/ServiceRenderer.test.tsx +81 -0
  268. package/src/components/resources/renderers/ServiceRenderer.tsx +19 -14
  269. package/src/components/settings/LocalIntegrationStatus.test.tsx +52 -0
  270. package/src/components/settings/LocalIntegrationStatus.tsx +3 -4
  271. package/src/components/settings/SettingsDialog.tsx +150 -34
  272. package/src/components/shared/LargeClusterNamespacePicker.tsx +11 -5
  273. package/src/components/timeline/TimelineList.tsx +25 -24
  274. package/src/components/timeline/TimelineLoadError.test.tsx +62 -0
  275. package/src/components/timeline/TimelineLoadError.tsx +96 -0
  276. package/src/components/timeline/TimelineRetentionNote.test.tsx +49 -0
  277. package/src/components/timeline/TimelineRetentionNote.tsx +20 -0
  278. package/src/components/timeline/TimelineView.tsx +25 -45
  279. package/src/components/timeline/ViewModeToggle.tsx +20 -0
  280. package/src/components/timeline/useStickyError.test.tsx +47 -0
  281. package/src/components/timeline/useStickyError.ts +15 -0
  282. package/src/components/traffic/TrafficConnectionsTable.tsx +144 -0
  283. package/src/components/traffic/TrafficFilterSidebar.tsx +22 -1
  284. package/src/components/traffic/TrafficFlowListContext.tsx +12 -4
  285. package/src/components/traffic/TrafficFocusSearch.tsx +172 -0
  286. package/src/components/traffic/TrafficGraph.tsx +109 -26
  287. package/src/components/traffic/TrafficGraphTooLarge.tsx +150 -0
  288. package/src/components/traffic/TrafficView.tsx +582 -149
  289. package/src/components/traffic/trafficFilters.test.ts +398 -1
  290. package/src/components/traffic/trafficFilters.ts +408 -0
  291. package/src/components/ui/DiagnosticsOverlay.tsx +34 -2
  292. package/src/components/ui/Markdown.test.tsx +65 -0
  293. package/src/components/ui/Markdown.tsx +144 -127
  294. package/src/components/whats-new/releaseNotes.ts +63 -1
  295. package/src/components/workload/WorkloadView.test.ts +11 -1
  296. package/src/components/workload/WorkloadView.tsx +162 -529
  297. package/src/components/workspace/layout.test.tsx +29 -0
  298. package/src/components/workspace/layout.tsx +259 -0
  299. package/src/components/workspace/table.ts +7 -0
  300. package/src/context/TimelineCustomization.tsx +15 -0
  301. package/src/contexts/CapabilitiesContext.tsx +1 -1
  302. package/src/hooks/useGitOpsWriteGuard.test.tsx +457 -0
  303. package/src/hooks/useGitOpsWriteGuard.ts +193 -0
  304. package/src/hooks/useResolvedGitOpsOwner.ts +256 -0
  305. package/src/integrations/resourceHost.test.tsx +55 -0
  306. package/src/integrations/resourceHost.ts +99 -0
  307. package/src/integrations/resourceHosts.test.tsx +141 -0
  308. package/src/integrations/resourceHosts.tsx +119 -0
  309. package/src/integrations/workspaceRoute.ts +20 -0
  310. package/src/integrations/workspaceRoutes.test.ts +40 -0
  311. package/src/integrations/workspaceRoutes.ts +22 -0
  312. package/src/integrations/workspaceScreens.tsx +14 -0
  313. package/src/utils/drawer-trail.ts +35 -0
  314. package/src/utils/kindFilterStore.test.ts +147 -0
  315. package/src/utils/kindFilterStore.ts +91 -0
  316. package/src/utils/navigation.test.ts +13 -1
  317. package/src/utils/navigation.ts +39 -2
  318. package/src/utils/page-links.ts +21 -0
  319. package/src/utils/tabStorage.ts +40 -0
  320. package/src/utils/viewMemory.test.ts +78 -0
  321. 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
+ })