foreman_remote_execution 9.1.0 → 10.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/release.yml +14 -0
  3. data/.packit.yaml +40 -0
  4. data/.tx/config +3 -1
  5. data/app/assets/javascripts/foreman_remote_execution/locale/de/foreman_remote_execution.js +1 -0
  6. data/app/assets/javascripts/foreman_remote_execution/locale/en/foreman_remote_execution.js +1 -0
  7. data/app/assets/javascripts/foreman_remote_execution/locale/en_GB/foreman_remote_execution.js +1 -0
  8. data/app/assets/javascripts/foreman_remote_execution/locale/es/foreman_remote_execution.js +1 -0
  9. data/app/assets/javascripts/foreman_remote_execution/locale/fr/foreman_remote_execution.js +1 -0
  10. data/app/assets/javascripts/foreman_remote_execution/locale/ja/foreman_remote_execution.js +1 -0
  11. data/app/assets/javascripts/foreman_remote_execution/locale/ko/foreman_remote_execution.js +1 -0
  12. data/app/assets/javascripts/foreman_remote_execution/locale/pt_BR/foreman_remote_execution.js +1 -0
  13. data/app/assets/javascripts/foreman_remote_execution/locale/ru/foreman_remote_execution.js +1 -0
  14. data/app/assets/javascripts/foreman_remote_execution/locale/zh_CN/foreman_remote_execution.js +1 -0
  15. data/app/assets/javascripts/foreman_remote_execution/locale/zh_TW/foreman_remote_execution.js +1 -0
  16. data/app/assets/javascripts/foreman_remote_execution/template_invocation.js +10 -1
  17. data/app/controllers/job_invocations_controller.rb +20 -1
  18. data/app/controllers/ui_job_wizard_controller.rb +0 -3
  19. data/app/lib/actions/remote_execution/run_host_job.rb +1 -1
  20. data/app/views/job_invocations/_form.html.erb +1 -1
  21. data/app/views/templates/script/run_command.erb +1 -0
  22. data/config/routes.rb +1 -0
  23. data/lib/foreman_remote_execution/engine.rb +8 -8
  24. data/lib/foreman_remote_execution/tasks/explain_proxy_selection.rake +12 -3
  25. data/lib/foreman_remote_execution/version.rb +1 -1
  26. data/locale/Makefile +6 -3
  27. data/locale/de/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  28. data/locale/de/foreman_remote_execution.po +53 -8
  29. data/locale/en/foreman_remote_execution.po +52 -7
  30. data/locale/en_GB/foreman_remote_execution.po +52 -7
  31. data/locale/es/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  32. data/locale/es/foreman_remote_execution.po +56 -11
  33. data/locale/foreman_remote_execution.pot +193 -126
  34. data/locale/fr/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  35. data/locale/fr/foreman_remote_execution.po +56 -11
  36. data/locale/ja/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  37. data/locale/ja/foreman_remote_execution.po +56 -11
  38. data/locale/ko/foreman_remote_execution.po +52 -7
  39. data/locale/pt_BR/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  40. data/locale/pt_BR/foreman_remote_execution.po +56 -11
  41. data/locale/ru/foreman_remote_execution.po +52 -7
  42. data/locale/zh_CN/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  43. data/locale/zh_CN/foreman_remote_execution.po +56 -11
  44. data/locale/zh_TW/foreman_remote_execution.po +52 -7
  45. data/webpack/JobWizard/Footer.js +104 -0
  46. data/webpack/JobWizard/JobWizard.js +24 -16
  47. data/webpack/JobWizard/JobWizard.scss +6 -17
  48. data/webpack/JobWizard/JobWizardConstants.js +1 -1
  49. data/webpack/JobWizard/JobWizardPageRerun.js +2 -0
  50. data/webpack/JobWizard/__tests__/fixtures.js +8 -1
  51. data/webpack/JobWizard/__tests__/validation.test.js +9 -0
  52. data/webpack/JobWizard/steps/HostsAndInputs/HostPreviewModal.js +2 -0
  53. data/webpack/JobWizard/steps/HostsAndInputs/SelectedChips.js +28 -14
  54. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/HostsAndInputs.test.js +3 -3
  55. data/webpack/JobWizard/steps/ReviewDetails/ReviewDetails.test.js +117 -0
  56. data/webpack/JobWizard/steps/ReviewDetails/helpers.js +43 -0
  57. data/webpack/JobWizard/steps/ReviewDetails/index.js +169 -18
  58. data/webpack/JobWizard/steps/Schedule/QueryType.js +1 -1
  59. data/webpack/JobWizard/steps/Schedule/RepeatHour.js +0 -1
  60. data/webpack/JobWizard/steps/Schedule/RepeatWeek.js +1 -1
  61. data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +19 -5
  62. data/webpack/JobWizard/steps/form/DateTimePicker.js +0 -1
  63. data/webpack/JobWizard/steps/form/GroupedSelectField.js +0 -1
  64. data/webpack/JobWizard/steps/form/SelectField.js +0 -1
  65. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +4 -4
  66. data/webpack/react_app/components/RecentJobsCard/RecentJobsTable.js +2 -2
  67. data/webpack/react_app/components/RecentJobsCard/constants.js +2 -2
  68. data/webpack/react_app/components/RegistrationExtension/RexPull.js +0 -2
  69. metadata +19 -3
@@ -323,6 +323,13 @@ export const jobInvocation = {
323
323
  created_in_katello: false,
324
324
  },
325
325
  inputs: {
326
- 'inputs[plain hidden]': 'test command',
326
+ 'inputs[adv plain hidden]': {
327
+ advanced: true,
328
+ value: 'adv_test_command',
329
+ },
330
+ 'inputs[plain hidden]': {
331
+ advanced: false,
332
+ value: 'test command',
333
+ },
327
334
  },
328
335
  };
@@ -154,6 +154,10 @@ describe('Job wizard validation', () => {
154
154
  });
155
155
 
156
156
  expect(screen.getByText(WIZARD_TITLES.schedule)).toBeDisabled();
157
+ expect(screen.getByText('Run on selected hosts')).toHaveAttribute(
158
+ 'aria-disabled',
159
+ 'true'
160
+ );
157
161
  expect(screen.getByText(WIZARD_TITLES.review)).toBeDisabled();
158
162
 
159
163
  await act(async () => {
@@ -162,6 +166,11 @@ describe('Job wizard validation', () => {
162
166
  });
163
167
  });
164
168
 
169
+ expect(screen.getByText('Run on selected hosts')).toBeEnabled();
170
+ expect(screen.getByText('Run on selected hosts')).toHaveAttribute(
171
+ 'aria-disabled',
172
+ 'false'
173
+ );
165
174
  expect(screen.getByText(WIZARD_TITLES.schedule)).toBeEnabled();
166
175
  expect(screen.getByText(WIZARD_TITLES.review)).toBeEnabled();
167
176
  });
@@ -29,6 +29,7 @@ export const HostPreviewModal = ({ isOpen, setIsOpen, searchQuery }) => {
29
29
  variant="link"
30
30
  target="_blank"
31
31
  rel="noreferrer"
32
+ isInline
32
33
  >
33
34
  {host}
34
35
  </Button>
@@ -42,6 +43,7 @@ export const HostPreviewModal = ({ isOpen, setIsOpen, searchQuery }) => {
42
43
  variant="link"
43
44
  target="_blank"
44
45
  rel="noreferrer"
46
+ isInline
45
47
  >
46
48
  {sprintf(
47
49
  __('...and %s more'),
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import { Chip, ChipGroup, Button } from '@patternfly/react-core';
4
- import { translate as __ } from 'foremanReact/common/I18n';
4
+ import { sprintf, translate as __ } from 'foremanReact/common/I18n';
5
5
  import { hostMethods } from '../../JobWizardConstants';
6
6
 
7
7
  const SelectedChip = ({ selected, setSelected, categoryName }) => {
@@ -10,19 +10,33 @@ const SelectedChip = ({ selected, setSelected, categoryName }) => {
10
10
  oldSelected.filter(({ id }) => id !== itemToRemove)
11
11
  );
12
12
  };
13
+ const NUM_CHIPS = 3;
13
14
  return (
14
- <ChipGroup className="hosts-chip-group" categoryName={categoryName}>
15
- {selected.map(({ name, id }, index) => (
16
- <Chip
17
- key={index}
18
- id={`${categoryName}-${id}`}
19
- onClick={() => deleteItem(id)}
20
- closeBtnAriaLabel={`Close ${name}`}
21
- >
22
- {name}
23
- </Chip>
24
- ))}
25
- </ChipGroup>
15
+ <>
16
+ <ChipGroup
17
+ className="hosts-chip-group"
18
+ categoryName={categoryName}
19
+ isClosable
20
+ closeBtnAriaLabel="Remove all"
21
+ collapsedText={sprintf(__('%s more'), selected.length - NUM_CHIPS)}
22
+ numChips={NUM_CHIPS}
23
+ onClick={() => {
24
+ setSelected(() => []);
25
+ }}
26
+ >
27
+ {selected.map(({ name, id }, index) => (
28
+ <Chip
29
+ key={index}
30
+ id={`${categoryName}-${id}`}
31
+ onClick={() => deleteItem(id)}
32
+ closeBtnAriaLabel={`Remove ${name}`}
33
+ >
34
+ {name}
35
+ </Chip>
36
+ ))}
37
+ </ChipGroup>
38
+ {selected.length > 0 && <br />}
39
+ </>
26
40
  );
27
41
  };
28
42
 
@@ -75,7 +89,7 @@ export const SelectedChips = ({
75
89
  />
76
90
  {showClear && (
77
91
  <Button variant="link" className="clear-chips" onClick={clearAll}>
78
- {__('Clear filters')}
92
+ {__('Clear all filters')}
79
93
  </Button>
80
94
  )}
81
95
  </div>
@@ -80,7 +80,7 @@ describe('Hosts', () => {
80
80
  expect(screen.queryAllByText('host1')).toHaveLength(1);
81
81
  expect(screen.queryAllByText('host2')).toHaveLength(1);
82
82
  expect(screen.queryAllByText('host3')).toHaveLength(0);
83
- const chip1 = screen.getByRole('button', { name: 'Close host1 host1' });
83
+ const chip1 = screen.getByRole('button', { name: 'Remove host1 host1' });
84
84
  await act(async () => {
85
85
  fireEvent.click(chip1);
86
86
  });
@@ -91,7 +91,7 @@ describe('Hosts', () => {
91
91
  expect(screen.queryAllByText('host_collection1')).toHaveLength(1);
92
92
 
93
93
  await act(async () => {
94
- fireEvent.click(screen.getByText('Category and Template'));
94
+ fireEvent.click(screen.getByText('Category and template'));
95
95
  });
96
96
  await act(async () => {
97
97
  fireEvent.click(screen.getByText('Target hosts and inputs'));
@@ -100,7 +100,7 @@ describe('Hosts', () => {
100
100
  expect(screen.queryAllByText('host_group1')).toHaveLength(1);
101
101
 
102
102
  await act(async () => {
103
- fireEvent.click(screen.getByText('Clear filters'));
103
+ fireEvent.click(screen.getByText('Clear all filters'));
104
104
  });
105
105
 
106
106
  expect(screen.queryAllByText('host2')).toHaveLength(0);
@@ -0,0 +1,117 @@
1
+ import React from 'react';
2
+ import { Provider } from 'react-redux';
3
+ import { render, fireEvent, screen, act } from '@testing-library/react';
4
+ import { MockedProvider } from '@apollo/client/testing';
5
+ import * as APIHooks from 'foremanReact/common/hooks/API/APIHooks';
6
+ import * as api from 'foremanReact/redux/API';
7
+
8
+ import JobWizardPageRerun from '../../JobWizardPageRerun';
9
+ import * as selectors from '../../JobWizardSelectors';
10
+ import {
11
+ testSetup,
12
+ mockApi,
13
+ gqlMock,
14
+ jobInvocation,
15
+ } from '../../__tests__/fixtures';
16
+
17
+ const store = testSetup(selectors, api);
18
+ mockApi(api);
19
+ jest.spyOn(APIHooks, 'useAPI');
20
+ APIHooks.useAPI.mockImplementation((action, url) => {
21
+ if (url === '/ui_job_wizard/job_invocation?id=57') {
22
+ return { response: jobInvocation, status: 'RESOLVED' };
23
+ }
24
+ return {};
25
+ });
26
+ jest.useFakeTimers();
27
+
28
+ describe('ReviewDetails', () => {
29
+ it('should call goToStepByName function when StepButton is clicked', async () => {
30
+ render(
31
+ <MockedProvider mocks={gqlMock} addTypename={false}>
32
+ <Provider store={store}>
33
+ <JobWizardPageRerun
34
+ match={{
35
+ params: { id: '57' },
36
+ }}
37
+ />
38
+ </Provider>
39
+ </MockedProvider>
40
+ );
41
+
42
+ act(() => {
43
+ fireEvent.click(screen.getByText('Type of execution'));
44
+ });
45
+ act(() => {
46
+ fireEvent.click(screen.getByText('Future execution'));
47
+
48
+ jest.advanceTimersByTime(1000); // to handle pf4 date picker popover useTimer
49
+ });
50
+ act(() => {
51
+ fireEvent.click(screen.getByRole('button', { name: 'Future execution' }));
52
+ jest.advanceTimersByTime(1000); // to handle pf4 date picker popover useTimer
53
+ });
54
+
55
+ const newStartAtDate = '2030/03/12';
56
+ const newStartAtTime = '14:27';
57
+ const startsAtDateField = () =>
58
+ screen.getByLabelText('starts at datepicker');
59
+ const startsAtTimeField = () =>
60
+ screen.getByLabelText('starts at timepicker');
61
+
62
+ await act(async () => {
63
+ await fireEvent.change(startsAtDateField(), {
64
+ target: { value: newStartAtDate },
65
+ });
66
+ fireEvent.change(startsAtTimeField(), {
67
+ target: { value: newStartAtTime },
68
+ });
69
+ jest.advanceTimersByTime(1000);
70
+ });
71
+
72
+ act(() => {
73
+ fireEvent.click(screen.getByText('Review details'));
74
+ });
75
+ expect(screen.getAllByText('Review details')).toHaveLength(3);
76
+ fireEvent.click(
77
+ screen.getByText('Job template', {
78
+ selector: '.pf-c-button',
79
+ })
80
+ );
81
+ expect(screen.getAllByText('Category and template')).toHaveLength(3);
82
+
83
+ await act(async () => {
84
+ fireEvent.click(screen.getByText('Review details'));
85
+ jest.advanceTimersByTime(1000);
86
+ });
87
+ act(() => {
88
+ fireEvent.click(
89
+ screen.getByText('Target hosts', {
90
+ selector: '.pf-c-button',
91
+ })
92
+ );
93
+ jest.advanceTimersByTime(1000); // to handle pf4 date picker popover useTimer
94
+ });
95
+ expect(screen.getAllByText('Target hosts and inputs')).toHaveLength(3);
96
+ act(() => {
97
+ fireEvent.click(screen.getByText('Review details'));
98
+ });
99
+ act(() => {
100
+ fireEvent.click(
101
+ screen.getByText('Advanced fields', {
102
+ selector: '.pf-c-button',
103
+ })
104
+ );
105
+ jest.advanceTimersByTime(1000);
106
+ });
107
+ expect(screen.getAllByText('Advanced fields')).toHaveLength(3);
108
+
109
+ act(() => {
110
+ fireEvent.click(screen.getByText('Review details'));
111
+ });
112
+ act(() => {
113
+ fireEvent.click(screen.getByText('Recurrence'));
114
+ });
115
+ expect(screen.getAllByText('Schedule')).toHaveLength(3);
116
+ });
117
+ });
@@ -0,0 +1,43 @@
1
+ import { translate as __, sprintf } from 'foremanReact/common/I18n';
2
+ import { getWeekDays } from '../Schedule/RepeatWeek';
3
+ import { repeatTypes } from '../../JobWizardConstants';
4
+
5
+ export const parseEnd = (ends, isNeverEnds, repeatAmount) => {
6
+ if (isNeverEnds) {
7
+ return __('Never');
8
+ }
9
+ if (ends) {
10
+ const endsDate = new Date(ends);
11
+ return endsDate.toString();
12
+ }
13
+ return sprintf(__('After %s occurences'), repeatAmount);
14
+ };
15
+
16
+ export const parseRepeat = (repeatType, repeatData) => {
17
+ switch (repeatType) {
18
+ case repeatTypes.hourly:
19
+ return sprintf(__('Every hour at minute %s'), repeatData.minute);
20
+ case repeatTypes.daily:
21
+ return sprintf(__('Every day at %s'), repeatData.at);
22
+ case repeatTypes.weekly: {
23
+ const daysKeys = Object.keys(repeatData.daysOfWeek).filter(
24
+ k => repeatData.daysOfWeek[k]
25
+ );
26
+ const days = getWeekDays()
27
+ .filter((d, index) => index in daysKeys)
28
+ .join(', ');
29
+ return sprintf(__('Every week on %s at %s'), days, repeatData.at);
30
+ }
31
+ case repeatTypes.monthly:
32
+ return sprintf(
33
+ __('Every month on %s at %s'),
34
+ repeatData.days,
35
+ repeatData.at
36
+ );
37
+ case repeatTypes.cronline:
38
+ return `${__('Cron line')} - ${repeatData.cronline}`;
39
+
40
+ default:
41
+ return '';
42
+ }
43
+ };
@@ -1,3 +1,4 @@
1
+ /* eslint-disable max-lines */
1
2
  import React, { useEffect, useState } from 'react';
2
3
  import {
3
4
  Button,
@@ -5,12 +6,12 @@ import {
5
6
  DescriptionListTerm,
6
7
  DescriptionListGroup,
7
8
  DescriptionListDescription,
9
+ WizardContextConsumer,
8
10
  } from '@patternfly/react-core';
9
11
  import PropTypes from 'prop-types';
10
12
  import { useDispatch, useSelector } from 'react-redux';
11
- import EllipsisWithTooltip from 'react-ellipsis-with-tooltip';
12
13
  import { get } from 'foremanReact/redux/API';
13
- import { translate as __, sprintf } from 'foremanReact/common/I18n';
14
+ import { translate as __ } from 'foremanReact/common/I18n';
14
15
  import {
15
16
  selectJobTemplates,
16
17
  selectHosts,
@@ -22,9 +23,12 @@ import {
22
23
  HOSTS_API,
23
24
  HOSTS_TO_PREVIEW_AMOUNT,
24
25
  WIZARD_TITLES,
26
+ SCHEDULE_TYPES,
25
27
  } from '../../JobWizardConstants';
26
28
  import { buildHostQuery } from '../HostsAndInputs/buildHostQuery';
27
29
  import { WizardTitle } from '../form/WizardTitle';
30
+ import { parseEnd, parseRepeat } from './helpers';
31
+ import { HostPreviewModal } from '../HostsAndInputs/HostPreviewModal';
28
32
 
29
33
  const ReviewDetails = ({
30
34
  jobCategory,
@@ -34,7 +38,20 @@ const ReviewDetails = ({
34
38
  templateValues,
35
39
  selectedTargets,
36
40
  hostsSearchQuery,
41
+ goToStepByName,
37
42
  }) => {
43
+ // eslint-disable-next-line react/prop-types
44
+ const StepButton = ({ stepName, children }) => (
45
+ <Button
46
+ variant="link"
47
+ isInline
48
+ onClick={() => {
49
+ goToStepByName(stepName);
50
+ }}
51
+ >
52
+ {children}
53
+ </Button>
54
+ );
38
55
  const dispatch = useDispatch();
39
56
  useEffect(() => {
40
57
  dispatch(
@@ -58,6 +75,7 @@ const ReviewDetails = ({
58
75
  const hosts = useSelector(selectHosts);
59
76
 
60
77
  const hostsCount = useSelector(selectHostCount);
78
+ const [hostPreviewOpen, setHostPreviewOpen] = useState(false);
61
79
  const stringHosts = () => {
62
80
  if (hosts.length === 0) {
63
81
  return __('No Target Hosts');
@@ -65,22 +83,57 @@ const ReviewDetails = ({
65
83
  if (hosts.length === 1 || hosts.length === 2) {
66
84
  return hosts.join(', ');
67
85
  }
68
- return `${hosts[0]}, ${hosts[1]} ${sprintf(
69
- __(', and %s more'),
70
- hostsCount - 2
71
- )}`;
86
+ return (
87
+ <div>
88
+ {hostsCount} {__('hosts')}{' '}
89
+ <Button
90
+ variant="link"
91
+ isInline
92
+ onClick={() => setHostPreviewOpen(true)}
93
+ >
94
+ {__('view host names')}
95
+ </Button>
96
+ </div>
97
+ );
72
98
  };
73
99
  const [isAdvancedShown, setIsAdvancedShown] = useState(false);
74
100
  const detailsFirstHalf = [
75
- { label: __('Job category'), value: jobCategory },
76
- { label: __('Job template'), value: jobTemplate },
77
- { label: __('Target hosts'), value: stringHosts() },
101
+ {
102
+ label: (
103
+ <StepButton stepName={WIZARD_TITLES.categoryAndTemplate}>
104
+ {__('Job category')}
105
+ </StepButton>
106
+ ),
107
+ value: jobCategory,
108
+ },
109
+ {
110
+ label: (
111
+ <StepButton stepName={WIZARD_TITLES.categoryAndTemplate}>
112
+ {__('Job template')}
113
+ </StepButton>
114
+ ),
115
+ value: jobTemplate,
116
+ },
117
+ {
118
+ label: (
119
+ <StepButton stepName={WIZARD_TITLES.hostsAndInputs}>
120
+ {__('Target hosts')}
121
+ </StepButton>
122
+ ),
123
+ value: stringHosts(),
124
+ },
78
125
  ...templateInputs.map(({ name }) => ({
79
- label: name,
126
+ label: (
127
+ <StepButton stepName={WIZARD_TITLES.hostsAndInputs}>{name}</StepButton>
128
+ ),
80
129
  value: templateValues[name],
81
130
  })),
82
131
  {
83
- label: __('Advanced fields'),
132
+ label: (
133
+ <StepButton stepName={WIZARD_TITLES.advanced}>
134
+ {__('Advanced fields')}
135
+ </StepButton>
136
+ ),
84
137
  value: isAdvancedShown ? (
85
138
  <Button
86
139
  variant="link"
@@ -107,15 +160,98 @@ const ReviewDetails = ({
107
160
 
108
161
  const detailsSecondHalf = [
109
162
  {
110
- label: __('Schedule type'),
163
+ label: (
164
+ <StepButton stepName={WIZARD_TITLES.typeOfExecution}>
165
+ {__('Schedule type')}
166
+ </StepButton>
167
+ ),
111
168
  value: scheduleValue.scheduleType,
112
169
  },
113
170
  {
114
- label: __('Recurrence'),
171
+ label: (
172
+ <StepButton
173
+ stepName={
174
+ scheduleValue.scheduleType === SCHEDULE_TYPES.RECURRING
175
+ ? SCHEDULE_TYPES.RECURRING
176
+ : WIZARD_TITLES.typeOfExecution
177
+ }
178
+ >
179
+ {__('Recurrence')}
180
+ </StepButton>
181
+ ),
115
182
  value: scheduleValue.repeatType,
116
183
  },
184
+ scheduleValue.scheduleType === SCHEDULE_TYPES.FUTURE &&
185
+ scheduleValue.startsAt && {
186
+ label: (
187
+ <StepButton
188
+ stepName={
189
+ scheduleValue.scheduleType === SCHEDULE_TYPES.RECURRING
190
+ ? SCHEDULE_TYPES.RECURRING
191
+ : SCHEDULE_TYPES.FUTURE
192
+ }
193
+ >
194
+ {__('Starts at')}
195
+ </StepButton>
196
+ ),
197
+ value: new Date(scheduleValue.startsAt).toString(),
198
+ },
199
+ scheduleValue.scheduleType === SCHEDULE_TYPES.FUTURE &&
200
+ scheduleValue.startsBefore && {
201
+ label: (
202
+ <StepButton stepName={SCHEDULE_TYPES.FUTURE}>
203
+ {__('Starts Before')}
204
+ </StepButton>
205
+ ),
206
+ value: new Date(scheduleValue.startsBefore).toString(),
207
+ },
208
+
209
+ scheduleValue.scheduleType === SCHEDULE_TYPES.RECURRING && {
210
+ label: (
211
+ <StepButton stepName={SCHEDULE_TYPES.RECURRING}>
212
+ {__('Starts')}
213
+ </StepButton>
214
+ ),
215
+ value: scheduleValue.isFuture
216
+ ? new Date(scheduleValue.startsAt).toString()
217
+ : __('Now'),
218
+ },
219
+
220
+ scheduleValue.scheduleType === SCHEDULE_TYPES.RECURRING && {
221
+ label: (
222
+ <StepButton stepName={SCHEDULE_TYPES.RECURRING}>
223
+ {__('Repeats')}
224
+ </StepButton>
225
+ ),
226
+ value: parseRepeat(scheduleValue.repeatType, scheduleValue.repeatData),
227
+ },
228
+ scheduleValue.scheduleType === SCHEDULE_TYPES.RECURRING && {
229
+ label: (
230
+ <StepButton stepName={SCHEDULE_TYPES.RECURRING}>
231
+ {__('Ends')}
232
+ </StepButton>
233
+ ),
234
+ value: parseEnd(
235
+ scheduleValue.ends,
236
+ scheduleValue.isNeverEnds,
237
+ scheduleValue.repeatAmount
238
+ ),
239
+ },
240
+
241
+ scheduleValue.scheduleType === SCHEDULE_TYPES.RECURRING && {
242
+ label: (
243
+ <StepButton stepName={SCHEDULE_TYPES.RECURRING}>
244
+ {__('Purpose')}
245
+ </StepButton>
246
+ ),
247
+ value: scheduleValue.purpose,
248
+ },
117
249
  {
118
- label: __('Type of query'),
250
+ label: (
251
+ <StepButton stepName={WIZARD_TITLES.typeOfExecution}>
252
+ {__('Type of query')}
253
+ </StepButton>
254
+ ),
119
255
  value: scheduleValue.isTypeStatic
120
256
  ? __('Static query')
121
257
  : __('Dynamic query'),
@@ -144,6 +280,11 @@ const ReviewDetails = ({
144
280
 
145
281
  return (
146
282
  <>
283
+ <HostPreviewModal
284
+ isOpen={hostPreviewOpen}
285
+ setIsOpen={setHostPreviewOpen}
286
+ searchQuery={buildHostQuery(selectedTargets, hostsSearchQuery)}
287
+ />
147
288
  <WizardTitle
148
289
  title={WIZARD_TITLES.review}
149
290
  className="advanced-fields-title"
@@ -153,7 +294,7 @@ const ReviewDetails = ({
153
294
  <DescriptionListGroup key={index}>
154
295
  <DescriptionListTerm>{label}</DescriptionListTerm>
155
296
  <DescriptionListDescription>
156
- <EllipsisWithTooltip>{value || ''}</EllipsisWithTooltip>
297
+ {value || ''}
157
298
  </DescriptionListDescription>
158
299
  </DescriptionListGroup>
159
300
  ))}
@@ -162,7 +303,7 @@ const ReviewDetails = ({
162
303
  <DescriptionListGroup key={index} className="advanced-fields">
163
304
  <DescriptionListTerm>{label}</DescriptionListTerm>
164
305
  <DescriptionListDescription>
165
- <EllipsisWithTooltip>{value || ''}</EllipsisWithTooltip>
306
+ {value || ''}
166
307
  </DescriptionListDescription>
167
308
  </DescriptionListGroup>
168
309
  ))}
@@ -170,7 +311,7 @@ const ReviewDetails = ({
170
311
  <DescriptionListGroup key={index}>
171
312
  <DescriptionListTerm>{label}</DescriptionListTerm>
172
313
  <DescriptionListDescription>
173
- <EllipsisWithTooltip>{value || ''}</EllipsisWithTooltip>
314
+ {value || ''}
174
315
  </DescriptionListDescription>
175
316
  </DescriptionListGroup>
176
317
  ))}
@@ -187,7 +328,17 @@ ReviewDetails.propTypes = {
187
328
  templateValues: PropTypes.object.isRequired,
188
329
  selectedTargets: PropTypes.object.isRequired,
189
330
  hostsSearchQuery: PropTypes.string.isRequired,
331
+ goToStepByName: PropTypes.func.isRequired,
190
332
  };
191
333
 
192
334
  ReviewDetails.defaultProps = { jobTemplateID: null };
193
- export default ReviewDetails;
335
+
336
+ const WrappedReviewDetails = props => (
337
+ <WizardContextConsumer>
338
+ {({ goToStepByName }) => (
339
+ <ReviewDetails goToStepByName={goToStepByName} {...props} />
340
+ )}
341
+ </WizardContextConsumer>
342
+ );
343
+
344
+ export default WrappedReviewDetails;
@@ -31,7 +31,7 @@ export const QueryType = ({ isTypeStatic, setIsTypeStatic }) => (
31
31
  id="query-type-dynamic"
32
32
  label={__('Dynamic query')}
33
33
  body={__(
34
- "evaluates just before the execution is started, so if it's planed in future, targeted hosts set may change before it"
34
+ "evaluates just before the execution is started, so if it's planned in future, targeted hosts set may change before it"
35
35
  )}
36
36
  />
37
37
  </FormGroup>
@@ -47,7 +47,6 @@ export const RepeatHour = ({ repeatData, setRepeatData }) => {
47
47
  }}
48
48
  isOpen={minuteOpen}
49
49
  width={125}
50
- menuAppendTo={() => document.querySelector('.pf-c-form.schedule-tab')}
51
50
  toggleAriaLabel="select minute toggle"
52
51
  validated={
53
52
  isValidMinute(minute)
@@ -5,7 +5,7 @@ import { translate as __, documentLocale } from 'foremanReact/common/I18n';
5
5
  import { RepeatDaily } from './RepeatDaily';
6
6
  import { noop } from '../../../helpers';
7
7
 
8
- const getWeekDays = () => {
8
+ export const getWeekDays = () => {
9
9
  const locale = documentLocale().replace(/-/g, '_');
10
10
  const baseDate = new Date(Date.UTC(2017, 0, 1)); // just a Sunday
11
11
  const weekDays = [];
@@ -139,7 +139,7 @@ describe('Schedule', () => {
139
139
  });
140
140
 
141
141
  act(() => {
142
- fireEvent.click(screen.getByText('Category and Template'));
142
+ fireEvent.click(screen.getByText('Category and template'));
143
143
  });
144
144
  act(() => {
145
145
  fireEvent.click(screen.getByRole('button', { name: 'Future execution' }));
@@ -246,7 +246,7 @@ describe('Schedule', () => {
246
246
  });
247
247
 
248
248
  act(() => {
249
- fireEvent.click(screen.getByText('Category and Template'));
249
+ fireEvent.click(screen.getByText('Category and template'));
250
250
  });
251
251
  act(() => {
252
252
  fireEvent.click(
@@ -294,7 +294,7 @@ describe('Schedule', () => {
294
294
  await act(async () => {
295
295
  fireEvent.click(screen.getByText('Cronline'));
296
296
  });
297
- const newCronline = '1 2';
297
+ const newCronline = '1 2 3 4 5';
298
298
  const cronline = screen.getByLabelText('cronline');
299
299
  expect(cronline.value).toBe('');
300
300
  await act(async () => {
@@ -306,9 +306,9 @@ describe('Schedule', () => {
306
306
  expect(screen.getByText('Review details').disabled).toBeFalsy();
307
307
 
308
308
  await act(async () => {
309
- fireEvent.click(screen.getByText('Category and Template'));
309
+ fireEvent.click(screen.getByText('Category and template'));
310
310
  });
311
- expect(screen.getAllByText('Category and Template')).toHaveLength(3);
311
+ expect(screen.getAllByText('Category and template')).toHaveLength(3);
312
312
 
313
313
  await act(async () => {
314
314
  fireEvent.click(
@@ -319,6 +319,20 @@ describe('Schedule', () => {
319
319
  expect(screen.queryAllByText('Recurring execution')).toHaveLength(3);
320
320
  expect(cronline.value).toBe(newCronline);
321
321
 
322
+ await act(async () => {
323
+ fireEvent.click(screen.getByText('Review details'));
324
+ });
325
+ expect(screen.queryAllByText('Review details')).toHaveLength(3);
326
+ expect(screen.getAllByText('Cron line - 1 2 3 4 5')).toHaveLength(1);
327
+
328
+ await act(async () => {
329
+ fireEvent.click(
330
+ screen.getByRole('button', { name: 'Recurring execution' })
331
+ );
332
+ jest.runAllTimers();
333
+ });
334
+ expect(screen.queryAllByText('Recurring execution')).toHaveLength(3);
335
+
322
336
  fireEvent.click(screen.getByText('Cronline'));
323
337
  await act(async () => {
324
338
  fireEvent.click(screen.getByText('Monthly'));