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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/api/v2/job_invocations_controller.rb +8 -0
  3. data/app/controllers/template_invocations_controller.rb +57 -0
  4. data/app/controllers/ui_job_wizard_controller.rb +6 -3
  5. data/app/helpers/remote_execution_helper.rb +5 -6
  6. data/app/views/api/v2/job_invocations/base.json.rabl +1 -1
  7. data/app/views/api/v2/job_invocations/hosts.json.rabl +1 -1
  8. data/app/views/api/v2/job_invocations/main.json.rabl +1 -1
  9. data/app/views/api/v2/job_invocations/show.json.rabl +18 -0
  10. data/app/views/templates/script/convert2rhel_analyze.erb +4 -4
  11. data/config/routes.rb +2 -0
  12. data/lib/foreman_remote_execution/engine.rb +3 -3
  13. data/lib/foreman_remote_execution/version.rb +1 -1
  14. data/webpack/JobInvocationDetail/JobAdditionInfo.js +214 -0
  15. data/webpack/JobInvocationDetail/JobInvocationConstants.js +40 -2
  16. data/webpack/JobInvocationDetail/JobInvocationDetail.scss +70 -0
  17. data/webpack/JobInvocationDetail/JobInvocationHostTable.js +177 -80
  18. data/webpack/JobInvocationDetail/JobInvocationHostTableToolbar.js +63 -0
  19. data/webpack/JobInvocationDetail/JobInvocationSelectors.js +8 -1
  20. data/webpack/JobInvocationDetail/JobInvocationSystemStatusChart.js +61 -10
  21. data/webpack/JobInvocationDetail/OpenAlInvocations.js +111 -0
  22. data/webpack/JobInvocationDetail/TemplateInvocation.js +202 -0
  23. data/webpack/JobInvocationDetail/TemplateInvocationComponents/OutputCodeBlock.js +124 -0
  24. data/webpack/JobInvocationDetail/TemplateInvocationComponents/OutputToggleGroup.js +156 -0
  25. data/webpack/JobInvocationDetail/TemplateInvocationComponents/PreviewTemplate.js +50 -0
  26. data/webpack/JobInvocationDetail/TemplateInvocationComponents/TemplateActionButtons.js +224 -0
  27. data/webpack/JobInvocationDetail/TemplateInvocationPage.js +53 -0
  28. data/webpack/JobInvocationDetail/__tests__/MainInformation.test.js +1 -1
  29. data/webpack/JobInvocationDetail/__tests__/OpenAlInvocations.test.js +110 -0
  30. data/webpack/JobInvocationDetail/__tests__/OutputCodeBlock.test.js +69 -0
  31. data/webpack/JobInvocationDetail/__tests__/TemplateInvocation.test.js +131 -0
  32. data/webpack/JobInvocationDetail/__tests__/fixtures.js +130 -0
  33. data/webpack/JobInvocationDetail/index.js +18 -3
  34. data/webpack/JobWizard/JobWizard.js +38 -16
  35. data/webpack/JobWizard/{StartsBeforeErrorAlert.js → StartsErrorAlert.js} +16 -1
  36. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +1 -1
  37. data/webpack/JobWizard/steps/Schedule/ScheduleFuture.js +1 -1
  38. data/webpack/JobWizard/steps/Schedule/ScheduleRecurring.js +5 -3
  39. data/webpack/JobWizard/steps/Schedule/ScheduleType.js +1 -1
  40. data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +3 -3
  41. data/webpack/JobWizard/steps/form/DateTimePicker.js +13 -0
  42. data/webpack/JobWizard/steps/form/Formatter.js +1 -0
  43. data/webpack/JobWizard/steps/form/ResourceSelect.js +34 -9
  44. data/webpack/Routes/routes.js +6 -0
  45. data/webpack/__mocks__/foremanReact/Root/Context/ForemanContext/index.js +1 -0
  46. data/webpack/react_app/components/RegistrationExtension/RexPull.js +27 -2
  47. data/webpack/react_app/components/TargetingHosts/components/HostStatus.js +1 -1
  48. 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
- }, [dispatch, task]);
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 './StartsBeforeErrorAlert';
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 updateStartsBeforeError = () => {
208
- setIsStartsBeforeError(
209
- scheduleValue.scheduleType === SCHEDULE_TYPES.FUTURE &&
210
- new Date().getTime() >= new Date(scheduleValue.startsBefore).getTime()
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
- let interval;
214
- if (scheduleValue.scheduleType === SCHEDULE_TYPES.FUTURE) {
215
- updateStartsBeforeError();
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.scheduleType, scheduleValue.startsBefore]);
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
+ );
@@ -399,7 +399,7 @@ describe('AdvancedFields', () => {
399
399
  target: { value: 'some search' },
400
400
  });
401
401
 
402
- jest.runAllTimers();
402
+ jest.advanceTimersByTime(10000);
403
403
  });
404
404
  expect(newStore.getActions()).toMatchSnapshot('resource search');
405
405
  });
@@ -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.length &&
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.length) {
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().toISOString(),
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.runAllTimers();
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
  };
@@ -143,6 +143,7 @@ export const formatter = (input, values, setValue) => {
143
143
  isRequired={required}
144
144
  >
145
145
  <DateTimePicker
146
+ isFutureOnly={false}
146
147
  ariaLabel={name}
147
148
  className={hidden ? 'masked-input' : null}
148
149
  id={id}
@@ -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 maxResults = 100;
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 %s results to display. Showing first %s results'),
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 || [])?.map((result, index) => (
63
- <SelectOption key={index + 1} value={result.id}>
64
- {result.name}
65
- </SelectOption>
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
- autoSearch(value || '');
114
+ if (value) {
115
+ autoSearch(value);
116
+ } else {
117
+ onClear();
118
+ }
94
119
  }}
95
120
  placeholderText={placeholderText}
96
121
  typeAheadAriaLabel={`${name} typeahead input`}
@@ -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
- {/* eslint-disable-next-line camelcase */
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
 
@@ -17,7 +17,7 @@ const HostStatus = ({ status }) => {
17
17
  <Icon type="fa" name="question" /> {__('Awaiting start')}
18
18
  </div>
19
19
  );
20
- case 'running':
20
+ case 'running' || 'pending':
21
21
  return (
22
22
  <div>
23
23
  <Icon type="pf" name="running" /> {__('Pending')}