foreman_remote_execution 4.7.0 → 5.1.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 (113) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +1 -0
  3. data/Gemfile +1 -1
  4. data/app/controllers/api/v2/job_invocations_controller.rb +16 -1
  5. data/app/controllers/ui_job_wizard_controller.rb +16 -4
  6. data/app/graphql/mutations/job_invocations/create.rb +43 -0
  7. data/app/graphql/types/job_invocation_input.rb +13 -0
  8. data/app/graphql/types/recurrence_input.rb +8 -0
  9. data/app/graphql/types/scheduling_input.rb +6 -0
  10. data/app/graphql/types/targeting_enum.rb +7 -0
  11. data/app/lib/actions/remote_execution/run_host_job.rb +8 -1
  12. data/app/lib/actions/remote_execution/run_hosts_job.rb +57 -3
  13. data/app/lib/foreman_remote_execution/renderer/scope/input.rb +1 -1
  14. data/app/mailers/rex_job_mailer.rb +15 -0
  15. data/app/models/concerns/foreman_remote_execution/host_extensions.rb +8 -0
  16. data/app/models/job_invocation.rb +6 -0
  17. data/app/models/job_invocation_composer.rb +21 -13
  18. data/app/models/job_template.rb +3 -1
  19. data/app/models/remote_execution_provider.rb +18 -2
  20. data/app/models/rex_mail_notification.rb +13 -0
  21. data/app/models/targeting.rb +2 -2
  22. data/app/services/ui_notifications/remote_execution_jobs/base_job_finish.rb +2 -1
  23. data/app/views/dashboard/_latest-jobs.html.erb +21 -0
  24. data/app/views/job_invocations/refresh.js.erb +1 -0
  25. data/app/views/job_templates/_custom_tabs.html.erb +4 -9
  26. data/app/views/rex_job_mailer/job_finished.html.erb +24 -0
  27. data/app/views/rex_job_mailer/job_finished.text.erb +9 -0
  28. data/app/views/template_invocations/show.html.erb +9 -2
  29. data/config/routes.rb +1 -0
  30. data/db/migrate/20210816100932_rex_setting_category_to_dsl.rb +5 -0
  31. data/db/seeds.d/50-notification_blueprints.rb +14 -0
  32. data/db/seeds.d/95-mail_notifications.rb +24 -0
  33. data/foreman_remote_execution.gemspec +1 -1
  34. data/lib/foreman_remote_execution/engine.rb +111 -6
  35. data/lib/foreman_remote_execution/version.rb +1 -1
  36. data/package.json +9 -7
  37. data/test/functional/api/v2/job_invocations_controller_test.rb +20 -0
  38. data/test/functional/cockpit_controller_test.rb +0 -1
  39. data/test/graphql/mutations/job_invocations/create.rb +58 -0
  40. data/test/helpers/remote_execution_helper_test.rb +0 -1
  41. data/test/unit/actions/run_host_job_test.rb +21 -0
  42. data/test/unit/actions/run_hosts_job_test.rb +99 -4
  43. data/test/unit/concerns/host_extensions_test.rb +36 -3
  44. data/test/unit/job_invocation_composer_test.rb +3 -5
  45. data/test/unit/job_invocation_report_template_test.rb +17 -14
  46. data/test/unit/job_template_effective_user_test.rb +0 -4
  47. data/test/unit/remote_execution_provider_test.rb +46 -4
  48. data/test/unit/targeting_test.rb +69 -2
  49. data/webpack/JobWizard/JobWizard.js +142 -28
  50. data/webpack/JobWizard/JobWizard.scss +86 -33
  51. data/webpack/JobWizard/JobWizardConstants.js +44 -0
  52. data/webpack/JobWizard/JobWizardSelectors.js +32 -0
  53. data/webpack/JobWizard/__tests__/fixtures.js +89 -6
  54. data/webpack/JobWizard/__tests__/integration.test.js +29 -22
  55. data/webpack/JobWizard/__tests__/validation.test.js +141 -0
  56. data/webpack/JobWizard/autofill.js +38 -0
  57. data/webpack/JobWizard/index.js +7 -0
  58. data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +23 -9
  59. data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +32 -9
  60. data/webpack/JobWizard/steps/AdvancedFields/Fields.js +48 -1
  61. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +242 -23
  62. data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +82 -0
  63. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +5 -2
  64. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +3 -2
  65. data/webpack/JobWizard/steps/CategoryAndTemplate/index.js +3 -2
  66. data/webpack/JobWizard/steps/HostsAndInputs/HostPreviewModal.js +62 -0
  67. data/webpack/JobWizard/steps/HostsAndInputs/HostSearch.js +54 -0
  68. data/webpack/JobWizard/steps/HostsAndInputs/SelectAPI.js +33 -0
  69. data/webpack/JobWizard/steps/HostsAndInputs/SelectGQL.js +52 -0
  70. data/webpack/JobWizard/steps/HostsAndInputs/SelectedChips.js +100 -0
  71. data/webpack/JobWizard/steps/HostsAndInputs/TemplateInputs.js +23 -0
  72. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/HostsAndInputs.test.js +151 -0
  73. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/TemplateInputs.test.js +53 -0
  74. data/webpack/JobWizard/steps/HostsAndInputs/buildHostQuery.js +18 -0
  75. data/webpack/JobWizard/steps/HostsAndInputs/hostgroups.gql +8 -0
  76. data/webpack/JobWizard/steps/HostsAndInputs/hosts.gql +8 -0
  77. data/webpack/JobWizard/steps/HostsAndInputs/index.js +214 -0
  78. data/webpack/JobWizard/steps/ReviewDetails/index.js +193 -0
  79. data/webpack/JobWizard/steps/Schedule/PurposeField.js +31 -0
  80. data/webpack/JobWizard/steps/Schedule/QueryType.js +46 -43
  81. data/webpack/JobWizard/steps/Schedule/RepeatCron.js +53 -0
  82. data/webpack/JobWizard/steps/Schedule/RepeatDaily.js +37 -0
  83. data/webpack/JobWizard/steps/Schedule/RepeatHour.js +54 -0
  84. data/webpack/JobWizard/steps/Schedule/RepeatMonth.js +46 -0
  85. data/webpack/JobWizard/steps/Schedule/RepeatOn.js +95 -31
  86. data/webpack/JobWizard/steps/Schedule/RepeatWeek.js +70 -0
  87. data/webpack/JobWizard/steps/Schedule/ScheduleType.js +24 -21
  88. data/webpack/JobWizard/steps/Schedule/StartEndDates.js +78 -23
  89. data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +402 -0
  90. data/webpack/JobWizard/steps/Schedule/__tests__/StartEndDates.test.js +20 -10
  91. data/webpack/JobWizard/steps/Schedule/index.js +166 -29
  92. data/webpack/JobWizard/steps/form/DateTimePicker.js +126 -0
  93. data/webpack/JobWizard/steps/form/FormHelpers.js +4 -0
  94. data/webpack/JobWizard/steps/form/Formatter.js +49 -17
  95. data/webpack/JobWizard/steps/form/NumberInput.js +5 -2
  96. data/webpack/JobWizard/steps/form/ResourceSelect.js +29 -0
  97. data/webpack/JobWizard/steps/form/SearchSelect.js +121 -0
  98. data/webpack/JobWizard/steps/form/SelectField.js +14 -3
  99. data/webpack/JobWizard/steps/form/WizardTitle.js +14 -0
  100. data/webpack/JobWizard/steps/form/__tests__/SelectSearch.test.js +33 -0
  101. data/webpack/JobWizard/submit.js +120 -0
  102. data/webpack/JobWizard/validation.js +53 -0
  103. data/webpack/__mocks__/foremanReact/Root/Context/ForemanContext/index.js +2 -0
  104. data/webpack/__mocks__/foremanReact/common/I18n.js +2 -0
  105. data/webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteActions.js +1 -0
  106. data/webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteConstants.js +1 -0
  107. data/webpack/__mocks__/foremanReact/routes/RouterSelector.js +1 -0
  108. data/webpack/helpers.js +1 -0
  109. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +2 -1
  110. metadata +53 -7
  111. data/app/models/setting/remote_execution.rb +0 -88
  112. data/webpack/JobWizard/steps/AdvancedFields/__tests__/DescriptionField.test.js +0 -23
  113. data/webpack/JobWizard/steps/form/__tests__/Formatter.test.js.example +0 -76
@@ -4,6 +4,7 @@ import { foremanUrl } from 'foremanReact/common/helpers';
4
4
  export const JOB_TEMPLATES = 'JOB_TEMPLATES';
5
5
  export const JOB_CATEGORIES = 'JOB_CATEGORIES';
6
6
  export const JOB_TEMPLATE = 'JOB_TEMPLATE';
7
+ export const JOB_INVOCATION = 'JOB_INVOCATION';
7
8
  export const templatesUrl = foremanUrl('/api/v2/job_templates');
8
9
 
9
10
  export const repeatTypes = {
@@ -14,3 +15,46 @@ export const repeatTypes = {
14
15
  daily: __('Daily'),
15
16
  hourly: __('Hourly'),
16
17
  };
18
+
19
+ export const WIZARD_TITLES = {
20
+ categoryAndTemplate: __('Category and Template'),
21
+ hostsAndInputs: __('Target hosts and inputs'),
22
+ advanced: __('Advanced Fields'),
23
+ schedule: __('Schedule'),
24
+ review: __('Review Details'),
25
+ };
26
+
27
+ export const initialScheduleState = {
28
+ repeatType: repeatTypes.noRepeat,
29
+ repeatAmount: '',
30
+ repeatData: {},
31
+ startsAt: '',
32
+ startsBefore: '',
33
+ ends: '',
34
+ isFuture: false,
35
+ isNeverEnds: false,
36
+ isTypeStatic: true,
37
+ purpose: '',
38
+ };
39
+ export const HOSTS_API = 'HOSTS_API';
40
+ export const HOSTS = 'HOSTS';
41
+ export const HOST_COLLECTIONS = 'HOST_COLLECTIONS';
42
+ export const HOST_GROUPS = 'HOST_GROUPS';
43
+ export const hostMethods = {
44
+ hosts: __('Hosts'),
45
+ hostCollections: __('Host collections'),
46
+ hostGroups: __('Host groups'),
47
+ searchQuery: __('Search query'),
48
+ };
49
+
50
+ export const hostQuerySearchID = 'searchBar'; // until https://projects.theforeman.org/issues/33737 is used
51
+ export const hostsController = 'hosts';
52
+
53
+ export const dataName = {
54
+ [HOSTS]: 'hosts',
55
+ [HOST_GROUPS]: 'hostgroups',
56
+ };
57
+ export const HOSTS_TO_PREVIEW_AMOUNT = 20;
58
+
59
+ export const DEBOUNCE_HOST_COUNT = 700;
60
+ export const HOST_IDS = 'HOST_IDS';
@@ -1,13 +1,18 @@
1
+ import URI from 'urijs';
1
2
  import {
2
3
  selectAPIResponse,
3
4
  selectAPIStatus,
4
5
  selectAPIErrorMessage,
5
6
  } from 'foremanReact/redux/API/APISelectors';
7
+ import { STATUS } from 'foremanReact/constants';
8
+ import { selectRouterLocation } from 'foremanReact/routes/RouterSelector';
6
9
 
7
10
  import {
8
11
  JOB_TEMPLATES,
9
12
  JOB_CATEGORIES,
10
13
  JOB_TEMPLATE,
14
+ HOSTS_API,
15
+ JOB_INVOCATION,
11
16
  } from './JobWizardConstants';
12
17
 
13
18
  export const selectJobTemplatesStatus = state =>
@@ -22,6 +27,9 @@ export const selectJobTemplates = state =>
22
27
  export const selectJobCategories = state =>
23
28
  selectAPIResponse(state, JOB_CATEGORIES).job_categories || [];
24
29
 
30
+ export const selectWithKatello = state =>
31
+ selectAPIResponse(state, JOB_CATEGORIES).with_katello || false;
32
+
25
33
  export const selectJobCategoriesStatus = state =>
26
34
  selectAPIStatus(state, JOB_CATEGORIES);
27
35
 
@@ -45,3 +53,27 @@ export const selectAdvancedTemplateInputs = state =>
45
53
 
46
54
  export const selectTemplateInputs = state =>
47
55
  selectAPIResponse(state, JOB_TEMPLATE).template_inputs || [];
56
+
57
+ export const selectHostCount = state =>
58
+ selectAPIResponse(state, HOSTS_API).subtotal || 0;
59
+
60
+ export const selectHosts = state =>
61
+ (selectAPIResponse(state, HOSTS_API).results || []).map(host => host.name);
62
+
63
+ export const selectIsLoadingHosts = state =>
64
+ !selectAPIStatus(state, HOSTS_API) ||
65
+ selectAPIStatus(state, HOSTS_API) === STATUS.PENDING;
66
+
67
+ export const selectResponse = selectAPIResponse;
68
+
69
+ export const selectIsLoading = (state, key) =>
70
+ selectAPIStatus(state, key) === STATUS.PENDING;
71
+
72
+ export const selectIsSubmitting = state =>
73
+ selectAPIStatus(state, JOB_INVOCATION) === STATUS.PENDING ||
74
+ selectAPIStatus(state, JOB_INVOCATION) === STATUS.RESOLVED;
75
+
76
+ export const selectRouterSearch = state => {
77
+ const { search } = selectRouterLocation(state);
78
+ return URI.parseQuery(search);
79
+ };
@@ -1,4 +1,6 @@
1
1
  import configureMockStore from 'redux-mock-store';
2
+ import hostsQuery from '../steps/HostsAndInputs/hosts.gql';
3
+ import hostgroupsQuery from '../steps/HostsAndInputs/hostgroups.gql';
2
4
 
3
5
  export const jobTemplate = {
4
6
  id: 178,
@@ -9,7 +11,6 @@ export const jobTemplate = {
9
11
  default: true,
10
12
  job_category: 'Ansible Commands',
11
13
  provider_type: 'Ansible',
12
- description_format: 'Run %{command}',
13
14
  execution_timeout_interval: 2,
14
15
  description: null,
15
16
  };
@@ -48,6 +49,16 @@ export const jobTemplateResponse = {
48
49
  default: '',
49
50
  hidden_value: false,
50
51
  },
52
+ {
53
+ name: 'adv resource select',
54
+ required: false,
55
+ input_type: 'user',
56
+ value_type: 'resource',
57
+ advanced: true,
58
+ resource_type: 'ForemanTasks::Task',
59
+ default: '',
60
+ hidden_value: false,
61
+ },
51
62
  {
52
63
  name: 'adv search',
53
64
  required: false,
@@ -57,6 +68,7 @@ export const jobTemplateResponse = {
57
68
  resource_type: 'foreman_tasks/tasks',
58
69
  default: '',
59
70
  hidden_value: false,
71
+ url: 'foreman_tasks/tasks',
60
72
  },
61
73
  {
62
74
  name: 'adv date',
@@ -92,21 +104,47 @@ export const testSetup = (selectors, api) => {
92
104
  jest.spyOn(selectors, 'selectJobTemplates');
93
105
  jest.spyOn(selectors, 'selectJobCategories');
94
106
  jest.spyOn(selectors, 'selectJobCategoriesStatus');
107
+ jest.spyOn(selectors, 'selectWithKatello');
95
108
 
109
+ jest.spyOn(selectors, 'selectTemplateInputs');
110
+ jest.spyOn(selectors, 'selectAdvancedTemplateInputs');
111
+ selectors.selectWithKatello.mockImplementation(() => true);
112
+ selectors.selectTemplateInputs.mockImplementation(
113
+ () => jobTemplateResponse.template_inputs
114
+ );
115
+ selectors.selectAdvancedTemplateInputs.mockImplementation(
116
+ () => jobTemplateResponse.advanced_template_inputs
117
+ );
96
118
  selectors.selectJobCategories.mockImplementation(() => jobCategories);
97
119
  selectors.selectJobTemplates.mockImplementation(() => [
98
120
  jobTemplate,
99
121
  { ...jobTemplate, id: 2, name: 'template2' },
100
122
  ]);
123
+ selectors.selectJobTemplate.mockImplementation(() => jobTemplateResponse);
101
124
  const mockStore = configureMockStore([]);
102
- const store = mockStore({});
125
+ const store = mockStore({
126
+ ForemanTasksTask: {
127
+ response: {
128
+ subtotal: 10,
129
+ results: [
130
+ { id: '1', name: 'resource1' },
131
+ { id: '2', name: 'resource2' },
132
+ ],
133
+ },
134
+ },
135
+ HOST_COLLECTIONS: {
136
+ response: {
137
+ subtotal: 3,
138
+ results: [
139
+ { id: '74', name: 'host_collection1' },
140
+ { id: '43', name: 'host_collection2' },
141
+ ],
142
+ },
143
+ },
144
+ });
103
145
  return store;
104
146
  };
105
147
 
106
- export const mockTemplate = selectors => {
107
- selectors.selectJobTemplate.mockImplementation(() => jobTemplate);
108
- selectors.selectJobCategoriesStatus.mockImplementation(() => 'RESOLVED');
109
- };
110
148
  export const mockApi = api => {
111
149
  api.get.mockImplementation(({ handleSuccess, ...action }) => {
112
150
  if (action.key === 'JOB_CATEGORIES') {
@@ -122,7 +160,52 @@ export const mockApi = api => {
122
160
  handleSuccess({
123
161
  data: { results: [jobTemplate] },
124
162
  });
163
+ } else if (action.key === 'HOST_IDS') {
164
+ handleSuccess &&
165
+ handleSuccess({
166
+ data: { results: [{ name: 'host1' }, { name: 'host3' }] },
167
+ });
125
168
  }
126
169
  return { type: 'get', ...action };
127
170
  });
128
171
  };
172
+
173
+ export const gqlMock = [
174
+ {
175
+ request: {
176
+ query: hostsQuery,
177
+ variables: {
178
+ search: 'name~"" and organization_id=1 and location_id=2',
179
+ },
180
+ },
181
+ result: {
182
+ data: {
183
+ hosts: {
184
+ totalCount: 3,
185
+ nodes: [{ name: 'host1' }, { name: 'host2' }, { name: 'host3' }],
186
+ },
187
+ },
188
+ },
189
+ },
190
+
191
+ {
192
+ request: {
193
+ query: hostgroupsQuery,
194
+ variables: {
195
+ search: 'name~"" and organization_id=1 and location_id=2',
196
+ },
197
+ },
198
+ result: {
199
+ data: {
200
+ hostgroups: {
201
+ totalCount: 3,
202
+ nodes: [
203
+ { name: 'host_group1' },
204
+ { name: 'host_group2' },
205
+ { name: 'host_group3' },
206
+ ],
207
+ },
208
+ },
209
+ },
210
+ },
211
+ ];
@@ -2,31 +2,41 @@ import React from 'react';
2
2
  import { Provider } from 'react-redux';
3
3
  import { mount } from '@theforeman/test';
4
4
  import { render, fireEvent, screen, act } from '@testing-library/react';
5
+ import { MockedProvider } from '@apollo/client/testing';
5
6
  import * as api from 'foremanReact/redux/API';
6
7
  import { JobWizard } from '../JobWizard';
7
8
  import * as selectors from '../JobWizardSelectors';
9
+ import { WIZARD_TITLES } from '../JobWizardConstants';
8
10
  import {
9
11
  testSetup,
10
12
  mockApi,
11
13
  jobCategories,
12
14
  jobTemplateResponse as jobTemplate,
15
+ gqlMock,
13
16
  } from './fixtures';
14
17
 
15
18
  const store = testSetup(selectors, api);
16
19
 
17
- selectors.selectJobTemplate.mockImplementation(() => {});
18
-
19
- api.get.mockImplementation(({ handleSuccess, ...action }) => {
20
- if (action.key === 'JOB_CATEGORIES') {
21
- handleSuccess && handleSuccess({ data: { job_categories: jobCategories } });
22
- }
23
- return { type: 'get', ...action };
24
- });
25
20
  describe('Job wizard fill', () => {
26
21
  it('should select template', async () => {
22
+ api.get.mockImplementation(({ handleSuccess, ...action }) => {
23
+ if (action.key === 'JOB_CATEGORIES') {
24
+ handleSuccess &&
25
+ handleSuccess({ data: { job_categories: jobCategories } });
26
+ } else if (action.key === 'JOB_TEMPLATE') {
27
+ handleSuccess &&
28
+ handleSuccess({
29
+ data: jobTemplate,
30
+ });
31
+ }
32
+ return { type: 'get', ...action };
33
+ });
34
+ selectors.selectJobTemplate.mockRestore();
35
+ jest.spyOn(selectors, 'selectJobTemplate');
36
+ selectors.selectJobTemplate.mockImplementation(() => ({}));
27
37
  const wrapper = mount(
28
38
  <Provider store={store}>
29
- <JobWizard advancedValues={{}} setAdvancedValues={jest.fn()} />
39
+ <JobWizard />
30
40
  </Provider>
31
41
  );
32
42
  expect(wrapper.find('.pf-c-wizard__nav-link.pf-m-disabled')).toHaveLength(
@@ -34,7 +44,8 @@ describe('Job wizard fill', () => {
34
44
  );
35
45
  selectors.selectJobCategoriesStatus.mockImplementation(() => 'RESOLVED');
36
46
  expect(store.getActions()).toMatchSnapshot('initial');
37
-
47
+ selectors.selectJobTemplate.mockRestore();
48
+ jest.spyOn(selectors, 'selectJobTemplate');
38
49
  selectors.selectJobTemplate.mockImplementation(() => jobTemplate);
39
50
  wrapper.find('.pf-c-button.pf-c-select__toggle-button').simulate('click');
40
51
  await act(async () => {
@@ -42,9 +53,9 @@ describe('Job wizard fill', () => {
42
53
  .find('.pf-c-select__menu-item')
43
54
  .first()
44
55
  .simulate('click');
45
- await wrapper.update();
46
56
  });
47
57
  expect(store.getActions().slice(-1)).toMatchSnapshot('select template');
58
+ wrapper.update();
48
59
  expect(wrapper.find('.pf-c-wizard__nav-link.pf-m-disabled')).toHaveLength(
49
60
  0
50
61
  );
@@ -52,23 +63,19 @@ describe('Job wizard fill', () => {
52
63
 
53
64
  it('have all steps', async () => {
54
65
  selectors.selectJobCategoriesStatus.mockImplementation(() => null);
55
- selectors.selectJobTemplate.mockRestore();
56
66
  selectors.selectJobTemplates.mockRestore();
57
67
  selectors.selectJobCategories.mockRestore();
58
68
  mockApi(api);
59
69
 
60
70
  render(
61
- <Provider store={store}>
62
- <JobWizard />
63
- </Provider>
71
+ <MockedProvider mocks={gqlMock} addTypename={false}>
72
+ <Provider store={store}>
73
+ <JobWizard />
74
+ </Provider>
75
+ </MockedProvider>
64
76
  );
65
- const steps = [
66
- 'Target Hosts',
67
- 'Advanced Fields',
68
- 'Schedule',
69
- 'Review Details',
70
- 'Category and Template',
71
- ];
77
+ const titles = Object.values(WIZARD_TITLES);
78
+ const steps = [titles[1], titles[0], ...titles.slice(2)]; // the first title is selected at the beggining
72
79
  // eslint-disable-next-line no-unused-vars
73
80
  for await (const step of steps) {
74
81
  const stepSelector = screen.getByText(step);
@@ -0,0 +1,141 @@
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 '@testing-library/jest-dom';
6
+ import * as api from 'foremanReact/redux/API';
7
+ import { JobWizard } from '../JobWizard';
8
+ import * as selectors from '../JobWizardSelectors';
9
+ import { testSetup, mockApi, jobTemplateResponse, gqlMock } from './fixtures';
10
+ import { WIZARD_TITLES } from '../JobWizardConstants';
11
+
12
+ const store = testSetup(selectors, api);
13
+
14
+ mockApi(api);
15
+ const templateInputs = [...jobTemplateResponse.template_inputs];
16
+ const advancedTemplateInputs = [
17
+ ...jobTemplateResponse.advanced_template_inputs,
18
+ ];
19
+ templateInputs[0].default = null;
20
+ advancedTemplateInputs[0].default = null;
21
+ selectors.selectTemplateInputs.mockImplementation(() => templateInputs);
22
+ selectors.selectAdvancedTemplateInputs.mockImplementation(
23
+ () => advancedTemplateInputs
24
+ );
25
+
26
+ describe('Job wizard validation', () => {
27
+ afterAll(() => {
28
+ selectors.selectTemplateInputs.mockRestore();
29
+ selectors.selectAdvancedTemplateInputs.mockRestore();
30
+ });
31
+ it('requeried', async () => {
32
+ render(
33
+ <MockedProvider mocks={gqlMock} addTypename={false}>
34
+ <Provider store={store}>
35
+ <JobWizard />
36
+ </Provider>
37
+ </MockedProvider>
38
+ );
39
+ expect(screen.getByText(WIZARD_TITLES.advanced)).toBeDisabled();
40
+ expect(screen.getByText(WIZARD_TITLES.schedule)).toBeDisabled();
41
+ expect(screen.getByText(WIZARD_TITLES.review)).toBeDisabled();
42
+ await act(async () => {
43
+ fireEvent.click(screen.getByText(WIZARD_TITLES.hostsAndInputs));
44
+ });
45
+ const textField = screen.getByLabelText('plain hidden', {
46
+ selector: 'textarea',
47
+ });
48
+ await act(async () => {
49
+ await fireEvent.change(textField, {
50
+ target: { value: 'text' },
51
+ });
52
+ });
53
+ expect(screen.getByText(WIZARD_TITLES.advanced)).toBeEnabled();
54
+ expect(screen.getByText(WIZARD_TITLES.schedule)).toBeDisabled();
55
+ expect(screen.getByText(WIZARD_TITLES.review)).toBeDisabled();
56
+
57
+ await act(async () => {
58
+ fireEvent.click(screen.getByText(WIZARD_TITLES.advanced));
59
+ });
60
+ const advTextField = screen.getByLabelText('adv plain hidden', {
61
+ selector: 'textarea',
62
+ });
63
+ await act(async () => {
64
+ await fireEvent.change(advTextField, {
65
+ target: { value: 'text' },
66
+ });
67
+ });
68
+
69
+ expect(
70
+ screen.getByText(WIZARD_TITLES.advanced, { selector: 'button' })
71
+ ).toBeEnabled();
72
+ expect(screen.getByText(WIZARD_TITLES.schedule)).toBeEnabled();
73
+ expect(screen.getByText(WIZARD_TITLES.review)).toBeEnabled();
74
+ });
75
+
76
+ it('advanced number', async () => {
77
+ render(
78
+ <MockedProvider mocks={gqlMock} addTypename={false}>
79
+ <Provider store={store}>
80
+ <JobWizard />
81
+ </Provider>
82
+ </MockedProvider>
83
+ );
84
+
85
+ // setup
86
+ await act(async () => {
87
+ fireEvent.click(screen.getByText(WIZARD_TITLES.hostsAndInputs));
88
+ });
89
+ await act(async () => {
90
+ await fireEvent.change(
91
+ screen.getByLabelText('plain hidden', {
92
+ selector: 'textarea',
93
+ }),
94
+ {
95
+ target: { value: 'text' },
96
+ }
97
+ );
98
+ });
99
+
100
+ await act(async () => {
101
+ fireEvent.click(screen.getByText(WIZARD_TITLES.advanced));
102
+ });
103
+ await act(async () => {
104
+ await fireEvent.change(
105
+ screen.getByLabelText('adv plain hidden', {
106
+ selector: 'textarea',
107
+ }),
108
+ {
109
+ target: { value: 'text' },
110
+ }
111
+ );
112
+ });
113
+ expect(
114
+ screen.getByText(WIZARD_TITLES.advanced, { selector: 'button' })
115
+ ).toBeEnabled();
116
+ expect(screen.getByText(WIZARD_TITLES.schedule)).toBeEnabled();
117
+ expect(screen.getByText(WIZARD_TITLES.review)).toBeEnabled();
118
+
119
+ // test
120
+ const timeoutField = screen.getByLabelText('timeout to kill', {
121
+ selector: 'input',
122
+ });
123
+ await act(async () => {
124
+ await fireEvent.change(timeoutField, {
125
+ target: { value: 'text' },
126
+ });
127
+ });
128
+
129
+ expect(screen.getByText(WIZARD_TITLES.schedule)).toBeDisabled();
130
+ expect(screen.getByText(WIZARD_TITLES.review)).toBeDisabled();
131
+
132
+ await act(async () => {
133
+ await fireEvent.change(timeoutField, {
134
+ target: { value: 123 },
135
+ });
136
+ });
137
+
138
+ expect(screen.getByText(WIZARD_TITLES.schedule)).toBeEnabled();
139
+ expect(screen.getByText(WIZARD_TITLES.review)).toBeEnabled();
140
+ });
141
+ });
@@ -0,0 +1,38 @@
1
+ import { useEffect } from 'react';
2
+ import { useDispatch, useSelector } from 'react-redux';
3
+ import { get } from 'foremanReact/redux/API';
4
+ import { HOST_IDS } from './JobWizardConstants';
5
+ import { selectRouterSearch } from './JobWizardSelectors';
6
+ import './JobWizard.scss';
7
+
8
+ export const useAutoFill = ({ setSelectedTargets, setHostsSearchQuery }) => {
9
+ const fills = useSelector(selectRouterSearch);
10
+ const dispatch = useDispatch();
11
+
12
+ useEffect(() => {
13
+ if (Object.keys(fills).length) {
14
+ if (fills['host_ids[]']) {
15
+ dispatch(
16
+ get({
17
+ key: HOST_IDS,
18
+ url: '/api/hosts',
19
+ params: { search: `id = ${fills['host_ids[]'].join(' or id = ')}` },
20
+ handleSuccess: ({ data }) => {
21
+ setSelectedTargets(currentTargets => ({
22
+ ...currentTargets,
23
+ hosts: (data.results || []).map(({ name }) => ({
24
+ id: name,
25
+ name,
26
+ })),
27
+ }));
28
+ },
29
+ })
30
+ );
31
+ }
32
+ if (fills.search) {
33
+ setHostsSearchQuery(fills.search);
34
+ }
35
+ }
36
+ // eslint-disable-next-line react-hooks/exhaustive-deps
37
+ }, []);
38
+ };
@@ -1,4 +1,5 @@
1
1
  import React from 'react';
2
+ import PropTypes from 'prop-types';
2
3
  import { Title, Divider } from '@patternfly/react-core';
3
4
  import { translate as __ } from 'foremanReact/common/I18n';
4
5
  import PageLayout from 'foremanReact/routes/common/PageLayout/PageLayout';
@@ -29,4 +30,10 @@ const JobWizardPage = () => {
29
30
  );
30
31
  };
31
32
 
33
+ JobWizardPage.propTypes = {
34
+ location: PropTypes.shape({
35
+ search: PropTypes.string,
36
+ }).isRequired,
37
+ };
38
+
32
39
  export default JobWizardPage;
@@ -1,12 +1,10 @@
1
1
  import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import { useSelector } from 'react-redux';
4
- import { Title, Form } from '@patternfly/react-core';
5
- import { translate as __ } from 'foremanReact/common/I18n';
4
+ import { Form } from '@patternfly/react-core';
6
5
  import {
7
6
  selectEffectiveUser,
8
7
  selectAdvancedTemplateInputs,
9
- selectTemplateInputs,
10
8
  } from '../../JobWizardSelectors';
11
9
  import {
12
10
  EffectiveUserField,
@@ -17,18 +15,25 @@ import {
17
15
  ConcurrencyLevelField,
18
16
  TimeSpanLevelField,
19
17
  TemplateInputsFields,
18
+ ExecutionOrderingField,
20
19
  } from './Fields';
21
20
  import { DescriptionField } from './DescriptionField';
21
+ import { WIZARD_TITLES } from '../../JobWizardConstants';
22
+ import { WizardTitle } from '../form/WizardTitle';
22
23
 
23
- export const AdvancedFields = ({ advancedValues, setAdvancedValues }) => {
24
+ export const AdvancedFields = ({
25
+ templateValues,
26
+ advancedValues,
27
+ setAdvancedValues,
28
+ }) => {
24
29
  const effectiveUser = useSelector(selectEffectiveUser);
25
30
  const advancedTemplateInputs = useSelector(selectAdvancedTemplateInputs);
26
- const templateInputs = useSelector(selectTemplateInputs);
27
31
  return (
28
32
  <>
29
- <Title headingLevel="h2" className="advanced-fields-title">
30
- {__('Advanced Fields')}
31
- </Title>
33
+ <WizardTitle
34
+ title={WIZARD_TITLES.advanced}
35
+ className="advanced-fields-title"
36
+ />
32
37
  <Form id="advanced-fields-job-template" autoComplete="off">
33
38
  <TemplateInputsFields
34
39
  inputs={advancedTemplateInputs}
@@ -46,7 +51,7 @@ export const AdvancedFields = ({ advancedValues, setAdvancedValues }) => {
46
51
  />
47
52
  )}
48
53
  <DescriptionField
49
- inputs={templateInputs}
54
+ inputValues={{ ...templateValues, ...advancedValues.templateValues }}
50
55
  value={advancedValues.description}
51
56
  setValue={newValue => setAdvancedValues({ description: newValue })}
52
57
  />
@@ -98,6 +103,14 @@ export const AdvancedFields = ({ advancedValues, setAdvancedValues }) => {
98
103
  })
99
104
  }
100
105
  />
106
+ <ExecutionOrderingField
107
+ isRandomizedOrdering={advancedValues.isRandomizedOrdering}
108
+ setValue={newValue =>
109
+ setAdvancedValues({
110
+ isRandomizedOrdering: newValue,
111
+ })
112
+ }
113
+ />
101
114
  </Form>
102
115
  </>
103
116
  );
@@ -106,5 +119,6 @@ export const AdvancedFields = ({ advancedValues, setAdvancedValues }) => {
106
119
  AdvancedFields.propTypes = {
107
120
  advancedValues: PropTypes.object.isRequired,
108
121
  setAdvancedValues: PropTypes.func.isRequired,
122
+ templateValues: PropTypes.object.isRequired,
109
123
  };
110
124
  export default AdvancedFields;