@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,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
+ }
@@ -0,0 +1,60 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { renderToString } from 'react-dom/server'
3
+ import { LookupFailureNote } from './LookupFailureNote'
4
+
5
+ function err(message: string, status?: number): Error {
6
+ return Object.assign(new Error(message), status === undefined ? {} : { status })
7
+ }
8
+
9
+ describe('LookupFailureNote', () => {
10
+ it('says nothing when every lookup came back', () => {
11
+ expect(renderToString(<LookupFailureNote errors={[]} what="x" />)).toBe('')
12
+ expect(renderToString(<LookupFailureNote errors={[null, undefined]} what="x" />)).toBe('')
13
+ })
14
+
15
+ // Being unable to see something is expected and not actionable; a fault is
16
+ // neither. The two must not read alike.
17
+ it('renders a permission answer as a calm note, not a red error', () => {
18
+ const html = renderToString(
19
+ <LookupFailureNote errors={[err('forbidden', 403)]} what="which clusters use this store" />,
20
+ )
21
+ expect(html).toContain('permission')
22
+ expect(html).not.toContain('text-red-400')
23
+ })
24
+
25
+ it('keeps a genuine fault loud, with the reason', () => {
26
+ const html = renderToString(
27
+ <LookupFailureNote errors={[err('Failed to fetch')]} what="what replicates here" />,
28
+ )
29
+ expect(html).toContain('text-red-400')
30
+ expect(html).toContain('Failed to fetch')
31
+ })
32
+
33
+ // A fault is actionable and a permission answer is not, so the actionable one
34
+ // is the one worth showing when both happened.
35
+ it('prefers the fault when a lookup was both forbidden and broken', () => {
36
+ const html = renderToString(
37
+ <LookupFailureNote errors={[err('forbidden', 403), err('boom', 500)]} what="x" />,
38
+ )
39
+ expect(html).toContain('boom')
40
+ expect(html).toContain('text-red-400')
41
+ })
42
+
43
+ // The case that loses counts: rows are on screen, so the failure means the
44
+ // list is short — not that nothing was found.
45
+ it('says the list is incomplete rather than unchecked when rows are showing', () => {
46
+ const html = renderToString(
47
+ <LookupFailureNote errors={[err('boom', 500)]} what="x" incomplete />,
48
+ )
49
+ expect(html).toContain('This may be incomplete')
50
+ expect(html).toContain('boom')
51
+ })
52
+
53
+ it('does the same for a partial permission answer', () => {
54
+ const html = renderToString(
55
+ <LookupFailureNote errors={[err('forbidden', 403)]} what="x" incomplete />,
56
+ )
57
+ expect(html).toContain('may be incomplete')
58
+ expect(html).not.toContain('text-red-400')
59
+ })
60
+ })
@@ -0,0 +1,42 @@
1
+ import { isForbiddenError } from '../../../types/fetch-error'
2
+
3
+ interface LookupFailureNoteProps {
4
+ /** Nulls are ignored, so callers can pass `[a.error, b.error]` from their hooks. */
5
+ errors: unknown[]
6
+ /** Tail of a sentence: "which clusters use this store". */
7
+ what: string
8
+ /** Rows are already on screen, so the failure shortened the list rather than
9
+ * emptying it. */
10
+ incomplete?: boolean
11
+ }
12
+
13
+ /**
14
+ * A reverse lookup that did not come back, told apart from one that found
15
+ * nothing. Same split as RBACErrorSection and sharing its predicate: a denial is
16
+ * expected and reads calm, a fault stays loud.
17
+ */
18
+ export function LookupFailureNote({ errors, what, incomplete }: LookupFailureNoteProps) {
19
+ const real = errors.filter(Boolean)
20
+ if (real.length === 0) return null
21
+
22
+ // A fault outranks a denial: only one of the two is actionable.
23
+ const fault = real.find((e) => !isForbiddenError(e))
24
+ if (!fault) {
25
+ return (
26
+ <div className="text-xs text-theme-text-tertiary">
27
+ {incomplete
28
+ ? `This may be incomplete: you don’t have permission to check ${what}.`
29
+ : `You don’t have permission to check ${what}.`}
30
+ </div>
31
+ )
32
+ }
33
+
34
+ const message = fault instanceof Error ? fault.message : String(fault)
35
+ return (
36
+ <div className="text-xs text-red-400">
37
+ {incomplete
38
+ ? `This may be incomplete — could not check ${what}: ${message}`
39
+ : `Could not check ${what}: ${message}`}
40
+ </div>
41
+ )
42
+ }
@@ -1,8 +1,15 @@
1
1
  import { clsx } from 'clsx'
2
+ import { Badge } from '../../ui/Badge'
2
3
  import { Tooltip } from '../../ui/Tooltip'
4
+ import {
5
+ NETWORK_POLICY_PEER_DOTS,
6
+ NETWORK_POLICY_PEER_STYLES,
7
+ type NetworkPolicyPeerType,
8
+ } from './network-policy-peer-styles'
3
9
 
4
10
  interface NetworkPolicyDiagramProps {
5
11
  spec: any
12
+ staged?: boolean
6
13
  }
7
14
 
8
15
  /**
@@ -11,7 +18,7 @@ interface NetworkPolicyDiagramProps {
11
18
  * Each rule is a horizontal band. Peers within a rule are OR'd (stacked).
12
19
  * Pure CSS/SVG — no ReactFlow needed for this static diagram.
13
20
  */
14
- export function NetworkPolicyDiagram({ spec }: NetworkPolicyDiagramProps) {
21
+ export function NetworkPolicyDiagram({ spec, staged = false }: NetworkPolicyDiagramProps) {
15
22
  const podSelector = spec.podSelector || {}
16
23
  const matchLabels = podSelector.matchLabels || {}
17
24
  const policyTypes: string[] = spec.policyTypes || []
@@ -29,6 +36,13 @@ export function NetworkPolicyDiagram({ spec }: NetworkPolicyDiagramProps) {
29
36
 
30
37
  return (
31
38
  <div className="card-inner-lg space-y-3">
39
+ {staged && (
40
+ <div className="flex items-center gap-2 text-[11px] text-theme-text-tertiary">
41
+ <Badge severity="warning" size="sm">Staged preview</Badge>
42
+ <span>Dashed paths are evaluated but not enforced</span>
43
+ </div>
44
+ )}
45
+
32
46
  {/* Ingress flows */}
33
47
  {hasIngress && (
34
48
  <div className="space-y-2">
@@ -43,6 +57,7 @@ export function NetworkPolicyDiagram({ spec }: NetworkPolicyDiagramProps) {
43
57
  ports={[]}
44
58
  direction="ingress"
45
59
  denied
60
+ staged={staged}
46
61
  />
47
62
  ) : (
48
63
  ingress?.map((rule, i) => (
@@ -52,6 +67,7 @@ export function NetworkPolicyDiagram({ spec }: NetworkPolicyDiagramProps) {
52
67
  target={targetLabel}
53
68
  ports={extractPorts(rule.ports)}
54
69
  direction="ingress"
70
+ staged={staged}
55
71
  />
56
72
  ))
57
73
  )}
@@ -72,6 +88,7 @@ export function NetworkPolicyDiagram({ spec }: NetworkPolicyDiagramProps) {
72
88
  ports={[]}
73
89
  direction="egress"
74
90
  denied
91
+ staged={staged}
75
92
  />
76
93
  ) : (
77
94
  egress?.map((rule, i) => (
@@ -81,6 +98,7 @@ export function NetworkPolicyDiagram({ spec }: NetworkPolicyDiagramProps) {
81
98
  target={targetLabel}
82
99
  ports={extractPorts(rule.ports)}
83
100
  direction="egress"
101
+ staged={staged}
84
102
  />
85
103
  ))
86
104
  )}
@@ -97,7 +115,7 @@ export function NetworkPolicyDiagram({ spec }: NetworkPolicyDiagramProps) {
97
115
  interface PeerInfo {
98
116
  label: string
99
117
  sublabel?: string
100
- type: 'pod' | 'namespace' | 'cidr' | 'all' | 'deny' | 'combined'
118
+ type: NetworkPolicyPeerType
101
119
  }
102
120
 
103
121
  function extractPeers(peers: any[] | undefined): PeerInfo[] {
@@ -155,37 +173,20 @@ function formatSelector(selector: any): string {
155
173
  return Object.entries(labels).map(([k, v]) => `${k}=${v}`).join(', ')
156
174
  }
157
175
 
158
- // Color coding for peer types
159
- const PEER_STYLES = {
160
- pod: 'border-emerald-500/30 bg-emerald-500/8',
161
- namespace: 'border-sky-500/30 bg-sky-500/8',
162
- cidr: 'border-amber-500/30 bg-amber-500/8',
163
- combined: 'border-emerald-500/30 bg-emerald-500/8',
164
- all: 'border-theme-border bg-theme-elevated/50',
165
- deny: 'border-red-500/30 bg-red-500/8',
166
- } as const
167
-
168
- const PEER_DOT = {
169
- pod: 'bg-emerald-500',
170
- namespace: 'bg-sky-500',
171
- cidr: 'bg-amber-500',
172
- combined: 'bg-emerald-500',
173
- all: 'bg-theme-text-tertiary',
174
- deny: 'bg-red-500',
175
- } as const
176
-
177
176
  function FlowRow({
178
177
  sources,
179
178
  target,
180
179
  ports,
181
180
  direction,
182
181
  denied = false,
182
+ staged = false,
183
183
  }: {
184
184
  sources: PeerInfo[]
185
185
  target: string
186
186
  ports: string[]
187
187
  direction: 'ingress' | 'egress'
188
188
  denied?: boolean
189
+ staged?: boolean
189
190
  }) {
190
191
  const isIngress = direction === 'ingress'
191
192
 
@@ -200,11 +201,11 @@ function FlowRow({
200
201
  <div
201
202
  className={clsx(
202
203
  'rounded-md border px-2 py-1.5 min-w-0 overflow-hidden',
203
- PEER_STYLES[peer.type],
204
+ NETWORK_POLICY_PEER_STYLES[peer.type],
204
205
  )}
205
206
  >
206
207
  <div className="flex items-center gap-1.5 min-w-0">
207
- <span className={clsx('w-1.5 h-1.5 rounded-full shrink-0', PEER_DOT[peer.type])} />
208
+ <span className={clsx('w-1.5 h-1.5 rounded-full shrink-0', NETWORK_POLICY_PEER_DOTS[peer.type])} />
208
209
  <span className={clsx('text-[11px] font-medium truncate min-w-0', denied && 'line-through text-red-400')}>
209
210
  {peer.label}
210
211
  </span>
@@ -245,14 +246,14 @@ function FlowRow({
245
246
  x1="2" y1="10" x2="32" y2="10"
246
247
  stroke={denied ? '#ef4444' : direction === 'ingress' ? '#3b82f6' : '#a855f7'}
247
248
  strokeWidth="1.5"
248
- strokeDasharray={denied ? '3 2' : undefined}
249
+ strokeDasharray={staged ? '4 3' : denied ? '3 2' : undefined}
249
250
  markerEnd={`url(#arrow-${direction}-${denied ? 'denied' : 'ok'})`}
250
251
  />
251
252
  </svg>
252
253
  {ports.length > 0 && (
253
- <div className="flex flex-wrap justify-center gap-0.5 mt-0.5">
254
+ <div className="flex flex-col items-center gap-0.5 mt-0.5">
254
255
  {ports.map((p, i) => (
255
- <span key={i} className="text-[8px] text-theme-text-tertiary font-mono leading-none">{p}</span>
256
+ <span key={i} className="whitespace-nowrap text-[8px] text-theme-text-tertiary font-mono leading-none">{p}</span>
256
257
  ))}
257
258
  </div>
258
259
  )}
@@ -5,9 +5,10 @@ import { NetworkPolicyDiagram } from './NetworkPolicyDiagram'
5
5
 
6
6
  interface NetworkPolicyRendererProps {
7
7
  data: any
8
+ staged?: boolean
8
9
  }
9
10
 
10
- export function NetworkPolicyRenderer({ data }: NetworkPolicyRendererProps) {
11
+ export function NetworkPolicyRenderer({ data, staged = false }: NetworkPolicyRendererProps) {
11
12
  const spec = data.spec || {}
12
13
  const podSelector = spec.podSelector || {}
13
14
  const policyTypes: string[] = spec.policyTypes || []
@@ -23,7 +24,7 @@ export function NetworkPolicyRenderer({ data }: NetworkPolicyRendererProps) {
23
24
  <>
24
25
  {hasDiagramContent && (
25
26
  <Section title="Policy Flow" icon={GitFork} defaultExpanded>
26
- <NetworkPolicyDiagram spec={spec} />
27
+ <NetworkPolicyDiagram spec={spec} staged={staged} />
27
28
  </Section>
28
29
  )}
29
30
 
@@ -125,7 +126,9 @@ function IngressEgressRuleCard({
125
126
  )}
126
127
 
127
128
  {peers.length === 0 && ports.length === 0 && (
128
- <div className="text-xs text-theme-text-tertiary">Allow all</div>
129
+ <div className="text-xs text-theme-text-tertiary">
130
+ {direction === 'from' ? 'All sources' : 'All destinations'}
131
+ </div>
129
132
  )}
130
133
  </div>
131
134
  )
@@ -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) => {