@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.
Files changed (33) hide show
  1. package/package.json +1 -1
  2. package/src/components/audit/AuditAlerts.tsx +88 -0
  3. package/src/components/audit/AuditCard.tsx +130 -0
  4. package/src/components/audit/AuditFindingsTable.tsx +526 -0
  5. package/src/components/audit/index.ts +3 -0
  6. package/src/components/dock/DockContext.tsx +5 -2
  7. package/src/components/dock/LocalTerminalTab.tsx +11 -0
  8. package/src/components/resources/ResourcesView.tsx +14 -0
  9. package/src/components/resources/renderers/CiliumNetworkPolicyRenderer.tsx +228 -0
  10. package/src/components/resources/renderers/ClusterNetworkPolicyRenderer.tsx +174 -0
  11. package/src/components/resources/renderers/GenericRenderer.tsx +1 -1
  12. package/src/components/resources/renderers/NetworkPolicyDiagram.tsx +277 -0
  13. package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +10 -1
  14. package/src/components/resources/renderers/ServiceRenderer.tsx +40 -28
  15. package/src/components/resources/renderers/index.ts +3 -0
  16. package/src/components/shared/CreateResourceDialog.tsx +236 -0
  17. package/src/components/shared/EditableYamlView.tsx +16 -1
  18. package/src/components/shared/ResourceActionsBar.tsx +9 -7
  19. package/src/components/shared/ResourceRendererDispatch.tsx +9 -2
  20. package/src/components/shared/index.ts +1 -0
  21. package/src/components/topology/K8sResourceNode.tsx +15 -0
  22. package/src/components/topology/TopologyControls.tsx +21 -1
  23. package/src/components/topology/TopologyGraph.tsx +1 -1
  24. package/src/components/ui/Badge.tsx +8 -0
  25. package/src/components/ui/drawer-components.tsx +20 -4
  26. package/src/components/workload/WorkloadView.tsx +55 -29
  27. package/src/index.ts +3 -0
  28. package/src/types/core.ts +2 -1
  29. package/src/utils/badge-colors.ts +7 -0
  30. package/src/utils/index.ts +2 -0
  31. package/src/utils/k8s-errors.ts +142 -0
  32. package/src/utils/resource-icons.ts +3 -0
  33. 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
- <Property label="Cluster IP" value={spec.clusterIP} copyable onCopy={onCopy} copied={copied} />
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.length > 0 && (
56
+ {lbIngress.map((ing: any, i: number) => (
54
57
  <Property
55
- label="Load Balancer"
56
- value={lbIngress[0].ip || lbIngress[0].hostname}
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
- <Section title="Ports" defaultExpanded>
68
- <div className="space-y-2">
69
- {ports.map((port: any, i: number) => (
70
- <div key={`${port.port}-${port.protocol || 'TCP'}`} className="card-inner text-sm">
71
- <div className="flex items-center justify-between">
72
- <span className="text-theme-text-primary font-medium">{port.name || `port-${i + 1}`}</span>
73
- <div className="flex items-center gap-2">
74
- {renderPortAction?.({
75
- namespace,
76
- serviceName,
77
- port: port.port,
78
- protocol: port.protocol || 'TCP',
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
- <div className="text-xs text-theme-text-secondary mt-1">
83
- {port.port} {port.targetPort !== port.port && `→ ${port.targetPort}`}
84
- {port.nodePort && ` (NodePort: ${port.nodePort})`}
85
- </div>
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
- <button
487
- onClick={() => setShowDeleteConfirm(true)}
488
- className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-red-400 hover:text-white hover:bg-red-600 border border-red-400/50 hover:border-red-600 rounded-lg transition-colors"
489
- >
490
- <Trash2 className="w-3.5 h-3.5" />
491
- Delete
492
- </button>
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', 'poddisruptionbudgets', 'serviceaccounts',
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: { id: string; source: string; target: string; type: string }[],
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-28 shrink-0">{label}</span>
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
- <RelationshipGroup label="Disruption Budget" refs={dedupeRefs(relationships.policies)} onNavigate={onNavigate} />
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
  )}