@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.
- package/package.json +4 -4
- package/src/components/gitops/GitOpsStatusBadge.tsx +40 -0
- package/src/components/gitops/GitOpsTableView.tsx +22 -2
- package/src/components/gitops/ReconcilingIndicator.test.tsx +31 -0
- package/src/components/gitops/detail-helpers.ts +4 -1
- package/src/components/issues/severity.ts +1 -0
- package/src/components/resources/ResourcesView.empty-note.test.ts +41 -0
- package/src/components/resources/ResourcesView.tsx +412 -5
- package/src/components/resources/index.ts +27 -0
- package/src/components/resources/kyverno-legacy-action.test.ts +98 -0
- package/src/components/resources/renderers/AlertRenderer.tsx +4 -1
- package/src/components/resources/renderers/AnalysisRunRenderer.test.tsx +241 -0
- package/src/components/resources/renderers/AnalysisRunRenderer.tsx +166 -0
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +31 -2
- package/src/components/resources/renderers/CNPGClusterRenderer.test.tsx +33 -0
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +71 -4
- package/src/components/resources/renderers/CNPGDeclarativeRenderer.tsx +243 -0
- package/src/components/resources/renderers/CNPGObjectStoreRenderer.test.tsx +99 -0
- package/src/components/resources/renderers/CNPGObjectStoreRenderer.tsx +235 -0
- package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +11 -0
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +23 -1
- package/src/components/resources/renderers/CNPGShared.tsx +27 -0
- package/src/components/resources/renderers/CalicoHostEndpointRenderer.tsx +71 -0
- package/src/components/resources/renderers/CalicoIPPoolRenderer.tsx +32 -0
- package/src/components/resources/renderers/CalicoNetworkPolicyDiagram.test.tsx +205 -0
- package/src/components/resources/renderers/CalicoNetworkPolicyDiagram.tsx +537 -0
- package/src/components/resources/renderers/CalicoNetworkPolicyRenderer.test.tsx +223 -0
- package/src/components/resources/renderers/CalicoNetworkPolicyRenderer.tsx +670 -0
- package/src/components/resources/renderers/CalicoTierRenderer.tsx +19 -0
- package/src/components/resources/renderers/CompositeRenderer.test.tsx +81 -0
- package/src/components/resources/renderers/CompositeRenderer.tsx +67 -7
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +4 -1
- package/src/components/resources/renderers/GitRepositoryRenderer.tsx +4 -1
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +4 -1
- package/src/components/resources/renderers/KustomizationRenderer.tsx +4 -1
- package/src/components/resources/renderers/KyvernoCELPolicyRenderers.tsx +13 -6
- package/src/components/resources/renderers/KyvernoExceptionRenderers.tsx +3 -2
- package/src/components/resources/renderers/KyvernoGlobalContextRenderer.test.ts +31 -0
- package/src/components/resources/renderers/KyvernoGlobalContextRenderer.tsx +124 -0
- package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +28 -2
- package/src/components/resources/renderers/KyvernoQueueRenderers.tsx +303 -0
- package/src/components/resources/renderers/LookupFailureNote.test.tsx +60 -0
- package/src/components/resources/renderers/LookupFailureNote.tsx +42 -0
- package/src/components/resources/renderers/NetworkPolicyDiagram.tsx +27 -26
- package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +6 -3
- package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +4 -1
- package/src/components/resources/renderers/PodRenderer.tsx +13 -0
- package/src/components/resources/renderers/PolicyCoverageSection.test.tsx +348 -0
- package/src/components/resources/renderers/PolicyCoverageSection.tsx +939 -0
- package/src/components/resources/renderers/PolicySection.test.tsx +159 -0
- package/src/components/resources/renderers/PolicySection.tsx +197 -0
- package/src/components/resources/renderers/RolloutRenderer.test.tsx +491 -0
- package/src/components/resources/renderers/RolloutRenderer.tsx +391 -92
- package/src/components/resources/renderers/VeleroBSLRenderer.test.tsx +374 -0
- package/src/components/resources/renderers/VeleroBSLRenderer.tsx +179 -3
- package/src/components/resources/renderers/VeleroBackupRenderer.test.tsx +221 -0
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +119 -6
- package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.test.tsx +69 -0
- package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.tsx +96 -0
- package/src/components/resources/renderers/VeleroRestoreRenderer.test.tsx +55 -0
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +22 -4
- package/src/components/resources/renderers/VeleroRunMessages.test.tsx +152 -0
- package/src/components/resources/renderers/VeleroRunMessages.tsx +139 -0
- package/src/components/resources/renderers/VeleroScheduleRenderer.test.tsx +88 -0
- package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +16 -6
- package/src/components/resources/renderers/WorkloadRenderer.tsx +15 -1
- package/src/components/resources/renderers/calico-cells.tsx +177 -0
- package/src/components/resources/renderers/cnpg-cells.tsx +100 -0
- package/src/components/resources/renderers/index.ts +16 -0
- package/src/components/resources/renderers/kyverno-cells.tsx +90 -0
- package/src/components/resources/renderers/network-policy-peer-styles.ts +25 -0
- package/src/components/resources/renderers/policyreport-subject.test.tsx +27 -0
- package/src/components/resources/renderers/velero-cells.tsx +29 -7
- package/src/components/resources/renderers/velero-dead-ends.test.tsx +142 -0
- package/src/components/resources/resource-utils-calico.test.ts +215 -0
- package/src/components/resources/resource-utils-calico.ts +261 -0
- package/src/components/resources/resource-utils-cnpg-declarative.test.ts +104 -0
- package/src/components/resources/resource-utils-cnpg-objectstore.test.ts +122 -0
- package/src/components/resources/resource-utils-cnpg.test.ts +117 -0
- package/src/components/resources/resource-utils-cnpg.ts +324 -3
- package/src/components/resources/resource-utils-crossplane.test.ts +68 -0
- package/src/components/resources/resource-utils-kyverno-modern.ts +24 -0
- package/src/components/resources/resource-utils-kyverno-queue.test.ts +164 -0
- package/src/components/resources/resource-utils-kyverno-queue.ts +198 -0
- package/src/components/resources/resource-utils-kyverno.ts +43 -7
- package/src/components/resources/resource-utils-velero.test.ts +26 -0
- package/src/components/resources/resource-utils-velero.ts +68 -0
- package/src/components/resources/resource-utils.ts +98 -0
- package/src/components/shared/ResourceActionsBar.tsx +101 -16
- package/src/components/shared/ResourceRendererDispatch.test.tsx +379 -3
- package/src/components/shared/ResourceRendererDispatch.tsx +199 -22
- package/src/components/shared/index.ts +1 -1
- package/src/components/shared/revision-role-badges.test.ts +120 -0
- package/src/components/topology/K8sResourceNode.test.ts +10 -0
- package/src/components/topology/K8sResourceNode.tsx +22 -0
- package/src/components/topology/TopologyFilterSidebar.test.tsx +31 -0
- package/src/components/topology/TopologyFilterSidebar.tsx +5 -0
- package/src/components/topology/TopologyGraph.tsx +6 -6
- package/src/components/topology/TopologySearch.tsx +7 -0
- package/src/components/topology/layout.ts +3 -0
- package/src/components/trace/ReachabilityView.tsx +1 -0
- package/src/components/trace/TracePanel.tsx +12 -2
- package/src/components/trace/reachFixtures.ts +26 -1
- package/src/components/trace/reachGraphModel.test.ts +20 -0
- package/src/components/trace/reachGraphModel.ts +8 -1
- package/src/components/trace/reachOrigins.test.ts +31 -0
- package/src/components/trace/reachOrigins.ts +11 -1
- package/src/components/ui/Badge.tsx +10 -0
- package/src/components/ui/ScheduleValue.test.tsx +80 -0
- package/src/components/ui/ScheduleValue.tsx +85 -0
- package/src/components/ui/drawer-components.test.tsx +17 -1
- package/src/components/ui/drawer-components.tsx +9 -2
- package/src/components/ui/index.ts +2 -0
- package/src/components/workload/WorkloadView.tsx +3 -0
- package/src/types/core.ts +29 -0
- package/src/types/gitops-flux-status.test.ts +163 -0
- package/src/types/gitops.ts +80 -24
- package/src/types/index.ts +1 -0
- package/src/types/policy.ts +193 -0
- package/src/utils/navigation.test.ts +5 -0
- package/src/utils/navigation.ts +5 -0
- package/src/utils/resource-icons.test.ts +49 -2
- 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:
|
|
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
|
-
|
|
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',
|
|
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-
|
|
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">
|
|
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) => {
|