@skyhook-io/k8s-ui 1.13.0 → 1.13.4

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 (110) hide show
  1. package/package.json +2 -2
  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 +41 -9
  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 +35 -7
  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 +973 -100
  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/printer-columns.test.ts +298 -0
  28. package/src/components/resources/printer-columns.ts +249 -0
  29. package/src/components/resources/renderers/EventRenderer.test.tsx +50 -1
  30. package/src/components/resources/renderers/EventRenderer.tsx +3 -5
  31. package/src/components/resources/renderers/GenericRenderer.tsx +18 -83
  32. package/src/components/resources/renderers/HPARenderer.test.tsx +28 -0
  33. package/src/components/resources/renderers/HPARenderer.tsx +6 -1
  34. package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +2 -1
  35. package/src/components/resources/renderers/NamespaceRenderer.test.ts +30 -0
  36. package/src/components/resources/renderers/NamespaceRenderer.tsx +7 -3
  37. package/src/components/resources/renderers/PVCRenderer.test.tsx +44 -0
  38. package/src/components/resources/renderers/PVCRenderer.tsx +19 -2
  39. package/src/components/resources/renderers/RolloutRenderer.test.tsx +35 -0
  40. package/src/components/resources/renderers/RolloutRenderer.tsx +22 -3
  41. package/src/components/resources/renderers/WorkloadRenderer.test.tsx +83 -0
  42. package/src/components/resources/renderers/WorkloadRenderer.tsx +25 -65
  43. package/src/components/resources/renderers/amd-gpu-cells.tsx +36 -0
  44. package/src/components/resources/renderers/inference-gateway-cells.tsx +62 -0
  45. package/src/components/resources/renderers/jobset-lws-cells.tsx +82 -0
  46. package/src/components/resources/renderers/kai-cells.tsx +67 -0
  47. package/src/components/resources/renderers/kaito-cells.tsx +62 -0
  48. package/src/components/resources/renderers/kserve-cells.tsx +145 -0
  49. package/src/components/resources/renderers/kubeflow-training-cells.tsx +72 -0
  50. package/src/components/resources/renderers/kueue-cells.tsx +176 -0
  51. package/src/components/resources/renderers/nim-cells.tsx +81 -0
  52. package/src/components/resources/renderers/ray-cells.tsx +124 -0
  53. package/src/components/resources/renderers/volcano-cells.tsx +140 -0
  54. package/src/components/resources/resource-utils-amd-gpu.ts +61 -0
  55. package/src/components/resources/resource-utils-gpu-ecosystem.test.ts +222 -0
  56. package/src/components/resources/resource-utils-hpa.test.ts +4 -0
  57. package/src/components/resources/resource-utils-hpa.ts +6 -2
  58. package/src/components/resources/resource-utils-inference-gateway.ts +119 -0
  59. package/src/components/resources/resource-utils-jobset-lws.ts +117 -0
  60. package/src/components/resources/resource-utils-kai.ts +105 -0
  61. package/src/components/resources/resource-utils-kaito.ts +114 -0
  62. package/src/components/resources/resource-utils-kserve.ts +186 -0
  63. package/src/components/resources/resource-utils-kubeflow-training.ts +121 -0
  64. package/src/components/resources/resource-utils-kueue.ts +198 -0
  65. package/src/components/resources/resource-utils-nim.ts +109 -0
  66. package/src/components/resources/resource-utils-ray.ts +214 -0
  67. package/src/components/resources/resource-utils-rollout.test.ts +183 -0
  68. package/src/components/resources/resource-utils-velero.test.ts +30 -0
  69. package/src/components/resources/resource-utils-volcano.ts +176 -0
  70. package/src/components/resources/resource-utils.ts +156 -47
  71. package/src/components/resources/resources-column-filter.test.ts +27 -1
  72. package/src/components/resources/workload-rollout-golden.test.ts +55 -0
  73. package/src/components/shared/CreateResourceDialog.tsx +1 -0
  74. package/src/components/shared/EditableYamlView.tsx +4 -2
  75. package/src/components/shared/ResourceActionsBar.tsx +170 -25
  76. package/src/components/shared/ResourceRendererDispatch.test.tsx +206 -1
  77. package/src/components/shared/ResourceRendererDispatch.tsx +84 -40
  78. package/src/components/shared/SetImageDialog.test.ts +129 -0
  79. package/src/components/shared/SetImageDialog.tsx +577 -0
  80. package/src/components/shared/index.ts +1 -0
  81. package/src/components/shared/revision-role-badges.test.ts +56 -4
  82. package/src/components/timeline/TimelineList.tsx +19 -3
  83. package/src/components/timeline/TimelineSwimlanes.tsx +11 -3
  84. package/src/components/trace/ReachabilityView.tsx +1 -1
  85. package/src/components/trace/TracePanel.tsx +1 -0
  86. package/src/components/ui/InClusterConsentDialog.tsx +5 -4
  87. package/src/components/ui/SelectMenu.tsx +202 -28
  88. package/src/components/ui/drawer-components.tsx +12 -0
  89. package/src/components/workload/WorkloadRolloutNotice.tsx +60 -0
  90. package/src/components/workload/WorkloadView.tsx +146 -83
  91. package/src/hooks/useProgressiveRefresh.ts +37 -0
  92. package/src/source-hygiene.test.ts +28 -0
  93. package/src/theme/tailwind-theme.css +4 -0
  94. package/src/types/core.ts +62 -4
  95. package/src/utils/api-resources.test.ts +40 -1
  96. package/src/utils/api-resources.ts +11 -1
  97. package/src/utils/applications.ts +50 -0
  98. package/src/utils/bulk-workload-actions.test.ts +20 -0
  99. package/src/utils/bulk-workload-actions.ts +15 -0
  100. package/src/utils/clipboard.test.ts +86 -0
  101. package/src/utils/clipboard.ts +50 -0
  102. package/src/utils/extended-resources.ts +4 -7
  103. package/src/utils/format.test.ts +83 -1
  104. package/src/utils/format.ts +80 -1
  105. package/src/utils/inClusterConsent.test.ts +9 -0
  106. package/src/utils/inClusterConsent.ts +9 -9
  107. package/src/utils/index.ts +1 -0
  108. package/src/utils/navigation.test.ts +31 -1
  109. package/src/utils/navigation.ts +21 -2
  110. package/src/utils/workload-rollout.ts +292 -0
@@ -0,0 +1,109 @@
1
+ // NVIDIA NIM Operator CRD utility functions
2
+
3
+ import type { StatusBadge } from './resource-utils'
4
+ import { healthColors } from './resource-utils'
5
+
6
+ // ============================================================================
7
+ // SHARED HELPERS
8
+ // ============================================================================
9
+
10
+ function stateToBadge(state: string | undefined): StatusBadge {
11
+ switch (state) {
12
+ case 'Ready':
13
+ return { text: 'Ready', color: healthColors.healthy, level: 'healthy' }
14
+ case 'Failed':
15
+ return { text: 'Failed', color: healthColors.unhealthy, level: 'unhealthy' }
16
+ case 'NotReady':
17
+ case 'Pending':
18
+ case 'InProgress':
19
+ case 'Started':
20
+ case 'PVC-Created':
21
+ return { text: state, color: healthColors.degraded, level: 'degraded' }
22
+ default:
23
+ return { text: state || 'Unknown', color: healthColors.unknown, level: 'unknown' }
24
+ }
25
+ }
26
+
27
+ // ============================================================================
28
+ // NIMSERVICE UTILITIES
29
+ // ============================================================================
30
+
31
+ export function getNIMServiceStatus(resource: any): StatusBadge {
32
+ return stateToBadge(resource.status?.state)
33
+ }
34
+
35
+ export function getNIMServiceImage(resource: any): string {
36
+ const image = resource.spec?.image
37
+ if (!image?.repository) return '-'
38
+ return image.tag ? `${image.repository}:${image.tag}` : image.repository
39
+ }
40
+
41
+ export function getNIMServiceModel(resource: any): string {
42
+ const modelName = resource.status?.model?.name
43
+ if (modelName) return modelName
44
+ return getNIMServiceImage(resource)
45
+ }
46
+
47
+ export function getNIMServiceReplicas(resource: any): string {
48
+ const available = resource.status?.availableReplicas ?? 0
49
+ const scale = resource.spec?.scale
50
+ if (scale?.enabled) {
51
+ const min = scale.hpa?.minReplicas
52
+ const max = scale.hpa?.maxReplicas
53
+ if (min !== undefined && max !== undefined) return `${available} (${min}-${max})`
54
+ return `${available} (hpa)`
55
+ }
56
+ const desired = resource.spec?.replicas
57
+ if (desired !== undefined && desired !== null) return `${available}/${desired}`
58
+ return `${available}`
59
+ }
60
+
61
+ // ============================================================================
62
+ // NIMCACHE UTILITIES
63
+ // ============================================================================
64
+
65
+ export function getNIMCacheStatus(resource: any): StatusBadge {
66
+ return stateToBadge(resource.status?.state)
67
+ }
68
+
69
+ export function getNIMCacheSourceType(resource: any): string {
70
+ const source = resource.spec?.source
71
+ if (!source) return '-'
72
+ if (source.ngc) return 'NGC'
73
+ if (source.dataStore) return 'DataStore'
74
+ if (source.hf) return 'HF'
75
+ return '-'
76
+ }
77
+
78
+ export function getNIMCacheModelSource(resource: any): string {
79
+ const source = resource.spec?.source
80
+ if (!source) return '-'
81
+ if (source.ngc) return source.ngc.modelPuller || source.ngc.modelEndpoint || '-'
82
+ const dshf = source.dataStore || source.hf
83
+ if (dshf) return dshf.modelName || dshf.datasetName || '-'
84
+ return '-'
85
+ }
86
+
87
+ export function getNIMCacheStorageSize(resource: any): string {
88
+ return resource.spec?.storage?.pvc?.size || '-'
89
+ }
90
+
91
+ export function getNIMCachePVCName(resource: any): string {
92
+ return resource.status?.pvc || resource.spec?.storage?.pvc?.name || '-'
93
+ }
94
+
95
+ // ============================================================================
96
+ // NIMPIPELINE UTILITIES
97
+ // ============================================================================
98
+
99
+ export function getNIMPipelineStatus(resource: any): StatusBadge {
100
+ return stateToBadge(resource.status?.state)
101
+ }
102
+
103
+ export function getNIMPipelineServiceCount(resource: any): string {
104
+ const services = resource.spec?.services || []
105
+ if (services.length === 0) return '0'
106
+ const enabled = services.filter((s: any) => s?.enabled !== false).length
107
+ if (enabled < services.length) return `${enabled}/${services.length} enabled`
108
+ return `${services.length}`
109
+ }
@@ -0,0 +1,214 @@
1
+ // KubeRay CRD utility functions (ray.io/v1)
2
+
3
+ import type { StatusBadge } from './resource-utils'
4
+ import { healthColors, formatAge } from './resource-utils'
5
+
6
+ // ============================================================================
7
+ // RAYCLUSTER UTILITIES
8
+ // ============================================================================
9
+
10
+ export function getRayClusterStatus(resource: any): StatusBadge {
11
+ const state = resource.status?.state
12
+ const conditions = resource.status?.conditions || []
13
+
14
+ const suspendingCond = conditions.find((c: any) => c.type === 'RayClusterSuspending')
15
+ if (suspendingCond?.status === 'True') {
16
+ return { text: 'Suspending', color: healthColors.degraded, level: 'degraded' }
17
+ }
18
+
19
+ const suspendedCond = conditions.find((c: any) => c.type === 'RayClusterSuspended')
20
+ if (suspendedCond?.status === 'True') {
21
+ return { text: 'Suspended', color: healthColors.neutral, level: 'neutral' }
22
+ }
23
+
24
+ if (resource.spec?.suspend === true || state === 'suspended') {
25
+ return { text: 'Suspended', color: healthColors.neutral, level: 'neutral' }
26
+ }
27
+
28
+ const replicaFailureCond = conditions.find((c: any) => c.type === 'RayClusterReplicaFailure')
29
+ if (replicaFailureCond?.status === 'True') {
30
+ return { text: replicaFailureCond.reason || 'ReplicaFailure', color: healthColors.unhealthy, level: 'unhealthy' }
31
+ }
32
+
33
+ if (state === 'failed') {
34
+ return { text: 'Failed', color: healthColors.unhealthy, level: 'unhealthy' }
35
+ }
36
+
37
+ const headReadyCond = conditions.find((c: any) => c.type === 'HeadPodReady')
38
+ if (headReadyCond?.status === 'False') {
39
+ return { text: headReadyCond.reason || 'HeadNotReady', color: healthColors.degraded, level: 'degraded' }
40
+ }
41
+ if (headReadyCond?.status === 'True') {
42
+ const availableWorkers = resource.status?.availableWorkerReplicas ?? 0
43
+ const desiredWorkers = resource.status?.desiredWorkerReplicas ?? 0
44
+ if (availableWorkers >= desiredWorkers) {
45
+ return { text: 'Ready', color: healthColors.healthy, level: 'healthy' }
46
+ }
47
+ return { text: `Workers ${availableWorkers}/${desiredWorkers}`, color: healthColors.degraded, level: 'degraded' }
48
+ }
49
+
50
+ if (conditions.length === 0 && state === 'ready') {
51
+ return { text: 'Ready', color: healthColors.healthy, level: 'healthy' }
52
+ }
53
+
54
+ const provisionedCond = conditions.find((c: any) => c.type === 'RayClusterProvisioned')
55
+ if (provisionedCond?.status === 'True') {
56
+ return { text: 'Provisioned', color: healthColors.neutral, level: 'neutral' }
57
+ }
58
+
59
+ if (resource.status) {
60
+ return { text: 'Provisioning', color: healthColors.degraded, level: 'degraded' }
61
+ }
62
+
63
+ return { text: 'Unknown', color: healthColors.unknown, level: 'unknown' }
64
+ }
65
+
66
+ export function getRayClusterVersion(resource: any): string {
67
+ return resource.spec?.rayVersion || '-'
68
+ }
69
+
70
+ export function getRayClusterWorkers(resource: any): string {
71
+ const status = resource.status
72
+ if (!status) return '-'
73
+ const available = status.availableWorkerReplicas ?? 0
74
+ const desired = status.desiredWorkerReplicas ?? 0
75
+ return `${available}/${desired}`
76
+ }
77
+
78
+ export function getRayClusterHeadService(resource: any): string {
79
+ return resource.status?.head?.serviceName || '-'
80
+ }
81
+
82
+ // ============================================================================
83
+ // RAYJOB UTILITIES
84
+ // ============================================================================
85
+
86
+ export function getRayJobStatus(resource: any): StatusBadge {
87
+ const jobStatus = resource.status?.jobStatus
88
+ const deploymentStatus = resource.status?.jobDeploymentStatus
89
+
90
+ if (deploymentStatus === 'Failed' || deploymentStatus === 'ValidationFailed') {
91
+ return { text: deploymentStatus, color: healthColors.unhealthy, level: 'unhealthy' }
92
+ }
93
+
94
+ switch (jobStatus) {
95
+ case 'SUCCEEDED':
96
+ return { text: 'Succeeded', color: healthColors.neutral, level: 'neutral' }
97
+ case 'RUNNING':
98
+ return { text: 'Running', color: healthColors.healthy, level: 'healthy' }
99
+ case 'FAILED':
100
+ return { text: 'Failed', color: healthColors.unhealthy, level: 'unhealthy' }
101
+ case 'STOPPED':
102
+ return { text: 'Stopped', color: healthColors.neutral, level: 'neutral' }
103
+ case 'PENDING':
104
+ return { text: 'Pending', color: healthColors.neutral, level: 'neutral' }
105
+ }
106
+
107
+ switch (deploymentStatus) {
108
+ case 'Suspended':
109
+ return { text: 'Suspended', color: healthColors.neutral, level: 'neutral' }
110
+ case 'Complete':
111
+ return { text: 'Complete', color: healthColors.neutral, level: 'neutral' }
112
+ case 'Running':
113
+ return { text: 'Running', color: healthColors.healthy, level: 'healthy' }
114
+ case 'Initializing':
115
+ case 'Suspending':
116
+ case 'Retrying':
117
+ case 'Waiting':
118
+ return { text: deploymentStatus, color: healthColors.degraded, level: 'degraded' }
119
+ }
120
+
121
+ return { text: 'Unknown', color: healthColors.unknown, level: 'unknown' }
122
+ }
123
+
124
+ export function getRayJobJobStatus(resource: any): string {
125
+ return resource.status?.jobStatus || '-'
126
+ }
127
+
128
+ export function getRayJobDeploymentStatus(resource: any): string {
129
+ return resource.status?.jobDeploymentStatus || '-'
130
+ }
131
+
132
+ export function getRayJobClusterName(resource: any): string {
133
+ return resource.status?.rayClusterName || '-'
134
+ }
135
+
136
+ // ============================================================================
137
+ // RAYSERVICE UTILITIES
138
+ // ============================================================================
139
+
140
+ export function getRayServiceStatus(resource: any): StatusBadge {
141
+ const conditions = resource.status?.conditions || []
142
+
143
+ const suspendingCond = conditions.find((c: any) => c.type === 'Suspending')
144
+ if (suspendingCond?.status === 'True') {
145
+ return { text: 'Suspending', color: healthColors.degraded, level: 'degraded' }
146
+ }
147
+
148
+ const suspendedCond = conditions.find((c: any) => c.type === 'Suspended')
149
+ if (suspendedCond?.status === 'True' || resource.spec?.suspend === true) {
150
+ return { text: 'Suspended', color: healthColors.neutral, level: 'neutral' }
151
+ }
152
+
153
+ const upgradeCond = conditions.find((c: any) => c.type === 'UpgradeInProgress')
154
+ if (upgradeCond?.status === 'True') {
155
+ return { text: 'Upgrading', color: healthColors.degraded, level: 'degraded' }
156
+ }
157
+
158
+ const rollbackCond = conditions.find((c: any) => c.type === 'RollbackInProgress')
159
+ if (rollbackCond?.status === 'True') {
160
+ return { text: 'RollingBack', color: healthColors.degraded, level: 'degraded' }
161
+ }
162
+
163
+ const readyCond = conditions.find((c: any) => c.type === 'Ready')
164
+ if (readyCond?.status === 'True') {
165
+ return { text: 'Ready', color: healthColors.healthy, level: 'healthy' }
166
+ }
167
+ if (readyCond?.status === 'False') {
168
+ return { text: readyCond.reason || 'NotReady', color: healthColors.degraded, level: 'degraded' }
169
+ }
170
+
171
+ if (resource.status?.serviceStatus === 'Running') {
172
+ return { text: 'Running', color: healthColors.healthy, level: 'healthy' }
173
+ }
174
+
175
+ return { text: 'Unknown', color: healthColors.unknown, level: 'unknown' }
176
+ }
177
+
178
+ export function getRayServiceServiceStatus(resource: any): string {
179
+ return resource.status?.serviceStatus || '-'
180
+ }
181
+
182
+ export function getRayServiceClusters(resource: any): string {
183
+ const active = resource.status?.activeServiceStatus?.rayClusterName
184
+ const pending = resource.status?.pendingServiceStatus?.rayClusterName
185
+ const parts: string[] = []
186
+ if (active) parts.push(active)
187
+ if (pending) parts.push(`pending: ${pending}`)
188
+ return parts.join(' ') || '-'
189
+ }
190
+
191
+ // ============================================================================
192
+ // RAYCRONJOB UTILITIES
193
+ // ============================================================================
194
+
195
+ export function getRayCronJobStatus(resource: any): StatusBadge {
196
+ if (resource.spec?.suspend === true) {
197
+ return { text: 'Suspended', color: healthColors.neutral, level: 'neutral' }
198
+ }
199
+ return { text: 'Active', color: healthColors.neutral, level: 'neutral' }
200
+ }
201
+
202
+ export function getRayCronJobSchedule(resource: any): string {
203
+ return resource.spec?.schedule || '-'
204
+ }
205
+
206
+ export function getRayCronJobSuspend(resource: any): boolean {
207
+ return resource.spec?.suspend === true
208
+ }
209
+
210
+ export function getRayCronJobLastSchedule(resource: any): string {
211
+ const lastSchedule = resource.status?.lastScheduleTime
212
+ if (!lastSchedule) return '-'
213
+ return formatAge(lastSchedule)
214
+ }
@@ -0,0 +1,183 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { getCellFilterValue, getRolloutStep, getWorkloadDisplayStatus, getWorkloadStatus, workloadStatusLabel } from './resource-utils'
3
+
4
+ describe('getRolloutStep', () => {
5
+ it('presents Argo currentStepIndex as a one-based step number', () => {
6
+ expect(getRolloutStep({
7
+ spec: { strategy: { canary: { steps: [{ setWeight: 25 }, { pause: {} }] } } },
8
+ status: { currentStepIndex: 0 },
9
+ })).toBe('1/2')
10
+ })
11
+
12
+ it('clamps Argo completed steps to the declared step count', () => {
13
+ expect(getRolloutStep({
14
+ spec: { strategy: { canary: { steps: [{ setWeight: 25 }, { pause: {} }] } } },
15
+ status: { currentStepIndex: 2 },
16
+ })).toBe('2/2')
17
+ })
18
+ })
19
+
20
+ describe('getWorkloadStatus', () => {
21
+ it('keeps a capacity-preserving surge healthy', () => {
22
+ expect(getWorkloadStatus({
23
+ spec: { replicas: 1 },
24
+ status: { readyReplicas: 2, availableReplicas: 1, updatedReplicas: 1 },
25
+ }, 'deployments')).toMatchObject({ text: '1/1', level: 'healthy' })
26
+ })
27
+
28
+ it('shows health instead of Stable for an idle under-replicated workload', () => {
29
+ expect(getWorkloadDisplayStatus({
30
+ metadata: { generation: 2 },
31
+ spec: { replicas: 3 },
32
+ status: {
33
+ observedGeneration: 2,
34
+ replicas: 2,
35
+ updatedReplicas: 3,
36
+ readyReplicas: 2,
37
+ availableReplicas: 2,
38
+ },
39
+ }, 'deployments').status).toMatchObject({ text: '2/3', level: 'degraded' })
40
+ })
41
+
42
+ it('shows rollout activity while a revision is progressing', () => {
43
+ expect(getWorkloadDisplayStatus({
44
+ metadata: { generation: 2 },
45
+ spec: { replicas: 3 },
46
+ status: {
47
+ observedGeneration: 2,
48
+ replicas: 4,
49
+ updatedReplicas: 2,
50
+ readyReplicas: 3,
51
+ availableReplicas: 3,
52
+ },
53
+ }, 'deployments').status).toMatchObject({ text: 'Rolling out', level: 'neutral' })
54
+ })
55
+
56
+ it('shows health instead of an inactive pause policy', () => {
57
+ expect(getWorkloadDisplayStatus({
58
+ metadata: { generation: 2 },
59
+ spec: { replicas: 3, paused: true },
60
+ status: {
61
+ observedGeneration: 2,
62
+ replicas: 3,
63
+ updatedReplicas: 3,
64
+ readyReplicas: 0,
65
+ availableReplicas: 0,
66
+ },
67
+ }, 'deployments').status).toMatchObject({ text: '0/3', level: 'unhealthy' })
68
+ })
69
+
70
+ it('keeps active rollout copy while preserving a worse health tone', () => {
71
+ expect(getWorkloadDisplayStatus({
72
+ metadata: { generation: 2 },
73
+ spec: { replicas: 3, paused: true },
74
+ status: {
75
+ observedGeneration: 2,
76
+ replicas: 3,
77
+ updatedReplicas: 0,
78
+ readyReplicas: 0,
79
+ availableReplicas: 0,
80
+ },
81
+ }, 'deployments').status).toMatchObject({ text: 'Unhealthy · Rollout paused', level: 'unhealthy' })
82
+ })
83
+
84
+ it('preserves unhealthy serving capacity while an Argo Rollout progresses', () => {
85
+ const resource = {
86
+ apiVersion: 'argoproj.io/v1alpha1',
87
+ metadata: { generation: 2 },
88
+ spec: { replicas: 3 },
89
+ status: {
90
+ observedGeneration: '2',
91
+ phase: 'Progressing',
92
+ updatedReplicas: 1,
93
+ readyReplicas: 0,
94
+ availableReplicas: 0,
95
+ },
96
+ }
97
+ const display = getWorkloadDisplayStatus(resource, 'rollouts')
98
+
99
+ expect(display.status).toMatchObject({ text: 'Degraded · Rolling out', level: 'degraded' })
100
+ expect(getCellFilterValue(resource, 'status', 'rollouts')).toBe('Degraded · Rolling out')
101
+ })
102
+
103
+ it('keeps Argo generation lag informational when observedGeneration is a string', () => {
104
+ const resource = {
105
+ apiVersion: 'argoproj.io/v1alpha1',
106
+ metadata: { generation: 3 },
107
+ spec: { replicas: 3 },
108
+ status: {
109
+ observedGeneration: '2',
110
+ phase: 'Healthy',
111
+ updatedReplicas: 0,
112
+ readyReplicas: 0,
113
+ availableReplicas: 0,
114
+ },
115
+ }
116
+
117
+ expect(getWorkloadDisplayStatus(resource, 'rollouts').status).toMatchObject({ text: 'Applying change', level: 'neutral' })
118
+ expect(getCellFilterValue(resource, 'status', 'rollouts')).toBe('Applying change')
119
+ })
120
+
121
+ it('uses semantic health for an idle Argo Rollout', () => {
122
+ const resource = {
123
+ apiVersion: 'argoproj.io/v1alpha1',
124
+ metadata: { generation: 2 },
125
+ spec: { replicas: 3 },
126
+ status: {
127
+ observedGeneration: '2',
128
+ phase: 'Healthy',
129
+ updatedReplicas: 3,
130
+ readyReplicas: 3,
131
+ availableReplicas: 3,
132
+ },
133
+ }
134
+
135
+ expect(getWorkloadDisplayStatus(resource, 'rollouts').status).toMatchObject({ text: '3/3', level: 'healthy' })
136
+ expect(getCellFilterValue(resource, 'status', 'rollouts')).toBe('Healthy')
137
+ })
138
+
139
+ it('does not fabricate Argo rollout state for a colliding CRD', () => {
140
+ const display = getWorkloadDisplayStatus({
141
+ apiVersion: 'rollouts.kruise.io/v1alpha1',
142
+ kind: 'Rollout',
143
+ metadata: { generation: 1 },
144
+ spec: { replicas: 3 },
145
+ status: { phase: 'Progressing', readyReplicas: 3, availableReplicas: 3, updatedReplicas: 3 },
146
+ }, 'rollouts')
147
+
148
+ expect(display.status).toMatchObject({ text: 'Progressing', level: 'unknown' })
149
+ expect(workloadStatusLabel(display.status)).toBe('Unknown')
150
+ })
151
+
152
+ it('shows health instead of a reached StatefulSet partition', () => {
153
+ const resource = {
154
+ metadata: { generation: 2 },
155
+ spec: { replicas: 5, updateStrategy: { rollingUpdate: { partition: 2 } } },
156
+ status: {
157
+ observedGeneration: 2,
158
+ updatedReplicas: 3,
159
+ readyReplicas: 4,
160
+ availableReplicas: 4,
161
+ },
162
+ }
163
+
164
+ expect(getWorkloadDisplayStatus(resource, 'statefulsets').status).toMatchObject({ text: '4/5', level: 'degraded' })
165
+ expect(getCellFilterValue(resource, 'status', 'statefulsets')).toBe('Degraded')
166
+ })
167
+
168
+ it('uses serving health as the filter value for an inactive pause policy', () => {
169
+ const resource = {
170
+ metadata: { generation: 2 },
171
+ spec: { replicas: 3, paused: true },
172
+ status: {
173
+ observedGeneration: 2,
174
+ replicas: 3,
175
+ updatedReplicas: 3,
176
+ readyReplicas: 0,
177
+ availableReplicas: 0,
178
+ },
179
+ }
180
+
181
+ expect(getCellFilterValue(resource, 'status', 'deployments')).toBe('Unhealthy')
182
+ })
183
+ })
@@ -94,6 +94,36 @@ describe('getCellFilterValue — Velero status column', () => {
94
94
  const bsl = { apiVersion: 'velero.io/v1', kind: 'BackupStorageLocation', status: { phase: 'Unavailable' } }
95
95
  expect(getCellFilterValue(bsl, 'status', 'backupstoragelocations')).toBe('Unavailable')
96
96
  })
97
+
98
+ // The cell dispatch sends a non-Velero BackupStorageLocation/BackupRepository
99
+ // to GenericCell. The filter dropdown and the sort key have to follow it
100
+ // there, or the column offers and orders values that appear on no row.
101
+ it('leaves a foreign BackupStorageLocation on the generic reader', () => {
102
+ // Conditions, not a phase: Velero's reader keys on phase and has nothing to
103
+ // say here, so this only passes if the generic derivation ran.
104
+ const foreign = {
105
+ apiVersion: 'other.io/v1',
106
+ kind: 'BackupStorageLocation',
107
+ status: { conditions: [{ type: 'Ready', status: 'True' }] },
108
+ }
109
+ expect(getCellFilterValue(foreign, 'status', 'backupstoragelocations')).toBe('Ready')
110
+ })
111
+
112
+ it('leaves a foreign BackupRepository on the generic reader', () => {
113
+ const foreign = {
114
+ apiVersion: 'other.io/v1',
115
+ kind: 'BackupRepository',
116
+ status: { conditions: [{ type: 'Ready', status: 'True' }] },
117
+ }
118
+ expect(getCellFilterValue(foreign, 'status', 'backuprepositories')).toBe('Ready')
119
+ })
120
+
121
+ // Velero's own readers speak a phase vocabulary a foreign CRD does not share,
122
+ // so the two must not produce the same string for the same object.
123
+ it('does not label a foreign resource with Velero vocabulary', () => {
124
+ const foreign = { apiVersion: 'other.io/v1', kind: 'BackupRepository', status: { phase: 'NotReady' } }
125
+ expect(getCellFilterValue(foreign, 'status', 'backuprepositories')).not.toBe('Not ready')
126
+ })
97
127
  })
98
128
 
99
129
  describe('Velero Backup phase vocabulary', () => {