@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.
- package/package.json +63 -0
- package/src/components/dock/BottomDock.tsx +204 -0
- package/src/components/dock/DockContext.tsx +212 -0
- package/src/components/dock/TerminalTab.tsx +333 -0
- package/src/components/dock/index.ts +3 -0
- package/src/components/gitops/GitOpsActions.tsx +227 -0
- package/src/components/gitops/GitOpsStatusBadge.tsx +221 -0
- package/src/components/gitops/ManagedResourcesList.tsx +210 -0
- package/src/components/gitops/SyncCountdown.tsx +146 -0
- package/src/components/gitops/index.ts +4 -0
- package/src/components/logs/JsonLogLine.tsx +90 -0
- package/src/components/logs/LogCore.tsx +553 -0
- package/src/components/logs/LogToolbarSelects.tsx +74 -0
- package/src/components/logs/LogsViewer.tsx +146 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +263 -0
- package/src/components/logs/index.ts +13 -0
- package/src/components/logs/useLogBuffer.ts +138 -0
- package/src/components/logs/useLogSearch.ts +151 -0
- package/src/components/logs/useLogStream.ts +61 -0
- package/src/components/resources/ResourcesView.tsx +5494 -0
- package/src/components/resources/index.ts +17 -0
- package/src/components/resources/renderers/AlertRenderer.tsx +166 -0
- package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +233 -0
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +89 -0
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +194 -0
- package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +79 -0
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +68 -0
- package/src/components/resources/renderers/CertificateRenderer.tsx +209 -0
- package/src/components/resources/renderers/CertificateRequestRenderer.tsx +123 -0
- package/src/components/resources/renderers/ChallengeRenderer.tsx +111 -0
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +164 -0
- package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +140 -0
- package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +153 -0
- package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +221 -0
- package/src/components/resources/renderers/ConfigMapRenderer.tsx +124 -0
- package/src/components/resources/renderers/CronJobRenderer.tsx +86 -0
- package/src/components/resources/renderers/EventRenderer.tsx +175 -0
- package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +119 -0
- package/src/components/resources/renderers/ExternalSecretRenderer.tsx +172 -0
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +242 -0
- package/src/components/resources/renderers/GRPCRouteRenderer.tsx +263 -0
- package/src/components/resources/renderers/GatewayClassRenderer.tsx +68 -0
- package/src/components/resources/renderers/GatewayRenderer.tsx +218 -0
- package/src/components/resources/renderers/GenericRenderer.tsx +476 -0
- package/src/components/resources/renderers/GitRepositoryRenderer.tsx +154 -0
- package/src/components/resources/renderers/HPARenderer.tsx +164 -0
- package/src/components/resources/renderers/HTTPRouteRenderer.tsx +303 -0
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +167 -0
- package/src/components/resources/renderers/IngressClassRenderer.tsx +49 -0
- package/src/components/resources/renderers/IngressRenderer.tsx +104 -0
- package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +175 -0
- package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +176 -0
- package/src/components/resources/renderers/IstioGatewayRenderer.tsx +125 -0
- package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +84 -0
- package/src/components/resources/renderers/IstioServiceEntryRenderer.tsx +112 -0
- package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +288 -0
- package/src/components/resources/renderers/JobRenderer.tsx +103 -0
- package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +157 -0
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +188 -0
- package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +148 -0
- package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +90 -0
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +136 -0
- package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +151 -0
- package/src/components/resources/renderers/KnativeConfigurationRenderer.tsx +70 -0
- package/src/components/resources/renderers/KnativeEventingRenderer.tsx +309 -0
- package/src/components/resources/renderers/KnativeFlowRenderer.tsx +142 -0
- package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +232 -0
- package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +97 -0
- package/src/components/resources/renderers/KnativeRouteRenderer.tsx +74 -0
- package/src/components/resources/renderers/KnativeServiceRenderer.tsx +138 -0
- package/src/components/resources/renderers/KnativeSourceRenderer.tsx +220 -0
- package/src/components/resources/renderers/KustomizationRenderer.tsx +151 -0
- package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +352 -0
- package/src/components/resources/renderers/LeaseRenderer.tsx +93 -0
- package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +208 -0
- package/src/components/resources/renderers/NodeRenderer.tsx +282 -0
- package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +153 -0
- package/src/components/resources/renderers/OrderRenderer.tsx +108 -0
- package/src/components/resources/renderers/PVCRenderer.tsx +98 -0
- package/src/components/resources/renderers/PersistentVolumeRenderer.tsx +138 -0
- package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +126 -0
- package/src/components/resources/renderers/PodMonitorRenderer.tsx +63 -0
- package/src/components/resources/renderers/PodRenderer.tsx +677 -0
- package/src/components/resources/renderers/PriorityClassRenderer.tsx +50 -0
- package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +62 -0
- package/src/components/resources/renderers/ReplicaSetRenderer.tsx +71 -0
- package/src/components/resources/renderers/RoleBindingRenderer.tsx +97 -0
- package/src/components/resources/renderers/RoleRenderer.tsx +192 -0
- package/src/components/resources/renderers/RolloutRenderer.tsx +227 -0
- package/src/components/resources/renderers/RuntimeClassRenderer.tsx +75 -0
- package/src/components/resources/renderers/SbomReportRenderer.tsx +98 -0
- package/src/components/resources/renderers/SealedSecretRenderer.tsx +121 -0
- package/src/components/resources/renderers/SecretRenderer.tsx +321 -0
- package/src/components/resources/renderers/SecretStoreRenderer.tsx +111 -0
- package/src/components/resources/renderers/ServiceAccountRenderer.tsx +81 -0
- package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +65 -0
- package/src/components/resources/renderers/ServiceRenderer.tsx +98 -0
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +195 -0
- package/src/components/resources/renderers/StorageClassRenderer.tsx +82 -0
- package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +257 -0
- package/src/components/resources/renderers/VPARenderer.tsx +193 -0
- package/src/components/resources/renderers/VeleroBSLRenderer.tsx +73 -0
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +193 -0
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +161 -0
- package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +134 -0
- package/src/components/resources/renderers/VeleroVSLRenderer.tsx +36 -0
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +347 -0
- package/src/components/resources/renderers/WebhookConfigRenderer.tsx +156 -0
- package/src/components/resources/renderers/WorkflowRenderer.tsx +202 -0
- package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +91 -0
- package/src/components/resources/renderers/WorkloadRenderer.tsx +311 -0
- package/src/components/resources/renderers/argo-cells.tsx +118 -0
- package/src/components/resources/renderers/certmanager-cells.tsx +164 -0
- package/src/components/resources/renderers/cnpg-cells.tsx +161 -0
- package/src/components/resources/renderers/eso-cells.tsx +115 -0
- package/src/components/resources/renderers/flux-cells.tsx +253 -0
- package/src/components/resources/renderers/index.ts +105 -0
- package/src/components/resources/renderers/istio-cells.tsx +210 -0
- package/src/components/resources/renderers/karpenter-cells.tsx +107 -0
- package/src/components/resources/renderers/keda-cells.tsx +94 -0
- package/src/components/resources/renderers/knative-cells.tsx +396 -0
- package/src/components/resources/renderers/kyverno-cells.tsx +85 -0
- package/src/components/resources/renderers/prometheus-cells.tsx +74 -0
- package/src/components/resources/renderers/traefik-cells.tsx +120 -0
- package/src/components/resources/renderers/trivy-cells.tsx +153 -0
- package/src/components/resources/renderers/trivy-shared.tsx +67 -0
- package/src/components/resources/renderers/velero-cells.tsx +164 -0
- package/src/components/resources/resource-utils-argo.ts +155 -0
- package/src/components/resources/resource-utils-certmanager.ts +198 -0
- package/src/components/resources/resource-utils-cnpg.ts +344 -0
- package/src/components/resources/resource-utils-eso.ts +385 -0
- package/src/components/resources/resource-utils-flux.ts +186 -0
- package/src/components/resources/resource-utils-istio.ts +372 -0
- package/src/components/resources/resource-utils-karpenter.ts +150 -0
- package/src/components/resources/resource-utils-keda.ts +203 -0
- package/src/components/resources/resource-utils-knative.ts +218 -0
- package/src/components/resources/resource-utils-kyverno.ts +224 -0
- package/src/components/resources/resource-utils-prometheus.ts +159 -0
- package/src/components/resources/resource-utils-traefik.ts +180 -0
- package/src/components/resources/resource-utils-trivy.ts +146 -0
- package/src/components/resources/resource-utils-velero.ts +293 -0
- package/src/components/resources/resource-utils.ts +1558 -0
- package/src/components/shared/EditableYamlView.tsx +302 -0
- package/src/components/shared/ResourceActionsBar.tsx +924 -0
- package/src/components/shared/ResourceRendererDispatch.tsx +557 -0
- package/src/components/shared/index.ts +3 -0
- package/src/components/timeline/index.ts +1 -0
- package/src/components/timeline/shared.tsx +614 -0
- package/src/components/topology/GroupNode.tsx +198 -0
- package/src/components/topology/K8sResourceNode.tsx +440 -0
- package/src/components/topology/TopologyControls.tsx +62 -0
- package/src/components/topology/TopologyFilterSidebar.tsx +377 -0
- package/src/components/topology/TopologyGraph.tsx +748 -0
- package/src/components/topology/TopologySearch.tsx +304 -0
- package/src/components/topology/index.ts +7 -0
- package/src/components/topology/layout.ts +734 -0
- package/src/components/topology/layout.worker.ts +272 -0
- package/src/components/topology/topology.css +129 -0
- package/src/components/ui/CodeViewer.tsx +451 -0
- package/src/components/ui/ConfirmDialog.tsx +188 -0
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +57 -0
- package/src/components/ui/HealthRing.tsx +72 -0
- package/src/components/ui/MetricsChart.tsx +259 -0
- package/src/components/ui/ResourceBar.tsx +68 -0
- package/src/components/ui/Toast.tsx +225 -0
- package/src/components/ui/Tooltip.tsx +165 -0
- package/src/components/ui/YamlEditor.tsx +280 -0
- package/src/components/ui/drawer-components.tsx +792 -0
- package/src/components/ui/index.ts +10 -0
- package/src/components/workload/ResourceDetailDrawer.tsx +158 -0
- package/src/components/workload/WorkloadView.tsx +1109 -0
- package/src/components/workload/index.ts +2 -0
- package/src/hooks/index.ts +9 -0
- package/src/hooks/useAnimatedUnmount.ts +30 -0
- package/src/hooks/useKeyboardShortcuts.tsx +336 -0
- package/src/hooks/useRefreshAnimation.ts +58 -0
- package/src/index.ts +35 -0
- package/src/types/core.ts +894 -0
- package/src/types/gitops.ts +458 -0
- package/src/types/index.ts +2 -0
- package/src/utils/animation.ts +40 -0
- package/src/utils/api-resources.ts +185 -0
- package/src/utils/badge-colors.ts +399 -0
- package/src/utils/context-name.ts +72 -0
- package/src/utils/download.ts +19 -0
- package/src/utils/format.ts +233 -0
- package/src/utils/index.ts +10 -0
- package/src/utils/log-format.ts +300 -0
- package/src/utils/navigation.ts +119 -0
- package/src/utils/resource-hierarchy.ts +630 -0
- package/src/utils/resource-icons.ts +206 -0
- package/src/vite-env.d.ts +1 -0
- 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
|
+
}
|