@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,45 @@
1
+ // @vitest-environment jsdom
2
+ import { act } from 'react'
3
+ import { createRoot } from 'react-dom/client'
4
+ import { expect, it, vi } from 'vitest'
5
+ import { CNPGOperatorConditions } from './CNPGOperatorConditions'
6
+
7
+ ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
8
+
9
+ it('starts folded and keeps every condition field, including unknown status and exact transition', () => {
10
+ vi.useFakeTimers(); vi.setSystemTime(new Date('2026-10-04T13:34:56Z'))
11
+ const host = document.createElement('div')
12
+ const root = createRoot(host)
13
+ act(() => root.render(<CNPGOperatorConditions conditions={[
14
+ { type: 'Ready', status: 'False', reason: 'Waiting', message: 'Waiting for the primary', lastTransitionTime: '2026-10-04T12:34:56Z' },
15
+ { type: 'ContinuousArchiving', status: 'Unknown', reason: 'Checking', message: 'Archive not checked yet' },
16
+ ]} />))
17
+ const button = host.querySelector('button')!
18
+ expect(button.textContent).toContain('Operator conditions')
19
+ expect(button.textContent).toContain('2 reported')
20
+ expect(button.getAttribute('aria-expanded')).toBe('false')
21
+ act(() => button.click())
22
+ expect(button.getAttribute('aria-expanded')).toBe('true')
23
+ for (const text of ['Ready', 'False', 'Waiting', 'Waiting for the primary', '1h ago', 'ContinuousArchiving', 'Unknown', 'Checking', 'Archive not checked yet', 'Last transition: Not reported']) expect(host.textContent).toContain(text)
24
+ expect(host.querySelector('time')?.getAttribute('datetime')).toBe('2026-10-04T12:34:56Z')
25
+ const badge = [...host.querySelectorAll('.badge-sm')].find((b) => b.textContent === 'False')
26
+ expect(badge).toBeDefined()
27
+ expect(badge?.className).not.toMatch(/success|warning|error/)
28
+ expect(badge?.className).toContain('bg-theme-hover/50 text-theme-text-secondary border-theme-border')
29
+ expect(badge?.className).not.toMatch(/emerald|amber|orange|red|sky-/)
30
+ const time = host.querySelector('time')!
31
+ act(() => time.parentElement!.dispatchEvent(new MouseEvent('mouseover', { bubbles: true })))
32
+ act(() => vi.advanceTimersByTime(1000))
33
+ expect(document.body.textContent).toContain('2026-10-04T12:34:56Z')
34
+ vi.useRealTimers()
35
+ act(() => root.unmount())
36
+ })
37
+
38
+ it('states that conditions have not been reported', () => {
39
+ const host = document.createElement('div')
40
+ const root = createRoot(host)
41
+ act(() => root.render(<CNPGOperatorConditions conditions={[]} />))
42
+ expect(host.textContent).toContain('No conditions reported')
43
+ expect(host.querySelector('button')!.getAttribute('aria-expanded')).toBe('false')
44
+ act(() => root.unmount())
45
+ })
@@ -0,0 +1,16 @@
1
+ import { Badge, FactGrid, FactRow, FoldSection, Tooltip, formatAge } from '@skyhook-io/k8s-ui'
2
+
3
+ export function CNPGOperatorConditions({ conditions }: { conditions: { type: string; status: string; reason?: string; message?: string; lastTransitionTime?: string }[] }) {
4
+ return (
5
+ <section className="mx-4 mb-4 rounded-xl border border-theme-border bg-theme-surface px-4 py-3 shadow-theme-sm">
6
+ <FoldSection title="Operator conditions" summary={conditions.length > 0 ? `${conditions.length} reported · status.conditions` : 'No conditions reported'} attention={false}>
7
+ <FactGrid>{conditions.map((c) => <FactRow key={c.type} label={<span className="inline-flex flex-wrap items-center gap-2">{c.type}<Badge severity="neutral" size="sm">{c.status}</Badge></span>}>
8
+ <div className="text-xs text-theme-text-secondary">Reason: {c.reason || 'Not reported'}</div>
9
+ <div className="break-words text-xs text-theme-text-secondary">{c.message || 'No message reported'}</div>
10
+ <div className="text-xs text-theme-text-tertiary">Last transition: {c.lastTransitionTime ? <Tooltip content={c.lastTransitionTime}><time dateTime={c.lastTransitionTime}>{formatAge(c.lastTransitionTime)} ago</time></Tooltip> : 'Not reported'}</div>
11
+ </FactRow>)}</FactGrid>
12
+ {conditions.length === 0 && <p className="text-sm text-theme-text-tertiary">The operator has not reported status.conditions.</p>}
13
+ </FoldSection>
14
+ </section>
15
+ )
16
+ }
@@ -0,0 +1,27 @@
1
+ // @vitest-environment jsdom
2
+ import { act } from 'react'
3
+ import { createRoot } from 'react-dom/client'
4
+ import { expect, it, vi } from 'vitest'
5
+ import { ConfigBlock } from './CNPGOperator'
6
+
7
+ ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
8
+
9
+ it('summarizes monitoring queries, then expands newline-preserving SQL with explicit truncation and inspection', () => {
10
+ const queries = `connections:\n query: |\n SELECT count(*)\n FROM pg_stat_activity;\nlong_query:\n query: '${'x'.repeat(8200)}'\n`
11
+ const inspect = vi.fn()
12
+ const host = document.createElement('div')
13
+ const root = createRoot(host)
14
+ act(() => root.render(<ConfigBlock config={{ kind: 'ConfigMap', namespace: 'operator', name: 'monitoring', purpose: 'monitoring', exists: true, readable: true, data: { queries } }} onInspect={inspect} />))
15
+ expect(host.textContent).toContain('2 queries · ConfigMap monitoring')
16
+ const fold = [...host.querySelectorAll('button')].find((b) => b.textContent?.includes('Query definitions'))!
17
+ expect(fold.getAttribute('aria-expanded')).toBe('false')
18
+ act(() => fold.click())
19
+ const preview = host.querySelector('pre')!
20
+ expect(preview.textContent).toBe(queries.slice(0, 8000))
21
+ expect(preview.classList.contains('whitespace-pre-wrap')).toBe(true)
22
+ expect(host.textContent).toContain(`Preview truncated to 8,000 of ${queries.length.toLocaleString()} characters`)
23
+ const link = [...host.querySelectorAll('button')].find((b) => b.textContent?.includes('Inspect ConfigMap'))!
24
+ act(() => link.click())
25
+ expect(inspect).toHaveBeenCalledWith({ kind: 'configmaps', group: '', namespace: 'operator', name: 'monitoring' })
26
+ act(() => root.unmount())
27
+ })
@@ -0,0 +1,247 @@
1
+ import type { ReactNode } from 'react'
2
+ import { Badge, formatAge, type CNPGFleet } from '@skyhook-io/k8s-ui'
3
+ import type { CNPGOperatorDiagnosis, CNPGOperatorReconcilePod, CNPGReadCoverage } from '../../api/cnpg-recovery'
4
+ import { buildWorkloadPath } from '../../utils/navigation'
5
+ import { GrantText, Mono, Sub } from '../workspace/layout'
6
+ import { useCNPGNavigate } from './useCNPGNavigate'
7
+
8
+ function Row({ label, children }: { label: string; children: ReactNode }) {
9
+ return (
10
+ <div className="grid grid-cols-[10rem_minmax(0,1fr)] gap-x-4 border-t border-theme-border px-4 py-2.5 first:border-t-0">
11
+ <div className="text-sm text-theme-text-tertiary">{label}</div>
12
+ <div className="min-w-0 text-sm text-theme-text-primary">{children}</div>
13
+ </div>
14
+ )
15
+ }
16
+
17
+ function Unread({ cov, what }: { cov: CNPGReadCoverage; what: string }) {
18
+ if (cov.state === 'denied' && cov.grant) {
19
+ return (
20
+ <span className="text-theme-text-tertiary">
21
+ No access to {what} · needs <GrantText grant={cov.grant} />
22
+ </span>
23
+ )
24
+ }
25
+ const text =
26
+ cov.state === 'denied'
27
+ ? `No access to ${what} (needs a grant you do not have)`
28
+ : cov.state === 'notFound'
29
+ ? `${what} not found`
30
+ : `${what} could not be read${cov.reason ? `: ${cov.reason}` : ''}`
31
+ return <span className="text-theme-text-tertiary">{text}</span>
32
+ }
33
+
34
+ function Leader({ d }: { d: CNPGOperatorDiagnosis }) {
35
+ const l = d.leader
36
+ if (l.state === 'disabled') return <span className="text-theme-text-secondary">Leader election is off ({l.reason}); run a single replica.</span>
37
+ if (l.state !== 'ok') return <Unread cov={{ state: l.state, grant: l.grant, reason: l.reason }} what={`Lease ${l.lease ?? ''}`.trim()} />
38
+ return (
39
+ <>
40
+ <div className="flex flex-wrap items-center gap-2">
41
+ {l.stale ? <Badge severity="error" size="sm">Not renewed</Badge> : <Badge severity="success" size="sm">Held</Badge>}
42
+ {l.holderPod ? <Mono>{l.holderPod}</Mono> : <span className="text-theme-text-tertiary">no holder</span>}
43
+ {l.holderPod && !l.holderIsCurrentPod && <span className="text-xs text-theme-text-tertiary">not one of the Deployment’s current Pods</span>}
44
+ </div>
45
+ <Sub>
46
+ Lease {l.lease}
47
+ {l.renewTime ? ` · renewed ${formatAge(l.renewTime)} ago` : ''}
48
+ {l.leaseDurationSeconds !== undefined ? ` · duration ${l.leaseDurationSeconds}s` : ''}
49
+ {l.transitions !== undefined ? ` · ${l.transitions} leader change${l.transitions === 1 ? '' : 's'}` : ''}
50
+ {l.stale ? ' · no operator instance is leading, so nothing reconciles' : ''}
51
+ </Sub>
52
+ </>
53
+ )
54
+ }
55
+
56
+ function Watch({ d, fleet }: { d: CNPGOperatorDiagnosis; fleet: CNPGFleet }) {
57
+ const w = d.watch
58
+ const outside = w.all || w.unresolved ? [] : fleet.rows.filter((r) => !w.namespaces.includes(r.namespace))
59
+ return (
60
+ <>
61
+ <div>{w.all ? 'All namespaces' : w.unresolved ? <span className="text-theme-text-secondary">{w.unresolved}</span> : w.namespaces.join(', ')}</div>
62
+ <Sub>{w.source}</Sub>
63
+ {outside.length > 0 && (
64
+ <div className="mt-1 text-xs text-theme-text-secondary">
65
+ Not reconciled by this operator: {outside.map((r) => `${r.namespace}/${r.name}`).join(', ')}
66
+ </div>
67
+ )}
68
+ </>
69
+ )
70
+ }
71
+
72
+ function Webhooks({ d }: { d: CNPGOperatorDiagnosis }) {
73
+ return (
74
+ <div className="space-y-2">
75
+ {d.webhooks.map((cfg) => {
76
+ if (cfg.state !== 'ok') {
77
+ return (
78
+ <div key={cfg.name}>
79
+ <div className="text-xs text-theme-text-secondary">{cfg.kind}</div>
80
+ {cfg.state === 'notFound' ? (
81
+ <span className="text-theme-text-secondary">
82
+ <Mono>{cfg.name}</Mono> is missing: the operator recreates it at startup; until then CloudNativePG objects are not validated or defaulted.
83
+ </span>
84
+ ) : (
85
+ <Unread cov={cfg} what={cfg.name} />
86
+ )}
87
+ </div>
88
+ )
89
+ }
90
+ const noCA = cfg.webhooks.filter((w) => !w.caBundleSet && !w.url)
91
+ const policies = [...new Set(cfg.webhooks.map((w) => w.failurePolicy))]
92
+ return (
93
+ <div key={cfg.name}>
94
+ <div className="text-xs text-theme-text-secondary">{cfg.kind}</div>
95
+ <div>
96
+ <Mono>{cfg.name}</Mono> · {cfg.webhooks.length} webhook{cfg.webhooks.length === 1 ? '' : 's'} · failurePolicy {policies.join(', ')}
97
+ </div>
98
+ {noCA.length > 0 ? (
99
+ <div className="text-xs text-theme-text-secondary">No CA bundle on {noCA.map((w) => w.name).join(', ')}: the apiserver cannot verify the webhook, so calls fail.</div>
100
+ ) : (
101
+ <Sub>CA bundle set on every webhook</Sub>
102
+ )}
103
+ </div>
104
+ )
105
+ })}
106
+ {d.webhookServices.map((svc) => {
107
+ const failClosed = d.webhooks.some((c) => c.webhooks.some((w) => w.service === `${svc.namespace}/${svc.name}` && w.failurePolicy === 'Fail'))
108
+ return (
109
+ <div key={`${svc.namespace}/${svc.name}`}>
110
+ <div className="text-xs text-theme-text-secondary">Service {svc.namespace}/{svc.name}</div>
111
+ {svc.state !== 'ok' || svc.readyEndpoints === null ? (
112
+ <Unread cov={svc} what="its endpoints" />
113
+ ) : svc.readyEndpoints > 0 ? (
114
+ <div>
115
+ {svc.readyEndpoints} ready endpoint{svc.readyEndpoints === 1 ? '' : 's'}
116
+ {svc.notReadyEndpoints ? <span className="text-theme-text-tertiary"> · {svc.notReadyEndpoints} not ready</span> : null}
117
+ </div>
118
+ ) : (
119
+ <div className="flex flex-wrap items-center gap-2">
120
+ <Badge severity="error" size="sm">No ready endpoint</Badge>
121
+ <span className="text-xs text-theme-text-secondary">
122
+ {failClosed ? 'With failurePolicy Fail, every create or update of a CloudNativePG object is rejected until the operator is ready.' : 'Webhook calls are skipped (failurePolicy Ignore).'}
123
+ </span>
124
+ </div>
125
+ )}
126
+ </div>
127
+ )
128
+ })}
129
+ </div>
130
+ )
131
+ }
132
+
133
+ function Reconcile({ pods }: { pods: CNPGOperatorReconcilePod[] }) {
134
+ if (pods.length === 0) return <span className="text-theme-text-tertiary">No operator Pods visible</span>
135
+ return (
136
+ <div className="space-y-3">
137
+ {pods.map((p) => (
138
+ <div key={p.pod}>
139
+ <div className="flex flex-wrap items-center gap-2">
140
+ <Mono>{p.pod}</Mono>
141
+ {p.leader && <Badge severity="info" size="sm">leader</Badge>}
142
+ <span className="text-xs text-theme-text-tertiary">
143
+ {p.state === 'ok' || p.state === 'partial'
144
+ ? p.startedAt
145
+ ? `counters since the operator container started ${formatAge(p.startedAt)} ago`
146
+ : 'counters since the operator container last started'
147
+ : null}
148
+ {!p.leader ? `${p.state === 'ok' || p.state === 'partial' ? ' · ' : ''}only the leader reconciles` : ''}
149
+ </span>
150
+ </div>
151
+ {p.state !== 'ok' && p.state !== 'partial' ? (
152
+ <span className="text-xs text-theme-text-tertiary">{p.error ?? p.state}</span>
153
+ ) : (
154
+ <table className="mt-1 w-full max-w-lg text-left text-xs">
155
+ <thead className="text-theme-text-tertiary">
156
+ <tr>
157
+ <th className="py-0.5 font-normal">Controller</th>
158
+ <th className="py-0.5 font-normal">Reconciles</th>
159
+ <th className="py-0.5 font-normal">Errors</th>
160
+ </tr>
161
+ </thead>
162
+ <tbody>
163
+ {p.controllers.map((c) => (
164
+ <tr key={c.controller} className="border-t border-theme-border">
165
+ <td className="py-0.5 font-mono">{c.controller}</td>
166
+ <td className="py-0.5">{c.total === null ? '—' : Math.round(c.total).toLocaleString()}</td>
167
+ <td className="py-0.5">
168
+ {c.errors === null ? '—' : c.errors > 0 ? <span className="font-medium text-theme-text-primary">{Math.round(c.errors).toLocaleString()}</span> : '0'}
169
+ </td>
170
+ </tr>
171
+ ))}
172
+ </tbody>
173
+ </table>
174
+ )}
175
+ {p.reason && <Sub>{p.reason}</Sub>}
176
+ </div>
177
+ ))}
178
+ <Sub>From controller-runtime metrics on the operator’s metrics port, read through pods/proxy. Errors are cumulative; a count that grows between refreshes is the signal.</Sub>
179
+ </div>
180
+ )
181
+ }
182
+
183
+ /**
184
+ * Why a Cluster may not be reconciling, from the operator's side: leader
185
+ * Lease, watched namespaces, webhook reachability, reconcile errors and
186
+ * recent events. Each fact reports its own access.
187
+ */
188
+ export function CNPGOperatorDiagnosisSection({ diagnosis, fleet }: { diagnosis: CNPGOperatorDiagnosis[]; fleet: CNPGFleet }) {
189
+ const navigate = useCNPGNavigate()
190
+ if (diagnosis.length === 0) return null
191
+ return (
192
+ <>
193
+ {diagnosis.map((d) => {
194
+ const warnings = d.events.items.filter((e) => e.type === 'Warning')
195
+ return (
196
+ <section key={`${d.namespace}/${d.deployment}`}>
197
+ <div className="mb-2 flex flex-wrap items-baseline gap-2">
198
+ <h2 className="text-sm font-semibold text-theme-text-primary">Operator diagnosis</h2>
199
+ <span className="text-xs text-theme-text-tertiary">
200
+ Deployment {d.deployment} in {d.namespace}
201
+ </span>
202
+ <button
203
+ type="button"
204
+ onClick={() => navigate(buildWorkloadPath({ kind: 'deployments', namespace: d.namespace, name: d.deployment, tab: 'logs' }))}
205
+ className="ml-auto text-xs text-accent-text hover:underline"
206
+ >
207
+ Operator logs
208
+ </button>
209
+ </div>
210
+ <div className="overflow-hidden rounded-xl border border-theme-border bg-theme-surface shadow-theme-sm">
211
+ <Row label="Leader">
212
+ <Leader d={d} />
213
+ </Row>
214
+ <Row label="Watched namespaces">
215
+ <Watch d={d} fleet={fleet} />
216
+ </Row>
217
+ <Row label="Admission webhooks">
218
+ <Webhooks d={d} />
219
+ </Row>
220
+ <Row label="Reconcile errors">
221
+ {d.podCoverage.state !== 'ok' ? <Unread cov={d.podCoverage} what="the operator Pods" /> : <Reconcile pods={d.reconcile} />}
222
+ </Row>
223
+ <Row label="Recent events">
224
+ {d.events.state !== 'ok' ? (
225
+ <Unread cov={d.events} what={`events in ${d.namespace}`} />
226
+ ) : d.events.items.length === 0 ? (
227
+ <span className="text-theme-text-tertiary">None retained for the operator’s Deployment, ReplicaSets or Pods</span>
228
+ ) : (
229
+ <ul className="space-y-1">
230
+ {[...warnings, ...d.events.items.filter((e) => e.type !== 'Warning')].slice(0, 8).map((e, i) => (
231
+ <li key={`${e.kind}/${e.name}/${e.reason}/${i}`} className="text-xs text-theme-text-secondary">
232
+ {e.type === 'Warning' && <Badge severity="warning" size="sm">Warning</Badge>}{' '}
233
+ <span className="font-medium text-theme-text-primary">{e.reason}</span> on {e.kind} {e.name}
234
+ {e.lastSeen ? ` · ${formatAge(e.lastSeen)} ago` : ''}
235
+ {e.count > 1 ? ` · ×${e.count}` : ''}: {e.message}
236
+ </li>
237
+ ))}
238
+ </ul>
239
+ )}
240
+ </Row>
241
+ </div>
242
+ </section>
243
+ )
244
+ })}
245
+ </>
246
+ )
247
+ }
@@ -0,0 +1,171 @@
1
+ import { renderToStaticMarkup } from 'react-dom/server'
2
+ import { MemoryRouter } from 'react-router-dom'
3
+ import { describe, expect, it, vi } from 'vitest'
4
+ import { buildCNPGFleet, CNPG_WORKSPACE_KEYS, type CNPGKindCoverage, type CNPGWorkspaceResponse } from '@skyhook-io/k8s-ui'
5
+
6
+ const sidebar = vi.hoisted(() => ({ data: undefined as CNPGWorkspaceResponse | undefined }))
7
+ vi.mock('../../api/cnpg', () => ({ useCNPGWorkspace: () => ({ data: sidebar.data }) }))
8
+ vi.mock('../../api/cnpg-storage', () => ({ useCNPGFleetDisk: () => ({}) }))
9
+ vi.mock('../../api/cnpg-history', () => ({ useCNPGFleetMetrics: () => ({}) }))
10
+ vi.mock('../../api/client', () => ({ useRadarFeature: () => ({ support: 'supported' }) }))
11
+
12
+ vi.mock('../../context/ConnectionContext', () => ({ useConnection: () => ({ connection: { context: 'kind-test' } }) }))
13
+ vi.mock('./CNPGOperatorBanner', () => ({ CNPGOperatorBanner: () => null }))
14
+
15
+ const { CNPGOverview } = await import('./CNPGOverview')
16
+ const { useCNPGSidebarWorkspace } = await import('./useCNPGSidebarWorkspace')
17
+
18
+ const cluster = (namespace: string, name: string) => ({
19
+ apiVersion: 'postgresql.cnpg.io/v1',
20
+ kind: 'Cluster',
21
+ metadata: { namespace, name },
22
+ spec: { instances: 1 },
23
+ status: { phase: 'Cluster in healthy state', readyInstances: 1 },
24
+ })
25
+
26
+ function response(clusters: ReturnType<typeof cluster>[], coverage: CNPGKindCoverage = { state: 'full' }): CNPGWorkspaceResponse {
27
+ return { installed: true, context: 'kind-test', namespaces: null, coverage: { clusters: coverage }, objects: { clusters }, issues: [], audit: [], backupsOmitted: 0 }
28
+ }
29
+
30
+ function render(resp: CNPGWorkspaceResponse, query = '', opts: { attention?: string[]; onCreate?: () => void; namespaces?: string[] } = {}) {
31
+ const fleet = buildCNPGFleet(resp)
32
+ for (const row of fleet.rows) row.attention = !!opts.attention?.includes(row.name)
33
+ fleet.attentionCount = fleet.rows.filter((r) => r.attention).length
34
+ return renderToStaticMarkup(
35
+ <MemoryRouter>
36
+ <CNPGOverview
37
+ data={resp}
38
+ fleet={fleet}
39
+ namespaces={opts.namespaces ?? []}
40
+ searchParams={new URLSearchParams(query)}
41
+ onSetParams={() => {}}
42
+ onInspect={() => {}}
43
+ inspected={null}
44
+ onClearNamespaces={() => {}}
45
+ onCreate={opts.onCreate}
46
+ />
47
+ </MemoryRouter>,
48
+ )
49
+ }
50
+
51
+ describe('CNPGOverview is the list of clusters', () => {
52
+ const two = response([cluster('pg', 'pg-bad'), cluster('pg', 'pg-good')])
53
+
54
+ it('lists every cluster unless asked for less, healthy ones included', () => {
55
+ const html = render(two, '', { attention: ['pg-bad'] })
56
+ expect(html).toContain('pg-bad')
57
+ expect(html).toContain('pg-good')
58
+ expect(html).toMatch(/aria-selected="true"[^>]*>All clusters/)
59
+ })
60
+
61
+ it('narrows to the clusters that need attention when asked', () => {
62
+ const html = render(two, 'filter=attention', { attention: ['pg-bad'] })
63
+ expect(html).toContain('pg-bad')
64
+ expect(html).not.toContain('pg-good')
65
+ })
66
+
67
+ it('counts a partly read list as a floor', () => {
68
+ const html = render(response([cluster('a', 'pg-a')], { state: 'partial', allowedNamespaces: ['a'] }))
69
+ expect(html).toContain('≥1 PostgreSQL cluster')
70
+ })
71
+
72
+ it('says what was not read instead of claiming there are no clusters', () => {
73
+ const html = render(response([], { state: 'uncached', uncachedNamespaces: ['c'] }))
74
+ expect(html).toContain('No visible PostgreSQL clusters')
75
+ expect(html).toContain('Radar does not cache PostgreSQL clusters in c')
76
+ expect(html).not.toContain('No PostgreSQL clusters in kind-test')
77
+ })
78
+
79
+ it('limits an empty read to the selected scope and keeps partial coverage distinct', () => {
80
+ const html = render(response([]), '', { namespaces: ['db', 'prod'] })
81
+ expect(html).toContain('No PostgreSQL clusters in the selected namespaces')
82
+ expect(html).toContain('None in db, prod in kind-test')
83
+ expect(html).not.toContain('No PostgreSQL clusters in kind-test')
84
+ expect(render(response([]))).toContain('No PostgreSQL clusters in kind-test')
85
+ expect(render(response([], { state: 'partial' }), '', { namespaces: ['db'] })).toContain('No visible PostgreSQL clusters')
86
+ })
87
+
88
+ it('says once, for the whole list, why no volume usage is measured', () => {
89
+ const resp = response([cluster('pg', 'pg-a'), cluster('pg', 'pg-b')])
90
+ const fleet = buildCNPGFleet(resp)
91
+ for (const row of fleet.rows) row.disk = { text: 'not measured', tone: 'unknown', source: 'no claims owned by this cluster' }
92
+ const html = renderToStaticMarkup(
93
+ <MemoryRouter>
94
+ <CNPGOverview data={resp} fleet={fleet} namespaces={[]} searchParams={new URLSearchParams()} onSetParams={() => {}} onInspect={() => {}} inspected={null} onClearNamespaces={() => {}} />
95
+ </MemoryRouter>,
96
+ )
97
+ expect(html).toContain('not measured for any cluster here (no claims owned by them)')
98
+ })
99
+
100
+ it('offers Create when the host can create', () => {
101
+ expect(render(two, '', { onCreate: () => {} })).toContain('Create')
102
+ expect(render(two)).not.toMatch(/>Create</)
103
+ })
104
+
105
+ it('names missing operator readiness and separately shows observed zero ready instance Pods', () => {
106
+ const analytics = cluster('db', 'analytics')
107
+ delete (analytics.status as Partial<typeof analytics.status>).readyInstances
108
+ const resp = response([analytics])
109
+ resp.coverage.pods = { state: 'full' }
110
+ resp.objects.pods = []
111
+ const html = render(resp)
112
+ expect(html).toContain('Not reported by the operator')
113
+ expect(html).toContain('0 of 1 instance Pods ready')
114
+ expect(html).not.toContain('–/1')
115
+ })
116
+
117
+ it('does not turn unread Pod readiness into an observed zero', () => {
118
+ const analytics = cluster('db', 'analytics')
119
+ delete (analytics.status as Partial<typeof analytics.status>).readyInstances
120
+ const resp = response([analytics])
121
+ resp.coverage.pods = { state: 'denied' }
122
+ const html = render(resp)
123
+ expect(html).toContain('Not reported by the operator')
124
+ expect(html).not.toContain('instance Pods ready')
125
+ expect(html).not.toContain('–/1')
126
+ })
127
+
128
+ it('keeps reported operator readiness as a ratio', () => {
129
+ expect(render(response([cluster('db', 'payments')]))).toContain('1/1')
130
+ })
131
+ })
132
+
133
+ it('qualifies heading, attention filter and category counts on both fleet routes', () => {
134
+ const resp = response([cluster('prod', 'orders')], { state: 'partial', allowedNamespaces: ['prod'] })
135
+ const fleet = buildCNPGFleet(resp)
136
+ fleet.attentionCount = 1; fleet.categoryCounts.availability = 1
137
+ const html = renderToStaticMarkup(<MemoryRouter><CNPGOverview data={resp} fleet={fleet} namespaces={[]} searchParams={new URLSearchParams()} onSetParams={() => {}} onInspect={() => {}} inspected={null} onClearNamespaces={() => {}} /></MemoryRouter>)
138
+ expect(html).toContain('≥1 need attention')
139
+ expect(html).toMatch(/Needs attention<span[^>]*>≥1</)
140
+ expect(html).toMatch(/Availability<span[^>]*>≥1</)
141
+ expect(render(resp)).toMatch(/Needs attention<span[^>]*>Unknown</)
142
+ expect(render(resp)).toContain('Needs attention: unknown')
143
+ expect(render(resp)).not.toContain('Unknown need attention')
144
+ })
145
+
146
+ it('does not claim an empty partial attention filter proves no clusters need attention', () => {
147
+ const html = render(response([cluster('prod', 'orders')], { state: 'partial', allowedNamespaces: ['prod'] }), 'filter=attention')
148
+ expect(html).toContain('No attention findings in the readable data; other data was not read.')
149
+ expect(html).not.toContain('No clusters need attention.')
150
+ })
151
+
152
+ it('uses an unknown sidebar count and tooltip for zero over partial coverage', () => {
153
+ sidebar.data = { installed: true, context: 'kind-test', namespaces: null, coverage: Object.fromEntries(CNPG_WORKSPACE_KEYS.map((k) => [k, { state: 'full' }])), objects: {}, issues: [], audit: [], backupsOmitted: 0 }
154
+ const result = { value: undefined as ReturnType<typeof useCNPGSidebarWorkspace> }
155
+ function Probe() {
156
+ result.value = useCNPGSidebarWorkspace({ apiResources: [{ group: 'postgresql.cnpg.io' }] as any, namespaces: [] })
157
+ return null
158
+ }
159
+ sidebar.data.coverage.clusters = { state: 'partial' }
160
+ renderToStaticMarkup(<MemoryRouter><Probe /></MemoryRouter>)
161
+ for (const d of result.value!.CloudNativePG.destinations.filter((d) => d.id !== 'operator')) {
162
+ expect(d.count).toBeNull()
163
+ expect(d.countLowerBound).toBe(true)
164
+ expect(d.countTitle).toMatch(/^Unknown: some CloudNativePG data could not be read\./)
165
+ expect(d.countTitle).not.toContain('At least 0')
166
+ }
167
+ sidebar.data.coverage.clusters = { state: 'full' }
168
+ renderToStaticMarkup(<MemoryRouter><Probe /></MemoryRouter>)
169
+ expect(result.value!.CloudNativePG.destinations.find((d) => d.id === 'protection')!.count).toBe(0)
170
+ sidebar.data = undefined
171
+ })