@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,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>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { useState } from 'react'
|
|
2
|
-
import { Package, ChevronDown, ChevronRight } from 'lucide-react'
|
|
1
|
+
import { useState, useMemo } from 'react'
|
|
2
|
+
import { Package, ChevronDown, ChevronRight, Search } from 'lucide-react'
|
|
3
3
|
import { Section, PropertyList, Property } from '../../ui/drawer-components'
|
|
4
4
|
import { formatAge } from '../resource-utils'
|
|
5
5
|
import { formatTrivyImage } from './trivy-shared'
|
|
@@ -13,6 +13,7 @@ const INITIAL_SHOW_COUNT = 100
|
|
|
13
13
|
export function SbomReportRenderer({ data }: SbomReportRendererProps) {
|
|
14
14
|
const [showAll, setShowAll] = useState(false)
|
|
15
15
|
const [expanded, setExpanded] = useState(true)
|
|
16
|
+
const [searchTerm, setSearchTerm] = useState('')
|
|
16
17
|
|
|
17
18
|
const report = data.report || {}
|
|
18
19
|
const scanner = report.scanner || {}
|
|
@@ -29,7 +30,18 @@ export function SbomReportRenderer({ data }: SbomReportRendererProps) {
|
|
|
29
30
|
const bomFormat = components.bomFormat || '-'
|
|
30
31
|
const specVersion = components.specVersion || '-'
|
|
31
32
|
|
|
32
|
-
const
|
|
33
|
+
const filteredBom = useMemo(() => {
|
|
34
|
+
if (!searchTerm) return bom
|
|
35
|
+
const term = searchTerm.toLowerCase()
|
|
36
|
+
return bom.filter((comp: any) =>
|
|
37
|
+
(comp.name || '').toLowerCase().includes(term) ||
|
|
38
|
+
(comp.purl || '').toLowerCase().includes(term) ||
|
|
39
|
+
(comp.type || '').toLowerCase().includes(term)
|
|
40
|
+
)
|
|
41
|
+
}, [bom, searchTerm])
|
|
42
|
+
|
|
43
|
+
const hasActiveFilter = searchTerm !== ''
|
|
44
|
+
const displayedComponents = showAll ? filteredBom : filteredBom.slice(0, INITIAL_SHOW_COUNT)
|
|
33
45
|
|
|
34
46
|
return (
|
|
35
47
|
<>
|
|
@@ -58,6 +70,30 @@ export function SbomReportRenderer({ data }: SbomReportRendererProps) {
|
|
|
58
70
|
</button>
|
|
59
71
|
{expanded && (
|
|
60
72
|
<div className="overflow-x-auto -mx-1">
|
|
73
|
+
{/* Search */}
|
|
74
|
+
<div className="flex items-center gap-2 mb-2 px-1">
|
|
75
|
+
<div className="relative flex-1">
|
|
76
|
+
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3 h-3 text-theme-text-tertiary" />
|
|
77
|
+
<input
|
|
78
|
+
type="text"
|
|
79
|
+
placeholder="Filter by name, type, or purl..."
|
|
80
|
+
value={searchTerm}
|
|
81
|
+
onChange={(e) => { setSearchTerm(e.target.value); setShowAll(false) }}
|
|
82
|
+
className="w-full pl-6 pr-2 py-1 text-xs bg-theme-bg border border-theme-border rounded text-theme-text-primary placeholder:text-theme-text-tertiary focus:outline-none focus:ring-1 focus:ring-blue-500/50"
|
|
83
|
+
/>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
{hasActiveFilter && (
|
|
87
|
+
<div className="flex items-center gap-1 mb-2 px-1 text-xs text-theme-text-tertiary">
|
|
88
|
+
Showing {filteredBom.length} of {bom.length}
|
|
89
|
+
<button
|
|
90
|
+
onClick={() => { setSearchTerm(''); setShowAll(false) }}
|
|
91
|
+
className="ml-1 text-blue-400 hover:text-blue-300 hover:underline"
|
|
92
|
+
>
|
|
93
|
+
Clear
|
|
94
|
+
</button>
|
|
95
|
+
</div>
|
|
96
|
+
)}
|
|
61
97
|
<table className="w-full text-xs">
|
|
62
98
|
<thead>
|
|
63
99
|
<tr className="border-b border-theme-border text-theme-text-tertiary">
|
|
@@ -81,12 +117,17 @@ export function SbomReportRenderer({ data }: SbomReportRendererProps) {
|
|
|
81
117
|
})}
|
|
82
118
|
</tbody>
|
|
83
119
|
</table>
|
|
84
|
-
{
|
|
120
|
+
{displayedComponents.length === 0 && (
|
|
121
|
+
<div className="py-4 text-center text-xs text-theme-text-tertiary">
|
|
122
|
+
No components match the current filter.
|
|
123
|
+
</div>
|
|
124
|
+
)}
|
|
125
|
+
{!showAll && filteredBom.length > INITIAL_SHOW_COUNT && (
|
|
85
126
|
<button
|
|
86
127
|
onClick={() => setShowAll(true)}
|
|
87
128
|
className="mt-2 text-xs text-blue-400 hover:text-blue-300 hover:underline"
|
|
88
129
|
>
|
|
89
|
-
Show all {
|
|
130
|
+
Show all {filteredBom.length} components
|
|
90
131
|
</button>
|
|
91
132
|
)}
|
|
92
133
|
</div>
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Lock, Key, FileText } 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, KeyValueBadgeList, AlertBanner, ResourceLink } from '../../ui/drawer-components'
|
|
4
4
|
|
|
5
5
|
interface SealedSecretRendererProps {
|
|
6
6
|
data: any
|
|
7
|
+
onNavigate?: (ref: { kind: string; namespace: string; name: string }) => void
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
function getScope(annotations: Record<string, string> | undefined): string {
|
|
@@ -13,7 +14,7 @@ function getScope(annotations: Record<string, string> | undefined): string {
|
|
|
13
14
|
return 'strict'
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
export function SealedSecretRenderer({ data }: SealedSecretRendererProps) {
|
|
17
|
+
export function SealedSecretRenderer({ data, onNavigate }: SealedSecretRendererProps) {
|
|
17
18
|
const spec = data.spec || {}
|
|
18
19
|
const status = data.status || {}
|
|
19
20
|
const conditions = status.conditions || []
|
|
@@ -65,6 +66,14 @@ export function SealedSecretRenderer({ data }: SealedSecretRendererProps) {
|
|
|
65
66
|
</span>
|
|
66
67
|
}
|
|
67
68
|
/>
|
|
69
|
+
<Property label="Target Secret" value={
|
|
70
|
+
<ResourceLink
|
|
71
|
+
name={data.spec?.template?.metadata?.name || data.metadata?.name || ''}
|
|
72
|
+
kind="secrets"
|
|
73
|
+
namespace={data.metadata?.namespace || ''}
|
|
74
|
+
onNavigate={onNavigate}
|
|
75
|
+
/>
|
|
76
|
+
} />
|
|
68
77
|
<Property label="Secret Type" value={secretType} />
|
|
69
78
|
<Property label="Scope" value={scope} />
|
|
70
79
|
<Property label="Observed Gen" value={status.observedGeneration} />
|
|
@@ -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
|
getServiceMonitorEndpoints,
|
|
5
5
|
getServiceMonitorJobLabel,
|
|
@@ -12,7 +12,7 @@ interface ServiceMonitorRendererProps {
|
|
|
12
12
|
|
|
13
13
|
export function ServiceMonitorRenderer({ data }: ServiceMonitorRendererProps) {
|
|
14
14
|
const endpoints = getServiceMonitorEndpoints(data)
|
|
15
|
-
const selector = data.spec?.selector
|
|
15
|
+
const selector = data.spec?.selector
|
|
16
16
|
const conditions = data.status?.conditions
|
|
17
17
|
|
|
18
18
|
return (
|
|
@@ -53,11 +53,9 @@ export function ServiceMonitorRenderer({ data }: ServiceMonitorRendererProps) {
|
|
|
53
53
|
</Section>
|
|
54
54
|
)}
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
</Section>
|
|
60
|
-
)}
|
|
56
|
+
<Section title="Selector">
|
|
57
|
+
<LabelSelectorDisplay selector={selector} emptyText="No selector" />
|
|
58
|
+
</Section>
|
|
61
59
|
|
|
62
60
|
<ConditionsSection conditions={conditions} />
|
|
63
61
|
</>
|
|
@@ -56,7 +56,7 @@ export function VeleroBackupRenderer({ data }: VeleroBackupRendererProps) {
|
|
|
56
56
|
<AlertBanner
|
|
57
57
|
variant="error"
|
|
58
58
|
title={isFailed ? 'Backup Failed' : 'Backup Partially Failed'}
|
|
59
|
-
message={`${errors} error(s) occurred during backup.`}
|
|
59
|
+
message={status.failureReason || `${errors} error(s) occurred during backup.`}
|
|
60
60
|
items={validationErrors.length > 0 ? validationErrors : undefined}
|
|
61
61
|
/>
|
|
62
62
|
)}
|
|
@@ -84,8 +84,16 @@ export function VeleroBackupRenderer({ data }: VeleroBackupRendererProps) {
|
|
|
84
84
|
)}
|
|
85
85
|
<Property label="Duration" value={getBackupDuration(data)} />
|
|
86
86
|
<Property label="Expiration" value={getBackupExpiry(data)} />
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
<Property label="Errors" value={
|
|
88
|
+
errors > 0
|
|
89
|
+
? <span className="text-red-500 dark:text-red-400 font-medium">{errors}</span>
|
|
90
|
+
: '0'
|
|
91
|
+
} />
|
|
92
|
+
<Property label="Warnings" value={
|
|
93
|
+
warnings > 0
|
|
94
|
+
? <span className="text-amber-500 dark:text-amber-400 font-medium">{warnings}</span>
|
|
95
|
+
: '0'
|
|
96
|
+
} />
|
|
89
97
|
</PropertyList>
|
|
90
98
|
</Section>
|
|
91
99
|
|
|
@@ -49,7 +49,7 @@ export function VeleroRestoreRenderer({ data }: VeleroRestoreRendererProps) {
|
|
|
49
49
|
<AlertBanner
|
|
50
50
|
variant="error"
|
|
51
51
|
title={isFailed ? 'Restore Failed' : 'Restore Partially Failed'}
|
|
52
|
-
message={`${errors} error(s) occurred during restore.`}
|
|
52
|
+
message={status.failureReason || `${errors} error(s) occurred during restore.`}
|
|
53
53
|
/>
|
|
54
54
|
)}
|
|
55
55
|
{warnings > 0 && !isFailed && (
|
|
@@ -76,8 +76,16 @@ export function VeleroRestoreRenderer({ data }: VeleroRestoreRendererProps) {
|
|
|
76
76
|
<Property label="Completed" value={formatAge(status.completionTimestamp) + ' ago'} />
|
|
77
77
|
)}
|
|
78
78
|
<Property label="Duration" value={getRestoreDuration(data)} />
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
<Property label="Errors" value={
|
|
80
|
+
errors > 0
|
|
81
|
+
? <span className="text-red-500 dark:text-red-400 font-medium">{errors}</span>
|
|
82
|
+
: '0'
|
|
83
|
+
} />
|
|
84
|
+
<Property label="Warnings" value={
|
|
85
|
+
warnings > 0
|
|
86
|
+
? <span className="text-amber-500 dark:text-amber-400 font-medium">{warnings}</span>
|
|
87
|
+
: '0'
|
|
88
|
+
} />
|
|
81
89
|
</PropertyList>
|
|
82
90
|
</Section>
|
|
83
91
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Shield } from 'lucide-react'
|
|
2
2
|
import { clsx } from 'clsx'
|
|
3
|
-
import { Section, PropertyList, Property, AlertBanner } from '../../ui/drawer-components'
|
|
3
|
+
import { Section, PropertyList, Property, AlertBanner, LabelSelectorDisplay } from '../../ui/drawer-components'
|
|
4
4
|
|
|
5
5
|
interface WebhookConfigRendererProps {
|
|
6
6
|
data: any
|
|
@@ -65,8 +65,8 @@ export function WebhookConfigRenderer({ data, isMutating }: WebhookConfigRendere
|
|
|
65
65
|
? `URL: ${url}`
|
|
66
66
|
: 'No target'
|
|
67
67
|
|
|
68
|
-
const
|
|
69
|
-
const
|
|
68
|
+
const nsSelector = wh.namespaceSelector
|
|
69
|
+
const objSelector = wh.objectSelector
|
|
70
70
|
|
|
71
71
|
return (
|
|
72
72
|
<div key={i} className="card-inner-lg">
|
|
@@ -119,26 +119,18 @@ export function WebhookConfigRenderer({ data, isMutating }: WebhookConfigRendere
|
|
|
119
119
|
)}
|
|
120
120
|
|
|
121
121
|
{/* Selectors */}
|
|
122
|
-
{(
|
|
122
|
+
{(nsSelector || objSelector) && (
|
|
123
123
|
<div className="mt-2 space-y-1">
|
|
124
|
-
{
|
|
124
|
+
{nsSelector && (
|
|
125
125
|
<div>
|
|
126
126
|
<div className="text-xs text-theme-text-tertiary mb-1">Namespace Selector</div>
|
|
127
|
-
<
|
|
128
|
-
{Object.entries(nsLabels).map(([k, v]) => (
|
|
129
|
-
<span key={k} className="badge-sm bg-theme-elevated text-theme-text-secondary">{k}={String(v)}</span>
|
|
130
|
-
))}
|
|
131
|
-
</div>
|
|
127
|
+
<LabelSelectorDisplay selector={nsSelector} />
|
|
132
128
|
</div>
|
|
133
129
|
)}
|
|
134
|
-
{
|
|
130
|
+
{objSelector && (
|
|
135
131
|
<div>
|
|
136
132
|
<div className="text-xs text-theme-text-tertiary mb-1">Object Selector</div>
|
|
137
|
-
<
|
|
138
|
-
{Object.entries(objLabels).map(([k, v]) => (
|
|
139
|
-
<span key={k} className="badge-sm bg-theme-elevated text-theme-text-secondary">{k}={String(v)}</span>
|
|
140
|
-
))}
|
|
141
|
-
</div>
|
|
133
|
+
<LabelSelectorDisplay selector={objSelector} />
|
|
142
134
|
</div>
|
|
143
135
|
)}
|
|
144
136
|
</div>
|