@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,23 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { renderCNPGSummary } from './CNPGSummaryHost'
3
+
4
+ const ctx = (apiVersion: string, kind: string) => ({
5
+ apiKind: kind,
6
+ namespace: 'pg',
7
+ name: 'x',
8
+ resource: { apiVersion, kind, metadata: { namespace: 'pg', name: 'x' } },
9
+ context: 'drawer' as const,
10
+ })
11
+
12
+ describe('renderCNPGSummary', () => {
13
+ it('returns null for resources without a CNPG summary, so they keep the default Overview', () => {
14
+ expect(renderCNPGSummary(ctx('v1', 'Pod'))).toBeNull()
15
+ expect(renderCNPGSummary(ctx('apps/v1', 'Deployment'))).toBeNull()
16
+ expect(renderCNPGSummary(ctx('velero.io/v1', 'Backup'))).toBeNull()
17
+ expect(renderCNPGSummary(ctx('cluster.x-k8s.io/v1beta1', 'Cluster'))).toBeNull()
18
+ })
19
+
20
+ it('returns a summary for a CNPG Cluster', () => {
21
+ expect(renderCNPGSummary(ctx('postgresql.cnpg.io/v1', 'Cluster'))).not.toBeNull()
22
+ })
23
+ })
@@ -1,7 +1,8 @@
1
1
  import type { ReactNode } from 'react'
2
- import { useNavigate } from 'react-router-dom'
2
+ import { useLocation, useSearchParams } from 'react-router-dom'
3
3
  import {
4
4
  CNPG_BARMAN_OBJECTSTORE_GROUP,
5
+ OpenIssueContext,
5
6
  CNPG_GROUP,
6
7
  CNPGBackupSummary,
7
8
  CNPGClusterSummary,
@@ -12,17 +13,42 @@ import {
12
13
  CNPGPublicationSummary,
13
14
  CNPGScheduledBackupSummary,
14
15
  CNPGSubscriptionSummary,
16
+ CNPGDatabaseRoleSummary,
17
+ relationUnavailable,
18
+ cnpgLogicalPaths,
19
+ cnpgLogicalSlotFact,
20
+ cnpgSubscriptionHostNamespace,
21
+ FactRow,
22
+ FactSource,
23
+ FactValue,
15
24
  PaneLoader,
16
25
  isApiGroup,
17
26
  refToSelectedResource,
18
- type CNPGNavigate,
19
- type CNPGRef,
27
+ type NavigateToRef,
28
+ type ResourceRef,
29
+ type CNPGLogicalPath,
20
30
  type CNPGWorkspaceResponse,
21
31
  type NavigateToResource,
22
32
  } from '@skyhook-io/k8s-ui'
23
33
  import { useCNPGFleet } from './useCNPGSidebarWorkspace'
24
- import { cnpgClusterFullPath, currentPageLabel } from './paths'
34
+ import { useCNPGRuntime, useCNPGScheduleCapabilities, useCNPGWorkspace, type CNPGRuntimeResponse } from '../../api/cnpg'
35
+ import { cnpgPublisherSlotsFrom, useCNPGPublisherSlots } from './logicalSlots'
36
+ import { cnpgBaseBackupFacts, describeCNPGBaseBackup } from './baseBackup'
37
+ import { useCNPGPoolerLive } from './useCNPGPoolerLive'
38
+ import { useCNPGClusterAssessment } from './useCNPGClusterAssessment'
39
+ import { CNPGMaintenanceBanner } from './actions/CNPGMaintenanceBanner'
40
+ import { CNPG_CONNECT_PARAM, cnpgConnectParamValue } from './actions/CNPGConnectButton'
41
+ import { CNPGOperatorBanner } from './CNPGOperatorBanner'
42
+ import { cnpgClusterFullPath, cnpgDimensionPath, cnpgDimensionTabLabel, cnpgWithinDetail } from './paths'
43
+ import { cnpgScreenPath } from './routes'
44
+ import { currentPageLabel, issuesPathForSubject } from '../../utils/page-links'
45
+ import { CNPGRestoreProgress } from './recovery/CNPGRestoreProgress'
46
+ import { restoreBackupDeclared } from './recovery/restoreModel'
25
47
  import { useConnection } from '../../context/ConnectionContext'
48
+ import { useNavCustomization } from '../../context/NavCustomization'
49
+ import { RefreshFailedNotice } from '../workspace/layout'
50
+ import { useCNPGNavigate } from './useCNPGNavigate'
51
+ import { CNPGOperatorConditions } from './CNPGOperatorConditions'
26
52
 
27
53
  interface SummaryContext {
28
54
  apiKind: string
@@ -34,70 +60,189 @@ interface SummaryContext {
34
60
  onNavigate?: NavigateToResource
35
61
  }
36
62
 
37
- function ClusterSummaryHost({ namespace, name, context, onNavigate }: SummaryContext) {
38
- const navigate = useNavigate()
63
+ function BaseBackupFact({ runtime }: { runtime: CNPGRuntimeResponse | undefined }) {
64
+ const bb = cnpgBaseBackupFacts(runtime)
65
+ if (!bb) return null
66
+ return (
67
+ <FactRow label="Standby cloning">
68
+ {bb.rows.length > 1 ? (
69
+ <ul className="space-y-0.5">
70
+ {bb.rows.map((r) => (
71
+ <li key={r.applicationName}>{describeCNPGBaseBackup(r)}</li>
72
+ ))}
73
+ </ul>
74
+ ) : (
75
+ <FactValue fact={bb.fact} />
76
+ )}
77
+ <FactSource fact={bb.fact} />
78
+ </FactRow>
79
+ )
80
+ }
81
+
82
+ function ClusterSummaryHost({ namespace, name, resource, context, onNavigate }: SummaryContext) {
83
+ const navigate = useCNPGNavigate()
84
+ const location = useLocation()
39
85
  const { connection } = useConnection()
40
86
  // The workspace is read for the object's own namespace: an explicitly opened
41
87
  // Cluster shows its facts whatever the namespace filter is.
42
- const { query, fleet } = useCNPGFleet([namespace])
43
- const row = fleet?.rows.find((r) => r.namespace === namespace && r.name === name)
88
+ const { query, runtime, ha, row, dimensions } = useCNPGClusterAssessment(namespace, name)
89
+ const operatorConditions = context === 'expanded' ? <CNPGOperatorConditions conditions={resource.status?.conditions ?? []} /> : null
44
90
  if (!row) {
45
- if (query.isLoading) return <PaneLoader label="Loading summary…" className="h-40" />
91
+ if (query.isLoading) return <><PaneLoader label="Loading summary…" className="h-40" />{operatorConditions}</>
46
92
  return (
47
- <div className="px-4 py-4 text-sm text-theme-text-secondary">
48
- {query.error instanceof Error
49
- ? `The CloudNativePG summary could not be loaded: ${query.error.message}`
50
- : 'This Cluster is not in the CloudNativePG workspace for your identity.'}{' '}
51
- Spec & status still shows everything the object reports.
52
- </div>
93
+ <>
94
+ <div className="px-4 py-4 text-sm text-theme-text-secondary">
95
+ {query.error instanceof Error
96
+ ? `The CloudNativePG summary could not be loaded: ${query.error.message}`
97
+ : 'The CloudNativePG workspace summary could not be read.'}{' '}
98
+ {context === 'expanded' ? 'Configuration shows the declared settings; Operator conditions are below. YAML holds the full object.' : 'Spec & status still shows the object’s fields.'}
99
+ </div>
100
+ {operatorConditions}
101
+ </>
53
102
  )
54
103
  }
55
- const go = onNavigate ? (ref: CNPGRef) => onNavigate(refToSelectedResource(ref)) : undefined
104
+ const goRef = onNavigate ? (ref: ResourceRef) => onNavigate(refToSelectedResource(ref)) : undefined
105
+ // On the Cluster's own page this is a tab change, applied like a tab click;
106
+ // anywhere else (the drawer, another page) it is a push to the full page
107
+ // with a return label.
108
+ const go = (path: string) => {
109
+ const inPlace = context === 'expanded' ? cnpgWithinDetail(location.pathname, location.search, path) : null
110
+ if (inPlace) navigate(inPlace, { replace: true, state: location.state })
111
+ else navigate(path, { state: { returnLabel: currentPageLabel(), returnCtx: connection.context } })
112
+ }
56
113
  return (
57
- <CNPGClusterSummary
58
- row={row}
59
- onNavigate={go}
60
- actions={
61
- context === 'drawer'
62
- ? [
63
- {
64
- label: 'Open cluster',
65
- primary: true,
66
- onClick: () =>
67
- navigate(cnpgClusterFullPath(namespace, name, connection.context || undefined), {
68
- state: { returnLabel: currentPageLabel(), returnCtx: connection.context },
69
- }),
70
- },
71
- {
72
- label: 'Logs',
73
- onClick: () =>
74
- navigate(cnpgClusterFullPath(namespace, name, connection.context || undefined, 'logs'), {
75
- state: { returnLabel: currentPageLabel(), returnCtx: connection.context },
76
- }),
77
- },
78
- {
79
- label: 'Protection',
80
- onClick: () =>
81
- navigate(cnpgClusterFullPath(namespace, name, connection.context || undefined, 'protection'), {
82
- state: { returnLabel: currentPageLabel(), returnCtx: connection.context },
83
- }),
84
- },
85
- ]
86
- : undefined
87
- }
88
- />
114
+ <div>
115
+ <CNPGClusterSummary
116
+ row={row}
117
+ framed={context === 'expanded'}
118
+ onNavigate={goRef}
119
+ lead={
120
+ <>
121
+ <RefreshFailedNotice queries={[runtime, ha]} />
122
+ {context === 'drawer' && <CNPGOperatorBanner namespaces={[namespace]} />}
123
+ <CNPGMaintenanceBanner namespace={namespace} name={name} maintenance={ha.data?.maintenance} />
124
+ {row.cluster?.spec?.bootstrap?.recovery && (
125
+ <CNPGRestoreProgress
126
+ namespace={namespace}
127
+ name={name}
128
+ nextSteps={{
129
+ backup: row.cluster ? restoreBackupDeclared(row.cluster) : undefined,
130
+ onOpen: (step) => {
131
+ if (step === 'connect') {
132
+ const p = new URLSearchParams(location.search)
133
+ p.set(CNPG_CONNECT_PARAM, cnpgConnectParamValue(namespace, name, context === 'drawer' ? 'drawer' : 'page'))
134
+ navigate({ search: p.toString() }, { replace: true, state: location.state })
135
+ return
136
+ }
137
+ const protection = cnpgClusterFullPath(namespace, name, connection.context || undefined, 'backups')
138
+ go(step === 'validate' ? `${protection}&validate=1` : step === 'backup' ? `${protection}&protectionSetup=1` : protection)
139
+ },
140
+ }}
141
+ />
142
+ )}
143
+ </>
144
+ }
145
+ onSelectDimension={(id) => go(cnpgDimensionPath(namespace, name, connection.context || undefined, id))}
146
+ dimensionLinkLabel={cnpgDimensionTabLabel}
147
+ onOpenOperator={() => navigate(cnpgScreenPath('operator'), { state: { returnLabel: currentPageLabel(), returnCtx: connection.context } })}
148
+ initialProblemsExpanded={context === 'expanded' && new URLSearchParams(location.search).get('problems') === 'all'}
149
+ dimensions={dimensions}
150
+ operationalFacts={<BaseBackupFact runtime={runtime.data} />}
151
+ actions={
152
+ context === 'drawer'
153
+ ? [
154
+ {
155
+ label: 'Open cluster',
156
+ primary: true,
157
+ onClick: () =>
158
+ navigate(cnpgClusterFullPath(namespace, name, connection.context || undefined), {
159
+ state: { returnLabel: currentPageLabel(), returnCtx: connection.context },
160
+ }),
161
+ },
162
+ {
163
+ label: 'Logs',
164
+ onClick: () =>
165
+ navigate(cnpgClusterFullPath(namespace, name, connection.context || undefined, 'logs'), {
166
+ state: { returnLabel: currentPageLabel(), returnCtx: connection.context },
167
+ }),
168
+ },
169
+ {
170
+ label: 'Backups',
171
+ onClick: () =>
172
+ navigate(cnpgClusterFullPath(namespace, name, connection.context || undefined, 'backups'), {
173
+ state: { returnLabel: currentPageLabel(), returnCtx: connection.context },
174
+ }),
175
+ },
176
+ ]
177
+ : undefined
178
+ }
179
+ />
180
+ {operatorConditions}
181
+ </div>
89
182
  )
90
183
  }
91
184
 
92
- type ObjectSummary = (props: { resource: any; workspace: CNPGWorkspaceResponse | null; onNavigate?: CNPGNavigate }) => ReactNode
185
+ type ObjectSummary = (props: { resource: any; workspace: CNPGWorkspaceResponse | null; onNavigate?: NavigateToRef }) => ReactNode
186
+
187
+ function ScheduledBackupSummaryHost(props: { resource: any; workspace: CNPGWorkspaceResponse | null; onNavigate?: NavigateToRef }) {
188
+ const ns = props.resource?.metadata?.namespace ?? ''
189
+ const name = props.resource?.metadata?.name ?? ''
190
+ const caps = useCNPGScheduleCapabilities(ns, name)
191
+ return <CNPGScheduledBackupSummary {...props} schedulePreview={caps.data?.facts.preview} />
192
+ }
193
+
194
+ // The publisher may live in another namespace than the subscriber: that
195
+ // namespace's Clusters, Publications and Poolers are read too.
196
+ function useLogicalWorkspace(ws: CNPGWorkspaceResponse | null, subscriptions: any[]) {
197
+ const hostNs = [...new Set(subscriptions.map((s) => cnpgSubscriptionHostNamespace(s, ws?.objects.clusters ?? [])).filter((n): n is string => !!n))]
198
+ const extra = ws?.namespaces === null ? [] : hostNs.filter((n) => !(ws?.namespaces ?? []).includes(n))
199
+ const other = useCNPGWorkspace(extra, { enabled: !!ws && extra.length > 0 })
200
+ const merged = (key: 'clusters' | 'publications' | 'poolers') => [...(ws?.objects[key] ?? []), ...(other.data?.installed ? other.data.objects[key] ?? [] : [])]
201
+ const publicationsUnavailable = (ns: string) =>
202
+ ws?.namespaces === null || ws?.namespaces?.includes(ns)
203
+ ? relationUnavailable(ws, 'publications', ns, 'Publications')
204
+ : relationUnavailable(other.data?.installed ? other.data : null, 'publications', ns, 'Publications')
205
+ return { clusters: merged('clusters'), publications: merged('publications'), poolers: merged('poolers'), publicationsUnavailable }
206
+ }
207
+
208
+ function LogicalPathSlot({ path, children }: { path: CNPGLogicalPath; children: (slot: ReturnType<typeof cnpgLogicalSlotFact>, notice: ReactNode) => ReactNode }) {
209
+ const { observed, query } = useCNPGPublisherSlots(path.publisher)
210
+ return <>{children(cnpgLogicalSlotFact(path, observed), <RefreshFailedNotice queries={[query]} />)}</>
211
+ }
212
+
213
+ function SubscriptionSummaryHost(props: { resource: any; workspace: CNPGWorkspaceResponse | null; onNavigate?: NavigateToRef }) {
214
+ const lw = useLogicalWorkspace(props.workspace, [props.resource])
215
+ const path = props.workspace ? cnpgLogicalPaths([props.resource], lw.clusters, lw.publications, lw.poolers, lw.publicationsUnavailable)[0] : undefined
216
+ if (!path) return <CNPGSubscriptionSummary {...props} />
217
+ return <LogicalPathSlot path={path}>{(slot, notice) => <CNPGSubscriptionSummary {...props} logicalPath={{ path, slot, notice }} />}</LogicalPathSlot>
218
+ }
219
+
220
+ function PublicationSummaryHost(props: { resource: any; workspace: CNPGWorkspaceResponse | null; onNavigate?: NavigateToRef }) {
221
+ // Subscribers are the Subscriptions in view; the publisher's own runtime
222
+ // answers for every slot.
223
+ const pubCluster = props.resource?.spec?.cluster?.name
224
+ const ns = props.resource?.metadata?.namespace ?? ''
225
+ const runtime = useCNPGRuntime(ns, pubCluster ?? '', !!pubCluster)
226
+ const all = useCNPGWorkspace([], { enabled: !!props.workspace })
227
+ const ws = all.data?.installed ? all.data : props.workspace
228
+ const paths = ws
229
+ ? cnpgLogicalPaths(ws.objects.subscriptions ?? [], ws.objects.clusters ?? [], ws.objects.publications ?? [], ws.objects.poolers ?? []).filter(
230
+ (p) => p.publication.object?.namespace === ns && p.publication.object?.name === props.resource?.metadata?.name,
231
+ )
232
+ : []
233
+ const observed = cnpgPublisherSlotsFrom(runtime.data, runtime.error, runtime.isRefetchError)
234
+ const notice = <RefreshFailedNotice queries={[runtime]} />
235
+ return <CNPGPublicationSummary {...props} subscribers={paths.map((path) => ({ path, slot: cnpgLogicalSlotFact(path, observed), notice }))} />
236
+ }
93
237
 
94
238
  const OBJECT_SUMMARIES: Record<string, ObjectSummary> = {
95
239
  Backup: CNPGBackupSummary,
96
- ScheduledBackup: CNPGScheduledBackupSummary,
240
+ ScheduledBackup: ScheduledBackupSummaryHost,
97
241
  Pooler: CNPGPoolerSummary,
98
242
  Database: CNPGDatabaseSummary,
99
- Publication: CNPGPublicationSummary,
100
- Subscription: CNPGSubscriptionSummary,
243
+ Publication: PublicationSummaryHost,
244
+ Subscription: SubscriptionSummaryHost,
245
+ DatabaseRole: CNPGDatabaseRoleSummary,
101
246
  ImageCatalog: CNPGImageCatalogSummary,
102
247
  ClusterImageCatalog: CNPGImageCatalogSummary,
103
248
  }
@@ -109,10 +254,19 @@ function ObjectSummaryHost({ ctx, Summary }: { ctx: SummaryContext; Summary: Obj
109
254
  const { query } = useCNPGFleet(clusterScoped ? [] : [ctx.namespace])
110
255
  if (query.isLoading) return <PaneLoader label="Loading summary…" className="h-40" />
111
256
  const workspace = query.data?.installed ? query.data : null
112
- const go = ctx.onNavigate ? (ref: CNPGRef) => ctx.onNavigate?.(refToSelectedResource(ref)) : undefined
257
+ const go = ctx.onNavigate ? (ref: ResourceRef) => ctx.onNavigate?.(refToSelectedResource(ref)) : undefined
113
258
  return <Summary resource={ctx.resource} workspace={workspace} onNavigate={go} />
114
259
  }
115
260
 
261
+ function PoolerSummaryHost({ ctx }: { ctx: SummaryContext }) {
262
+ const { query } = useCNPGFleet([ctx.namespace])
263
+ const { live, queries } = useCNPGPoolerLive(ctx.namespace, ctx.name)
264
+ if (query.isLoading) return <PaneLoader label="Loading summary…" className="h-40" />
265
+ const workspace = query.data?.installed ? query.data : null
266
+ const go = ctx.onNavigate ? (ref: ResourceRef) => ctx.onNavigate?.(refToSelectedResource(ref)) : undefined
267
+ return <CNPGPoolerSummary resource={ctx.resource} workspace={workspace} onNavigate={go} live={live} lead={<RefreshFailedNotice queries={queries} />} />
268
+ }
269
+
116
270
  // The object's own apiVersion decides: Velero also ships a Backup kind.
117
271
  function groupOf(ctx: SummaryContext): string | undefined {
118
272
  const apiVersion = ctx.resource?.apiVersion
@@ -130,6 +284,7 @@ function renderSummaryFor(ctx: SummaryContext): ReactNode {
130
284
  }
131
285
  if (group !== CNPG_GROUP) return null
132
286
  if (kind === 'Cluster') return <ClusterSummaryHost {...ctx} />
287
+ if (kind === 'Pooler') return <PoolerSummaryHost ctx={ctx} />
133
288
  const Summary = OBJECT_SUMMARIES[kind]
134
289
  return Summary ? <ObjectSummaryHost ctx={ctx} Summary={Summary} /> : null
135
290
  }
@@ -139,5 +294,24 @@ function renderSummaryFor(ctx: SummaryContext): ReactNode {
139
294
  * without one, which keeps the default Overview.
140
295
  */
141
296
  export function renderCNPGSummary(ctx: SummaryContext): ReactNode {
142
- return renderSummaryFor(ctx)
297
+ const summary = renderSummaryFor(ctx)
298
+ return summary ? <IssueLinks>{summary}</IssueLinks> : null
299
+ }
300
+
301
+ // "See in Issues →" on every CNPG problem: the Issues page narrowed to the
302
+ // problem's subject (it has no link to a single issue). Omitted where the host
303
+ // takes Issues over, since its page does not read the subject.
304
+ function IssueLinks({ children }: { children: ReactNode }) {
305
+ const navigate = useCNPGNavigate()
306
+ const [searchParams] = useSearchParams()
307
+ const issuesTakenOver = !!useNavCustomization().fleetTakeoverHref?.('issues')
308
+ return (
309
+ <OpenIssueContext.Provider
310
+ value={issuesTakenOver ? undefined : (p) =>
311
+ navigate(issuesPathForSubject(p.subject, searchParams.get('namespaces')))
312
+ }
313
+ >
314
+ {children}
315
+ </OpenIssueContext.Provider>
316
+ )
143
317
  }
@@ -0,0 +1,33 @@
1
+ import { renderToStaticMarkup } from 'react-dom/server'
2
+ import { MemoryRouter } from 'react-router-dom'
3
+ import { describe, expect, it, vi } from 'vitest'
4
+
5
+ vi.mock('../../api/cnpg-history', async (orig) => ({
6
+ ...(await orig<typeof import('../../api/cnpg-history')>()),
7
+ useCNPGClusterHistory: () => ({ data: { source: 'none', reason: 'Radar is not connected to Prometheus' }, isLoading: false, error: null }),
8
+ }))
9
+
10
+ const { CNPGTrends } = await import('./CNPGTrends')
11
+
12
+ const render = (samplingDenied?: string) =>
13
+ renderToStaticMarkup(
14
+ <MemoryRouter>
15
+ <CNPGTrends namespace="db" name="pg" samples={[]} samplingDenied={samplingDenied} />
16
+ </MemoryRouter>,
17
+ )
18
+
19
+ describe('CNPGTrends without Prometheus', () => {
20
+ it('does not wait for samples that can never arrive when the proxy is denied', () => {
21
+ const html = render('get pods/proxy in db')
22
+ expect(html).toContain('History needs Prometheus')
23
+ expect(html).toContain('In-page samples need get pods/proxy in db too.')
24
+ expect(html).not.toContain('Collecting samples')
25
+ expect(html).not.toContain('since this page opened')
26
+ expect(html).not.toContain('Drag across a chart')
27
+ expect(html).toContain('History needs Prometheus. Radar is not connected to Prometheus.')
28
+ })
29
+ it('samples in the page when the proxy is allowed', () => {
30
+ const html = render()
31
+ expect(html).toContain('Collecting samples')
32
+ })
33
+ })
@@ -0,0 +1,69 @@
1
+ import { renderToStaticMarkup } from 'react-dom/server'
2
+ import { MemoryRouter } from 'react-router-dom'
3
+ import { describe, expect, it, vi } from 'vitest'
4
+
5
+ const lag = (series: string[], state = 'ok') => ({
6
+ id: 'replicationLag',
7
+ title: 'Replay lag per standby',
8
+ unit: 'seconds',
9
+ source: 'cnpg_pg_replication_lag',
10
+ seriesBy: 'pod',
11
+ state,
12
+ reason: state === 'empty' ? 'no instance was a standby in this range' : undefined,
13
+ series: series.map((pod) => ({
14
+ labels: { pod },
15
+ dataPoints: [
16
+ { timestamp: 60, value: 0 },
17
+ { timestamp: 120, value: 0 },
18
+ ],
19
+ })),
20
+ steps: 2,
21
+ covered: 2,
22
+ })
23
+
24
+ let chart = lag(['pg-2'])
25
+ vi.mock('../../api/cnpg-history', async (orig) => ({
26
+ ...(await orig<typeof import('../../api/cnpg-history')>()),
27
+ useCNPGClusterHistory: () => ({
28
+ data: {
29
+ source: 'prometheus',
30
+ state: 'ok',
31
+ range: '1h',
32
+ start: '1970-01-01T00:01:00Z',
33
+ end: '1970-01-01T00:02:00Z',
34
+ stepSeconds: 60,
35
+ sampledAt: '',
36
+ cluster: { namespace: 'db', name: 'pg', uid: 'u' },
37
+ charts: [chart],
38
+ },
39
+ isLoading: false,
40
+ error: null,
41
+ }),
42
+ }))
43
+
44
+ const { CNPGTrends } = await import('./CNPGTrends')
45
+
46
+ const render = (standbys?: string[]) =>
47
+ renderToStaticMarkup(
48
+ <MemoryRouter>
49
+ <CNPGTrends namespace="db" name="pg" samples={[]} standbys={standbys} instancePods={['pg-1', 'pg-2', 'pg-3']} />
50
+ </MemoryRouter>,
51
+ )
52
+
53
+ describe('replay-lag history with a standby that does not report', () => {
54
+ it('names the standby instead of drawing only the calm one', () => {
55
+ chart = lag(['pg-2'])
56
+ expect(render(['pg-1', 'pg-2'])).toContain('No line for pg-1: Prometheus has no samples from it in this range.')
57
+ })
58
+ it('says nothing when every standby has a line, or when the standbys are unknown', () => {
59
+ chart = lag(['pg-2'])
60
+ expect(render(['pg-2'])).not.toContain('No line for')
61
+ expect(render()).not.toContain('No line for')
62
+ })
63
+ it('replaces "no instance was a standby" when standbys exist but none reported', () => {
64
+ chart = lag([], 'empty')
65
+ const html = render(['pg-1', 'pg-2'])
66
+ expect(html).toContain('No line for pg-1, pg-2: Prometheus has no samples from them in this range.')
67
+ expect(html).not.toContain('no instance was a standby')
68
+ })
69
+ })
@@ -0,0 +1,38 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { historyGaps, sampleGaps, unchartedInstances } from './CNPGTrends'
3
+
4
+ const s = (points: [number, number | null][], label = 'a') => ({ labels: { pod: label }, dataPoints: points.map(([timestamp, value]) => ({ timestamp, value })) })
5
+
6
+ describe('historyGaps', () => {
7
+ it('hatches steps no series covers and says a leading run predates the series', () => {
8
+ const gaps = historyGaps([s([[120, 1], [150, 2]]), s([[240, 1]], 'b')], 60, 240, 30)
9
+ expect(gaps).toEqual([
10
+ { start: 60, end: 105, label: expect.stringContaining('before this series existed') },
11
+ { start: 165, end: 225, label: expect.stringContaining('not scraped') },
12
+ ])
13
+ })
14
+
15
+ it('reports nothing when every step has a sample', () => {
16
+ expect(historyGaps([s([[0, 1], [30, 0], [60, 3]])], 0, 60, 30)).toEqual([])
17
+ })
18
+ })
19
+
20
+ describe('sampleGaps', () => {
21
+ it('spans from the last sample before a null run to the first after it', () => {
22
+ const gaps = sampleGaps([s([[0, 1], [5, null], [10, null], [15, 4]])])
23
+ expect(gaps).toHaveLength(1)
24
+ expect(gaps[0]).toMatchObject({ start: 0, end: 15 })
25
+ })
26
+ })
27
+
28
+
29
+ describe('unchartedInstances', () => {
30
+ it('names the expected instances a chart has no line for', () => {
31
+ expect(unchartedInstances(['pg-1', 'pg-2'], ['pg-2'])).toEqual(['pg-1'])
32
+ expect(unchartedInstances(['pg-1'], ['pg-1'])).toEqual([])
33
+ })
34
+ it('claims nothing when the expected set is unknown', () => {
35
+ expect(unchartedInstances(undefined, [])).toEqual([])
36
+ expect(unchartedInstances([], ['pg-1'])).toEqual([])
37
+ })
38
+ })