@skyhook-io/k8s-ui 1.3.2 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- 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 +14 -0
- package/src/components/resources/renderers/CiliumNetworkPolicyRenderer.tsx +228 -0
- package/src/components/resources/renderers/ClusterNetworkPolicyRenderer.tsx +174 -0
- package/src/components/resources/renderers/GenericRenderer.tsx +1 -1
- package/src/components/resources/renderers/NetworkPolicyDiagram.tsx +277 -0
- package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +10 -1
- package/src/components/resources/renderers/ServiceRenderer.tsx +40 -28
- package/src/components/resources/renderers/index.ts +3 -0
- 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 +9 -2
- package/src/components/shared/index.ts +1 -0
- 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 +20 -4
- package/src/components/workload/WorkloadView.tsx +55 -29
- package/src/index.ts +3 -0
- package/src/types/core.ts +2 -1
- 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/resource-icons.ts +3 -0
- package/src/utils/skeleton-yaml.ts +302 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { Shield, ArrowDownToLine, ArrowUpFromLine } from 'lucide-react'
|
|
1
|
+
import { Shield, ArrowDownToLine, ArrowUpFromLine, GitFork } from 'lucide-react'
|
|
2
2
|
import { clsx } from 'clsx'
|
|
3
3
|
import { Section, PropertyList, Property } from '../../ui/drawer-components'
|
|
4
|
+
import { NetworkPolicyDiagram } from './NetworkPolicyDiagram'
|
|
4
5
|
|
|
5
6
|
interface NetworkPolicyRendererProps {
|
|
6
7
|
data: any
|
|
@@ -18,8 +19,16 @@ export function NetworkPolicyRenderer({ data }: NetworkPolicyRendererProps) {
|
|
|
18
19
|
const hasIngress = policyTypes.includes('Ingress')
|
|
19
20
|
const hasEgress = policyTypes.includes('Egress')
|
|
20
21
|
|
|
22
|
+
const hasDiagramContent = hasIngress || hasEgress
|
|
23
|
+
|
|
21
24
|
return (
|
|
22
25
|
<>
|
|
26
|
+
{hasDiagramContent && (
|
|
27
|
+
<Section title="Policy Flow" icon={GitFork} defaultExpanded>
|
|
28
|
+
<NetworkPolicyDiagram spec={spec} />
|
|
29
|
+
</Section>
|
|
30
|
+
)}
|
|
31
|
+
|
|
23
32
|
<Section title="Target" icon={Shield}>
|
|
24
33
|
<PropertyList>
|
|
25
34
|
<Property
|
|
@@ -16,11 +16,10 @@ export function ServiceRenderer({ data, onCopy, copied, renderPortAction }: Serv
|
|
|
16
16
|
const namespace = data.metadata?.namespace
|
|
17
17
|
const serviceName = data.metadata?.name
|
|
18
18
|
|
|
19
|
-
// Check for issues
|
|
20
19
|
const isLoadBalancer = spec.type === 'LoadBalancer'
|
|
20
|
+
const isExternalName = spec.type === 'ExternalName'
|
|
21
21
|
const lbPending = isLoadBalancer && lbIngress.length === 0
|
|
22
22
|
const hasNoSelector = !spec.selector || Object.keys(spec.selector).length === 0
|
|
23
|
-
const isExternalName = spec.type === 'ExternalName'
|
|
24
23
|
|
|
25
24
|
return (
|
|
26
25
|
<>
|
|
@@ -46,47 +45,60 @@ export function ServiceRenderer({ data, onCopy, copied, renderPortAction }: Serv
|
|
|
46
45
|
<Section title="Service" icon={Globe}>
|
|
47
46
|
<PropertyList>
|
|
48
47
|
<Property label="Type" value={spec.type || 'ClusterIP'} />
|
|
49
|
-
|
|
48
|
+
{isExternalName ? (
|
|
49
|
+
<Property label="External Name" value={spec.externalName} copyable onCopy={onCopy} copied={copied} />
|
|
50
|
+
) : (
|
|
51
|
+
<Property label="Cluster IP" value={spec.clusterIP} copyable onCopy={onCopy} copied={copied} />
|
|
52
|
+
)}
|
|
50
53
|
{spec.externalIPs?.length > 0 && (
|
|
51
54
|
<Property label="External IPs" value={spec.externalIPs.join(', ')} copyable onCopy={onCopy} copied={copied} />
|
|
52
55
|
)}
|
|
53
|
-
{lbIngress.
|
|
56
|
+
{lbIngress.map((ing: any, i: number) => (
|
|
54
57
|
<Property
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
key={i}
|
|
59
|
+
label={lbIngress.length > 1 ? `Load Balancer ${i + 1}` : 'Load Balancer'}
|
|
60
|
+
value={ing.ip || ing.hostname}
|
|
57
61
|
copyable
|
|
58
62
|
onCopy={onCopy}
|
|
59
63
|
copied={copied}
|
|
60
64
|
/>
|
|
61
|
-
)}
|
|
65
|
+
))}
|
|
62
66
|
<Property label="Session Affinity" value={spec.sessionAffinity} />
|
|
63
67
|
<Property label="External Traffic" value={spec.externalTrafficPolicy} />
|
|
68
|
+
<Property label="Internal Traffic" value={spec.internalTrafficPolicy} />
|
|
69
|
+
{spec.ipFamilyPolicy && <Property label="IP Family Policy" value={spec.ipFamilyPolicy} />}
|
|
70
|
+
{spec.ipFamilies?.length > 0 && <Property label="IP Families" value={spec.ipFamilies.join(', ')} />}
|
|
64
71
|
</PropertyList>
|
|
65
72
|
</Section>
|
|
66
73
|
|
|
67
|
-
|
|
68
|
-
<
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
<div className="
|
|
72
|
-
<
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
74
|
+
{ports.length > 0 && (
|
|
75
|
+
<Section title="Ports" defaultExpanded>
|
|
76
|
+
<div className="space-y-2">
|
|
77
|
+
{ports.map((port: any, i: number) => (
|
|
78
|
+
<div key={`${port.port}-${port.protocol || 'TCP'}`} className="card-inner text-sm">
|
|
79
|
+
<div className="flex items-center justify-between">
|
|
80
|
+
<div className="flex items-center gap-2">
|
|
81
|
+
<span className="text-theme-text-primary font-medium">{port.name || `port-${i + 1}`}</span>
|
|
82
|
+
<span className="text-xs text-theme-text-tertiary">{port.protocol || 'TCP'}</span>
|
|
83
|
+
</div>
|
|
84
|
+
<div className="flex items-center gap-2">
|
|
85
|
+
{renderPortAction?.({
|
|
86
|
+
namespace,
|
|
87
|
+
serviceName,
|
|
88
|
+
port: port.port,
|
|
89
|
+
protocol: port.protocol || 'TCP',
|
|
90
|
+
})}
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
<div className="text-xs text-theme-text-secondary mt-1">
|
|
94
|
+
{port.port}{port.targetPort != null && port.targetPort !== port.port ? ` → ${port.targetPort}` : ''}
|
|
95
|
+
{port.nodePort ? ` (NodePort: ${port.nodePort})` : ''}
|
|
80
96
|
</div>
|
|
81
97
|
</div>
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
</div>
|
|
87
|
-
))}
|
|
88
|
-
</div>
|
|
89
|
-
</Section>
|
|
98
|
+
))}
|
|
99
|
+
</div>
|
|
100
|
+
</Section>
|
|
101
|
+
)}
|
|
90
102
|
|
|
91
103
|
{spec.selector && (
|
|
92
104
|
<Section title="Selector">
|
|
@@ -62,6 +62,9 @@ export * from './kyverno-cells'
|
|
|
62
62
|
export * from './KyvernoPolicyReportRenderer'
|
|
63
63
|
export * from './LeaseRenderer'
|
|
64
64
|
export * from './NetworkPolicyRenderer'
|
|
65
|
+
export * from './NetworkPolicyDiagram'
|
|
66
|
+
export * from './CiliumNetworkPolicyRenderer'
|
|
67
|
+
export * from './ClusterNetworkPolicyRenderer'
|
|
65
68
|
export * from './NodeRenderer'
|
|
66
69
|
export * from './OCIRepositoryRenderer'
|
|
67
70
|
export * from './OrderRenderer'
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { useState, useCallback, useEffect } from 'react'
|
|
2
|
+
import { X, Loader2, Check, AlertTriangle, ChevronDown, ChevronRight } from 'lucide-react'
|
|
3
|
+
import { DialogPortal } from '../ui/DialogPortal'
|
|
4
|
+
import { YamlEditor } from '../ui/YamlEditor'
|
|
5
|
+
import { Tooltip } from '../ui/Tooltip'
|
|
6
|
+
import { formatApplyError } from '../../utils/k8s-errors'
|
|
7
|
+
|
|
8
|
+
export interface ApplyResult {
|
|
9
|
+
name: string
|
|
10
|
+
namespace: string
|
|
11
|
+
kind: string
|
|
12
|
+
created: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface CreateResourceDialogProps {
|
|
16
|
+
open: boolean
|
|
17
|
+
onClose: () => void
|
|
18
|
+
initialYaml?: string
|
|
19
|
+
title?: string
|
|
20
|
+
// Injected by platform (decouples from data-fetching hooks)
|
|
21
|
+
onApply: (params: { yaml: string; mode: 'apply' | 'create'; dryRun: boolean }) => Promise<ApplyResult[]>
|
|
22
|
+
isApplying: boolean
|
|
23
|
+
/** Called after a successful non-dry-run apply with the first created resource */
|
|
24
|
+
onCreated?: (result: ApplyResult) => void
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function CreateResourceDialog({ open, onClose, initialYaml = '', title, onApply, isApplying, onCreated }: CreateResourceDialogProps) {
|
|
28
|
+
const [yaml, setYaml] = useState(initialYaml)
|
|
29
|
+
const [mode, setMode] = useState<'apply' | 'create'>('apply')
|
|
30
|
+
const [dryRun, setDryRun] = useState(false)
|
|
31
|
+
const [yamlValid, setYamlValid] = useState(true)
|
|
32
|
+
const [error, setError] = useState<string | null>(null)
|
|
33
|
+
const [success, setSuccess] = useState<string | null>(null)
|
|
34
|
+
|
|
35
|
+
// Reset state when dialog opens or initialYaml changes
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (open) {
|
|
38
|
+
setYaml(initialYaml)
|
|
39
|
+
setMode('apply')
|
|
40
|
+
setDryRun(false)
|
|
41
|
+
setError(null)
|
|
42
|
+
setSuccess(null)
|
|
43
|
+
}
|
|
44
|
+
}, [open, initialYaml])
|
|
45
|
+
|
|
46
|
+
const handleClose = useCallback(() => {
|
|
47
|
+
if (isApplying) return
|
|
48
|
+
setError(null)
|
|
49
|
+
setSuccess(null)
|
|
50
|
+
setYaml('')
|
|
51
|
+
onClose()
|
|
52
|
+
}, [onClose, isApplying])
|
|
53
|
+
|
|
54
|
+
const handleValidate = useCallback((_isValid: boolean, errors: string[]) => {
|
|
55
|
+
setYamlValid(errors.length === 0)
|
|
56
|
+
}, [])
|
|
57
|
+
|
|
58
|
+
const handleSubmit = useCallback(async () => {
|
|
59
|
+
if (!yaml.trim()) {
|
|
60
|
+
setError('YAML content is required')
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
setError(null)
|
|
64
|
+
setSuccess(null)
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const results = await onApply({ yaml, mode, dryRun })
|
|
68
|
+
const action = mode === 'create' ? 'Created' : 'Applied'
|
|
69
|
+
const dryRunLabel = dryRun ? ' (dry run)' : ''
|
|
70
|
+
|
|
71
|
+
if (results.length === 1) {
|
|
72
|
+
const r = results[0]
|
|
73
|
+
setSuccess(`${action} ${r.kind} ${r.namespace ? r.namespace + '/' : ''}${r.name}${dryRunLabel}`)
|
|
74
|
+
} else {
|
|
75
|
+
setSuccess(`${action} ${results.length} resources${dryRunLabel}`)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!dryRun) {
|
|
79
|
+
if (onCreated && results.length > 0) {
|
|
80
|
+
// Close immediately and navigate to the created resource
|
|
81
|
+
handleClose()
|
|
82
|
+
onCreated(results[0])
|
|
83
|
+
} else {
|
|
84
|
+
setTimeout(handleClose, 1200)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
} catch (err) {
|
|
88
|
+
setError(err instanceof Error ? err.message : 'Unknown error')
|
|
89
|
+
}
|
|
90
|
+
}, [yaml, mode, dryRun, onApply, onCreated, handleClose])
|
|
91
|
+
|
|
92
|
+
const dialogTitle = title || 'Create Resource'
|
|
93
|
+
const submitLabel = mode === 'create' ? 'Create' : 'Apply'
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<DialogPortal open={open} onClose={handleClose} closable={!isApplying} className="w-[700px] max-h-[85vh] flex flex-col">
|
|
97
|
+
{/* Header */}
|
|
98
|
+
<div className="flex items-center justify-between px-5 py-3.5 border-b border-theme-border shrink-0">
|
|
99
|
+
<h2 className="text-sm font-semibold text-theme-text-primary">{dialogTitle}</h2>
|
|
100
|
+
<Tooltip content="Close">
|
|
101
|
+
<button
|
|
102
|
+
onClick={handleClose}
|
|
103
|
+
className="p-1 rounded hover:bg-theme-hover text-theme-text-secondary transition-colors"
|
|
104
|
+
>
|
|
105
|
+
<X className="w-4 h-4" />
|
|
106
|
+
</button>
|
|
107
|
+
</Tooltip>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
{/* Editor */}
|
|
111
|
+
<div className="flex-1 min-h-0 px-5 py-3">
|
|
112
|
+
<YamlEditor
|
|
113
|
+
value={yaml}
|
|
114
|
+
onChange={setYaml}
|
|
115
|
+
height="400px"
|
|
116
|
+
onValidate={handleValidate}
|
|
117
|
+
/>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
{/* Status messages — single location for feedback (no toast) */}
|
|
121
|
+
{error && <ApplyErrorBanner error={error} />}
|
|
122
|
+
{success && (
|
|
123
|
+
<div className="mx-5 mb-2 px-3 py-2 rounded-md bg-emerald-500/10 border border-emerald-500/30 text-emerald-400 text-xs flex items-center gap-2">
|
|
124
|
+
<Check className="w-3.5 h-3.5 shrink-0" />
|
|
125
|
+
<span>{success}</span>
|
|
126
|
+
</div>
|
|
127
|
+
)}
|
|
128
|
+
|
|
129
|
+
{/* Footer */}
|
|
130
|
+
<div className="flex items-center justify-between px-5 py-3 border-t border-theme-border shrink-0">
|
|
131
|
+
<div className="flex items-center gap-3">
|
|
132
|
+
{/* Mode toggle — pill segmented control */}
|
|
133
|
+
<Tooltip content="Apply: create or update (idempotent). Create: fail if exists." position="bottom">
|
|
134
|
+
<div className="flex items-center rounded-md bg-theme-base border border-theme-border p-0.5" role="radiogroup" aria-label="Apply mode">
|
|
135
|
+
<button
|
|
136
|
+
onClick={() => setMode('apply')}
|
|
137
|
+
role="radio"
|
|
138
|
+
aria-checked={mode === 'apply'}
|
|
139
|
+
className={`px-2.5 py-1 rounded text-xs font-medium transition-colors ${
|
|
140
|
+
mode === 'apply'
|
|
141
|
+
? 'bg-theme-elevated text-theme-text-primary shadow-sm'
|
|
142
|
+
: 'text-theme-text-tertiary hover:text-theme-text-secondary'
|
|
143
|
+
}`}
|
|
144
|
+
>
|
|
145
|
+
Apply
|
|
146
|
+
</button>
|
|
147
|
+
<button
|
|
148
|
+
onClick={() => setMode('create')}
|
|
149
|
+
role="radio"
|
|
150
|
+
aria-checked={mode === 'create'}
|
|
151
|
+
className={`px-2.5 py-1 rounded text-xs font-medium transition-colors ${
|
|
152
|
+
mode === 'create'
|
|
153
|
+
? 'bg-theme-elevated text-theme-text-primary shadow-sm'
|
|
154
|
+
: 'text-theme-text-tertiary hover:text-theme-text-secondary'
|
|
155
|
+
}`}
|
|
156
|
+
>
|
|
157
|
+
Create
|
|
158
|
+
</button>
|
|
159
|
+
</div>
|
|
160
|
+
</Tooltip>
|
|
161
|
+
|
|
162
|
+
{/* Dry run checkbox */}
|
|
163
|
+
<Tooltip content="Validate against the cluster without persisting changes" position="bottom">
|
|
164
|
+
<label className="flex items-center gap-1.5 text-xs text-theme-text-secondary cursor-pointer">
|
|
165
|
+
<input
|
|
166
|
+
type="checkbox"
|
|
167
|
+
checked={dryRun}
|
|
168
|
+
onChange={(e) => setDryRun(e.target.checked)}
|
|
169
|
+
className="w-3.5 h-3.5 rounded border-theme-border bg-theme-base"
|
|
170
|
+
/>
|
|
171
|
+
Dry run
|
|
172
|
+
</label>
|
|
173
|
+
</Tooltip>
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
<div className="flex items-center gap-2">
|
|
177
|
+
<button
|
|
178
|
+
onClick={handleClose}
|
|
179
|
+
className="px-3 py-1.5 text-xs rounded-lg hover:bg-theme-hover text-theme-text-secondary transition-colors"
|
|
180
|
+
>
|
|
181
|
+
Cancel
|
|
182
|
+
</button>
|
|
183
|
+
<button
|
|
184
|
+
onClick={handleSubmit}
|
|
185
|
+
disabled={isApplying || !yaml.trim() || !yamlValid}
|
|
186
|
+
className="px-4 py-1.5 text-xs rounded-lg btn-brand font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1.5"
|
|
187
|
+
>
|
|
188
|
+
{isApplying ? (
|
|
189
|
+
<>
|
|
190
|
+
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
|
191
|
+
{submitLabel === 'Apply' ? 'Applying...' : 'Creating...'}
|
|
192
|
+
</>
|
|
193
|
+
) : (
|
|
194
|
+
submitLabel
|
|
195
|
+
)}
|
|
196
|
+
</button>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
</DialogPortal>
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function ApplyErrorBanner({ error }: { error: string }) {
|
|
204
|
+
const [expanded, setExpanded] = useState(false)
|
|
205
|
+
const parsed = formatApplyError(error)
|
|
206
|
+
const hasFriendly = !!parsed.suggestion
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<div className="mx-5 mb-2 rounded-md bg-red-500/10 border border-red-500/30 text-xs">
|
|
210
|
+
<div className="px-3 py-2 flex items-start gap-2 text-red-400">
|
|
211
|
+
<AlertTriangle className="w-3.5 h-3.5 mt-0.5 shrink-0" />
|
|
212
|
+
<div className="min-w-0 flex-1">
|
|
213
|
+
<span className="font-medium">{parsed.summary}</span>
|
|
214
|
+
{parsed.suggestion && (
|
|
215
|
+
<p className="mt-1 text-red-400/80">{parsed.suggestion}</p>
|
|
216
|
+
)}
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
{hasFriendly && (
|
|
220
|
+
<button
|
|
221
|
+
type="button"
|
|
222
|
+
onClick={() => setExpanded(!expanded)}
|
|
223
|
+
className="flex items-center gap-1 px-3 pb-2 text-red-400/60 hover:text-red-400/80 transition-colors"
|
|
224
|
+
>
|
|
225
|
+
{expanded ? <ChevronDown className="w-3 h-3" /> : <ChevronRight className="w-3 h-3" />}
|
|
226
|
+
<span>Details</span>
|
|
227
|
+
</button>
|
|
228
|
+
)}
|
|
229
|
+
{(expanded || !hasFriendly) && hasFriendly && (
|
|
230
|
+
<div className="px-3 pb-2 text-red-400/60 break-all font-mono leading-relaxed">
|
|
231
|
+
{parsed.raw}
|
|
232
|
+
</div>
|
|
233
|
+
)}
|
|
234
|
+
</div>
|
|
235
|
+
)
|
|
236
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useState, useCallback } from 'react'
|
|
2
2
|
import {
|
|
3
3
|
Copy,
|
|
4
|
+
CopyPlus,
|
|
4
5
|
Check,
|
|
5
6
|
RefreshCw,
|
|
6
7
|
Pencil,
|
|
@@ -11,6 +12,7 @@ import {
|
|
|
11
12
|
import { stringify as yamlStringify } from 'yaml'
|
|
12
13
|
import { CodeViewer } from '../ui/CodeViewer'
|
|
13
14
|
import { YamlEditor } from '../ui/YamlEditor'
|
|
15
|
+
import { Tooltip } from '../ui/Tooltip'
|
|
14
16
|
import type { SelectedResource } from '../../types'
|
|
15
17
|
|
|
16
18
|
// ============================================================================
|
|
@@ -102,9 +104,11 @@ interface EditableYamlViewProps {
|
|
|
102
104
|
isSaving?: boolean
|
|
103
105
|
/** Error message from the last save attempt */
|
|
104
106
|
saveError?: string | null
|
|
107
|
+
/** Duplicate handler — opens create dialog with this resource's YAML */
|
|
108
|
+
onDuplicate?: (params: { kind: string; namespace: string; name: string; yaml: string }) => void
|
|
105
109
|
}
|
|
106
110
|
|
|
107
|
-
export function EditableYamlView({ resource, data, onCopy, copied, onSaved, onSave, isSaving, saveError }: EditableYamlViewProps) {
|
|
111
|
+
export function EditableYamlView({ resource, data, onCopy, copied, onSaved, onSave, isSaving, saveError, onDuplicate }: EditableYamlViewProps) {
|
|
108
112
|
const [isEditing, setIsEditing] = useState(false)
|
|
109
113
|
const [editedYaml, setEditedYaml] = useState('')
|
|
110
114
|
const [yamlErrors, setYamlErrors] = useState<string[]>([])
|
|
@@ -289,6 +293,17 @@ export function EditableYamlView({ resource, data, onCopy, copied, onSaved, onSa
|
|
|
289
293
|
{copied ? <Check className="w-3.5 h-3.5 text-green-400" /> : <Copy className="w-3.5 h-3.5" />}
|
|
290
294
|
Copy
|
|
291
295
|
</button>
|
|
296
|
+
{onDuplicate && (
|
|
297
|
+
<Tooltip content="Duplicate as new resource">
|
|
298
|
+
<button
|
|
299
|
+
onClick={() => onDuplicate({ kind: resource.kind, namespace: resource.namespace, name: resource.name, yaml: yamlContent })}
|
|
300
|
+
className="flex items-center gap-1 px-2 py-1 text-xs text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded"
|
|
301
|
+
>
|
|
302
|
+
<CopyPlus className="w-3.5 h-3.5" />
|
|
303
|
+
Duplicate
|
|
304
|
+
</button>
|
|
305
|
+
</Tooltip>
|
|
306
|
+
)}
|
|
292
307
|
</div>
|
|
293
308
|
</div>
|
|
294
309
|
<CodeViewer
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
} from 'lucide-react'
|
|
19
19
|
import { createTwoFilesPatch } from 'diff'
|
|
20
20
|
import { clsx } from 'clsx'
|
|
21
|
+
import { Tooltip } from '../ui/Tooltip'
|
|
21
22
|
import { ForceDeleteConfirmDialog, type CascadeDependent } from '../ui/ForceDeleteConfirmDialog'
|
|
22
23
|
import { ConfirmDialog } from '../ui/ConfirmDialog'
|
|
23
24
|
import { DialogPortal } from '../ui/DialogPortal'
|
|
@@ -483,13 +484,14 @@ export function ResourceActionsBar({
|
|
|
483
484
|
)}
|
|
484
485
|
|
|
485
486
|
{onDelete && (
|
|
486
|
-
<
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
487
|
+
<Tooltip content="Delete resource">
|
|
488
|
+
<button
|
|
489
|
+
onClick={() => setShowDeleteConfirm(true)}
|
|
490
|
+
className="p-1.5 text-theme-text-secondary border border-theme-border-light rounded-lg hover:text-red-400 hover:border-red-400/50 hover:bg-red-500/10 transition-colors"
|
|
491
|
+
>
|
|
492
|
+
<Trash2 className="w-3.5 h-3.5" />
|
|
493
|
+
</button>
|
|
494
|
+
</Tooltip>
|
|
493
495
|
)}
|
|
494
496
|
|
|
495
497
|
<ForceDeleteConfirmDialog
|
|
@@ -97,6 +97,8 @@ import {
|
|
|
97
97
|
SealedSecretRenderer,
|
|
98
98
|
WorkflowTemplateRenderer,
|
|
99
99
|
NetworkPolicyRenderer,
|
|
100
|
+
CiliumNetworkPolicyRenderer,
|
|
101
|
+
ClusterNetworkPolicyRenderer,
|
|
100
102
|
PodDisruptionBudgetRenderer,
|
|
101
103
|
ServiceAccountRenderer,
|
|
102
104
|
RoleRenderer,
|
|
@@ -212,7 +214,10 @@ const KNOWN_KINDS = new Set([
|
|
|
212
214
|
'storageclasses', 'certificaterequests', 'clusterissuers', 'issuers',
|
|
213
215
|
'orders', 'challenges',
|
|
214
216
|
'gateways', 'gatewayclasses', 'httproutes', 'grpcroutes', 'tcproutes', 'tlsroutes', 'sealedsecrets', 'workflowtemplates',
|
|
215
|
-
'networkpolicies', '
|
|
217
|
+
'networkpolicies', 'networkpolicy',
|
|
218
|
+
'ciliumnetworkpolicies', 'ciliumnetworkpolicy', 'ciliumclusterwidenetworkpolicies', 'ciliumclusterwidenetworkpolicy',
|
|
219
|
+
'clusternetworkpolicies', 'clusternetworkpolicy',
|
|
220
|
+
'poddisruptionbudgets', 'serviceaccounts',
|
|
216
221
|
'roles', 'clusterroles', 'rolebindings', 'clusterrolebindings',
|
|
217
222
|
'events', 'gitrepositories', 'ocirepositories', 'helmrepositories',
|
|
218
223
|
'kustomizations', 'helmreleases', 'alerts', 'applications',
|
|
@@ -350,7 +355,9 @@ export function ResourceRendererDispatch({
|
|
|
350
355
|
{kind === 'tlsroutes' && <SimpleRouteRenderer data={data} kind="TLSRoute" onNavigate={onNavigate} />}
|
|
351
356
|
{kind === 'sealedsecrets' && <SealedSecretRenderer data={data} />}
|
|
352
357
|
{kind === 'workflowtemplates' && <WorkflowTemplateRenderer data={data} />}
|
|
353
|
-
{kind === 'networkpolicies' && <NetworkPolicyRenderer data={data} />}
|
|
358
|
+
{(kind === 'networkpolicies' || kind === 'networkpolicy') && <NetworkPolicyRenderer data={data} />}
|
|
359
|
+
{(kind === 'ciliumnetworkpolicies' || kind === 'ciliumnetworkpolicy' || kind === 'ciliumclusterwidenetworkpolicies' || kind === 'ciliumclusterwidenetworkpolicy') && <CiliumNetworkPolicyRenderer data={data} />}
|
|
360
|
+
{(kind === 'clusternetworkpolicies' || kind === 'clusternetworkpolicy') && <ClusterNetworkPolicyRenderer data={data} />}
|
|
354
361
|
{kind === 'poddisruptionbudgets' && <PodDisruptionBudgetRenderer data={data} />}
|
|
355
362
|
{kind === 'serviceaccounts' && <ServiceAccountRenderer data={data} />}
|
|
356
363
|
{(kind === 'roles' || kind === 'clusterroles') && <RoleRenderer data={data} />}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { ResourceRendererDispatch, getResourceStatus, type RendererOverrides } from './ResourceRendererDispatch'
|
|
2
2
|
export { EditableYamlView, SaveSuccessAnimation } from './EditableYamlView'
|
|
3
3
|
export { ResourceActionsBar, RevisionHistoryDialog } from './ResourceActionsBar'
|
|
4
|
+
export { CreateResourceDialog, type CreateResourceDialogProps, type ApplyResult } from './CreateResourceDialog'
|
|
@@ -302,6 +302,7 @@ export const K8sResourceNode = memo(function K8sResourceNode({
|
|
|
302
302
|
const canCollapse = isPodGroup && onCollapse && isExpanded
|
|
303
303
|
const statusIssue = nodeData.statusIssue as string | undefined
|
|
304
304
|
const issueTooltip = getIssueTooltip(statusIssue)
|
|
305
|
+
const policyStatus = nodeData.policyStatus as string | undefined
|
|
305
306
|
|
|
306
307
|
// CSS class for icon (replaces Lucide SVG - saves ~5 DOM elements per node)
|
|
307
308
|
const iconClass = `topology-icon topology-icon-${kind.toLowerCase()}`
|
|
@@ -417,6 +418,20 @@ export const K8sResourceNode = memo(function K8sResourceNode({
|
|
|
417
418
|
)}
|
|
418
419
|
</div>
|
|
419
420
|
|
|
421
|
+
{/* Network Policy coverage indicator */}
|
|
422
|
+
{policyStatus && (
|
|
423
|
+
<Tooltip content={policyStatus === 'protected' ? 'Protected by NetworkPolicy' : 'No NetworkPolicy coverage'} position="right">
|
|
424
|
+
<span className={clsx(
|
|
425
|
+
'inline-flex items-center justify-center w-3.5 h-3.5 rounded-sm text-[8px] font-bold cursor-help',
|
|
426
|
+
policyStatus === 'protected'
|
|
427
|
+
? 'bg-green-500/20 text-green-500 border border-green-500/30'
|
|
428
|
+
: 'bg-yellow-500/20 text-yellow-500 border border-yellow-500/30',
|
|
429
|
+
)}>
|
|
430
|
+
{policyStatus === 'protected' ? '✓' : '!'}
|
|
431
|
+
</span>
|
|
432
|
+
</Tooltip>
|
|
433
|
+
)}
|
|
434
|
+
|
|
420
435
|
{/* Name */}
|
|
421
436
|
<div className="text-sm font-medium text-theme-text-primary truncate pr-1">
|
|
422
437
|
{name}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FolderTree } from 'lucide-react'
|
|
1
|
+
import { FolderTree, ShieldCheck } from 'lucide-react'
|
|
2
2
|
import type { TopologyMode, GroupingMode } from '../../types/core'
|
|
3
3
|
|
|
4
4
|
interface TopologyControlsProps {
|
|
@@ -7,6 +7,8 @@ interface TopologyControlsProps {
|
|
|
7
7
|
groupingMode: GroupingMode
|
|
8
8
|
onGroupingModeChange: (mode: GroupingMode) => void
|
|
9
9
|
showNoGrouping?: boolean
|
|
10
|
+
showPolicyEffect?: boolean
|
|
11
|
+
onShowPolicyEffectChange?: (show: boolean) => void
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
export function TopologyControls({
|
|
@@ -15,9 +17,27 @@ export function TopologyControls({
|
|
|
15
17
|
groupingMode,
|
|
16
18
|
onGroupingModeChange,
|
|
17
19
|
showNoGrouping = true,
|
|
20
|
+
showPolicyEffect = false,
|
|
21
|
+
onShowPolicyEffectChange,
|
|
18
22
|
}: TopologyControlsProps) {
|
|
19
23
|
return (
|
|
20
24
|
<div className="absolute top-4 right-4 z-10 flex items-center gap-2">
|
|
25
|
+
{/* Policy effect toggle */}
|
|
26
|
+
{onShowPolicyEffectChange && (
|
|
27
|
+
<button
|
|
28
|
+
onClick={() => onShowPolicyEffectChange(!showPolicyEffect)}
|
|
29
|
+
className={`flex items-center gap-1.5 px-2.5 py-1.5 text-xs rounded-lg border transition-colors ${
|
|
30
|
+
showPolicyEffect
|
|
31
|
+
? 'bg-indigo-600 text-white border-indigo-600'
|
|
32
|
+
: 'bg-theme-surface/90 backdrop-blur text-theme-text-secondary border-theme-border hover:text-theme-text-primary'
|
|
33
|
+
}`}
|
|
34
|
+
title="Show NetworkPolicy effects on edges"
|
|
35
|
+
>
|
|
36
|
+
<ShieldCheck className="w-3.5 h-3.5" />
|
|
37
|
+
Policies
|
|
38
|
+
</button>
|
|
39
|
+
)}
|
|
40
|
+
|
|
21
41
|
{/* Grouping selector */}
|
|
22
42
|
<div className="flex items-center gap-1.5 px-2 py-1.5 bg-theme-surface/90 backdrop-blur border border-theme-border rounded-lg">
|
|
23
43
|
<FolderTree className="w-3.5 h-3.5 text-theme-text-secondary" />
|
|
@@ -73,7 +73,7 @@ const LARGE_CLUSTER_NS_THRESHOLD = 5
|
|
|
73
73
|
|
|
74
74
|
// Build edges, handling collapsed groups
|
|
75
75
|
function buildEdges(
|
|
76
|
-
topologyEdges:
|
|
76
|
+
topologyEdges: TopologyEdge[],
|
|
77
77
|
collapsedGroups: Set<string>,
|
|
78
78
|
groupMap: Map<string, string[]>,
|
|
79
79
|
groupingMode: GroupingMode,
|
|
@@ -73,6 +73,10 @@ const KIND: Record<string, string> = {
|
|
|
73
73
|
HorizontalPodAutoscaler: 'bg-pink-100 text-pink-800 border-pink-300 dark:bg-pink-950/50 dark:text-pink-300 dark:border-pink-700/40',
|
|
74
74
|
PersistentVolumeClaim: 'bg-cyan-100 text-cyan-800 border-cyan-300 dark:bg-cyan-950/50 dark:text-cyan-400 dark:border-cyan-700/40',
|
|
75
75
|
PodDisruptionBudget: 'bg-orange-100 text-orange-800 border-orange-300 dark:bg-orange-950/50 dark:text-orange-300 dark:border-orange-700/40',
|
|
76
|
+
NetworkPolicy: 'bg-indigo-100 text-indigo-800 border-indigo-300 dark:bg-indigo-950/50 dark:text-indigo-300 dark:border-indigo-700/40',
|
|
77
|
+
CiliumNetworkPolicy: 'bg-indigo-100 text-indigo-800 border-indigo-300 dark:bg-indigo-950/50 dark:text-indigo-300 dark:border-indigo-700/40',
|
|
78
|
+
CiliumClusterwideNetworkPolicy: 'bg-indigo-100 text-indigo-800 border-indigo-300 dark:bg-indigo-950/50 dark:text-indigo-300 dark:border-indigo-700/40',
|
|
79
|
+
ClusterNetworkPolicy: 'bg-violet-100 text-violet-800 border-violet-300 dark:bg-violet-950/50 dark:text-violet-300 dark:border-violet-700/40',
|
|
76
80
|
|
|
77
81
|
// Cluster-scoped
|
|
78
82
|
Node: 'bg-sky-100 text-sky-700 border-sky-300 dark:bg-sky-950/50 dark:text-sky-400 dark:border-sky-700/40',
|
|
@@ -147,6 +151,10 @@ export function getKindColorClass(kind: string): string {
|
|
|
147
151
|
horizontalpodautoscalers: 'HorizontalPodAutoscaler',
|
|
148
152
|
persistentvolumeclaims: 'PersistentVolumeClaim',
|
|
149
153
|
poddisruptionbudgets: 'PodDisruptionBudget',
|
|
154
|
+
networkpolicies: 'NetworkPolicy',
|
|
155
|
+
ciliumnetworkpolicies: 'CiliumNetworkPolicy',
|
|
156
|
+
ciliumclusterwidenetworkpolicies: 'CiliumClusterwideNetworkPolicy',
|
|
157
|
+
clusternetworkpolicies: 'ClusterNetworkPolicy',
|
|
150
158
|
rollouts: 'Rollout', httproutes: 'HTTPRoute', grpcroutes: 'GRPCRoute',
|
|
151
159
|
events: 'Event', helmreleases: 'HelmRelease',
|
|
152
160
|
}
|
|
@@ -117,7 +117,7 @@ export function Property({ label, value, copyable, onCopy, copied }: PropertyPro
|
|
|
117
117
|
|
|
118
118
|
return (
|
|
119
119
|
<div className="flex items-start gap-2 text-sm">
|
|
120
|
-
<span className="text-theme-text-tertiary w-
|
|
120
|
+
<span className="text-theme-text-tertiary w-40 shrink-0">{label}</span>
|
|
121
121
|
<span className="text-theme-text-primary break-all flex-1">{displayValue}</span>
|
|
122
122
|
{copyable && onCopy && !isReactElement(value) && (
|
|
123
123
|
<button
|
|
@@ -568,9 +568,25 @@ export function RelatedResourcesSection({ relationships, onNavigate }: RelatedRe
|
|
|
568
568
|
{relationships.scalers && relationships.scalers.length > 0 && (
|
|
569
569
|
<RelationshipGroup label="Autoscaler" refs={dedupeRefs(relationships.scalers)} onNavigate={onNavigate} />
|
|
570
570
|
)}
|
|
571
|
-
{relationships.policies && relationships.policies.length > 0 && (
|
|
572
|
-
|
|
573
|
-
|
|
571
|
+
{relationships.policies && relationships.policies.length > 0 && (() => {
|
|
572
|
+
const policyKinds = new Set(['NetworkPolicy', 'CiliumNetworkPolicy', 'CiliumClusterwideNetworkPolicy', 'ClusterNetworkPolicy'])
|
|
573
|
+
const pdbs = relationships.policies.filter(r => r.kind === 'PodDisruptionBudget')
|
|
574
|
+
const netpols = relationships.policies.filter(r => policyKinds.has(r.kind))
|
|
575
|
+
const other = relationships.policies.filter(r => r.kind !== 'PodDisruptionBudget' && !policyKinds.has(r.kind))
|
|
576
|
+
return (
|
|
577
|
+
<>
|
|
578
|
+
{pdbs.length > 0 && (
|
|
579
|
+
<RelationshipGroup label="Disruption Budget" refs={dedupeRefs(pdbs)} onNavigate={onNavigate} />
|
|
580
|
+
)}
|
|
581
|
+
{netpols.length > 0 && (
|
|
582
|
+
<RelationshipGroup label="Network Policies" refs={dedupeRefs(netpols)} onNavigate={onNavigate} />
|
|
583
|
+
)}
|
|
584
|
+
{other.length > 0 && (
|
|
585
|
+
<RelationshipGroup label="Policies" refs={dedupeRefs(other)} onNavigate={onNavigate} />
|
|
586
|
+
)}
|
|
587
|
+
</>
|
|
588
|
+
)
|
|
589
|
+
})()}
|
|
574
590
|
{relationships.scaleTarget && (
|
|
575
591
|
<RelationshipGroup label="Scale Target" refs={[relationships.scaleTarget]} onNavigate={onNavigate} />
|
|
576
592
|
)}
|