@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.
- package/package.json +3 -2
- package/src/components/applications/ApplicationDetail.tsx +24 -11
- package/src/components/audit/AuditAlerts.tsx +1 -3
- package/src/components/audit/AuditCoveragePresentation.test.tsx +48 -2
- package/src/components/checks/ChecksView.test.tsx +55 -0
- package/src/components/checks/ChecksView.tsx +38 -27
- package/src/components/dock/NodeTerminalTab.test.tsx +89 -0
- package/src/components/dock/NodeTerminalTab.tsx +51 -47
- package/src/components/gitops/GitOpsHealthSourceNotice.tsx +17 -9
- package/src/components/gitops/detail-helpers.test.ts +32 -0
- package/src/components/gitops/detail-helpers.ts +21 -0
- package/src/components/gitops/health-provenance.test.ts +4 -3
- package/src/components/gitops/index.ts +2 -1
- package/src/components/gitops/insights/ArgoResourceDiff.tsx +131 -54
- package/src/components/issues/IssuesView.tsx +6 -0
- package/src/components/issues/ResourceIssuesSection.test.tsx +36 -0
- package/src/components/issues/diagnostic.ts +4 -0
- package/src/components/logs/LogCore.tsx +3 -3
- package/src/components/logs/WorkloadLogsViewer.tsx +41 -11
- package/src/components/logs/useLogBuffer.ts +1 -0
- package/src/components/logs/useLogStream.ts +1 -2
- package/src/components/resources/KueueAdmissionSection.test.tsx +95 -0
- package/src/components/resources/KueueAdmissionSection.tsx +87 -0
- package/src/components/resources/ResourcesView.default-sort.test.ts +26 -0
- package/src/components/resources/ResourcesView.tsx +91 -16
- package/src/components/resources/curated-column-ownership.test.ts +45 -0
- package/src/components/resources/renderers/CAPIClusterRenderer.tsx +4 -3
- package/src/components/resources/renderers/ContainerEnvironmentSection.tsx +10 -3
- package/src/components/resources/renderers/JobSetRenderer.test.tsx +280 -0
- package/src/components/resources/renderers/JobSetRenderer.tsx +322 -0
- package/src/components/resources/renderers/KueueProvisioningRenderers.test.tsx +67 -0
- package/src/components/resources/renderers/KueueProvisioningRenderers.tsx +91 -0
- package/src/components/resources/renderers/KueueQueueNavigation.test.tsx +69 -0
- package/src/components/resources/renderers/KueueQueueRenderers.test.tsx +123 -0
- package/src/components/resources/renderers/KueueQueueRenderers.tsx +299 -0
- package/src/components/resources/renderers/KueueWorkloadRenderer.test.tsx +121 -0
- package/src/components/resources/renderers/KueueWorkloadRenderer.tsx +230 -0
- package/src/components/resources/renderers/RayClusterRenderer.test.tsx +25 -0
- package/src/components/resources/renderers/RayClusterRenderer.tsx +93 -0
- package/src/components/resources/renderers/RayServiceRenderer.test.tsx +62 -0
- package/src/components/resources/renderers/RayServiceRenderer.tsx +114 -0
- package/src/components/resources/renderers/index.ts +11 -0
- package/src/components/resources/resource-utils-gpu-ecosystem.test.ts +103 -2
- package/src/components/resources/resource-utils-jobset-lws.ts +43 -9
- package/src/components/resources/resource-utils-kueue.ts +145 -50
- package/src/components/resources/resource-utils-ray.test.ts +58 -1
- package/src/components/resources/resource-utils-ray.ts +56 -90
- package/src/components/shared/ResourceRendererDispatch.test.tsx +117 -2
- package/src/components/shared/ResourceRendererDispatch.tsx +47 -10
- package/src/components/timeline/TimelineList.tsx +5 -0
- package/src/components/timeline/TimelineSwimlanes.test.tsx +15 -0
- package/src/components/timeline/TimelineSwimlanes.tsx +43 -23
- package/src/components/topology/K8sResourceNode.tsx +3 -3
- package/src/components/ui/CodeViewer.tsx +4 -0
- package/src/components/ui/Toast.tsx +9 -3
- package/src/components/ui/drawer-components.tsx +3 -1
- package/src/components/workload/PodList.tsx +154 -0
- package/src/components/workload/WorkloadView.tsx +29 -159
- package/src/components/workload/index.ts +11 -1
- package/src/components/workload/workload-logs-availability.test.ts +20 -0
- package/src/components/workload/workload-pod-presentation.test.ts +27 -0
- package/src/types/core.ts +24 -0
- package/src/types/gitops-tree.ts +5 -2
- package/src/types/scheduling.ts +71 -0
- package/src/utils/api-resources.test.ts +20 -1
- package/src/utils/api-resources.ts +26 -0
- package/src/utils/application-topology.test.ts +25 -0
- package/src/utils/application-topology.ts +2 -1
- package/src/utils/applications.test.ts +82 -0
- package/src/utils/applications.ts +62 -24
- package/src/utils/gitops-route.test.ts +20 -1
- package/src/utils/gitops-route.ts +15 -7
- package/src/utils/log-format.ts +5 -2
- package/src/utils/log-stream-error.test.ts +13 -0
- package/src/utils/navigation.test.ts +2 -0
- package/src/utils/navigation.ts +1 -1
- package/src/utils/resource-hierarchy.test.ts +402 -15
- package/src/utils/resource-hierarchy.ts +263 -178
- package/src/utils/topology-neighborhood.test.ts +72 -1
- package/src/utils/topology-neighborhood.ts +40 -18
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { renderToString } from 'react-dom/server'
|
|
3
|
+
import { JobSetRenderer, getJobSetConditionTone } from './JobSetRenderer'
|
|
4
|
+
|
|
5
|
+
function render(data: any): string {
|
|
6
|
+
return renderToString(<JobSetRenderer data={data} />).replace(/<!-- -->/g, '')
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function currentJobSet(): any {
|
|
10
|
+
return {
|
|
11
|
+
apiVersion: 'jobset.x-k8s.io/v1alpha2',
|
|
12
|
+
kind: 'JobSet',
|
|
13
|
+
metadata: { name: 'training', namespace: 'ml' },
|
|
14
|
+
spec: {
|
|
15
|
+
suspend: false,
|
|
16
|
+
replicatedJobs: [
|
|
17
|
+
{
|
|
18
|
+
name: 'leader',
|
|
19
|
+
groupName: 'control',
|
|
20
|
+
replicas: 1,
|
|
21
|
+
template: { spec: { parallelism: 1, completions: 1 } },
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: 'workers',
|
|
25
|
+
groupName: 'training',
|
|
26
|
+
replicas: 3,
|
|
27
|
+
dependsOn: [{ name: 'leader', status: 'Ready' }],
|
|
28
|
+
template: {
|
|
29
|
+
spec: {
|
|
30
|
+
parallelism: 2,
|
|
31
|
+
completions: 2,
|
|
32
|
+
completionMode: 'Indexed',
|
|
33
|
+
backoffLimit: 1,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
successPolicy: { operator: 'Any', targetReplicatedJobs: ['workers'] },
|
|
39
|
+
failurePolicy: {
|
|
40
|
+
maxRestarts: 3,
|
|
41
|
+
restartStrategy: 'Recreate',
|
|
42
|
+
rules: [
|
|
43
|
+
{
|
|
44
|
+
name: 'restart-workers',
|
|
45
|
+
action: 'RestartJob',
|
|
46
|
+
targetReplicatedJobs: ['workers'],
|
|
47
|
+
onJobFailureReasons: ['BackoffLimitExceeded'],
|
|
48
|
+
onJobFailureMessagePatterns: ['temporary.*'],
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
coordinator: { replicatedJob: 'leader', jobIndex: 0, podIndex: 0 },
|
|
53
|
+
network: {
|
|
54
|
+
subdomain: 'training-workers',
|
|
55
|
+
enableDNSHostnames: true,
|
|
56
|
+
publishNotReadyAddresses: false,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
status: {
|
|
60
|
+
restarts: 1,
|
|
61
|
+
restartsCountTowardsMax: 1,
|
|
62
|
+
replicatedJobsStatus: [
|
|
63
|
+
{
|
|
64
|
+
name: 'leader',
|
|
65
|
+
ready: 1,
|
|
66
|
+
active: 1,
|
|
67
|
+
succeeded: 0,
|
|
68
|
+
failed: 0,
|
|
69
|
+
suspended: 0,
|
|
70
|
+
jobRestarts: [0],
|
|
71
|
+
jobRestartsCountTowardsMax: [0],
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: 'workers',
|
|
75
|
+
ready: 2,
|
|
76
|
+
active: 3,
|
|
77
|
+
succeeded: 0,
|
|
78
|
+
failed: 0,
|
|
79
|
+
suspended: 0,
|
|
80
|
+
jobRestarts: [0, 1, 0],
|
|
81
|
+
jobRestartsCountTowardsMax: [0, 1, 0],
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
conditions: [{ type: 'Suspended', status: 'False', reason: 'Resumed' }],
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
describe('JobSetRenderer', () => {
|
|
90
|
+
it('shows each replicated role without flattening away Job indexes or controller counts', () => {
|
|
91
|
+
const html = render(currentJobSet())
|
|
92
|
+
|
|
93
|
+
expect(html).toContain('Replicated jobs (2)')
|
|
94
|
+
expect(html).toContain('leader')
|
|
95
|
+
expect(html).toContain('workers')
|
|
96
|
+
expect(html).toContain('Group training')
|
|
97
|
+
expect(html).toContain('0–2')
|
|
98
|
+
expect(html).toContain('Parallelism / Job')
|
|
99
|
+
expect(html).toContain('Indexed')
|
|
100
|
+
expect(html).toContain('Ready</span> <span')
|
|
101
|
+
expect(html).toContain('>2/3</span>')
|
|
102
|
+
expect(html).toContain('Starts after')
|
|
103
|
+
expect(html).toContain('leader · Ready')
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('preserves current success and ordered failure-policy semantics', () => {
|
|
107
|
+
const html = render(currentJobSet())
|
|
108
|
+
|
|
109
|
+
expect(html).toContain('Completion and restart policies')
|
|
110
|
+
expect(html).toContain('Any')
|
|
111
|
+
expect(html).toContain('across workers')
|
|
112
|
+
expect(html).toContain('Restart limit')
|
|
113
|
+
expect(html).toContain('RestartJobSet')
|
|
114
|
+
expect(html).toContain('1. restart-workers')
|
|
115
|
+
expect(html).toContain('RestartJob')
|
|
116
|
+
expect(html).toContain('BackoffLimitExceeded')
|
|
117
|
+
expect(html).toContain('temporary.*')
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('reports native restart counters separately without assuming the RestartJob feature gate', () => {
|
|
121
|
+
const html = render(currentJobSet())
|
|
122
|
+
|
|
123
|
+
expect(html).toContain('Global restarts counted toward limit')
|
|
124
|
+
expect(html).toContain('Individual restarts counted toward limit')
|
|
125
|
+
expect(html).toContain('1 (2 of 2 roles reported)')
|
|
126
|
+
expect(html).not.toContain('Restart budget used')
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('distinguishes absent restart observations from a reported zero and partial role coverage', () => {
|
|
130
|
+
const data = currentJobSet()
|
|
131
|
+
data.status = {}
|
|
132
|
+
expect(render(data)).toContain('Not reported')
|
|
133
|
+
expect(render(data)).not.toContain('0 (2 of 2 roles reported)')
|
|
134
|
+
data.status = { restarts: 0, replicatedJobsStatus: [{ name: 'leader' }] }
|
|
135
|
+
expect(render(data)).toContain('0 (1 of 2 roles reported)')
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('keeps controller counts separate from the bounded member list and preserves missing status', () => {
|
|
139
|
+
const data = currentJobSet()
|
|
140
|
+
data.status.replicatedJobsStatus = [data.status.replicatedJobsStatus[0]]
|
|
141
|
+
const html = renderToString(<JobSetRenderer data={data} mode="overview" shownMemberCounts={new Map([['workers', 1]])} onSelectRole={() => {}} />).replace(/<!-- -->/g, '')
|
|
142
|
+
expect(html).toContain('Role progress')
|
|
143
|
+
expect(html).toContain('leader failed: 0')
|
|
144
|
+
expect(html).toContain('workers ready: not reported')
|
|
145
|
+
expect(html).toContain('Status not reported')
|
|
146
|
+
expect(html).toContain('Inspect members (1 shown)')
|
|
147
|
+
expect(html).toContain('leader Ready')
|
|
148
|
+
expect(html).not.toContain('Parallelism / Job')
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('explains external ownership without claiming that remote execution succeeded', () => {
|
|
152
|
+
const data = currentJobSet()
|
|
153
|
+
data.spec.managedBy = 'kueue.x-k8s.io/multikueue'
|
|
154
|
+
expect(render(data)).toContain('Execution may occur on another cluster')
|
|
155
|
+
data.spec.managedBy = 'jobset.sigs.k8s.io/jobset-controller'
|
|
156
|
+
expect(render(data)).not.toContain('does not create Jobs')
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('keeps zero-valued coordinator indexes and explicit network choices visible', () => {
|
|
160
|
+
const html = render(currentJobSet())
|
|
161
|
+
|
|
162
|
+
expect(html).toContain('Coordination and network')
|
|
163
|
+
expect(html).toContain('Coordinator Job index')
|
|
164
|
+
expect(html).toContain('Coordinator Pod index')
|
|
165
|
+
expect(html).toContain('training-workers')
|
|
166
|
+
expect(html).toContain('Enabled')
|
|
167
|
+
expect(html).toContain('No')
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('does not turn missing role status into zero progress', () => {
|
|
171
|
+
const data = currentJobSet()
|
|
172
|
+
data.status.replicatedJobsStatus = []
|
|
173
|
+
const html = render(data)
|
|
174
|
+
|
|
175
|
+
expect(html).toContain('Controller status has not been reported for this role.')
|
|
176
|
+
expect(html).not.toContain('Ready</span> <span')
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('uses the API default of one Job when replicas is omitted and preserves explicit zero', () => {
|
|
180
|
+
const defaulted = currentJobSet()
|
|
181
|
+
defaulted.spec.replicatedJobs = [{ name: 'defaulted', template: { spec: {} } }]
|
|
182
|
+
defaulted.status.replicatedJobsStatus = [{ name: 'defaulted', ready: 1 }]
|
|
183
|
+
const defaultedHtml = render(defaulted)
|
|
184
|
+
|
|
185
|
+
expect(defaultedHtml).toMatch(/Jobs<\/span><span[^>]*>1<\/span>/)
|
|
186
|
+
expect(defaultedHtml).toMatch(/Job indexes<\/span><span[^>]*>0<\/span>/)
|
|
187
|
+
expect(defaultedHtml).toContain('>1/1</span>')
|
|
188
|
+
|
|
189
|
+
const disabled = currentJobSet()
|
|
190
|
+
disabled.spec.replicatedJobs = [{ name: 'disabled', replicas: 0, template: { spec: {} } }]
|
|
191
|
+
disabled.status.replicatedJobsStatus = [{ name: 'disabled', ready: 0 }]
|
|
192
|
+
const disabledHtml = render(disabled)
|
|
193
|
+
|
|
194
|
+
expect(disabledHtml).toMatch(/Jobs<\/span><span[^>]*>0<\/span>/)
|
|
195
|
+
expect(disabledHtml).toMatch(/Job indexes<\/span><span[^>]*>None<\/span>/)
|
|
196
|
+
expect(disabledHtml).toContain('>0/0</span>')
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it("raises only the JobSet controller's own terminal failure", () => {
|
|
200
|
+
const data = currentJobSet()
|
|
201
|
+
data.status.terminalState = 'Failed'
|
|
202
|
+
data.status.conditions = [
|
|
203
|
+
{
|
|
204
|
+
type: 'Failed',
|
|
205
|
+
status: 'True',
|
|
206
|
+
reason: 'RetriesExhausted',
|
|
207
|
+
message: 'Global restart limit reached',
|
|
208
|
+
},
|
|
209
|
+
]
|
|
210
|
+
const html = render(data)
|
|
211
|
+
|
|
212
|
+
expect(html).toContain('Issue Detected')
|
|
213
|
+
expect(html).toContain('Global restart limit reached')
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it('does not raise a stale failure condition after terminal completion', () => {
|
|
217
|
+
const data = currentJobSet()
|
|
218
|
+
data.status.terminalState = 'Completed'
|
|
219
|
+
data.status.conditions = [{ type: 'Failed', status: 'True', message: 'Stale failure' }]
|
|
220
|
+
const html = render(data)
|
|
221
|
+
|
|
222
|
+
expect(html).toContain('Completed')
|
|
223
|
+
expect(html).not.toContain('Issue Detected')
|
|
224
|
+
expect(html).toContain('Stale failure')
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it('renders safely when the controller has not populated optional fields', () => {
|
|
228
|
+
const html = render({
|
|
229
|
+
apiVersion: 'jobset.x-k8s.io/v1alpha2',
|
|
230
|
+
kind: 'JobSet',
|
|
231
|
+
metadata: { name: 'new' },
|
|
232
|
+
spec: {},
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
expect(html).toContain('JobSet status')
|
|
236
|
+
expect(html).toContain('Unknown')
|
|
237
|
+
expect(html).not.toContain('Replicated jobs (')
|
|
238
|
+
expect(html).not.toContain('Coordination and network')
|
|
239
|
+
})
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
describe('JobSet condition polarity', () => {
|
|
243
|
+
it('treats normal incomplete work as unknown rather than failing', () => {
|
|
244
|
+
expect(getJobSetConditionTone({ type: 'Completed', status: 'False' })).toBe('unknown')
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
it('distinguishes intentional suspension from failure', () => {
|
|
248
|
+
expect(getJobSetConditionTone({ type: 'Suspended', status: 'True' })).toBe('warning')
|
|
249
|
+
expect(getJobSetConditionTone({ type: 'Failed', status: 'True' })).toBe('fail')
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
it('does not mark normal restart and startup transitions as failures', () => {
|
|
253
|
+
expect(getJobSetConditionTone({ type: 'RestartingJobSet', status: 'True' })).toBe('warning')
|
|
254
|
+
expect(getJobSetConditionTone({ type: 'RestartingJobSet', status: 'False' })).toBe('ok')
|
|
255
|
+
expect(
|
|
256
|
+
getJobSetConditionTone({
|
|
257
|
+
type: 'StartupPolicyInProgress',
|
|
258
|
+
status: 'True',
|
|
259
|
+
}),
|
|
260
|
+
).toBe('unknown')
|
|
261
|
+
expect(
|
|
262
|
+
getJobSetConditionTone({
|
|
263
|
+
type: 'StartupPolicyInProgress',
|
|
264
|
+
status: 'False',
|
|
265
|
+
}),
|
|
266
|
+
).toBe('ok')
|
|
267
|
+
expect(
|
|
268
|
+
getJobSetConditionTone({
|
|
269
|
+
type: 'StartupPolicyCompleted',
|
|
270
|
+
status: 'True',
|
|
271
|
+
}),
|
|
272
|
+
).toBe('ok')
|
|
273
|
+
expect(
|
|
274
|
+
getJobSetConditionTone({
|
|
275
|
+
type: 'StartupPolicyCompleted',
|
|
276
|
+
status: 'False',
|
|
277
|
+
}),
|
|
278
|
+
).toBe('unknown')
|
|
279
|
+
})
|
|
280
|
+
})
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { Activity, Boxes, Network as NetworkIcon, ShieldCheck } from 'lucide-react'
|
|
2
|
+
import {
|
|
3
|
+
ConditionsSection,
|
|
4
|
+
ProblemAlerts,
|
|
5
|
+
Property,
|
|
6
|
+
PropertyList,
|
|
7
|
+
Section,
|
|
8
|
+
type ConditionTone,
|
|
9
|
+
type Problem,
|
|
10
|
+
} from '../../ui/drawer-components'
|
|
11
|
+
import { Badge } from '../../ui/Badge'
|
|
12
|
+
import { getJobSetStatus } from '../resource-utils-jobset-lws'
|
|
13
|
+
|
|
14
|
+
interface JobSetRendererProps {
|
|
15
|
+
data: any
|
|
16
|
+
mode?: 'detail' | 'overview' | 'configuration'
|
|
17
|
+
shownMemberCounts?: ReadonlyMap<string, number>
|
|
18
|
+
onSelectRole?: (role: string) => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getJobSetConditionTone(condition: any): ConditionTone | undefined {
|
|
22
|
+
if (condition?.status !== 'True' && condition?.status !== 'False') return 'unknown'
|
|
23
|
+
|
|
24
|
+
switch (condition.type) {
|
|
25
|
+
case 'Failed':
|
|
26
|
+
return condition.status === 'True' ? 'fail' : 'ok'
|
|
27
|
+
case 'Completed':
|
|
28
|
+
return condition.status === 'True' ? 'ok' : 'unknown'
|
|
29
|
+
case 'Suspended':
|
|
30
|
+
return condition.status === 'True' ? 'warning' : 'ok'
|
|
31
|
+
case 'RestartingJobSet':
|
|
32
|
+
return condition.status === 'True' ? 'warning' : 'ok'
|
|
33
|
+
case 'StartupPolicyInProgress':
|
|
34
|
+
return condition.status === 'True' ? 'unknown' : 'ok'
|
|
35
|
+
case 'StartupPolicyCompleted':
|
|
36
|
+
return condition.status === 'True' ? 'ok' : 'unknown'
|
|
37
|
+
default:
|
|
38
|
+
return undefined
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function jobIndexRange(replicas: unknown): string | undefined {
|
|
43
|
+
if (typeof replicas !== 'number') return undefined
|
|
44
|
+
if (replicas <= 0) return 'None'
|
|
45
|
+
return replicas === 1 ? '0' : `0–${replicas - 1}`
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function sumCountArray(value: unknown): number | undefined {
|
|
49
|
+
if (!Array.isArray(value)) return undefined
|
|
50
|
+
return value.reduce((total: number, count: unknown) => total + (typeof count === 'number' ? count : 0), 0)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function JobSetRenderer({ data, mode = 'detail', shownMemberCounts, onSelectRole }: JobSetRendererProps) {
|
|
54
|
+
const spec = data?.spec || {}
|
|
55
|
+
const status = data?.status || {}
|
|
56
|
+
const conditions: any[] = Array.isArray(status.conditions) ? status.conditions : []
|
|
57
|
+
const replicatedJobs: any[] = Array.isArray(spec.replicatedJobs) ? spec.replicatedJobs : []
|
|
58
|
+
const replicatedJobsStatus: any[] = Array.isArray(status.replicatedJobsStatus) ? status.replicatedJobsStatus : []
|
|
59
|
+
const statusByName = new Map(replicatedJobsStatus.map((entry) => [entry?.name, entry]))
|
|
60
|
+
const displayedStatus = getJobSetStatus(data)
|
|
61
|
+
const failedCondition = conditions.find((condition) => condition?.type === 'Failed' && condition?.status === 'True')
|
|
62
|
+
const problems: Problem[] =
|
|
63
|
+
displayedStatus.level === 'unhealthy'
|
|
64
|
+
? [
|
|
65
|
+
{
|
|
66
|
+
color: 'red',
|
|
67
|
+
message: failedCondition?.message || failedCondition?.reason || 'The JobSet reached a terminal failed state.',
|
|
68
|
+
},
|
|
69
|
+
]
|
|
70
|
+
: []
|
|
71
|
+
const successPolicy = spec.successPolicy
|
|
72
|
+
const failurePolicy = spec.failurePolicy
|
|
73
|
+
const failureRules: any[] = Array.isArray(failurePolicy?.rules) ? failurePolicy.rules : []
|
|
74
|
+
const globalCountedRestarts = status.restartsCountTowardsMax ?? (typeof status.restarts === 'number' ? 0 : undefined)
|
|
75
|
+
const individualCountedRestarts = Array.isArray(status.replicatedJobsStatus)
|
|
76
|
+
? replicatedJobsStatus.reduce((total, entry) => total + (sumCountArray(entry?.jobRestartsCountTowardsMax) ?? 0), 0)
|
|
77
|
+
: undefined
|
|
78
|
+
const observedRoles = replicatedJobs.filter((role) => statusByName.has(role.name)).length
|
|
79
|
+
const externalController = spec.managedBy && spec.managedBy !== 'jobset.sigs.k8s.io/jobset-controller'
|
|
80
|
+
const showSummary = mode !== 'configuration'
|
|
81
|
+
const showConfiguration = mode !== 'overview'
|
|
82
|
+
const coordinator = spec.coordinator
|
|
83
|
+
const network = spec.network
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<>
|
|
87
|
+
{showSummary && <ProblemAlerts problems={problems} />}
|
|
88
|
+
|
|
89
|
+
{showSummary && externalController && (
|
|
90
|
+
<div className="mb-3 rounded border border-theme-border bg-theme-elevated p-3 text-sm text-theme-text-secondary">
|
|
91
|
+
Managed by <span className="font-mono">{spec.managedBy}</span>. The built-in JobSet controller does not create Jobs for this resource.
|
|
92
|
+
{spec.managedBy === 'kueue.x-k8s.io/multikueue' && ' Execution may occur on another cluster; local member absence does not establish remote execution state.'}
|
|
93
|
+
</div>
|
|
94
|
+
)}
|
|
95
|
+
|
|
96
|
+
{showSummary && <Section title="JobSet status" icon={Activity}>
|
|
97
|
+
{mode === 'overview' ? (
|
|
98
|
+
<div className="flex flex-wrap items-center gap-x-5 gap-y-2 text-sm text-theme-text-secondary">
|
|
99
|
+
<span className={`badge ${displayedStatus.color}`}>{displayedStatus.text}</span>
|
|
100
|
+
<span>Suspend requested: {spec.suspend ? 'Yes' : 'No'}</span>
|
|
101
|
+
<span>Global restarts: {status.restarts ?? 'Not reported'}</span>
|
|
102
|
+
{observedRoles < replicatedJobs.length && <span>Roles reporting status: {observedRoles} of {replicatedJobs.length}</span>}
|
|
103
|
+
</div>
|
|
104
|
+
) : (
|
|
105
|
+
<PropertyList>
|
|
106
|
+
<Property label="State" value={<span className={`badge ${displayedStatus.color}`}>{displayedStatus.text}</span>} />
|
|
107
|
+
<Property label="Terminal state" value={status.terminalState} />
|
|
108
|
+
<Property label="Global restarts" value={status.restarts} />
|
|
109
|
+
<Property label="Suspend requested" value={spec.suspend ? 'Yes' : 'No'} />
|
|
110
|
+
<Property label="Managed by" value={spec.managedBy} />
|
|
111
|
+
<Property
|
|
112
|
+
label="Delete after finish"
|
|
113
|
+
value={spec.ttlSecondsAfterFinished === undefined ? undefined : `${spec.ttlSecondsAfterFinished}s`}
|
|
114
|
+
/>
|
|
115
|
+
</PropertyList>
|
|
116
|
+
)}
|
|
117
|
+
</Section>}
|
|
118
|
+
|
|
119
|
+
{mode === 'overview' && replicatedJobs.length > 0 && (
|
|
120
|
+
<Section title="Role progress" icon={Boxes}>
|
|
121
|
+
<p className="mb-3 text-xs text-theme-text-secondary">Counts are controller-reported Jobs, not Pods. Active Jobs can have Pending Pods. Dependencies describe startup requirements.</p>
|
|
122
|
+
<div className="overflow-x-auto">
|
|
123
|
+
<table className="w-full text-left text-xs">
|
|
124
|
+
<thead className="text-theme-text-tertiary"><tr>
|
|
125
|
+
{['Role', 'Declared Jobs', 'Ready', 'Active', 'Succeeded', 'Failed', 'Suspended', 'Starts after', ...(onSelectRole ? ['Members'] : [])].map((label) => <th key={label} className="whitespace-nowrap border-b border-theme-border px-2 py-2 font-medium">{label}</th>)}
|
|
126
|
+
</tr></thead>
|
|
127
|
+
<tbody>{replicatedJobs.map((role) => {
|
|
128
|
+
const observation = statusByName.get(role.name)
|
|
129
|
+
const shown = shownMemberCounts?.get(role.name) ?? 0
|
|
130
|
+
return <tr key={role.name} className="border-b border-theme-border-subtle last:border-0">
|
|
131
|
+
<th scope="row" className="px-2 py-3 font-medium text-theme-text-primary">{role.name}{!observation && <span className="mt-1 block font-normal text-theme-text-tertiary">Status not reported</span>}</th>
|
|
132
|
+
<td className="px-2 py-3">{role.replicas ?? 1}</td>
|
|
133
|
+
{['ready', 'active', 'succeeded', 'failed', 'suspended'].map((field) => <td key={field} className="px-2 py-3" aria-label={`${role.name} ${field}: ${observation?.[field] ?? 'not reported'}`}>{observation?.[field] ?? '—'}</td>)}
|
|
134
|
+
<td className="px-2 py-3">{role.dependsOn?.map((dependency: any) => `${dependency.name} ${dependency.status}`).join(', ') || 'None'}</td>
|
|
135
|
+
{onSelectRole && <td className="px-2 py-3"><button type="button" className="whitespace-nowrap text-accent-text hover:underline" onClick={() => onSelectRole(role.name)}>Inspect members{shown > 0 ? ` (${shown} shown)` : ''}</button></td>}
|
|
136
|
+
</tr>
|
|
137
|
+
})}</tbody>
|
|
138
|
+
</table>
|
|
139
|
+
</div>
|
|
140
|
+
</Section>
|
|
141
|
+
)}
|
|
142
|
+
|
|
143
|
+
{mode === 'configuration' && (spec.managedBy || spec.ttlSecondsAfterFinished !== undefined) && (
|
|
144
|
+
<PropertyList>
|
|
145
|
+
<Property label="Managed by" value={spec.managedBy} />
|
|
146
|
+
<Property label="Delete after finish" value={spec.ttlSecondsAfterFinished === undefined ? undefined : `${spec.ttlSecondsAfterFinished}s`} />
|
|
147
|
+
</PropertyList>
|
|
148
|
+
)}
|
|
149
|
+
|
|
150
|
+
{showConfiguration && replicatedJobs.length > 0 && (
|
|
151
|
+
<Section title={`Replicated jobs (${replicatedJobs.length})`} icon={Boxes} defaultExpanded={mode === 'detail'}>
|
|
152
|
+
<div className="space-y-2">
|
|
153
|
+
{replicatedJobs.map((replicatedJob, index) => {
|
|
154
|
+
const name = replicatedJob?.name || `Role ${index + 1}`
|
|
155
|
+
const roleStatus = statusByName.get(replicatedJob?.name)
|
|
156
|
+
const template = replicatedJob?.template?.spec || {}
|
|
157
|
+
const dependencies: any[] = Array.isArray(replicatedJob?.dependsOn) ? replicatedJob.dependsOn : []
|
|
158
|
+
const replicas = replicatedJob?.replicas ?? 1
|
|
159
|
+
const ready =
|
|
160
|
+
typeof roleStatus?.ready === 'number' && typeof replicas === 'number'
|
|
161
|
+
? `${roleStatus.ready}/${replicas}`
|
|
162
|
+
: roleStatus?.ready
|
|
163
|
+
const counts = [
|
|
164
|
+
{ label: 'Ready', value: ready },
|
|
165
|
+
{ label: 'Active', value: roleStatus?.active },
|
|
166
|
+
{ label: 'Succeeded', value: roleStatus?.succeeded },
|
|
167
|
+
{ label: 'Failed', value: roleStatus?.failed },
|
|
168
|
+
{ label: 'Suspended', value: roleStatus?.suspended },
|
|
169
|
+
].filter((count) => count.value !== undefined && count.value !== null)
|
|
170
|
+
const jobRestarts = sumCountArray(roleStatus?.jobRestarts)
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<div key={`${name}-${index}`} className="card-inner space-y-2">
|
|
174
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
175
|
+
<span className="font-medium text-theme-text-primary">{name}</span>
|
|
176
|
+
{replicatedJob?.groupName && (
|
|
177
|
+
<Badge tone="structural" size="sm">
|
|
178
|
+
Group {replicatedJob.groupName}
|
|
179
|
+
</Badge>
|
|
180
|
+
)}
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
<PropertyList>
|
|
184
|
+
<Property label="Jobs" value={replicas} />
|
|
185
|
+
<Property label="Job indexes" value={jobIndexRange(replicas)} />
|
|
186
|
+
<Property label="Parallelism / Job" value={template.parallelism} />
|
|
187
|
+
<Property label="Completions / Job" value={template.completions} />
|
|
188
|
+
<Property label="Completion mode" value={template.completionMode} />
|
|
189
|
+
<Property label="Backoff limit" value={template.backoffLimit} />
|
|
190
|
+
<Property label="Individual restarts" value={jobRestarts} />
|
|
191
|
+
</PropertyList>
|
|
192
|
+
|
|
193
|
+
{mode === 'detail' && (counts.length > 0 ? (
|
|
194
|
+
<div className="flex flex-wrap gap-x-4 gap-y-1 border-t border-theme-border-subtle pt-2 text-xs text-theme-text-secondary">
|
|
195
|
+
{counts.map((count) => (
|
|
196
|
+
<span key={count.label}>
|
|
197
|
+
<span className="text-theme-text-tertiary">{count.label}</span>{' '}
|
|
198
|
+
<span className="font-medium text-theme-text-primary">{String(count.value)}</span>
|
|
199
|
+
</span>
|
|
200
|
+
))}
|
|
201
|
+
</div>
|
|
202
|
+
) : (
|
|
203
|
+
<div className="border-t border-theme-border-subtle pt-2 text-xs text-theme-text-tertiary">
|
|
204
|
+
Controller status has not been reported for this role.
|
|
205
|
+
</div>
|
|
206
|
+
))}
|
|
207
|
+
|
|
208
|
+
{mode === 'detail' && dependencies.length > 0 && (
|
|
209
|
+
<div className="flex flex-wrap items-center gap-1.5 text-xs">
|
|
210
|
+
<span className="text-theme-text-tertiary">Starts after</span>
|
|
211
|
+
{dependencies.map((dependency, dependencyIndex) => (
|
|
212
|
+
<Badge key={`${dependency?.name || 'dependency'}-${dependencyIndex}`} tone="structural" size="sm">
|
|
213
|
+
{dependency?.name || 'Unknown role'}
|
|
214
|
+
{dependency?.status ? ` · ${dependency.status}` : ''}
|
|
215
|
+
</Badge>
|
|
216
|
+
))}
|
|
217
|
+
</div>
|
|
218
|
+
)}
|
|
219
|
+
</div>
|
|
220
|
+
)
|
|
221
|
+
})}
|
|
222
|
+
</div>
|
|
223
|
+
</Section>
|
|
224
|
+
)}
|
|
225
|
+
|
|
226
|
+
{showConfiguration && (successPolicy || failurePolicy || spec.startupPolicy) && (
|
|
227
|
+
<Section title="Completion and restart policies" icon={ShieldCheck} defaultExpanded={mode === 'detail'}>
|
|
228
|
+
<PropertyList>
|
|
229
|
+
{successPolicy?.operator && (
|
|
230
|
+
<Property
|
|
231
|
+
label="Success"
|
|
232
|
+
value={
|
|
233
|
+
<span className="inline-flex flex-wrap items-center gap-1.5">
|
|
234
|
+
<Badge tone="structural" size="sm">
|
|
235
|
+
{successPolicy.operator}
|
|
236
|
+
</Badge>
|
|
237
|
+
<span>
|
|
238
|
+
across{' '}
|
|
239
|
+
{Array.isArray(successPolicy.targetReplicatedJobs) && successPolicy.targetReplicatedJobs.length > 0
|
|
240
|
+
? successPolicy.targetReplicatedJobs.join(', ')
|
|
241
|
+
: 'all replicated jobs'}
|
|
242
|
+
</span>
|
|
243
|
+
</span>
|
|
244
|
+
}
|
|
245
|
+
/>
|
|
246
|
+
)}
|
|
247
|
+
<Property label="Restart limit" value={failurePolicy?.maxRestarts} />
|
|
248
|
+
<Property label="Global restarts counted toward limit" value={globalCountedRestarts ?? 'Not reported'} />
|
|
249
|
+
<Property label="Individual restarts counted toward limit" value={individualCountedRestarts == null ? 'Not reported' : `${individualCountedRestarts} (${observedRoles} of ${replicatedJobs.length} roles reported)`} />
|
|
250
|
+
<Property label="Restart strategy" value={failurePolicy?.restartStrategy} />
|
|
251
|
+
{failureRules.length > 0 && <Property label="Default action" value="RestartJobSet" />}
|
|
252
|
+
<Property label="Startup order" value={spec.startupPolicy?.startupPolicyOrder} />
|
|
253
|
+
</PropertyList>
|
|
254
|
+
|
|
255
|
+
{failureRules.length > 0 && (
|
|
256
|
+
<div className="mt-3 space-y-2">
|
|
257
|
+
{failureRules.map((rule, index) => {
|
|
258
|
+
const targets =
|
|
259
|
+
Array.isArray(rule?.targetReplicatedJobs) && rule.targetReplicatedJobs.length > 0
|
|
260
|
+
? rule.targetReplicatedJobs.join(', ')
|
|
261
|
+
: 'All replicated jobs'
|
|
262
|
+
const reasons =
|
|
263
|
+
Array.isArray(rule?.onJobFailureReasons) && rule.onJobFailureReasons.length > 0
|
|
264
|
+
? rule.onJobFailureReasons.join(', ')
|
|
265
|
+
: 'Any Job failure reason'
|
|
266
|
+
const messagePatterns =
|
|
267
|
+
Array.isArray(rule?.onJobFailureMessagePatterns) && rule.onJobFailureMessagePatterns.length > 0
|
|
268
|
+
? rule.onJobFailureMessagePatterns.join(', ')
|
|
269
|
+
: 'Any failure message'
|
|
270
|
+
|
|
271
|
+
return (
|
|
272
|
+
<div key={`${rule?.name || 'rule'}-${index}`} className="card-inner space-y-1.5">
|
|
273
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
274
|
+
<span className="text-sm font-medium text-theme-text-primary">
|
|
275
|
+
{index + 1}. {rule?.name || `Rule ${index + 1}`}
|
|
276
|
+
</span>
|
|
277
|
+
{rule?.action && (
|
|
278
|
+
<Badge tone="structural" size="sm">
|
|
279
|
+
{rule.action}
|
|
280
|
+
</Badge>
|
|
281
|
+
)}
|
|
282
|
+
</div>
|
|
283
|
+
<div className="text-xs text-theme-text-secondary">
|
|
284
|
+
<span className="text-theme-text-tertiary">Applies to:</span> {targets}
|
|
285
|
+
</div>
|
|
286
|
+
<div className="text-xs text-theme-text-secondary">
|
|
287
|
+
<span className="text-theme-text-tertiary">Reasons:</span> {reasons}
|
|
288
|
+
</div>
|
|
289
|
+
<div className="break-words text-xs text-theme-text-secondary">
|
|
290
|
+
<span className="text-theme-text-tertiary">Messages:</span> {messagePatterns}
|
|
291
|
+
</div>
|
|
292
|
+
</div>
|
|
293
|
+
)
|
|
294
|
+
})}
|
|
295
|
+
</div>
|
|
296
|
+
)}
|
|
297
|
+
</Section>
|
|
298
|
+
)}
|
|
299
|
+
|
|
300
|
+
{showConfiguration && (coordinator || network) && (
|
|
301
|
+
<Section title="Coordination and network" icon={NetworkIcon} defaultExpanded={mode === 'detail'}>
|
|
302
|
+
<PropertyList>
|
|
303
|
+
<Property label="Coordinator role" value={coordinator?.replicatedJob} />
|
|
304
|
+
<Property label="Coordinator Job index" value={coordinator?.jobIndex} />
|
|
305
|
+
<Property label="Coordinator Pod index" value={coordinator?.podIndex} />
|
|
306
|
+
<Property label="Subdomain" value={network?.subdomain} />
|
|
307
|
+
<Property
|
|
308
|
+
label="DNS hostnames"
|
|
309
|
+
value={network?.enableDNSHostnames === undefined ? undefined : network.enableDNSHostnames ? 'Enabled' : 'Disabled'}
|
|
310
|
+
/>
|
|
311
|
+
<Property
|
|
312
|
+
label="Publish before ready"
|
|
313
|
+
value={network?.publishNotReadyAddresses === undefined ? undefined : network.publishNotReadyAddresses ? 'Yes' : 'No'}
|
|
314
|
+
/>
|
|
315
|
+
</PropertyList>
|
|
316
|
+
</Section>
|
|
317
|
+
)}
|
|
318
|
+
|
|
319
|
+
{showSummary && <ConditionsSection conditions={conditions} getConditionTone={getJobSetConditionTone} defaultExpanded={mode === 'overview' ? false : undefined} />}
|
|
320
|
+
</>
|
|
321
|
+
)
|
|
322
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { AdmissionCheckRenderer, ProvisioningRequestRenderer, provisioningConditionTone } from './KueueProvisioningRenderers'
|
|
4
|
+
import { getAdmissionCheckStatus, getProvisioningRequestStatus, getProvisioningRequestMessage } from '../resource-utils-kueue'
|
|
5
|
+
|
|
6
|
+
const resource = (conditions: any[] = []) => ({ apiVersion: 'autoscaling.x-k8s.io/v1', metadata: { generation: 3, namespace: 'ml' }, spec: { provisioningClassName: 'provider.example' }, status: { conditions } })
|
|
7
|
+
const condition = (type: string, extra = {}) => ({ type, status: 'True', observedGeneration: 3, ...extra })
|
|
8
|
+
|
|
9
|
+
describe('provisioning evidence', () => {
|
|
10
|
+
it('uses current provisioning progress while Accepted, without turning the false condition into failure', () => {
|
|
11
|
+
const data = resource([condition('Accepted', { message: 'Request accepted' }), condition('Provisioned', { status: 'False', message: 'Waiting for capacity' })])
|
|
12
|
+
expect(getProvisioningRequestStatus(data).text).toBe('Accepted')
|
|
13
|
+
expect(getProvisioningRequestMessage(data)).toBe('Waiting for capacity')
|
|
14
|
+
data.status.conditions[1].observedGeneration = 2
|
|
15
|
+
expect(getProvisioningRequestMessage(data)).toBe('Request accepted')
|
|
16
|
+
})
|
|
17
|
+
it('keeps negative outcomes above old success regardless of condition order', () => {
|
|
18
|
+
for (const type of ['Failed', 'CapacityRevoked', 'BookingExpired']) {
|
|
19
|
+
for (const conditions of [[condition('Provisioned'), condition(type)], [condition(type), condition('Provisioned')]]) {
|
|
20
|
+
expect(getProvisioningRequestStatus(resource(conditions)).text).toBe(type)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
expect(getProvisioningRequestStatus(resource([condition('BookingExpired')])).level).toBe('neutral')
|
|
24
|
+
expect(getProvisioningRequestStatus(resource([condition('CapacityRevoked')])).level).toBe('unhealthy')
|
|
25
|
+
})
|
|
26
|
+
it('does not invent a pending or healthy outcome from missing, unknown or stale observations', () => {
|
|
27
|
+
expect(getProvisioningRequestStatus(resource()).text).toBe('Unknown')
|
|
28
|
+
expect(getProvisioningRequestStatus(resource([condition('Provisioned', { status: 'Unknown' })])).text).toBe('Unknown')
|
|
29
|
+
expect(getProvisioningRequestStatus(resource([condition('Provisioned', { observedGeneration: 2 })])).level).toBe('unknown')
|
|
30
|
+
expect(provisioningConditionTone(condition('Failed'))).toBe('fail')
|
|
31
|
+
expect(provisioningConditionTone(condition('BookingExpired'))).toBe('unknown')
|
|
32
|
+
})
|
|
33
|
+
it('renders requested template/count and outcome without claiming running Pods', () => {
|
|
34
|
+
const data = { ...resource([condition('Provisioned')]), spec: { provisioningClassName: 'provider.example', podSets: [{ podTemplateRef: { name: 'workers' }, count: 0 }, { podTemplateRef: { name: 'missing-count' } }] } }
|
|
35
|
+
const html = renderToStaticMarkup(<ProvisioningRequestRenderer data={data} />)
|
|
36
|
+
for (const text of ['workers', 'Requested Pods', 'Not specified', 'not a live check', 'Provisioned does not mean the Workload is running']) expect(html).toContain(text)
|
|
37
|
+
expect(html).toContain('>0<')
|
|
38
|
+
})
|
|
39
|
+
it('keeps failure reasons/messages and does not treat foreign Workload owners as Kueue', () => {
|
|
40
|
+
const data = resource([condition('Failed', { reason: 'CapacityUnavailable', message: 'No matching capacity' })])
|
|
41
|
+
Object.assign(data.metadata, { ownerReferences: [{ controller: true, kind: 'Workload', apiVersion: 'foreign.io/v1', name: 'foreign-owner' }] })
|
|
42
|
+
const html = renderToStaticMarkup(<ProvisioningRequestRenderer data={data} />)
|
|
43
|
+
expect(html).toContain('No matching capacity')
|
|
44
|
+
expect(html).toContain('CapacityUnavailable')
|
|
45
|
+
expect(html).not.toContain('foreign-owner')
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
describe('AdmissionCheck controller readiness', () => {
|
|
50
|
+
it('distinguishes active controller from Workload approval and preserves native failure', () => {
|
|
51
|
+
for (const version of ['v1beta1', 'v1beta2']) {
|
|
52
|
+
const data = { ...resource([condition('Active')]), apiVersion: `kueue.x-k8s.io/${version}`, spec: { controllerName: 'kueue.x-k8s.io/provisioning-request' } }
|
|
53
|
+
const html = renderToStaticMarkup(<AdmissionCheckRenderer data={data} />)
|
|
54
|
+
expect(html).toContain('Each Workload reports its own')
|
|
55
|
+
expect(getAdmissionCheckStatus(data).text).toBe('Active')
|
|
56
|
+
data.status.conditions = [condition('Active', { status: 'False', reason: 'BadParametersRef', message: 'Config not found' })]
|
|
57
|
+
expect(renderToStaticMarkup(<AdmissionCheckRenderer data={data} />)).toContain('Config not found')
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
it('keeps stale and missing readiness unknown, and only displays declared v1beta1 retry delay', () => {
|
|
61
|
+
const data = { ...resource([condition('Active', { observedGeneration: 2 })]), apiVersion: 'kueue.x-k8s.io/v1beta1', spec: { retryDelayMinutes: 0 } }
|
|
62
|
+
expect(getAdmissionCheckStatus(data).level).toBe('unknown')
|
|
63
|
+
expect(renderToStaticMarkup(<AdmissionCheckRenderer data={data} />)).toContain('0 minutes')
|
|
64
|
+
expect(renderToStaticMarkup(<AdmissionCheckRenderer data={{ ...data, apiVersion: 'kueue.x-k8s.io/v1beta2' }} />)).not.toContain('0 minutes')
|
|
65
|
+
expect(getAdmissionCheckStatus(resource()).level).toBe('unknown')
|
|
66
|
+
})
|
|
67
|
+
})
|