@skyhook-io/k8s-ui 1.4.0 → 1.4.2
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.
- package/package.json +2 -2
- package/src/components/resources/ResourcesSidebar.tsx +152 -57
- package/src/components/resources/ResourcesView.tsx +141 -49
- package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +190 -28
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +22 -0
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +156 -5
- package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +28 -1
- package/src/components/resources/renderers/CertificateRenderer.tsx +47 -8
- package/src/components/resources/renderers/ChallengeRenderer.tsx +22 -3
- package/src/components/resources/renderers/CiliumNetworkPolicyRenderer.tsx +262 -45
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +81 -9
- package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +10 -3
- package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +59 -1
- package/src/components/resources/renderers/ClusterNetworkPolicyRenderer.tsx +7 -21
- package/src/components/resources/renderers/ExternalSecretRenderer.tsx +3 -5
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +91 -3
- package/src/components/resources/renderers/GitRepositoryRenderer.tsx +3 -42
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +3 -41
- package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +66 -2
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +31 -1
- package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +66 -1
- package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +3 -0
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +55 -1
- package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +23 -0
- package/src/components/resources/renderers/KustomizationRenderer.tsx +38 -3
- package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +7 -56
- package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +3 -42
- package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +3 -8
- package/src/components/resources/renderers/PodMonitorRenderer.tsx +5 -7
- package/src/components/resources/renderers/PodRenderer.tsx +22 -74
- package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +178 -20
- package/src/components/resources/renderers/ReplicaSetRenderer.tsx +2 -2
- package/src/components/resources/renderers/RolloutRenderer.tsx +105 -3
- package/src/components/resources/renderers/SbomReportRenderer.tsx +46 -5
- package/src/components/resources/renderers/SealedSecretRenderer.tsx +11 -2
- package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +5 -7
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +11 -3
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +11 -3
- package/src/components/resources/renderers/WebhookConfigRenderer.tsx +8 -16
- package/src/components/resources/renderers/WorkflowRenderer.tsx +71 -17
- package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +46 -1
- package/src/components/resources/renderers/karpenter-cells.tsx +18 -3
- package/src/components/resources/resource-utils-cnpg.ts +47 -0
- package/src/components/resources/resource-utils-karpenter.ts +6 -0
- package/src/components/resources/resource-utils-prometheus.ts +50 -6
- package/src/components/resources/resource-utils.ts +131 -33
- package/src/components/shared/EditableYamlView.tsx +38 -3
- package/src/components/shared/ResourceRendererDispatch.tsx +4 -4
- package/src/components/timeline/TimelineList.tsx +11 -4
- package/src/components/ui/Tooltip.tsx +16 -0
- package/src/components/ui/drawer-components.tsx +48 -1
- package/src/theme/variables.css +1 -1
- package/src/types/gitops.ts +8 -0
- package/src/utils/navigation.test.ts +29 -2
- package/src/utils/navigation.ts +45 -24
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Shield, Clock, Globe } from 'lucide-react'
|
|
1
|
+
import { Shield, Clock, Globe, Key } from 'lucide-react'
|
|
2
2
|
import { clsx } from 'clsx'
|
|
3
3
|
import { Section, PropertyList, Property, ConditionsSection, AlertBanner } from '../../ui/drawer-components'
|
|
4
4
|
|
|
@@ -26,6 +26,10 @@ export function CertificateRenderer({ data }: CertificateRendererProps) {
|
|
|
26
26
|
const issuerRef = spec.issuerRef || {}
|
|
27
27
|
const usages = spec.usages || []
|
|
28
28
|
|
|
29
|
+
const privateKey = spec.privateKey
|
|
30
|
+
const failedIssuanceAttempts = status.failedIssuanceAttempts
|
|
31
|
+
const lastFailureTime = status.lastFailureTime
|
|
32
|
+
|
|
29
33
|
const readyCond = conditions.find((c: any) => c.type === 'Ready')
|
|
30
34
|
const isReady = readyCond?.status === 'True'
|
|
31
35
|
const isNotReady = readyCond?.status === 'False'
|
|
@@ -96,30 +100,53 @@ export function CertificateRenderer({ data }: CertificateRendererProps) {
|
|
|
96
100
|
/>
|
|
97
101
|
)}
|
|
98
102
|
|
|
103
|
+
{failedIssuanceAttempts > 0 && (
|
|
104
|
+
<AlertBanner
|
|
105
|
+
variant="warning"
|
|
106
|
+
title={`${failedIssuanceAttempts} failed issuance attempt${failedIssuanceAttempts !== 1 ? 's' : ''}`}
|
|
107
|
+
message={lastFailureTime ? `Last failure: ${formatDate(lastFailureTime)}` : undefined}
|
|
108
|
+
/>
|
|
109
|
+
)}
|
|
110
|
+
|
|
99
111
|
{/* Certificate Info */}
|
|
100
112
|
<Section title="Certificate Info" icon={Shield}>
|
|
101
113
|
<PropertyList>
|
|
102
114
|
<Property
|
|
103
115
|
label="Status"
|
|
104
116
|
value={
|
|
105
|
-
<span className=
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
117
|
+
<span className="flex items-center gap-2">
|
|
118
|
+
<span className={clsx(
|
|
119
|
+
'badge',
|
|
120
|
+
isReady
|
|
121
|
+
? 'bg-green-500/20 text-green-400'
|
|
122
|
+
: 'bg-red-500/20 text-red-400'
|
|
123
|
+
)}>
|
|
124
|
+
{isReady ? 'Ready' : 'Not Ready'}
|
|
125
|
+
</span>
|
|
126
|
+
{spec.isCA && (
|
|
127
|
+
<span className="badge bg-purple-500/20 text-purple-400">
|
|
128
|
+
CA Certificate
|
|
129
|
+
</span>
|
|
130
|
+
)}
|
|
112
131
|
</span>
|
|
113
132
|
}
|
|
114
133
|
/>
|
|
115
134
|
<Property label="Secret Name" value={spec.secretName} />
|
|
116
135
|
<Property label="Revision" value={status.revision} />
|
|
136
|
+
{failedIssuanceAttempts > 0 && (
|
|
137
|
+
<Property label="Failed Issuance Attempts" value={String(failedIssuanceAttempts)} />
|
|
138
|
+
)}
|
|
139
|
+
{lastFailureTime && (
|
|
140
|
+
<Property label="Last Failure Time" value={formatDate(lastFailureTime)} />
|
|
141
|
+
)}
|
|
117
142
|
</PropertyList>
|
|
118
143
|
</Section>
|
|
119
144
|
|
|
120
145
|
{/* Validity */}
|
|
121
146
|
<Section title="Validity" icon={Clock}>
|
|
122
147
|
<PropertyList>
|
|
148
|
+
<Property label="Duration" value={spec.duration} />
|
|
149
|
+
<Property label="Renew Before" value={spec.renewBefore} />
|
|
123
150
|
<Property label="Not Before" value={notBefore ? formatDate(notBefore) : '-'} />
|
|
124
151
|
<Property
|
|
125
152
|
label="Not After"
|
|
@@ -161,6 +188,18 @@ export function CertificateRenderer({ data }: CertificateRendererProps) {
|
|
|
161
188
|
)}
|
|
162
189
|
</Section>
|
|
163
190
|
|
|
191
|
+
{/* Private Key */}
|
|
192
|
+
{privateKey && (
|
|
193
|
+
<Section title="Private Key" icon={Key}>
|
|
194
|
+
<PropertyList>
|
|
195
|
+
<Property label="Algorithm" value={privateKey.algorithm} />
|
|
196
|
+
<Property label="Size" value={privateKey.size != null ? String(privateKey.size) : undefined} />
|
|
197
|
+
<Property label="Encoding" value={privateKey.encoding} />
|
|
198
|
+
<Property label="Rotation Policy" value={privateKey.rotationPolicy} />
|
|
199
|
+
</PropertyList>
|
|
200
|
+
</Section>
|
|
201
|
+
)}
|
|
202
|
+
|
|
164
203
|
{/* Domains */}
|
|
165
204
|
{dnsNames.length > 0 && (
|
|
166
205
|
<Section title="Domains" icon={Globe}>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Shield, AlertTriangle, Globe, Key } from 'lucide-react'
|
|
1
|
+
import { Shield, AlertTriangle, Globe, Key, FileText } from 'lucide-react'
|
|
2
2
|
import { clsx } from 'clsx'
|
|
3
3
|
import { Section, PropertyList, Property, ConditionsSection } from '../../ui/drawer-components'
|
|
4
4
|
import { getSolverType } from './ClusterIssuerRenderer'
|
|
@@ -68,11 +68,19 @@ export function ChallengeRenderer({ data }: { data: any }) {
|
|
|
68
68
|
</span>
|
|
69
69
|
}
|
|
70
70
|
/>
|
|
71
|
+
{status.reason && !isError && <Property label="Reason" value={status.reason} />}
|
|
71
72
|
<Property
|
|
72
73
|
label="Type"
|
|
73
74
|
value={
|
|
74
|
-
<span className="
|
|
75
|
-
|
|
75
|
+
<span className="flex items-center gap-2">
|
|
76
|
+
<span className="badge bg-theme-elevated text-theme-text-secondary">
|
|
77
|
+
{challengeType}
|
|
78
|
+
</span>
|
|
79
|
+
{spec.wildcard && (
|
|
80
|
+
<span className="badge bg-purple-500/20 text-purple-400">
|
|
81
|
+
Wildcard
|
|
82
|
+
</span>
|
|
83
|
+
)}
|
|
76
84
|
</span>
|
|
77
85
|
}
|
|
78
86
|
/>
|
|
@@ -84,6 +92,17 @@ export function ChallengeRenderer({ data }: { data: any }) {
|
|
|
84
92
|
</PropertyList>
|
|
85
93
|
</Section>
|
|
86
94
|
|
|
95
|
+
{/* Issuer */}
|
|
96
|
+
{spec.issuerRef && (
|
|
97
|
+
<Section title="Issuer" icon={FileText}>
|
|
98
|
+
<PropertyList>
|
|
99
|
+
<Property label="Name" value={spec.issuerRef.name} />
|
|
100
|
+
<Property label="Kind" value={spec.issuerRef.kind} />
|
|
101
|
+
<Property label="Group" value={spec.issuerRef.group} />
|
|
102
|
+
</PropertyList>
|
|
103
|
+
</Section>
|
|
104
|
+
)}
|
|
105
|
+
|
|
87
106
|
{/* ACME */}
|
|
88
107
|
{(spec.url || spec.token) && (
|
|
89
108
|
<Section title="ACME" icon={Globe}>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Shield, ArrowDownToLine, ArrowUpFromLine, Ban } from 'lucide-react'
|
|
2
|
-
import { Section,
|
|
1
|
+
import { Shield, ArrowDownToLine, ArrowUpFromLine, Ban, Lock } from 'lucide-react'
|
|
2
|
+
import { Section, LabelSelectorDisplay } from '../../ui/drawer-components'
|
|
3
3
|
|
|
4
4
|
interface CiliumNetworkPolicyRendererProps {
|
|
5
5
|
data: any
|
|
@@ -8,13 +8,12 @@ interface CiliumNetworkPolicyRendererProps {
|
|
|
8
8
|
export function CiliumNetworkPolicyRenderer({ data }: CiliumNetworkPolicyRendererProps) {
|
|
9
9
|
const spec = data.spec || data.specs || {}
|
|
10
10
|
const endpointSelector = spec.endpointSelector || {}
|
|
11
|
-
const matchLabels = endpointSelector.matchLabels || {}
|
|
12
|
-
const hasMatchLabels = Object.keys(matchLabels).length > 0
|
|
13
11
|
const ingress: any[] | undefined = spec.ingress
|
|
14
12
|
const ingressDeny: any[] | undefined = spec.ingressDeny
|
|
15
13
|
const egress: any[] | undefined = spec.egress
|
|
16
14
|
const egressDeny: any[] | undefined = spec.egressDeny
|
|
17
15
|
const description = spec.description || ''
|
|
16
|
+
const enableDefaultDeny = spec.enableDefaultDeny
|
|
18
17
|
|
|
19
18
|
return (
|
|
20
19
|
<>
|
|
@@ -22,33 +21,30 @@ export function CiliumNetworkPolicyRenderer({ data }: CiliumNetworkPolicyRendere
|
|
|
22
21
|
{description && (
|
|
23
22
|
<p className="text-xs text-theme-text-secondary mb-2">{description}</p>
|
|
24
23
|
)}
|
|
25
|
-
<
|
|
26
|
-
<
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
</PropertyList>
|
|
31
|
-
{hasMatchLabels && (
|
|
24
|
+
<div className="mt-2">
|
|
25
|
+
<div className="text-xs text-theme-text-tertiary mb-1">Endpoint Selector</div>
|
|
26
|
+
<LabelSelectorDisplay selector={endpointSelector} emptyText="All endpoints in namespace" />
|
|
27
|
+
</div>
|
|
28
|
+
{spec.nodeSelector && (
|
|
32
29
|
<div className="mt-2">
|
|
33
|
-
<div className="text-xs text-theme-text-tertiary mb-1">
|
|
34
|
-
<
|
|
35
|
-
{Object.entries(matchLabels).map(([k, v]) => (
|
|
36
|
-
<span key={k} className="badge bg-theme-elevated text-theme-text-secondary">
|
|
37
|
-
{k}={String(v)}
|
|
38
|
-
</span>
|
|
39
|
-
))}
|
|
40
|
-
</div>
|
|
30
|
+
<div className="text-xs text-theme-text-tertiary mb-1">Node Selector</div>
|
|
31
|
+
<LabelSelectorDisplay selector={spec.nodeSelector} emptyText="All nodes" />
|
|
41
32
|
</div>
|
|
42
33
|
)}
|
|
43
|
-
{
|
|
34
|
+
{enableDefaultDeny && (
|
|
44
35
|
<div className="mt-2">
|
|
45
|
-
<div className="text-xs text-theme-text-tertiary mb-1">
|
|
36
|
+
<div className="text-xs text-theme-text-tertiary mb-1">Default Deny</div>
|
|
46
37
|
<div className="flex flex-wrap gap-1">
|
|
47
|
-
{
|
|
48
|
-
<span
|
|
49
|
-
{
|
|
38
|
+
{enableDefaultDeny.ingress !== undefined && (
|
|
39
|
+
<span className="badge bg-theme-elevated text-theme-text-secondary">
|
|
40
|
+
Ingress: {enableDefaultDeny.ingress ? 'Yes' : 'No'}
|
|
41
|
+
</span>
|
|
42
|
+
)}
|
|
43
|
+
{enableDefaultDeny.egress !== undefined && (
|
|
44
|
+
<span className="badge bg-theme-elevated text-theme-text-secondary">
|
|
45
|
+
Egress: {enableDefaultDeny.egress ? 'Yes' : 'No'}
|
|
50
46
|
</span>
|
|
51
|
-
)
|
|
47
|
+
)}
|
|
52
48
|
</div>
|
|
53
49
|
</div>
|
|
54
50
|
)}
|
|
@@ -102,20 +98,42 @@ function CiliumRuleCard({ rule }: { rule: any }) {
|
|
|
102
98
|
const fromEntities: string[] = rule.fromEntities || []
|
|
103
99
|
const fromCIDR: string[] = rule.fromCIDR || []
|
|
104
100
|
const fromCIDRSet: any[] = rule.fromCIDRSet || []
|
|
101
|
+
const fromFQDNs: any[] = rule.fromFQDNs || []
|
|
102
|
+
const fromNodes: any[] = rule.fromNodes || []
|
|
103
|
+
const fromGroups: any[] = rule.fromGroups || []
|
|
105
104
|
const toEndpoints: any[] = rule.toEndpoints || []
|
|
106
105
|
const toEntities: string[] = rule.toEntities || []
|
|
107
106
|
const toCIDR: string[] = rule.toCIDR || []
|
|
108
107
|
const toCIDRSet: any[] = rule.toCIDRSet || []
|
|
108
|
+
const toFQDNs: any[] = rule.toFQDNs || []
|
|
109
|
+
const toServices: any[] = rule.toServices || []
|
|
110
|
+
const toNodes: any[] = rule.toNodes || []
|
|
111
|
+
const toGroups: any[] = rule.toGroups || []
|
|
109
112
|
const toPorts: any[] = rule.toPorts || []
|
|
113
|
+
const icmps: any[] = rule.icmps || []
|
|
114
|
+
const authentication = rule.authentication
|
|
115
|
+
|
|
116
|
+
const hasFrom = fromEndpoints.length > 0 || fromEntities.length > 0 || fromCIDR.length > 0 ||
|
|
117
|
+
fromCIDRSet.length > 0 || fromFQDNs.length > 0 || fromNodes.length > 0 || fromGroups.length > 0
|
|
118
|
+
const hasTo = toEndpoints.length > 0 || toEntities.length > 0 || toCIDR.length > 0 ||
|
|
119
|
+
toCIDRSet.length > 0 || toFQDNs.length > 0 || toServices.length > 0 || toNodes.length > 0 || toGroups.length > 0
|
|
120
|
+
const hasRules = hasFrom || hasTo || toPorts.length > 0 || icmps.length > 0
|
|
110
121
|
|
|
111
122
|
return (
|
|
112
123
|
<div className="card-inner-lg">
|
|
124
|
+
{authentication && (
|
|
125
|
+
<div className="mb-2 flex items-center gap-1.5">
|
|
126
|
+
<Lock className="h-3 w-3 text-yellow-400" />
|
|
127
|
+
<span className="text-xs text-yellow-400">Authentication: {authentication.mode || 'required'}</span>
|
|
128
|
+
</div>
|
|
129
|
+
)}
|
|
130
|
+
|
|
113
131
|
{fromEndpoints.length > 0 && (
|
|
114
132
|
<div className="mb-2">
|
|
115
133
|
<div className="text-xs text-theme-text-tertiary mb-1">From Endpoints</div>
|
|
116
134
|
<div className="space-y-1">
|
|
117
135
|
{fromEndpoints.map((ep: any, j: number) => (
|
|
118
|
-
<
|
|
136
|
+
<LabelSelectorDisplay key={j} selector={ep} emptyText="all endpoints" inline />
|
|
119
137
|
))}
|
|
120
138
|
</div>
|
|
121
139
|
</div>
|
|
@@ -146,12 +164,47 @@ function CiliumRuleCard({ rule }: { rule: any }) {
|
|
|
146
164
|
</div>
|
|
147
165
|
)}
|
|
148
166
|
|
|
167
|
+
{fromFQDNs.length > 0 && (
|
|
168
|
+
<div className="mb-2">
|
|
169
|
+
<div className="text-xs text-theme-text-tertiary mb-1">From FQDNs</div>
|
|
170
|
+
<div className="flex flex-wrap gap-1">
|
|
171
|
+
{fromFQDNs.map((fqdn: any, j: number) => (
|
|
172
|
+
<span key={j} className="badge bg-theme-elevated text-theme-text-secondary">
|
|
173
|
+
{fqdn.matchName || fqdn.matchPattern || JSON.stringify(fqdn)}
|
|
174
|
+
</span>
|
|
175
|
+
))}
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
)}
|
|
179
|
+
|
|
180
|
+
{fromNodes.length > 0 && (
|
|
181
|
+
<div className="mb-2">
|
|
182
|
+
<div className="text-xs text-theme-text-tertiary mb-1">From Nodes</div>
|
|
183
|
+
<div className="space-y-1">
|
|
184
|
+
{fromNodes.map((node: any, j: number) => (
|
|
185
|
+
<LabelSelectorDisplay key={j} selector={node} />
|
|
186
|
+
))}
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
)}
|
|
190
|
+
|
|
191
|
+
{fromGroups.length > 0 && (
|
|
192
|
+
<div className="mb-2">
|
|
193
|
+
<div className="text-xs text-theme-text-tertiary mb-1">From Groups</div>
|
|
194
|
+
<div className="flex flex-wrap gap-1">
|
|
195
|
+
{fromGroups.map((g: any, j: number) => (
|
|
196
|
+
<CloudGroupEntry key={j} group={g} />
|
|
197
|
+
))}
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
)}
|
|
201
|
+
|
|
149
202
|
{toEndpoints.length > 0 && (
|
|
150
203
|
<div className="mb-2">
|
|
151
204
|
<div className="text-xs text-theme-text-tertiary mb-1">To Endpoints</div>
|
|
152
205
|
<div className="space-y-1">
|
|
153
206
|
{toEndpoints.map((ep: any, j: number) => (
|
|
154
|
-
<
|
|
207
|
+
<LabelSelectorDisplay key={j} selector={ep} emptyText="all endpoints" inline />
|
|
155
208
|
))}
|
|
156
209
|
</div>
|
|
157
210
|
</div>
|
|
@@ -182,8 +235,71 @@ function CiliumRuleCard({ rule }: { rule: any }) {
|
|
|
182
235
|
</div>
|
|
183
236
|
)}
|
|
184
237
|
|
|
238
|
+
{toFQDNs.length > 0 && (
|
|
239
|
+
<div className="mb-2">
|
|
240
|
+
<div className="text-xs text-theme-text-tertiary mb-1">To FQDNs</div>
|
|
241
|
+
<div className="flex flex-wrap gap-1">
|
|
242
|
+
{toFQDNs.map((fqdn: any, j: number) => (
|
|
243
|
+
<span key={j} className="badge bg-theme-elevated text-theme-text-secondary">
|
|
244
|
+
{fqdn.matchName || fqdn.matchPattern || JSON.stringify(fqdn)}
|
|
245
|
+
</span>
|
|
246
|
+
))}
|
|
247
|
+
</div>
|
|
248
|
+
</div>
|
|
249
|
+
)}
|
|
250
|
+
|
|
251
|
+
{toServices.length > 0 && (
|
|
252
|
+
<div className="mb-2">
|
|
253
|
+
<div className="text-xs text-theme-text-tertiary mb-1">To Services</div>
|
|
254
|
+
<div className="flex flex-wrap gap-1">
|
|
255
|
+
{toServices.map((svc: any, j: number) => {
|
|
256
|
+
const k8s = svc.k8sService
|
|
257
|
+
const sel = svc.k8sServiceSelector
|
|
258
|
+
if (k8s) {
|
|
259
|
+
const ns = k8s.namespace ? `${k8s.namespace}/` : ''
|
|
260
|
+
return (
|
|
261
|
+
<span key={j} className="badge bg-theme-elevated text-theme-text-secondary">
|
|
262
|
+
{ns}{k8s.serviceName}
|
|
263
|
+
</span>
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
if (sel) {
|
|
267
|
+
return <LabelSelectorDisplay key={j} selector={sel.selector || sel} />
|
|
268
|
+
}
|
|
269
|
+
return (
|
|
270
|
+
<span key={j} className="badge bg-theme-elevated text-theme-text-secondary">
|
|
271
|
+
{JSON.stringify(svc)}
|
|
272
|
+
</span>
|
|
273
|
+
)
|
|
274
|
+
})}
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
)}
|
|
278
|
+
|
|
279
|
+
{toNodes.length > 0 && (
|
|
280
|
+
<div className="mb-2">
|
|
281
|
+
<div className="text-xs text-theme-text-tertiary mb-1">To Nodes</div>
|
|
282
|
+
<div className="space-y-1">
|
|
283
|
+
{toNodes.map((node: any, j: number) => (
|
|
284
|
+
<LabelSelectorDisplay key={j} selector={node} />
|
|
285
|
+
))}
|
|
286
|
+
</div>
|
|
287
|
+
</div>
|
|
288
|
+
)}
|
|
289
|
+
|
|
290
|
+
{toGroups.length > 0 && (
|
|
291
|
+
<div className="mb-2">
|
|
292
|
+
<div className="text-xs text-theme-text-tertiary mb-1">To Groups</div>
|
|
293
|
+
<div className="flex flex-wrap gap-1">
|
|
294
|
+
{toGroups.map((g: any, j: number) => (
|
|
295
|
+
<CloudGroupEntry key={j} group={g} />
|
|
296
|
+
))}
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
)}
|
|
300
|
+
|
|
185
301
|
{toPorts.length > 0 && (
|
|
186
|
-
<div>
|
|
302
|
+
<div className="mb-2">
|
|
187
303
|
<div className="text-xs text-theme-text-tertiary mb-1">Ports</div>
|
|
188
304
|
<div className="flex flex-wrap gap-1">
|
|
189
305
|
{toPorts.map((p: any, j: number) => {
|
|
@@ -195,34 +311,135 @@ function CiliumRuleCard({ rule }: { rule: any }) {
|
|
|
195
311
|
))
|
|
196
312
|
})}
|
|
197
313
|
</div>
|
|
314
|
+
{toPorts.some((p: any) => p.rules) && (
|
|
315
|
+
<div className="mt-2">
|
|
316
|
+
{toPorts.map((p: any, j: number) => {
|
|
317
|
+
if (!p.rules) return null
|
|
318
|
+
return <L7Rules key={j} rules={p.rules} ports={p.ports} />
|
|
319
|
+
})}
|
|
320
|
+
</div>
|
|
321
|
+
)}
|
|
322
|
+
</div>
|
|
323
|
+
)}
|
|
324
|
+
|
|
325
|
+
{icmps.length > 0 && (
|
|
326
|
+
<div className="mb-2">
|
|
327
|
+
<div className="text-xs text-theme-text-tertiary mb-1">ICMP</div>
|
|
328
|
+
<div className="flex flex-wrap gap-1">
|
|
329
|
+
{icmps.map((icmp: any, j: number) => {
|
|
330
|
+
const fields = icmp.fields || []
|
|
331
|
+
return fields.map((f: any, k: number) => (
|
|
332
|
+
<span key={`${j}-${k}`} className="badge bg-theme-elevated text-theme-text-secondary">
|
|
333
|
+
{f.family || 'IPv4'} type {f.type}
|
|
334
|
+
</span>
|
|
335
|
+
))
|
|
336
|
+
})}
|
|
337
|
+
</div>
|
|
198
338
|
</div>
|
|
199
339
|
)}
|
|
200
340
|
|
|
201
|
-
{
|
|
202
|
-
toEndpoints.length === 0 && toEntities.length === 0 && toCIDR.length === 0 && toCIDRSet.length === 0 &&
|
|
203
|
-
toPorts.length === 0 && (
|
|
341
|
+
{!hasRules && (
|
|
204
342
|
<div className="text-xs text-theme-text-tertiary">Allow all</div>
|
|
205
343
|
)}
|
|
206
344
|
</div>
|
|
207
345
|
)
|
|
208
346
|
}
|
|
209
347
|
|
|
210
|
-
function
|
|
211
|
-
const
|
|
212
|
-
const
|
|
348
|
+
function L7Rules({ rules, ports }: { rules: any; ports?: any[] }) {
|
|
349
|
+
const portLabel = ports?.map((p: any) => `${p.protocol || 'TCP'}/${p.port}`).join(', ')
|
|
350
|
+
const httpRules: any[] = rules.http || []
|
|
351
|
+
const dnsRules: any[] = rules.dns || []
|
|
352
|
+
const kafkaRules: any[] = rules.kafka || []
|
|
353
|
+
|
|
354
|
+
if (httpRules.length === 0 && dnsRules.length === 0 && kafkaRules.length === 0) return null
|
|
355
|
+
|
|
213
356
|
return (
|
|
214
|
-
<div className="text-
|
|
215
|
-
{
|
|
216
|
-
<
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
357
|
+
<div className="card-inner text-xs space-y-2">
|
|
358
|
+
{portLabel && (
|
|
359
|
+
<div className="text-theme-text-tertiary">L7 rules for {portLabel}</div>
|
|
360
|
+
)}
|
|
361
|
+
{httpRules.length > 0 && (
|
|
362
|
+
<div>
|
|
363
|
+
<div className="text-theme-text-tertiary mb-1">HTTP</div>
|
|
364
|
+
<div className="space-y-1">
|
|
365
|
+
{httpRules.map((r: any, i: number) => (
|
|
366
|
+
<div key={i} className="flex flex-wrap items-center gap-1">
|
|
367
|
+
{r.method && (
|
|
368
|
+
<span className="badge badge-sm bg-blue-500/20 text-blue-400 border-blue-500/30">
|
|
369
|
+
{r.method}
|
|
370
|
+
</span>
|
|
371
|
+
)}
|
|
372
|
+
{r.path && (
|
|
373
|
+
<span className="text-theme-text-secondary font-mono">{r.path}</span>
|
|
374
|
+
)}
|
|
375
|
+
{r.headers && r.headers.map((h: any, hi: number) => (
|
|
376
|
+
<span key={hi} className="badge badge-sm bg-theme-elevated text-theme-text-secondary">
|
|
377
|
+
{Object.entries(h).map(([k, v]) => `${k}: ${v}`).join(', ')}
|
|
378
|
+
</span>
|
|
379
|
+
))}
|
|
380
|
+
</div>
|
|
381
|
+
))}
|
|
382
|
+
</div>
|
|
383
|
+
</div>
|
|
384
|
+
)}
|
|
385
|
+
{dnsRules.length > 0 && (
|
|
386
|
+
<div>
|
|
387
|
+
<div className="text-theme-text-tertiary mb-1">DNS</div>
|
|
388
|
+
<div className="flex flex-wrap gap-1">
|
|
389
|
+
{dnsRules.map((r: any, i: number) => (
|
|
390
|
+
<span key={i} className="badge badge-sm bg-theme-elevated text-theme-text-secondary">
|
|
391
|
+
{r.matchName || r.matchPattern || JSON.stringify(r)}
|
|
392
|
+
</span>
|
|
393
|
+
))}
|
|
394
|
+
</div>
|
|
395
|
+
</div>
|
|
396
|
+
)}
|
|
397
|
+
{kafkaRules.length > 0 && (
|
|
398
|
+
<div>
|
|
399
|
+
<div className="text-theme-text-tertiary mb-1">Kafka</div>
|
|
400
|
+
<div className="space-y-1">
|
|
401
|
+
{kafkaRules.map((r: any, i: number) => (
|
|
402
|
+
<div key={i} className="flex flex-wrap gap-1">
|
|
403
|
+
{r.apiKey && (
|
|
404
|
+
<span className="badge badge-sm bg-theme-elevated text-theme-text-secondary">
|
|
405
|
+
{r.apiKey}
|
|
406
|
+
</span>
|
|
407
|
+
)}
|
|
408
|
+
{r.topic && (
|
|
409
|
+
<span className="badge badge-sm bg-theme-elevated text-theme-text-secondary">
|
|
410
|
+
topic: {r.topic}
|
|
411
|
+
</span>
|
|
412
|
+
)}
|
|
413
|
+
{r.role && (
|
|
414
|
+
<span className="badge badge-sm bg-theme-elevated text-theme-text-secondary">
|
|
415
|
+
role: {r.role}
|
|
416
|
+
</span>
|
|
417
|
+
)}
|
|
418
|
+
</div>
|
|
419
|
+
))}
|
|
420
|
+
</div>
|
|
421
|
+
</div>
|
|
225
422
|
)}
|
|
226
423
|
</div>
|
|
227
424
|
)
|
|
228
425
|
}
|
|
426
|
+
|
|
427
|
+
function CloudGroupEntry({ group }: { group: any }) {
|
|
428
|
+
const aws = group.aws
|
|
429
|
+
if (aws) {
|
|
430
|
+
const parts: string[] = []
|
|
431
|
+
if (aws.region) parts.push(aws.region)
|
|
432
|
+
if (aws.securityGroupsIds) parts.push(...aws.securityGroupsIds)
|
|
433
|
+
if (aws.securityGroupsNames) parts.push(...aws.securityGroupsNames)
|
|
434
|
+
return (
|
|
435
|
+
<span className="badge bg-theme-elevated text-theme-text-secondary">
|
|
436
|
+
AWS: {parts.join(', ') || JSON.stringify(aws)}
|
|
437
|
+
</span>
|
|
438
|
+
)
|
|
439
|
+
}
|
|
440
|
+
return (
|
|
441
|
+
<span className="badge bg-theme-elevated text-theme-text-secondary">
|
|
442
|
+
{JSON.stringify(group)}
|
|
443
|
+
</span>
|
|
444
|
+
)
|
|
445
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { useState } from 'react'
|
|
2
|
-
import { ShieldCheck, ChevronDown, ChevronRight, CheckCircle2, XCircle } from 'lucide-react'
|
|
1
|
+
import { useState, useMemo } from 'react'
|
|
2
|
+
import { ShieldCheck, ChevronDown, ChevronRight, CheckCircle2, XCircle, Search } from 'lucide-react'
|
|
3
3
|
import { clsx } from 'clsx'
|
|
4
4
|
import { Section, PropertyList, Property, AlertBanner } from '../../ui/drawer-components'
|
|
5
5
|
import { formatAge } from '../resource-utils'
|
|
@@ -12,6 +12,8 @@ interface ClusterComplianceReportRendererProps {
|
|
|
12
12
|
export function ClusterComplianceReportRenderer({ data }: ClusterComplianceReportRendererProps) {
|
|
13
13
|
const [expandedSection, setExpandedSection] = useState(true)
|
|
14
14
|
const [expandedControls, setExpandedControls] = useState<Set<string>>(new Set())
|
|
15
|
+
const [searchTerm, setSearchTerm] = useState('')
|
|
16
|
+
const [showFailedOnly, setShowFailedOnly] = useState(false)
|
|
15
17
|
|
|
16
18
|
const compliance = data.spec?.compliance || {}
|
|
17
19
|
const status = data.status || {}
|
|
@@ -24,18 +26,40 @@ export function ClusterComplianceReportRenderer({ data }: ClusterComplianceRepor
|
|
|
24
26
|
const total = passCount + failCount
|
|
25
27
|
|
|
26
28
|
// Build a map of control definitions for descriptions
|
|
27
|
-
const controlMap =
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
const controlMap = useMemo(() => {
|
|
30
|
+
const map = new Map<string, any>()
|
|
31
|
+
for (const ctrl of controls) {
|
|
32
|
+
if (ctrl.id) map.set(ctrl.id, ctrl)
|
|
33
|
+
}
|
|
34
|
+
return map
|
|
35
|
+
}, [controls])
|
|
31
36
|
|
|
32
37
|
// Sort: failed first by severity, then passed
|
|
33
|
-
const sortedChecks = [...controlChecks].sort((a: any, b: any) => {
|
|
38
|
+
const sortedChecks = useMemo(() => [...controlChecks].sort((a: any, b: any) => {
|
|
34
39
|
const aFail = (a.totalFail || 0) > 0
|
|
35
40
|
const bFail = (b.totalFail || 0) > 0
|
|
36
41
|
if (aFail !== bFail) return aFail ? -1 : 1
|
|
37
42
|
return (SEVERITY_ORDER[a.severity] ?? 99) - (SEVERITY_ORDER[b.severity] ?? 99)
|
|
38
|
-
})
|
|
43
|
+
}), [controlChecks])
|
|
44
|
+
|
|
45
|
+
const filteredChecks = useMemo(() => {
|
|
46
|
+
let result = sortedChecks
|
|
47
|
+
if (showFailedOnly) {
|
|
48
|
+
result = result.filter((c: any) => (c.totalFail || 0) > 0)
|
|
49
|
+
}
|
|
50
|
+
if (searchTerm) {
|
|
51
|
+
const term = searchTerm.toLowerCase()
|
|
52
|
+
result = result.filter((c: any) => {
|
|
53
|
+
const def = controlMap.get(c.id)
|
|
54
|
+
return (c.name || '').toLowerCase().includes(term) ||
|
|
55
|
+
(c.id || '').toLowerCase().includes(term) ||
|
|
56
|
+
(def?.description || '').toLowerCase().includes(term)
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
return result
|
|
60
|
+
}, [sortedChecks, searchTerm, showFailedOnly, controlMap])
|
|
61
|
+
|
|
62
|
+
const hasActiveFilter = searchTerm !== '' || showFailedOnly
|
|
39
63
|
|
|
40
64
|
const toggleControl = (id: string) => {
|
|
41
65
|
setExpandedControls(prev => {
|
|
@@ -82,6 +106,11 @@ export function ClusterComplianceReportRenderer({ data }: ClusterComplianceRepor
|
|
|
82
106
|
<span className="text-sm font-medium text-red-400">{failCount}</span>
|
|
83
107
|
<span className="text-xs text-theme-text-tertiary">failed</span>
|
|
84
108
|
</div>
|
|
109
|
+
{total > 0 && (
|
|
110
|
+
<div className="ml-auto text-sm font-medium text-theme-text-secondary">
|
|
111
|
+
{Math.round((passCount / total) * 100)}% compliant
|
|
112
|
+
</div>
|
|
113
|
+
)}
|
|
85
114
|
</div>
|
|
86
115
|
{total > 0 && (
|
|
87
116
|
<div className="h-3 rounded overflow-hidden flex">
|
|
@@ -102,8 +131,45 @@ export function ClusterComplianceReportRenderer({ data }: ClusterComplianceRepor
|
|
|
102
131
|
{sortedChecks.length} controls
|
|
103
132
|
</button>
|
|
104
133
|
{expandedSection && (
|
|
134
|
+
<>
|
|
135
|
+
{/* Search and filter */}
|
|
136
|
+
<div className="flex items-center gap-2 mb-2">
|
|
137
|
+
<div className="relative flex-1">
|
|
138
|
+
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3 h-3 text-theme-text-tertiary" />
|
|
139
|
+
<input
|
|
140
|
+
type="text"
|
|
141
|
+
placeholder="Filter by control name or ID..."
|
|
142
|
+
value={searchTerm}
|
|
143
|
+
onChange={(e) => setSearchTerm(e.target.value)}
|
|
144
|
+
className="w-full pl-6 pr-2 py-1 text-xs bg-theme-bg border border-theme-border rounded text-theme-text-primary placeholder:text-theme-text-tertiary focus:outline-none focus:ring-1 focus:ring-blue-500/50"
|
|
145
|
+
/>
|
|
146
|
+
</div>
|
|
147
|
+
<button
|
|
148
|
+
onClick={() => setShowFailedOnly(!showFailedOnly)}
|
|
149
|
+
className={clsx(
|
|
150
|
+
'flex items-center gap-1 px-2 py-1 text-xs rounded border transition-colors whitespace-nowrap',
|
|
151
|
+
showFailedOnly
|
|
152
|
+
? 'bg-red-500/15 border-red-500/30 text-red-400'
|
|
153
|
+
: 'bg-theme-bg border-theme-border text-theme-text-tertiary hover:text-theme-text-secondary'
|
|
154
|
+
)}
|
|
155
|
+
>
|
|
156
|
+
<XCircle className="w-3 h-3" />
|
|
157
|
+
Failed
|
|
158
|
+
</button>
|
|
159
|
+
</div>
|
|
160
|
+
{hasActiveFilter && (
|
|
161
|
+
<div className="flex items-center gap-1 mb-2 text-xs text-theme-text-tertiary">
|
|
162
|
+
Showing {filteredChecks.length} of {sortedChecks.length}
|
|
163
|
+
<button
|
|
164
|
+
onClick={() => { setSearchTerm(''); setShowFailedOnly(false) }}
|
|
165
|
+
className="ml-1 text-blue-400 hover:text-blue-300 hover:underline"
|
|
166
|
+
>
|
|
167
|
+
Clear
|
|
168
|
+
</button>
|
|
169
|
+
</div>
|
|
170
|
+
)}
|
|
105
171
|
<div className="space-y-0.5">
|
|
106
|
-
{
|
|
172
|
+
{filteredChecks.map((check: any) => {
|
|
107
173
|
const hasFail = (check.totalFail || 0) > 0
|
|
108
174
|
const controlDef = controlMap.get(check.id)
|
|
109
175
|
const isExpanded = expandedControls.has(check.id)
|
|
@@ -155,7 +221,13 @@ export function ClusterComplianceReportRenderer({ data }: ClusterComplianceRepor
|
|
|
155
221
|
</div>
|
|
156
222
|
)
|
|
157
223
|
})}
|
|
224
|
+
{filteredChecks.length === 0 && (
|
|
225
|
+
<div className="py-4 text-center text-xs text-theme-text-tertiary">
|
|
226
|
+
No controls match the current filter.
|
|
227
|
+
</div>
|
|
228
|
+
)}
|
|
158
229
|
</div>
|
|
230
|
+
</>
|
|
159
231
|
)}
|
|
160
232
|
</Section>
|
|
161
233
|
)}
|