@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,670 @@
1
+ import {
2
+ ArrowDownToLine,
3
+ ArrowUpFromLine,
4
+ GitFork,
5
+ ShieldCheck,
6
+ } from 'lucide-react'
7
+ import { Badge, type BadgeSeverity } from '../../ui/Badge'
8
+ import {
9
+ Property,
10
+ PropertyList,
11
+ ResourceRefBadge,
12
+ Section,
13
+ } from '../../ui/drawer-components'
14
+ import type { ResourceRef } from '../../../types'
15
+ import {
16
+ getCalicoTierRef,
17
+ getCalicoPolicyKindLabel,
18
+ getCalicoPolicySelector,
19
+ getCalicoPolicyTypes,
20
+ formatKubernetesLabelSelector,
21
+ isCalicoStagedDeletion,
22
+ isCalicoStagedIgnored,
23
+ } from '../resource-utils-calico'
24
+ import { CalicoNetworkPolicyDiagram } from './CalicoNetworkPolicyDiagram'
25
+
26
+ interface CalicoNetworkPolicyRendererProps {
27
+ data: any
28
+ onNavigate?: (ref: ResourceRef) => void
29
+ }
30
+
31
+ const ACTION_SEVERITY: Record<string, BadgeSeverity> = {
32
+ allow: 'success',
33
+ deny: 'error',
34
+ log: 'warning',
35
+ pass: 'info',
36
+ }
37
+
38
+ export function CalicoNetworkPolicyRenderer({
39
+ data,
40
+ onNavigate,
41
+ }: CalicoNetworkPolicyRendererProps) {
42
+ const spec = data?.spec ?? {}
43
+ const kindLabel = getCalicoPolicyKindLabel(data?.kind)
44
+ const staged = kindLabel.includes('Staged')
45
+ const ingress = Array.isArray(spec.ingress) ? spec.ingress : undefined
46
+ const egress = Array.isArray(spec.egress) ? spec.egress : undefined
47
+ const types = getCalicoPolicyTypes(data)
48
+ const tierRef = getCalicoTierRef(data)
49
+ // A staged deletion has no selector and no rules to draw — it stages the
50
+ // removal of the enforced policy with the same name. An ignored staged policy
51
+ // has rules, but Calico skips it, so they are not a preview of anything.
52
+ const stagedDeletion = isCalicoStagedDeletion(data)
53
+ const stagedIgnored = isCalicoStagedIgnored(data)
54
+
55
+ return (
56
+ <>
57
+ <Section title="Policy Flow" icon={GitFork} defaultExpanded>
58
+ {stagedDeletion || stagedIgnored ? (
59
+ <div className="card-inner-lg flex flex-col gap-1.5">
60
+ <div className="flex items-center gap-2 text-[11px] text-theme-text-tertiary">
61
+ <Badge severity="warning" size="sm">
62
+ {stagedDeletion ? 'Staged deletion' : 'Staged, ignored'}
63
+ </Badge>
64
+ <span>Nothing is previewed as protected</span>
65
+ </div>
66
+ <span className="text-xs text-theme-text-secondary">
67
+ {stagedDeletion ? (
68
+ <>
69
+ Promoting this removes {data?.metadata?.name ?? 'the policy'}{' '}
70
+ from the {String(spec.tier ?? 'default')} tier. Any workload
71
+ protected only by that policy loses its coverage.
72
+ </>
73
+ ) : (
74
+ <>
75
+ Calico skips a staged policy whose action is Ignore, so
76
+ promoting the staged set changes nothing here.
77
+ </>
78
+ )}
79
+ </span>
80
+ </div>
81
+ ) : (
82
+ <CalicoNetworkPolicyDiagram spec={spec} staged={staged} />
83
+ )}
84
+ </Section>
85
+
86
+ <Section title="Policy" icon={ShieldCheck}>
87
+ <div className="space-y-3">
88
+ <Badge kind={kindLabel}>{kindLabel}</Badge>
89
+ <PropertyList>
90
+ <Property
91
+ label="Selector"
92
+ value={
93
+ <SelectorValue
94
+ value={stagedDeletion ? '' : getCalicoPolicySelector(data)}
95
+ emptyText={
96
+ stagedDeletion ? 'not applicable' : 'all workloads'
97
+ }
98
+ />
99
+ }
100
+ />
101
+ {spec.namespaceSelector !== undefined && (
102
+ <Property
103
+ label="Namespace Selector"
104
+ value={
105
+ <SelectorValue
106
+ value={spec.namespaceSelector}
107
+ emptyText="all namespaces"
108
+ />
109
+ }
110
+ />
111
+ )}
112
+ {spec.serviceAccountSelector !== undefined && (
113
+ <Property
114
+ label="Service Account Selector"
115
+ value={
116
+ <SelectorValue
117
+ value={spec.serviceAccountSelector}
118
+ emptyText="all service accounts"
119
+ />
120
+ }
121
+ />
122
+ )}
123
+ <Property
124
+ label="Tier"
125
+ value={
126
+ <ResourceRefBadge resourceRef={tierRef} onClick={onNavigate} />
127
+ }
128
+ />
129
+ {spec.order !== undefined && (
130
+ <Property label="Order" value={spec.order} />
131
+ )}
132
+ {types.length > 0 && (
133
+ <Property label="Types" value={<BadgeList values={types} />} />
134
+ )}
135
+ {staged && spec.stagedAction !== undefined && (
136
+ <Property
137
+ label="Staged Action"
138
+ value={<Badge tone="note">{String(spec.stagedAction)}</Badge>}
139
+ />
140
+ )}
141
+ {spec.preDNAT !== undefined && (
142
+ <Property label="Pre-DNAT" value={yesNo(spec.preDNAT)} />
143
+ )}
144
+ {spec.applyOnForward !== undefined && (
145
+ <Property
146
+ label="Apply On Forward"
147
+ value={yesNo(spec.applyOnForward)}
148
+ />
149
+ )}
150
+ {spec.doNotTrack !== undefined && (
151
+ <Property label="Do Not Track" value={yesNo(spec.doNotTrack)} />
152
+ )}
153
+ {Array.isArray(spec.performanceHints) &&
154
+ spec.performanceHints.length > 0 && (
155
+ <Property
156
+ label="Performance Hints"
157
+ value={
158
+ <BadgeList values={spec.performanceHints} tone="note" />
159
+ }
160
+ />
161
+ )}
162
+ </PropertyList>
163
+ </div>
164
+ </Section>
165
+
166
+ {ingress && (
167
+ <RuleSection
168
+ title={`Ingress Rules (${ingress.length})`}
169
+ icon={ArrowDownToLine}
170
+ rules={ingress}
171
+ direction="ingress"
172
+ />
173
+ )}
174
+ {egress && (
175
+ <RuleSection
176
+ title={`Egress Rules (${egress.length})`}
177
+ icon={ArrowUpFromLine}
178
+ rules={egress}
179
+ direction="egress"
180
+ />
181
+ )}
182
+ </>
183
+ )
184
+ }
185
+
186
+ function RuleSection({
187
+ title,
188
+ icon: Icon,
189
+ rules,
190
+ direction,
191
+ }: {
192
+ title: string
193
+ icon: React.ComponentType<{ className?: string }>
194
+ rules: any[]
195
+ direction: 'ingress' | 'egress'
196
+ }) {
197
+ return (
198
+ <Section title={title} icon={Icon} defaultExpanded>
199
+ {rules.length === 0 ? (
200
+ <div className="text-sm text-theme-text-tertiary">No rules</div>
201
+ ) : (
202
+ <div className="space-y-3">
203
+ {rules.map((rule, index) => (
204
+ <CalicoRuleCard key={index} rule={rule} direction={direction} />
205
+ ))}
206
+ </div>
207
+ )}
208
+ </Section>
209
+ )
210
+ }
211
+
212
+ function CalicoRuleCard({
213
+ rule,
214
+ direction,
215
+ }: {
216
+ rule: any
217
+ direction: 'ingress' | 'egress'
218
+ }) {
219
+ const action = String(rule?.action ?? 'Allow')
220
+ const protocol = rule?.protocol
221
+ const notProtocol = rule?.notProtocol
222
+ const hasPorts = hasCalicoPortConstraints(rule)
223
+ const primaryEntity =
224
+ direction === 'ingress' ? rule?.source : rule?.destination
225
+ const secondaryEntity =
226
+ direction === 'ingress' ? rule?.destination : rule?.source
227
+ const primaryLabel = direction === 'ingress' ? 'Source' : 'Destination'
228
+ const secondaryLabel = direction === 'ingress' ? 'Destination' : 'Source'
229
+
230
+ return (
231
+ <div className="card-inner-lg space-y-3">
232
+ <div className="mb-2 flex flex-wrap items-center gap-2">
233
+ <Badge severity={ACTION_SEVERITY[action.toLowerCase()] ?? 'neutral'}>
234
+ {action}
235
+ </Badge>
236
+ {protocol !== undefined && !hasPorts && (
237
+ <Badge protocol={formatProtocol(protocol)} size="sm">
238
+ {formatProtocol(protocol)}
239
+ </Badge>
240
+ )}
241
+ {notProtocol !== undefined && (
242
+ <Badge tone="note" size="sm">
243
+ not {formatProtocol(notProtocol)}
244
+ </Badge>
245
+ )}
246
+ </div>
247
+
248
+ <EntityRuleBlock label={primaryLabel} entity={primaryEntity} />
249
+ <EntityRuleBlock label={secondaryLabel} entity={secondaryEntity} />
250
+ <PortBlock
251
+ label="Source Ports"
252
+ entity={rule?.source}
253
+ ruleProtocol={protocol}
254
+ />
255
+ <PortBlock
256
+ label="Ports"
257
+ entity={rule?.destination}
258
+ ruleProtocol={protocol}
259
+ />
260
+
261
+ {(rule?.http !== undefined ||
262
+ rule?.icmp !== undefined ||
263
+ rule?.notICMP !== undefined) && (
264
+ <div className="space-y-2">
265
+ {rule.http !== undefined && <HTTPMatch value={rule.http} />}
266
+ {rule.icmp !== undefined && (
267
+ <ICMPMatch label="ICMP" value={rule.icmp} />
268
+ )}
269
+ {rule.notICMP !== undefined && (
270
+ <ICMPMatch label="Not ICMP" value={rule.notICMP} negative />
271
+ )}
272
+ </div>
273
+ )}
274
+ </div>
275
+ )
276
+ }
277
+
278
+ interface EntityRuleField {
279
+ label: string
280
+ value: React.ReactNode
281
+ negative?: boolean
282
+ }
283
+
284
+ function EntityRuleBlock({ label, entity }: { label: string; entity: any }) {
285
+ const peers = (Array.isArray(entity) ? entity : [entity])
286
+ .map(entityFields)
287
+ .filter((fields) => fields.length > 0)
288
+ if (peers.length === 0) return null
289
+
290
+ return (
291
+ <div className="mb-2">
292
+ <div className="text-xs text-theme-text-tertiary mb-1">{label}</div>
293
+ <div className="space-y-1.5">
294
+ {peers.map((fields, peerIndex) => (
295
+ <div key={peerIndex} className="space-y-0.5 text-sm">
296
+ {fields.map((field, fieldIndex) => (
297
+ <div
298
+ key={`${field.label}-${fieldIndex}`}
299
+ className="flex flex-wrap items-baseline gap-1"
300
+ >
301
+ <span
302
+ className={
303
+ field.negative
304
+ ? 'text-warning-text text-xs shrink-0'
305
+ : 'text-theme-text-secondary text-xs shrink-0'
306
+ }
307
+ >
308
+ {field.label}:
309
+ </span>
310
+ <div className="flex flex-wrap gap-1 min-w-0">
311
+ {field.value}
312
+ </div>
313
+ </div>
314
+ ))}
315
+ </div>
316
+ ))}
317
+ </div>
318
+ </div>
319
+ )
320
+ }
321
+
322
+ function entityFields(entity: any): EntityRuleField[] {
323
+ if (!entity || typeof entity !== 'object') return []
324
+
325
+ const fields: EntityRuleField[] = []
326
+ addEntityField(
327
+ fields,
328
+ 'Pod Selector',
329
+ entity.podSelector === undefined
330
+ ? undefined
331
+ : formatKubernetesLabelSelector(entity.podSelector),
332
+ )
333
+ addEntityField(fields, 'Selector', entity.selector)
334
+ addEntityField(fields, 'Not Selector', entity.notSelector, true)
335
+ addEntityField(
336
+ fields,
337
+ 'Namespace Selector',
338
+ entity.namespaceSelector && typeof entity.namespaceSelector === 'object'
339
+ ? formatKubernetesLabelSelector(entity.namespaceSelector)
340
+ : entity.namespaceSelector,
341
+ )
342
+ addEntityField(fields, 'IP Block', entity.ipBlock)
343
+ addEntityField(
344
+ fields,
345
+ 'Not Namespace Selector',
346
+ entity.notNamespaceSelector,
347
+ true,
348
+ )
349
+ addEntityField(fields, 'Nets', entity.nets)
350
+ addEntityField(fields, 'Not Nets', entity.notNets, true)
351
+ addEntityField(fields, 'Service Accounts', entity.serviceAccounts)
352
+ addEntityField(
353
+ fields,
354
+ 'Not Service Accounts',
355
+ entity.notServiceAccounts,
356
+ true,
357
+ )
358
+ return fields
359
+ }
360
+
361
+ function addEntityField(
362
+ fields: EntityRuleField[],
363
+ label: string,
364
+ value: any,
365
+ negative = false,
366
+ ) {
367
+ if (
368
+ value === undefined ||
369
+ value === null ||
370
+ value === '' ||
371
+ (Array.isArray(value) && value.length === 0)
372
+ )
373
+ return
374
+ fields.push({ label, value: renderEntityValue(value, negative), negative })
375
+ }
376
+
377
+ function PortBlock({
378
+ label,
379
+ entity,
380
+ ruleProtocol,
381
+ }: {
382
+ label: string
383
+ entity: any
384
+ ruleProtocol: any
385
+ }) {
386
+ const entries = [
387
+ ...valuesFor(entity?.ports).map((port) => ({
388
+ value: formatPort(port, ruleProtocol),
389
+ negative: false,
390
+ })),
391
+ ...valuesFor(entity?.notPorts).map((port) => ({
392
+ value: formatPort(port, ruleProtocol),
393
+ negative: true,
394
+ })),
395
+ ]
396
+ if (entries.length === 0) return null
397
+
398
+ return (
399
+ <div className="mb-2">
400
+ <div className="text-xs text-theme-text-tertiary mb-1">{label}</div>
401
+ <div className="flex flex-wrap gap-1">
402
+ {entries.map((entry, index) => (
403
+ <Badge
404
+ key={`${entry.value}-${index}`}
405
+ tone={entry.negative ? 'note' : 'structural'}
406
+ size="sm"
407
+ className={
408
+ entry.negative
409
+ ? 'font-mono whitespace-nowrap line-through'
410
+ : 'font-mono whitespace-nowrap'
411
+ }
412
+ >
413
+ {entry.negative ? `not ${entry.value}` : entry.value}
414
+ </Badge>
415
+ ))}
416
+ </div>
417
+ </div>
418
+ )
419
+ }
420
+
421
+ function hasCalicoPortConstraints(rule: any): boolean {
422
+ return (
423
+ valuesFor(rule?.source?.ports).length > 0 ||
424
+ valuesFor(rule?.source?.notPorts).length > 0 ||
425
+ valuesFor(rule?.destination?.ports).length > 0 ||
426
+ valuesFor(rule?.destination?.notPorts).length > 0
427
+ )
428
+ }
429
+
430
+ function valuesFor(value: any): any[] {
431
+ if (value === undefined || value === null || value === '') return []
432
+ return Array.isArray(value) ? value : [value]
433
+ }
434
+
435
+ function formatPort(value: any, ruleProtocol?: any): string {
436
+ if (value && typeof value === 'object') {
437
+ const protocol = value.protocol
438
+ ? formatProtocol(value.protocol).toUpperCase()
439
+ : ruleProtocol !== undefined
440
+ ? formatProtocol(ruleProtocol).toUpperCase()
441
+ : 'TCP'
442
+ if ('port' in value) {
443
+ const endPort = value.endPort !== undefined ? `-${value.endPort}` : ''
444
+ return `${protocol}/${value.port}${endPort}`
445
+ }
446
+ if ('start' in value || 'end' in value) {
447
+ return `${protocol}/${value.start ?? ''}-${value.end ?? ''}`
448
+ }
449
+ if ('strVal' in value) return `${protocol}/${value.strVal}`
450
+ if ('intVal' in value) return `${protocol}/${value.intVal}`
451
+ }
452
+ const protocol =
453
+ ruleProtocol !== undefined
454
+ ? formatProtocol(ruleProtocol).toUpperCase()
455
+ : 'TCP'
456
+ return `${protocol}/${formatValue(value)}`
457
+ }
458
+
459
+ function renderEntityValue(value: any, negative = false): React.ReactNode {
460
+ if (
461
+ value &&
462
+ typeof value === 'object' &&
463
+ !Array.isArray(value) &&
464
+ ('selector' in value || 'namespaces' in value || 'names' in value)
465
+ ) {
466
+ const items: React.ReactNode[] = []
467
+ if (value.selector !== undefined)
468
+ items.push(
469
+ <SelectorValue
470
+ key="selector"
471
+ value={value.selector}
472
+ negative={negative}
473
+ />,
474
+ )
475
+ for (const name of value.names ?? value.namespaces ?? []) {
476
+ items.push(
477
+ <ValueBadge key={String(name)} value={name} negative={negative} />,
478
+ )
479
+ }
480
+ return items.length > 0 ? (
481
+ items
482
+ ) : (
483
+ <ValueBadge value={JSON.stringify(value)} negative={negative} />
484
+ )
485
+ }
486
+
487
+ const values = Array.isArray(value) ? value : [value]
488
+ return values.map((item, index) => (
489
+ <ValueBadge
490
+ key={`${String(item)}-${index}`}
491
+ value={formatValue(item)}
492
+ negative={negative}
493
+ />
494
+ ))
495
+ }
496
+
497
+ function HTTPMatch({ value }: { value: any }) {
498
+ return (
499
+ <div className="card-inner text-xs space-y-2">
500
+ <div className="text-theme-text-tertiary">HTTP</div>
501
+ {value?.methods?.length > 0 && (
502
+ <InlineField label="Methods" values={value.methods} tone="accent1" />
503
+ )}
504
+ {value?.notMethods?.length > 0 && (
505
+ <InlineField
506
+ label="Not Methods"
507
+ values={value.notMethods}
508
+ tone="note"
509
+ />
510
+ )}
511
+ {value?.paths?.length > 0 && (
512
+ <InlineField label="Paths" values={value.paths} />
513
+ )}
514
+ {value?.notPaths?.length > 0 && (
515
+ <InlineField label="Not Paths" values={value.notPaths} tone="note" />
516
+ )}
517
+ {!value?.methods?.length &&
518
+ !value?.notMethods?.length &&
519
+ !value?.paths?.length &&
520
+ !value?.notPaths?.length && (
521
+ <span className="text-theme-text-secondary">Any HTTP request</span>
522
+ )}
523
+ </div>
524
+ )
525
+ }
526
+
527
+ function ICMPMatch({
528
+ label,
529
+ value,
530
+ negative = false,
531
+ }: {
532
+ label: string
533
+ value: any
534
+ negative?: boolean
535
+ }) {
536
+ const fields = Array.isArray(value) ? value : [value]
537
+ return (
538
+ <div className="card-inner text-xs">
539
+ <div className="text-theme-text-tertiary mb-1">{label}</div>
540
+ <div className="flex flex-wrap gap-1">
541
+ {fields.map((field, index) => {
542
+ const text =
543
+ field && typeof field === 'object'
544
+ ? [
545
+ field.family !== undefined && `family ${field.family}`,
546
+ field.type !== undefined && `type ${field.type}`,
547
+ field.code !== undefined && `code ${field.code}`,
548
+ ]
549
+ .filter(Boolean)
550
+ .join(', ') || JSON.stringify(field)
551
+ : String(field)
552
+ return (
553
+ <ValueBadge
554
+ key={`${text}-${index}`}
555
+ value={text || 'any'}
556
+ negative={negative}
557
+ />
558
+ )
559
+ })}
560
+ </div>
561
+ </div>
562
+ )
563
+ }
564
+
565
+ function InlineField({
566
+ label,
567
+ values,
568
+ tone,
569
+ }: {
570
+ label: string
571
+ values: any[]
572
+ tone?: 'note' | 'accent1' | 'structural'
573
+ }) {
574
+ return (
575
+ <div className="flex flex-wrap items-center gap-1">
576
+ <span className="text-theme-text-secondary mr-1">{label}:</span>
577
+ {values.map((value, index) => (
578
+ <ValueBadge
579
+ key={`${String(value)}-${index}`}
580
+ value={value}
581
+ tone={tone}
582
+ />
583
+ ))}
584
+ </div>
585
+ )
586
+ }
587
+
588
+ function SelectorValue({
589
+ value,
590
+ emptyText,
591
+ negative = false,
592
+ }: {
593
+ value: any
594
+ emptyText?: string
595
+ negative?: boolean
596
+ }) {
597
+ if (value === undefined || value === null || value === '') {
598
+ return (
599
+ <span className="text-theme-text-tertiary">{emptyText ?? 'Any'}</span>
600
+ )
601
+ }
602
+ return <ValueBadge value={value} negative={negative} />
603
+ }
604
+
605
+ function ValueBadge({
606
+ value,
607
+ negative = false,
608
+ tone,
609
+ }: {
610
+ value: any
611
+ negative?: boolean
612
+ tone?: 'note' | 'accent1' | 'structural'
613
+ }) {
614
+ return (
615
+ <Badge
616
+ tone={tone ?? (negative ? 'note' : 'structural')}
617
+ size="sm"
618
+ className="font-mono whitespace-normal break-all"
619
+ >
620
+ {String(value)}
621
+ </Badge>
622
+ )
623
+ }
624
+
625
+ function BadgeList({
626
+ values,
627
+ tone = 'structural',
628
+ }: {
629
+ values: any[]
630
+ tone?: 'note' | 'accent1' | 'structural'
631
+ }) {
632
+ return (
633
+ <span className="flex flex-wrap gap-1">
634
+ {values.map((value, index) => (
635
+ <Badge key={`${String(value)}-${index}`} tone={tone} size="sm">
636
+ {String(value)}
637
+ </Badge>
638
+ ))}
639
+ </span>
640
+ )
641
+ }
642
+
643
+ function formatValue(value: any): string {
644
+ if (value && typeof value === 'object') {
645
+ if ('port' in value) {
646
+ const protocol = value.protocol ? `${value.protocol}/` : ''
647
+ const end = value.endPort !== undefined ? `-${value.endPort}` : ''
648
+ return `${protocol}${value.port}${end}`
649
+ }
650
+ if ('start' in value || 'end' in value)
651
+ return `${value.start ?? ''}-${value.end ?? ''}`
652
+ if ('strVal' in value) return String(value.strVal)
653
+ if ('intVal' in value) return String(value.intVal)
654
+ return JSON.stringify(value)
655
+ }
656
+ return String(value)
657
+ }
658
+
659
+ function yesNo(value: any): string {
660
+ return value ? 'Yes' : 'No'
661
+ }
662
+
663
+ function formatProtocol(value: any): string {
664
+ if (value && typeof value === 'object') {
665
+ if (value.strVal !== undefined) return String(value.strVal)
666
+ if (value.intVal !== undefined) return String(value.intVal)
667
+ return JSON.stringify(value)
668
+ }
669
+ return String(value)
670
+ }
@@ -0,0 +1,19 @@
1
+ import { Network } from 'lucide-react'
2
+ import { Property, PropertyList, Section } from '../../ui/drawer-components'
3
+
4
+ interface CalicoTierRendererProps {
5
+ data: any
6
+ }
7
+
8
+ export function CalicoTierRenderer({ data }: CalicoTierRendererProps) {
9
+ const spec = data?.spec ?? {}
10
+
11
+ return (
12
+ <Section title="Tier" icon={Network}>
13
+ <PropertyList>
14
+ <Property label="Default Action" value={spec.defaultAction ?? 'Deny'} />
15
+ <Property label="Order" value={spec.order ?? 'Last (lowest precedence)'} />
16
+ </PropertyList>
17
+ </Section>
18
+ )
19
+ }