@skyhook-io/k8s-ui 1.10.5 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- 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/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 +261 -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/AlertRenderer.tsx +4 -1
- package/src/components/resources/renderers/CNPGClusterRenderer.test.tsx +85 -0
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +133 -9
- package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +19 -4
- package/src/components/resources/renderers/CompositeRenderer.test.tsx +81 -0
- package/src/components/resources/renderers/CompositeRenderer.tsx +67 -7
- package/src/components/resources/renderers/EventRenderer.test.tsx +23 -0
- package/src/components/resources/renderers/EventRenderer.tsx +10 -18
- 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 +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/OCIRepositoryRenderer.tsx +4 -1
- package/src/components/resources/renderers/PodRenderer.tsx +13 -0
- package/src/components/resources/renderers/PolicySection.test.tsx +102 -0
- package/src/components/resources/renderers/PolicySection.tsx +170 -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/WorkloadRenderer.tsx +15 -1
- 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 +5 -0
- package/src/components/resources/renderers/kyverno-cells.tsx +9 -0
- package/src/components/resources/renderers/kyverno-modern-cells.tsx +152 -0
- package/src/components/resources/renderers/policyreport-subject.test.tsx +27 -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 +590 -1
- package/src/components/resources/resource-utils-cnpg.ts +433 -56
- package/src/components/resources/resource-utils-crossplane.test.ts +68 -0
- 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 +6 -0
- package/src/components/trace/ReachabilityGraph.tsx +20 -6
- package/src/components/trace/ReachabilityView.tsx +253 -52
- package/src/components/trace/problemRows.test.ts +107 -0
- package/src/components/trace/reachGraphModel.test.ts +16 -0
- package/src/components/trace/reachInspector.test.ts +233 -10
- package/src/components/trace/reachInspector.ts +166 -36
- package/src/components/trace/reachMarks.test.ts +32 -1
- package/src/components/trace/reachMarks.ts +44 -0
- package/src/components/trace/reachOrigins.ts +12 -0
- package/src/components/trace/types.ts +25 -0
- package/src/components/ui/Badge.tsx +36 -0
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +12 -1
- package/src/theme/components.css +26 -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 +42 -0
- package/src/utils/api-resources.ts +2 -0
- package/src/utils/resource-icons.test.ts +33 -0
- package/src/utils/resource-icons.ts +65 -2
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { ShieldCheck, ShieldAlert, ShieldQuestion } from 'lucide-react'
|
|
2
|
+
import { clsx } from 'clsx'
|
|
3
|
+
import { Section } from '../../ui/drawer-components'
|
|
4
|
+
import { SEVERITY_BADGE } from '../../../utils/badge-colors'
|
|
5
|
+
import { isForbiddenError } from '../../../types/fetch-error'
|
|
6
|
+
import type { PolicyResourceResponse, PolicyResourceFinding } from '../../../types/policy'
|
|
7
|
+
|
|
8
|
+
interface PolicySectionProps {
|
|
9
|
+
data: PolicyResourceResponse | null
|
|
10
|
+
loading?: boolean
|
|
11
|
+
error?: Error | null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const TITLE = 'Policy'
|
|
15
|
+
|
|
16
|
+
// Result → badge tone. `error` means the engine could not evaluate the rule,
|
|
17
|
+
// which is not the same as the rule failing, so it does not share fail's red.
|
|
18
|
+
function resultTone(result: string): { badge: string; label: string } {
|
|
19
|
+
switch (result.toLowerCase()) {
|
|
20
|
+
case 'fail':
|
|
21
|
+
return { badge: SEVERITY_BADGE.error, label: 'Fail' }
|
|
22
|
+
case 'warn':
|
|
23
|
+
return { badge: SEVERITY_BADGE.warning, label: 'Warn' }
|
|
24
|
+
case 'error':
|
|
25
|
+
return { badge: SEVERITY_BADGE.alert, label: 'Error' }
|
|
26
|
+
case 'skip':
|
|
27
|
+
return { badge: SEVERITY_BADGE.neutral, label: 'Skipped' }
|
|
28
|
+
default:
|
|
29
|
+
return { badge: SEVERITY_BADGE.neutral, label: result }
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* PolicySection reports what the cluster's policy engine says about this one
|
|
35
|
+
* resource.
|
|
36
|
+
*
|
|
37
|
+
* The unhappy paths carry the weight here. An empty finding list means one of
|
|
38
|
+
* several unrelated things, and rendering them all as blank space would tell an
|
|
39
|
+
* operator they are compliant in the cases where nothing was actually checked —
|
|
40
|
+
* the failure mode this section exists to avoid. Each state therefore says which
|
|
41
|
+
* one it is, in its own words, and the red treatment is reserved for genuine
|
|
42
|
+
* faults. Same division the RBAC sections already draw.
|
|
43
|
+
*/
|
|
44
|
+
export function PolicySection({ data, loading, error }: PolicySectionProps) {
|
|
45
|
+
if (loading) {
|
|
46
|
+
return (
|
|
47
|
+
<Section title={TITLE} icon={ShieldQuestion}>
|
|
48
|
+
<div className="text-sm text-theme-text-tertiary">Loading policy results…</div>
|
|
49
|
+
</Section>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (error) {
|
|
54
|
+
// A denial is an expected state on a namespace-scoped account, not a fault.
|
|
55
|
+
if (isForbiddenError(error)) {
|
|
56
|
+
return (
|
|
57
|
+
<Section title={TITLE} icon={ShieldQuestion}>
|
|
58
|
+
<div className="text-sm text-theme-text-tertiary">
|
|
59
|
+
You don’t have permission to view policy results in this namespace.
|
|
60
|
+
</div>
|
|
61
|
+
</Section>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
return (
|
|
65
|
+
<Section title={TITLE} icon={ShieldAlert}>
|
|
66
|
+
<div className="text-sm text-red-400">{`Could not load policy results: ${error.message}`}</div>
|
|
67
|
+
</Section>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// No engine installed — the section is genuinely not applicable, so it is
|
|
72
|
+
// absent rather than showing a reassuring "nothing to report". A disclaimer on
|
|
73
|
+
// every cluster without a policy engine would be noise.
|
|
74
|
+
if (!data || data.status === 'not_installed') return null
|
|
75
|
+
|
|
76
|
+
if (!data.evaluated) {
|
|
77
|
+
return (
|
|
78
|
+
<Section title={TITLE} icon={ShieldQuestion}>
|
|
79
|
+
<div className="text-sm text-theme-text-tertiary">
|
|
80
|
+
{(data.status === 'warmup'
|
|
81
|
+
? 'Policy results are still loading from the cluster.'
|
|
82
|
+
: 'Policy results are unavailable, so this resource has not been checked.') +
|
|
83
|
+
(data.reasonCode ? ` (${data.reasonCode})` : '')}
|
|
84
|
+
</div>
|
|
85
|
+
</Section>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const { counts, findings } = data
|
|
90
|
+
const checked = counts.pass + counts.fail + counts.warn + counts.error + counts.skip
|
|
91
|
+
|
|
92
|
+
// Evaluated, but no policy selected this resource at all. Distinct from
|
|
93
|
+
// "passing everything": nothing examined it, so there is nothing to pass.
|
|
94
|
+
if (checked === 0) {
|
|
95
|
+
return (
|
|
96
|
+
<Section title={TITLE} icon={ShieldQuestion}>
|
|
97
|
+
<div className="text-sm text-theme-text-tertiary">
|
|
98
|
+
No policy applies to this resource.
|
|
99
|
+
</div>
|
|
100
|
+
<PartialCoverageNote data={data} />
|
|
101
|
+
</Section>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const failing = findings.length
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<Section
|
|
109
|
+
title={TITLE}
|
|
110
|
+
icon={failing > 0 ? ShieldAlert : ShieldCheck}
|
|
111
|
+
defaultExpanded={failing > 0}
|
|
112
|
+
>
|
|
113
|
+
{failing === 0 ? (
|
|
114
|
+
<div className="text-sm text-theme-text-secondary">
|
|
115
|
+
{`All ${counts.pass} ${counts.pass === 1 ? 'check' : 'checks'} passing.`}
|
|
116
|
+
</div>
|
|
117
|
+
) : (
|
|
118
|
+
<div className="space-y-2">
|
|
119
|
+
{findings.map((f, i) => (
|
|
120
|
+
<PolicyFindingRow key={`${f.policy}/${f.rule}/${i}`} finding={f} />
|
|
121
|
+
))}
|
|
122
|
+
{counts.pass > 0 && (
|
|
123
|
+
<div className="text-xs text-theme-text-tertiary pt-1">
|
|
124
|
+
{`${counts.pass} other ${counts.pass === 1 ? 'check' : 'checks'} passing`}
|
|
125
|
+
</div>
|
|
126
|
+
)}
|
|
127
|
+
</div>
|
|
128
|
+
)}
|
|
129
|
+
<PartialCoverageNote data={data} />
|
|
130
|
+
</Section>
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function PolicyFindingRow({ finding }: { finding: PolicyResourceFinding }) {
|
|
135
|
+
const tone = resultTone(finding.result)
|
|
136
|
+
return (
|
|
137
|
+
<div className="flex items-start gap-2 min-w-0">
|
|
138
|
+
<span className={clsx('badge shrink-0', tone.badge)}>{tone.label}</span>
|
|
139
|
+
<div className="min-w-0">
|
|
140
|
+
<div className="text-sm text-theme-text-primary truncate" title={finding.policy}>
|
|
141
|
+
{finding.policy}
|
|
142
|
+
{finding.rule && finding.rule !== finding.policy && (
|
|
143
|
+
<span className="text-theme-text-tertiary"> · {finding.rule}</span>
|
|
144
|
+
)}
|
|
145
|
+
</div>
|
|
146
|
+
{finding.message && (
|
|
147
|
+
<div className="text-xs text-theme-text-secondary">{finding.message}</div>
|
|
148
|
+
)}
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* A result can be real and incomplete at the same time: some report families
|
|
156
|
+
* may be unreadable by this identity, or the index may be frozen. Saying
|
|
157
|
+
* "all checks passing" without mentioning that claims coverage we do not have.
|
|
158
|
+
*/
|
|
159
|
+
function PartialCoverageNote({ data }: { data: PolicyResourceResponse }) {
|
|
160
|
+
const denied = data.deniedGroups ?? []
|
|
161
|
+
if (denied.length === 0 && data.liveUpdates) return null
|
|
162
|
+
return (
|
|
163
|
+
<div className="mt-2 pt-2 border-t border-theme-border text-xs text-theme-text-tertiary">
|
|
164
|
+
{denied.length > 0 && (
|
|
165
|
+
<div>{`Some policy results could not be read (${denied.join(', ')}), so this list may be incomplete.`}</div>
|
|
166
|
+
)}
|
|
167
|
+
{!data.liveUpdates && <div>These results are not updating live.</div>}
|
|
168
|
+
</div>
|
|
169
|
+
)
|
|
170
|
+
}
|
|
@@ -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>
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { useState, useEffect } from 'react'
|
|
2
2
|
import { Server, ExternalLink, Scale, Minus, Plus, Loader2, 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, PodTemplateSection, AlertBanner, ResourceLink, ResourceRefBadge, useOperationalIssuesShown } from '../../ui/drawer-components'
|
|
5
7
|
import { DialogPortal } from '../../ui/DialogPortal'
|
|
6
8
|
import { Tooltip } from '../../ui/Tooltip'
|
|
@@ -40,6 +42,14 @@ interface WorkloadRendererProps {
|
|
|
40
42
|
rbacData?: RBACSubjectResponse | null
|
|
41
43
|
rbacLoading?: boolean
|
|
42
44
|
rbacError?: Error | null
|
|
45
|
+
/**
|
|
46
|
+
* Policy findings for this workload. Undefined means the host didn't wire the
|
|
47
|
+
* fetch, so the section is omitted entirely — consumers that skip it get the
|
|
48
|
+
* original sections and nothing breaks.
|
|
49
|
+
*/
|
|
50
|
+
policyData?: PolicyResourceResponse | null
|
|
51
|
+
policyLoading?: boolean
|
|
52
|
+
policyError?: Error | null
|
|
43
53
|
}
|
|
44
54
|
|
|
45
55
|
// Check if the workload is actively progressing (scaling, rolling update)
|
|
@@ -140,7 +150,7 @@ function compactHPASummary(diagnosis: HPADiagnosis): string {
|
|
|
140
150
|
return diagnosis.summary
|
|
141
151
|
}
|
|
142
152
|
|
|
143
|
-
export function WorkloadRenderer({ kind, data, onNavigate, onViewPods, onScale, isScalePending, scaleBlockedBy, scalerDiagnostics, onRequestRefresh, rbacData, rbacLoading, rbacError }: WorkloadRendererProps) {
|
|
153
|
+
export function WorkloadRenderer({ kind, data, onNavigate, onViewPods, onScale, isScalePending, scaleBlockedBy, scalerDiagnostics, onRequestRefresh, rbacData, rbacLoading, rbacError, policyData, policyLoading, policyError }: WorkloadRendererProps) {
|
|
144
154
|
const status = data.status || {}
|
|
145
155
|
const spec = data.spec || {}
|
|
146
156
|
const metadata = data.metadata || {}
|
|
@@ -372,6 +382,10 @@ export function WorkloadRenderer({ kind, data, onNavigate, onViewPods, onScale,
|
|
|
372
382
|
</div>
|
|
373
383
|
</DialogPortal>
|
|
374
384
|
|
|
385
|
+
{policyData !== undefined && (
|
|
386
|
+
<PolicySection data={policyData} loading={policyLoading} error={policyError} />
|
|
387
|
+
)}
|
|
388
|
+
|
|
375
389
|
<Section title="Strategy">
|
|
376
390
|
<PropertyList>
|
|
377
391
|
{isDaemonSet || isStatefulSet ? (
|
|
@@ -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'
|
|
@@ -156,3 +160,4 @@ export * from './ResourceSliceRenderer'
|
|
|
156
160
|
export * from './nvidia-cells'
|
|
157
161
|
export * from './NvidiaClusterPolicyRenderer'
|
|
158
162
|
export * from './NvidiaDriverRenderer'
|
|
163
|
+
export { PolicySection } from './PolicySection'
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { clsx } from 'clsx'
|
|
4
4
|
import {
|
|
5
|
+
getPolicyReportScope,
|
|
5
6
|
getPolicyReportStatus,
|
|
6
7
|
getPolicyReportSummary,
|
|
7
8
|
getKyvernoPolicyStatus,
|
|
@@ -19,6 +20,14 @@ export function PolicyReportCell({ resource, column }: { resource: any; column:
|
|
|
19
20
|
</span>
|
|
20
21
|
)
|
|
21
22
|
}
|
|
23
|
+
// Kyverno names a per-resource report after the subject's UID, so the Name
|
|
24
|
+
// column reads as a bare UUID. Without the subject the whole table is
|
|
25
|
+
// unidentifiable rows with counts beside them — you cannot tell which of
|
|
26
|
+
// your workloads a failing report belongs to without opening every one.
|
|
27
|
+
case 'scope': {
|
|
28
|
+
const scope = getPolicyReportScope(resource)
|
|
29
|
+
return <span className="truncate text-theme-text-primary" title={scope}>{scope}</span>
|
|
30
|
+
}
|
|
22
31
|
case 'pass': {
|
|
23
32
|
const summary = getPolicyReportSummary(resource)
|
|
24
33
|
return <span className={clsx('text-sm', summary.pass > 0 ? 'text-green-400' : 'text-theme-text-tertiary')}>{summary.pass}</span>
|