@skyhook-io/k8s-ui 1.10.5 → 1.12.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 (80) hide show
  1. package/package.json +1 -1
  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/IssuesView.tsx +9 -0
  7. package/src/components/issues/diagnostic.ts +3 -0
  8. package/src/components/issues/issues.test.ts +11 -0
  9. package/src/components/issues/severity.ts +3 -0
  10. package/src/components/issues/types.ts +3 -0
  11. package/src/components/resources/ResourcesSidebar.tsx +1 -1
  12. package/src/components/resources/ResourcesView.tsx +261 -30
  13. package/src/components/resources/index.ts +2 -0
  14. package/src/components/resources/kyverno-cell-gating.test.ts +93 -0
  15. package/src/components/resources/kyverno-modern-posture.test.ts +290 -0
  16. package/src/components/resources/renderers/AlertRenderer.tsx +4 -1
  17. package/src/components/resources/renderers/CNPGClusterRenderer.test.tsx +85 -0
  18. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +133 -9
  19. package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +19 -4
  20. package/src/components/resources/renderers/CompositeRenderer.test.tsx +81 -0
  21. package/src/components/resources/renderers/CompositeRenderer.tsx +67 -7
  22. package/src/components/resources/renderers/EventRenderer.test.tsx +23 -0
  23. package/src/components/resources/renderers/EventRenderer.tsx +10 -18
  24. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +4 -1
  25. package/src/components/resources/renderers/GitRepositoryRenderer.tsx +4 -1
  26. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +4 -1
  27. package/src/components/resources/renderers/KustomizationRenderer.tsx +4 -1
  28. package/src/components/resources/renderers/KyvernoCELPolicyRenderers.tsx +317 -0
  29. package/src/components/resources/renderers/KyvernoExceptionRenderers.tsx +264 -0
  30. package/src/components/resources/renderers/KyvernoPolicyShared.tsx +212 -0
  31. package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +4 -1
  32. package/src/components/resources/renderers/PodRenderer.tsx +13 -0
  33. package/src/components/resources/renderers/PolicySection.test.tsx +102 -0
  34. package/src/components/resources/renderers/PolicySection.tsx +170 -0
  35. package/src/components/resources/renderers/VeleroBSLRenderer.tsx +2 -4
  36. package/src/components/resources/renderers/VeleroBackupRenderer.tsx +32 -9
  37. package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +30 -9
  38. package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +12 -6
  39. package/src/components/resources/renderers/WorkloadRenderer.tsx +15 -1
  40. package/src/components/resources/renderers/badge-no-handrolled.test.tsx +1 -1
  41. package/src/components/resources/renderers/cnpg-cells.tsx +37 -2
  42. package/src/components/resources/renderers/index.ts +5 -0
  43. package/src/components/resources/renderers/kyverno-cells.tsx +9 -0
  44. package/src/components/resources/renderers/kyverno-modern-cells.tsx +152 -0
  45. package/src/components/resources/renderers/policyreport-subject.test.tsx +27 -0
  46. package/src/components/resources/renderers/velero-cells.tsx +128 -9
  47. package/src/components/resources/renderers/velero-phase-recovery.test.tsx +79 -0
  48. package/src/components/resources/resource-utils-cnpg.golden.test.ts +112 -0
  49. package/src/components/resources/resource-utils-cnpg.test.ts +590 -1
  50. package/src/components/resources/resource-utils-cnpg.ts +433 -56
  51. package/src/components/resources/resource-utils-crossplane.test.ts +68 -0
  52. package/src/components/resources/resource-utils-kyverno-exceptions.ts +182 -0
  53. package/src/components/resources/resource-utils-kyverno-modern.ts +588 -0
  54. package/src/components/resources/resource-utils-velero.test.ts +237 -0
  55. package/src/components/resources/resource-utils-velero.ts +214 -22
  56. package/src/components/resources/resource-utils.ts +70 -3
  57. package/src/components/shared/ResourceActionsBar.tsx +3 -1
  58. package/src/components/shared/ResourceRendererDispatch.test.tsx +119 -1
  59. package/src/components/shared/ResourceRendererDispatch.tsx +116 -22
  60. package/src/components/topology/K8sResourceNode.tsx +6 -0
  61. package/src/components/trace/ReachabilityGraph.tsx +20 -6
  62. package/src/components/trace/ReachabilityView.tsx +253 -52
  63. package/src/components/trace/problemRows.test.ts +107 -0
  64. package/src/components/trace/reachGraphModel.test.ts +16 -0
  65. package/src/components/trace/reachInspector.test.ts +233 -10
  66. package/src/components/trace/reachInspector.ts +166 -36
  67. package/src/components/trace/reachMarks.test.ts +32 -1
  68. package/src/components/trace/reachMarks.ts +44 -0
  69. package/src/components/trace/reachOrigins.ts +12 -0
  70. package/src/components/trace/types.ts +25 -0
  71. package/src/components/ui/Badge.tsx +36 -0
  72. package/src/components/ui/ForceDeleteConfirmDialog.tsx +12 -1
  73. package/src/theme/components.css +26 -0
  74. package/src/types/gitops-flux-status.test.ts +163 -0
  75. package/src/types/gitops.ts +80 -24
  76. package/src/types/index.ts +1 -0
  77. package/src/types/policy.ts +42 -0
  78. package/src/utils/api-resources.ts +2 -0
  79. package/src/utils/resource-icons.test.ts +33 -0
  80. package/src/utils/resource-icons.ts +65 -2
@@ -0,0 +1,264 @@
1
+ // PolicyException and CleanupPolicy renderers.
2
+ //
3
+ // PolicyException is the record of who was let off the hook — the most
4
+ // operationally interesting Kyverno object after the policy itself. One
5
+ // renderer serves both API families; the accessors it calls are
6
+ // group-qualified, so the same component reads whichever shape it is handed.
7
+
8
+ import { ShieldOff, Filter, Clock, Trash2, AlertTriangle, ScrollText } from 'lucide-react'
9
+ import {
10
+ Section,
11
+ PropertyList,
12
+ Property,
13
+ ConditionsSection,
14
+ AlertBanner,
15
+ LabelSelectorDisplay,
16
+ } from '../../ui/drawer-components'
17
+ import { KyvernoExpressionList } from './KyvernoPolicyShared'
18
+ import {
19
+ getKyvernoCleanupConditions,
20
+ getKyvernoCleanupExclude,
21
+ getKyvernoCleanupLastExecution,
22
+ getKyvernoCleanupMatch,
23
+ getKyvernoCleanupMatchedKinds,
24
+ getKyvernoCleanupPropagationPolicy,
25
+ getKyvernoCleanupSchedule,
26
+ getKyvernoExceptedPolicies,
27
+ getKyvernoExceptionLegacyConditions,
28
+ getKyvernoExceptionMatch,
29
+ getKyvernoExceptionMatchConditions,
30
+ getKyvernoExceptionReportResult,
31
+ isBlanketKyvernoException,
32
+ isModernKyvernoPolicyException,
33
+ } from '../resource-utils-kyverno-exceptions'
34
+
35
+ /**
36
+ * Renders the legacy any/all match block. Each entry carries a
37
+ * `resources` selector with kinds/names/namespaces/selector.
38
+ */
39
+ function LegacyMatchBlock({ match }: { match: any }) {
40
+ const blocks: Array<{ operator: string; entries: any[] }> = []
41
+ if (Array.isArray(match?.any)) blocks.push({ operator: 'any', entries: match.any })
42
+ if (Array.isArray(match?.all)) blocks.push({ operator: 'all', entries: match.all })
43
+ if (blocks.length === 0) return null
44
+
45
+ return (
46
+ <div className="space-y-2">
47
+ {blocks.map((block) => (
48
+ <div key={block.operator}>
49
+ <div className="text-xs uppercase tracking-wide text-theme-text-tertiary mb-1">Match {block.operator}</div>
50
+ <div className="space-y-2">
51
+ {block.entries.map((entry, i) => {
52
+ const resources = entry?.resources || {}
53
+ return (
54
+ <div key={i} className="card-inner">
55
+ <PropertyList>
56
+ {Array.isArray(resources.kinds) && resources.kinds.length > 0 && (
57
+ <Property label="Kinds" value={resources.kinds.join(', ')} />
58
+ )}
59
+ {Array.isArray(resources.names) && resources.names.length > 0 && (
60
+ <Property label="Names" value={resources.names.join(', ')} />
61
+ )}
62
+ {Array.isArray(resources.namespaces) && resources.namespaces.length > 0 && (
63
+ <Property label="Namespaces" value={resources.namespaces.join(', ')} />
64
+ )}
65
+ {resources.selector && (
66
+ <Property label="Selector" value={<LabelSelectorDisplay selector={resources.selector} inline />} />
67
+ )}
68
+ {resources.namespaceSelector && (
69
+ <Property
70
+ label="Namespace Selector"
71
+ value={<LabelSelectorDisplay selector={resources.namespaceSelector} inline />}
72
+ />
73
+ )}
74
+ </PropertyList>
75
+ </div>
76
+ )
77
+ })}
78
+ </div>
79
+ </div>
80
+ ))}
81
+ </div>
82
+ )
83
+ }
84
+
85
+ export function KyvernoPolicyExceptionRenderer({ data }: { data: any }) {
86
+ const isModern = isModernKyvernoPolicyException(data)
87
+ const excepted = getKyvernoExceptedPolicies(data)
88
+ const blanket = isBlanketKyvernoException(data)
89
+ const matchConditions = getKyvernoExceptionMatchConditions(data)
90
+ const legacyMatch = getKyvernoExceptionMatch(data)
91
+ const legacyConditions = getKyvernoExceptionLegacyConditions(data)
92
+ const reportResult = getKyvernoExceptionReportResult(data)
93
+
94
+ return (
95
+ <div className="space-y-4">
96
+ {/* An exception naming no policy exempts everything in its match scope.
97
+ That is the widest exemption expressible here and the operator has to
98
+ see it before anything else. */}
99
+ {blanket && (
100
+ <AlertBanner
101
+ variant="warning"
102
+ icon={AlertTriangle}
103
+ title="Blanket exception"
104
+ message="This exception names no specific policy, so every policy matching its scope is bypassed."
105
+ />
106
+ )}
107
+
108
+ <div className="card-inner-lg">
109
+ <div className="flex items-center gap-2 flex-wrap">
110
+ <span className="text-xs uppercase tracking-wide text-theme-text-tertiary">Exempts</span>
111
+ <span className={`badge ${blanket ? 'status-alert' : 'status-neutral'}`}>
112
+ {blanket ? 'All matching policies' : `${excepted.length} ${excepted.length === 1 ? 'policy' : 'policies'}`}
113
+ </span>
114
+ <span className="badge status-neutral">{isModern ? 'policies.kyverno.io' : 'kyverno.io'}</span>
115
+ </div>
116
+ {/* Kyverno has no expiry field on either shape — the upstream pattern
117
+ is a companion cleanup policy — so an exception persists until
118
+ somebody deletes it. */}
119
+ <div className="mt-2 text-xs text-theme-text-secondary">
120
+ Exceptions do not expire; this one applies until it is deleted.
121
+ </div>
122
+ </div>
123
+
124
+ {excepted.length > 0 && (
125
+ <Section title={`Excepted Policies (${excepted.length})`} icon={ShieldOff}>
126
+ <div className="space-y-2">
127
+ {excepted.map((policy, i) => (
128
+ <div key={i} className="card-inner">
129
+ <div className="flex items-center gap-2 flex-wrap">
130
+ <span className="text-sm font-medium text-theme-text-primary">{policy.name}</span>
131
+ {policy.kind && <span className="badge status-neutral">{policy.kind}</span>}
132
+ </div>
133
+ {policy.ruleNames.length > 0 ? (
134
+ <div className="mt-1 flex items-center gap-1 flex-wrap">
135
+ <span className="text-xs text-theme-text-tertiary">Rules:</span>
136
+ {policy.ruleNames.map((rule) => (
137
+ <span key={rule} className="badge status-neutral">
138
+ {rule}
139
+ </span>
140
+ ))}
141
+ </div>
142
+ ) : (
143
+ !isModern && <div className="mt-1 text-xs text-theme-text-tertiary">All rules in this policy</div>
144
+ )}
145
+ </div>
146
+ ))}
147
+ </div>
148
+ </Section>
149
+ )}
150
+
151
+ {reportResult && (
152
+ <Section title="Reporting" icon={ScrollText}>
153
+ <PropertyList>
154
+ <Property
155
+ label="Report Result"
156
+ value={
157
+ <span className="flex items-center gap-2">
158
+ <span className="badge status-neutral">{reportResult}</span>
159
+ <span className="text-xs text-theme-text-tertiary">how excepted resources appear in reports</span>
160
+ </span>
161
+ }
162
+ />
163
+ </PropertyList>
164
+ </Section>
165
+ )}
166
+
167
+ {legacyMatch && (
168
+ <Section title="Match Scope" icon={Filter}>
169
+ <LegacyMatchBlock match={legacyMatch} />
170
+ </Section>
171
+ )}
172
+
173
+ {matchConditions.length > 0 && (
174
+ <Section title={`Match Conditions (${matchConditions.length})`} icon={Filter}>
175
+ <KyvernoExpressionList items={matchConditions} />
176
+ </Section>
177
+ )}
178
+
179
+ {legacyConditions && (
180
+ <Section title="Conditions" icon={Filter} defaultExpanded={false}>
181
+ <pre className="text-xs text-theme-text-secondary font-mono whitespace-pre-wrap break-all">
182
+ {JSON.stringify(legacyConditions, null, 2)}
183
+ </pre>
184
+ </Section>
185
+ )}
186
+
187
+ <ConditionsSection conditions={data?.status?.conditions} />
188
+ </div>
189
+ )
190
+ }
191
+
192
+ /**
193
+ * CleanupPolicy / ClusterCleanupPolicy (kyverno.io/v2), deprecated in Kyverno
194
+ * 1.18 in favour of DeletingPolicy. Rendered because the installed base runs
195
+ * it today and "what deleted my resource?" is an expensive question to answer
196
+ * without it.
197
+ */
198
+ export function KyvernoCleanupPolicyRenderer({ data }: { data: any }) {
199
+ const schedule = getKyvernoCleanupSchedule(data)
200
+ const kinds = getKyvernoCleanupMatchedKinds(data)
201
+ const lastExecution = getKyvernoCleanupLastExecution(data)
202
+ const propagation = getKyvernoCleanupPropagationPolicy(data)
203
+ const match = getKyvernoCleanupMatch(data)
204
+ const exclude = getKyvernoCleanupExclude(data)
205
+ const conditions = getKyvernoCleanupConditions(data)
206
+
207
+ return (
208
+ <div className="space-y-4">
209
+ <div className="card-inner-lg">
210
+ <div className="flex items-center gap-2 flex-wrap">
211
+ <span className="text-xs uppercase tracking-wide text-theme-text-tertiary">Deletes</span>
212
+ <span className="badge status-alert">{kinds.length > 0 ? kinds.join(', ') : 'matched resources'}</span>
213
+ {schedule ? (
214
+ <span className="text-xs text-theme-text-secondary">
215
+ on schedule <span className="font-mono">{schedule}</span>
216
+ </span>
217
+ ) : (
218
+ <span className="text-xs text-theme-text-secondary">no schedule set — this policy will not run</span>
219
+ )}
220
+ </div>
221
+ <div className="mt-2 text-xs text-theme-text-secondary">
222
+ Deprecated in Kyverno 1.18; DeletingPolicy (policies.kyverno.io) is its replacement.
223
+ </div>
224
+ </div>
225
+
226
+ <Section title="Schedule" icon={Clock}>
227
+ <PropertyList>
228
+ <Property label="Cron" value={schedule ? <span className="font-mono">{schedule}</span> : 'Not set'} />
229
+ {propagation && <Property label="Deletion Propagation" value={propagation} />}
230
+ {lastExecution && <Property label="Last Execution" value={lastExecution} />}
231
+ </PropertyList>
232
+ </Section>
233
+
234
+ {match && (
235
+ <Section title="Match Scope" icon={Filter}>
236
+ <LegacyMatchBlock match={match} />
237
+ </Section>
238
+ )}
239
+
240
+ {exclude && (
241
+ <Section title="Excluded" icon={Filter} defaultExpanded={false}>
242
+ <LegacyMatchBlock match={exclude} />
243
+ </Section>
244
+ )}
245
+
246
+ {conditions ? (
247
+ <Section title="Deletion Conditions" icon={Trash2}>
248
+ <pre className="text-xs text-theme-text-secondary font-mono whitespace-pre-wrap break-all">
249
+ {JSON.stringify(conditions, null, 2)}
250
+ </pre>
251
+ </Section>
252
+ ) : (
253
+ <AlertBanner
254
+ variant="warning"
255
+ icon={Trash2}
256
+ title="No deletion conditions"
257
+ message="Every resource matched by this policy is deleted when the schedule fires."
258
+ />
259
+ )}
260
+
261
+ <ConditionsSection conditions={data?.status?.conditions} />
262
+ </div>
263
+ )
264
+ }
@@ -0,0 +1,212 @@
1
+ // Shared building blocks for the modern Kyverno CEL policy family
2
+ // (policies.kyverno.io). Each of the five policy kinds gets its own thin
3
+ // renderer; everything they genuinely have in common lives here so the
4
+ // enforcement-posture answer can't drift between them.
5
+
6
+ import { Shield, Filter, Code, AlertTriangle } from 'lucide-react'
7
+ import { Section, PropertyList, Property, LabelSelectorDisplay, AlertBanner } from '../../ui/drawer-components'
8
+ import {
9
+ formatKyvernoResourceRule,
10
+ getKyvernoEnforcementPosture,
11
+ getKyvernoExcludeResourceRules,
12
+ getKyvernoFailurePolicy,
13
+ getKyvernoMatchConditions,
14
+ getKyvernoNamespaceSelector,
15
+ getKyvernoObjectSelector,
16
+ getKyvernoPolicyReady,
17
+ getKyvernoPolicyStatusMessage,
18
+ getKyvernoResourceRules,
19
+ getKyvernoVariables,
20
+ isKyvernoAdmissionEnabled,
21
+ isKyvernoBackgroundEnabled,
22
+ type KyvernoNamedExpression,
23
+ type KyvernoPolicyFamily,
24
+ } from '../resource-utils-kyverno-modern'
25
+
26
+ const POSTURE_TONE: Record<string, string> = {
27
+ healthy: 'status-healthy',
28
+ degraded: 'status-degraded',
29
+ alert: 'status-alert',
30
+ unhealthy: 'status-unhealthy',
31
+ neutral: 'status-neutral',
32
+ unknown: 'status-unknown',
33
+ }
34
+
35
+ /**
36
+ * The answer to the operator's first question — "does this block, and what
37
+ * does it match?" — rendered above everything else.
38
+ *
39
+ * It states the EFFECTIVE posture, not the declared one. A policy declaring
40
+ * `validationActions: [Deny]` with admission evaluation disabled blocks
41
+ * nothing, and showing it as a red "Deny" badge would report a cluster as
42
+ * protected when it isn't.
43
+ */
44
+ export function KyvernoPostureHeader({
45
+ data,
46
+ family,
47
+ }: {
48
+ data: any
49
+ family: KyvernoPolicyFamily | undefined
50
+ }) {
51
+ const posture = getKyvernoEnforcementPosture(data, family)
52
+ const ready = getKyvernoPolicyReady(data)
53
+ const statusMessage = getKyvernoPolicyStatusMessage(data)
54
+ const rules = getKyvernoResourceRules(data)
55
+
56
+ return (
57
+ <div className="space-y-2">
58
+ {/* A policy that failed to compile enforces nothing whatever its spec
59
+ says — the most common "why isn't my policy working" cause, so it
60
+ outranks the posture badge. */}
61
+ {ready === false && (
62
+ <AlertBanner
63
+ variant="error"
64
+ icon={AlertTriangle}
65
+ title="Policy not ready"
66
+ message={
67
+ statusMessage ||
68
+ 'The controller has not accepted this policy. It is not being enforced — check the CEL expressions for compile errors.'
69
+ }
70
+ />
71
+ )}
72
+
73
+ <div className="card-inner-lg">
74
+ <div className="flex items-center gap-2 flex-wrap">
75
+ <span className="text-xs uppercase tracking-wide text-theme-text-tertiary">Enforcement</span>
76
+ <span className={`badge ${POSTURE_TONE[posture.level] || 'status-unknown'}`}>{posture.label}</span>
77
+ {posture.summary && <span className="text-xs text-theme-text-secondary">{posture.summary}</span>}
78
+ </div>
79
+
80
+ {posture.note && <div className="mt-2 text-xs text-theme-text-secondary">{posture.note}</div>}
81
+
82
+ {rules.length > 0 && (
83
+ <div className="mt-3">
84
+ <div className="text-xs uppercase tracking-wide text-theme-text-tertiary mb-1">Applies to</div>
85
+ <div className="space-y-1">
86
+ {rules.map((rule, i) => (
87
+ <div key={i} className="text-xs font-mono text-theme-text-primary break-all">
88
+ {formatKyvernoResourceRule(rule)}
89
+ </div>
90
+ ))}
91
+ </div>
92
+ </div>
93
+ )}
94
+ </div>
95
+ </div>
96
+ )
97
+ }
98
+
99
+ /**
100
+ * Match scope beyond the headline resourceRules — exclusions and selectors.
101
+ * The header already shows what the policy applies to; this section is the
102
+ * detail an operator opens when the headline doesn't explain why a given
103
+ * resource was or wasn't caught.
104
+ */
105
+ export function KyvernoMatchScopeSection({ data }: { data: any }) {
106
+ const excludeRules = getKyvernoExcludeResourceRules(data)
107
+ const namespaceSelector = getKyvernoNamespaceSelector(data)
108
+ const objectSelector = getKyvernoObjectSelector(data)
109
+ const matchConditions = getKyvernoMatchConditions(data)
110
+
111
+ const hasSelectors =
112
+ (namespaceSelector && Object.keys(namespaceSelector).length > 0) ||
113
+ (objectSelector && Object.keys(objectSelector).length > 0)
114
+
115
+ if (excludeRules.length === 0 && !hasSelectors && matchConditions.length === 0) return null
116
+
117
+ return (
118
+ <Section title="Match Scope" icon={Filter}>
119
+ <PropertyList>
120
+ {namespaceSelector && Object.keys(namespaceSelector).length > 0 && (
121
+ <Property label="Namespace Selector" value={<LabelSelectorDisplay selector={namespaceSelector} inline />} />
122
+ )}
123
+ {objectSelector && Object.keys(objectSelector).length > 0 && (
124
+ <Property label="Object Selector" value={<LabelSelectorDisplay selector={objectSelector} inline />} />
125
+ )}
126
+ </PropertyList>
127
+
128
+ {excludeRules.length > 0 && (
129
+ <div className="mt-3">
130
+ <div className="text-xs uppercase tracking-wide text-theme-text-tertiary mb-1">Excluded</div>
131
+ <div className="space-y-1">
132
+ {excludeRules.map((rule, i) => (
133
+ <div key={i} className="text-xs font-mono text-theme-text-secondary break-all">
134
+ {formatKyvernoResourceRule(rule)}
135
+ </div>
136
+ ))}
137
+ </div>
138
+ </div>
139
+ )}
140
+
141
+ {matchConditions.length > 0 && (
142
+ <div className="mt-3">
143
+ <div className="text-xs uppercase tracking-wide text-theme-text-tertiary mb-1">
144
+ Match Conditions ({matchConditions.length})
145
+ </div>
146
+ <KyvernoExpressionList items={matchConditions} />
147
+ </div>
148
+ )}
149
+ </Section>
150
+ )
151
+ }
152
+
153
+ /** A list of named CEL expressions in a mono block. */
154
+ export function KyvernoExpressionList({ items }: { items: KyvernoNamedExpression[] }) {
155
+ if (items.length === 0) return null
156
+ return (
157
+ <div className="space-y-2">
158
+ {items.map((item, i) => (
159
+ <div key={i} className="card-inner">
160
+ {item.name && <div className="text-xs font-medium text-theme-text-primary mb-1">{item.name}</div>}
161
+ <pre className="text-xs text-theme-text-secondary font-mono whitespace-pre-wrap break-all">
162
+ {item.expression}
163
+ </pre>
164
+ </div>
165
+ ))}
166
+ </div>
167
+ )
168
+ }
169
+
170
+ export function KyvernoVariablesSection({ data }: { data: any }) {
171
+ const variables = getKyvernoVariables(data)
172
+ if (variables.length === 0) return null
173
+ return (
174
+ <Section title={`Variables (${variables.length})`} icon={Code} defaultExpanded={false}>
175
+ <KyvernoExpressionList items={variables} />
176
+ </Section>
177
+ )
178
+ }
179
+
180
+ /**
181
+ * The evaluation/webhook knobs shared by the four admission-shaped kinds.
182
+ * DeletingPolicy has none of these — it is scheduled, not admission-driven —
183
+ * and deliberately does not render this section.
184
+ */
185
+ export function KyvernoEvaluationSection({ data }: { data: any }) {
186
+ const failurePolicy = getKyvernoFailurePolicy(data)
187
+ return (
188
+ <Section title="Evaluation" icon={Shield} defaultExpanded={false}>
189
+ <PropertyList>
190
+ <Property label="Admission" value={isKyvernoAdmissionEnabled(data) ? 'Enabled' : 'Disabled'} />
191
+ <Property label="Background Scans" value={isKyvernoBackgroundEnabled(data) ? 'Enabled' : 'Disabled'} />
192
+ {failurePolicy && (
193
+ <Property
194
+ label="Failure Policy"
195
+ value={
196
+ <span className="flex items-center gap-2">
197
+ <span className={`badge ${failurePolicy === 'Ignore' ? 'status-degraded' : 'status-neutral'}`}>
198
+ {failurePolicy}
199
+ </span>
200
+ <span className="text-xs text-theme-text-tertiary">
201
+ {failurePolicy === 'Ignore'
202
+ ? 'requests are admitted when the engine itself errors'
203
+ : 'requests are rejected when the engine itself errors'}
204
+ </span>
205
+ </span>
206
+ }
207
+ />
208
+ )}
209
+ </PropertyList>
210
+ </Section>
211
+ )
212
+ }
@@ -16,7 +16,10 @@ export function OCIRepositoryRenderer({ data }: OCIRepositoryRendererProps) {
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 }> = []
@@ -1,6 +1,8 @@
1
1
  import { useState, type ReactNode, type JSX } from 'react'
2
2
  import { Server, HardDrive, Terminal as TerminalIcon, FileText, Activity, CirclePlay, FolderOpen, List, Eye, EyeOff, Shield } from 'lucide-react'
3
3
  import { clsx } from 'clsx'
4
+ import { PolicySection } from './PolicySection'
5
+ import type { PolicyResourceResponse } from '../../../types/policy'
4
6
  import { Section, PropertyList, Property, ConditionsSection, CopyHandler, AlertBanner, ResourceLink, useOperationalIssuesShown } from '../../ui/drawer-components'
5
7
  import { formatResources, formatDuration, getPodProblems, getPodPhaseDisplay, healthColors, SEVERITY_DOT_COLOR, getDefaultContainerName } from '../resource-utils'
6
8
  import { getResourceStatusColor, SEVERITY_BADGE_BORDERED } from '../../../utils/badge-colors'
@@ -108,6 +110,10 @@ interface PodRendererProps {
108
110
  rbacData?: RBACSubjectResponse | null
109
111
  rbacLoading?: boolean
110
112
  rbacError?: Error | null
113
+ /** Undefined means the host didn't wire the fetch; the section is omitted. */
114
+ policyData?: PolicyResourceResponse | null
115
+ policyLoading?: boolean
116
+ policyError?: Error | null
111
117
  }
112
118
 
113
119
  // ── Env vars section — extracted to use hooks (useState for reveal) ──────────
@@ -287,6 +293,9 @@ export function PodRenderer({
287
293
  rbacData,
288
294
  rbacLoading,
289
295
  rbacError,
296
+ policyData,
297
+ policyLoading,
298
+ policyError,
290
299
  }: PodRendererProps) {
291
300
  const containerStatuses = data.status?.containerStatuses || []
292
301
  const containers = data.spec?.containers || []
@@ -582,6 +591,10 @@ export function PodRenderer({
582
591
  )}
583
592
 
584
593
  {/* Container Status */}
594
+ {policyData !== undefined && (
595
+ <PolicySection data={policyData} loading={policyLoading} error={policyError} />
596
+ )}
597
+
585
598
  <Section title="Containers" icon={HardDrive} defaultExpanded>
586
599
  <div className="space-y-3">
587
600
  {containers.map((container: any) => {
@@ -0,0 +1,102 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { renderToString } from 'react-dom/server'
3
+ import { PolicySection } from './PolicySection'
4
+ import type { PolicyResourceResponse } from '../../../types/policy'
5
+
6
+ const base: PolicyResourceResponse = {
7
+ evaluated: true,
8
+ status: 'ready',
9
+ liveUpdates: true,
10
+ counts: { pass: 0, fail: 0, warn: 0, error: 0, skip: 0 },
11
+ findings: [],
12
+ }
13
+ const resp = (over: Partial<PolicyResourceResponse>): PolicyResourceResponse => ({ ...base, ...over })
14
+
15
+
16
+ // The unhappy paths carry this section. An empty finding list means several
17
+ // unrelated things, and rendering them all as blank space would tell an
18
+ // operator they are compliant in the cases where nothing was checked.
19
+ describe('PolicySection — what an empty result means', () => {
20
+ it('says which rules failed when there are violations', () => {
21
+ const html = renderToString(<PolicySection data={resp({
22
+ counts: { pass: 2, fail: 1, warn: 0, error: 0, skip: 0 },
23
+ findings: [{ policy: 'require-run-as-nonroot', result: 'fail', message: 'must set runAsNonRoot' }],
24
+ })} />)
25
+ expect(html).toContain('require-run-as-nonroot')
26
+ expect(html).toContain('must set runAsNonRoot')
27
+ // The passing checks are counted, not listed — otherwise two failures drown
28
+ // in a wall of green on a heavily-policed workload.
29
+ expect(html).toContain('2 other checks passing')
30
+ })
31
+
32
+ it('distinguishes "everything passed" from "nothing was checked"', () => {
33
+ const passing = renderToString(<PolicySection data={resp({
34
+ counts: { pass: 5, fail: 0, warn: 0, error: 0, skip: 0 },
35
+ })} />)
36
+ expect(passing).toContain('All 5 checks passing')
37
+
38
+ const unchecked = renderToString(<PolicySection data={resp({})} />)
39
+ expect(unchecked).toContain('No policy applies')
40
+ expect(unchecked).not.toContain('passing')
41
+ })
42
+
43
+ it('never claims compliance when the results could not be read', () => {
44
+ const html = renderToString(<PolicySection data={resp({
45
+ evaluated: false, status: 'deferred', reasonCode: 'rbac_denied',
46
+ })} />)
47
+ expect(html).toContain('has not been checked')
48
+ expect(html).toContain('rbac_denied')
49
+ expect(html).not.toContain('passing')
50
+ })
51
+
52
+ it('says results are still loading during warmup rather than reporting clean', () => {
53
+ const html = renderToString(<PolicySection data={resp({ evaluated: false, status: 'warmup' })} />)
54
+ expect(html).toContain('still loading')
55
+ })
56
+
57
+ it('renders nothing at all when no policy engine is installed', () => {
58
+ // A disclaimer on every cluster without a policy engine is pure noise.
59
+ expect(renderToString(<PolicySection data={resp({ evaluated: false, status: 'not_installed' })} />)).toBe('')
60
+ expect(renderToString(<PolicySection data={null} />)).toBe('')
61
+ })
62
+ })
63
+
64
+ describe('PolicySection — partial coverage', () => {
65
+ it('admits when some report families were unreadable, even while passing', () => {
66
+ // "All checks passing" plus an unreadable family is a claim of coverage we
67
+ // do not have.
68
+ const html = renderToString(<PolicySection data={resp({
69
+ counts: { pass: 3, fail: 0, warn: 0, error: 0, skip: 0 },
70
+ deniedGroups: ['openreports.io'],
71
+ })} />)
72
+ expect(html).toContain('All 3 checks passing')
73
+ expect(html).toContain('may be incomplete')
74
+ expect(html).toContain('openreports.io')
75
+ })
76
+
77
+ it('admits when the results are frozen', () => {
78
+ const html = renderToString(<PolicySection data={resp({
79
+ counts: { pass: 1, fail: 0, warn: 0, error: 0, skip: 0 }, liveUpdates: false,
80
+ })} />)
81
+ expect(html).toContain('not updating live')
82
+ })
83
+ })
84
+
85
+ describe('PolicySection — request states', () => {
86
+ it('shows loading rather than an empty result', () => {
87
+ expect(renderToString(<PolicySection data={null} loading />)).toContain('Loading policy results')
88
+ })
89
+
90
+ it('treats a permission denial as an expected state, not a red failure', () => {
91
+ const denied = Object.assign(new Error('forbidden'), { status: 403 })
92
+ const html = renderToString(<PolicySection data={null} error={denied} />)
93
+ expect(html).toContain('don’t have permission')
94
+ expect(html).not.toContain('text-red')
95
+ })
96
+
97
+ it('keeps a genuine fault loud', () => {
98
+ const html = renderToString(<PolicySection data={null} error={new Error('connection reset')} />)
99
+ expect(html).toContain('connection reset')
100
+ expect(html).toContain('text-red')
101
+ })
102
+ })