@skyhook-io/k8s-ui 1.4.0 → 1.4.2

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 (55) hide show
  1. package/package.json +2 -2
  2. package/src/components/resources/ResourcesSidebar.tsx +152 -57
  3. package/src/components/resources/ResourcesView.tsx +141 -49
  4. package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +190 -28
  5. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +22 -0
  6. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +156 -5
  7. package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +28 -1
  8. package/src/components/resources/renderers/CertificateRenderer.tsx +47 -8
  9. package/src/components/resources/renderers/ChallengeRenderer.tsx +22 -3
  10. package/src/components/resources/renderers/CiliumNetworkPolicyRenderer.tsx +262 -45
  11. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +81 -9
  12. package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +10 -3
  13. package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +59 -1
  14. package/src/components/resources/renderers/ClusterNetworkPolicyRenderer.tsx +7 -21
  15. package/src/components/resources/renderers/ExternalSecretRenderer.tsx +3 -5
  16. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +91 -3
  17. package/src/components/resources/renderers/GitRepositoryRenderer.tsx +3 -42
  18. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +3 -41
  19. package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +66 -2
  20. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +31 -1
  21. package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +66 -1
  22. package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +3 -0
  23. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +55 -1
  24. package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +23 -0
  25. package/src/components/resources/renderers/KustomizationRenderer.tsx +38 -3
  26. package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +7 -56
  27. package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +3 -42
  28. package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +3 -8
  29. package/src/components/resources/renderers/PodMonitorRenderer.tsx +5 -7
  30. package/src/components/resources/renderers/PodRenderer.tsx +22 -74
  31. package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +178 -20
  32. package/src/components/resources/renderers/ReplicaSetRenderer.tsx +2 -2
  33. package/src/components/resources/renderers/RolloutRenderer.tsx +105 -3
  34. package/src/components/resources/renderers/SbomReportRenderer.tsx +46 -5
  35. package/src/components/resources/renderers/SealedSecretRenderer.tsx +11 -2
  36. package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +5 -7
  37. package/src/components/resources/renderers/VeleroBackupRenderer.tsx +11 -3
  38. package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +11 -3
  39. package/src/components/resources/renderers/WebhookConfigRenderer.tsx +8 -16
  40. package/src/components/resources/renderers/WorkflowRenderer.tsx +71 -17
  41. package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +46 -1
  42. package/src/components/resources/renderers/karpenter-cells.tsx +18 -3
  43. package/src/components/resources/resource-utils-cnpg.ts +47 -0
  44. package/src/components/resources/resource-utils-karpenter.ts +6 -0
  45. package/src/components/resources/resource-utils-prometheus.ts +50 -6
  46. package/src/components/resources/resource-utils.ts +131 -33
  47. package/src/components/shared/EditableYamlView.tsx +38 -3
  48. package/src/components/shared/ResourceRendererDispatch.tsx +4 -4
  49. package/src/components/timeline/TimelineList.tsx +11 -4
  50. package/src/components/ui/Tooltip.tsx +16 -0
  51. package/src/components/ui/drawer-components.tsx +48 -1
  52. package/src/theme/variables.css +1 -1
  53. package/src/types/gitops.ts +8 -0
  54. package/src/utils/navigation.test.ts +29 -2
  55. package/src/utils/navigation.ts +45 -24
@@ -1,4 +1,4 @@
1
- import { Server, Settings, Shield, Cpu, Tag } from 'lucide-react'
1
+ import { Server, Settings, Shield, Cpu, Tag, BarChart3 } from 'lucide-react'
2
2
  import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink } from '../../ui/drawer-components'
3
3
  import { kindToPlural } from '../../../utils/navigation'
4
4
  import {
@@ -9,6 +9,16 @@ import {
9
9
  getNodePoolWeight,
10
10
  } from '../resource-utils-karpenter'
11
11
 
12
+ function formatCpuCores(value: string): string {
13
+ // Karpenter status.resources CPU is typically in millicores (e.g. "12000m") or cores (e.g. "12")
14
+ if (value.endsWith('m')) {
15
+ const millis = parseInt(value, 10)
16
+ if (!isNaN(millis)) return String(millis / 1000)
17
+ }
18
+ return value
19
+ }
20
+
21
+
12
22
  interface KarpenterNodePoolRendererProps {
13
23
  data: any
14
24
  onNavigate?: (ref: { kind: string; namespace: string; name: string; group?: string }) => void
@@ -28,6 +38,9 @@ export function KarpenterNodePoolRenderer({ data, onNavigate }: KarpenterNodePoo
28
38
  const templateLabels = spec.template?.metadata?.labels || {}
29
39
  const templateExpireAfter = spec.template?.spec?.expireAfter
30
40
  const nodeClassRef = spec.template?.spec?.nodeClassRef
41
+ const templateTaints = spec.template?.spec?.taints || []
42
+ const templateStartupTaints = spec.template?.spec?.startupTaints || []
43
+ const statusResources = status.resources || {}
31
44
 
32
45
  return (
33
46
  <>
@@ -77,6 +90,26 @@ export function KarpenterNodePoolRenderer({ data, onNavigate }: KarpenterNodePoo
77
90
  </PropertyList>
78
91
  </Section>
79
92
 
93
+ {/* Resource Usage — from status.resources vs spec.limits */}
94
+ {(statusResources.cpu || statusResources.memory) && (
95
+ <Section title="Resource Usage" icon={BarChart3} defaultExpanded>
96
+ <PropertyList>
97
+ {statusResources.cpu && (
98
+ <Property
99
+ label="CPU"
100
+ value={`${formatCpuCores(statusResources.cpu)}${spec.limits?.cpu ? ` / ${formatCpuCores(spec.limits.cpu)}` : ''}`}
101
+ />
102
+ )}
103
+ {statusResources.memory && (
104
+ <Property
105
+ label="Memory"
106
+ value={`${statusResources.memory}${spec.limits?.memory ? ` / ${spec.limits.memory}` : ''}`}
107
+ />
108
+ )}
109
+ </PropertyList>
110
+ </Section>
111
+ )}
112
+
80
113
  {/* Disruption */}
81
114
  <Section title="Disruption" icon={Shield}>
82
115
  <PropertyList>
@@ -118,6 +151,38 @@ export function KarpenterNodePoolRenderer({ data, onNavigate }: KarpenterNodePoo
118
151
  </Section>
119
152
  )}
120
153
 
154
+ {/* Template Taints */}
155
+ {templateTaints.length > 0 && (
156
+ <Section title={`Template Taints (${templateTaints.length})`} icon={Shield} defaultExpanded>
157
+ <div className="flex flex-wrap gap-1">
158
+ {templateTaints.map((taint: any, i: number) => (
159
+ <span
160
+ key={i}
161
+ className="badge-sm bg-theme-hover text-theme-text-secondary"
162
+ >
163
+ {taint.key}={taint.value || ''}:{taint.effect || ''}
164
+ </span>
165
+ ))}
166
+ </div>
167
+ </Section>
168
+ )}
169
+
170
+ {/* Template Startup Taints */}
171
+ {templateStartupTaints.length > 0 && (
172
+ <Section title={`Startup Taints (${templateStartupTaints.length})`} icon={Shield} defaultExpanded>
173
+ <div className="flex flex-wrap gap-1">
174
+ {templateStartupTaints.map((taint: any, i: number) => (
175
+ <span
176
+ key={i}
177
+ className="badge-sm bg-theme-hover text-theme-text-secondary"
178
+ >
179
+ {taint.key}={taint.value || ''}:{taint.effect || ''}
180
+ </span>
181
+ ))}
182
+ </div>
183
+ </Section>
184
+ )}
185
+
121
186
  {/* Requirements */}
122
187
  {requirements.length > 0 && (
123
188
  <Section title={`Requirements (${requirements.length})`} icon={Settings} defaultExpanded>
@@ -46,6 +46,9 @@ export function KedaScaledJobRenderer({ data }: KedaScaledJobRendererProps) {
46
46
  {spec.failedJobsHistoryLimit !== undefined && (
47
47
  <Property label="Failure History" value={String(spec.failedJobsHistoryLimit)} />
48
48
  )}
49
+ {spec.minReplicaCount !== undefined && (
50
+ <Property label="Min Replicas" value={String(spec.minReplicaCount)} />
51
+ )}
49
52
  {spec.maxReplicaCount !== undefined && (
50
53
  <Property label="Max Replicas" value={String(spec.maxReplicaCount)} />
51
54
  )}
@@ -1,4 +1,4 @@
1
- import { Cpu } from 'lucide-react'
1
+ import { Cpu, Settings } from 'lucide-react'
2
2
  import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink } from '../../ui/drawer-components'
3
3
  import { kindToPlural } from '../../../utils/navigation'
4
4
  import {
@@ -12,6 +12,16 @@ import {
12
12
  getScaledObjectCooldownPeriod,
13
13
  } from '../resource-utils-keda'
14
14
 
15
+ function summarizeScalingPolicies(policies: any[] | undefined, periodSeconds?: number): string {
16
+ if (!policies || policies.length === 0) return '-'
17
+ return policies.map((p: any) => {
18
+ const value = p.value ?? '?'
19
+ const type = p.type === 'Percent' ? '%' : ` pod${value !== 1 ? 's' : ''}`
20
+ const period = p.periodSeconds ?? periodSeconds ?? '?'
21
+ return `max ${value}${type}/${period}s`
22
+ }).join(', ')
23
+ }
24
+
15
25
  interface KedaScaledObjectRendererProps {
16
26
  data: any
17
27
  onNavigate?: (ref: { kind: string; namespace: string; name: string }) => void
@@ -131,6 +141,50 @@ export function KedaScaledObjectRenderer({ data, onNavigate }: KedaScaledObjectR
131
141
  </Section>
132
142
  )}
133
143
 
144
+ {/* Advanced section */}
145
+ {data.spec?.advanced && (
146
+ <Section title="Advanced" icon={Settings} defaultExpanded={false}>
147
+ <PropertyList>
148
+ {data.spec.advanced.restoreToOriginalReplicaCount != null && (
149
+ <Property
150
+ label="Restore Original Replicas"
151
+ value={data.spec.advanced.restoreToOriginalReplicaCount ? 'Yes' : 'No'}
152
+ />
153
+ )}
154
+ {data.spec.advanced.horizontalPodAutoscalerConfig?.behavior?.scaleUp && (
155
+ <Property
156
+ label="Scale Up"
157
+ value={summarizeScalingPolicies(
158
+ data.spec.advanced.horizontalPodAutoscalerConfig.behavior.scaleUp.policies,
159
+ data.spec.advanced.horizontalPodAutoscalerConfig.behavior.scaleUp.stabilizationWindowSeconds
160
+ )}
161
+ />
162
+ )}
163
+ {data.spec.advanced.horizontalPodAutoscalerConfig?.behavior?.scaleDown && (
164
+ <Property
165
+ label="Scale Down"
166
+ value={summarizeScalingPolicies(
167
+ data.spec.advanced.horizontalPodAutoscalerConfig.behavior.scaleDown.policies,
168
+ data.spec.advanced.horizontalPodAutoscalerConfig.behavior.scaleDown.stabilizationWindowSeconds
169
+ )}
170
+ />
171
+ )}
172
+ {data.spec.advanced.horizontalPodAutoscalerConfig?.behavior?.scaleUp?.stabilizationWindowSeconds != null && (
173
+ <Property
174
+ label="Scale Up Stabilization"
175
+ value={`${data.spec.advanced.horizontalPodAutoscalerConfig.behavior.scaleUp.stabilizationWindowSeconds}s`}
176
+ />
177
+ )}
178
+ {data.spec.advanced.horizontalPodAutoscalerConfig?.behavior?.scaleDown?.stabilizationWindowSeconds != null && (
179
+ <Property
180
+ label="Scale Down Stabilization"
181
+ value={`${data.spec.advanced.horizontalPodAutoscalerConfig.behavior.scaleDown.stabilizationWindowSeconds}s`}
182
+ />
183
+ )}
184
+ </PropertyList>
185
+ </Section>
186
+ )}
187
+
134
188
  <ConditionsSection conditions={conditions} />
135
189
  </>
136
190
  )
@@ -19,6 +19,7 @@ export function KedaTriggerAuthRenderer({ data, onNavigate }: KedaTriggerAuthRen
19
19
  const vault = spec.hashiCorpVault
20
20
  const azureKeyVault = spec.azureKeyVault
21
21
  const awsSecretManager = spec.awsSecretManager
22
+ const gcpSecretManager = spec.gcpSecretManager
22
23
  const podIdentity = spec.podIdentity
23
24
 
24
25
  return (
@@ -145,6 +146,28 @@ export function KedaTriggerAuthRenderer({ data, onNavigate }: KedaTriggerAuthRen
145
146
  </Section>
146
147
  )}
147
148
 
149
+ {/* GCP Secret Manager */}
150
+ {gcpSecretManager && (
151
+ <Section title="GCP Secret Manager" icon={Cloud}>
152
+ <PropertyList>
153
+ {gcpSecretManager.credentials?.clientSecret?.name && (
154
+ <Property label="Credentials Secret" value={gcpSecretManager.credentials.clientSecret.name} />
155
+ )}
156
+ </PropertyList>
157
+ {gcpSecretManager.secrets && gcpSecretManager.secrets.length > 0 && (
158
+ <div className="mt-2 space-y-1">
159
+ <div className="text-xs font-medium text-theme-text-secondary uppercase tracking-wider mb-1">Secrets</div>
160
+ {gcpSecretManager.secrets.map((s: any, i: number) => (
161
+ <div key={i} className="card-inner text-sm">
162
+ <span className="text-theme-text-primary font-medium">{s.parameter}</span>
163
+ <span className="text-theme-text-secondary ml-2">{s.id}{s.version ? ` (v${s.version})` : ''}</span>
164
+ </div>
165
+ ))}
166
+ </div>
167
+ )}
168
+ </Section>
169
+ )}
170
+
148
171
  {conditions.length > 0 && <ConditionsSection conditions={conditions} />}
149
172
  </>
150
173
  )
@@ -1,14 +1,15 @@
1
- import { Layers, FolderTree } from 'lucide-react'
2
- import { Section, PropertyList, Property, ConditionsSection, ProblemAlerts } from '../../ui/drawer-components'
1
+ import { Layers, FolderTree, GitBranch } from 'lucide-react'
2
+ import { Section, PropertyList, Property, ConditionsSection, ProblemAlerts, ResourceLink } from '../../ui/drawer-components'
3
3
  import { formatAge } from '../resource-utils'
4
4
  import { GitOpsStatusBadge, ManagedResourcesList, SyncCountdown } from '../../gitops'
5
5
  import { fluxConditionsToGitOpsStatus, parseFluxInventory, type FluxCondition } from '../../../types/gitops'
6
6
 
7
7
  interface KustomizationRendererProps {
8
8
  data: any
9
+ onNavigate?: (ref: { kind: string; namespace: string; name: string }) => void
9
10
  }
10
11
 
11
- export function KustomizationRenderer({ data }: KustomizationRendererProps) {
12
+ export function KustomizationRenderer({ data, onNavigate }: KustomizationRendererProps) {
12
13
  const status = data.status || {}
13
14
  const spec = data.spec || {}
14
15
  const conditions = (status.conditions || []) as FluxCondition[]
@@ -39,6 +40,23 @@ export function KustomizationRenderer({ data }: KustomizationRendererProps) {
39
40
  })
40
41
  }
41
42
 
43
+ // Revision mismatch detection — only flag when Ready=False to avoid false positives during active reconciliation
44
+ const isReadyFalse = conditions.find((c: any) => c.type === 'Ready')?.status === 'False'
45
+ if (
46
+ isReadyFalse &&
47
+ status.lastAppliedRevision &&
48
+ status.lastAttemptedRevision &&
49
+ status.lastAppliedRevision !== status.lastAttemptedRevision
50
+ ) {
51
+ problems.push({
52
+ color: 'red',
53
+ message: `Revision mismatch: attempted ${status.lastAttemptedRevision} but applied ${status.lastAppliedRevision} — the latest reconciliation failed.`,
54
+ })
55
+ }
56
+
57
+ // Dependencies
58
+ const dependsOn: Array<{ name: string; namespace?: string }> = spec.dependsOn || []
59
+
42
60
  // Source reference
43
61
  const sourceRef = spec.sourceRef || {}
44
62
 
@@ -94,6 +112,23 @@ export function KustomizationRenderer({ data }: KustomizationRendererProps) {
94
112
  </PropertyList>
95
113
  </Section>
96
114
 
115
+ {/* Dependencies */}
116
+ {dependsOn.length > 0 && (
117
+ <Section title={`Dependencies (${dependsOn.length})`} icon={GitBranch}>
118
+ <div className="flex flex-wrap gap-1">
119
+ {dependsOn.map((dep, idx) => (
120
+ <ResourceLink
121
+ key={idx}
122
+ name={dep.name}
123
+ kind="kustomizations"
124
+ namespace={dep.namespace || data.metadata?.namespace || ''}
125
+ onNavigate={onNavigate}
126
+ />
127
+ ))}
128
+ </div>
129
+ </Section>
130
+ )}
131
+
97
132
  {/* Health checks */}
98
133
  {spec.healthChecks && spec.healthChecks.length > 0 && (
99
134
  <Section title={`Health Checks (${spec.healthChecks.length})`} defaultExpanded={false}>
@@ -1,6 +1,6 @@
1
1
  import { Shield, ArrowDownToLine, ArrowUpFromLine, GitFork } from 'lucide-react'
2
2
  import { clsx } from 'clsx'
3
- import { Section, PropertyList, Property } from '../../ui/drawer-components'
3
+ import { Section, LabelSelectorDisplay } from '../../ui/drawer-components'
4
4
  import { NetworkPolicyDiagram } from './NetworkPolicyDiagram'
5
5
 
6
6
  interface NetworkPolicyRendererProps {
@@ -14,8 +14,6 @@ export function NetworkPolicyRenderer({ data }: NetworkPolicyRendererProps) {
14
14
  const ingress: any[] | undefined = spec.ingress
15
15
  const egress: any[] | undefined = spec.egress
16
16
 
17
- const matchLabels = podSelector.matchLabels || {}
18
- const hasMatchLabels = Object.keys(matchLabels).length > 0
19
17
  const hasIngress = policyTypes.includes('Ingress')
20
18
  const hasEgress = policyTypes.includes('Egress')
21
19
 
@@ -30,27 +28,10 @@ export function NetworkPolicyRenderer({ data }: NetworkPolicyRendererProps) {
30
28
  )}
31
29
 
32
30
  <Section title="Target" icon={Shield}>
33
- <PropertyList>
34
- <Property
35
- label="Pod Selector"
36
- value={hasMatchLabels ? undefined : 'All pods in namespace'}
37
- />
38
- </PropertyList>
39
- {hasMatchLabels && (
40
- <div className="mt-2">
41
- <div className="text-xs text-theme-text-tertiary mb-1">Pod Selector</div>
42
- <div className="flex flex-wrap gap-1">
43
- {Object.entries(matchLabels).map(([k, v]) => (
44
- <span
45
- key={k}
46
- className="badge bg-theme-elevated text-theme-text-secondary"
47
- >
48
- {k}={String(v)}
49
- </span>
50
- ))}
51
- </div>
52
- </div>
53
- )}
31
+ <div className="mt-2">
32
+ <div className="text-xs text-theme-text-tertiary mb-1">Pod Selector</div>
33
+ <LabelSelectorDisplay selector={podSelector} emptyText="All pods in namespace" />
34
+ </div>
54
35
  {policyTypes.length > 0 && (
55
36
  <div className="mt-2">
56
37
  <div className="text-xs text-theme-text-tertiary mb-1">Policy Types</div>
@@ -152,49 +133,19 @@ function IngressEgressRuleCard({
152
133
 
153
134
  function PeerEntry({ peer }: { peer: any }) {
154
135
  if (peer.podSelector) {
155
- const labels = peer.podSelector.matchLabels || {}
156
- const hasLabels = Object.keys(labels).length > 0
157
136
  return (
158
137
  <div className="text-sm">
159
138
  <span className="text-theme-text-secondary text-xs">podSelector: </span>
160
- {hasLabels ? (
161
- <span className="inline-flex flex-wrap gap-1 align-middle">
162
- {Object.entries(labels).map(([k, v]) => (
163
- <span
164
- key={k}
165
- className="badge bg-theme-elevated text-theme-text-secondary"
166
- >
167
- {k}={String(v)}
168
- </span>
169
- ))}
170
- </span>
171
- ) : (
172
- <span className="text-xs text-theme-text-tertiary">all pods</span>
173
- )}
139
+ <LabelSelectorDisplay selector={peer.podSelector} emptyText="all pods" inline />
174
140
  </div>
175
141
  )
176
142
  }
177
143
 
178
144
  if (peer.namespaceSelector) {
179
- const labels = peer.namespaceSelector.matchLabels || {}
180
- const hasLabels = Object.keys(labels).length > 0
181
145
  return (
182
146
  <div className="text-sm">
183
147
  <span className="text-theme-text-secondary text-xs">namespaceSelector: </span>
184
- {hasLabels ? (
185
- <span className="inline-flex flex-wrap gap-1 align-middle">
186
- {Object.entries(labels).map(([k, v]) => (
187
- <span
188
- key={k}
189
- className="badge bg-theme-elevated text-theme-text-secondary"
190
- >
191
- {k}={String(v)}
192
- </span>
193
- ))}
194
- </span>
195
- ) : (
196
- <span className="text-xs text-theme-text-tertiary">all namespaces</span>
197
- )}
148
+ <LabelSelectorDisplay selector={peer.namespaceSelector} emptyText="all namespaces" inline />
198
149
  </div>
199
150
  )
200
151
  }
@@ -1,6 +1,5 @@
1
- import { Package, AlertTriangle, CheckCircle2 } from 'lucide-react'
2
- import { clsx } from 'clsx'
3
- import { Section, PropertyList, Property, ConditionsSection } from '../../ui/drawer-components'
1
+ import { Package, CheckCircle2 } from 'lucide-react'
2
+ import { Section, PropertyList, Property, ConditionsSection, ProblemAlerts } from '../../ui/drawer-components'
4
3
  import { formatAge } from '../resource-utils'
5
4
  import { formatBytes } from '../../../utils/format'
6
5
  import { GitOpsStatusBadge, SyncCountdown } from '../../gitops'
@@ -36,45 +35,7 @@ export function OCIRepositoryRenderer({ data }: OCIRepositoryRendererProps) {
36
35
 
37
36
  return (
38
37
  <>
39
- {/* Problem alerts */}
40
- {problems.map((problem, i) => (
41
- <div
42
- key={i}
43
- className={clsx(
44
- 'mb-4 p-3 border rounded-lg',
45
- problem.color === 'red'
46
- ? 'bg-red-500/10 border-red-500/30'
47
- : 'bg-yellow-500/10 border-yellow-500/30'
48
- )}
49
- >
50
- <div className="flex items-start gap-2">
51
- <AlertTriangle
52
- className={clsx(
53
- 'w-4 h-4 mt-0.5 shrink-0',
54
- problem.color === 'red' ? 'text-red-400' : 'text-yellow-400'
55
- )}
56
- />
57
- <div className="flex-1 min-w-0">
58
- <div
59
- className={clsx(
60
- 'text-sm font-medium',
61
- problem.color === 'red' ? 'text-red-400' : 'text-yellow-400'
62
- )}
63
- >
64
- {problem.color === 'red' ? 'Issue Detected' : 'Warning'}
65
- </div>
66
- <div
67
- className={clsx(
68
- 'text-xs mt-1',
69
- problem.color === 'red' ? 'text-red-300/80' : 'text-yellow-300/80'
70
- )}
71
- >
72
- {problem.message}
73
- </div>
74
- </div>
75
- </div>
76
- </div>
77
- ))}
38
+ <ProblemAlerts problems={problems} />
78
39
 
79
40
  {/* Status section */}
80
41
  <Section title="Status">
@@ -1,6 +1,6 @@
1
1
  import { Shield, Activity } from 'lucide-react'
2
2
  import { clsx } from 'clsx'
3
- import { Section, PropertyList, Property, ConditionsSection, KeyValueBadgeList, AlertBanner } from '../../ui/drawer-components'
3
+ import { Section, PropertyList, Property, ConditionsSection, LabelSelectorDisplay, AlertBanner } from '../../ui/drawer-components'
4
4
 
5
5
  interface PodDisruptionBudgetRendererProps {
6
6
  data: any
@@ -9,8 +9,7 @@ interface PodDisruptionBudgetRendererProps {
9
9
  export function PodDisruptionBudgetRenderer({ data }: PodDisruptionBudgetRendererProps) {
10
10
  const spec = data.spec || {}
11
11
  const status = data.status || {}
12
- const matchLabels = spec.selector?.matchLabels || {}
13
- const hasSelector = Object.keys(matchLabels).length > 0
12
+ const selector = spec.selector
14
13
 
15
14
  // Determine budget type
16
15
  const hasMaxUnavailable = spec.maxUnavailable !== undefined && spec.maxUnavailable !== null
@@ -113,11 +112,7 @@ export function PodDisruptionBudgetRenderer({ data }: PodDisruptionBudgetRendere
113
112
  </Section>
114
113
 
115
114
  <Section title="Selector">
116
- {hasSelector ? (
117
- <KeyValueBadgeList items={matchLabels} />
118
- ) : (
119
- <div className="text-sm text-theme-text-tertiary">All pods in namespace</div>
120
- )}
115
+ <LabelSelectorDisplay selector={selector} emptyText="All pods in namespace" />
121
116
  </Section>
122
117
 
123
118
  <ConditionsSection conditions={status.conditions} />
@@ -1,5 +1,5 @@
1
1
  import { Radio } from 'lucide-react'
2
- import { Section, PropertyList, Property, ConditionsSection, KeyValueBadgeList } from '../../ui/drawer-components'
2
+ import { Section, PropertyList, Property, ConditionsSection, LabelSelectorDisplay } from '../../ui/drawer-components'
3
3
  import {
4
4
  getPodMonitorEndpoints,
5
5
  getPodMonitorNamespaceSelector,
@@ -11,7 +11,7 @@ interface PodMonitorRendererProps {
11
11
 
12
12
  export function PodMonitorRenderer({ data }: PodMonitorRendererProps) {
13
13
  const endpoints = getPodMonitorEndpoints(data)
14
- const selector = data.spec?.selector?.matchLabels
14
+ const selector = data.spec?.selector
15
15
  const conditions = data.status?.conditions
16
16
 
17
17
  return (
@@ -51,11 +51,9 @@ export function PodMonitorRenderer({ data }: PodMonitorRendererProps) {
51
51
  </Section>
52
52
  )}
53
53
 
54
- {selector && Object.keys(selector).length > 0 && (
55
- <Section title="Selector">
56
- <KeyValueBadgeList items={selector} />
57
- </Section>
58
- )}
54
+ <Section title="Selector">
55
+ <LabelSelectorDisplay selector={selector} emptyText="No selector" />
56
+ </Section>
59
57
 
60
58
  <ConditionsSection conditions={conditions} />
61
59
  </>
@@ -2,7 +2,7 @@ import { useState, type ReactNode, type JSX } from 'react'
2
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
- import { formatResources, formatDuration } from '../resource-utils'
5
+ import { formatResources, formatDuration, getPodProblems, SEVERITY_DOT_COLOR } from '../resource-utils'
6
6
  import { getResourceStatusColor, SEVERITY_BADGE_BORDERED } from '../../../utils/badge-colors'
7
7
  import type { ResolvedEnvFrom } from '../../../types'
8
8
  import { Tooltip } from '../../ui/Tooltip'
@@ -77,70 +77,6 @@ interface PodRendererProps {
77
77
  resolvedEnvFrom?: ResolvedEnvFrom
78
78
  }
79
79
 
80
- // Extract problems from pod status and conditions
81
- function getPodProblems(data: any): string[] {
82
- const problems: string[] = []
83
- const phase = data.status?.phase
84
- const conditions = data.status?.conditions || []
85
- const containerStatuses = data.status?.containerStatuses || []
86
- const initContainerStatuses = data.status?.initContainerStatuses || []
87
-
88
- // Check phase
89
- if (phase === 'Failed' || phase === 'Unknown') {
90
- problems.push(`Pod is in ${phase} state`)
91
- }
92
-
93
- // Check conditions for scheduling/init issues
94
- for (const cond of conditions) {
95
- if (cond.status === 'False') {
96
- if (cond.type === 'PodScheduled' && cond.reason) {
97
- problems.push(`Scheduling: ${cond.reason}${cond.message ? ' - ' + cond.message : ''}`)
98
- } else if (cond.type === 'Initialized' && cond.message) {
99
- problems.push(`Init failed: ${cond.message}`)
100
- } else if (cond.type === 'ContainersReady' && cond.reason === 'ContainersNotReady') {
101
- // Will be covered by container status details
102
- } else if (cond.message) {
103
- problems.push(`${cond.type}: ${cond.message}`)
104
- }
105
- }
106
- }
107
-
108
- // Check init container failures
109
- for (const initStatus of initContainerStatuses) {
110
- if (initStatus.state?.waiting?.reason && initStatus.state.waiting.reason !== 'PodInitializing') {
111
- problems.push(`Init container "${initStatus.name}": ${initStatus.state.waiting.reason}`)
112
- }
113
- if (initStatus.state?.terminated?.exitCode && initStatus.state.terminated.exitCode !== 0) {
114
- problems.push(`Init container "${initStatus.name}" failed with exit code ${initStatus.state.terminated.exitCode}`)
115
- }
116
- }
117
-
118
- // Check container issues (most important)
119
- for (const status of containerStatuses) {
120
- const waiting = status.state?.waiting
121
- const terminated = status.state?.terminated
122
-
123
- if (waiting?.reason && waiting.reason !== 'ContainerCreating') {
124
- const msg = `Container "${status.name}": ${waiting.reason}`
125
- if (waiting.reason === 'ImagePullBackOff' || waiting.reason === 'ErrImagePull') {
126
- problems.push(msg + (waiting.message ? ` - ${waiting.message.slice(0, 100)}` : ''))
127
- } else if (waiting.reason === 'CrashLoopBackOff') {
128
- problems.push(msg + ' (container keeps crashing)')
129
- } else {
130
- problems.push(msg)
131
- }
132
- }
133
-
134
- if (terminated?.reason === 'OOMKilled') {
135
- problems.push(`Container "${status.name}" was OOMKilled - increase memory limit`)
136
- } else if (terminated?.exitCode && terminated.exitCode !== 0) {
137
- problems.push(`Container "${status.name}" exited with code ${terminated.exitCode}`)
138
- }
139
- }
140
-
141
- return problems
142
- }
143
-
144
80
  // ── Env vars section — extracted to use hooks (useState for reveal) ──────────
145
81
 
146
82
  function SecretValueCell({ value }: { value: string }) {
@@ -189,13 +125,13 @@ function resolveEnvValueNode(env: any): JSX.Element {
189
125
  }
190
126
  if (env.valueFrom?.configMapKeyRef) {
191
127
  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>
128
+ return <span className="text-blue-700 dark:text-blue-400/80 text-[10px] shrink-0 self-center px-1 py-0.5 bg-blue-500/10 rounded">configmap:{name}[{key}]</span>
193
129
  }
194
130
  if (env.valueFrom?.fieldRef) {
195
- return <span className="text-purple-400/70 break-all">field:{env.valueFrom.fieldRef.fieldPath}</span>
131
+ return <span className="text-purple-700 dark:text-purple-400/80 break-all">field:{env.valueFrom.fieldRef.fieldPath}</span>
196
132
  }
197
133
  if (env.valueFrom?.resourceFieldRef) {
198
- return <span className="text-purple-400/70 break-all">resource:{env.valueFrom.resourceFieldRef.resource}</span>
134
+ return <span className="text-purple-700 dark:text-purple-400/80 break-all">resource:{env.valueFrom.resourceFieldRef.resource}</span>
199
135
  }
200
136
  return <span className="text-theme-text-primary break-all min-w-0">{env.value ?? ''}</span>
201
137
  }
@@ -218,10 +154,13 @@ function EnvVarsSection({
218
154
  resolvedEnvFrom?: ResolvedEnvFrom
219
155
  }) {
220
156
  const allContainers = [...initContainers, ...containers]
221
- const multiContainer = allContainers.filter((c: any) => c.env?.length > 0 || c.envFrom?.length > 0).length > 1
157
+ const containersWithEnv = allContainers.filter((c: any) => c.env?.length > 0 || c.envFrom?.length > 0)
158
+ const multiContainer = containersWithEnv.length > 1
159
+ const totalVars = allContainers.reduce((sum: number, c: any) => sum + (c.env?.length || 0) + (c.envFrom?.length || 0), 0)
160
+ const subtitle = multiContainer ? `${totalVars} vars across ${containersWithEnv.length} containers` : `${totalVars} vars`
222
161
 
223
162
  return (
224
- <Section title="Environment Variables" icon={List}>
163
+ <Section title={`Environment Variables · ${subtitle}`} icon={List} defaultExpanded={false}>
225
164
  <div className="space-y-4">
226
165
  {allContainers.map((container: any) => {
227
166
  const envVars: any[] = container.env || []
@@ -247,7 +186,7 @@ function EnvVarsSection({
247
186
  <div className="flex items-center gap-1.5 text-xs font-mono py-0.5">
248
187
  <span className={clsx(
249
188
  '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'
189
+ isSecret ? 'bg-amber-500/10 dark:bg-yellow-500/10 text-amber-700 dark:text-yellow-400' : 'bg-blue-500/10 text-blue-700 dark:text-blue-400/80'
251
190
  )}>
252
191
  {prefix}
253
192
  </span>
@@ -312,8 +251,8 @@ export function PodRenderer({
312
251
  const isRunning = data.status?.phase === 'Running'
313
252
 
314
253
  // Check for problems
315
- const problems = getPodProblems(data)
316
- const hasProblems = problems.length > 0
254
+ const podProblems = getPodProblems(data)
255
+ const hasProblems = podProblems.length > 0
317
256
 
318
257
  // Image filesystem modal state
319
258
  const [selectedImage, setSelectedImage] = useState<string | null>(null)
@@ -379,7 +318,16 @@ export function PodRenderer({
379
318
  <>
380
319
  {/* Problems alert - shown at top when there are issues */}
381
320
  {hasProblems && (
382
- <AlertBanner variant="error" title="Issues Detected" items={problems} />
321
+ <AlertBanner variant="error" title="Issues Detected">
322
+ <ul className="text-xs space-y-1">
323
+ {podProblems.map((p, i) => (
324
+ <li key={i} className="flex items-center gap-1.5">
325
+ <span className={clsx('w-1.5 h-1.5 rounded-full shrink-0', SEVERITY_DOT_COLOR[p.severity])} />
326
+ <span className="text-red-600 dark:text-red-400">{p.message}</span>
327
+ </li>
328
+ ))}
329
+ </ul>
330
+ </AlertBanner>
383
331
  )}
384
332
 
385
333
  {/* Status section */}