@skyhook-io/k8s-ui 1.3.2 → 1.4.1
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/audit/AuditAlerts.tsx +88 -0
- package/src/components/audit/AuditCard.tsx +130 -0
- package/src/components/audit/AuditFindingsTable.tsx +526 -0
- package/src/components/audit/index.ts +3 -0
- package/src/components/dock/DockContext.tsx +5 -2
- package/src/components/dock/LocalTerminalTab.tsx +11 -0
- package/src/components/resources/ResourcesView.tsx +126 -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 +445 -0
- 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 +160 -0
- package/src/components/resources/renderers/ExternalSecretRenderer.tsx +3 -5
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +91 -3
- package/src/components/resources/renderers/GenericRenderer.tsx +1 -1
- 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/NetworkPolicyDiagram.tsx +277 -0
- package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +17 -57
- 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/ServiceRenderer.tsx +40 -28
- 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/index.ts +3 -0
- 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/CreateResourceDialog.tsx +236 -0
- package/src/components/shared/EditableYamlView.tsx +16 -1
- package/src/components/shared/ResourceActionsBar.tsx +9 -7
- package/src/components/shared/ResourceRendererDispatch.tsx +13 -6
- package/src/components/shared/index.ts +1 -0
- package/src/components/timeline/TimelineList.tsx +11 -4
- package/src/components/topology/K8sResourceNode.tsx +15 -0
- package/src/components/topology/TopologyControls.tsx +21 -1
- package/src/components/topology/TopologyGraph.tsx +1 -1
- package/src/components/ui/Badge.tsx +8 -0
- package/src/components/ui/drawer-components.tsx +68 -5
- package/src/components/workload/WorkloadView.tsx +55 -29
- package/src/index.ts +3 -0
- package/src/theme/variables.css +1 -1
- package/src/types/core.ts +2 -1
- package/src/types/gitops.ts +8 -0
- package/src/utils/badge-colors.ts +7 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/k8s-errors.ts +142 -0
- package/src/utils/navigation.test.ts +29 -2
- package/src/utils/navigation.ts +45 -24
- package/src/utils/resource-icons.ts +3 -0
- package/src/utils/skeleton-yaml.ts +302 -0
|
@@ -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 */}
|
|
@@ -1,15 +1,168 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useState, useMemo } from 'react'
|
|
2
|
+
import { Bell, Search, ChevronRight } from 'lucide-react'
|
|
3
|
+
import { clsx } from 'clsx'
|
|
2
4
|
import { Section, PropertyList, Property, ConditionsSection } from '../../ui/drawer-components'
|
|
5
|
+
import { BADGE_SEVERITY_COLORS } from '../../ui/Badge'
|
|
3
6
|
import {
|
|
4
7
|
getPrometheusRuleGroups,
|
|
5
8
|
getPrometheusRuleTotalRules,
|
|
6
9
|
getPrometheusRuleGroupCount,
|
|
7
10
|
} from '../resource-utils-prometheus'
|
|
11
|
+
import type { PrometheusRuleGroup, PrometheusRule, PrometheusAlertRule, PrometheusRecordingRule } from '../resource-utils-prometheus'
|
|
8
12
|
|
|
9
13
|
interface PrometheusRuleRendererProps {
|
|
10
14
|
data: any
|
|
11
15
|
}
|
|
12
16
|
|
|
17
|
+
// Map Prometheus severity names to centralized badge colors
|
|
18
|
+
const SEVERITY_BADGE: Record<string, string> = {
|
|
19
|
+
critical: BADGE_SEVERITY_COLORS.error,
|
|
20
|
+
warning: BADGE_SEVERITY_COLORS.warning,
|
|
21
|
+
info: BADGE_SEVERITY_COLORS.info,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function matchesSearch(rule: PrometheusRule, term: string): boolean {
|
|
25
|
+
if (rule.type === 'alert') {
|
|
26
|
+
return (
|
|
27
|
+
rule.alert.toLowerCase().includes(term) ||
|
|
28
|
+
rule.expr.toLowerCase().includes(term) ||
|
|
29
|
+
(rule.severity || '').toLowerCase().includes(term) ||
|
|
30
|
+
(rule.summary || '').toLowerCase().includes(term) ||
|
|
31
|
+
(rule.description || '').toLowerCase().includes(term)
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
return (
|
|
35
|
+
(rule.record || '').toLowerCase().includes(term) ||
|
|
36
|
+
rule.expr.toLowerCase().includes(term)
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const EXPR_TRUNCATE_LEN = 200
|
|
41
|
+
const SUMMARY_TRUNCATE_LEN = 100
|
|
42
|
+
|
|
43
|
+
function TruncatedExpr({ expr }: { expr: string }) {
|
|
44
|
+
const [expanded, setExpanded] = useState(false)
|
|
45
|
+
const needsTruncation = expr.length > EXPR_TRUNCATE_LEN
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className="mt-1">
|
|
49
|
+
<pre className="text-xs font-mono text-theme-text-secondary bg-theme-elevated rounded px-2 py-1.5 whitespace-pre-wrap break-all">
|
|
50
|
+
{expanded || !needsTruncation ? expr : expr.slice(0, EXPR_TRUNCATE_LEN) + '...'}
|
|
51
|
+
</pre>
|
|
52
|
+
{needsTruncation && (
|
|
53
|
+
<button
|
|
54
|
+
onClick={() => setExpanded(!expanded)}
|
|
55
|
+
className="text-[10px] text-accent-text hover:underline mt-0.5"
|
|
56
|
+
>
|
|
57
|
+
{expanded ? 'Show less' : 'Show more'}
|
|
58
|
+
</button>
|
|
59
|
+
)}
|
|
60
|
+
</div>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function AlertRuleCard({ rule }: { rule: PrometheusAlertRule }) {
|
|
65
|
+
const severityClass = rule.severity ? SEVERITY_BADGE[rule.severity] : undefined
|
|
66
|
+
const summaryText = rule.summary || rule.description || ''
|
|
67
|
+
const truncatedSummary = summaryText.length > SUMMARY_TRUNCATE_LEN
|
|
68
|
+
? summaryText.slice(0, SUMMARY_TRUNCATE_LEN) + '...'
|
|
69
|
+
: summaryText
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div className="card-inner text-sm">
|
|
73
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
74
|
+
<span className="text-theme-text-primary font-medium">{rule.alert}</span>
|
|
75
|
+
{rule.severity && (
|
|
76
|
+
<span className={clsx('badge-sm', severityClass || 'bg-theme-hover text-theme-text-secondary')}>
|
|
77
|
+
{rule.severity}
|
|
78
|
+
</span>
|
|
79
|
+
)}
|
|
80
|
+
{rule.for && (
|
|
81
|
+
<span className="badge-sm bg-theme-hover text-theme-text-secondary">
|
|
82
|
+
for: {rule.for}
|
|
83
|
+
</span>
|
|
84
|
+
)}
|
|
85
|
+
</div>
|
|
86
|
+
{truncatedSummary && (
|
|
87
|
+
<div className="text-xs text-theme-text-tertiary mt-1">{truncatedSummary}</div>
|
|
88
|
+
)}
|
|
89
|
+
<TruncatedExpr expr={rule.expr} />
|
|
90
|
+
</div>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function RecordingRuleCard({ rule }: { rule: PrometheusRecordingRule }) {
|
|
95
|
+
return (
|
|
96
|
+
<div className="card-inner text-sm">
|
|
97
|
+
<div className="flex items-center gap-2">
|
|
98
|
+
<span className="text-theme-text-primary font-medium">{rule.record}</span>
|
|
99
|
+
<span className="badge-sm bg-theme-hover text-theme-text-tertiary">recording</span>
|
|
100
|
+
</div>
|
|
101
|
+
<TruncatedExpr expr={rule.expr} />
|
|
102
|
+
</div>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function RuleGroupSection({ group, searchTerm }: { group: PrometheusRuleGroup; searchTerm: string }) {
|
|
107
|
+
const [manualExpanded, setManualExpanded] = useState(group.ruleCount <= 10)
|
|
108
|
+
|
|
109
|
+
const filteredRules = useMemo(() => {
|
|
110
|
+
if (!searchTerm) return group.rules
|
|
111
|
+
const term = searchTerm.toLowerCase()
|
|
112
|
+
return group.rules.filter((rule) => matchesSearch(rule, term))
|
|
113
|
+
}, [group.rules, searchTerm])
|
|
114
|
+
|
|
115
|
+
// Auto-expand when searching so matched rules are visible
|
|
116
|
+
const expanded = searchTerm ? filteredRules.length > 0 : manualExpanded
|
|
117
|
+
|
|
118
|
+
// If searching and no matches, hide the group entirely
|
|
119
|
+
if (searchTerm && filteredRules.length === 0) return null
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<div className="card-inner">
|
|
123
|
+
<button
|
|
124
|
+
onClick={() => setManualExpanded(!manualExpanded)}
|
|
125
|
+
className="flex items-center justify-between w-full text-left"
|
|
126
|
+
>
|
|
127
|
+
<div className="flex items-center gap-2">
|
|
128
|
+
<ChevronRight className={clsx('w-3.5 h-3.5 text-theme-text-tertiary transition-transform duration-200', expanded && 'rotate-90')} />
|
|
129
|
+
<span className="text-sm text-theme-text-primary font-medium">{group.name}</span>
|
|
130
|
+
</div>
|
|
131
|
+
<div className="flex items-center gap-2">
|
|
132
|
+
{group.interval && (
|
|
133
|
+
<span className="badge-sm bg-theme-hover text-theme-text-secondary">
|
|
134
|
+
{group.interval}
|
|
135
|
+
</span>
|
|
136
|
+
)}
|
|
137
|
+
<span className="text-xs text-theme-text-tertiary">
|
|
138
|
+
{searchTerm ? `${filteredRules.length}/${group.ruleCount}` : group.ruleCount} rule{group.ruleCount !== 1 ? 's' : ''}
|
|
139
|
+
</span>
|
|
140
|
+
</div>
|
|
141
|
+
</button>
|
|
142
|
+
{!expanded && (
|
|
143
|
+
<div className="text-xs text-theme-text-secondary mt-1 ml-5.5 flex gap-3">
|
|
144
|
+
{group.alertCount > 0 && <span>{group.alertCount} alert{group.alertCount !== 1 ? 's' : ''}</span>}
|
|
145
|
+
{group.recordCount > 0 && <span>{group.recordCount} recording</span>}
|
|
146
|
+
</div>
|
|
147
|
+
)}
|
|
148
|
+
<div
|
|
149
|
+
className="grid transition-[grid-template-rows] duration-200 ease-out"
|
|
150
|
+
style={{ gridTemplateRows: expanded ? '1fr' : '0fr' }}
|
|
151
|
+
>
|
|
152
|
+
<div className="overflow-hidden">
|
|
153
|
+
<div className="mt-2 space-y-2">
|
|
154
|
+
{filteredRules.map((rule, i) => (
|
|
155
|
+
rule.type === 'alert'
|
|
156
|
+
? <AlertRuleCard key={`alert-${rule.alert}-${i}`} rule={rule} />
|
|
157
|
+
: <RecordingRuleCard key={`rec-${rule.record}-${i}`} rule={rule} />
|
|
158
|
+
))}
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
13
166
|
export function PrometheusRuleRenderer({ data }: PrometheusRuleRendererProps) {
|
|
14
167
|
const groups = getPrometheusRuleGroups(data)
|
|
15
168
|
const totalRules = getPrometheusRuleTotalRules(data)
|
|
@@ -17,6 +170,8 @@ export function PrometheusRuleRenderer({ data }: PrometheusRuleRendererProps) {
|
|
|
17
170
|
const totalRecords = groups.reduce((sum, g) => sum + g.recordCount, 0)
|
|
18
171
|
const conditions = data.status?.conditions
|
|
19
172
|
|
|
173
|
+
const [searchTerm, setSearchTerm] = useState('')
|
|
174
|
+
|
|
20
175
|
return (
|
|
21
176
|
<>
|
|
22
177
|
<Section title="PrometheusRule" icon={Bell}>
|
|
@@ -30,28 +185,31 @@ export function PrometheusRuleRenderer({ data }: PrometheusRuleRendererProps) {
|
|
|
30
185
|
|
|
31
186
|
{groups.length > 0 && (
|
|
32
187
|
<Section title={`Rule Groups (${groups.length})`} defaultExpanded>
|
|
188
|
+
{/* Search bar */}
|
|
189
|
+
{totalRules > 5 && (
|
|
190
|
+
<div className="relative mb-3">
|
|
191
|
+
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-theme-text-tertiary" />
|
|
192
|
+
<input
|
|
193
|
+
type="text"
|
|
194
|
+
placeholder="Filter rules by name or expression..."
|
|
195
|
+
value={searchTerm}
|
|
196
|
+
onChange={(e) => setSearchTerm(e.target.value)}
|
|
197
|
+
className="w-full pl-7 pr-2 py-1.5 text-xs bg-theme-elevated border border-theme-border rounded-md text-theme-text-primary placeholder:text-theme-text-tertiary focus:outline-none focus:ring-1 focus:ring-accent/50"
|
|
198
|
+
/>
|
|
199
|
+
</div>
|
|
200
|
+
)}
|
|
33
201
|
<div className="space-y-2">
|
|
34
202
|
{groups.map((group, i) => (
|
|
35
|
-
<
|
|
36
|
-
<div className="flex items-center justify-between">
|
|
37
|
-
<span className="text-theme-text-primary font-medium">{group.name}</span>
|
|
38
|
-
<div className="flex items-center gap-2">
|
|
39
|
-
{group.interval && (
|
|
40
|
-
<span className="badge-sm bg-theme-hover text-theme-text-secondary">
|
|
41
|
-
{group.interval}
|
|
42
|
-
</span>
|
|
43
|
-
)}
|
|
44
|
-
<span className="text-xs text-theme-text-tertiary">
|
|
45
|
-
{group.ruleCount} rule{group.ruleCount !== 1 ? 's' : ''}
|
|
46
|
-
</span>
|
|
47
|
-
</div>
|
|
48
|
-
</div>
|
|
49
|
-
<div className="text-xs text-theme-text-secondary mt-1 flex gap-3">
|
|
50
|
-
{group.alertCount > 0 && <span>{group.alertCount} alert{group.alertCount !== 1 ? 's' : ''}</span>}
|
|
51
|
-
{group.recordCount > 0 && <span>{group.recordCount} recording</span>}
|
|
52
|
-
</div>
|
|
53
|
-
</div>
|
|
203
|
+
<RuleGroupSection key={i} group={group} searchTerm={searchTerm} />
|
|
54
204
|
))}
|
|
205
|
+
{searchTerm && groups.every(g => {
|
|
206
|
+
const term = searchTerm.toLowerCase()
|
|
207
|
+
return g.rules.every(rule => !matchesSearch(rule, term))
|
|
208
|
+
}) && (
|
|
209
|
+
<div className="text-xs text-theme-text-tertiary py-3 text-center">
|
|
210
|
+
No rules match "{searchTerm}".
|
|
211
|
+
</div>
|
|
212
|
+
)}
|
|
55
213
|
</div>
|
|
56
214
|
</Section>
|
|
57
215
|
)}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Server } from 'lucide-react'
|
|
2
|
-
import { Section, PropertyList, Property,
|
|
2
|
+
import { Section, PropertyList, Property, LabelSelectorDisplay, ConditionsSection, AlertBanner } from '../../ui/drawer-components'
|
|
3
3
|
|
|
4
4
|
interface ReplicaSetRendererProps {
|
|
5
5
|
data: any
|
|
@@ -62,7 +62,7 @@ export function ReplicaSetRenderer({ data }: ReplicaSetRendererProps) {
|
|
|
62
62
|
</Section>
|
|
63
63
|
|
|
64
64
|
<Section title="Selector">
|
|
65
|
-
<
|
|
65
|
+
<LabelSelectorDisplay selector={data.spec?.selector} />
|
|
66
66
|
</Section>
|
|
67
67
|
|
|
68
68
|
<ConditionsSection conditions={data.status?.conditions} />
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { Server, GitBranch, AlertTriangle } from 'lucide-react'
|
|
1
|
+
import { Server, GitBranch, AlertTriangle, Network } from 'lucide-react'
|
|
2
2
|
import { clsx } from 'clsx'
|
|
3
3
|
import { Section, PropertyList, Property, ConditionsSection, PodTemplateSection } from '../../ui/drawer-components'
|
|
4
|
+
import { formatAge } from '../resource-utils'
|
|
4
5
|
|
|
5
6
|
interface RolloutRendererProps {
|
|
6
7
|
data: any
|
|
@@ -17,10 +18,29 @@ export function RolloutRenderer({ data }: RolloutRendererProps) {
|
|
|
17
18
|
const isCanary = !!canaryStrategy
|
|
18
19
|
const steps = canaryStrategy?.steps || []
|
|
19
20
|
const currentStepIndex = status.currentStepIndex
|
|
21
|
+
const trafficRouting = canaryStrategy?.trafficRouting
|
|
22
|
+
|
|
23
|
+
// Detect traffic routing provider
|
|
24
|
+
const trafficProvider = trafficRouting
|
|
25
|
+
? (() => {
|
|
26
|
+
if (trafficRouting.istio) return { name: 'Istio', details: trafficRouting.istio }
|
|
27
|
+
if (trafficRouting.nginx) return { name: 'Nginx', details: trafficRouting.nginx }
|
|
28
|
+
if (trafficRouting.alb) return { name: 'ALB', details: trafficRouting.alb }
|
|
29
|
+
if (trafficRouting.smi) return { name: 'SMI', details: trafficRouting.smi }
|
|
30
|
+
if (trafficRouting.traefik) return { name: 'Traefik', details: trafficRouting.traefik }
|
|
31
|
+
if (trafficRouting.ambassador) return { name: 'Ambassador', details: trafficRouting.ambassador }
|
|
32
|
+
return null
|
|
33
|
+
})()
|
|
34
|
+
: null
|
|
20
35
|
|
|
21
36
|
// Problem detection
|
|
22
37
|
const problems: Array<{ color: 'red' | 'yellow'; message: string }> = []
|
|
23
38
|
|
|
39
|
+
// Aborted rollout detection — must come before generic paused check
|
|
40
|
+
if (status.abort === true) {
|
|
41
|
+
problems.push({ color: 'red', message: status.message || 'Rollout was aborted' })
|
|
42
|
+
}
|
|
43
|
+
|
|
24
44
|
if (phase === 'Degraded') {
|
|
25
45
|
problems.push({ color: 'red', message: status.message || 'Rollout is degraded' })
|
|
26
46
|
}
|
|
@@ -39,8 +59,19 @@ export function RolloutRenderer({ data }: RolloutRendererProps) {
|
|
|
39
59
|
problems.push({ color: 'red', message: invalidSpecCond.message || 'Invalid rollout spec' })
|
|
40
60
|
}
|
|
41
61
|
|
|
42
|
-
|
|
43
|
-
|
|
62
|
+
// Pause conditions — show specific reasons instead of generic "Rollout is paused"
|
|
63
|
+
const pauseConditions: Array<{ reason: string; startTime?: string }> = status.pauseConditions || []
|
|
64
|
+
if (phase === 'Paused' && !status.abort) {
|
|
65
|
+
if (pauseConditions.length > 0) {
|
|
66
|
+
const reasons = pauseConditions.map((pc: any) => {
|
|
67
|
+
const reason = pc.reason || 'Unknown'
|
|
68
|
+
const since = pc.startTime ? ` (since ${formatAge(pc.startTime)})` : ''
|
|
69
|
+
return `${reason}${since}`
|
|
70
|
+
})
|
|
71
|
+
problems.push({ color: 'yellow', message: `Rollout is paused: ${reasons.join('; ')}` })
|
|
72
|
+
} else {
|
|
73
|
+
problems.push({ color: 'yellow', message: 'Rollout is paused' })
|
|
74
|
+
}
|
|
44
75
|
}
|
|
45
76
|
|
|
46
77
|
// Phase badge color
|
|
@@ -153,6 +184,77 @@ export function RolloutRenderer({ data }: RolloutRendererProps) {
|
|
|
153
184
|
</PropertyList>
|
|
154
185
|
</Section>
|
|
155
186
|
|
|
187
|
+
{/* Traffic Routing */}
|
|
188
|
+
{trafficProvider && (
|
|
189
|
+
<Section title="Traffic Routing" icon={Network}>
|
|
190
|
+
<PropertyList>
|
|
191
|
+
<Property
|
|
192
|
+
label="Provider"
|
|
193
|
+
value={
|
|
194
|
+
<span className="badge-sm status-neutral">{trafficProvider.name}</span>
|
|
195
|
+
}
|
|
196
|
+
/>
|
|
197
|
+
{trafficProvider.name === 'Istio' && (
|
|
198
|
+
<>
|
|
199
|
+
{trafficProvider.details.virtualService?.name && (
|
|
200
|
+
<Property label="VirtualService" value={trafficProvider.details.virtualService.name} />
|
|
201
|
+
)}
|
|
202
|
+
{trafficProvider.details.destinationRule?.name && (
|
|
203
|
+
<Property label="DestinationRule" value={trafficProvider.details.destinationRule.name} />
|
|
204
|
+
)}
|
|
205
|
+
</>
|
|
206
|
+
)}
|
|
207
|
+
{trafficProvider.name === 'ALB' && (
|
|
208
|
+
<>
|
|
209
|
+
{trafficProvider.details.ingress && (
|
|
210
|
+
<Property label="Ingress" value={trafficProvider.details.ingress} />
|
|
211
|
+
)}
|
|
212
|
+
{trafficProvider.details.servicePort != null && (
|
|
213
|
+
<Property label="Service Port" value={trafficProvider.details.servicePort} />
|
|
214
|
+
)}
|
|
215
|
+
</>
|
|
216
|
+
)}
|
|
217
|
+
{trafficProvider.name === 'Nginx' && (
|
|
218
|
+
<>
|
|
219
|
+
{trafficProvider.details.stableIngress && (
|
|
220
|
+
<Property label="Stable Ingress" value={trafficProvider.details.stableIngress} />
|
|
221
|
+
)}
|
|
222
|
+
{trafficProvider.details.additionalIngressAnnotations && (
|
|
223
|
+
<Property
|
|
224
|
+
label="Annotations"
|
|
225
|
+
value={Object.keys(trafficProvider.details.additionalIngressAnnotations).length + ' annotations'}
|
|
226
|
+
/>
|
|
227
|
+
)}
|
|
228
|
+
</>
|
|
229
|
+
)}
|
|
230
|
+
{trafficProvider.name === 'SMI' && (
|
|
231
|
+
<>
|
|
232
|
+
{trafficProvider.details.rootService && (
|
|
233
|
+
<Property label="Root Service" value={trafficProvider.details.rootService} />
|
|
234
|
+
)}
|
|
235
|
+
{trafficProvider.details.trafficSplitName && (
|
|
236
|
+
<Property label="TrafficSplit" value={trafficProvider.details.trafficSplitName} />
|
|
237
|
+
)}
|
|
238
|
+
</>
|
|
239
|
+
)}
|
|
240
|
+
{trafficProvider.name === 'Traefik' && (
|
|
241
|
+
<>
|
|
242
|
+
{trafficProvider.details.weightedTraefikServiceName && (
|
|
243
|
+
<Property label="Weighted Service" value={trafficProvider.details.weightedTraefikServiceName} />
|
|
244
|
+
)}
|
|
245
|
+
</>
|
|
246
|
+
)}
|
|
247
|
+
{trafficProvider.name === 'Ambassador' && (
|
|
248
|
+
<>
|
|
249
|
+
{trafficProvider.details.mappings && (
|
|
250
|
+
<Property label="Mappings" value={trafficProvider.details.mappings.join(', ')} />
|
|
251
|
+
)}
|
|
252
|
+
</>
|
|
253
|
+
)}
|
|
254
|
+
</PropertyList>
|
|
255
|
+
</Section>
|
|
256
|
+
)}
|
|
257
|
+
|
|
156
258
|
{/* Canary Steps visual */}
|
|
157
259
|
{isCanary && steps.length > 0 && (
|
|
158
260
|
<Section title={`Canary Steps (${steps.length})`} defaultExpanded>
|