@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.
- package/package.json +2 -2
- package/src/components/resources/ResourcesSidebar.tsx +152 -57
- package/src/components/resources/ResourcesView.tsx +141 -49
- package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +190 -28
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +22 -0
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +156 -5
- package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +28 -1
- package/src/components/resources/renderers/CertificateRenderer.tsx +47 -8
- package/src/components/resources/renderers/ChallengeRenderer.tsx +22 -3
- package/src/components/resources/renderers/CiliumNetworkPolicyRenderer.tsx +262 -45
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +81 -9
- package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +10 -3
- package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +59 -1
- package/src/components/resources/renderers/ClusterNetworkPolicyRenderer.tsx +7 -21
- package/src/components/resources/renderers/ExternalSecretRenderer.tsx +3 -5
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +91 -3
- package/src/components/resources/renderers/GitRepositoryRenderer.tsx +3 -42
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +3 -41
- package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +66 -2
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +31 -1
- package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +66 -1
- package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +3 -0
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +55 -1
- package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +23 -0
- package/src/components/resources/renderers/KustomizationRenderer.tsx +38 -3
- package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +7 -56
- package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +3 -42
- package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +3 -8
- package/src/components/resources/renderers/PodMonitorRenderer.tsx +5 -7
- package/src/components/resources/renderers/PodRenderer.tsx +22 -74
- package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +178 -20
- package/src/components/resources/renderers/ReplicaSetRenderer.tsx +2 -2
- package/src/components/resources/renderers/RolloutRenderer.tsx +105 -3
- package/src/components/resources/renderers/SbomReportRenderer.tsx +46 -5
- package/src/components/resources/renderers/SealedSecretRenderer.tsx +11 -2
- package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +5 -7
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +11 -3
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +11 -3
- package/src/components/resources/renderers/WebhookConfigRenderer.tsx +8 -16
- package/src/components/resources/renderers/WorkflowRenderer.tsx +71 -17
- package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +46 -1
- package/src/components/resources/renderers/karpenter-cells.tsx +18 -3
- package/src/components/resources/resource-utils-cnpg.ts +47 -0
- package/src/components/resources/resource-utils-karpenter.ts +6 -0
- package/src/components/resources/resource-utils-prometheus.ts +50 -6
- package/src/components/resources/resource-utils.ts +131 -33
- package/src/components/shared/EditableYamlView.tsx +38 -3
- package/src/components/shared/ResourceRendererDispatch.tsx +4 -4
- package/src/components/timeline/TimelineList.tsx +11 -4
- package/src/components/ui/Tooltip.tsx +16 -0
- package/src/components/ui/drawer-components.tsx +48 -1
- package/src/theme/variables.css +1 -1
- package/src/types/gitops.ts +8 -0
- package/src/utils/navigation.test.ts +29 -2
- 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,
|
|
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
|
-
<
|
|
34
|
-
<
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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,
|
|
2
|
-
import {
|
|
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
|
-
{
|
|
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,
|
|
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
|
|
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
|
-
{
|
|
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,
|
|
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
|
|
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
|
-
|
|
55
|
-
<
|
|
56
|
-
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
|
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=
|
|
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
|
|
316
|
-
const hasProblems =
|
|
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"
|
|
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 */}
|