@skyhook-io/k8s-ui 1.4.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/package.json +2 -2
  2. package/src/components/resources/ResourcesView.tsx +112 -49
  3. package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +190 -28
  4. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +22 -0
  5. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +156 -5
  6. package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +28 -1
  7. package/src/components/resources/renderers/CertificateRenderer.tsx +47 -8
  8. package/src/components/resources/renderers/ChallengeRenderer.tsx +22 -3
  9. package/src/components/resources/renderers/CiliumNetworkPolicyRenderer.tsx +262 -45
  10. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +81 -9
  11. package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +10 -3
  12. package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +59 -1
  13. package/src/components/resources/renderers/ClusterNetworkPolicyRenderer.tsx +7 -21
  14. package/src/components/resources/renderers/ExternalSecretRenderer.tsx +3 -5
  15. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +91 -3
  16. package/src/components/resources/renderers/GitRepositoryRenderer.tsx +3 -42
  17. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +3 -41
  18. package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +66 -2
  19. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +31 -1
  20. package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +66 -1
  21. package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +3 -0
  22. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +55 -1
  23. package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +23 -0
  24. package/src/components/resources/renderers/KustomizationRenderer.tsx +38 -3
  25. package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +7 -56
  26. package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +3 -42
  27. package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +3 -8
  28. package/src/components/resources/renderers/PodMonitorRenderer.tsx +5 -7
  29. package/src/components/resources/renderers/PodRenderer.tsx +22 -74
  30. package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +178 -20
  31. package/src/components/resources/renderers/ReplicaSetRenderer.tsx +2 -2
  32. package/src/components/resources/renderers/RolloutRenderer.tsx +105 -3
  33. package/src/components/resources/renderers/SbomReportRenderer.tsx +46 -5
  34. package/src/components/resources/renderers/SealedSecretRenderer.tsx +11 -2
  35. package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +5 -7
  36. package/src/components/resources/renderers/VeleroBackupRenderer.tsx +11 -3
  37. package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +11 -3
  38. package/src/components/resources/renderers/WebhookConfigRenderer.tsx +8 -16
  39. package/src/components/resources/renderers/WorkflowRenderer.tsx +71 -17
  40. package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +46 -1
  41. package/src/components/resources/renderers/karpenter-cells.tsx +18 -3
  42. package/src/components/resources/resource-utils-cnpg.ts +47 -0
  43. package/src/components/resources/resource-utils-karpenter.ts +6 -0
  44. package/src/components/resources/resource-utils-prometheus.ts +50 -6
  45. package/src/components/resources/resource-utils.ts +131 -33
  46. package/src/components/shared/ResourceRendererDispatch.tsx +4 -4
  47. package/src/components/timeline/TimelineList.tsx +11 -4
  48. package/src/components/ui/drawer-components.tsx +48 -1
  49. package/src/theme/variables.css +1 -1
  50. package/src/types/gitops.ts +8 -0
  51. package/src/utils/navigation.test.ts +29 -2
  52. package/src/utils/navigation.ts +45 -24
@@ -1,5 +1,5 @@
1
- import { Shield, ArrowDownToLine, ArrowUpFromLine, Ban } from 'lucide-react'
2
- import { Section, PropertyList, Property } from '../../ui/drawer-components'
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
- <PropertyList>
26
- <Property
27
- label="Endpoint Selector"
28
- value={hasMatchLabels ? undefined : 'All endpoints in namespace'}
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">Endpoint Selector</div>
34
- <div className="flex flex-wrap gap-1">
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
- {spec.nodeSelector && (
34
+ {enableDefaultDeny && (
44
35
  <div className="mt-2">
45
- <div className="text-xs text-theme-text-tertiary mb-1">Node Selector</div>
36
+ <div className="text-xs text-theme-text-tertiary mb-1">Default Deny</div>
46
37
  <div className="flex flex-wrap gap-1">
47
- {Object.entries(spec.nodeSelector.matchLabels || {}).map(([k, v]) => (
48
- <span key={k} className="badge bg-theme-elevated text-theme-text-secondary">
49
- {k}={String(v)}
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
- <SelectorEntry key={j} selector={ep} />
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
- <SelectorEntry key={j} selector={ep} />
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
- {fromEndpoints.length === 0 && fromEntities.length === 0 && fromCIDR.length === 0 && fromCIDRSet.length === 0 &&
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 SelectorEntry({ selector }: { selector: any }) {
211
- const matchLabels = selector.matchLabels || {}
212
- const hasLabels = Object.keys(matchLabels).length > 0
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-sm">
215
- {hasLabels ? (
216
- <span className="inline-flex flex-wrap gap-1 align-middle">
217
- {Object.entries(matchLabels).map(([k, v]) => (
218
- <span key={k} className="badge bg-theme-elevated text-theme-text-secondary">
219
- {k}={String(v)}
220
- </span>
221
- ))}
222
- </span>
223
- ) : (
224
- <span className="text-xs text-theme-text-tertiary">all endpoints</span>
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 = new Map<string, any>()
28
- for (const ctrl of controls) {
29
- if (ctrl.id) controlMap.set(ctrl.id, ctrl)
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
- {sortedChecks.map((check: any) => {
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
  )}
@@ -1,5 +1,5 @@
1
1
  import { Globe, AlertTriangle } from 'lucide-react'
2
- import { Section, PropertyList, Property, ConditionsSection, AlertBanner } from '../../ui/drawer-components'
2
+ import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink } from '../../ui/drawer-components'
3
3
  import {
4
4
  getClusterExternalSecretStatus,
5
5
  getClusterExternalSecretProvisionedNamespaces,
@@ -10,9 +10,10 @@ import {
10
10
 
11
11
  interface ClusterExternalSecretRendererProps {
12
12
  data: any
13
+ onNavigate?: (ref: { kind: string; namespace: string; name: string }) => void
13
14
  }
14
15
 
15
- export function ClusterExternalSecretRenderer({ data }: ClusterExternalSecretRendererProps) {
16
+ export function ClusterExternalSecretRenderer({ data, onNavigate }: ClusterExternalSecretRendererProps) {
16
17
  const status = data.status || {}
17
18
  const conditions = status.conditions || []
18
19
 
@@ -26,6 +27,8 @@ export function ClusterExternalSecretRenderer({ data }: ClusterExternalSecretRen
26
27
  const esSpec = data.spec?.externalSecretSpec
27
28
  const refreshInterval = esSpec?.refreshInterval || '-'
28
29
  const storeRef = esSpec?.secretStoreRef
30
+ const storeName = storeRef?.name
31
+ const storeKindSingular = storeRef?.kind || 'SecretStore'
29
32
  const dataCount = (esSpec?.data || []).length
30
33
  const dataFromCount = (esSpec?.dataFrom || []).length
31
34
 
@@ -119,7 +122,11 @@ export function ClusterExternalSecretRenderer({ data }: ClusterExternalSecretRen
119
122
  <Property label="Refresh Interval" value={refreshInterval} />
120
123
  {storeRef && (
121
124
  <>
122
- <Property label="Store Name" value={storeRef.name || '-'} />
125
+ <Property label="Store Name" value={
126
+ storeName && storeKindSingular === 'ClusterSecretStore'
127
+ ? <ResourceLink name={storeName} kind={storeKindSingular} namespace="" onNavigate={onNavigate} />
128
+ : storeName || '-'
129
+ } />
123
130
  <Property label="Store Kind" value={storeRef.kind || 'SecretStore'} />
124
131
  </>
125
132
  )}
@@ -1,4 +1,4 @@
1
- import { Shield, AlertTriangle, Globe, Lock, Key } from 'lucide-react'
1
+ import { Shield, AlertTriangle, Globe, Lock, Key, Server } from 'lucide-react'
2
2
  import { clsx } from 'clsx'
3
3
  import { Section, PropertyList, Property, ConditionsSection } from '../../ui/drawer-components'
4
4
 
@@ -7,6 +7,15 @@ function detectIssuerType(spec: any): string {
7
7
  if (spec.ca) return 'CA'
8
8
  if (spec.selfSigned !== undefined) return 'SelfSigned'
9
9
  if (spec.vault) return 'Vault'
10
+ if (spec.venafi) return 'Venafi'
11
+ return 'Unknown'
12
+ }
13
+
14
+ function getVaultAuthMethod(auth: any): string {
15
+ if (!auth) return 'Unknown'
16
+ if (auth.tokenSecretRef) return 'Token'
17
+ if (auth.appRole) return 'AppRole'
18
+ if (auth.kubernetes) return 'Kubernetes'
10
19
  return 'Unknown'
11
20
  }
12
21
 
@@ -138,6 +147,55 @@ function IssuerRendererBase({ data, kind }: { data: any; kind: string }) {
138
147
  </Section>
139
148
  )}
140
149
 
150
+ {/* Vault section */}
151
+ {spec.vault && (
152
+ <Section title="Vault Configuration" icon={Server}>
153
+ <PropertyList>
154
+ <Property label="Server" value={spec.vault.server} />
155
+ <Property label="Path" value={spec.vault.path} />
156
+ <Property label="Auth Method" value={getVaultAuthMethod(spec.vault.auth)} />
157
+ {spec.vault.auth?.appRole?.roleId && (
158
+ <Property label="AppRole Role ID" value={spec.vault.auth.appRole.roleId} />
159
+ )}
160
+ {spec.vault.auth?.appRole?.secretRef?.name && (
161
+ <Property label="AppRole Secret Ref" value={spec.vault.auth.appRole.secretRef.name} />
162
+ )}
163
+ {spec.vault.auth?.tokenSecretRef?.name && (
164
+ <Property label="Token Secret Ref" value={spec.vault.auth.tokenSecretRef.name} />
165
+ )}
166
+ {spec.vault.auth?.kubernetes?.role && (
167
+ <Property label="K8s Auth Role" value={spec.vault.auth.kubernetes.role} />
168
+ )}
169
+ {spec.vault.auth?.kubernetes?.secretRef?.name && (
170
+ <Property label="K8s Secret Ref" value={spec.vault.auth.kubernetes.secretRef.name} />
171
+ )}
172
+ <Property label="Custom CA" value={spec.vault.caBundle ? 'Yes' : 'No'} />
173
+ {spec.vault.namespace && (
174
+ <Property label="Vault Namespace" value={spec.vault.namespace} />
175
+ )}
176
+ </PropertyList>
177
+ </Section>
178
+ )}
179
+
180
+ {/* Venafi section */}
181
+ {spec.venafi && (
182
+ <Section title="Venafi Configuration" icon={Shield}>
183
+ <PropertyList>
184
+ <Property label="Zone" value={spec.venafi.zone} />
185
+ <Property
186
+ label="Platform"
187
+ value={spec.venafi.cloud ? 'Cloud' : spec.venafi.tpp ? 'TPP' : '-'}
188
+ />
189
+ {spec.venafi.cloud?.url && (
190
+ <Property label="Cloud URL" value={spec.venafi.cloud.url} />
191
+ )}
192
+ {spec.venafi.tpp?.url && (
193
+ <Property label="TPP URL" value={spec.venafi.tpp.url} />
194
+ )}
195
+ </PropertyList>
196
+ </Section>
197
+ )}
198
+
141
199
  {/* Conditions */}
142
200
  <ConditionsSection conditions={conditions} />
143
201
  </>