@skyhook-io/k8s-ui 1.10.4 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -5
- package/src/components/applications/ApplicationDetail.test.tsx +46 -0
- package/src/components/applications/ApplicationDetail.tsx +14 -15
- package/src/components/issues/IssuesView.tsx +9 -0
- package/src/components/issues/diagnostic.ts +3 -0
- package/src/components/issues/issues.test.ts +11 -0
- package/src/components/issues/severity.ts +3 -0
- package/src/components/issues/types.ts +3 -0
- package/src/components/resources/ResourcesSidebar.tsx +1 -1
- package/src/components/resources/ResourcesView.tsx +259 -30
- package/src/components/resources/index.ts +2 -0
- package/src/components/resources/kyverno-cell-gating.test.ts +93 -0
- package/src/components/resources/kyverno-modern-posture.test.ts +290 -0
- package/src/components/resources/renderers/CNPGClusterRenderer.test.tsx +85 -0
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +109 -9
- package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +8 -4
- package/src/components/resources/renderers/EventRenderer.test.tsx +23 -0
- package/src/components/resources/renderers/EventRenderer.tsx +10 -18
- package/src/components/resources/renderers/KyvernoCELPolicyRenderers.tsx +317 -0
- package/src/components/resources/renderers/KyvernoExceptionRenderers.tsx +264 -0
- package/src/components/resources/renderers/KyvernoPolicyShared.tsx +212 -0
- package/src/components/resources/renderers/VeleroBSLRenderer.tsx +2 -4
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +32 -9
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +30 -9
- package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +12 -6
- package/src/components/resources/renderers/badge-no-handrolled.test.tsx +1 -1
- package/src/components/resources/renderers/cnpg-cells.tsx +37 -2
- package/src/components/resources/renderers/index.ts +4 -0
- package/src/components/resources/renderers/kyverno-modern-cells.tsx +152 -0
- package/src/components/resources/renderers/velero-cells.tsx +128 -9
- package/src/components/resources/renderers/velero-phase-recovery.test.tsx +79 -0
- package/src/components/resources/resource-utils-cnpg.golden.test.ts +112 -0
- package/src/components/resources/resource-utils-cnpg.test.ts +527 -1
- package/src/components/resources/resource-utils-cnpg.ts +373 -56
- package/src/components/resources/resource-utils-kyverno-exceptions.ts +182 -0
- package/src/components/resources/resource-utils-kyverno-modern.ts +588 -0
- package/src/components/resources/resource-utils-velero.test.ts +237 -0
- package/src/components/resources/resource-utils-velero.ts +214 -22
- package/src/components/resources/resource-utils.ts +70 -3
- package/src/components/shared/ResourceActionsBar.tsx +3 -1
- package/src/components/shared/ResourceRendererDispatch.test.tsx +119 -1
- package/src/components/shared/ResourceRendererDispatch.tsx +116 -22
- package/src/components/topology/K8sResourceNode.tsx +11 -0
- package/src/components/topology/TopologyGraph.tsx +85 -5
- package/src/components/trace/ReachabilityGraph.tsx +517 -0
- package/src/components/trace/ReachabilityView.tsx +1173 -0
- package/src/components/trace/TracePanel.test.ts +172 -0
- package/src/components/trace/TracePanel.tsx +558 -0
- package/src/components/trace/TraceSummary.test.ts +213 -0
- package/src/components/trace/TraceSummary.tsx +168 -0
- package/src/components/trace/inClusterSummary.test.ts +98 -0
- package/src/components/trace/inClusterSummary.ts +81 -0
- package/src/components/trace/index.ts +19 -0
- package/src/components/trace/podReach.test.ts +71 -0
- package/src/components/trace/podReach.ts +37 -0
- package/src/components/trace/probe-display.test.ts +107 -0
- package/src/components/trace/probe-display.ts +37 -0
- package/src/components/trace/problemRows.test.ts +107 -0
- package/src/components/trace/reachFixtures.ts +261 -0
- package/src/components/trace/reachGraphModel.test.ts +1423 -0
- package/src/components/trace/reachGraphModel.ts +1521 -0
- package/src/components/trace/reachInspector.test.ts +892 -0
- package/src/components/trace/reachInspector.ts +797 -0
- package/src/components/trace/reachMarks.test.ts +698 -0
- package/src/components/trace/reachMarks.ts +623 -0
- package/src/components/trace/reachOrigins.test.ts +392 -0
- package/src/components/trace/reachOrigins.ts +449 -0
- package/src/components/trace/reachVerdict.test.ts +537 -0
- package/src/components/trace/reachVerdict.ts +601 -0
- package/src/components/trace/traceFingerprint.test.ts +214 -0
- package/src/components/trace/traceFingerprint.ts +122 -0
- package/src/components/trace/traceToSubgraph.test.ts +623 -0
- package/src/components/trace/traceToSubgraph.ts +463 -0
- package/src/components/trace/types.ts +414 -0
- package/src/components/ui/Badge.tsx +36 -0
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +12 -1
- package/src/components/ui/InClusterConsentDialog.test.tsx +83 -0
- package/src/components/ui/InClusterConsentDialog.tsx +141 -0
- package/src/components/ui/index.ts +1 -0
- package/src/components/workload/WorkloadView.tsx +58 -5
- package/src/components/workload/index.ts +1 -1
- package/src/components/workload/reachabilityTab.test.ts +47 -0
- package/src/index.ts +5 -0
- package/src/theme/components.css +64 -0
- package/src/theme/variables.css +5 -0
- package/src/types/core.ts +8 -0
- package/src/utils/api-resources.ts +2 -0
- package/src/utils/application-history.test.ts +1 -1
- package/src/utils/applications.test.ts +63 -1
- package/src/utils/applications.ts +27 -0
- package/src/utils/inClusterConsent.test.ts +109 -0
- package/src/utils/inClusterConsent.ts +72 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/navigation.test.ts +61 -0
- package/src/utils/navigation.ts +11 -9
- package/src/utils/pluralize.test.ts +80 -1
- package/src/utils/pluralize.ts +34 -0
- package/src/utils/resource-icons.test.ts +33 -0
- package/src/utils/resource-icons.ts +65 -2
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// Cell components for the modern Kyverno CEL family, PolicyException and
|
|
2
|
+
// CleanupPolicy rows in ResourcesView.
|
|
3
|
+
|
|
4
|
+
import { clsx } from 'clsx'
|
|
5
|
+
import { formatAge } from '../resource-utils'
|
|
6
|
+
import {
|
|
7
|
+
getKyvernoAttestors,
|
|
8
|
+
getKyvernoDeletionConditions,
|
|
9
|
+
getKyvernoGenerations,
|
|
10
|
+
getKyvernoImageReferences,
|
|
11
|
+
getKyvernoMatchSummary,
|
|
12
|
+
getKyvernoMutations,
|
|
13
|
+
getKyvernoPolicyFamily,
|
|
14
|
+
getKyvernoLastExecutionTime,
|
|
15
|
+
getKyvernoSchedule,
|
|
16
|
+
getKyvernoValidations,
|
|
17
|
+
getModernKyvernoPolicyStatus,
|
|
18
|
+
} from '../resource-utils-kyverno-modern'
|
|
19
|
+
import {
|
|
20
|
+
getKyvernoCleanupMatchedKinds,
|
|
21
|
+
getKyvernoCleanupPolicyStatus,
|
|
22
|
+
getKyvernoCleanupSchedule,
|
|
23
|
+
getKyvernoExceptedPolicies,
|
|
24
|
+
getKyvernoPolicyExceptionStatus,
|
|
25
|
+
isBlanketKyvernoException,
|
|
26
|
+
} from '../resource-utils-kyverno-exceptions'
|
|
27
|
+
|
|
28
|
+
const dash = <span className="text-sm text-theme-text-tertiary">-</span>
|
|
29
|
+
|
|
30
|
+
/** Counts the per-family "rules" the row's Rules/Mutations/Generates column shows. */
|
|
31
|
+
function ruleCount(resource: any): number {
|
|
32
|
+
switch (getKyvernoPolicyFamily(resource?.kind)) {
|
|
33
|
+
case 'mutating':
|
|
34
|
+
return getKyvernoMutations(resource).length
|
|
35
|
+
case 'generating':
|
|
36
|
+
return getKyvernoGenerations(resource).length
|
|
37
|
+
case 'deleting':
|
|
38
|
+
return getKyvernoDeletionConditions(resource).length
|
|
39
|
+
default:
|
|
40
|
+
return getKyvernoValidations(resource).length
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function KyvernoModernPolicyCell({ resource, column }: { resource: any; column: string }) {
|
|
45
|
+
switch (column) {
|
|
46
|
+
case 'status': {
|
|
47
|
+
const status = getModernKyvernoPolicyStatus(resource)
|
|
48
|
+
return <span className={clsx('badge', status.color)}>{status.text}</span>
|
|
49
|
+
}
|
|
50
|
+
case 'appliesTo':
|
|
51
|
+
return <span className="text-sm text-theme-text-secondary">{getKyvernoMatchSummary(resource)}</span>
|
|
52
|
+
case 'rules': {
|
|
53
|
+
const count = ruleCount(resource)
|
|
54
|
+
// A DeletingPolicy with zero conditions deletes everything it matches,
|
|
55
|
+
// so a bare "0" would read as harmless when it is the opposite.
|
|
56
|
+
if (getKyvernoPolicyFamily(resource?.kind) === 'deleting' && count === 0) {
|
|
57
|
+
return <span className="text-sm text-theme-text-secondary">All matched</span>
|
|
58
|
+
}
|
|
59
|
+
return <span className="text-sm text-theme-text-secondary">{count}</span>
|
|
60
|
+
}
|
|
61
|
+
case 'lastRun': {
|
|
62
|
+
// Absence is stated, never blank. A blank cell reads the same whether the
|
|
63
|
+
// policy ran and we failed to show it or it has never run at all — and
|
|
64
|
+
// "never run" is the actual failure mode for a scheduled policy, so it
|
|
65
|
+
// has to be the loud state rather than the empty one.
|
|
66
|
+
const last = getKyvernoLastExecutionTime(resource)
|
|
67
|
+
if (!last) {
|
|
68
|
+
return (
|
|
69
|
+
<span className="badge status-degraded" title="This policy has not run since it was created">
|
|
70
|
+
Never run
|
|
71
|
+
</span>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
return <span className="text-sm text-theme-text-secondary">{formatAge(last)} ago</span>
|
|
75
|
+
}
|
|
76
|
+
case 'schedule': {
|
|
77
|
+
const schedule = getKyvernoSchedule(resource)
|
|
78
|
+
return schedule ? (
|
|
79
|
+
<span className="text-sm font-mono text-theme-text-secondary">{schedule}</span>
|
|
80
|
+
) : (
|
|
81
|
+
<span className="text-sm text-theme-text-tertiary">Not set</span>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
case 'images': {
|
|
85
|
+
const refs = getKyvernoImageReferences(resource)
|
|
86
|
+
if (refs.length === 0) return <span className="text-sm text-theme-text-tertiary">All</span>
|
|
87
|
+
return (
|
|
88
|
+
<span className="text-sm font-mono text-theme-text-secondary truncate">
|
|
89
|
+
{refs.length === 1 ? refs[0] : `${refs[0]} +${refs.length - 1}`}
|
|
90
|
+
</span>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
case 'attestors': {
|
|
94
|
+
const attestors = getKyvernoAttestors(resource)
|
|
95
|
+
// No attestor means nothing to verify signatures against.
|
|
96
|
+
if (attestors.length === 0) return <span className="badge status-degraded">None</span>
|
|
97
|
+
return <span className="text-sm text-theme-text-secondary">{attestors.length}</span>
|
|
98
|
+
}
|
|
99
|
+
default:
|
|
100
|
+
return dash
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function KyvernoPolicyExceptionCell({ resource, column }: { resource: any; column: string }) {
|
|
105
|
+
switch (column) {
|
|
106
|
+
case 'status': {
|
|
107
|
+
const status = getKyvernoPolicyExceptionStatus(resource)
|
|
108
|
+
return <span className={clsx('badge', status.color)}>{status.text}</span>
|
|
109
|
+
}
|
|
110
|
+
case 'policies': {
|
|
111
|
+
if (isBlanketKyvernoException(resource)) {
|
|
112
|
+
return <span className="text-sm text-theme-text-secondary">Every matching policy</span>
|
|
113
|
+
}
|
|
114
|
+
const names = getKyvernoExceptedPolicies(resource).map((p) => p.name)
|
|
115
|
+
return (
|
|
116
|
+
<span className="text-sm text-theme-text-secondary truncate">
|
|
117
|
+
{names.length <= 2 ? names.join(', ') : `${names.slice(0, 2).join(', ')} +${names.length - 2}`}
|
|
118
|
+
</span>
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
default:
|
|
122
|
+
return dash
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function KyvernoCleanupPolicyCell({ resource, column }: { resource: any; column: string }) {
|
|
127
|
+
switch (column) {
|
|
128
|
+
case 'status': {
|
|
129
|
+
const status = getKyvernoCleanupPolicyStatus(resource)
|
|
130
|
+
return <span className={clsx('badge', status.color)}>{status.text}</span>
|
|
131
|
+
}
|
|
132
|
+
case 'schedule': {
|
|
133
|
+
const schedule = getKyvernoCleanupSchedule(resource)
|
|
134
|
+
return schedule ? (
|
|
135
|
+
<span className="text-sm font-mono text-theme-text-secondary">{schedule}</span>
|
|
136
|
+
) : (
|
|
137
|
+
<span className="text-sm text-theme-text-tertiary">Not set</span>
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
case 'appliesTo': {
|
|
141
|
+
const kinds = getKyvernoCleanupMatchedKinds(resource)
|
|
142
|
+
if (kinds.length === 0) return dash
|
|
143
|
+
return (
|
|
144
|
+
<span className="text-sm text-theme-text-secondary truncate">
|
|
145
|
+
{kinds.length <= 3 ? kinds.join(', ') : `${kinds.slice(0, 3).join(', ')} +${kinds.length - 3}`}
|
|
146
|
+
</span>
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
default:
|
|
150
|
+
return dash
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Velero cell components for ResourcesView table
|
|
2
2
|
|
|
3
3
|
import { clsx } from 'clsx'
|
|
4
|
+
import { Pause } from 'lucide-react'
|
|
5
|
+
import { Tooltip } from '../../ui/Tooltip'
|
|
4
6
|
import {
|
|
5
7
|
getBackupStatus,
|
|
6
8
|
getBackupStorageLocation,
|
|
@@ -15,16 +17,48 @@ import {
|
|
|
15
17
|
getRestoreDuration,
|
|
16
18
|
getRestoreErrors,
|
|
17
19
|
getScheduleStatus,
|
|
18
|
-
getScheduleCron,
|
|
19
|
-
getScheduleLastBackup,
|
|
20
20
|
getSchedulePaused,
|
|
21
|
+
getScheduleCronInfo,
|
|
22
|
+
getScheduleLastBackup,
|
|
21
23
|
getBSLStatus,
|
|
22
24
|
getBSLProvider,
|
|
23
25
|
getBSLBucket,
|
|
24
26
|
getBSLDefault,
|
|
25
27
|
getBSLLastValidation,
|
|
28
|
+
getVSLProvider,
|
|
29
|
+
getVSLConfig,
|
|
30
|
+
getBackupRepositoryStatus,
|
|
31
|
+
getBackupRepositoryType,
|
|
26
32
|
} from '../resource-utils-velero'
|
|
27
33
|
|
|
34
|
+
// The detail-page Phase value. The badge carries the same label the table
|
|
35
|
+
// showed, so the row you clicked and the drawer you landed on agree — but the
|
|
36
|
+
// drawer has no width budget, so the raw `.status.phase` rides alongside it
|
|
37
|
+
// whenever the two differ. Without this the three collapsed partial-failure
|
|
38
|
+
// phases would be unreachable outside the YAML tab, and the label would stop
|
|
39
|
+
// being a display choice and start being a loss of information.
|
|
40
|
+
export function VeleroPhaseValue({
|
|
41
|
+
status,
|
|
42
|
+
phase,
|
|
43
|
+
}: {
|
|
44
|
+
status: { text: string; color: string }
|
|
45
|
+
phase: string
|
|
46
|
+
}) {
|
|
47
|
+
// Stacked rather than side-by-side: the property column is ~300px and the
|
|
48
|
+
// longest phase is 40 unbroken characters, so a row would clip the phase and
|
|
49
|
+
// wrap the badge at the same time. `break-all` because the token has no break
|
|
50
|
+
// opportunity — in a detail panel wrapping is the right answer, since the
|
|
51
|
+
// whole point of the value is that it is exact.
|
|
52
|
+
return (
|
|
53
|
+
<span className="flex flex-col items-start gap-1 min-w-0">
|
|
54
|
+
<span className={clsx('badge whitespace-nowrap', status.color)}>{status.text}</span>
|
|
55
|
+
{phase && phase !== status.text && (
|
|
56
|
+
<span className="text-xs font-mono text-theme-text-tertiary break-all">{phase}</span>
|
|
57
|
+
)}
|
|
58
|
+
</span>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
28
62
|
export function BackupCell({ resource, column }: { resource: any; column: string }) {
|
|
29
63
|
switch (column) {
|
|
30
64
|
case 'status': {
|
|
@@ -109,23 +143,108 @@ export function ScheduleCell({ resource, column }: { resource: any; column: stri
|
|
|
109
143
|
switch (column) {
|
|
110
144
|
case 'status': {
|
|
111
145
|
const status = getScheduleStatus(resource)
|
|
146
|
+
// A paused schedule that is *also* rejected carries two independent facts
|
|
147
|
+
// and the badge can only show one. The failure takes the badge because it
|
|
148
|
+
// is the one to act on; paused rides alongside as a chip — the same
|
|
149
|
+
// primary-plus-secondary shape CellContent uses for Terminating in the
|
|
150
|
+
// name column. A chip rather than a second word is what keeps it inside
|
|
151
|
+
// the column budget: "Rejected" plus a spelled-out "Paused" does not fit.
|
|
152
|
+
const alsoPaused = getSchedulePaused(resource) && status.text !== 'Paused'
|
|
112
153
|
return (
|
|
113
|
-
<span className=
|
|
114
|
-
{status.
|
|
154
|
+
<span className="inline-flex items-center gap-1 min-w-0">
|
|
155
|
+
<span className={clsx('badge', status.color)}>
|
|
156
|
+
{status.text}
|
|
157
|
+
</span>
|
|
158
|
+
{alsoPaused && (
|
|
159
|
+
<Tooltip content="Also paused — this schedule will not run until it is resumed">
|
|
160
|
+
<Pause className="w-3 h-3 shrink-0 text-amber-600 dark:text-amber-400" />
|
|
161
|
+
</Tooltip>
|
|
162
|
+
)}
|
|
115
163
|
</span>
|
|
116
164
|
)
|
|
117
165
|
}
|
|
118
166
|
case 'schedule': {
|
|
119
|
-
const cron =
|
|
120
|
-
return
|
|
167
|
+
const { cron, readable, malformed } = getScheduleCronInfo(resource)
|
|
168
|
+
return (
|
|
169
|
+
<div className="flex flex-col min-w-0">
|
|
170
|
+
<span
|
|
171
|
+
className={clsx(
|
|
172
|
+
'text-sm font-mono truncate',
|
|
173
|
+
malformed ? 'text-red-600 dark:text-red-400' : 'text-theme-text-secondary'
|
|
174
|
+
)}
|
|
175
|
+
// Cron has no bounded width — `*/15 9-17 * * 1-5` measures 143px
|
|
176
|
+
// against 128px of cell — so the raw value carries a title on every
|
|
177
|
+
// row, not just the malformed ones.
|
|
178
|
+
title={malformed ? `${cron} is not a valid cron expression — this schedule cannot run` : cron}
|
|
179
|
+
>
|
|
180
|
+
{cron}
|
|
181
|
+
</span>
|
|
182
|
+
{/* The second line is always occupied, even when there is nothing to
|
|
183
|
+
say. Dropping it for the rows cronToHuman can't phrase left those
|
|
184
|
+
rows 10px shorter than their neighbours, which is the ragged
|
|
185
|
+
rhythm this table already had once. `invisible` reserves the line
|
|
186
|
+
without painting or announcing it. */}
|
|
187
|
+
{malformed ? (
|
|
188
|
+
<span className="text-xs text-red-600 dark:text-red-400">Invalid cron</span>
|
|
189
|
+
) : readable ? (
|
|
190
|
+
<span className="text-xs text-theme-text-tertiary truncate">{readable}</span>
|
|
191
|
+
) : (
|
|
192
|
+
<span aria-hidden className="text-xs invisible"> </span>
|
|
193
|
+
)}
|
|
194
|
+
</div>
|
|
195
|
+
)
|
|
121
196
|
}
|
|
122
197
|
case 'lastBackup': {
|
|
123
198
|
const last = getScheduleLastBackup(resource)
|
|
124
199
|
return <span className="text-sm text-theme-text-secondary">{last}</span>
|
|
125
200
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
201
|
+
default:
|
|
202
|
+
return <span className="text-sm text-theme-text-tertiary">-</span>
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function BackupRepositoryCell({ resource, column }: { resource: any; column: string }) {
|
|
207
|
+
switch (column) {
|
|
208
|
+
case 'status': {
|
|
209
|
+
const status = getBackupRepositoryStatus(resource)
|
|
210
|
+
return (
|
|
211
|
+
<span className={clsx('badge', status.color)}>
|
|
212
|
+
{status.text}
|
|
213
|
+
</span>
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
case 'repositoryType': {
|
|
217
|
+
const type = getBackupRepositoryType(resource)
|
|
218
|
+
// restic is on its way out (no new backups since v1.17, restore dropped in
|
|
219
|
+
// v1.19), so flag it rather than rendering it as neutral configuration.
|
|
220
|
+
const isLegacy = type === 'restic'
|
|
221
|
+
return (
|
|
222
|
+
<span className={clsx('text-sm', isLegacy ? 'text-amber-600 dark:text-amber-400 font-medium' : 'text-theme-text-secondary')}>
|
|
223
|
+
{type}
|
|
224
|
+
</span>
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
default:
|
|
228
|
+
return <span className="text-sm text-theme-text-tertiary">-</span>
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// VolumeSnapshotLocation has no status column on purpose: the VSL controller
|
|
233
|
+
// does not populate status.phase, so a badge would read "Unknown" forever.
|
|
234
|
+
export function VolumeSnapshotLocationCell({ resource, column }: { resource: any; column: string }) {
|
|
235
|
+
switch (column) {
|
|
236
|
+
case 'provider': {
|
|
237
|
+
const provider = getVSLProvider(resource)
|
|
238
|
+
return <span className="text-sm text-theme-text-secondary truncate block">{provider}</span>
|
|
239
|
+
}
|
|
240
|
+
case 'config': {
|
|
241
|
+
const config = getVSLConfig(resource)
|
|
242
|
+
const keys = Object.keys(config)
|
|
243
|
+
if (keys.length === 0) {
|
|
244
|
+
return <span className="text-sm text-theme-text-tertiary">-</span>
|
|
245
|
+
}
|
|
246
|
+
const summary = keys.map((k) => `${k}=${config[k]}`).join(', ')
|
|
247
|
+
return <span className="text-sm text-theme-text-secondary truncate block" title={summary}>{summary}</span>
|
|
129
248
|
}
|
|
130
249
|
default:
|
|
131
250
|
return <span className="text-sm text-theme-text-tertiary">-</span>
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { renderToString } from 'react-dom/server'
|
|
3
|
+
import { VeleroPhaseValue } from './velero-cells'
|
|
4
|
+
import { VeleroBackupRenderer } from './VeleroBackupRenderer'
|
|
5
|
+
import { VeleroRestoreRenderer } from './VeleroRestoreRenderer'
|
|
6
|
+
import { VeleroScheduleRenderer } from './VeleroScheduleRenderer'
|
|
7
|
+
import { VeleroBSLRenderer } from './VeleroBSLRenderer'
|
|
8
|
+
|
|
9
|
+
// The table deliberately shows a shortened label, and three distinct phases
|
|
10
|
+
// collapse onto one of them. That is only defensible while the exact phase
|
|
11
|
+
// stays reachable without reading YAML, so these tests guard the drawer.
|
|
12
|
+
|
|
13
|
+
const backup = (phase: string) => ({
|
|
14
|
+
metadata: { name: 'b', namespace: 'velero' },
|
|
15
|
+
spec: {},
|
|
16
|
+
status: { phase },
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
describe('exact Velero phase stays reachable in the drawer', () => {
|
|
20
|
+
it.each([
|
|
21
|
+
'WaitingForPluginOperationsPartiallyFailed',
|
|
22
|
+
'FinalizingPartiallyFailed',
|
|
23
|
+
'PartiallyFailed',
|
|
24
|
+
'WaitingForPluginOperations',
|
|
25
|
+
'FailedValidation',
|
|
26
|
+
'InProgress',
|
|
27
|
+
'ReadyToStart',
|
|
28
|
+
])('Backup detail prints the raw %s', (phase) => {
|
|
29
|
+
expect(renderToString(<VeleroBackupRenderer data={backup(phase)} />)).toContain(phase)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('Restore detail prints the raw phase', () => {
|
|
33
|
+
const html = renderToString(
|
|
34
|
+
<VeleroRestoreRenderer data={{ metadata: { name: 'r' }, spec: {}, status: { phase: 'WaitingForPluginOperationsPartiallyFailed' } }} />
|
|
35
|
+
)
|
|
36
|
+
expect(html).toContain('WaitingForPluginOperationsPartiallyFailed')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('Schedule detail prints the raw phase even when the badge says Paused', () => {
|
|
40
|
+
// Velero leaves .status.phase at Enabled while a schedule is paused, so the
|
|
41
|
+
// badge and the phase disagree by design; the drawer must show both.
|
|
42
|
+
const html = renderToString(
|
|
43
|
+
<VeleroScheduleRenderer data={{ metadata: { name: 's' }, spec: { schedule: '0 1 * * *', paused: true }, status: { phase: 'Enabled' } }} />
|
|
44
|
+
)
|
|
45
|
+
expect(html).toContain('Paused')
|
|
46
|
+
expect(html).toContain('Enabled')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('BSL detail prints the raw phase', () => {
|
|
50
|
+
const html = renderToString(
|
|
51
|
+
<VeleroBSLRenderer data={{ metadata: { name: 'bsl' }, spec: { provider: 'aws' }, status: { phase: 'Unavailable' } }} />
|
|
52
|
+
)
|
|
53
|
+
expect(html).toContain('Unavailable')
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
describe('VeleroPhaseValue', () => {
|
|
58
|
+
it('does not repeat the phase when the label already is the phase', () => {
|
|
59
|
+
const html = renderToString(
|
|
60
|
+
<VeleroPhaseValue status={{ text: 'Completed', color: 'x' }} phase="Completed" />
|
|
61
|
+
)
|
|
62
|
+
expect(html.match(/Completed/g)).toHaveLength(1)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('shows the phase alongside the label when they differ', () => {
|
|
66
|
+
const html = renderToString(
|
|
67
|
+
<VeleroPhaseValue status={{ text: 'Partially failed', color: 'x' }} phase="FinalizingPartiallyFailed" />
|
|
68
|
+
)
|
|
69
|
+
expect(html).toContain('Partially failed')
|
|
70
|
+
expect(html).toContain('FinalizingPartiallyFailed')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('renders nothing extra when the resource has no phase', () => {
|
|
74
|
+
const html = renderToString(
|
|
75
|
+
<VeleroPhaseValue status={{ text: 'Unknown', color: 'x' }} phase="" />
|
|
76
|
+
)
|
|
77
|
+
expect(html.match(/Unknown/g)).toHaveLength(1)
|
|
78
|
+
})
|
|
79
|
+
})
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { readFileSync } from 'node:fs'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { dirname, resolve } from 'node:path'
|
|
5
|
+
import {
|
|
6
|
+
getCNPGClusterStatus,
|
|
7
|
+
getCNPGClusterInstances,
|
|
8
|
+
classifyCNPGClusterPhase,
|
|
9
|
+
CNPG_CLUSTER_PHASES_HEALTHY,
|
|
10
|
+
CNPG_CLUSTER_PHASES_TRANSIENT,
|
|
11
|
+
CNPG_CLUSTER_PHASES_FAILING,
|
|
12
|
+
CNPG_CLUSTER_PHASES_TERMINAL,
|
|
13
|
+
CNPG_CLUSTER_PHASES_ATTENTION,
|
|
14
|
+
} from './resource-utils-cnpg'
|
|
15
|
+
|
|
16
|
+
// Shared specification, also asserted from Go in
|
|
17
|
+
// internal/issues/source_cnpg_golden_test.go. The sibling parity test compares
|
|
18
|
+
// only phase SPELLING between the two languages, which cannot catch a
|
|
19
|
+
// precedence, readiness-presence or condition difference — every badge/issue
|
|
20
|
+
// disagreement found on this branch slipped past it.
|
|
21
|
+
const GOLDEN = resolve(
|
|
22
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
23
|
+
'../../../../../testdata/cnpg/badge-issue-matrix.json',
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
interface GoldenCase {
|
|
27
|
+
name: string
|
|
28
|
+
spec: Record<string, unknown>
|
|
29
|
+
status: Record<string, unknown>
|
|
30
|
+
badge: { text: string; level: string }
|
|
31
|
+
issues: string[]
|
|
32
|
+
worst: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const cases: GoldenCase[] = JSON.parse(readFileSync(GOLDEN, 'utf8')).cases
|
|
36
|
+
|
|
37
|
+
describe('CNPG golden matrix — badge side', () => {
|
|
38
|
+
it('loads the shared specification', () => {
|
|
39
|
+
expect(cases.length).toBeGreaterThan(0)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it.each(cases.map((c) => [c.name, c] as const))('%s', (_name, tc) => {
|
|
43
|
+
const badge = getCNPGClusterStatus({ spec: tc.spec, status: tc.status })
|
|
44
|
+
expect({ text: badge.text, level: badge.level }).toEqual(tc.badge)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
// The invariant itself, stated directly against the spec rather than inferred:
|
|
48
|
+
// a green badge must never accompany an issue, and a red badge must never
|
|
49
|
+
// accompany silence.
|
|
50
|
+
it.each(cases.map((c) => [c.name, c] as const))(
|
|
51
|
+
'badge and issues do not contradict — %s',
|
|
52
|
+
(_name, tc) => {
|
|
53
|
+
const badge = getCNPGClusterStatus({ spec: tc.spec, status: tc.status })
|
|
54
|
+
if (badge.level === 'healthy') {
|
|
55
|
+
expect(tc.issues, 'a healthy badge must not sit next to an issue').toEqual([])
|
|
56
|
+
}
|
|
57
|
+
if (badge.level === 'unhealthy') {
|
|
58
|
+
expect(tc.worst, 'an unhealthy badge must not sit next to silence').not.toBe('none')
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
// The Instances CELL, derived from the same cases rather than specified in
|
|
64
|
+
// them. The matrix stays a Go/TS parity spec — Go has no instances cell, so a
|
|
65
|
+
// `cell` field would add a column only one language can assert to a file
|
|
66
|
+
// whose whole purpose is that both do. Deriving costs no schema change and
|
|
67
|
+
// covers every case added later for badge reasons.
|
|
68
|
+
//
|
|
69
|
+
// This is the gap that let the bug through: the matrix already pinned
|
|
70
|
+
// "readyInstances absent — unknown, not zero; must not fabricate an outage"
|
|
71
|
+
// for the badge, and the cell rendered 0/N on that very case.
|
|
72
|
+
it.each(cases.map((c) => [c.name, c] as const))(
|
|
73
|
+
'instances cell does not claim an outage the badge denies — %s',
|
|
74
|
+
(_name, tc) => {
|
|
75
|
+
const cell = getCNPGClusterInstances({ spec: tc.spec, status: tc.status })
|
|
76
|
+
const ready = cell.split('/')[0]
|
|
77
|
+
const readyReported = typeof tc.status.readyInstances === 'number'
|
|
78
|
+
expect(
|
|
79
|
+
ready === '-',
|
|
80
|
+
`readyInstances ${readyReported ? 'is reported' : 'is absent'} but the cell reads ${cell}`,
|
|
81
|
+
).toBe(!readyReported)
|
|
82
|
+
},
|
|
83
|
+
)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
describe('CNPG phase buckets', () => {
|
|
87
|
+
// TS checks transient before attention; Go checks attention before transient.
|
|
88
|
+
// A phase in both would classify differently per language while the spelling
|
|
89
|
+
// parity test still passed.
|
|
90
|
+
it('are disjoint', () => {
|
|
91
|
+
const seen = new Map<string, string>()
|
|
92
|
+
const buckets: Record<string, readonly string[]> = {
|
|
93
|
+
healthy: CNPG_CLUSTER_PHASES_HEALTHY,
|
|
94
|
+
transient: CNPG_CLUSTER_PHASES_TRANSIENT,
|
|
95
|
+
failing: CNPG_CLUSTER_PHASES_FAILING,
|
|
96
|
+
terminal: CNPG_CLUSTER_PHASES_TERMINAL,
|
|
97
|
+
attention: CNPG_CLUSTER_PHASES_ATTENTION,
|
|
98
|
+
}
|
|
99
|
+
for (const [bucket, phases] of Object.entries(buckets)) {
|
|
100
|
+
for (const phase of phases) {
|
|
101
|
+
expect(seen.has(phase), `${phase} is in both ${seen.get(phase)} and ${bucket}`).toBe(false)
|
|
102
|
+
seen.set(phase, bucket)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('classify every listed phase into its own bucket', () => {
|
|
108
|
+
for (const p of CNPG_CLUSTER_PHASES_TERMINAL) expect(classifyCNPGClusterPhase(p)).toBe('terminal')
|
|
109
|
+
for (const p of CNPG_CLUSTER_PHASES_TRANSIENT) expect(classifyCNPGClusterPhase(p)).toBe('transient')
|
|
110
|
+
for (const p of CNPG_CLUSTER_PHASES_ATTENTION) expect(classifyCNPGClusterPhase(p)).toBe('attention')
|
|
111
|
+
})
|
|
112
|
+
})
|