@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,5 +1,8 @@
1
+ // @vitest-environment jsdom
1
2
  import { describe, expect, it, vi } from "vitest";
2
3
  import { renderToString } from "react-dom/server";
4
+ import { act } from "react";
5
+ import { createRoot } from "react-dom/client";
3
6
  import { MemoryRouter } from "react-router-dom";
4
7
  import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
5
8
  import type {
@@ -24,7 +27,7 @@ import type {
24
27
  } from "@skyhook-io/k8s-ui";
25
28
  import { CapacityView } from "./CapacityView";
26
29
  import { updateDemandSearchParam } from "./CapacityDemand";
27
- import { integrationBlock } from "./shared";
30
+ import { integrationBlock, NodeReadyBadge } from "./shared";
28
31
  import { ApiError } from "../../api/client";
29
32
  import { RadarFeatureUnsupportedError } from "../../api/radarFeatures";
30
33
 
@@ -1337,6 +1340,326 @@ describe("CapacityView overview", () => {
1337
1340
  });
1338
1341
  });
1339
1342
 
1343
+ describe("Capacity scale-down observations", () => {
1344
+ const noCandidatesExplanation =
1345
+ "The autoscaler found no node it can remove. Usually every node is above its utilization threshold, or pods on emptier nodes can't move (PodDisruptionBudgets, local storage, kube-system pods, safe-to-evict annotations). A node's drain plan shows what would block it.";
1346
+ const candidateExplanation =
1347
+ "Scale-down candidates are observations, not guaranteed removals. PodDisruptionBudgets, local storage, kube-system pods, or safe-to-evict annotations can block removal. Radar can't tell which nodes the autoscaler picked, but a node's drain plan shows what would block removing it.";
1348
+
1349
+ function renderScaleDown(child: CapacityAutoscalerChildObservation) {
1350
+ return renderCapacity("/capacity", (client) =>
1351
+ client.setQueryData(["capacity", "overview"], {
1352
+ ...comprehensiveOverview(),
1353
+ groups: [
1354
+ {
1355
+ ...gkeGroup,
1356
+ scaling: [
1357
+ {
1358
+ code: "at_min_size",
1359
+ summary: "at minimum size — can't scale down",
1360
+ },
1361
+ ],
1362
+ children: [child],
1363
+ childrenMeta: boundedMeta(1),
1364
+ },
1365
+ ],
1366
+ orphanAutoscalerGroups: [child],
1367
+ }),
1368
+ );
1369
+ }
1370
+
1371
+ it("explains the floor and keeps published zero candidates", () => {
1372
+ const clock = vi.spyOn(Date, "now").mockReturnValue(Date.parse(generatedAt) + 6 * 60 * 1000);
1373
+ const html = renderScaleDown({
1374
+ ...gkeChild,
1375
+ scaleDown: {
1376
+ status: "NoCandidates",
1377
+ candidates: 0,
1378
+ lastTransitionTime: generatedAt,
1379
+ asOf: generatedAt,
1380
+ },
1381
+ });
1382
+ clock.mockRestore();
1383
+ expect(html).toContain("at minimum size");
1384
+ expect(html).toContain(
1385
+ "Every observed autoscaler group has its target at its published minimum.",
1386
+ );
1387
+ expect(html).toContain(
1388
+ "Lowering the pool's minimum is required for it to shrink below this size.",
1389
+ );
1390
+ expect(html).not.toContain("No node can be removed right now");
1391
+ expect(html).toContain(noCandidatesExplanation.replaceAll("'", "'"));
1392
+ expect(html).not.toContain("autoscaler child");
1393
+ expect(html).toContain("No scale-down candidates");
1394
+ expect(html).toContain("Candidate count published by the autoscaler: 0.");
1395
+ expect(html).not.toContain("Candidates:");
1396
+ expect(html).toContain("Status since");
1397
+ expect(html).toContain("(for 6m)");
1398
+ expect(html).not.toContain("As of");
1399
+ expect(html).toContain("PodDisruptionBudgets");
1400
+ expect(html).not.toContain("ETA");
1401
+ });
1402
+
1403
+ it("explains no candidates above the minimum on the collapsed pool and both child tables", () => {
1404
+ const child: CapacityAutoscalerChildObservation = {
1405
+ ...gkeChild,
1406
+ scaleDown: { status: "NoCandidates", candidates: 0 },
1407
+ };
1408
+ const html = renderCapacity("/capacity", (client) =>
1409
+ client.setQueryData(["capacity", "overview"], {
1410
+ ...comprehensiveOverview(),
1411
+ groups: [{ ...gkeGroup, children: [child], childrenMeta: boundedMeta(1) }],
1412
+ orphanAutoscalerGroups: [child],
1413
+ }),
1414
+ );
1415
+ expect(html).toContain('aria-expanded="false"');
1416
+ const explanation = noCandidatesExplanation.replaceAll("'", "'");
1417
+ expect(html).toContain(
1418
+ `tabindex="0" role="note" aria-label="No node can be removed right now. ${explanation}"`,
1419
+ );
1420
+ expect(html.split(`No scale-down candidates. ${explanation}`)).toHaveLength(3);
1421
+ expect(html).not.toContain("Scale-down candidates are observations");
1422
+ });
1423
+
1424
+ it.each([
1425
+ ["missing count", { scaleDown: { status: "NoCandidates" } }, false],
1426
+ ["missing observation", { scaleDown: undefined }, false],
1427
+ ["candidates present", { scaleDown: { status: "CandidatesPresent", candidates: 1 } }, false],
1428
+ ["missing minimum", { minSize: undefined }, false],
1429
+ ["missing target", { target: undefined }, false],
1430
+ ["below the minimum", { target: -1 }, false],
1431
+ ["truncated children", {}, true],
1432
+ ])("does not assert no removable nodes with %s", (_label, override, truncated) => {
1433
+ const html = renderCapacity("/capacity", (client) =>
1434
+ client.setQueryData(["capacity", "overview"], {
1435
+ ...comprehensiveOverview(),
1436
+ groups: [{
1437
+ ...gkeGroup,
1438
+ children: [
1439
+ { ...gkeChild, scaleDown: { status: "NoCandidates", candidates: 0 } },
1440
+ { ...gkeChildBackoff, scaleDown: { status: "NoCandidates", candidates: 0 }, ...override },
1441
+ ],
1442
+ childrenMeta: { total: truncated ? 3 : 2, returned: 2, truncated },
1443
+ }],
1444
+ }),
1445
+ );
1446
+ expect(html).not.toContain("No node can be removed right now");
1447
+ });
1448
+
1449
+ it("keeps scale-down probe freshness separate from health in both tables", () => {
1450
+ const clock = vi
1451
+ .spyOn(Date, "now")
1452
+ .mockReturnValue(Date.parse(generatedAt) + 6 * 60 * 1000);
1453
+ try {
1454
+ const html = renderScaleDown({
1455
+ ...gkeChild,
1456
+ scaleDown: {
1457
+ status: "InProgress",
1458
+ asOf: "2026-07-13T07:06:00Z",
1459
+ },
1460
+ });
1461
+ expect(html.match(/Health observed<\/th>/g)).toHaveLength(2);
1462
+ expect(html.match(/Probe: <span[^>]*>1h ago<\/span>/g)).toHaveLength(2);
1463
+ expect(html.match(/>6m ago<\/span><\/td>/g)).toHaveLength(2);
1464
+ } finally {
1465
+ clock.mockRestore();
1466
+ }
1467
+ });
1468
+
1469
+ it("shows an absent scale-down probe independently of published health", () => {
1470
+ const html = renderScaleDown({
1471
+ ...gkeChild,
1472
+ scaleDown: { status: "InProgress" },
1473
+ });
1474
+ expect(html.match(/Probe: <span[^>]*>not probed<\/span>/g)).toHaveLength(2);
1475
+ });
1476
+
1477
+ it.each([
1478
+ "InProgress",
1479
+ "CandidatesPresent",
1480
+ "NoCandidates",
1481
+ "NewControllerStatus",
1482
+ ])(
1483
+ "makes the %s removal caveat keyboard reachable without a count glyph",
1484
+ (status) => {
1485
+ const html = renderScaleDown({
1486
+ ...gkeChild,
1487
+ scaleDown: { status },
1488
+ });
1489
+ expect(
1490
+ html.match(
1491
+ /tabindex="0" role="note" aria-label="[^"]*PodDisruptionBudgets[^"]*"/g,
1492
+ ),
1493
+ ).toHaveLength(2);
1494
+ },
1495
+ );
1496
+
1497
+ it("never invents a zero count from NoCandidates or missing observations", () => {
1498
+ const html = renderScaleDown({
1499
+ ...gkeChild,
1500
+ scaleDown: { status: "NoCandidates" },
1501
+ });
1502
+ expect(html).toMatch(/Candidates:(?:<!-- -->|\s)*not published/);
1503
+ expect(html).toContain("Candidate count not published by the autoscaler.");
1504
+ expect(html).not.toContain("Probe time not published");
1505
+ expect(html).not.toContain("Status since");
1506
+ expect(html).not.toMatch(/Candidates:(?:<!-- -->|\s)*0/);
1507
+ const older = renderScaleDown(gkeChild);
1508
+ expect(older).not.toContain("Scale-down</th>");
1509
+ expect(older).not.toContain("Not published");
1510
+ expect(older).not.toContain("PodDisruptionBudgets");
1511
+ });
1512
+
1513
+ it("keeps a status-only CandidatesPresent count unknown", () => {
1514
+ const html = renderScaleDown({
1515
+ ...gkeChild,
1516
+ scaleDown: { status: "CandidatesPresent" },
1517
+ });
1518
+ expect(html).toContain("Scale-down candidates present");
1519
+ expect(html).toContain("Candidates: not published");
1520
+ expect(html).not.toContain("0 scale-down candidates");
1521
+ });
1522
+
1523
+ it.each(["joined", "orphan"])(
1524
+ "shows both tables when only a %s child has scale-down data",
1525
+ (source) => {
1526
+ const withScaleDown = {
1527
+ ...gkeChild,
1528
+ scaleDown: { status: "NoCandidates", candidates: 0 },
1529
+ };
1530
+ const html = renderCapacity("/capacity", (client) =>
1531
+ client.setQueryData(["capacity", "overview"], {
1532
+ ...comprehensiveOverview(),
1533
+ groups: [
1534
+ {
1535
+ ...gkeGroup,
1536
+ children: [source === "joined" ? withScaleDown : gkeChild],
1537
+ },
1538
+ ],
1539
+ orphanAutoscalerGroups: [
1540
+ source === "orphan" ? withScaleDown : gkeChild,
1541
+ ],
1542
+ }),
1543
+ );
1544
+ expect(html.match(/Scale-down<\/th>/g)).toHaveLength(2);
1545
+ expect(html).toContain("Not published");
1546
+ expect(html).toContain("No scale-down candidates");
1547
+ },
1548
+ );
1549
+
1550
+ it("opens the removal caveat on status focus and dismisses it on blur", async () => {
1551
+ const client = new QueryClient({
1552
+ defaultOptions: { queries: { retry: false, staleTime: Infinity } },
1553
+ });
1554
+ client.setQueryData(["capacity", "overview"], {
1555
+ ...comprehensiveOverview(),
1556
+ groups: [],
1557
+ orphanAutoscalerGroups: [
1558
+ {
1559
+ ...gkeChild,
1560
+ scaleDown: { status: "InProgress", candidates: 2 },
1561
+ },
1562
+ ],
1563
+ });
1564
+ const container = document.createElement("div");
1565
+ document.body.append(container);
1566
+ const root = createRoot(container);
1567
+ vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
1568
+ vi.useFakeTimers();
1569
+ try {
1570
+ await act(async () => {
1571
+ root.render(
1572
+ <MemoryRouter initialEntries={["/capacity"]}>
1573
+ <QueryClientProvider client={client}>
1574
+ <CapacityView onOpenResource={() => {}} />
1575
+ </QueryClientProvider>
1576
+ </MemoryRouter>,
1577
+ );
1578
+ });
1579
+ const trigger = container.querySelector<HTMLElement>(
1580
+ '[role="note"][aria-label^="Scale-down in progress."]',
1581
+ );
1582
+ expect(trigger).not.toBeNull();
1583
+ expect(trigger!.textContent).toBe("Scale-down in progress");
1584
+ expect(trigger!.querySelector('[tabindex="0"]')).toBeNull();
1585
+ await act(async () => {
1586
+ trigger!.focus();
1587
+ await vi.advanceTimersByTimeAsync(400);
1588
+ });
1589
+ expect(document.activeElement).toBe(trigger);
1590
+ expect(document.querySelector('[role="tooltip"]')?.textContent).toContain(
1591
+ "PodDisruptionBudgets",
1592
+ );
1593
+ await act(async () => {
1594
+ trigger!.blur();
1595
+ });
1596
+ expect(document.querySelector('[role="tooltip"]')).toBeNull();
1597
+ } finally {
1598
+ await act(async () => {
1599
+ root.unmount();
1600
+ });
1601
+ client.clear();
1602
+ container.remove();
1603
+ vi.useRealTimers();
1604
+ vi.unstubAllGlobals();
1605
+ }
1606
+ });
1607
+
1608
+ it("renders the server candidate fact on the collapsed pool", () => {
1609
+ const html = renderCapacity("/capacity", (client) =>
1610
+ client.setQueryData(["capacity", "overview"], {
1611
+ ...comprehensiveOverview(),
1612
+ groups: [
1613
+ {
1614
+ ...gkeGroup,
1615
+ scaling: [
1616
+ {
1617
+ code: "scale_down_candidates",
1618
+ summary: "3 scale-down candidates",
1619
+ },
1620
+ ],
1621
+ },
1622
+ ],
1623
+ }),
1624
+ );
1625
+ expect(html).toContain("3 scale-down candidates");
1626
+ expect(html).toMatch(/tabindex="0" role="note" aria-label="3 scale-down candidates\. [^"]*PodDisruptionBudgets/);
1627
+ expect(html).toContain(candidateExplanation.replaceAll("'", "&#x27;"));
1628
+ expect(html).not.toContain("scale-down delay");
1629
+ expect(html).not.toContain("Radar can&#x27;t see those blockers");
1630
+ expect(html).toContain('aria-expanded="false"');
1631
+ });
1632
+
1633
+ it.each([
1634
+ ["CandidatesPresent", "2 scale-down candidates"],
1635
+ ["InProgress", "Scale-down in progress"],
1636
+ ["NewControllerStatus", "NewControllerStatus"],
1637
+ ["constructor", "constructor"],
1638
+ ])("renders %s without promising a removal time", (status, label) => {
1639
+ const html = renderScaleDown({
1640
+ ...gkeChild,
1641
+ scaleDown: { status, candidates: 2 },
1642
+ });
1643
+ expect(html).toContain(label);
1644
+ expect(html).toContain("2 scale-down candidates");
1645
+ expect(html).not.toContain("unneeded");
1646
+ expect(html).not.toContain("Candidates:");
1647
+ expect(html).not.toContain("Status since");
1648
+ });
1649
+
1650
+ it("names a single published candidate the same way the pool row does", () => {
1651
+ const html = renderScaleDown({
1652
+ ...gkeChild,
1653
+ scaleDown: { status: "CandidatesPresent", candidates: 1 },
1654
+ });
1655
+ expect(html).toMatch(
1656
+ /role="note" aria-label="1 scale-down candidate\. Scale-down candidates are observations/,
1657
+ );
1658
+ expect(html).toContain(candidateExplanation.replaceAll("'", "&#x27;"));
1659
+ expect(html).not.toContain("1 scale-down candidates");
1660
+ });
1661
+ });
1662
+
1340
1663
  describe("CapacityView pool detail", () => {
1341
1664
  it("renders the capacity ledger with headers and a certainty legend", () => {
1342
1665
  const html = renderCapacity("/capacity/pools/default", (client) =>
@@ -2315,3 +2638,33 @@ describe("integrationBlock version skew", () => {
2315
2638
  expect(html).not.toContain("needs a newer Radar");
2316
2639
  });
2317
2640
  });
2641
+
2642
+ describe("Node lifecycle in Capacity", () => {
2643
+ it("uses backend lifecycle while preserving raw readiness", () => {
2644
+ const lifecycle = { label: "Removing (Karpenter)", level: "neutral" as const, removing: true, readinessFailed: false, delayed: false };
2645
+ for (const ready of [true, false, undefined]) {
2646
+ const html = renderToString(<NodeReadyBadge ready={ready} cordoned={false} lifecycle={lifecycle} />);
2647
+ expect(html).toContain("Removing (Karpenter)");
2648
+ expect(html).not.toContain("NotReady");
2649
+ expect(html).not.toContain("text-red-");
2650
+ }
2651
+ const failed = renderToString(<NodeReadyBadge ready={false} cordoned={false} lifecycle={{ ...lifecycle, label: "Removing (Karpenter) · NotReady", level: "unhealthy", readinessFailed: true }} />);
2652
+ expect(failed).toContain("NotReady");
2653
+ expect(failed).toContain("text-red-");
2654
+ const raw = renderToString(<NodeReadyBadge ready={false} cordoned={false} />);
2655
+ expect(raw).toContain("NotReady (condition)");
2656
+ expect(raw).toContain("text-red-");
2657
+ });
2658
+ it("renders exclusive operational counts, not overlapping raw counts", () => {
2659
+ const html = renderCapacity("/capacity/pools/default", (client) => client.setQueryData(
2660
+ ["capacity", "pool", "default"], poolDetailResponse({ ...cleanPoolDetail, nodes: {
2661
+ total: 6, ready: 4, notReady: 2, cordoned: 1, terminating: 2,
2662
+ operational: { total: 6, ready: 2, notReady: 1, cordoned: 1, removing: 2, removingUnhealthy: 1 },
2663
+ } }),
2664
+ ));
2665
+ expect(html).toMatch(/>2<\/div><div[^>]*>Ready<\/div>/);
2666
+ expect(html).toMatch(/>1<\/div><div[^>]*>Not ready<\/div>/);
2667
+ expect(html).toMatch(/>2<\/div><div[^>]*>Removing<\/div>/);
2668
+ expect(html.replace(/<!-- -->/g, "")).toContain("1 removing node has critical pressure or prolonged removal.");
2669
+ });
2670
+ });
@@ -9,16 +9,8 @@ import {
9
9
  parseCPUToNanocores,
10
10
  parseQuantityToNumber,
11
11
  } from "@skyhook-io/k8s-ui/utils/format";
12
- import {
13
- CertaintyGlyph,
14
- LinkButton,
15
- SectionCard,
16
- certaintyGlyph,
17
- certaintyValueLabel,
18
- formatQuantity,
19
- quantityResourceRank,
20
- resourceLabel,
21
- } from "./shared";
12
+ import { LinkButton, SectionCard, formatQuantity, quantityResourceRank, resourceLabel } from "./shared";
13
+ import { CertaintyGlyph, certaintyGlyph, certaintyValueLabel } from "@skyhook-io/k8s-ui";
22
14
 
23
15
  // Karpenter scheduling capacity — the fleet-level "how full is Karpenter, and
24
16
  // how much is knocking at the door" bar. Karpenter-pooled nodes only: on mixed
@@ -4,15 +4,8 @@ import type {
4
4
  CapacityActivityState,
5
5
  CapacityQuantityObservation,
6
6
  } from "@skyhook-io/k8s-ui";
7
- import {
8
- DeniedCapacityState,
9
- ActivityStateBadge,
10
- CertaintyGlyph,
11
- InventoryQuantityCell,
12
- QuantityInline,
13
- certaintyGlyph,
14
- managerStatusRank,
15
- } from "./shared";
7
+ import { DeniedCapacityState, ActivityStateBadge, InventoryQuantityCell, QuantityInline, managerStatusRank } from "./shared";
8
+ import { CertaintyGlyph, certaintyGlyph } from "@skyhook-io/k8s-ui";
16
9
 
17
10
  function observation(
18
11
  certainty: CapacityQuantityObservation["certainty"],
@@ -0,0 +1,42 @@
1
+ export type CapacityTopTab = "overview" | "demand" | "activity";
2
+ export type PoolSection = "summary" | "workloads" | "members" | "configuration";
3
+
4
+ export interface CapacityRoute {
5
+ topTab: CapacityTopTab;
6
+ poolName?: string;
7
+ poolSection: PoolSection;
8
+ }
9
+
10
+ export function parseCapacityRoute(pathname: string): CapacityRoute {
11
+ const segments = pathname.replace(/^\/+|\/+$/g, "").split("/");
12
+ if (segments[0] !== "capacity")
13
+ return { topTab: "overview", poolSection: "summary" };
14
+ if (segments[1] === "pools" && segments[2]) {
15
+ const section = segments[3];
16
+ return {
17
+ topTab: "overview",
18
+ poolName: decodePathSegment(segments[2]),
19
+ poolSection:
20
+ section === "workloads" ||
21
+ section === "members" ||
22
+ section === "configuration"
23
+ ? section
24
+ : "summary",
25
+ };
26
+ }
27
+ if (segments[1] === "demand" || segments[1] === "activity")
28
+ return { topTab: segments[1], poolSection: "summary" };
29
+ return { topTab: "overview", poolSection: "summary" };
30
+ }
31
+
32
+ function decodePathSegment(value: string): string {
33
+ try {
34
+ return decodeURIComponent(value);
35
+ } catch {
36
+ return value;
37
+ }
38
+ }
39
+
40
+ export function capacityPoolPath(name: string): string {
41
+ return `/capacity/pools/${encodeURIComponent(name)}`;
42
+ }