@skyhook-io/radar-app 1.16.3 → 1.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (321) hide show
  1. package/README.md +1 -1
  2. package/package.json +3 -3
  3. package/src/App.tsx +138 -93
  4. package/src/RadarApp.tsx +27 -17
  5. package/src/api/actions.errorCode.test.ts +40 -0
  6. package/src/api/actions.test.ts +48 -0
  7. package/src/api/actions.ts +79 -0
  8. package/src/api/apiResources.navigation.test.tsx +232 -0
  9. package/src/api/apiResources.ts +46 -3
  10. package/src/api/client.goneResource.test.ts +49 -0
  11. package/src/api/client.helmSource.test.ts +39 -0
  12. package/src/api/client.namespaceSwitch.hook.test.tsx +54 -0
  13. package/src/api/client.namespaceSwitch.test.ts +107 -0
  14. package/src/api/client.ts +381 -47
  15. package/src/api/cnpg-ha.live.test.ts +140 -0
  16. package/src/api/cnpg-ha.test.ts +78 -0
  17. package/src/api/cnpg-ha.ts +19 -0
  18. package/src/api/cnpg-history.ts +123 -0
  19. package/src/api/cnpg-inspect.ts +102 -0
  20. package/src/api/cnpg-protection.ts +112 -0
  21. package/src/api/cnpg-recovery.test.tsx +60 -0
  22. package/src/api/cnpg-recovery.ts +262 -0
  23. package/src/api/cnpg-sessions.ts +175 -0
  24. package/src/api/cnpg-storage.ts +138 -0
  25. package/src/api/cnpg.legacy-reads.test.ts +43 -0
  26. package/src/api/cnpg.pooler-runtime.test.ts +15 -0
  27. package/src/api/cnpg.ts +428 -10
  28. package/src/api/diagnose.test.ts +40 -0
  29. package/src/api/diagnose.ts +12 -1
  30. package/src/api/radarFeatures.render.test.tsx +65 -3
  31. package/src/api/radarFeatures.ts +6 -0
  32. package/src/api/reviewedActionMutation.test.tsx +164 -0
  33. package/src/api/reviewedActionMutation.ts +66 -0
  34. package/src/api/timelineSource.test.ts +9 -0
  35. package/src/api/timelineSource.ts +8 -0
  36. package/src/api/traffic.sources.test.ts +55 -0
  37. package/src/api/traffic.ts +173 -14
  38. package/src/components/applications/ApplicationsView.tsx +3 -0
  39. package/src/components/audit/AuditView.tsx +9 -0
  40. package/src/components/capacity/CapacityActivity.tsx +3 -31
  41. package/src/components/capacity/CapacityDemand.tsx +4 -36
  42. package/src/components/capacity/CapacityOverview.tsx +217 -56
  43. package/src/components/capacity/CapacityPoolDetail.tsx +24 -62
  44. package/src/components/capacity/CapacityView.test.tsx +354 -1
  45. package/src/components/capacity/ClusterSchedulingCard.tsx +2 -10
  46. package/src/components/capacity/certaintyGlyph.test.tsx +2 -9
  47. package/src/components/capacity/routes.ts +42 -0
  48. package/src/components/capacity/shared.tsx +23 -150
  49. package/src/components/capacity/workspaceRoute.ts +18 -0
  50. package/src/components/cnpg/CNPGActionHost.compatibility.test.tsx +30 -0
  51. package/src/components/cnpg/CNPGArchivingRepair.tsx +191 -0
  52. package/src/components/cnpg/CNPGBlockingSessions.test.tsx +50 -0
  53. package/src/components/cnpg/CNPGBlockingSessions.tsx +333 -0
  54. package/src/components/cnpg/CNPGClusterActivity.test.tsx +11 -0
  55. package/src/components/cnpg/CNPGClusterActivity.tsx +18 -9
  56. package/src/components/cnpg/CNPGClusterLogs.compatibility.test.tsx +130 -0
  57. package/src/components/cnpg/CNPGClusterLogs.empty.test.tsx +29 -0
  58. package/src/components/cnpg/CNPGClusterLogs.test.ts +9 -0
  59. package/src/components/cnpg/CNPGClusterLogs.tsx +81 -24
  60. package/src/components/cnpg/CNPGClusterTabs.test.tsx +152 -0
  61. package/src/components/cnpg/CNPGClusterTabs.tsx +187 -0
  62. package/src/components/cnpg/CNPGClustersKindView.tsx +46 -0
  63. package/src/components/cnpg/CNPGConfiguration.test.tsx +223 -0
  64. package/src/components/cnpg/CNPGConfiguration.tsx +190 -0
  65. package/src/components/cnpg/CNPGCreateClusterDialog.test.tsx +113 -0
  66. package/src/components/cnpg/CNPGCreateClusterDialog.tsx +74 -0
  67. package/src/components/cnpg/CNPGDeclarations.test.ts +107 -0
  68. package/src/components/cnpg/CNPGDeclarations.tsx +236 -78
  69. package/src/components/cnpg/CNPGDetailPage.test.tsx +62 -0
  70. package/src/components/cnpg/CNPGDetailPage.tsx +159 -98
  71. package/src/components/cnpg/CNPGDrawerTrail.tsx +2 -2
  72. package/src/components/cnpg/CNPGInvestigationAction.test.tsx +30 -0
  73. package/src/components/cnpg/CNPGOperator.test.tsx +34 -0
  74. package/src/components/cnpg/CNPGOperator.tsx +78 -30
  75. package/src/components/cnpg/CNPGOperatorBanner.tsx +39 -0
  76. package/src/components/cnpg/CNPGOperatorConditions.test.tsx +45 -0
  77. package/src/components/cnpg/CNPGOperatorConditions.tsx +16 -0
  78. package/src/components/cnpg/CNPGOperatorConfig.test.tsx +27 -0
  79. package/src/components/cnpg/CNPGOperatorDiagnosis.tsx +247 -0
  80. package/src/components/cnpg/CNPGOverview.test.tsx +171 -0
  81. package/src/components/cnpg/CNPGOverview.tsx +238 -63
  82. package/src/components/cnpg/CNPGParametersInEffect.test.tsx +51 -0
  83. package/src/components/cnpg/CNPGParametersInEffect.tsx +120 -0
  84. package/src/components/cnpg/CNPGPerformance.prerequisite.test.tsx +48 -0
  85. package/src/components/cnpg/CNPGPerformance.test.tsx +73 -0
  86. package/src/components/cnpg/CNPGPerformance.tsx +477 -0
  87. package/src/components/cnpg/CNPGPooling.test.tsx +89 -0
  88. package/src/components/cnpg/CNPGPooling.tsx +97 -40
  89. package/src/components/cnpg/CNPGProtection.test.tsx +145 -0
  90. package/src/components/cnpg/CNPGProtection.tsx +230 -124
  91. package/src/components/cnpg/CNPGReplicationTab.test.tsx +87 -0
  92. package/src/components/cnpg/CNPGReplicationTab.tsx +212 -0
  93. package/src/components/cnpg/CNPGReplicationView.tsx +264 -0
  94. package/src/components/cnpg/CNPGStorage.test.tsx +140 -0
  95. package/src/components/cnpg/CNPGStorage.tsx +619 -0
  96. package/src/components/cnpg/CNPGSummaryHost.overview.test.tsx +44 -0
  97. package/src/components/cnpg/CNPGSummaryHost.test.tsx +23 -0
  98. package/src/components/cnpg/CNPGSummaryHost.tsx +228 -54
  99. package/src/components/cnpg/CNPGTrends.denied.test.tsx +33 -0
  100. package/src/components/cnpg/CNPGTrends.missing.test.tsx +69 -0
  101. package/src/components/cnpg/CNPGTrends.test.ts +38 -0
  102. package/src/components/cnpg/CNPGTrends.tsx +702 -0
  103. package/src/components/cnpg/CNPGView.tsx +34 -21
  104. package/src/components/cnpg/CNPGWorkspace.compatibility.test.tsx +90 -0
  105. package/src/components/cnpg/actions/CNPGClusterActions.dialog.test.tsx +79 -0
  106. package/src/components/cnpg/actions/CNPGClusterActions.test.tsx +142 -0
  107. package/src/components/cnpg/actions/CNPGClusterActions.tsx +853 -0
  108. package/src/components/cnpg/actions/CNPGConnectButton.read.test.tsx +40 -0
  109. package/src/components/cnpg/actions/CNPGConnectButton.test.ts +25 -0
  110. package/src/components/cnpg/actions/CNPGConnectButton.tsx +131 -0
  111. package/src/components/cnpg/actions/CNPGDestroyInstanceDialog.tsx +185 -0
  112. package/src/components/cnpg/actions/CNPGInstanceActions.test.tsx +38 -0
  113. package/src/components/cnpg/actions/CNPGInstanceActions.tsx +74 -0
  114. package/src/components/cnpg/actions/CNPGMaintenanceBanner.tsx +53 -0
  115. package/src/components/cnpg/actions/CNPGPoolerActions.test.tsx +35 -0
  116. package/src/components/cnpg/actions/CNPGPoolerActions.tsx +79 -0
  117. package/src/components/cnpg/actions/CNPGReportDialog.test.tsx +12 -0
  118. package/src/components/cnpg/actions/CNPGReportDialog.tsx +91 -0
  119. package/src/components/cnpg/actions/CNPGScheduleActions.run.test.tsx +58 -0
  120. package/src/components/cnpg/actions/CNPGScheduleActions.test.tsx +54 -0
  121. package/src/components/cnpg/actions/CNPGScheduleActions.tsx +208 -0
  122. package/src/components/cnpg/actions/actionModel.test.ts +111 -0
  123. package/src/components/cnpg/actions/actionModel.ts +127 -0
  124. package/src/components/cnpg/actions/renderCNPGHeaderActions.tsx +60 -0
  125. package/src/components/cnpg/actions/useCNPGWriteGuard.tsx +58 -0
  126. package/src/components/cnpg/actions/useOpenCNPGPsql.ts +22 -0
  127. package/src/components/cnpg/archivingRepair.test.ts +104 -0
  128. package/src/components/cnpg/archivingRepair.ts +145 -0
  129. package/src/components/cnpg/baseBackup.test.ts +36 -0
  130. package/src/components/cnpg/baseBackup.ts +36 -0
  131. package/src/components/cnpg/blocking.test.ts +59 -0
  132. package/src/components/cnpg/blocking.ts +131 -0
  133. package/src/components/cnpg/coverageText.test.ts +27 -0
  134. package/src/components/cnpg/creation/CNPGTargetFields.tsx +110 -0
  135. package/src/components/cnpg/creation/targetModel.test.ts +137 -0
  136. package/src/components/cnpg/creation/targetModel.ts +149 -0
  137. package/src/components/cnpg/fleetStatus.test.ts +46 -0
  138. package/src/components/cnpg/fleetStatus.ts +43 -0
  139. package/src/components/cnpg/formFields.ts +2 -0
  140. package/src/components/cnpg/grantText.test.tsx +11 -0
  141. package/src/components/cnpg/host.test.tsx +24 -0
  142. package/src/components/cnpg/host.tsx +101 -0
  143. package/src/components/cnpg/inspectModel.test.ts +120 -0
  144. package/src/components/cnpg/inspectModel.ts +140 -0
  145. package/src/components/cnpg/logicalSlots.test.ts +30 -0
  146. package/src/components/cnpg/logicalSlots.ts +29 -0
  147. package/src/components/cnpg/lsn.test.ts +40 -0
  148. package/src/components/cnpg/lsn.ts +43 -0
  149. package/src/components/cnpg/operations/CNPGFleetOperation.tsx +30 -0
  150. package/src/components/cnpg/operations/CNPGOperationTracker.test.tsx +247 -0
  151. package/src/components/cnpg/operations/CNPGOperationTracker.tsx +255 -0
  152. package/src/components/cnpg/operations/model.test.ts +510 -0
  153. package/src/components/cnpg/operations/model.ts +499 -0
  154. package/src/components/cnpg/operations/presentation.test.ts +80 -0
  155. package/src/components/cnpg/operations/presentation.ts +84 -0
  156. package/src/components/cnpg/operations/store.ts +98 -0
  157. package/src/components/cnpg/operatorStatus.test.ts +134 -0
  158. package/src/components/cnpg/operatorStatus.ts +194 -0
  159. package/src/components/cnpg/paths.test.ts +54 -0
  160. package/src/components/cnpg/paths.ts +47 -4
  161. package/src/components/cnpg/protection/CNPGAttachArchiveDialog.tsx +185 -0
  162. package/src/components/cnpg/protection/CNPGCreateScheduleDialog.tsx +196 -0
  163. package/src/components/cnpg/protection/CNPGProtectionSetup.test.tsx +121 -0
  164. package/src/components/cnpg/protection/CNPGProtectionSetup.tsx +452 -0
  165. package/src/components/cnpg/protection/CNPGScheduleInput.tsx +115 -0
  166. package/src/components/cnpg/protection/CNPGScheduleRepairButton.tsx +120 -0
  167. package/src/components/cnpg/protection/scheduleModel.test.ts +19 -0
  168. package/src/components/cnpg/protection/scheduleModel.ts +16 -0
  169. package/src/components/cnpg/recovery/CNPGRecoveryPoint.tsx +220 -0
  170. package/src/components/cnpg/recovery/CNPGRestoreButton.test.tsx +29 -0
  171. package/src/components/cnpg/recovery/CNPGRestoreButton.tsx +26 -0
  172. package/src/components/cnpg/recovery/CNPGRestoreChecks.test.tsx +23 -0
  173. package/src/components/cnpg/recovery/CNPGRestoreChecks.tsx +153 -0
  174. package/src/components/cnpg/recovery/CNPGRestoreDialog.back.test.tsx +149 -0
  175. package/src/components/cnpg/recovery/CNPGRestoreDialog.test.tsx +46 -0
  176. package/src/components/cnpg/recovery/CNPGRestoreDialog.tsx +333 -0
  177. package/src/components/cnpg/recovery/CNPGRestoreProgress.tsx +182 -0
  178. package/src/components/cnpg/recovery/CNPGRestoreValidation.tsx +216 -0
  179. package/src/components/cnpg/recovery/restoreModel.test.ts +709 -0
  180. package/src/components/cnpg/recovery/restoreModel.ts +685 -0
  181. package/src/components/cnpg/recovery/restoreOperation.ts +56 -0
  182. package/src/components/cnpg/refresh.test.ts +38 -0
  183. package/src/components/cnpg/refresh.ts +17 -0
  184. package/src/components/cnpg/refreshNotice.test.tsx +29 -0
  185. package/src/components/cnpg/routes.test.ts +34 -3
  186. package/src/components/cnpg/routes.ts +60 -48
  187. package/src/components/cnpg/runtimeAssessment.ts +198 -0
  188. package/src/components/cnpg/runtimeModel.test.ts +175 -0
  189. package/src/components/cnpg/runtimeModel.ts +196 -0
  190. package/src/components/cnpg/runtimeParts.test.tsx +17 -0
  191. package/src/components/cnpg/runtimeParts.tsx +79 -0
  192. package/src/components/cnpg/shared.test.tsx +59 -0
  193. package/src/components/cnpg/shared.tsx +100 -182
  194. package/src/components/cnpg/storageModel.test.ts +107 -0
  195. package/src/components/cnpg/storageModel.ts +101 -0
  196. package/src/components/cnpg/trendSamples.test.ts +148 -0
  197. package/src/components/cnpg/trendSamples.ts +222 -0
  198. package/src/components/cnpg/useCNPGClusterAssessment.ts +30 -0
  199. package/src/components/cnpg/useCNPGKubectlContext.test.tsx +45 -0
  200. package/src/components/cnpg/useCNPGKubectlContext.ts +13 -0
  201. package/src/components/cnpg/useCNPGNavigate.test.tsx +58 -0
  202. package/src/components/cnpg/useCNPGNavigate.ts +21 -0
  203. package/src/components/cnpg/useCNPGPoolerLive.ts +46 -0
  204. package/src/components/cnpg/useCNPGScreenParams.ts +24 -0
  205. package/src/components/cnpg/useCNPGSidebarWorkspace.ts +66 -16
  206. package/src/components/cnpg/workspaceRoute.ts +23 -0
  207. package/src/components/cost/ApplicationCostTab.test.ts +7 -0
  208. package/src/components/cost/ApplicationCostTab.tsx +7 -7
  209. package/src/components/cost/CostView.test.ts +2 -1
  210. package/src/components/cost/CostView.tsx +0 -2
  211. package/src/components/cost/WorkloadCostTab.tsx +2 -4
  212. package/src/components/cost/errors.ts +3 -1
  213. package/src/components/cost/source.test.ts +10 -0
  214. package/src/components/cost/source.ts +5 -1
  215. package/src/components/diagnose/AgentSetupNotice.test.tsx +58 -0
  216. package/src/components/diagnose/AgentSetupNotice.tsx +142 -51
  217. package/src/components/diagnose/ApplyDialog.ack.test.tsx +47 -0
  218. package/src/components/diagnose/ApplyDialog.test.tsx +59 -6
  219. package/src/components/diagnose/ApplyDialog.tsx +26 -28
  220. package/src/components/diagnose/DiagnoseContext.test.ts +43 -0
  221. package/src/components/diagnose/DiagnoseContext.tsx +136 -54
  222. package/src/components/diagnose/DiagnoseSurface.tsx +16 -3
  223. package/src/components/diagnose/InvestigationView.tsx +39 -2
  224. package/src/components/diagnose/LocalDiagnoseAction.tsx +40 -20
  225. package/src/components/diagnose/agentCatalog.ts +2 -2
  226. package/src/components/diagnose/diagnoseEvidenceTypes.ts +2 -0
  227. package/src/components/diagnose/investigationEvidence/adapters/issues.ts +8 -0
  228. package/src/components/diagnose/investigationEvidence/projection.test.ts +16 -0
  229. package/src/components/dock/BottomDock.tsx +2 -0
  230. package/src/components/dock/TerminalTab.test.tsx +17 -0
  231. package/src/components/dock/TerminalTab.tsx +7 -3
  232. package/src/components/dock/TrafficFlowListTab.tsx +27 -6
  233. package/src/components/execution/BatchExecutionView.tsx +11 -11
  234. package/src/components/gitops/ArgoResourceDiffLoader.tsx +4 -2
  235. package/src/components/gitops/GitOpsView.tsx +16 -23
  236. package/src/components/helm/ChartBrowser.tsx +3 -3
  237. package/src/components/helm/HelmCompareRoute.test.tsx +59 -0
  238. package/src/components/helm/HelmCompareRoute.tsx +57 -26
  239. package/src/components/helm/HelmReleaseDrawer.test.ts +55 -3
  240. package/src/components/helm/HelmReleaseDrawer.tsx +50 -12
  241. package/src/components/helm/RevisionHistory.tsx +3 -2
  242. package/src/components/helm/TrackChartSourceDialog.render.test.tsx +120 -0
  243. package/src/components/helm/TrackChartSourceDialog.test.ts +50 -0
  244. package/src/components/helm/TrackChartSourceDialog.tsx +153 -176
  245. package/src/components/helm/ValuesViewer.test.tsx +301 -0
  246. package/src/components/helm/ValuesViewer.tsx +169 -78
  247. package/src/components/home/ActivitySummary.tsx +2 -1
  248. package/src/components/home/ClusterHealthCard.tsx +7 -0
  249. package/src/components/home/HomeView.tsx +20 -9
  250. package/src/components/home/ProblemsPanel.test.tsx +81 -0
  251. package/src/components/home/TopologyPreview.tsx +2 -2
  252. package/src/components/home/mcpToolCatalog.ts +1 -1
  253. package/src/components/issues/IssuesPane.render.test.tsx +122 -0
  254. package/src/components/issues/IssuesPane.test.ts +65 -1
  255. package/src/components/issues/IssuesPane.tsx +188 -25
  256. package/src/components/issues/uncheckedSources.ts +21 -0
  257. package/src/components/resource/PVCUsageBar.render.test.tsx +32 -0
  258. package/src/components/resource/PVCUsageBar.test.tsx +24 -0
  259. package/src/components/resource/PVCUsageBar.tsx +15 -6
  260. package/src/components/resource/WorkloadMetricsHelpDialog.test.tsx +3 -1
  261. package/src/components/resource/WorkloadMetricsHelpDialog.tsx +1 -1
  262. package/src/components/resources/CompositeRenderer.tsx +4 -4
  263. package/src/components/resources/ResourceDetailDrawer.tsx +2 -2
  264. package/src/components/resources/ResourcesView.tsx +26 -12
  265. package/src/components/resources/renderers/KyvernoPolicyQueued.tsx +2 -1
  266. package/src/components/resources/renderers/NodeRenderer.tsx +7 -1
  267. package/src/components/resources/renderers/ServiceRenderer.test.tsx +81 -0
  268. package/src/components/resources/renderers/ServiceRenderer.tsx +19 -14
  269. package/src/components/settings/LocalIntegrationStatus.test.tsx +52 -0
  270. package/src/components/settings/LocalIntegrationStatus.tsx +3 -4
  271. package/src/components/settings/SettingsDialog.tsx +150 -34
  272. package/src/components/shared/LargeClusterNamespacePicker.tsx +11 -5
  273. package/src/components/timeline/TimelineList.tsx +25 -24
  274. package/src/components/timeline/TimelineLoadError.test.tsx +62 -0
  275. package/src/components/timeline/TimelineLoadError.tsx +96 -0
  276. package/src/components/timeline/TimelineRetentionNote.test.tsx +49 -0
  277. package/src/components/timeline/TimelineRetentionNote.tsx +20 -0
  278. package/src/components/timeline/TimelineView.tsx +25 -45
  279. package/src/components/timeline/ViewModeToggle.tsx +20 -0
  280. package/src/components/timeline/useStickyError.test.tsx +47 -0
  281. package/src/components/timeline/useStickyError.ts +15 -0
  282. package/src/components/traffic/TrafficConnectionsTable.tsx +144 -0
  283. package/src/components/traffic/TrafficFilterSidebar.tsx +22 -1
  284. package/src/components/traffic/TrafficFlowListContext.tsx +12 -4
  285. package/src/components/traffic/TrafficFocusSearch.tsx +172 -0
  286. package/src/components/traffic/TrafficGraph.tsx +109 -26
  287. package/src/components/traffic/TrafficGraphTooLarge.tsx +150 -0
  288. package/src/components/traffic/TrafficView.tsx +582 -149
  289. package/src/components/traffic/trafficFilters.test.ts +398 -1
  290. package/src/components/traffic/trafficFilters.ts +408 -0
  291. package/src/components/ui/DiagnosticsOverlay.tsx +34 -2
  292. package/src/components/ui/Markdown.test.tsx +65 -0
  293. package/src/components/ui/Markdown.tsx +144 -127
  294. package/src/components/whats-new/releaseNotes.ts +63 -1
  295. package/src/components/workload/WorkloadView.test.ts +11 -1
  296. package/src/components/workload/WorkloadView.tsx +162 -529
  297. package/src/components/workspace/layout.test.tsx +29 -0
  298. package/src/components/workspace/layout.tsx +259 -0
  299. package/src/components/workspace/table.ts +7 -0
  300. package/src/context/TimelineCustomization.tsx +15 -0
  301. package/src/contexts/CapabilitiesContext.tsx +1 -1
  302. package/src/hooks/useGitOpsWriteGuard.test.tsx +457 -0
  303. package/src/hooks/useGitOpsWriteGuard.ts +193 -0
  304. package/src/hooks/useResolvedGitOpsOwner.ts +256 -0
  305. package/src/integrations/resourceHost.test.tsx +55 -0
  306. package/src/integrations/resourceHost.ts +99 -0
  307. package/src/integrations/resourceHosts.test.tsx +141 -0
  308. package/src/integrations/resourceHosts.tsx +119 -0
  309. package/src/integrations/workspaceRoute.ts +20 -0
  310. package/src/integrations/workspaceRoutes.test.ts +40 -0
  311. package/src/integrations/workspaceRoutes.ts +22 -0
  312. package/src/integrations/workspaceScreens.tsx +14 -0
  313. package/src/utils/drawer-trail.ts +35 -0
  314. package/src/utils/kindFilterStore.test.ts +147 -0
  315. package/src/utils/kindFilterStore.ts +91 -0
  316. package/src/utils/navigation.test.ts +13 -1
  317. package/src/utils/navigation.ts +39 -2
  318. package/src/utils/page-links.ts +21 -0
  319. package/src/utils/tabStorage.ts +40 -0
  320. package/src/utils/viewMemory.test.ts +78 -0
  321. package/src/utils/viewMemory.ts +54 -0
@@ -0,0 +1,98 @@
1
+ import { useSyncExternalStore } from 'react'
2
+ import { getApiBase } from '../../../api/config'
3
+ import { supersedeFor, type CNPGTrackedOperation } from './model'
4
+
5
+ // Tracked operations for this browser session: they survive tab changes and
6
+ // reloads of the page, and end with the session. Storage can be unavailable
7
+ // (private windows, blocked site data); the tracker then lives in memory. One
8
+ // key holds the records of every cluster a Radar Hub tab shows, so each record
9
+ // carries the API base it was issued against.
10
+
11
+ const STORAGE_KEY = 'radar.cnpg.operations.v1'
12
+ const MAX_OPS = 50
13
+ // Finished operations stay visible this long, then drop out on their own.
14
+ const KEEP_FINISHED_MS = 30 * 60_000
15
+
16
+ let ops: CNPGTrackedOperation[] = load()
17
+ const listeners = new Set<() => void>()
18
+
19
+ function load(): CNPGTrackedOperation[] {
20
+ try {
21
+ const raw = sessionStorage.getItem(STORAGE_KEY)
22
+ const parsed = raw ? JSON.parse(raw) : []
23
+ return Array.isArray(parsed) ? parsed : []
24
+ } catch {
25
+ return []
26
+ }
27
+ }
28
+
29
+ function save() {
30
+ try {
31
+ sessionStorage.setItem(STORAGE_KEY, JSON.stringify(ops))
32
+ } catch {
33
+ // Storage unavailable: keep the in-memory copy.
34
+ }
35
+ }
36
+
37
+ function emit(next: CNPGTrackedOperation[]) {
38
+ const now = Date.now()
39
+ ops = next.filter((o) => !o.finishedAt || now - o.finishedAt < KEEP_FINISHED_MS).slice(-MAX_OPS)
40
+ save()
41
+ for (const l of listeners) l()
42
+ }
43
+
44
+ function subscribe(l: () => void) {
45
+ listeners.add(l)
46
+ return () => listeners.delete(l)
47
+ }
48
+
49
+ export interface TrackCNPGOperationInput {
50
+ kind: string
51
+ label: string
52
+ context: string
53
+ namespace: string
54
+ cluster: string
55
+ clusterUID?: string
56
+ target?: { name: string; uid?: string }
57
+ baseline?: Record<string, unknown>
58
+ link?: CNPGTrackedOperation['link']
59
+ }
60
+
61
+ /**
62
+ * Start following an operation the server accepted. Any unfinished operation
63
+ * the new one makes moot is marked superseded.
64
+ */
65
+ export function trackCNPGOperation(input: TrackCNPGOperationInput): string {
66
+ const now = Date.now()
67
+ const op: CNPGTrackedOperation = {
68
+ id: `${now.toString(36)}-${Math.random().toString(36).slice(2, 8)}`,
69
+ kind: input.kind,
70
+ label: input.label,
71
+ apiBase: getApiBase(),
72
+ context: input.context,
73
+ namespace: input.namespace,
74
+ cluster: input.cluster,
75
+ clusterUID: input.clusterUID,
76
+ target: input.target,
77
+ startedAt: now,
78
+ baseline: input.baseline ?? {},
79
+ state: 'requested',
80
+ lastProgressAt: now,
81
+ link: input.link,
82
+ }
83
+ emit([...supersedeFor(ops, op), op])
84
+ return op.id
85
+ }
86
+
87
+ export function updateCNPGOperations(update: (all: CNPGTrackedOperation[]) => CNPGTrackedOperation[]) {
88
+ const next = update(ops)
89
+ if (next !== ops) emit(next)
90
+ }
91
+
92
+ export function dismissCNPGOperation(id: string) {
93
+ emit(ops.filter((o) => o.id !== id))
94
+ }
95
+
96
+ export function useCNPGOperations(): CNPGTrackedOperation[] {
97
+ return useSyncExternalStore(subscribe, () => ops, () => ops)
98
+ }
@@ -0,0 +1,134 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import type { CNPGOperatorResponse, CNPGOperatorVerdict } from '../../api/cnpg'
3
+ import { cnpgOperatorActionNote, cnpgOperatorBannerModel, cnpgOperatorConcerns, cnpgOperatorState, cnpgRestartHistory } from './operatorStatus'
4
+
5
+ const down: CNPGOperatorVerdict = {
6
+ state: 'notReconciling',
7
+ reasons: ['no operator instance has renewed the leader lease cnpg-system/db9c8771.cnpg.io'],
8
+ webhookRejects: true,
9
+ webhookReason: 'the admission webhook Service cnpg-system/cnpg-webhook-service has no ready endpoint and fails closed, so the API server rejects writes to CloudNativePG objects',
10
+ operator: 'cnpg-system/cnpg-controller-manager',
11
+ }
12
+ const up: CNPGOperatorVerdict = { state: 'reconciling', webhookRejects: false }
13
+ const unknown: CNPGOperatorVerdict = { state: 'unknown', unknown: 'the leader lease is not readable (needs get leases in cnpg-system)', webhookRejects: null }
14
+
15
+ describe('cnpgOperatorBannerModel', () => {
16
+ it('names the namespaces whose operator is down and why', () => {
17
+ const m = cnpgOperatorBannerModel({ pg: down, other: up }, ['pg', 'other', 'pg'])
18
+ expect(m?.stale?.namespaces).toEqual(['pg'])
19
+ expect(m?.stale?.reasons[0]).toMatch(/^No operator instance has renewed the leader lease/)
20
+ expect(m?.rejects).toMatch(/no ready endpoint/)
21
+ })
22
+ it('stays quiet when reconciling or unknown', () => {
23
+ expect(cnpgOperatorBannerModel({ pg: up }, ['pg'])).toBeNull()
24
+ expect(cnpgOperatorBannerModel({ pg: unknown }, ['pg'])).toBeNull()
25
+ expect(cnpgOperatorBannerModel(undefined, ['pg'])).toBeNull()
26
+ })
27
+ })
28
+
29
+ describe('cnpgOperatorActionNote', () => {
30
+ it('warns when the operator is observed down and notes when unknown', () => {
31
+ expect(cnpgOperatorActionNote(down)?.tone).toBe('warning')
32
+ expect(cnpgOperatorActionNote(unknown)).toEqual({ tone: 'info', text: expect.stringContaining('needs get leases') })
33
+ expect(cnpgOperatorActionNote(up)).toBeNull()
34
+ })
35
+ })
36
+
37
+ describe('cnpgOperatorActionNote when unknown', () => {
38
+ it('says briefly what could not be confirmed', () => {
39
+ expect(cnpgOperatorActionNote({ state: 'unknown', unknown: "couldn't read its leader lease" } as never)).toEqual({
40
+ tone: 'info',
41
+ text: "Radar couldn't confirm the operator is running (couldn't read its leader lease).",
42
+ })
43
+ })
44
+ })
45
+
46
+ describe('operator current state', () => {
47
+ const now = Date.parse('2026-10-04T10:00:00Z')
48
+ const op = (pods: NonNullable<CNPGOperatorResponse['components'][number]['pods']>): CNPGOperatorResponse => ({
49
+ coverage: { deployments: { state: 'full' }, services: { state: 'full' } },
50
+ components: [{ role: 'operator', namespace: 'cnpg-system', deployment: 'cnpg-controller-manager', readyReplicas: 1, replicas: 1, pods }],
51
+ config: [],
52
+ })
53
+ it('words restart history with when the last one ended, and only a recent one as trouble', () => {
54
+ const old = op([{ name: 'm-1', ready: true, restarts: 33, lastTermination: { container: 'manager', reason: 'Error', exitCode: 1, finishedAt: '2026-10-04T07:53:31Z' } }])
55
+ expect(cnpgRestartHistory(old.components[0], now)).toMatchObject({ recent: false })
56
+ expect(cnpgRestartHistory(old.components[0], now)!.text).toMatch(/^33 restarts since the Pod was created · last ended .* ago \(Error, exit 1\)$/)
57
+ expect(cnpgOperatorConcerns(old, now)).toEqual([expect.objectContaining({ tone: 'neutral' })])
58
+ const fresh = op([{ name: 'm-1', ready: true, restarts: 2, lastTermination: { container: 'manager', reason: 'OOMKilled', exitCode: 137, finishedAt: '2026-10-04T09:40:00Z' } }])
59
+ expect(cnpgOperatorConcerns(fresh, now)[0]).toMatchObject({ tone: 'degraded' })
60
+ // Ready at this read is not "ready" for a component that just restarted.
61
+ expect(cnpgOperatorState(old, now).confirmed).toContain('every component is ready')
62
+ expect(cnpgOperatorState(fresh, now).confirmed).toContain('every component reports ready right now (see the restarts above)')
63
+ })
64
+ it('says nothing about restarts that never happened, and leads with readiness', () => {
65
+ expect(cnpgRestartHistory({ pods: [{ name: 'm-1', ready: true, restarts: 0 }] }, now)).toBeNull()
66
+ const notReady = op([{ name: 'm-1', ready: false, restarts: 0 }])
67
+ notReady.components[0].readyReplicas = 0
68
+ expect(cnpgOperatorConcerns(notReady, now)).toEqual([{ tone: 'unhealthy', text: 'The operator has 0 of 1 replicas ready.' }])
69
+ })
70
+ })
71
+
72
+ describe('operator current state claims only what it read', () => {
73
+ const diag = (webhooks: any[]) => ({
74
+ namespace: 'cnpg-system',
75
+ deployment: 'cnpg-controller-manager',
76
+ pods: [],
77
+ podCoverage: { state: 'ok' },
78
+ leader: { state: 'ok', holderIsCurrentPod: true, stale: false },
79
+ watch: { all: true, namespaces: [], source: '' },
80
+ webhooks,
81
+ webhookServices: [{ state: 'ok', namespace: 'cnpg-system', name: 'cnpg-webhook-service', readyEndpoints: 1, notReadyEndpoints: 0 }],
82
+ metricsPort: 8080,
83
+ reconcile: [],
84
+ events: { state: 'ok', items: [] },
85
+ })
86
+ const op = (webhooks: any[]): CNPGOperatorResponse =>
87
+ ({
88
+ coverage: { deployments: { state: 'full' }, services: { state: 'full' } },
89
+ components: [{ role: 'operator', namespace: 'cnpg-system', deployment: 'cnpg-controller-manager', readyReplicas: 1, replicas: 1, pods: [] }],
90
+ config: [],
91
+ diagnosis: [diag(webhooks)],
92
+ }) as unknown as CNPGOperatorResponse
93
+ const mutating = { state: 'ok', kind: 'MutatingWebhookConfiguration', name: 'cnpg-mutating', webhooks: [{ name: 'm', failurePolicy: 'Fail', caBundleSet: true, service: 'cnpg-system/cnpg-webhook-service', url: false }] }
94
+ it('confirms webhooks only when every configuration was read', () => {
95
+ expect(cnpgOperatorState(op([mutating])).confirmed).toContain('its webhooks have ready endpoints')
96
+ const partial = cnpgOperatorState(op([mutating, { state: 'denied', kind: 'ValidatingWebhookConfiguration', name: 'cnpg-validating', webhooks: [] }]))
97
+ expect(partial.confirmed).not.toContain('its webhooks have ready endpoints')
98
+ expect(partial.unread.join(' ')).toContain('cnpg-validating')
99
+ })
100
+ it('treats an operator scaled to zero as not reconciling', () => {
101
+ const scaled = op([mutating])
102
+ scaled.components[0].readyReplicas = 0
103
+ scaled.components[0].replicas = 0
104
+ expect(cnpgOperatorState(scaled).concerns[0]).toEqual({ tone: 'unhealthy', text: 'The operator is scaled to 0: nothing reconciles.' })
105
+ })
106
+ })
107
+
108
+ describe('clusters blocked on a plugin', () => {
109
+ it('joins the cluster’s plugin phase to that plugin’s Deployment, side by side', () => {
110
+ const op = {
111
+ coverage: { deployments: { state: 'full' }, services: { state: 'full' } },
112
+ components: [{ role: 'plugin', pluginName: 'barman-cloud.cloudnative-pg.io', namespace: 'cnpg-system', deployment: 'barman-cloud', readyReplicas: 1, replicas: 1, pods: [{ name: 'b-1', ready: true, restarts: 0 }] }],
113
+ config: [],
114
+ } as unknown as CNPGOperatorResponse
115
+ const row = {
116
+ namespace: 'pgrt',
117
+ name: 'pg-runtime',
118
+ cluster: { status: { phase: 'Cluster cannot proceed to reconciliation due to an unknown plugin being required' }, spec: { plugins: [{ name: 'barman-cloud.cloudnative-pg.io' }, { name: 'missing.example.io' }] } },
119
+ } as any
120
+ const out = cnpgOperatorState(op, Date.now(), [row]).concerns
121
+ expect(out[0]).toEqual({
122
+ tone: 'unhealthy',
123
+ text: 'Cluster pgrt/pg-runtime requires a plugin the operator does not know — barman-cloud.cloudnative-pg.io: 1/1 ready; missing.example.io: no Deployment serving it was found.',
124
+ })
125
+ })
126
+ })
127
+
128
+ it('qualifies unseen operator components without contradicting the ready components read', () => {
129
+ const data = { coverage: { deployments: { state: 'partial' }, services: { state: 'full' } }, components: [{ role: 'operator', namespace: 'system', deployment: 'cnpg', replicas: 1, readyReplicas: 1 }], config: [] } as unknown as CNPGOperatorResponse
130
+ const state = cnpgOperatorState(data)
131
+ expect(state.unread.join(' ')).toContain('Other operator or plugin components may be outside this read: Deployments not read in some namespaces')
132
+ expect(state.unread.join(' ')).not.toContain('Operator health cannot be assessed')
133
+ expect(state.confirmed.join(' ')).toContain('every component is ready')
134
+ })
@@ -0,0 +1,194 @@
1
+ import { coverageLabel } from './shared'
2
+ import type { CNPGOperatorComponent, CNPGOperatorResponse, CNPGOperatorVerdict } from '../../api/cnpg'
3
+ import { cnpgClusterPlugins, cnpgPluginPhase, formatAge, type CNPGFleetRow } from '@skyhook-io/k8s-ui'
4
+
5
+ export interface CNPGOperatorBannerModel {
6
+ /** The operator watching these namespaces is not leading, so their CNPG status may be stale. */
7
+ stale: { reasons: string[]; namespaces: string[] } | null
8
+ /** Why the admission webhook rejects CNPG writes, when it does. */
9
+ rejects: string | null
10
+ }
11
+
12
+ function sentence(s: string): string {
13
+ const t = s.trim()
14
+ if (!t) return t
15
+ return t[0].toUpperCase() + t.slice(1) + (/[.!?]$/.test(t) ? '' : '.')
16
+ }
17
+
18
+ /**
19
+ * What the fleet and cluster pages say about the operator for the namespaces
20
+ * they show. Only an observed "not leading" or "webhook rejects" raises
21
+ * anything: an unknown or unwatched state stays quiet here and the Operator
22
+ * screen explains it.
23
+ */
24
+ export function cnpgOperatorBannerModel(verdicts: Record<string, CNPGOperatorVerdict> | undefined, namespaces: string[]): CNPGOperatorBannerModel | null {
25
+ if (!verdicts) return null
26
+ const staleNs: string[] = []
27
+ const reasons = new Set<string>()
28
+ let rejects: string | null = null
29
+ for (const ns of [...new Set(namespaces)].sort()) {
30
+ const v = verdicts[ns]
31
+ if (!v) continue
32
+ if (v.state === 'notReconciling') {
33
+ staleNs.push(ns)
34
+ for (const r of v.reasons ?? []) reasons.add(sentence(r))
35
+ }
36
+ if (v.webhookRejects === true && v.webhookReason) rejects = sentence(v.webhookReason)
37
+ }
38
+ if (staleNs.length === 0 && !rejects) return null
39
+ return { stale: staleNs.length > 0 ? { reasons: [...reasons], namespaces: staleNs } : null, rejects }
40
+ }
41
+
42
+ /** A dialog's note about the operator: a warning when it is observed down, a note when unknown. */
43
+ export function cnpgOperatorActionNote(v: CNPGOperatorVerdict | undefined): { tone: 'warning' | 'info'; text: string } | null {
44
+ if (!v) return null
45
+ if (v.state === 'notReconciling') {
46
+ const why = (v.reasons ?? []).map(sentence).join(' ')
47
+ return {
48
+ tone: 'warning',
49
+ text: `The CloudNativePG operator is not reconciling this cluster. ${why} The API server may accept this change, but nothing acts on it until an operator instance leads again.`.replace(/\s+/g, ' ').trim(),
50
+ }
51
+ }
52
+ if (v.state === 'unknown' && v.unknown) {
53
+ return { tone: 'info', text: `Radar couldn't confirm the operator is running (${v.unknown}).` }
54
+ }
55
+ return null
56
+ }
57
+
58
+ /** A restart that ended within this long is "recent": the operator may still be crash-looping. */
59
+ export const CNPG_OPERATOR_RECENT_RESTART_MS = 60 * 60 * 1000
60
+
61
+ export interface CNPGOperatorConcern {
62
+ tone: 'unhealthy' | 'degraded' | 'neutral'
63
+ text: string
64
+ }
65
+
66
+ /** A component's restart history in one line, with when the last one ended and why; null when it never restarted. */
67
+ export function cnpgRestartHistory(c: Pick<CNPGOperatorComponent, 'pods'>, now = Date.now()): { text: string; recent: boolean } | null {
68
+ const pods = c.pods ?? []
69
+ const restarts = pods.reduce((n, p) => n + p.restarts, 0)
70
+ if (restarts === 0) return null
71
+ const last = pods
72
+ .map((p) => p.lastTermination)
73
+ .filter((t): t is NonNullable<typeof t> => !!t?.finishedAt)
74
+ .sort((a, b) => Date.parse(b.finishedAt!) - Date.parse(a.finishedAt!))[0]
75
+ const ended = last?.finishedAt ? Date.parse(last.finishedAt) : NaN
76
+ const recent = Number.isFinite(ended) && now - ended <= CNPG_OPERATOR_RECENT_RESTART_MS
77
+ const why = last ? ` (${[last.reason, `exit ${last.exitCode}`].filter(Boolean).join(', ')})` : ''
78
+ const when = last?.finishedAt ? ` · last ended ${formatAge(last.finishedAt)} ago${why}` : ''
79
+ return { text: `${restarts} restart${restarts === 1 ? '' : 's'} since the Pod was created${when}`, recent }
80
+ }
81
+
82
+ function componentName(c: CNPGOperatorComponent): string {
83
+ return c.role === 'operator' ? 'The operator' : `Plugin ${c.pluginName ?? c.deployment}`
84
+ }
85
+
86
+ export interface CNPGOperatorState {
87
+ concerns: CNPGOperatorConcern[]
88
+ /** What was read and found in order, said only from evidence. */
89
+ confirmed: string[]
90
+ /** What could not be read, so its absence from `concerns` proves nothing. */
91
+ unread: string[]
92
+ }
93
+
94
+ /**
95
+ * What the Operator screen leads with: the states that stop or slow
96
+ * reconciliation, then restart history with when it last happened, and what
97
+ * could not be read. Restart totals are cumulative, so only a recent one is
98
+ * worded as current trouble.
99
+ */
100
+ export function cnpgOperatorConcerns(op: CNPGOperatorResponse, now = Date.now()): CNPGOperatorConcern[] {
101
+ return cnpgOperatorState(op, now).concerns
102
+ }
103
+
104
+ export function cnpgOperatorState(op: CNPGOperatorResponse, now = Date.now(), clusters: CNPGFleetRow[] = []): CNPGOperatorState {
105
+ const concerns: CNPGOperatorConcern[] = cnpgPluginBlockedClusters(op, clusters, now)
106
+ const confirmed: string[] = []
107
+ const unread: string[] = []
108
+ const operators = op.components.filter((c) => c.role === 'operator')
109
+ const coverageGaps = (['deployments', 'services'] as const).filter((k) => op.coverage[k]?.state !== 'full')
110
+ if (coverageGaps.length > 0) unread.push(`${operators.length === 0 ? 'Operator health cannot be assessed with your access' : 'Other operator or plugin components may be outside this read'}: ${coverageGaps.map((k) => `${k === 'deployments' ? 'Deployments' : 'Services'} ${coverageLabel(op.coverage[k])}`).join('; ')}.`)
111
+ else if (operators.length === 0) unread.push('No operator Deployment was found in the namespaces you can read.')
112
+ let allReady = op.components.length > 0
113
+ for (const c of op.components) {
114
+ if (c.readyReplicas === null || c.replicas === null) {
115
+ allReady = false
116
+ unread.push(`${componentName(c)}: readiness not reported.`)
117
+ } else if (c.replicas === 0) {
118
+ allReady = false
119
+ concerns.push({ tone: c.role === 'operator' ? 'unhealthy' : 'degraded', text: `${componentName(c)} is scaled to 0${c.role === 'operator' ? ': nothing reconciles' : ''}.` })
120
+ } else if (c.readyReplicas < c.replicas) {
121
+ allReady = false
122
+ concerns.push({ tone: c.readyReplicas === 0 ? 'unhealthy' : 'degraded', text: `${componentName(c)} has ${c.readyReplicas} of ${c.replicas} replicas ready.` })
123
+ }
124
+ }
125
+ // Ready now says little about a component that keeps restarting.
126
+ const restartedRecently = op.components.some((c) => cnpgRestartHistory(c, now)?.recent)
127
+ if (allReady) confirmed.push(restartedRecently ? 'every component reports ready right now (see the restarts above)' : 'every component is ready')
128
+ const diagnoses = op.diagnosis ?? []
129
+ if (operators.length > 0 && diagnoses.length === 0) unread.push('Leadership and webhooks were not read.')
130
+ let leading = diagnoses.length > 0
131
+ let webhooksServed = diagnoses.length > 0
132
+ for (const d of diagnoses) {
133
+ if (d.leader.state === 'ok') {
134
+ if (d.leader.stale) {
135
+ leading = false
136
+ concerns.push({ tone: 'unhealthy', text: `No operator instance is leading in ${d.namespace}: the leader Lease was not renewed, so nothing reconciles.` })
137
+ }
138
+ } else if (d.leader.state !== 'disabled') {
139
+ leading = false
140
+ unread.push(`Leadership in ${d.namespace} not read${d.leader.reason ? `: ${d.leader.reason}` : ''}.`)
141
+ }
142
+ for (const svc of d.webhookServices) {
143
+ const fails = d.webhooks.some((w) => w.state === 'ok' && w.webhooks.some((h) => h.failurePolicy === 'Fail' && h.service === `${svc.namespace}/${svc.name}`))
144
+ if (svc.state !== 'ok' || svc.readyEndpoints === null) {
145
+ webhooksServed = false
146
+ unread.push(`Endpoints of the webhook Service ${svc.namespace}/${svc.name} not read.`)
147
+ } else if (svc.readyEndpoints === 0) {
148
+ webhooksServed = false
149
+ if (fails) concerns.push({ tone: 'unhealthy', text: `The admission webhook Service ${svc.namespace}/${svc.name} has no ready endpoint and its failure policy is Fail: every CloudNativePG write is rejected.` })
150
+ }
151
+ }
152
+ const unreadHooks = d.webhooks.filter((w) => w.state !== 'ok')
153
+ if (unreadHooks.length > 0) {
154
+ // Services are found through readable configurations only, so an unread
155
+ // one may name a Service with no endpoint.
156
+ webhooksServed = false
157
+ unread.push(`Admission webhook configurations not read (${unreadHooks.map((w) => w.name).join(', ')}).`)
158
+ }
159
+ if (d.webhookServices.length === 0) webhooksServed = false
160
+ }
161
+ if (leading) confirmed.push('the operator is leading')
162
+ if (webhooksServed) confirmed.push('its webhooks have ready endpoints')
163
+ for (const c of op.components) {
164
+ const h = cnpgRestartHistory(c, now)
165
+ if (h) concerns.push({ tone: h.recent ? 'degraded' : 'neutral', text: `${componentName(c)}: ${h.text}.` })
166
+ else if (!c.pods && c.podCoverage && c.podCoverage.state !== 'ok') unread.push(`${componentName(c)}: restart history not read${c.podCoverage.reason ? ` (${c.podCoverage.reason})` : ''}.`)
167
+ }
168
+ return { concerns, confirmed, unread }
169
+ }
170
+
171
+ /**
172
+ * Clusters whose phase says the operator is stuck on a plugin, joined to what
173
+ * this screen knows about that plugin: whether its Deployment is here, ready,
174
+ * and when it last restarted. Facts side by side; which one caused the other
175
+ * is not claimed.
176
+ */
177
+ export function cnpgPluginBlockedClusters(op: CNPGOperatorResponse, clusters: CNPGFleetRow[], now = Date.now()): CNPGOperatorConcern[] {
178
+ const out: CNPGOperatorConcern[] = []
179
+ for (const r of clusters) {
180
+ const phase = cnpgPluginPhase(r.cluster)
181
+ if (!phase) continue
182
+ const what = phase === 'unknownPlugin' ? 'requires a plugin the operator does not know' : 'hit an error talking to a plugin'
183
+ const plugins = cnpgClusterPlugins(r.cluster)
184
+ const parts = plugins.map((name) => {
185
+ const c = op.components.find((x) => x.role === 'plugin' && x.pluginName === name)
186
+ if (!c) return `${name}: no Deployment serving it was found`
187
+ const ready = c.readyReplicas !== null && c.replicas !== null ? `${c.readyReplicas}/${c.replicas} ready` : 'readiness not reported'
188
+ const h = cnpgRestartHistory(c, now)
189
+ return `${name}: ${ready}${h ? `, ${h.text}` : ''}`
190
+ })
191
+ out.push({ tone: 'unhealthy', text: `Cluster ${r.namespace}/${r.name} ${what}${parts.length ? ` — ${parts.join('; ')}` : ''}.` })
192
+ }
193
+ return out
194
+ }
@@ -0,0 +1,54 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { cnpgClusterProblemsPath, cnpgDimensionPath, cnpgWithinDetail } from './paths'
3
+ import { issuesPathForSubject } from '../../utils/page-links'
4
+ import { writeViewParams } from '../../utils/navigation'
5
+ import { issueSubjectFromParams } from '../issues/IssuesPane'
6
+
7
+ describe('cnpgDimensionPath', () => {
8
+ it('opens each health dimension where it is explained', () => {
9
+ expect(cnpgDimensionPath('db', 'pg', 'kind', 'replication')).toBe('/cnpg/clusters/db/pg?ctx=kind&tab=replication')
10
+ expect(cnpgDimensionPath('db', 'pg', 'kind', 'serving')).toBe('/cnpg/clusters/db/pg?ctx=kind&tab=replication')
11
+ expect(cnpgDimensionPath('db', 'pg', 'kind', 'storage')).toBe('/cnpg/clusters/db/pg?ctx=kind&tab=storage')
12
+ expect(cnpgDimensionPath('db', 'pg', undefined, 'protection')).toBe('/cnpg/clusters/db/pg?tab=backups')
13
+ })
14
+ })
15
+
16
+ describe('cnpgWithinDetail', () => {
17
+ it('turns a link to the open Cluster page into a tab change that keeps its other params', () => {
18
+ expect(cnpgWithinDetail('/cnpg/clusters/db/pg', '?ctx=kind&tab=performance&section=history&charts=storage&drawer=x', '/cnpg/clusters/db/pg?ctx=kind&tab=backups')).toBe(
19
+ '/cnpg/clusters/db/pg?ctx=kind&tab=backups&drawer=x',
20
+ )
21
+ expect(cnpgWithinDetail('/cnpg/clusters/db/pg', '?ctx=kind', '/cnpg/clusters/db/pg?ctx=kind&tab=performance&section=history&charts=replication')).toBe(
22
+ '/cnpg/clusters/db/pg?ctx=kind&tab=performance&section=history&charts=replication',
23
+ )
24
+ })
25
+ it('leaves a link to another page alone', () => {
26
+ expect(cnpgWithinDetail('/cnpg', '', '/cnpg/clusters/db/pg?tab=replication')).toBeNull()
27
+ expect(cnpgWithinDetail('/cnpg/clusters/db/other', '', '/cnpg/clusters/db/pg?tab=replication')).toBeNull()
28
+ })
29
+ })
30
+
31
+ describe('cnpgClusterProblemsPath', () => {
32
+ it('opens the Cluster with its problems listed', () => {
33
+ expect(cnpgClusterProblemsPath('db', 'pg', 'kind')).toBe('/cnpg/clusters/db/pg?ctx=kind&problems=all')
34
+ expect(cnpgClusterProblemsPath('db', 'pg')).toBe('/cnpg/clusters/db/pg?problems=all')
35
+ })
36
+ })
37
+
38
+ describe('issuesPathForSubject', () => {
39
+ it('links to the Issues page narrowed to the subject', () => {
40
+ expect(issuesPathForSubject({ kind: 'Backup', namespace: 'pg', name: 'b-1' })).toBe('/issues?kind=Backup&resource=pg%2Fb-1')
41
+ expect(issuesPathForSubject({ kind: 'ClusterImageCatalog', namespace: '', name: 'pg' })).toBe('/issues?kind=ClusterImageCatalog&resource=pg')
42
+ })
43
+ it('keeps the current namespace view filter', () => {
44
+ expect(issuesPathForSubject({ kind: 'Cluster', namespace: 'pg', name: 'main' }, 'pg,app')).toBe('/issues?namespaces=pg%2Capp&kind=Cluster&resource=pg%2Fmain')
45
+ })
46
+ it('carries the API group through App URL synchronization', () => {
47
+ const subject = { kind: 'Cluster', group: 'postgresql.cnpg.io', namespace: 'pg', name: 'main' }
48
+ const href = issuesPathForSubject(subject)
49
+ expect(href).toBe('/issues?kind=Cluster&apiGroup=postgresql.cnpg.io&resource=pg%2Fmain')
50
+ const params = new URLSearchParams(href.split('?')[1])
51
+ writeViewParams(params, ['pg'], null)
52
+ expect(issueSubjectFromParams(params)).toEqual(subject)
53
+ })
54
+ })
@@ -1,13 +1,56 @@
1
+ import type { CNPGDimension } from '@skyhook-io/k8s-ui'
1
2
  import { cnpgDetailPath } from './routes'
2
3
 
3
4
  export function cnpgClusterFullPath(namespace: string, name: string, ctx?: string, tab?: string): string {
4
5
  return cnpgDetailPath({ plural: 'clusters', namespace, name }, ctx, tab)
5
6
  }
6
7
 
8
+ /** The Cluster's page with its problems list open (`problems=all`). */
9
+ export function cnpgClusterProblemsPath(namespace: string, name: string, ctx?: string): string {
10
+ const path = cnpgClusterFullPath(namespace, name, ctx)
11
+ return `${path}${path.includes('?') ? '&' : '?'}problems=all`
12
+ }
13
+
14
+ /** The Cluster page's tabs, in order: the task tabs, then the record (activity, logs, configuration, YAML). */
15
+ export const CNPG_CLUSTER_TAB_ORDER = ['overview', 'replication', 'storage', 'performance', 'backups', 'activity', 'logs', 'spec', 'yaml']
16
+
17
+ /** The tab that explains a health dimension: Serving and Replication in Replication, Storage in Storage, Backups in Backups. */
18
+ export function cnpgDimensionTab(id: CNPGDimension['id']): string {
19
+ switch (id) {
20
+ case 'protection':
21
+ return 'backups'
22
+ case 'storage':
23
+ return 'storage'
24
+ default:
25
+ return 'replication'
26
+ }
27
+ }
28
+
29
+ const TAB_LABEL: Record<string, string> = { replication: 'Replication', storage: 'Storage', backups: 'Backups' }
30
+
31
+ /** The name of the tab cnpgDimensionTab opens, for a link that leads there. */
32
+ export function cnpgDimensionTabLabel(id: CNPGDimension['id']): string {
33
+ return TAB_LABEL[cnpgDimensionTab(id)]
34
+ }
35
+
36
+ /** The Cluster page tab where a health dimension is explained (see cnpgDimensionTab). */
37
+ export function cnpgDimensionPath(namespace: string, name: string, ctx: string | undefined, id: CNPGDimension['id']): string {
38
+ return cnpgClusterFullPath(namespace, name, ctx, cnpgDimensionTab(id))
39
+ }
40
+
7
41
  /**
8
- * The label for "← back" on the page a push lands on: the title of the page
9
- * being left, which Radar keeps in the document title.
42
+ * `target` as a tab change on the detail page already open, or null when it
43
+ * is another page. A tab change keeps the page's other params, drops the
44
+ * previous tab's section unless `target` names one, and is applied like a tab
45
+ * click (replacing the history entry) so the page's return label still leads
46
+ * where it says.
10
47
  */
11
- export function currentPageLabel(): string {
12
- return document.title.replace(/\s*·\s*Radar$/, '') || 'previous page'
48
+ export function cnpgWithinDetail(currentPathname: string, currentSearch: string, target: string): string | null {
49
+ const [path, query = ''] = target.split('?')
50
+ if (path !== currentPathname) return null
51
+ const params = new URLSearchParams(currentSearch)
52
+ for (const k of ['section', 'charts', 'instance', 'validate']) params.delete(k)
53
+ for (const [k, v] of new URLSearchParams(query)) params.set(k, v)
54
+ const qs = params.toString()
55
+ return qs ? `${path}?${qs}` : path
13
56
  }