foreman_remote_execution 14.1.4 → 15.0.0
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.
- 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
|
|