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.
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')}