@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,3 +1,4 @@
1
+ import type React from 'react'
1
2
  // Renderers for Kyverno's modern CEL policy family (policies.kyverno.io),
2
3
  // stabilized in Kyverno 1.17/1.18 and the family that survives the removal of
3
4
  // kyverno.io Policy/ClusterPolicy planned for 1.20.
@@ -14,6 +15,7 @@
14
15
 
15
16
  import { FileWarning, ShieldCheck, Wand2, Sparkles, Trash2, Clock, Fingerprint, ScrollText } from 'lucide-react'
16
17
  import { Section, PropertyList, Property, ConditionsSection, AlertBanner } from '../../ui/drawer-components'
18
+ import { CronValue } from '../../ui/ScheduleValue'
17
19
  import {
18
20
  KyvernoEvaluationSection,
19
21
  KyvernoExpressionList,
@@ -70,10 +72,11 @@ function ValidationsSection({ validations }: { validations: KyvernoCELValidation
70
72
  )
71
73
  }
72
74
 
73
- export function KyvernoValidatingPolicyRenderer({ data }: { data: any }) {
75
+ export function KyvernoValidatingPolicyRenderer({ data, coverage }: { data: any; coverage?: React.ReactNode }) {
74
76
  return (
75
77
  <div className="space-y-4">
76
78
  <KyvernoPostureHeader data={data} family="validating" />
79
+ {coverage}
77
80
  <ValidationsSection validations={getKyvernoValidations(data)} />
78
81
  <KyvernoMatchScopeSection data={data} />
79
82
  <KyvernoVariablesSection data={data} />
@@ -83,7 +86,7 @@ export function KyvernoValidatingPolicyRenderer({ data }: { data: any }) {
83
86
  )
84
87
  }
85
88
 
86
- export function KyvernoImageValidatingPolicyRenderer({ data }: { data: any }) {
89
+ export function KyvernoImageValidatingPolicyRenderer({ data, coverage }: { data: any; coverage?: React.ReactNode }) {
87
90
  const attestors = getKyvernoAttestors(data)
88
91
  const attestations = getKyvernoAttestations(data)
89
92
  const imageRefs = getKyvernoImageReferences(data)
@@ -92,6 +95,7 @@ export function KyvernoImageValidatingPolicyRenderer({ data }: { data: any }) {
92
95
  return (
93
96
  <div className="space-y-4">
94
97
  <KyvernoPostureHeader data={data} family="imageValidating" />
98
+ {coverage}
95
99
 
96
100
  {imageRefs.length > 0 && (
97
101
  <Section title={`Image References (${imageRefs.length})`} icon={ScrollText}>
@@ -163,13 +167,14 @@ export function KyvernoImageValidatingPolicyRenderer({ data }: { data: any }) {
163
167
  )
164
168
  }
165
169
 
166
- export function KyvernoMutatingPolicyRenderer({ data }: { data: any }) {
170
+ export function KyvernoMutatingPolicyRenderer({ data, coverage }: { data: any; coverage?: React.ReactNode }) {
167
171
  const mutations = getKyvernoMutations(data)
168
172
  const reinvocation = getKyvernoReinvocationPolicy(data)
169
173
 
170
174
  return (
171
175
  <div className="space-y-4">
172
176
  <KyvernoPostureHeader data={data} family="mutating" />
177
+ {coverage}
173
178
 
174
179
  {mutations.length > 0 && (
175
180
  <Section title={`Mutations (${mutations.length})`} icon={Wand2}>
@@ -218,13 +223,14 @@ export function KyvernoMutatingPolicyRenderer({ data }: { data: any }) {
218
223
  )
219
224
  }
220
225
 
221
- export function KyvernoGeneratingPolicyRenderer({ data }: { data: any }) {
226
+ export function KyvernoGeneratingPolicyRenderer({ data, coverage }: { data: any; coverage?: React.ReactNode }) {
222
227
  const generations = getKyvernoGenerations(data)
223
228
  const settings = getKyvernoGenerationSettings(data)
224
229
 
225
230
  return (
226
231
  <div className="space-y-4">
227
232
  <KyvernoPostureHeader data={data} family="generating" />
233
+ {coverage}
228
234
 
229
235
  {generations.length > 0 && (
230
236
  <Section title={`Generates (${generations.length})`} icon={Sparkles}>
@@ -274,7 +280,7 @@ export function KyvernoGeneratingPolicyRenderer({ data }: { data: any }) {
274
280
  )
275
281
  }
276
282
 
277
- export function KyvernoDeletingPolicyRenderer({ data }: { data: any }) {
283
+ export function KyvernoDeletingPolicyRenderer({ data, coverage }: { data: any; coverage?: React.ReactNode }) {
278
284
  const schedule = getKyvernoSchedule(data)
279
285
  const conditions = getKyvernoDeletionConditions(data)
280
286
  const propagation = getKyvernoDeletionPropagationPolicy(data)
@@ -282,12 +288,13 @@ export function KyvernoDeletingPolicyRenderer({ data }: { data: any }) {
282
288
  return (
283
289
  <div className="space-y-4">
284
290
  <KyvernoPostureHeader data={data} family="deleting" />
291
+ {coverage}
285
292
 
286
293
  <Section title="Schedule" icon={Clock}>
287
294
  <PropertyList>
288
295
  <Property
289
296
  label="Cron"
290
- value={schedule ? <span className="font-mono">{schedule}</span> : 'Not set — this policy will not run'}
297
+ value={schedule ? <CronValue cron={schedule} /> : 'Not set — this policy will not run'}
291
298
  />
292
299
  {propagation && <Property label="Deletion Propagation" value={propagation} />}
293
300
  </PropertyList>
@@ -14,6 +14,7 @@ import {
14
14
  AlertBanner,
15
15
  LabelSelectorDisplay,
16
16
  } from '../../ui/drawer-components'
17
+ import { CronValue, TimeValue } from '../../ui/ScheduleValue'
17
18
  import { KyvernoExpressionList } from './KyvernoPolicyShared'
18
19
  import {
19
20
  getKyvernoCleanupConditions,
@@ -225,9 +226,9 @@ export function KyvernoCleanupPolicyRenderer({ data }: { data: any }) {
225
226
 
226
227
  <Section title="Schedule" icon={Clock}>
227
228
  <PropertyList>
228
- <Property label="Cron" value={schedule ? <span className="font-mono">{schedule}</span> : 'Not set'} />
229
+ <Property label="Cron" value={schedule ? <CronValue cron={schedule} /> : 'Not set'} />
229
230
  {propagation && <Property label="Deletion Propagation" value={propagation} />}
230
- {lastExecution && <Property label="Last Execution" value={lastExecution} />}
231
+ {lastExecution && <Property label="Last Execution" value={<TimeValue timestamp={lastExecution} />} />}
231
232
  </PropertyList>
232
233
  </Section>
233
234
 
@@ -0,0 +1,31 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { getKyvernoContextRefresh } from './KyvernoGlobalContextRenderer'
3
+
4
+ /**
5
+ * Kyverno records a successful refresh and records nothing at all for a failed
6
+ * one — no message, no condition. Verified on 1.18.2: `cluster-namespaces` came
7
+ * back with `lastRefreshTime`, `broken-lookup` came back with an empty status.
8
+ * Age is therefore the only thing separating "failing" from "not yet".
9
+ */
10
+ describe('global context refresh state', () => {
11
+ const ago = (ms: number) => new Date(Date.now() - ms).toISOString()
12
+
13
+ it('is healthy once it has refreshed', () => {
14
+ const r = getKyvernoContextRefresh({ status: { lastRefreshTime: ago(60_000) } })
15
+ expect(r.refreshed).toBe(true)
16
+ expect(r.level).toBe('healthy')
17
+ })
18
+
19
+ it('is unknown while the entry is young enough not to have run yet', () => {
20
+ const r = getKyvernoContextRefresh({ metadata: { creationTimestamp: ago(10_000) }, status: {} })
21
+ expect(r.level).toBe('unknown')
22
+ })
23
+
24
+ // Old and never refreshed is the diagnosis; anything sooner would accuse the
25
+ // entry before its first attempt.
26
+ it('is unhealthy once an old entry still has no refresh', () => {
27
+ const r = getKyvernoContextRefresh({ metadata: { creationTimestamp: ago(10 * 60_000) }, status: {} })
28
+ expect(r.level).toBe('unhealthy')
29
+ expect(r.refreshed).toBe(false)
30
+ })
31
+ })
@@ -0,0 +1,124 @@
1
+ import { Globe, RefreshCw } from 'lucide-react'
2
+ import { clsx } from 'clsx'
3
+ import { Section, PropertyList, Property, AlertBanner, ResourceLink } from '../../ui/drawer-components'
4
+ import { HEALTH_BADGE_COLORS } from '../../../utils/badge-colors'
5
+ import { formatAge, healthColors } from '../resource-utils'
6
+ import type { StatusBadge } from '../resource-utils'
7
+
8
+ type Nav = (ref: { kind: string; namespace: string; name: string; group?: string }) => void
9
+
10
+ /**
11
+ * Whether the entry has ever produced data.
12
+ *
13
+ * Kyverno records success as `status.lastRefreshTime` and records failure as
14
+ * nothing at all — there is no message, no condition, no reason. So the only
15
+ * honest reading is "it has refreshed" versus "it has never refreshed", and the
16
+ * age of the entry is what turns the second into a diagnosis. Inventing a cause
17
+ * for a silent failure would be worse than naming the silence.
18
+ */
19
+ export function getKyvernoContextRefresh(resource: any): {
20
+ refreshed: boolean
21
+ at?: string
22
+ level: 'healthy' | 'unhealthy' | 'unknown'
23
+ } {
24
+ const at = resource?.status?.lastRefreshTime
25
+ if (typeof at === 'string' && at !== '') return { refreshed: true, at, level: 'healthy' }
26
+ // Young enough that the first refresh may simply not have happened yet.
27
+ const created = resource?.metadata?.creationTimestamp
28
+ const ageMs = created ? Date.now() - new Date(created).getTime() : 0
29
+ return { refreshed: false, level: ageMs > 2 * 60 * 1000 ? 'unhealthy' : 'unknown' }
30
+ }
31
+
32
+ /**
33
+ * GlobalContextEntry — external data a policy reads while it evaluates.
34
+ *
35
+ * It matters because a policy referencing an entry that never resolves fails at
36
+ * evaluation time, and the failure surfaces as a policy error with no obvious
37
+ * cause. This page is where that cause lives.
38
+ */
39
+ export function KyvernoGlobalContextRenderer({ data, onNavigate }: { data: any; onNavigate?: Nav }) {
40
+ const spec = data?.spec ?? {}
41
+ const api = spec.apiCall
42
+ const cm = spec.configMap
43
+ const refresh = getKyvernoContextRefresh(data)
44
+
45
+ return (
46
+ <>
47
+ {refresh.level === 'unhealthy' && (
48
+ <AlertBanner
49
+ variant="error"
50
+ title="This entry has never refreshed"
51
+ // Deliberately not a diagnosis: Kyverno records no reason for a failed
52
+ // refresh, so the page reports the fact and where to look.
53
+ message="Kyverno records no reason when a refresh fails. Any policy referencing this entry will error while it evaluates. Check the source below is reachable and that Kyverno may read it."
54
+ />
55
+ )}
56
+
57
+ <Section title="Refresh" icon={RefreshCw} defaultExpanded>
58
+ <PropertyList>
59
+ <Property
60
+ label="State"
61
+ value={
62
+ <span
63
+ className={clsx('badge', HEALTH_BADGE_COLORS[refresh.level as keyof typeof HEALTH_BADGE_COLORS])}
64
+ >
65
+ {refresh.refreshed ? 'Refreshed' : refresh.level === 'unhealthy' ? 'Never refreshed' : 'Not refreshed yet'}
66
+ </span>
67
+ }
68
+ />
69
+ {refresh.at && <Property label="Last Refreshed" value={`${formatAge(refresh.at)} ago`} />}
70
+ {api?.refreshInterval && <Property label="Interval" value={api.refreshInterval} />}
71
+ {api?.retryLimit != null && <Property label="Retry Limit" value={String(api.retryLimit)} />}
72
+ </PropertyList>
73
+ {!refresh.refreshed && refresh.level === 'unknown' && (
74
+ <div className="mt-2 pt-2 border-t border-theme-border text-xs text-theme-text-secondary">
75
+ Created moments ago — the first refresh may not have run yet.
76
+ </div>
77
+ )}
78
+ </Section>
79
+
80
+ <Section title="Source" icon={Globe} defaultExpanded>
81
+ {api ? (
82
+ <PropertyList>
83
+ <Property label="Kind" value="Kubernetes API call" />
84
+ <Property label="Method" value={api.method || 'GET'} />
85
+ <Property label="Path" value={<span className="font-mono text-xs">{api.urlPath || '-'}</span>} />
86
+ {api.jmesPath && (
87
+ <Property label="Filter" value={<span className="font-mono text-xs">{api.jmesPath}</span>} />
88
+ )}
89
+ </PropertyList>
90
+ ) : cm ? (
91
+ <PropertyList>
92
+ <Property label="Kind" value="ConfigMap" />
93
+ <Property
94
+ label="ConfigMap"
95
+ value={
96
+ <ResourceLink
97
+ name={cm.name}
98
+ kind="configmaps"
99
+ namespace={cm.namespace ?? ''}
100
+ onNavigate={onNavigate}
101
+ />
102
+ }
103
+ />
104
+ {cm.namespace && <Property label="Namespace" value={cm.namespace} />}
105
+ </PropertyList>
106
+ ) : (
107
+ <div className="text-sm text-theme-text-tertiary">
108
+ No API call or ConfigMap source is declared, so this entry can never produce data.
109
+ </div>
110
+ )}
111
+ </Section>
112
+ </>
113
+ )
114
+ }
115
+
116
+ /** List badge for GlobalContextEntry, mirroring the detail page's three states. */
117
+ export function getKyvernoGlobalContextStatus(resource: any): StatusBadge {
118
+ const r = getKyvernoContextRefresh(resource)
119
+ if (r.refreshed) return { text: 'Refreshed', color: healthColors.healthy, level: 'healthy' }
120
+ if (r.level === 'unhealthy') {
121
+ return { text: 'Never Refreshed', color: healthColors.unhealthy, level: 'unhealthy' }
122
+ }
123
+ return { text: 'Pending', color: healthColors.unknown, level: 'unknown' }
124
+ }
@@ -1,3 +1,4 @@
1
+ import type React from 'react'
1
2
  import { Shield, ShieldCheck, ShieldAlert, FileWarning, ListChecks, ChevronDown, ChevronRight } from 'lucide-react'
2
3
  import { clsx } from 'clsx'
3
4
  import { useState } from 'react'
@@ -225,6 +226,13 @@ export function PolicyReportRenderer({ data }: PolicyReportRendererProps) {
225
226
 
226
227
  interface KyvernoPolicyRendererProps {
227
228
  data: any
229
+ /** Filled by the host with the policy's coverage section — what this policy
230
+ * actually decided about the cluster. */
231
+ coverage?: React.ReactNode
232
+ /** Filled by the host with the work this policy has queued and not finished.
233
+ * The failure worth catching here is a pile-up, not one request, so it
234
+ * belongs beside the policy rather than only on its own page. */
235
+ queued?: React.ReactNode
228
236
  }
229
237
 
230
238
  const ruleTypeColorMap: Record<string, string> = {
@@ -234,7 +242,7 @@ const ruleTypeColorMap: Record<string, string> = {
234
242
  verifyImages: 'bg-orange-500/20 text-orange-400',
235
243
  }
236
244
 
237
- export function KyvernoPolicyRenderer({ data }: KyvernoPolicyRendererProps) {
245
+ export function KyvernoPolicyRenderer({ data, coverage, queued }: KyvernoPolicyRendererProps) {
238
246
  const spec = data.spec || {}
239
247
  const status = data.status || {}
240
248
  const conditions = status.conditions || []
@@ -249,9 +257,18 @@ export function KyvernoPolicyRenderer({ data }: KyvernoPolicyRendererProps) {
249
257
 
250
258
  return (
251
259
  <>
260
+ {coverage}
261
+ {queued}
252
262
  {/* Configuration */}
253
263
  <Section title="Configuration" icon={Shield}>
254
264
  <PropertyList>
265
+ {/* Audit / Enforce governs the rules that can REJECT — validation and
266
+ image verification. A policy whose rules only generate or mutate
267
+ carries the field inertly, and showing it as the policy's mode
268
+ reads as "this one only reports" about a policy that is actively
269
+ writing to the cluster. status.rulecount is Kyverno's own count,
270
+ so this is checkable rather than guessed. */}
271
+ {(ruleCountByType.validate > 0 || ruleCountByType.verifyImages > 0) && (
255
272
  <Property label="Failure Action" value={
256
273
  <span className={clsx(
257
274
  'badge',
@@ -260,6 +277,7 @@ export function KyvernoPolicyRenderer({ data }: KyvernoPolicyRendererProps) {
260
277
  {action}
261
278
  </span>
262
279
  } />
280
+ )}
263
281
  <Property label="Background" value={background ? 'Enabled' : 'Disabled'} />
264
282
  {spec.webhookTimeoutSeconds && (
265
283
  <Property label="Webhook Timeout" value={`${spec.webhookTimeoutSeconds}s`} />
@@ -342,7 +360,15 @@ export function KyvernoPolicyRenderer({ data }: KyvernoPolicyRendererProps) {
342
360
  {status.rulecount.validate !== undefined && <Property label="Validate" value={status.rulecount.validate} />}
343
361
  {status.rulecount.mutate !== undefined && <Property label="Mutate" value={status.rulecount.mutate} />}
344
362
  {status.rulecount.generate !== undefined && <Property label="Generate" value={status.rulecount.generate} />}
345
- {status.rulecount.verifyImages !== undefined && <Property label="Verify Images" value={status.rulecount.verifyImages} />}
363
+ {/* All-lowercase in Kyverno's own status, so the camelCase read
364
+ dropped the only non-zero row on an image-verification policy
365
+ and left three zeros reading as "no rules". */}
366
+ {(status.rulecount.verifyImages ?? status.rulecount.verifyimages) !== undefined && (
367
+ <Property
368
+ label="Verify Images"
369
+ value={status.rulecount.verifyImages ?? status.rulecount.verifyimages}
370
+ />
371
+ )}
346
372
  </PropertyList>
347
373
  </Section>
348
374
  )}
@@ -0,0 +1,303 @@
1
+ import { useState } from 'react'
2
+ import { clsx } from 'clsx'
3
+ import { ListChecks, Workflow, Boxes, FileWarning } from 'lucide-react'
4
+ import { Section, PropertyList, Property, AlertBanner, ResourceLink } from '../../ui/drawer-components'
5
+ import { HEALTH_BADGE_COLORS, SEVERITY_BADGE } from '../../../utils/badge-colors'
6
+ import { formatAge } from '../resource-utils'
7
+ import {
8
+ getKyvernoRequestType,
9
+ getKyvernoRequestTriggers,
10
+ getKyvernoRequestGenerated,
11
+ getKyvernoRequestState,
12
+ getKyvernoRequestPolicy,
13
+ getKyvernoRequestMessage,
14
+ getKyvernoReportSubject,
15
+ getKyvernoReportSource,
16
+ getKyvernoReportSummary,
17
+ getKyvernoReportResults,
18
+ kyvernoReportTimestamp,
19
+ } from '../resource-utils-kyverno-queue'
20
+
21
+ type Nav = (ref: { kind: string; namespace: string; name: string; group?: string }) => void
22
+
23
+ /** Long trigger lists fold at the same point as the house relationship lists. */
24
+ const FOLD = 10
25
+
26
+ function RefList({
27
+ refs,
28
+ onNavigate,
29
+ empty,
30
+ }: {
31
+ refs: Array<{ apiVersion?: string; kind?: string; name?: string; namespace?: string }>
32
+ onNavigate?: Nav
33
+ empty: string
34
+ }) {
35
+ const [expanded, setExpanded] = useState(false)
36
+ if (refs.length === 0) return <div className="text-sm text-theme-text-tertiary">{empty}</div>
37
+
38
+ const shown = expanded ? refs : refs.slice(0, FOLD)
39
+ return (
40
+ <div className="space-y-1.5">
41
+ <div className="flex flex-wrap gap-1.5">
42
+ {shown.map((r, i) => (
43
+ <ResourceLink
44
+ key={`${r.kind}/${r.namespace}/${r.name}/${i}`}
45
+ name={r.name ?? ''}
46
+ kind={r.kind ?? ''}
47
+ namespace={r.namespace ?? ''}
48
+ group={r.apiVersion?.includes('/') ? r.apiVersion.split('/')[0] : undefined}
49
+ onNavigate={onNavigate}
50
+ />
51
+ ))}
52
+ </div>
53
+ {refs.length > FOLD && (
54
+ <button
55
+ className="text-xs text-theme-text-secondary hover:text-theme-text-primary"
56
+ onClick={() => setExpanded(!expanded)}
57
+ >
58
+ {expanded ? 'Show fewer' : `Show all ${refs.length}`}
59
+ </button>
60
+ )}
61
+ </div>
62
+ )
63
+ }
64
+
65
+ /**
66
+ * UpdateRequest — the record of work Kyverno queued rather than did inline.
67
+ *
68
+ * It is where a generation or a mutate-existing that silently stopped is
69
+ * diagnosed: the policy looks Ready, the target never appears, and the only
70
+ * evidence is a request sitting in Pending.
71
+ *
72
+ * The two request types are shaped so differently that one renderer reading a
73
+ * single field set would be blank for half of them — see the accessors.
74
+ */
75
+ export function KyvernoUpdateRequestRenderer({ data, onNavigate }: { data: any; onNavigate?: Nav }) {
76
+ const type = getKyvernoRequestType(data)
77
+ const state = getKyvernoRequestState(data)
78
+ const triggers = getKyvernoRequestTriggers(data)
79
+ const generated = getKyvernoRequestGenerated(data)
80
+ const policy = getKyvernoRequestPolicy(data)
81
+ const message = getKyvernoRequestMessage(data)
82
+ // Every trigger in one request comes from the same rule; a generate request
83
+ // repeats it per entry, a mutate request states it once.
84
+ const rule = data?.spec?.rule || triggers.find((t) => t.rule)?.rule
85
+ const deletesDownstream = data?.spec?.deleteDownstream === true || triggers.some((t) => t.deleteDownstream)
86
+
87
+ return (
88
+ <>
89
+ {state.level === 'unhealthy' && (
90
+ <AlertBanner
91
+ variant="error"
92
+ title="This request failed"
93
+ message={message || 'Kyverno could not complete this request. The resources below were not updated.'}
94
+ />
95
+ )}
96
+ {state.level === 'degraded' && (
97
+ <AlertBanner
98
+ variant="warning"
99
+ title="Still queued"
100
+ // Pending is normal for seconds and a symptom for longer, and this
101
+ // object carries no timestamp of its own beyond creation.
102
+ message="Kyverno has not processed this request yet. If it stays here, the background controller is not running or cannot reach what it needs."
103
+ />
104
+ )}
105
+
106
+ <Section title="Request" icon={Workflow} defaultExpanded>
107
+ <PropertyList>
108
+ <Property
109
+ label="State"
110
+ value={
111
+ <span className={clsx('badge', HEALTH_BADGE_COLORS[state.level as keyof typeof HEALTH_BADGE_COLORS])}>
112
+ {state.text}
113
+ </span>
114
+ }
115
+ />
116
+ <Property label="Type" value={type === 'unknown' ? '-' : type} />
117
+ {policy && (
118
+ <Property
119
+ label="Policy"
120
+ value={
121
+ // `spec.policy` is a bare name with no kind and no namespace, and
122
+ // a namespaced Policy may share it with a ClusterPolicy. A
123
+ // qualified name is the one case that identifies itself; anything
124
+ // else links to a page that may not be the policy that queued
125
+ // this, so it stays plain text rather than guessing.
126
+ policy.includes('/') ? (
127
+ <ResourceLink
128
+ name={policy.split('/')[1]}
129
+ // `policies` is what the API serves and what a URL can be
130
+ // built from; `kyvernopolicies` is an internal display alias
131
+ // and 400s when asked for. The group is what disambiguates.
132
+ kind="policies"
133
+ namespace={policy.split('/')[0]}
134
+ group="kyverno.io"
135
+ label={policy}
136
+ onNavigate={onNavigate}
137
+ />
138
+ ) : (
139
+ policy
140
+ )
141
+ }
142
+ />
143
+ )}
144
+ {rule && <Property label="Rule" value={rule} />}
145
+ <Property
146
+ label="On trigger delete"
147
+ value={
148
+ deletesDownstream ? (
149
+ <span className="text-warning-text">
150
+ Removes what this rule created for it
151
+ </span>
152
+ ) : (
153
+ 'Leaves what this rule created in place'
154
+ )
155
+ }
156
+ />
157
+ {data?.metadata?.creationTimestamp && (
158
+ <Property label="Queued" value={`${formatAge(data.metadata.creationTimestamp)} ago`} />
159
+ )}
160
+ </PropertyList>
161
+ {message && state.level !== 'unhealthy' && (
162
+ <div className="mt-2 pt-2 border-t border-theme-border text-xs text-theme-text-secondary">{message}</div>
163
+ )}
164
+ </Section>
165
+
166
+ <Section title={`Triggered By (${triggers.length})`} icon={ListChecks} defaultExpanded>
167
+ <RefList
168
+ refs={triggers}
169
+ onNavigate={onNavigate}
170
+ // A queued generate request has not been given its triggers yet, so
171
+ // the empty list means "not written down", not "acts on nothing".
172
+ empty={
173
+ state.level === 'degraded'
174
+ ? 'Kyverno has not recorded what this request is for yet.'
175
+ : 'This request names no trigger, so there is nothing for it to act on.'
176
+ }
177
+ />
178
+ </Section>
179
+
180
+ {type === 'generate' && (
181
+ <Section title={`Created (${generated.length})`} icon={Boxes} defaultExpanded>
182
+ <RefList
183
+ refs={generated}
184
+ onNavigate={onNavigate}
185
+ empty={
186
+ state.level === 'healthy'
187
+ ? 'Kyverno records nothing created for this request.'
188
+ : 'Nothing has been created yet.'
189
+ }
190
+ />
191
+ </Section>
192
+ )}
193
+ </>
194
+ )
195
+ }
196
+
197
+ // Same mapping the policy coverage view uses, so a result reads identically
198
+ // wherever it appears.
199
+ const RESULT_TONE: Record<string, string> = {
200
+ pass: SEVERITY_BADGE.success,
201
+ fail: SEVERITY_BADGE.error,
202
+ warn: SEVERITY_BADGE.warning,
203
+ error: SEVERITY_BADGE.alert,
204
+ skip: SEVERITY_BADGE.neutral,
205
+ }
206
+
207
+ /**
208
+ * EphemeralReport — a background scan's findings for one resource, before they
209
+ * are folded into a PolicyReport.
210
+ *
211
+ * Worth a surface because it is what exists during a scan, and because a
212
+ * cluster mid-migration can show findings here that have not reached any
213
+ * PolicyReport yet. It is also genuinely short-lived: an empty list means the
214
+ * scan has finished, not that nothing was checked.
215
+ */
216
+ export function KyvernoEphemeralReportRenderer({ data, onNavigate }: { data: any; onNavigate?: Nav }) {
217
+ const subject = getKyvernoReportSubject(data)
218
+ const summary = getKyvernoReportSummary(data)
219
+ const results = getKyvernoReportResults(data)
220
+ const source = getKyvernoReportSource(data)
221
+
222
+ return (
223
+ <>
224
+ <Section title="Scan" icon={FileWarning} defaultExpanded>
225
+ <PropertyList>
226
+ <Property
227
+ label="About"
228
+ value={
229
+ subject?.name ? (
230
+ <ResourceLink
231
+ name={subject.name}
232
+ kind={subject.kind ?? ''}
233
+ namespace={subject.namespace ?? ''}
234
+ group={subject.apiVersion?.includes('/') ? subject.apiVersion.split('/')[0] : undefined}
235
+ onNavigate={onNavigate}
236
+ />
237
+ ) : subject?.kind ? (
238
+ // The subject is gone; the labels still say what it was.
239
+ `${subject.kind} (deleted)`
240
+ ) : (
241
+ '-'
242
+ )
243
+ }
244
+ />
245
+ {source && <Property label="Produced by" value={source.replace(/-/g, ' ')} />}
246
+ <Property
247
+ label="Outcome"
248
+ value={
249
+ summary.total === 0
250
+ ? 'Nothing was checked'
251
+ : [
252
+ summary.fail ? `${summary.fail} fail` : '',
253
+ summary.error ? `${summary.error} error` : '',
254
+ summary.warn ? `${summary.warn} warn` : '',
255
+ summary.pass ? `${summary.pass} pass` : '',
256
+ summary.skip ? `${summary.skip} skipped` : '',
257
+ ]
258
+ .filter(Boolean)
259
+ .join(' · ')
260
+ }
261
+ />
262
+ </PropertyList>
263
+ </Section>
264
+
265
+ <Section title={`Results (${results.length})`} icon={ListChecks} defaultExpanded>
266
+ {results.length === 0 ? (
267
+ <div className="text-sm text-theme-text-tertiary">
268
+ This report carries no results. Kyverno deletes these once the scan is folded into a
269
+ PolicyReport, so an empty one is finished work rather than a problem.
270
+ </div>
271
+ ) : (
272
+ <div className="space-y-1">
273
+ {results.map((r: any, i: number) => {
274
+ const at = kyvernoReportTimestamp(r?.timestamp)
275
+ return (
276
+ <div
277
+ key={`${r?.policy}/${r?.rule}/${i}`}
278
+ className="flex items-baseline gap-2 text-xs py-1 border-b border-theme-border/30 last:border-b-0"
279
+ >
280
+ <span
281
+ className={clsx('badge badge-sm shrink-0', RESULT_TONE[r?.result] || SEVERITY_BADGE.neutral)}
282
+ >
283
+ {r?.result || '-'}
284
+ </span>
285
+ <span className="text-theme-text-secondary truncate">{r?.policy || '-'}</span>
286
+ {r?.rule && <span className="text-theme-text-tertiary truncate">/ {r.rule}</span>}
287
+ {r?.message && (
288
+ <span className="text-theme-text-tertiary truncate" title={r.message}>
289
+ {r.message}
290
+ </span>
291
+ )}
292
+ {at && (
293
+ <span className="ml-auto shrink-0 text-theme-text-tertiary">{formatAge(at)} ago</span>
294
+ )}
295
+ </div>
296
+ )
297
+ })}
298
+ </div>
299
+ )}
300
+ </Section>
301
+ </>
302
+ )
303
+ }