@skyhook-io/k8s-ui 1.12.0 → 1.13.3

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 (117) hide show
  1. package/package.json +4 -4
  2. package/src/components/applications/ApplicationDetail.tsx +2 -2
  3. package/src/components/cluster-switcher/ClusterSwitcher.tsx +32 -7
  4. package/src/components/issues/severity.ts +1 -0
  5. package/src/components/resources/ResourcesView.empty-note.test.ts +41 -0
  6. package/src/components/resources/ResourcesView.tsx +419 -9
  7. package/src/components/resources/index.ts +27 -0
  8. package/src/components/resources/kyverno-legacy-action.test.ts +98 -0
  9. package/src/components/resources/renderers/AnalysisRunRenderer.test.tsx +241 -0
  10. package/src/components/resources/renderers/AnalysisRunRenderer.tsx +166 -0
  11. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +31 -2
  12. package/src/components/resources/renderers/CNPGClusterRenderer.test.tsx +33 -0
  13. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +47 -4
  14. package/src/components/resources/renderers/CNPGDeclarativeRenderer.tsx +243 -0
  15. package/src/components/resources/renderers/CNPGObjectStoreRenderer.test.tsx +99 -0
  16. package/src/components/resources/renderers/CNPGObjectStoreRenderer.tsx +235 -0
  17. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +23 -1
  18. package/src/components/resources/renderers/CNPGShared.tsx +27 -0
  19. package/src/components/resources/renderers/CalicoHostEndpointRenderer.tsx +71 -0
  20. package/src/components/resources/renderers/CalicoIPPoolRenderer.tsx +32 -0
  21. package/src/components/resources/renderers/CalicoNetworkPolicyDiagram.test.tsx +205 -0
  22. package/src/components/resources/renderers/CalicoNetworkPolicyDiagram.tsx +537 -0
  23. package/src/components/resources/renderers/CalicoNetworkPolicyRenderer.test.tsx +223 -0
  24. package/src/components/resources/renderers/CalicoNetworkPolicyRenderer.tsx +670 -0
  25. package/src/components/resources/renderers/CalicoTierRenderer.tsx +19 -0
  26. package/src/components/resources/renderers/KyvernoCELPolicyRenderers.tsx +13 -6
  27. package/src/components/resources/renderers/KyvernoExceptionRenderers.tsx +3 -2
  28. package/src/components/resources/renderers/KyvernoGlobalContextRenderer.test.ts +31 -0
  29. package/src/components/resources/renderers/KyvernoGlobalContextRenderer.tsx +124 -0
  30. package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +28 -2
  31. package/src/components/resources/renderers/KyvernoQueueRenderers.tsx +303 -0
  32. package/src/components/resources/renderers/LookupFailureNote.test.tsx +60 -0
  33. package/src/components/resources/renderers/LookupFailureNote.tsx +42 -0
  34. package/src/components/resources/renderers/NamespaceRenderer.test.ts +30 -0
  35. package/src/components/resources/renderers/NamespaceRenderer.tsx +7 -3
  36. package/src/components/resources/renderers/NetworkPolicyDiagram.tsx +27 -26
  37. package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +6 -3
  38. package/src/components/resources/renderers/PolicyCoverageSection.test.tsx +348 -0
  39. package/src/components/resources/renderers/PolicyCoverageSection.tsx +939 -0
  40. package/src/components/resources/renderers/PolicySection.test.tsx +59 -2
  41. package/src/components/resources/renderers/PolicySection.tsx +32 -5
  42. package/src/components/resources/renderers/RolloutRenderer.test.tsx +491 -0
  43. package/src/components/resources/renderers/RolloutRenderer.tsx +391 -92
  44. package/src/components/resources/renderers/VeleroBSLRenderer.test.tsx +374 -0
  45. package/src/components/resources/renderers/VeleroBSLRenderer.tsx +179 -3
  46. package/src/components/resources/renderers/VeleroBackupRenderer.test.tsx +221 -0
  47. package/src/components/resources/renderers/VeleroBackupRenderer.tsx +119 -6
  48. package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.test.tsx +69 -0
  49. package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.tsx +96 -0
  50. package/src/components/resources/renderers/VeleroRestoreRenderer.test.tsx +55 -0
  51. package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +22 -4
  52. package/src/components/resources/renderers/VeleroRunMessages.test.tsx +152 -0
  53. package/src/components/resources/renderers/VeleroRunMessages.tsx +139 -0
  54. package/src/components/resources/renderers/VeleroScheduleRenderer.test.tsx +88 -0
  55. package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +16 -6
  56. package/src/components/resources/renderers/calico-cells.tsx +177 -0
  57. package/src/components/resources/renderers/cnpg-cells.tsx +100 -0
  58. package/src/components/resources/renderers/index.ts +16 -1
  59. package/src/components/resources/renderers/kyverno-cells.tsx +81 -0
  60. package/src/components/resources/renderers/network-policy-peer-styles.ts +25 -0
  61. package/src/components/resources/renderers/velero-cells.tsx +29 -7
  62. package/src/components/resources/renderers/velero-dead-ends.test.tsx +142 -0
  63. package/src/components/resources/resource-utils-calico.test.ts +215 -0
  64. package/src/components/resources/resource-utils-calico.ts +261 -0
  65. package/src/components/resources/resource-utils-cnpg-declarative.test.ts +104 -0
  66. package/src/components/resources/resource-utils-cnpg-objectstore.test.ts +122 -0
  67. package/src/components/resources/resource-utils-cnpg.test.ts +54 -0
  68. package/src/components/resources/resource-utils-cnpg.ts +264 -3
  69. package/src/components/resources/resource-utils-kyverno-modern.ts +24 -0
  70. package/src/components/resources/resource-utils-kyverno-queue.test.ts +164 -0
  71. package/src/components/resources/resource-utils-kyverno-queue.ts +198 -0
  72. package/src/components/resources/resource-utils-kyverno.ts +43 -7
  73. package/src/components/resources/resource-utils-velero.test.ts +26 -0
  74. package/src/components/resources/resource-utils-velero.ts +68 -0
  75. package/src/components/resources/resource-utils.ts +98 -0
  76. package/src/components/shared/ResourceActionsBar.tsx +101 -16
  77. package/src/components/shared/ResourceRendererDispatch.test.tsx +379 -3
  78. package/src/components/shared/ResourceRendererDispatch.tsx +199 -22
  79. package/src/components/shared/index.ts +1 -1
  80. package/src/components/shared/revision-role-badges.test.ts +120 -0
  81. package/src/components/topology/K8sResourceNode.test.ts +10 -0
  82. package/src/components/topology/K8sResourceNode.tsx +22 -0
  83. package/src/components/topology/TopologyFilterSidebar.test.tsx +31 -0
  84. package/src/components/topology/TopologyFilterSidebar.tsx +5 -0
  85. package/src/components/topology/TopologyGraph.tsx +6 -6
  86. package/src/components/topology/TopologySearch.tsx +7 -0
  87. package/src/components/topology/layout.ts +3 -0
  88. package/src/components/trace/ReachabilityView.tsx +1 -0
  89. package/src/components/trace/TracePanel.tsx +13 -2
  90. package/src/components/trace/reachFixtures.ts +26 -1
  91. package/src/components/trace/reachGraphModel.test.ts +20 -0
  92. package/src/components/trace/reachGraphModel.ts +8 -1
  93. package/src/components/trace/reachOrigins.test.ts +31 -0
  94. package/src/components/trace/reachOrigins.ts +11 -1
  95. package/src/components/ui/Badge.tsx +10 -0
  96. package/src/components/ui/InClusterConsentDialog.tsx +5 -4
  97. package/src/components/ui/ScheduleValue.test.tsx +80 -0
  98. package/src/components/ui/ScheduleValue.tsx +85 -0
  99. package/src/components/ui/SelectMenu.tsx +202 -28
  100. package/src/components/ui/drawer-components.test.tsx +17 -1
  101. package/src/components/ui/drawer-components.tsx +9 -2
  102. package/src/components/ui/index.ts +2 -0
  103. package/src/components/workload/WorkloadView.tsx +5 -2
  104. package/src/types/core.ts +36 -4
  105. package/src/types/policy.ts +153 -2
  106. package/src/utils/clipboard.test.ts +86 -0
  107. package/src/utils/clipboard.ts +50 -0
  108. package/src/utils/extended-resources.ts +4 -7
  109. package/src/utils/format.test.ts +83 -1
  110. package/src/utils/format.ts +80 -1
  111. package/src/utils/inClusterConsent.test.ts +9 -0
  112. package/src/utils/inClusterConsent.ts +9 -9
  113. package/src/utils/index.ts +1 -0
  114. package/src/utils/navigation.test.ts +5 -0
  115. package/src/utils/navigation.ts +5 -0
  116. package/src/utils/resource-icons.test.ts +49 -2
  117. package/src/utils/resource-icons.ts +29 -2
@@ -0,0 +1,537 @@
1
+ import { useId } from 'react'
2
+ import { clsx } from 'clsx'
3
+ import { Badge, type BadgeSeverity } from '../../ui/Badge'
4
+ import { Tooltip } from '../../ui/Tooltip'
5
+ import {
6
+ formatKubernetesLabelSelector,
7
+ getCalicoPolicyTypes,
8
+ } from '../resource-utils-calico'
9
+ import {
10
+ NETWORK_POLICY_PEER_DOTS,
11
+ NETWORK_POLICY_PEER_STYLES,
12
+ type NetworkPolicyPeerType,
13
+ } from './network-policy-peer-styles'
14
+
15
+ interface CalicoNetworkPolicyDiagramProps {
16
+ spec: any
17
+ staged?: boolean
18
+ }
19
+
20
+ type Direction = 'ingress' | 'egress'
21
+
22
+ interface EntityField {
23
+ label: string
24
+ values: string[]
25
+ negative?: boolean
26
+ }
27
+
28
+ const ACTION_SEVERITY: Record<string, BadgeSeverity> = {
29
+ allow: 'success',
30
+ deny: 'error',
31
+ log: 'warning',
32
+ pass: 'info',
33
+ }
34
+
35
+ export function CalicoNetworkPolicyDiagram({
36
+ spec,
37
+ staged = false,
38
+ }: CalicoNetworkPolicyDiagramProps) {
39
+ const policyTypes = getCalicoPolicyTypes({ spec }).map((type) =>
40
+ type.toLowerCase(),
41
+ )
42
+ const directions = [
43
+ {
44
+ direction: 'ingress' as const,
45
+ label: 'Ingress',
46
+ enabled:
47
+ policyTypes.length > 0
48
+ ? policyTypes.includes('ingress')
49
+ : Array.isArray(spec?.ingress),
50
+ },
51
+ {
52
+ direction: 'egress' as const,
53
+ label: 'Egress',
54
+ enabled:
55
+ policyTypes.length > 0
56
+ ? policyTypes.includes('egress')
57
+ : Array.isArray(spec?.egress),
58
+ },
59
+ ].filter((entry) => entry.enabled)
60
+ const target = {
61
+ selector: spec?.selector,
62
+ namespaceSelector: spec?.namespaceSelector,
63
+ serviceAccountSelector: spec?.serviceAccountSelector,
64
+ }
65
+
66
+ return (
67
+ <div className="card-inner-lg space-y-3">
68
+ {staged && (
69
+ <div className="flex items-center gap-2 text-[11px] text-theme-text-tertiary">
70
+ <Badge severity="warning" size="sm">
71
+ Staged preview
72
+ </Badge>
73
+ <span>Dashed paths are evaluated but not enforced</span>
74
+ </div>
75
+ )}
76
+
77
+ {directions.map(({ direction, label }) => (
78
+ <DirectionSection
79
+ key={direction}
80
+ direction={direction}
81
+ label={label}
82
+ rules={Array.isArray(spec?.[direction]) ? spec[direction] : []}
83
+ target={target}
84
+ staged={staged}
85
+ />
86
+ ))}
87
+
88
+ {directions.length === 0 && (
89
+ <div className="py-2 text-center text-xs text-theme-text-tertiary">
90
+ No explicit ingress or egress rules
91
+ </div>
92
+ )}
93
+ </div>
94
+ )
95
+ }
96
+
97
+ function DirectionSection({
98
+ direction,
99
+ label,
100
+ rules,
101
+ target,
102
+ staged,
103
+ }: {
104
+ direction: Direction
105
+ label: string
106
+ rules: any[]
107
+ target: any
108
+ staged: boolean
109
+ }) {
110
+ return (
111
+ <div className="space-y-2">
112
+ <div className="flex items-center gap-1.5">
113
+ <span
114
+ className={clsx(
115
+ 'h-1.5 w-1.5 rounded-full',
116
+ direction === 'ingress' ? 'bg-blue-500' : 'bg-purple-500',
117
+ )}
118
+ />
119
+ <span
120
+ className={clsx(
121
+ 'text-[10px] font-semibold uppercase tracking-wider',
122
+ direction === 'ingress' ? 'text-blue-400' : 'text-purple-400',
123
+ )}
124
+ >
125
+ {label}
126
+ </span>
127
+ </div>
128
+
129
+ {rules.length === 0 ? (
130
+ <div className="rounded-md border border-theme-border-subtle px-2 py-2 text-xs text-theme-text-tertiary">
131
+ No explicit {direction} rules
132
+ </div>
133
+ ) : (
134
+ <div className="divide-y divide-theme-border-subtle rounded-md border border-theme-border-subtle bg-theme-base/40">
135
+ {rules.map((rule, index) => (
136
+ <FlowRow
137
+ key={index}
138
+ direction={direction}
139
+ rule={rule}
140
+ target={target}
141
+ staged={staged}
142
+ />
143
+ ))}
144
+ </div>
145
+ )}
146
+ </div>
147
+ )
148
+ }
149
+
150
+ function FlowRow({
151
+ direction,
152
+ rule,
153
+ target,
154
+ staged,
155
+ }: {
156
+ direction: Direction
157
+ rule: any
158
+ target: any
159
+ staged: boolean
160
+ }) {
161
+ const action = String(rule?.action ?? 'Allow')
162
+ const protocol = rule?.protocol
163
+ const notProtocol = rule?.notProtocol
164
+ const isIngress = direction === 'ingress'
165
+ const constraints = pathConstraints(rule)
166
+ const flowConstraints =
167
+ constraints.length > 0
168
+ ? constraints
169
+ : protocol !== undefined
170
+ ? [`${formatProtocol(protocol).toUpperCase()}/*`]
171
+ : []
172
+ const leftEntities = isIngress ? [rule?.source] : [target, rule?.source]
173
+ const rightEntities = isIngress
174
+ ? [target, rule?.destination]
175
+ : [rule?.destination]
176
+
177
+ return (
178
+ <div className="space-y-2 px-2 py-2">
179
+ <div className="flex flex-wrap items-center gap-1.5">
180
+ <Badge
181
+ severity={ACTION_SEVERITY[action.toLowerCase()] ?? 'neutral'}
182
+ size="sm"
183
+ >
184
+ {action}
185
+ </Badge>
186
+ {notProtocol !== undefined && (
187
+ <Badge tone="note" size="sm">
188
+ not {formatProtocol(notProtocol)}
189
+ </Badge>
190
+ )}
191
+ </div>
192
+
193
+ <div className="grid grid-cols-[minmax(0,1fr)_3.5rem_minmax(0,1fr)] items-center gap-1">
194
+ <Endpoint
195
+ entities={leftEntities}
196
+ emptyText={isIngress ? 'Any source' : 'All workloads'}
197
+ target={!isIngress}
198
+ />
199
+ <FlowArrow
200
+ action={action}
201
+ direction={direction}
202
+ staged={staged}
203
+ constraints={flowConstraints}
204
+ />
205
+ <Endpoint
206
+ entities={rightEntities}
207
+ emptyText={isIngress ? 'All workloads' : 'Any destination'}
208
+ target={isIngress}
209
+ />
210
+ </div>
211
+ </div>
212
+ )
213
+ }
214
+
215
+ function FlowArrow({
216
+ action,
217
+ direction,
218
+ staged,
219
+ constraints,
220
+ }: {
221
+ action: string
222
+ direction: Direction
223
+ staged: boolean
224
+ constraints: string[]
225
+ }) {
226
+ const markerId = useId().replace(/:/g, '')
227
+ const color = arrowColor(action, direction)
228
+ const dashed = staged || action.toLowerCase() !== 'allow'
229
+
230
+ return (
231
+ <div className="flex min-w-0 flex-col items-center justify-center">
232
+ <svg
233
+ width="48"
234
+ height="18"
235
+ viewBox="0 0 48 18"
236
+ className="overflow-visible"
237
+ aria-hidden="true"
238
+ >
239
+ <defs>
240
+ <marker
241
+ id={markerId}
242
+ markerWidth="6"
243
+ markerHeight="6"
244
+ refX="5"
245
+ refY="3"
246
+ orient="auto"
247
+ >
248
+ <path
249
+ d="M0,0 L6,3 L0,6"
250
+ fill="none"
251
+ stroke={color}
252
+ strokeWidth="1.5"
253
+ />
254
+ </marker>
255
+ </defs>
256
+ <line
257
+ x1="2"
258
+ y1="9"
259
+ x2="40"
260
+ y2="9"
261
+ stroke={color}
262
+ strokeWidth="1.5"
263
+ strokeDasharray={dashed ? '4 3' : undefined}
264
+ markerEnd={`url(#${markerId})`}
265
+ />
266
+ </svg>
267
+ {constraints.length > 0 && (
268
+ <Tooltip content={constraints.join('\n')} position="top">
269
+ <span className="flex flex-col items-center gap-0.5 font-mono text-[8px] leading-none text-theme-text-tertiary">
270
+ {constraints.map((constraint, index) => (
271
+ <span
272
+ key={`${constraint}-${index}`}
273
+ className="whitespace-nowrap"
274
+ >
275
+ {constraint}
276
+ </span>
277
+ ))}
278
+ </span>
279
+ </Tooltip>
280
+ )}
281
+ </div>
282
+ )
283
+ }
284
+
285
+ function Endpoint({
286
+ entities,
287
+ emptyText,
288
+ target = false,
289
+ }: {
290
+ entities: any[]
291
+ emptyText: string
292
+ target?: boolean
293
+ }) {
294
+ const fields = entities.flatMap(entityFields)
295
+ const peerType = classifyPeer(fields)
296
+ const tooltip = fields.length
297
+ ? fields
298
+ .map((field) => `${field.label}: ${field.values.join(', ')}`)
299
+ .join('\n')
300
+ : emptyText
301
+
302
+ return (
303
+ <Tooltip content={tooltip} position="top">
304
+ <div
305
+ className={clsx(
306
+ 'min-w-0 overflow-hidden rounded-md border px-2 py-1.5',
307
+ target
308
+ ? 'border-indigo-500/30 bg-indigo-500/8'
309
+ : NETWORK_POLICY_PEER_STYLES[peerType],
310
+ )}
311
+ >
312
+ <div className="flex min-w-0 items-start gap-1.5">
313
+ <span
314
+ className={clsx(
315
+ 'mt-1 h-1.5 w-1.5 shrink-0 rounded-full',
316
+ target ? 'bg-indigo-500' : NETWORK_POLICY_PEER_DOTS[peerType],
317
+ )}
318
+ />
319
+ <div className="min-w-0 flex-1">
320
+ {fields.length > 0 ? (
321
+ <EntityFields fields={fields} />
322
+ ) : (
323
+ <div className="truncate text-[11px] font-medium">
324
+ {emptyText}
325
+ </div>
326
+ )}
327
+ </div>
328
+ </div>
329
+ </div>
330
+ </Tooltip>
331
+ )
332
+ }
333
+
334
+ function EntityFields({ fields }: { fields: EntityField[] }) {
335
+ return (
336
+ <div className="min-w-0 space-y-0.5">
337
+ {fields.map((field, index) => (
338
+ <div
339
+ key={`${field.label}-${index}`}
340
+ className="flex min-w-0 items-baseline gap-1 text-[9px]"
341
+ >
342
+ <span
343
+ className={clsx(
344
+ 'shrink-0 text-theme-text-tertiary',
345
+ field.negative && 'text-warning-text',
346
+ )}
347
+ >
348
+ {field.label}
349
+ </span>
350
+ <span
351
+ className={clsx(
352
+ 'min-w-0 truncate font-mono text-[10px] text-theme-text-primary',
353
+ field.negative && 'line-through',
354
+ )}
355
+ >
356
+ {field.values.join(', ')}
357
+ </span>
358
+ </div>
359
+ ))}
360
+ </div>
361
+ )
362
+ }
363
+
364
+ function classifyPeer(fields: EntityField[]): NetworkPolicyPeerType {
365
+ const labels = new Set(fields.map((field) => field.label))
366
+ const hasPodMatch = [
367
+ 'Selector',
368
+ 'Pod selector',
369
+ 'Not selector',
370
+ 'Service account',
371
+ 'Service accounts',
372
+ 'Not service accounts',
373
+ ].some((label) => labels.has(label))
374
+ const hasNamespaceMatch = ['Namespace', 'Not namespace'].some((label) =>
375
+ labels.has(label),
376
+ )
377
+ const hasNetworkMatch = ['Nets', 'Not Nets', 'IP block'].some((label) =>
378
+ labels.has(label),
379
+ )
380
+
381
+ if (hasPodMatch && hasNamespaceMatch) return 'combined'
382
+ if (hasPodMatch) return 'pod'
383
+ if (hasNamespaceMatch) return 'namespace'
384
+ if (hasNetworkMatch) return 'cidr'
385
+ return 'all'
386
+ }
387
+
388
+ function entityFields(entity: any): EntityField[] {
389
+ if (!entity || typeof entity !== 'object') return []
390
+
391
+ const fields: EntityField[] = []
392
+ addField(fields, 'Selector', entity.selector)
393
+ addField(
394
+ fields,
395
+ 'Pod selector',
396
+ entity.podSelector === undefined
397
+ ? undefined
398
+ : formatKubernetesLabelSelector(entity.podSelector),
399
+ )
400
+ addField(fields, 'Not selector', entity.notSelector, true)
401
+ addField(
402
+ fields,
403
+ 'Namespace',
404
+ entity.namespaceSelector && typeof entity.namespaceSelector === 'object'
405
+ ? formatKubernetesLabelSelector(entity.namespaceSelector)
406
+ : entity.namespaceSelector,
407
+ )
408
+ addField(fields, 'Not namespace', entity.notNamespaceSelector, true)
409
+ addField(fields, 'Service account', entity.serviceAccountSelector)
410
+ addField(fields, 'Nets', entity.nets)
411
+ addField(fields, 'IP block', entity.ipBlock)
412
+ addField(fields, 'Not Nets', entity.notNets, true)
413
+ addField(
414
+ fields,
415
+ 'Service accounts',
416
+ serviceAccountValues(entity.serviceAccounts),
417
+ )
418
+ addField(
419
+ fields,
420
+ 'Not service accounts',
421
+ serviceAccountValues(entity.notServiceAccounts),
422
+ true,
423
+ )
424
+ return fields
425
+ }
426
+
427
+ function addField(
428
+ fields: EntityField[],
429
+ label: string,
430
+ value: any,
431
+ negative = false,
432
+ ) {
433
+ const values = valuesFor(value)
434
+ .map(formatValue)
435
+ .filter((item) => item !== '')
436
+ if (values.length === 0) return
437
+ fields.push({ label, values, negative })
438
+ }
439
+
440
+ function valuesFor(value: any): any[] {
441
+ if (value === undefined || value === null || value === '') return []
442
+ return Array.isArray(value) ? value : [value]
443
+ }
444
+
445
+ function serviceAccountValues(value: any): string[] {
446
+ if (!value || typeof value !== 'object') return []
447
+ return [
448
+ ...(Array.isArray(value.names) ? value.names.map(String) : []),
449
+ ...(value.selector !== undefined
450
+ ? [`selector: ${String(value.selector)}`]
451
+ : []),
452
+ ]
453
+ }
454
+
455
+ function pathConstraints(rule: any): string[] {
456
+ const sourcePorts = valuesFor(rule?.source?.ports).map((port) =>
457
+ formatPort(port, rule?.protocol),
458
+ )
459
+ const notSourcePorts = valuesFor(rule?.source?.notPorts).map((port) =>
460
+ formatPort(port, rule?.protocol),
461
+ )
462
+ const destinationPorts = valuesFor(rule?.destination?.ports).map((port) =>
463
+ formatPort(port, rule?.protocol),
464
+ )
465
+ const notDestinationPorts = valuesFor(rule?.destination?.notPorts).map(
466
+ (port) => formatPort(port, rule?.protocol),
467
+ )
468
+ return [
469
+ ...sourcePorts.map((port) => `src:${port}`),
470
+ ...notSourcePorts.map((port) => `src:!${port}`),
471
+ ...destinationPorts,
472
+ ...notDestinationPorts.map((port) => `!${port}`),
473
+ ]
474
+ }
475
+
476
+ function arrowColor(action: string, direction: Direction): string {
477
+ switch (action.toLowerCase()) {
478
+ case 'deny':
479
+ return '#ef4444'
480
+ case 'log':
481
+ return '#f59e0b'
482
+ case 'pass':
483
+ return '#3b82f6'
484
+ default:
485
+ return direction === 'ingress' ? '#3b82f6' : '#a855f7'
486
+ }
487
+ }
488
+
489
+ function formatValue(value: any): string {
490
+ if (value && typeof value === 'object') {
491
+ if ('port' in value) {
492
+ const protocol = value.protocol ? `${value.protocol}/` : ''
493
+ const endPort = value.endPort !== undefined ? `-${value.endPort}` : ''
494
+ return `${protocol}${value.port}${endPort}`
495
+ }
496
+ if ('start' in value || 'end' in value) {
497
+ return `${value.start ?? ''}-${value.end ?? ''}`
498
+ }
499
+ if ('strVal' in value) return String(value.strVal)
500
+ if ('intVal' in value) return String(value.intVal)
501
+ return JSON.stringify(value)
502
+ }
503
+ return String(value)
504
+ }
505
+
506
+ function formatPort(value: any, ruleProtocol?: any): string {
507
+ if (value && typeof value === 'object') {
508
+ const protocol = value.protocol
509
+ ? formatProtocol(value.protocol).toUpperCase()
510
+ : ruleProtocol !== undefined
511
+ ? formatProtocol(ruleProtocol).toUpperCase()
512
+ : 'TCP'
513
+ if ('port' in value) {
514
+ const endPort = value.endPort !== undefined ? `-${value.endPort}` : ''
515
+ return `${protocol}/${value.port}${endPort}`
516
+ }
517
+ if ('start' in value || 'end' in value) {
518
+ return `${protocol}/${value.start ?? ''}-${value.end ?? ''}`
519
+ }
520
+ if ('strVal' in value) return `${protocol}/${value.strVal}`
521
+ if ('intVal' in value) return `${protocol}/${value.intVal}`
522
+ }
523
+ const protocol =
524
+ ruleProtocol !== undefined
525
+ ? formatProtocol(ruleProtocol).toUpperCase()
526
+ : 'TCP'
527
+ return `${protocol}/${formatValue(value)}`
528
+ }
529
+
530
+ function formatProtocol(value: any): string {
531
+ if (value && typeof value === 'object') {
532
+ if (value.strVal !== undefined) return String(value.strVal)
533
+ if (value.intVal !== undefined) return String(value.intVal)
534
+ return JSON.stringify(value)
535
+ }
536
+ return String(value)
537
+ }