@skyhook-io/k8s-ui 1.1.0 → 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 +8 -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 +186 -67
- package/src/components/resources/renderers/ContourHTTPProxyRenderer.tsx +207 -0
- package/src/components/resources/renderers/HPARenderer.tsx +2 -1
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +2 -1
- package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +2 -1
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +2 -1
- package/src/components/resources/renderers/KnativeEventingRenderer.tsx +7 -6
- package/src/components/resources/renderers/KnativeFlowRenderer.tsx +2 -1
- package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +3 -2
- package/src/components/resources/renderers/KnativeSourceRenderer.tsx +3 -2
- package/src/components/resources/renderers/PodRenderer.tsx +164 -8
- package/src/components/resources/renderers/VPARenderer.tsx +2 -1
- package/src/components/resources/renderers/WorkloadRenderer.tsx +53 -55
- 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 +276 -144
- package/src/components/shared/ResourceRendererDispatch.tsx +59 -5
- package/src/components/shared/index.ts +1 -1
- 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/TopologyGraph.tsx +2 -17
- 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/ConfirmDialog.tsx +85 -130
- package/src/components/ui/DialogPortal.tsx +82 -0
- 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/ui/index.ts +1 -0
- package/src/components/workload/WorkloadView.tsx +30 -2
- 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 +13 -2
- 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/navigation.test.ts +142 -0
- package/src/utils/navigation.ts +52 -47
- package/src/utils/resource-hierarchy.ts +5 -0
- package/src/utils/resource-icons.ts +3 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Server, Settings, Shield, Cpu, Tag } from 'lucide-react'
|
|
2
2
|
import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink } from '../../ui/drawer-components'
|
|
3
|
+
import { kindToPlural } from '../../../utils/navigation'
|
|
3
4
|
import {
|
|
4
5
|
getNodePoolStatus,
|
|
5
6
|
getNodePoolNodeClassRef,
|
|
@@ -47,7 +48,7 @@ export function KarpenterNodePoolRenderer({ data, onNavigate }: KarpenterNodePoo
|
|
|
47
48
|
value={nodeClassRef?.name ? (
|
|
48
49
|
<ResourceLink
|
|
49
50
|
name={nodeClassRef.name}
|
|
50
|
-
kind={(nodeClassRef.kind || 'EC2NodeClass')
|
|
51
|
+
kind={kindToPlural(nodeClassRef.kind || 'EC2NodeClass')}
|
|
51
52
|
namespace=""
|
|
52
53
|
group={nodeClassRef.group}
|
|
53
54
|
label={getNodePoolNodeClassRef(data)}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Cpu } from 'lucide-react'
|
|
2
2
|
import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink } from '../../ui/drawer-components'
|
|
3
|
+
import { kindToPlural } from '../../../utils/navigation'
|
|
3
4
|
import {
|
|
4
5
|
getScaledObjectStatus,
|
|
5
6
|
getScaledObjectTarget,
|
|
@@ -66,7 +67,7 @@ export function KedaScaledObjectRenderer({ data, onNavigate }: KedaScaledObjectR
|
|
|
66
67
|
return (
|
|
67
68
|
<ResourceLink
|
|
68
69
|
name={target.name}
|
|
69
|
-
kind={(target.kind || 'Deployment')
|
|
70
|
+
kind={kindToPlural(target.kind || 'Deployment')}
|
|
70
71
|
namespace={data.metadata?.namespace || ''}
|
|
71
72
|
label={getScaledObjectTarget(data)}
|
|
72
73
|
onNavigate={onNavigate}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Radio, Filter, FileType, Inbox, ArrowRightLeft } from 'lucide-react'
|
|
2
2
|
import { clsx } from 'clsx'
|
|
3
3
|
import { Section, PropertyList, Property, ConditionsSection, KnativeNotReadyBanner, ResourceLink } from '../../ui/drawer-components'
|
|
4
|
+
import { kindToPlural } from '../../../utils/navigation'
|
|
4
5
|
import { getKnativeConditionStatus } from '../resource-utils-knative'
|
|
5
6
|
|
|
6
7
|
interface RendererProps {
|
|
@@ -48,7 +49,7 @@ export function BrokerRenderer({ data, onNavigate }: RendererProps) {
|
|
|
48
49
|
<Property label="Dead Letter" value={
|
|
49
50
|
<ResourceLink
|
|
50
51
|
name={delivery.deadLetterSink.ref.name}
|
|
51
|
-
kind={delivery.deadLetterSink.ref.kind
|
|
52
|
+
kind={kindToPlural(delivery.deadLetterSink.ref.kind || 'Service')}
|
|
52
53
|
namespace={delivery.deadLetterSink.ref.namespace || ns}
|
|
53
54
|
onNavigate={onNavigate}
|
|
54
55
|
/>
|
|
@@ -106,7 +107,7 @@ export function TriggerRenderer({ data, onNavigate }: RendererProps) {
|
|
|
106
107
|
subscriberRef ? (
|
|
107
108
|
<ResourceLink
|
|
108
109
|
name={subscriberRef.name}
|
|
109
|
-
kind={(subscriberRef.kind
|
|
110
|
+
kind={kindToPlural(subscriberRef.kind || 'Service')}
|
|
110
111
|
namespace={subscriberRef.namespace || ns}
|
|
111
112
|
onNavigate={onNavigate}
|
|
112
113
|
/>
|
|
@@ -248,7 +249,7 @@ export function SubscriptionRenderer({ data, onNavigate }: RendererProps) {
|
|
|
248
249
|
<Property label="Channel" value={
|
|
249
250
|
<ResourceLink
|
|
250
251
|
name={channelRef.name}
|
|
251
|
-
kind={(channelRef.kind
|
|
252
|
+
kind={kindToPlural(channelRef.kind || 'Channel')}
|
|
252
253
|
namespace={ns}
|
|
253
254
|
onNavigate={onNavigate}
|
|
254
255
|
/>
|
|
@@ -258,7 +259,7 @@ export function SubscriptionRenderer({ data, onNavigate }: RendererProps) {
|
|
|
258
259
|
subscriberRef ? (
|
|
259
260
|
<ResourceLink
|
|
260
261
|
name={subscriberRef.name}
|
|
261
|
-
kind={(subscriberRef.kind
|
|
262
|
+
kind={kindToPlural(subscriberRef.kind || 'Service')}
|
|
262
263
|
namespace={subscriberRef.namespace || ns}
|
|
263
264
|
onNavigate={onNavigate}
|
|
264
265
|
/>
|
|
@@ -270,7 +271,7 @@ export function SubscriptionRenderer({ data, onNavigate }: RendererProps) {
|
|
|
270
271
|
replyRef ? (
|
|
271
272
|
<ResourceLink
|
|
272
273
|
name={replyRef.name}
|
|
273
|
-
kind={(replyRef.kind
|
|
274
|
+
kind={kindToPlural(replyRef.kind || 'Channel')}
|
|
274
275
|
namespace={replyRef.namespace || ns}
|
|
275
276
|
onNavigate={onNavigate}
|
|
276
277
|
/>
|
|
@@ -288,7 +289,7 @@ export function SubscriptionRenderer({ data, onNavigate }: RendererProps) {
|
|
|
288
289
|
<Property label="Sink" value={
|
|
289
290
|
<ResourceLink
|
|
290
291
|
name={delivery.deadLetterSink.ref.name}
|
|
291
|
-
kind={(delivery.deadLetterSink.ref.kind
|
|
292
|
+
kind={kindToPlural(delivery.deadLetterSink.ref.kind || 'Service')}
|
|
292
293
|
namespace={delivery.deadLetterSink.ref.namespace || ns}
|
|
293
294
|
onNavigate={onNavigate}
|
|
294
295
|
/>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ListOrdered, GitFork } from 'lucide-react'
|
|
2
2
|
import { clsx } from 'clsx'
|
|
3
3
|
import { Section, PropertyList, Property, ConditionsSection, KnativeNotReadyBanner, ResourceLink } from '../../ui/drawer-components'
|
|
4
|
+
import { kindToPlural } from '../../../utils/navigation'
|
|
4
5
|
import { getKnativeConditionStatus } from '../resource-utils-knative'
|
|
5
6
|
|
|
6
7
|
interface RendererProps {
|
|
@@ -15,7 +16,7 @@ function RefDisplay({ ref: destRef, ns, onNavigate }: { ref: any; ns: string; on
|
|
|
15
16
|
return (
|
|
16
17
|
<ResourceLink
|
|
17
18
|
name={destRef.ref.name}
|
|
18
|
-
kind={(destRef.ref.kind
|
|
19
|
+
kind={kindToPlural(destRef.ref.kind || 'Service')}
|
|
19
20
|
namespace={destRef.ref.namespace || ns}
|
|
20
21
|
onNavigate={onNavigate}
|
|
21
22
|
/>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Network, ShieldCheck, Server, Globe } from 'lucide-react'
|
|
2
2
|
import { clsx } from 'clsx'
|
|
3
3
|
import { Section, PropertyList, Property, ConditionsSection, KnativeNotReadyBanner, ResourceLink } from '../../ui/drawer-components'
|
|
4
|
+
import { kindToPlural } from '../../../utils/navigation'
|
|
4
5
|
import { getKnativeConditionStatus } from '../resource-utils-knative'
|
|
5
6
|
|
|
6
7
|
interface RendererProps {
|
|
@@ -169,7 +170,7 @@ export function ServerlessServiceRenderer({ data, onNavigate }: RendererProps) {
|
|
|
169
170
|
<Property label="Target" value={
|
|
170
171
|
<ResourceLink
|
|
171
172
|
name={objectRef.name}
|
|
172
|
-
kind={(objectRef.kind
|
|
173
|
+
kind={kindToPlural(objectRef.kind || 'Deployment')}
|
|
173
174
|
namespace={ns}
|
|
174
175
|
onNavigate={onNavigate}
|
|
175
176
|
/>
|
|
@@ -216,7 +217,7 @@ export function DomainMappingRenderer({ data, onNavigate }: RendererProps) {
|
|
|
216
217
|
<Property label="Target" value={
|
|
217
218
|
<ResourceLink
|
|
218
219
|
name={ref.name}
|
|
219
|
-
kind={(ref.kind
|
|
220
|
+
kind={kindToPlural(ref.kind || 'Service')}
|
|
220
221
|
namespace={ns}
|
|
221
222
|
onNavigate={onNavigate}
|
|
222
223
|
/>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Clock, Server, Container, Link2 } from 'lucide-react'
|
|
2
2
|
import { clsx } from 'clsx'
|
|
3
3
|
import { Section, PropertyList, Property, ConditionsSection, KnativeNotReadyBanner, ResourceLink } from '../../ui/drawer-components'
|
|
4
|
+
import { kindToPlural } from '../../../utils/navigation'
|
|
4
5
|
import { getKnativeConditionStatus } from '../resource-utils-knative'
|
|
5
6
|
|
|
6
7
|
interface RendererProps {
|
|
@@ -16,7 +17,7 @@ function SinkProperty({ sink, ns, onNavigate }: { sink: any; ns: string; onNavig
|
|
|
16
17
|
<Property label="Sink" value={
|
|
17
18
|
<ResourceLink
|
|
18
19
|
name={sink.ref.name}
|
|
19
|
-
kind={(sink.ref.kind
|
|
20
|
+
kind={kindToPlural(sink.ref.kind || 'Service')}
|
|
20
21
|
namespace={sink.ref.namespace || ns}
|
|
21
22
|
onNavigate={onNavigate}
|
|
22
23
|
/>
|
|
@@ -195,7 +196,7 @@ export function SinkBindingRenderer({ data, onNavigate }: RendererProps) {
|
|
|
195
196
|
<Property label="Subject" value={
|
|
196
197
|
<ResourceLink
|
|
197
198
|
name={subjectRef.name}
|
|
198
|
-
kind={(subjectRef.kind
|
|
199
|
+
kind={kindToPlural(subjectRef.kind || 'Deployment')}
|
|
199
200
|
namespace={subjectRef.namespace || ns}
|
|
200
201
|
onNavigate={onNavigate}
|
|
201
202
|
/>
|
|
@@ -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>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Cpu } from 'lucide-react'
|
|
2
2
|
import { clsx } from 'clsx'
|
|
3
3
|
import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink } from '../../ui/drawer-components'
|
|
4
|
+
import { kindToPlural } from '../../../utils/navigation'
|
|
4
5
|
|
|
5
6
|
interface VPARendererProps {
|
|
6
7
|
data: any
|
|
@@ -65,7 +66,7 @@ export function VPARenderer({ data, onNavigate }: VPARendererProps) {
|
|
|
65
66
|
targetRef.name ? (
|
|
66
67
|
<ResourceLink
|
|
67
68
|
name={targetRef.name}
|
|
68
|
-
kind={(targetRef.kind || 'Deployment')
|
|
69
|
+
kind={kindToPlural(targetRef.kind || 'Deployment')}
|
|
69
70
|
namespace={data.metadata?.namespace || ''}
|
|
70
71
|
label={`${targetRef.kind}/${targetRef.name}`}
|
|
71
72
|
onNavigate={onNavigate}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useState, useEffect } from 'react'
|
|
2
2
|
import { Server, ExternalLink, Scale, Minus, Plus, Loader2 } from 'lucide-react'
|
|
3
3
|
import { Section, PropertyList, Property, ConditionsSection, PodTemplateSection, AlertBanner, ResourceLink } from '../../ui/drawer-components'
|
|
4
|
+
import { DialogPortal } from '../../ui/DialogPortal'
|
|
4
5
|
|
|
5
6
|
interface WorkloadRendererProps {
|
|
6
7
|
kind: string
|
|
@@ -213,67 +214,64 @@ export function WorkloadRenderer({ kind, data, onNavigate, onViewPods, onScale,
|
|
|
213
214
|
</Section>
|
|
214
215
|
|
|
215
216
|
{/* Scale Dialog */}
|
|
216
|
-
{showScaleDialog
|
|
217
|
-
<
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
Scale {metadata.name}
|
|
221
|
-
</h3>
|
|
217
|
+
<DialogPortal open={showScaleDialog} onClose={() => setShowScaleDialog(false)} className="w-80 p-4">
|
|
218
|
+
<h3 className="text-sm font-medium text-theme-text-primary mb-4">
|
|
219
|
+
Scale {metadata.name}
|
|
220
|
+
</h3>
|
|
222
221
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
222
|
+
<div className="flex items-center justify-center gap-4 mb-4">
|
|
223
|
+
<button
|
|
224
|
+
onClick={() => setTargetReplicas(Math.max(0, targetReplicas - 1))}
|
|
225
|
+
className="p-2 rounded-lg bg-theme-elevated hover:bg-theme-hover text-theme-text-secondary hover:text-theme-text-primary transition-colors"
|
|
226
|
+
disabled={targetReplicas <= 0}
|
|
227
|
+
>
|
|
228
|
+
<Minus className="w-5 h-5" />
|
|
229
|
+
</button>
|
|
231
230
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
231
|
+
<input
|
|
232
|
+
type="number"
|
|
233
|
+
min="0"
|
|
234
|
+
max="10000"
|
|
235
|
+
value={targetReplicas}
|
|
236
|
+
onChange={(e) => setTargetReplicas(Math.min(10000, Math.max(0, parseInt(e.target.value) || 0)))}
|
|
237
|
+
className="w-20 text-center text-2xl font-semibold bg-theme-elevated border border-theme-border rounded-lg py-2 text-theme-text-primary focus:outline-none focus:border-blue-500"
|
|
238
|
+
autoFocus
|
|
239
|
+
/>
|
|
240
240
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
241
|
+
<button
|
|
242
|
+
onClick={() => setTargetReplicas(Math.min(10000, targetReplicas + 1))}
|
|
243
|
+
disabled={targetReplicas >= 10000}
|
|
244
|
+
className="p-2 rounded-lg bg-theme-elevated hover:bg-theme-hover text-theme-text-secondary hover:text-theme-text-primary transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
245
|
+
>
|
|
246
|
+
<Plus className="w-5 h-5" />
|
|
247
|
+
</button>
|
|
248
|
+
</div>
|
|
249
249
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
250
|
+
<div className="text-xs text-theme-text-tertiary text-center mb-4">
|
|
251
|
+
Current: {spec.replicas || 0} replicas
|
|
252
|
+
{targetReplicas !== (spec.replicas || 0) && (
|
|
253
|
+
<span className="text-theme-text-secondary">
|
|
254
|
+
{' '}→ {targetReplicas}
|
|
255
|
+
</span>
|
|
256
|
+
)}
|
|
257
|
+
</div>
|
|
258
258
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
</div>
|
|
274
|
-
</div>
|
|
259
|
+
<div className="flex gap-2">
|
|
260
|
+
<button
|
|
261
|
+
onClick={() => setShowScaleDialog(false)}
|
|
262
|
+
className="flex-1 px-3 py-2 text-sm text-theme-text-secondary hover:text-theme-text-primary bg-theme-elevated hover:bg-theme-hover border border-theme-border rounded-lg transition-colors"
|
|
263
|
+
>
|
|
264
|
+
Cancel
|
|
265
|
+
</button>
|
|
266
|
+
<button
|
|
267
|
+
onClick={handleScale}
|
|
268
|
+
disabled={isScalePending || targetReplicas === (spec.replicas || 0)}
|
|
269
|
+
className="flex-1 px-3 py-2 text-sm text-white bg-blue-600 hover:bg-blue-500 disabled:bg-blue-600/50 disabled:cursor-not-allowed rounded-lg transition-colors"
|
|
270
|
+
>
|
|
271
|
+
{isScalePending ? 'Scaling...' : 'Apply'}
|
|
272
|
+
</button>
|
|
275
273
|
</div>
|
|
276
|
-
|
|
274
|
+
</DialogPortal>
|
|
277
275
|
|
|
278
276
|
<Section title="Strategy">
|
|
279
277
|
<PropertyList>
|
|
@@ -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,
|