@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.
Files changed (41) hide show
  1. package/package.json +6 -2
  2. package/src/components/dock/BottomDock.tsx +14 -15
  3. package/src/components/dock/DockContext.tsx +40 -1
  4. package/src/components/dock/LocalTerminalTab.tsx +241 -0
  5. package/src/components/dock/NodeTerminalTab.tsx +129 -0
  6. package/src/components/dock/TerminalTab.tsx +1 -1
  7. package/src/components/dock/index.ts +2 -0
  8. package/src/components/gitops/GitOpsStatusBadge.tsx +18 -67
  9. package/src/components/logs/LogCore.tsx +30 -4
  10. package/src/components/logs/StructuredLogLine.tsx +168 -0
  11. package/src/components/logs/index.ts +1 -1
  12. package/src/components/logs/useLogBuffer.ts +16 -10
  13. package/src/components/resources/ResourcesView.tsx +136 -24
  14. package/src/components/resources/renderers/ContourHTTPProxyRenderer.tsx +207 -0
  15. package/src/components/resources/renderers/PodRenderer.tsx +164 -8
  16. package/src/components/resources/renderers/contour-cells.tsx +48 -0
  17. package/src/components/resources/renderers/index.ts +2 -0
  18. package/src/components/resources/renderers/trivy-shared.tsx +9 -25
  19. package/src/components/resources/resource-utils-contour.ts +34 -0
  20. package/src/components/resources/resource-utils.ts +91 -0
  21. package/src/components/shared/ResourceActionsBar.tsx +135 -2
  22. package/src/components/shared/ResourceRendererDispatch.tsx +23 -5
  23. package/src/components/timeline/shared.tsx +8 -10
  24. package/src/components/topology/K8sResourceNode.tsx +1 -0
  25. package/src/components/topology/TopologyFilterSidebar.tsx +3 -0
  26. package/src/components/topology/layout.ts +1 -0
  27. package/src/components/topology/topology.css +1 -0
  28. package/src/components/ui/CodeViewer.tsx +1 -2
  29. package/src/components/ui/ForceDeleteConfirmDialog.tsx +84 -10
  30. package/src/components/ui/Toast.tsx +5 -5
  31. package/src/components/ui/drawer-components.tsx +28 -22
  32. package/src/components/workload/WorkloadView.tsx +11 -3
  33. package/src/theme/index.ts +3 -0
  34. package/src/theme/tailwind-theme.css +61 -0
  35. package/src/theme/variables.css +121 -0
  36. package/src/types/core.ts +12 -0
  37. package/src/utils/animation.ts +1 -1
  38. package/src/utils/badge-colors.ts +158 -123
  39. package/src/utils/log-format.ts +127 -35
  40. package/src/utils/resource-hierarchy.ts +5 -0
  41. 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
+ }
@@ -103,3 +103,5 @@ export * from './WorkflowTemplateRenderer'
103
103
  export * from './WorkloadRenderer'
104
104
  export * from './TraefikIngressRouteRenderer'
105
105
  export * from './traefik-cells'
106
+ export * from './ContourHTTPProxyRenderer'
107
+ export * from './contour-cells'
@@ -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
- // Shared severity color maps for all Trivy report renderers
4
-
5
- export const SEVERITY_BADGE_COLORS: Record<string, string> = {
6
- CRITICAL: 'bg-red-500/20 text-red-400',
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
  // ============================================================================