@skyhook-io/k8s-ui 1.0.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 (193) hide show
  1. package/package.json +63 -0
  2. package/src/components/dock/BottomDock.tsx +204 -0
  3. package/src/components/dock/DockContext.tsx +212 -0
  4. package/src/components/dock/TerminalTab.tsx +333 -0
  5. package/src/components/dock/index.ts +3 -0
  6. package/src/components/gitops/GitOpsActions.tsx +227 -0
  7. package/src/components/gitops/GitOpsStatusBadge.tsx +221 -0
  8. package/src/components/gitops/ManagedResourcesList.tsx +210 -0
  9. package/src/components/gitops/SyncCountdown.tsx +146 -0
  10. package/src/components/gitops/index.ts +4 -0
  11. package/src/components/logs/JsonLogLine.tsx +90 -0
  12. package/src/components/logs/LogCore.tsx +553 -0
  13. package/src/components/logs/LogToolbarSelects.tsx +74 -0
  14. package/src/components/logs/LogsViewer.tsx +146 -0
  15. package/src/components/logs/WorkloadLogsViewer.tsx +263 -0
  16. package/src/components/logs/index.ts +13 -0
  17. package/src/components/logs/useLogBuffer.ts +138 -0
  18. package/src/components/logs/useLogSearch.ts +151 -0
  19. package/src/components/logs/useLogStream.ts +61 -0
  20. package/src/components/resources/ResourcesView.tsx +5494 -0
  21. package/src/components/resources/index.ts +17 -0
  22. package/src/components/resources/renderers/AlertRenderer.tsx +166 -0
  23. package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +233 -0
  24. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +89 -0
  25. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +194 -0
  26. package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +79 -0
  27. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +68 -0
  28. package/src/components/resources/renderers/CertificateRenderer.tsx +209 -0
  29. package/src/components/resources/renderers/CertificateRequestRenderer.tsx +123 -0
  30. package/src/components/resources/renderers/ChallengeRenderer.tsx +111 -0
  31. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +164 -0
  32. package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +140 -0
  33. package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +153 -0
  34. package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +221 -0
  35. package/src/components/resources/renderers/ConfigMapRenderer.tsx +124 -0
  36. package/src/components/resources/renderers/CronJobRenderer.tsx +86 -0
  37. package/src/components/resources/renderers/EventRenderer.tsx +175 -0
  38. package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +119 -0
  39. package/src/components/resources/renderers/ExternalSecretRenderer.tsx +172 -0
  40. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +242 -0
  41. package/src/components/resources/renderers/GRPCRouteRenderer.tsx +263 -0
  42. package/src/components/resources/renderers/GatewayClassRenderer.tsx +68 -0
  43. package/src/components/resources/renderers/GatewayRenderer.tsx +218 -0
  44. package/src/components/resources/renderers/GenericRenderer.tsx +476 -0
  45. package/src/components/resources/renderers/GitRepositoryRenderer.tsx +154 -0
  46. package/src/components/resources/renderers/HPARenderer.tsx +164 -0
  47. package/src/components/resources/renderers/HTTPRouteRenderer.tsx +303 -0
  48. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +167 -0
  49. package/src/components/resources/renderers/IngressClassRenderer.tsx +49 -0
  50. package/src/components/resources/renderers/IngressRenderer.tsx +104 -0
  51. package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +175 -0
  52. package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +176 -0
  53. package/src/components/resources/renderers/IstioGatewayRenderer.tsx +125 -0
  54. package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +84 -0
  55. package/src/components/resources/renderers/IstioServiceEntryRenderer.tsx +112 -0
  56. package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +288 -0
  57. package/src/components/resources/renderers/JobRenderer.tsx +103 -0
  58. package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +157 -0
  59. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +188 -0
  60. package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +148 -0
  61. package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +90 -0
  62. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +136 -0
  63. package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +151 -0
  64. package/src/components/resources/renderers/KnativeConfigurationRenderer.tsx +70 -0
  65. package/src/components/resources/renderers/KnativeEventingRenderer.tsx +309 -0
  66. package/src/components/resources/renderers/KnativeFlowRenderer.tsx +142 -0
  67. package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +232 -0
  68. package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +97 -0
  69. package/src/components/resources/renderers/KnativeRouteRenderer.tsx +74 -0
  70. package/src/components/resources/renderers/KnativeServiceRenderer.tsx +138 -0
  71. package/src/components/resources/renderers/KnativeSourceRenderer.tsx +220 -0
  72. package/src/components/resources/renderers/KustomizationRenderer.tsx +151 -0
  73. package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +352 -0
  74. package/src/components/resources/renderers/LeaseRenderer.tsx +93 -0
  75. package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +208 -0
  76. package/src/components/resources/renderers/NodeRenderer.tsx +282 -0
  77. package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +153 -0
  78. package/src/components/resources/renderers/OrderRenderer.tsx +108 -0
  79. package/src/components/resources/renderers/PVCRenderer.tsx +98 -0
  80. package/src/components/resources/renderers/PersistentVolumeRenderer.tsx +138 -0
  81. package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +126 -0
  82. package/src/components/resources/renderers/PodMonitorRenderer.tsx +63 -0
  83. package/src/components/resources/renderers/PodRenderer.tsx +677 -0
  84. package/src/components/resources/renderers/PriorityClassRenderer.tsx +50 -0
  85. package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +62 -0
  86. package/src/components/resources/renderers/ReplicaSetRenderer.tsx +71 -0
  87. package/src/components/resources/renderers/RoleBindingRenderer.tsx +97 -0
  88. package/src/components/resources/renderers/RoleRenderer.tsx +192 -0
  89. package/src/components/resources/renderers/RolloutRenderer.tsx +227 -0
  90. package/src/components/resources/renderers/RuntimeClassRenderer.tsx +75 -0
  91. package/src/components/resources/renderers/SbomReportRenderer.tsx +98 -0
  92. package/src/components/resources/renderers/SealedSecretRenderer.tsx +121 -0
  93. package/src/components/resources/renderers/SecretRenderer.tsx +321 -0
  94. package/src/components/resources/renderers/SecretStoreRenderer.tsx +111 -0
  95. package/src/components/resources/renderers/ServiceAccountRenderer.tsx +81 -0
  96. package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +65 -0
  97. package/src/components/resources/renderers/ServiceRenderer.tsx +98 -0
  98. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +195 -0
  99. package/src/components/resources/renderers/StorageClassRenderer.tsx +82 -0
  100. package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +257 -0
  101. package/src/components/resources/renderers/VPARenderer.tsx +193 -0
  102. package/src/components/resources/renderers/VeleroBSLRenderer.tsx +73 -0
  103. package/src/components/resources/renderers/VeleroBackupRenderer.tsx +193 -0
  104. package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +161 -0
  105. package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +134 -0
  106. package/src/components/resources/renderers/VeleroVSLRenderer.tsx +36 -0
  107. package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +347 -0
  108. package/src/components/resources/renderers/WebhookConfigRenderer.tsx +156 -0
  109. package/src/components/resources/renderers/WorkflowRenderer.tsx +202 -0
  110. package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +91 -0
  111. package/src/components/resources/renderers/WorkloadRenderer.tsx +311 -0
  112. package/src/components/resources/renderers/argo-cells.tsx +118 -0
  113. package/src/components/resources/renderers/certmanager-cells.tsx +164 -0
  114. package/src/components/resources/renderers/cnpg-cells.tsx +161 -0
  115. package/src/components/resources/renderers/eso-cells.tsx +115 -0
  116. package/src/components/resources/renderers/flux-cells.tsx +253 -0
  117. package/src/components/resources/renderers/index.ts +105 -0
  118. package/src/components/resources/renderers/istio-cells.tsx +210 -0
  119. package/src/components/resources/renderers/karpenter-cells.tsx +107 -0
  120. package/src/components/resources/renderers/keda-cells.tsx +94 -0
  121. package/src/components/resources/renderers/knative-cells.tsx +396 -0
  122. package/src/components/resources/renderers/kyverno-cells.tsx +85 -0
  123. package/src/components/resources/renderers/prometheus-cells.tsx +74 -0
  124. package/src/components/resources/renderers/traefik-cells.tsx +120 -0
  125. package/src/components/resources/renderers/trivy-cells.tsx +153 -0
  126. package/src/components/resources/renderers/trivy-shared.tsx +67 -0
  127. package/src/components/resources/renderers/velero-cells.tsx +164 -0
  128. package/src/components/resources/resource-utils-argo.ts +155 -0
  129. package/src/components/resources/resource-utils-certmanager.ts +198 -0
  130. package/src/components/resources/resource-utils-cnpg.ts +344 -0
  131. package/src/components/resources/resource-utils-eso.ts +385 -0
  132. package/src/components/resources/resource-utils-flux.ts +186 -0
  133. package/src/components/resources/resource-utils-istio.ts +372 -0
  134. package/src/components/resources/resource-utils-karpenter.ts +150 -0
  135. package/src/components/resources/resource-utils-keda.ts +203 -0
  136. package/src/components/resources/resource-utils-knative.ts +218 -0
  137. package/src/components/resources/resource-utils-kyverno.ts +224 -0
  138. package/src/components/resources/resource-utils-prometheus.ts +159 -0
  139. package/src/components/resources/resource-utils-traefik.ts +180 -0
  140. package/src/components/resources/resource-utils-trivy.ts +146 -0
  141. package/src/components/resources/resource-utils-velero.ts +293 -0
  142. package/src/components/resources/resource-utils.ts +1558 -0
  143. package/src/components/shared/EditableYamlView.tsx +302 -0
  144. package/src/components/shared/ResourceActionsBar.tsx +924 -0
  145. package/src/components/shared/ResourceRendererDispatch.tsx +557 -0
  146. package/src/components/shared/index.ts +3 -0
  147. package/src/components/timeline/index.ts +1 -0
  148. package/src/components/timeline/shared.tsx +614 -0
  149. package/src/components/topology/GroupNode.tsx +198 -0
  150. package/src/components/topology/K8sResourceNode.tsx +440 -0
  151. package/src/components/topology/TopologyControls.tsx +62 -0
  152. package/src/components/topology/TopologyFilterSidebar.tsx +377 -0
  153. package/src/components/topology/TopologyGraph.tsx +748 -0
  154. package/src/components/topology/TopologySearch.tsx +304 -0
  155. package/src/components/topology/index.ts +7 -0
  156. package/src/components/topology/layout.ts +734 -0
  157. package/src/components/topology/layout.worker.ts +272 -0
  158. package/src/components/topology/topology.css +129 -0
  159. package/src/components/ui/CodeViewer.tsx +451 -0
  160. package/src/components/ui/ConfirmDialog.tsx +188 -0
  161. package/src/components/ui/ForceDeleteConfirmDialog.tsx +57 -0
  162. package/src/components/ui/HealthRing.tsx +72 -0
  163. package/src/components/ui/MetricsChart.tsx +259 -0
  164. package/src/components/ui/ResourceBar.tsx +68 -0
  165. package/src/components/ui/Toast.tsx +225 -0
  166. package/src/components/ui/Tooltip.tsx +165 -0
  167. package/src/components/ui/YamlEditor.tsx +280 -0
  168. package/src/components/ui/drawer-components.tsx +792 -0
  169. package/src/components/ui/index.ts +10 -0
  170. package/src/components/workload/ResourceDetailDrawer.tsx +158 -0
  171. package/src/components/workload/WorkloadView.tsx +1109 -0
  172. package/src/components/workload/index.ts +2 -0
  173. package/src/hooks/index.ts +9 -0
  174. package/src/hooks/useAnimatedUnmount.ts +30 -0
  175. package/src/hooks/useKeyboardShortcuts.tsx +336 -0
  176. package/src/hooks/useRefreshAnimation.ts +58 -0
  177. package/src/index.ts +35 -0
  178. package/src/types/core.ts +894 -0
  179. package/src/types/gitops.ts +458 -0
  180. package/src/types/index.ts +2 -0
  181. package/src/utils/animation.ts +40 -0
  182. package/src/utils/api-resources.ts +185 -0
  183. package/src/utils/badge-colors.ts +399 -0
  184. package/src/utils/context-name.ts +72 -0
  185. package/src/utils/download.ts +19 -0
  186. package/src/utils/format.ts +233 -0
  187. package/src/utils/index.ts +10 -0
  188. package/src/utils/log-format.ts +300 -0
  189. package/src/utils/navigation.ts +119 -0
  190. package/src/utils/resource-hierarchy.ts +630 -0
  191. package/src/utils/resource-icons.ts +206 -0
  192. package/src/vite-env.d.ts +1 -0
  193. package/tsconfig.json +24 -0
@@ -0,0 +1,677 @@
1
+ import { useState, type ReactNode } from 'react'
2
+ import { Server, HardDrive, Terminal as TerminalIcon, FileText, Activity, CirclePlay, FolderOpen } from 'lucide-react'
3
+ import { clsx } from 'clsx'
4
+ import { Section, PropertyList, Property, ConditionsSection, CopyHandler, AlertBanner, ResourceLink } from '../../ui/drawer-components'
5
+ import { formatResources, formatDuration } from '../resource-utils'
6
+ import { Tooltip } from '../../ui/Tooltip'
7
+ import { MetricsChart } from '../../ui/MetricsChart'
8
+
9
+ function parseValidDate(dateStr: string): Date | null {
10
+ const d = new Date(dateStr)
11
+ return isNaN(d.getTime()) ? null : d
12
+ }
13
+
14
+ function formatMsAgo(dateStr: string): string | null {
15
+ const d = parseValidDate(dateStr)
16
+ if (!d) return null
17
+ const ms = Date.now() - d.getTime()
18
+ if (ms < 60000) return 'just now'
19
+ if (ms < 3600000) return `${Math.floor(ms / 60000)}m ago`
20
+ if (ms < 86400000) return `${Math.floor(ms / 3600000)}h ago`
21
+ return `${Math.floor(ms / 86400000)}d ago`
22
+ }
23
+
24
+ function getRestartRecency(finishedAt: string | undefined, restarts: number): { color: string; label: string | null } {
25
+ const d = finishedAt ? parseValidDate(finishedAt) : null
26
+ const ms = d ? Date.now() - d.getTime() : null
27
+
28
+ if (ms !== null) {
29
+ let color = 'text-theme-text-tertiary'
30
+ if (ms < 10 * 60 * 1000) color = 'text-red-400'
31
+ else if (ms < 60 * 60 * 1000) color = 'text-yellow-400'
32
+ const label = formatMsAgo(finishedAt!)
33
+ return { color, label }
34
+ }
35
+
36
+ return { color: restarts > 5 ? 'text-red-400' : 'text-yellow-400', label: null }
37
+ }
38
+
39
+ function getRunDuration(startedAt: string, finishedAt: string): string | null {
40
+ const start = parseValidDate(startedAt)
41
+ const end = parseValidDate(finishedAt)
42
+ if (!start || !end) return null
43
+ const dur = end.getTime() - start.getTime()
44
+ if (dur <= 0) return null
45
+ return formatDuration(dur, true)
46
+ }
47
+
48
+ interface PodRendererProps {
49
+ data: any
50
+ onCopy: CopyHandler
51
+ copied: string | null
52
+ onNavigate?: (ref: { kind: string; namespace: string; name: string }) => void
53
+ /** When provided, container-level Logs buttons call this instead of onOpenLogsPanel */
54
+ onOpenLogs?: (podName: string, containerName: string) => void
55
+ // Platform capabilities
56
+ canExec?: boolean
57
+ canViewLogs?: boolean
58
+ canPortForward?: boolean
59
+ // Dock actions
60
+ onOpenTerminal?: (params: { namespace: string; podName: string; containerName: string; containers: string[] }) => void
61
+ onOpenLogsPanel?: (params: { namespace: string; podName: string; containers: string[]; containerName?: string }) => void
62
+ // Port forward render prop
63
+ renderPortAction?: (props: { namespace: string; podName: string; port: number; protocol: string; disabled?: boolean }) => ReactNode
64
+ // Metrics data injection
65
+ metrics?: { containers?: any[]; timestamp?: string }
66
+ metricsHistory?: { containers?: any[]; collectionError?: string }
67
+ hideMetricsServer?: boolean
68
+ // Filesystem browser render props
69
+ renderImageBrowser?: (props: { image: string; namespace: string; podName: string; pullSecrets: string[]; onClose: () => void; onSwitchToPodFiles?: () => void }) => ReactNode
70
+ renderPodBrowser?: (props: { namespace: string; podName: string; containers: string[]; initialContainer: string; onClose: () => void; onSwitchToImageFiles: () => void }) => ReactNode
71
+ }
72
+
73
+ // Extract problems from pod status and conditions
74
+ function getPodProblems(data: any): string[] {
75
+ const problems: string[] = []
76
+ const phase = data.status?.phase
77
+ const conditions = data.status?.conditions || []
78
+ const containerStatuses = data.status?.containerStatuses || []
79
+ const initContainerStatuses = data.status?.initContainerStatuses || []
80
+
81
+ // Check phase
82
+ if (phase === 'Failed' || phase === 'Unknown') {
83
+ problems.push(`Pod is in ${phase} state`)
84
+ }
85
+
86
+ // Check conditions for scheduling/init issues
87
+ for (const cond of conditions) {
88
+ if (cond.status === 'False') {
89
+ if (cond.type === 'PodScheduled' && cond.reason) {
90
+ problems.push(`Scheduling: ${cond.reason}${cond.message ? ' - ' + cond.message : ''}`)
91
+ } else if (cond.type === 'Initialized' && cond.message) {
92
+ problems.push(`Init failed: ${cond.message}`)
93
+ } else if (cond.type === 'ContainersReady' && cond.reason === 'ContainersNotReady') {
94
+ // Will be covered by container status details
95
+ } else if (cond.message) {
96
+ problems.push(`${cond.type}: ${cond.message}`)
97
+ }
98
+ }
99
+ }
100
+
101
+ // Check init container failures
102
+ for (const initStatus of initContainerStatuses) {
103
+ if (initStatus.state?.waiting?.reason && initStatus.state.waiting.reason !== 'PodInitializing') {
104
+ problems.push(`Init container "${initStatus.name}": ${initStatus.state.waiting.reason}`)
105
+ }
106
+ if (initStatus.state?.terminated?.exitCode && initStatus.state.terminated.exitCode !== 0) {
107
+ problems.push(`Init container "${initStatus.name}" failed with exit code ${initStatus.state.terminated.exitCode}`)
108
+ }
109
+ }
110
+
111
+ // Check container issues (most important)
112
+ for (const status of containerStatuses) {
113
+ const waiting = status.state?.waiting
114
+ const terminated = status.state?.terminated
115
+
116
+ if (waiting?.reason && waiting.reason !== 'ContainerCreating') {
117
+ const msg = `Container "${status.name}": ${waiting.reason}`
118
+ if (waiting.reason === 'ImagePullBackOff' || waiting.reason === 'ErrImagePull') {
119
+ problems.push(msg + (waiting.message ? ` - ${waiting.message.slice(0, 100)}` : ''))
120
+ } else if (waiting.reason === 'CrashLoopBackOff') {
121
+ problems.push(msg + ' (container keeps crashing)')
122
+ } else {
123
+ problems.push(msg)
124
+ }
125
+ }
126
+
127
+ if (terminated?.reason === 'OOMKilled') {
128
+ problems.push(`Container "${status.name}" was OOMKilled - increase memory limit`)
129
+ } else if (terminated?.exitCode && terminated.exitCode !== 0) {
130
+ problems.push(`Container "${status.name}" exited with code ${terminated.exitCode}`)
131
+ }
132
+ }
133
+
134
+ return problems
135
+ }
136
+
137
+ export function PodRenderer({
138
+ data,
139
+ onCopy,
140
+ copied,
141
+ onNavigate,
142
+ onOpenLogs: onOpenLogsOverride,
143
+ canExec,
144
+ canViewLogs,
145
+ canPortForward,
146
+ onOpenTerminal,
147
+ onOpenLogsPanel,
148
+ renderPortAction,
149
+ metrics,
150
+ metricsHistory,
151
+ hideMetricsServer,
152
+ renderImageBrowser,
153
+ renderPodBrowser,
154
+ }: PodRendererProps) {
155
+ const containerStatuses = data.status?.containerStatuses || []
156
+ const containers = data.spec?.containers || []
157
+ const initContainers = data.spec?.initContainers || []
158
+ const initContainerStatuses = data.status?.initContainerStatuses || []
159
+
160
+ const namespace = data.metadata?.namespace
161
+ const podName = data.metadata?.name
162
+ const isRunning = data.status?.phase === 'Running'
163
+
164
+ // Check for problems
165
+ const problems = getPodProblems(data)
166
+ const hasProblems = problems.length > 0
167
+
168
+ // Image filesystem modal state
169
+ const [selectedImage, setSelectedImage] = useState<string | null>(null)
170
+ const imagePullSecrets = data.spec?.imagePullSecrets?.map((s: { name: string }) => s.name) || []
171
+
172
+ // Pod filesystem modal state
173
+ const [podFilesContainer, setPodFilesContainer] = useState<string | null>(null)
174
+
175
+ const handleOpenTerminal = (containerName?: string) => {
176
+ const container = containerName || containers[0]?.name
177
+ if (namespace && podName && container) {
178
+ onOpenTerminal?.({
179
+ namespace,
180
+ podName,
181
+ containerName: container,
182
+ containers: containers.map((c: { name: string }) => c.name),
183
+ })
184
+ }
185
+ }
186
+
187
+ const allContainerNames = [
188
+ ...initContainers.map((c: { name: string }) => c.name),
189
+ ...containers.map((c: { name: string }) => c.name),
190
+ ]
191
+
192
+ const handleOpenLogs = (containerName?: string) => {
193
+ if (onOpenLogsOverride && podName && containerName) {
194
+ onOpenLogsOverride(podName, containerName)
195
+ return
196
+ }
197
+ if (namespace && podName) {
198
+ onOpenLogsPanel?.({
199
+ namespace,
200
+ podName,
201
+ containers: allContainerNames,
202
+ containerName,
203
+ })
204
+ }
205
+ }
206
+
207
+ // Render image name — clickable when renderImageBrowser is provided
208
+ const renderImageName = (image: string) => {
209
+ if (renderImageBrowser) {
210
+ return (
211
+ <Tooltip content="Browse image filesystem from registry" delay={150} position="bottom">
212
+ <button
213
+ className="truncate text-blue-400 hover:text-blue-300 hover:underline text-left w-full"
214
+ onClick={() => setSelectedImage(image)}
215
+ >
216
+ Image: {image}
217
+ </button>
218
+ </Tooltip>
219
+ )
220
+ }
221
+ return (
222
+ <div className="truncate text-theme-text-secondary">
223
+ Image: {image}
224
+ </div>
225
+ )
226
+ }
227
+
228
+ return (
229
+ <>
230
+ {/* Problems alert - shown at top when there are issues */}
231
+ {hasProblems && (
232
+ <AlertBanner variant="error" title="Issues Detected" items={problems} />
233
+ )}
234
+
235
+ {/* Status section */}
236
+ <Section title="Status" icon={Server}>
237
+ <PropertyList>
238
+ <Property label="Phase" value={data.status?.phase} />
239
+ <Property label="Node" value={
240
+ data.spec?.nodeName ? <ResourceLink name={data.spec.nodeName} kind="nodes" onNavigate={onNavigate} /> : undefined
241
+ } copyable onCopy={onCopy} copied={copied} />
242
+ <Property label="Pod IP" value={data.status?.podIP} copyable onCopy={onCopy} copied={copied} />
243
+ <Property label="Host IP" value={data.status?.hostIP} />
244
+ <Property
245
+ label={
246
+ <Tooltip
247
+ content={
248
+ data.status?.qosClass === 'Guaranteed'
249
+ ? 'Guaranteed: Pod has exact resource requests=limits. Least likely to be evicted.'
250
+ : data.status?.qosClass === 'Burstable'
251
+ ? 'Burstable: Pod has some resource requests/limits. May be evicted if node is under pressure.'
252
+ : 'BestEffort: No resource requests/limits. First to be evicted under memory pressure.'
253
+ }
254
+ position="right"
255
+ >
256
+ <span className="border-b border-dotted border-theme-text-tertiary cursor-help">QoS Class</span>
257
+ </Tooltip>
258
+ }
259
+ value={data.status?.qosClass}
260
+ />
261
+ <Property label="Service Account" value={
262
+ data.spec?.serviceAccountName ? <ResourceLink name={data.spec.serviceAccountName} kind="serviceaccounts" namespace={data.metadata?.namespace || ''} onNavigate={onNavigate} /> : undefined
263
+ } />
264
+ </PropertyList>
265
+ </Section>
266
+
267
+ {/* Init Containers - shown only when present */}
268
+ {initContainers.length > 0 && (
269
+ <Section title={`Init Containers (${initContainers.length})`} icon={CirclePlay} defaultExpanded>
270
+ <div className="space-y-3">
271
+ {initContainers.map((container: any, index: number) => {
272
+ const status = initContainerStatuses.find((s: any) => s.name === container.name)
273
+ const state = status?.state
274
+ const stateKey = state ? Object.keys(state)[0] : 'unknown'
275
+ const restarts = status?.restartCount || 0
276
+
277
+ // Determine completion status
278
+ const exitCode = state?.terminated?.exitCode
279
+ const isCompleted = stateKey === 'terminated' && exitCode === 0
280
+ const isFailed = stateKey === 'terminated' && exitCode != null && exitCode !== 0
281
+ const isWaiting = stateKey === 'waiting'
282
+ const isInitRunning = stateKey === 'running'
283
+
284
+ // Status label and color
285
+ let statusLabel: string
286
+ let statusColor: string
287
+ if (isCompleted) {
288
+ statusLabel = 'Completed'
289
+ statusColor = 'bg-green-500/20 text-green-400'
290
+ } else if (isFailed) {
291
+ statusLabel = `Exit ${exitCode}`
292
+ statusColor = 'bg-red-500/20 text-red-400'
293
+ } else if (isInitRunning) {
294
+ statusLabel = 'Running'
295
+ statusColor = 'bg-blue-500/20 text-blue-400'
296
+ } else if (isWaiting) {
297
+ statusLabel = state?.waiting?.reason || 'Waiting'
298
+ statusColor = 'bg-yellow-500/20 text-yellow-400'
299
+ } else {
300
+ statusLabel = 'Pending'
301
+ statusColor = 'bg-gray-500/20 text-gray-400'
302
+ }
303
+
304
+ // Build command string
305
+ const command = container.command || container.args
306
+ ? [...(container.command || []), ...(container.args || [])].join(' ')
307
+ : null
308
+
309
+ return (
310
+ <div key={container.name} className={clsx(
311
+ 'rounded-lg p-3 border-l-2',
312
+ isCompleted ? 'bg-theme-elevated/20 border-green-500/40' :
313
+ isFailed ? 'bg-theme-elevated/30 border-red-500/50' :
314
+ isInitRunning ? 'bg-theme-elevated/30 border-blue-500/50' :
315
+ 'bg-theme-elevated/30 border-yellow-500/40'
316
+ )}>
317
+ <div className="flex items-center justify-between mb-2">
318
+ <div className="flex items-center gap-2">
319
+ <span className="text-[10px] font-mono text-theme-text-tertiary bg-theme-elevated rounded px-1.5 py-0.5">
320
+ {index + 1}/{initContainers.length}
321
+ </span>
322
+ <span className="text-sm font-medium text-theme-text-primary">{container.name}</span>
323
+ </div>
324
+ <div className="flex items-center gap-2">
325
+ {canViewLogs && (
326
+ <Tooltip content="Logs" delay={150}>
327
+ <button
328
+ onClick={() => handleOpenLogs(container.name)}
329
+ className="p-1 text-slate-400 hover:text-blue-400 hover:bg-slate-600/50 rounded transition-colors"
330
+ >
331
+ <FileText className="w-4 h-4" />
332
+ </button>
333
+ </Tooltip>
334
+ )}
335
+ <span className={clsx('px-2 py-0.5 text-xs rounded', statusColor)}>
336
+ {statusLabel}
337
+ </span>
338
+ </div>
339
+ </div>
340
+ <div className="text-xs text-theme-text-secondary space-y-1">
341
+ {renderImageName(container.image)}
342
+ {command && (
343
+ <div className="text-theme-text-tertiary font-mono break-all">
344
+ $ {command}
345
+ </div>
346
+ )}
347
+ {restarts > 0 && (
348
+ <div className={restarts > 5 ? 'text-red-400' : 'text-yellow-400'}>
349
+ Restarts: {restarts}
350
+ </div>
351
+ )}
352
+ {/* Waiting reason detail */}
353
+ {isWaiting && state?.waiting?.reason && state.waiting.reason !== 'PodInitializing' && (
354
+ <div className="text-red-400 flex items-center gap-1">
355
+ <span className="font-medium">{state.waiting.reason}</span>
356
+ {state.waiting.message && (
357
+ <span className="text-theme-text-tertiary truncate" title={state.waiting.message}>
358
+ — {state.waiting.message.slice(0, 60)}{state.waiting.message.length > 60 ? '...' : ''}
359
+ </span>
360
+ )}
361
+ </div>
362
+ )}
363
+ {/* Failed termination detail */}
364
+ {isFailed && (
365
+ <div className="text-red-400 flex items-center gap-1">
366
+ <span className="font-medium">
367
+ {state?.terminated?.reason || 'Failed'}
368
+ </span>
369
+ {state?.terminated?.message && (
370
+ <span className="text-theme-text-tertiary truncate" title={state.terminated.message}>
371
+ — {state.terminated.message.slice(0, 80)}{state.terminated.message.length > 80 ? '...' : ''}
372
+ </span>
373
+ )}
374
+ </div>
375
+ )}
376
+ {(container.resources?.requests || container.resources?.limits) && (
377
+ <div className="flex gap-4 mt-1">
378
+ {container.resources?.requests && (
379
+ <span>Requests: {formatResources(container.resources.requests)}</span>
380
+ )}
381
+ {container.resources?.limits && (
382
+ <span>Limits: {formatResources(container.resources.limits)}</span>
383
+ )}
384
+ </div>
385
+ )}
386
+ </div>
387
+ </div>
388
+ )
389
+ })}
390
+ </div>
391
+ </Section>
392
+ )}
393
+
394
+ {/* Container Status */}
395
+ <Section title="Containers" icon={HardDrive} defaultExpanded>
396
+ <div className="space-y-3">
397
+ {containers.map((container: any) => {
398
+ const status = containerStatuses.find((s: any) => s.name === container.name)
399
+ const state = status?.state
400
+ const stateKey = state ? Object.keys(state)[0] : 'unknown'
401
+ const isReady = status?.ready
402
+ const restarts = status?.restartCount || 0
403
+
404
+ // Get last termination info for troubleshooting
405
+ const lastTermination = status?.lastState?.terminated
406
+ const currentWaiting = status?.state?.waiting
407
+ const currentTerminated = status?.state?.terminated
408
+
409
+ return (
410
+ <div key={container.name} className="bg-theme-elevated/30 rounded-lg p-3">
411
+ <div className="flex items-center justify-between mb-2">
412
+ <span className="text-sm font-medium text-theme-text-primary">{container.name}</span>
413
+ <div className="flex items-center gap-2">
414
+ {stateKey === 'running' && canExec && onOpenTerminal && (
415
+ <Tooltip content="Terminal" delay={150}>
416
+ <button
417
+ onClick={() => handleOpenTerminal(container.name)}
418
+ className="p-1 text-slate-400 hover:text-blue-400 hover:bg-slate-600/50 rounded transition-colors"
419
+ >
420
+ <TerminalIcon className="w-4 h-4" />
421
+ </button>
422
+ </Tooltip>
423
+ )}
424
+ {stateKey === 'running' && canExec && renderPodBrowser && (
425
+ <Tooltip content="Browse files" delay={150}>
426
+ <button
427
+ onClick={() => setPodFilesContainer(container.name)}
428
+ className="p-1 text-slate-400 hover:text-blue-400 hover:bg-slate-600/50 rounded transition-colors"
429
+ >
430
+ <FolderOpen className="w-4 h-4" />
431
+ </button>
432
+ </Tooltip>
433
+ )}
434
+ {canViewLogs && (
435
+ <Tooltip content="Logs" delay={150}>
436
+ <button
437
+ onClick={() => handleOpenLogs(container.name)}
438
+ className="p-1 text-slate-400 hover:text-blue-400 hover:bg-slate-600/50 rounded transition-colors"
439
+ >
440
+ <FileText className="w-4 h-4" />
441
+ </button>
442
+ </Tooltip>
443
+ )}
444
+ <span className={clsx(
445
+ 'px-2 py-0.5 text-xs rounded',
446
+ isReady ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400'
447
+ )}>
448
+ {isReady ? 'Ready' : 'Not Ready'}
449
+ </span>
450
+ <span className={clsx(
451
+ 'px-2 py-0.5 text-xs rounded',
452
+ stateKey === 'running' ? 'bg-green-500/20 text-green-400' :
453
+ stateKey === 'waiting' ? 'bg-yellow-500/20 text-yellow-400' :
454
+ 'bg-red-500/20 text-red-400'
455
+ )}>
456
+ {stateKey}
457
+ </span>
458
+ </div>
459
+ </div>
460
+ <div className="text-xs text-theme-text-secondary space-y-1">
461
+ {renderImageName(container.image)}
462
+ {restarts > 0 && (() => {
463
+ const { color, label } = getRestartRecency(lastTermination?.finishedAt, restarts)
464
+ return (
465
+ <div className={color}>
466
+ Restarts: {restarts}{label ? ` (last: ${label})` : ''}
467
+ </div>
468
+ )
469
+ })()}
470
+ {/* Show current waiting reason (e.g., CrashLoopBackOff) */}
471
+ {currentWaiting?.reason && currentWaiting.reason !== 'ContainerCreating' && (
472
+ <div className="text-red-400 flex items-center gap-1">
473
+ <span className="font-medium">{currentWaiting.reason}</span>
474
+ {currentWaiting.message && (
475
+ <span className="text-theme-text-tertiary truncate" title={currentWaiting.message}>
476
+ — {currentWaiting.message.slice(0, 60)}{currentWaiting.message.length > 60 ? '...' : ''}
477
+ </span>
478
+ )}
479
+ </div>
480
+ )}
481
+ {/* Show current terminated reason */}
482
+ {currentTerminated?.reason && (
483
+ <div className="text-red-400 flex items-center gap-1">
484
+ <span className="font-medium">Terminated: {currentTerminated.reason}</span>
485
+ {currentTerminated.exitCode !== undefined && currentTerminated.exitCode !== 0 && (
486
+ <span className="text-theme-text-tertiary">(exit code {currentTerminated.exitCode})</span>
487
+ )}
488
+ </div>
489
+ )}
490
+ {/* Show last termination info if container restarted */}
491
+ {lastTermination && restarts > 0 && !currentTerminated && (
492
+ <div className="text-amber-400/80 space-y-0.5">
493
+ <div className="flex items-center gap-1">
494
+ <span className="font-medium">Last exit: {lastTermination.reason || 'Error'}</span>
495
+ {lastTermination.exitCode !== undefined && lastTermination.exitCode !== 0 && (
496
+ <span className="text-theme-text-tertiary">(code {lastTermination.exitCode})</span>
497
+ )}
498
+ {lastTermination.reason === 'OOMKilled' && container.resources?.limits?.memory && (
499
+ <span className="text-theme-text-tertiary">— limit: {container.resources.limits.memory}</span>
500
+ )}
501
+ </div>
502
+ {lastTermination.finishedAt && (() => {
503
+ const agoLabel = formatMsAgo(lastTermination.finishedAt)
504
+ if (!agoLabel) return null
505
+ const runDur = lastTermination.startedAt
506
+ ? getRunDuration(lastTermination.startedAt, lastTermination.finishedAt)
507
+ : null
508
+ return (
509
+ <div className="text-theme-text-tertiary flex items-center gap-1">
510
+ <span>Terminated {agoLabel}</span>
511
+ {runDur && <span>(ran {runDur})</span>}
512
+ </div>
513
+ )
514
+ })()}
515
+ </div>
516
+ )}
517
+ {container.ports && container.ports.length > 0 && (
518
+ <div className="flex items-center gap-2 flex-wrap">
519
+ <span>Ports:</span>
520
+ {container.ports.map((p: any) => (
521
+ canPortForward && renderPortAction ? (
522
+ <span key={`${p.containerPort}-${p.protocol || 'TCP'}`}>
523
+ {renderPortAction({
524
+ namespace,
525
+ podName,
526
+ port: p.containerPort,
527
+ protocol: p.protocol || 'TCP',
528
+ disabled: !isRunning,
529
+ })}
530
+ </span>
531
+ ) : (
532
+ <span key={`${p.containerPort}-${p.protocol || 'TCP'}`} className="text-theme-text-tertiary">
533
+ {p.containerPort}/{p.protocol || 'TCP'}
534
+ </span>
535
+ )
536
+ ))}
537
+ </div>
538
+ )}
539
+ {(container.resources?.requests || container.resources?.limits) && (
540
+ <div className="flex gap-4 mt-1">
541
+ {container.resources?.requests && (
542
+ <span>Requests: {formatResources(container.resources.requests)}</span>
543
+ )}
544
+ {container.resources?.limits && (
545
+ <span>Limits: {formatResources(container.resources.limits)}</span>
546
+ )}
547
+ </div>
548
+ )}
549
+ </div>
550
+ </div>
551
+ )
552
+ })}
553
+ </div>
554
+ </Section>
555
+
556
+ {/* Resource Usage (from metrics-server) — hidden when Prometheus has CPU/memory data */}
557
+ {!hideMetricsServer && !!(metrics?.containers?.length || metricsHistory?.containers?.length || metricsHistory?.collectionError) && (
558
+ <Section title="Resource Usage" icon={Activity} defaultExpanded>
559
+ {metricsHistory?.collectionError && !metricsHistory?.containers?.length && (
560
+ <div className="mb-3 rounded-lg border border-yellow-500/30 bg-yellow-500/5 px-3 py-2 text-xs text-yellow-400">
561
+ <span className="font-medium">Metrics collection error:</span>{' '}
562
+ <span className="break-all">{metricsHistory.collectionError}</span>
563
+ </div>
564
+ )}
565
+ <div className="space-y-4">
566
+ {(metricsHistory?.containers || metrics?.containers || []).map((historyContainer) => {
567
+ // Find current metrics for this container
568
+ const currentMetrics = metrics?.containers?.find(c => c.name === historyContainer.name)
569
+ // Find the container spec to compare against limits
570
+ const containerSpec = containers.find((c: any) => c.name === historyContainer.name)
571
+ const limits = containerSpec?.resources?.limits
572
+ const requests = containerSpec?.resources?.requests
573
+
574
+ // Get historical data points (from history or empty)
575
+ const dataPoints = 'dataPoints' in historyContainer ? historyContainer.dataPoints : []
576
+
577
+ return (
578
+ <div key={historyContainer.name} className="bg-theme-elevated/30 rounded-lg p-3">
579
+ <div className="flex items-center justify-between mb-3">
580
+ <span className="text-sm font-medium text-theme-text-primary">{historyContainer.name}</span>
581
+ </div>
582
+
583
+ {dataPoints && dataPoints.length > 0 ? (
584
+ <div className="grid grid-cols-2 gap-6">
585
+ <div>
586
+ <div className="text-xs text-theme-text-tertiary mb-2">CPU</div>
587
+ <MetricsChart
588
+ dataPoints={dataPoints}
589
+ type="cpu"
590
+ height={80}
591
+ showAxis={true}
592
+ limit={limits?.cpu}
593
+ request={requests?.cpu}
594
+ />
595
+ </div>
596
+ <div>
597
+ <div className="text-xs text-theme-text-tertiary mb-2">Memory</div>
598
+ <MetricsChart
599
+ dataPoints={dataPoints}
600
+ type="memory"
601
+ height={80}
602
+ showAxis={true}
603
+ limit={limits?.memory}
604
+ request={requests?.memory}
605
+ />
606
+ </div>
607
+ </div>
608
+ ) : currentMetrics ? (
609
+ /* Fallback to simple display if no history yet */
610
+ <div className="grid grid-cols-2 gap-4 text-xs">
611
+ <div>
612
+ <div className="text-theme-text-tertiary mb-1">CPU</div>
613
+ <div className="flex items-baseline gap-1">
614
+ <span className="text-sm font-medium text-blue-400">{currentMetrics.usage.cpu}</span>
615
+ {limits?.cpu && (
616
+ <span className="text-theme-text-tertiary">/ {limits.cpu} limit</span>
617
+ )}
618
+ </div>
619
+ </div>
620
+ <div>
621
+ <div className="text-theme-text-tertiary mb-1">Memory</div>
622
+ <div className="flex items-baseline gap-1">
623
+ <span className="text-sm font-medium text-purple-400">{currentMetrics.usage.memory}</span>
624
+ {limits?.memory && (
625
+ <span className="text-theme-text-tertiary">/ {limits.memory} limit</span>
626
+ )}
627
+ </div>
628
+ </div>
629
+ </div>
630
+ ) : (
631
+ <div className="text-xs text-theme-text-tertiary">Collecting metrics data...</div>
632
+ )}
633
+ </div>
634
+ )
635
+ })}
636
+ </div>
637
+ {metrics?.timestamp && (
638
+ <div className="mt-2 text-xs text-theme-text-tertiary">
639
+ Last updated: {new Date(metrics.timestamp).toLocaleTimeString()}
640
+ </div>
641
+ )}
642
+ </Section>
643
+ )}
644
+
645
+ {/* Conditions */}
646
+ <ConditionsSection conditions={data.status?.conditions} />
647
+
648
+ {/* Image Filesystem Modal (via render prop) */}
649
+ {selectedImage && renderImageBrowser && renderImageBrowser({
650
+ image: selectedImage,
651
+ namespace: namespace || '',
652
+ podName: podName || '',
653
+ pullSecrets: imagePullSecrets,
654
+ onClose: () => setSelectedImage(null),
655
+ onSwitchToPodFiles: isRunning && canExec && renderPodBrowser ? () => {
656
+ // Find which container uses this image and open pod files for it
657
+ const match = containers.find((c: any) => c.image === selectedImage)
658
+ setPodFilesContainer(match?.name || containers[0]?.name)
659
+ } : undefined,
660
+ })}
661
+
662
+ {/* Pod Filesystem Modal (via render prop) */}
663
+ {podFilesContainer && renderPodBrowser && renderPodBrowser({
664
+ namespace: namespace || '',
665
+ podName: podName || '',
666
+ containers: containers.map((c: { name: string }) => c.name),
667
+ initialContainer: podFilesContainer,
668
+ onClose: () => setPodFilesContainer(null),
669
+ onSwitchToImageFiles: () => {
670
+ // Find which image this container uses and open image filesystem
671
+ const match = containers.find((c: any) => c.name === podFilesContainer)
672
+ if (match?.image) setSelectedImage(match.image)
673
+ },
674
+ })}
675
+ </>
676
+ )
677
+ }