@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,9 +1,11 @@
1
1
  import { useMemo } from 'react'
2
- import { useNavigate } from 'react-router-dom'
3
2
  import { clsx } from 'clsx'
4
- import { ArrowRight, Database, FileText, Search } from 'lucide-react'
3
+ import { ArrowRight, Database, FileText, Plus, Search } from 'lucide-react'
5
4
  import {
6
5
  CNPG_PROBLEM_CATEGORIES,
6
+ CNPG_PROBLEM_TONE,
7
+ cnpgDimensions,
8
+ cnpgReadyInstances,
7
9
  FactValue,
8
10
  StatusDot,
9
11
  Tooltip,
@@ -13,43 +15,175 @@ import {
13
15
  } from '@skyhook-io/k8s-ui'
14
16
  import type { SelectedResource } from '../../types'
15
17
  import { useConnection } from '../../context/ConnectionContext'
16
- import { EmptyState, ROW_HOVER, TABLE_HEAD, TABLE_WRAP, TBODY, TD, TH } from '../capacity/shared'
17
- import { CNPGWorkspaceHeader, CoverageNotice, FilterChips, type CNPGScreenProps } from './shared'
18
- import { cnpgClusterFullPath, currentPageLabel } from './paths'
19
- import { sameResource } from './routes'
18
+ import { getApiBase } from '../../api/config'
19
+ import { CNPGWorkspaceHeader, CoverageNotice, coverageEmpty, type CNPGScreenProps } from './shared'
20
+ import { cnpgClusterFullPath, cnpgClusterProblemsPath } from './paths'
21
+ import { currentPageLabel } from '../../utils/page-links'
22
+ import { CNPGOperatorBanner } from './CNPGOperatorBanner'
23
+ import { cnpgInstancePillLabel, cnpgPillsToShow, cnpgRowStatus } from './fleetStatus'
24
+ import { BreakText, FilterChips, ScreenEmptyState } from '../workspace/layout'
25
+ import { ROW_HOVER, TABLE_HEAD, TABLE_WRAP, TBODY, TD, TH } from '../workspace/table'
26
+ import { sameSelectedResource } from '../../utils/drawer-trail'
27
+ import { useCNPGNavigate } from './useCNPGNavigate'
28
+ import { useCNPGOperations } from './operations/store'
29
+ import { latestCNPGOperation } from './operations/presentation'
30
+ import { CNPGFleetOperation } from './operations/CNPGFleetOperation'
20
31
 
21
32
  type Filter = 'attention' | 'all'
22
33
 
34
+ // Headers may wrap: at a 1280px window the columns are narrower than their labels.
35
+ const TH_WRAP = TH.replace('whitespace-nowrap', '')
36
+
37
+ // Five pills fit the Ready column; the rest are counted, listed on hover.
38
+ const MAX_PILLS = 5
39
+
23
40
  function InstancePills({ row }: { row: CNPGFleetRow }) {
24
41
  if (row.pods.length === 0) return null
42
+ const { shown, hidden } = cnpgPillsToShow(row.pods, MAX_PILLS)
25
43
  return (
26
- <div className="mt-1 flex flex-wrap gap-1">
27
- {row.pods.map((p) => {
44
+ <div className="mt-1 flex flex-nowrap items-center gap-0.5 font-sans">
45
+ {shown.map((p) => {
28
46
  const tone = p.ready === true ? 'healthy' : p.ready === false ? 'unhealthy' : 'unknown'
29
47
  return (
30
- <Tooltip key={p.name} content={`${p.name} · ${p.role} · ${p.ready === true ? 'ready' : p.ready === false ? 'not ready' : 'readiness unknown'}`}>
31
- <span className="inline-flex items-center gap-1 rounded border border-theme-border bg-theme-base px-1 text-[10.5px] font-mono text-theme-text-secondary">
48
+ <Tooltip key={p.name} content={cnpgInstancePillLabel(p)}>
49
+ <span
50
+ aria-label={cnpgInstancePillLabel(p)}
51
+ className="inline-flex items-center gap-0.5 rounded border border-theme-border bg-theme-base px-0.5 font-mono text-[10.5px] text-theme-text-secondary"
52
+ >
32
53
  <StatusDot tone={tone} size="xs" />
33
54
  {p.role === 'primary' ? 'P' : p.role === 'replica' ? 'R' : '?'}
34
55
  </span>
35
56
  </Tooltip>
36
57
  )
37
58
  })}
59
+ {hidden.length > 0 && (
60
+ <Tooltip
61
+ content={
62
+ <ul className="space-y-0.5">
63
+ {hidden.map((p) => (
64
+ <li key={p.name}>{cnpgInstancePillLabel(p)}</li>
65
+ ))}
66
+ </ul>
67
+ }
68
+ >
69
+ <span aria-label={`${hidden.length} more instances`} className="px-0.5 text-[10.5px] text-theme-text-tertiary">
70
+ +{hidden.length}
71
+ </span>
72
+ </Tooltip>
73
+ )}
38
74
  </div>
39
75
  )
40
76
  }
41
77
 
42
- function AttentionCell({ row }: { row: CNPGFleetRow }) {
78
+ function ReadyCell({ row }: { row: CNPGFleetRow }) {
79
+ const r = cnpgReadyInstances(row)
80
+ return (
81
+ <>
82
+ {r.note ? (
83
+ <Tooltip content={r.note}>
84
+ <span className={clsx('underline decoration-dotted underline-offset-2', toneTextClass(r.tone ?? 'unknown'))}>{r.text} Pods</span>
85
+ </Tooltip>
86
+ ) : (
87
+ r.text
88
+ )}
89
+ {r.podText && <span className="block text-xs text-theme-text-secondary">{r.podText}</span>}
90
+ <InstancePills row={row} />
91
+ </>
92
+ )
93
+ }
94
+
95
+ // The Storage cell is the same assessment as the Cluster page's Storage chip:
96
+ // measured usage, or the WAL an inactive slot holds, or why neither is known.
97
+ function storageFact(row: CNPGFleetRow) {
98
+ const d = cnpgDimensions({ row }).find((x) => x.id === 'storage')!
99
+ return { text: d.text, tone: d.tone, source: d.source }
100
+ }
101
+
102
+ /** The one reason no cluster's volume usage is measured, when that is so; the column then says it once. */
103
+ function cnpgStorageUnmeasured(rows: CNPGFleetRow[]): string | null {
104
+ if (rows.length === 0 || rows.some((r) => !r.disk || r.disk.tone !== 'unknown')) return null
105
+ const reasons = new Set(rows.map((r) => (r.disk?.source ?? r.disk?.text ?? '').replace(/this cluster's/g, 'their').replace(/this cluster/g, 'them')))
106
+ return reasons.size === 1 ? [...reasons][0] || 'no measurement' : 'no measurement'
107
+ }
108
+
109
+ function StorageCell({ row, quiet }: { row: CNPGFleetRow; quiet: boolean }) {
110
+ const fact = storageFact(row)
111
+ if (quiet && fact.tone === 'unknown') {
112
+ return (
113
+ <Tooltip content={fact.source ?? 'Volume usage not measured'}>
114
+ <span className="text-theme-text-tertiary">—</span>
115
+ </Tooltip>
116
+ )
117
+ }
118
+ return (
119
+ <>
120
+ <FactValue fact={fact} />
121
+ {row.diskGrowth && <div className="text-xs"><FactValue fact={row.diskGrowth} className="text-theme-text-tertiary" /></div>}
122
+ </>
123
+ )
124
+ }
125
+
126
+ function RowStatusDot({ row }: { row: CNPGFleetRow }) {
127
+ const status = cnpgRowStatus(row)
128
+ return (
129
+ <Tooltip content={status.label}>
130
+ <span role="img" aria-label={status.label} className="inline-flex">
131
+ <StatusDot tone={status.tone} />
132
+ </span>
133
+ </Tooltip>
134
+ )
135
+ }
136
+
137
+ // A token this long cannot wrap at a word boundary within the cell.
138
+ const UNBREAKABLE_TOKEN = 24
139
+
140
+ // A measured problem names what measured it, so a qualified match stays qualified in the fleet.
141
+ function problemTip(p: CNPGFleetRow['problems'][number]): string {
142
+ return p.source === 'measurement' && p.measuredBy ? `${p.title} (measured by ${p.measuredBy})` : p.title
143
+ }
144
+
145
+ function AttentionCell({ row, onOpenAll }: { row: CNPGFleetRow; onOpenAll: () => void }) {
43
146
  const top = row.problems.find((p) => p.severity !== 'posture') ?? row.problems[0]
44
147
  if (!top) return <span className="text-theme-text-tertiary">—</span>
45
- const tone = top.severity === 'critical' ? 'unhealthy' : top.severity === 'warning' ? 'degraded' : 'neutral'
46
- const more = row.problems.length - 1
148
+ const others = row.problems.filter((p) => p !== top)
149
+ const headline = top.shortTitle ?? top.title
150
+ const unbreakable = headline.split(/\s+/).some((w) => w.length > UNBREAKABLE_TOKEN)
47
151
  return (
48
152
  <div className="min-w-0">
49
- <Tooltip content={top.title} wrapperClassName="block">
50
- <div className={clsx('line-clamp-2 break-words', toneTextClass(tone))}>{top.title}</div>
153
+ <Tooltip content={problemTip(top)} wrapperClassName="w-full">
154
+ <div className={clsx('[overflow-wrap:normal]', unbreakable ? 'truncate' : 'line-clamp-3', toneTextClass(CNPG_PROBLEM_TONE[top.severity]))}>{headline}</div>
51
155
  </Tooltip>
52
- {more > 0 && <div className="text-xs text-theme-text-tertiary">+{more} more</div>}
156
+ {top.unverifiedMatch && (
157
+ <div className="text-[11px] text-theme-text-tertiary">measured by {top.measuredBy}</div>
158
+ )}
159
+ {others.length > 0 && (
160
+ <Tooltip
161
+ content={
162
+ <ul className="space-y-1">
163
+ {others.map((p) => (
164
+ <li key={p.id} className="flex items-start gap-1.5">
165
+ <span className="mt-1 shrink-0">
166
+ <StatusDot tone={CNPG_PROBLEM_TONE[p.severity]} size="xs" />
167
+ </span>
168
+ <span>{problemTip(p)}</span>
169
+ </li>
170
+ ))}
171
+ </ul>
172
+ }
173
+ >
174
+ <button
175
+ type="button"
176
+ onClick={(e) => {
177
+ e.stopPropagation()
178
+ onOpenAll()
179
+ }}
180
+ aria-label={`${others.length} more problems: open ${row.name} with every problem listed`}
181
+ className="rounded text-xs text-accent-text hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent"
182
+ >
183
+ +{others.length} more
184
+ </button>
185
+ </Tooltip>
186
+ )}
53
187
  </div>
54
188
  )
55
189
  }
@@ -63,13 +197,17 @@ export function CNPGOverview({
63
197
  onInspect,
64
198
  inspected,
65
199
  onClearNamespaces,
200
+ onCreate,
66
201
  }: CNPGScreenProps) {
67
- const navigate = useNavigate()
202
+ const navigate = useCNPGNavigate()
68
203
  const { connection } = useConnection()
204
+ const operations = useCNPGOperations()
205
+ const apiBase = getApiBase()
69
206
  const q = searchParams.get('q') ?? ''
70
207
  const cat = (searchParams.get('cat') as CNPGProblemCategory | null) ?? null
71
208
  const rawFilter = searchParams.get('filter') as Filter | null
72
- const filter: Filter = rawFilter ?? (fleet.attentionCount > 0 ? 'attention' : 'all')
209
+ // The view is the list of clusters: all of them unless asked for less.
210
+ const filter: Filter = rawFilter === 'attention' ? 'attention' : 'all'
73
211
 
74
212
  const rows = useMemo(() => {
75
213
  let list = fleet.rows
@@ -85,6 +223,14 @@ export function CNPGOverview({
85
223
  const clustersCov = data.coverage.clusters
86
224
  const total = fleet.rows.length
87
225
  const context = connection.context || data.context
226
+ // Some namespaces were not read: the count is a floor, not the cluster's total.
227
+ const totalText = `${clustersCov?.state === 'partial' ? '≥' : ''}${total}`
228
+ const createAction = onCreate ? (
229
+ <button type="button" onClick={onCreate} className="btn-secondary inline-flex items-center gap-1.5 px-3 py-1.5 text-sm">
230
+ <Plus className="h-3.5 w-3.5" />
231
+ Create
232
+ </button>
233
+ ) : undefined
88
234
 
89
235
  if (total === 0) {
90
236
  const state = clustersCov?.state ?? 'notInstalled'
@@ -95,15 +241,15 @@ export function CNPGOverview({
95
241
  ? { title: 'Loading PostgreSQL clusters', detail: 'Radar is still syncing CloudNativePG Clusters from the API server.' }
96
242
  : state === 'error'
97
243
  ? { title: 'PostgreSQL clusters could not be read', detail: 'Reading CloudNativePG Clusters failed; see the Radar server log.' }
98
- : state === 'partial'
99
- ? { title: 'No visible PostgreSQL clusters', detail: 'None in the namespaces you can read. Clusters in namespaces you cannot list are not shown.' }
244
+ : state === 'partial' || state === 'uncached'
245
+ ? { title: 'No visible PostgreSQL clusters', detail: coverageEmpty(clustersCov, 'PostgreSQL clusters') }
100
246
  : namespaces.length > 0
101
- ? { title: `No PostgreSQL clusters in ${context}`, detail: `None in namespace ${namespaces.join(', ')}. Clear the namespace filter to see the whole cluster.` }
247
+ ? { title: 'No PostgreSQL clusters in the selected namespaces', detail: `None in ${namespaces.join(', ')} in ${context}. Clear the namespace filter to see the whole cluster.` }
102
248
  : { title: `No PostgreSQL clusters in ${context}`, detail: 'The CloudNativePG CRDs are installed. Clusters, backups and declarations appear here once they exist.' }
103
249
  return (
104
250
  <div className="flex min-h-0 flex-1 flex-col">
105
- <CNPGWorkspaceHeader title="Overview" />
106
- <EmptyState
251
+ <CNPGWorkspaceHeader title="Clusters" actions={createAction} />
252
+ <ScreenEmptyState
107
253
  icon={Database}
108
254
  title={empty.title}
109
255
  detail={empty.detail}
@@ -124,7 +270,7 @@ export function CNPGOverview({
124
270
  if (q) chips.push({ label: `Search: ${q}`, onClear: () => onSetParams({ q: null }) })
125
271
  if (namespaces.length > 0) chips.push({ label: `Namespace: ${namespaces.join(', ')}`, onClear: onClearNamespaces })
126
272
 
127
- const segment = (id: Filter, label: string, n: number) => {
273
+ const segment = (id: Filter, label: string, n: number | string) => {
128
274
  const on = filter === id
129
275
  return (
130
276
  <button
@@ -132,7 +278,7 @@ export function CNPGOverview({
132
278
  type="button"
133
279
  role="tab"
134
280
  aria-selected={on}
135
- onClick={() => onSetParams({ filter: id })}
281
+ onClick={() => onSetParams({ filter: id === 'all' ? null : id })}
136
282
  className={clsx(
137
283
  'inline-flex items-center gap-1.5 rounded-md px-3 py-1 text-sm font-medium transition-colors',
138
284
  on ? 'bg-theme-surface text-theme-text-primary shadow-theme-sm' : 'text-theme-text-secondary hover:text-theme-text-primary',
@@ -145,27 +291,30 @@ export function CNPGOverview({
145
291
  }
146
292
 
147
293
  const lowerBound = fleet.incompleteKinds.length > 0
294
+ const attentionText = lowerBound ? fleet.attentionCount > 0 ? `≥${fleet.attentionCount}` : 'Unknown' : String(fleet.attentionCount)
295
+ const storageUnmeasured = cnpgStorageUnmeasured(fleet.rows)
148
296
 
149
297
  return (
150
298
  <div className="flex min-h-0 flex-1 flex-col">
151
299
  <CNPGWorkspaceHeader
152
- title="Overview"
300
+ title="Clusters"
153
301
  subtitle={
154
302
  <>
155
- {total} PostgreSQL {total === 1 ? 'cluster' : 'clusters'} · {fleet.attentionCount}
156
- {lowerBound ? '+' : ''} need attention
303
+ {totalText} PostgreSQL {total === 1 ? 'cluster' : 'clusters'} · {lowerBound && fleet.attentionCount === 0 ? 'Needs attention: unknown' : `${attentionText} need attention`}
157
304
  <span className="text-theme-text-tertiary"> · {context}</span>
158
305
  </>
159
306
  }
307
+ actions={createAction}
160
308
  />
161
309
  <div className="min-h-0 flex-1 overflow-y-auto [scrollbar-gutter:stable]">
162
310
  <div className="space-y-3 px-5 pb-6 pt-3 xl:px-7">
163
- <CoverageNotice fleet={fleet} data={data} />
311
+ <CoverageNotice fleet={fleet} data={data} kinds={['clusters', 'pods', 'backups', 'scheduledBackups', 'objectStores', 'poolers', 'databases', 'databaseRoles', 'publications', 'subscriptions']} includeJobs />
312
+ <CNPGOperatorBanner namespaces={fleet.rows.map((r) => r.namespace)} className="" />
164
313
 
165
314
  <div className="flex flex-wrap items-center gap-2">
166
315
  <div role="tablist" aria-label="Clusters" className="inline-flex rounded-lg bg-theme-elevated p-0.5">
167
- {segment('attention', 'Needs attention', fleet.attentionCount)}
168
- {segment('all', 'All clusters', total)}
316
+ {segment('all', 'All clusters', totalText)}
317
+ {segment('attention', 'Needs attention', attentionText)}
169
318
  </div>
170
319
  {CNPG_PROBLEM_CATEGORIES.filter((c) => fleet.categoryCounts[c.id] > 0).map((c) => {
171
320
  const on = cat === c.id
@@ -181,7 +330,7 @@ export function CNPGOverview({
181
330
  )}
182
331
  >
183
332
  {c.label}
184
- <span className="font-mono">{fleet.categoryCounts[c.id]}</span>
333
+ <span className="font-mono">{lowerBound ? '≥' : ''}{fleet.categoryCounts[c.id]}</span>
185
334
  </button>
186
335
  )
187
336
  })}
@@ -198,36 +347,40 @@ export function CNPGOverview({
198
347
  </div>
199
348
 
200
349
  <FilterChips chips={chips} />
350
+ {storageUnmeasured && (
351
+ <div className="text-xs text-theme-text-tertiary">
352
+ Storage: volume usage is not measured for any cluster here ({storageUnmeasured}). Retained WAL and resize state are on each cluster’s Storage tab.
353
+ </div>
354
+ )}
201
355
 
202
356
  <div className="overflow-hidden rounded-xl border border-theme-border bg-theme-surface shadow-theme-sm">
203
357
  <div className={TABLE_WRAP}>
204
- <table className="w-full min-w-[980px] table-fixed">
358
+ {/* Fits an ~850px content area (a 1280px window) without scrolling: the
359
+ two fixed columns take 16rem and the percentages stay under the rest. */}
360
+ <table className="w-full min-w-[780px] table-fixed">
205
361
  <colgroup>
206
362
  <col className="w-[22%]" />
207
- <col className="w-[7%]" />
363
+ <col className="w-[18%]" />
364
+ <col className="w-[13%]" />
208
365
  <col className="w-[15%]" />
209
- <col className="w-[15%]" />
210
- <col className="w-[12%]" />
211
- <col className="w-[5%]" />
212
- <col className="w-[14%]" />
213
- <col className="w-[10%]" />
366
+ <col />
367
+ <col className="w-[7.5rem]" />
214
368
  </colgroup>
215
369
  <thead className={TABLE_HEAD}>
216
370
  <tr>
217
- <th className={TH}>Cluster</th>
218
- <th className={TH}>Ready</th>
219
- <th className={TH}>Replication</th>
220
- <th className={TH}>Protection</th>
221
- <th className={TH}>Declarations</th>
222
- <th className={TH}>PG</th>
223
- <th className={TH}>Needs attention</th>
224
- <th className={TH}><span className="sr-only">Actions</span></th>
371
+ <th className={TH_WRAP}>Cluster</th>
372
+ <th className={TH_WRAP}>Replication</th>
373
+ <th className={TH_WRAP}>Storage</th>
374
+ <th className={TH_WRAP}>Backups</th>
375
+ <th className={TH_WRAP}>Needs attention</th>
376
+ <th className={TH_WRAP}><span className="sr-only">Actions</span></th>
225
377
  </tr>
226
378
  </thead>
227
379
  <tbody className={TBODY}>
228
380
  {rows.map((row) => {
229
381
  const ref: SelectedResource = { kind: 'clusters', group: 'postgresql.cnpg.io', namespace: row.namespace, name: row.name }
230
- const active = sameResource(inspected, ref)
382
+ const active = sameSelectedResource(inspected, ref)
383
+ const operation = latestCNPGOperation(operations, { apiBase, context, namespace: row.namespace, name: row.name, uid: row.cluster.metadata.uid })
231
384
  return (
232
385
  <tr
233
386
  key={row.key}
@@ -236,23 +389,44 @@ export function CNPGOverview({
236
389
  aria-selected={active}
237
390
  >
238
391
  <td className={TD}>
239
- <div className="flex items-center gap-2 min-w-0">
240
- <StatusDot tone={row.attention ? (row.problems.some((p) => p.severity === 'critical') ? 'unhealthy' : 'degraded') : row.controllerStatus.level} />
241
- <Tooltip content={row.name} wrapperClassName="min-w-0"><span className="block truncate font-medium">{row.name}</span></Tooltip>
392
+ <div className="flex min-w-0 items-start gap-2">
393
+ <span className="mt-1.5 shrink-0"><RowStatusDot row={row} /></span>
394
+ <div className="min-w-0">
395
+ <BreakText value={row.name} after="-" className="font-medium" />
396
+ {operation && <CNPGFleetOperation op={operation} onFollow={() => navigate(cnpgClusterFullPath(row.namespace, row.name, context))} />}
397
+ <div className="flex min-w-0 items-baseline text-xs text-theme-text-tertiary">
398
+ <Tooltip content={`Namespace ${row.namespace}`} wrapperClassName="min-w-0">
399
+ <span className="block truncate">{row.namespace}</span>
400
+ </Tooltip>
401
+ {row.pgVersion && (
402
+ <span className="shrink-0 whitespace-pre">
403
+ {' · '}
404
+ <Tooltip content="PostgreSQL version"><span>PG {row.pgVersion}</span></Tooltip>
405
+ </span>
406
+ )}
407
+ </div>
408
+ <div className="mt-0.5 font-mono text-xs">
409
+ <ReadyCell row={row} />
410
+ </div>
411
+ </div>
242
412
  </div>
243
- <div className="truncate pl-4 text-xs text-theme-text-tertiary">{row.namespace}</div>
244
- <div className="pl-4"><InstancePills row={row} /></div>
245
- </td>
246
- <td className={clsx(TD, 'font-mono')}>
247
- {row.instances.ready ?? '–'}/{row.instances.desired ?? '–'}
248
413
  </td>
249
414
  <td className={TD}><FactValue fact={row.replication} /></td>
415
+ <td className={TD}><StorageCell row={row} quiet={!!storageUnmeasured} /></td>
250
416
  <td className={TD}><FactValue fact={row.protection.summary} /></td>
251
- <td className={TD}><FactValue fact={row.declarations.summary} /></td>
252
- <td className={clsx(TD, 'font-mono')}>{row.pgVersion ?? '—'}</td>
253
- <td className={TD}><AttentionCell row={row} /></td>
417
+ <td className={clsx(TD, 'overflow-hidden')}>
418
+ <AttentionCell
419
+ row={row}
420
+ onOpenAll={() =>
421
+ navigate(cnpgClusterProblemsPath(row.namespace, row.name, connection.context || undefined), {
422
+ state: { returnLabel: currentPageLabel(), returnCtx: connection.context },
423
+ })
424
+ }
425
+ />
426
+ </td>
254
427
  <td className={clsx(TD, 'text-right')}>
255
- <div className="flex items-center justify-end gap-1">
428
+ <div className="flex items-center justify-end gap-0.5">
429
+ <Tooltip content="Logs from every instance">
256
430
  <button
257
431
  type="button"
258
432
  aria-label={`Logs from every instance of ${row.name}`}
@@ -264,10 +438,11 @@ export function CNPGOverview({
264
438
  state: { returnLabel: currentPageLabel(), returnCtx: connection.context },
265
439
  })
266
440
  }}
267
- className="inline-flex items-center gap-1 whitespace-nowrap rounded-md px-2 py-1 text-xs font-medium text-theme-text-secondary hover:bg-theme-hover hover:text-theme-text-primary"
441
+ className="inline-flex items-center rounded-md p-1.5 text-theme-text-secondary hover:bg-theme-hover hover:text-theme-text-primary"
268
442
  >
269
- <FileText className="h-3 w-3" /> Logs
443
+ <FileText className="h-3.5 w-3.5" />
270
444
  </button>
445
+ </Tooltip>
271
446
  <button
272
447
  type="button"
273
448
  aria-label={`Open ${row.name}`}
@@ -292,7 +467,7 @@ export function CNPGOverview({
292
467
  {rows.length === 0 && (
293
468
  <div className="px-4 py-6 text-sm text-theme-text-tertiary">
294
469
  {filter === 'attention' && !cat && !q
295
- ? 'No clusters need attention.'
470
+ ? lowerBound ? 'No attention findings in the readable data; other data was not read.' : 'No clusters need attention.'
296
471
  : 'No clusters match these filters.'}{' '}
297
472
  <button type="button" onClick={() => onSetParams({ filter: 'all', cat: null, q: null })} className="font-medium text-accent-text hover:underline">
298
473
  Show all clusters
@@ -0,0 +1,51 @@
1
+ import { renderToStaticMarkup } from 'react-dom/server'
2
+ import { expect, it } from 'vitest'
3
+ import { toneTextClass } from '@skyhook-io/k8s-ui'
4
+ import { ParameterRow, cnpgParameterBoolean } from './CNPGParametersInEffect'
5
+ import { cnpgParametersView } from './inspectModel'
6
+ import type { CNPGParametersResponse } from '../../api/cnpg-inspect'
7
+
8
+ function render(values: string[]) {
9
+ const reports = values.map((value, i) => ({ pod: `orders-${i + 1}`, value }))
10
+ const response: CNPGParametersResponse = { cluster: { name: 'orders', namespace: 'db', uid: 'u' }, sampledAt: '', permission: { exec: 'allowed' }, state: 'ok', declared: [{ name: 'max_connections', value: '100' }], instances: reports.map(({ pod, value }) => ({ pod, role: 'replica', state: 'ok', settings: [{ name: 'max_connections', value, source: 'configuration file', context: 'postmaster', pendingRestart: false, setByClient: false }] })) }
11
+ const row = cnpgParametersView(response).rows[0]
12
+ return renderToStaticMarkup(<table><tbody><ParameterRow row={row} reports={reports} readPods={reports.map((p) => p.pod)} unreadText="Not reported" /></tbody></table>)
13
+ }
14
+ it('renders an agreed value once without repeating the summary read set', () => {
15
+ const html = render(['100', '100', '100'])
16
+ expect(html).toContain('font-mono text-theme-text-primary">100</span>')
17
+ expect(html).not.toContain('on orders-1, orders-2, orders-3')
18
+ expect(html).not.toContain('orders-1: 100')
19
+ })
20
+ it('retains the per-instance list when reported values differ', () => {
21
+ const html = render(['100', '200'])
22
+ expect(html).toContain('orders-1: 100 · orders-2: 200')
23
+ expect(html).toContain(toneTextClass('degraded'))
24
+ })
25
+
26
+ it.each(['on', 'ON', 'true', 't', 'yes', 'y', '1'])('accepts PostgreSQL true spelling %s', (value) => expect(cnpgParameterBoolean(value)).toBe(true))
27
+ it.each(['off', 'false', 'f', 'no', 'n', '0'])('accepts PostgreSQL false spelling %s', (value) => expect(cnpgParameterBoolean(value)).toBe(false))
28
+ it.each(['o', '', 'truth', '10', 'ofx'])('does not treat %s as a boolean', (value) => expect(cnpgParameterBoolean(value)).toBeUndefined())
29
+ it('hints equivalent booleans without inferring drift from different text', () => {
30
+ const row = { name: 'log_truncate_on_rotation', declared: 'false', value: 'off', takesEffect: 'reload', setByClient: false, pendingRestart: [], unreported: [] }
31
+ const renderRow = () => renderToStaticMarkup(<table><tbody><ParameterRow row={row} reports={[{ pod: 'orders-1', value: row.value }]} readPods={['orders-1']} unreadText="Not reported" /></tbody></table>)
32
+ expect(renderRow()).toContain('same as declared')
33
+ row.declared = 'off'
34
+ expect(renderRow()).not.toContain('same as declared')
35
+ row.declared = 'false'
36
+ row.value = 'on'
37
+ expect(renderRow()).not.toContain('same as declared')
38
+ expect(renderRow()).not.toContain('mismatch')
39
+ row.declared = '0'; row.value = '0'
40
+ expect(renderRow()).not.toContain('same as declared')
41
+ row.declared = '1024MB'; row.value = '1GB'
42
+ expect(renderRow()).not.toContain('mismatch')
43
+ })
44
+ it('names reporting subsets and restart exceptions in the row', () => {
45
+ const row = { name: 'work_mem', declared: '8MB', value: '8MB', takesEffect: 'reload', setByClient: false, pendingRestart: [], unreported: ['orders-2'] }
46
+ const html = renderToStaticMarkup(<table><tbody><ParameterRow row={row} reports={[{ pod: 'orders-1', value: '8MB' }]} readPods={['orders-1', 'orders-2']} unreadText="Not reported" /></tbody></table>)
47
+ expect(html).toContain('on orders-1')
48
+ expect(html).toContain('Not reported by orders-2')
49
+ row.unreported = []; row.pendingRestart = ['orders-1'] as never[]
50
+ expect(renderToStaticMarkup(<table><tbody><ParameterRow row={row} reports={[{ pod: 'orders-1', value: '8MB' }]} readPods={['orders-1']} unreadText="Not reported" /></tbody></table>)).toContain('Restart pending on orders-1')
51
+ })
@@ -0,0 +1,120 @@
1
+ import { SectionHeading, formatAge, formatGrant, toneTextClass } from '@skyhook-io/k8s-ui'
2
+ import { useCNPGParameters } from '../../api/cnpg-inspect'
3
+ import { cnpgParametersView, type CNPGParameterRow } from './inspectModel'
4
+ import { RefreshFailedNotice } from '../workspace/layout'
5
+
6
+ /**
7
+ * The parameters the Cluster declares beside what each instance's PostgreSQL
8
+ * reports: whether a change took effect, and whether it waits for a restart.
9
+ * Observation only — changes go through the Cluster spec.
10
+ */
11
+ export function CNPGParametersInEffect({ namespace, name, declared, scope }: { namespace: string; name: string; declared: Record<string, string>; scope?: { declaredInstances?: number; expectedInstances?: string[] } }) {
12
+ const q = useCNPGParameters(namespace, name)
13
+ const d = q.data
14
+ const declarations = Object.entries(declared).sort(([a], [b]) => a.localeCompare(b)).map(([name, value]) => ({ name, value }))
15
+ const view = cnpgParametersView(d ?? {
16
+ cluster: { namespace, name, uid: '' }, sampledAt: '', permission: { exec: 'unknown' }, state: 'error', instances: [],
17
+ declared: declarations,
18
+ }, declarations, scope)
19
+ const unavailable = !d && q.error instanceof Error
20
+ ? `Not read: ${q.error.message}`
21
+ : !d ? (q.isLoading ? 'Not read: reading each instance…' : 'Not read: instance values unavailable')
22
+ : d.state === 'denied' ? `Reading instance values needs ${formatGrant(d.permission.grant) ?? `create pods/exec in namespace ${namespace}`}.`
23
+ : d.error ? `Not read: ${d.error}` : null
24
+ const content = (
25
+ <>
26
+ <RefreshFailedNotice queries={[q]} className="mb-2" />
27
+ {unavailable && <p className="mb-2 text-sm text-theme-text-tertiary">{unavailable}</p>}
28
+ {d && !unavailable && declarations.length > 0 && <p className={`mb-2 text-sm ${toneTextClass(view.summary.tone)}`}>{view.summary.text}</p>}
29
+ {declarations.length === 0 && <p className="text-sm text-theme-text-tertiary">No parameters declared in spec.postgresql.parameters.</p>}
30
+ {declarations.length > 0 && (
31
+ <div className="overflow-x-auto">
32
+ <table className="w-full text-sm">
33
+ <thead>
34
+ <tr className="border-b border-theme-border text-left text-[11px] uppercase tracking-wide text-theme-text-tertiary">
35
+ <th className="py-1.5 pr-4 font-medium">Parameter</th>
36
+ <th className="py-1.5 pr-4 font-medium">Declared</th>
37
+ <th className="py-1.5 pr-4 font-medium">Reported by instance</th>
38
+ <th className="py-1.5 font-medium">A change applies on</th>
39
+ </tr>
40
+ </thead>
41
+ <tbody>
42
+ {view.rows.map((r) => view.read.length === 0 ? (
43
+ <tr key={r.name} className="border-b border-theme-border/60 align-top last:border-0">
44
+ <td className="py-1.5 pr-4 font-mono text-xs text-theme-text-primary">{r.name}</td>
45
+ <td className="py-1.5 pr-4 font-mono text-xs text-theme-text-secondary">{r.declared === '' ? <span className="text-theme-text-tertiary">(empty)</span> : r.declared}</td>
46
+ <td /><td />
47
+ </tr>
48
+ ) : (
49
+ <ParameterRow key={r.name} row={r} readPods={view.read.map((i) => i.pod)}
50
+ unreadText={d?.skipped?.includes(r.name) ? 'Not read: not a parameter name'
51
+ : !d ? (q.isLoading ? 'Not read: loading' : 'Not read: request failed')
52
+ : d.state === 'denied' ? 'Not read: pods/exec denied'
53
+ : !d.declared.some((p) => p.name.toLowerCase() === r.name.toLowerCase()) ? 'Not sampled'
54
+ : view.read.length === 0 ? (d.instances.length === 0 ? 'Not read: no instance Pod' : 'Not read: no instance answered')
55
+ : 'Not reported'}
56
+ reports={view.read.flatMap((i) => {
57
+ const setting = i.settings!.find((s) => s.name.toLowerCase() === r.name.toLowerCase())
58
+ return setting?.value != null ? [{ pod: i.pod, value: setting.value }] : []
59
+ })} />
60
+ ))}
61
+ </tbody>
62
+ </table>
63
+ </div>
64
+ )}
65
+ {d && view.read.length > 0 && <p className="mt-2 text-[11.5px] text-theme-text-tertiary">
66
+ Read {d.sampledAt ? `${formatAge(d.sampledAt)} ago` : 'just now'} from a new Radar session on each of {view.read.map((i) => i.pod).join(', ')}. A role, database or
67
+ client can set its own value for a parameter a session can change. PostgreSQL normalizes units and boolean spellings (such as false to off), so different text alone does not mean a different value.
68
+ </p>}
69
+ {d?.skipped && d.skipped.length > 0 && <p className="mt-1 text-xs text-theme-text-tertiary">Not read (not a parameter name): {d.skipped.join(', ')}.</p>}
70
+ {!!d?.omitted && <p className="mt-1 text-xs text-theme-text-tertiary">{d.omitted} declaration{d.omitted === 1 ? ' was' : 's were'} not sampled.</p>}
71
+ {view.unread.length > 0 && (
72
+ <p className={`mt-1 text-xs ${toneTextClass('unknown')}`}>
73
+ Not read on {view.unread.map((i) => `${i.pod} (${i.error ?? i.state})`).join(', ')}.
74
+ </p>
75
+ )}
76
+ </>
77
+ )
78
+ return <><SectionHeading hint="spec.postgresql.parameters">PostgreSQL parameters</SectionHeading>{content}</>
79
+ }
80
+
81
+ export function ParameterRow({ row, reports, unreadText, readPods }: { row: CNPGParameterRow; reports: { pod: string; value: string }[]; unreadText: string; readPods: string[] }) {
82
+ const namesNeeded = !!row.perInstance || row.pendingRestart.length > 0 || row.unreported.length > 0 || reports.length !== readPods.length || reports.some((p) => !readPods.includes(p.pod))
83
+ const declaredBoolean = cnpgParameterBoolean(row.declared)
84
+ const sameBoolean = declaredBoolean !== undefined && reports.length > 0 && reports.some((p) => p.value !== row.declared) && reports.every((p) => ['on', 'off'].includes(p.value.trim().toLowerCase()) && cnpgParameterBoolean(p.value) === declaredBoolean)
85
+
86
+ return (
87
+ <tr className="border-b border-theme-border/60 align-top last:border-0">
88
+ <td className="py-1.5 pr-4 font-mono text-xs text-theme-text-primary">{row.name}</td>
89
+ <td className="py-1.5 pr-4 font-mono text-xs text-theme-text-secondary">{row.declared === '' ? <span className="text-theme-text-tertiary">(empty)</span> : row.declared}</td>
90
+ <td className="py-1.5 pr-4 text-xs">
91
+ {reports.length > 0 ? (
92
+ row.perInstance
93
+ ? <span className={toneTextClass('degraded')}>{reports.map((p) => `${p.pod}: ${p.value === '' ? '(empty)' : p.value}`).join(' · ')}</span>
94
+ : <><span className="font-mono text-theme-text-primary">{row.value === '' ? '(empty)' : row.value}</span>{namesNeeded && <div className="text-theme-text-tertiary">on {reports.map((p) => p.pod).join(', ')}</div>}</>
95
+ ) : row.setByClient ? (
96
+ <span className="text-theme-text-tertiary">Set by each connection, so not readable here</span>
97
+ ) : (
98
+ <span className="text-theme-text-tertiary">{unreadText}</span>
99
+ )}
100
+ {sameBoolean && <span className="ml-1.5 text-theme-text-tertiary">same as declared</span>}
101
+ {row.source && row.source !== 'configuration file' && !row.setByClient && <span className="ml-1.5 text-theme-text-tertiary">source: {row.source}</span>}
102
+ {row.pendingRestart.length > 0 && <div className={toneTextClass('degraded')}>Restart pending on {row.pendingRestart.join(', ')}</div>}
103
+ {row.unreported.length > 0 && unreadText === 'Not reported' && (
104
+ <div className="text-theme-text-tertiary">Not reported by {row.unreported.join(', ')}</div>
105
+ )}
106
+ </td>
107
+ <td className="py-1.5 text-xs text-theme-text-secondary">{row.takesEffect}</td>
108
+ </tr>
109
+ )
110
+ }
111
+
112
+ export function cnpgParameterBoolean(value: string): boolean | undefined {
113
+ const normalized = value.trim().toLowerCase()
114
+ if (normalized === '1') return true
115
+ if (normalized === '0') return false
116
+ if (!normalized) return undefined
117
+ const matches = ['on', 'off', 'true', 'false', 'yes', 'no'].filter((spelling) => spelling.startsWith(normalized))
118
+ if (matches.length !== 1) return undefined
119
+ return ['on', 'true', 'yes'].includes(matches[0])
120
+ }