foreman_remote_execution 14.1.4 → 15.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/controllers/api/v2/job_invocations_controller.rb +8 -0
- data/app/controllers/template_invocations_controller.rb +57 -0
- data/app/controllers/ui_job_wizard_controller.rb +6 -3
- data/app/helpers/remote_execution_helper.rb +5 -6
- data/app/views/api/v2/job_invocations/base.json.rabl +1 -1
- data/app/views/api/v2/job_invocations/hosts.json.rabl +1 -1
- data/app/views/api/v2/job_invocations/main.json.rabl +1 -1
- data/app/views/api/v2/job_invocations/show.json.rabl +18 -0
- data/app/views/templates/script/convert2rhel_analyze.erb +4 -4
- data/config/routes.rb +2 -0
- data/lib/foreman_remote_execution/engine.rb +3 -3
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/webpack/JobInvocationDetail/JobAdditionInfo.js +214 -0
- data/webpack/JobInvocationDetail/JobInvocationConstants.js +40 -2
- data/webpack/JobInvocationDetail/JobInvocationDetail.scss +70 -0
- data/webpack/JobInvocationDetail/JobInvocationHostTable.js +177 -80
- data/webpack/JobInvocationDetail/JobInvocationHostTableToolbar.js +63 -0
- data/webpack/JobInvocationDetail/JobInvocationSelectors.js +8 -1
- data/webpack/JobInvocationDetail/JobInvocationSystemStatusChart.js +61 -10
- data/webpack/JobInvocationDetail/OpenAlInvocations.js +111 -0
- data/webpack/JobInvocationDetail/TemplateInvocation.js +202 -0
- data/webpack/JobInvocationDetail/TemplateInvocationComponents/OutputCodeBlock.js +124 -0
- data/webpack/JobInvocationDetail/TemplateInvocationComponents/OutputToggleGroup.js +156 -0
- data/webpack/JobInvocationDetail/TemplateInvocationComponents/PreviewTemplate.js +50 -0
- data/webpack/JobInvocationDetail/TemplateInvocationComponents/TemplateActionButtons.js +224 -0
- data/webpack/JobInvocationDetail/TemplateInvocationPage.js +53 -0
- data/webpack/JobInvocationDetail/__tests__/MainInformation.test.js +1 -1
- data/webpack/JobInvocationDetail/__tests__/OpenAlInvocations.test.js +110 -0
- data/webpack/JobInvocationDetail/__tests__/OutputCodeBlock.test.js +69 -0
- data/webpack/JobInvocationDetail/__tests__/TemplateInvocation.test.js +131 -0
- data/webpack/JobInvocationDetail/__tests__/fixtures.js +130 -0
- data/webpack/JobInvocationDetail/index.js +18 -3
- data/webpack/JobWizard/JobWizard.js +38 -16
- data/webpack/JobWizard/{StartsBeforeErrorAlert.js → StartsErrorAlert.js} +16 -1
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +1 -1
- data/webpack/JobWizard/steps/Schedule/ScheduleFuture.js +1 -1
- data/webpack/JobWizard/steps/Schedule/ScheduleRecurring.js +5 -3
- data/webpack/JobWizard/steps/Schedule/ScheduleType.js +1 -1
- data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +3 -3
- data/webpack/JobWizard/steps/form/DateTimePicker.js +13 -0
- data/webpack/JobWizard/steps/form/Formatter.js +1 -0
- data/webpack/JobWizard/steps/form/ResourceSelect.js +34 -9
- data/webpack/Routes/routes.js +6 -0
- data/webpack/__mocks__/foremanReact/Root/Context/ForemanContext/index.js +1 -0
- data/webpack/react_app/components/RegistrationExtension/RexPull.js +27 -2
- data/webpack/react_app/components/TargetingHosts/components/HostStatus.js +1 -1
- metadata +15 -3
@@ -1,3 +1,20 @@
|
|
1
|
+
const targeting = {
|
2
|
+
bookmark_id: null,
|
3
|
+
bookmark_name: null,
|
4
|
+
search_query: 'id ^ (50)',
|
5
|
+
targeting_type: 'static_query',
|
6
|
+
user_id: 4,
|
7
|
+
randomized_ordering: null,
|
8
|
+
hosts: [
|
9
|
+
{
|
10
|
+
name: 'alton-bennette.iris-starley.kari-stadtler.example.net',
|
11
|
+
id: 50,
|
12
|
+
display_name: 'alton-bennette.iris-starley.kari-stadtler.example.net',
|
13
|
+
job_status: 'N/A',
|
14
|
+
},
|
15
|
+
],
|
16
|
+
};
|
17
|
+
|
1
18
|
export const jobInvocationData = {
|
2
19
|
search: '',
|
3
20
|
per_page: 20,
|
@@ -40,6 +57,7 @@ export const jobInvocationData = {
|
|
40
57
|
],
|
41
58
|
},
|
42
59
|
],
|
60
|
+
targeting,
|
43
61
|
};
|
44
62
|
|
45
63
|
export const jobInvocationDataScheduled = {
|
@@ -65,6 +83,7 @@ export const jobInvocationDataScheduled = {
|
|
65
83
|
total: 6,
|
66
84
|
missing: 5,
|
67
85
|
total_hosts: 6,
|
86
|
+
targeting,
|
68
87
|
};
|
69
88
|
|
70
89
|
export const jobInvocationDataRecurring = {
|
@@ -109,6 +128,7 @@ export const jobInvocationDataRecurring = {
|
|
109
128
|
last_occurence: null,
|
110
129
|
next_occurence: '3000-01-01 12:00:00 +0100',
|
111
130
|
},
|
131
|
+
targeting,
|
112
132
|
};
|
113
133
|
|
114
134
|
export const mockPermissionsData = {
|
@@ -124,3 +144,113 @@ export const mockReportTemplatesResponse = {
|
|
124
144
|
export const mockReportTemplateInputsResponse = {
|
125
145
|
results: [{ id: '34', name: 'job_id' }],
|
126
146
|
};
|
147
|
+
|
148
|
+
const templateInvocationID = 157;
|
149
|
+
|
150
|
+
export const jobInvocationOutput = [
|
151
|
+
{
|
152
|
+
id: 1958,
|
153
|
+
template_invocation_id: templateInvocationID,
|
154
|
+
timestamp: 1733931147.2044532,
|
155
|
+
meta: null,
|
156
|
+
external_id: '0',
|
157
|
+
output_type: 'stdout',
|
158
|
+
output:
|
159
|
+
'\u001b[31mThis is red text\u001b[0m\n\u001b[32mThis is green text\u001b[0m\n\u001b[33mThis is yellow text\u001b[0m\n\u001b[34mThis is blue text\u001b[0m\n\u001b[35mThis is magenta text\u001b[0m\n\u001b[36mThis is cyan text\u001b[0m\n\u001b[0mThis is default text\n',
|
160
|
+
},
|
161
|
+
{
|
162
|
+
output_type: 'stdout',
|
163
|
+
output: 'Exit status: 6',
|
164
|
+
timestamp: 1733931142.2044532,
|
165
|
+
},
|
166
|
+
{
|
167
|
+
output_type: 'stdout',
|
168
|
+
output: 'Exit status: 5',
|
169
|
+
timestamp: 1733931143.2044532,
|
170
|
+
},
|
171
|
+
{
|
172
|
+
output_type: 'stdout',
|
173
|
+
output: 'Exit status: 4',
|
174
|
+
timestamp: 1733931144.2044532,
|
175
|
+
},
|
176
|
+
{
|
177
|
+
output_type: 'stdout',
|
178
|
+
output: 'Exit status: 3',
|
179
|
+
timestamp: 1733931145.2044532,
|
180
|
+
},
|
181
|
+
{
|
182
|
+
output_type: 'stdout',
|
183
|
+
output: 'Exit status: 2',
|
184
|
+
timestamp: 1733931146.2044532,
|
185
|
+
},
|
186
|
+
{
|
187
|
+
output_type: 'stdout',
|
188
|
+
output: 'Exit status: 1',
|
189
|
+
timestamp: 1733931147.2044532,
|
190
|
+
},
|
191
|
+
|
192
|
+
{
|
193
|
+
output_type: 'stdout',
|
194
|
+
output: 'Exit status: 0',
|
195
|
+
timestamp: 1733931148.2044532,
|
196
|
+
},
|
197
|
+
|
198
|
+
{
|
199
|
+
id: 1907,
|
200
|
+
template_invocation_id: templateInvocationID,
|
201
|
+
timestamp: 1718719863.184878,
|
202
|
+
meta: null,
|
203
|
+
external_id: '15',
|
204
|
+
output_type: 'debug',
|
205
|
+
output: 'StandardError: Job execution failed',
|
206
|
+
},
|
207
|
+
{
|
208
|
+
id: 1892,
|
209
|
+
template_invocation_id: templateInvocationID,
|
210
|
+
timestamp: 1718719857.078763,
|
211
|
+
meta: null,
|
212
|
+
external_id: '0',
|
213
|
+
output_type: 'stderr',
|
214
|
+
output:
|
215
|
+
'[DEPRECATION WARNING]: ANSIBLE_CALLBACK_WHITELIST option, normalizing names to \n',
|
216
|
+
},
|
217
|
+
];
|
218
|
+
|
219
|
+
export const mockTemplateInvocationResponse = {
|
220
|
+
output: jobInvocationOutput,
|
221
|
+
preview: {
|
222
|
+
plain: 'PREVIEW TEXT \n TEST',
|
223
|
+
},
|
224
|
+
input_values: [
|
225
|
+
{
|
226
|
+
id: 40,
|
227
|
+
template_invocation_id: 157,
|
228
|
+
template_input_id: 74,
|
229
|
+
value:
|
230
|
+
'echo -e "\\e[31mThis is red text\\e[0m"\necho -e "\\e[32mThis is green text\\e[0m"\necho -e "\\e[33mThis is yellow text\\e[0m"\necho -e "\\e[34mThis is blue text\\e[0m"\necho -e "\\e[35mThis is magenta text\\e[0m"\necho -e "\\e[36mThis is cyan text\\e[0m"\necho -e "\\e[0mThis is default text"',
|
231
|
+
template_input: {
|
232
|
+
id: 74,
|
233
|
+
name: 'command',
|
234
|
+
required: true,
|
235
|
+
input_type: 'user',
|
236
|
+
fact_name: null,
|
237
|
+
variable_name: null,
|
238
|
+
puppet_class_name: null,
|
239
|
+
puppet_parameter_name: null,
|
240
|
+
description: 'Command to run on the host',
|
241
|
+
template_id: 189,
|
242
|
+
created_at: '2024-06-11T10:31:24.084+01:00',
|
243
|
+
updated_at: '2024-06-11T10:31:24.084+01:00',
|
244
|
+
options: null,
|
245
|
+
advanced: false,
|
246
|
+
value_type: 'plain',
|
247
|
+
resource_type: null,
|
248
|
+
default: null,
|
249
|
+
hidden_value: false,
|
250
|
+
},
|
251
|
+
},
|
252
|
+
],
|
253
|
+
|
254
|
+
job_invocation_description: 'Run tst',
|
255
|
+
host_name: 'alton-bennette.iris-starley.kari-stadtler.example.net',
|
256
|
+
};
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import PropTypes from 'prop-types';
|
2
|
-
import React, { useEffect } from 'react';
|
2
|
+
import React, { useEffect, useState } from 'react';
|
3
3
|
import { useDispatch, useSelector } from 'react-redux';
|
4
4
|
import {
|
5
5
|
Divider,
|
@@ -25,6 +25,7 @@ import { selectItems } from './JobInvocationSelectors';
|
|
25
25
|
import JobInvocationSystemStatusChart from './JobInvocationSystemStatusChart';
|
26
26
|
import JobInvocationToolbarButtons from './JobInvocationToolbarButtons';
|
27
27
|
import JobInvocationHostTable from './JobInvocationHostTable';
|
28
|
+
import { JobAdditionInfo } from './JobAdditionInfo';
|
28
29
|
|
29
30
|
const JobInvocationDetailPage = ({
|
30
31
|
match: {
|
@@ -50,6 +51,11 @@ const JobInvocationDetailPage = ({
|
|
50
51
|
currentPermissionsUrl,
|
51
52
|
CURRENT_PERMISSIONS
|
52
53
|
);
|
54
|
+
const [selectedFilter, setSelectedFilter] = useState('');
|
55
|
+
|
56
|
+
const handleFilterChange = filter => {
|
57
|
+
setSelectedFilter(filter);
|
58
|
+
};
|
53
59
|
|
54
60
|
let isAlreadyStarted = false;
|
55
61
|
let formattedStartDate;
|
@@ -78,7 +84,8 @@ const JobInvocationDetailPage = ({
|
|
78
84
|
if (task?.id !== undefined) {
|
79
85
|
dispatch(getTask(`${task?.id}`));
|
80
86
|
}
|
81
|
-
|
87
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
88
|
+
}, [dispatch, task?.id]);
|
82
89
|
|
83
90
|
const breadcrumbOptions = {
|
84
91
|
breadcrumbItems: [
|
@@ -125,13 +132,20 @@ const JobInvocationDetailPage = ({
|
|
125
132
|
data={items}
|
126
133
|
isAlreadyStarted={isAlreadyStarted}
|
127
134
|
formattedStartDate={formattedStartDate}
|
135
|
+
onFilterChange={handleFilterChange}
|
128
136
|
/>
|
129
137
|
</Flex>
|
130
138
|
</Flex>
|
139
|
+
<PageSection
|
140
|
+
variant={PageSectionVariants.light}
|
141
|
+
className="job-additional-info"
|
142
|
+
>
|
143
|
+
{items.id !== undefined && <JobAdditionInfo data={items} />}
|
144
|
+
</PageSection>
|
131
145
|
</PageLayout>
|
132
146
|
<PageSection
|
133
147
|
variant={PageSectionVariants.light}
|
134
|
-
className="table-section"
|
148
|
+
className="job-details-table-section table-section"
|
135
149
|
>
|
136
150
|
{items.id !== undefined && (
|
137
151
|
<JobInvocationHostTable
|
@@ -139,6 +153,7 @@ const JobInvocationDetailPage = ({
|
|
139
153
|
targeting={targeting}
|
140
154
|
finished={finished}
|
141
155
|
autoRefresh={autoRefresh}
|
156
|
+
initialFilter={selectedFilter}
|
142
157
|
/>
|
143
158
|
)}
|
144
159
|
</PageSection>
|
@@ -36,7 +36,7 @@ import { useValidation } from './validation';
|
|
36
36
|
import { useAutoFill } from './autofill';
|
37
37
|
import { submit } from './submit';
|
38
38
|
import { generateDefaultDescription } from './JobWizardHelpers';
|
39
|
-
import { StartsBeforeErrorAlert } from './
|
39
|
+
import { StartsBeforeErrorAlert, StartsAtErrorAlert } from './StartsErrorAlert';
|
40
40
|
import { Footer } from './Footer';
|
41
41
|
import './JobWizard.scss';
|
42
42
|
|
@@ -203,22 +203,37 @@ export const JobWizard = ({ rerunData }) => {
|
|
203
203
|
}, [rerunData, jobTemplateID, dispatch]);
|
204
204
|
|
205
205
|
const [isStartsBeforeError, setIsStartsBeforeError] = useState(false);
|
206
|
+
const [isStartsAtError, setIsStartsAtError] = useState(false);
|
206
207
|
useEffect(() => {
|
207
|
-
const
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
208
|
+
const updateStartsError = () => {
|
209
|
+
if (scheduleValue.scheduleType === SCHEDULE_TYPES.FUTURE) {
|
210
|
+
setIsStartsAtError(
|
211
|
+
!!scheduleValue?.startsAt?.length &&
|
212
|
+
new Date().getTime() >= new Date(scheduleValue.startsAt).getTime()
|
213
|
+
);
|
214
|
+
setIsStartsBeforeError(
|
215
|
+
!!scheduleValue?.startsBefore?.length &&
|
216
|
+
new Date().getTime() >=
|
217
|
+
new Date(scheduleValue.startsBefore).getTime()
|
218
|
+
);
|
219
|
+
} else if (scheduleValue.scheduleType === SCHEDULE_TYPES.RECURRING) {
|
220
|
+
setIsStartsAtError(
|
221
|
+
!!scheduleValue?.startsAt?.length &&
|
222
|
+
new Date().getTime() >= new Date(scheduleValue.startsAt).getTime()
|
223
|
+
);
|
224
|
+
setIsStartsBeforeError(false);
|
225
|
+
} else {
|
226
|
+
setIsStartsAtError(false);
|
227
|
+
setIsStartsBeforeError(false);
|
228
|
+
}
|
212
229
|
};
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
interval = setInterval(updateStartsBeforeError, 5000);
|
217
|
-
}
|
230
|
+
updateStartsError();
|
231
|
+
const interval = setInterval(updateStartsError, 5000);
|
232
|
+
|
218
233
|
return () => {
|
219
234
|
interval && clearInterval(interval);
|
220
235
|
};
|
221
|
-
}, [scheduleValue
|
236
|
+
}, [scheduleValue]);
|
222
237
|
|
223
238
|
const [valid, setValid] = useValidation({
|
224
239
|
advancedValues,
|
@@ -369,7 +384,9 @@ export const JobWizard = ({ rerunData }) => {
|
|
369
384
|
valid.hostsAndInputs &&
|
370
385
|
areHostsSelected &&
|
371
386
|
valid.advanced &&
|
372
|
-
valid.schedule
|
387
|
+
valid.schedule &&
|
388
|
+
!isStartsBeforeError &&
|
389
|
+
!isStartsAtError,
|
373
390
|
},
|
374
391
|
]
|
375
392
|
: []),
|
@@ -399,7 +416,8 @@ export const JobWizard = ({ rerunData }) => {
|
|
399
416
|
valid.hostsAndInputs &&
|
400
417
|
areHostsSelected &&
|
401
418
|
valid.advanced &&
|
402
|
-
valid.schedule
|
419
|
+
valid.schedule &&
|
420
|
+
!isStartsAtError,
|
403
421
|
},
|
404
422
|
]
|
405
423
|
: []),
|
@@ -424,7 +442,9 @@ export const JobWizard = ({ rerunData }) => {
|
|
424
442
|
valid.advanced &&
|
425
443
|
valid.hostsAndInputs &&
|
426
444
|
areHostsSelected &&
|
427
|
-
valid.schedule
|
445
|
+
valid.schedule &&
|
446
|
+
!isStartsBeforeError &&
|
447
|
+
!isStartsAtError,
|
428
448
|
enableNext:
|
429
449
|
isTemplate &&
|
430
450
|
valid.hostsAndInputs &&
|
@@ -432,7 +452,8 @@ export const JobWizard = ({ rerunData }) => {
|
|
432
452
|
valid.advanced &&
|
433
453
|
valid.schedule &&
|
434
454
|
!isSubmitting &&
|
435
|
-
!isStartsBeforeError
|
455
|
+
!isStartsBeforeError &&
|
456
|
+
!isStartsAtError,
|
436
457
|
},
|
437
458
|
];
|
438
459
|
const location = useForemanLocation();
|
@@ -456,6 +477,7 @@ export const JobWizard = ({ rerunData }) => {
|
|
456
477
|
return (
|
457
478
|
<>
|
458
479
|
{isStartsBeforeError && <StartsBeforeErrorAlert />}
|
480
|
+
{isStartsAtError && <StartsAtErrorAlert />}
|
459
481
|
<Wizard
|
460
482
|
onClose={() => history.goBack()}
|
461
483
|
navAriaLabel="Run Job steps"
|
@@ -7,7 +7,7 @@ export const StartsBeforeErrorAlert = () => (
|
|
7
7
|
<Alert
|
8
8
|
ouiaId="starts-before-error-alert"
|
9
9
|
variant="danger"
|
10
|
-
title={__("'Starts before' date must in the future")}
|
10
|
+
title={__("'Starts before' date must be in the future")}
|
11
11
|
>
|
12
12
|
{__(
|
13
13
|
'Please go back to "Schedule" - "Future execution" step to fix the error'
|
@@ -16,3 +16,18 @@ export const StartsBeforeErrorAlert = () => (
|
|
16
16
|
<Divider component="div" />
|
17
17
|
</>
|
18
18
|
);
|
19
|
+
|
20
|
+
export const StartsAtErrorAlert = () => (
|
21
|
+
<>
|
22
|
+
<Alert
|
23
|
+
ouiaId="starts-at-error-alert"
|
24
|
+
variant="danger"
|
25
|
+
title={__("'Starts at' date must be in the future")}
|
26
|
+
>
|
27
|
+
{__(
|
28
|
+
'Please go back to "Schedule" - "Future execution" or "Recurring execution" step to fix the error'
|
29
|
+
)}
|
30
|
+
</Alert>
|
31
|
+
<Divider component="div" />
|
32
|
+
</>
|
33
|
+
);
|
@@ -38,7 +38,7 @@ export const ScheduleFuture = ({
|
|
38
38
|
setError(__("'Starts before' date must be after 'Starts at' date"));
|
39
39
|
} else if (new Date().getTime() >= new Date(startsBefore).getTime()) {
|
40
40
|
wrappedSetValid(false);
|
41
|
-
setError(__("'Starts before' date must in the future"));
|
41
|
+
setError(__("'Starts before' date must be in the future"));
|
42
42
|
} else {
|
43
43
|
wrappedSetValid(true);
|
44
44
|
setError(null);
|
@@ -51,7 +51,7 @@ export const ScheduleRecurring = ({
|
|
51
51
|
if (isNeverEnds) setValidEnd(true);
|
52
52
|
else if (!ends) setValidEnd(true);
|
53
53
|
else if (
|
54
|
-
!startsAt
|
54
|
+
!startsAt?.length &&
|
55
55
|
new Date().getTime() <= new Date(ends).getTime()
|
56
56
|
)
|
57
57
|
setValidEnd(true);
|
@@ -63,7 +63,7 @@ export const ScheduleRecurring = ({
|
|
63
63
|
|
64
64
|
if (!validEnd || !repeatValid) {
|
65
65
|
wrappedSetValid(false);
|
66
|
-
} else if (isFuture && startsAt
|
66
|
+
} else if (isFuture && startsAt?.length) {
|
67
67
|
wrappedSetValid(true);
|
68
68
|
} else if (!isFuture) {
|
69
69
|
wrappedSetValid(true);
|
@@ -111,7 +111,9 @@ export const ScheduleRecurring = ({
|
|
111
111
|
onChange={() =>
|
112
112
|
setScheduleValue(current => ({
|
113
113
|
...current,
|
114
|
-
startsAt: new Date(
|
114
|
+
startsAt: new Date(
|
115
|
+
new Date().getTime() + 60000
|
116
|
+
).toISOString(), // 1 minute in the future
|
115
117
|
isFuture: true,
|
116
118
|
}))
|
117
119
|
}
|
@@ -49,7 +49,7 @@ export const ScheduleType = ({
|
|
49
49
|
onChange={() => {
|
50
50
|
setScheduleValue(current => ({
|
51
51
|
...current,
|
52
|
-
startsAt: new Date().toISOString(),
|
52
|
+
startsAt: new Date(new Date().getTime() + 60000).toISOString(), // 1 minute in the future
|
53
53
|
scheduleType: SCHEDULE_TYPES.FUTURE,
|
54
54
|
repeatType: repeatTypes.noRepeat,
|
55
55
|
}));
|
@@ -168,7 +168,7 @@ describe('Schedule', () => {
|
|
168
168
|
).toHaveLength(1);
|
169
169
|
|
170
170
|
expect(
|
171
|
-
screen.queryAllByText("'Starts before' date must in the future")
|
171
|
+
screen.queryAllByText("'Starts before' date must be in the future")
|
172
172
|
).toHaveLength(0);
|
173
173
|
await act(async () => {
|
174
174
|
await fireEvent.change(startsBeforeDateField(), {
|
@@ -182,7 +182,7 @@ describe('Schedule', () => {
|
|
182
182
|
|
183
183
|
expect(startsBeforeDateField().value).toBe('2019/03/11');
|
184
184
|
expect(
|
185
|
-
screen.getAllByText("'Starts before' date must in the future")
|
185
|
+
screen.getAllByText("'Starts before' date must be in the future")
|
186
186
|
).toHaveLength(2);
|
187
187
|
});
|
188
188
|
|
@@ -329,7 +329,7 @@ describe('Schedule', () => {
|
|
329
329
|
fireEvent.click(
|
330
330
|
screen.getByRole('button', { name: 'Recurring execution' })
|
331
331
|
);
|
332
|
-
jest.
|
332
|
+
jest.advanceTimersByTime(1000);
|
333
333
|
});
|
334
334
|
expect(screen.queryAllByText('Recurring execution')).toHaveLength(3);
|
335
335
|
|
@@ -22,6 +22,7 @@ export const DateTimePicker = ({
|
|
22
22
|
ariaLabel,
|
23
23
|
allowEmpty,
|
24
24
|
includeSeconds,
|
25
|
+
isFutureOnly,
|
25
26
|
}) => {
|
26
27
|
const [validated, setValidated] = useState();
|
27
28
|
const dateFormat = date =>
|
@@ -87,6 +88,15 @@ export const DateTimePicker = ({
|
|
87
88
|
setValidated(ValidatedOptions.error);
|
88
89
|
}
|
89
90
|
};
|
91
|
+
const validateFuture = date => {
|
92
|
+
if (
|
93
|
+
isFutureOnly &&
|
94
|
+
date.setHours(1, 0, 0, 0) < new Date().setHours(0, 0, 0, 0)
|
95
|
+
) {
|
96
|
+
return __('Date must be in the future');
|
97
|
+
}
|
98
|
+
return '';
|
99
|
+
};
|
90
100
|
return (
|
91
101
|
<>
|
92
102
|
<DatePicker
|
@@ -105,6 +115,7 @@ export const DateTimePicker = ({
|
|
105
115
|
validated === ValidatedOptions.error ? __('Invalid date') : ''
|
106
116
|
}
|
107
117
|
inputProps={{ validated }}
|
118
|
+
validators={[validateFuture]}
|
108
119
|
/>
|
109
120
|
<TimePicker
|
110
121
|
aria-label={`${ariaLabel} timepicker`}
|
@@ -132,6 +143,7 @@ DateTimePicker.propTypes = {
|
|
132
143
|
ariaLabel: PropTypes.string,
|
133
144
|
allowEmpty: PropTypes.bool,
|
134
145
|
includeSeconds: PropTypes.bool,
|
146
|
+
isFutureOnly: PropTypes.bool,
|
135
147
|
};
|
136
148
|
DateTimePicker.defaultProps = {
|
137
149
|
dateTime: null,
|
@@ -139,4 +151,5 @@ DateTimePicker.defaultProps = {
|
|
139
151
|
ariaLabel: '',
|
140
152
|
allowEmpty: true,
|
141
153
|
includeSeconds: false,
|
154
|
+
isFutureOnly: true,
|
142
155
|
};
|
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
|
3
3
|
import { Select, SelectOption, SelectVariant } from '@patternfly/react-core';
|
4
4
|
import Immutable from 'seamless-immutable';
|
5
5
|
import { sprintf, translate as __ } from 'foremanReact/common/I18n';
|
6
|
+
import { useForemanSettings } from 'foremanReact/Root/Context/ForemanContext';
|
6
7
|
import { useSelector, useDispatch } from 'react-redux';
|
7
8
|
import URI from 'urijs';
|
8
9
|
import { get } from 'foremanReact/redux/API';
|
@@ -17,7 +18,8 @@ export const ResourceSelect = ({
|
|
17
18
|
apiKey,
|
18
19
|
url,
|
19
20
|
}) => {
|
20
|
-
const
|
21
|
+
const { perPage } = useForemanSettings();
|
22
|
+
const maxResults = perPage;
|
21
23
|
const dispatch = useDispatch();
|
22
24
|
const uri = new URI(url);
|
23
25
|
const onSearch = search => {
|
@@ -50,8 +52,7 @@ export const ResourceSelect = ({
|
|
50
52
|
description={__('Please refine your search.')}
|
51
53
|
>
|
52
54
|
{sprintf(
|
53
|
-
__('You have
|
54
|
-
response.subtotal,
|
55
|
+
__('You have more results to display. Showing first %s results'),
|
55
56
|
maxResults
|
56
57
|
)}
|
57
58
|
</SelectOption>,
|
@@ -59,17 +60,35 @@ export const ResourceSelect = ({
|
|
59
60
|
}
|
60
61
|
selectOptions = [
|
61
62
|
...selectOptions,
|
62
|
-
...Immutable.asMutable(response?.results || [])
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
63
|
+
...Immutable.asMutable(response?.results || [])
|
64
|
+
?.slice(0, maxResults)
|
65
|
+
?.map((result, index) => (
|
66
|
+
<SelectOption key={index + 1} value={result.id}>
|
67
|
+
{result.name}
|
68
|
+
</SelectOption>
|
69
|
+
)),
|
67
70
|
];
|
68
71
|
|
69
72
|
const onSelect = (event, selection) => {
|
70
73
|
setSelected(selection);
|
71
74
|
setIsOpen(false);
|
72
75
|
};
|
76
|
+
const onFilter = (_, value) => {
|
77
|
+
if (!value) {
|
78
|
+
return selectOptions;
|
79
|
+
}
|
80
|
+
return selectOptions.filter(
|
81
|
+
o =>
|
82
|
+
typeof o.props.children === 'string' &&
|
83
|
+
o.props.children.toLowerCase().indexOf(value.toLowerCase()) > -1
|
84
|
+
);
|
85
|
+
};
|
86
|
+
const onClear = () => {
|
87
|
+
setSelected(null);
|
88
|
+
setIsOpen(false);
|
89
|
+
if (typingTimeout) clearTimeout(typingTimeout);
|
90
|
+
onSearch({});
|
91
|
+
};
|
73
92
|
const autoSearch = searchTerm => {
|
74
93
|
if (typingTimeout) clearTimeout(typingTimeout);
|
75
94
|
setTypingTimeout(
|
@@ -86,11 +105,17 @@ export const ResourceSelect = ({
|
|
86
105
|
loadingVariant={isLoading ? 'spinner' : null}
|
87
106
|
onSelect={onSelect}
|
88
107
|
onToggle={setIsOpen}
|
108
|
+
onFilter={onFilter}
|
109
|
+
onClear={onClear}
|
89
110
|
isOpen={isOpen}
|
90
111
|
className="without_select2"
|
91
112
|
maxHeight="45vh"
|
92
113
|
onTypeaheadInputChanged={value => {
|
93
|
-
|
114
|
+
if (value) {
|
115
|
+
autoSearch(value);
|
116
|
+
} else {
|
117
|
+
onClear();
|
118
|
+
}
|
94
119
|
}}
|
95
120
|
placeholderText={placeholderText}
|
96
121
|
typeAheadAriaLabel={`${name} typeahead input`}
|
data/webpack/Routes/routes.js
CHANGED
@@ -2,6 +2,7 @@ import React from 'react';
|
|
2
2
|
import JobWizardPage from '../JobWizard';
|
3
3
|
import JobWizardPageRerun from '../JobWizard/JobWizardPageRerun';
|
4
4
|
import JobInvocationDetailPage from '../JobInvocationDetail';
|
5
|
+
import TemplateInvocationPage from '../JobInvocationDetail/TemplateInvocationPage';
|
5
6
|
|
6
7
|
const ForemanREXRoutes = [
|
7
8
|
{
|
@@ -19,6 +20,11 @@ const ForemanREXRoutes = [
|
|
19
20
|
exact: true,
|
20
21
|
render: props => <JobInvocationDetailPage {...props} />,
|
21
22
|
},
|
23
|
+
{
|
24
|
+
path: '/job_invocations_detail/:jobID/host_invocation/:hostID',
|
25
|
+
exact: true,
|
26
|
+
render: props => <TemplateInvocationPage {...props} />,
|
27
|
+
},
|
22
28
|
];
|
23
29
|
|
24
30
|
export default ForemanREXRoutes;
|
@@ -3,3 +3,4 @@ export const useForemanLocation = () => ({ id: 2 });
|
|
3
3
|
export const useForemanVersion = () => '3.7';
|
4
4
|
export const useForemanHostsPageUrl = () => '/hosts';
|
5
5
|
export const useForemanHostDetailsPageUrl = () => '/hosts/';
|
6
|
+
export const useForemanSettings = () => ({ perPage: 20 });
|
@@ -1,8 +1,10 @@
|
|
1
|
+
/* eslint-disable camelcase */
|
1
2
|
import React from 'react';
|
2
3
|
import PropTypes from 'prop-types';
|
3
4
|
|
4
5
|
import { translate as __ } from 'foremanReact/common/I18n';
|
5
6
|
import LabelIcon from 'foremanReact/components/common/LabelIcon';
|
7
|
+
import { Alert } from 'patternfly-react';
|
6
8
|
|
7
9
|
import {
|
8
10
|
FormGroup,
|
@@ -23,6 +25,25 @@ const options = (value = '') => {
|
|
23
25
|
);
|
24
26
|
};
|
25
27
|
|
28
|
+
const pullWarning = (
|
29
|
+
<Alert type="info" isInline style={{ marginTop: '10px' }}>
|
30
|
+
{__(
|
31
|
+
'Please make sure that the Smart Proxy is configured correctly for the Pull provider.'
|
32
|
+
)}
|
33
|
+
</Alert>
|
34
|
+
);
|
35
|
+
|
36
|
+
function showPullWarning(valueFromParam, value) {
|
37
|
+
if (value === 'true') {
|
38
|
+
return pullWarning;
|
39
|
+
}
|
40
|
+
if (valueFromParam && (value === undefined || value === '')) {
|
41
|
+
return pullWarning;
|
42
|
+
}
|
43
|
+
|
44
|
+
return null;
|
45
|
+
}
|
46
|
+
|
26
47
|
const RexPull = ({ isLoading, onChange, pluginValues, configParams }) => (
|
27
48
|
<FormGroup
|
28
49
|
label={__('REX pull mode')}
|
@@ -45,9 +66,13 @@ const RexPull = ({ isLoading, onChange, pluginValues, configParams }) => (
|
|
45
66
|
id="registration_setup_remote_execution_pull"
|
46
67
|
isDisabled={isLoading}
|
47
68
|
>
|
48
|
-
{
|
49
|
-
options(configParams?.host_registration_remote_execution_pull)}
|
69
|
+
{options(configParams?.host_registration_remote_execution_pull)}
|
50
70
|
</FormSelect>
|
71
|
+
|
72
|
+
{showPullWarning(
|
73
|
+
configParams?.host_registration_remote_execution_pull,
|
74
|
+
pluginValues.setupRemoteExecutionPull
|
75
|
+
)}
|
51
76
|
</FormGroup>
|
52
77
|
);
|
53
78
|
|