@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.
Files changed (80) hide show
  1. package/package.json +1 -1
  2. package/src/components/gitops/GitOpsStatusBadge.tsx +40 -0
  3. package/src/components/gitops/GitOpsTableView.tsx +22 -2
  4. package/src/components/gitops/ReconcilingIndicator.test.tsx +31 -0
  5. package/src/components/gitops/detail-helpers.ts +4 -1
  6. package/src/components/issues/IssuesView.tsx +9 -0
  7. package/src/components/issues/diagnostic.ts +3 -0
  8. package/src/components/issues/issues.test.ts +11 -0
  9. package/src/components/issues/severity.ts +3 -0
  10. package/src/components/issues/types.ts +3 -0
  11. package/src/components/resources/ResourcesSidebar.tsx +1 -1
  12. package/src/components/resources/ResourcesView.tsx +261 -30
  13. package/src/components/resources/index.ts +2 -0
  14. package/src/components/resources/kyverno-cell-gating.test.ts +93 -0
  15. package/src/components/resources/kyverno-modern-posture.test.ts +290 -0
  16. package/src/components/resources/renderers/AlertRenderer.tsx +4 -1
  17. package/src/components/resources/renderers/CNPGClusterRenderer.test.tsx +85 -0
  18. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +133 -9
  19. package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +19 -4
  20. package/src/components/resources/renderers/CompositeRenderer.test.tsx +81 -0
  21. package/src/components/resources/renderers/CompositeRenderer.tsx +67 -7
  22. package/src/components/resources/renderers/EventRenderer.test.tsx +23 -0
  23. package/src/components/resources/renderers/EventRenderer.tsx +10 -18
  24. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +4 -1
  25. package/src/components/resources/renderers/GitRepositoryRenderer.tsx +4 -1
  26. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +4 -1
  27. package/src/components/resources/renderers/KustomizationRenderer.tsx +4 -1
  28. package/src/components/resources/renderers/KyvernoCELPolicyRenderers.tsx +317 -0
  29. package/src/components/resources/renderers/KyvernoExceptionRenderers.tsx +264 -0
  30. package/src/components/resources/renderers/KyvernoPolicyShared.tsx +212 -0
  31. package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +4 -1
  32. package/src/components/resources/renderers/PodRenderer.tsx +13 -0
  33. package/src/components/resources/renderers/PolicySection.test.tsx +102 -0
  34. package/src/components/resources/renderers/PolicySection.tsx +170 -0
  35. package/src/components/resources/renderers/VeleroBSLRenderer.tsx +2 -4
  36. package/src/components/resources/renderers/VeleroBackupRenderer.tsx +32 -9
  37. package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +30 -9
  38. package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +12 -6
  39. package/src/components/resources/renderers/WorkloadRenderer.tsx +15 -1
  40. package/src/components/resources/renderers/badge-no-handrolled.test.tsx +1 -1
  41. package/src/components/resources/renderers/cnpg-cells.tsx +37 -2
  42. package/src/components/resources/renderers/index.ts +5 -0
  43. package/src/components/resources/renderers/kyverno-cells.tsx +9 -0
  44. package/src/components/resources/renderers/kyverno-modern-cells.tsx +152 -0
  45. package/src/components/resources/renderers/policyreport-subject.test.tsx +27 -0
  46. package/src/components/resources/renderers/velero-cells.tsx +128 -9
  47. package/src/components/resources/renderers/velero-phase-recovery.test.tsx +79 -0
  48. package/src/components/resources/resource-utils-cnpg.golden.test.ts +112 -0
  49. package/src/components/resources/resource-utils-cnpg.test.ts +590 -1
  50. package/src/components/resources/resource-utils-cnpg.ts +433 -56
  51. package/src/components/resources/resource-utils-crossplane.test.ts +68 -0
  52. package/src/components/resources/resource-utils-kyverno-exceptions.ts +182 -0
  53. package/src/components/resources/resource-utils-kyverno-modern.ts +588 -0
  54. package/src/components/resources/resource-utils-velero.test.ts +237 -0
  55. package/src/components/resources/resource-utils-velero.ts +214 -22
  56. package/src/components/resources/resource-utils.ts +70 -3
  57. package/src/components/shared/ResourceActionsBar.tsx +3 -1
  58. package/src/components/shared/ResourceRendererDispatch.test.tsx +119 -1
  59. package/src/components/shared/ResourceRendererDispatch.tsx +116 -22
  60. package/src/components/topology/K8sResourceNode.tsx +6 -0
  61. package/src/components/trace/ReachabilityGraph.tsx +20 -6
  62. package/src/components/trace/ReachabilityView.tsx +253 -52
  63. package/src/components/trace/problemRows.test.ts +107 -0
  64. package/src/components/trace/reachGraphModel.test.ts +16 -0
  65. package/src/components/trace/reachInspector.test.ts +233 -10
  66. package/src/components/trace/reachInspector.ts +166 -36
  67. package/src/components/trace/reachMarks.test.ts +32 -1
  68. package/src/components/trace/reachMarks.ts +44 -0
  69. package/src/components/trace/reachOrigins.ts +12 -0
  70. package/src/components/trace/types.ts +25 -0
  71. package/src/components/ui/Badge.tsx +36 -0
  72. package/src/components/ui/ForceDeleteConfirmDialog.tsx +12 -1
  73. package/src/theme/components.css +26 -0
  74. package/src/types/gitops-flux-status.test.ts +163 -0
  75. package/src/types/gitops.ts +80 -24
  76. package/src/types/index.ts +1 -0
  77. package/src/types/policy.ts +42 -0
  78. package/src/utils/api-resources.ts +2 -0
  79. package/src/utils/resource-icons.test.ts +33 -0
  80. 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
- <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>
@@ -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', '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'
@@ -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>