@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.
Files changed (99) hide show
  1. package/package.json +5 -5
  2. package/src/components/applications/ApplicationDetail.test.tsx +46 -0
  3. package/src/components/applications/ApplicationDetail.tsx +14 -15
  4. package/src/components/issues/IssuesView.tsx +9 -0
  5. package/src/components/issues/diagnostic.ts +3 -0
  6. package/src/components/issues/issues.test.ts +11 -0
  7. package/src/components/issues/severity.ts +3 -0
  8. package/src/components/issues/types.ts +3 -0
  9. package/src/components/resources/ResourcesSidebar.tsx +1 -1
  10. package/src/components/resources/ResourcesView.tsx +259 -30
  11. package/src/components/resources/index.ts +2 -0
  12. package/src/components/resources/kyverno-cell-gating.test.ts +93 -0
  13. package/src/components/resources/kyverno-modern-posture.test.ts +290 -0
  14. package/src/components/resources/renderers/CNPGClusterRenderer.test.tsx +85 -0
  15. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +109 -9
  16. package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +8 -4
  17. package/src/components/resources/renderers/EventRenderer.test.tsx +23 -0
  18. package/src/components/resources/renderers/EventRenderer.tsx +10 -18
  19. package/src/components/resources/renderers/KyvernoCELPolicyRenderers.tsx +317 -0
  20. package/src/components/resources/renderers/KyvernoExceptionRenderers.tsx +264 -0
  21. package/src/components/resources/renderers/KyvernoPolicyShared.tsx +212 -0
  22. package/src/components/resources/renderers/VeleroBSLRenderer.tsx +2 -4
  23. package/src/components/resources/renderers/VeleroBackupRenderer.tsx +32 -9
  24. package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +30 -9
  25. package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +12 -6
  26. package/src/components/resources/renderers/badge-no-handrolled.test.tsx +1 -1
  27. package/src/components/resources/renderers/cnpg-cells.tsx +37 -2
  28. package/src/components/resources/renderers/index.ts +4 -0
  29. package/src/components/resources/renderers/kyverno-modern-cells.tsx +152 -0
  30. package/src/components/resources/renderers/velero-cells.tsx +128 -9
  31. package/src/components/resources/renderers/velero-phase-recovery.test.tsx +79 -0
  32. package/src/components/resources/resource-utils-cnpg.golden.test.ts +112 -0
  33. package/src/components/resources/resource-utils-cnpg.test.ts +527 -1
  34. package/src/components/resources/resource-utils-cnpg.ts +373 -56
  35. package/src/components/resources/resource-utils-kyverno-exceptions.ts +182 -0
  36. package/src/components/resources/resource-utils-kyverno-modern.ts +588 -0
  37. package/src/components/resources/resource-utils-velero.test.ts +237 -0
  38. package/src/components/resources/resource-utils-velero.ts +214 -22
  39. package/src/components/resources/resource-utils.ts +70 -3
  40. package/src/components/shared/ResourceActionsBar.tsx +3 -1
  41. package/src/components/shared/ResourceRendererDispatch.test.tsx +119 -1
  42. package/src/components/shared/ResourceRendererDispatch.tsx +116 -22
  43. package/src/components/topology/K8sResourceNode.tsx +11 -0
  44. package/src/components/topology/TopologyGraph.tsx +85 -5
  45. package/src/components/trace/ReachabilityGraph.tsx +517 -0
  46. package/src/components/trace/ReachabilityView.tsx +1173 -0
  47. package/src/components/trace/TracePanel.test.ts +172 -0
  48. package/src/components/trace/TracePanel.tsx +558 -0
  49. package/src/components/trace/TraceSummary.test.ts +213 -0
  50. package/src/components/trace/TraceSummary.tsx +168 -0
  51. package/src/components/trace/inClusterSummary.test.ts +98 -0
  52. package/src/components/trace/inClusterSummary.ts +81 -0
  53. package/src/components/trace/index.ts +19 -0
  54. package/src/components/trace/podReach.test.ts +71 -0
  55. package/src/components/trace/podReach.ts +37 -0
  56. package/src/components/trace/probe-display.test.ts +107 -0
  57. package/src/components/trace/probe-display.ts +37 -0
  58. package/src/components/trace/problemRows.test.ts +107 -0
  59. package/src/components/trace/reachFixtures.ts +261 -0
  60. package/src/components/trace/reachGraphModel.test.ts +1423 -0
  61. package/src/components/trace/reachGraphModel.ts +1521 -0
  62. package/src/components/trace/reachInspector.test.ts +892 -0
  63. package/src/components/trace/reachInspector.ts +797 -0
  64. package/src/components/trace/reachMarks.test.ts +698 -0
  65. package/src/components/trace/reachMarks.ts +623 -0
  66. package/src/components/trace/reachOrigins.test.ts +392 -0
  67. package/src/components/trace/reachOrigins.ts +449 -0
  68. package/src/components/trace/reachVerdict.test.ts +537 -0
  69. package/src/components/trace/reachVerdict.ts +601 -0
  70. package/src/components/trace/traceFingerprint.test.ts +214 -0
  71. package/src/components/trace/traceFingerprint.ts +122 -0
  72. package/src/components/trace/traceToSubgraph.test.ts +623 -0
  73. package/src/components/trace/traceToSubgraph.ts +463 -0
  74. package/src/components/trace/types.ts +414 -0
  75. package/src/components/ui/Badge.tsx +36 -0
  76. package/src/components/ui/ForceDeleteConfirmDialog.tsx +12 -1
  77. package/src/components/ui/InClusterConsentDialog.test.tsx +83 -0
  78. package/src/components/ui/InClusterConsentDialog.tsx +141 -0
  79. package/src/components/ui/index.ts +1 -0
  80. package/src/components/workload/WorkloadView.tsx +58 -5
  81. package/src/components/workload/index.ts +1 -1
  82. package/src/components/workload/reachabilityTab.test.ts +47 -0
  83. package/src/index.ts +5 -0
  84. package/src/theme/components.css +64 -0
  85. package/src/theme/variables.css +5 -0
  86. package/src/types/core.ts +8 -0
  87. package/src/utils/api-resources.ts +2 -0
  88. package/src/utils/application-history.test.ts +1 -1
  89. package/src/utils/applications.test.ts +63 -1
  90. package/src/utils/applications.ts +27 -0
  91. package/src/utils/inClusterConsent.test.ts +109 -0
  92. package/src/utils/inClusterConsent.ts +72 -0
  93. package/src/utils/index.ts +1 -0
  94. package/src/utils/navigation.test.ts +61 -0
  95. package/src/utils/navigation.ts +11 -9
  96. package/src/utils/pluralize.test.ts +80 -1
  97. package/src/utils/pluralize.ts +34 -0
  98. package/src/utils/resource-icons.test.ts +33 -0
  99. 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
- <span className={clsx('badge', bslStatus.color)}>
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 isPartiallyFailed = backupStatus.text === 'PartiallyFailed'
44
- const isInProgress = backupStatus.text === 'InProgress' || backupStatus.text === 'Uploading'
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
- {(isFailed || isPartiallyFailed) && (
61
+ {isValidationFailure && (
56
62
  <AlertBanner
57
63
  variant="error"
58
- title={isFailed ? 'Backup Failed' : 'Backup Partially Failed'}
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
- {warnings > 0 && !isFailed && (
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
- <span className={clsx('badge', backupStatus.color)}>
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 isPartiallyFailed = restoreStatus.text === 'PartiallyFailed'
37
- const isInProgress = restoreStatus.text === 'InProgress'
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
- {(isFailed || isPartiallyFailed) && (
54
+ {isValidationFailure && (
49
55
  <AlertBanner
50
56
  variant="error"
51
- title={isFailed ? 'Restore Failed' : 'Restore Partially Failed'}
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
- <span className={clsx('badge', restoreStatus.color)}>
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
- {scheduleStatus.text === 'FailedValidation' && (
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="The schedule spec failed validation and is not active."
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
- <span className={clsx('badge', scheduleStatus.color)}>
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', 'EventRenderer.tsx', 'GatewayRenderer.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 ready = resource.status?.readyInstances ?? 0
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'