@skyhook-io/k8s-ui 1.13.3 → 1.14.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 (119) hide show
  1. package/package.json +4 -4
  2. package/src/components/applications/AppTooltips.tsx +13 -1
  3. package/src/components/applications/ApplicationDetail.test.tsx +25 -0
  4. package/src/components/applications/ApplicationDetail.tsx +39 -7
  5. package/src/components/applications/ApplicationsView.tsx +18 -3
  6. package/src/components/applications/ReadyBar.tsx +3 -2
  7. package/src/components/applications/applications-view.test.tsx +91 -0
  8. package/src/components/cluster-switcher/ClusterSwitcher.tsx +4 -1
  9. package/src/components/issues/IssuesView.tsx +40 -31
  10. package/src/components/issues/ResourceIssuesSection.tsx +3 -2
  11. package/src/components/issues/index.ts +3 -1
  12. package/src/components/issues/issue-timing.ts +123 -4
  13. package/src/components/issues/issues.test.ts +221 -10
  14. package/src/components/issues/types.ts +31 -5
  15. package/src/components/namespace-switcher/NamespacePicker.tsx +21 -13
  16. package/src/components/resources/ResourcesView.tsx +966 -98
  17. package/src/components/resources/column-settings-key.test.ts +98 -0
  18. package/src/components/resources/column-visibility-merge.test.ts +64 -0
  19. package/src/components/resources/curated-column-ownership.test.ts +236 -0
  20. package/src/components/resources/gateway-policy-status.test.ts +291 -0
  21. package/src/components/resources/gateway-policy-status.ts +237 -0
  22. package/src/components/resources/generic-status.test.ts +199 -0
  23. package/src/components/resources/generic-status.ts +171 -0
  24. package/src/components/resources/gpu-ecosystem-registry.test.ts +63 -0
  25. package/src/components/resources/index.ts +9 -1
  26. package/src/components/resources/istio-gateway-filter.test.ts +97 -0
  27. package/src/components/resources/kyverno-legacy-action.test.ts +100 -1
  28. package/src/components/resources/kyverno-modern-posture.test.ts +36 -0
  29. package/src/components/resources/printer-columns.test.ts +298 -0
  30. package/src/components/resources/printer-columns.ts +249 -0
  31. package/src/components/resources/renderers/CNPGBackupRenderer.test.tsx +29 -0
  32. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +3 -2
  33. package/src/components/resources/renderers/CNPGClusterRenderer.test.tsx +69 -3
  34. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +6 -2
  35. package/src/components/resources/renderers/EventRenderer.test.tsx +50 -1
  36. package/src/components/resources/renderers/EventRenderer.tsx +3 -5
  37. package/src/components/resources/renderers/GenericRenderer.tsx +18 -83
  38. package/src/components/resources/renderers/HPARenderer.test.tsx +28 -0
  39. package/src/components/resources/renderers/HPARenderer.tsx +6 -1
  40. package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +12 -7
  41. package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +2 -1
  42. package/src/components/resources/renderers/PVCRenderer.test.tsx +44 -0
  43. package/src/components/resources/renderers/PVCRenderer.tsx +19 -2
  44. package/src/components/resources/renderers/PolicyCoverageSection.test.tsx +92 -1
  45. package/src/components/resources/renderers/PolicyCoverageSection.tsx +64 -8
  46. package/src/components/resources/renderers/RolloutRenderer.test.tsx +35 -0
  47. package/src/components/resources/renderers/RolloutRenderer.tsx +22 -3
  48. package/src/components/resources/renderers/VeleroBSLRenderer.test.tsx +47 -0
  49. package/src/components/resources/renderers/VeleroBSLRenderer.tsx +16 -1
  50. package/src/components/resources/renderers/VeleroRestoreRenderer.test.tsx +88 -0
  51. package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +44 -2
  52. package/src/components/resources/renderers/WorkloadRenderer.test.tsx +83 -0
  53. package/src/components/resources/renderers/WorkloadRenderer.tsx +25 -65
  54. package/src/components/resources/renderers/amd-gpu-cells.tsx +36 -0
  55. package/src/components/resources/renderers/cnpg-cells.tsx +12 -1
  56. package/src/components/resources/renderers/inference-gateway-cells.tsx +62 -0
  57. package/src/components/resources/renderers/jobset-lws-cells.tsx +82 -0
  58. package/src/components/resources/renderers/kai-cells.tsx +67 -0
  59. package/src/components/resources/renderers/kaito-cells.tsx +62 -0
  60. package/src/components/resources/renderers/kserve-cells.tsx +145 -0
  61. package/src/components/resources/renderers/kubeflow-training-cells.tsx +72 -0
  62. package/src/components/resources/renderers/kueue-cells.tsx +176 -0
  63. package/src/components/resources/renderers/kyverno-cells.tsx +8 -4
  64. package/src/components/resources/renderers/nim-cells.tsx +81 -0
  65. package/src/components/resources/renderers/ray-cells.tsx +124 -0
  66. package/src/components/resources/renderers/volcano-cells.tsx +140 -0
  67. package/src/components/resources/resource-utils-amd-gpu.ts +61 -0
  68. package/src/components/resources/resource-utils-cnpg.golden.test.ts +13 -5
  69. package/src/components/resources/resource-utils-cnpg.test.ts +142 -2
  70. package/src/components/resources/resource-utils-cnpg.ts +132 -7
  71. package/src/components/resources/resource-utils-gpu-ecosystem.test.ts +236 -0
  72. package/src/components/resources/resource-utils-hpa.test.ts +4 -0
  73. package/src/components/resources/resource-utils-hpa.ts +6 -2
  74. package/src/components/resources/resource-utils-inference-gateway.ts +119 -0
  75. package/src/components/resources/resource-utils-jobset-lws.ts +117 -0
  76. package/src/components/resources/resource-utils-kai.ts +105 -0
  77. package/src/components/resources/resource-utils-kaito.ts +114 -0
  78. package/src/components/resources/resource-utils-kserve.ts +186 -0
  79. package/src/components/resources/resource-utils-kubeflow-training.ts +121 -0
  80. package/src/components/resources/resource-utils-kueue.ts +200 -0
  81. package/src/components/resources/resource-utils-kyverno-modern.ts +3 -1
  82. package/src/components/resources/resource-utils-kyverno.ts +51 -2
  83. package/src/components/resources/resource-utils-nim.ts +109 -0
  84. package/src/components/resources/resource-utils-ray.ts +214 -0
  85. package/src/components/resources/resource-utils-rollout.test.ts +183 -0
  86. package/src/components/resources/resource-utils-velero.test.ts +30 -0
  87. package/src/components/resources/resource-utils-velero.ts +32 -0
  88. package/src/components/resources/resource-utils-volcano.ts +176 -0
  89. package/src/components/resources/resource-utils.ts +156 -47
  90. package/src/components/resources/resources-column-filter.test.ts +27 -1
  91. package/src/components/resources/workload-rollout-golden.test.ts +55 -0
  92. package/src/components/shared/CreateResourceDialog.tsx +1 -0
  93. package/src/components/shared/EditableYamlView.tsx +4 -2
  94. package/src/components/shared/ResourceActionsBar.tsx +170 -25
  95. package/src/components/shared/ResourceRendererDispatch.test.tsx +206 -1
  96. package/src/components/shared/ResourceRendererDispatch.tsx +84 -40
  97. package/src/components/shared/SetImageDialog.test.ts +129 -0
  98. package/src/components/shared/SetImageDialog.tsx +577 -0
  99. package/src/components/shared/index.ts +1 -0
  100. package/src/components/shared/revision-role-badges.test.ts +56 -4
  101. package/src/components/timeline/TimelineList.tsx +19 -3
  102. package/src/components/timeline/TimelineSwimlanes.tsx +11 -3
  103. package/src/components/trace/ReachabilityView.tsx +1 -1
  104. package/src/components/ui/SelectMenu.tsx +10 -1
  105. package/src/components/ui/drawer-components.tsx +12 -0
  106. package/src/components/workload/WorkloadRolloutNotice.tsx +60 -0
  107. package/src/components/workload/WorkloadView.tsx +144 -81
  108. package/src/hooks/useProgressiveRefresh.ts +37 -0
  109. package/src/source-hygiene.test.ts +28 -0
  110. package/src/theme/tailwind-theme.css +4 -0
  111. package/src/types/core.ts +55 -0
  112. package/src/utils/api-resources.test.ts +40 -1
  113. package/src/utils/api-resources.ts +11 -1
  114. package/src/utils/applications.ts +50 -0
  115. package/src/utils/bulk-workload-actions.test.ts +20 -0
  116. package/src/utils/bulk-workload-actions.ts +15 -0
  117. package/src/utils/navigation.test.ts +31 -1
  118. package/src/utils/navigation.ts +21 -2
  119. package/src/utils/workload-rollout.ts +292 -0
@@ -0,0 +1,292 @@
1
+ import type { WorkloadPodInfo } from '../types/core'
2
+
3
+ export type WorkloadRolloutPhase =
4
+ | 'idle'
5
+ | 'applying'
6
+ | 'progressing'
7
+ | 'waiting'
8
+ | 'paused'
9
+ | 'partition-reached'
10
+ | 'stalled'
11
+
12
+ export interface WorkloadRolloutActivity {
13
+ phase: WorkloadRolloutPhase
14
+ active: boolean
15
+ manual: boolean
16
+ label: string
17
+ detail?: string
18
+ desired: number
19
+ updated: number
20
+ ready: number
21
+ available: number
22
+ }
23
+
24
+ export function isArgoRolloutResource(resource: any): boolean {
25
+ return String(resource?.apiVersion || '').startsWith('argoproj.io/')
26
+ }
27
+
28
+ export function rolloutMayAdvanceAutomatically(activity: WorkloadRolloutActivity): boolean {
29
+ return activity.phase === 'applying' ||
30
+ activity.phase === 'progressing' ||
31
+ (activity.phase === 'waiting' && !activity.manual)
32
+ }
33
+
34
+ export function isRolloutActivityVisible(activity: WorkloadRolloutActivity): boolean {
35
+ return activity.active || activity.phase === 'stalled'
36
+ }
37
+
38
+ export function getObservedGeneration(resource: any): number {
39
+ const observed = resource?.status?.observedGeneration
40
+ if (typeof observed === 'number' && Number.isFinite(observed)) return observed
41
+ return typeof observed === 'string' && /^\d+$/.test(observed)
42
+ ? Number.parseInt(observed, 10)
43
+ : 0
44
+ }
45
+
46
+ export function getWorkloadRolloutActivity(
47
+ resource: any,
48
+ kind?: string,
49
+ workloadPods?: WorkloadPodInfo[],
50
+ ): WorkloadRolloutActivity {
51
+ const normalized = (kind || resource?.kind || '').toLowerCase().replace(/s$/, '')
52
+ if (normalized === 'rollout' && !isArgoRolloutResource(resource)) {
53
+ return activity('idle', false, false, 'Stable', '', 0, 0, 0, 0)
54
+ }
55
+ let result: WorkloadRolloutActivity
56
+ switch (normalized) {
57
+ case 'deployment':
58
+ result = deploymentActivity(resource)
59
+ break
60
+ case 'statefulset':
61
+ result = statefulSetActivity(resource)
62
+ break
63
+ case 'daemonset':
64
+ result = daemonSetActivity(resource)
65
+ break
66
+ case 'rollout':
67
+ result = argoRolloutActivity(resource)
68
+ break
69
+ default:
70
+ result = activity('idle', false, false, 'Stable', '', 0, 0, 0, 0)
71
+ }
72
+ return withUpdatedRevisionFailure(result, workloadPods)
73
+ }
74
+
75
+ function deploymentActivity(resource: any): WorkloadRolloutActivity {
76
+ const spec = resource?.spec || {}
77
+ const status = resource?.status || {}
78
+ const desired = spec.replicas ?? 1
79
+ const updated = status.updatedReplicas ?? 0
80
+ const ready = status.readyReplicas ?? 0
81
+ const available = status.availableReplicas ?? 0
82
+ const base = activity('idle', false, false, 'Stable', '', desired, updated, ready, available)
83
+ const progress = (status.conditions || []).find((condition: any) => condition.type === 'Progressing')
84
+
85
+ if ((status.observedGeneration ?? 0) < (resource?.metadata?.generation ?? 0)) {
86
+ return merge(base, 'applying', true, 'Applying change', 'Waiting for the Deployment controller to observe generation')
87
+ }
88
+ if (progress?.status === 'False' || progress?.reason === 'ProgressDeadlineExceeded') {
89
+ return merge(base, 'stalled', false, 'Rollout stalled', progress.message || 'Progress deadline exceeded')
90
+ }
91
+ const old = Math.max(0, (status.replicas ?? 0) - updated)
92
+ const pending = updated < desired || old > 0
93
+ if (spec.paused) {
94
+ return merge(base, 'paused', pending, 'Rollout paused', pending ? `Paused at ${updated}/${desired} updated` : 'No rollout is pending')
95
+ }
96
+ if (desired === 0) return merge(base, 'idle', false, 'Scaled to zero', 'No replicas requested')
97
+ if (!pending) return merge(base, 'idle', false, 'Stable', `${available}/${desired} available`)
98
+ if (spec.strategy?.type === 'Recreate') {
99
+ if (updated === 0 && old > 0) {
100
+ return merge(base, 'waiting', true, 'Replacing replicas', `Waiting for old replicas to stop · ${available} available`)
101
+ }
102
+ return updated === 0
103
+ ? merge(base, 'waiting', true, 'Waiting for new revision', replicaDetail(updated, desired, available))
104
+ : merge(base, 'progressing', true, 'Replacing replicas', replicaDetail(updated, desired, available))
105
+ }
106
+ if (updated === 0) return merge(base, 'waiting', true, 'Waiting for new revision', replicaDetail(updated, desired, available))
107
+ return old === 0 && updated === (status.replicas ?? 0)
108
+ ? merge(base, 'progressing', true, 'Scaling', replicaDetail(updated, desired, available))
109
+ : merge(base, 'progressing', true, 'Rolling out', replicaDetail(updated, desired, available))
110
+ }
111
+
112
+ function statefulSetActivity(resource: any): WorkloadRolloutActivity {
113
+ const spec = resource?.spec || {}
114
+ const status = resource?.status || {}
115
+ const desired = spec.replicas ?? 1
116
+ const updated = status.updatedReplicas ?? 0
117
+ const ready = status.readyReplicas ?? 0
118
+ const base = activity('idle', false, false, 'Stable', '', desired, updated, ready, ready)
119
+
120
+ if ((status.observedGeneration ?? 0) < (resource?.metadata?.generation ?? 0)) {
121
+ return merge(base, 'applying', true, 'Applying change', 'Waiting for the StatefulSet controller to observe generation')
122
+ }
123
+ if (desired === 0) return merge(base, 'idle', false, 'Scaled to zero', 'No replicas requested')
124
+ if (spec.updateStrategy?.type === 'OnDelete') {
125
+ return updated === desired && ready === desired
126
+ ? merge(base, 'idle', false, 'Stable', `${ready}/${desired} ready`)
127
+ : mergeManual(base, 'waiting', true, 'Waiting for Pod restart', `OnDelete strategy · ${updated}/${desired} updated`)
128
+ }
129
+ const partition = spec.updateStrategy?.rollingUpdate?.partition ?? 0
130
+ const target = Math.max(0, desired - partition)
131
+ if (updated >= target) {
132
+ const pendingRevision = Boolean(status.currentRevision && status.updateRevision && status.currentRevision !== status.updateRevision)
133
+ return partition > 0 && pendingRevision
134
+ ? merge(base, 'partition-reached', false, 'Partition reached', `${partition}/${desired} Pods intentionally retained`)
135
+ : merge(base, 'idle', false, 'Stable', `${ready}/${desired} ready`)
136
+ }
137
+ const detail = `${updated}/${target} target Pods updated · ${ready}/${desired} ready`
138
+ return updated === 0
139
+ ? merge(base, 'waiting', true, 'Waiting for new revision', detail)
140
+ : merge(base, 'progressing', true, 'Rolling out', detail)
141
+ }
142
+
143
+ function daemonSetActivity(resource: any): WorkloadRolloutActivity {
144
+ const spec = resource?.spec || {}
145
+ const status = resource?.status || {}
146
+ const desired = status.desiredNumberScheduled ?? 0
147
+ const updated = status.updatedNumberScheduled ?? 0
148
+ const ready = status.numberReady ?? 0
149
+ const available = status.numberAvailable ?? 0
150
+ const base = activity('idle', false, false, 'Stable', '', desired, updated, ready, available)
151
+
152
+ if ((status.observedGeneration ?? 0) < (resource?.metadata?.generation ?? 0)) {
153
+ return merge(base, 'applying', true, 'Applying change', 'Waiting for the DaemonSet controller to observe generation')
154
+ }
155
+ if (desired === 0) return merge(base, 'idle', false, 'No targets', 'Selector matches no nodes')
156
+ if (spec.updateStrategy?.type === 'OnDelete') {
157
+ return updated === desired && available === desired
158
+ ? merge(base, 'idle', false, 'Stable', `${available}/${desired} available`)
159
+ : mergeManual(base, 'waiting', true, 'Waiting for Pod restart', `OnDelete strategy · ${updated}/${desired} updated`)
160
+ }
161
+ if (updated === desired) return merge(base, 'idle', false, 'Stable', `${available}/${desired} available`)
162
+ return updated === 0
163
+ ? merge(base, 'waiting', true, 'Waiting for new revision', replicaDetail(updated, desired, available))
164
+ : merge(base, 'progressing', true, 'Rolling out', replicaDetail(updated, desired, available))
165
+ }
166
+
167
+ function argoRolloutActivity(resource: any): WorkloadRolloutActivity {
168
+ const spec = resource?.spec || {}
169
+ const status = resource?.status || {}
170
+ const desired = spec.replicas ?? ((status.replicas ?? 0) || 1)
171
+ const updated = status.updatedReplicas ?? 0
172
+ const ready = status.readyReplicas ?? 0
173
+ const available = status.availableReplicas ?? 0
174
+ const base = activity('idle', false, false, 'Stable', '', desired, updated, ready, available)
175
+ const detail = argoDetail(resource, updated, desired, available)
176
+ const observedGeneration = getObservedGeneration(resource)
177
+ if (observedGeneration > 0 && observedGeneration < (resource?.metadata?.generation ?? 0)) {
178
+ return merge(base, 'applying', true, 'Applying change', 'Waiting for the Rollout controller to observe generation')
179
+ }
180
+ const failedCondition = (status.conditions || []).find((condition: any) =>
181
+ (condition.type === 'InvalidSpec' && condition.status === 'True') ||
182
+ (condition.type === 'Progressing' && (condition.status === 'False' || condition.reason === 'ProgressDeadlineExceeded')),
183
+ )
184
+ if (failedCondition || status.abort) {
185
+ return merge(base, 'stalled', false, 'Rollout stalled', failedCondition?.message || failedCondition?.reason || status.message || (status.abort ? 'The Rollout was aborted' : '') || 'The Rollout controller reported a failed revision')
186
+ }
187
+ switch (String(status.phase || '').toLowerCase()) {
188
+ case 'degraded':
189
+ case 'error':
190
+ case 'failed':
191
+ case 'aborted':
192
+ return merge(base, 'stalled', false, 'Rollout stalled', status.message || 'The Rollout controller reported a failed revision')
193
+ case 'paused':
194
+ return merge(base, 'paused', true, 'Rollout paused', detail)
195
+ case 'progressing':
196
+ return updated === 0
197
+ ? merge(base, 'waiting', true, 'Waiting for new revision', detail)
198
+ : merge(base, 'progressing', true, 'Rolling out', detail)
199
+ case 'healthy':
200
+ return merge(base, 'idle', false, 'Stable', `${available}/${desired} available`)
201
+ default:
202
+ return desired > 0 && (updated < desired || available < desired)
203
+ ? merge(base, 'applying', true, 'Applying change', detail)
204
+ : merge(base, 'idle', false, 'Stable', status.message || '')
205
+ }
206
+ }
207
+
208
+ function withUpdatedRevisionFailure(
209
+ rollout: WorkloadRolloutActivity,
210
+ pods?: WorkloadPodInfo[],
211
+ ): WorkloadRolloutActivity {
212
+ if ((!rollout.active && rollout.phase !== 'stalled') || rollout.phase === 'applying' || !pods) return rollout
213
+ const failed = pods
214
+ .filter((pod) => pod.updatedRevision === true && pod.healthLevel === 'unhealthy')
215
+ .sort((a, b) => a.name.localeCompare(b.name))[0]
216
+ if (!failed) return rollout
217
+ const reason = failed.reason || failed.lastTerminationReason || failed.phase || 'Pod failed'
218
+ return merge(rollout, 'stalled', false, 'New revision cannot start', `${reason} · Pod ${failed.name}`)
219
+ }
220
+
221
+ export function getArgoRolloutStepNumber(resource: any): number | null {
222
+ const steps = resource?.spec?.strategy?.canary?.steps || []
223
+ const currentIndex = resource?.status?.currentStepIndex
224
+ if (steps.length === 0 || typeof currentIndex !== 'number') return null
225
+ return Math.min(Math.max(currentIndex + 1, 1), steps.length)
226
+ }
227
+
228
+ function argoDetail(resource: any, updated: number, desired: number, available: number): string {
229
+ const stepNumber = getArgoRolloutStepNumber(resource)
230
+ const step = stepNumber === null ? '' : `Step ${stepNumber} · `
231
+ return `${step}${updated}/${desired} updated · ${available} available`
232
+ }
233
+
234
+ function replicaDetail(updated: number, desired: number, available: number): string {
235
+ return `${updated}/${desired} updated · ${available} available`
236
+ }
237
+
238
+ function activity(
239
+ phase: WorkloadRolloutPhase,
240
+ active: boolean,
241
+ manual: boolean,
242
+ label: string,
243
+ detail: string,
244
+ desired: number,
245
+ updated: number,
246
+ ready: number,
247
+ available: number,
248
+ ): WorkloadRolloutActivity {
249
+ return { phase, active, manual, label, detail, desired, updated, ready, available }
250
+ }
251
+
252
+ function merge(
253
+ base: WorkloadRolloutActivity,
254
+ phase: WorkloadRolloutPhase,
255
+ active: boolean,
256
+ label: string,
257
+ detail: string,
258
+ ): WorkloadRolloutActivity {
259
+ return { ...base, phase, active, manual: false, label, detail }
260
+ }
261
+
262
+ function mergeManual(
263
+ base: WorkloadRolloutActivity,
264
+ phase: WorkloadRolloutPhase,
265
+ active: boolean,
266
+ label: string,
267
+ detail: string,
268
+ ): WorkloadRolloutActivity {
269
+ return { ...base, phase, active, manual: true, label, detail }
270
+ }
271
+
272
+ export function rolloutActivityLevel(activity: WorkloadRolloutActivity): 'healthy' | 'neutral' | 'degraded' | 'unhealthy' {
273
+ switch (activity.phase) {
274
+ case 'stalled':
275
+ return 'unhealthy'
276
+ case 'paused':
277
+ return 'degraded'
278
+ case 'waiting':
279
+ return activity.manual ? 'degraded' : 'neutral'
280
+ case 'applying':
281
+ case 'progressing':
282
+ case 'partition-reached':
283
+ return 'neutral'
284
+ default:
285
+ return activity.label === 'Stable' ? 'healthy' : 'neutral'
286
+ }
287
+ }
288
+
289
+ export function rolloutActivityBadge(activity: WorkloadRolloutActivity): { text: string; color: string; level: 'healthy' | 'neutral' | 'degraded' | 'unhealthy' } {
290
+ const level = rolloutActivityLevel(activity)
291
+ return { text: activity.label, color: `status-${level}`, level }
292
+ }