@skyhook-io/radar-app 1.17.0 → 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 (276) hide show
  1. package/README.md +1 -1
  2. package/package.json +3 -3
  3. package/src/App.tsx +125 -87
  4. package/src/api/actions.errorCode.test.ts +40 -0
  5. package/src/api/actions.test.ts +48 -0
  6. package/src/api/actions.ts +79 -0
  7. package/src/api/apiResources.navigation.test.tsx +232 -0
  8. package/src/api/apiResources.ts +46 -3
  9. package/src/api/client.helmSource.test.ts +39 -0
  10. package/src/api/client.namespaceSwitch.hook.test.tsx +54 -0
  11. package/src/api/client.namespaceSwitch.test.ts +107 -0
  12. package/src/api/client.ts +266 -39
  13. package/src/api/cnpg-ha.live.test.ts +140 -0
  14. package/src/api/cnpg-ha.test.ts +78 -0
  15. package/src/api/cnpg-ha.ts +19 -0
  16. package/src/api/cnpg-history.ts +123 -0
  17. package/src/api/cnpg-inspect.ts +102 -0
  18. package/src/api/cnpg-protection.ts +112 -0
  19. package/src/api/cnpg-recovery.test.tsx +60 -0
  20. package/src/api/cnpg-recovery.ts +262 -0
  21. package/src/api/cnpg-sessions.ts +175 -0
  22. package/src/api/cnpg-storage.ts +138 -0
  23. package/src/api/cnpg.legacy-reads.test.ts +43 -0
  24. package/src/api/cnpg.pooler-runtime.test.ts +15 -0
  25. package/src/api/cnpg.ts +428 -10
  26. package/src/api/radarFeatures.render.test.tsx +47 -3
  27. package/src/api/radarFeatures.ts +4 -1
  28. package/src/api/reviewedActionMutation.test.tsx +164 -0
  29. package/src/api/reviewedActionMutation.ts +66 -0
  30. package/src/api/timelineSource.test.ts +9 -0
  31. package/src/api/timelineSource.ts +8 -0
  32. package/src/components/audit/AuditView.tsx +9 -0
  33. package/src/components/capacity/CapacityActivity.tsx +3 -31
  34. package/src/components/capacity/CapacityDemand.tsx +4 -36
  35. package/src/components/capacity/CapacityOverview.tsx +217 -56
  36. package/src/components/capacity/CapacityPoolDetail.tsx +6 -55
  37. package/src/components/capacity/CapacityView.test.tsx +323 -0
  38. package/src/components/capacity/ClusterSchedulingCard.tsx +2 -10
  39. package/src/components/capacity/certaintyGlyph.test.tsx +2 -9
  40. package/src/components/capacity/routes.ts +42 -0
  41. package/src/components/capacity/shared.tsx +11 -129
  42. package/src/components/capacity/workspaceRoute.ts +18 -0
  43. package/src/components/cnpg/CNPGActionHost.compatibility.test.tsx +30 -0
  44. package/src/components/cnpg/CNPGArchivingRepair.tsx +191 -0
  45. package/src/components/cnpg/CNPGBlockingSessions.test.tsx +50 -0
  46. package/src/components/cnpg/CNPGBlockingSessions.tsx +333 -0
  47. package/src/components/cnpg/CNPGClusterActivity.test.tsx +11 -0
  48. package/src/components/cnpg/CNPGClusterActivity.tsx +18 -9
  49. package/src/components/cnpg/CNPGClusterLogs.compatibility.test.tsx +130 -0
  50. package/src/components/cnpg/CNPGClusterLogs.empty.test.tsx +29 -0
  51. package/src/components/cnpg/CNPGClusterLogs.test.ts +9 -0
  52. package/src/components/cnpg/CNPGClusterLogs.tsx +81 -24
  53. package/src/components/cnpg/CNPGClusterTabs.test.tsx +152 -0
  54. package/src/components/cnpg/CNPGClusterTabs.tsx +187 -0
  55. package/src/components/cnpg/CNPGClustersKindView.tsx +46 -0
  56. package/src/components/cnpg/CNPGConfiguration.test.tsx +223 -0
  57. package/src/components/cnpg/CNPGConfiguration.tsx +190 -0
  58. package/src/components/cnpg/CNPGCreateClusterDialog.test.tsx +113 -0
  59. package/src/components/cnpg/CNPGCreateClusterDialog.tsx +74 -0
  60. package/src/components/cnpg/CNPGDeclarations.test.ts +107 -0
  61. package/src/components/cnpg/CNPGDeclarations.tsx +236 -78
  62. package/src/components/cnpg/CNPGDetailPage.test.tsx +62 -0
  63. package/src/components/cnpg/CNPGDetailPage.tsx +159 -98
  64. package/src/components/cnpg/CNPGDrawerTrail.tsx +2 -2
  65. package/src/components/cnpg/CNPGInvestigationAction.test.tsx +30 -0
  66. package/src/components/cnpg/CNPGOperator.test.tsx +34 -0
  67. package/src/components/cnpg/CNPGOperator.tsx +78 -30
  68. package/src/components/cnpg/CNPGOperatorBanner.tsx +39 -0
  69. package/src/components/cnpg/CNPGOperatorConditions.test.tsx +45 -0
  70. package/src/components/cnpg/CNPGOperatorConditions.tsx +16 -0
  71. package/src/components/cnpg/CNPGOperatorConfig.test.tsx +27 -0
  72. package/src/components/cnpg/CNPGOperatorDiagnosis.tsx +247 -0
  73. package/src/components/cnpg/CNPGOverview.test.tsx +171 -0
  74. package/src/components/cnpg/CNPGOverview.tsx +238 -63
  75. package/src/components/cnpg/CNPGParametersInEffect.test.tsx +51 -0
  76. package/src/components/cnpg/CNPGParametersInEffect.tsx +120 -0
  77. package/src/components/cnpg/CNPGPerformance.prerequisite.test.tsx +48 -0
  78. package/src/components/cnpg/CNPGPerformance.test.tsx +73 -0
  79. package/src/components/cnpg/CNPGPerformance.tsx +477 -0
  80. package/src/components/cnpg/CNPGPooling.test.tsx +89 -0
  81. package/src/components/cnpg/CNPGPooling.tsx +97 -40
  82. package/src/components/cnpg/CNPGProtection.test.tsx +145 -0
  83. package/src/components/cnpg/CNPGProtection.tsx +228 -122
  84. package/src/components/cnpg/CNPGReplicationTab.test.tsx +87 -0
  85. package/src/components/cnpg/CNPGReplicationTab.tsx +212 -0
  86. package/src/components/cnpg/CNPGReplicationView.tsx +264 -0
  87. package/src/components/cnpg/CNPGStorage.test.tsx +140 -0
  88. package/src/components/cnpg/CNPGStorage.tsx +619 -0
  89. package/src/components/cnpg/CNPGSummaryHost.overview.test.tsx +44 -0
  90. package/src/components/cnpg/CNPGSummaryHost.test.tsx +23 -0
  91. package/src/components/cnpg/CNPGSummaryHost.tsx +228 -54
  92. package/src/components/cnpg/CNPGTrends.denied.test.tsx +33 -0
  93. package/src/components/cnpg/CNPGTrends.missing.test.tsx +69 -0
  94. package/src/components/cnpg/CNPGTrends.test.ts +38 -0
  95. package/src/components/cnpg/CNPGTrends.tsx +702 -0
  96. package/src/components/cnpg/CNPGView.tsx +34 -21
  97. package/src/components/cnpg/CNPGWorkspace.compatibility.test.tsx +90 -0
  98. package/src/components/cnpg/actions/CNPGClusterActions.dialog.test.tsx +79 -0
  99. package/src/components/cnpg/actions/CNPGClusterActions.test.tsx +142 -0
  100. package/src/components/cnpg/actions/CNPGClusterActions.tsx +853 -0
  101. package/src/components/cnpg/actions/CNPGConnectButton.read.test.tsx +40 -0
  102. package/src/components/cnpg/actions/CNPGConnectButton.test.ts +25 -0
  103. package/src/components/cnpg/actions/CNPGConnectButton.tsx +131 -0
  104. package/src/components/cnpg/actions/CNPGDestroyInstanceDialog.tsx +185 -0
  105. package/src/components/cnpg/actions/CNPGInstanceActions.test.tsx +38 -0
  106. package/src/components/cnpg/actions/CNPGInstanceActions.tsx +74 -0
  107. package/src/components/cnpg/actions/CNPGMaintenanceBanner.tsx +53 -0
  108. package/src/components/cnpg/actions/CNPGPoolerActions.test.tsx +35 -0
  109. package/src/components/cnpg/actions/CNPGPoolerActions.tsx +79 -0
  110. package/src/components/cnpg/actions/CNPGReportDialog.test.tsx +12 -0
  111. package/src/components/cnpg/actions/CNPGReportDialog.tsx +91 -0
  112. package/src/components/cnpg/actions/CNPGScheduleActions.run.test.tsx +58 -0
  113. package/src/components/cnpg/actions/CNPGScheduleActions.test.tsx +54 -0
  114. package/src/components/cnpg/actions/CNPGScheduleActions.tsx +208 -0
  115. package/src/components/cnpg/actions/actionModel.test.ts +111 -0
  116. package/src/components/cnpg/actions/actionModel.ts +127 -0
  117. package/src/components/cnpg/actions/renderCNPGHeaderActions.tsx +60 -0
  118. package/src/components/cnpg/actions/useCNPGWriteGuard.tsx +58 -0
  119. package/src/components/cnpg/actions/useOpenCNPGPsql.ts +22 -0
  120. package/src/components/cnpg/archivingRepair.test.ts +104 -0
  121. package/src/components/cnpg/archivingRepair.ts +145 -0
  122. package/src/components/cnpg/baseBackup.test.ts +36 -0
  123. package/src/components/cnpg/baseBackup.ts +36 -0
  124. package/src/components/cnpg/blocking.test.ts +59 -0
  125. package/src/components/cnpg/blocking.ts +131 -0
  126. package/src/components/cnpg/coverageText.test.ts +27 -0
  127. package/src/components/cnpg/creation/CNPGTargetFields.tsx +110 -0
  128. package/src/components/cnpg/creation/targetModel.test.ts +137 -0
  129. package/src/components/cnpg/creation/targetModel.ts +149 -0
  130. package/src/components/cnpg/fleetStatus.test.ts +46 -0
  131. package/src/components/cnpg/fleetStatus.ts +43 -0
  132. package/src/components/cnpg/formFields.ts +2 -0
  133. package/src/components/cnpg/grantText.test.tsx +11 -0
  134. package/src/components/cnpg/host.test.tsx +24 -0
  135. package/src/components/cnpg/host.tsx +101 -0
  136. package/src/components/cnpg/inspectModel.test.ts +120 -0
  137. package/src/components/cnpg/inspectModel.ts +140 -0
  138. package/src/components/cnpg/logicalSlots.test.ts +30 -0
  139. package/src/components/cnpg/logicalSlots.ts +29 -0
  140. package/src/components/cnpg/lsn.test.ts +40 -0
  141. package/src/components/cnpg/lsn.ts +43 -0
  142. package/src/components/cnpg/operations/CNPGFleetOperation.tsx +30 -0
  143. package/src/components/cnpg/operations/CNPGOperationTracker.test.tsx +247 -0
  144. package/src/components/cnpg/operations/CNPGOperationTracker.tsx +255 -0
  145. package/src/components/cnpg/operations/model.test.ts +510 -0
  146. package/src/components/cnpg/operations/model.ts +499 -0
  147. package/src/components/cnpg/operations/presentation.test.ts +80 -0
  148. package/src/components/cnpg/operations/presentation.ts +84 -0
  149. package/src/components/cnpg/operations/store.ts +98 -0
  150. package/src/components/cnpg/operatorStatus.test.ts +134 -0
  151. package/src/components/cnpg/operatorStatus.ts +194 -0
  152. package/src/components/cnpg/paths.test.ts +54 -0
  153. package/src/components/cnpg/paths.ts +47 -4
  154. package/src/components/cnpg/protection/CNPGAttachArchiveDialog.tsx +185 -0
  155. package/src/components/cnpg/protection/CNPGCreateScheduleDialog.tsx +196 -0
  156. package/src/components/cnpg/protection/CNPGProtectionSetup.test.tsx +121 -0
  157. package/src/components/cnpg/protection/CNPGProtectionSetup.tsx +452 -0
  158. package/src/components/cnpg/protection/CNPGScheduleInput.tsx +115 -0
  159. package/src/components/cnpg/protection/CNPGScheduleRepairButton.tsx +120 -0
  160. package/src/components/cnpg/protection/scheduleModel.test.ts +19 -0
  161. package/src/components/cnpg/protection/scheduleModel.ts +16 -0
  162. package/src/components/cnpg/recovery/CNPGRecoveryPoint.tsx +220 -0
  163. package/src/components/cnpg/recovery/CNPGRestoreButton.test.tsx +29 -0
  164. package/src/components/cnpg/recovery/CNPGRestoreButton.tsx +26 -0
  165. package/src/components/cnpg/recovery/CNPGRestoreChecks.test.tsx +23 -0
  166. package/src/components/cnpg/recovery/CNPGRestoreChecks.tsx +153 -0
  167. package/src/components/cnpg/recovery/CNPGRestoreDialog.back.test.tsx +149 -0
  168. package/src/components/cnpg/recovery/CNPGRestoreDialog.test.tsx +46 -0
  169. package/src/components/cnpg/recovery/CNPGRestoreDialog.tsx +333 -0
  170. package/src/components/cnpg/recovery/CNPGRestoreProgress.tsx +182 -0
  171. package/src/components/cnpg/recovery/CNPGRestoreValidation.tsx +216 -0
  172. package/src/components/cnpg/recovery/restoreModel.test.ts +709 -0
  173. package/src/components/cnpg/recovery/restoreModel.ts +685 -0
  174. package/src/components/cnpg/recovery/restoreOperation.ts +56 -0
  175. package/src/components/cnpg/refresh.test.ts +38 -0
  176. package/src/components/cnpg/refresh.ts +17 -0
  177. package/src/components/cnpg/refreshNotice.test.tsx +29 -0
  178. package/src/components/cnpg/routes.test.ts +34 -3
  179. package/src/components/cnpg/routes.ts +60 -48
  180. package/src/components/cnpg/runtimeAssessment.ts +198 -0
  181. package/src/components/cnpg/runtimeModel.test.ts +175 -0
  182. package/src/components/cnpg/runtimeModel.ts +196 -0
  183. package/src/components/cnpg/runtimeParts.test.tsx +17 -0
  184. package/src/components/cnpg/runtimeParts.tsx +79 -0
  185. package/src/components/cnpg/shared.test.tsx +59 -0
  186. package/src/components/cnpg/shared.tsx +100 -182
  187. package/src/components/cnpg/storageModel.test.ts +107 -0
  188. package/src/components/cnpg/storageModel.ts +101 -0
  189. package/src/components/cnpg/trendSamples.test.ts +148 -0
  190. package/src/components/cnpg/trendSamples.ts +222 -0
  191. package/src/components/cnpg/useCNPGClusterAssessment.ts +30 -0
  192. package/src/components/cnpg/useCNPGKubectlContext.test.tsx +45 -0
  193. package/src/components/cnpg/useCNPGKubectlContext.ts +13 -0
  194. package/src/components/cnpg/useCNPGNavigate.test.tsx +58 -0
  195. package/src/components/cnpg/useCNPGNavigate.ts +21 -0
  196. package/src/components/cnpg/useCNPGPoolerLive.ts +46 -0
  197. package/src/components/cnpg/useCNPGScreenParams.ts +24 -0
  198. package/src/components/cnpg/useCNPGSidebarWorkspace.ts +66 -16
  199. package/src/components/cnpg/workspaceRoute.ts +23 -0
  200. package/src/components/cost/ApplicationCostTab.test.ts +7 -0
  201. package/src/components/cost/ApplicationCostTab.tsx +7 -7
  202. package/src/components/cost/CostView.test.ts +2 -1
  203. package/src/components/cost/CostView.tsx +0 -2
  204. package/src/components/cost/WorkloadCostTab.tsx +2 -4
  205. package/src/components/cost/errors.ts +3 -1
  206. package/src/components/cost/source.test.ts +10 -0
  207. package/src/components/cost/source.ts +5 -1
  208. package/src/components/diagnose/investigationEvidence/adapters/issues.ts +8 -0
  209. package/src/components/diagnose/investigationEvidence/projection.test.ts +16 -0
  210. package/src/components/dock/BottomDock.tsx +2 -0
  211. package/src/components/dock/TerminalTab.test.tsx +17 -0
  212. package/src/components/dock/TerminalTab.tsx +7 -3
  213. package/src/components/gitops/ArgoResourceDiffLoader.tsx +4 -2
  214. package/src/components/gitops/GitOpsView.tsx +16 -23
  215. package/src/components/helm/HelmCompareRoute.test.tsx +59 -0
  216. package/src/components/helm/HelmCompareRoute.tsx +57 -26
  217. package/src/components/helm/HelmReleaseDrawer.test.ts +55 -3
  218. package/src/components/helm/HelmReleaseDrawer.tsx +50 -12
  219. package/src/components/helm/TrackChartSourceDialog.render.test.tsx +120 -0
  220. package/src/components/helm/TrackChartSourceDialog.test.ts +50 -0
  221. package/src/components/helm/TrackChartSourceDialog.tsx +153 -176
  222. package/src/components/helm/ValuesViewer.test.tsx +301 -0
  223. package/src/components/helm/ValuesViewer.tsx +169 -78
  224. package/src/components/home/ActivitySummary.tsx +2 -1
  225. package/src/components/home/HomeView.tsx +20 -9
  226. package/src/components/home/ProblemsPanel.test.tsx +81 -0
  227. package/src/components/home/TopologyPreview.tsx +2 -2
  228. package/src/components/issues/IssuesPane.render.test.tsx +122 -0
  229. package/src/components/issues/IssuesPane.test.ts +65 -1
  230. package/src/components/issues/IssuesPane.tsx +188 -25
  231. package/src/components/issues/uncheckedSources.ts +21 -0
  232. package/src/components/resource/PVCUsageBar.render.test.tsx +32 -0
  233. package/src/components/resource/PVCUsageBar.test.tsx +24 -0
  234. package/src/components/resource/PVCUsageBar.tsx +15 -6
  235. package/src/components/resource/WorkloadMetricsHelpDialog.test.tsx +3 -1
  236. package/src/components/resource/WorkloadMetricsHelpDialog.tsx +1 -1
  237. package/src/components/resources/CompositeRenderer.tsx +4 -4
  238. package/src/components/resources/ResourceDetailDrawer.tsx +2 -2
  239. package/src/components/resources/ResourcesView.tsx +26 -12
  240. package/src/components/settings/LocalIntegrationStatus.test.tsx +52 -0
  241. package/src/components/settings/LocalIntegrationStatus.tsx +3 -4
  242. package/src/components/settings/SettingsDialog.tsx +28 -15
  243. package/src/components/timeline/TimelineList.tsx +22 -24
  244. package/src/components/timeline/TimelineLoadError.test.tsx +62 -0
  245. package/src/components/timeline/TimelineLoadError.tsx +96 -0
  246. package/src/components/timeline/TimelineView.tsx +18 -45
  247. package/src/components/timeline/ViewModeToggle.tsx +20 -0
  248. package/src/components/timeline/useStickyError.test.tsx +47 -0
  249. package/src/components/timeline/useStickyError.ts +15 -0
  250. package/src/components/ui/DiagnosticsOverlay.tsx +34 -2
  251. package/src/components/ui/Markdown.test.tsx +65 -0
  252. package/src/components/ui/Markdown.tsx +144 -127
  253. package/src/components/whats-new/releaseNotes.ts +63 -1
  254. package/src/components/workload/WorkloadView.test.ts +10 -1
  255. package/src/components/workload/WorkloadView.tsx +27 -239
  256. package/src/components/workspace/layout.test.tsx +29 -0
  257. package/src/components/workspace/layout.tsx +259 -0
  258. package/src/components/workspace/table.ts +7 -0
  259. package/src/contexts/CapabilitiesContext.tsx +1 -1
  260. package/src/integrations/resourceHost.test.tsx +55 -0
  261. package/src/integrations/resourceHost.ts +99 -0
  262. package/src/integrations/resourceHosts.test.tsx +141 -0
  263. package/src/integrations/resourceHosts.tsx +119 -0
  264. package/src/integrations/workspaceRoute.ts +20 -0
  265. package/src/integrations/workspaceRoutes.test.ts +40 -0
  266. package/src/integrations/workspaceRoutes.ts +22 -0
  267. package/src/integrations/workspaceScreens.tsx +14 -0
  268. package/src/utils/drawer-trail.ts +35 -0
  269. package/src/utils/kindFilterStore.test.ts +147 -0
  270. package/src/utils/kindFilterStore.ts +91 -0
  271. package/src/utils/navigation.test.ts +13 -1
  272. package/src/utils/navigation.ts +39 -2
  273. package/src/utils/page-links.ts +21 -0
  274. package/src/utils/tabStorage.ts +40 -0
  275. package/src/utils/viewMemory.test.ts +78 -0
  276. package/src/utils/viewMemory.ts +54 -0
@@ -3,10 +3,13 @@ import { Layers3, X } from "lucide-react";
3
3
  import {
4
4
  Collapse,
5
5
  CollapseChevron,
6
+ formatDuration,
6
7
  StatusDot,
8
+ Tooltip,
7
9
  WithTooltip,
8
10
  mapHealthToTone,
9
11
  type CapacityAutoscalerBackoff,
12
+ type CapacityAutoscalerScaleDown,
10
13
  type CapacityAutoscalerChildObservation,
11
14
  type CapacityBoundedResultMeta,
12
15
  type CapacityCertainty,
@@ -25,48 +28,10 @@ import { ClusterSchedulingCard } from "./ClusterSchedulingCard";
25
28
  import { Badge } from "@skyhook-io/k8s-ui/components/ui/Badge";
26
29
  import type { useCapacityOverview } from "../../api/client";
27
30
  import type { SelectedResource } from "../../types";
28
- import {
29
- actionSeverity,
30
- CapacityFreshness,
31
- capacityManagerLabel,
32
- coverageHasObservations,
33
- coverageIsDenied,
34
- coverageIsLowerBound,
35
- CertaintyGlyph,
36
- formatQuantity,
37
- coverageMessage,
38
- DeniedBadge,
39
- EmptyState,
40
- errorMessage,
41
- formatTimestamp,
42
- humanizeCode,
43
- identityToSelectedResource,
44
- InlineEmpty,
45
- integrationBlock,
46
- InventoryQuantityCell,
47
- KpiTile,
48
- LinkButton,
49
- managerStatusTone,
50
- Notice,
51
- pickWorstPressure,
52
- PoolReadyBadge,
53
- poolReadinessDetail,
54
- RefreshError,
55
- relativeTime,
56
- ROW_HOVER,
57
- ScopeBadges,
58
- ScrollableContent,
59
- SectionCard,
60
- shortResourceLabel,
61
- sortedResourceEntries,
62
- TABLE_HEAD,
63
- TABLE_WRAP,
64
- TBODY,
65
- TD,
66
- TH,
67
- worstManagerStatus,
68
- type CapacityConnectionState,
69
- } from "./shared";
31
+ import { actionSeverity, CapacityFreshness, capacityManagerLabel, coverageHasObservations, coverageIsDenied, coverageIsLowerBound, formatQuantity, coverageMessage, DeniedBadge, errorMessage, formatTimestamp, humanizeCode, identityToSelectedResource, InlineEmpty, integrationBlock, InventoryQuantityCell, KpiTile, LinkButton, managerStatusTone, pickWorstPressure, PoolReadyBadge, poolReadinessDetail, RefreshError, relativeTime, ScopeBadges, ScrollableContent, SectionCard, shortResourceLabel, sortedResourceEntries, worstManagerStatus, type CapacityConnectionState } from "./shared";
32
+ import { CertaintyGlyph } from "@skyhook-io/k8s-ui";
33
+ import { Notice, ScreenEmptyState } from "../workspace/layout";
34
+ import { ROW_HOVER, TABLE_HEAD, TABLE_WRAP, TBODY, TD, TH } from "../workspace/table";
70
35
 
71
36
  const EXPLAIN_CARDS: { term: string; body: string }[] = [
72
37
  {
@@ -337,7 +302,7 @@ export function CapacityOverview({
337
302
  // NodePool coverage by definition — that is not an error to block on.
338
303
  if (karpenterActive && !coverageHasObservations(data.coverage.nodePools)) {
339
304
  return (
340
- <EmptyState
305
+ <ScreenEmptyState
341
306
  icon={Layers3}
342
307
  title="NodePool inventory unavailable"
343
308
  detail={coverageMessage(data.coverage.nodePools, "NodePool inventory")}
@@ -345,6 +310,10 @@ export function CapacityOverview({
345
310
  );
346
311
  }
347
312
 
313
+ const showScaleDown =
314
+ data.groups.some((group) =>
315
+ group.children.some((child) => child.scaleDown),
316
+ ) || data.orphanAutoscalerGroups.some((child) => child.scaleDown);
348
317
  const summary = data.summary;
349
318
  const coverage = data.coverage;
350
319
  const podsDeniedFlag = coverageIsDenied(coverage.pods);
@@ -569,6 +538,7 @@ export function CapacityOverview({
569
538
  )}
570
539
 
571
540
  <NodeGroupsSection
541
+ showScaleDown={showScaleDown}
572
542
  sectionRef={inventoryRef}
573
543
  groups={data.groups}
574
544
  pools={data.pools}
@@ -580,6 +550,7 @@ export function CapacityOverview({
580
550
  />
581
551
 
582
552
  <OrphanAutoscalerSection
553
+ showScaleDown={showScaleDown}
583
554
  orphans={data.orphanAutoscalerGroups}
584
555
  meta={data.orphanAutoscalerGroupsMeta}
585
556
  />
@@ -762,6 +733,7 @@ function ManagerLine({
762
733
  // ============================================================================
763
734
 
764
735
  function NodeGroupsSection({
736
+ showScaleDown,
765
737
  sectionRef,
766
738
  groups,
767
739
  pools,
@@ -771,6 +743,7 @@ function NodeGroupsSection({
771
743
  onOpenPool,
772
744
  onOpenResource,
773
745
  }: {
746
+ showScaleDown: boolean;
774
747
  sectionRef: RefObject<HTMLDivElement | null>;
775
748
  groups: CapacityGroupSummary[];
776
749
  pools: CapacityPoolSummary[];
@@ -843,6 +816,7 @@ function NodeGroupsSection({
843
816
  <GroupRow
844
817
  key={group.id}
845
818
  group={group}
819
+ showScaleDown={showScaleDown}
846
820
  pool={
847
821
  group.manager === "karpenter"
848
822
  ? poolByName.get(group.name)
@@ -980,12 +954,14 @@ function worstChildHealth(
980
954
 
981
955
  function GroupRow({
982
956
  group,
957
+ showScaleDown,
983
958
  pool,
984
959
  detectionFailure,
985
960
  onOpenPool,
986
961
  onOpenResource,
987
962
  }: {
988
963
  group: CapacityGroupSummary;
964
+ showScaleDown: boolean;
989
965
  pool?: CapacityPoolSummary;
990
966
  /** Why manager detection could not run, when it could not. Non-null forbids
991
967
  * the "none detected" conclusion for a group with no attributed manager. */
@@ -1000,8 +976,24 @@ function GroupRow({
1000
976
  ? pickWorstPressure(pool.ledger.limitPressure)
1001
977
  : undefined;
1002
978
  const childHealth = worstChildHealth(group.children);
1003
- // Server-rendered prose — joined, never sorted or parsed.
1004
- const scalingText = group.scaling.map((fact) => fact.summary).join(" · ");
979
+ const noScaleDownCandidates =
980
+ hasChildren &&
981
+ !group.childrenMeta.truncated &&
982
+ !group.scaling.some((fact) => fact.code === "at_min_size") &&
983
+ group.children.every(
984
+ (child) =>
985
+ child.minSize !== undefined &&
986
+ child.target !== undefined &&
987
+ child.target >= child.minSize &&
988
+ child.scaleDown?.status === "NoCandidates" &&
989
+ child.scaleDown.candidates === 0,
990
+ ) &&
991
+ group.children.some(
992
+ (child) =>
993
+ child.target !== undefined &&
994
+ child.minSize !== undefined &&
995
+ child.target > child.minSize,
996
+ );
1005
997
 
1006
998
  return (
1007
999
  <>
@@ -1020,7 +1012,7 @@ function GroupRow({
1020
1012
  : undefined
1021
1013
  }
1022
1014
  >
1023
- <td className={TD}>
1015
+ <td className={`${TD} whitespace-nowrap`}>
1024
1016
  <div className="flex items-center gap-1.5">
1025
1017
  {hasChildren ? (
1026
1018
  <button
@@ -1092,7 +1084,7 @@ function GroupRow({
1092
1084
  )}
1093
1085
  </div>
1094
1086
  </td>
1095
- <td className={TD}>
1087
+ <td className={`${TD} whitespace-nowrap`}>
1096
1088
  {group.manager ? (
1097
1089
  <span className="flex flex-wrap items-center gap-1.5">
1098
1090
  <Badge tone="structural" size="sm">
@@ -1119,27 +1111,88 @@ function GroupRow({
1119
1111
  >
1120
1112
  {`${group.readyNodeCount}/${group.nodeCount}`}
1121
1113
  </td>
1122
- <td className={TD}>
1114
+ <td className={`${TD} whitespace-nowrap`}>
1123
1115
  <InventoryQuantityCell
1124
1116
  observation={group.allocatable}
1125
1117
  empty="Not observed"
1126
1118
  />
1127
1119
  </td>
1128
- <td className={TD}>
1120
+ <td className={`${TD} whitespace-nowrap`}>
1129
1121
  <InventoryQuantityCell
1130
1122
  observation={group.scheduledRequests}
1131
1123
  empty="Not observed"
1132
1124
  />
1133
1125
  </td>
1134
1126
  <td className={`${TD} text-xs text-theme-text-secondary`}>
1135
- {scalingText || <span className="text-theme-text-tertiary">—</span>}
1127
+ {/* The only wrapping column, so long prose adds lines instead of
1128
+ squeezing names and badges. Inline tooltip triggers let a fact
1129
+ wrap with the text around it rather than as one box. */}
1130
+ <div className="min-w-[10rem]">
1131
+ {group.scaling.map((fact, index) => (
1132
+ <span key={fact.code}>
1133
+ {index > 0 && " · "}
1134
+ {fact.code === "at_min_size" ? (
1135
+ <Tooltip
1136
+ content={MIN_SIZE_EXPLANATION}
1137
+ wrapperClassName="!inline"
1138
+ >
1139
+ <span
1140
+ tabIndex={0}
1141
+ role="note"
1142
+ aria-label={`${fact.summary}. ${MIN_SIZE_EXPLANATION}`}
1143
+ className="cursor-help rounded focus-visible:outline focus-visible:outline-2 focus-visible:outline-skyhook-500"
1144
+ >
1145
+ {fact.summary}
1146
+ </span>
1147
+ </Tooltip>
1148
+ ) : fact.code === "scale_down_candidates" ? (
1149
+ <Tooltip
1150
+ content={SCALE_DOWN_EXPLANATION}
1151
+ wrapperClassName="!inline"
1152
+ >
1153
+ <span
1154
+ tabIndex={0}
1155
+ role="note"
1156
+ aria-label={`${fact.summary}. ${SCALE_DOWN_EXPLANATION}`}
1157
+ className="cursor-help rounded focus-visible:outline focus-visible:outline-2 focus-visible:outline-skyhook-500"
1158
+ >
1159
+ {fact.summary}
1160
+ </span>
1161
+ </Tooltip>
1162
+ ) : (
1163
+ fact.summary
1164
+ )}
1165
+ </span>
1166
+ ))}
1167
+ {noScaleDownCandidates && (
1168
+ <>
1169
+ {group.scaling.length > 0 && " · "}
1170
+ <Tooltip
1171
+ content={NO_CANDIDATES_EXPLANATION}
1172
+ wrapperClassName="!inline"
1173
+ >
1174
+ <span
1175
+ tabIndex={0}
1176
+ role="note"
1177
+ aria-label={`No node can be removed right now. ${NO_CANDIDATES_EXPLANATION}`}
1178
+ className="cursor-help rounded focus-visible:outline focus-visible:outline-2 focus-visible:outline-skyhook-500"
1179
+ >
1180
+ No node can be removed right now
1181
+ </span>
1182
+ </Tooltip>
1183
+ </>
1184
+ )}
1185
+ {group.scaling.length === 0 && !noScaleDownCandidates && (
1186
+ <span className="text-theme-text-tertiary">—</span>
1187
+ )}
1188
+ </div>
1136
1189
  </td>
1137
1190
  </tr>
1138
1191
  {hasChildren && (
1139
1192
  <tr>
1140
1193
  <td colSpan={7} className="p-0">
1141
1194
  <Collapse open={expanded}>
1142
- <ChildSubTable group={group} />
1195
+ <ChildSubTable group={group} showScaleDown={showScaleDown} />
1143
1196
  </Collapse>
1144
1197
  </td>
1145
1198
  </tr>
@@ -1148,10 +1201,30 @@ function GroupRow({
1148
1201
  );
1149
1202
  }
1150
1203
 
1204
+ const MIN_SIZE_EXPLANATION =
1205
+ "Every observed autoscaler group has its target at its published minimum. Lowering the pool's minimum is required for it to shrink below this size. Unattributed groups are separate; current registered nodes may still exceed the target.";
1206
+
1207
+ const SCALE_DOWN_EXPLANATION =
1208
+ "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.";
1209
+ const NO_CANDIDATES_EXPLANATION =
1210
+ "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.";
1211
+ const SCALE_DOWN_STATUS_LABELS = new Map([
1212
+ ["NoCandidates", "No scale-down candidates"],
1213
+ ["CandidatesPresent", "Scale-down candidates present"],
1214
+ ["InProgress", "Scale-down in progress"],
1215
+ ["NoActivity", "No scale-down activity"],
1216
+ ]);
1217
+
1151
1218
  const CHILD_TH = "px-3 py-1.5 text-left font-medium whitespace-nowrap";
1152
1219
  const CHILD_TD = "px-3 py-1.5 align-top text-xs text-theme-text-primary";
1153
1220
 
1154
- function ChildSubTable({ group }: { group: CapacityGroupSummary }) {
1221
+ function ChildSubTable({
1222
+ group,
1223
+ showScaleDown,
1224
+ }: {
1225
+ group: CapacityGroupSummary;
1226
+ showScaleDown: boolean;
1227
+ }) {
1155
1228
  return (
1156
1229
  <div className="border-t border-theme-border-subtle bg-theme-base/40 px-4 py-3">
1157
1230
  <div className="mb-1.5 text-[11px] font-medium uppercase tracking-wide text-theme-text-tertiary">
@@ -1167,13 +1240,14 @@ function ChildSubTable({ group }: { group: CapacityGroupSummary }) {
1167
1240
  <th className={CHILD_TH}>Ready / total</th>
1168
1241
  <th className={CHILD_TH}>Health</th>
1169
1242
  <th className={CHILD_TH}>Backoff</th>
1170
- <th className={CHILD_TH}>Observed</th>
1243
+ {showScaleDown && <th className={CHILD_TH}>Scale-down</th>}
1244
+ <th className={CHILD_TH}>Health observed</th>
1171
1245
  </tr>
1172
1246
  </thead>
1173
1247
  <tbody className={TBODY}>
1174
1248
  {group.children.map((child) => (
1175
1249
  <tr key={child.id} className={ROW_HOVER}>
1176
- <td className={`${CHILD_TD} font-mono`}>
1250
+ <td className={`${CHILD_TD} whitespace-nowrap font-mono`}>
1177
1251
  <WithTooltip tip={child.name}>
1178
1252
  <span>{child.id}</span>
1179
1253
  </WithTooltip>
@@ -1197,6 +1271,11 @@ function ChildSubTable({ group }: { group: CapacityGroupSummary }) {
1197
1271
  <td className={CHILD_TD}>
1198
1272
  <ChildBackoff backoff={child.backoff} />
1199
1273
  </td>
1274
+ {showScaleDown && (
1275
+ <td className={CHILD_TD}>
1276
+ <ChildScaleDown scaleDown={child.scaleDown} />
1277
+ </td>
1278
+ )}
1200
1279
  <td className={CHILD_TD}>
1201
1280
  <ChildObserved asOf={child.asOf} />
1202
1281
  </td>
@@ -1219,9 +1298,11 @@ function ChildSubTable({ group }: { group: CapacityGroupSummary }) {
1219
1298
  // ============================================================================
1220
1299
 
1221
1300
  function OrphanAutoscalerSection({
1301
+ showScaleDown,
1222
1302
  orphans,
1223
1303
  meta,
1224
1304
  }: {
1305
+ showScaleDown: boolean;
1225
1306
  orphans: CapacityAutoscalerChildObservation[];
1226
1307
  meta: CapacityBoundedResultMeta;
1227
1308
  }) {
@@ -1240,13 +1321,14 @@ function OrphanAutoscalerSection({
1240
1321
  <th className={TH}>Min–max</th>
1241
1322
  <th className={TH}>Target</th>
1242
1323
  <th className={TH}>Health</th>
1243
- <th className={TH}>Observed</th>
1324
+ {showScaleDown && <th className={TH}>Scale-down</th>}
1325
+ <th className={TH}>Health observed</th>
1244
1326
  </tr>
1245
1327
  </thead>
1246
1328
  <tbody className={TBODY}>
1247
1329
  {orphans.map((child) => (
1248
1330
  <tr key={child.id} className={ROW_HOVER}>
1249
- <td className={`${TD} font-mono text-xs`}>
1331
+ <td className={`${TD} whitespace-nowrap font-mono text-xs`}>
1250
1332
  <WithTooltip tip={child.name}>
1251
1333
  <span>{child.id}</span>
1252
1334
  </WithTooltip>
@@ -1262,6 +1344,11 @@ function OrphanAutoscalerSection({
1262
1344
  <td className={TD}>
1263
1345
  <ChildHealth health={child.health} />
1264
1346
  </td>
1347
+ {showScaleDown && (
1348
+ <td className={TD}>
1349
+ <ChildScaleDown scaleDown={child.scaleDown} />
1350
+ </td>
1351
+ )}
1265
1352
  <td className={TD}>
1266
1353
  <ChildObserved asOf={child.asOf} />
1267
1354
  </td>
@@ -1337,6 +1424,80 @@ function ChildBackoff({ backoff }: { backoff?: CapacityAutoscalerBackoff }) {
1337
1424
  );
1338
1425
  }
1339
1426
 
1427
+ function ChildScaleDown({
1428
+ scaleDown,
1429
+ }: {
1430
+ scaleDown?: CapacityAutoscalerScaleDown;
1431
+ }) {
1432
+ if (!scaleDown) {
1433
+ return <span className="text-theme-text-tertiary">Not published</span>;
1434
+ }
1435
+ const candidatesLabel =
1436
+ scaleDown.candidates === undefined
1437
+ ? "Candidates: not published"
1438
+ : `${scaleDown.candidates} scale-down ${scaleDown.candidates === 1 ? "candidate" : "candidates"}`;
1439
+ const statusLabel = scaleDown.status
1440
+ ? (SCALE_DOWN_STATUS_LABELS.get(scaleDown.status) ?? scaleDown.status)
1441
+ : "Status not published";
1442
+ const countIsStatus =
1443
+ scaleDown.status === "CandidatesPresent" &&
1444
+ scaleDown.candidates !== undefined;
1445
+ const noCandidates =
1446
+ scaleDown.status === "NoCandidates" && scaleDown.candidates === 0;
1447
+ const explanation =
1448
+ scaleDown.status === "NoCandidates"
1449
+ ? NO_CANDIDATES_EXPLANATION
1450
+ : SCALE_DOWN_EXPLANATION;
1451
+ const transition = scaleDown.lastTransitionTime
1452
+ ? Date.parse(scaleDown.lastTransitionTime)
1453
+ : NaN;
1454
+ return (
1455
+ <div className="space-y-1 text-theme-text-secondary">
1456
+ <div className="flex items-center gap-1">
1457
+ <WithTooltip tip={`${statusLabel}. ${explanation}`}>
1458
+ <span
1459
+ tabIndex={0}
1460
+ role="note"
1461
+ aria-label={`${countIsStatus ? candidatesLabel : statusLabel}. ${explanation}`}
1462
+ className="cursor-help rounded focus-visible:outline focus-visible:outline-2 focus-visible:outline-skyhook-500"
1463
+ >
1464
+ {countIsStatus ? candidatesLabel : statusLabel}
1465
+ </span>
1466
+ </WithTooltip>
1467
+ {(countIsStatus || noCandidates) && (
1468
+ <CertaintyGlyph
1469
+ certainty="exact"
1470
+ title={`Candidate count published by the autoscaler: ${scaleDown.candidates}.`}
1471
+ />
1472
+ )}
1473
+ </div>
1474
+ {!countIsStatus && !noCandidates && (
1475
+ <div className="flex items-center gap-1 text-theme-text-tertiary">
1476
+ {candidatesLabel}
1477
+ <CertaintyGlyph
1478
+ certainty={scaleDown.candidates === undefined ? "unknown" : "exact"}
1479
+ title={
1480
+ scaleDown.candidates === undefined
1481
+ ? "Candidate count not published by the autoscaler."
1482
+ : "Candidate count published by the autoscaler."
1483
+ }
1484
+ />
1485
+ </div>
1486
+ )}
1487
+ <div className="text-theme-text-tertiary">
1488
+ Probe: <ChildObserved asOf={scaleDown.asOf} />
1489
+ </div>
1490
+ {scaleDown.lastTransitionTime && (
1491
+ <div className="text-theme-text-tertiary">
1492
+ Status since {formatTimestamp(scaleDown.lastTransitionTime)}
1493
+ {Number.isFinite(transition) &&
1494
+ ` (for ${formatDuration(Math.max(0, Date.now() - transition))})`}
1495
+ </div>
1496
+ )}
1497
+ </div>
1498
+ );
1499
+ }
1500
+
1340
1501
  /** Relative freshness; an absent `asOf` is "not probed" — never a zero time
1341
1502
  * and never "now" (the payload published null, meaning we never checked). */
1342
1503
  function ChildObserved({ asOf }: { asOf?: string }) {
@@ -19,59 +19,10 @@ import {
19
19
  } from "../../api/client";
20
20
  import type { SelectedResource } from "../../types";
21
21
  import { refToSelectedResource } from "../../utils/navigation";
22
- import {
23
- ActualUsageDetail,
24
- ActualUsageInline,
25
- CapacityFreshness,
26
- CapacityIssueEvidence,
27
- CertaintyGlyph,
28
- ClaimStageBadge,
29
- conditionSummary,
30
- ConditionBadge,
31
- coverageHasObservations,
32
- coverageIsDenied,
33
- coverageIsLowerBound,
34
- coverageMessage,
35
- DeniedBadge,
36
- EmptyState,
37
- errorMessage,
38
- formatQuantity,
39
- formatTaint,
40
- humanizeCode,
41
- identityKey,
42
- identityToSelectedResource,
43
- InlineEmpty,
44
- integrationBlock,
45
- KeyValueRows,
46
- LinkButton,
47
- memberCoverageSource,
48
- MiniCount,
49
- NodeReadyBadge,
50
- Notice,
51
- observationTitle,
52
- PoolReadyBadge,
53
- POOL_SECTIONS,
54
- PressureDetail,
55
- quantityResourceRank,
56
- QuantityInline,
57
- RefreshError,
58
- resourceLabel,
59
- ResourceLink,
60
- ROW_HOVER,
61
- ScopeBadges,
62
- ScrollableContent,
63
- SectionCard,
64
- TABLE_HEAD,
65
- TABLE_WRAP,
66
- TBODY,
67
- TD,
68
- TH,
69
- TokenGroup,
70
- useCapacityCursorRecovery,
71
- useCapacityPagination,
72
- type CapacityConnectionState,
73
- type PoolSection,
74
- } from "./shared";
22
+ import { ActualUsageDetail, ActualUsageInline, CapacityFreshness, CapacityIssueEvidence, ClaimStageBadge, conditionSummary, ConditionBadge, coverageHasObservations, coverageIsDenied, coverageIsLowerBound, coverageMessage, DeniedBadge, errorMessage, formatQuantity, formatTaint, humanizeCode, identityKey, identityToSelectedResource, InlineEmpty, integrationBlock, KeyValueRows, LinkButton, memberCoverageSource, MiniCount, NodeReadyBadge, observationTitle, PoolReadyBadge, POOL_SECTIONS, PressureDetail, quantityResourceRank, QuantityInline, RefreshError, resourceLabel, ResourceLink, ScopeBadges, ScrollableContent, SectionCard, TokenGroup, useCapacityCursorRecovery, useCapacityPagination, type CapacityConnectionState, type PoolSection } from "./shared";
23
+ import { CertaintyGlyph } from "@skyhook-io/k8s-ui";
24
+ import { Notice, ScreenEmptyState } from "../workspace/layout";
25
+ import { ROW_HOVER, TABLE_HEAD, TABLE_WRAP, TBODY, TD, TH } from "../workspace/table";
75
26
 
76
27
  export function CapacityPoolDetail({
77
28
  name,
@@ -123,7 +74,7 @@ export function CapacityPoolDetail({
123
74
  const response = query.data;
124
75
  if (response && !coverageHasObservations(response.coverage.nodePools)) {
125
76
  return (
126
- <EmptyState
77
+ <ScreenEmptyState
127
78
  icon={Layers3}
128
79
  title="NodePool inventory unavailable"
129
80
  detail={coverageMessage(
@@ -136,7 +87,7 @@ export function CapacityPoolDetail({
136
87
  const pool = response?.pool;
137
88
  if (!response || !pool) {
138
89
  return (
139
- <EmptyState
90
+ <ScreenEmptyState
140
91
  icon={CircleHelp}
141
92
  title="NodePool unavailable"
142
93
  detail="The Capacity API did not return an observation for this NodePool."