@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,176 @@
1
+ // Volcano 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 summarizeResourceList(list: any): string {
11
+ if (!list || typeof list !== 'object') return '-'
12
+ const parts: string[] = []
13
+ if (list.cpu !== undefined) parts.push(`CPU: ${list.cpu}`)
14
+ if (list.memory !== undefined) parts.push(`Mem: ${list.memory}`)
15
+ if (list['nvidia.com/gpu'] !== undefined) parts.push(`GPU: ${list['nvidia.com/gpu']}`)
16
+ const rest = Object.keys(list).filter((k) => k !== 'cpu' && k !== 'memory' && k !== 'nvidia.com/gpu')
17
+ if (rest.length > 0) parts.push(`+${rest.length} more`)
18
+ return parts.length > 0 ? parts.join(', ') : '-'
19
+ }
20
+
21
+ function summarizeCounts(entries: Array<[string, number]>): string {
22
+ const parts = entries.filter(([, count]) => count > 0).map(([label, count]) => `${count} ${label}`)
23
+ return parts.length > 0 ? parts.join(', ') : '-'
24
+ }
25
+
26
+ // ============================================================================
27
+ // VOLCANO JOB UTILITIES (batch.volcano.sh/v1alpha1)
28
+ // ============================================================================
29
+
30
+ export function getVolcanoJobStatus(resource: any): StatusBadge {
31
+ const phase = resource.status?.state?.phase
32
+ switch (phase) {
33
+ case 'Running':
34
+ return { text: 'Running', color: healthColors.healthy, level: 'healthy' }
35
+ case 'Pending':
36
+ case 'Completing':
37
+ case 'Completed':
38
+ return { text: phase, color: healthColors.neutral, level: 'neutral' }
39
+ case 'Failed':
40
+ case 'Aborted':
41
+ case 'Terminated':
42
+ return { text: phase, color: healthColors.unhealthy, level: 'unhealthy' }
43
+ case 'Aborting':
44
+ case 'Restarting':
45
+ case 'Terminating':
46
+ return { text: phase, color: healthColors.degraded, level: 'degraded' }
47
+ default:
48
+ return { text: phase || 'Unknown', color: healthColors.unknown, level: 'unknown' }
49
+ }
50
+ }
51
+
52
+ export function getVolcanoJobQueue(resource: any): string {
53
+ return resource.spec?.queue || 'default'
54
+ }
55
+
56
+ export function getVolcanoJobMinAvailable(resource: any): string {
57
+ const min = resource.spec?.minAvailable ?? resource.status?.minAvailable
58
+ return min !== undefined && min !== null ? String(min) : '-'
59
+ }
60
+
61
+ export function getVolcanoJobPodCounts(resource: any): string {
62
+ const status = resource.status || {}
63
+ return summarizeCounts([
64
+ ['running', status.running || 0],
65
+ ['succeeded', status.succeeded || 0],
66
+ ['failed', status.failed || 0],
67
+ ])
68
+ }
69
+
70
+ // ============================================================================
71
+ // VOLCANO QUEUE UTILITIES (scheduling.volcano.sh/v1beta1, cluster-scoped)
72
+ // ============================================================================
73
+
74
+ export function getVolcanoQueueStatus(resource: any): StatusBadge {
75
+ const state = resource.status?.state
76
+ switch (state) {
77
+ case 'Open':
78
+ return { text: 'Open', color: healthColors.healthy, level: 'healthy' }
79
+ case 'Closed':
80
+ return { text: 'Closed', color: healthColors.neutral, level: 'neutral' }
81
+ case 'Closing':
82
+ return { text: 'Closing', color: healthColors.degraded, level: 'degraded' }
83
+ default:
84
+ return { text: state || 'Unknown', color: healthColors.unknown, level: 'unknown' }
85
+ }
86
+ }
87
+
88
+ export function getVolcanoQueueWeight(resource: any): string {
89
+ const weight = resource.spec?.weight
90
+ return weight !== undefined && weight !== null ? String(weight) : '1'
91
+ }
92
+
93
+ export function getVolcanoQueueCapability(resource: any): string {
94
+ return summarizeResourceList(resource.spec?.capability)
95
+ }
96
+
97
+ export function getVolcanoQueueAllocated(resource: any): string {
98
+ return summarizeResourceList(resource.status?.allocated)
99
+ }
100
+
101
+ export function getVolcanoQueuePodGroupCounts(resource: any): string {
102
+ const status = resource.status || {}
103
+ return summarizeCounts([
104
+ ['running', status.running || 0],
105
+ ['inqueue', status.inqueue || 0],
106
+ ['pending', status.pending || 0],
107
+ ['completed', status.completed || 0],
108
+ ['unknown', status.unknown || 0],
109
+ ])
110
+ }
111
+
112
+ // ============================================================================
113
+ // VOLCANO PODGROUP UTILITIES (scheduling.volcano.sh/v1beta1)
114
+ // ============================================================================
115
+
116
+ export function getVolcanoPodGroupStatus(resource: any): StatusBadge {
117
+ const phase = resource.status?.phase
118
+ if (phase === 'Running') {
119
+ return { text: 'Running', color: healthColors.healthy, level: 'healthy' }
120
+ }
121
+ const conditions = resource.status?.conditions || []
122
+ const unschedulable = conditions.some((c: any) => c.type === 'Unschedulable' && c.status === 'True')
123
+ if (unschedulable) {
124
+ return { text: 'Unschedulable', color: healthColors.alert, level: 'alert' }
125
+ }
126
+ if (phase === 'Pending' || phase === 'Inqueue' || phase === 'Completed') {
127
+ return { text: phase, color: healthColors.neutral, level: 'neutral' }
128
+ }
129
+ return { text: phase || 'Unknown', color: healthColors.unknown, level: 'unknown' }
130
+ }
131
+
132
+ export function getVolcanoPodGroupQueue(resource: any): string {
133
+ return resource.spec?.queue || 'default'
134
+ }
135
+
136
+ export function getVolcanoPodGroupMinMember(resource: any): string {
137
+ const min = resource.spec?.minMember
138
+ return min !== undefined && min !== null ? String(min) : '-'
139
+ }
140
+
141
+ // ============================================================================
142
+ // VOLCANO JOBFLOW UTILITIES (flow.volcano.sh/v1alpha1)
143
+ // ============================================================================
144
+
145
+ export function getJobFlowStatus(resource: any): StatusBadge {
146
+ const phase = resource.status?.state?.phase
147
+ switch (phase) {
148
+ case 'Running':
149
+ return { text: 'Running', color: healthColors.healthy, level: 'healthy' }
150
+ case 'Succeed':
151
+ case 'Pending':
152
+ return { text: phase, color: healthColors.neutral, level: 'neutral' }
153
+ case 'Failed':
154
+ return { text: 'Failed', color: healthColors.unhealthy, level: 'unhealthy' }
155
+ case 'Terminating':
156
+ return { text: 'Terminating', color: healthColors.degraded, level: 'degraded' }
157
+ default:
158
+ return { text: phase || 'Unknown', color: healthColors.unknown, level: 'unknown' }
159
+ }
160
+ }
161
+
162
+ export function getJobFlowFlowCount(resource: any): number {
163
+ return (resource.spec?.flows || []).length
164
+ }
165
+
166
+ // ============================================================================
167
+ // VOLCANO JOBTEMPLATE UTILITIES (flow.volcano.sh/v1alpha1)
168
+ // ============================================================================
169
+
170
+ export function getJobTemplateStatus(_resource: any): StatusBadge {
171
+ return { text: 'Template', color: healthColors.neutral, level: 'neutral' }
172
+ }
173
+
174
+ export function getJobTemplateTaskCount(resource: any): number {
175
+ return (resource.spec?.tasks || []).length
176
+ }
@@ -2,6 +2,8 @@
2
2
 
3
3
  import { formatCPUString, formatMemoryString, formatBytes } from '../../utils/format'
4
4
  import { pluralize } from '../../utils/pluralize'
5
+ import type { WorkloadPodInfo } from '../../types/core'
6
+ import { getArgoRolloutStepNumber, getObservedGeneration, getWorkloadRolloutActivity, isArgoRolloutResource, isRolloutActivityVisible, rolloutActivityBadge, type WorkloadRolloutActivity } from '../../utils/workload-rollout'
5
7
 
6
8
  // Import functions from sub-modules used internally by getCellFilterValue
7
9
  import { getCertificateStatus, getCertificateRequestStatus, getClusterIssuerStatus, getClusterIssuerType, getOrderState, getChallengeState, getChallengeType } from './resource-utils-certmanager'
@@ -12,10 +14,23 @@ import { getArgoApplicationStatus, getArgoApplicationSetStatus, getArgoApplicati
12
14
  import { getPolicyReportStatus as _getPolicyReportStatus, getKyvernoPolicyStatus as _getKyvernoPolicyStatus } from './resource-utils-kyverno'
13
15
  import { getResourceClaimStatus as _getResourceClaimStatus, getResourceClaimDeviceClasses as _getResourceClaimDeviceClasses, getResourceClaimTemplateDeviceClasses as _getResourceClaimTemplateDeviceClasses, getResourceClaimAllocation as _getResourceClaimAllocation, getResourceClaimReservedFor as _getResourceClaimReservedFor } from './resource-utils-dra'
14
16
  import { getNvidiaClusterPolicyStatus as _getNvidiaClusterPolicyStatus, getNvidiaClusterPolicyEnabledComponents as _getNvidiaClusterPolicyEnabledComponents, getNvidiaDriverStatus as _getNvidiaDriverStatus } from './resource-utils-nvidia'
17
+ import { getClusterQueueStatus as _getClusterQueueStatus, getLocalQueueStatus as _getLocalQueueStatus, getKueueWorkloadStatus as _getKueueWorkloadStatus, getResourceFlavorStatus as _getResourceFlavorStatus, getAdmissionCheckStatus as _getAdmissionCheckStatus, getProvisioningRequestStatus as _getProvisioningRequestStatus } from './resource-utils-kueue'
18
+ import { getRayClusterStatus as _getRayClusterStatus, getRayJobStatus as _getRayJobStatus, getRayServiceStatus as _getRayServiceStatus, getRayCronJobStatus as _getRayCronJobStatus } from './resource-utils-ray'
19
+ import { getLeaderWorkerSetStatus as _getLeaderWorkerSetStatus, getJobSetStatus as _getJobSetStatus } from './resource-utils-jobset-lws'
20
+ import { getInferenceServiceStatus as _getInferenceServiceStatus, getServingRuntimeStatus as _getServingRuntimeStatus, getInferenceGraphStatus as _getInferenceGraphStatus, getTrainedModelStatus as _getTrainedModelStatus, getLLMInferenceServiceStatus as _getLLMInferenceServiceStatus } from './resource-utils-kserve'
21
+ import { getInferencePoolStatus as _getInferencePoolStatus, getInferenceObjectiveStatus as _getInferenceObjectiveStatus } from './resource-utils-inference-gateway'
22
+ import { getVolcanoJobStatus as _getVolcanoJobStatus, getVolcanoQueueStatus as _getVolcanoQueueStatus, getVolcanoPodGroupStatus as _getVolcanoPodGroupStatus, getJobFlowStatus as _getJobFlowStatus, getJobTemplateStatus as _getJobTemplateStatus } from './resource-utils-volcano'
23
+ import { getKaiQueueStatus as _getKaiQueueStatus, getKaiPodGroupStatus as _getKaiPodGroupStatus } from './resource-utils-kai'
24
+ import { getKaitoWorkspaceStatus as _getKaitoWorkspaceStatus, getRAGEngineStatus as _getRAGEngineStatus } from './resource-utils-kaito'
25
+ import { getNIMServiceStatus as _getNIMServiceStatus, getNIMCacheStatus as _getNIMCacheStatus, getNIMPipelineStatus as _getNIMPipelineStatus } from './resource-utils-nim'
26
+ import { getAMDDeviceConfigStatus as _getAMDDeviceConfigStatus } from './resource-utils-amd-gpu'
27
+ import { getPyTorchJobStatus as _getPyTorchJobStatus, getTFJobStatus as _getTFJobStatus, getMPIJobStatus as _getMPIJobStatus, getTrainJobStatus as _getTrainJobStatus } from './resource-utils-kubeflow-training'
15
28
  import { getBackupStatus as _getBackupStatus, getRestoreStatus as _getRestoreStatus, getScheduleStatus as _getScheduleStatus, getBSLStatus as _getBSLStatus, getBackupRepositoryStatus as _getBackupRepositoryStatus } from './resource-utils-velero'
16
29
  import { getExternalSecretStatus as _getExternalSecretStatus, getClusterExternalSecretStatus as _getClusterExternalSecretStatus, getSecretStoreStatus as _getSecretStoreStatus, getClusterSecretStoreStatus as _getClusterSecretStoreStatus, getSecretStoreProviderType as _getSecretStoreProviderType } from './resource-utils-eso'
17
30
  import { getHPATableState, hpaStatusFromState } from './resource-utils-hpa'
18
31
  import { getCNPGClusterStatus as _getCNPGClusterStatus, getCNPGBackupStatus as _getCNPGBackupStatus, getCNPGScheduledBackupStatus as _getCNPGScheduledBackupStatus, getCNPGPoolerStatus as _getCNPGPoolerStatus, isApiGroup as _isApiGroup, CNPG_GROUP as _CNPG_GROUP } from './resource-utils-cnpg'
32
+ import { getGenericResourceStatus } from './generic-status'
33
+ import { getIstioGatewayStatus as _getIstioGatewayStatus, getIstioGatewayServerCount as _getIstioGatewayServerCount, getIstioGatewaySelectorString as _getIstioGatewaySelectorString } from './resource-utils-istio'
19
34
  import { getCalicoIPPoolAllowedUses, getCalicoIPPoolBlockSize, getCalicoIPPoolEncapsulation, getCalicoPolicyNamespaceSelector, getCalicoPolicyServiceAccountSelector, getCalicoPolicyTypes, isCalicoApiVersion, isCalicoPolicyResource } from './resource-utils-calico'
20
35
 
21
36
  // ============================================================================
@@ -762,8 +777,8 @@ export function getContainerSquareStates(pod: any): ContainerSquareState[] {
762
777
  */
763
778
  function isConverging(resource: any): boolean {
764
779
  const generation = resource?.metadata?.generation
765
- const observed = resource?.status?.observedGeneration
766
- return typeof generation === 'number' && typeof observed === 'number' && observed > 0 && observed < generation
780
+ const observed = getObservedGeneration(resource)
781
+ return typeof generation === 'number' && observed > 0 && observed < generation
767
782
  }
768
783
 
769
784
  /** Whether the pod was created by a batch Job. The owner's API group is checked
@@ -787,8 +802,8 @@ export function getWorkloadStatus(resource: any, kind: string): StatusBadge {
787
802
  // 0 desired = the node selector matches no nodes — intentional/idle, not a
788
803
  // fault and not "unknown" (matches pkg/health.Workload). Sky.
789
804
  if (desired === 0) return { text: '0 nodes', color: healthColors.neutral, level: 'neutral' }
790
- if (ready === desired && updated === desired) {
791
- return { text: `${ready}/${desired}`, color: healthColors.healthy, level: 'healthy' }
805
+ if (ready >= desired && updated >= desired) {
806
+ return { text: `${Math.min(ready, desired)}/${desired}`, color: healthColors.healthy, level: 'healthy' }
792
807
  }
793
808
  // Convergence grace, same as the replica kinds below. pkg/health.Workload
794
809
  // passes this signal for DaemonSets too, so omitting it here would render a
@@ -803,7 +818,7 @@ export function getWorkloadStatus(resource: any, kind: string): StatusBadge {
803
818
  }
804
819
 
805
820
  // Deployment, StatefulSet, ReplicaSet
806
- const desired = spec.replicas ?? status.replicas ?? 0
821
+ const desired = spec.replicas ?? 1
807
822
  const ready = status.readyReplicas || 0
808
823
  const updated = status.updatedReplicas || 0
809
824
  const available = status.availableReplicas || 0
@@ -818,17 +833,17 @@ export function getWorkloadStatus(resource: any, kind: string): StatusBadge {
818
833
  // an outage. Neutral, never healthy: declining to call it broken is not the
819
834
  // same as calling it fine. Guarded on "not already fully ready" so a
820
835
  // converged workload still takes the healthy path below, matching Go's order.
821
- if (isConverging(resource) && !(ready === desired && available === desired)) {
836
+ if (isConverging(resource) && !(ready >= desired && available >= desired)) {
822
837
  return { text: `${ready}/${desired}`, color: healthColors.neutral, level: 'neutral' }
823
838
  }
824
839
 
825
- // Check if updating
826
- if (updated < desired && updated > 0) {
827
- return { text: `Updating ${updated}/${desired}`, color: healthColors.degraded, level: 'degraded' }
840
+ if (ready >= desired && available >= desired) {
841
+ return { text: `${Math.min(ready, desired)}/${desired}`, color: healthColors.healthy, level: 'healthy' }
828
842
  }
829
-
830
- if (ready === desired && available === desired) {
831
- return { text: `${ready}/${desired}`, color: healthColors.healthy, level: 'healthy' }
843
+ const partition = kind === 'statefulsets' ? (spec.updateStrategy?.rollingUpdate?.partition ?? 0) : 0
844
+ const updateTarget = Math.max(0, desired - partition)
845
+ if (updated < updateTarget && updated > 0) {
846
+ return { text: `Updating ${updated}/${updateTarget}`, color: healthColors.degraded, level: 'degraded' }
832
847
  }
833
848
  if (ready > 0) {
834
849
  return { text: `${ready}/${desired}`, color: healthColors.degraded, level: 'degraded' }
@@ -836,6 +851,50 @@ export function getWorkloadStatus(resource: any, kind: string): StatusBadge {
836
851
  return { text: `${ready}/${desired}`, color: healthColors.unhealthy, level: 'unhealthy' }
837
852
  }
838
853
 
854
+ export function getWorkloadDisplayStatus(
855
+ resource: any,
856
+ kind: string,
857
+ workloadPods?: WorkloadPodInfo[],
858
+ ): { activity: WorkloadRolloutActivity; status: StatusBadge } {
859
+ const normalizedKind = kind.toLowerCase().replace(/s$/, '')
860
+ const activity = getWorkloadRolloutActivity(resource, normalizedKind, workloadPods)
861
+ if (normalizedKind === 'rollout' && !isArgoRolloutResource(resource)) {
862
+ return { activity, status: getRolloutStatus(resource) }
863
+ }
864
+ const healthStatus = getWorkloadStatus(resource, `${normalizedKind}s`)
865
+ if (!isRolloutActivityVisible(activity)) {
866
+ return { activity, status: healthStatus }
867
+ }
868
+ const status: StatusBadge = rolloutActivityBadge(activity)
869
+ if (workloadStatusLevelRank[healthStatus.level] > workloadStatusLevelRank[status.level]) {
870
+ status.level = healthStatus.level
871
+ status.color = healthStatus.color
872
+ status.text = `${workloadStatusLabel(healthStatus)} · ${status.text}`
873
+ }
874
+ return { activity, status }
875
+ }
876
+
877
+ export function workloadStatusLabel(status: StatusBadge): string {
878
+ if (status.text === 'Scaled to 0') return status.text
879
+ return {
880
+ healthy: 'Healthy',
881
+ degraded: 'Degraded',
882
+ alert: 'Alert',
883
+ unhealthy: 'Unhealthy',
884
+ neutral: 'Neutral',
885
+ unknown: 'Unknown',
886
+ }[status.level]
887
+ }
888
+
889
+ const workloadStatusLevelRank: Record<HealthLevel, number> = {
890
+ neutral: 0,
891
+ healthy: 0,
892
+ unknown: 2,
893
+ degraded: 3,
894
+ alert: 4,
895
+ unhealthy: 5,
896
+ }
897
+
839
898
  /** Detect problems for Deployments, StatefulSets, DaemonSets. Parallel to getPodProblems. */
840
899
  export function getWorkloadProblems(resource: any, kind: string): PodProblem[] {
841
900
  const problems: PodProblem[] = []
@@ -1232,7 +1291,7 @@ export function getHPAStatus(hpa: any): StatusBadge {
1232
1291
  export function getHPAReplicas(hpa: any): { current: number; min: number; max: number } {
1233
1292
  return {
1234
1293
  current: hpa.status?.currentReplicas || 0,
1235
- min: hpa.spec?.minReplicas || 1,
1294
+ min: hpa.spec?.minReplicas ?? 1,
1236
1295
  max: hpa.spec?.maxReplicas || 0,
1237
1296
  }
1238
1297
  }
@@ -1478,19 +1537,10 @@ export function getPVCAccessModes(pvc: any): string {
1478
1537
  // ============================================================================
1479
1538
 
1480
1539
  export function getRolloutStatus(rollout: any): StatusBadge {
1481
- const phase = rollout.status?.phase || 'Unknown'
1482
- switch (phase) {
1483
- case 'Healthy':
1484
- return { text: 'Healthy', color: healthColors.healthy, level: 'healthy' }
1485
- case 'Paused':
1486
- return { text: 'Paused', color: healthColors.degraded, level: 'degraded' }
1487
- case 'Progressing':
1488
- return { text: 'Progressing', color: healthColors.degraded, level: 'degraded' }
1489
- case 'Degraded':
1490
- return { text: 'Degraded', color: healthColors.unhealthy, level: 'unhealthy' }
1491
- default:
1492
- return { text: phase, color: healthColors.unknown, level: 'unknown' }
1540
+ if (!isArgoRolloutResource(rollout)) {
1541
+ return { text: rollout.status?.phase || 'Unknown', color: healthColors.unknown, level: 'unknown' }
1493
1542
  }
1543
+ return rolloutActivityBadge(getWorkloadRolloutActivity(rollout, 'rollout'))
1494
1544
  }
1495
1545
 
1496
1546
  export function getAnalysisRunStatus(run: any): StatusBadge {
@@ -1543,9 +1593,9 @@ export function getRolloutReady(rollout: any): string {
1543
1593
 
1544
1594
  export function getRolloutStep(rollout: any): string | null {
1545
1595
  const steps = rollout.spec?.strategy?.canary?.steps || []
1546
- const currentIndex = rollout.status?.currentStepIndex
1547
- if (steps.length === 0 || currentIndex === undefined) return null
1548
- return `${currentIndex}/${steps.length}`
1596
+ const stepNumber = getArgoRolloutStepNumber(rollout)
1597
+ if (stepNumber === null) return null
1598
+ return `${stepNumber}/${steps.length}`
1549
1599
  }
1550
1600
 
1551
1601
  // ============================================================================
@@ -2134,13 +2184,16 @@ export function getCellFilterValue(resource: any, column: string, kind: string):
2134
2184
  return resource.spec?.type || resource.type || ''
2135
2185
  case 'status':
2136
2186
  if (kindLower === 'pods') return getPodStatus(resource).text
2137
- if (['deployments', 'statefulsets', 'daemonsets', 'replicasets'].includes(kindLower)) {
2138
- const status = getWorkloadStatus(resource, kindLower)
2139
- if (status.text === 'Scaled to 0') return 'Scaled to 0'
2140
- if (status.level === 'healthy') return 'Healthy'
2141
- if (status.level === 'degraded') return 'Degraded'
2142
- if (status.level === 'unhealthy') return 'Unhealthy'
2143
- return 'Unknown'
2187
+ if (['deployments', 'statefulsets', 'daemonsets', 'replicasets', 'rollouts'].includes(kindLower)) {
2188
+ let status: StatusBadge
2189
+ if (kindLower === 'replicasets') {
2190
+ status = getWorkloadStatus(resource, kindLower)
2191
+ } else {
2192
+ const display = getWorkloadDisplayStatus(resource, kindLower)
2193
+ if (isRolloutActivityVisible(display.activity)) return display.status.text
2194
+ status = display.status
2195
+ }
2196
+ return workloadStatusLabel(status)
2144
2197
  }
2145
2198
  if (kindLower === 'nodes') return getNodeStatus(resource).text
2146
2199
  if (kindLower === 'jobs') return getJobStatus(resource).text
@@ -2150,7 +2203,6 @@ export function getCellFilterValue(resource: any, column: string, kind: string):
2150
2203
  if (kindLower === 'clusterissuers' || kindLower === 'issuers') return getClusterIssuerStatus(resource).text
2151
2204
  if (kindLower === 'persistentvolumeclaims') return getPVCStatus(resource).text
2152
2205
  if (kindLower === 'persistentvolumes') return getPVStatus(resource).text
2153
- if (kindLower === 'rollouts') return getRolloutStatus(resource).text
2154
2206
  if (kindLower === 'analysisruns') return getAnalysisRunStatus(resource).text
2155
2207
  if (kindLower === 'workflows') return getWorkflowStatus(resource).text
2156
2208
  if (kindLower === 'cronworkflows') return getCronWorkflowStatus(resource).text
@@ -2177,7 +2229,52 @@ export function getCellFilterValue(resource: any, column: string, kind: string):
2177
2229
  if (kindLower === 'nvidiaclusterpolicies') return _getNvidiaClusterPolicyStatus(resource).text
2178
2230
  if (kindLower === 'nvidiadrivers') return _getNvidiaDriverStatus(resource).text
2179
2231
  if (kindLower === 'resourceclaims') return _getResourceClaimStatus(resource).text
2180
- if (kindLower === 'backups') return _getBackupStatus(resource).text
2232
+ if (kindLower === 'clusterqueues') return _getClusterQueueStatus(resource).text
2233
+ if (kindLower === 'localqueues') return _getLocalQueueStatus(resource).text
2234
+ if (kindLower === 'workloads') return _getKueueWorkloadStatus(resource).text
2235
+ if (kindLower === 'resourceflavors') return _getResourceFlavorStatus(resource).text
2236
+ if (kindLower === 'admissionchecks') return _getAdmissionCheckStatus(resource).text
2237
+ if (kindLower === 'provisioningrequests') return _getProvisioningRequestStatus(resource).text
2238
+ if (kindLower === 'rayclusters') return _getRayClusterStatus(resource).text
2239
+ if (kindLower === 'rayjobs') return _getRayJobStatus(resource).text
2240
+ if (kindLower === 'rayservices') return _getRayServiceStatus(resource).text
2241
+ if (kindLower === 'raycronjobs') return _getRayCronJobStatus(resource).text
2242
+ if (kindLower === 'leaderworkersets') return _getLeaderWorkerSetStatus(resource).text
2243
+ if (kindLower === 'jobsets') return _getJobSetStatus(resource).text
2244
+ if (kindLower === 'inferenceservices') return _getInferenceServiceStatus(resource).text
2245
+ if (kindLower === 'servingruntimes' || kindLower === 'clusterservingruntimes') return _getServingRuntimeStatus(resource).text
2246
+ if (kindLower === 'inferencegraphs') return _getInferenceGraphStatus(resource).text
2247
+ if (kindLower === 'trainedmodels') return _getTrainedModelStatus(resource).text
2248
+ if (kindLower === 'llminferenceservices') return _getLLMInferenceServiceStatus(resource).text
2249
+ if (kindLower === 'inferencepools') return _getInferencePoolStatus(resource).text
2250
+ if (kindLower === 'inferenceobjectives') return _getInferenceObjectiveStatus(resource).text
2251
+ if (kindLower === 'volcanojobs') return _getVolcanoJobStatus(resource).text
2252
+ if (kindLower === 'volcanoqueues') return _getVolcanoQueueStatus(resource).text
2253
+ if (kindLower === 'volcanopodgroups') return _getVolcanoPodGroupStatus(resource).text
2254
+ if (kindLower === 'jobflows') return _getJobFlowStatus(resource).text
2255
+ if (kindLower === 'jobtemplates') return _getJobTemplateStatus(resource).text
2256
+ if (kindLower === 'kaiqueues') return _getKaiQueueStatus(resource).text
2257
+ if (kindLower === 'kaipodgroups') return _getKaiPodGroupStatus(resource).text
2258
+ if (kindLower === 'kaitoworkspaces') return _getKaitoWorkspaceStatus(resource).text
2259
+ if (kindLower === 'ragengines') return _getRAGEngineStatus(resource).text
2260
+ if (kindLower === 'nimservices') return _getNIMServiceStatus(resource).text
2261
+ if (kindLower === 'nimcaches') return _getNIMCacheStatus(resource).text
2262
+ if (kindLower === 'nimpipelines') return _getNIMPipelineStatus(resource).text
2263
+ if (kindLower === 'deviceconfigs') return _getAMDDeviceConfigStatus(resource).text
2264
+ if (kindLower === 'pytorchjobs') return _getPyTorchJobStatus(resource).text
2265
+ if (kindLower === 'tfjobs') return _getTFJobStatus(resource).text
2266
+ if (kindLower === 'mpijobs') return _getMPIJobStatus(resource).text
2267
+ if (kindLower === 'trainjobs') return _getTrainJobStatus(resource).text
2268
+ // Positively gated, matching the cell dispatch: `backups` is reached here
2269
+ // only when the group is unknown to normalizeKindToPlural, and a third
2270
+ // vendor's Backup renders through the generic path — so the sort key has
2271
+ // to come from there too, or the column sorts on a Velero-derived string
2272
+ // the row never displayed.
2273
+ if (kindLower === 'backups') {
2274
+ if (_isApiGroup(resource.apiVersion, _CNPG_GROUP)) return _getCNPGBackupStatus(resource).text
2275
+ if (_isApiGroup(resource.apiVersion, 'velero.io')) return _getBackupStatus(resource).text
2276
+ return getGenericResourceStatus(resource)?.text ?? ''
2277
+ }
2181
2278
  // Velero's Restore/Schedule keys are group-qualified (see
2182
2279
  // GROUP_QUALIFIED_COLUMN_KEYS) because those plurals are shared with
2183
2280
  // rancher/backup-restore-operator and others. The unqualified plural
@@ -2185,8 +2282,17 @@ export function getCellFilterValue(resource: any, column: string, kind: string):
2185
2282
  // reader below rather than being filtered as though it were Velero.
2186
2283
  if (kindLower === 'velerorestores') return _getRestoreStatus(resource).text
2187
2284
  if (kindLower === 'veleroschedules') return _getScheduleStatus(resource).text
2188
- if (kindLower === 'backupstoragelocations') return _getBSLStatus(resource).text
2189
- if (kindLower === 'backuprepositories') return _getBackupRepositoryStatus(resource).text
2285
+ // Same positive gate as `backups` above, for the same reason: the cell
2286
+ // dispatch sends a non-Velero resource to GenericCell, so the sort and
2287
+ // filter keys have to come from there too.
2288
+ if (kindLower === 'backupstoragelocations') {
2289
+ if (_isApiGroup(resource.apiVersion, 'velero.io')) return _getBSLStatus(resource).text
2290
+ return getGenericResourceStatus(resource)?.text ?? ''
2291
+ }
2292
+ if (kindLower === 'backuprepositories') {
2293
+ if (_isApiGroup(resource.apiVersion, 'velero.io')) return _getBackupRepositoryStatus(resource).text
2294
+ return getGenericResourceStatus(resource)?.text ?? ''
2295
+ }
2190
2296
  if (kindLower === 'externalsecrets') return _getExternalSecretStatus(resource).text
2191
2297
  if (kindLower === 'clusterexternalsecrets') return _getClusterExternalSecretStatus(resource).text
2192
2298
  if (kindLower === 'secretstores') return _getSecretStoreStatus(resource).text
@@ -2203,14 +2309,17 @@ export function getCellFilterValue(resource: any, column: string, kind: string):
2203
2309
  if (kindLower === 'cnpgbackups') return _getCNPGBackupStatus(resource).text
2204
2310
  if (kindLower === 'scheduledbackups' && _isApiGroup(resource.apiVersion, _CNPG_GROUP)) return _getCNPGScheduledBackupStatus(resource).text
2205
2311
  if (kindLower === 'poolers' && _isApiGroup(resource.apiVersion, _CNPG_GROUP)) return _getCNPGPoolerStatus(resource).text
2206
- // Generic CRDs: try status.phase, then Ready condition
2207
- if (resource.status?.phase) return resource.status.phase
2208
- {
2209
- const conditions = resource.status?.conditions || []
2210
- const ready = conditions.find((c: any) => c.type === 'Ready')
2211
- if (ready?.status === 'True') return 'Ready'
2212
- if (ready?.status === 'False') return 'Not Ready'
2213
- }
2312
+ if (kindLower === 'istiogateways') return _getIstioGatewayStatus(resource).text
2313
+ // Generic CRDs. Must read the same text the cell renders or the dropdown
2314
+ // offers strings that appear on no row — hence the shared derivation.
2315
+ return getGenericResourceStatus(resource)?.text ?? ''
2316
+ // Istio Gateway's own columns. Without these the filter dropdown is empty
2317
+ // for a column the table is visibly populating.
2318
+ case 'servers':
2319
+ if (kindLower === 'istiogateways') return String(_getIstioGatewayServerCount(resource))
2320
+ return ''
2321
+ case 'selector':
2322
+ if (kindLower === 'istiogateways') return _getIstioGatewaySelectorString(resource)
2214
2323
  return ''
2215
2324
  case 'state':
2216
2325
  if (kindLower === 'orders') return getOrderState(resource).text
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from 'vitest'
2
- import { isColumnFilterableByDistinctCount, SKIP_FILTER_COLUMNS } from './ResourcesView'
2
+ import { getCellFilterKind, hasCuratedColumns, isColumnFilterableByDistinctCount, normalizeKindToPlural, SKIP_FILTER_COLUMNS } from './ResourcesView'
3
3
 
4
4
  describe('isColumnFilterableByDistinctCount', () => {
5
5
  // Caps were tuned after a customer-visible regression where the namespace
@@ -72,3 +72,29 @@ describe('isColumnFilterableByDistinctCount', () => {
72
72
  })
73
73
  })
74
74
  })
75
+
76
+ describe('GPU ecosystem column routing', () => {
77
+ it('uses specialized columns only for the owning API group', () => {
78
+ expect(hasCuratedColumns('workloads', 'kueue.x-k8s.io')).toBe(true)
79
+ expect(hasCuratedColumns('workloads', 'scheduling.k8s.io')).toBe(false)
80
+ expect(hasCuratedColumns('Workload', 'scheduling.k8s.io')).toBe(false)
81
+ expect(hasCuratedColumns('deviceconfigs', 'amd.com')).toBe(true)
82
+ expect(hasCuratedColumns('deviceconfigs', 'example.io')).toBe(false)
83
+ expect(getCellFilterKind('workloads', 'kueue.x-k8s.io')).toBe('workloads')
84
+ expect(getCellFilterKind('workloads', 'scheduling.k8s.io')).toBe('__generic_workloads')
85
+ expect(getCellFilterKind('deviceconfigs', 'example.io')).toBe('__generic_deviceconfigs')
86
+ })
87
+
88
+ it('keeps core, Volcano, and foreign Jobs on separate column paths', () => {
89
+ expect(normalizeKindToPlural('jobs', 'batch')).toBe('jobs')
90
+ expect(normalizeKindToPlural('jobs', 'batch.volcano.sh')).toBe('volcanojobs')
91
+ expect(normalizeKindToPlural('jobs', 'example.io')).toBe('__generic_jobs')
92
+ expect(normalizeKindToPlural('Job', 'example.io')).toBe('__generic_job')
93
+ })
94
+
95
+ it('recognizes both the current and deployed InferenceObjective groups', () => {
96
+ expect(hasCuratedColumns('inferenceobjectives', 'llm-d.ai')).toBe(true)
97
+ expect(hasCuratedColumns('inferenceobjectives', 'inference.networking.x-k8s.io')).toBe(true)
98
+ expect(hasCuratedColumns('inferenceobjectives', 'example.io')).toBe(false)
99
+ })
100
+ })
@@ -0,0 +1,55 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { readFileSync } from 'node:fs'
3
+ import { dirname, resolve } from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+ import { getWorkloadRolloutActivity, rolloutMayAdvanceAutomatically, type WorkloadRolloutPhase } from '../../utils/workload-rollout'
6
+
7
+ interface GoldenVector {
8
+ name: string
9
+ kind: string
10
+ phase: WorkloadRolloutPhase
11
+ active: boolean
12
+ manual: boolean
13
+ label: string
14
+ detail: string
15
+ desired: number
16
+ updated: number
17
+ ready: number
18
+ available: number
19
+ object: any
20
+ }
21
+
22
+ const here = dirname(fileURLToPath(import.meta.url))
23
+ const fixturePath = resolve(here, '../../../../../pkg/health/testdata/workload_rollout_vectors.json')
24
+ const vectors: GoldenVector[] = JSON.parse(readFileSync(fixturePath, 'utf8')).vectors
25
+
26
+ describe('workload rollout golden vectors', () => {
27
+ it('loads the same fixture as pkg/health', () => {
28
+ expect(vectors.length).toBeGreaterThan(0)
29
+ })
30
+
31
+ for (const vector of vectors) {
32
+ it(`${vector.kind}: ${vector.name}`, () => {
33
+ const result = getWorkloadRolloutActivity(vector.object, vector.kind)
34
+ expect(result).toEqual({
35
+ phase: vector.phase,
36
+ active: vector.active,
37
+ manual: vector.manual,
38
+ label: vector.label,
39
+ detail: vector.detail,
40
+ desired: vector.desired,
41
+ updated: vector.updated,
42
+ ready: vector.ready,
43
+ available: vector.available,
44
+ })
45
+ })
46
+ }
47
+
48
+ it('polls only states that can advance without operator action', () => {
49
+ const base = { active: true, manual: false, label: 'Rolling out', desired: 1, updated: 0, ready: 1, available: 1 }
50
+ expect(rolloutMayAdvanceAutomatically({ ...base, phase: 'progressing' })).toBe(true)
51
+ expect(rolloutMayAdvanceAutomatically({ ...base, phase: 'waiting', label: 'Waiting for new revision' })).toBe(true)
52
+ expect(rolloutMayAdvanceAutomatically({ ...base, phase: 'waiting', manual: true, label: 'Waiting for Pod restart' })).toBe(false)
53
+ expect(rolloutMayAdvanceAutomatically({ ...base, phase: 'paused', label: 'Rollout paused' })).toBe(false)
54
+ })
55
+ })
@@ -14,6 +14,7 @@ export interface ApplyResult {
14
14
  name: string
15
15
  namespace: string
16
16
  kind: string
17
+ apiVersion?: string
17
18
  created: boolean
18
19
  }
19
20
 
@@ -20,6 +20,7 @@ import { Tooltip } from '../ui/Tooltip'
20
20
  import type { SelectedResource } from '../../types'
21
21
  import { resourceToYaml } from '../../utils/yaml'
22
22
  import { triggerDownload } from '../../utils/download'
23
+ import { isCoreBatchJob } from '../../utils/api-resources'
23
24
 
24
25
  // ============================================================================
25
26
  // SUCCESS ANIMATION
@@ -45,6 +46,7 @@ export function SaveSuccessAnimation() {
45
46
  // Get edit warning for resource types with limited editability
46
47
  function getEditWarning(
47
48
  kind: string,
49
+ group?: string,
48
50
  ): { message: string; tip: string; learnMoreUrl?: string } | null {
49
51
  const k = kind.toLowerCase()
50
52
  if (k === 'pods' || k === 'pod') {
@@ -55,7 +57,7 @@ function getEditWarning(
55
57
  'https://kubernetes.io/docs/concepts/workloads/pods/#pod-update-and-replacement',
56
58
  }
57
59
  }
58
- if (k === 'jobs' || k === 'job') {
60
+ if (isCoreBatchJob(k, group)) {
59
61
  return {
60
62
  message: 'Jobs cannot be modified after creation.',
61
63
  tip: 'Delete and recreate the Job to make changes.',
@@ -351,7 +353,7 @@ export function EditableYamlView({
351
353
  }, [])
352
354
 
353
355
  const yamlContent = yamlStringify(data, { lineWidth: 0, indent: 2 })
354
- const editWarning = getEditWarning(resource.kind)
356
+ const editWarning = getEditWarning(resource.kind, resource.group)
355
357
  const formattedError = saveError ? formatSaveError(saveError) : null
356
358
  const isPending = (isSaving ?? false) || (isPreviewing ?? false)
357
359