@skyhook-io/k8s-ui 1.11.0 → 1.13.0

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 (123) hide show
  1. package/package.json +4 -4
  2. package/src/components/gitops/GitOpsStatusBadge.tsx +40 -0
  3. package/src/components/gitops/GitOpsTableView.tsx +22 -2
  4. package/src/components/gitops/ReconcilingIndicator.test.tsx +31 -0
  5. package/src/components/gitops/detail-helpers.ts +4 -1
  6. package/src/components/issues/severity.ts +1 -0
  7. package/src/components/resources/ResourcesView.empty-note.test.ts +41 -0
  8. package/src/components/resources/ResourcesView.tsx +412 -5
  9. package/src/components/resources/index.ts +27 -0
  10. package/src/components/resources/kyverno-legacy-action.test.ts +98 -0
  11. package/src/components/resources/renderers/AlertRenderer.tsx +4 -1
  12. package/src/components/resources/renderers/AnalysisRunRenderer.test.tsx +241 -0
  13. package/src/components/resources/renderers/AnalysisRunRenderer.tsx +166 -0
  14. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +31 -2
  15. package/src/components/resources/renderers/CNPGClusterRenderer.test.tsx +33 -0
  16. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +71 -4
  17. package/src/components/resources/renderers/CNPGDeclarativeRenderer.tsx +243 -0
  18. package/src/components/resources/renderers/CNPGObjectStoreRenderer.test.tsx +99 -0
  19. package/src/components/resources/renderers/CNPGObjectStoreRenderer.tsx +235 -0
  20. package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +11 -0
  21. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +23 -1
  22. package/src/components/resources/renderers/CNPGShared.tsx +27 -0
  23. package/src/components/resources/renderers/CalicoHostEndpointRenderer.tsx +71 -0
  24. package/src/components/resources/renderers/CalicoIPPoolRenderer.tsx +32 -0
  25. package/src/components/resources/renderers/CalicoNetworkPolicyDiagram.test.tsx +205 -0
  26. package/src/components/resources/renderers/CalicoNetworkPolicyDiagram.tsx +537 -0
  27. package/src/components/resources/renderers/CalicoNetworkPolicyRenderer.test.tsx +223 -0
  28. package/src/components/resources/renderers/CalicoNetworkPolicyRenderer.tsx +670 -0
  29. package/src/components/resources/renderers/CalicoTierRenderer.tsx +19 -0
  30. package/src/components/resources/renderers/CompositeRenderer.test.tsx +81 -0
  31. package/src/components/resources/renderers/CompositeRenderer.tsx +67 -7
  32. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +4 -1
  33. package/src/components/resources/renderers/GitRepositoryRenderer.tsx +4 -1
  34. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +4 -1
  35. package/src/components/resources/renderers/KustomizationRenderer.tsx +4 -1
  36. package/src/components/resources/renderers/KyvernoCELPolicyRenderers.tsx +13 -6
  37. package/src/components/resources/renderers/KyvernoExceptionRenderers.tsx +3 -2
  38. package/src/components/resources/renderers/KyvernoGlobalContextRenderer.test.ts +31 -0
  39. package/src/components/resources/renderers/KyvernoGlobalContextRenderer.tsx +124 -0
  40. package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +28 -2
  41. package/src/components/resources/renderers/KyvernoQueueRenderers.tsx +303 -0
  42. package/src/components/resources/renderers/LookupFailureNote.test.tsx +60 -0
  43. package/src/components/resources/renderers/LookupFailureNote.tsx +42 -0
  44. package/src/components/resources/renderers/NetworkPolicyDiagram.tsx +27 -26
  45. package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +6 -3
  46. package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +4 -1
  47. package/src/components/resources/renderers/PodRenderer.tsx +13 -0
  48. package/src/components/resources/renderers/PolicyCoverageSection.test.tsx +348 -0
  49. package/src/components/resources/renderers/PolicyCoverageSection.tsx +939 -0
  50. package/src/components/resources/renderers/PolicySection.test.tsx +159 -0
  51. package/src/components/resources/renderers/PolicySection.tsx +197 -0
  52. package/src/components/resources/renderers/RolloutRenderer.test.tsx +491 -0
  53. package/src/components/resources/renderers/RolloutRenderer.tsx +391 -92
  54. package/src/components/resources/renderers/VeleroBSLRenderer.test.tsx +374 -0
  55. package/src/components/resources/renderers/VeleroBSLRenderer.tsx +179 -3
  56. package/src/components/resources/renderers/VeleroBackupRenderer.test.tsx +221 -0
  57. package/src/components/resources/renderers/VeleroBackupRenderer.tsx +119 -6
  58. package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.test.tsx +69 -0
  59. package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.tsx +96 -0
  60. package/src/components/resources/renderers/VeleroRestoreRenderer.test.tsx +55 -0
  61. package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +22 -4
  62. package/src/components/resources/renderers/VeleroRunMessages.test.tsx +152 -0
  63. package/src/components/resources/renderers/VeleroRunMessages.tsx +139 -0
  64. package/src/components/resources/renderers/VeleroScheduleRenderer.test.tsx +88 -0
  65. package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +16 -6
  66. package/src/components/resources/renderers/WorkloadRenderer.tsx +15 -1
  67. package/src/components/resources/renderers/calico-cells.tsx +177 -0
  68. package/src/components/resources/renderers/cnpg-cells.tsx +100 -0
  69. package/src/components/resources/renderers/index.ts +16 -0
  70. package/src/components/resources/renderers/kyverno-cells.tsx +90 -0
  71. package/src/components/resources/renderers/network-policy-peer-styles.ts +25 -0
  72. package/src/components/resources/renderers/policyreport-subject.test.tsx +27 -0
  73. package/src/components/resources/renderers/velero-cells.tsx +29 -7
  74. package/src/components/resources/renderers/velero-dead-ends.test.tsx +142 -0
  75. package/src/components/resources/resource-utils-calico.test.ts +215 -0
  76. package/src/components/resources/resource-utils-calico.ts +261 -0
  77. package/src/components/resources/resource-utils-cnpg-declarative.test.ts +104 -0
  78. package/src/components/resources/resource-utils-cnpg-objectstore.test.ts +122 -0
  79. package/src/components/resources/resource-utils-cnpg.test.ts +117 -0
  80. package/src/components/resources/resource-utils-cnpg.ts +324 -3
  81. package/src/components/resources/resource-utils-crossplane.test.ts +68 -0
  82. package/src/components/resources/resource-utils-kyverno-modern.ts +24 -0
  83. package/src/components/resources/resource-utils-kyverno-queue.test.ts +164 -0
  84. package/src/components/resources/resource-utils-kyverno-queue.ts +198 -0
  85. package/src/components/resources/resource-utils-kyverno.ts +43 -7
  86. package/src/components/resources/resource-utils-velero.test.ts +26 -0
  87. package/src/components/resources/resource-utils-velero.ts +68 -0
  88. package/src/components/resources/resource-utils.ts +98 -0
  89. package/src/components/shared/ResourceActionsBar.tsx +101 -16
  90. package/src/components/shared/ResourceRendererDispatch.test.tsx +379 -3
  91. package/src/components/shared/ResourceRendererDispatch.tsx +199 -22
  92. package/src/components/shared/index.ts +1 -1
  93. package/src/components/shared/revision-role-badges.test.ts +120 -0
  94. package/src/components/topology/K8sResourceNode.test.ts +10 -0
  95. package/src/components/topology/K8sResourceNode.tsx +22 -0
  96. package/src/components/topology/TopologyFilterSidebar.test.tsx +31 -0
  97. package/src/components/topology/TopologyFilterSidebar.tsx +5 -0
  98. package/src/components/topology/TopologyGraph.tsx +6 -6
  99. package/src/components/topology/TopologySearch.tsx +7 -0
  100. package/src/components/topology/layout.ts +3 -0
  101. package/src/components/trace/ReachabilityView.tsx +1 -0
  102. package/src/components/trace/TracePanel.tsx +12 -2
  103. package/src/components/trace/reachFixtures.ts +26 -1
  104. package/src/components/trace/reachGraphModel.test.ts +20 -0
  105. package/src/components/trace/reachGraphModel.ts +8 -1
  106. package/src/components/trace/reachOrigins.test.ts +31 -0
  107. package/src/components/trace/reachOrigins.ts +11 -1
  108. package/src/components/ui/Badge.tsx +10 -0
  109. package/src/components/ui/ScheduleValue.test.tsx +80 -0
  110. package/src/components/ui/ScheduleValue.tsx +85 -0
  111. package/src/components/ui/drawer-components.test.tsx +17 -1
  112. package/src/components/ui/drawer-components.tsx +9 -2
  113. package/src/components/ui/index.ts +2 -0
  114. package/src/components/workload/WorkloadView.tsx +3 -0
  115. package/src/types/core.ts +29 -0
  116. package/src/types/gitops-flux-status.test.ts +163 -0
  117. package/src/types/gitops.ts +80 -24
  118. package/src/types/index.ts +1 -0
  119. package/src/types/policy.ts +193 -0
  120. package/src/utils/navigation.test.ts +5 -0
  121. package/src/utils/navigation.ts +5 -0
  122. package/src/utils/resource-icons.test.ts +49 -2
  123. package/src/utils/resource-icons.ts +29 -2
@@ -0,0 +1,81 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { renderToString } from 'react-dom/server'
3
+ import { CompositeRenderer } from './CompositeRenderer'
4
+ import type { CrossplaneResourceRef } from '../resource-utils-crossplane'
5
+
6
+ const claim = {
7
+ apiVersion: 'platform.example.org/v1alpha1',
8
+ kind: 'DatabaseClaim',
9
+ metadata: { name: 'example-database', namespace: 'demo-app' },
10
+ spec: {
11
+ compositionRef: { name: 'database' },
12
+ resourceRef: {
13
+ apiVersion: 'platform.example.org/v1alpha1',
14
+ kind: 'Database',
15
+ name: 'example-database-x7k2m',
16
+ },
17
+ },
18
+ }
19
+
20
+ const composed: CrossplaneResourceRef[] = [
21
+ { apiVersion: 'database.example.org/v1beta1', kind: 'Instance', name: 'example-database-instance' },
22
+ { apiVersion: 'identity.example.org/v1beta1', kind: 'Role', name: 'example-database-access' },
23
+ { apiVersion: 'kubernetes.crossplane.io/v1alpha2', kind: 'Object', name: 'example-database-connection' },
24
+ ]
25
+
26
+ const html = (props: any) => renderToString(<CompositeRenderer {...props} />)
27
+
28
+ describe('CompositeRenderer — claim composed-resources panel (issue coverage)', () => {
29
+ it('lists composed resources resolved from the bound XR (the fix)', () => {
30
+ const out = html({ data: claim, composedRefs: composed })
31
+ expect(out).toContain('Composed Resources (3)')
32
+ for (const ref of composed) {
33
+ expect(out).toContain(ref.name)
34
+ }
35
+ expect(out).not.toContain('No composed resources yet')
36
+ // claim identity section + the bound-XR link
37
+ expect(out).toContain('Claim')
38
+ expect(out).toContain('Bound Composite')
39
+ expect(out).toContain('example-database-x7k2m')
40
+ })
41
+
42
+ it('shows a loading state while the bound XR is being fetched — not "none"', () => {
43
+ const out = html({ data: claim, composedRefs: [], boundXRStatus: { loading: true } })
44
+ expect(out).toContain('Loading composed resources')
45
+ expect(out).not.toContain('No composed resources yet')
46
+ })
47
+
48
+ it('distinguishes a not-found / unreadable bound XR from genuinely-none', () => {
49
+ const out = html({ data: claim, composedRefs: [], boundXRStatus: { missing: true } })
50
+ expect(out).toContain('was not found')
51
+ expect(out).not.toContain('No composed resources yet')
52
+ })
53
+
54
+ it('surfaces a bound-XR read error (RBAC/500) distinctly, with the message', () => {
55
+ const out = html({
56
+ data: claim,
57
+ composedRefs: [],
58
+ boundXRStatus: { error: true, errorMessage: 'forbidden: cannot get databases' },
59
+ })
60
+ expect(out).toContain('read the bound composite') // apostrophe is HTML-escaped in SSR output
61
+ expect(out).toContain('forbidden: cannot get databases')
62
+ expect(out).not.toContain('No composed resources yet')
63
+ })
64
+
65
+ it('falls back to the genuine empty state for a claim whose XR simply has none yet', () => {
66
+ const out = html({ data: claim, composedRefs: [] })
67
+ expect(out).toContain('No composed resources yet')
68
+ })
69
+
70
+ it('for an XR viewed directly (no bound-XR status), reads its own resourceRefs', () => {
71
+ const xr = {
72
+ apiVersion: 'platform.example.org/v1alpha1',
73
+ kind: 'Database',
74
+ metadata: { name: 'example-database-x7k2m' },
75
+ spec: { resourceRefs: composed },
76
+ }
77
+ const out = html({ data: xr })
78
+ expect(out).toContain('Composed Resources (3)')
79
+ expect(out).toContain('Composite Resource') // not "Claim"
80
+ })
81
+ })
@@ -52,6 +52,28 @@ interface CompositeRendererProps {
52
52
  * of an unrelated Bucket from another provider. Populated by the host
53
53
  * wrapper. When absent, the renderer shows refs without status badges. */
54
54
  composedRefStatuses?: Map<string, ComposedRefStatus>
55
+ /** Composed-resource refs resolved by the host. A v1 Claim carries only a
56
+ * singular spec.resourceRef to its bound XR — the composed refs live on that
57
+ * XR, not the claim — so the host follows the ref, fetches the XR, and passes
58
+ * its refs here. When omitted, refs are read off `data` directly (correct for
59
+ * an XR/Composite viewed on its own). */
60
+ composedRefs?: CrossplaneResourceRef[]
61
+ /** State of the host's fetch of the claim's bound XR. Set only for a v1 Claim.
62
+ * Without it, a claim whose XR is still loading or unreadable (404/RBAC) would
63
+ * fall into the empty branch and read "No composed resources yet" — which is
64
+ * indistinguishable from a claim that genuinely has none. */
65
+ boundXRStatus?: BoundXRStatus
66
+ }
67
+
68
+ /** Fetch state of a claim's bound XR, reported by the host wrapper. */
69
+ export interface BoundXRStatus {
70
+ /** true while the XR fetch is in flight */
71
+ loading?: boolean
72
+ /** 404 — the referenced XR does not exist (dangling resourceRef) */
73
+ missing?: boolean
74
+ /** any non-404 failure (auth, network, server error); message in errorMessage */
75
+ error?: boolean
76
+ errorMessage?: string
55
77
  }
56
78
 
57
79
  function makeRefKey(ref: CrossplaneResourceRef): string {
@@ -67,13 +89,13 @@ function groupFromApiVersion(apiVersion: string | undefined): string {
67
89
  return slash < 0 ? '' : apiVersion.slice(0, slash)
68
90
  }
69
91
 
70
- export function CompositeRenderer({ data, onNavigate, composedRefStatuses }: CompositeRendererProps) {
92
+ export function CompositeRenderer({ data, onNavigate, composedRefStatuses, composedRefs, boundXRStatus }: CompositeRendererProps) {
71
93
  const status = getCrossplaneStatus(data)
72
94
  const reason = getCrossplaneStatusReason(data)
73
95
  const compositionRef = getCompositionRef(data)
74
96
  const compositionRevisionRef = getCompositionRevisionRef(data)
75
97
  const compositionUpdatePolicy = getCompositionUpdatePolicy(data)
76
- const resourceRefs = getCrossplaneResourceRefs(data)
98
+ const resourceRefs = composedRefs ?? getCrossplaneResourceRefs(data)
77
99
  const claim = isClaim(data)
78
100
  const boundXR = claim ? getBoundXRRef(data) : null
79
101
  const paused = isCrossplanePaused(data)
@@ -153,9 +175,7 @@ export function CompositeRenderer({ data, onNavigate, composedRefStatuses }: Com
153
175
  defaultExpanded
154
176
  >
155
177
  {resourceRefs.length === 0 ? (
156
- <div className="text-sm text-theme-text-tertiary py-2">
157
- No composed resources yet — the composition has not produced any managed resources.
158
- </div>
178
+ <ComposedResourcesEmpty boundXRStatus={boundXRStatus} boundXRName={boundXR?.name} />
159
179
  ) : (
160
180
  <div className="space-y-1">
161
181
  {resourceRefs.map((ref) => (
@@ -175,6 +195,44 @@ export function CompositeRenderer({ data, onNavigate, composedRefStatuses }: Com
175
195
  )
176
196
  }
177
197
 
198
+ function ComposedResourcesEmpty({
199
+ boundXRStatus,
200
+ boundXRName,
201
+ }: {
202
+ boundXRStatus?: BoundXRStatus
203
+ boundXRName?: string
204
+ }) {
205
+ if (boundXRStatus?.loading) {
206
+ return (
207
+ <div className="text-sm text-theme-text-tertiary py-2">
208
+ Loading composed resources from the bound composite…
209
+ </div>
210
+ )
211
+ }
212
+ if (boundXRStatus?.missing) {
213
+ const named = boundXRName ? ` (${boundXRName})` : ''
214
+ return (
215
+ <div className="text-sm text-theme-text-tertiary py-2">
216
+ The bound composite{named} referenced by this claim was not found — it may not have been
217
+ created yet, or you may not have permission to read it.
218
+ </div>
219
+ )
220
+ }
221
+ if (boundXRStatus?.error) {
222
+ const named = boundXRName ? ` (${boundXRName})` : ''
223
+ return (
224
+ <div className="text-sm status-unhealthy rounded px-2 py-2">
225
+ Couldn't read the bound composite{named}: {boundXRStatus.errorMessage || 'fetch failed'}
226
+ </div>
227
+ )
228
+ }
229
+ return (
230
+ <div className="text-sm text-theme-text-tertiary py-2">
231
+ No composed resources yet — the composition has not produced any managed resources.
232
+ </div>
233
+ )
234
+ }
235
+
178
236
  function ComposedRefRow({
179
237
  ref_,
180
238
  statusEntry,
@@ -189,7 +247,9 @@ function ComposedRefRow({
189
247
  return (
190
248
  <div className="flex items-center gap-2 py-1 px-2 rounded hover:bg-theme-hover text-sm">
191
249
  <Box className="w-3.5 h-3.5 text-theme-text-tertiary shrink-0" />
192
- <span className="text-theme-text-tertiary font-mono shrink-0 w-32 truncate">{ref_.kind}</span>
250
+ {/* Kind is content-sized (capped) so the resource name — the field that
251
+ distinguishes rows — keeps the width instead of truncating. */}
252
+ <span className="text-theme-text-tertiary font-mono shrink-0 max-w-[8rem] truncate">{ref_.kind}</span>
193
253
  <span className="flex-1 min-w-0 truncate">
194
254
  <ResourceLink
195
255
  name={ref_.name}
@@ -200,7 +260,7 @@ function ComposedRefRow({
200
260
  />
201
261
  </span>
202
262
  {ref_.namespace && (
203
- <span className="text-xs text-theme-text-tertiary truncate w-32 text-right">{ref_.namespace}</span>
263
+ <span className="text-xs text-theme-text-tertiary truncate max-w-[8rem] shrink-0 text-right">{ref_.namespace}</span>
204
264
  )}
205
265
  <StatusCellInline statusEntry={statusEntry} />
206
266
  <ChevronRight className="w-3.5 h-3.5 text-theme-text-tertiary shrink-0" />
@@ -17,7 +17,10 @@ export function FluxHelmReleaseRenderer({ data, onNavigate }: FluxHelmReleaseRen
17
17
  const history = status.history || []
18
18
 
19
19
  // Convert to unified GitOps status
20
- const gitOpsStatus = fluxConditionsToGitOpsStatus(conditions, spec.suspend === true)
20
+ const gitOpsStatus = fluxConditionsToGitOpsStatus(conditions, spec.suspend === true, {
21
+ generation: data.metadata?.generation,
22
+ observedGeneration: status.observedGeneration,
23
+ })
21
24
 
22
25
  // Problem detection
23
26
  const problems: Array<{ color: 'red' | 'yellow'; message: string }> = []
@@ -16,7 +16,10 @@ export function GitRepositoryRenderer({ data }: GitRepositoryRendererProps) {
16
16
  const artifact = status.artifact || {}
17
17
 
18
18
  // Convert to unified GitOps status
19
- const gitOpsStatus = fluxConditionsToGitOpsStatus(conditions, spec.suspend === true)
19
+ const gitOpsStatus = fluxConditionsToGitOpsStatus(conditions, spec.suspend === true, {
20
+ generation: data.metadata?.generation,
21
+ observedGeneration: status.observedGeneration,
22
+ })
20
23
 
21
24
  // Problem detection
22
25
  const problems: Array<{ color: 'red' | 'yellow'; message: string }> = []
@@ -17,7 +17,10 @@ export function HelmRepositoryRenderer({ data }: HelmRepositoryRendererProps) {
17
17
  const artifact = status.artifact || {}
18
18
 
19
19
  // Convert to unified GitOps status
20
- const gitOpsStatus = fluxConditionsToGitOpsStatus(conditions, spec.suspend === true)
20
+ const gitOpsStatus = fluxConditionsToGitOpsStatus(conditions, spec.suspend === true, {
21
+ generation: data.metadata?.generation,
22
+ observedGeneration: status.observedGeneration,
23
+ })
21
24
 
22
25
  // Determine repository type
23
26
  const isOCI = spec.type === 'oci'
@@ -16,7 +16,10 @@ export function KustomizationRenderer({ data, onNavigate }: KustomizationRendere
16
16
  const inventoryEntries = status.inventory?.entries || []
17
17
 
18
18
  // Convert to unified GitOps status
19
- const gitOpsStatus = fluxConditionsToGitOpsStatus(conditions, spec.suspend === true)
19
+ const gitOpsStatus = fluxConditionsToGitOpsStatus(conditions, spec.suspend === true, {
20
+ generation: data.metadata?.generation,
21
+ observedGeneration: status.observedGeneration,
22
+ })
20
23
 
21
24
  // Parse inventory to managed resources
22
25
  const managedResources = parseFluxInventory(inventoryEntries)
@@ -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
  )}