@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,114 @@
1
+ // KAITO CRD utility functions
2
+
3
+ import type { StatusBadge } from './resource-utils'
4
+ import { healthColors } from './resource-utils'
5
+
6
+ // ============================================================================
7
+ // KAITO WORKSPACE UTILITIES
8
+ // ============================================================================
9
+
10
+ // Workspace has top-level resource/inference/tuning fields, not under spec
11
+ export function getKaitoWorkspaceStatus(resource: any): StatusBadge {
12
+ const state = resource.status?.state
13
+ if (state === 'Failed') {
14
+ return { text: 'Failed', color: healthColors.unhealthy, level: 'unhealthy' }
15
+ }
16
+ if (state === 'NotReady') {
17
+ return { text: 'NotReady', color: healthColors.degraded, level: 'degraded' }
18
+ }
19
+ if (state === 'Ready' || state === 'Running') {
20
+ return { text: state, color: healthColors.healthy, level: 'healthy' }
21
+ }
22
+ if (state === 'Succeeded') {
23
+ return { text: 'Succeeded', color: healthColors.neutral, level: 'neutral' }
24
+ }
25
+ if (state === 'Pending') {
26
+ return { text: 'Pending', color: healthColors.neutral, level: 'neutral' }
27
+ }
28
+
29
+ const conditions = resource.status?.conditions || []
30
+ if (conditions.length === 0) {
31
+ return { text: 'Unknown', color: healthColors.unknown, level: 'unknown' }
32
+ }
33
+
34
+ const find = (type: string) => conditions.find((c: any) => c.type === type)
35
+ const succeeded = find('WorkspaceSucceeded')
36
+ const resourceReady = find('ResourceReady')
37
+ const inferenceReady = find('InferenceReady')
38
+ const jobStarted = find('JobStarted')
39
+
40
+ if (succeeded?.status === 'True') {
41
+ return { text: 'Succeeded', color: healthColors.neutral, level: 'neutral' }
42
+ }
43
+ if (resourceReady?.status === 'False') {
44
+ return { text: resourceReady.reason || 'ResourcesNotReady', color: healthColors.alert, level: 'alert' }
45
+ }
46
+ if (inferenceReady?.status === 'False') {
47
+ return { text: inferenceReady.reason || 'InferenceNotReady', color: healthColors.degraded, level: 'degraded' }
48
+ }
49
+ if (succeeded?.status === 'False') {
50
+ return { text: succeeded.reason || 'NotReady', color: healthColors.degraded, level: 'degraded' }
51
+ }
52
+ if (resourceReady?.status === 'True' || inferenceReady?.status === 'True' || jobStarted?.status === 'True') {
53
+ return { text: 'Progressing', color: healthColors.degraded, level: 'degraded' }
54
+ }
55
+
56
+ return { text: 'Unknown', color: healthColors.unknown, level: 'unknown' }
57
+ }
58
+
59
+ export function getKaitoWorkspaceInstanceType(resource: any): string {
60
+ return resource.resource?.instanceType || '-'
61
+ }
62
+
63
+ export function getKaitoWorkspacePreset(resource: any): string {
64
+ return resource.inference?.preset?.name || resource.tuning?.preset?.name || '-'
65
+ }
66
+
67
+ export function getKaitoWorkspaceNodeCount(resource: any): string {
68
+ const actual = (resource.status?.workerNodes || []).length
69
+ const desired = resource.status?.targetNodeCount ?? resource.resource?.count
70
+ if (desired !== undefined && desired !== null) return `${actual}/${desired}`
71
+ return `${actual}`
72
+ }
73
+
74
+ // ============================================================================
75
+ // KAITO RAGENGINE UTILITIES
76
+ // ============================================================================
77
+
78
+ export function getRAGEngineStatus(resource: any): StatusBadge {
79
+ const conditions = resource.status?.conditions || []
80
+ if (conditions.length === 0) {
81
+ return { text: 'Unknown', color: healthColors.unknown, level: 'unknown' }
82
+ }
83
+
84
+ const find = (type: string) => conditions.find((c: any) => c.type === type)
85
+ const resourceReady = find('ResourceReady')
86
+ const serviceReady = find('ServiceReady')
87
+
88
+ if (resourceReady?.status === 'False') {
89
+ return { text: resourceReady.reason || 'ResourcesNotReady', color: healthColors.alert, level: 'alert' }
90
+ }
91
+ if (serviceReady?.status === 'False') {
92
+ return { text: serviceReady.reason || 'ServiceNotReady', color: healthColors.degraded, level: 'degraded' }
93
+ }
94
+ if (resourceReady?.status === 'True' && serviceReady?.status === 'True') {
95
+ return { text: 'Ready', color: healthColors.healthy, level: 'healthy' }
96
+ }
97
+ if (resourceReady?.status === 'True' || serviceReady?.status === 'True') {
98
+ return { text: 'Progressing', color: healthColors.degraded, level: 'degraded' }
99
+ }
100
+
101
+ return { text: 'Unknown', color: healthColors.unknown, level: 'unknown' }
102
+ }
103
+
104
+ export function getRAGEngineEmbeddingModel(resource: any): string {
105
+ const embedding = resource.spec?.embedding
106
+ if (!embedding) return '-'
107
+ if (embedding.local) return embedding.local.modelID || embedding.local.image || '-'
108
+ if (embedding.remote) return embedding.remote.url || 'remote'
109
+ return '-'
110
+ }
111
+
112
+ export function getRAGEngineInstanceType(resource: any): string {
113
+ return resource.spec?.compute?.instanceType || '-'
114
+ }
@@ -0,0 +1,186 @@
1
+ // KServe 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 findCondition(resource: any, type: string): any {
11
+ const conditions = resource.status?.conditions
12
+ if (!Array.isArray(conditions)) return undefined
13
+ return conditions.find((c: any) => c?.type === type)
14
+ }
15
+
16
+ function truncateMiddle(value: string, max = 44): string {
17
+ if (value.length <= max) return value
18
+ const keep = Math.floor((max - 1) / 2)
19
+ return `${value.slice(0, keep)}…${value.slice(-keep)}`
20
+ }
21
+
22
+ // ============================================================================
23
+ // KSERVE INFERENCESERVICE UTILITIES
24
+ // ============================================================================
25
+
26
+ export function getInferenceServiceStatus(resource: any): StatusBadge {
27
+ const transition = resource.status?.modelStatus?.transitionStatus
28
+ if (transition === 'BlockedByFailedLoad') {
29
+ return { text: 'Load failed', color: healthColors.unhealthy, level: 'unhealthy' }
30
+ }
31
+ if (transition === 'InvalidSpec') {
32
+ return { text: 'Invalid spec', color: healthColors.unhealthy, level: 'unhealthy' }
33
+ }
34
+
35
+ const readyCond = findCondition(resource, 'Ready')
36
+ if (readyCond?.status === 'True') {
37
+ return { text: 'Ready', color: healthColors.healthy, level: 'healthy' }
38
+ }
39
+ if (readyCond) {
40
+ return { text: readyCond.reason || 'NotReady', color: healthColors.degraded, level: 'degraded' }
41
+ }
42
+
43
+ return { text: 'Unknown', color: healthColors.unknown, level: 'unknown' }
44
+ }
45
+
46
+ export function getInferenceServiceUrl(resource: any): string {
47
+ const url = resource.status?.url
48
+ return typeof url === 'string' && url ? url : '-'
49
+ }
50
+
51
+ export function getInferenceServiceModelFormat(resource: any): string {
52
+ return resource.spec?.predictor?.model?.modelFormat?.name || '-'
53
+ }
54
+
55
+ export function getInferenceServiceRuntime(resource: any): string {
56
+ return resource.spec?.predictor?.model?.runtime || '-'
57
+ }
58
+
59
+ export function getInferenceServiceDeploymentMode(resource: any): string {
60
+ const annotations = resource.metadata?.annotations || {}
61
+ return annotations['serving.kserve.io/deploymentMode'] || resource.status?.deploymentMode || '-'
62
+ }
63
+
64
+ // ============================================================================
65
+ // KSERVE SERVINGRUNTIME / CLUSTERSERVINGRUNTIME UTILITIES
66
+ // ============================================================================
67
+
68
+ export function getServingRuntimeStatus(resource: any): StatusBadge {
69
+ if (resource.spec?.disabled === true) {
70
+ return { text: 'Disabled', color: healthColors.neutral, level: 'neutral' }
71
+ }
72
+ return { text: 'Enabled', color: healthColors.neutral, level: 'neutral' }
73
+ }
74
+
75
+ export function getServingRuntimeModelFormats(resource: any): string {
76
+ const formats = resource.spec?.supportedModelFormats
77
+ if (!Array.isArray(formats) || formats.length === 0) return '-'
78
+ const names = [...new Set(formats.map((f: any) => f?.name).filter(Boolean))] as string[]
79
+ if (names.length === 0) return '-'
80
+ if (names.length > 3) return `${names.slice(0, 3).join(', ')} +${names.length - 3}`
81
+ return names.join(', ')
82
+ }
83
+
84
+ export function getServingRuntimeImage(resource: any): string {
85
+ const containers = resource.spec?.containers
86
+ if (!Array.isArray(containers) || containers.length === 0) return '-'
87
+ return containers[0]?.image || '-'
88
+ }
89
+
90
+ // ============================================================================
91
+ // KSERVE INFERENCEGRAPH UTILITIES
92
+ // ============================================================================
93
+
94
+ export function getInferenceGraphStatus(resource: any): StatusBadge {
95
+ const readyCond = findCondition(resource, 'Ready')
96
+ if (readyCond?.status === 'True') {
97
+ return { text: 'Ready', color: healthColors.healthy, level: 'healthy' }
98
+ }
99
+ if (readyCond?.status === 'False') {
100
+ return { text: readyCond.reason || 'NotReady', color: healthColors.unhealthy, level: 'unhealthy' }
101
+ }
102
+ return { text: 'Unknown', color: healthColors.unknown, level: 'unknown' }
103
+ }
104
+
105
+ export function getInferenceGraphNodeCount(resource: any): number {
106
+ const nodes = resource.spec?.nodes
107
+ if (!nodes || typeof nodes !== 'object' || Array.isArray(nodes)) return 0
108
+ return Object.keys(nodes).length
109
+ }
110
+
111
+ // ============================================================================
112
+ // KSERVE TRAINEDMODEL UTILITIES
113
+ // ============================================================================
114
+
115
+ export function getTrainedModelStatus(resource: any): StatusBadge {
116
+ const readyCond = findCondition(resource, 'Ready')
117
+ if (readyCond?.status === 'True') {
118
+ return { text: 'Ready', color: healthColors.healthy, level: 'healthy' }
119
+ }
120
+ if (readyCond?.status === 'False') {
121
+ return { text: readyCond.reason || 'NotReady', color: healthColors.unhealthy, level: 'unhealthy' }
122
+ }
123
+ return { text: 'Unknown', color: healthColors.unknown, level: 'unknown' }
124
+ }
125
+
126
+ export function getTrainedModelFramework(resource: any): string {
127
+ return resource.spec?.model?.framework || '-'
128
+ }
129
+
130
+ export function getTrainedModelStorageUri(resource: any): string {
131
+ const uri = resource.spec?.model?.storageUri
132
+ if (typeof uri !== 'string' || !uri) return '-'
133
+ return truncateMiddle(uri)
134
+ }
135
+
136
+ export function getTrainedModelInferenceService(resource: any): string {
137
+ return resource.spec?.inferenceService || '-'
138
+ }
139
+
140
+ // ============================================================================
141
+ // KSERVE LLMINFERENCESERVICE UTILITIES
142
+ // ============================================================================
143
+
144
+ export function getLLMInferenceServiceStatus(resource: any): StatusBadge {
145
+ const conditions = Array.isArray(resource.status?.conditions) ? resource.status.conditions : []
146
+
147
+ const readyCond = conditions.find((c: any) => c?.type === 'Ready')
148
+ if (readyCond?.status === 'True') {
149
+ return { text: 'Ready', color: healthColors.healthy, level: 'healthy' }
150
+ }
151
+ if (readyCond?.status === 'False') {
152
+ return { text: readyCond.reason || 'NotReady', color: healthColors.unhealthy, level: 'unhealthy' }
153
+ }
154
+
155
+ if (conditions.length > 0) {
156
+ const failing = conditions.find((c: any) => c?.status === 'False')
157
+ if (failing) {
158
+ return { text: failing.reason || failing.type || 'NotReady', color: healthColors.degraded, level: 'degraded' }
159
+ }
160
+ if (conditions.every((c: any) => c?.status === 'True')) {
161
+ return { text: 'Ready', color: healthColors.healthy, level: 'healthy' }
162
+ }
163
+ }
164
+
165
+ return { text: 'Unknown', color: healthColors.unknown, level: 'unknown' }
166
+ }
167
+
168
+ export function getLLMInferenceServiceModel(resource: any): string {
169
+ const model = resource.spec?.model
170
+ if (!model || typeof model !== 'object') return '-'
171
+ if (model.name) return model.name
172
+ if (typeof model.uri === 'string' && model.uri) return truncateMiddle(model.uri)
173
+ return '-'
174
+ }
175
+
176
+ export function getLLMInferenceServiceModelUri(resource: any): string {
177
+ const uri = resource.spec?.model?.uri
178
+ if (typeof uri !== 'string' || !uri) return '-'
179
+ return truncateMiddle(uri)
180
+ }
181
+
182
+ export function getLLMInferenceServiceReplicas(resource: any): string {
183
+ const replicas = resource.spec?.replicas
184
+ if (replicas === undefined || replicas === null) return '-'
185
+ return String(replicas)
186
+ }
@@ -0,0 +1,121 @@
1
+ // Kubeflow training CRD utility functions
2
+ // Covers training-operator v1 (PyTorchJob/TFJob/MPIJob at kubeflow.org/v1),
3
+ // mpi-operator (MPIJob at kubeflow.org/v2beta1), and Trainer v2 (TrainJob at trainer.kubeflow.org/v1alpha1)
4
+
5
+ import type { StatusBadge } from './resource-utils'
6
+ import { healthColors, formatDuration } from './resource-utils'
7
+
8
+ // ============================================================================
9
+ // SHARED HELPERS (kubeflow.org JobCondition pattern)
10
+ // ============================================================================
11
+
12
+ const REPLICA_SPEC_KEYS = ['pytorchReplicaSpecs', 'tfReplicaSpecs', 'mpiReplicaSpecs']
13
+
14
+ function getReplicaSpecs(resource: any): Record<string, any> {
15
+ const spec = resource.spec || {}
16
+ for (const key of REPLICA_SPEC_KEYS) {
17
+ if (spec[key]) return spec[key]
18
+ }
19
+ return {}
20
+ }
21
+
22
+ export function trainingJobStatus(resource: any): StatusBadge {
23
+ const conditions = resource.status?.conditions || []
24
+ const isTrue = (type: string) => conditions.some((condition: any) => condition?.type === type && condition?.status === 'True')
25
+ if (isTrue('Failed')) {
26
+ return { text: 'Failed', color: healthColors.unhealthy, level: 'unhealthy' }
27
+ }
28
+ if (isTrue('Succeeded')) {
29
+ return { text: 'Succeeded', color: healthColors.neutral, level: 'neutral' }
30
+ }
31
+ if (isTrue('Suspended')) {
32
+ return { text: 'Suspended', color: healthColors.degraded, level: 'degraded' }
33
+ }
34
+ if (isTrue('Restarting')) {
35
+ return { text: 'Restarting', color: healthColors.degraded, level: 'degraded' }
36
+ }
37
+ if (isTrue('Running')) {
38
+ return { text: 'Running', color: healthColors.healthy, level: 'healthy' }
39
+ }
40
+ return { text: 'Pending', color: healthColors.neutral, level: 'neutral' }
41
+ }
42
+
43
+ export function getTrainingJobReplicas(resource: any): string {
44
+ const specs = getReplicaSpecs(resource)
45
+ const statuses = resource.status?.replicaStatuses || {}
46
+ const types = Object.keys(specs)
47
+ for (const type of Object.keys(statuses)) {
48
+ if (!types.includes(type)) types.push(type)
49
+ }
50
+ if (types.length === 0) return '-'
51
+ return types
52
+ .map((type) => {
53
+ const status = statuses[type] || {}
54
+ const progress = (status.active ?? 0) + (status.succeeded ?? 0)
55
+ const failed = status.failed ?? 0
56
+ const desired = specs[type]?.replicas
57
+ // Unset spec replicas are defaulted in-controller (and the default
58
+ // differs per operator and replica type), so omit the denominator
59
+ // rather than guess
60
+ const count = desired == null ? `${type} ${progress}` : `${type} ${progress}/${desired}`
61
+ return failed > 0 ? `${count} (${failed} failed)` : count
62
+ })
63
+ .join(', ')
64
+ }
65
+
66
+ export function getTrainingJobElapsed(resource: any): string {
67
+ const startTime = resource.status?.startTime
68
+ if (!startTime) return '-'
69
+ const start = new Date(startTime)
70
+ const completionTime = resource.status?.completionTime
71
+ const end = completionTime ? new Date(completionTime) : new Date()
72
+ return formatDuration(end.getTime() - start.getTime())
73
+ }
74
+
75
+ // ============================================================================
76
+ // PYTORCHJOB / TFJOB / MPIJOB STATUS
77
+ // ============================================================================
78
+
79
+ export function getPyTorchJobStatus(resource: any): StatusBadge {
80
+ return trainingJobStatus(resource)
81
+ }
82
+
83
+ export function getTFJobStatus(resource: any): StatusBadge {
84
+ return trainingJobStatus(resource)
85
+ }
86
+
87
+ export function getMPIJobStatus(resource: any): StatusBadge {
88
+ return trainingJobStatus(resource)
89
+ }
90
+
91
+ // ============================================================================
92
+ // TRAINJOB (trainer.kubeflow.org/v1alpha1)
93
+ // ============================================================================
94
+
95
+ export function getTrainJobStatus(resource: any): StatusBadge {
96
+ const conditions = resource.status?.conditions || []
97
+ const isTrue = (type: string) => conditions.some((c: any) => c?.type === type && c?.status === 'True')
98
+
99
+ if (isTrue('Failed')) {
100
+ return { text: 'Failed', color: healthColors.unhealthy, level: 'unhealthy' }
101
+ }
102
+ if (isTrue('Complete')) {
103
+ return { text: 'Complete', color: healthColors.neutral, level: 'neutral' }
104
+ }
105
+ if (isTrue('Suspended') || resource.spec?.suspend === true) {
106
+ return { text: 'Suspended', color: healthColors.degraded, level: 'degraded' }
107
+ }
108
+ const jobsStatus = Array.isArray(resource.status?.jobsStatus) ? resource.status.jobsStatus : []
109
+ if (jobsStatus.some((job: any) => Number(job?.active) > 0)) {
110
+ return { text: 'Running', color: healthColors.healthy, level: 'healthy' }
111
+ }
112
+ return { text: 'Pending', color: healthColors.neutral, level: 'neutral' }
113
+ }
114
+
115
+ export function getTrainJobRuntime(resource: any): string {
116
+ return resource.spec?.runtimeRef?.name || '-'
117
+ }
118
+
119
+ export function getTrainJobSuspended(resource: any): boolean {
120
+ return resource.spec?.suspend === true
121
+ }
@@ -0,0 +1,198 @@
1
+ // Kueue + Cluster Autoscaler ProvisioningRequest 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 findCondition(resource: any, type: string): any {
11
+ return (resource?.status?.conditions || []).find((c: any) => c?.type === type)
12
+ }
13
+
14
+ function activeConditionStatus(resource: any): StatusBadge {
15
+ const active = findCondition(resource, 'Active')
16
+ if (active?.status === 'True') {
17
+ return { text: 'Active', color: healthColors.healthy, level: 'healthy' }
18
+ }
19
+ if (active?.status === 'False') {
20
+ return { text: active.reason || 'Inactive', color: healthColors.alert, level: 'alert' }
21
+ }
22
+ return { text: 'Unknown', color: healthColors.unknown, level: 'unknown' }
23
+ }
24
+
25
+ function formatWorkloadCount(value: any): string {
26
+ return typeof value === 'number' ? String(value) : '-'
27
+ }
28
+
29
+ // ============================================================================
30
+ // KUEUE CLUSTERQUEUE UTILITIES
31
+ // ============================================================================
32
+
33
+ export function getClusterQueueStatus(resource: any): StatusBadge {
34
+ return activeConditionStatus(resource)
35
+ }
36
+
37
+ export function getClusterQueueCohort(resource: any): string {
38
+ // v1beta2 renamed spec.cohort to spec.cohortName
39
+ return resource?.spec?.cohortName || resource?.spec?.cohort || '-'
40
+ }
41
+
42
+ export function getClusterQueuePendingWorkloads(resource: any): string {
43
+ return formatWorkloadCount(resource?.status?.pendingWorkloads)
44
+ }
45
+
46
+ export function getClusterQueueAdmittedWorkloads(resource: any): string {
47
+ return formatWorkloadCount(resource?.status?.admittedWorkloads)
48
+ }
49
+
50
+ export function getClusterQueueFlavors(resource: any): string {
51
+ const groups = resource?.spec?.resourceGroups || []
52
+ const flavors = [
53
+ ...new Set(groups.flatMap((g: any) => (g?.flavors || []).map((f: any) => f?.name).filter(Boolean))),
54
+ ] as string[]
55
+ if (flavors.length === 0) return '-'
56
+ if (flavors.length > 3) return `${flavors.slice(0, 3).join(', ')} +${flavors.length - 3}`
57
+ return flavors.join(', ')
58
+ }
59
+
60
+ // ============================================================================
61
+ // KUEUE LOCALQUEUE UTILITIES
62
+ // ============================================================================
63
+
64
+ export function getLocalQueueStatus(resource: any): StatusBadge {
65
+ return activeConditionStatus(resource)
66
+ }
67
+
68
+ export function getLocalQueueClusterQueue(resource: any): string {
69
+ return resource?.spec?.clusterQueue || '-'
70
+ }
71
+
72
+ export function getLocalQueuePendingWorkloads(resource: any): string {
73
+ return formatWorkloadCount(resource?.status?.pendingWorkloads)
74
+ }
75
+
76
+ export function getLocalQueueAdmittedWorkloads(resource: any): string {
77
+ return formatWorkloadCount(resource?.status?.admittedWorkloads)
78
+ }
79
+
80
+ // ============================================================================
81
+ // KUEUE WORKLOAD UTILITIES
82
+ // ============================================================================
83
+
84
+ export function getKueueWorkloadStatus(resource: any): StatusBadge {
85
+ const finished = findCondition(resource, 'Finished')
86
+ if (finished?.status === 'True') {
87
+ if (finished.reason === 'Failed') {
88
+ return { text: 'Failed', color: healthColors.unhealthy, level: 'unhealthy' }
89
+ }
90
+ return { text: 'Finished', color: healthColors.neutral, level: 'neutral' }
91
+ }
92
+
93
+ const evicted = findCondition(resource, 'Evicted')
94
+ if (evicted?.status === 'True') {
95
+ return { text: 'Evicted', color: healthColors.degraded, level: 'degraded' }
96
+ }
97
+
98
+ const preempted = findCondition(resource, 'Preempted')
99
+ if (preempted?.status === 'True') {
100
+ return { text: 'Preempted', color: healthColors.degraded, level: 'degraded' }
101
+ }
102
+
103
+ const admitted = findCondition(resource, 'Admitted')
104
+ if (admitted?.status === 'True') {
105
+ return { text: 'Admitted', color: healthColors.healthy, level: 'healthy' }
106
+ }
107
+
108
+ const quotaReserved = findCondition(resource, 'QuotaReserved')
109
+ if (quotaReserved?.status === 'True') {
110
+ return { text: 'QuotaReserved', color: healthColors.neutral, level: 'neutral' }
111
+ }
112
+
113
+ return { text: 'Pending', color: healthColors.neutral, level: 'neutral' }
114
+ }
115
+
116
+ export function getKueueWorkloadQueueName(resource: any): string {
117
+ return resource?.spec?.queueName || '-'
118
+ }
119
+
120
+ export function getKueueWorkloadAdmittedBy(resource: any): string {
121
+ return resource?.status?.admission?.clusterQueue || '-'
122
+ }
123
+
124
+ export function getKueueWorkloadPriority(resource: any): string {
125
+ const priority = resource?.spec?.priority
126
+ if (typeof priority === 'number') return String(priority)
127
+ // v1beta1 uses spec.priorityClassName, v1beta2 uses spec.priorityClassRef
128
+ return resource?.spec?.priorityClassRef?.name || resource?.spec?.priorityClassName || '-'
129
+ }
130
+
131
+ // ============================================================================
132
+ // KUEUE RESOURCEFLAVOR UTILITIES
133
+ // ============================================================================
134
+
135
+ export function getResourceFlavorStatus(_resource: any): StatusBadge {
136
+ return { text: 'Configured', color: healthColors.neutral, level: 'neutral' }
137
+ }
138
+
139
+ export function getResourceFlavorNodeLabelCount(resource: any): number {
140
+ return Object.keys(resource?.spec?.nodeLabels || {}).length
141
+ }
142
+
143
+ export function getResourceFlavorTaintCount(resource: any): number {
144
+ return (resource?.spec?.nodeTaints || []).length
145
+ }
146
+
147
+ // ============================================================================
148
+ // KUEUE ADMISSIONCHECK UTILITIES
149
+ // ============================================================================
150
+
151
+ export function getAdmissionCheckStatus(resource: any): StatusBadge {
152
+ return activeConditionStatus(resource)
153
+ }
154
+
155
+ export function getAdmissionCheckControllerName(resource: any): string {
156
+ return resource?.spec?.controllerName || '-'
157
+ }
158
+
159
+ // ============================================================================
160
+ // CLUSTER AUTOSCALER PROVISIONINGREQUEST UTILITIES
161
+ // ============================================================================
162
+
163
+ export function getProvisioningRequestStatus(resource: any): StatusBadge {
164
+ const failed = findCondition(resource, 'Failed')
165
+ if (failed?.status === 'True') {
166
+ return { text: 'Failed', color: healthColors.unhealthy, level: 'unhealthy' }
167
+ }
168
+
169
+ const capacityRevoked = findCondition(resource, 'CapacityRevoked')
170
+ if (capacityRevoked?.status === 'True') {
171
+ return { text: 'CapacityRevoked', color: healthColors.degraded, level: 'degraded' }
172
+ }
173
+
174
+ const bookingExpired = findCondition(resource, 'BookingExpired')
175
+ if (bookingExpired?.status === 'True') {
176
+ return { text: 'BookingExpired', color: healthColors.degraded, level: 'degraded' }
177
+ }
178
+
179
+ const provisioned = findCondition(resource, 'Provisioned')
180
+ if (provisioned?.status === 'True') {
181
+ return { text: 'Provisioned', color: healthColors.healthy, level: 'healthy' }
182
+ }
183
+
184
+ const accepted = findCondition(resource, 'Accepted')
185
+ if (accepted?.status === 'True') {
186
+ return { text: 'Accepted', color: healthColors.neutral, level: 'neutral' }
187
+ }
188
+
189
+ return { text: 'Pending', color: healthColors.neutral, level: 'neutral' }
190
+ }
191
+
192
+ export function getProvisioningRequestClassName(resource: any): string {
193
+ return resource?.spec?.provisioningClassName || '-'
194
+ }
195
+
196
+ export function getProvisioningRequestPodSetCount(resource: any): number {
197
+ return (resource?.spec?.podSets || []).length
198
+ }