@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
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
- export interface CNPGOperatorCoverage {
24
- state: 'full' | 'partial' | 'denied' | 'syncing' | 'error'
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
  }
@@ -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
+ });
@@ -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
- return res.json();
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 />)