@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.
Files changed (58) hide show
  1. package/package.json +8 -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 +186 -67
  14. package/src/components/resources/renderers/ContourHTTPProxyRenderer.tsx +207 -0
  15. package/src/components/resources/renderers/HPARenderer.tsx +2 -1
  16. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +2 -1
  17. package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +2 -1
  18. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +2 -1
  19. package/src/components/resources/renderers/KnativeEventingRenderer.tsx +7 -6
  20. package/src/components/resources/renderers/KnativeFlowRenderer.tsx +2 -1
  21. package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +3 -2
  22. package/src/components/resources/renderers/KnativeSourceRenderer.tsx +3 -2
  23. package/src/components/resources/renderers/PodRenderer.tsx +164 -8
  24. package/src/components/resources/renderers/VPARenderer.tsx +2 -1
  25. package/src/components/resources/renderers/WorkloadRenderer.tsx +53 -55
  26. package/src/components/resources/renderers/contour-cells.tsx +48 -0
  27. package/src/components/resources/renderers/index.ts +2 -0
  28. package/src/components/resources/renderers/trivy-shared.tsx +9 -25
  29. package/src/components/resources/resource-utils-contour.ts +34 -0
  30. package/src/components/resources/resource-utils.ts +91 -0
  31. package/src/components/shared/ResourceActionsBar.tsx +276 -144
  32. package/src/components/shared/ResourceRendererDispatch.tsx +59 -5
  33. package/src/components/shared/index.ts +1 -1
  34. package/src/components/timeline/shared.tsx +8 -10
  35. package/src/components/topology/K8sResourceNode.tsx +1 -0
  36. package/src/components/topology/TopologyFilterSidebar.tsx +3 -0
  37. package/src/components/topology/TopologyGraph.tsx +2 -17
  38. package/src/components/topology/layout.ts +1 -0
  39. package/src/components/topology/topology.css +1 -0
  40. package/src/components/ui/CodeViewer.tsx +1 -2
  41. package/src/components/ui/ConfirmDialog.tsx +85 -130
  42. package/src/components/ui/DialogPortal.tsx +82 -0
  43. package/src/components/ui/ForceDeleteConfirmDialog.tsx +84 -10
  44. package/src/components/ui/Toast.tsx +5 -5
  45. package/src/components/ui/drawer-components.tsx +28 -22
  46. package/src/components/ui/index.ts +1 -0
  47. package/src/components/workload/WorkloadView.tsx +30 -2
  48. package/src/theme/index.ts +3 -0
  49. package/src/theme/tailwind-theme.css +61 -0
  50. package/src/theme/variables.css +121 -0
  51. package/src/types/core.ts +13 -2
  52. package/src/utils/animation.ts +1 -1
  53. package/src/utils/badge-colors.ts +158 -123
  54. package/src/utils/log-format.ts +127 -35
  55. package/src/utils/navigation.test.ts +142 -0
  56. package/src/utils/navigation.ts +52 -47
  57. package/src/utils/resource-hierarchy.ts +5 -0
  58. 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').toLowerCase() + 's'}
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').toLowerCase() + 's'}
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?.toLowerCase() + 's'}
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?.toLowerCase() || 'services') + (subscriberRef.kind?.toLowerCase().endsWith('s') ? '' : 's')}
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?.toLowerCase() || 'channels') + (channelRef.kind?.toLowerCase().endsWith('s') ? '' : 's')}
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?.toLowerCase() || 'services') + (subscriberRef.kind?.toLowerCase().endsWith('s') ? '' : 's')}
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?.toLowerCase() || 'channels') + (replyRef.kind?.toLowerCase().endsWith('s') ? '' : 's')}
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?.toLowerCase() || 'services') + 's'}
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?.toLowerCase() || 'services') + (destRef.ref.kind?.toLowerCase().endsWith('s') ? '' : 's')}
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?.toLowerCase() || 'deployments') + 's'}
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?.toLowerCase() || 'services') + (ref.kind?.toLowerCase().endsWith('s') ? '' : 's')}
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?.toLowerCase() || 'services') + (sink.ref.kind?.toLowerCase().endsWith('s') ? '' : 's')}
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?.toLowerCase() || 'deployments') + 's'}
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').toLowerCase() + 's'}
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
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
218
- <div className="bg-theme-surface border border-theme-border rounded-lg shadow-xl w-80 p-4">
219
- <h3 className="text-sm font-medium text-theme-text-primary mb-4">
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
- <div className="flex items-center justify-center gap-4 mb-4">
224
- <button
225
- onClick={() => setTargetReplicas(Math.max(0, targetReplicas - 1))}
226
- className="p-2 rounded-lg bg-theme-elevated hover:bg-theme-hover text-theme-text-secondary hover:text-theme-text-primary transition-colors"
227
- disabled={targetReplicas <= 0}
228
- >
229
- <Minus className="w-5 h-5" />
230
- </button>
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
- <input
233
- type="number"
234
- min="0"
235
- max="10000"
236
- value={targetReplicas}
237
- onChange={(e) => setTargetReplicas(Math.min(10000, Math.max(0, parseInt(e.target.value) || 0)))}
238
- 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"
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
- <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>
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
- <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>
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
- <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>
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
+ }
@@ -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,