@skyhook-io/k8s-ui 1.14.9 → 1.14.11

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 (80) hide show
  1. package/package.json +3 -2
  2. package/src/components/applications/ApplicationDetail.tsx +24 -11
  3. package/src/components/audit/AuditAlerts.tsx +1 -3
  4. package/src/components/audit/AuditCoveragePresentation.test.tsx +48 -2
  5. package/src/components/checks/ChecksView.test.tsx +55 -0
  6. package/src/components/checks/ChecksView.tsx +38 -27
  7. package/src/components/dock/NodeTerminalTab.test.tsx +89 -0
  8. package/src/components/dock/NodeTerminalTab.tsx +51 -47
  9. package/src/components/gitops/GitOpsHealthSourceNotice.tsx +17 -9
  10. package/src/components/gitops/detail-helpers.test.ts +32 -0
  11. package/src/components/gitops/detail-helpers.ts +21 -0
  12. package/src/components/gitops/health-provenance.test.ts +4 -3
  13. package/src/components/gitops/index.ts +2 -1
  14. package/src/components/gitops/insights/ArgoResourceDiff.tsx +131 -54
  15. package/src/components/issues/IssuesView.tsx +6 -0
  16. package/src/components/issues/ResourceIssuesSection.test.tsx +36 -0
  17. package/src/components/issues/diagnostic.ts +4 -0
  18. package/src/components/logs/LogCore.tsx +3 -3
  19. package/src/components/logs/WorkloadLogsViewer.tsx +41 -11
  20. package/src/components/logs/useLogBuffer.ts +1 -0
  21. package/src/components/logs/useLogStream.ts +1 -2
  22. package/src/components/resources/KueueAdmissionSection.test.tsx +95 -0
  23. package/src/components/resources/KueueAdmissionSection.tsx +87 -0
  24. package/src/components/resources/ResourcesView.default-sort.test.ts +26 -0
  25. package/src/components/resources/ResourcesView.tsx +91 -16
  26. package/src/components/resources/curated-column-ownership.test.ts +45 -0
  27. package/src/components/resources/renderers/CAPIClusterRenderer.tsx +4 -3
  28. package/src/components/resources/renderers/ContainerEnvironmentSection.tsx +10 -3
  29. package/src/components/resources/renderers/JobSetRenderer.test.tsx +280 -0
  30. package/src/components/resources/renderers/JobSetRenderer.tsx +322 -0
  31. package/src/components/resources/renderers/KueueProvisioningRenderers.test.tsx +67 -0
  32. package/src/components/resources/renderers/KueueProvisioningRenderers.tsx +91 -0
  33. package/src/components/resources/renderers/KueueQueueNavigation.test.tsx +69 -0
  34. package/src/components/resources/renderers/KueueQueueRenderers.test.tsx +123 -0
  35. package/src/components/resources/renderers/KueueQueueRenderers.tsx +299 -0
  36. package/src/components/resources/renderers/KueueWorkloadRenderer.test.tsx +121 -0
  37. package/src/components/resources/renderers/KueueWorkloadRenderer.tsx +230 -0
  38. package/src/components/resources/renderers/RayClusterRenderer.test.tsx +25 -0
  39. package/src/components/resources/renderers/RayClusterRenderer.tsx +93 -0
  40. package/src/components/resources/renderers/RayServiceRenderer.test.tsx +62 -0
  41. package/src/components/resources/renderers/RayServiceRenderer.tsx +114 -0
  42. package/src/components/resources/renderers/index.ts +11 -0
  43. package/src/components/resources/resource-utils-gpu-ecosystem.test.ts +103 -2
  44. package/src/components/resources/resource-utils-jobset-lws.ts +43 -9
  45. package/src/components/resources/resource-utils-kueue.ts +145 -50
  46. package/src/components/resources/resource-utils-ray.test.ts +58 -1
  47. package/src/components/resources/resource-utils-ray.ts +56 -90
  48. package/src/components/shared/ResourceRendererDispatch.test.tsx +117 -2
  49. package/src/components/shared/ResourceRendererDispatch.tsx +47 -10
  50. package/src/components/timeline/TimelineList.tsx +5 -0
  51. package/src/components/timeline/TimelineSwimlanes.test.tsx +15 -0
  52. package/src/components/timeline/TimelineSwimlanes.tsx +43 -23
  53. package/src/components/topology/K8sResourceNode.tsx +3 -3
  54. package/src/components/ui/CodeViewer.tsx +4 -0
  55. package/src/components/ui/Toast.tsx +9 -3
  56. package/src/components/ui/drawer-components.tsx +3 -1
  57. package/src/components/workload/PodList.tsx +154 -0
  58. package/src/components/workload/WorkloadView.tsx +29 -159
  59. package/src/components/workload/index.ts +11 -1
  60. package/src/components/workload/workload-logs-availability.test.ts +20 -0
  61. package/src/components/workload/workload-pod-presentation.test.ts +27 -0
  62. package/src/types/core.ts +24 -0
  63. package/src/types/gitops-tree.ts +5 -2
  64. package/src/types/scheduling.ts +71 -0
  65. package/src/utils/api-resources.test.ts +20 -1
  66. package/src/utils/api-resources.ts +26 -0
  67. package/src/utils/application-topology.test.ts +25 -0
  68. package/src/utils/application-topology.ts +2 -1
  69. package/src/utils/applications.test.ts +82 -0
  70. package/src/utils/applications.ts +62 -24
  71. package/src/utils/gitops-route.test.ts +20 -1
  72. package/src/utils/gitops-route.ts +15 -7
  73. package/src/utils/log-format.ts +5 -2
  74. package/src/utils/log-stream-error.test.ts +13 -0
  75. package/src/utils/navigation.test.ts +2 -0
  76. package/src/utils/navigation.ts +1 -1
  77. package/src/utils/resource-hierarchy.test.ts +402 -15
  78. package/src/utils/resource-hierarchy.ts +263 -178
  79. package/src/utils/topology-neighborhood.test.ts +72 -1
  80. package/src/utils/topology-neighborhood.ts +40 -18
@@ -0,0 +1,114 @@
1
+ import type { ReactNode } from 'react'
2
+ import { Activity, GitBranch } from 'lucide-react'
3
+ import type { ResourceRef } from '../../../types'
4
+ import { Badge, type BadgeSeverity } from '../../ui/Badge'
5
+ import { AlertBanner, ConditionsSection, type ConditionTone, Property, PropertyList, ResourceLink, Section } from '../../ui/drawer-components'
6
+ import { isRayObservationStale, isRayServiceConditionStale } from '../resource-utils-ray'
7
+
8
+ export interface RayServiceRendererProps {
9
+ data: any
10
+ onNavigate?: (ref: ResourceRef) => void
11
+ runtimeEvidence?: Partial<Record<'active' | 'pending', ReactNode>>
12
+ }
13
+
14
+ const visibleEntries = 20
15
+
16
+ export function serveStateSeverity(state?: string): BadgeSeverity {
17
+ if (state === 'RUNNING' || state === 'HEALTHY') return 'success'
18
+ if (state === 'DEPLOY_FAILED' || state === 'UNHEALTHY') return 'error'
19
+ if (['DEPLOYING', 'UPDATING', 'UPSCALING', 'DOWNSCALING', 'DELETING'].includes(state ?? '')) return 'warning'
20
+ return 'neutral'
21
+ }
22
+
23
+ function orderedStatuses(statuses: Record<string, any> = {}) {
24
+ const rank = (status?: string) => {
25
+ const severity = serveStateSeverity(status)
26
+ return severity === 'error' ? 0 : severity === 'neutral' ? 1 : severity === 'warning' ? 2 : 3
27
+ }
28
+ const entryRank = (entry: any) => Math.min(rank(entry.status), ...Object.values(entry.serveDeploymentStatuses ?? {}).map((deployment: any) => rank(deployment.status)))
29
+ return Object.entries(statuses).sort(([a, av], [b, bv]) => entryRank(av) - entryRank(bv) || a.localeCompare(b))
30
+ }
31
+
32
+ export function rayServiceConditionTone(condition: any): ConditionTone {
33
+ if (!['True', 'False'].includes(condition.status)) return 'unknown'
34
+ if (condition.type === 'Ready') return condition.status === 'True' ? 'ok' : 'warning'
35
+ if (['UpgradeInProgress', 'RollbackInProgress', 'Suspending'].includes(condition.type)) return condition.status === 'True' ? 'warning' : 'unknown'
36
+ return 'unknown'
37
+ }
38
+
39
+ function NativeState({ name, data }: { name: string; data: any }) {
40
+ return <>
41
+ <div className="grid grid-cols-[minmax(0,1fr)_auto] items-start gap-3">
42
+ <span className="break-words text-sm font-medium text-theme-text-primary">{name}</span>
43
+ <Badge severity={serveStateSeverity(data.status)} size="sm">{data.status || 'Not reported'}</Badge>
44
+ </div>
45
+ {data.message && <p className="mt-1 whitespace-pre-wrap break-words text-sm text-theme-text-secondary">{data.message}</p>}
46
+ </>
47
+ }
48
+
49
+ function ServeApplications({ slot }: { slot: any }) {
50
+ const apps = orderedStatuses(slot.applicationStatuses)
51
+ return <div className="mt-3 space-y-3">
52
+ <p className="text-xs text-theme-text-tertiary">Application states are snapshots reported by KubeRay, not a live Serve dashboard query.</p>
53
+ {apps.length === 0 ? <p className="text-sm text-theme-text-secondary">Application status is not reported for this revision. During a NewCluster upgrade, the active application map can be cleared while its endpoint still serves requests.</p>
54
+ : apps.slice(0, visibleEntries).map(([name, app]) => {
55
+ const deployments = orderedStatuses(app.serveDeploymentStatuses)
56
+ return <div className="card-inner min-w-0" key={name}>
57
+ <NativeState name={name} data={app} />
58
+ <div className="mt-3 space-y-2 border-t border-theme-border pt-2">
59
+ <p className="text-xs text-theme-text-tertiary">Serve deployments{deployments.length > 0 ? ` (${deployments.length})` : ''}</p>
60
+ {deployments.slice(0, visibleEntries).map(([deployment, state]) => <div className="min-w-0" key={deployment}><NativeState name={deployment} data={state} /></div>)}
61
+ {deployments.length === 0 && <p className="text-sm text-theme-text-secondary">Deployment status is not reported.</p>}
62
+ {deployments.length > visibleEntries && <p className="text-xs text-theme-text-tertiary">Showing {visibleEntries} of {deployments.length} deployments, failures first. See YAML for the full list.</p>}
63
+ </div>
64
+ </div>
65
+ })}
66
+ {apps.length > visibleEntries && <p className="text-xs text-theme-text-tertiary">Showing {visibleEntries} of {apps.length} applications, failures first. See YAML for the full list.</p>}
67
+ </div>
68
+ }
69
+
70
+ export function RayServiceRenderer({ data, onNavigate, runtimeEvidence }: RayServiceRendererProps) {
71
+ const status = data.status ?? {}
72
+ const conditions = status.conditions ?? []
73
+ const generation = data.metadata?.generation
74
+ const suspended = data.spec?.suspend === true && conditions.some((condition: any) => condition.type === 'Suspended' && condition.status === 'True')
75
+ const behind = isRayObservationStale(generation, status.observedGeneration)
76
+ const ready = conditions.find((condition: any) => condition.type === 'Ready')
77
+ const readyStale = isRayServiceConditionStale(data, ready)
78
+ const readyLabel = ready?.status === 'True' ? 'Ready' : ready?.status === 'False' ? 'Not ready' : ready ? 'Unknown' : 'Not reported'
79
+ const slots = (['active', 'pending'] as const).filter(slot => status[`${slot}ServiceStatus`]?.rayClusterName)
80
+ return <>
81
+ {behind && !suspended && <AlertBanner variant="info" title="Observed generation differs" message={`Specification generation ${generation}; controller reports observed generation ${status.observedGeneration}. KubeRay can retain this value during suspension or terminal failure; conditions below preserve its reported state.`} />}
82
+ {behind && suspended && <p className="mb-3 text-sm text-theme-text-secondary">Reconciliation is paused while suspended; the controller may retain an earlier observed generation.</p>}
83
+ <Section title="Serving and Rollout" icon={Activity} defaultExpanded>
84
+ <PropertyList>
85
+ <Property label="Proxy Readiness" value={<Badge severity={readyStale ? 'neutral' : ready?.status === 'True' ? 'success' : ready?.status === 'False' ? 'warning' : 'neutral'}>{readyLabel}{readyStale ? ' (stale)' : ''}</Badge>} />
86
+ {ready?.reason && <Property label="Readiness Reason" value={ready.reason} />}
87
+ {status.numServeEndpoints != null && <Property label="Reported Serve Endpoints" value={status.numServeEndpoints} />}
88
+ <Property label="Suspension Requested" value={data.spec?.suspend === true ? 'Yes' : 'No'} />
89
+ <Property label="Declared Upgrade Strategy" value={data.spec?.upgradeStrategy?.type || 'Not specified'} />
90
+ {conditions.filter((condition: any) => ['UpgradeInProgress', 'RollbackInProgress', 'Suspending', 'Suspended'].includes(condition.type) && condition.status === 'True').map((condition: any) => <Property key={condition.type} label={condition.type} value={<Badge severity={isRayServiceConditionStale(data, condition) ? 'neutral' : condition.type === 'Suspended' ? 'info' : 'warning'}>{condition.reason || 'True'}{isRayServiceConditionStale(data, condition) ? ' (stale)' : ''}</Badge>} />)}
91
+ </PropertyList>
92
+ {ready?.message && <p className="mt-2 whitespace-pre-wrap break-words text-sm text-theme-text-secondary">{ready.message}</p>}
93
+ <p className="mt-2 text-xs text-theme-text-tertiary">Ready means Serve proxy endpoints are reported, not that every application or revision is healthy. Endpoint counts span revisions. Suspension is complete only when the controller reports Suspended.</p>
94
+ {status.observedGeneration == null && <p className="mt-2 text-xs text-theme-text-tertiary">The controller has not reported an observed generation.</p>}
95
+ </Section>
96
+ {slots.length === 0 && <Section title="Runtime Revisions" icon={GitBranch} defaultExpanded><p className="text-sm text-theme-text-secondary">No named runtime revision is reported.</p></Section>}
97
+ {slots.map(role => {
98
+ const slot = status[`${role}ServiceStatus`]
99
+ return <Section key={role} title={`${role === 'active' ? 'Active' : 'Pending'} Revision`} icon={GitBranch} defaultExpanded>
100
+ <div className="min-w-0 break-all [&_button]:text-left"><ResourceLink kind="rayclusters" group="ray.io" namespace={data.metadata?.namespace} name={slot.rayClusterName} onNavigate={onNavigate} /></div>
101
+ {runtimeEvidence?.[role] ?? <p className="mt-2 text-sm text-theme-text-secondary">Open the RayCluster for its directly observed runtime status.</p>}
102
+ {(slot.targetCapacity != null || slot.trafficRoutedPercent != null) && <div className="mt-3">
103
+ <PropertyList>
104
+ {slot.targetCapacity != null && <Property label="Serve Target Capacity" value={`${slot.targetCapacity}%`} />}
105
+ {slot.trafficRoutedPercent != null && <Property label="Configured Traffic Share" value={`${slot.trafficRoutedPercent}%`} />}
106
+ </PropertyList>
107
+ <p className="mt-1 text-xs text-theme-text-tertiary">Target capacity scales Serve replica targets. Traffic share is configured route weight, not measured requests.</p>
108
+ </div>}
109
+ <ServeApplications slot={slot} />
110
+ </Section>
111
+ })}
112
+ <ConditionsSection conditions={conditions} getConditionTone={condition => isRayServiceConditionStale(data, condition) ? 'unknown' : rayServiceConditionTone(condition)} defaultExpanded />
113
+ </>
114
+ }
@@ -55,6 +55,7 @@ export * from './IstioPeerAuthenticationRenderer'
55
55
  export * from './IstioServiceEntryRenderer'
56
56
  export * from './IstioVirtualServiceRenderer'
57
57
  export * from './JobRenderer'
58
+ export * from './JobSetRenderer'
58
59
  export * from './karpenter-cells'
59
60
  export * from './KarpenterEC2NodeClassRenderer'
60
61
  export * from './KarpenterNodeClaimRenderer'
@@ -179,3 +180,13 @@ export * from './KyvernoGlobalContextRenderer'
179
180
  export * from './KyvernoQueueRenderers'
180
181
  export * from './CNPGObjectStoreRenderer'
181
182
  export * from './CNPGDeclarativeRenderer'
183
+
184
+ export { KueueWorkloadRenderer } from './KueueWorkloadRenderer'
185
+
186
+ export { LocalQueueRenderer, ClusterQueueRenderer } from './KueueQueueRenderers'
187
+
188
+ export { AdmissionCheckRenderer, ProvisioningRequestRenderer } from './KueueProvisioningRenderers'
189
+
190
+ export { RayServiceRenderer } from './RayServiceRenderer'
191
+
192
+ export { RayClusterRenderer } from './RayClusterRenderer'
@@ -74,13 +74,15 @@ describe('GPU ecosystem API contracts', () => {
74
74
  expect(getRayClusterStatus({
75
75
  status: {
76
76
  conditions: [{ type: 'HeadPodReady', status: 'True' }],
77
+ readyWorkerReplicas: 1,
77
78
  availableWorkerReplicas: 1,
78
79
  desiredWorkerReplicas: 2,
79
80
  },
80
- }).text).toBe('Workers 1/2')
81
+ }).text).toBe('Workers 1/2 ready')
81
82
  expect(getRayClusterStatus({
82
83
  status: {
83
84
  conditions: [{ type: 'HeadPodReady', status: 'True' }],
85
+ readyWorkerReplicas: 2,
84
86
  availableWorkerReplicas: 2,
85
87
  desiredWorkerReplicas: 2,
86
88
  },
@@ -139,7 +141,87 @@ describe('GPU ecosystem API contracts', () => {
139
141
 
140
142
  it('distinguishes pending JobSets from active work', () => {
141
143
  expect(getJobSetStatus({ status: { replicatedJobsStatus: [{ active: 0, ready: 0 }] } }).text).toBe('Pending')
142
- expect(getJobSetStatus({ status: { replicatedJobsStatus: [{ active: 1, ready: 0 }] } }).text).toBe('Running')
144
+ expect(getJobSetStatus({ status: { replicatedJobsStatus: [{ active: 1, ready: 0 }] } }).text).toBe('Active')
145
+ expect(getJobSetStatus({}).text).toBe('Unknown')
146
+ })
147
+
148
+ it('uses the same JobSet state precedence as execution context', () => {
149
+ const liveCounts = { replicatedJobsStatus: [{ active: 1, ready: 1 }] }
150
+
151
+ expect(getJobSetStatus({
152
+ spec: { suspend: true },
153
+ status: {
154
+ ...liveCounts,
155
+ terminalState: 'Failed',
156
+ conditions: [{ type: 'Completed', status: 'True' }],
157
+ },
158
+ }).text).toBe('Failed')
159
+ expect(getJobSetStatus({
160
+ status: {
161
+ ...liveCounts,
162
+ terminalState: 'Completed',
163
+ conditions: [{ type: 'Failed', status: 'True', reason: 'ReachedMaxRestarts' }],
164
+ },
165
+ }).text).toBe('Completed')
166
+ expect(getJobSetStatus({
167
+ status: {
168
+ ...liveCounts,
169
+ conditions: [
170
+ { type: 'Failed', status: 'True', reason: 'ReachedMaxRestarts' },
171
+ { type: 'Completed', status: 'True' },
172
+ ],
173
+ },
174
+ }).text).toBe('ReachedMaxRestarts')
175
+ expect(getJobSetStatus({
176
+ status: {
177
+ ...liveCounts,
178
+ conditions: [
179
+ { type: 'Completed', status: 'True' },
180
+ { type: 'Suspended', status: 'True' },
181
+ ],
182
+ },
183
+ }).text).toBe('Completed')
184
+ expect(getJobSetStatus({
185
+ status: {
186
+ ...liveCounts,
187
+ conditions: [
188
+ { type: 'Suspended', status: 'True' },
189
+ { type: 'RestartingJobSet', status: 'True' },
190
+ ],
191
+ },
192
+ }).text).toBe('Suspended')
193
+ expect(getJobSetStatus({
194
+ spec: { suspend: true },
195
+ status: {
196
+ ...liveCounts,
197
+ conditions: [{ type: 'RestartingJobSet', status: 'True' }],
198
+ },
199
+ }).text).toBe('Restarting')
200
+ expect(getJobSetStatus({
201
+ status: {
202
+ ...liveCounts,
203
+ conditions: [
204
+ { type: 'RestartingJobSet', status: 'True' },
205
+ { type: 'StartupPolicyInProgress', status: 'True' },
206
+ ],
207
+ },
208
+ }).text).toBe('Starting')
209
+ expect(getJobSetStatus({
210
+ status: {
211
+ ...liveCounts,
212
+ conditions: [{ type: 'StartupPolicyInProgress', status: 'True' }],
213
+ },
214
+ }).text).toBe('Starting')
215
+ })
216
+
217
+ it('reports JobSet restart recovery and terminal-state fallbacks', () => {
218
+ expect(
219
+ getJobSetStatus({
220
+ status: { conditions: [{ type: 'RestartingJobSet', status: 'True' }], replicatedJobsStatus: [] },
221
+ }).text,
222
+ ).toBe('Restarting')
223
+ expect(getJobSetStatus({ status: { terminalState: 'Completed' } }).text).toBe('Completed')
224
+ expect(getJobSetStatus({ status: { terminalState: 'Failed' } }).text).toBe('Failed')
143
225
  })
144
226
 
145
227
  it('surfaces an unavailable LeaderWorkerSet before progress', () => {
@@ -234,3 +316,22 @@ describe('GPU ecosystem API contracts', () => {
234
316
  })).toBe('Worker 2/4 (2 failed)')
235
317
  })
236
318
  })
319
+
320
+ describe('JobSet status evidence boundaries', () => {
321
+ it('does not turn pending Pods or child failures into root health or outcomes', () => {
322
+ for (const counts of [{ active: 1, ready: 0 }, { failed: 1 }, { succeeded: 1 }]) {
323
+ expect(getJobSetStatus({ status: { replicatedJobsStatus: [counts] } })).toMatchObject({ text: 'Active', level: 'neutral' })
324
+ }
325
+ })
326
+ it('requires observed counts for pending and preserves unknown native states', () => {
327
+ expect(getJobSetStatus({ status: {} }).text).toBe('Unknown')
328
+ expect(getJobSetStatus({ status: { replicatedJobsStatus: [{ active: 0 }] } }).text).toBe('Pending')
329
+ expect(getJobSetStatus({ status: { terminalState: 'Future', conditions: [{ type: 'Completed', status: 'True' }] } }).text).toBe('Unknown')
330
+ })
331
+ it('separates suspension intent and in-order resume evidence', () => {
332
+ expect(getJobSetStatus({ spec: { suspend: true }, status: { replicatedJobsStatus: [{ active: 1 }] } }).text).toBe('Active')
333
+ const conditions = [{ type: 'Suspended', status: 'True' }, { type: 'StartupPolicyInProgress', status: 'True' }]
334
+ expect(getJobSetStatus({ spec: { suspend: false }, status: { conditions } }).text).toBe('Starting')
335
+ expect(getJobSetStatus({ spec: { suspend: true }, status: { conditions } }).text).toBe('Suspended')
336
+ })
337
+ })
@@ -3,6 +3,12 @@
3
3
  import type { StatusBadge } from './resource-utils'
4
4
  import { healthColors } from './resource-utils'
5
5
 
6
+ export const JOBSET_API_VERSION = 'jobset.x-k8s.io/v1alpha2'
7
+
8
+ export function isJobSetV1Alpha2(resource: any): boolean {
9
+ return resource?.kind === 'JobSet' && resource?.apiVersion === JOBSET_API_VERSION
10
+ }
11
+
6
12
  // ============================================================================
7
13
  // LEADERWORKERSET UTILITIES
8
14
  // ============================================================================
@@ -61,28 +67,56 @@ export function getJobSetStatus(resource: any): StatusBadge {
61
67
  const conditions = resource.status?.conditions || []
62
68
 
63
69
  const failedCond = conditions.find((c: any) => c.type === 'Failed')
70
+ const completedCond = conditions.find((c: any) => c.type === 'Completed')
71
+
72
+ if (resource.status?.terminalState === 'Failed') {
73
+ return { text: failedCond?.status === 'True' ? failedCond.reason || 'Failed' : 'Failed', color: healthColors.unhealthy, level: 'unhealthy' }
74
+ }
75
+
76
+ if (resource.status?.terminalState === 'Completed') {
77
+ return { text: 'Completed', color: healthColors.neutral, level: 'neutral' }
78
+ }
79
+
80
+ if (resource.status?.terminalState) {
81
+ return { text: 'Unknown', color: healthColors.unknown, level: 'unknown' }
82
+ }
83
+
64
84
  if (failedCond?.status === 'True') {
65
85
  return { text: failedCond.reason || 'Failed', color: healthColors.unhealthy, level: 'unhealthy' }
66
86
  }
67
87
 
68
- const completedCond = conditions.find((c: any) => c.type === 'Completed')
69
88
  if (completedCond?.status === 'True') {
70
89
  return { text: 'Completed', color: healthColors.neutral, level: 'neutral' }
71
90
  }
72
91
 
92
+ const startupCond = conditions.find((c: any) => c.type === 'StartupPolicyInProgress')
93
+ // In-order resume retains Suspended until every role starts.
94
+ if (startupCond?.status === 'True' && resource.spec?.suspend !== true) {
95
+ return { text: 'Starting', color: healthColors.degraded, level: 'degraded' }
96
+ }
97
+
73
98
  const suspendedCond = conditions.find((c: any) => c.type === 'Suspended')
74
- if (suspendedCond?.status === 'True' || resource.spec?.suspend === true) {
99
+ if (suspendedCond?.status === 'True') {
75
100
  return { text: 'Suspended', color: healthColors.neutral, level: 'neutral' }
76
101
  }
77
102
 
78
- // Running only when child jobs are actually live — a fresh JobSet carries a
79
- // minimal status (no conditions, zeroed counts) before reconciliation.
80
- const live = sumReplicatedJobsField(resource, 'active') + sumReplicatedJobsField(resource, 'ready')
81
- if (live > 0) {
82
- return { text: 'Running', color: healthColors.healthy, level: 'healthy' }
103
+ const restartingCond = conditions.find((c: any) => c.type === 'RestartingJobSet')
104
+ if (restartingCond?.status === 'True') {
105
+ return { text: 'Restarting', color: healthColors.degraded, level: 'degraded' }
83
106
  }
84
- if (resource.status) {
85
- return { text: 'Pending', color: healthColors.neutral, level: 'neutral' }
107
+
108
+ if (startupCond?.status === 'True') {
109
+ return { text: 'Starting', color: healthColors.degraded, level: 'degraded' }
110
+ }
111
+
112
+ const statuses = resource.status?.replicatedJobsStatus
113
+ if (Array.isArray(statuses)) {
114
+ // Child activity includes Pending Pods and cleanup, not application health.
115
+ const activity = ['active', 'ready', 'succeeded', 'failed', 'suspended']
116
+ .some((field) => sumReplicatedJobsField(resource, field) > 0)
117
+ return activity
118
+ ? { text: 'Active', color: healthColors.neutral, level: 'neutral' }
119
+ : { text: 'Pending', color: healthColors.neutral, level: 'neutral' }
86
120
  }
87
121
 
88
122
  return { text: 'Unknown', color: healthColors.unknown, level: 'unknown' }
@@ -5,6 +5,14 @@ import { healthColors } from './resource-utils'
5
5
 
6
6
  const failedFinishedReasons = new Set(['Failed', 'FailedToStart', 'OutOfSync', 'OwnerNotFound'])
7
7
 
8
+ export function isKueueConditionStale(condition: { observedGeneration?: number } | undefined, generation?: number): boolean {
9
+ return !!condition?.observedGeneration && !!generation && condition.observedGeneration < generation
10
+ }
11
+
12
+ export function isKueueWorkloadFailureReason(reason: string): boolean {
13
+ return failedFinishedReasons.has(reason)
14
+ }
15
+
8
16
  // ============================================================================
9
17
  // SHARED HELPERS
10
18
  // ============================================================================
@@ -24,6 +32,88 @@ function activeConditionStatus(resource: any): StatusBadge {
24
32
  return { text: 'Unknown', color: healthColors.unknown, level: 'unknown' }
25
33
  }
26
34
 
35
+ export function isKueueQueueResource(resource: any): boolean {
36
+ return resource?.apiVersion === 'kueue.x-k8s.io/v1beta1' || resource?.apiVersion === 'kueue.x-k8s.io/v1beta2'
37
+ }
38
+
39
+ export function getKueueQueueCondition(resource: any): any {
40
+ return findCondition(resource, 'Active')
41
+ }
42
+
43
+ function queueConditionStatus(resource: any): StatusBadge {
44
+ const active = getKueueQueueCondition(resource)
45
+ if (isKueueConditionStale(active, resource?.metadata?.generation)) {
46
+ return { text: `${active.status === 'True' ? 'Active' : active.reason || 'Unknown'} (stale)`, color: healthColors.unknown, level: 'unknown' }
47
+ }
48
+ if (active?.status === 'False' && active.reason === 'Stopped') {
49
+ return { text: 'Stopped', color: healthColors.neutral, level: 'neutral' }
50
+ }
51
+ return activeConditionStatus(resource)
52
+ }
53
+
54
+ type Quantity = string | number
55
+ interface QueueResourceUsage {
56
+ name: string
57
+ total?: Quantity
58
+ borrowed?: Quantity
59
+ }
60
+ interface QueueFlavorUsage {
61
+ name: string
62
+ resources?: QueueResourceUsage[]
63
+ }
64
+ export interface KueueQuotaRow {
65
+ flavor: string
66
+ resource: string
67
+ configured: boolean
68
+ nominal?: Quantity
69
+ borrowingLimit?: Quantity
70
+ lendingLimit?: Quantity
71
+ reserved?: Quantity
72
+ used?: Quantity
73
+ borrowedReservation?: Quantity
74
+ borrowedUsage?: Quantity
75
+ }
76
+
77
+ export function getKueueQueueQuotaRows(resource: any, clusterQueue: boolean): KueueQuotaRow[] {
78
+ const rows = new Map<string, KueueQuotaRow>()
79
+ const rowFor = (flavor: string, name: string) => {
80
+ const key = JSON.stringify([flavor, name])
81
+ let row = rows.get(key)
82
+ if (!row) {
83
+ row = { flavor, resource: name, configured: false }
84
+ rows.set(key, row)
85
+ }
86
+ return row
87
+ }
88
+ if (clusterQueue) {
89
+ for (const group of resource?.spec?.resourceGroups ?? []) {
90
+ for (const flavor of group.flavors ?? []) {
91
+ for (const quota of flavor.resources ?? []) {
92
+ Object.assign(rowFor(flavor.name, quota.name), {
93
+ configured: true, nominal: quota.nominalQuota,
94
+ borrowingLimit: quota.borrowingLimit, lendingLimit: quota.lendingLimit,
95
+ })
96
+ }
97
+ }
98
+ }
99
+ }
100
+ const reservation: QueueFlavorUsage[] = resource?.status?.flavorsReservation ?? []
101
+ const usage: QueueFlavorUsage[] = !clusterQueue && resource?.apiVersion === 'kueue.x-k8s.io/v1beta1'
102
+ ? resource?.status?.flavorUsage ?? []
103
+ : resource?.status?.flavorsUsage ?? []
104
+ for (const flavor of reservation) {
105
+ for (const entry of flavor.resources ?? []) {
106
+ Object.assign(rowFor(flavor.name, entry.name), { reserved: entry.total, borrowedReservation: entry.borrowed })
107
+ }
108
+ }
109
+ for (const flavor of usage) {
110
+ for (const entry of flavor.resources ?? []) {
111
+ Object.assign(rowFor(flavor.name, entry.name), { used: entry.total, borrowedUsage: entry.borrowed })
112
+ }
113
+ }
114
+ return [...rows.values()]
115
+ }
116
+
27
117
  function formatWorkloadCount(value: any): string {
28
118
  return typeof value === 'number' ? String(value) : '-'
29
119
  }
@@ -33,7 +123,7 @@ function formatWorkloadCount(value: any): string {
33
123
  // ============================================================================
34
124
 
35
125
  export function getClusterQueueStatus(resource: any): StatusBadge {
36
- return activeConditionStatus(resource)
126
+ return queueConditionStatus(resource)
37
127
  }
38
128
 
39
129
  export function getClusterQueueCohort(resource: any): string {
@@ -64,7 +154,7 @@ export function getClusterQueueFlavors(resource: any): string {
64
154
  // ============================================================================
65
155
 
66
156
  export function getLocalQueueStatus(resource: any): StatusBadge {
67
- return activeConditionStatus(resource)
157
+ return queueConditionStatus(resource)
68
158
  }
69
159
 
70
160
  export function getLocalQueueClusterQueue(resource: any): string {
@@ -83,36 +173,30 @@ export function getLocalQueueAdmittedWorkloads(resource: any): string {
83
173
  // KUEUE WORKLOAD UTILITIES
84
174
  // ============================================================================
85
175
 
86
- export function getKueueWorkloadStatus(resource: any): StatusBadge {
87
- const finished = findCondition(resource, 'Finished')
88
- if (finished?.status === 'True') {
89
- if (failedFinishedReasons.has(finished.reason)) {
90
- return { text: finished.reason, color: healthColors.unhealthy, level: 'unhealthy' }
91
- }
92
- return { text: 'Finished', color: healthColors.neutral, level: 'neutral' }
93
- }
94
-
95
- const evicted = findCondition(resource, 'Evicted')
96
- if (evicted?.status === 'True') {
97
- return { text: 'Evicted', color: healthColors.degraded, level: 'degraded' }
98
- }
99
-
100
- const preempted = findCondition(resource, 'Preempted')
101
- if (preempted?.status === 'True') {
102
- return { text: 'Preempted', color: healthColors.degraded, level: 'degraded' }
103
- }
104
-
105
- const admitted = findCondition(resource, 'Admitted')
106
- if (admitted?.status === 'True') {
107
- return { text: 'Admitted', color: healthColors.healthy, level: 'healthy' }
176
+ export function getKueueWorkloadStatusCondition(resource: any): any {
177
+ for (const type of ['Finished', 'Evicted', 'Preempted', 'Admitted', 'QuotaReserved']) {
178
+ const condition = findCondition(resource, type)
179
+ if (condition?.status === 'True') return condition
108
180
  }
181
+ }
109
182
 
110
- const quotaReserved = findCondition(resource, 'QuotaReserved')
111
- if (quotaReserved?.status === 'True') {
112
- return { text: 'QuotaReserved', color: healthColors.neutral, level: 'neutral' }
183
+ export function getKueueWorkloadStatus(resource: any): StatusBadge {
184
+ const condition = getKueueWorkloadStatusCondition(resource)
185
+ switch (condition?.type) {
186
+ case 'Finished':
187
+ return isKueueWorkloadFailureReason(condition.reason)
188
+ ? { text: condition.reason, color: healthColors.unhealthy, level: 'unhealthy' }
189
+ : { text: 'Finished', color: healthColors.neutral, level: 'neutral' }
190
+ case 'Evicted':
191
+ case 'Preempted':
192
+ return { text: condition.type, color: healthColors.degraded, level: 'degraded' }
193
+ case 'Admitted':
194
+ return { text: 'Admitted', color: healthColors.healthy, level: 'healthy' }
195
+ case 'QuotaReserved':
196
+ return { text: 'QuotaReserved', color: healthColors.neutral, level: 'neutral' }
197
+ default:
198
+ return { text: 'Pending', color: healthColors.neutral, level: 'neutral' }
113
199
  }
114
-
115
- return { text: 'Pending', color: healthColors.neutral, level: 'neutral' }
116
200
  }
117
201
 
118
202
  export function getKueueWorkloadQueueName(resource: any): string {
@@ -151,6 +235,10 @@ export function getResourceFlavorTaintCount(resource: any): number {
151
235
  // ============================================================================
152
236
 
153
237
  export function getAdmissionCheckStatus(resource: any): StatusBadge {
238
+ const condition = findCondition(resource, 'Active')
239
+ if (isKueueConditionStale(condition, resource?.metadata?.generation)) {
240
+ return { text: 'Stale', color: healthColors.unknown, level: 'unknown' }
241
+ }
154
242
  return activeConditionStatus(resource)
155
243
  }
156
244
 
@@ -162,33 +250,40 @@ export function getAdmissionCheckControllerName(resource: any): string {
162
250
  // CLUSTER AUTOSCALER PROVISIONINGREQUEST UTILITIES
163
251
  // ============================================================================
164
252
 
165
- export function getProvisioningRequestStatus(resource: any): StatusBadge {
166
- const failed = findCondition(resource, 'Failed')
167
- if (failed?.status === 'True') {
168
- return { text: 'Failed', color: healthColors.unhealthy, level: 'unhealthy' }
169
- }
170
-
171
- const capacityRevoked = findCondition(resource, 'CapacityRevoked')
172
- if (capacityRevoked?.status === 'True') {
173
- return { text: 'CapacityRevoked', color: healthColors.degraded, level: 'degraded' }
253
+ export function getProvisioningRequestStatusCondition(resource: any): any {
254
+ for (const type of ['Failed', 'CapacityRevoked', 'BookingExpired', 'Provisioned', 'Accepted']) {
255
+ const condition = findCondition(resource, type)
256
+ if (condition?.status === 'True') return condition
174
257
  }
258
+ }
175
259
 
176
- const bookingExpired = findCondition(resource, 'BookingExpired')
177
- if (bookingExpired?.status === 'True') {
178
- return { text: 'BookingExpired', color: healthColors.degraded, level: 'degraded' }
260
+ export function getProvisioningRequestMessage(resource: any): string | undefined {
261
+ const condition = getProvisioningRequestStatusCondition(resource)
262
+ if (condition?.type === 'Accepted') {
263
+ const progress = findCondition(resource, 'Provisioned')
264
+ if (progress?.status === 'False' && !isKueueConditionStale(progress, resource?.metadata?.generation) && progress.message) return progress.message
179
265
  }
266
+ return condition?.message
267
+ }
180
268
 
181
- const provisioned = findCondition(resource, 'Provisioned')
182
- if (provisioned?.status === 'True') {
183
- return { text: 'Provisioned', color: healthColors.healthy, level: 'healthy' }
269
+ export function getProvisioningRequestStatus(resource: any): StatusBadge {
270
+ const condition = getProvisioningRequestStatusCondition(resource)
271
+ if (isKueueConditionStale(condition, resource?.metadata?.generation)) {
272
+ return { text: `${condition.type} (stale)`, color: healthColors.unknown, level: 'unknown' }
184
273
  }
185
-
186
- const accepted = findCondition(resource, 'Accepted')
187
- if (accepted?.status === 'True') {
188
- return { text: 'Accepted', color: healthColors.neutral, level: 'neutral' }
274
+ switch (condition?.type) {
275
+ case 'Failed':
276
+ case 'CapacityRevoked':
277
+ return { text: condition.type, color: healthColors.unhealthy, level: 'unhealthy' }
278
+ case 'BookingExpired':
279
+ return { text: 'BookingExpired', color: healthColors.neutral, level: 'neutral' }
280
+ case 'Provisioned':
281
+ return { text: 'Provisioned', color: healthColors.healthy, level: 'healthy' }
282
+ case 'Accepted':
283
+ return { text: 'Accepted', color: healthColors.neutral, level: 'neutral' }
284
+ default:
285
+ return { text: 'Unknown', color: healthColors.unknown, level: 'unknown' }
189
286
  }
190
-
191
- return { text: 'Pending', color: healthColors.neutral, level: 'neutral' }
192
287
  }
193
288
 
194
289
  export function getProvisioningRequestClassName(resource: any): string {