@skyhook-io/k8s-ui 1.10.4 → 1.11.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 +5 -5
- package/src/components/applications/ApplicationDetail.test.tsx +46 -0
- package/src/components/applications/ApplicationDetail.tsx +14 -15
- package/src/components/issues/IssuesView.tsx +9 -0
- package/src/components/issues/diagnostic.ts +3 -0
- package/src/components/issues/issues.test.ts +11 -0
- package/src/components/issues/severity.ts +3 -0
- package/src/components/issues/types.ts +3 -0
- package/src/components/resources/ResourcesSidebar.tsx +1 -1
- package/src/components/resources/ResourcesView.tsx +259 -30
- package/src/components/resources/index.ts +2 -0
- package/src/components/resources/kyverno-cell-gating.test.ts +93 -0
- package/src/components/resources/kyverno-modern-posture.test.ts +290 -0
- package/src/components/resources/renderers/CNPGClusterRenderer.test.tsx +85 -0
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +109 -9
- package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +8 -4
- package/src/components/resources/renderers/EventRenderer.test.tsx +23 -0
- package/src/components/resources/renderers/EventRenderer.tsx +10 -18
- package/src/components/resources/renderers/KyvernoCELPolicyRenderers.tsx +317 -0
- package/src/components/resources/renderers/KyvernoExceptionRenderers.tsx +264 -0
- package/src/components/resources/renderers/KyvernoPolicyShared.tsx +212 -0
- package/src/components/resources/renderers/VeleroBSLRenderer.tsx +2 -4
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +32 -9
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +30 -9
- package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +12 -6
- package/src/components/resources/renderers/badge-no-handrolled.test.tsx +1 -1
- package/src/components/resources/renderers/cnpg-cells.tsx +37 -2
- package/src/components/resources/renderers/index.ts +4 -0
- package/src/components/resources/renderers/kyverno-modern-cells.tsx +152 -0
- package/src/components/resources/renderers/velero-cells.tsx +128 -9
- package/src/components/resources/renderers/velero-phase-recovery.test.tsx +79 -0
- package/src/components/resources/resource-utils-cnpg.golden.test.ts +112 -0
- package/src/components/resources/resource-utils-cnpg.test.ts +527 -1
- package/src/components/resources/resource-utils-cnpg.ts +373 -56
- package/src/components/resources/resource-utils-kyverno-exceptions.ts +182 -0
- package/src/components/resources/resource-utils-kyverno-modern.ts +588 -0
- package/src/components/resources/resource-utils-velero.test.ts +237 -0
- package/src/components/resources/resource-utils-velero.ts +214 -22
- package/src/components/resources/resource-utils.ts +70 -3
- package/src/components/shared/ResourceActionsBar.tsx +3 -1
- package/src/components/shared/ResourceRendererDispatch.test.tsx +119 -1
- package/src/components/shared/ResourceRendererDispatch.tsx +116 -22
- package/src/components/topology/K8sResourceNode.tsx +11 -0
- package/src/components/topology/TopologyGraph.tsx +85 -5
- package/src/components/trace/ReachabilityGraph.tsx +517 -0
- package/src/components/trace/ReachabilityView.tsx +1173 -0
- package/src/components/trace/TracePanel.test.ts +172 -0
- package/src/components/trace/TracePanel.tsx +558 -0
- package/src/components/trace/TraceSummary.test.ts +213 -0
- package/src/components/trace/TraceSummary.tsx +168 -0
- package/src/components/trace/inClusterSummary.test.ts +98 -0
- package/src/components/trace/inClusterSummary.ts +81 -0
- package/src/components/trace/index.ts +19 -0
- package/src/components/trace/podReach.test.ts +71 -0
- package/src/components/trace/podReach.ts +37 -0
- package/src/components/trace/probe-display.test.ts +107 -0
- package/src/components/trace/probe-display.ts +37 -0
- package/src/components/trace/problemRows.test.ts +107 -0
- package/src/components/trace/reachFixtures.ts +261 -0
- package/src/components/trace/reachGraphModel.test.ts +1423 -0
- package/src/components/trace/reachGraphModel.ts +1521 -0
- package/src/components/trace/reachInspector.test.ts +892 -0
- package/src/components/trace/reachInspector.ts +797 -0
- package/src/components/trace/reachMarks.test.ts +698 -0
- package/src/components/trace/reachMarks.ts +623 -0
- package/src/components/trace/reachOrigins.test.ts +392 -0
- package/src/components/trace/reachOrigins.ts +449 -0
- package/src/components/trace/reachVerdict.test.ts +537 -0
- package/src/components/trace/reachVerdict.ts +601 -0
- package/src/components/trace/traceFingerprint.test.ts +214 -0
- package/src/components/trace/traceFingerprint.ts +122 -0
- package/src/components/trace/traceToSubgraph.test.ts +623 -0
- package/src/components/trace/traceToSubgraph.ts +463 -0
- package/src/components/trace/types.ts +414 -0
- package/src/components/ui/Badge.tsx +36 -0
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +12 -1
- package/src/components/ui/InClusterConsentDialog.test.tsx +83 -0
- package/src/components/ui/InClusterConsentDialog.tsx +141 -0
- package/src/components/ui/index.ts +1 -0
- package/src/components/workload/WorkloadView.tsx +58 -5
- package/src/components/workload/index.ts +1 -1
- package/src/components/workload/reachabilityTab.test.ts +47 -0
- package/src/index.ts +5 -0
- package/src/theme/components.css +64 -0
- package/src/theme/variables.css +5 -0
- package/src/types/core.ts +8 -0
- package/src/utils/api-resources.ts +2 -0
- package/src/utils/application-history.test.ts +1 -1
- package/src/utils/applications.test.ts +63 -1
- package/src/utils/applications.ts +27 -0
- package/src/utils/inClusterConsent.test.ts +109 -0
- package/src/utils/inClusterConsent.ts +72 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/navigation.test.ts +61 -0
- package/src/utils/navigation.ts +11 -9
- package/src/utils/pluralize.test.ts +80 -1
- package/src/utils/pluralize.ts +34 -0
- package/src/utils/resource-icons.test.ts +33 -0
- package/src/utils/resource-icons.ts +65 -2
|
@@ -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
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { HardDrive, Clock } from 'lucide-react'
|
|
2
|
-
import { clsx } from 'clsx'
|
|
3
2
|
import { Section, PropertyList, Property, ConditionsSection } from '../../ui/drawer-components'
|
|
4
3
|
import {
|
|
5
4
|
getBSLStatus,
|
|
@@ -12,6 +11,7 @@ import {
|
|
|
12
11
|
getBSLLastValidation,
|
|
13
12
|
getBSLLastSynced,
|
|
14
13
|
} from '../resource-utils-velero'
|
|
14
|
+
import { VeleroPhaseValue } from './velero-cells'
|
|
15
15
|
|
|
16
16
|
interface VeleroBSLRendererProps {
|
|
17
17
|
data: any
|
|
@@ -29,9 +29,7 @@ export function VeleroBSLRenderer({ data }: VeleroBSLRendererProps) {
|
|
|
29
29
|
<Section title="Status" icon={Clock} defaultExpanded>
|
|
30
30
|
<PropertyList>
|
|
31
31
|
<Property label="Phase" value={
|
|
32
|
-
<
|
|
33
|
-
{bslStatus.text}
|
|
34
|
-
</span>
|
|
32
|
+
<VeleroPhaseValue status={bslStatus} phase={status.phase || ''} />
|
|
35
33
|
} />
|
|
36
34
|
<Property label="Last Validation" value={getBSLLastValidation(data)} />
|
|
37
35
|
<Property label="Last Synced" value={getBSLLastSynced(data)} />
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Archive, Clock, HardDrive, Filter } from 'lucide-react'
|
|
2
|
-
import { clsx } from 'clsx'
|
|
3
2
|
import { Section, PropertyList, Property, ConditionsSection, AlertBanner } from '../../ui/drawer-components'
|
|
4
3
|
import {
|
|
5
4
|
getBackupStatus,
|
|
@@ -18,7 +17,11 @@ import {
|
|
|
18
17
|
getBackupSnapshotVolumes,
|
|
19
18
|
getBackupDefaultVolumesToFsBackup,
|
|
20
19
|
getBackupVolumeSnapshotLocations,
|
|
20
|
+
getBackupQueuePosition,
|
|
21
|
+
isBackupActivePhase,
|
|
22
|
+
isBackupPartialFailurePhase,
|
|
21
23
|
} from '../resource-utils-velero'
|
|
24
|
+
import { VeleroPhaseValue } from './velero-cells'
|
|
22
25
|
import { formatAge } from '../resource-utils'
|
|
23
26
|
|
|
24
27
|
interface VeleroBackupRendererProps {
|
|
@@ -39,9 +42,12 @@ export function VeleroBackupRenderer({ data }: VeleroBackupRendererProps) {
|
|
|
39
42
|
const excludedResources = getBackupExcludedResources(data)
|
|
40
43
|
const vslLocations = getBackupVolumeSnapshotLocations(data)
|
|
41
44
|
|
|
45
|
+
const phase = status.phase || ''
|
|
42
46
|
const isFailed = backupStatus.level === 'unhealthy'
|
|
43
|
-
const
|
|
44
|
-
const
|
|
47
|
+
const isValidationFailure = phase === 'FailedValidation'
|
|
48
|
+
const isPartiallyFailed = isBackupPartialFailurePhase(phase)
|
|
49
|
+
const isInProgress = isBackupActivePhase(phase)
|
|
50
|
+
const queuePosition = getBackupQueuePosition(data)
|
|
45
51
|
|
|
46
52
|
// Progress data
|
|
47
53
|
const progress = status.progress
|
|
@@ -52,15 +58,31 @@ export function VeleroBackupRenderer({ data }: VeleroBackupRendererProps) {
|
|
|
52
58
|
return (
|
|
53
59
|
<>
|
|
54
60
|
{/* Problem alerts */}
|
|
55
|
-
{
|
|
61
|
+
{isValidationFailure && (
|
|
56
62
|
<AlertBanner
|
|
57
63
|
variant="error"
|
|
58
|
-
title=
|
|
64
|
+
title="Backup Validation Failed"
|
|
65
|
+
message={status.failureReason || 'Velero rejected this backup before it started — nothing was backed up.'}
|
|
66
|
+
items={validationErrors.length > 0 ? validationErrors : undefined}
|
|
67
|
+
/>
|
|
68
|
+
)}
|
|
69
|
+
{isFailed && !isValidationFailure && (
|
|
70
|
+
<AlertBanner
|
|
71
|
+
variant="error"
|
|
72
|
+
title="Backup Failed"
|
|
59
73
|
message={status.failureReason || `${errors} error(s) occurred during backup.`}
|
|
60
74
|
items={validationErrors.length > 0 ? validationErrors : undefined}
|
|
61
75
|
/>
|
|
62
76
|
)}
|
|
63
|
-
{
|
|
77
|
+
{isPartiallyFailed && (
|
|
78
|
+
<AlertBanner
|
|
79
|
+
variant="warning"
|
|
80
|
+
title="Backup Partially Failed"
|
|
81
|
+
message={status.failureReason || `${errors} error(s) occurred — some items were not backed up. Do not assume this backup is complete.`}
|
|
82
|
+
items={validationErrors.length > 0 ? validationErrors : undefined}
|
|
83
|
+
/>
|
|
84
|
+
)}
|
|
85
|
+
{warnings > 0 && !isFailed && !isPartiallyFailed && (
|
|
64
86
|
<AlertBanner
|
|
65
87
|
variant="warning"
|
|
66
88
|
title={`${warnings} Warning(s)`}
|
|
@@ -72,10 +94,11 @@ export function VeleroBackupRenderer({ data }: VeleroBackupRendererProps) {
|
|
|
72
94
|
<Section title="Status" icon={Archive} defaultExpanded>
|
|
73
95
|
<PropertyList>
|
|
74
96
|
<Property label="Phase" value={
|
|
75
|
-
<
|
|
76
|
-
{backupStatus.text}
|
|
77
|
-
</span>
|
|
97
|
+
<VeleroPhaseValue status={backupStatus} phase={phase} />
|
|
78
98
|
} />
|
|
99
|
+
{queuePosition !== null && (
|
|
100
|
+
<Property label="Queue Position" value={String(queuePosition)} />
|
|
101
|
+
)}
|
|
79
102
|
{status.startTimestamp && (
|
|
80
103
|
<Property label="Started" value={formatAge(status.startTimestamp) + ' ago'} />
|
|
81
104
|
)}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { ArchiveRestore, Filter } from 'lucide-react'
|
|
2
|
-
import { clsx } from 'clsx'
|
|
3
2
|
import { Section, PropertyList, Property, ConditionsSection, AlertBanner } from '../../ui/drawer-components'
|
|
4
3
|
import {
|
|
5
4
|
getRestoreStatus,
|
|
@@ -13,7 +12,11 @@ import {
|
|
|
13
12
|
getRestoreWarnings,
|
|
14
13
|
getRestorePVs,
|
|
15
14
|
getRestoreExistingResourcePolicy,
|
|
15
|
+
getRestoreValidationErrors,
|
|
16
|
+
isBackupActivePhase,
|
|
17
|
+
isBackupPartialFailurePhase,
|
|
16
18
|
} from '../resource-utils-velero'
|
|
19
|
+
import { VeleroPhaseValue } from './velero-cells'
|
|
17
20
|
import { formatAge } from '../resource-utils'
|
|
18
21
|
|
|
19
22
|
interface VeleroRestoreRendererProps {
|
|
@@ -32,9 +35,12 @@ export function VeleroRestoreRenderer({ data }: VeleroRestoreRendererProps) {
|
|
|
32
35
|
const includedResources = getRestoreIncludedResources(data)
|
|
33
36
|
const excludedResources = getRestoreExcludedResources(data)
|
|
34
37
|
|
|
38
|
+
const phase = status.phase || ''
|
|
39
|
+
const validationErrors = getRestoreValidationErrors(data)
|
|
35
40
|
const isFailed = restoreStatus.level === 'unhealthy'
|
|
36
|
-
const
|
|
37
|
-
const
|
|
41
|
+
const isValidationFailure = phase === 'FailedValidation'
|
|
42
|
+
const isPartiallyFailed = isBackupPartialFailurePhase(phase)
|
|
43
|
+
const isInProgress = isBackupActivePhase(phase)
|
|
38
44
|
|
|
39
45
|
// Progress data
|
|
40
46
|
const progress = status.progress
|
|
@@ -45,14 +51,31 @@ export function VeleroRestoreRenderer({ data }: VeleroRestoreRendererProps) {
|
|
|
45
51
|
return (
|
|
46
52
|
<>
|
|
47
53
|
{/* Problem alerts */}
|
|
48
|
-
{
|
|
54
|
+
{isValidationFailure && (
|
|
49
55
|
<AlertBanner
|
|
50
56
|
variant="error"
|
|
51
|
-
title=
|
|
57
|
+
title="Restore Validation Failed"
|
|
58
|
+
message={status.failureReason || 'Velero rejected this restore before it started — nothing was restored.'}
|
|
59
|
+
items={validationErrors.length > 0 ? validationErrors : undefined}
|
|
60
|
+
/>
|
|
61
|
+
)}
|
|
62
|
+
{isFailed && !isValidationFailure && (
|
|
63
|
+
<AlertBanner
|
|
64
|
+
variant="error"
|
|
65
|
+
title="Restore Failed"
|
|
52
66
|
message={status.failureReason || `${errors} error(s) occurred during restore.`}
|
|
67
|
+
items={validationErrors.length > 0 ? validationErrors : undefined}
|
|
68
|
+
/>
|
|
69
|
+
)}
|
|
70
|
+
{isPartiallyFailed && (
|
|
71
|
+
<AlertBanner
|
|
72
|
+
variant="warning"
|
|
73
|
+
title="Restore Partially Failed"
|
|
74
|
+
message={status.failureReason || `${errors} error(s) occurred — some items were not restored.`}
|
|
75
|
+
items={validationErrors.length > 0 ? validationErrors : undefined}
|
|
53
76
|
/>
|
|
54
77
|
)}
|
|
55
|
-
{warnings > 0 && !isFailed && (
|
|
78
|
+
{warnings > 0 && !isFailed && !isPartiallyFailed && (
|
|
56
79
|
<AlertBanner
|
|
57
80
|
variant="warning"
|
|
58
81
|
title={`${warnings} Warning(s)`}
|
|
@@ -64,9 +87,7 @@ export function VeleroRestoreRenderer({ data }: VeleroRestoreRendererProps) {
|
|
|
64
87
|
<Section title="Status" icon={ArchiveRestore} defaultExpanded>
|
|
65
88
|
<PropertyList>
|
|
66
89
|
<Property label="Phase" value={
|
|
67
|
-
<
|
|
68
|
-
{restoreStatus.text}
|
|
69
|
-
</span>
|
|
90
|
+
<VeleroPhaseValue status={restoreStatus} phase={phase} />
|
|
70
91
|
} />
|
|
71
92
|
<Property label="Backup" value={getRestoreBackupName(data)} />
|
|
72
93
|
{status.startTimestamp && (
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Clock, Archive } from 'lucide-react'
|
|
2
|
-
import { clsx } from 'clsx'
|
|
3
2
|
import { Section, PropertyList, Property, ConditionsSection, AlertBanner } from '../../ui/drawer-components'
|
|
4
3
|
import {
|
|
5
4
|
getScheduleStatus,
|
|
@@ -8,7 +7,9 @@ import {
|
|
|
8
7
|
getSchedulePaused,
|
|
9
8
|
getScheduleTemplate,
|
|
10
9
|
getScheduleUseOwnerReferences,
|
|
10
|
+
getScheduleValidationErrors,
|
|
11
11
|
} from '../resource-utils-velero'
|
|
12
|
+
import { VeleroPhaseValue } from './velero-cells'
|
|
12
13
|
|
|
13
14
|
interface VeleroScheduleRendererProps {
|
|
14
15
|
data: any
|
|
@@ -21,6 +22,7 @@ export function VeleroScheduleRenderer({ data }: VeleroScheduleRendererProps) {
|
|
|
21
22
|
const scheduleStatus = getScheduleStatus(data)
|
|
22
23
|
const isPaused = getSchedulePaused(data)
|
|
23
24
|
const template = getScheduleTemplate(data)
|
|
25
|
+
const validationErrors = getScheduleValidationErrors(data)
|
|
24
26
|
|
|
25
27
|
const templateIncludedNs = template.includedNamespaces || []
|
|
26
28
|
const templateExcludedNs = template.excludedNamespaces || []
|
|
@@ -37,11 +39,17 @@ export function VeleroScheduleRenderer({ data }: VeleroScheduleRendererProps) {
|
|
|
37
39
|
message="This backup schedule is currently paused. No new backups will be created."
|
|
38
40
|
/>
|
|
39
41
|
)}
|
|
40
|
-
{
|
|
42
|
+
{/* Gated on the errors themselves, not on the badge: a paused schedule
|
|
43
|
+
badges as "Paused", which would otherwise hide the validation errors
|
|
44
|
+
the user needs to fix before resuming. */}
|
|
45
|
+
{(validationErrors.length > 0 || status.phase === 'FailedValidation') && (
|
|
41
46
|
<AlertBanner
|
|
42
47
|
variant="error"
|
|
43
48
|
title="Validation Failed"
|
|
44
|
-
message=
|
|
49
|
+
message={isPaused
|
|
50
|
+
? 'The schedule spec failed validation. It will not create backups when resumed until this is fixed.'
|
|
51
|
+
: 'The schedule spec failed validation and is not active — no backups are being created.'}
|
|
52
|
+
items={validationErrors.length > 0 ? validationErrors : undefined}
|
|
45
53
|
/>
|
|
46
54
|
)}
|
|
47
55
|
|
|
@@ -49,9 +57,7 @@ export function VeleroScheduleRenderer({ data }: VeleroScheduleRendererProps) {
|
|
|
49
57
|
<Section title="Schedule" icon={Clock} defaultExpanded>
|
|
50
58
|
<PropertyList>
|
|
51
59
|
<Property label="Status" value={
|
|
52
|
-
<
|
|
53
|
-
{scheduleStatus.text}
|
|
54
|
-
</span>
|
|
60
|
+
<VeleroPhaseValue status={scheduleStatus} phase={status.phase || ''} />
|
|
55
61
|
} />
|
|
56
62
|
<Property label="Cron Schedule" value={
|
|
57
63
|
<span className="font-mono text-sm">{getScheduleCron(data)}</span>
|
|
@@ -23,7 +23,7 @@ const BASELINE = new Set<string>([
|
|
|
23
23
|
'AlertRenderer.tsx', 'ArgoApplicationRenderer.tsx', 'CertificateRenderer.tsx',
|
|
24
24
|
'CertificateRequestRenderer.tsx', 'ChallengeRenderer.tsx', 'CiliumNetworkPolicyRenderer.tsx',
|
|
25
25
|
'ClusterExternalSecretRenderer.tsx', 'ClusterIssuerRenderer.tsx', 'ClusterNetworkPolicyRenderer.tsx',
|
|
26
|
-
'cnpg-cells.tsx', 'CNPGClusterRenderer.tsx', '
|
|
26
|
+
'cnpg-cells.tsx', 'CNPGClusterRenderer.tsx', 'GatewayRenderer.tsx',
|
|
27
27
|
'GenericRenderer.tsx', 'IngressClassRenderer.tsx', 'IstioPeerAuthenticationRenderer.tsx',
|
|
28
28
|
'IstioServiceEntryRenderer.tsx', 'KarpenterNodeClaimRenderer.tsx', 'knative-cells.tsx',
|
|
29
29
|
'KnativeEventingRenderer.tsx', 'KnativeRevisionRenderer.tsx', 'kyverno-cells.tsx',
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// CloudNativePG cell components for ResourcesView table
|
|
2
2
|
|
|
3
3
|
import { clsx } from 'clsx'
|
|
4
|
+
import { Tooltip } from '../../ui/Tooltip'
|
|
4
5
|
import {
|
|
5
6
|
getCNPGClusterStatus,
|
|
7
|
+
classifyCNPGClusterPhase,
|
|
6
8
|
getCNPGClusterInstances,
|
|
7
9
|
getCNPGClusterPrimary,
|
|
8
10
|
getCNPGClusterImageTag,
|
|
@@ -11,6 +13,7 @@ import {
|
|
|
11
13
|
getCNPGBackupCluster,
|
|
12
14
|
getCNPGBackupMethod,
|
|
13
15
|
getCNPGBackupDuration,
|
|
16
|
+
getCNPGBackupStartedAt,
|
|
14
17
|
getCNPGScheduledBackupStatus,
|
|
15
18
|
getCNPGScheduledBackupCluster,
|
|
16
19
|
getCNPGScheduleCron,
|
|
@@ -23,6 +26,11 @@ import {
|
|
|
23
26
|
getCNPGPoolerInstances,
|
|
24
27
|
} from '../resource-utils-cnpg'
|
|
25
28
|
|
|
29
|
+
/** Terminal = the operator has stopped reconciling; neighbouring cells are stale. */
|
|
30
|
+
function isTerminalPhase(resource: any): boolean {
|
|
31
|
+
return classifyCNPGClusterPhase(resource?.status?.phase || '') === 'terminal'
|
|
32
|
+
}
|
|
33
|
+
|
|
26
34
|
export function CNPGClusterCell({ resource, column }: { resource: any; column: string }) {
|
|
27
35
|
switch (column) {
|
|
28
36
|
case 'status': {
|
|
@@ -36,15 +44,38 @@ export function CNPGClusterCell({ resource, column }: { resource: any; column: s
|
|
|
36
44
|
case 'instances': {
|
|
37
45
|
const instances = getCNPGClusterInstances(resource)
|
|
38
46
|
const desired = resource.spec?.instances ?? 0
|
|
39
|
-
const
|
|
47
|
+
const readyKnown = typeof resource.status?.readyInstances === 'number'
|
|
48
|
+
const ready = readyKnown ? resource.status.readyInstances : 0
|
|
49
|
+
// A terminal cluster still reports a ready count, and the count is true —
|
|
50
|
+
// unrecoverable at 2/2 means the data is probably intact, at 0/2 it is down
|
|
51
|
+
// too. That distinction is worth keeping, so the cell is muted rather than
|
|
52
|
+
// hidden. Muting alone reads as "less important"; the tooltip is what says
|
|
53
|
+
// "this does not mean what you think".
|
|
54
|
+
if (isTerminalPhase(resource)) {
|
|
55
|
+
return (
|
|
56
|
+
<Tooltip content="Reconciliation has stopped — this count reflects the pods still running, not a working cluster.">
|
|
57
|
+
<span className="text-sm text-theme-text-tertiary">{instances}</span>
|
|
58
|
+
</Tooltip>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
40
61
|
return (
|
|
41
|
-
<span className={clsx('text-sm', ready < desired ? 'text-yellow-400' : 'text-theme-text-secondary')}>
|
|
62
|
+
<span className={clsx('text-sm', readyKnown && ready < desired ? 'text-yellow-400' : 'text-theme-text-secondary')}>
|
|
42
63
|
{instances}
|
|
43
64
|
</span>
|
|
44
65
|
)
|
|
45
66
|
}
|
|
46
67
|
case 'primary': {
|
|
47
68
|
const primary = getCNPGClusterPrimary(resource)
|
|
69
|
+
// status.currentPrimary is LAST KNOWN — CNPG never clears it. Naming a
|
|
70
|
+
// primary on a cluster that cannot elect one is an assertion Radar makes,
|
|
71
|
+
// not one CNPG makes, so say so rather than presenting it as current.
|
|
72
|
+
if (isTerminalPhase(resource)) {
|
|
73
|
+
return (
|
|
74
|
+
<Tooltip content="Last known primary — this cluster cannot currently elect one.">
|
|
75
|
+
<span className="text-sm text-theme-text-tertiary truncate block">{primary}</span>
|
|
76
|
+
</Tooltip>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
48
79
|
return <span className="text-sm text-theme-text-secondary truncate block">{primary}</span>
|
|
49
80
|
}
|
|
50
81
|
case 'image': {
|
|
@@ -78,6 +109,10 @@ export function CNPGBackupCell({ resource, column }: { resource: any; column: st
|
|
|
78
109
|
const method = getCNPGBackupMethod(resource)
|
|
79
110
|
return <span className="text-sm text-theme-text-secondary">{method}</span>
|
|
80
111
|
}
|
|
112
|
+
case 'started': {
|
|
113
|
+
const started = getCNPGBackupStartedAt(resource)
|
|
114
|
+
return <span className="text-sm text-theme-text-secondary">{started}</span>
|
|
115
|
+
}
|
|
81
116
|
case 'duration': {
|
|
82
117
|
const duration = getCNPGBackupDuration(resource)
|
|
83
118
|
return <span className="text-sm text-theme-text-secondary">{duration}</span>
|
|
@@ -68,7 +68,11 @@ export * from './KnativeServiceRenderer'
|
|
|
68
68
|
export * from './KnativeSourceRenderer'
|
|
69
69
|
export * from './KustomizationRenderer'
|
|
70
70
|
export * from './kyverno-cells'
|
|
71
|
+
export * from './kyverno-modern-cells'
|
|
71
72
|
export * from './KyvernoPolicyReportRenderer'
|
|
73
|
+
export * from './KyvernoPolicyShared'
|
|
74
|
+
export * from './KyvernoCELPolicyRenderers'
|
|
75
|
+
export * from './KyvernoExceptionRenderers'
|
|
72
76
|
export * from './LeaseRenderer'
|
|
73
77
|
export * from './NetworkPolicyRenderer'
|
|
74
78
|
export * from './NetworkPolicyDiagram'
|