@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
@@ -1,5 +1,6 @@
1
1
  import { Clock, Database } from 'lucide-react'
2
2
  import { Section, PropertyList, Property, AlertBanner, ResourceLink } from '../../ui/drawer-components'
3
+ import { CronValue } from '../../ui/ScheduleValue'
3
4
  import {
4
5
  getCNPGScheduledBackupCluster,
5
6
  getCNPGScheduleCron,
@@ -8,7 +9,9 @@ import {
8
9
  getCNPGScheduledBackupNextSchedule,
9
10
  getCNPGScheduledBackupIsSuspended,
10
11
  getCNPGScheduledBackupIsImmediate,
12
+ getCNPGBackupPlugin,
11
13
  getCNPGScheduledBackupOwnerRef,
14
+ CNPG_BARMAN_OBJECTSTORE_GROUP,
12
15
  } from '../resource-utils-cnpg'
13
16
 
14
17
  interface CNPGScheduledBackupRendererProps {
@@ -19,6 +22,7 @@ interface CNPGScheduledBackupRendererProps {
19
22
  export function CNPGScheduledBackupRenderer({ data, onNavigate }: CNPGScheduledBackupRendererProps) {
20
23
  const isSuspended = getCNPGScheduledBackupIsSuspended(data)
21
24
  const clusterName = getCNPGScheduledBackupCluster(data)
25
+ const schedulePlugin = getCNPGBackupPlugin(data)
22
26
 
23
27
  return (
24
28
  <>
@@ -34,7 +38,7 @@ export function CNPGScheduledBackupRenderer({ data, onNavigate }: CNPGScheduledB
34
38
  {/* Schedule */}
35
39
  <Section title="Schedule" icon={Clock} defaultExpanded>
36
40
  <PropertyList>
37
- <Property label="Cron Expression" value={getCNPGScheduleCron(data)} />
41
+ <Property label="Cron Expression" value={<CronValue cron={getCNPGScheduleCron(data)} dialect="seconds" />} />
38
42
  <Property label="Last Schedule" value={getCNPGScheduledBackupLastSchedule(data)} />
39
43
  <Property label="Next Schedule" value={getCNPGScheduledBackupNextSchedule(data)} />
40
44
  <Property label="Suspended" value={isSuspended ? 'Yes' : 'No'} />
@@ -60,6 +64,24 @@ export function CNPGScheduledBackupRenderer({ data, onNavigate }: CNPGScheduledB
60
64
  return clusterName
61
65
  })()} />
62
66
  <Property label="Method" value={getCNPGScheduledBackupMethod(data)} />
67
+ {/* Under the plugin method the destination lives on an ObjectStore in
68
+ another API group, so naming it is the only route from here to
69
+ where these backups will actually land. */}
70
+ {schedulePlugin && <Property label="Plugin" value={schedulePlugin.name} />}
71
+ {schedulePlugin?.parameters?.barmanObjectName && (
72
+ <Property
73
+ label="Object Store"
74
+ value={
75
+ <ResourceLink
76
+ name={schedulePlugin.parameters.barmanObjectName}
77
+ kind="objectstores"
78
+ group={CNPG_BARMAN_OBJECTSTORE_GROUP}
79
+ namespace={data.metadata?.namespace || ''}
80
+ onNavigate={onNavigate}
81
+ />
82
+ }
83
+ />
84
+ )}
63
85
  <Property label="Owner Reference" value={getCNPGScheduledBackupOwnerRef(data)} />
64
86
  </PropertyList>
65
87
  </Section>
@@ -0,0 +1,27 @@
1
+ import { formatAge } from '../resource-utils'
2
+
3
+ /**
4
+ * A recovery point, stated absolutely with the interval beside it.
5
+ *
6
+ * "1d ago" is the wrong unit for this question on its own. An RPO conversation
7
+ * needs a timestamp — and "Last Successful Backup 1d ago" next to "Last Failed
8
+ * Attempt 1d ago" is the same string for events a week apart in the failure
9
+ * history, which is precisely when you are reading this screen. Same treatment
10
+ * the certificate rows already use.
11
+ *
12
+ * Shared by the Cluster and ObjectStore pages: the same recovery point is
13
+ * reachable from both depending on whether backups run through the plugin, and
14
+ * the two must not describe it differently.
15
+ */
16
+ export function RecoveryTime({ at }: { at: string }) {
17
+ const d = new Date(at)
18
+ if (Number.isNaN(d.getTime())) return <>{at}</>
19
+ return (
20
+ <span className="inline-flex items-baseline gap-2 min-w-0">
21
+ <span>{d.toISOString().replace('T', ' ').replace(/\.\d+Z$/, ' UTC')}</span>
22
+ {/* One interpolation, so the age and its unit stay a single text node
23
+ rather than being split by a renderer comment marker. */}
24
+ <span className="text-xs text-theme-text-tertiary shrink-0">{`${formatAge(at)} ago`}</span>
25
+ </span>
26
+ )
27
+ }
@@ -0,0 +1,71 @@
1
+ import { Network } from 'lucide-react'
2
+ import { Badge } from '../../ui/Badge'
3
+ import { ResourceLink, Section } from '../../ui/drawer-components'
4
+
5
+ interface CalicoHostEndpointRendererProps {
6
+ data: any
7
+ onNavigate?: (ref: { kind: string; namespace: string; name: string }) => void
8
+ }
9
+
10
+ export function CalicoHostEndpointRenderer({ data, onNavigate }: CalicoHostEndpointRendererProps) {
11
+ const spec = data?.spec ?? {}
12
+ const expectedIPs: string[] = spec.expectedIPs ?? []
13
+ const profiles: string[] = spec.profiles ?? []
14
+ const ports: Array<{ name?: string; port?: number; protocol?: string }> = spec.ports ?? []
15
+
16
+ return (
17
+ <Section title="Host Endpoint" icon={Network}>
18
+ <div className="card-inner text-sm space-y-3">
19
+ <div className="flex items-start justify-between gap-3">
20
+ <div className="min-w-0">
21
+ <div className="text-xs text-theme-text-tertiary mb-0.5">Node</div>
22
+ <div className="font-medium text-theme-text-primary break-all">
23
+ {spec.node ? <ResourceLink name={spec.node} kind="nodes" namespace="" onNavigate={onNavigate} /> : 'Not specified'}
24
+ </div>
25
+ </div>
26
+ {spec.interfaceName && (
27
+ <div className="shrink-0 text-right">
28
+ <div className="text-xs text-theme-text-tertiary mb-0.5">Interface Name</div>
29
+ <Badge tone="structural" size="sm" className="font-mono">{spec.interfaceName}</Badge>
30
+ </div>
31
+ )}
32
+ </div>
33
+
34
+ {expectedIPs.length > 0 && (
35
+ <div className="border-t border-theme-border-subtle pt-3">
36
+ <div className="text-xs text-theme-text-tertiary mb-1.5">Expected IPs</div>
37
+ <div className="flex flex-wrap gap-1">
38
+ {expectedIPs.map((ip) => (
39
+ <Badge key={ip} tone="structural" size="sm" className="font-mono">{ip}</Badge>
40
+ ))}
41
+ </div>
42
+ </div>
43
+ )}
44
+
45
+ {profiles.length > 0 && (
46
+ <div className="border-t border-theme-border-subtle pt-3">
47
+ <div className="text-xs text-theme-text-tertiary mb-1.5">Profiles</div>
48
+ <div className="flex flex-wrap gap-1">
49
+ {profiles.map((profile) => (
50
+ <Badge key={profile} tone="structural" size="sm">{profile}</Badge>
51
+ ))}
52
+ </div>
53
+ </div>
54
+ )}
55
+
56
+ {ports.length > 0 && (
57
+ <div className="border-t border-theme-border-subtle pt-3">
58
+ <div className="text-xs text-theme-text-tertiary mb-1.5">Named Ports</div>
59
+ <div className="flex flex-wrap gap-1">
60
+ {ports.map((port, index) => (
61
+ <Badge key={`${port.name}-${port.protocol}-${port.port}-${index}`} tone="structural" size="sm" className="font-mono">
62
+ {`${port.name ? `${port.name}: ` : ''}${port.protocol}/${port.port}`}
63
+ </Badge>
64
+ ))}
65
+ </div>
66
+ </div>
67
+ )}
68
+ </div>
69
+ </Section>
70
+ )
71
+ }
@@ -0,0 +1,32 @@
1
+ import { Network } from 'lucide-react'
2
+ import { Property, PropertyList, Section } from '../../ui/drawer-components'
3
+ import {
4
+ getCalicoIPPoolAllowedUses,
5
+ getCalicoIPPoolBlockSize,
6
+ } from '../resource-utils-calico'
7
+
8
+ interface CalicoIPPoolRendererProps {
9
+ data: any
10
+ }
11
+
12
+ export function CalicoIPPoolRenderer({ data }: CalicoIPPoolRendererProps) {
13
+ const spec = data?.spec ?? {}
14
+
15
+ return (
16
+ <Section title="IP Pool" icon={Network}>
17
+ <PropertyList>
18
+ <Property label="Allowed Uses" value={getCalicoIPPoolAllowedUses(data)} />
19
+ <Property label="Assignment Mode" value={spec.assignmentMode ?? 'Automatic'} />
20
+ <Property label="Block Size" value={getCalicoIPPoolBlockSize(data)} />
21
+ <Property label="CIDR" value={spec.cidr} />
22
+ <Property label="IP-in-IP Mode" value={spec.ipipMode ?? 'Never'} />
23
+ <Property label="VXLAN Mode" value={spec.vxlanMode ?? 'Never'} />
24
+ <Property label="NAT Outgoing" value={spec.natOutgoing ? 'Yes' : 'No'} />
25
+ <Property label="Disabled" value={spec.disabled ? 'Yes' : 'No'} />
26
+ <Property label="BGP Export Disabled" value={spec.disableBGPExport ? 'Yes' : 'No'} />
27
+ <Property label="Node Selector" value={spec.nodeSelector ?? 'all()'} />
28
+ <Property label="Namespace Selector" value={spec.namespaceSelector} />
29
+ </PropertyList>
30
+ </Section>
31
+ )
32
+ }
@@ -0,0 +1,205 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { renderToString } from 'react-dom/server'
3
+ import { CalicoNetworkPolicyDiagram } from './CalicoNetworkPolicyDiagram'
4
+
5
+ describe('CalicoNetworkPolicyDiagram', () => {
6
+ it('shows ordered actions and Calico match fields in both directions', () => {
7
+ const html = renderToString(
8
+ <CalicoNetworkPolicyDiagram
9
+ spec={{
10
+ selector: "app == 'api'",
11
+ namespaceSelector: "project == 'prod'",
12
+ serviceAccountSelector: "role == 'api'",
13
+ types: ['Ingress', 'Egress'],
14
+ ingress: [
15
+ {
16
+ action: 'Allow',
17
+ protocol: 'TCP',
18
+ source: {
19
+ selector: "role == 'frontend'",
20
+ nets: ['10.0.0.0/8'],
21
+ serviceAccounts: { names: ['frontend'] },
22
+ },
23
+ destination: { ports: [80] },
24
+ },
25
+ { action: 'Deny', source: { notSelector: "app == 'debug'" } },
26
+ ],
27
+ egress: [
28
+ {
29
+ action: 'Log',
30
+ protocol: 'UDP',
31
+ source: { serviceAccounts: { selector: "role == 'api'" } },
32
+ destination: {
33
+ selector: "app == 'db'",
34
+ nets: ['192.168.0.0/16'],
35
+ ports: [{ port: 5432, endPort: 5433 }],
36
+ },
37
+ },
38
+ { action: 'Pass', destination: { notNets: ['0.0.0.0/0'] } },
39
+ ],
40
+ }}
41
+ />,
42
+ )
43
+
44
+ for (const value of [
45
+ 'app == &#x27;api&#x27;',
46
+ 'role == &#x27;api&#x27;',
47
+ 'Ingress',
48
+ 'Egress',
49
+ 'Deny',
50
+ 'Log',
51
+ 'Pass',
52
+ 'TCP',
53
+ 'UDP',
54
+ 'role == &#x27;frontend&#x27;',
55
+ '10.0.0.0/8',
56
+ 'frontend',
57
+ 'TCP/80',
58
+ 'app == &#x27;db&#x27;',
59
+ '192.168.0.0/16',
60
+ 'UDP/5432-5433',
61
+ 'Not Nets',
62
+ '0.0.0.0/0',
63
+ ]) {
64
+ expect(html).toContain(value)
65
+ }
66
+
67
+ expect(html).not.toContain('Policy target')
68
+ expect(html).not.toContain('Source')
69
+ expect(html).not.toContain('Destination')
70
+ expect(html).not.toContain('Rule 1')
71
+ expect(html).toContain('Allow')
72
+ expect(html).not.toMatch(/badge-sm[^>]*>TCP<\/span>/)
73
+ expect(html).not.toMatch(/badge-sm[^>]*>UDP<\/span>/)
74
+ expect(html).toContain('text-warning-text')
75
+ expect(html).toContain('line-through')
76
+ })
77
+
78
+ it('keeps port constraints readable and distinguishes source exclusions', () => {
79
+ const html = renderToString(
80
+ <CalicoNetworkPolicyDiagram
81
+ spec={{
82
+ types: ['Egress'],
83
+ egress: [
84
+ {
85
+ protocol: 'UDP',
86
+ source: {
87
+ ports: [8080],
88
+ notPorts: [{ protocol: 'UDP', port: 5353 }],
89
+ },
90
+ destination: {
91
+ ports: [
92
+ { protocol: 'TCP', port: 8080 },
93
+ { protocol: 'TCP', port: 8443 },
94
+ ],
95
+ notPorts: [443],
96
+ },
97
+ },
98
+ ],
99
+ }}
100
+ />,
101
+ )
102
+
103
+ for (const value of [
104
+ 'src:UDP/8080',
105
+ 'src:!UDP/5353',
106
+ 'TCP/8080',
107
+ 'TCP/8443',
108
+ '!UDP/443',
109
+ ]) {
110
+ expect(html).toContain(value)
111
+ }
112
+ expect(html).not.toContain('dst:')
113
+ expect(html).not.toContain('max-w-14')
114
+ expect(html).not.toContain('truncate font-mono text-[8px]')
115
+ expect(html).not.toMatch(/badge-sm[^>]*>UDP<\/span>/)
116
+ })
117
+
118
+ it('renders protocol-only rules as wildcard arrow labels', () => {
119
+ const html = renderToString(
120
+ <CalicoNetworkPolicyDiagram
121
+ spec={{
122
+ types: ['Ingress'],
123
+ ingress: [
124
+ {
125
+ action: 'Deny',
126
+ protocol: 'TCP',
127
+ notProtocol: 'UDP',
128
+ source: { selector: "app == 'client'" },
129
+ },
130
+ ],
131
+ }}
132
+ />,
133
+ )
134
+
135
+ expect(html).toContain('TCP/*')
136
+ expect(html).not.toMatch(/badge-sm[^>]*>TCP<\/span>/)
137
+ expect(html).toContain('not')
138
+ expect(html).toContain('UDP')
139
+ })
140
+
141
+ it('uses native peer colors with stable match priority', () => {
142
+ const renderDestination = (destination: any) =>
143
+ renderToString(
144
+ <CalicoNetworkPolicyDiagram
145
+ spec={{
146
+ selector: "app == 'target'",
147
+ types: ['Egress'],
148
+ egress: [{ destination }],
149
+ }}
150
+ />,
151
+ )
152
+
153
+ const combined = renderDestination({
154
+ selector: "app == 'workload'",
155
+ namespaceSelector: "team == 'prod'",
156
+ nets: ['10.0.0.0/8'],
157
+ })
158
+ const namespace = renderDestination({
159
+ namespaceSelector: "team == 'prod'",
160
+ })
161
+ const network = renderDestination({ nets: ['10.0.0.0/8'] })
162
+ const serviceAccount = renderDestination({
163
+ serviceAccountSelector: "role == 'api'",
164
+ })
165
+ const unknown = renderDestination({ unsupportedMatch: 'value' })
166
+
167
+ expect(combined).toContain('border-emerald-500/30 bg-emerald-500/8')
168
+ expect(combined).not.toContain('border-sky-500/30 bg-sky-500/8')
169
+ expect(combined).not.toContain('border-amber-500/30 bg-amber-500/8')
170
+ expect(namespace).toContain('border-sky-500/30 bg-sky-500/8')
171
+ expect(network).toContain('border-amber-500/30 bg-amber-500/8')
172
+ expect(serviceAccount).toContain('border-emerald-500/30 bg-emerald-500/8')
173
+ expect(unknown).toContain('border-theme-border bg-theme-elevated/50')
174
+ expect(combined).toContain('border-indigo-500/30 bg-indigo-500/8')
175
+ })
176
+
177
+ it('does not imply deny-all when a policy has no explicit rules', () => {
178
+ const html = renderToString(
179
+ <CalicoNetworkPolicyDiagram spec={{ types: ['Ingress'], ingress: [] }} />,
180
+ )
181
+
182
+ expect(html).toContain('No explicit')
183
+ expect(html).toContain('ingress')
184
+ expect(html).not.toContain('Deny all')
185
+ })
186
+
187
+ it('renders staged rules as non-enforced dashed paths', () => {
188
+ const html = renderToString(
189
+ <CalicoNetworkPolicyDiagram
190
+ staged
191
+ spec={{
192
+ selector: "app == 'api'",
193
+ types: ['Ingress'],
194
+ ingress: [{ action: 'Allow', source: {} }],
195
+ }}
196
+ />,
197
+ )
198
+
199
+ expect(html).toContain('Staged preview')
200
+ expect(html).toContain('not enforced')
201
+ expect(html).toContain('stroke-dasharray="4 3"')
202
+ expect(html).toContain('Allow')
203
+ expect(html).not.toContain('Rule 1')
204
+ })
205
+ })