@skyhook-io/k8s-ui 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 (309) hide show
  1. package/README.md +41 -0
  2. package/package.json +3 -3
  3. package/src/components/applications/ApplicationDetail.test.tsx +15 -0
  4. package/src/components/applications/ApplicationDetail.tsx +17 -15
  5. package/src/components/charts/AreaChart.interaction.test.tsx +118 -0
  6. package/src/components/charts/AreaChart.test.tsx +25 -0
  7. package/src/components/charts/AreaChart.tsx +141 -12
  8. package/src/components/charts/index.ts +1 -0
  9. package/src/components/checks/ChecksView.tsx +18 -1
  10. package/src/components/checks/types.ts +2 -13
  11. package/src/components/cnpg/CNPGBackupSummary.tsx +57 -9
  12. package/src/components/cnpg/CNPGClusterHASection.test.tsx +90 -0
  13. package/src/components/cnpg/CNPGClusterHASection.tsx +331 -0
  14. package/src/components/cnpg/CNPGClusterSummary.test.tsx +275 -0
  15. package/src/components/cnpg/CNPGClusterSummary.tsx +310 -96
  16. package/src/components/cnpg/CNPGConnectSection.test.tsx +64 -0
  17. package/src/components/cnpg/CNPGConnectSection.tsx +178 -0
  18. package/src/components/cnpg/CNPGDeclarativeSummary.tsx +157 -26
  19. package/src/components/cnpg/CNPGImageCatalogSummary.tsx +7 -4
  20. package/src/components/cnpg/CNPGLogicalPath.test.tsx +22 -0
  21. package/src/components/cnpg/CNPGLogicalPath.tsx +150 -0
  22. package/src/components/cnpg/CNPGObjectStoreSummary.tsx +9 -6
  23. package/src/components/cnpg/CNPGObjectSummary.test.tsx +193 -2
  24. package/src/components/cnpg/CNPGPoolerScheduling.test.tsx +36 -0
  25. package/src/components/cnpg/CNPGPoolerSummary.tsx +241 -14
  26. package/src/components/cnpg/CNPGSharedSummary.tsx +15 -8
  27. package/src/components/cnpg/CNPGWALArchivingFact.test.tsx +39 -0
  28. package/src/components/cnpg/backupRuns.test.ts +17 -0
  29. package/src/components/cnpg/backupRuns.ts +19 -0
  30. package/src/components/cnpg/connect.test.ts +124 -0
  31. package/src/components/cnpg/connect.ts +155 -0
  32. package/src/components/cnpg/databaseRole.test.ts +38 -0
  33. package/src/components/cnpg/databaseRole.ts +80 -0
  34. package/src/components/cnpg/ha.test.ts +446 -0
  35. package/src/components/cnpg/ha.ts +621 -0
  36. package/src/components/cnpg/index.ts +18 -0
  37. package/src/components/cnpg/logicalReplication.test.ts +121 -0
  38. package/src/components/cnpg/logicalReplication.ts +252 -0
  39. package/src/components/cnpg/pooler.test.ts +84 -0
  40. package/src/components/cnpg/pooler.ts +225 -0
  41. package/src/components/cnpg/primitives.tsx +21 -117
  42. package/src/components/cnpg/problems.ts +12 -0
  43. package/src/components/cnpg/relations.test.ts +63 -13
  44. package/src/components/cnpg/relations.ts +61 -52
  45. package/src/components/cnpg/schedule.ts +35 -0
  46. package/src/components/cnpg/workspace-disk.test.ts +97 -0
  47. package/src/components/cnpg/workspace-fleet-metrics.test.ts +250 -0
  48. package/src/components/cnpg/workspace-problems.test.ts +175 -0
  49. package/src/components/cnpg/workspace.test.ts +380 -9
  50. package/src/components/cnpg/workspace.ts +1137 -136
  51. package/src/components/dock/DockContext.test.tsx +32 -0
  52. package/src/components/dock/DockContext.tsx +18 -4
  53. package/src/components/dock/LocalTerminalTab.test.tsx +40 -0
  54. package/src/components/dock/LocalTerminalTab.tsx +4 -3
  55. package/src/components/dock/TerminalTab.tsx +20 -8
  56. package/src/components/facts/certainty.tsx +36 -0
  57. package/src/components/facts/facts.test.tsx +24 -0
  58. package/src/components/facts/facts.tsx +62 -0
  59. package/src/components/facts/index.ts +6 -0
  60. package/src/components/facts/managed-by.test.tsx +17 -0
  61. package/src/components/facts/managed-by.tsx +36 -0
  62. package/src/components/facts/public-exports.test.tsx +35 -0
  63. package/src/components/gitops/GitOpsGraphFilterRail.tsx +6 -2
  64. package/src/components/gitops/GitOpsHealthSourceNotice.test.tsx +38 -0
  65. package/src/components/gitops/GitOpsHealthSourceNotice.tsx +13 -3
  66. package/src/components/gitops/GitOpsTableView.tsx +9 -24
  67. package/src/components/gitops/detail-helpers.ts +1 -1
  68. package/src/components/gitops/insights/ArgoResourceDiff.test.tsx +115 -0
  69. package/src/components/gitops/insights/ArgoResourceDiff.tsx +22 -5
  70. package/src/components/gitops/insights/DriftPanel.tsx +62 -0
  71. package/src/components/gitops/insights/GitOpsInsightViews.tsx +29 -83
  72. package/src/components/gitops/tree/GitOpsTreeGraph.test.ts +26 -1
  73. package/src/components/gitops/tree/GitOpsTreeGraph.tsx +40 -60
  74. package/src/components/gitops/tree/layout.test.ts +111 -0
  75. package/src/components/gitops/tree/layout.ts +128 -0
  76. package/src/components/gitops/tree/tree-helpers.test.ts +17 -0
  77. package/src/components/gitops/tree/tree-helpers.ts +7 -1
  78. package/src/components/issues/ConditionalCleanup.test.tsx +82 -0
  79. package/src/components/issues/IssuesView.tsx +40 -5
  80. package/src/components/issues/ResourceIssuesSection.test.tsx +28 -0
  81. package/src/components/issues/ResourceIssuesSection.tsx +3 -0
  82. package/src/components/issues/index.ts +3 -1
  83. package/src/components/issues/issue-timing.ts +14 -3
  84. package/src/components/issues/issues.test.ts +50 -2
  85. package/src/components/issues/severity.ts +21 -0
  86. package/src/components/issues/types.ts +23 -0
  87. package/src/components/logs/LogCore.theme.test.tsx +81 -3
  88. package/src/components/logs/LogCore.tsx +25 -17
  89. package/src/components/logs/LogToolbarSelects.tsx +3 -1
  90. package/src/components/logs/LogsViewer.test.tsx +291 -0
  91. package/src/components/logs/LogsViewer.tsx +72 -11
  92. package/src/components/logs/StructuredLogLine.test.tsx +76 -0
  93. package/src/components/logs/StructuredLogLine.tsx +22 -30
  94. package/src/components/logs/WorkloadLogsViewer.notice.test.tsx +59 -0
  95. package/src/components/logs/WorkloadLogsViewer.test.tsx +65 -0
  96. package/src/components/logs/WorkloadLogsViewer.tsx +44 -10
  97. package/src/components/logs/useLogBuffer.test.ts +8 -0
  98. package/src/components/logs/useLogStream.ts +28 -1
  99. package/src/components/problems/index.ts +1 -0
  100. package/src/components/problems/problems.test.tsx +63 -0
  101. package/src/components/problems/problems.tsx +165 -0
  102. package/src/components/resources/ActiveFilterBar.test.tsx +78 -0
  103. package/src/components/resources/ActiveFilterBar.tsx +86 -0
  104. package/src/components/resources/ResourcesSidebar.keyboard.test.tsx +42 -0
  105. package/src/components/resources/ResourcesSidebar.test.tsx +36 -2
  106. package/src/components/resources/ResourcesSidebar.tsx +92 -41
  107. package/src/components/resources/ResourcesView.kind-filter-memory.test.ts +52 -0
  108. package/src/components/resources/ResourcesView.kind-view.test.tsx +149 -0
  109. package/src/components/resources/ResourcesView.per-container.test.ts +56 -1
  110. package/src/components/resources/ResourcesView.tsx +309 -117
  111. package/src/components/resources/curated-column-ownership.test.ts +3 -3
  112. package/src/components/resources/renderers/AnalysisRunRenderer.tsx +3 -3
  113. package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +6 -5
  114. package/src/components/resources/renderers/AutoscalingReferences.test.tsx +286 -0
  115. package/src/components/resources/renderers/CAPIClusterRenderer.tsx +5 -4
  116. package/src/components/resources/renderers/CAPIExactReferenceNavigation.test.tsx +27 -0
  117. package/src/components/resources/renderers/CAPIKubeadmControlPlaneRenderer.tsx +2 -2
  118. package/src/components/resources/renderers/CAPIMachineDeploymentRenderer.tsx +2 -2
  119. package/src/components/resources/renderers/CAPIMachinePoolRenderer.tsx +2 -2
  120. package/src/components/resources/renderers/CAPIMachineRenderer.tsx +2 -2
  121. package/src/components/resources/renderers/CAPIMachineSetRenderer.tsx +2 -2
  122. package/src/components/resources/renderers/CNPGBackupRenderer.test.tsx +23 -0
  123. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +8 -19
  124. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +7 -8
  125. package/src/components/resources/renderers/CNPGDeclarativeRenderer.tsx +25 -0
  126. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.test.tsx +32 -0
  127. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +9 -17
  128. package/src/components/resources/renderers/CNPGShared.tsx +2 -2
  129. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +2 -2
  130. package/src/components/resources/renderers/CompositeRenderer.navigation.test.tsx +35 -0
  131. package/src/components/resources/renderers/CompositeRenderer.tsx +3 -3
  132. package/src/components/resources/renderers/CompositionRenderer.tsx +3 -3
  133. package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +2 -2
  134. package/src/components/resources/renderers/ContourExactReferenceNavigation.test.tsx +34 -0
  135. package/src/components/resources/renderers/ContourHTTPProxyRenderer.tsx +11 -10
  136. package/src/components/resources/renderers/CronJobRenderer.tsx +6 -4
  137. package/src/components/resources/renderers/CronWorkflowRenderer.tsx +3 -2
  138. package/src/components/resources/renderers/EndpointSliceRenderer.test.tsx +62 -0
  139. package/src/components/resources/renderers/EndpointSliceRenderer.tsx +15 -14
  140. package/src/components/resources/renderers/EventRenderer.tsx +3 -3
  141. package/src/components/resources/renderers/ExperimentRenderer.tsx +2 -2
  142. package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +2 -2
  143. package/src/components/resources/renderers/ExternalSecretRenderer.tsx +2 -2
  144. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +4 -4
  145. package/src/components/resources/renderers/GRPCRouteRenderer.tsx +11 -24
  146. package/src/components/resources/renderers/GatewayRouteReferences.test.tsx +63 -0
  147. package/src/components/resources/renderers/GitRepositoryRenderer.tsx +3 -3
  148. package/src/components/resources/renderers/HPARenderer.tsx +57 -17
  149. package/src/components/resources/renderers/HTTPRouteRenderer.tsx +12 -25
  150. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +3 -3
  151. package/src/components/resources/renderers/IngressClassReferences.test.tsx +52 -0
  152. package/src/components/resources/renderers/IngressClassRenderer.tsx +10 -3
  153. package/src/components/resources/renderers/IngressRenderer.defaultBackend.test.tsx +33 -0
  154. package/src/components/resources/renderers/IngressRenderer.tsx +36 -5
  155. package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +9 -9
  156. package/src/components/resources/renderers/IstioExactReferenceNavigation.test.tsx +34 -0
  157. package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +5 -8
  158. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +2 -14
  159. package/src/components/resources/renderers/KedaCredentials.test.tsx +85 -0
  160. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +15 -13
  161. package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +6 -3
  162. package/src/components/resources/renderers/KnativeEventingRenderer.tsx +24 -31
  163. package/src/components/resources/renderers/KnativeExactReferenceNavigation.test.tsx +47 -0
  164. package/src/components/resources/renderers/KnativeFlowRenderer.tsx +6 -7
  165. package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +7 -9
  166. package/src/components/resources/renderers/KnativeReferenceLink.tsx +14 -0
  167. package/src/components/resources/renderers/KnativeReferenceLinks.test.tsx +60 -0
  168. package/src/components/resources/renderers/KnativeSourceRenderer.tsx +9 -11
  169. package/src/components/resources/renderers/KustomizationRenderer.tsx +2 -2
  170. package/src/components/resources/renderers/KyvernoGlobalContextRenderer.tsx +3 -2
  171. package/src/components/resources/renderers/KyvernoQueueRenderers.tsx +4 -3
  172. package/src/components/resources/renderers/LeaseRenderer.tsx +3 -15
  173. package/src/components/resources/renderers/ManagedResourceRenderer.tsx +2 -2
  174. package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +3 -2
  175. package/src/components/resources/renderers/NamespaceRenderer.test.ts +69 -2
  176. package/src/components/resources/renderers/NamespaceRenderer.tsx +6 -1
  177. package/src/components/resources/renderers/NodeRenderer.test.tsx +81 -1
  178. package/src/components/resources/renderers/NodeRenderer.tsx +56 -15
  179. package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +3 -3
  180. package/src/components/resources/renderers/PodClassReferences.test.tsx +42 -0
  181. package/src/components/resources/renderers/PodNomination.test.tsx +57 -0
  182. package/src/components/resources/renderers/PodRenderer.test.tsx +32 -0
  183. package/src/components/resources/renderers/PodRenderer.tsx +62 -36
  184. package/src/components/resources/renderers/RolloutRenderer.test.tsx +1 -1
  185. package/src/components/resources/renderers/RolloutRenderer.tsx +3 -2
  186. package/src/components/resources/renderers/SbomReportRenderer.tsx +2 -2
  187. package/src/components/resources/renderers/SecretRenderer.test.tsx +88 -0
  188. package/src/components/resources/renderers/SecretRenderer.tsx +34 -5
  189. package/src/components/resources/renderers/ServiceRenderer.test.tsx +43 -0
  190. package/src/components/resources/renderers/ServiceRenderer.tsx +11 -3
  191. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +9 -22
  192. package/src/components/resources/renderers/VPARenderer.tsx +17 -11
  193. package/src/components/resources/renderers/VeleroBSLRenderer.tsx +5 -7
  194. package/src/components/resources/renderers/VeleroBackupRenderer.tsx +3 -3
  195. package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.tsx +2 -2
  196. package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +3 -3
  197. package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +2 -2
  198. package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +2 -2
  199. package/src/components/resources/renderers/WorkflowRenderer.tsx +4 -3
  200. package/src/components/resources/renderers/kyverno-modern-cells.tsx +1 -1
  201. package/src/components/resources/renderers/rollout/ReplicaSetProgression.tsx +2 -2
  202. package/src/components/resources/resource-utils-capi.ts +1 -1
  203. package/src/components/resources/resource-utils-cnpg.test.ts +35 -10
  204. package/src/components/resources/resource-utils-cnpg.ts +51 -24
  205. package/src/components/resources/resource-utils-contour.test.ts +44 -0
  206. package/src/components/resources/resource-utils-contour.ts +16 -0
  207. package/src/components/resources/resource-utils-istio.ts +12 -8
  208. package/src/components/resources/resource-utils-keda.ts +1 -7
  209. package/src/components/resources/resource-utils-knative.ts +18 -0
  210. package/src/components/resources/resource-utils-kyverno.ts +1 -7
  211. package/src/components/resources/resource-utils-velero.ts +0 -12
  212. package/src/components/resources/resource-utils.ts +99 -39
  213. package/src/components/resources/rules-column-width.test.ts +1 -1
  214. package/src/components/resources/summarize-scheduler-message.test.ts +30 -0
  215. package/src/components/shared/ActionConfirmDialog.test.tsx +154 -0
  216. package/src/components/shared/ActionConfirmDialog.tsx +246 -0
  217. package/src/components/shared/CreateResourceDialog.test.tsx +330 -0
  218. package/src/components/shared/CreateResourceDialog.tsx +126 -80
  219. package/src/components/shared/DetailShell.test.tsx +80 -0
  220. package/src/components/shared/DetailShell.tsx +85 -10
  221. package/src/components/shared/DrainPlanDialog.tsx +1 -0
  222. package/src/components/shared/EditableYamlView.test.tsx +7 -0
  223. package/src/components/shared/EditableYamlView.tsx +1 -1
  224. package/src/components/shared/GitOpsWriteWarning.test.tsx +86 -0
  225. package/src/components/shared/GitOpsWriteWarning.tsx +135 -0
  226. package/src/components/shared/ResourceActionsBar.tsx +20 -24
  227. package/src/components/shared/ResourceRendererDispatch.tsx +13 -10
  228. package/src/components/shared/SetImageDialog.test.ts +75 -6
  229. package/src/components/shared/SetImageDialog.tsx +92 -84
  230. package/src/components/shared/index.ts +4 -1
  231. package/src/components/timeline/TimelineList.tsx +2 -15
  232. package/src/components/timeline/TimelineSwimlanes.tsx +3 -11
  233. package/src/components/timeline/timeline-filters.ts +1 -0
  234. package/src/components/topology/K8sResourceNode.test.ts +18 -1
  235. package/src/components/topology/K8sResourceNode.tsx +44 -2
  236. package/src/components/topology/TopologyFilterSidebar.tsx +1 -0
  237. package/src/components/topology/TopologyGraph.tsx +8 -74
  238. package/src/components/topology/TopologySearch.tsx +1 -0
  239. package/src/components/topology/internet-groups.test.ts +35 -0
  240. package/src/components/topology/internet-groups.ts +55 -0
  241. package/src/components/topology/layout.ts +1 -1
  242. package/src/components/ui/Badge.tsx +1 -0
  243. package/src/components/ui/FoldSection.tsx +68 -0
  244. package/src/components/ui/ForceDeleteConfirmDialog.test.tsx +62 -0
  245. package/src/components/ui/ForceDeleteConfirmDialog.tsx +24 -12
  246. package/src/components/ui/RefLink.tsx +20 -0
  247. package/src/components/ui/RowActionMenu.tsx +8 -3
  248. package/src/components/ui/ScheduleValue.test.tsx +5 -0
  249. package/src/components/ui/ScheduleValue.tsx +3 -2
  250. package/src/components/ui/crd-definition-navigation.test.tsx +160 -0
  251. package/src/components/ui/drawer-components.test.tsx +175 -1
  252. package/src/components/ui/drawer-components.tsx +103 -36
  253. package/src/components/ui/index.ts +3 -1
  254. package/src/components/ui/monacoEditorFeatures.ts +77 -0
  255. package/src/components/ui/monacoRuntime.ts +14 -19
  256. package/src/components/ui/status-tone.test.ts +10 -1
  257. package/src/components/ui/status-tone.tsx +29 -0
  258. package/src/components/workload/WorkloadView.deleted.test.tsx +101 -0
  259. package/src/components/workload/WorkloadView.deletedDialog.test.tsx +45 -0
  260. package/src/components/workload/WorkloadView.relationships.test.tsx +37 -0
  261. package/src/components/workload/WorkloadView.secretSave.test.tsx +62 -0
  262. package/src/components/workload/WorkloadView.spec.test.tsx +149 -0
  263. package/src/components/workload/WorkloadView.tsx +297 -104
  264. package/src/index.ts +14 -1
  265. package/src/monaco-deep.d.ts +19 -12
  266. package/src/theme/components.css +13 -0
  267. package/src/types/capacity.ts +21 -2
  268. package/src/types/core.ts +60 -2
  269. package/src/types/gitops-flux-status.test.ts +32 -1
  270. package/src/types/gitops-insights.ts +19 -2
  271. package/src/types/gitops-tree.ts +10 -0
  272. package/src/types/gitops.ts +26 -6
  273. package/src/utils/api-resources.ts +7 -0
  274. package/src/utils/application-history.ts +1 -0
  275. package/src/utils/cnpg-backup.test.ts +26 -0
  276. package/src/utils/cnpg-backup.ts +70 -0
  277. package/src/utils/format.test.ts +49 -2
  278. package/src/utils/format.ts +9 -1
  279. package/src/utils/gateway-references.test.ts +17 -0
  280. package/src/utils/gateway-references.ts +29 -0
  281. package/src/utils/gitops-owner.ts +3 -2
  282. package/src/utils/gitops-write-guard.test.ts +401 -0
  283. package/src/utils/gitops-write-guard.ts +509 -0
  284. package/src/utils/grant.test.ts +19 -0
  285. package/src/utils/grant.ts +35 -0
  286. package/src/utils/index.ts +6 -0
  287. package/src/utils/ingress-class-references.ts +22 -0
  288. package/src/utils/k8s-errors.ts +7 -7
  289. package/src/utils/log-format.test.ts +41 -0
  290. package/src/utils/log-format.ts +71 -15
  291. package/src/utils/log-level.ts +12 -8
  292. package/src/utils/navigation.test.ts +70 -0
  293. package/src/utils/navigation.ts +91 -8
  294. package/src/utils/node-lifecycle.test.ts +36 -0
  295. package/src/utils/node-lifecycle.ts +77 -0
  296. package/src/utils/object-reference-navigation.test.ts +43 -0
  297. package/src/utils/resource-hierarchy.test.ts +69 -0
  298. package/src/utils/resource-hierarchy.ts +41 -7
  299. package/src/utils/resource-icons.ts +2 -0
  300. package/src/utils/resource-refs.test.ts +23 -0
  301. package/src/utils/resource-refs.ts +30 -0
  302. package/src/utils/skeleton-yaml.test.ts +21 -0
  303. package/src/utils/skeleton-yaml.ts +16 -0
  304. package/src/utils/url-path.test.ts +35 -0
  305. package/src/utils/url-path.ts +21 -0
  306. package/src/utils/validators.test.ts +4 -13
  307. package/src/utils/validators.ts +5 -19
  308. package/src/utils/view-transition.test.ts +35 -1
  309. package/src/utils/view-transition.ts +16 -1
@@ -4,6 +4,9 @@ import { Section, PropertyList, Property, ConditionsSection, AlertBanner } from
4
4
  import { MetricsChart } from '../../ui/MetricsChart'
5
5
  import { formatMemoryString } from '../../../utils/format'
6
6
  import { getExtendedCapacityRows } from '../../../utils/extended-resources'
7
+ import { getNodeLifecycle } from '../../../utils/node-lifecycle'
8
+ import { formatRelativeAgeTime } from '../../../utils/format'
9
+ import type { DrainPlan } from '../../shared/DrainPlanDialog'
7
10
  import type { MetricsDataPoint } from '../../../types/core'
8
11
  import { MetricsUnavailableNotice } from './MetricsUnavailableNotice'
9
12
 
@@ -15,6 +18,9 @@ interface NodeRendererProps {
15
18
  metricsHistory?: { dataPoints?: MetricsDataPoint[]; collectionError?: string; metricsUnavailableReason?: string; metricsUnavailableDiagnosis?: string }
16
19
  metricsUnavailable?: boolean
17
20
  hideMetricsServer?: boolean
21
+ removalPlan?: DrainPlan
22
+ removalPlanLoading?: boolean
23
+ removalPlanError?: string
18
24
  }
19
25
 
20
26
  // Helper to handle undefined values
@@ -31,18 +37,18 @@ function formatStorage(value: string | undefined): string {
31
37
  // Extract genuine problems from node status. Cordoned (unschedulable) is
32
38
  // deliberately NOT included here — it's an intentional operator action
33
39
  // (cordon/drain), surfaced separately as a calm advisory, not a red error.
34
- function getNodeProblems(data: any): string[] {
40
+ function getNodeProblems(data: any, lifecycle: ReturnType<typeof getNodeLifecycle>): string[] {
35
41
  const problems: string[] = []
36
42
  const conditions = data.status?.conditions || []
37
43
 
38
44
  for (const cond of conditions) {
39
45
  // NotReady is a problem when status is not True
40
- if (cond.type === 'Ready' && cond.status !== 'True') {
46
+ if (cond.type === 'Ready' && lifecycle.readinessFailed) {
41
47
  problems.push(`Node is NotReady${cond.message ? ': ' + cond.message : ''}`)
42
48
  }
43
49
 
44
50
  // These conditions are problems when True
45
- if (cond.status === 'True') {
51
+ if (cond.status === 'True' && lifecycle.problems.includes(cond.type)) {
46
52
  if (cond.type === 'DiskPressure') {
47
53
  problems.push(`Disk pressure${cond.message ? ': ' + cond.message : ''}`)
48
54
  }
@@ -61,7 +67,7 @@ function getNodeProblems(data: any): string[] {
61
67
  return problems
62
68
  }
63
69
 
64
- export function NodeRenderer({ data, relationships, onViewPods, metrics, metricsHistory, metricsUnavailable, hideMetricsServer }: NodeRendererProps) {
70
+ export function NodeRenderer({ data, relationships, onViewPods, metrics, metricsHistory, metricsUnavailable, hideMetricsServer, removalPlan, removalPlanLoading, removalPlanError }: NodeRendererProps) {
65
71
  const status = data.status || {}
66
72
  const spec = data.spec || {}
67
73
  const metadata = data.metadata || {}
@@ -73,9 +79,14 @@ export function NodeRenderer({ data, relationships, onViewPods, metrics, metrics
73
79
  const taints = spec.taints || []
74
80
 
75
81
  // Check for problems
76
- const problems = getNodeProblems(data)
82
+ const lifecycle = getNodeLifecycle(data)
83
+ const problems = getNodeProblems(data, lifecycle)
77
84
  const hasProblems = problems.length > 0
78
85
  const isCordoned = !!spec.unschedulable
86
+ const currentPlan = !removalPlanError && removalPlan?.node === metadata.name ? removalPlan : undefined
87
+ const possibleBlockers = currentPlan?.pods.filter(pod => pod.outcome === 'may-block') ?? []
88
+ const terminatingPods = currentPlan?.pods.filter(pod => pod.terminating).length
89
+ const terminationKnown = currentPlan?.pods.every(pod => pod.terminating !== undefined)
79
90
  const showMetricsUnavailable = !!metricsUnavailable && !metricsHistory?.collectionError
80
91
  const hasMetricsHistory = !!metricsHistory?.dataPoints?.length
81
92
  const currentMetrics = metricsUnavailable ? undefined : metrics
@@ -92,20 +103,50 @@ export function NodeRenderer({ data, relationships, onViewPods, metrics, metrics
92
103
  <>
93
104
  {/* Problems alert - shown at top when there are genuine issues */}
94
105
  {hasProblems && (
95
- <AlertBanner variant="error" title="Issues Detected" items={problems} />
106
+ <AlertBanner variant={lifecycle.level === 'unhealthy' ? 'error' : 'warning'} title="Issues Detected" items={problems} />
96
107
  )}
97
108
 
98
- {/* Cordoned is intentional but consequential — it removes scheduling
99
- capacity and a forgotten cordon strands a node. So it's a warning (amber),
100
- matching the node table badge + the Cordoned audit check — NOT the calm
101
- sky of a no-op intentional state (suspended/idle), and not a red error. */}
102
- {isCordoned && (
109
+ {lifecycle.removing ? (
110
+ <AlertBanner
111
+ variant={lifecycle.level === 'unhealthy' ? 'error' : lifecycle.level === 'degraded' ? 'warning' : 'info'}
112
+ title={lifecycle.delayed ? `${lifecycle.label} — taking longer than expected` : lifecycle.label}
113
+ message={lifecycle.delayed
114
+ ? 'Check remaining pods, disruption budgets and controller events. Do not uncordon during removal.'
115
+ : 'This node is marked for removal. New pods are kept off it while the controller prepares or completes removal.'}
116
+ >
117
+ <div className="mt-2 space-y-2 text-xs">
118
+ <p>{lifecycle.startedAt !== undefined ? `Marked for removal ${formatRelativeAgeTime(new Date(lifecycle.startedAt).toISOString())}.` : 'Removal start time is not reported.'}</p>
119
+ {removalPlanLoading && <p>Checking remaining pods and disruption budgets…</p>}
120
+ {removalPlanError && <p>Drain details unavailable: {removalPlanError}</p>}
121
+ {!removalPlanLoading && !removalPlanError && !currentPlan && <p>Pod and disruption-budget checks are not available from this host.</p>}
122
+ {currentPlan && (
123
+ <>
124
+ <p>{currentPlan.pods.length} pods in the current snapshot · {terminationKnown ? `${terminatingPods} terminating` : 'termination state unavailable from this Radar'} · {currentPlan.summary.skip} skipped by drain estimate (DaemonSet, static or completed).</p>
125
+ <p className="text-theme-text-secondary">Checked {formatRelativeAgeTime(currentPlan.generatedAt)}. This is a snapshot, not an eviction history. Disruption-budget checks estimate what the Eviction API would allow now.</p>
126
+ <p className="text-theme-text-secondary">Removal controllers can also hold pods marked karpenter.sh/do-not-disrupt or cluster-autoscaler.kubernetes.io/safe-to-evict=false. Those policies are not evaluated by this drain estimate.</p>
127
+ <ul className="max-h-48 overflow-y-auto space-y-1">
128
+ {currentPlan.pods.map(pod => (
129
+ <li key={`${pod.namespace}/${pod.name}`} className="flex justify-between gap-2">
130
+ <span className="break-all font-mono">{pod.namespace}/{pod.name}</span>
131
+ <span className="shrink-0 text-theme-text-secondary">{pod.terminating ? 'Terminating' : pod.outcome === 'may-block' ? 'Budget may block' : pod.outcome === 'skip' ? 'Skipped by drain estimate' : 'Remaining'}</span>
132
+ </li>
133
+ ))}
134
+ </ul>
135
+ {!currentPlan.pdbsEvaluated && <p>Disruption-budget checks are incomplete: {currentPlan.pdbError}</p>}
136
+ {possibleBlockers.length > 0 && (
137
+ <AlertBanner variant="warning" title="Disruption budgets may block removal" items={possibleBlockers.map(pod => `${pod.namespace}/${pod.name}: ${pod.reason}`)} />
138
+ )}
139
+ </>
140
+ )}
141
+ </div>
142
+ </AlertBanner>
143
+ ) : isCordoned ? (
103
144
  <AlertBanner
104
145
  variant="warning"
105
- title="Cordoned (unschedulable)"
106
- message="New pods won't be scheduled here. Uncordon to resume scheduling."
146
+ title="Cordoned"
147
+ message="New pods won't be scheduled here. The node may be under maintenance; who cordoned it is not recorded. Resume scheduling only after confirming maintenance has finished."
107
148
  />
108
- )}
149
+ ) : null}
109
150
 
110
151
  {/* Node Info */}
111
152
  <Section title="Node Info" icon={Server}>
@@ -314,7 +355,7 @@ export function NodeRenderer({ data, relationships, onViewPods, metrics, metrics
314
355
  )}
315
356
 
316
357
  {/* Conditions */}
317
- <ConditionsSection conditions={status.conditions} />
358
+ <ConditionsSection conditions={status.conditions} getConditionTone={condition => condition.type === 'Ready' && lifecycle.removing && !lifecycle.readinessFailed && condition.status !== 'True' ? 'unknown' : undefined} />
318
359
  </>
319
360
  )
320
361
  }
@@ -1,9 +1,9 @@
1
1
  import { Package, CheckCircle2 } from 'lucide-react'
2
2
  import { Section, PropertyList, Property, ConditionsSection, ProblemAlerts } from '../../ui/drawer-components'
3
- import { formatAge } from '../resource-utils'
4
3
  import { formatBytes } from '../../../utils/format'
5
4
  import { GitOpsStatusBadge, SyncCountdown } from '../../gitops'
6
5
  import { fluxConditionsToGitOpsStatus, type FluxCondition } from '../../../types/gitops'
6
+ import { TimeValue } from '../../ui/ScheduleValue'
7
7
 
8
8
  interface OCIRepositoryRendererProps {
9
9
  data: any
@@ -80,7 +80,7 @@ export function OCIRepositoryRenderer({ data }: OCIRepositoryRendererProps) {
80
80
  <Property label="Digest" value={artifact.digest} />
81
81
  <Property
82
82
  label="Last Updated"
83
- value={artifact.lastUpdateTime ? formatAge(artifact.lastUpdateTime) : '-'}
83
+ value={<TimeValue timestamp={artifact.lastUpdateTime} />}
84
84
  />
85
85
  {artifact.size && (
86
86
  <Property label="Size" value={formatBytes(artifact.size)} />
@@ -103,7 +103,7 @@ export function OCIRepositoryRenderer({ data }: OCIRepositoryRendererProps) {
103
103
  {status.lastHandledReconcileAt && (
104
104
  <Property
105
105
  label="Last Reconciled"
106
- value={formatAge(status.lastHandledReconcileAt)}
106
+ value={<TimeValue timestamp={status.lastHandledReconcileAt} />}
107
107
  />
108
108
  )}
109
109
  </PropertyList>
@@ -0,0 +1,42 @@
1
+ // @vitest-environment jsdom
2
+ import { act } from 'react'
3
+ import { createRoot } from 'react-dom/client'
4
+ import { renderToStaticMarkup } from 'react-dom/server'
5
+ import { describe, expect, it, vi } from 'vitest'
6
+ import { PodRenderer } from './PodRenderer'
7
+
8
+ const data = {
9
+ metadata: { name: 'worker', namespace: 'app' },
10
+ spec: { containers: [], priority: 1000, priorityClassName: 'urgent', runtimeClassName: 'sandbox' },
11
+ status: { phase: 'Pending' },
12
+ }
13
+
14
+ describe('PodRenderer class references', () => {
15
+ it('links explicit class names with their API groups and cluster scope', async () => {
16
+ Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })
17
+ const container = document.createElement('div')
18
+ const root = createRoot(container)
19
+ const onNavigate = vi.fn()
20
+ try {
21
+ await act(async () => root.render(<PodRenderer data={data} onCopy={() => {}} copied={null} onNavigate={onNavigate} />))
22
+ for (const [name, kind, group] of [
23
+ ['urgent', 'priorityclasses', 'scheduling.k8s.io'],
24
+ ['sandbox', 'runtimeclasses', 'node.k8s.io'],
25
+ ]) {
26
+ const button = [...container.querySelectorAll('button')].find(button => button.textContent === name)
27
+ expect(button).toBeDefined()
28
+ await act(async () => button!.click())
29
+ expect(onNavigate).toHaveBeenLastCalledWith({ kind, group, namespace: '', name })
30
+ }
31
+ } finally {
32
+ await act(async () => root.unmount())
33
+ Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: false })
34
+ }
35
+ })
36
+
37
+ it('does not infer a class from numeric priority alone', () => {
38
+ const html = renderToStaticMarkup(<PodRenderer data={{ ...data, spec: { containers: [], priority: 1000 } }} onCopy={() => {}} copied={null} />)
39
+ expect(html).not.toContain('Priority Class')
40
+ expect(html).not.toContain('Runtime Class')
41
+ })
42
+ })
@@ -0,0 +1,57 @@
1
+ // @vitest-environment jsdom
2
+ import { act } from 'react'
3
+ import { createRoot } from 'react-dom/client'
4
+ import { renderToStaticMarkup } from 'react-dom/server'
5
+ import { describe, expect, it, vi } from 'vitest'
6
+ import { PodRenderer } from './PodRenderer'
7
+
8
+ describe('PodRenderer nominated Node', () => {
9
+ it('links the nominated Node of an unbound Pod', async () => {
10
+ Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })
11
+ const container = document.createElement('div')
12
+ const root = createRoot(container)
13
+ const onNavigate = vi.fn()
14
+ try {
15
+ await act(async () => root.render(
16
+ <PodRenderer
17
+ data={{ metadata: { name: 'pending', namespace: 'app' }, spec: { containers: [] }, status: { phase: 'Pending', nominatedNodeName: 'candidate' } }}
18
+ onCopy={() => {}}
19
+ copied={null}
20
+ onNavigate={onNavigate}
21
+ />,
22
+ ))
23
+ expect(container.textContent).toContain('Nominated Node')
24
+ const button = [...container.querySelectorAll('button')].find(button => button.textContent === 'candidate')
25
+ expect(button).toBeDefined()
26
+ await act(async () => button!.click())
27
+ expect(onNavigate).toHaveBeenCalledWith({ kind: 'nodes', namespace: '', name: 'candidate', group: undefined })
28
+ } finally {
29
+ await act(async () => root.unmount())
30
+ Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: false })
31
+ }
32
+ })
33
+
34
+ it('hides a nomination on a terminal or terminating unbound Pod', () => {
35
+ for (const data of [
36
+ { metadata: { name: 'failed', namespace: 'app' }, spec: { containers: [] }, status: { phase: 'Failed', nominatedNodeName: 'stale' } },
37
+ { metadata: { name: 'deleting', namespace: 'app', deletionTimestamp: '2026-10-07T00:00:00Z' }, spec: { containers: [] }, status: { phase: 'Pending', nominatedNodeName: 'stale' } },
38
+ ]) {
39
+ const html = renderToStaticMarkup(<PodRenderer data={data} onCopy={() => {}} copied={null} />)
40
+ expect(html).not.toContain('Nominated Node')
41
+ expect(html).not.toContain('stale')
42
+ }
43
+ })
44
+
45
+ it('hides a nomination once the Pod is bound, even when it names another node', () => {
46
+ const html = renderToStaticMarkup(
47
+ <PodRenderer
48
+ data={{ metadata: { name: 'running', namespace: 'app' }, spec: { nodeName: 'assigned', containers: [] }, status: { phase: 'Running', nominatedNodeName: 'stale' } }}
49
+ onCopy={() => {}}
50
+ copied={null}
51
+ />,
52
+ )
53
+ expect(html).not.toContain('Nominated Node')
54
+ expect(html).not.toContain('stale')
55
+ expect(html).toContain('assigned')
56
+ })
57
+ })
@@ -450,3 +450,35 @@ describe('PodRenderer on a Radar without variable sources', () => {
450
450
  expect(html).not.toContain('could not be loaded')
451
451
  })
452
452
  })
453
+
454
+ describe('PodRenderer pending Pods, including non-CNPG workloads', () => {
455
+ const pending = { apiVersion: 'v1', kind: 'Pod', metadata: { name: 'nginx-join', namespace: 'default' }, spec: { containers: [{ name: 'nginx', image: 'nginx' }], initContainers: [{ name: 'setup', image: 'busybox' }] }, status: { phase: 'Pending' } }
456
+ const render = (data: any, metricsHistory: any = { containers: [], metricsAPIReachable: true }) => renderToString(<PodRenderer data={data} onCopy={() => {}} copied={null} metricsUnavailable metricsHistory={metricsHistory} />)
457
+ it('shows no usage yet only with a successful metrics API collection', () => {
458
+ expect(render(pending)).toContain('No usage yet: this Pod is not running')
459
+ expect(render(pending)).not.toContain('Radar cannot read metrics.k8s.io')
460
+ expect(render(pending, { containers: [], metricsAPIReachable: false })).toContain('Radar cannot read metrics.k8s.io')
461
+ expect(render(pending, { containers: [], collectionError: 'forbidden' })).toContain('forbidden')
462
+ expect(render({ ...pending, status: { phase: 'Running' } })).toContain('Radar cannot read metrics.k8s.io')
463
+ expect(render({ ...pending, status: { phase: 'Pending', initContainerStatuses: [{ name: 'setup', state: { running: {} } }] } })).not.toContain('No usage yet')
464
+ })
465
+ it('shows unstarted regular and init containers neutrally', () => {
466
+ const html = render(pending)
467
+ expect(html.match(/Not started/g)).toHaveLength(2)
468
+ expect(html).not.toContain('Not Ready')
469
+ expect(html).not.toMatch(/>unknown</)
470
+ })
471
+ it.each(['ContainerCreating', 'PodInitializing', 'ImagePullBackOff', 'CrashLoopBackOff'])('tones reported waiting reason %s appropriately', (reason) => {
472
+ const html = render({ ...pending, status: { phase: 'Pending', containerStatuses: [{ name: 'nginx', state: { waiting: { reason } } }] } })
473
+ const bad = ['ImagePullBackOff', 'CrashLoopBackOff'].includes(reason)
474
+ expect(html).toContain(bad ? 'bg-red-100' : 'bg-theme-hover/50')
475
+ if (bad) expect(html).toContain(reason)
476
+ })
477
+ })
478
+
479
+ it('shows reported running-but-unready nginx readiness as degraded', () => {
480
+ const data = { ...pod, status: { phase: 'Running', containerStatuses: [{ name: pod.spec.containers[0].name, ready: false, state: { running: {} } }] } }
481
+ const html = renderToString(<PodRenderer data={data} onCopy={() => {}} copied={null} />)
482
+ expect(html).toMatch(/bg-amber-100[^>]*>Not Ready</)
483
+ expect(html).toMatch(/>running</)
484
+ })
@@ -5,7 +5,7 @@ import { PolicySection } from './PolicySection'
5
5
  import type { PolicyResourceResponse } from '../../../types/policy'
6
6
  import { Section, PropertyList, Property, ConditionsSection, CopyHandler, AlertBanner, ResourceLink, useOperationalIssuesShown } from '../../ui/drawer-components'
7
7
  import { formatResources, formatDuration, getPodProblems, getPodPhaseDisplay, healthColors, SEVERITY_DOT_COLOR, getDefaultContainerName } from '../resource-utils'
8
- import { getResourceStatusColor, SEVERITY_BADGE_BORDERED } from '../../../utils/badge-colors'
8
+ import { getResourceStatusColor, SEVERITY_BORDER } from '../../../utils/badge-colors'
9
9
  import {
10
10
  rbacVerbBadgeClass,
11
11
  rbacResourceBadgeClass,
@@ -26,8 +26,10 @@ import { Tooltip } from '../../ui/Tooltip'
26
26
  import { RadarUpgradeNote } from '../../ui/RadarUpgradeNote'
27
27
  import { getRadarUpgradeRequirement, type RadarUpgradeRequirement } from '../../../types/fetch-error'
28
28
  import { MetricsChart } from '../../ui/MetricsChart'
29
+ import { Badge } from '../../ui/Badge'
29
30
  import { MetricsUnavailableNotice } from './MetricsUnavailableNotice'
30
31
  import { ContainerEnvironmentSection } from './ContainerEnvironmentSection'
32
+ import { formatRelativeAgeTime } from '../../../utils/format'
31
33
 
32
34
  function parseValidDate(dateStr: string): Date | null {
33
35
  const d = new Date(dateStr)
@@ -35,13 +37,7 @@ function parseValidDate(dateStr: string): Date | null {
35
37
  }
36
38
 
37
39
  function formatMsAgo(dateStr: string): string | null {
38
- const d = parseValidDate(dateStr)
39
- if (!d) return null
40
- const ms = Date.now() - d.getTime()
41
- if (ms < 60000) return 'just now'
42
- if (ms < 3600000) return `${Math.floor(ms / 60000)}m ago`
43
- if (ms < 86400000) return `${Math.floor(ms / 3600000)}h ago`
44
- return `${Math.floor(ms / 86400000)}d ago`
40
+ return parseValidDate(dateStr) ? formatRelativeAgeTime(dateStr) : null
45
41
  }
46
42
 
47
43
  function getRestartRecency(finishedAt: string | undefined, restarts: number): { color: string; label: string | null } {
@@ -90,7 +86,7 @@ interface PodRendererProps {
90
86
  renderPortAction?: (props: { namespace: string; podName: string; port: number; protocol: string; disabled?: boolean }) => ReactNode
91
87
  // Metrics data injection
92
88
  metrics?: { containers?: any[]; timestamp?: string }
93
- metricsHistory?: { containers?: any[]; collectionError?: string; metricsUnavailableReason?: string; metricsUnavailableDiagnosis?: string }
89
+ metricsHistory?: { metricsAPIReachable?: boolean; containers?: any[]; collectionError?: string; metricsUnavailableReason?: string; metricsUnavailableDiagnosis?: string }
94
90
  metricsUnavailable?: boolean
95
91
  hideMetricsServer?: boolean
96
92
  // Filesystem browser render props
@@ -310,6 +306,7 @@ export function PodRenderer({
310
306
  const containerStatuses = data.status?.containerStatuses || []
311
307
  const containers = data.spec?.containers || []
312
308
  const initContainers = data.spec?.initContainers || []
309
+ const podResources = data.spec?.resources
313
310
  const initContainerStatuses = data.status?.initContainerStatuses || []
314
311
  const hasEnvironmentDeclarations = [...initContainers, ...containers].some(
315
312
  (container: any) => container.env?.length > 0 || container.envFrom?.length > 0,
@@ -330,7 +327,9 @@ export function PodRenderer({
330
327
  const operationalIssuesShown = useOperationalIssuesShown()
331
328
  const podProblems = getPodProblems(data)
332
329
  const hasProblems = podProblems.length > 0 && !operationalIssuesShown
333
- const showMetricsUnavailable = !!metricsUnavailable && !metricsHistory?.collectionError
330
+ const notRunning = data.status?.phase === 'Pending' && ![...containerStatuses, ...initContainerStatuses].some((s: any) => s.state?.running)
331
+ const noUsageYet = notRunning && metricsHistory?.metricsAPIReachable === true && !metricsHistory.collectionError && !metricsHistory.containers?.length && !metrics?.containers?.length
332
+ const showMetricsUnavailable = !!metricsUnavailable && !metricsHistory?.collectionError && !noUsageYet
334
333
  const hasMetricsHistory = !!metricsHistory?.containers?.length
335
334
  const currentMetrics = metricsUnavailable ? undefined : metrics
336
335
 
@@ -453,6 +452,25 @@ export function PodRenderer({
453
452
  <Property label="Node" value={
454
453
  data.spec?.nodeName ? <ResourceLink name={data.spec.nodeName} kind="nodes" onNavigate={onNavigate} /> : undefined
455
454
  } copyable onCopy={onCopy} copied={copied} />
455
+ {/* A nomination outlives its meaning: the API server clears it on binding by
456
+ default only from Kubernetes 1.35, and pod GC fails a terminating unbound
457
+ Pod without clearing it. Only a Pending, unbound, live Pod can still use it. */}
458
+ {data.status?.nominatedNodeName && !data.spec?.nodeName && data.status?.phase === 'Pending' && !data.metadata?.deletionTimestamp && (
459
+ <Property
460
+ label={
461
+ <Tooltip content="The node this Pod is expected to run on, set by the scheduler or another component. It can change before the Pod is bound." position="right">
462
+ <span className="border-b border-dotted border-theme-text-tertiary cursor-help">Nominated Node</span>
463
+ </Tooltip>
464
+ }
465
+ value={<ResourceLink name={data.status.nominatedNodeName} kind="nodes" onNavigate={onNavigate} />}
466
+ />
467
+ )}
468
+ {data.spec?.priorityClassName && <Property label="Priority Class" value={
469
+ <ResourceLink name={data.spec.priorityClassName} kind="priorityclasses" group="scheduling.k8s.io" onNavigate={onNavigate} />
470
+ } />}
471
+ {data.spec?.runtimeClassName && <Property label="Runtime Class" value={
472
+ <ResourceLink name={data.spec.runtimeClassName} kind="runtimeclasses" group="node.k8s.io" onNavigate={onNavigate} />
473
+ } />}
456
474
  <Property label="Pod IP" value={data.status?.podIP} copyable onCopy={onCopy} copied={copied} />
457
475
  <Property label="Host IP" value={data.status?.hostIP} />
458
476
  <Property
@@ -506,10 +524,11 @@ export function PodRenderer({
506
524
  } else if (isWaiting) {
507
525
  statusLabel = state?.waiting?.reason || 'Waiting'
508
526
  } else {
509
- statusLabel = 'Pending'
527
+ statusLabel = 'Not started'
510
528
  }
529
+ const reportedFailure = isFailed || !!(isWaiting && state?.waiting?.reason && !['ContainerCreating', 'PodInitializing'].includes(state.waiting.reason))
511
530
  const statusColor = getResourceStatusColor(
512
- isCompleted ? 'succeeded' : isFailed ? 'failed' : isInitRunning ? 'running' : isWaiting ? 'waiting' : 'pending'
531
+ isCompleted ? 'succeeded' : reportedFailure ? 'failed' : isInitRunning ? 'running' : 'unknown'
513
532
  )
514
533
 
515
534
  // Build command string
@@ -520,10 +539,11 @@ export function PodRenderer({
520
539
  return (
521
540
  <div key={container.name} className={clsx(
522
541
  'rounded-lg p-3 border-l-2',
523
- isCompleted ? 'bg-theme-elevated/20 border-green-500/40' :
524
- isFailed ? 'bg-theme-elevated/30 border-red-500/50' :
525
- isInitRunning ? 'bg-theme-elevated/30 border-blue-500/50' :
526
- 'bg-theme-elevated/30 border-yellow-500/40'
542
+ isCompleted ? 'bg-theme-elevated/20' : 'bg-theme-elevated/30',
543
+ isCompleted ? SEVERITY_BORDER.success :
544
+ reportedFailure ? 'border-l-semantic-error' :
545
+ isInitRunning ? SEVERITY_BORDER.info :
546
+ SEVERITY_BORDER.neutral
527
547
  )}>
528
548
  <div className="flex items-center justify-between mb-2">
529
549
  <div className="flex items-center gap-2">
@@ -561,7 +581,7 @@ export function PodRenderer({
561
581
  </div>
562
582
  )}
563
583
  {/* Waiting reason detail */}
564
- {isWaiting && state?.waiting?.reason && state.waiting.reason !== 'PodInitializing' && (
584
+ {isWaiting && state?.waiting?.reason && !['ContainerCreating', 'PodInitializing'].includes(state.waiting.reason) && (
565
585
  <div className="text-red-400 flex items-center gap-1">
566
586
  <span className="font-medium">{state.waiting.reason}</span>
567
587
  {state.waiting.message && (
@@ -609,6 +629,20 @@ export function PodRenderer({
609
629
 
610
630
  <Section title="Containers" icon={HardDrive} defaultExpanded>
611
631
  <div className="space-y-3">
632
+ {/* Pod-level budget (spec.resources) is shared by all containers, so it
633
+ stays its own row rather than being merged into the container rows. */}
634
+ {(podResources?.requests || podResources?.limits) && (
635
+ <div className="rounded-[10px] border border-dashed border-theme-border p-3">
636
+ <div className="flex items-baseline justify-between gap-2 mb-2">
637
+ <span className="text-sm font-medium text-theme-text-primary">Pod (aggregate)</span>
638
+ <span className="text-xs text-theme-text-tertiary">Shared by all containers</span>
639
+ </div>
640
+ <div className="text-xs text-theme-text-secondary space-y-1">
641
+ {podResources.requests && <div>Requests: {formatResources(podResources.requests)}</div>}
642
+ {podResources.limits && <div>Limits: {formatResources(podResources.limits)}</div>}
643
+ </div>
644
+ </div>
645
+ )}
612
646
  {containers.map((container: any) => {
613
647
  const status = containerStatuses.find((s: any) => s.name === container.name)
614
648
  const state = status?.state
@@ -624,6 +658,7 @@ export function PodRenderer({
624
658
  // failure — tone its badges/text sky, not red, so the drawer agrees
625
659
  // with the calm "Completed" table badge instead of screaming red.
626
660
  const terminatedOk = currentTerminated?.exitCode === 0
661
+ const reportedFailure = !!((currentTerminated?.exitCode != null && currentTerminated.exitCode !== 0) || (currentWaiting?.reason && !['ContainerCreating', 'PodInitializing'].includes(currentWaiting.reason)))
627
662
 
628
663
  return (
629
664
  <div key={container.name} className="card-inner-lg">
@@ -660,23 +695,12 @@ export function PodRenderer({
660
695
  </button>
661
696
  </Tooltip>
662
697
  )}
663
- <span className={clsx(
664
- 'badge',
665
- isReady ? SEVERITY_BADGE_BORDERED.success :
666
- terminatedOk ? SEVERITY_BADGE_BORDERED.info :
667
- SEVERITY_BADGE_BORDERED.error
668
- )}>
669
- {isReady ? 'Ready' : terminatedOk ? 'Completed' : 'Not Ready'}
670
- </span>
671
- <span className={clsx(
672
- 'badge',
673
- stateKey === 'running' ? SEVERITY_BADGE_BORDERED.success :
674
- stateKey === 'waiting' ? SEVERITY_BADGE_BORDERED.warning :
675
- terminatedOk ? SEVERITY_BADGE_BORDERED.info :
676
- SEVERITY_BADGE_BORDERED.error
677
- )}>
698
+ <Badge severity={isReady ? 'success' : terminatedOk ? 'info' : reportedFailure ? 'error' : stateKey === 'running' && isReady === false ? 'warning' : 'neutral'}>
699
+ {isReady ? 'Ready' : terminatedOk ? 'Completed' : !stateKey || stateKey === 'unknown' ? 'Not started' : 'Not Ready'}
700
+ </Badge>
701
+ {stateKey !== 'unknown' && <Badge severity={stateKey === 'running' ? 'success' : terminatedOk ? 'info' : reportedFailure ? 'error' : 'neutral'}>
678
702
  {stateKey}
679
- </span>
703
+ </Badge>}
680
704
  </div>
681
705
  </div>
682
706
  <div className="text-xs text-theme-text-secondary space-y-1">
@@ -690,7 +714,7 @@ export function PodRenderer({
690
714
  )
691
715
  })()}
692
716
  {/* Show current waiting reason (e.g., CrashLoopBackOff) */}
693
- {currentWaiting?.reason && currentWaiting.reason !== 'ContainerCreating' && (
717
+ {currentWaiting?.reason && !['ContainerCreating', 'PodInitializing'].includes(currentWaiting.reason) && (
694
718
  <div className="text-red-400 flex items-center gap-1">
695
719
  <span className="font-medium">{currentWaiting.reason}</span>
696
720
  {currentWaiting.message && (
@@ -817,8 +841,9 @@ export function PodRenderer({
817
841
  )}
818
842
 
819
843
  {/* Resource Usage (from metrics-server) — hidden when Prometheus has CPU/memory data */}
820
- {!hideMetricsServer && !!(currentMetrics?.containers?.length || hasMetricsHistory || metricsHistory?.collectionError || showMetricsUnavailable) && (
844
+ {!hideMetricsServer && !!(currentMetrics?.containers?.length || hasMetricsHistory || metricsHistory?.collectionError || showMetricsUnavailable || noUsageYet) && (
821
845
  <Section title="Resource Usage" icon={Activity} defaultExpanded>
846
+ {noUsageYet && <MetricsUnavailableNotice noUsageYet />}
822
847
  {showMetricsUnavailable && (
823
848
  <MetricsUnavailableNotice rawError={metricsHistory?.metricsUnavailableReason} diagnosis={metricsHistory?.metricsUnavailableDiagnosis} />
824
849
  )}
@@ -838,7 +863,8 @@ export function PodRenderer({
838
863
  // fall through to them or their chart shows no limit line.
839
864
  const containerSpec = containers.find((c: any) => c.name === historyContainer.name)
840
865
  || initContainers.find((c: any) => c.name === historyContainer.name && c.restartPolicy === 'Always')
841
- const limits = containerSpec?.resources?.limits
866
+ // A container without its own limit is still bounded by the pod-level one.
867
+ const limits = { ...podResources?.limits, ...containerSpec?.resources?.limits }
842
868
  const requests = containerSpec?.resources?.requests
843
869
 
844
870
  // Get historical data points (from history or empty)
@@ -269,7 +269,7 @@ describe('rolloutProblems', () => {
269
269
  )
270
270
  expect(problems).toHaveLength(1)
271
271
  expect(problems[0].color).toBe('yellow')
272
- expect(problems[0].message).toMatch(/^Rollout is paused: CanaryPauseStep \(since .+\)$/)
272
+ expect(problems[0].message).toMatch(/^Rollout is paused: CanaryPauseStep \(for \d+d\)$/)
273
273
  })
274
274
 
275
275
  it('does not report a paused rollout that is actually aborted', () => {
@@ -9,6 +9,7 @@ import { CanaryStepTimeline, BlueGreenTimeline } from './rollout/CanaryStepTimel
9
9
  import { ReplicaSetProgression } from './rollout/ReplicaSetProgression'
10
10
  import { LookupFailureNote } from './LookupFailureNote'
11
11
  import type { WorkloadRevision, WorkloadPodInfo } from '../../../types'
12
+ import { formatRelativeAgeTime } from '../../../utils/format'
12
13
 
13
14
  export type RolloutAction = 'abort' | 'retry' | 'promote' | 'promote-full' | 'skip-step'
14
15
 
@@ -214,7 +215,7 @@ export function rolloutProblems(data: any): RolloutProblem[] {
214
215
 
215
216
  if (phase === 'Paused' && !status.abort) {
216
217
  const reasons = (status.pauseConditions || []).map((pc: any) => {
217
- const since = pc.startTime ? ` (since ${formatAge(pc.startTime)})` : ''
218
+ const since = pc.startTime ? ` (for ${formatAge(pc.startTime)})` : ''
218
219
  return `${pc.reason || 'Unknown'}${since}`
219
220
  })
220
221
  problems.push({
@@ -741,7 +742,7 @@ export function RolloutRenderer({ data, onNavigate, capabilities, onAction, pend
741
742
  </span>
742
743
  )}
743
744
  <span className={clsx('text-xs text-theme-text-tertiary', run.metricsTotal === 0 && 'ml-auto')}>
744
- {formatAge(run.createdAt)}
745
+ {formatRelativeAgeTime(run.createdAt)}
745
746
  </span>
746
747
  </div>
747
748
  ))}
@@ -3,8 +3,8 @@ import { Package, Search } from 'lucide-react'
3
3
  import { Section, PropertyList, Property } from '../../ui/drawer-components'
4
4
  import { Collapse, CollapseChevron, useDisclosure } from '../../ui/Collapse'
5
5
  import { Input } from '../../ui/Input'
6
- import { formatAge } from '../resource-utils'
7
6
  import { formatTrivyImage } from './trivy-shared'
7
+ import { TimeValue } from '../../ui/ScheduleValue'
8
8
 
9
9
  interface SbomReportRendererProps {
10
10
  data: any
@@ -57,7 +57,7 @@ export function SbomReportRenderer({ data }: SbomReportRendererProps) {
57
57
  <Property label="Components" value={String(componentsCount)} />
58
58
  {depsCount > 0 && <Property label="Dependencies" value={String(depsCount)} />}
59
59
  <Property label="Scanner" value={scanner.name ? `${scanner.name} ${scanner.version || ''}`.trim() : '-'} />
60
- <Property label="Scanned" value={report.updateTimestamp ? formatAge(report.updateTimestamp) + ' ago' : '-'} />
60
+ <Property label="Scanned" value={<TimeValue timestamp={report.updateTimestamp} />} />
61
61
  </PropertyList>
62
62
  </Section>
63
63