@skyhook-io/k8s-ui 1.17.0 → 1.18.0

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 (250) hide show
  1. package/README.md +41 -0
  2. package/package.json +6 -6
  3. package/src/components/charts/AreaChart.interaction.test.tsx +118 -0
  4. package/src/components/charts/AreaChart.test.tsx +25 -0
  5. package/src/components/charts/AreaChart.tsx +141 -12
  6. package/src/components/charts/index.ts +1 -0
  7. package/src/components/checks/ChecksView.tsx +18 -1
  8. package/src/components/cnpg/CNPGBackupSummary.tsx +57 -9
  9. package/src/components/cnpg/CNPGClusterHASection.test.tsx +90 -0
  10. package/src/components/cnpg/CNPGClusterHASection.tsx +331 -0
  11. package/src/components/cnpg/CNPGClusterSummary.test.tsx +275 -0
  12. package/src/components/cnpg/CNPGClusterSummary.tsx +310 -96
  13. package/src/components/cnpg/CNPGConnectSection.test.tsx +64 -0
  14. package/src/components/cnpg/CNPGConnectSection.tsx +178 -0
  15. package/src/components/cnpg/CNPGDeclarativeSummary.tsx +157 -26
  16. package/src/components/cnpg/CNPGImageCatalogSummary.tsx +7 -4
  17. package/src/components/cnpg/CNPGLogicalPath.test.tsx +22 -0
  18. package/src/components/cnpg/CNPGLogicalPath.tsx +150 -0
  19. package/src/components/cnpg/CNPGObjectStoreSummary.tsx +9 -6
  20. package/src/components/cnpg/CNPGObjectSummary.test.tsx +193 -2
  21. package/src/components/cnpg/CNPGPoolerScheduling.test.tsx +36 -0
  22. package/src/components/cnpg/CNPGPoolerSummary.tsx +241 -14
  23. package/src/components/cnpg/CNPGSharedSummary.tsx +15 -8
  24. package/src/components/cnpg/CNPGWALArchivingFact.test.tsx +39 -0
  25. package/src/components/cnpg/backupRuns.test.ts +17 -0
  26. package/src/components/cnpg/backupRuns.ts +19 -0
  27. package/src/components/cnpg/connect.test.ts +124 -0
  28. package/src/components/cnpg/connect.ts +155 -0
  29. package/src/components/cnpg/databaseRole.test.ts +38 -0
  30. package/src/components/cnpg/databaseRole.ts +80 -0
  31. package/src/components/cnpg/ha.test.ts +446 -0
  32. package/src/components/cnpg/ha.ts +621 -0
  33. package/src/components/cnpg/index.ts +18 -0
  34. package/src/components/cnpg/logicalReplication.test.ts +121 -0
  35. package/src/components/cnpg/logicalReplication.ts +252 -0
  36. package/src/components/cnpg/pooler.test.ts +84 -0
  37. package/src/components/cnpg/pooler.ts +225 -0
  38. package/src/components/cnpg/primitives.tsx +21 -117
  39. package/src/components/cnpg/problems.ts +12 -0
  40. package/src/components/cnpg/relations.test.ts +63 -13
  41. package/src/components/cnpg/relations.ts +61 -52
  42. package/src/components/cnpg/schedule.ts +35 -0
  43. package/src/components/cnpg/workspace-disk.test.ts +97 -0
  44. package/src/components/cnpg/workspace-fleet-metrics.test.ts +250 -0
  45. package/src/components/cnpg/workspace-problems.test.ts +175 -0
  46. package/src/components/cnpg/workspace.test.ts +380 -9
  47. package/src/components/cnpg/workspace.ts +1137 -136
  48. package/src/components/dock/DockContext.test.tsx +32 -0
  49. package/src/components/dock/DockContext.tsx +18 -4
  50. package/src/components/dock/LocalTerminalTab.test.tsx +40 -0
  51. package/src/components/dock/LocalTerminalTab.tsx +4 -3
  52. package/src/components/dock/TerminalTab.tsx +20 -8
  53. package/src/components/facts/certainty.tsx +36 -0
  54. package/src/components/facts/facts.test.tsx +24 -0
  55. package/src/components/facts/facts.tsx +62 -0
  56. package/src/components/facts/index.ts +6 -0
  57. package/src/components/facts/managed-by.test.tsx +17 -0
  58. package/src/components/facts/managed-by.tsx +36 -0
  59. package/src/components/facts/public-exports.test.tsx +35 -0
  60. package/src/components/gitops/GitOpsGraphFilterRail.tsx +7 -3
  61. package/src/components/gitops/GitOpsHealthSourceNotice.test.tsx +38 -0
  62. package/src/components/gitops/GitOpsHealthSourceNotice.tsx +13 -3
  63. package/src/components/gitops/insights/ArgoResourceDiff.test.tsx +142 -0
  64. package/src/components/gitops/insights/ArgoResourceDiff.tsx +22 -5
  65. package/src/components/gitops/insights/DriftPanel.tsx +62 -0
  66. package/src/components/gitops/insights/GitOpsInsightViews.tsx +40 -81
  67. package/src/components/gitops/insights/insights-helpers.test.ts +18 -0
  68. package/src/components/gitops/insights/insights-helpers.ts +7 -0
  69. package/src/components/gitops/tree/GitOpsTreeGraph.test.ts +91 -2
  70. package/src/components/gitops/tree/GitOpsTreeGraph.tsx +76 -76
  71. package/src/components/gitops/tree/layout.test.ts +111 -0
  72. package/src/components/gitops/tree/layout.ts +128 -0
  73. package/src/components/gitops/tree/tree-helpers.test.ts +23 -0
  74. package/src/components/gitops/tree/tree-helpers.ts +12 -2
  75. package/src/components/issues/ConditionalCleanup.test.tsx +82 -0
  76. package/src/components/issues/IssuesView.tsx +44 -6
  77. package/src/components/issues/ResourceIssuesSection.test.tsx +28 -0
  78. package/src/components/issues/ResourceIssuesSection.tsx +8 -2
  79. package/src/components/issues/diagnostic.ts +2 -0
  80. package/src/components/issues/index.ts +3 -1
  81. package/src/components/issues/issue-timing.ts +12 -1
  82. package/src/components/issues/issues.test.ts +75 -1
  83. package/src/components/issues/severity.ts +30 -2
  84. package/src/components/issues/types.ts +55 -4
  85. package/src/components/logs/LogCore.theme.test.tsx +206 -3
  86. package/src/components/logs/LogCore.tsx +180 -53
  87. package/src/components/logs/LogToolbarSelects.tsx +3 -1
  88. package/src/components/logs/LogsViewer.test.tsx +369 -0
  89. package/src/components/logs/LogsViewer.tsx +91 -13
  90. package/src/components/logs/StructuredLogLine.test.tsx +76 -0
  91. package/src/components/logs/StructuredLogLine.tsx +23 -31
  92. package/src/components/logs/WorkloadLogsViewer.test.tsx +65 -0
  93. package/src/components/logs/WorkloadLogsViewer.tsx +46 -11
  94. package/src/components/logs/useLogBuffer.reading.test.tsx +149 -0
  95. package/src/components/logs/useLogBuffer.test.ts +8 -0
  96. package/src/components/logs/useLogBuffer.ts +48 -18
  97. package/src/components/logs/useLogSearch.ts +3 -1
  98. package/src/components/logs/useLogStream.ts +17 -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/ColumnPickerList.test.tsx +46 -0
  105. package/src/components/resources/ColumnPickerList.tsx +174 -0
  106. package/src/components/resources/ResourcesSidebar.keyboard.test.tsx +42 -0
  107. package/src/components/resources/ResourcesSidebar.test.tsx +36 -2
  108. package/src/components/resources/ResourcesSidebar.tsx +92 -41
  109. package/src/components/resources/ResourcesView.kind-filter-memory.test.ts +52 -0
  110. package/src/components/resources/ResourcesView.kind-view.test.tsx +296 -0
  111. package/src/components/resources/ResourcesView.per-container.test.ts +56 -1
  112. package/src/components/resources/ResourcesView.tsx +392 -181
  113. package/src/components/resources/column-order.test.ts +104 -0
  114. package/src/components/resources/column-order.ts +64 -0
  115. package/src/components/resources/curated-column-ownership.test.ts +3 -3
  116. package/src/components/resources/renderers/ArgoApplicationRenderer.test.tsx +15 -0
  117. package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +4 -1
  118. package/src/components/resources/renderers/AutoscalingReferences.test.tsx +286 -0
  119. package/src/components/resources/renderers/CAPIClusterRenderer.tsx +3 -2
  120. package/src/components/resources/renderers/CAPIExactReferenceNavigation.test.tsx +27 -0
  121. package/src/components/resources/renderers/CNPGBackupRenderer.test.tsx +23 -0
  122. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +8 -19
  123. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +5 -7
  124. package/src/components/resources/renderers/CNPGDeclarativeRenderer.tsx +25 -0
  125. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.test.tsx +32 -0
  126. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +7 -15
  127. package/src/components/resources/renderers/CompositeRenderer.navigation.test.tsx +35 -0
  128. package/src/components/resources/renderers/CompositeRenderer.tsx +3 -3
  129. package/src/components/resources/renderers/CompositionRenderer.tsx +3 -3
  130. package/src/components/resources/renderers/ContourExactReferenceNavigation.test.tsx +34 -0
  131. package/src/components/resources/renderers/ContourHTTPProxyRenderer.tsx +11 -10
  132. package/src/components/resources/renderers/CronJobRenderer.test.tsx +31 -0
  133. package/src/components/resources/renderers/CronJobRenderer.tsx +28 -20
  134. package/src/components/resources/renderers/EndpointSliceRenderer.test.tsx +39 -1
  135. package/src/components/resources/renderers/EndpointSliceRenderer.tsx +14 -13
  136. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +22 -9
  137. package/src/components/resources/renderers/HPARenderer.tsx +56 -16
  138. package/src/components/resources/renderers/IngressRenderer.defaultBackend.test.tsx +33 -0
  139. package/src/components/resources/renderers/IngressRenderer.tsx +30 -3
  140. package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +9 -9
  141. package/src/components/resources/renderers/IstioExactReferenceNavigation.test.tsx +34 -0
  142. package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +5 -8
  143. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +13 -11
  144. package/src/components/resources/renderers/KnativeEventingRenderer.tsx +24 -31
  145. package/src/components/resources/renderers/KnativeExactReferenceNavigation.test.tsx +47 -0
  146. package/src/components/resources/renderers/KnativeFlowRenderer.tsx +6 -7
  147. package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +7 -9
  148. package/src/components/resources/renderers/KnativeReferenceLink.tsx +14 -0
  149. package/src/components/resources/renderers/KnativeReferenceLinks.test.tsx +60 -0
  150. package/src/components/resources/renderers/KnativeSourceRenderer.tsx +9 -11
  151. package/src/components/resources/renderers/ManagedResourceRenderer.tsx +2 -2
  152. package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +3 -2
  153. package/src/components/resources/renderers/PodRenderer.sidecar-terminal.test.tsx +200 -0
  154. package/src/components/resources/renderers/PodRenderer.test.tsx +32 -0
  155. package/src/components/resources/renderers/PodRenderer.tsx +81 -40
  156. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +1 -1
  157. package/src/components/resources/renderers/VPARenderer.tsx +17 -11
  158. package/src/components/resources/resource-utils-capi.ts +1 -1
  159. package/src/components/resources/resource-utils-cnpg.test.ts +35 -10
  160. package/src/components/resources/resource-utils-cnpg.ts +51 -20
  161. package/src/components/resources/resource-utils-contour.test.ts +44 -0
  162. package/src/components/resources/resource-utils-contour.ts +16 -0
  163. package/src/components/resources/resource-utils-istio.ts +12 -8
  164. package/src/components/resources/resource-utils-knative.ts +18 -0
  165. package/src/components/resources/resource-utils.ts +96 -7
  166. package/src/components/resources/rules-column-width.test.ts +1 -1
  167. package/src/components/resources/summarize-scheduler-message.test.ts +31 -0
  168. package/src/components/shared/ActionConfirmDialog.test.tsx +154 -0
  169. package/src/components/shared/ActionConfirmDialog.tsx +246 -0
  170. package/src/components/shared/CreateResourceDialog.test.tsx +330 -0
  171. package/src/components/shared/CreateResourceDialog.tsx +126 -80
  172. package/src/components/shared/DetailShell.test.tsx +80 -0
  173. package/src/components/shared/DetailShell.tsx +85 -10
  174. package/src/components/shared/DrainPlanDialog.test.tsx +7 -0
  175. package/src/components/shared/DrainPlanDialog.tsx +6 -2
  176. package/src/components/shared/EditableYamlView.test.tsx +7 -0
  177. package/src/components/shared/EditableYamlView.tsx +1 -1
  178. package/src/components/shared/ResourceActionsBar.tsx +14 -7
  179. package/src/components/shared/ResourceRendererDispatch.tsx +15 -10
  180. package/src/components/shared/index.ts +2 -0
  181. package/src/components/timeline/DiffViewer.test.tsx +69 -0
  182. package/src/components/timeline/DiffViewer.tsx +113 -4
  183. package/src/components/timeline/TimelineSwimlanes.tsx +1 -1
  184. package/src/components/timeline/shared.test.tsx +48 -0
  185. package/src/components/timeline/shared.tsx +14 -19
  186. package/src/components/timeline/timeline-filters.ts +1 -0
  187. package/src/components/topology/K8sResourceNode.test.ts +18 -1
  188. package/src/components/topology/K8sResourceNode.tsx +44 -2
  189. package/src/components/topology/TopologyFilterSidebar.tsx +1 -0
  190. package/src/components/topology/TopologyGraph.tsx +3 -71
  191. package/src/components/topology/TopologySearch.tsx +1 -0
  192. package/src/components/topology/internet-groups.test.ts +35 -0
  193. package/src/components/topology/internet-groups.ts +55 -0
  194. package/src/components/topology/layout.ts +1 -1
  195. package/src/components/ui/Badge.tsx +1 -0
  196. package/src/components/ui/FoldSection.tsx +68 -0
  197. package/src/components/ui/ForceDeleteConfirmDialog.test.tsx +62 -0
  198. package/src/components/ui/ForceDeleteConfirmDialog.tsx +24 -12
  199. package/src/components/ui/RefLink.tsx +20 -0
  200. package/src/components/ui/RowActionMenu.tsx +8 -3
  201. package/src/components/ui/Toast.tsx +15 -4
  202. package/src/components/ui/crd-definition-navigation.test.tsx +160 -0
  203. package/src/components/ui/drawer-components.test.tsx +110 -1
  204. package/src/components/ui/drawer-components.tsx +62 -9
  205. package/src/components/ui/index.ts +4 -2
  206. package/src/components/ui/monacoEditorFeatures.ts +77 -0
  207. package/src/components/ui/monacoRuntime.ts +14 -19
  208. package/src/components/ui/status-tone.test.ts +10 -1
  209. package/src/components/ui/status-tone.tsx +29 -0
  210. package/src/components/workload/WorkloadView.deleted.test.tsx +43 -0
  211. package/src/components/workload/WorkloadView.relationships.test.tsx +37 -0
  212. package/src/components/workload/WorkloadView.spec.test.tsx +149 -0
  213. package/src/components/workload/WorkloadView.tsx +193 -71
  214. package/src/index.ts +14 -1
  215. package/src/monaco-deep.d.ts +19 -12
  216. package/src/theme/components.css +13 -0
  217. package/src/types/capacity.ts +11 -2
  218. package/src/types/core.ts +53 -2
  219. package/src/types/gitops-flux-status.test.ts +32 -1
  220. package/src/types/gitops-insights.ts +20 -2
  221. package/src/types/gitops-tree.ts +12 -0
  222. package/src/types/gitops.ts +26 -6
  223. package/src/utils/api-resources.ts +7 -0
  224. package/src/utils/application-history.ts +1 -0
  225. package/src/utils/cnpg-backup.test.ts +26 -0
  226. package/src/utils/cnpg-backup.ts +70 -0
  227. package/src/utils/format.test.ts +9 -0
  228. package/src/utils/format.ts +4 -0
  229. package/src/utils/gitops-owner.ts +3 -2
  230. package/src/utils/grant.test.ts +19 -0
  231. package/src/utils/grant.ts +35 -0
  232. package/src/utils/index.ts +4 -0
  233. package/src/utils/k8s-errors.ts +7 -7
  234. package/src/utils/log-format.test.ts +41 -0
  235. package/src/utils/log-format.ts +71 -15
  236. package/src/utils/log-level.ts +12 -8
  237. package/src/utils/navigation.test.ts +57 -0
  238. package/src/utils/navigation.ts +86 -8
  239. package/src/utils/object-reference-navigation.test.ts +43 -0
  240. package/src/utils/resource-hierarchy.test.ts +37 -0
  241. package/src/utils/resource-hierarchy.ts +15 -7
  242. package/src/utils/resource-icons.ts +2 -0
  243. package/src/utils/skeleton-yaml.test.ts +21 -0
  244. package/src/utils/skeleton-yaml.ts +16 -0
  245. package/src/utils/url-path.test.ts +35 -0
  246. package/src/utils/url-path.ts +21 -0
  247. package/src/utils/validators.test.ts +4 -13
  248. package/src/utils/validators.ts +5 -19
  249. package/src/utils/view-transition.test.ts +35 -1
  250. package/src/utils/view-transition.ts +16 -1
@@ -0,0 +1,60 @@
1
+ // @vitest-environment jsdom
2
+ import { act } from 'react'
3
+ import { createRoot } from 'react-dom/client'
4
+ import { describe, expect, it, vi } from 'vitest'
5
+ import { PingSourceRenderer } from './KnativeSourceRenderer'
6
+ import { DomainMappingRenderer } from './KnativeNetworkingRenderer'
7
+ import { initNavigationMap, resetNavigationMap } from '../../../utils/navigation'
8
+
9
+ describe('Knative reference links', () => {
10
+ it('keep the sink reference API group instead of resolving to the core Kind', async () => {
11
+ Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })
12
+ initNavigationMap([
13
+ { group: '', version: 'v1', kind: 'Service', name: 'services', namespaced: true, isCrd: false, verbs: ['get'] },
14
+ { group: 'serving.knative.dev', version: 'v1', kind: 'Service', name: 'services', namespaced: true, isCrd: true, verbs: ['get'] },
15
+ { group: 'relationships.radar.test', version: 'v1', kind: 'Service', name: 'virtualservices', namespaced: true, isCrd: true, verbs: ['get'] },
16
+ ])
17
+ const container = document.createElement('div')
18
+ const root = createRoot(container)
19
+ const onNavigate = vi.fn()
20
+ const clickSink = async (sinkRef: Record<string, string>) => {
21
+ await act(async () => root.render(<PingSourceRenderer onNavigate={onNavigate} data={{
22
+ metadata: { namespace: 'team', name: 'ping' },
23
+ spec: { schedule: '* * * * *', sink: { ref: sinkRef } },
24
+ }} />))
25
+ const link = [...container.querySelectorAll('button')].find(button => button.textContent === sinkRef.name)
26
+ expect(link).toBeDefined()
27
+ await act(async () => link!.click())
28
+ }
29
+ try {
30
+ await clickSink({ apiVersion: 'serving.knative.dev/v1', kind: 'Service', name: 'knative-backend' })
31
+ expect(onNavigate).toHaveBeenLastCalledWith(expect.objectContaining({ kind: 'services', group: 'serving.knative.dev', namespace: 'team', name: 'knative-backend' }))
32
+ await clickSink({ group: 'serving.knative.dev', kind: 'Service', name: 'group-only-backend' })
33
+ expect(onNavigate).toHaveBeenLastCalledWith(expect.objectContaining({ kind: 'services', group: 'serving.knative.dev', name: 'group-only-backend' }))
34
+ await clickSink({ apiVersion: 'relationships.radar.test/v1', kind: 'Service', name: 'custom-backend' })
35
+ expect(onNavigate).toHaveBeenLastCalledWith(expect.objectContaining({ kind: 'virtualservices', group: 'relationships.radar.test', name: 'custom-backend' }))
36
+ await clickSink({ apiVersion: 'v1', kind: 'Service', name: 'core-backend' })
37
+ expect(onNavigate).toHaveBeenLastCalledWith(expect.objectContaining({ kind: 'services', name: 'core-backend' }))
38
+ expect(onNavigate.mock.lastCall?.[0].group ?? '').toBe('')
39
+
40
+ await act(async () => root.render(<PingSourceRenderer onNavigate={onNavigate} data={{
41
+ metadata: { namespace: 'team' }, spec: { sink: { ref: { kind: 'Service', name: 'unknown-group' } } },
42
+ }} />))
43
+ expect(container.textContent).toContain('unknown-group')
44
+ expect([...container.querySelectorAll('button')].some(button => button.textContent === 'unknown-group')).toBe(false)
45
+
46
+ await act(async () => root.render(<DomainMappingRenderer onNavigate={onNavigate} data={{
47
+ metadata: { namespace: 'team', name: 'example.com' },
48
+ spec: { ref: { apiVersion: 'serving.knative.dev/v1', kind: 'Service', name: 'mapped-backend' } },
49
+ }} />))
50
+ const target = [...container.querySelectorAll('button')].find(button => button.textContent === 'mapped-backend')
51
+ expect(target).toBeDefined()
52
+ await act(async () => target!.click())
53
+ expect(onNavigate).toHaveBeenLastCalledWith(expect.objectContaining({ kind: 'services', group: 'serving.knative.dev', name: 'mapped-backend' }))
54
+ } finally {
55
+ await act(async () => root.unmount())
56
+ resetNavigationMap()
57
+ Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: false })
58
+ }
59
+ })
60
+ })
@@ -1,12 +1,12 @@
1
+ import { KnativeReferenceLink } from './KnativeReferenceLink'
1
2
  import { Clock, Server, Container, Link2 } from 'lucide-react'
2
3
  import { clsx } from 'clsx'
3
- import { Section, PropertyList, Property, ConditionsSection, KnativeNotReadyBanner, ResourceLink } from '../../ui/drawer-components'
4
- import { kindToPlural } from '../../../utils/navigation'
4
+ import { Section, PropertyList, Property, ConditionsSection, KnativeNotReadyBanner } from '../../ui/drawer-components'
5
5
  import { getKnativeConditionStatus } from '../resource-utils-knative'
6
6
 
7
7
  interface RendererProps {
8
8
  data: any
9
- onNavigate?: (ref: { kind: string; namespace: string; name: string }) => void
9
+ onNavigate?: (ref: { kind: string; namespace: string; name: string; group?: string }) => void
10
10
  }
11
11
 
12
12
  function SinkProperty({ sink, ns, onNavigate }: { sink: any; ns: string; onNavigate?: RendererProps['onNavigate'] }) {
@@ -15,10 +15,9 @@ function SinkProperty({ sink, ns, onNavigate }: { sink: any; ns: string; onNavig
15
15
  if (sink.ref) {
16
16
  return (
17
17
  <Property label="Sink" value={
18
- <ResourceLink
19
- name={sink.ref.name}
20
- kind={kindToPlural(sink.ref.kind || 'Service')}
21
- namespace={sink.ref.namespace || ns}
18
+ <KnativeReferenceLink
19
+ reference={sink.ref}
20
+ namespace={ns}
22
21
  onNavigate={onNavigate}
23
22
  />
24
23
  } />
@@ -194,10 +193,9 @@ export function SinkBindingRenderer({ data, onNavigate }: RendererProps) {
194
193
  <Property label="Subject API" value={subjectRef.apiVersion} />
195
194
  {subjectRef.name && (
196
195
  <Property label="Subject" value={
197
- <ResourceLink
198
- name={subjectRef.name}
199
- kind={kindToPlural(subjectRef.kind || 'Deployment')}
200
- namespace={subjectRef.namespace || ns}
196
+ <KnativeReferenceLink
197
+ reference={subjectRef}
198
+ namespace={ns}
201
199
  onNavigate={onNavigate}
202
200
  />
203
201
  } />
@@ -11,7 +11,7 @@ import {
11
11
  isCrossplanePaused,
12
12
  getComposingXRRef,
13
13
  } from '../resource-utils-crossplane'
14
- import { kindToPlural } from '../../../utils/navigation'
14
+ import { kindToPluralWithGroup } from '../../../utils/navigation'
15
15
 
16
16
  interface ManagedResourceRendererProps {
17
17
  data: any
@@ -82,7 +82,7 @@ export function ManagedResourceRenderer({ data, onNavigate }: ManagedResourceRen
82
82
  value={
83
83
  <ResourceLink
84
84
  name={composingXR.name}
85
- kind={kindToPlural(composingXR.kind)}
85
+ kind={kindToPluralWithGroup(composingXR.kind, extractApiGroup(composingXR.apiVersion))}
86
86
  namespace={namespace}
87
87
  group={extractApiGroup(composingXR.apiVersion) || undefined}
88
88
  onNavigate={onNavigate}
@@ -2,16 +2,17 @@ import { Info } from 'lucide-react'
2
2
  import { Tooltip } from '../../ui/Tooltip'
3
3
 
4
4
  interface MetricsUnavailableNoticeProps {
5
+ noUsageYet?: boolean
5
6
  rawError?: string
6
7
  diagnosis?: string
7
8
  }
8
9
 
9
- export function MetricsUnavailableNotice({ rawError, diagnosis }: MetricsUnavailableNoticeProps) {
10
+ export function MetricsUnavailableNotice({ rawError, diagnosis, noUsageYet }: MetricsUnavailableNoticeProps) {
10
11
  return (
11
12
  <div className="card-inner-lg text-xs text-theme-text-tertiary">
12
13
  <div className="flex items-center gap-1.5">
13
14
  <span className="min-w-0 leading-5">
14
- Metrics unavailable. Radar cannot read metrics.k8s.io.
15
+ {noUsageYet ? 'No usage yet: this Pod is not running' : 'Metrics unavailable. Radar cannot read metrics.k8s.io.'}
15
16
  </span>
16
17
  {rawError && (
17
18
  <Tooltip
@@ -0,0 +1,200 @@
1
+ // @vitest-environment jsdom
2
+ import { act } from 'react'
3
+ import { createRoot, type Root } from 'react-dom/client'
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
5
+ import { PodRenderer } from './PodRenderer'
6
+ import { getExecContainerNames } from '../resource-utils'
7
+
8
+ Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })
9
+
10
+ const running = { running: { startedAt: '2026-10-16T10:00:00Z' } }
11
+
12
+ const pod = {
13
+ metadata: { name: 'app-with-sidecar', namespace: 'shop' },
14
+ spec: {
15
+ initContainers: [
16
+ { name: 'migrate', image: 'busybox' },
17
+ { name: 'log-shipper', image: 'fluent-bit:3', restartPolicy: 'Always' },
18
+ { name: 'stopped-sidecar', image: 'busybox', restartPolicy: 'Always' },
19
+ ],
20
+ containers: [{ name: 'app', image: 'shop/app:1' }],
21
+ },
22
+ status: {
23
+ phase: 'Running',
24
+ initContainerStatuses: [
25
+ { name: 'migrate', state: running, restartCount: 0 },
26
+ { name: 'log-shipper', state: running, restartCount: 0, ready: true },
27
+ { name: 'stopped-sidecar', state: { waiting: { reason: 'CrashLoopBackOff' } }, restartCount: 3 },
28
+ ],
29
+ containerStatuses: [{ name: 'app', state: running, restartCount: 0, ready: true }],
30
+ },
31
+ }
32
+
33
+ let container: HTMLDivElement
34
+ let root: Root
35
+
36
+ beforeEach(() => {
37
+ container = document.createElement('div')
38
+ document.body.appendChild(container)
39
+ root = createRoot(container)
40
+ })
41
+
42
+ afterEach(() => {
43
+ act(() => root.unmount())
44
+ container.remove()
45
+ })
46
+
47
+ function initCard(name: string): HTMLElement {
48
+ const label = Array.from(container.querySelectorAll('span')).find((s) => s.textContent === name)
49
+ if (!label) throw new Error(`no init container card for ${name}`)
50
+ return label.closest('.rounded-lg') as HTMLElement
51
+ }
52
+
53
+ const terminalButton = (card: HTMLElement) => card.querySelector('svg.lucide-terminal')?.closest('button') ?? null
54
+
55
+ describe('PodRenderer native sidecar terminal', () => {
56
+ it('offers Terminal on a running native sidecar only, and lists sidecars in the terminal', () => {
57
+ const onOpenTerminal = vi.fn()
58
+ act(() => {
59
+ root.render(
60
+ <PodRenderer data={pod} onCopy={() => undefined} copied={null} canExec canViewLogs onOpenTerminal={onOpenTerminal} />,
61
+ )
62
+ })
63
+
64
+ expect(terminalButton(initCard('migrate'))).toBeNull()
65
+ expect(terminalButton(initCard('stopped-sidecar'))).toBeNull()
66
+
67
+ const sidecarTerminal = terminalButton(initCard('log-shipper'))
68
+ expect(sidecarTerminal).not.toBeNull()
69
+ act(() => sidecarTerminal!.click())
70
+
71
+ expect(onOpenTerminal).toHaveBeenCalledWith({
72
+ namespace: 'shop',
73
+ podName: 'app-with-sidecar',
74
+ containerName: 'log-shipper',
75
+ containers: ['app', 'log-shipper', 'stopped-sidecar'],
76
+ })
77
+ })
78
+
79
+ it('hides the sidecar Terminal without exec permission', () => {
80
+ act(() => {
81
+ root.render(
82
+ <PodRenderer data={pod} onCopy={() => undefined} copied={null} canViewLogs onOpenTerminal={vi.fn()} />,
83
+ )
84
+ })
85
+ expect(terminalButton(initCard('log-shipper'))).toBeNull()
86
+ })
87
+ })
88
+
89
+ describe('PodRenderer sidecar files and image', () => {
90
+ it('switches between a sidecar\'s files and its image, not the first container\'s', () => {
91
+ let browser: { containers: string[]; initialContainer: string; onSwitchToImageFiles: (c?: string) => void } | undefined
92
+ let imageBrowser: { image: string; onSwitchToPodFiles?: () => void } | undefined
93
+ act(() => {
94
+ root.render(
95
+ <PodRenderer
96
+ data={pod}
97
+ onCopy={() => undefined}
98
+ copied={null}
99
+ canExec
100
+ renderPodBrowser={(props) => { browser = props; return null }}
101
+ renderImageBrowser={(props) => { imageBrowser = props; return null }}
102
+ />,
103
+ )
104
+ })
105
+
106
+ const card = initCard('log-shipper')
107
+ act(() => (card.querySelector('svg.lucide-folder-open')!.closest('button') as HTMLButtonElement).click())
108
+ expect(browser?.initialContainer).toBe('log-shipper')
109
+ expect(browser?.containers).toEqual(['app', 'log-shipper', 'stopped-sidecar'])
110
+
111
+ // The user switched the dialog to app before asking for image files.
112
+ act(() => browser!.onSwitchToImageFiles('app'))
113
+ expect(imageBrowser?.image).toBe('shop/app:1')
114
+ })
115
+
116
+ it('opens a sidecar\'s files from its image', () => {
117
+ let browser: { initialContainer: string; onSwitchToImageFiles: (c?: string) => void } | undefined
118
+ let imageBrowser: { image: string; onSwitchToPodFiles?: () => void } | undefined
119
+ act(() => {
120
+ root.render(
121
+ <PodRenderer
122
+ data={pod}
123
+ onCopy={() => undefined}
124
+ copied={null}
125
+ canExec
126
+ renderPodBrowser={(props) => { browser = props; return null }}
127
+ renderImageBrowser={(props) => { imageBrowser = props; return null }}
128
+ />,
129
+ )
130
+ })
131
+ const card = initCard('log-shipper')
132
+ act(() => (card.querySelector('svg.lucide-folder-open')!.closest('button') as HTMLButtonElement).click())
133
+ act(() => browser!.onSwitchToImageFiles())
134
+ expect(imageBrowser?.image).toBe('fluent-bit:3')
135
+ act(() => imageBrowser!.onSwitchToPodFiles!())
136
+ expect(browser?.initialContainer).toBe('log-shipper')
137
+ })
138
+ })
139
+
140
+ describe('PodRenderer image to live files', () => {
141
+ function renderWith(data: typeof pod) {
142
+ let browser: { initialContainer: string } | undefined
143
+ let imageBrowser: { image: string; onSwitchToPodFiles?: () => void } | undefined
144
+ act(() => {
145
+ root.render(
146
+ <PodRenderer
147
+ data={data}
148
+ onCopy={() => undefined}
149
+ copied={null}
150
+ canExec
151
+ renderPodBrowser={(props) => { browser = props; return null }}
152
+ renderImageBrowser={(props) => { imageBrowser = props; return null }}
153
+ />,
154
+ )
155
+ })
156
+ return { getBrowser: () => browser, getImageBrowser: () => imageBrowser }
157
+ }
158
+
159
+ const openImage = (name: string) => {
160
+ const card = initCard(name)
161
+ const link = Array.from(card.querySelectorAll('button')).find((el) => el.textContent?.startsWith('Image:'))
162
+ if (!link) throw new Error(`no image link on ${name}`)
163
+ act(() => (link as HTMLElement).click())
164
+ }
165
+
166
+ it('does not send a stopped sidecar\'s image to its live files', () => {
167
+ const { getBrowser, getImageBrowser } = renderWith(pod)
168
+ // stopped-sidecar and migrate share the busybox image; neither is a running exec target.
169
+ openImage('stopped-sidecar')
170
+ expect(getImageBrowser()?.image).toBe('busybox')
171
+ act(() => getImageBrowser()!.onSwitchToPodFiles!())
172
+ expect(getBrowser()?.initialContainer).toBe('app')
173
+ })
174
+
175
+ it('keeps the live-files link for a running sidecar while the pod is still Pending', () => {
176
+ const pending = { ...pod, status: { ...pod.status, phase: 'Pending' } }
177
+ const { getBrowser, getImageBrowser } = renderWith(pending)
178
+ openImage('log-shipper')
179
+ expect(getImageBrowser()?.onSwitchToPodFiles).toBeDefined()
180
+ act(() => getImageBrowser()!.onSwitchToPodFiles!())
181
+ expect(getBrowser()?.initialContainer).toBe('log-shipper')
182
+ })
183
+
184
+ it('offers no live-files link in a Pending pod when nothing using the image runs', () => {
185
+ const pending = { ...pod, status: { ...pod.status, phase: 'Pending' } }
186
+ const { getImageBrowser } = renderWith(pending)
187
+ openImage('stopped-sidecar')
188
+ expect(getImageBrowser()?.onSwitchToPodFiles).toBeUndefined()
189
+ })
190
+ })
191
+
192
+ describe('getExecContainerNames', () => {
193
+ it('lists regular containers first, then native sidecars, never one-shot init containers', () => {
194
+ expect(getExecContainerNames(pod)).toEqual(['app', 'log-shipper', 'stopped-sidecar'])
195
+ })
196
+
197
+ it('handles a pod without init containers', () => {
198
+ expect(getExecContainerNames({ spec: { containers: [{ name: 'app' }] } })).toEqual(['app'])
199
+ })
200
+ })
@@ -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
+ })
@@ -4,8 +4,8 @@ import { clsx } from 'clsx'
4
4
  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
- import { formatResources, formatDuration, getPodProblems, getPodPhaseDisplay, healthColors, SEVERITY_DOT_COLOR, getDefaultContainerName } from '../resource-utils'
8
- import { getResourceStatusColor, SEVERITY_BADGE_BORDERED } from '../../../utils/badge-colors'
7
+ import { formatResources, formatDuration, getPodProblems, getPodPhaseDisplay, healthColors, SEVERITY_DOT_COLOR, getDefaultContainerName, getExecContainerNames } from '../resource-utils'
8
+ import { getResourceStatusColor, SEVERITY_BORDER } from '../../../utils/badge-colors'
9
9
  import {
10
10
  rbacVerbBadgeClass,
11
11
  rbacResourceBadgeClass,
@@ -26,6 +26,7 @@ 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'
31
32
  import { formatRelativeAgeTime } from '../../../utils/format'
@@ -85,12 +86,12 @@ interface PodRendererProps {
85
86
  renderPortAction?: (props: { namespace: string; podName: string; port: number; protocol: string; disabled?: boolean }) => ReactNode
86
87
  // Metrics data injection
87
88
  metrics?: { containers?: any[]; timestamp?: string }
88
- metricsHistory?: { containers?: any[]; collectionError?: string; metricsUnavailableReason?: string; metricsUnavailableDiagnosis?: string }
89
+ metricsHistory?: { metricsAPIReachable?: boolean; containers?: any[]; collectionError?: string; metricsUnavailableReason?: string; metricsUnavailableDiagnosis?: string }
89
90
  metricsUnavailable?: boolean
90
91
  hideMetricsServer?: boolean
91
92
  // Filesystem browser render props
92
93
  renderImageBrowser?: (props: { image: string; namespace: string; podName: string; pullSecrets: string[]; onClose: () => void; onSwitchToPodFiles?: () => void }) => ReactNode
93
- renderPodBrowser?: (props: { namespace: string; podName: string; containers: string[]; initialContainer: string; onClose: () => void; onSwitchToImageFiles: () => void }) => ReactNode
94
+ renderPodBrowser?: (props: { namespace: string; podName: string; containers: string[]; initialContainer: string; onClose: () => void; onSwitchToImageFiles: (containerName?: string) => void }) => ReactNode
94
95
  /**
95
96
  * Resolved content for envFrom references.
96
97
  * When provided, expands ConfigMap/Secret keys inline instead of showing "(all keys)".
@@ -305,6 +306,7 @@ export function PodRenderer({
305
306
  const containerStatuses = data.status?.containerStatuses || []
306
307
  const containers = data.spec?.containers || []
307
308
  const initContainers = data.spec?.initContainers || []
309
+ const podResources = data.spec?.resources
308
310
  const initContainerStatuses = data.status?.initContainerStatuses || []
309
311
  const hasEnvironmentDeclarations = [...initContainers, ...containers].some(
310
312
  (container: any) => container.env?.length > 0 || container.envFrom?.length > 0,
@@ -325,7 +327,9 @@ export function PodRenderer({
325
327
  const operationalIssuesShown = useOperationalIssuesShown()
326
328
  const podProblems = getPodProblems(data)
327
329
  const hasProblems = podProblems.length > 0 && !operationalIssuesShown
328
- 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
329
333
  const hasMetricsHistory = !!metricsHistory?.containers?.length
330
334
  const currentMetrics = metricsUnavailable ? undefined : metrics
331
335
 
@@ -336,6 +340,16 @@ export function PodRenderer({
336
340
  // Pod filesystem modal state
337
341
  const [podFilesContainer, setPodFilesContainer] = useState<string | null>(null)
338
342
 
343
+ // A sidecar can be running while the pod is still Pending, and a stopped
344
+ // container has no live files, so match the image against running containers.
345
+ const runningImageContainer = selectedImage
346
+ ? getExecContainerNames(data).find((name) => {
347
+ const spec = [...containers, ...initContainers].find((c: any) => c.name === name)
348
+ const status = [...containerStatuses, ...initContainerStatuses].find((st: any) => st.name === name)
349
+ return spec?.image === selectedImage && !!status?.state?.running
350
+ })
351
+ : undefined
352
+
339
353
  const handleOpenTerminal = (containerName?: string) => {
340
354
  const container = containerName || getDefaultContainerName(data)
341
355
  if (namespace && podName && container) {
@@ -343,7 +357,7 @@ export function PodRenderer({
343
357
  namespace,
344
358
  podName,
345
359
  containerName: container,
346
- containers: containers.map((c: { name: string }) => c.name),
360
+ containers: getExecContainerNames(data),
347
361
  })
348
362
  }
349
363
  }
@@ -508,6 +522,7 @@ export function PodRenderer({
508
522
  const isFailed = stateKey === 'terminated' && exitCode != null && exitCode !== 0
509
523
  const isWaiting = stateKey === 'waiting'
510
524
  const isInitRunning = stateKey === 'running'
525
+ const isSidecar = container.restartPolicy === 'Always'
511
526
 
512
527
  // Status label and color
513
528
  let statusLabel: string
@@ -520,10 +535,11 @@ export function PodRenderer({
520
535
  } else if (isWaiting) {
521
536
  statusLabel = state?.waiting?.reason || 'Waiting'
522
537
  } else {
523
- statusLabel = 'Pending'
538
+ statusLabel = 'Not started'
524
539
  }
540
+ const reportedFailure = isFailed || !!(isWaiting && state?.waiting?.reason && !['ContainerCreating', 'PodInitializing'].includes(state.waiting.reason))
525
541
  const statusColor = getResourceStatusColor(
526
- isCompleted ? 'succeeded' : isFailed ? 'failed' : isInitRunning ? 'running' : isWaiting ? 'waiting' : 'pending'
542
+ isCompleted ? 'succeeded' : reportedFailure ? 'failed' : isInitRunning ? 'running' : 'unknown'
527
543
  )
528
544
 
529
545
  // Build command string
@@ -534,10 +550,11 @@ export function PodRenderer({
534
550
  return (
535
551
  <div key={container.name} className={clsx(
536
552
  'rounded-lg p-3 border-l-2',
537
- isCompleted ? 'bg-theme-elevated/20 border-green-500/40' :
538
- isFailed ? 'bg-theme-elevated/30 border-red-500/50' :
539
- isInitRunning ? 'bg-theme-elevated/30 border-blue-500/50' :
540
- 'bg-theme-elevated/30 border-yellow-500/40'
553
+ isCompleted ? 'bg-theme-elevated/20' : 'bg-theme-elevated/30',
554
+ isCompleted ? SEVERITY_BORDER.success :
555
+ reportedFailure ? 'border-l-semantic-error' :
556
+ isInitRunning ? SEVERITY_BORDER.info :
557
+ SEVERITY_BORDER.neutral
541
558
  )}>
542
559
  <div className="flex items-center justify-between mb-2">
543
560
  <div className="flex items-center gap-2">
@@ -547,6 +564,26 @@ export function PodRenderer({
547
564
  <span className="text-sm font-medium text-theme-text-primary">{container.name}</span>
548
565
  </div>
549
566
  <div className="flex items-center gap-2">
567
+ {isSidecar && isInitRunning && canExec && onOpenTerminal && (
568
+ <Tooltip content="Terminal" delay={150}>
569
+ <button
570
+ onClick={() => handleOpenTerminal(container.name)}
571
+ className="p-1 text-slate-400 hover:text-blue-400 hover:bg-slate-600/50 rounded transition-colors"
572
+ >
573
+ <TerminalIcon className="w-4 h-4" />
574
+ </button>
575
+ </Tooltip>
576
+ )}
577
+ {isSidecar && isInitRunning && canExec && renderPodBrowser && (
578
+ <Tooltip content="Browse files" delay={150}>
579
+ <button
580
+ onClick={() => setPodFilesContainer(container.name)}
581
+ className="p-1 text-slate-400 hover:text-blue-400 hover:bg-slate-600/50 rounded transition-colors"
582
+ >
583
+ <FolderOpen className="w-4 h-4" />
584
+ </button>
585
+ </Tooltip>
586
+ )}
550
587
  {canViewLogs && (
551
588
  <Tooltip content="Logs" delay={150}>
552
589
  <button
@@ -575,7 +612,7 @@ export function PodRenderer({
575
612
  </div>
576
613
  )}
577
614
  {/* Waiting reason detail */}
578
- {isWaiting && state?.waiting?.reason && state.waiting.reason !== 'PodInitializing' && (
615
+ {isWaiting && state?.waiting?.reason && !['ContainerCreating', 'PodInitializing'].includes(state.waiting.reason) && (
579
616
  <div className="text-red-400 flex items-center gap-1">
580
617
  <span className="font-medium">{state.waiting.reason}</span>
581
618
  {state.waiting.message && (
@@ -623,6 +660,20 @@ export function PodRenderer({
623
660
 
624
661
  <Section title="Containers" icon={HardDrive} defaultExpanded>
625
662
  <div className="space-y-3">
663
+ {/* Pod-level budget (spec.resources) is shared by all containers, so it
664
+ stays its own row rather than being merged into the container rows. */}
665
+ {(podResources?.requests || podResources?.limits) && (
666
+ <div className="rounded-[10px] border border-dashed border-theme-border p-3">
667
+ <div className="flex items-baseline justify-between gap-2 mb-2">
668
+ <span className="text-sm font-medium text-theme-text-primary">Pod (aggregate)</span>
669
+ <span className="text-xs text-theme-text-tertiary">Shared by all containers</span>
670
+ </div>
671
+ <div className="text-xs text-theme-text-secondary space-y-1">
672
+ {podResources.requests && <div>Requests: {formatResources(podResources.requests)}</div>}
673
+ {podResources.limits && <div>Limits: {formatResources(podResources.limits)}</div>}
674
+ </div>
675
+ </div>
676
+ )}
626
677
  {containers.map((container: any) => {
627
678
  const status = containerStatuses.find((s: any) => s.name === container.name)
628
679
  const state = status?.state
@@ -638,6 +689,7 @@ export function PodRenderer({
638
689
  // failure — tone its badges/text sky, not red, so the drawer agrees
639
690
  // with the calm "Completed" table badge instead of screaming red.
640
691
  const terminatedOk = currentTerminated?.exitCode === 0
692
+ const reportedFailure = !!((currentTerminated?.exitCode != null && currentTerminated.exitCode !== 0) || (currentWaiting?.reason && !['ContainerCreating', 'PodInitializing'].includes(currentWaiting.reason)))
641
693
 
642
694
  return (
643
695
  <div key={container.name} className="card-inner-lg">
@@ -674,23 +726,12 @@ export function PodRenderer({
674
726
  </button>
675
727
  </Tooltip>
676
728
  )}
677
- <span className={clsx(
678
- 'badge',
679
- isReady ? SEVERITY_BADGE_BORDERED.success :
680
- terminatedOk ? SEVERITY_BADGE_BORDERED.info :
681
- SEVERITY_BADGE_BORDERED.error
682
- )}>
683
- {isReady ? 'Ready' : terminatedOk ? 'Completed' : 'Not Ready'}
684
- </span>
685
- <span className={clsx(
686
- 'badge',
687
- stateKey === 'running' ? SEVERITY_BADGE_BORDERED.success :
688
- stateKey === 'waiting' ? SEVERITY_BADGE_BORDERED.warning :
689
- terminatedOk ? SEVERITY_BADGE_BORDERED.info :
690
- SEVERITY_BADGE_BORDERED.error
691
- )}>
729
+ <Badge severity={isReady ? 'success' : terminatedOk ? 'info' : reportedFailure ? 'error' : stateKey === 'running' && isReady === false ? 'warning' : 'neutral'}>
730
+ {isReady ? 'Ready' : terminatedOk ? 'Completed' : !stateKey || stateKey === 'unknown' ? 'Not started' : 'Not Ready'}
731
+ </Badge>
732
+ {stateKey !== 'unknown' && <Badge severity={stateKey === 'running' ? 'success' : terminatedOk ? 'info' : reportedFailure ? 'error' : 'neutral'}>
692
733
  {stateKey}
693
- </span>
734
+ </Badge>}
694
735
  </div>
695
736
  </div>
696
737
  <div className="text-xs text-theme-text-secondary space-y-1">
@@ -704,7 +745,7 @@ export function PodRenderer({
704
745
  )
705
746
  })()}
706
747
  {/* Show current waiting reason (e.g., CrashLoopBackOff) */}
707
- {currentWaiting?.reason && currentWaiting.reason !== 'ContainerCreating' && (
748
+ {currentWaiting?.reason && !['ContainerCreating', 'PodInitializing'].includes(currentWaiting.reason) && (
708
749
  <div className="text-red-400 flex items-center gap-1">
709
750
  <span className="font-medium">{currentWaiting.reason}</span>
710
751
  {currentWaiting.message && (
@@ -831,8 +872,9 @@ export function PodRenderer({
831
872
  )}
832
873
 
833
874
  {/* Resource Usage (from metrics-server) — hidden when Prometheus has CPU/memory data */}
834
- {!hideMetricsServer && !!(currentMetrics?.containers?.length || hasMetricsHistory || metricsHistory?.collectionError || showMetricsUnavailable) && (
875
+ {!hideMetricsServer && !!(currentMetrics?.containers?.length || hasMetricsHistory || metricsHistory?.collectionError || showMetricsUnavailable || noUsageYet) && (
835
876
  <Section title="Resource Usage" icon={Activity} defaultExpanded>
877
+ {noUsageYet && <MetricsUnavailableNotice noUsageYet />}
836
878
  {showMetricsUnavailable && (
837
879
  <MetricsUnavailableNotice rawError={metricsHistory?.metricsUnavailableReason} diagnosis={metricsHistory?.metricsUnavailableDiagnosis} />
838
880
  )}
@@ -852,7 +894,8 @@ export function PodRenderer({
852
894
  // fall through to them or their chart shows no limit line.
853
895
  const containerSpec = containers.find((c: any) => c.name === historyContainer.name)
854
896
  || initContainers.find((c: any) => c.name === historyContainer.name && c.restartPolicy === 'Always')
855
- const limits = containerSpec?.resources?.limits
897
+ // A container without its own limit is still bounded by the pod-level one.
898
+ const limits = { ...podResources?.limits, ...containerSpec?.resources?.limits }
856
899
  const requests = containerSpec?.resources?.requests
857
900
 
858
901
  // Get historical data points (from history or empty)
@@ -961,10 +1004,8 @@ export function PodRenderer({
961
1004
  podName: podName || '',
962
1005
  pullSecrets: imagePullSecrets,
963
1006
  onClose: () => setSelectedImage(null),
964
- onSwitchToPodFiles: isRunning && canExec && renderPodBrowser ? () => {
965
- // Find which container uses this image and open pod files for it
966
- const match = containers.find((c: any) => c.image === selectedImage)
967
- setPodFilesContainer(match?.name || containers[0]?.name)
1007
+ onSwitchToPodFiles: canExec && renderPodBrowser && (isRunning || runningImageContainer) ? () => {
1008
+ setPodFilesContainer(runningImageContainer || containers[0]?.name)
968
1009
  } : undefined,
969
1010
  })}
970
1011
 
@@ -972,12 +1013,12 @@ export function PodRenderer({
972
1013
  {podFilesContainer && renderPodBrowser && renderPodBrowser({
973
1014
  namespace: namespace || '',
974
1015
  podName: podName || '',
975
- containers: containers.map((c: { name: string }) => c.name),
1016
+ containers: getExecContainerNames(data),
976
1017
  initialContainer: podFilesContainer,
977
1018
  onClose: () => setPodFilesContainer(null),
978
- onSwitchToImageFiles: () => {
979
- // Find which image this container uses and open image filesystem
980
- const match = containers.find((c: any) => c.name === podFilesContainer)
1019
+ onSwitchToImageFiles: (containerName?: string) => {
1020
+ const selected = containerName || podFilesContainer
1021
+ const match = [...containers, ...initContainers].find((c: any) => c.name === selected)
981
1022
  if (match?.image) setSelectedImage(match.image)
982
1023
  },
983
1024
  })}