@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
@@ -2,7 +2,6 @@ import {
2
2
  useCallback,
3
3
  useEffect,
4
4
  useState,
5
- type ComponentType,
6
5
  type ReactNode,
7
6
  } from "react";
8
7
  import {
@@ -14,6 +13,7 @@ import {
14
13
  } from "lucide-react";
15
14
  import {
16
15
  FreshnessControl,
16
+ healthToSeverity,
17
17
  formatDuration,
18
18
  getRadarUpgradeRequirement,
19
19
  RadarUpgradeAction,
@@ -24,6 +24,8 @@ import {
24
24
  ResourceBar,
25
25
  Tooltip,
26
26
  WithTooltip,
27
+ certaintyValueLabel,
28
+ CertaintyGlyph,
27
29
  type CapacityActivityEpisode,
28
30
  type CapacityCertainty,
29
31
  type CapacityClaimStage,
@@ -38,6 +40,7 @@ import {
38
40
  type CapacityManagerRollupStatus,
39
41
  type CapacityManagerSummary,
40
42
  type CapacityMemberType,
43
+ type CapacityNodeMember,
41
44
  type CapacityPoolSummary,
42
45
  type CapacityQuantityObservation,
43
46
  type CapacityResourceIdentity,
@@ -59,22 +62,19 @@ import {
59
62
  } from "../../api/client";
60
63
  import type { SelectedResource } from "../../types";
61
64
  import { refToSelectedResource } from "../../utils/navigation";
65
+ import { Notice, ScreenEmptyState } from "../workspace/layout";
62
66
 
63
67
  // ============================================================================
64
68
  // Route model + navigation
65
69
  // ============================================================================
66
70
 
67
- export type CapacityTopTab = "overview" | "demand" | "activity";
68
- export type PoolSection = "summary" | "workloads" | "members" | "configuration";
71
+ import type { PoolSection } from './routes'
72
+ export { capacityPoolPath, parseCapacityRoute } from './routes'
73
+ export type { CapacityRoute, CapacityTopTab, PoolSection } from './routes'
74
+
69
75
  export type CapacityConnectionState =
70
76
  "connected" | "disconnected" | "connecting";
71
77
 
72
- export interface CapacityRoute {
73
- topTab: CapacityTopTab;
74
- poolName?: string;
75
- poolSection: PoolSection;
76
- }
77
-
78
78
  export const POOL_SECTIONS: { id: PoolSection; label: string }[] = [
79
79
  { id: "summary", label: "Summary" },
80
80
  { id: "workloads", label: "Workloads" },
@@ -82,40 +82,6 @@ export const POOL_SECTIONS: { id: PoolSection; label: string }[] = [
82
82
  { id: "configuration", label: "Configuration" },
83
83
  ];
84
84
 
85
- export function parseCapacityRoute(pathname: string): CapacityRoute {
86
- const segments = pathname.replace(/^\/+|\/+$/g, "").split("/");
87
- if (segments[0] !== "capacity")
88
- return { topTab: "overview", poolSection: "summary" };
89
- if (segments[1] === "pools" && segments[2]) {
90
- const section = segments[3];
91
- return {
92
- topTab: "overview",
93
- poolName: decodePathSegment(segments[2]),
94
- poolSection:
95
- section === "workloads" ||
96
- section === "members" ||
97
- section === "configuration"
98
- ? section
99
- : "summary",
100
- };
101
- }
102
- if (segments[1] === "demand" || segments[1] === "activity")
103
- return { topTab: segments[1], poolSection: "summary" };
104
- return { topTab: "overview", poolSection: "summary" };
105
- }
106
-
107
- function decodePathSegment(value: string): string {
108
- try {
109
- return decodeURIComponent(value);
110
- } catch {
111
- return value;
112
- }
113
- }
114
-
115
- export function capacityPoolPath(name: string): string {
116
- return `/capacity/pools/${encodeURIComponent(name)}`;
117
- }
118
-
119
85
  export function identityToSelectedResource(
120
86
  identity: CapacityResourceIdentity,
121
87
  ): SelectedResource {
@@ -264,20 +230,6 @@ function missingUsageMessage(coverage?: CapacitySourceCoverage): string {
264
230
  // = exact · ≥ lower bound · ≤ upper bound · ? unknown
265
231
  // ============================================================================
266
232
 
267
- export function certaintyGlyph(certainty: CapacityCertainty): string {
268
- if (certainty === "exact") return "=";
269
- if (certainty === "lower_bound") return "≥";
270
- if (certainty === "upper_bound") return "≤";
271
- return "?";
272
- }
273
-
274
- export function certaintyValueLabel(certainty: CapacityCertainty): string {
275
- if (certainty === "exact") return "Exact";
276
- if (certainty === "lower_bound") return "Lower bound";
277
- if (certainty === "upper_bound") return "Upper bound";
278
- return "Unknown certainty";
279
- }
280
-
281
233
  export function observationTitle(
282
234
  observation: CapacityQuantityObservation,
283
235
  definition?: string,
@@ -293,28 +245,6 @@ export function observationTitle(
293
245
  return parts.join(" ");
294
246
  }
295
247
 
296
- /** Small bordered mono glyph chip; hover reveals certainty/source/asOf. */
297
- export function CertaintyGlyph({
298
- certainty,
299
- title,
300
- }: {
301
- certainty: CapacityCertainty;
302
- title?: string;
303
- }) {
304
- return (
305
- <WithTooltip tip={title ?? certaintyValueLabel(certainty)}>
306
- <span
307
- tabIndex={0}
308
- role="note"
309
- aria-label={title ?? certaintyValueLabel(certainty)}
310
- className="cursor-help rounded border border-theme-border-light px-1 font-mono text-[10px] leading-tight text-theme-text-tertiary focus-visible:outline focus-visible:outline-2 focus-visible:outline-skyhook-500"
311
- >
312
- {certaintyGlyph(certainty)}
313
- </span>
314
- </WithTooltip>
315
- );
316
- }
317
-
318
248
  // ============================================================================
319
249
  // Quantity / resource formatting
320
250
  // ============================================================================
@@ -603,32 +533,21 @@ export function PoolReadyBadge({ ready }: { ready?: boolean }) {
603
533
  export function NodeReadyBadge({
604
534
  ready,
605
535
  cordoned,
536
+ lifecycle,
606
537
  }: {
607
538
  ready?: boolean;
608
539
  cordoned: boolean;
540
+ lifecycle?: CapacityNodeMember["lifecycle"];
609
541
  }) {
610
- if (ready === false)
611
- return (
612
- <Badge severity="error" size="sm">
613
- NotReady
614
- </Badge>
615
- );
616
- if (ready === undefined)
617
- return (
618
- <Badge severity="neutral" size="sm">
619
- Unknown
620
- </Badge>
621
- );
622
- if (cordoned)
623
- return (
624
- <Badge severity="warning" size="sm">
625
- Cordoned
626
- </Badge>
627
- );
542
+ if (lifecycle) {
543
+ return <Badge severity={healthToSeverity(lifecycle.level)} size="sm">{lifecycle.label}</Badge>;
544
+ }
628
545
  return (
629
- <Badge severity="success" size="sm">
630
- Ready
631
- </Badge>
546
+ <WithTooltip tip="Kubernetes readiness only; removal status is unavailable from this Radar.">
547
+ <Badge severity={ready === false ? "error" : ready === undefined ? "neutral" : cordoned ? "warning" : "success"} size="sm">
548
+ {ready === false ? "NotReady (condition)" : ready === undefined ? "Readiness unknown" : cordoned ? "Cordoned" : "Ready (condition)"}
549
+ </Badge>
550
+ </WithTooltip>
632
551
  );
633
552
  }
634
553
 
@@ -1250,15 +1169,6 @@ export function TokenGroup({
1250
1169
  );
1251
1170
  }
1252
1171
 
1253
- export function Notice({ children }: { children: ReactNode }) {
1254
- return (
1255
- <div className="flex items-start gap-2 rounded-lg border border-theme-border bg-theme-surface px-3 py-2 text-sm text-theme-text-secondary">
1256
- <AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-theme-text-tertiary" />
1257
- <div>{children}</div>
1258
- </div>
1259
- );
1260
- }
1261
-
1262
1172
  export function RefreshError({ message }: { message: string }) {
1263
1173
  return (
1264
1174
  <Notice>
@@ -1336,31 +1246,6 @@ export function InlineEmpty({
1336
1246
  );
1337
1247
  }
1338
1248
 
1339
- export function EmptyState({
1340
- icon: Icon,
1341
- title,
1342
- detail,
1343
- action,
1344
- }: {
1345
- icon: ComponentType<{ className?: string }>;
1346
- title: string;
1347
- detail: ReactNode;
1348
- action?: ReactNode;
1349
- }) {
1350
- return (
1351
- <div className="flex min-h-0 flex-1 items-center justify-center bg-theme-base p-6">
1352
- <div className="max-w-md text-center">
1353
- <Icon className="mx-auto h-9 w-9 text-theme-text-tertiary/50" />
1354
- <h2 className="mt-3 text-lg font-medium text-theme-text-primary">
1355
- {title}
1356
- </h2>
1357
- <p className="mt-1 text-sm text-theme-text-secondary">{detail}</p>
1358
- {action}
1359
- </div>
1360
- </div>
1361
- );
1362
- }
1363
-
1364
1249
  export function ScrollableContent({ children }: { children: ReactNode }) {
1365
1250
  // Expanding/collapsing cards can toggle the scrollbar; a stable gutter
1366
1251
  // keeps the centered column from shifting sideways when that happens.
@@ -1469,7 +1354,7 @@ rules:
1469
1354
  export function DeniedCapacityState({ detail }: { detail: string }) {
1470
1355
  const [copied, setCopied] = useState(false);
1471
1356
  return (
1472
- <EmptyState
1357
+ <ScreenEmptyState
1473
1358
  icon={Shield}
1474
1359
  title="Capacity access denied"
1475
1360
  detail={detail}
@@ -1528,7 +1413,7 @@ export function integrationBlock(
1528
1413
  const upgrade = getRadarUpgradeRequirement(error);
1529
1414
  if (upgrade)
1530
1415
  return (
1531
- <EmptyState
1416
+ <ScreenEmptyState
1532
1417
  icon={Gauge}
1533
1418
  title={radarUpgradeHeadline("Capacity")}
1534
1419
  detail={radarUpgradeDetail(upgrade)}
@@ -1540,7 +1425,7 @@ export function integrationBlock(
1540
1425
  />
1541
1426
  );
1542
1427
  return (
1543
- <EmptyState
1428
+ <ScreenEmptyState
1544
1429
  icon={AlertTriangle}
1545
1430
  title="Capacity unavailable"
1546
1431
  detail={errorMessage(error)}
@@ -1558,7 +1443,7 @@ export function integrationBlock(
1558
1443
  );
1559
1444
  if (response.state === "not_detected")
1560
1445
  return (
1561
- <EmptyState
1446
+ <ScreenEmptyState
1562
1447
  icon={Gauge}
1563
1448
  title="Karpenter not detected"
1564
1449
  detail="Capacity appears automatically when this cluster exposes Karpenter NodePools."
@@ -1847,15 +1732,3 @@ export function PoolSelector({
1847
1732
  </div>
1848
1733
  );
1849
1734
  }
1850
-
1851
- // ============================================================================
1852
- // Shared table cell classes (keeps every capacity table visually identical)
1853
- // ============================================================================
1854
-
1855
- export const TABLE_WRAP = "overflow-x-auto";
1856
- export const TABLE_HEAD =
1857
- "border-b border-theme-border bg-theme-base/60 text-[11px] uppercase tracking-wide text-theme-text-tertiary";
1858
- export const TH = "px-3 py-2.5 text-left font-medium whitespace-nowrap";
1859
- export const TD = "px-3 py-2.5 align-top text-sm text-theme-text-primary";
1860
- export const TBODY = "table-divide-subtle";
1861
- export const ROW_HOVER = "transition-colors hover:bg-theme-hover/50";
@@ -0,0 +1,18 @@
1
+ import type { WorkspaceRoute } from '../../integrations/workspaceRoute'
2
+ import { parseCapacityRoute } from './routes'
3
+
4
+ export const capacityWorkspaceRoute: WorkspaceRoute = {
5
+ id: 'capacity',
6
+ path: '/capacity',
7
+ label: 'Capacity',
8
+ navView: 'capacity',
9
+ namespaceScope: 'cluster',
10
+ namespaceFilterTooltip: 'Capacity is reported across the cluster — the namespace filter doesn’t apply here.',
11
+ pageTitle(pathname) {
12
+ const route = parseCapacityRoute(pathname)
13
+ if (route.poolName) return route.poolName
14
+ if (route.topTab === 'demand') return 'Capacity Demand'
15
+ if (route.topTab === 'activity') return 'Capacity Activity'
16
+ return 'Capacity'
17
+ },
18
+ }
@@ -0,0 +1,30 @@
1
+ import { renderToStaticMarkup } from 'react-dom/server'
2
+ import { beforeEach, expect, it, vi } from 'vitest'
3
+ import { cnpgHost } from './host'
4
+ import { renderCNPGHeaderActions } from './actions/renderCNPGHeaderActions'
5
+ import { radarFeatureSupport } from '../../api/radarFeatures'
6
+ const state = vi.hoisted(() => ({ capabilities: { features: { resourceIssues: true } } as any }))
7
+ vi.mock('../../api/client', async original => ({ ...await original<typeof import('../../api/client')>(), useRadarFeature: () => ({ support: radarFeatureSupport('cnpgWorkspace', state.capabilities, 'v1.16.0') }) }))
8
+ vi.mock('./CNPGClusterTabs', () => ({ CNPGInvestigationAction: () => <span>CNPG investigation</span> }))
9
+ vi.mock('./actions/CNPGClusterActions', () => ({ CNPGClusterActions: () => <span>CNPG write actions</span> }))
10
+ vi.mock('./actions/CNPGConnectButton', () => ({ CNPGConnectButton: () => null }))
11
+ vi.mock('./actions/CNPGScheduleActions', () => ({ CNPGScheduleActions: () => null }))
12
+ vi.mock('./actions/CNPGPoolerActions', () => ({ CNPGPoolerActions: () => null }))
13
+ vi.mock('./operations/CNPGOperationTracker', () => ({ CNPGOperationTracker: () => null }))
14
+ vi.mock('./recovery/CNPGRestoreButton', () => ({ CNPGRestoreButton: () => null }))
15
+ beforeEach(() => { state.capabilities = { features: { resourceIssues: true } } })
16
+ const context = { kind: 'Cluster', group: 'postgresql.cnpg.io', namespace: 'db', name: 'pg', compact: false }
17
+ const resource = { apiVersion: 'postgresql.cnpg.io/v1', kind: 'Cluster', metadata: { name: 'pg', namespace: 'db', uid: 'uid-pg' } }
18
+ it('preserves ordinary Diagnose and omits new action affordances on the shipped missing-flag state', () => {
19
+ const original = vi.fn(() => <span>ordinary investigation</span>)
20
+ expect(renderToStaticMarkup(cnpgHost.diagnoseAction(original)!(context))).toContain('ordinary investigation')
21
+ expect(original).toHaveBeenCalledWith(context)
22
+ expect(renderToStaticMarkup(renderCNPGHeaderActions({ resource, namespace: 'db', name: 'pg', compact: false }))).toBe('')
23
+ })
24
+ it('mounts the CNPG decorations once their contracts are advertised', () => {
25
+ state.capabilities = { features: { cnpgWorkspace: true } }
26
+ const original = vi.fn(() => <span>ordinary investigation</span>)
27
+ expect(renderToStaticMarkup(cnpgHost.diagnoseAction(original)!(context))).toContain('CNPG investigation')
28
+ expect(original).not.toHaveBeenCalled()
29
+ expect(renderToStaticMarkup(renderCNPGHeaderActions({ resource, namespace: 'db', name: 'pg', compact: false }))).toContain('CNPG write actions')
30
+ })
@@ -0,0 +1,191 @@
1
+ import { cnpgClusterPlugins, cnpgPluginPhase, formatAge, getCNPGClusterBarmanPlugin, StatusDot, toneTextClass, type CNPGFleetRow } from '@skyhook-io/k8s-ui'
2
+ import type { SelectedResource } from '../../types'
3
+ import type { CNPGRuntimeInstance } from '../../api/cnpg'
4
+ import { backupMethod, cnpgArchiveDestination, cnpgRecoveryBase, resumeBoundary, type CNPGRecoveryBase } from './archivingRepair'
5
+
6
+ const RESUMED_WINDOW_MS = 24 * 60 * 60 * 1000
7
+
8
+ /**
9
+ * WAL archiving, as a repair: why it fails, where the destination and its
10
+ * credentials are declared, where the archiver logs, then whether archiving
11
+ * resumed and a fresh base backup exists. Shown while archiving fails, and
12
+ * for a day after it resumes so the fix can be confirmed here.
13
+ */
14
+ export function CNPGArchivingRepair({
15
+ row,
16
+ primary,
17
+ objectStores,
18
+ backups,
19
+ onOpenLogs,
20
+ onOpenOperator,
21
+ onInspect,
22
+ }: {
23
+ row: CNPGFleetRow
24
+ primary?: CNPGRuntimeInstance
25
+ objectStores: any[]
26
+ /** This namespace's CNPG Backups, or null when they could not be read. */
27
+ backups: any[] | null
28
+ onOpenLogs?: (pod: string, container: string) => void
29
+ onOpenOperator?: () => void
30
+ onInspect: (r: SelectedResource) => void
31
+ }) {
32
+ const wal = row.protection.walArchiving
33
+ const arch = primary && (primary.status.state === 'ok' || primary.status.state === 'partial') ? primary.status.archiving : undefined
34
+ const failedAt = arch?.lastFailedAt ? Date.parse(arch.lastFailedAt) : NaN
35
+ const archivedAt = arch?.lastArchivedAt ? Date.parse(arch.lastArchivedAt) : NaN
36
+ const failingNow = wal.tone === 'unhealthy' || (Number.isFinite(failedAt) && (!Number.isFinite(archivedAt) || failedAt > archivedAt))
37
+ // Only the instance manager records that archiving failed: the condition
38
+ // turning True also happens when archiving is first set up, so it marks a
39
+ // resume only after a failure the instance manager saw. Its stats restart
40
+ // with the primary, and the panel then has nothing left to show.
41
+ const runtimeResumed = Number.isFinite(failedAt) && Number.isFinite(archivedAt) && archivedAt > failedAt
42
+ const boundary = resumeBoundary(row.cluster, failedAt)
43
+ const resumed = !failingNow && wal.tone === 'healthy' && runtimeResumed && Number.isFinite(boundary) && Date.now() - boundary < RESUMED_WINDOW_MS
44
+ if (!failingNow && !resumed) return null
45
+
46
+ // A plugin archives WAL only when it is marked isWALArchiver; otherwise the
47
+ // in-tree barmanObjectStore (if any) does, from the postgres container.
48
+ const barman = getCNPGClusterBarmanPlugin(row.cluster)
49
+ const plugin = barman?.isWALArchiver ? barman : null
50
+ const storeName = plugin?.barmanObjectName
51
+ const store = storeName ? objectStores.find((s) => s?.metadata?.namespace === row.namespace && s?.metadata?.name === storeName) : undefined
52
+ const inTree = row.cluster?.spec?.backup?.barmanObjectStore
53
+ const dest = store
54
+ ? cnpgArchiveDestination(store.spec?.configuration, `ObjectStore ${storeName}`)
55
+ : inTree
56
+ ? cnpgArchiveDestination(inTree, 'the Cluster’s spec.backup.barmanObjectStore')
57
+ : undefined
58
+ const archiverContainer = plugin ? 'plugin-barman-cloud' : 'postgres'
59
+ const pluginPhase = cnpgPluginPhase(row.cluster)
60
+ const primaryPod = primary?.pod ?? row.cluster?.status?.currentPrimary
61
+ const lastBackup = row.protection.lastSuccessfulBackup
62
+ const base = resumed ? cnpgRecoveryBase(row, backups, boundary, plugin ? { method: 'plugin', plugin: plugin.name } : { method: 'barmanObjectStore' }, arch?.lastFailedWal) : undefined
63
+ const baseDone = base?.state === 'verified'
64
+
65
+ return (
66
+ <section className="rounded-xl border border-theme-border bg-theme-surface px-4 py-3 shadow-theme-sm">
67
+ <h3 className={`text-sm font-semibold ${failingNow ? toneTextClass('unhealthy') : toneTextClass('healthy')}`}>
68
+ {failingNow ? 'WAL archiving is failing' : 'WAL archiving resumed'}
69
+ </h3>
70
+ <div className="mt-1.5 space-y-1.5 text-sm">
71
+ {failingNow && (
72
+ <p className="text-theme-text-primary">
73
+ {wal.tone === 'unhealthy' ? (
74
+ wal.detail ? (
75
+ <>
76
+ The operator reports: <span className="font-mono text-xs">{wal.detail}</span>
77
+ </>
78
+ ) : (
79
+ 'The operator reports ContinuousArchiving False without a message.'
80
+ )
81
+ ) : (
82
+ `The primary’s instance manager reports a failed archive after its last archived WAL${wal.tone === 'healthy' ? '; the operator’s ContinuousArchiving condition still reads True' : ''}.`
83
+ )}
84
+ </p>
85
+ )}
86
+ {arch && (
87
+ <p className="text-xs text-theme-text-secondary">
88
+ {arch.lastArchivedAt ? `Last archived ${formatAge(arch.lastArchivedAt)} ago${arch.lastArchivedWal ? ` (${arch.lastArchivedWal})` : ''}` : 'Nothing archived since the instance manager started'}
89
+ {arch.lastFailedAt ? ` · last failure ${formatAge(arch.lastFailedAt)} ago${arch.lastFailedWal ? ` (${arch.lastFailedWal})` : ''}` : ''}
90
+ {arch.readyWalFiles !== undefined ? ` · ${arch.readyWalFiles} files waiting` : ''} · from the primary’s instance manager
91
+ </p>
92
+ )}
93
+ {failingNow && dest && (
94
+ <div className="text-xs text-theme-text-secondary">
95
+ <div>
96
+ Destination <span className="font-mono text-theme-text-primary">{dest.path ?? 'path not declared'}</span>
97
+ {dest.endpointURL && <> via <span className="font-mono">{dest.endpointURL}</span></>}, declared in{' '}
98
+ {store ? (
99
+ <button type="button" className="text-accent-text hover:underline" onClick={() => onInspect({ kind: 'objectstores', group: 'barmancloud.cnpg.io', namespace: row.namespace, name: storeName! })}>
100
+ {dest.declaredIn}
101
+ </button>
102
+ ) : (
103
+ dest.declaredIn
104
+ )}
105
+ .
106
+ </div>
107
+ <div className="mt-0.5">
108
+ Credentials:{' '}
109
+ {dest.identity && (
110
+ <>
111
+ {dest.identity}
112
+ {dest.secrets.length > 0 && ' · '}
113
+ </>
114
+ )}
115
+ {!dest.identity && dest.secrets.length === 0
116
+ ? 'none referenced'
117
+ : dest.secrets.map((s, i) => (
118
+ <span key={`${s.secret}/${s.key}/${s.what}`}>
119
+ {i > 0 && ' · '}
120
+ {s.what} in Secret{' '}
121
+ <button type="button" className="font-mono text-accent-text hover:underline" onClick={() => onInspect({ kind: 'secrets', group: '', namespace: row.namespace, name: s.secret })}>
122
+ {s.secret}
123
+ </button>
124
+ {s.key ? <span className="font-mono">/{s.key}</span> : null}
125
+ </span>
126
+ ))}
127
+ . Radar names them and does not read them.
128
+ </div>
129
+ </div>
130
+ )}
131
+ {failingNow && primaryPod && (
132
+ <p className="text-xs text-theme-text-secondary">
133
+ The primary archives from its <span className="font-mono">{archiverContainer}</span> container; the failing command’s output is in its logs.{' '}
134
+ {onOpenLogs && (
135
+ <button type="button" className="text-accent-text hover:underline" onClick={() => onOpenLogs(primaryPod, archiverContainer)}>
136
+ {archiverContainer} logs of {primaryPod} →
137
+ </button>
138
+ )}
139
+ </p>
140
+ )}
141
+ {failingNow && pluginPhase && (
142
+ <p className="text-xs text-theme-text-secondary">
143
+ Separately, the Cluster’s phase says the operator {pluginPhase === 'unknownPlugin' ? 'does not know a plugin this cluster requires' : 'hit an error talking to one of this cluster’s plugins'} (spec.plugins:{' '}
144
+ {cnpgClusterPlugins(row.cluster).join(', ') || 'none listed'}), so it cannot reconcile this cluster until that clears.{' '}
145
+ {onOpenOperator && (
146
+ <button type="button" className="text-accent-text hover:underline" onClick={onOpenOperator}>
147
+ Operator and plugins →
148
+ </button>
149
+ )}
150
+ </p>
151
+ )}
152
+ <ol className="mt-1 space-y-1 text-xs">
153
+ <li className="flex items-start gap-2">
154
+ <span className="flex h-4 shrink-0 items-center"><StatusDot tone={failingNow ? 'unknown' : 'healthy'} /></span>
155
+ <span className={failingNow ? 'text-theme-text-secondary' : 'text-theme-text-primary'}>
156
+ Archiving resumes: the operator reports ContinuousArchiving True and a WAL file archives after the last failure.
157
+ {failingNow ? ' Not yet.' : ` Done${arch?.lastArchivedAt ? `, ${formatAge(arch.lastArchivedAt)} ago` : ''}.`}
158
+ </span>
159
+ </li>
160
+ <li className="flex items-start gap-2">
161
+ <span className="flex h-4 shrink-0 items-center"><StatusDot tone={baseDone ? 'healthy' : 'unknown'} /></span>
162
+ <span className={baseDone ? 'text-theme-text-primary' : 'text-theme-text-secondary'}>
163
+ A base backup that begins after the last WAL that failed to archive completes (Back up now, above), so a restore from it needs none of that WAL.{' '}
164
+ {failingNow || !base
165
+ ? `Not yet. Newest successful backup: ${lastBackup.at ? `${formatAge(lastBackup.at)} ago` : lastBackup.text.toLowerCase()}.`
166
+ : recoveryBaseText(base)}
167
+ </span>
168
+ </li>
169
+ </ol>
170
+ </div>
171
+ </section>
172
+ )
173
+ }
174
+
175
+ function recoveryBaseText(base: CNPGRecoveryBase): string {
176
+ const label = (b: any) => `Backup ${b?.metadata?.name} (${backupMethod(b)})`
177
+ switch (base.state) {
178
+ case 'unread':
179
+ return 'Radar cannot read this namespace’s Backups, so it cannot tell.'
180
+ case 'none':
181
+ return 'No completed Backup of this cluster has started since archiving resumed.'
182
+ case 'verified':
183
+ return `Done: ${label(base.backup)} begins at WAL ${base.backup.status.beginWal}, after ${base.failedWal}, and completed ${formatAge(base.backup.status.stoppedAt ?? base.backup.status.startedAt)} ago.`
184
+ case 'beginsBefore':
185
+ return `${label(base.backup)} started after archiving resumed but begins at WAL ${base.backup?.status?.beginWal ?? '(not reported)'}, not after ${base.failedWal} — likely from a standby that had not replayed past it. Take another.`
186
+ case 'unverifiable':
187
+ return base.missing === 'beginWal'
188
+ ? `${label(base.backup)} started after archiving resumed and completed, but its status does not say which WAL it begins at, so Radar cannot compare it with the last failed one.`
189
+ : `${label(base.backup)} started after archiving resumed and completed; Radar cannot compare where it begins with the last failed WAL, which only the primary’s instance manager reports and is not reporting now.`
190
+ }
191
+ }
@@ -0,0 +1,50 @@
1
+ // @vitest-environment jsdom
2
+ import { renderToStaticMarkup } from 'react-dom/server'
3
+ import { describe, expect, it, vi } from 'vitest'
4
+
5
+ const state = vi.hoisted(() => ({ data: { pod: 'pg-1', state: 'denied', permission: { exec: 'denied', grant: { verb: 'create', resource: 'pods', subresource: 'exec', namespace: 'db' } }, instances: [] } as any, navigate: vi.fn() }))
6
+ vi.mock('./useCNPGNavigate', () => ({ useCNPGNavigate: () => state.navigate }))
7
+ vi.mock('react-router-dom', async (original) => ({ ...await original<typeof import('react-router-dom')>(), useLocation: () => ({ pathname: '/cnpg/clusters/db/pg', search: '?ctx=test&tab=performance&drawer=x', state: { returnLabel: 'Clusters' } }) }))
8
+ vi.mock('@tanstack/react-query', async (original) => ({ ...await original<typeof import('@tanstack/react-query')>(), useQueries: () => [{ data: { containers: [] } }] }))
9
+ vi.mock('../../api/client', () => ({ podMetricsQuery: () => ({}), usePodMetrics: () => ({ data: { containers: [{ name: 'postgres', usage: { cpu: '30000000n', memory: '59Mi' } }] } }) }))
10
+ vi.mock('../../api/cnpg-sessions', () => ({
11
+ useCNPGSessions: () => ({
12
+ data: state.data,
13
+ isLoading: false,
14
+ error: null,
15
+ isRefetchError: false,
16
+ dataUpdatedAt: Date.now(),
17
+ }),
18
+ }))
19
+
20
+ const { CNPGBlockingSessions } = await import('./CNPGBlockingSessions')
21
+
22
+ describe('CNPGBlockingSessions with exec denied', () => {
23
+ it('explains only the exec grant for blocking detail', () => {
24
+ const html = renderToStaticMarkup(<CNPGBlockingSessions namespace="db" cluster="pg" />)
25
+ expect(html).toContain('Who blocks whom is read inside PostgreSQL')
26
+ expect(html).toContain('create pods/exec')
27
+ expect(html).not.toContain('aggregate')
28
+ expect(html).not.toContain('get pods/proxy')
29
+ })
30
+ })
31
+
32
+ it('keeps query-text prose out of unavailable states while the host owns the prerequisite', () => {
33
+ expect(renderToStaticMarkup(<CNPGBlockingSessions namespace="db" cluster="pg" />)).not.toContain('Query text')
34
+ state.data = { pod: '', state: 'unavailable', permission: { exec: 'unknown' }, instances: [] }
35
+ const html = renderToStaticMarkup(<CNPGBlockingSessions namespace="db" cluster="pg" />)
36
+ expect(html).toContain('Blocking detail not measured')
37
+ expect(html).not.toContain('Available once the primary is running')
38
+ expect(html).not.toContain('See Overview')
39
+ expect(html).not.toContain('could not be read')
40
+ expect(html).not.toContain('Query text')
41
+ })
42
+ it('keeps CPU and memory measurements and limits on separate rows with unbreakable units', () => {
43
+ state.data = { pod: 'pg-1', state: 'ok', permission: { exec: 'allowed' }, sessions: [], instances: [{ pod: 'pg-1', role: 'primary', memoryLimit: '384Mi' }] }
44
+ const html = renderToStaticMarkup(<CNPGBlockingSessions namespace="db" cluster="pg" />)
45
+ expect(html).toMatch(/CPU <span class="whitespace-nowrap">0.03 cores<\/span>/)
46
+ expect(html).toMatch(/Memory <span class="whitespace-nowrap">59 MiB<\/span>/)
47
+ expect(html).toContain('limit <span class="whitespace-nowrap">384 MiB</span>')
48
+ expect(html).not.toContain('Query text')
49
+ expect(html.indexOf('No session is waiting')).toBeLessThan(html.indexOf('Resource usage'))
50
+ })