@skyhook-io/k8s-ui 1.3.2 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/package.json +2 -2
  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 +126 -49
  9. package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +190 -28
  10. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +22 -0
  11. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +156 -5
  12. package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +28 -1
  13. package/src/components/resources/renderers/CertificateRenderer.tsx +47 -8
  14. package/src/components/resources/renderers/ChallengeRenderer.tsx +22 -3
  15. package/src/components/resources/renderers/CiliumNetworkPolicyRenderer.tsx +445 -0
  16. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +81 -9
  17. package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +10 -3
  18. package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +59 -1
  19. package/src/components/resources/renderers/ClusterNetworkPolicyRenderer.tsx +160 -0
  20. package/src/components/resources/renderers/ExternalSecretRenderer.tsx +3 -5
  21. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +91 -3
  22. package/src/components/resources/renderers/GenericRenderer.tsx +1 -1
  23. package/src/components/resources/renderers/GitRepositoryRenderer.tsx +3 -42
  24. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +3 -41
  25. package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +66 -2
  26. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +31 -1
  27. package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +66 -1
  28. package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +3 -0
  29. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +55 -1
  30. package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +23 -0
  31. package/src/components/resources/renderers/KustomizationRenderer.tsx +38 -3
  32. package/src/components/resources/renderers/NetworkPolicyDiagram.tsx +277 -0
  33. package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +17 -57
  34. package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +3 -42
  35. package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +3 -8
  36. package/src/components/resources/renderers/PodMonitorRenderer.tsx +5 -7
  37. package/src/components/resources/renderers/PodRenderer.tsx +22 -74
  38. package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +178 -20
  39. package/src/components/resources/renderers/ReplicaSetRenderer.tsx +2 -2
  40. package/src/components/resources/renderers/RolloutRenderer.tsx +105 -3
  41. package/src/components/resources/renderers/SbomReportRenderer.tsx +46 -5
  42. package/src/components/resources/renderers/SealedSecretRenderer.tsx +11 -2
  43. package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +5 -7
  44. package/src/components/resources/renderers/ServiceRenderer.tsx +40 -28
  45. package/src/components/resources/renderers/VeleroBackupRenderer.tsx +11 -3
  46. package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +11 -3
  47. package/src/components/resources/renderers/WebhookConfigRenderer.tsx +8 -16
  48. package/src/components/resources/renderers/WorkflowRenderer.tsx +71 -17
  49. package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +46 -1
  50. package/src/components/resources/renderers/index.ts +3 -0
  51. package/src/components/resources/renderers/karpenter-cells.tsx +18 -3
  52. package/src/components/resources/resource-utils-cnpg.ts +47 -0
  53. package/src/components/resources/resource-utils-karpenter.ts +6 -0
  54. package/src/components/resources/resource-utils-prometheus.ts +50 -6
  55. package/src/components/resources/resource-utils.ts +131 -33
  56. package/src/components/shared/CreateResourceDialog.tsx +236 -0
  57. package/src/components/shared/EditableYamlView.tsx +16 -1
  58. package/src/components/shared/ResourceActionsBar.tsx +9 -7
  59. package/src/components/shared/ResourceRendererDispatch.tsx +13 -6
  60. package/src/components/shared/index.ts +1 -0
  61. package/src/components/timeline/TimelineList.tsx +11 -4
  62. package/src/components/topology/K8sResourceNode.tsx +15 -0
  63. package/src/components/topology/TopologyControls.tsx +21 -1
  64. package/src/components/topology/TopologyGraph.tsx +1 -1
  65. package/src/components/ui/Badge.tsx +8 -0
  66. package/src/components/ui/drawer-components.tsx +68 -5
  67. package/src/components/workload/WorkloadView.tsx +55 -29
  68. package/src/index.ts +3 -0
  69. package/src/theme/variables.css +1 -1
  70. package/src/types/core.ts +2 -1
  71. package/src/types/gitops.ts +8 -0
  72. package/src/utils/badge-colors.ts +7 -0
  73. package/src/utils/index.ts +2 -0
  74. package/src/utils/k8s-errors.ts +142 -0
  75. package/src/utils/navigation.test.ts +29 -2
  76. package/src/utils/navigation.ts +45 -24
  77. package/src/utils/resource-icons.ts +3 -0
  78. package/src/utils/skeleton-yaml.ts +302 -0
@@ -1,4 +1,4 @@
1
- import { Server, Settings, Shield, Cpu, Tag } from 'lucide-react'
1
+ import { Server, Settings, Shield, Cpu, Tag, BarChart3 } from 'lucide-react'
2
2
  import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink } from '../../ui/drawer-components'
3
3
  import { kindToPlural } from '../../../utils/navigation'
4
4
  import {
@@ -9,6 +9,16 @@ import {
9
9
  getNodePoolWeight,
10
10
  } from '../resource-utils-karpenter'
11
11
 
12
+ function formatCpuCores(value: string): string {
13
+ // Karpenter status.resources CPU is typically in millicores (e.g. "12000m") or cores (e.g. "12")
14
+ if (value.endsWith('m')) {
15
+ const millis = parseInt(value, 10)
16
+ if (!isNaN(millis)) return String(millis / 1000)
17
+ }
18
+ return value
19
+ }
20
+
21
+
12
22
  interface KarpenterNodePoolRendererProps {
13
23
  data: any
14
24
  onNavigate?: (ref: { kind: string; namespace: string; name: string; group?: string }) => void
@@ -28,6 +38,9 @@ export function KarpenterNodePoolRenderer({ data, onNavigate }: KarpenterNodePoo
28
38
  const templateLabels = spec.template?.metadata?.labels || {}
29
39
  const templateExpireAfter = spec.template?.spec?.expireAfter
30
40
  const nodeClassRef = spec.template?.spec?.nodeClassRef
41
+ const templateTaints = spec.template?.spec?.taints || []
42
+ const templateStartupTaints = spec.template?.spec?.startupTaints || []
43
+ const statusResources = status.resources || {}
31
44
 
32
45
  return (
33
46
  <>
@@ -77,6 +90,26 @@ export function KarpenterNodePoolRenderer({ data, onNavigate }: KarpenterNodePoo
77
90
  </PropertyList>
78
91
  </Section>
79
92
 
93
+ {/* Resource Usage — from status.resources vs spec.limits */}
94
+ {(statusResources.cpu || statusResources.memory) && (
95
+ <Section title="Resource Usage" icon={BarChart3} defaultExpanded>
96
+ <PropertyList>
97
+ {statusResources.cpu && (
98
+ <Property
99
+ label="CPU"
100
+ value={`${formatCpuCores(statusResources.cpu)}${spec.limits?.cpu ? ` / ${formatCpuCores(spec.limits.cpu)}` : ''}`}
101
+ />
102
+ )}
103
+ {statusResources.memory && (
104
+ <Property
105
+ label="Memory"
106
+ value={`${statusResources.memory}${spec.limits?.memory ? ` / ${spec.limits.memory}` : ''}`}
107
+ />
108
+ )}
109
+ </PropertyList>
110
+ </Section>
111
+ )}
112
+
80
113
  {/* Disruption */}
81
114
  <Section title="Disruption" icon={Shield}>
82
115
  <PropertyList>
@@ -118,6 +151,38 @@ export function KarpenterNodePoolRenderer({ data, onNavigate }: KarpenterNodePoo
118
151
  </Section>
119
152
  )}
120
153
 
154
+ {/* Template Taints */}
155
+ {templateTaints.length > 0 && (
156
+ <Section title={`Template Taints (${templateTaints.length})`} icon={Shield} defaultExpanded>
157
+ <div className="flex flex-wrap gap-1">
158
+ {templateTaints.map((taint: any, i: number) => (
159
+ <span
160
+ key={i}
161
+ className="badge-sm bg-theme-hover text-theme-text-secondary"
162
+ >
163
+ {taint.key}={taint.value || ''}:{taint.effect || ''}
164
+ </span>
165
+ ))}
166
+ </div>
167
+ </Section>
168
+ )}
169
+
170
+ {/* Template Startup Taints */}
171
+ {templateStartupTaints.length > 0 && (
172
+ <Section title={`Startup Taints (${templateStartupTaints.length})`} icon={Shield} defaultExpanded>
173
+ <div className="flex flex-wrap gap-1">
174
+ {templateStartupTaints.map((taint: any, i: number) => (
175
+ <span
176
+ key={i}
177
+ className="badge-sm bg-theme-hover text-theme-text-secondary"
178
+ >
179
+ {taint.key}={taint.value || ''}:{taint.effect || ''}
180
+ </span>
181
+ ))}
182
+ </div>
183
+ </Section>
184
+ )}
185
+
121
186
  {/* Requirements */}
122
187
  {requirements.length > 0 && (
123
188
  <Section title={`Requirements (${requirements.length})`} icon={Settings} defaultExpanded>
@@ -46,6 +46,9 @@ export function KedaScaledJobRenderer({ data }: KedaScaledJobRendererProps) {
46
46
  {spec.failedJobsHistoryLimit !== undefined && (
47
47
  <Property label="Failure History" value={String(spec.failedJobsHistoryLimit)} />
48
48
  )}
49
+ {spec.minReplicaCount !== undefined && (
50
+ <Property label="Min Replicas" value={String(spec.minReplicaCount)} />
51
+ )}
49
52
  {spec.maxReplicaCount !== undefined && (
50
53
  <Property label="Max Replicas" value={String(spec.maxReplicaCount)} />
51
54
  )}
@@ -1,4 +1,4 @@
1
- import { Cpu } from 'lucide-react'
1
+ import { Cpu, Settings } from 'lucide-react'
2
2
  import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink } from '../../ui/drawer-components'
3
3
  import { kindToPlural } from '../../../utils/navigation'
4
4
  import {
@@ -12,6 +12,16 @@ import {
12
12
  getScaledObjectCooldownPeriod,
13
13
  } from '../resource-utils-keda'
14
14
 
15
+ function summarizeScalingPolicies(policies: any[] | undefined, periodSeconds?: number): string {
16
+ if (!policies || policies.length === 0) return '-'
17
+ return policies.map((p: any) => {
18
+ const value = p.value ?? '?'
19
+ const type = p.type === 'Percent' ? '%' : ` pod${value !== 1 ? 's' : ''}`
20
+ const period = p.periodSeconds ?? periodSeconds ?? '?'
21
+ return `max ${value}${type}/${period}s`
22
+ }).join(', ')
23
+ }
24
+
15
25
  interface KedaScaledObjectRendererProps {
16
26
  data: any
17
27
  onNavigate?: (ref: { kind: string; namespace: string; name: string }) => void
@@ -131,6 +141,50 @@ export function KedaScaledObjectRenderer({ data, onNavigate }: KedaScaledObjectR
131
141
  </Section>
132
142
  )}
133
143
 
144
+ {/* Advanced section */}
145
+ {data.spec?.advanced && (
146
+ <Section title="Advanced" icon={Settings} defaultExpanded={false}>
147
+ <PropertyList>
148
+ {data.spec.advanced.restoreToOriginalReplicaCount != null && (
149
+ <Property
150
+ label="Restore Original Replicas"
151
+ value={data.spec.advanced.restoreToOriginalReplicaCount ? 'Yes' : 'No'}
152
+ />
153
+ )}
154
+ {data.spec.advanced.horizontalPodAutoscalerConfig?.behavior?.scaleUp && (
155
+ <Property
156
+ label="Scale Up"
157
+ value={summarizeScalingPolicies(
158
+ data.spec.advanced.horizontalPodAutoscalerConfig.behavior.scaleUp.policies,
159
+ data.spec.advanced.horizontalPodAutoscalerConfig.behavior.scaleUp.stabilizationWindowSeconds
160
+ )}
161
+ />
162
+ )}
163
+ {data.spec.advanced.horizontalPodAutoscalerConfig?.behavior?.scaleDown && (
164
+ <Property
165
+ label="Scale Down"
166
+ value={summarizeScalingPolicies(
167
+ data.spec.advanced.horizontalPodAutoscalerConfig.behavior.scaleDown.policies,
168
+ data.spec.advanced.horizontalPodAutoscalerConfig.behavior.scaleDown.stabilizationWindowSeconds
169
+ )}
170
+ />
171
+ )}
172
+ {data.spec.advanced.horizontalPodAutoscalerConfig?.behavior?.scaleUp?.stabilizationWindowSeconds != null && (
173
+ <Property
174
+ label="Scale Up Stabilization"
175
+ value={`${data.spec.advanced.horizontalPodAutoscalerConfig.behavior.scaleUp.stabilizationWindowSeconds}s`}
176
+ />
177
+ )}
178
+ {data.spec.advanced.horizontalPodAutoscalerConfig?.behavior?.scaleDown?.stabilizationWindowSeconds != null && (
179
+ <Property
180
+ label="Scale Down Stabilization"
181
+ value={`${data.spec.advanced.horizontalPodAutoscalerConfig.behavior.scaleDown.stabilizationWindowSeconds}s`}
182
+ />
183
+ )}
184
+ </PropertyList>
185
+ </Section>
186
+ )}
187
+
134
188
  <ConditionsSection conditions={conditions} />
135
189
  </>
136
190
  )
@@ -19,6 +19,7 @@ export function KedaTriggerAuthRenderer({ data, onNavigate }: KedaTriggerAuthRen
19
19
  const vault = spec.hashiCorpVault
20
20
  const azureKeyVault = spec.azureKeyVault
21
21
  const awsSecretManager = spec.awsSecretManager
22
+ const gcpSecretManager = spec.gcpSecretManager
22
23
  const podIdentity = spec.podIdentity
23
24
 
24
25
  return (
@@ -145,6 +146,28 @@ export function KedaTriggerAuthRenderer({ data, onNavigate }: KedaTriggerAuthRen
145
146
  </Section>
146
147
  )}
147
148
 
149
+ {/* GCP Secret Manager */}
150
+ {gcpSecretManager && (
151
+ <Section title="GCP Secret Manager" icon={Cloud}>
152
+ <PropertyList>
153
+ {gcpSecretManager.credentials?.clientSecret?.name && (
154
+ <Property label="Credentials Secret" value={gcpSecretManager.credentials.clientSecret.name} />
155
+ )}
156
+ </PropertyList>
157
+ {gcpSecretManager.secrets && gcpSecretManager.secrets.length > 0 && (
158
+ <div className="mt-2 space-y-1">
159
+ <div className="text-xs font-medium text-theme-text-secondary uppercase tracking-wider mb-1">Secrets</div>
160
+ {gcpSecretManager.secrets.map((s: any, i: number) => (
161
+ <div key={i} className="card-inner text-sm">
162
+ <span className="text-theme-text-primary font-medium">{s.parameter}</span>
163
+ <span className="text-theme-text-secondary ml-2">{s.id}{s.version ? ` (v${s.version})` : ''}</span>
164
+ </div>
165
+ ))}
166
+ </div>
167
+ )}
168
+ </Section>
169
+ )}
170
+
148
171
  {conditions.length > 0 && <ConditionsSection conditions={conditions} />}
149
172
  </>
150
173
  )
@@ -1,14 +1,15 @@
1
- import { Layers, FolderTree } from 'lucide-react'
2
- import { Section, PropertyList, Property, ConditionsSection, ProblemAlerts } from '../../ui/drawer-components'
1
+ import { Layers, FolderTree, GitBranch } from 'lucide-react'
2
+ import { Section, PropertyList, Property, ConditionsSection, ProblemAlerts, ResourceLink } from '../../ui/drawer-components'
3
3
  import { formatAge } from '../resource-utils'
4
4
  import { GitOpsStatusBadge, ManagedResourcesList, SyncCountdown } from '../../gitops'
5
5
  import { fluxConditionsToGitOpsStatus, parseFluxInventory, type FluxCondition } from '../../../types/gitops'
6
6
 
7
7
  interface KustomizationRendererProps {
8
8
  data: any
9
+ onNavigate?: (ref: { kind: string; namespace: string; name: string }) => void
9
10
  }
10
11
 
11
- export function KustomizationRenderer({ data }: KustomizationRendererProps) {
12
+ export function KustomizationRenderer({ data, onNavigate }: KustomizationRendererProps) {
12
13
  const status = data.status || {}
13
14
  const spec = data.spec || {}
14
15
  const conditions = (status.conditions || []) as FluxCondition[]
@@ -39,6 +40,23 @@ export function KustomizationRenderer({ data }: KustomizationRendererProps) {
39
40
  })
40
41
  }
41
42
 
43
+ // Revision mismatch detection — only flag when Ready=False to avoid false positives during active reconciliation
44
+ const isReadyFalse = conditions.find((c: any) => c.type === 'Ready')?.status === 'False'
45
+ if (
46
+ isReadyFalse &&
47
+ status.lastAppliedRevision &&
48
+ status.lastAttemptedRevision &&
49
+ status.lastAppliedRevision !== status.lastAttemptedRevision
50
+ ) {
51
+ problems.push({
52
+ color: 'red',
53
+ message: `Revision mismatch: attempted ${status.lastAttemptedRevision} but applied ${status.lastAppliedRevision} — the latest reconciliation failed.`,
54
+ })
55
+ }
56
+
57
+ // Dependencies
58
+ const dependsOn: Array<{ name: string; namespace?: string }> = spec.dependsOn || []
59
+
42
60
  // Source reference
43
61
  const sourceRef = spec.sourceRef || {}
44
62
 
@@ -94,6 +112,23 @@ export function KustomizationRenderer({ data }: KustomizationRendererProps) {
94
112
  </PropertyList>
95
113
  </Section>
96
114
 
115
+ {/* Dependencies */}
116
+ {dependsOn.length > 0 && (
117
+ <Section title={`Dependencies (${dependsOn.length})`} icon={GitBranch}>
118
+ <div className="flex flex-wrap gap-1">
119
+ {dependsOn.map((dep, idx) => (
120
+ <ResourceLink
121
+ key={idx}
122
+ name={dep.name}
123
+ kind="kustomizations"
124
+ namespace={dep.namespace || data.metadata?.namespace || ''}
125
+ onNavigate={onNavigate}
126
+ />
127
+ ))}
128
+ </div>
129
+ </Section>
130
+ )}
131
+
97
132
  {/* Health checks */}
98
133
  {spec.healthChecks && spec.healthChecks.length > 0 && (
99
134
  <Section title={`Health Checks (${spec.healthChecks.length})`} defaultExpanded={false}>
@@ -0,0 +1,277 @@
1
+ import { clsx } from 'clsx'
2
+ import { Tooltip } from '../../ui/Tooltip'
3
+
4
+ interface NetworkPolicyDiagramProps {
5
+ spec: any
6
+ }
7
+
8
+ /**
9
+ * Visual flow diagram for a NetworkPolicy.
10
+ * Three-column layout: Sources -> Target -> Destinations
11
+ * Each rule is a horizontal band. Peers within a rule are OR'd (stacked).
12
+ * Pure CSS/SVG — no ReactFlow needed for this static diagram.
13
+ */
14
+ export function NetworkPolicyDiagram({ spec }: NetworkPolicyDiagramProps) {
15
+ const podSelector = spec.podSelector || {}
16
+ const matchLabels = podSelector.matchLabels || {}
17
+ const policyTypes: string[] = spec.policyTypes || []
18
+ const ingress: any[] | undefined = spec.ingress
19
+ const egress: any[] | undefined = spec.egress
20
+
21
+ const hasIngress = policyTypes.includes('Ingress')
22
+ const hasEgress = policyTypes.includes('Egress')
23
+ const targetLabel = Object.keys(matchLabels).length > 0
24
+ ? Object.entries(matchLabels).map(([k, v]) => `${k}=${v}`).join(', ')
25
+ : 'All pods'
26
+
27
+ const ingressDenied = hasIngress && (!ingress || ingress.length === 0)
28
+ const egressDenied = hasEgress && (!egress || egress.length === 0)
29
+
30
+ return (
31
+ <div className="card-inner-lg space-y-3">
32
+ {/* Ingress flows */}
33
+ {hasIngress && (
34
+ <div className="space-y-2">
35
+ <div className="flex items-center gap-1.5">
36
+ <span className="w-1.5 h-1.5 rounded-full bg-blue-500" />
37
+ <span className="text-[10px] font-semibold uppercase tracking-wider text-blue-400">Ingress</span>
38
+ </div>
39
+ {ingressDenied ? (
40
+ <FlowRow
41
+ sources={[{ label: 'All traffic', type: 'deny' as const }]}
42
+ target={targetLabel}
43
+ ports={[]}
44
+ direction="ingress"
45
+ denied
46
+ />
47
+ ) : (
48
+ ingress?.map((rule, i) => (
49
+ <FlowRow
50
+ key={i}
51
+ sources={extractPeers(rule.from)}
52
+ target={targetLabel}
53
+ ports={extractPorts(rule.ports)}
54
+ direction="ingress"
55
+ />
56
+ ))
57
+ )}
58
+ </div>
59
+ )}
60
+
61
+ {/* Egress flows */}
62
+ {hasEgress && (
63
+ <div className="space-y-2">
64
+ <div className="flex items-center gap-1.5">
65
+ <span className="w-1.5 h-1.5 rounded-full bg-purple-500" />
66
+ <span className="text-[10px] font-semibold uppercase tracking-wider text-purple-400">Egress</span>
67
+ </div>
68
+ {egressDenied ? (
69
+ <FlowRow
70
+ sources={[{ label: 'All traffic', type: 'deny' as const }]}
71
+ target={targetLabel}
72
+ ports={[]}
73
+ direction="egress"
74
+ denied
75
+ />
76
+ ) : (
77
+ egress?.map((rule, i) => (
78
+ <FlowRow
79
+ key={i}
80
+ sources={extractPeers(rule.to)}
81
+ target={targetLabel}
82
+ ports={extractPorts(rule.ports)}
83
+ direction="egress"
84
+ />
85
+ ))
86
+ )}
87
+ </div>
88
+ )}
89
+
90
+ {!hasIngress && !hasEgress && (
91
+ <div className="text-xs text-theme-text-tertiary text-center py-2">No policy types specified</div>
92
+ )}
93
+ </div>
94
+ )
95
+ }
96
+
97
+ interface PeerInfo {
98
+ label: string
99
+ sublabel?: string
100
+ type: 'pod' | 'namespace' | 'cidr' | 'all' | 'deny' | 'combined'
101
+ }
102
+
103
+ function extractPeers(peers: any[] | undefined): PeerInfo[] {
104
+ if (!peers || peers.length === 0) {
105
+ return [{ label: 'Any source', type: 'all' }]
106
+ }
107
+ return peers.map(peer => {
108
+ const hasPodSel = peer.podSelector !== undefined
109
+ const hasNsSel = peer.namespaceSelector !== undefined
110
+ const hasIpBlock = peer.ipBlock !== undefined
111
+
112
+ if (hasPodSel && hasNsSel) {
113
+ // AND: both must match
114
+ const podLabels = formatSelector(peer.podSelector)
115
+ const nsLabels = formatSelector(peer.namespaceSelector)
116
+ return {
117
+ label: podLabels,
118
+ sublabel: `in ${nsLabels === 'all' ? 'any namespace' : nsLabels}`,
119
+ type: 'combined' as const,
120
+ }
121
+ }
122
+ if (hasPodSel) {
123
+ return { label: formatSelector(peer.podSelector), type: 'pod' as const }
124
+ }
125
+ if (hasNsSel) {
126
+ const nsLabels = formatSelector(peer.namespaceSelector)
127
+ return { label: nsLabels === 'all' ? 'All namespaces' : nsLabels, type: 'namespace' as const }
128
+ }
129
+ if (hasIpBlock) {
130
+ const cidr = peer.ipBlock.cidr || '?'
131
+ const except = peer.ipBlock.except
132
+ return {
133
+ label: cidr,
134
+ sublabel: except?.length ? `except ${except.join(', ')}` : undefined,
135
+ type: 'cidr' as const,
136
+ }
137
+ }
138
+ return { label: '?', type: 'all' as const }
139
+ })
140
+ }
141
+
142
+ function extractPorts(ports: any[] | undefined): string[] {
143
+ if (!ports || ports.length === 0) return []
144
+ return ports.map((p: any) => {
145
+ const proto = p.protocol || 'TCP'
146
+ const port = p.port || '*'
147
+ const endPort = p.endPort
148
+ return endPort ? `${proto}/${port}-${endPort}` : `${proto}/${port}`
149
+ })
150
+ }
151
+
152
+ function formatSelector(selector: any): string {
153
+ const labels = selector?.matchLabels || {}
154
+ if (Object.keys(labels).length === 0) return 'all'
155
+ return Object.entries(labels).map(([k, v]) => `${k}=${v}`).join(', ')
156
+ }
157
+
158
+ // Color coding for peer types
159
+ const PEER_STYLES = {
160
+ pod: 'border-emerald-500/30 bg-emerald-500/8',
161
+ namespace: 'border-sky-500/30 bg-sky-500/8',
162
+ cidr: 'border-amber-500/30 bg-amber-500/8',
163
+ combined: 'border-emerald-500/30 bg-emerald-500/8',
164
+ all: 'border-theme-border bg-theme-elevated/50',
165
+ deny: 'border-red-500/30 bg-red-500/8',
166
+ } as const
167
+
168
+ const PEER_DOT = {
169
+ pod: 'bg-emerald-500',
170
+ namespace: 'bg-sky-500',
171
+ cidr: 'bg-amber-500',
172
+ combined: 'bg-emerald-500',
173
+ all: 'bg-theme-text-tertiary',
174
+ deny: 'bg-red-500',
175
+ } as const
176
+
177
+ function FlowRow({
178
+ sources,
179
+ target,
180
+ ports,
181
+ direction,
182
+ denied = false,
183
+ }: {
184
+ sources: PeerInfo[]
185
+ target: string
186
+ ports: string[]
187
+ direction: 'ingress' | 'egress'
188
+ denied?: boolean
189
+ }) {
190
+ const isIngress = direction === 'ingress'
191
+
192
+ const sourceNodes = (
193
+ <div className="flex-1 min-w-0 space-y-1">
194
+ {sources.map((peer, i) => (
195
+ <div key={i}>
196
+ {i > 0 && (
197
+ <div className="text-[9px] text-theme-text-tertiary uppercase tracking-widest text-center my-0.5">or</div>
198
+ )}
199
+ <Tooltip content={peer.sublabel ? `${peer.label}\n${peer.sublabel}` : peer.label} position="top">
200
+ <div
201
+ className={clsx(
202
+ 'rounded-md border px-2 py-1.5',
203
+ PEER_STYLES[peer.type],
204
+ )}
205
+ >
206
+ <div className="flex items-center gap-1.5">
207
+ <span className={clsx('w-1.5 h-1.5 rounded-full shrink-0', PEER_DOT[peer.type])} />
208
+ <span className={clsx('text-[11px] font-medium truncate', denied && 'line-through text-red-400')}>
209
+ {peer.label}
210
+ </span>
211
+ </div>
212
+ {peer.sublabel && (
213
+ <div className="text-[10px] text-theme-text-tertiary ml-3 truncate">{peer.sublabel}</div>
214
+ )}
215
+ </div>
216
+ </Tooltip>
217
+ </div>
218
+ ))}
219
+ </div>
220
+ )
221
+
222
+ const targetNode = (
223
+ <Tooltip content={target} position="top">
224
+ <div className="rounded-md border border-indigo-500/30 bg-indigo-500/8 px-2 py-1.5 flex-1 min-w-0">
225
+ <div className="flex items-center gap-1.5">
226
+ <span className="w-1.5 h-1.5 rounded-full shrink-0 bg-indigo-500" />
227
+ <span className="text-[11px] font-medium truncate">{target}</span>
228
+ </div>
229
+ </div>
230
+ </Tooltip>
231
+ )
232
+
233
+ const arrow = (
234
+ <div className="flex flex-col items-center justify-center shrink-0 w-10">
235
+ {/* Arrow line with optional port label */}
236
+ <svg width="40" height="20" viewBox="0 0 40 20" className="overflow-visible">
237
+ <defs>
238
+ <marker id={`arrow-${direction}-${denied ? 'denied' : 'ok'}`} markerWidth="6" markerHeight="6" refX="5" refY="3" orient="auto">
239
+ <path d="M0,0 L6,3 L0,6" fill="none" stroke={denied ? '#ef4444' : direction === 'ingress' ? '#3b82f6' : '#a855f7'} strokeWidth="1.5" />
240
+ </marker>
241
+ </defs>
242
+ <line
243
+ x1="2" y1="10" x2="32" y2="10"
244
+ stroke={denied ? '#ef4444' : direction === 'ingress' ? '#3b82f6' : '#a855f7'}
245
+ strokeWidth="1.5"
246
+ strokeDasharray={denied ? '3 2' : undefined}
247
+ markerEnd={`url(#arrow-${direction}-${denied ? 'denied' : 'ok'})`}
248
+ />
249
+ </svg>
250
+ {ports.length > 0 && (
251
+ <div className="flex flex-wrap justify-center gap-0.5 mt-0.5">
252
+ {ports.map((p, i) => (
253
+ <span key={i} className="text-[8px] text-theme-text-tertiary font-mono leading-none">{p}</span>
254
+ ))}
255
+ </div>
256
+ )}
257
+ </div>
258
+ )
259
+
260
+ return (
261
+ <div className="flex items-center gap-1">
262
+ {isIngress ? (
263
+ <>
264
+ {sourceNodes}
265
+ {arrow}
266
+ {targetNode}
267
+ </>
268
+ ) : (
269
+ <>
270
+ {targetNode}
271
+ {arrow}
272
+ {sourceNodes}
273
+ </>
274
+ )}
275
+ </div>
276
+ )
277
+ }
@@ -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
- import { Section, PropertyList, Property } from '../../ui/drawer-components'
3
+ import { Section, LabelSelectorDisplay } from '../../ui/drawer-components'
4
+ import { NetworkPolicyDiagram } from './NetworkPolicyDiagram'
4
5
 
5
6
  interface NetworkPolicyRendererProps {
6
7
  data: any
@@ -13,35 +14,24 @@ export function NetworkPolicyRenderer({ data }: NetworkPolicyRendererProps) {
13
14
  const ingress: any[] | undefined = spec.ingress
14
15
  const egress: any[] | undefined = spec.egress
15
16
 
16
- const matchLabels = podSelector.matchLabels || {}
17
- const hasMatchLabels = Object.keys(matchLabels).length > 0
18
17
  const hasIngress = policyTypes.includes('Ingress')
19
18
  const hasEgress = policyTypes.includes('Egress')
20
19
 
20
+ const hasDiagramContent = hasIngress || hasEgress
21
+
21
22
  return (
22
23
  <>
24
+ {hasDiagramContent && (
25
+ <Section title="Policy Flow" icon={GitFork} defaultExpanded>
26
+ <NetworkPolicyDiagram spec={spec} />
27
+ </Section>
28
+ )}
29
+
23
30
  <Section title="Target" icon={Shield}>
24
- <PropertyList>
25
- <Property
26
- label="Pod Selector"
27
- value={hasMatchLabels ? undefined : 'All pods in namespace'}
28
- />
29
- </PropertyList>
30
- {hasMatchLabels && (
31
- <div className="mt-2">
32
- <div className="text-xs text-theme-text-tertiary mb-1">Pod Selector</div>
33
- <div className="flex flex-wrap gap-1">
34
- {Object.entries(matchLabels).map(([k, v]) => (
35
- <span
36
- key={k}
37
- className="badge bg-theme-elevated text-theme-text-secondary"
38
- >
39
- {k}={String(v)}
40
- </span>
41
- ))}
42
- </div>
43
- </div>
44
- )}
31
+ <div className="mt-2">
32
+ <div className="text-xs text-theme-text-tertiary mb-1">Pod Selector</div>
33
+ <LabelSelectorDisplay selector={podSelector} emptyText="All pods in namespace" />
34
+ </div>
45
35
  {policyTypes.length > 0 && (
46
36
  <div className="mt-2">
47
37
  <div className="text-xs text-theme-text-tertiary mb-1">Policy Types</div>
@@ -143,49 +133,19 @@ function IngressEgressRuleCard({
143
133
 
144
134
  function PeerEntry({ peer }: { peer: any }) {
145
135
  if (peer.podSelector) {
146
- const labels = peer.podSelector.matchLabels || {}
147
- const hasLabels = Object.keys(labels).length > 0
148
136
  return (
149
137
  <div className="text-sm">
150
138
  <span className="text-theme-text-secondary text-xs">podSelector: </span>
151
- {hasLabels ? (
152
- <span className="inline-flex flex-wrap gap-1 align-middle">
153
- {Object.entries(labels).map(([k, v]) => (
154
- <span
155
- key={k}
156
- className="badge bg-theme-elevated text-theme-text-secondary"
157
- >
158
- {k}={String(v)}
159
- </span>
160
- ))}
161
- </span>
162
- ) : (
163
- <span className="text-xs text-theme-text-tertiary">all pods</span>
164
- )}
139
+ <LabelSelectorDisplay selector={peer.podSelector} emptyText="all pods" inline />
165
140
  </div>
166
141
  )
167
142
  }
168
143
 
169
144
  if (peer.namespaceSelector) {
170
- const labels = peer.namespaceSelector.matchLabels || {}
171
- const hasLabels = Object.keys(labels).length > 0
172
145
  return (
173
146
  <div className="text-sm">
174
147
  <span className="text-theme-text-secondary text-xs">namespaceSelector: </span>
175
- {hasLabels ? (
176
- <span className="inline-flex flex-wrap gap-1 align-middle">
177
- {Object.entries(labels).map(([k, v]) => (
178
- <span
179
- key={k}
180
- className="badge bg-theme-elevated text-theme-text-secondary"
181
- >
182
- {k}={String(v)}
183
- </span>
184
- ))}
185
- </span>
186
- ) : (
187
- <span className="text-xs text-theme-text-tertiary">all namespaces</span>
188
- )}
148
+ <LabelSelectorDisplay selector={peer.namespaceSelector} emptyText="all namespaces" inline />
189
149
  </div>
190
150
  )
191
151
  }