@skyhook-io/k8s-ui 1.1.1 → 1.2.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 +6 -2
- package/src/components/dock/BottomDock.tsx +14 -15
- package/src/components/dock/DockContext.tsx +40 -1
- package/src/components/dock/LocalTerminalTab.tsx +241 -0
- package/src/components/dock/NodeTerminalTab.tsx +129 -0
- package/src/components/dock/TerminalTab.tsx +1 -1
- package/src/components/dock/index.ts +2 -0
- package/src/components/gitops/GitOpsStatusBadge.tsx +18 -67
- package/src/components/logs/LogCore.tsx +30 -4
- package/src/components/logs/StructuredLogLine.tsx +168 -0
- package/src/components/logs/index.ts +1 -1
- package/src/components/logs/useLogBuffer.ts +16 -10
- package/src/components/resources/ResourcesView.tsx +136 -24
- package/src/components/resources/renderers/ContourHTTPProxyRenderer.tsx +207 -0
- package/src/components/resources/renderers/PodRenderer.tsx +164 -8
- package/src/components/resources/renderers/contour-cells.tsx +48 -0
- package/src/components/resources/renderers/index.ts +2 -0
- package/src/components/resources/renderers/trivy-shared.tsx +9 -25
- package/src/components/resources/resource-utils-contour.ts +34 -0
- package/src/components/resources/resource-utils.ts +91 -0
- package/src/components/shared/ResourceActionsBar.tsx +135 -2
- package/src/components/shared/ResourceRendererDispatch.tsx +23 -5
- package/src/components/timeline/shared.tsx +8 -10
- package/src/components/topology/K8sResourceNode.tsx +1 -0
- package/src/components/topology/TopologyFilterSidebar.tsx +3 -0
- package/src/components/topology/layout.ts +1 -0
- package/src/components/topology/topology.css +1 -0
- package/src/components/ui/CodeViewer.tsx +1 -2
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +84 -10
- package/src/components/ui/Toast.tsx +5 -5
- package/src/components/ui/drawer-components.tsx +28 -22
- package/src/components/workload/WorkloadView.tsx +11 -3
- package/src/theme/index.ts +3 -0
- package/src/theme/tailwind-theme.css +61 -0
- package/src/theme/variables.css +121 -0
- package/src/types/core.ts +12 -0
- package/src/utils/animation.ts +1 -1
- package/src/utils/badge-colors.ts +158 -123
- package/src/utils/log-format.ts +127 -35
- package/src/utils/resource-hierarchy.ts +5 -0
- package/src/utils/resource-icons.ts +3 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { Globe, Route, Layers, Lock } from 'lucide-react'
|
|
2
|
+
import { Section, PropertyList, Property, AlertBanner, ConditionsSection, ResourceLink } from '../../ui/drawer-components'
|
|
3
|
+
import {
|
|
4
|
+
getHTTPProxyFQDN,
|
|
5
|
+
getHTTPProxyRouteCount,
|
|
6
|
+
getHTTPProxyServiceCount,
|
|
7
|
+
getHTTPProxyStatus,
|
|
8
|
+
hasHTTPProxyTLS,
|
|
9
|
+
} from '../resource-utils-contour'
|
|
10
|
+
|
|
11
|
+
interface ContourHTTPProxyRendererProps {
|
|
12
|
+
data: any
|
|
13
|
+
onNavigate?: (ref: { kind: string; name: string; namespace: string }) => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function ContourHTTPProxyRenderer({ data, onNavigate }: ContourHTTPProxyRendererProps) {
|
|
17
|
+
const spec = data.spec || {}
|
|
18
|
+
const status = data.status || {}
|
|
19
|
+
const routes = spec.routes || []
|
|
20
|
+
const includes = spec.includes || []
|
|
21
|
+
const tls = spec.virtualhost?.tls
|
|
22
|
+
const tcpproxy = spec.tcpproxy
|
|
23
|
+
const ns = data.metadata?.namespace || ''
|
|
24
|
+
const conditions = status.conditions
|
|
25
|
+
|
|
26
|
+
const { label: statusLabel } = getHTTPProxyStatus(data)
|
|
27
|
+
const currentStatus = status.currentStatus?.toLowerCase()
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<>
|
|
31
|
+
{currentStatus === 'invalid' && (
|
|
32
|
+
<AlertBanner
|
|
33
|
+
variant="error"
|
|
34
|
+
title="Invalid HTTPProxy"
|
|
35
|
+
message={status.description || 'This HTTPProxy has an invalid configuration.'}
|
|
36
|
+
/>
|
|
37
|
+
)}
|
|
38
|
+
|
|
39
|
+
{currentStatus === 'orphaned' && (
|
|
40
|
+
<AlertBanner
|
|
41
|
+
variant="warning"
|
|
42
|
+
title="Orphaned HTTPProxy"
|
|
43
|
+
message={status.description || 'This HTTPProxy is orphaned — it is not part of any valid delegation chain.'}
|
|
44
|
+
/>
|
|
45
|
+
)}
|
|
46
|
+
|
|
47
|
+
<Section title="HTTPProxy" icon={Globe} defaultExpanded>
|
|
48
|
+
<PropertyList>
|
|
49
|
+
<Property label="FQDN" value={getHTTPProxyFQDN(data)} />
|
|
50
|
+
<Property label="TLS" value={hasHTTPProxyTLS(data) ? 'Enabled' : 'None'} />
|
|
51
|
+
<Property label="Status" value={statusLabel} />
|
|
52
|
+
<Property label="Routes" value={`${getHTTPProxyRouteCount(data)}`} />
|
|
53
|
+
<Property label="Services" value={`${getHTTPProxyServiceCount(data)}`} />
|
|
54
|
+
</PropertyList>
|
|
55
|
+
</Section>
|
|
56
|
+
|
|
57
|
+
<Section title={`Routes (${routes.length})`} icon={Route} defaultExpanded>
|
|
58
|
+
<div className="space-y-3">
|
|
59
|
+
{routes.map((route: any, i: number) => {
|
|
60
|
+
const services = route.services || []
|
|
61
|
+
const routeConditions = route.conditions || []
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div key={i} className="bg-theme-elevated/30 rounded p-3">
|
|
65
|
+
{/* Conditions (prefix match, header match, etc.) */}
|
|
66
|
+
{routeConditions.length > 0 && (
|
|
67
|
+
<div className="flex items-start gap-2 mb-2">
|
|
68
|
+
<span className="text-sm font-medium text-theme-text-primary break-all">
|
|
69
|
+
{routeConditions.map((c: any) => {
|
|
70
|
+
if (c.prefix) return `prefix: ${c.prefix}`
|
|
71
|
+
if (c.header) return `header: ${c.header.name} ${c.header.contains || c.header.exact || c.header.present ? 'match' : ''}`
|
|
72
|
+
return JSON.stringify(c)
|
|
73
|
+
}).join(', ')}
|
|
74
|
+
</span>
|
|
75
|
+
</div>
|
|
76
|
+
)}
|
|
77
|
+
|
|
78
|
+
{routeConditions.length === 0 && (
|
|
79
|
+
<div className="flex items-start gap-2 mb-2">
|
|
80
|
+
<span className="text-sm font-medium text-theme-text-primary">
|
|
81
|
+
(no conditions)
|
|
82
|
+
</span>
|
|
83
|
+
</div>
|
|
84
|
+
)}
|
|
85
|
+
|
|
86
|
+
{/* Services */}
|
|
87
|
+
{services.length > 0 && (
|
|
88
|
+
<div>
|
|
89
|
+
<div className="text-[10px] font-medium text-theme-text-tertiary uppercase tracking-wider mb-1">Services</div>
|
|
90
|
+
<div className="space-y-1">
|
|
91
|
+
{services.map((svc: any, si: number) => {
|
|
92
|
+
const svcNs = svc.namespace || ns
|
|
93
|
+
const port = svc.port ? `:${svc.port}` : ''
|
|
94
|
+
const weight = svc.weight !== undefined ? ` (${svc.weight}%)` : ''
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div key={si} className="flex items-center gap-2 text-xs">
|
|
98
|
+
<ResourceLink
|
|
99
|
+
name={svc.name}
|
|
100
|
+
kind="services"
|
|
101
|
+
namespace={svcNs}
|
|
102
|
+
label={<span className="text-blue-400">{svc.name}{port}{weight}</span>}
|
|
103
|
+
onNavigate={onNavigate}
|
|
104
|
+
/>
|
|
105
|
+
</div>
|
|
106
|
+
)
|
|
107
|
+
})}
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
)}
|
|
111
|
+
</div>
|
|
112
|
+
)
|
|
113
|
+
})}
|
|
114
|
+
|
|
115
|
+
{routes.length === 0 && (
|
|
116
|
+
<div className="text-sm text-theme-text-tertiary">No routes configured</div>
|
|
117
|
+
)}
|
|
118
|
+
</div>
|
|
119
|
+
</Section>
|
|
120
|
+
|
|
121
|
+
{includes.length > 0 && (
|
|
122
|
+
<Section title={`Includes (${includes.length})`} icon={Layers} defaultExpanded>
|
|
123
|
+
<div className="space-y-2">
|
|
124
|
+
{includes.map((inc: any, i: number) => {
|
|
125
|
+
const incNs = inc.namespace || ns
|
|
126
|
+
const incConditions = inc.conditions || []
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<div key={i} className="flex items-center gap-2 text-xs">
|
|
130
|
+
<ResourceLink
|
|
131
|
+
name={inc.name}
|
|
132
|
+
kind="httpproxies"
|
|
133
|
+
namespace={incNs}
|
|
134
|
+
label={
|
|
135
|
+
<span className="text-blue-400">
|
|
136
|
+
{incNs !== ns ? `${incNs}/` : ''}{inc.name}
|
|
137
|
+
</span>
|
|
138
|
+
}
|
|
139
|
+
onNavigate={onNavigate}
|
|
140
|
+
/>
|
|
141
|
+
{incConditions.length > 0 && (
|
|
142
|
+
<span className="text-theme-text-tertiary">
|
|
143
|
+
({incConditions.map((c: any) => c.prefix ? `prefix: ${c.prefix}` : JSON.stringify(c)).join(', ')})
|
|
144
|
+
</span>
|
|
145
|
+
)}
|
|
146
|
+
</div>
|
|
147
|
+
)
|
|
148
|
+
})}
|
|
149
|
+
</div>
|
|
150
|
+
</Section>
|
|
151
|
+
)}
|
|
152
|
+
|
|
153
|
+
{tcpproxy && (
|
|
154
|
+
<Section title="TCP Proxy" icon={Route} defaultExpanded>
|
|
155
|
+
<div className="space-y-1">
|
|
156
|
+
{(tcpproxy.services || []).map((svc: any, i: number) => {
|
|
157
|
+
const port = svc.port ? `:${svc.port}` : ''
|
|
158
|
+
return (
|
|
159
|
+
<div key={i} className="flex items-center gap-2 text-xs">
|
|
160
|
+
<ResourceLink
|
|
161
|
+
name={svc.name}
|
|
162
|
+
kind="services"
|
|
163
|
+
namespace={svc.namespace || ns}
|
|
164
|
+
label={<span className="text-blue-400">{svc.name}{port}</span>}
|
|
165
|
+
onNavigate={onNavigate}
|
|
166
|
+
/>
|
|
167
|
+
</div>
|
|
168
|
+
)
|
|
169
|
+
})}
|
|
170
|
+
</div>
|
|
171
|
+
</Section>
|
|
172
|
+
)}
|
|
173
|
+
|
|
174
|
+
{tls && (
|
|
175
|
+
<Section title="TLS" icon={Lock} defaultExpanded>
|
|
176
|
+
<PropertyList>
|
|
177
|
+
{tls.secretName && (
|
|
178
|
+
<Property label="Secret" value={
|
|
179
|
+
<ResourceLink
|
|
180
|
+
name={tls.secretName}
|
|
181
|
+
kind="secrets"
|
|
182
|
+
namespace={ns}
|
|
183
|
+
onNavigate={onNavigate}
|
|
184
|
+
/>
|
|
185
|
+
} />
|
|
186
|
+
)}
|
|
187
|
+
{tls.minimumProtocolVersion && (
|
|
188
|
+
<Property label="Min Protocol" value={tls.minimumProtocolVersion} />
|
|
189
|
+
)}
|
|
190
|
+
{tls.passthrough !== undefined && (
|
|
191
|
+
<Property label="Passthrough" value={tls.passthrough ? 'Yes' : 'No'} />
|
|
192
|
+
)}
|
|
193
|
+
{!tls.secretName && !tls.passthrough && (
|
|
194
|
+
<Property label="Mode" value="TLS termination (no explicit secret)" />
|
|
195
|
+
)}
|
|
196
|
+
</PropertyList>
|
|
197
|
+
</Section>
|
|
198
|
+
)}
|
|
199
|
+
|
|
200
|
+
{conditions && conditions.length > 0 && (
|
|
201
|
+
<Section title="Status" defaultExpanded>
|
|
202
|
+
<ConditionsSection conditions={conditions} />
|
|
203
|
+
</Section>
|
|
204
|
+
)}
|
|
205
|
+
</>
|
|
206
|
+
)
|
|
207
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { useState, type ReactNode } from 'react'
|
|
2
|
-
import { Server, HardDrive, Terminal as TerminalIcon, FileText, Activity, CirclePlay, FolderOpen } from 'lucide-react'
|
|
1
|
+
import { useState, type ReactNode, type JSX } from 'react'
|
|
2
|
+
import { Server, HardDrive, Terminal as TerminalIcon, FileText, Activity, CirclePlay, FolderOpen, List, Eye, EyeOff } from 'lucide-react'
|
|
3
3
|
import { clsx } from 'clsx'
|
|
4
4
|
import { Section, PropertyList, Property, ConditionsSection, CopyHandler, AlertBanner, ResourceLink } from '../../ui/drawer-components'
|
|
5
5
|
import { formatResources, formatDuration } from '../resource-utils'
|
|
6
|
+
import { getResourceStatusColor } from '../../../utils/badge-colors'
|
|
7
|
+
import type { ResolvedEnvFrom } from '../../../types'
|
|
6
8
|
import { Tooltip } from '../../ui/Tooltip'
|
|
7
9
|
import { MetricsChart } from '../../ui/MetricsChart'
|
|
8
10
|
|
|
@@ -68,6 +70,11 @@ interface PodRendererProps {
|
|
|
68
70
|
// Filesystem browser render props
|
|
69
71
|
renderImageBrowser?: (props: { image: string; namespace: string; podName: string; pullSecrets: string[]; onClose: () => void; onSwitchToPodFiles?: () => void }) => ReactNode
|
|
70
72
|
renderPodBrowser?: (props: { namespace: string; podName: string; containers: string[]; initialContainer: string; onClose: () => void; onSwitchToImageFiles: () => void }) => ReactNode
|
|
73
|
+
/**
|
|
74
|
+
* Resolved content for envFrom references.
|
|
75
|
+
* When provided, expands ConfigMap/Secret keys inline instead of showing "(all keys)".
|
|
76
|
+
*/
|
|
77
|
+
resolvedEnvFrom?: ResolvedEnvFrom
|
|
71
78
|
}
|
|
72
79
|
|
|
73
80
|
// Extract problems from pod status and conditions
|
|
@@ -134,6 +141,148 @@ function getPodProblems(data: any): string[] {
|
|
|
134
141
|
return problems
|
|
135
142
|
}
|
|
136
143
|
|
|
144
|
+
// ── Env vars section — extracted to use hooks (useState for reveal) ──────────
|
|
145
|
+
|
|
146
|
+
function SecretValueCell({ value }: { value: string }) {
|
|
147
|
+
const [revealed, setRevealed] = useState(false)
|
|
148
|
+
return (
|
|
149
|
+
<span className="flex items-center gap-1 min-w-0">
|
|
150
|
+
<span className={clsx('break-all text-amber-700 dark:text-yellow-400/80', !revealed && 'blur-[3px] select-none')}>
|
|
151
|
+
{value || '•••'}
|
|
152
|
+
</span>
|
|
153
|
+
<button
|
|
154
|
+
onClick={() => setRevealed(r => !r)}
|
|
155
|
+
className="shrink-0 text-theme-text-tertiary hover:text-amber-700 dark:hover:text-yellow-400 transition-colors"
|
|
156
|
+
title={revealed ? 'Hide value' : 'Reveal value'}
|
|
157
|
+
>
|
|
158
|
+
{revealed ? <EyeOff className="w-3 h-3" /> : <Eye className="w-3 h-3" />}
|
|
159
|
+
</button>
|
|
160
|
+
</span>
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function EnvRowShell({ name, children }: { name: string; children: ReactNode }) {
|
|
165
|
+
return (
|
|
166
|
+
<div className="flex items-start gap-1 text-xs font-mono py-0.5 border-b border-theme-border/30 last:border-0">
|
|
167
|
+
<span className="text-theme-text-secondary shrink-0 break-all">{name}</span>
|
|
168
|
+
<span className="text-theme-text-tertiary shrink-0">=</span>
|
|
169
|
+
{children}
|
|
170
|
+
</div>
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function EnvRow({ name, value, isSecret }: { name: string; value: string; isSecret: boolean }) {
|
|
175
|
+
return (
|
|
176
|
+
<EnvRowShell name={name}>
|
|
177
|
+
{isSecret
|
|
178
|
+
? <SecretValueCell value={value} />
|
|
179
|
+
: <span className="text-theme-text-primary break-all min-w-0">{value}</span>
|
|
180
|
+
}
|
|
181
|
+
</EnvRowShell>
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function resolveEnvValueNode(env: any): JSX.Element {
|
|
186
|
+
if (env.valueFrom?.secretKeyRef) {
|
|
187
|
+
const { name, key } = env.valueFrom.secretKeyRef
|
|
188
|
+
return <span className="text-amber-700 dark:text-yellow-400/60 text-[10px] shrink-0 self-center px-1 py-0.5 bg-amber-500/10 dark:bg-yellow-500/10 rounded">secret:{name}[{key}]</span>
|
|
189
|
+
}
|
|
190
|
+
if (env.valueFrom?.configMapKeyRef) {
|
|
191
|
+
const { name, key } = env.valueFrom.configMapKeyRef
|
|
192
|
+
return <span className="text-blue-400/70 text-[10px] shrink-0 self-center px-1 py-0.5 bg-blue-500/10 rounded">configmap:{name}[{key}]</span>
|
|
193
|
+
}
|
|
194
|
+
if (env.valueFrom?.fieldRef) {
|
|
195
|
+
return <span className="text-purple-400/70 break-all">field:{env.valueFrom.fieldRef.fieldPath}</span>
|
|
196
|
+
}
|
|
197
|
+
if (env.valueFrom?.resourceFieldRef) {
|
|
198
|
+
return <span className="text-purple-400/70 break-all">resource:{env.valueFrom.resourceFieldRef.resource}</span>
|
|
199
|
+
}
|
|
200
|
+
return <span className="text-theme-text-primary break-all min-w-0">{env.value ?? ''}</span>
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function EnvVarRow({ env }: { env: any }) {
|
|
204
|
+
return (
|
|
205
|
+
<EnvRowShell name={env.name}>
|
|
206
|
+
{resolveEnvValueNode(env)}
|
|
207
|
+
</EnvRowShell>
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function EnvVarsSection({
|
|
212
|
+
initContainers,
|
|
213
|
+
containers,
|
|
214
|
+
resolvedEnvFrom,
|
|
215
|
+
}: {
|
|
216
|
+
initContainers: any[]
|
|
217
|
+
containers: any[]
|
|
218
|
+
resolvedEnvFrom?: ResolvedEnvFrom
|
|
219
|
+
}) {
|
|
220
|
+
const allContainers = [...initContainers, ...containers]
|
|
221
|
+
const multiContainer = allContainers.filter((c: any) => c.env?.length > 0 || c.envFrom?.length > 0).length > 1
|
|
222
|
+
|
|
223
|
+
return (
|
|
224
|
+
<Section title="Environment Variables" icon={List}>
|
|
225
|
+
<div className="space-y-4">
|
|
226
|
+
{allContainers.map((container: any) => {
|
|
227
|
+
const envVars: any[] = container.env || []
|
|
228
|
+
const envFrom: any[] = container.envFrom || []
|
|
229
|
+
if (!envVars.length && !envFrom.length) return null
|
|
230
|
+
return (
|
|
231
|
+
<div key={container.name}>
|
|
232
|
+
{/* Only show container name label when there are multiple containers with env vars */}
|
|
233
|
+
{multiContainer && (
|
|
234
|
+
<div className="text-xs font-medium text-theme-text-tertiary mb-2 uppercase tracking-wide">
|
|
235
|
+
{container.name}
|
|
236
|
+
</div>
|
|
237
|
+
)}
|
|
238
|
+
<div className="space-y-1">
|
|
239
|
+
{/* envFrom — ConfigMap / Secret bulk injections */}
|
|
240
|
+
{envFrom.map((ef: any, i: number) => {
|
|
241
|
+
const isSecret = !!ef.secretRef
|
|
242
|
+
const sourceName = ef.configMapRef?.name ?? ef.secretRef?.name ?? 'unknown'
|
|
243
|
+
const prefix = ef.configMapRef ? 'ConfigMap' : ef.secretRef ? 'Secret' : 'Source'
|
|
244
|
+
const resolved = resolvedEnvFrom?.[sourceName]
|
|
245
|
+
return (
|
|
246
|
+
<div key={i} className="mb-1">
|
|
247
|
+
<div className="flex items-center gap-1.5 text-xs font-mono py-0.5">
|
|
248
|
+
<span className={clsx(
|
|
249
|
+
'shrink-0 px-1 py-0.5 rounded text-[10px]',
|
|
250
|
+
isSecret ? 'bg-amber-500/10 dark:bg-yellow-500/10 text-amber-700 dark:text-yellow-400' : 'bg-blue-500/10 text-blue-400'
|
|
251
|
+
)}>
|
|
252
|
+
{prefix}
|
|
253
|
+
</span>
|
|
254
|
+
<span className="text-theme-text-secondary">{sourceName}</span>
|
|
255
|
+
{!resolved && <span className="text-theme-text-tertiary">(all keys)</span>}
|
|
256
|
+
</div>
|
|
257
|
+
{resolved && resolved.keys.length > 0 && (
|
|
258
|
+
<div className="ml-2 mt-0.5">
|
|
259
|
+
{resolved.keys.map((key) => (
|
|
260
|
+
<EnvRow
|
|
261
|
+
key={key}
|
|
262
|
+
name={key}
|
|
263
|
+
value={resolved.values[key] ?? ''}
|
|
264
|
+
isSecret={isSecret}
|
|
265
|
+
/>
|
|
266
|
+
))}
|
|
267
|
+
</div>
|
|
268
|
+
)}
|
|
269
|
+
</div>
|
|
270
|
+
)
|
|
271
|
+
})}
|
|
272
|
+
|
|
273
|
+
{/* Individual env vars */}
|
|
274
|
+
{envVars.map((env: any) => (
|
|
275
|
+
<EnvVarRow key={env.name} env={env} />
|
|
276
|
+
))}
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
)
|
|
280
|
+
})}
|
|
281
|
+
</div>
|
|
282
|
+
</Section>
|
|
283
|
+
)
|
|
284
|
+
}
|
|
285
|
+
|
|
137
286
|
export function PodRenderer({
|
|
138
287
|
data,
|
|
139
288
|
onCopy,
|
|
@@ -151,6 +300,7 @@ export function PodRenderer({
|
|
|
151
300
|
hideMetricsServer,
|
|
152
301
|
renderImageBrowser,
|
|
153
302
|
renderPodBrowser,
|
|
303
|
+
resolvedEnvFrom,
|
|
154
304
|
}: PodRendererProps) {
|
|
155
305
|
const containerStatuses = data.status?.containerStatuses || []
|
|
156
306
|
const containers = data.spec?.containers || []
|
|
@@ -283,23 +433,20 @@ export function PodRenderer({
|
|
|
283
433
|
|
|
284
434
|
// Status label and color
|
|
285
435
|
let statusLabel: string
|
|
286
|
-
let statusColor: string
|
|
287
436
|
if (isCompleted) {
|
|
288
437
|
statusLabel = 'Completed'
|
|
289
|
-
statusColor = 'bg-green-500/20 text-green-400'
|
|
290
438
|
} else if (isFailed) {
|
|
291
439
|
statusLabel = `Exit ${exitCode}`
|
|
292
|
-
statusColor = 'bg-red-500/20 text-red-400'
|
|
293
440
|
} else if (isInitRunning) {
|
|
294
441
|
statusLabel = 'Running'
|
|
295
|
-
statusColor = 'bg-blue-500/20 text-blue-400'
|
|
296
442
|
} else if (isWaiting) {
|
|
297
443
|
statusLabel = state?.waiting?.reason || 'Waiting'
|
|
298
|
-
statusColor = 'bg-yellow-500/20 text-yellow-400'
|
|
299
444
|
} else {
|
|
300
445
|
statusLabel = 'Pending'
|
|
301
|
-
statusColor = 'bg-gray-500/20 text-gray-400'
|
|
302
446
|
}
|
|
447
|
+
const statusColor = getResourceStatusColor(
|
|
448
|
+
isCompleted ? 'succeeded' : isFailed ? 'failed' : isInitRunning ? 'running' : isWaiting ? 'waiting' : 'pending'
|
|
449
|
+
)
|
|
303
450
|
|
|
304
451
|
// Build command string
|
|
305
452
|
const command = container.command || container.args
|
|
@@ -553,6 +700,15 @@ export function PodRenderer({
|
|
|
553
700
|
</div>
|
|
554
701
|
</Section>
|
|
555
702
|
|
|
703
|
+
{/* Environment Variables */}
|
|
704
|
+
{[...initContainers, ...containers].some((c: any) => c.env?.length > 0 || c.envFrom?.length > 0) && (
|
|
705
|
+
<EnvVarsSection
|
|
706
|
+
initContainers={initContainers}
|
|
707
|
+
containers={containers}
|
|
708
|
+
resolvedEnvFrom={resolvedEnvFrom}
|
|
709
|
+
/>
|
|
710
|
+
)}
|
|
711
|
+
|
|
556
712
|
{/* Resource Usage (from metrics-server) — hidden when Prometheus has CPU/memory data */}
|
|
557
713
|
{!hideMetricsServer && !!(metrics?.containers?.length || metricsHistory?.containers?.length || metricsHistory?.collectionError) && (
|
|
558
714
|
<Section title="Resource Usage" icon={Activity} defaultExpanded>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Contour cell components for ResourcesView table
|
|
2
|
+
|
|
3
|
+
import { Shield } from 'lucide-react'
|
|
4
|
+
import { Tooltip } from '../../ui/Tooltip'
|
|
5
|
+
import {
|
|
6
|
+
getHTTPProxyFQDN,
|
|
7
|
+
getHTTPProxyRouteCount,
|
|
8
|
+
getHTTPProxyIncludeCount,
|
|
9
|
+
hasHTTPProxyTLS,
|
|
10
|
+
getHTTPProxyStatus,
|
|
11
|
+
} from '../resource-utils-contour'
|
|
12
|
+
|
|
13
|
+
export function HTTPProxyCell({ resource, column }: { resource: any; column: string }) {
|
|
14
|
+
switch (column) {
|
|
15
|
+
case 'fqdn': {
|
|
16
|
+
const fqdn = getHTTPProxyFQDN(resource)
|
|
17
|
+
return <span className="text-sm truncate" title={fqdn}>{fqdn}</span>
|
|
18
|
+
}
|
|
19
|
+
case 'routes':
|
|
20
|
+
return <span className="text-sm">{getHTTPProxyRouteCount(resource) || '-'}</span>
|
|
21
|
+
case 'includes': {
|
|
22
|
+
const count = getHTTPProxyIncludeCount(resource)
|
|
23
|
+
return <span className="text-sm">{count > 0 ? count : '-'}</span>
|
|
24
|
+
}
|
|
25
|
+
case 'tls': {
|
|
26
|
+
const hasTLS = hasHTTPProxyTLS(resource)
|
|
27
|
+
return hasTLS ? (
|
|
28
|
+
<Tooltip content="TLS Enabled">
|
|
29
|
+
<span>
|
|
30
|
+
<Shield className="w-4 h-4 text-green-400" />
|
|
31
|
+
</span>
|
|
32
|
+
</Tooltip>
|
|
33
|
+
) : (
|
|
34
|
+
<span className="text-sm text-theme-text-tertiary">-</span>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
case 'status': {
|
|
38
|
+
const { label } = getHTTPProxyStatus(resource)
|
|
39
|
+
const color = label === 'Valid' ? 'text-green-500'
|
|
40
|
+
: label === 'Invalid' ? 'text-red-500'
|
|
41
|
+
: label === 'Orphaned' ? 'text-yellow-500'
|
|
42
|
+
: 'text-theme-text-secondary'
|
|
43
|
+
return <span className={`text-sm ${color}`}>{label}</span>
|
|
44
|
+
}
|
|
45
|
+
default:
|
|
46
|
+
return <span className="text-sm text-theme-text-tertiary">-</span>
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -1,30 +1,14 @@
|
|
|
1
1
|
import { AlertBanner } from '../../ui/drawer-components'
|
|
2
|
+
import {
|
|
3
|
+
VULN_SEVERITY_BADGE,
|
|
4
|
+
VULN_SEVERITY_BAR,
|
|
5
|
+
VULN_SEVERITY_TEXT,
|
|
6
|
+
} from '../../../utils/badge-colors'
|
|
2
7
|
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
export const
|
|
6
|
-
|
|
7
|
-
HIGH: 'bg-orange-500/20 text-orange-400',
|
|
8
|
-
MEDIUM: 'bg-yellow-500/20 text-yellow-400',
|
|
9
|
-
LOW: 'bg-blue-500/20 text-blue-400',
|
|
10
|
-
UNKNOWN: 'bg-gray-500/20 text-gray-400',
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export const SEVERITY_BAR_COLORS: Record<string, string> = {
|
|
14
|
-
CRITICAL: 'bg-red-500',
|
|
15
|
-
HIGH: 'bg-orange-500',
|
|
16
|
-
MEDIUM: 'bg-yellow-500',
|
|
17
|
-
LOW: 'bg-blue-500',
|
|
18
|
-
UNKNOWN: 'bg-gray-500',
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export const SEVERITY_TEXT_COLORS: Record<string, string> = {
|
|
22
|
-
CRITICAL: 'text-red-400',
|
|
23
|
-
HIGH: 'text-orange-400',
|
|
24
|
-
MEDIUM: 'text-yellow-400',
|
|
25
|
-
LOW: 'text-blue-400',
|
|
26
|
-
UNKNOWN: 'text-gray-400',
|
|
27
|
-
}
|
|
8
|
+
// Re-export from centralized badge-colors under the legacy names used by Trivy renderers
|
|
9
|
+
export const SEVERITY_BADGE_COLORS = VULN_SEVERITY_BADGE
|
|
10
|
+
export const SEVERITY_BAR_COLORS = VULN_SEVERITY_BAR
|
|
11
|
+
export const SEVERITY_TEXT_COLORS = VULN_SEVERITY_TEXT
|
|
28
12
|
|
|
29
13
|
export const SEVERITY_ORDER: Record<string, number> = {
|
|
30
14
|
CRITICAL: 0,
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Contour HTTPProxy CRD utility functions for resource list cells and detail renderers
|
|
2
|
+
|
|
3
|
+
export function getHTTPProxyFQDN(resource: any): string {
|
|
4
|
+
return resource.spec?.virtualhost?.fqdn || '-'
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function getHTTPProxyStatus(resource: any): { status: string; label: string } {
|
|
8
|
+
const currentStatus = resource.status?.currentStatus?.toLowerCase()
|
|
9
|
+
if (currentStatus === 'valid') return { status: 'healthy', label: 'Valid' }
|
|
10
|
+
if (currentStatus === 'invalid') return { status: 'unhealthy', label: 'Invalid' }
|
|
11
|
+
if (currentStatus === 'orphaned') return { status: 'degraded', label: 'Orphaned' }
|
|
12
|
+
return { status: 'unknown', label: '-' }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getHTTPProxyRouteCount(resource: any): number {
|
|
16
|
+
return resource.spec?.routes?.length || 0
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getHTTPProxyServiceCount(resource: any): number {
|
|
20
|
+
let count = 0
|
|
21
|
+
for (const route of resource.spec?.routes || []) {
|
|
22
|
+
count += (route.services || []).length
|
|
23
|
+
}
|
|
24
|
+
count += (resource.spec?.tcpproxy?.services || []).length
|
|
25
|
+
return count
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getHTTPProxyIncludeCount(resource: any): number {
|
|
29
|
+
return resource.spec?.includes?.length || 0
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function hasHTTPProxyTLS(resource: any): boolean {
|
|
33
|
+
return !!resource.spec?.virtualhost?.tls
|
|
34
|
+
}
|
|
@@ -197,6 +197,97 @@ export function getPodRestarts(pod: any): number {
|
|
|
197
197
|
return containerStatuses.reduce((sum: number, c: any) => sum + (c.restartCount || 0), 0)
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
+
export interface ContainerSquareState {
|
|
201
|
+
name: string
|
|
202
|
+
status: 'ready' | 'running' | 'waiting' | 'completed' | 'terminated' | 'unknown'
|
|
203
|
+
restarts: number
|
|
204
|
+
reason?: string
|
|
205
|
+
message?: string
|
|
206
|
+
exitCode?: number
|
|
207
|
+
startedAt?: string
|
|
208
|
+
finishedAt?: string
|
|
209
|
+
isInit?: boolean
|
|
210
|
+
/** Last termination info — crucial for debugging CrashLoopBackOff */
|
|
211
|
+
lastTermination?: {
|
|
212
|
+
reason?: string
|
|
213
|
+
exitCode?: number
|
|
214
|
+
startedAt?: string
|
|
215
|
+
finishedAt?: string
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function getContainerSquareStates(pod: any): ContainerSquareState[] {
|
|
220
|
+
const result: ContainerSquareState[] = []
|
|
221
|
+
const initStatuses = pod.status?.initContainerStatuses || []
|
|
222
|
+
const containerStatuses = pod.status?.containerStatuses || []
|
|
223
|
+
const specContainers = pod.spec?.containers || []
|
|
224
|
+
|
|
225
|
+
for (const cs of initStatuses) {
|
|
226
|
+
const stateKey = cs.state ? Object.keys(cs.state)[0] : 'unknown'
|
|
227
|
+
let status: ContainerSquareState['status'] = 'unknown'
|
|
228
|
+
if (stateKey === 'terminated' && cs.state?.terminated?.exitCode === 0) {
|
|
229
|
+
status = 'completed'
|
|
230
|
+
} else if (stateKey === 'running') {
|
|
231
|
+
status = cs.ready ? 'ready' : 'running'
|
|
232
|
+
} else if (stateKey === 'waiting') {
|
|
233
|
+
status = 'waiting'
|
|
234
|
+
} else if (stateKey === 'terminated') {
|
|
235
|
+
status = 'terminated'
|
|
236
|
+
}
|
|
237
|
+
const stateDetail = cs.state?.[stateKey]
|
|
238
|
+
const lastTerm = cs.lastState?.terminated
|
|
239
|
+
result.push({
|
|
240
|
+
name: cs.name,
|
|
241
|
+
status,
|
|
242
|
+
restarts: cs.restartCount || 0,
|
|
243
|
+
reason: stateDetail?.reason,
|
|
244
|
+
message: stateDetail?.message,
|
|
245
|
+
exitCode: stateDetail?.exitCode,
|
|
246
|
+
startedAt: stateDetail?.startedAt,
|
|
247
|
+
finishedAt: stateDetail?.finishedAt,
|
|
248
|
+
isInit: true,
|
|
249
|
+
lastTermination: lastTerm ? { reason: lastTerm.reason, exitCode: lastTerm.exitCode, startedAt: lastTerm.startedAt, finishedAt: lastTerm.finishedAt } : undefined,
|
|
250
|
+
})
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (containerStatuses.length > 0) {
|
|
254
|
+
for (const cs of containerStatuses) {
|
|
255
|
+
const stateKey = cs.state ? Object.keys(cs.state)[0] : 'unknown'
|
|
256
|
+
let status: ContainerSquareState['status'] = 'unknown'
|
|
257
|
+
if (stateKey === 'running' && cs.ready) {
|
|
258
|
+
status = 'ready'
|
|
259
|
+
} else if (stateKey === 'running') {
|
|
260
|
+
status = 'running'
|
|
261
|
+
} else if (stateKey === 'terminated' && cs.state?.terminated?.exitCode === 0) {
|
|
262
|
+
status = 'completed'
|
|
263
|
+
} else if (stateKey === 'terminated') {
|
|
264
|
+
status = 'terminated'
|
|
265
|
+
} else if (stateKey === 'waiting') {
|
|
266
|
+
status = 'waiting'
|
|
267
|
+
}
|
|
268
|
+
const stateDetail = cs.state?.[stateKey]
|
|
269
|
+
const lastTerm = cs.lastState?.terminated
|
|
270
|
+
result.push({
|
|
271
|
+
name: cs.name,
|
|
272
|
+
status,
|
|
273
|
+
restarts: cs.restartCount || 0,
|
|
274
|
+
reason: stateDetail?.reason,
|
|
275
|
+
message: stateDetail?.message,
|
|
276
|
+
exitCode: stateDetail?.exitCode,
|
|
277
|
+
startedAt: stateDetail?.startedAt,
|
|
278
|
+
finishedAt: stateDetail?.finishedAt,
|
|
279
|
+
lastTermination: lastTerm ? { reason: lastTerm.reason, exitCode: lastTerm.exitCode, startedAt: lastTerm.startedAt, finishedAt: lastTerm.finishedAt } : undefined,
|
|
280
|
+
})
|
|
281
|
+
}
|
|
282
|
+
} else {
|
|
283
|
+
for (const c of specContainers) {
|
|
284
|
+
result.push({ name: c.name, status: 'unknown', restarts: 0 })
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return result
|
|
289
|
+
}
|
|
290
|
+
|
|
200
291
|
// ============================================================================
|
|
201
292
|
// WORKLOAD UTILITIES (Deployment, StatefulSet, DaemonSet, ReplicaSet)
|
|
202
293
|
// ============================================================================
|