@skyhook-io/k8s-ui 1.12.0 → 1.13.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 +4 -4
- package/src/components/issues/severity.ts +1 -0
- package/src/components/resources/ResourcesView.empty-note.test.ts +41 -0
- package/src/components/resources/ResourcesView.tsx +410 -5
- package/src/components/resources/index.ts +27 -0
- package/src/components/resources/kyverno-legacy-action.test.ts +98 -0
- package/src/components/resources/renderers/AnalysisRunRenderer.test.tsx +241 -0
- package/src/components/resources/renderers/AnalysisRunRenderer.tsx +166 -0
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +31 -2
- package/src/components/resources/renderers/CNPGClusterRenderer.test.tsx +33 -0
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +47 -4
- package/src/components/resources/renderers/CNPGDeclarativeRenderer.tsx +243 -0
- package/src/components/resources/renderers/CNPGObjectStoreRenderer.test.tsx +99 -0
- package/src/components/resources/renderers/CNPGObjectStoreRenderer.tsx +235 -0
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +23 -1
- package/src/components/resources/renderers/CNPGShared.tsx +27 -0
- package/src/components/resources/renderers/CalicoHostEndpointRenderer.tsx +71 -0
- package/src/components/resources/renderers/CalicoIPPoolRenderer.tsx +32 -0
- package/src/components/resources/renderers/CalicoNetworkPolicyDiagram.test.tsx +205 -0
- package/src/components/resources/renderers/CalicoNetworkPolicyDiagram.tsx +537 -0
- package/src/components/resources/renderers/CalicoNetworkPolicyRenderer.test.tsx +223 -0
- package/src/components/resources/renderers/CalicoNetworkPolicyRenderer.tsx +670 -0
- package/src/components/resources/renderers/CalicoTierRenderer.tsx +19 -0
- package/src/components/resources/renderers/KyvernoCELPolicyRenderers.tsx +13 -6
- package/src/components/resources/renderers/KyvernoExceptionRenderers.tsx +3 -2
- package/src/components/resources/renderers/KyvernoGlobalContextRenderer.test.ts +31 -0
- package/src/components/resources/renderers/KyvernoGlobalContextRenderer.tsx +124 -0
- package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +28 -2
- package/src/components/resources/renderers/KyvernoQueueRenderers.tsx +303 -0
- package/src/components/resources/renderers/LookupFailureNote.test.tsx +60 -0
- package/src/components/resources/renderers/LookupFailureNote.tsx +42 -0
- package/src/components/resources/renderers/NetworkPolicyDiagram.tsx +27 -26
- package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +6 -3
- package/src/components/resources/renderers/PolicyCoverageSection.test.tsx +348 -0
- package/src/components/resources/renderers/PolicyCoverageSection.tsx +939 -0
- package/src/components/resources/renderers/PolicySection.test.tsx +59 -2
- package/src/components/resources/renderers/PolicySection.tsx +32 -5
- package/src/components/resources/renderers/RolloutRenderer.test.tsx +491 -0
- package/src/components/resources/renderers/RolloutRenderer.tsx +391 -92
- package/src/components/resources/renderers/VeleroBSLRenderer.test.tsx +374 -0
- package/src/components/resources/renderers/VeleroBSLRenderer.tsx +179 -3
- package/src/components/resources/renderers/VeleroBackupRenderer.test.tsx +221 -0
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +119 -6
- package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.test.tsx +69 -0
- package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.tsx +96 -0
- package/src/components/resources/renderers/VeleroRestoreRenderer.test.tsx +55 -0
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +22 -4
- package/src/components/resources/renderers/VeleroRunMessages.test.tsx +152 -0
- package/src/components/resources/renderers/VeleroRunMessages.tsx +139 -0
- package/src/components/resources/renderers/VeleroScheduleRenderer.test.tsx +88 -0
- package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +16 -6
- package/src/components/resources/renderers/calico-cells.tsx +177 -0
- package/src/components/resources/renderers/cnpg-cells.tsx +100 -0
- package/src/components/resources/renderers/index.ts +16 -1
- package/src/components/resources/renderers/kyverno-cells.tsx +81 -0
- package/src/components/resources/renderers/network-policy-peer-styles.ts +25 -0
- package/src/components/resources/renderers/velero-cells.tsx +29 -7
- package/src/components/resources/renderers/velero-dead-ends.test.tsx +142 -0
- package/src/components/resources/resource-utils-calico.test.ts +215 -0
- package/src/components/resources/resource-utils-calico.ts +261 -0
- package/src/components/resources/resource-utils-cnpg-declarative.test.ts +104 -0
- package/src/components/resources/resource-utils-cnpg-objectstore.test.ts +122 -0
- package/src/components/resources/resource-utils-cnpg.test.ts +54 -0
- package/src/components/resources/resource-utils-cnpg.ts +264 -3
- package/src/components/resources/resource-utils-kyverno-modern.ts +24 -0
- package/src/components/resources/resource-utils-kyverno-queue.test.ts +164 -0
- package/src/components/resources/resource-utils-kyverno-queue.ts +198 -0
- package/src/components/resources/resource-utils-kyverno.ts +43 -7
- package/src/components/resources/resource-utils-velero.test.ts +26 -0
- package/src/components/resources/resource-utils-velero.ts +68 -0
- package/src/components/resources/resource-utils.ts +98 -0
- package/src/components/shared/ResourceActionsBar.tsx +101 -16
- package/src/components/shared/ResourceRendererDispatch.test.tsx +379 -3
- package/src/components/shared/ResourceRendererDispatch.tsx +199 -22
- package/src/components/shared/index.ts +1 -1
- package/src/components/shared/revision-role-badges.test.ts +120 -0
- package/src/components/topology/K8sResourceNode.test.ts +10 -0
- package/src/components/topology/K8sResourceNode.tsx +22 -0
- package/src/components/topology/TopologyFilterSidebar.test.tsx +31 -0
- package/src/components/topology/TopologyFilterSidebar.tsx +5 -0
- package/src/components/topology/TopologyGraph.tsx +6 -6
- package/src/components/topology/TopologySearch.tsx +7 -0
- package/src/components/topology/layout.ts +3 -0
- package/src/components/trace/ReachabilityView.tsx +1 -0
- package/src/components/trace/TracePanel.tsx +12 -2
- package/src/components/trace/reachFixtures.ts +26 -1
- package/src/components/trace/reachGraphModel.test.ts +20 -0
- package/src/components/trace/reachGraphModel.ts +8 -1
- package/src/components/trace/reachOrigins.test.ts +31 -0
- package/src/components/trace/reachOrigins.ts +11 -1
- package/src/components/ui/Badge.tsx +10 -0
- package/src/components/ui/ScheduleValue.test.tsx +80 -0
- package/src/components/ui/ScheduleValue.tsx +85 -0
- package/src/components/ui/drawer-components.test.tsx +17 -1
- package/src/components/ui/drawer-components.tsx +9 -2
- package/src/components/ui/index.ts +2 -0
- package/src/components/workload/WorkloadView.tsx +3 -0
- package/src/types/core.ts +29 -0
- package/src/types/policy.ts +153 -2
- package/src/utils/navigation.test.ts +5 -0
- package/src/utils/navigation.ts +5 -0
- package/src/utils/resource-icons.test.ts +49 -2
- package/src/utils/resource-icons.ts +29 -2
|
@@ -1,18 +1,301 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { Server, GitBranch, Network, Activity, Play, FastForward, SkipForward, RotateCcw, Undo2 } from 'lucide-react'
|
|
2
3
|
import { clsx } from 'clsx'
|
|
3
|
-
import { Section, PropertyList, Property, ConditionsSection, PodTemplateSection } from '../../ui/drawer-components'
|
|
4
|
+
import { AlertBanner, Section, PropertyList, Property, ConditionsSection, PodTemplateSection, type ConditionTone } from '../../ui/drawer-components'
|
|
5
|
+
import { Tooltip } from '../../ui/Tooltip'
|
|
6
|
+
import { ConfirmDialog } from '../../ui/ConfirmDialog'
|
|
4
7
|
import { formatAge } from '../resource-utils'
|
|
5
8
|
import { BADGE_INACTIVE } from '../../../utils/badge-colors'
|
|
6
9
|
|
|
10
|
+
export type RolloutAction = 'abort' | 'retry' | 'promote' | 'promote-full' | 'skip-step'
|
|
11
|
+
|
|
12
|
+
export interface RolloutCapabilities {
|
|
13
|
+
abort: boolean
|
|
14
|
+
retry: boolean
|
|
15
|
+
promote: boolean
|
|
16
|
+
promoteFull: boolean
|
|
17
|
+
skipStep: boolean
|
|
18
|
+
rollback: boolean
|
|
19
|
+
restart: boolean
|
|
20
|
+
strategy: string
|
|
21
|
+
terminating: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface RolloutActionConfirm {
|
|
25
|
+
title: string
|
|
26
|
+
message: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface RolloutActionSpec {
|
|
30
|
+
action: RolloutAction
|
|
31
|
+
label: string
|
|
32
|
+
pendingLabel: string
|
|
33
|
+
icon: typeof Play
|
|
34
|
+
hint: string
|
|
35
|
+
blocked?: string
|
|
36
|
+
destructive?: boolean
|
|
37
|
+
// Set on the verbs that shift production traffic in one click.
|
|
38
|
+
confirm?: RolloutActionConfirm
|
|
39
|
+
}
|
|
40
|
+
|
|
7
41
|
interface RolloutRendererProps {
|
|
8
42
|
data: any
|
|
43
|
+
onNavigate?: (ref: { kind: string; namespace: string; name: string }) => void
|
|
44
|
+
capabilities?: RolloutCapabilities
|
|
45
|
+
onAction?: (action: RolloutAction) => void
|
|
46
|
+
pendingAction?: RolloutAction | null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// A denied capability omits the verb entirely; `blocked` covers wrong-state only.
|
|
50
|
+
export function rolloutActions(data: any, capabilities?: RolloutCapabilities): RolloutActionSpec[] {
|
|
51
|
+
if (!capabilities) return []
|
|
52
|
+
|
|
53
|
+
const status = data?.status || {}
|
|
54
|
+
const phase = status.phase || 'Unknown'
|
|
55
|
+
const steps = data?.spec?.strategy?.canary?.steps || []
|
|
56
|
+
const isAborted = status.abort === true
|
|
57
|
+
const isSettled = phase === 'Healthy' && !isAborted
|
|
58
|
+
const stepsRemaining = steps.length > 0 && (status.currentStepIndex ?? 0) < steps.length
|
|
59
|
+
// Mid-analysis a canary is Progressing with nothing paused, and promote advances
|
|
60
|
+
// the step rather than clearing a pause — so gating on Paused alone hides it.
|
|
61
|
+
const stepAnalysis = status.canary?.currentStepAnalysisRunStatus?.status
|
|
62
|
+
const analysisInFlight =
|
|
63
|
+
stepAnalysis === 'Running' || stepAnalysis === 'Pending' || stepAnalysis === 'Inconclusive'
|
|
64
|
+
const promotable = phase === 'Paused' || analysisInFlight
|
|
65
|
+
|
|
66
|
+
const actions: RolloutActionSpec[] = []
|
|
67
|
+
if (capabilities.promote) {
|
|
68
|
+
actions.push({
|
|
69
|
+
action: 'promote',
|
|
70
|
+
label: 'Promote',
|
|
71
|
+
pendingLabel: 'Promoting…',
|
|
72
|
+
icon: Play,
|
|
73
|
+
hint: 'Clear the current pause, or advance past the running analysis, and continue',
|
|
74
|
+
// An aborted Rollout keeps an Inconclusive analysis status, so promotable stays
|
|
75
|
+
// true; Argo's own promote reports success there and advances nothing.
|
|
76
|
+
blocked: isAborted
|
|
77
|
+
? 'Retry the rollout first'
|
|
78
|
+
: !promotable
|
|
79
|
+
? 'Rollout is not paused and no analysis is running'
|
|
80
|
+
: undefined,
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
if (capabilities.promoteFull) {
|
|
84
|
+
actions.push({
|
|
85
|
+
action: 'promote-full',
|
|
86
|
+
label: 'Promote full',
|
|
87
|
+
pendingLabel: 'Promoting…',
|
|
88
|
+
icon: FastForward,
|
|
89
|
+
hint: 'Skip every remaining step, pause, and analysis — emergency hotfix path',
|
|
90
|
+
blocked: isSettled ? 'Nothing left to promote' : isAborted ? 'Retry the rollout first' : undefined,
|
|
91
|
+
confirm: {
|
|
92
|
+
title: 'Promote fully?',
|
|
93
|
+
message:
|
|
94
|
+
'Every remaining step, pause, and analysis is skipped and the new version goes to 100% of production traffic immediately.',
|
|
95
|
+
},
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
if (capabilities.skipStep) {
|
|
99
|
+
actions.push({
|
|
100
|
+
action: 'skip-step',
|
|
101
|
+
label: 'Skip step',
|
|
102
|
+
pendingLabel: 'Skipping…',
|
|
103
|
+
icon: SkipForward,
|
|
104
|
+
hint: 'Advance one canary step without waiting for it',
|
|
105
|
+
blocked: !stepsRemaining ? 'No canary step left to skip' : isAborted ? 'Retry the rollout first' : undefined,
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
if (capabilities.retry && isAborted) {
|
|
109
|
+
actions.push({
|
|
110
|
+
action: 'retry',
|
|
111
|
+
label: 'Retry',
|
|
112
|
+
pendingLabel: 'Retrying…',
|
|
113
|
+
icon: RotateCcw,
|
|
114
|
+
hint: 'Clear the abort and resume the rollout from its current step',
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
if (capabilities.abort && !isAborted) {
|
|
118
|
+
actions.push({
|
|
119
|
+
action: 'abort',
|
|
120
|
+
label: 'Abort',
|
|
121
|
+
pendingLabel: 'Aborting…',
|
|
122
|
+
icon: Undo2,
|
|
123
|
+
hint: 'Shift all traffic back to the stable revision and stop progressing',
|
|
124
|
+
blocked: isSettled ? 'Rollout is fully promoted — roll back instead' : undefined,
|
|
125
|
+
destructive: true,
|
|
126
|
+
confirm: {
|
|
127
|
+
title: 'Abort rollout?',
|
|
128
|
+
message:
|
|
129
|
+
'All production traffic shifts back to the stable revision and the rollout stops progressing.',
|
|
130
|
+
},
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
return actions
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface RolloutProblem {
|
|
137
|
+
color: 'red' | 'yellow'
|
|
138
|
+
message: string
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function rolloutProblems(data: any): RolloutProblem[] {
|
|
142
|
+
const status = data?.status || {}
|
|
143
|
+
const phase = status.phase || 'Unknown'
|
|
144
|
+
const conditions: any[] = status.conditions || []
|
|
145
|
+
const problems: RolloutProblem[] = []
|
|
146
|
+
|
|
147
|
+
if (status.abort === true) {
|
|
148
|
+
problems.push({ color: 'red', message: status.message || 'Rollout was aborted' })
|
|
149
|
+
}
|
|
150
|
+
if (phase === 'Degraded') {
|
|
151
|
+
problems.push({ color: 'red', message: status.message || 'Rollout is degraded' })
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const progressDeadline = conditions.find(
|
|
155
|
+
(c) => c.type === 'Progressing' && c.status === 'False' && c.reason === 'ProgressDeadlineExceeded'
|
|
156
|
+
)
|
|
157
|
+
if (progressDeadline) {
|
|
158
|
+
problems.push({ color: 'red', message: progressDeadline.message || 'Progress deadline exceeded' })
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const invalidSpec = conditions.find((c) => c.type === 'InvalidSpec' && c.status === 'True')
|
|
162
|
+
if (invalidSpec) {
|
|
163
|
+
problems.push({ color: 'red', message: invalidSpec.message || 'Invalid rollout spec' })
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (phase === 'Paused' && !status.abort) {
|
|
167
|
+
const reasons = (status.pauseConditions || []).map((pc: any) => {
|
|
168
|
+
const since = pc.startTime ? ` (since ${formatAge(pc.startTime)})` : ''
|
|
169
|
+
return `${pc.reason || 'Unknown'}${since}`
|
|
170
|
+
})
|
|
171
|
+
problems.push({
|
|
172
|
+
color: 'yellow',
|
|
173
|
+
message: reasons.length > 0 ? `Rollout is paused: ${reasons.join('; ')}` : 'Rollout is paused',
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
for (const run of rolloutAnalysisRuns(status)) {
|
|
178
|
+
if (run.status === 'Inconclusive' || run.status === 'Failed' || run.status === 'Error') {
|
|
179
|
+
problems.push({
|
|
180
|
+
color: run.status === 'Inconclusive' ? 'yellow' : 'red',
|
|
181
|
+
message: `${run.label} ${run.status.toLowerCase()} (${run.name})${run.message ? `: ${run.message}` : ''}`,
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// An aborted Rollout is also Degraded, and both read status.message — so the
|
|
187
|
+
// same sentence arrives twice. Keep the first, which carries the abort framing.
|
|
188
|
+
const seen = new Set<string>()
|
|
189
|
+
return problems.filter((p) => {
|
|
190
|
+
if (seen.has(p.message)) return false
|
|
191
|
+
seen.add(p.message)
|
|
192
|
+
return true
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Argo inverts polarity on two condition types: Paused=False and InvalidSpec=False
|
|
197
|
+
// are the good states, which the generic True-is-healthy rule scores as failing.
|
|
198
|
+
export function rolloutConditionTone(cond: { type?: string; status?: string }): ConditionTone | undefined {
|
|
199
|
+
if (cond.status !== 'True' && cond.status !== 'False') return undefined
|
|
200
|
+
const isTrue = cond.status === 'True'
|
|
201
|
+
switch (cond.type) {
|
|
202
|
+
case 'Paused':
|
|
203
|
+
return isTrue ? 'warning' : 'ok'
|
|
204
|
+
case 'InvalidSpec':
|
|
205
|
+
return isTrue ? 'fail' : 'ok'
|
|
206
|
+
default:
|
|
207
|
+
return undefined
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Every CanaryStep variant Argo defines; raw JSON is unreadable in a step list. */
|
|
212
|
+
export function canaryStepLabel(step: any): string {
|
|
213
|
+
if (!step || typeof step !== 'object') return 'Unknown step'
|
|
214
|
+
|
|
215
|
+
if (step.setWeight !== undefined) return `Set weight: ${step.setWeight}%`
|
|
216
|
+
|
|
217
|
+
if (step.pause !== undefined) {
|
|
218
|
+
return step.pause?.duration ? `Pause: ${step.pause.duration}` : 'Pause: until promoted'
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (step.analysis) {
|
|
222
|
+
const templates = (step.analysis.templates || [])
|
|
223
|
+
.map((t: any) => t.templateName || t.clusterTemplateName)
|
|
224
|
+
.filter(Boolean)
|
|
225
|
+
return templates.length > 0 ? `Analysis: ${templates.join(', ')}` : 'Analysis'
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (step.experiment) {
|
|
229
|
+
const templates = (step.experiment.templates || []).map((t: any) => t.name).filter(Boolean)
|
|
230
|
+
const duration = step.experiment.duration ? ` for ${step.experiment.duration}` : ''
|
|
231
|
+
return templates.length > 0
|
|
232
|
+
? `Experiment: ${templates.join(', ')}${duration}`
|
|
233
|
+
: `Experiment${duration}`
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (step.setCanaryScale) {
|
|
237
|
+
const { weight, replicas, matchTrafficWeight } = step.setCanaryScale
|
|
238
|
+
if (matchTrafficWeight) return 'Set canary scale: match traffic weight'
|
|
239
|
+
if (replicas !== undefined) return `Set canary scale: ${replicas} replicas`
|
|
240
|
+
if (weight !== undefined) return `Set canary scale: ${weight}%`
|
|
241
|
+
return 'Set canary scale'
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (step.setHeaderRoute) {
|
|
245
|
+
const { name, match } = step.setHeaderRoute
|
|
246
|
+
// An empty match list is how a header route is torn down again.
|
|
247
|
+
if (!match || match.length === 0) return `Remove header route${name ? `: ${name}` : ''}`
|
|
248
|
+
const headers = match.map((m: any) => m.headerName).filter(Boolean)
|
|
249
|
+
return `Header route${name ? ` ${name}` : ''}${headers.length ? `: ${headers.join(', ')}` : ''}`
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (step.setMirrorRoute) {
|
|
253
|
+
const { name, match, percentage } = step.setMirrorRoute
|
|
254
|
+
if (!match || match.length === 0) return `Remove mirror route${name ? `: ${name}` : ''}`
|
|
255
|
+
const pct = percentage !== undefined ? ` (${percentage}%)` : ''
|
|
256
|
+
return `Mirror route${name ? ` ${name}` : ''}${pct}`
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (step.plugin) return `Plugin: ${step.plugin.name || 'unnamed'}`
|
|
260
|
+
|
|
261
|
+
const key = Object.keys(step)[0]
|
|
262
|
+
return key ? `Unrecognized step: ${key}` : 'Unknown step'
|
|
9
263
|
}
|
|
10
264
|
|
|
11
|
-
|
|
265
|
+
/** Populated analysis slots, in the order the controller runs them. */
|
|
266
|
+
export function rolloutAnalysisRuns(
|
|
267
|
+
status: any
|
|
268
|
+
): Array<{ label: string; name?: string; status?: string; message?: string }> {
|
|
269
|
+
return [
|
|
270
|
+
{ label: 'Step analysis', ...(status?.canary?.currentStepAnalysisRunStatus || {}) },
|
|
271
|
+
{ label: 'Background analysis', ...(status?.canary?.currentBackgroundAnalysisRunStatus || {}) },
|
|
272
|
+
{ label: 'Pre-promotion analysis', ...(status?.blueGreen?.prePromotionAnalysisRunStatus || {}) },
|
|
273
|
+
{ label: 'Post-promotion analysis', ...(status?.blueGreen?.postPromotionAnalysisRunStatus || {}) },
|
|
274
|
+
].filter((run) => run.name)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function analysisStatusClass(status?: string): string {
|
|
278
|
+
switch (status) {
|
|
279
|
+
case 'Successful':
|
|
280
|
+
return 'status-healthy'
|
|
281
|
+
case 'Running':
|
|
282
|
+
case 'Pending':
|
|
283
|
+
return 'status-degraded'
|
|
284
|
+
case 'Inconclusive':
|
|
285
|
+
return 'status-alert'
|
|
286
|
+
case 'Failed':
|
|
287
|
+
case 'Error':
|
|
288
|
+
return 'status-unhealthy'
|
|
289
|
+
default:
|
|
290
|
+
return 'status-unknown'
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function RolloutRenderer({ data, onNavigate, capabilities, onAction, pendingAction }: RolloutRendererProps) {
|
|
295
|
+
const [confirming, setConfirming] = useState<RolloutActionSpec | null>(null)
|
|
12
296
|
const status = data.status || {}
|
|
13
297
|
const spec = data.spec || {}
|
|
14
298
|
const phase = status.phase || 'Unknown'
|
|
15
|
-
const conditions = status.conditions || []
|
|
16
299
|
|
|
17
300
|
const canaryStrategy = spec.strategy?.canary
|
|
18
301
|
const blueGreenStrategy = spec.strategy?.blueGreen
|
|
@@ -34,46 +317,9 @@ export function RolloutRenderer({ data }: RolloutRendererProps) {
|
|
|
34
317
|
})()
|
|
35
318
|
: null
|
|
36
319
|
|
|
37
|
-
|
|
38
|
-
const problems
|
|
39
|
-
|
|
40
|
-
// Aborted rollout detection — must come before generic paused check
|
|
41
|
-
if (status.abort === true) {
|
|
42
|
-
problems.push({ color: 'red', message: status.message || 'Rollout was aborted' })
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (phase === 'Degraded') {
|
|
46
|
-
problems.push({ color: 'red', message: status.message || 'Rollout is degraded' })
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const progressDeadlineCond = conditions.find(
|
|
50
|
-
(c: any) => c.type === 'Progressing' && c.status === 'False' && c.reason === 'ProgressDeadlineExceeded'
|
|
51
|
-
)
|
|
52
|
-
if (progressDeadlineCond) {
|
|
53
|
-
problems.push({ color: 'red', message: progressDeadlineCond.message || 'Progress deadline exceeded' })
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const invalidSpecCond = conditions.find(
|
|
57
|
-
(c: any) => c.type === 'InvalidSpec' && c.status === 'True'
|
|
58
|
-
)
|
|
59
|
-
if (invalidSpecCond) {
|
|
60
|
-
problems.push({ color: 'red', message: invalidSpecCond.message || 'Invalid rollout spec' })
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Pause conditions — show specific reasons instead of generic "Rollout is paused"
|
|
64
|
-
const pauseConditions: Array<{ reason: string; startTime?: string }> = status.pauseConditions || []
|
|
65
|
-
if (phase === 'Paused' && !status.abort) {
|
|
66
|
-
if (pauseConditions.length > 0) {
|
|
67
|
-
const reasons = pauseConditions.map((pc: any) => {
|
|
68
|
-
const reason = pc.reason || 'Unknown'
|
|
69
|
-
const since = pc.startTime ? ` (since ${formatAge(pc.startTime)})` : ''
|
|
70
|
-
return `${reason}${since}`
|
|
71
|
-
})
|
|
72
|
-
problems.push({ color: 'yellow', message: `Rollout is paused: ${reasons.join('; ')}` })
|
|
73
|
-
} else {
|
|
74
|
-
problems.push({ color: 'yellow', message: 'Rollout is paused' })
|
|
75
|
-
}
|
|
76
|
-
}
|
|
320
|
+
const analysisRuns = rolloutAnalysisRuns(status)
|
|
321
|
+
const problems = rolloutProblems(data)
|
|
322
|
+
const actions = onAction ? rolloutActions(data, capabilities) : []
|
|
77
323
|
|
|
78
324
|
// Phase badge color
|
|
79
325
|
const phaseColor = (() => {
|
|
@@ -88,46 +334,59 @@ export function RolloutRenderer({ data }: RolloutRendererProps) {
|
|
|
88
334
|
|
|
89
335
|
return (
|
|
90
336
|
<>
|
|
91
|
-
{/* Problem alerts */}
|
|
92
337
|
{problems.map((problem, i) => (
|
|
93
|
-
<
|
|
338
|
+
<AlertBanner
|
|
94
339
|
key={i}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
: 'bg-yellow-500/10 border-yellow-500/30'
|
|
100
|
-
)}
|
|
101
|
-
>
|
|
102
|
-
<div className="flex items-start gap-2">
|
|
103
|
-
<AlertTriangle
|
|
104
|
-
className={clsx(
|
|
105
|
-
'w-4 h-4 mt-0.5 shrink-0',
|
|
106
|
-
problem.color === 'red' ? 'text-red-400' : 'text-yellow-400'
|
|
107
|
-
)}
|
|
108
|
-
/>
|
|
109
|
-
<div className="flex-1 min-w-0">
|
|
110
|
-
<div
|
|
111
|
-
className={clsx(
|
|
112
|
-
'text-sm font-medium',
|
|
113
|
-
problem.color === 'red' ? 'text-red-400' : 'text-yellow-400'
|
|
114
|
-
)}
|
|
115
|
-
>
|
|
116
|
-
{problem.color === 'red' ? 'Issue Detected' : 'Warning'}
|
|
117
|
-
</div>
|
|
118
|
-
<div
|
|
119
|
-
className={clsx(
|
|
120
|
-
'text-xs mt-1',
|
|
121
|
-
problem.color === 'red' ? 'text-red-300/80' : 'text-yellow-300/80'
|
|
122
|
-
)}
|
|
123
|
-
>
|
|
124
|
-
{problem.message}
|
|
125
|
-
</div>
|
|
126
|
-
</div>
|
|
127
|
-
</div>
|
|
128
|
-
</div>
|
|
340
|
+
variant={problem.color === 'red' ? 'error' : 'warning'}
|
|
341
|
+
title={problem.color === 'red' ? 'Issue Detected' : 'Warning'}
|
|
342
|
+
message={problem.message}
|
|
343
|
+
/>
|
|
129
344
|
))}
|
|
130
345
|
|
|
346
|
+
{actions.length > 0 && (
|
|
347
|
+
<div className="mb-4 flex flex-wrap items-center gap-2">
|
|
348
|
+
{actions.map((spec) => {
|
|
349
|
+
const { action, label, pendingLabel, icon: Icon, hint, blocked, destructive, confirm } = spec
|
|
350
|
+
const pending = pendingAction === action
|
|
351
|
+
const disabled = Boolean(blocked) || pendingAction != null || capabilities?.terminating
|
|
352
|
+
return (
|
|
353
|
+
<Tooltip key={action} content={capabilities?.terminating ? 'Rollout is being deleted' : blocked || hint} delay={150}>
|
|
354
|
+
<button
|
|
355
|
+
onClick={() => (confirm ? setConfirming(spec) : onAction?.(action))}
|
|
356
|
+
disabled={disabled}
|
|
357
|
+
className={clsx(
|
|
358
|
+
'flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-xs font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-40',
|
|
359
|
+
destructive
|
|
360
|
+
? 'border border-red-400/50 text-red-400 enabled:hover:bg-red-500/10'
|
|
361
|
+
: 'btn-brand-muted'
|
|
362
|
+
)}
|
|
363
|
+
>
|
|
364
|
+
<Icon className={clsx('h-3.5 w-3.5', pending && 'animate-pulse')} />
|
|
365
|
+
{pending ? pendingLabel : label}
|
|
366
|
+
</button>
|
|
367
|
+
</Tooltip>
|
|
368
|
+
)
|
|
369
|
+
})}
|
|
370
|
+
</div>
|
|
371
|
+
)}
|
|
372
|
+
|
|
373
|
+
{confirming?.confirm && (
|
|
374
|
+
<ConfirmDialog
|
|
375
|
+
open
|
|
376
|
+
onClose={() => setConfirming(null)}
|
|
377
|
+
onConfirm={() => {
|
|
378
|
+
const action = confirming.action
|
|
379
|
+
setConfirming(null)
|
|
380
|
+
onAction?.(action)
|
|
381
|
+
}}
|
|
382
|
+
title={confirming.confirm.title}
|
|
383
|
+
message={confirming.confirm.message}
|
|
384
|
+
details={`${data.metadata?.namespace}/${data.metadata?.name}`}
|
|
385
|
+
confirmLabel={confirming.label}
|
|
386
|
+
variant={confirming.destructive ? 'danger' : 'warning'}
|
|
387
|
+
/>
|
|
388
|
+
)}
|
|
389
|
+
|
|
131
390
|
{/* Status section */}
|
|
132
391
|
<Section title="Status" icon={Server}>
|
|
133
392
|
<PropertyList>
|
|
@@ -164,12 +423,19 @@ export function RolloutRenderer({ data }: RolloutRendererProps) {
|
|
|
164
423
|
)}
|
|
165
424
|
<Property label="Canary Service" value={canaryStrategy.canaryService} />
|
|
166
425
|
<Property label="Stable Service" value={canaryStrategy.stableService} />
|
|
426
|
+
{status.canary?.currentExperiment && (
|
|
427
|
+
<Property label="Running Experiment" value={status.canary.currentExperiment} />
|
|
428
|
+
)}
|
|
167
429
|
</>
|
|
168
430
|
) : blueGreenStrategy ? (
|
|
169
431
|
<>
|
|
170
432
|
<Property label="Strategy" value="Blue-Green" />
|
|
171
433
|
<Property label="Active Service" value={blueGreenStrategy.activeService} />
|
|
172
434
|
<Property label="Preview Service" value={blueGreenStrategy.previewService} />
|
|
435
|
+
{/* The selectors are what each Service actually serves right now — a
|
|
436
|
+
rolled-back revision sits in preview until it is promoted. */}
|
|
437
|
+
<Property label="Active Selector" value={status.blueGreen?.activeSelector} />
|
|
438
|
+
<Property label="Preview Selector" value={status.blueGreen?.previewSelector} />
|
|
173
439
|
<Property
|
|
174
440
|
label="Auto Promote"
|
|
175
441
|
value={
|
|
@@ -178,6 +444,15 @@ export function RolloutRenderer({ data }: RolloutRendererProps) {
|
|
|
178
444
|
: undefined
|
|
179
445
|
}
|
|
180
446
|
/>
|
|
447
|
+
{blueGreenStrategy.autoPromotionSeconds !== undefined && (
|
|
448
|
+
<Property
|
|
449
|
+
label="Auto Promote After"
|
|
450
|
+
value={`${blueGreenStrategy.autoPromotionSeconds}s`}
|
|
451
|
+
/>
|
|
452
|
+
)}
|
|
453
|
+
{status.blueGreen?.scaleUpPreviewCheckPoint && (
|
|
454
|
+
<Property label="Preview" value="Scaled up, awaiting promotion" />
|
|
455
|
+
)}
|
|
181
456
|
</>
|
|
182
457
|
) : (
|
|
183
458
|
<Property label="Strategy" value="Unknown" />
|
|
@@ -256,6 +531,40 @@ export function RolloutRenderer({ data }: RolloutRendererProps) {
|
|
|
256
531
|
</Section>
|
|
257
532
|
)}
|
|
258
533
|
|
|
534
|
+
{analysisRuns.length > 0 && (
|
|
535
|
+
<Section title="Analysis" icon={Activity}>
|
|
536
|
+
<PropertyList>
|
|
537
|
+
{analysisRuns.map((run) => (
|
|
538
|
+
<Property
|
|
539
|
+
key={run.label}
|
|
540
|
+
label={run.label}
|
|
541
|
+
value={
|
|
542
|
+
<span className="flex items-center gap-2">
|
|
543
|
+
<span className={clsx('badge', analysisStatusClass(run.status))}>{run.status || 'Unknown'}</span>
|
|
544
|
+
{onNavigate ? (
|
|
545
|
+
<button
|
|
546
|
+
onClick={() =>
|
|
547
|
+
onNavigate({
|
|
548
|
+
kind: 'AnalysisRun',
|
|
549
|
+
namespace: data?.metadata?.namespace ?? '',
|
|
550
|
+
name: run.name!,
|
|
551
|
+
})
|
|
552
|
+
}
|
|
553
|
+
className="font-mono text-xs text-brand hover:underline"
|
|
554
|
+
>
|
|
555
|
+
{run.name}
|
|
556
|
+
</button>
|
|
557
|
+
) : (
|
|
558
|
+
<span className="font-mono text-xs text-theme-text-secondary">{run.name}</span>
|
|
559
|
+
)}
|
|
560
|
+
</span>
|
|
561
|
+
}
|
|
562
|
+
/>
|
|
563
|
+
))}
|
|
564
|
+
</PropertyList>
|
|
565
|
+
</Section>
|
|
566
|
+
)}
|
|
567
|
+
|
|
259
568
|
{/* Canary Steps visual */}
|
|
260
569
|
{isCanary && steps.length > 0 && (
|
|
261
570
|
<Section title={`Canary Steps (${steps.length})`} defaultExpanded>
|
|
@@ -265,17 +574,7 @@ export function RolloutRenderer({ data }: RolloutRendererProps) {
|
|
|
265
574
|
const isCurrent = currentStepIndex !== undefined && index === currentStepIndex
|
|
266
575
|
const isPending = currentStepIndex === undefined || index > currentStepIndex
|
|
267
576
|
|
|
268
|
-
const stepLabel = (
|
|
269
|
-
if (step.setWeight !== undefined) return `Set Weight: ${step.setWeight}%`
|
|
270
|
-
if (step.pause !== undefined) {
|
|
271
|
-
if (step.pause.duration) return `Pause: ${step.pause.duration}`
|
|
272
|
-
return 'Pause: manual'
|
|
273
|
-
}
|
|
274
|
-
// Handle other step types generically
|
|
275
|
-
const key = Object.keys(step)[0]
|
|
276
|
-
if (key) return `${key}: ${JSON.stringify(step[key])}`
|
|
277
|
-
return 'Unknown step'
|
|
278
|
-
})()
|
|
577
|
+
const stepLabel = canaryStepLabel(step)
|
|
279
578
|
|
|
280
579
|
return (
|
|
281
580
|
<div
|
|
@@ -324,7 +623,7 @@ export function RolloutRenderer({ data }: RolloutRendererProps) {
|
|
|
324
623
|
</Section>
|
|
325
624
|
|
|
326
625
|
{/* Conditions section */}
|
|
327
|
-
<ConditionsSection conditions={status.conditions} />
|
|
626
|
+
<ConditionsSection conditions={status.conditions} getConditionTone={rolloutConditionTone} />
|
|
328
627
|
</>
|
|
329
628
|
)
|
|
330
629
|
}
|