@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
@@ -1,52 +1,38 @@
1
1
  import { useCallback, useEffect, useMemo } from 'react'
2
- import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'
3
- import { Activity, ArrowLeft, Database, ShieldCheck, Unplug } from 'lucide-react'
4
- import type { WorkloadExtraTab } from '@skyhook-io/k8s-ui'
2
+ import { useQueryClient } from '@tanstack/react-query'
3
+ import { refetchCNPGDetail } from './refresh'
4
+ import { useLocation, useSearchParams } from 'react-router-dom'
5
+ import { Activity, Database, Gauge, HardDrive, Network, Settings2, ShieldCheck, Unplug } from 'lucide-react'
6
+ import { CNPG_GROUP, refToSelectedResource, Tooltip, Badge, type WorkloadExtraTab } from '@skyhook-io/k8s-ui'
5
7
  import type { SelectedResource } from '../../types'
6
8
  import { useConnection } from '../../context/ConnectionContext'
7
- import { useContexts } from '../../api/client'
9
+ import { useContexts, useRadarFeature } from '../../api/client'
8
10
  import { useContextSwitchFlow } from '../useContextSwitchFlow'
9
11
  import { WorkloadView } from '../workload/WorkloadView'
10
- import { EmptyState } from '../capacity/shared'
11
12
  import { CNPGClusterActivity } from './CNPGClusterActivity'
12
- import { CNPGProtection } from './CNPGProtection'
13
- import { CNPGScreenGate } from './shared'
14
- import { CNPG_DETAIL_KINDS, CNPG_SCREENS, cnpgDetailKindFor, cnpgDetailPath, cnpgScreenPath, type CNPGDetailTarget } from './routes'
15
- import { currentPageLabel } from './paths'
16
- import { useCNPGFleet } from './useCNPGSidebarWorkspace'
13
+ import { CNPGPerformance } from './CNPGPerformance'
14
+ import { CNPGReplicationTab } from './CNPGReplicationTab'
15
+ import { CNPGBackupsTab, CNPGClusterServing, CNPGStorageTab, CNPGTabMark } from './CNPGClusterTabs'
16
+ import { CNPGConfiguration } from './CNPGConfiguration'
17
+ import type { CNPGChartGroup } from './CNPGTrends'
18
+ import { CNPG_DETAIL_KINDS, CNPG_SCREENS, cnpgDetailKindFor, cnpgDetailPath, cnpgScreenPath, cnpgViewNamedForKind, type CNPGDetailTarget } from './routes'
19
+ import { CNPG_CLUSTER_TAB_ORDER, cnpgDimensionTab } from './paths'
20
+ import { currentPageLabel } from '../../utils/page-links'
21
+ import { CNPGOperatorBanner } from './CNPGOperatorBanner'
22
+ import { ScreenEmptyState } from '../workspace/layout'
23
+ import { useCNPGNavigate } from './useCNPGNavigate'
17
24
 
25
+ // Set by drill-downs; Esc then goes back to that page instead of the home view.
18
26
  interface ReturnState {
19
27
  returnLabel?: string
20
28
  returnCtx?: string
21
29
  }
22
30
 
23
- function ClusterProtectionTab({ namespace, name, onInspect }: { namespace: string; name: string; onInspect: (r: SelectedResource) => void }) {
24
- const { query, fleet } = useCNPGFleet([namespace])
25
- const [searchParams] = useSearchParams()
26
- return (
27
- <CNPGScreenGate query={query} fleet={fleet}>
28
- {(data, readyFleet) => (
29
- <CNPGProtection
30
- data={data}
31
- fleet={readyFleet}
32
- namespaces={[namespace]}
33
- searchParams={searchParams}
34
- onSetParams={() => {}}
35
- onInspect={onInspect}
36
- inspected={null}
37
- onClearNamespaces={() => {}}
38
- scopeCluster={{ namespace, name }}
39
- />
40
- )}
41
- </CNPGScreenGate>
42
- )
43
- }
44
-
45
31
  /**
46
32
  * The full detail of a CloudNativePG object, framed by the workspace: the
47
- * Resources sidebar keeps the workspace destination highlighted, a return
48
- * control goes back to the task the user came from, and the crumb names the
49
- * object's place. The object's own sections come from Radar's detail view.
33
+ * Resources sidebar keeps the workspace destination highlighted and the crumb
34
+ * names the object's place. The object's own sections come from Radar's
35
+ * detail view.
50
36
  *
51
37
  * `ctx` in the URL pins the Kubernetes context; when the active context is a
52
38
  * different one, the page says so instead of loading a same-named object.
@@ -61,9 +47,12 @@ export function CNPGDetailPage({
61
47
  onOpenResource: (resource: SelectedResource) => void
62
48
  }) {
63
49
  const location = useLocation()
64
- const navigate = useNavigate()
50
+ const navigate = useCNPGNavigate()
65
51
  const [searchParams, setSearchParams] = useSearchParams()
66
52
  const { connection } = useConnection()
53
+ const queryClient = useQueryClient()
54
+ const { plural, group, namespace, name } = target
55
+ const refresh = useCallback(() => refetchCNPGDetail(queryClient, { plural, group, namespace, name }), [queryClient, plural, group, namespace, name])
67
56
  const activeContext = connection.context
68
57
  const pinnedContext = searchParams.get('ctx')
69
58
 
@@ -95,27 +84,95 @@ export function CNPGDetailPage({
95
84
  [navigate, activeContext, onOpenResource],
96
85
  )
97
86
 
87
+ // The extra tabs require the advertised runtime contracts.
88
+ const cnpgWorkspace = useRadarFeature('cnpgWorkspace').support === 'supported'
89
+ const isCluster = target.plural === 'clusters' && target.group === CNPG_GROUP && cnpgWorkspace
90
+ // A tab change on this page, applied like a tab click: the previous tab's
91
+ // own params (section, chart group, instance) are dropped unless named.
92
+ const goTab = useCallback(
93
+ (tab: string, extra: Record<string, string> = {}) => {
94
+ const params = new URLSearchParams(searchParams)
95
+ for (const k of ['section', 'charts', 'instance', 'pod', 'container', 'since', 'until']) params.delete(k)
96
+ params.set('tab', tab)
97
+ for (const [k, v] of Object.entries(extra)) params.set(k, v)
98
+ setSearchParams(params, { replace: true, state: location.state })
99
+ },
100
+ [searchParams, setSearchParams, location.state],
101
+ )
102
+ const openHistory = useCallback((charts: CNPGChartGroup) => goTab('performance', { section: 'history', charts }), [goTab])
98
103
  const extraTabs = useMemo<WorkloadExtraTab[] | undefined>(() => {
99
- if (target.plural !== 'clusters') return undefined
104
+ if (target.plural !== 'clusters' || target.group !== CNPG_GROUP) return undefined
105
+ const ns = target.namespace
106
+ const name = target.name
107
+ const activity: WorkloadExtraTab = {
108
+ id: 'activity',
109
+ label: 'Activity',
110
+ icon: <Activity className="h-4 w-4" />,
111
+ replaces: 'timeline',
112
+ render: () => <CNPGClusterActivity namespace={ns} name={name} onNavigate={openRelated} />,
113
+ }
114
+ if (!cnpgWorkspace) return [activity]
100
115
  return [
101
116
  {
102
- id: 'protection',
103
- label: 'Protection',
104
- icon: <ShieldCheck className="h-4 w-4" />,
105
- after: 'spec',
106
- render: () => <ClusterProtectionTab namespace={target.namespace} name={target.name} onInspect={onOpenResource} />,
117
+ id: 'replication',
118
+ label: 'Replication',
119
+ badge: <CNPGTabMark namespace={ns} name={name} id="replication" />,
120
+ icon: <Network className="h-4 w-4" />,
121
+ render: () => (
122
+ <CNPGReplicationTab
123
+ namespace={ns}
124
+ name={name}
125
+ onOpenLogs={(pod) => goTab('logs', { pod })}
126
+ onOpenHistory={() => openHistory('replication')}
127
+ onNavigate={(ref) => openRelated(refToSelectedResource(ref))}
128
+ />
129
+ ),
107
130
  },
108
131
  {
109
- id: 'activity',
110
- label: 'Activity',
111
- icon: <Activity className="h-4 w-4" />,
112
- replaces: 'timeline',
132
+ id: 'storage',
133
+ label: 'Storage',
134
+ badge: <CNPGTabMark namespace={ns} name={name} id="storage" />,
135
+ icon: <HardDrive className="h-4 w-4" />,
136
+ render: () => <CNPGStorageTab namespace={ns} name={name} onOpenHistory={() => openHistory('storage')} onOpenReplication={() => goTab('replication')} />,
137
+ },
138
+ {
139
+ id: 'performance',
140
+ label: 'Performance',
141
+ icon: <Gauge className="h-4 w-4" />,
142
+ render: () => (
143
+ <CNPGPerformance
144
+ namespace={ns}
145
+ name={name}
146
+ onOpenInterval={(tab, since, until) => {
147
+ const params = new URLSearchParams(searchParams)
148
+ params.delete('pod')
149
+ params.set('tab', tab)
150
+ params.set('since', since)
151
+ params.set('until', until)
152
+ setSearchParams(params, { state: location.state })
153
+ }}
154
+ />
155
+ ),
156
+ },
157
+ {
158
+ id: 'backups',
159
+ label: 'Backups',
160
+ badge: <CNPGTabMark namespace={ns} name={name} id="protection" />,
161
+ icon: <ShieldCheck className="h-4 w-4" />,
113
162
  render: () => (
114
- <CNPGClusterActivity namespace={target.namespace} name={target.name} onNavigate={openRelated} />
163
+ <CNPGBackupsTab
164
+ namespace={ns}
165
+ name={name}
166
+ onInspect={onOpenResource}
167
+ onOpenYaml={() => goTab('yaml')}
168
+ onOpenLogs={(pod, container) => goTab('logs', { pod, container })}
169
+ onOpenOperator={() => navigate(cnpgScreenPath('operator'), { state: { returnLabel: currentPageLabel(), returnCtx: activeContext } satisfies ReturnState })}
170
+ />
115
171
  ),
116
172
  },
173
+ activity,
117
174
  ]
118
- }, [target.plural, target.namespace, target.name, onOpenResource, openRelated])
175
+ }, [cnpgWorkspace, target.plural, target.group, target.namespace, target.name, goTab, openHistory, onOpenResource, openRelated, navigate, activeContext, searchParams, setSearchParams, location.state])
119
176
 
120
177
  if (pinnedContext && activeContext && pinnedContext !== activeContext) {
121
178
  return <NotInContext target={target} pinnedContext={pinnedContext} activeContext={activeContext} homeLabel={home.label} homePath={home.path} />
@@ -123,61 +180,65 @@ export function CNPGDetailPage({
123
180
 
124
181
  const outsideFilter = namespaces.length > 0 && !!target.namespace && !namespaces.includes(target.namespace)
125
182
 
126
- const breadcrumb = (
127
- <div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-sm">
128
- {returnLabel && (
129
- <>
130
- <button
131
- type="button"
132
- onClick={() => navigate(-1)}
133
- aria-label={`Return to ${returnLabel}`}
134
- className="inline-flex items-center gap-1 rounded-md py-0.5 pr-2 text-theme-text-secondary hover:text-theme-text-primary"
135
- >
136
- <ArrowLeft className="h-3.5 w-3.5" />
137
- {returnLabel}
138
- </button>
139
- <span className="h-4 w-px bg-theme-border" aria-hidden />
140
- </>
141
- )}
142
- <nav aria-label="Location" className="flex min-w-0 items-center gap-1.5 text-theme-text-tertiary">
183
+ // Where the object lives, on its title line: workspace / view /. The way
184
+ // back to the previous page is the browser's Back, or Esc.
185
+ const titlePrefix = (
186
+ <div className="flex min-w-0 items-center gap-x-2 text-sm">
187
+ <nav aria-label="Location" className="flex shrink-0 items-center gap-1.5 text-theme-text-tertiary">
143
188
  <Database className="h-3.5 w-3.5" />
144
- <button type="button" onClick={() => navigate(cnpgScreenPath('overview'))} className="hover:text-theme-text-primary hover:underline">
145
- CloudNativePG
189
+ <span>CloudNativePG</span>
190
+ <span>/</span>
191
+ <button type="button" onClick={() => navigate(home.path)} className="hover:text-theme-text-primary hover:underline">
192
+ {home.label}
146
193
  </button>
147
- {home.id !== 'overview' && (
148
- <>
149
- <span>/</span>
150
- <button type="button" onClick={() => navigate(home.path)} className="hover:text-theme-text-primary hover:underline">
151
- {home.label}
152
- </button>
153
- </>
154
- )}
155
194
  <span>/</span>
156
- <span className="truncate text-theme-text-secondary">{target.name}</span>
157
195
  </nav>
158
- {outsideFilter && (
159
- <span className="text-xs text-theme-text-tertiary">
160
- Namespace {target.namespace} is outside your namespace filter; this object stays open.
161
- </span>
162
- )}
163
196
  </div>
164
197
  )
198
+ const namespaceNote = outsideFilter ? (
199
+ <Tooltip content="Your namespace filter leaves this namespace out; an object you open stays open." position="bottom">
200
+ <span className="whitespace-nowrap text-xs text-theme-text-tertiary">· outside your namespace filter</span>
201
+ </Tooltip>
202
+ ) : undefined
165
203
 
166
204
  return (
167
205
  <div className="flex h-full min-h-0 min-w-0 flex-1 flex-col">
168
- <WorkloadView
169
- key={`${target.plural}/${target.namespace}/${target.name}`}
170
- kind={target.plural}
171
- namespace={target.namespace}
172
- name={target.name}
173
- group={target.group}
174
- expanded
175
- onBack={() => (returnLabel ? navigate(-1) : navigate(home.path))}
176
- hideBackButton
177
- breadcrumb={breadcrumb}
178
- onNavigateToResource={openRelated}
179
- extraTabs={extraTabs}
180
- />
206
+ {/* Above the title line, before any status the page shows. */}
207
+ {target.plural === 'clusters' && <CNPGOperatorBanner namespaces={[target.namespace]} className="mx-6 mt-3 shrink-0" />}
208
+ <div className="flex min-h-0 flex-1 flex-col">
209
+ <WorkloadView
210
+ key={`${target.plural}/${target.namespace}/${target.name}`}
211
+ kind={target.plural}
212
+ namespace={target.namespace}
213
+ name={target.name}
214
+ group={target.group}
215
+ expanded
216
+ onBack={() => (returnLabel ? navigate(-1) : navigate(home.path))}
217
+ hideBackButton
218
+ titlePrefix={titlePrefix}
219
+ inlineBadges
220
+ namespaceNote={namespaceNote}
221
+ hideKindBadge={cnpgViewNamedForKind(target.plural)}
222
+ onNavigateToResource={openRelated}
223
+ onRefresh={cnpgWorkspace ? refresh : undefined}
224
+ extraTabs={extraTabs}
225
+ tabOrder={isCluster ? CNPG_CLUSTER_TAB_ORDER : undefined}
226
+ renderStatusBadge={isCluster ? (resource) => (
227
+ <Tooltip content={resource?.status?.phase ? `status.phase, reported by CloudNativePG: ${resource.status.phase}` : 'CloudNativePG has not reported status.phase'}>
228
+ <Badge severity="neutral">
229
+ <span className="mr-1 font-normal text-theme-text-tertiary">Phase</span>
230
+ {resource?.status?.phase ? resource.status.phase : 'Not reported'}
231
+ </Badge>
232
+ </Tooltip>
233
+ ) : undefined}
234
+ statusNote={isCluster ? <CNPGClusterServing namespace={target.namespace} name={target.name} onSelect={() => goTab(cnpgDimensionTab('serving'))} /> : undefined}
235
+ specTab={
236
+ isCluster
237
+ ? { label: 'Configuration', icon: <Settings2 className="h-4 w-4" />, render: () => <CNPGConfiguration namespace={target.namespace} name={target.name} onNavigate={openRelated} onSelectTab={goTab} /> }
238
+ : undefined
239
+ }
240
+ />
241
+ </div>
181
242
  </div>
182
243
  )
183
244
  }
@@ -195,13 +256,13 @@ function NotInContext({
195
256
  homeLabel: string
196
257
  homePath: string
197
258
  }) {
198
- const navigate = useNavigate()
259
+ const navigate = useCNPGNavigate()
199
260
  const { data: contexts } = useContexts()
200
261
  const { requestSwitch, confirmDialog } = useContextSwitchFlow()
201
262
  const pinned = contexts?.find((c) => c.name === pinnedContext)
202
263
  return (
203
264
  <>
204
- <EmptyState
265
+ <ScreenEmptyState
205
266
  icon={Unplug}
206
267
  title={`${target.name} is not in ${activeContext}`}
207
268
  detail={`This link points at ${CNPG_DETAIL_KINDS[target.plural].kind} ${target.name} in context ${pinnedContext}. Radar does not open a same-named object from another cluster.`}
@@ -215,9 +276,9 @@ function NotInContext({
215
276
  <button
216
277
  type="button"
217
278
  onClick={() => navigate(homePath)}
218
- className="rounded-lg border border-theme-border bg-theme-surface px-3 py-1.5 text-sm text-theme-text-primary hover:bg-theme-hover"
279
+ className="btn-secondary px-3 py-1.5 text-sm"
219
280
  >
220
- Go to {homeLabel === 'Overview' ? 'CloudNativePG Overview' : homeLabel}
281
+ Go to CloudNativePG {homeLabel}
221
282
  </button>
222
283
  </div>
223
284
  }
@@ -2,7 +2,7 @@ import { useLocation, useSearchParams } from 'react-router-dom'
2
2
  import { ArrowLeft } from 'lucide-react'
3
3
  import { CNPG_KIND_BY_KEY } from '@skyhook-io/k8s-ui'
4
4
  import type { SelectedResource } from '../../types'
5
- import { decodeDrawerTrail, encodeDrawerTrail, sameResource } from './routes'
5
+ import { decodeDrawerTrail, encodeDrawerTrail, sameSelectedResource } from '../../utils/drawer-trail'
6
6
 
7
7
  const KIND_BY_PLURAL: Record<string, string> = Object.fromEntries(
8
8
  Object.values(CNPG_KIND_BY_KEY).map((k) => [k.plural, k.kind]),
@@ -19,7 +19,7 @@ export function CNPGDrawerTrailBack({ resource }: { resource: SelectedResource }
19
19
  const [searchParams, setSearchParams] = useSearchParams()
20
20
  if (!location.pathname.startsWith('/cnpg')) return null
21
21
  const trail = decodeDrawerTrail(searchParams.get('drawer'))
22
- if (trail.length < 2 || !sameResource(trail[trail.length - 1], resource)) return null
22
+ if (trail.length < 2 || !sameSelectedResource(trail[trail.length - 1], resource)) return null
23
23
  const prev = trail[trail.length - 2]
24
24
  const back = () => {
25
25
  const params = new URLSearchParams(searchParams)
@@ -0,0 +1,30 @@
1
+ import { renderToStaticMarkup } from 'react-dom/server'
2
+ import { expect, it, vi } from 'vitest'
3
+ import { CNPGInvestigationAction } from './CNPGClusterTabs'
4
+ const state = vi.hoisted(() => ({ row: undefined as any, dimensions: undefined as any }))
5
+ vi.mock('./useCNPGClusterAssessment', () => ({ useCNPGClusterAssessment: () => state }))
6
+ it('labels investigation for any CNPG problem or degraded dimension including a healthy controller phase', () => {
7
+ const render = vi.fn((ctx) => <button>{ctx.health === 'problem' ? 'Investigate' : 'Sparkle'}</button>)
8
+ const draw = () => renderToStaticMarkup(<CNPGInvestigationAction namespace="db" name="payments" render={render} context={{ kind: 'Cluster', group: 'postgresql.cnpg.io', namespace: 'db', name: 'payments', health: 'healthy' }} />)
9
+ state.row = { problems: [{ severity: 'warning' }], controllerStatus: { level: 'healthy' } }
10
+ expect(draw()).toContain('Investigate')
11
+ state.row = { problems: [] }
12
+ for (const tone of ['degraded', 'unhealthy', 'alert']) { state.dimensions = [{ tone }]; expect(draw()).toContain('Investigate') }
13
+ state.dimensions = [{ tone: 'healthy' }]; expect(draw()).toContain('Sparkle')
14
+ state.dimensions = [{ tone: 'unknown' }]; draw()
15
+ expect(render).toHaveBeenLastCalledWith(expect.objectContaining({ health: 'unknown' }))
16
+ state.row = undefined; state.dimensions = undefined; draw()
17
+ expect(render).toHaveBeenLastCalledWith(expect.objectContaining({ health: 'unknown', name: 'payments' }))
18
+ })
19
+
20
+ it('honors the requested any-problem rule for posture and degraded protection without altering fleet attention', () => {
21
+ const render = vi.fn(() => null)
22
+ state.row = { attention: false, problems: [{ id: 'audit:cnpgNoDeclarativeBackup', severity: 'posture' }] }
23
+ state.dimensions = [{ tone: 'healthy' }]
24
+ renderToStaticMarkup(<CNPGInvestigationAction namespace="db" name="payments" render={render} context={{ kind: 'Cluster', group: 'postgresql.cnpg.io', namespace: 'db', name: 'payments', health: 'healthy' }} />)
25
+ expect(render).toHaveBeenLastCalledWith(expect.objectContaining({ health: 'problem' }))
26
+ expect(state.row.attention).toBe(false)
27
+ state.row = { attention: false, problems: [] }; state.dimensions = [{ tone: 'degraded' }]
28
+ renderToStaticMarkup(<CNPGInvestigationAction namespace="db" name="payments" render={render} context={{ kind: 'Cluster', group: 'postgresql.cnpg.io', namespace: 'db', name: 'payments', health: 'healthy' }} />)
29
+ expect(render).toHaveBeenLastCalledWith(expect.objectContaining({ health: 'problem' }))
30
+ })
@@ -0,0 +1,34 @@
1
+ import { renderToStaticMarkup } from 'react-dom/server'
2
+ import { expect, it, vi } from 'vitest'
3
+ import { buildCNPGFleet, CNPG_WORKSPACE_KEYS, type CNPGWorkspaceResponse } from '@skyhook-io/k8s-ui'
4
+ import { CNPGOperator, ConfigBlock } from './CNPGOperator'
5
+ vi.mock('./useCNPGNavigate', () => ({ useCNPGNavigate: () => vi.fn() }))
6
+ const operator = vi.hoisted(() => ({ partial: false }))
7
+ vi.mock('../../api/cnpg', () => ({ useCNPGOperator: () => ({ data: { coverage: { deployments: { state: operator.partial ? 'partial' : 'full' }, services: { state: operator.partial ? 'partial' : 'full' } }, components: operator.partial ? [] : [{ role: 'operator', namespace: 'operator', deployment: 'cnpg', readyReplicas: 1, replicas: 1 }], config: [], diagnosis: [] }, isRefetchError: true, error: new Error('Operator timeout'), dataUpdatedAt: Date.now() - 240_000 }) }))
8
+ vi.mock('../../context/ConnectionContext', () => ({ useConnection: () => ({ connection: { context: 'test' } }) }))
9
+ it('labels retained operator data after a failed refresh', () => {
10
+ const data: CNPGWorkspaceResponse = { installed: true, context: 'test', namespaces: null, coverage: Object.fromEntries(CNPG_WORKSPACE_KEYS.map((k) => [k, { state: 'full' }])), objects: {}, issues: [], audit: [], backupsOmitted: 0 }
11
+ const html = renderToStaticMarkup(<CNPGOperator data={data} fleet={buildCNPGFleet(data)} namespaces={[]} searchParams={new URLSearchParams()} onSetParams={() => {}} onInspect={() => {}} inspected={null} onClearNamespaces={() => {}} />)
12
+ expect(html).toContain('CloudNativePG operator')
13
+ expect(html).toContain('Last refresh failed: Operator timeout')
14
+ expect(html).toContain('4m ago')
15
+ })
16
+
17
+ it('renders a known-absent ConfigMap as text and does not claim default settings', () => {
18
+ const html = renderToStaticMarkup(<ConfigBlock config={{ kind: 'ConfigMap', namespace: 'operator', name: 'missing-settings', purpose: 'operator', readable: true, exists: false }} onInspect={vi.fn()} />)
19
+ expect(html).toContain('This referenced ConfigMap does not exist; settings from other sources are not shown here')
20
+ expect(html).not.toContain('<button')
21
+ expect(html).not.toContain('defaults')
22
+ })
23
+
24
+ it('states unread operator health once and keeps a short component empty line', () => {
25
+ operator.partial = true
26
+ const data: CNPGWorkspaceResponse = { installed: true, context: 'test', namespaces: null, coverage: Object.fromEntries(CNPG_WORKSPACE_KEYS.map((k) => [k, { state: 'full' }])), objects: {}, issues: [], audit: [], backupsOmitted: 0 }
27
+ const html = renderToStaticMarkup(<CNPGOperator data={data} fleet={buildCNPGFleet(data)} namespaces={[]} searchParams={new URLSearchParams()} onSetParams={() => {}} onInspect={() => {}} inspected={null} onClearNamespaces={() => {}} />)
28
+ expect(html.match(/Operator health cannot be assessed with your access/g)).toHaveLength(1)
29
+ expect(html).toContain('Deployments not read in some namespaces')
30
+ expect(html).toContain('No operator or plugin Deployments found')
31
+ expect(html).not.toContain('No operator Deployment was found')
32
+ expect(html).not.toContain('Some workloads are not readable')
33
+ operator.partial = false
34
+ })
@@ -1,19 +1,11 @@
1
1
  import { useMemo } from 'react'
2
- import { Badge, getCNPGImageCatalogEntries, isApiGroup, PaneLoader, Tooltip } from '@skyhook-io/k8s-ui'
2
+ import { Badge, FoldSection, getCNPGImageCatalogEntries, isApiGroup, PaneLoader, StatusDot, Tooltip, toneTextClass, type CNPGFleetRow } from '@skyhook-io/k8s-ui'
3
+ import { parse } from 'yaml'
3
4
  import { useCNPGOperator, type CNPGOperatorComponent, type CNPGOperatorConfig } from '../../api/cnpg'
4
- import { Notice } from '../capacity/shared'
5
- import {
6
- CNPGWorkspaceHeader,
7
- CoverageNotice,
8
- coverageEmpty,
9
- worstCoverage,
10
- Mono,
11
- ScreenBody,
12
- SectionTable,
13
- Sub,
14
- cnpgResource,
15
- type CNPGScreenProps,
16
- } from './shared'
5
+ import { CNPGOperatorDiagnosisSection } from './CNPGOperatorDiagnosis'
6
+ import { cnpgOperatorState, cnpgRestartHistory } from './operatorStatus'
7
+ import { CNPGWorkspaceHeader, CoverageNotice, coverageEmpty, worstCoverage, cnpgResource, type CNPGScreenProps } from './shared'
8
+ import { Mono, Notice, RefreshFailedNotice, ScreenBody, SectionTable, Sub } from '../workspace/layout'
17
9
 
18
10
  interface CatalogRow {
19
11
  key: string
@@ -28,7 +20,53 @@ function readiness(c: CNPGOperatorComponent) {
28
20
  if (c.readyReplicas === null || c.replicas === null) return <span className="text-theme-text-tertiary">Unknown</span>
29
21
  if (c.replicas === 0) return <Badge severity="warning" size="sm">Scaled to 0</Badge>
30
22
  const ok = c.readyReplicas >= c.replicas
31
- return <Badge severity={ok ? 'success' : c.readyReplicas === 0 ? 'error' : 'warning'} size="sm">{c.readyReplicas}/{c.replicas} ready</Badge>
23
+ const history = cnpgRestartHistory(c)
24
+ return (
25
+ <>
26
+ <Badge severity={ok ? 'success' : c.readyReplicas === 0 ? 'error' : 'warning'} size="sm">{c.readyReplicas}/{c.replicas} ready</Badge>
27
+ {history ? (
28
+ <Sub>
29
+ <span className={history.recent ? toneTextClass('degraded') : undefined}>{history.text}</span>
30
+ </Sub>
31
+ ) : c.pods ? (
32
+ <Sub>no restarts</Sub>
33
+ ) : c.podCoverage && c.podCoverage.state !== 'ok' ? (
34
+ <Sub>restarts not read{c.podCoverage.reason ? `: ${c.podCoverage.reason}` : ''}</Sub>
35
+ ) : null}
36
+ </>
37
+ )
38
+ }
39
+
40
+ function CurrentState({ op, clusters }: { op: NonNullable<ReturnType<typeof useCNPGOperator>['data']>; clusters: CNPGFleetRow[] }) {
41
+ const { concerns, confirmed, unread } = cnpgOperatorState(op, Date.now(), clusters)
42
+ return (
43
+ <section className="rounded-xl border border-theme-border bg-theme-surface px-4 py-3 shadow-theme-sm">
44
+ <h3 className="text-sm font-semibold text-theme-text-primary">Current state</h3>
45
+ {concerns.length > 0 && (
46
+ <ul className="mt-1.5 space-y-1">
47
+ {concerns.map((c) => (
48
+ <li key={c.text} className="flex items-start gap-2 text-sm">
49
+ <span className="flex h-5 shrink-0 items-center"><StatusDot tone={c.tone} /></span>
50
+ <span className={c.tone === 'neutral' ? 'text-theme-text-secondary' : toneTextClass(c.tone)}>{c.text}</span>
51
+ </li>
52
+ ))}
53
+ </ul>
54
+ )}
55
+ {confirmed.length > 0 && (
56
+ <p className="mt-1.5 text-sm text-theme-text-secondary">
57
+ {concerns.length === 0 ? 'No concerns in what Radar read: ' : 'Otherwise: '}
58
+ {confirmed.join(', ')}.
59
+ </p>
60
+ )}
61
+ {unread.length > 0 && (
62
+ <ul className="mt-1.5 space-y-0.5">
63
+ {unread.map((u) => (
64
+ <li key={u} className="text-xs text-theme-text-tertiary">{u}</li>
65
+ ))}
66
+ </ul>
67
+ )}
68
+ </section>
69
+ )
32
70
  }
33
71
 
34
72
  export function CNPGOperator({ data, fleet, onInspect, inspected }: CNPGScreenProps) {
@@ -59,29 +97,23 @@ export function CNPGOperator({ data, fleet, onInspect, inspected }: CNPGScreenPr
59
97
 
60
98
  const direct = fleet.rows.filter((r) => !r.cluster?.spec?.imageCatalogRef)
61
99
  const op = operator.data
62
- const coverageGaps = op
63
- ? (['deployments', 'services'] as const).filter((k) => op.coverage[k]?.state !== 'full')
64
- : []
65
100
 
66
101
  return (
67
102
  <div className="flex min-h-0 flex-1 flex-col">
68
103
  <CNPGWorkspaceHeader
69
104
  title="Operator"
70
- subtitle="Operator and plugin workloads, their versions, image catalogs and operator configuration."
105
+ subtitle="Operator and plugin workloads, whether the operator is leading, watching and reachable, image catalogs and operator configuration."
71
106
  />
72
107
  <ScreenBody>
73
- <CoverageNotice fleet={fleet} data={data} />
108
+ <CoverageNotice fleet={fleet} data={data} kinds={['clusters', 'imageCatalogs', 'clusterImageCatalogs']} />
109
+ <RefreshFailedNotice queries={[operator]} />
74
110
  {operator.isLoading && !op ? (
75
111
  <PaneLoader label="Loading operator…" className="h-32" />
76
112
  ) : !op ? (
77
113
  <Notice>Operator details could not be loaded{operator.error instanceof Error ? `: ${operator.error.message}` : '.'}</Notice>
78
114
  ) : (
79
115
  <>
80
- {coverageGaps.length > 0 && (
81
- <Notice>
82
- Some workloads are not readable ({coverageGaps.map((k) => `${k}: ${op.coverage[k].state}`).join(', ')}), so an operator or plugin running in those namespaces may be missing below.
83
- </Notice>
84
- )}
116
+ <CurrentState op={op} clusters={fleet.rows} />
85
117
  <SectionTable
86
118
  title="Operator and plugins"
87
119
  columns={[
@@ -104,10 +136,10 @@ export function CNPGOperator({ data, fleet, onInspect, inspected }: CNPGScreenPr
104
136
  </Tooltip>
105
137
  ),
106
138
  },
107
- { header: 'Ready', width: '14%', cell: readiness },
139
+ { header: 'Ready', width: '24%', cell: readiness },
108
140
  {
109
141
  header: 'Workload',
110
- width: '42%',
142
+ width: '32%',
111
143
  cell: (c) =>
112
144
  c.deployment ? (
113
145
  <>Deployment <Mono>{c.deployment}</Mono><Sub>{c.namespace}</Sub></>
@@ -126,6 +158,7 @@ export function CNPGOperator({ data, fleet, onInspect, inspected }: CNPGScreenPr
126
158
  inspected={inspected}
127
159
  empty="No operator or plugin Deployments found in the namespaces you can read."
128
160
  />
161
+ {op.diagnosis && <CNPGOperatorDiagnosisSection diagnosis={op.diagnosis} fleet={fleet} />}
129
162
  </>
130
163
  )}
131
164
 
@@ -191,12 +224,12 @@ export function CNPGOperator({ data, fleet, onInspect, inspected }: CNPGScreenPr
191
224
  )
192
225
  }
193
226
 
194
- function ConfigBlock({ config, onInspect }: { config: CNPGOperatorConfig; onInspect: CNPGScreenProps['onInspect'] }) {
227
+ export function ConfigBlock({ config, onInspect }: { config: CNPGOperatorConfig; onInspect: CNPGScreenProps['onInspect'] }) {
195
228
  const open = () => onInspect({ kind: config.kind === 'ConfigMap' ? 'configmaps' : 'secrets', group: '', namespace: config.namespace, name: config.name })
196
229
  const title = (
197
230
  <div className="flex flex-wrap items-baseline gap-x-2 border-b border-theme-border px-4 py-2.5">
198
231
  <span className="text-xs text-theme-text-tertiary">{config.kind}</span>
199
- <button type="button" onClick={open} className="font-mono text-sm text-accent-text hover:underline">{config.name}</button>
232
+ {config.exists === false ? <span className="font-mono text-sm text-theme-text-primary">{config.name}</span> : <button type="button" onClick={open} className="font-mono text-sm text-accent-text hover:underline">{config.name}</button>}
200
233
  <span className="text-xs text-theme-text-tertiary">
201
234
  {config.namespace} · {config.purpose === 'monitoring' ? 'monitoring queries' : 'operator settings'}
202
235
  </span>
@@ -206,9 +239,24 @@ function ConfigBlock({ config, onInspect }: { config: CNPGOperatorConfig; onInsp
206
239
  if (config.kind === 'Secret') {
207
240
  body = <div className="px-4 py-2.5 text-sm text-theme-text-secondary">Referenced by the operator. Secret contents are not shown here.</div>
208
241
  } else if (config.exists === false) {
209
- body = <div className="px-4 py-2.5 text-sm text-theme-text-secondary">Referenced but does not exist; the operator runs with its defaults.</div>
242
+ body = <div className="px-4 py-2.5 text-sm text-theme-text-secondary">This referenced ConfigMap does not exist; settings from other sources are not shown here.</div>
210
243
  } else if (!config.readable) {
211
244
  body = <div className="px-4 py-2.5 text-sm text-theme-text-tertiary">{config.reason ?? 'Not readable with your access.'}</div>
245
+ } else if (config.purpose === 'monitoring') {
246
+ const queries = config.data?.queries ?? ''
247
+ let count: number | null = null
248
+ try {
249
+ const parsed = parse(queries)
250
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) count = Object.keys(parsed).length
251
+ } catch { /* The raw configuration remains inspectable when it cannot be parsed. */ }
252
+ body = <div className="px-4 py-2.5 text-sm text-theme-text-secondary">
253
+ <p>{count === null ? 'Query count unavailable' : `${count} ${count === 1 ? 'query' : 'queries'}`} · ConfigMap {config.name}</p>
254
+ <FoldSection title="Query definitions" summary="Show SQL and settings" attention={false}>
255
+ <pre className="max-h-80 overflow-auto whitespace-pre-wrap break-normal font-mono text-xs [overflow-wrap:anywhere]">{queries.slice(0, 8000)}</pre>
256
+ {queries.length > 8000 && <p className="mt-1 text-xs text-theme-text-tertiary">Preview truncated to 8,000 of {queries.length.toLocaleString()} characters.</p>}
257
+ <button type="button" onClick={open} className="mt-2 text-xs text-accent-text hover:underline">Inspect ConfigMap {config.name} →</button>
258
+ </FoldSection>
259
+ </div>
212
260
  } else {
213
261
  const entries = Object.entries(config.data ?? {})
214
262
  body =
@@ -0,0 +1,39 @@
1
+ import { clsx } from 'clsx'
2
+ import { AlertBanner } from '@skyhook-io/k8s-ui'
3
+ import { useCNPGOperatorStatus } from '../../api/cnpg'
4
+ import { cnpgScreenPath } from './routes'
5
+ import { cnpgOperatorBannerModel } from './operatorStatus'
6
+ import { useCNPGNavigate } from './useCNPGNavigate'
7
+
8
+ /**
9
+ * Shown above CNPG status while the operator that watches these namespaces is
10
+ * observed not leading, or its admission webhook rejects writes: the phase and
11
+ * ready counts below are then what the operator last wrote.
12
+ */
13
+ export function CNPGOperatorBanner({ namespaces, className }: { namespaces: string[]; className?: string }) {
14
+ const navigate = useCNPGNavigate()
15
+ const q = useCNPGOperatorStatus(namespaces)
16
+ const model = cnpgOperatorBannerModel(q.data?.namespaces, namespaces)
17
+ if (!model) return null
18
+ const { stale, rejects } = model
19
+ const title = stale
20
+ ? 'The CloudNativePG operator is not reconciling: status below may be stale'
21
+ : 'CloudNativePG writes are being rejected'
22
+ const scope = stale && stale.namespaces.length < new Set(namespaces).size ? `Clusters in ${stale.namespaces.join(', ')}.` : ''
23
+ // Tighter than the default banner, never smaller: the title, icon, colours
24
+ // and every reason stay; the link moves onto the title line. A host that
25
+ // passes no className keeps the default bottom margin.
26
+ return (
27
+ <AlertBanner
28
+ variant="warning"
29
+ title={title}
30
+ message={[scope, ...(stale?.reasons ?? []), rejects ?? ''].filter(Boolean).join(' ') || undefined}
31
+ action={
32
+ <button type="button" onClick={() => navigate(cnpgScreenPath('operator'))} className="text-xs font-medium text-accent-text hover:underline">
33
+ Open Operator →
34
+ </button>
35
+ }
36
+ className={clsx('px-3 py-2', className ?? 'mb-4')}
37
+ />
38
+ )
39
+ }