foreman_remote_execution 4.6.0 → 5.0.1

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 (143) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby_ci.yml +7 -0
  3. data/.rubocop_todo.yml +1 -0
  4. data/app/controllers/api/v2/job_invocations_controller.rb +16 -1
  5. data/app/controllers/job_invocations_controller.rb +1 -1
  6. data/app/controllers/ui_job_wizard_controller.rb +21 -2
  7. data/app/graphql/mutations/job_invocations/create.rb +43 -0
  8. data/app/graphql/types/job_invocation.rb +16 -0
  9. data/app/graphql/types/job_invocation_input.rb +13 -0
  10. data/app/graphql/types/recurrence_input.rb +8 -0
  11. data/app/graphql/types/scheduling_input.rb +6 -0
  12. data/app/graphql/types/targeting_enum.rb +7 -0
  13. data/app/helpers/concerns/foreman_remote_execution/hosts_helper_extensions.rb +5 -1
  14. data/app/helpers/remote_execution_helper.rb +9 -3
  15. data/app/lib/actions/remote_execution/run_host_job.rb +10 -1
  16. data/app/lib/actions/remote_execution/run_hosts_job.rb +58 -4
  17. data/app/mailers/rex_job_mailer.rb +15 -0
  18. data/app/models/concerns/foreman_remote_execution/host_extensions.rb +10 -0
  19. data/app/models/concerns/foreman_remote_execution/smart_proxy_extensions.rb +6 -0
  20. data/app/models/host_proxy_invocation.rb +4 -0
  21. data/app/models/host_status/execution_status.rb +3 -3
  22. data/app/models/job_invocation.rb +12 -5
  23. data/app/models/job_invocation_composer.rb +25 -17
  24. data/app/models/job_template.rb +1 -1
  25. data/app/models/remote_execution_feature.rb +5 -1
  26. data/app/models/remote_execution_provider.rb +18 -2
  27. data/app/models/rex_mail_notification.rb +13 -0
  28. data/app/models/targeting.rb +7 -3
  29. data/app/services/ui_notifications/remote_execution_jobs/base_job_finish.rb +2 -1
  30. data/app/views/dashboard/_latest-jobs.html.erb +21 -0
  31. data/app/views/job_invocations/index.html.erb +1 -1
  32. data/app/views/job_invocations/refresh.js.erb +1 -0
  33. data/app/views/rex_job_mailer/job_finished.html.erb +24 -0
  34. data/app/views/rex_job_mailer/job_finished.text.erb +9 -0
  35. data/app/views/template_invocations/show.html.erb +2 -1
  36. data/app/views/templates/ssh/module_action.erb +1 -0
  37. data/app/views/templates/ssh/power_action.erb +2 -0
  38. data/app/views/templates/ssh/puppet_run_once.erb +1 -0
  39. data/config/routes.rb +1 -0
  40. data/db/migrate/2021051713291621250977_add_host_proxy_invocations.rb +12 -0
  41. data/db/migrate/20210816100932_rex_setting_category_to_dsl.rb +5 -0
  42. data/db/seeds.d/50-notification_blueprints.rb +14 -0
  43. data/db/seeds.d/95-mail_notifications.rb +24 -0
  44. data/foreman_remote_execution.gemspec +2 -3
  45. data/lib/foreman_remote_execution/engine.rb +114 -8
  46. data/lib/foreman_remote_execution/version.rb +1 -1
  47. data/package.json +9 -7
  48. data/test/functional/api/v2/job_invocations_controller_test.rb +20 -0
  49. data/test/functional/cockpit_controller_test.rb +0 -1
  50. data/test/graphql/mutations/job_invocations/create.rb +58 -0
  51. data/test/graphql/queries/job_invocation_query_test.rb +31 -0
  52. data/test/graphql/queries/job_invocations_query_test.rb +35 -0
  53. data/test/helpers/remote_execution_helper_test.rb +0 -1
  54. data/test/unit/actions/run_host_job_test.rb +21 -0
  55. data/test/unit/actions/run_hosts_job_test.rb +99 -4
  56. data/test/unit/concerns/host_extensions_test.rb +40 -7
  57. data/test/unit/input_template_renderer_test.rb +1 -89
  58. data/test/unit/job_invocation_composer_test.rb +18 -18
  59. data/test/unit/job_invocation_report_template_test.rb +16 -13
  60. data/test/unit/job_invocation_test.rb +1 -1
  61. data/test/unit/job_template_effective_user_test.rb +0 -4
  62. data/test/unit/remote_execution_provider_test.rb +46 -4
  63. data/test/unit/targeting_test.rb +68 -1
  64. data/webpack/JobWizard/JobWizard.js +158 -24
  65. data/webpack/JobWizard/JobWizard.scss +93 -1
  66. data/webpack/JobWizard/JobWizardConstants.js +54 -0
  67. data/webpack/JobWizard/JobWizardSelectors.js +41 -0
  68. data/webpack/JobWizard/__tests__/fixtures.js +188 -3
  69. data/webpack/JobWizard/__tests__/integration.test.js +41 -106
  70. data/webpack/JobWizard/__tests__/validation.test.js +141 -0
  71. data/webpack/JobWizard/autofill.js +38 -0
  72. data/webpack/JobWizard/index.js +7 -0
  73. data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +41 -10
  74. data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +90 -0
  75. data/webpack/JobWizard/steps/AdvancedFields/Fields.js +116 -55
  76. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +354 -16
  77. data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +79 -246
  78. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +5 -2
  79. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +123 -51
  80. data/webpack/JobWizard/steps/CategoryAndTemplate/index.js +3 -2
  81. data/webpack/JobWizard/steps/HostsAndInputs/HostPreviewModal.js +62 -0
  82. data/webpack/JobWizard/steps/HostsAndInputs/HostSearch.js +54 -0
  83. data/webpack/JobWizard/steps/HostsAndInputs/SelectAPI.js +33 -0
  84. data/webpack/JobWizard/steps/HostsAndInputs/SelectGQL.js +52 -0
  85. data/webpack/JobWizard/steps/HostsAndInputs/SelectedChips.js +100 -0
  86. data/webpack/JobWizard/steps/HostsAndInputs/TemplateInputs.js +23 -0
  87. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/HostsAndInputs.test.js +151 -0
  88. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/TemplateInputs.test.js +53 -0
  89. data/webpack/JobWizard/steps/HostsAndInputs/buildHostQuery.js +18 -0
  90. data/webpack/JobWizard/steps/HostsAndInputs/hostgroups.gql +8 -0
  91. data/webpack/JobWizard/steps/HostsAndInputs/hosts.gql +8 -0
  92. data/webpack/JobWizard/steps/HostsAndInputs/index.js +214 -0
  93. data/webpack/JobWizard/steps/ReviewDetails/index.js +193 -0
  94. data/webpack/JobWizard/steps/Schedule/PurposeField.js +31 -0
  95. data/webpack/JobWizard/steps/Schedule/QueryType.js +51 -0
  96. data/webpack/JobWizard/steps/Schedule/RepeatCron.js +53 -0
  97. data/webpack/JobWizard/steps/Schedule/RepeatDaily.js +37 -0
  98. data/webpack/JobWizard/steps/Schedule/RepeatHour.js +54 -0
  99. data/webpack/JobWizard/steps/Schedule/RepeatMonth.js +46 -0
  100. data/webpack/JobWizard/steps/Schedule/RepeatOn.js +125 -0
  101. data/webpack/JobWizard/steps/Schedule/RepeatWeek.js +70 -0
  102. data/webpack/JobWizard/steps/Schedule/ScheduleType.js +28 -0
  103. data/webpack/JobWizard/steps/Schedule/StartEndDates.js +106 -0
  104. data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +402 -0
  105. data/webpack/JobWizard/steps/Schedule/__tests__/StartEndDates.test.js +32 -0
  106. data/webpack/JobWizard/steps/Schedule/index.js +178 -0
  107. data/webpack/JobWizard/steps/form/DateTimePicker.js +126 -0
  108. data/webpack/JobWizard/steps/form/FormHelpers.js +5 -0
  109. data/webpack/JobWizard/steps/form/Formatter.js +181 -0
  110. data/webpack/JobWizard/steps/form/NumberInput.js +36 -0
  111. data/webpack/JobWizard/steps/form/ResourceSelect.js +29 -0
  112. data/webpack/JobWizard/steps/form/SearchSelect.js +121 -0
  113. data/webpack/JobWizard/steps/form/SelectField.js +28 -5
  114. data/webpack/JobWizard/steps/form/WizardTitle.js +14 -0
  115. data/webpack/JobWizard/steps/form/__tests__/SelectSearch.test.js +33 -0
  116. data/webpack/JobWizard/submit.js +120 -0
  117. data/webpack/JobWizard/validation.js +53 -0
  118. data/webpack/__mocks__/foremanReact/Root/Context/ForemanContext/index.js +2 -0
  119. data/webpack/__mocks__/foremanReact/common/I18n.js +2 -0
  120. data/webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteActions.js +1 -0
  121. data/webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteConstants.js +1 -0
  122. data/webpack/__mocks__/foremanReact/components/SearchBar.js +18 -1
  123. data/webpack/__mocks__/foremanReact/routes/RouterSelector.js +1 -0
  124. data/webpack/helpers.js +1 -0
  125. data/webpack/react_app/components/RecentJobsCard/JobStatusIcon.js +43 -0
  126. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +73 -66
  127. data/webpack/react_app/components/RecentJobsCard/RecentJobsTable.js +98 -0
  128. data/webpack/react_app/components/RecentJobsCard/constants.js +11 -0
  129. data/webpack/react_app/components/RecentJobsCard/styles.scss +11 -0
  130. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +1 -0
  131. data/webpack/react_app/extend/fillRecentJobsCard.js +1 -1
  132. metadata +71 -16
  133. data/app/models/concerns/foreman_remote_execution/orchestration/ssh.rb +0 -70
  134. data/app/models/setting/remote_execution.rb +0 -88
  135. data/test/models/orchestration/ssh_test.rb +0 -56
  136. data/webpack/JobWizard/__tests__/JobWizard.test.js +0 -13
  137. data/webpack/JobWizard/__tests__/__snapshots__/JobWizard.test.js.snap +0 -32
  138. data/webpack/JobWizard/steps/CategoryAndTemplate/__snapshots__/CategoryAndTemplate.test.js.snap +0 -113
  139. data/webpack/JobWizard/steps/form/__tests__/GroupedSelectField.test.js +0 -38
  140. data/webpack/JobWizard/steps/form/__tests__/SelectField.test.js +0 -23
  141. data/webpack/JobWizard/steps/form/__tests__/__snapshots__/GroupedSelectField.test.js.snap +0 -37
  142. data/webpack/JobWizard/steps/form/__tests__/__snapshots__/SelectField.test.js.snap +0 -23
  143. data/webpack/react_app/components/RecentJobsCard/styles.css +0 -15
@@ -1,13 +1,16 @@
1
- const jobTemplate = {
1
+ import configureMockStore from 'redux-mock-store';
2
+ import hostsQuery from '../steps/HostsAndInputs/hosts.gql';
3
+ import hostgroupsQuery from '../steps/HostsAndInputs/hostgroups.gql';
4
+
5
+ export const jobTemplate = {
2
6
  id: 178,
3
- name: 'Run Command - Ansible Default',
7
+ name: 'template1',
4
8
  template:
5
9
  "---\n- hosts: all\n tasks:\n - shell:\n cmd: |\n<%= indent(10) { input('command') } %>\n register: out\n - debug: var=out",
6
10
  snippet: false,
7
11
  default: true,
8
12
  job_category: 'Ansible Commands',
9
13
  provider_type: 'Ansible',
10
- description_format: 'Run %{command}',
11
14
  execution_timeout_interval: 2,
12
15
  description: null,
13
16
  };
@@ -23,4 +26,186 @@ export const jobTemplateResponse = {
23
26
  overridable: true,
24
27
  current_user: false,
25
28
  },
29
+ advanced_template_inputs: [
30
+ {
31
+ name: 'adv plain hidden',
32
+ required: true,
33
+ input_type: 'user',
34
+ description: 'some Description',
35
+ advanced: true,
36
+ value_type: 'plain',
37
+ resource_type: 'ansible_roles',
38
+ default: 'Default val',
39
+ hidden_value: true,
40
+ },
41
+ {
42
+ name: 'adv plain select',
43
+ required: false,
44
+ input_type: 'user',
45
+ options: 'option 1\r\noption 2\r\noption 3\r\noption 4',
46
+ advanced: true,
47
+ value_type: 'plain',
48
+ resource_type: 'ansible_roles',
49
+ default: '',
50
+ hidden_value: false,
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
+ },
62
+ {
63
+ name: 'adv search',
64
+ required: false,
65
+ options: '',
66
+ advanced: true,
67
+ value_type: 'search',
68
+ resource_type: 'foreman_tasks/tasks',
69
+ default: '',
70
+ hidden_value: false,
71
+ url: 'foreman_tasks/tasks',
72
+ },
73
+ {
74
+ name: 'adv date',
75
+ required: false,
76
+ options: '',
77
+ advanced: true,
78
+ value_type: 'date',
79
+ resource_type: 'ansible_roles',
80
+ default: '',
81
+ hidden_value: false,
82
+ },
83
+ ],
84
+ template_inputs: [
85
+ {
86
+ name: 'plain hidden',
87
+ required: true,
88
+ input_type: 'user',
89
+ description: 'some Description',
90
+ advanced: false,
91
+ value_type: 'plain',
92
+ resource_type: 'ansible_roles',
93
+ default: 'Default val',
94
+ hidden_value: true,
95
+ },
96
+ ],
97
+ };
98
+
99
+ export const jobCategories = ['Ansible Commands', 'Puppet', 'Services'];
100
+
101
+ export const testSetup = (selectors, api) => {
102
+ jest.spyOn(api, 'get');
103
+ jest.spyOn(selectors, 'selectJobTemplate');
104
+ jest.spyOn(selectors, 'selectJobTemplates');
105
+ jest.spyOn(selectors, 'selectJobCategories');
106
+ jest.spyOn(selectors, 'selectJobCategoriesStatus');
107
+ jest.spyOn(selectors, 'selectWithKatello');
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
+ );
118
+ selectors.selectJobCategories.mockImplementation(() => jobCategories);
119
+ selectors.selectJobTemplates.mockImplementation(() => [
120
+ jobTemplate,
121
+ { ...jobTemplate, id: 2, name: 'template2' },
122
+ ]);
123
+ selectors.selectJobTemplate.mockImplementation(() => jobTemplateResponse);
124
+ const mockStore = configureMockStore([]);
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
+ });
145
+ return store;
26
146
  };
147
+
148
+ export const mockApi = api => {
149
+ api.get.mockImplementation(({ handleSuccess, ...action }) => {
150
+ if (action.key === 'JOB_CATEGORIES') {
151
+ handleSuccess &&
152
+ handleSuccess({ data: { job_categories: jobCategories } });
153
+ } else if (action.key === 'JOB_TEMPLATE') {
154
+ handleSuccess &&
155
+ handleSuccess({
156
+ data: jobTemplateResponse,
157
+ });
158
+ } else if (action.key === 'JOB_TEMPLATES') {
159
+ handleSuccess &&
160
+ handleSuccess({
161
+ data: { results: [jobTemplate] },
162
+ });
163
+ } else if (action.key === 'HOST_IDS') {
164
+ handleSuccess &&
165
+ handleSuccess({
166
+ data: { results: [{ name: 'host1' }, { name: 'host3' }] },
167
+ });
168
+ }
169
+ return { type: 'get', ...action };
170
+ });
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
+ ];
@@ -1,45 +1,42 @@
1
1
  import React from 'react';
2
2
  import { Provider } from 'react-redux';
3
- import configureMockStore from 'redux-mock-store';
4
3
  import { mount } from '@theforeman/test';
5
4
  import { render, fireEvent, screen, act } from '@testing-library/react';
5
+ import { MockedProvider } from '@apollo/client/testing';
6
6
  import * as api from 'foremanReact/redux/API';
7
7
  import { JobWizard } from '../JobWizard';
8
8
  import * as selectors from '../JobWizardSelectors';
9
- import { jobTemplates, jobTemplateResponse as jobTemplate } from './fixtures';
9
+ import { WIZARD_TITLES } from '../JobWizardConstants';
10
+ import {
11
+ testSetup,
12
+ mockApi,
13
+ jobCategories,
14
+ jobTemplateResponse as jobTemplate,
15
+ gqlMock,
16
+ } from './fixtures';
10
17
 
11
- jest.spyOn(api, 'get');
12
- jest.spyOn(selectors, 'selectJobTemplate');
13
- jest.spyOn(selectors, 'selectJobTemplates');
14
- jest.spyOn(selectors, 'selectJobCategories');
15
- jest.spyOn(selectors, 'selectJobCategoriesStatus');
18
+ const store = testSetup(selectors, api);
16
19
 
17
- const jobCategories = ['Ansible Commands', 'Puppet', 'Services'];
18
-
19
- api.get.mockImplementation(({ handleSuccess, ...action }) => {
20
- if (action.key === 'JOB_CATEGORIES') {
21
- handleSuccess && handleSuccess({ data: { job_categories: jobCategories } });
22
- } else if (action.key === 'JOB_TEMPLATE') {
23
- handleSuccess &&
24
- handleSuccess({
25
- data: jobTemplate,
26
- });
27
- }
28
- return { type: 'get', ...action };
29
- });
30
-
31
- selectors.selectJobTemplate.mockImplementation(() => null);
32
- selectors.selectJobCategories.mockImplementation(() => jobCategories);
33
- selectors.selectJobCategoriesStatus.mockImplementation(() => null);
34
- selectors.selectJobTemplates.mockImplementation(() => jobTemplates);
35
-
36
- const mockStore = configureMockStore([]);
37
- const store = mockStore({});
38
20
  describe('Job wizard fill', () => {
39
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(() => ({}));
40
37
  const wrapper = mount(
41
38
  <Provider store={store}>
42
- <JobWizard advancedValues={{}} setAdvancedValues={jest.fn()} />
39
+ <JobWizard />
43
40
  </Provider>
44
41
  );
45
42
  expect(wrapper.find('.pf-c-wizard__nav-link.pf-m-disabled')).toHaveLength(
@@ -47,100 +44,38 @@ describe('Job wizard fill', () => {
47
44
  );
48
45
  selectors.selectJobCategoriesStatus.mockImplementation(() => 'RESOLVED');
49
46
  expect(store.getActions()).toMatchSnapshot('initial');
50
-
47
+ selectors.selectJobTemplate.mockRestore();
48
+ jest.spyOn(selectors, 'selectJobTemplate');
51
49
  selectors.selectJobTemplate.mockImplementation(() => jobTemplate);
52
50
  wrapper.find('.pf-c-button.pf-c-select__toggle-button').simulate('click');
53
51
  await act(async () => {
54
- await wrapper.find('.pf-c-select__menu-item').simulate('click');
55
- await wrapper.update();
52
+ await wrapper
53
+ .find('.pf-c-select__menu-item')
54
+ .first()
55
+ .simulate('click');
56
56
  });
57
57
  expect(store.getActions().slice(-1)).toMatchSnapshot('select template');
58
+ wrapper.update();
58
59
  expect(wrapper.find('.pf-c-wizard__nav-link.pf-m-disabled')).toHaveLength(
59
60
  0
60
61
  );
61
62
  });
62
63
 
63
- it('should save data between steps for advanced fields', async () => {
64
- const wrapper = mount(
65
- <Provider store={store}>
66
- <JobWizard advancedValues={{}} setAdvancedValues={jest.fn()} />
67
- </Provider>
68
- );
69
- // setup
70
- selectors.selectJobCategoriesStatus.mockImplementation(() => 'RESOLVED');
71
- selectors.selectJobTemplate.mockImplementation(() => jobTemplate);
72
- wrapper.find('.pf-c-button.pf-c-select__toggle-button').simulate('click');
73
- wrapper.find('.pf-c-select__menu-item').simulate('click');
74
-
75
- // test
76
- expect(wrapper.find('.pf-c-wizard__nav-link.pf-m-disabled')).toHaveLength(
77
- 0
78
- );
79
- wrapper
80
- .find('.pf-c-wizard__nav-link')
81
- .at(2)
82
- .simulate('click'); // Advanced step
83
- const effectiveUserInput = () => wrapper.find('input#effective-user');
84
- const effectiveUesrValue = 'effective user new value';
85
- effectiveUserInput().getDOMNode().value = effectiveUesrValue;
86
- await act(async () => {
87
- await effectiveUserInput().simulate('change');
88
- wrapper.update();
89
- });
90
-
91
- expect(effectiveUserInput().prop('value')).toEqual(effectiveUesrValue);
92
-
93
- wrapper
94
- .find('.pf-c-wizard__nav-link')
95
- .at(1)
96
- .simulate('click');
97
-
98
- expect(wrapper.find('.pf-c-wizard__nav-link.pf-m-current').text()).toEqual(
99
- 'Target Hosts'
100
- );
101
- wrapper
102
- .find('.pf-c-wizard__nav-link')
103
- .at(2)
104
- .simulate('click'); // Advanced step
105
-
106
- expect(effectiveUserInput().prop('value')).toEqual(effectiveUesrValue);
107
- });
108
-
109
64
  it('have all steps', async () => {
110
65
  selectors.selectJobCategoriesStatus.mockImplementation(() => null);
111
- selectors.selectJobTemplate.mockRestore();
112
66
  selectors.selectJobTemplates.mockRestore();
113
67
  selectors.selectJobCategories.mockRestore();
114
- api.get.mockImplementation(({ handleSuccess, ...action }) => {
115
- if (action.key === 'JOB_CATEGORIES') {
116
- handleSuccess &&
117
- handleSuccess({ data: { job_categories: jobCategories } });
118
- } else if (action.key === 'JOB_TEMPLATE') {
119
- handleSuccess &&
120
- handleSuccess({
121
- data: jobTemplate,
122
- });
123
- } else if (action.key === 'JOB_TEMPLATES') {
124
- handleSuccess &&
125
- handleSuccess({
126
- data: { results: [jobTemplate.job_template] },
127
- });
128
- }
129
- return { type: 'get', ...action };
130
- });
68
+ mockApi(api);
131
69
 
132
70
  render(
133
- <Provider store={store}>
134
- <JobWizard />
135
- </Provider>
71
+ <MockedProvider mocks={gqlMock} addTypename={false}>
72
+ <Provider store={store}>
73
+ <JobWizard />
74
+ </Provider>
75
+ </MockedProvider>
136
76
  );
137
- const steps = [
138
- 'Target Hosts',
139
- 'Advanced Fields',
140
- 'Schedule',
141
- 'Review Details',
142
- 'Category and Template',
143
- ];
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
144
79
  // eslint-disable-next-line no-unused-vars
145
80
  for await (const step of steps) {
146
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,9 +1,11 @@
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';
6
- import { selectJobTemplate } from '../../JobWizardSelectors';
4
+ import { Form } from '@patternfly/react-core';
5
+ import {
6
+ selectEffectiveUser,
7
+ selectAdvancedTemplateInputs,
8
+ } from '../../JobWizardSelectors';
7
9
  import {
8
10
  EffectiveUserField,
9
11
  TimeoutToKillField,
@@ -12,17 +14,32 @@ import {
12
14
  EffectiveUserPasswordField,
13
15
  ConcurrencyLevelField,
14
16
  TimeSpanLevelField,
17
+ TemplateInputsFields,
18
+ ExecutionOrderingField,
15
19
  } from './Fields';
20
+ import { DescriptionField } from './DescriptionField';
21
+ import { WIZARD_TITLES } from '../../JobWizardConstants';
22
+ import { WizardTitle } from '../form/WizardTitle';
16
23
 
17
- export const AdvancedFields = ({ advancedValues, setAdvancedValues }) => {
18
- const jobTemplate = useSelector(selectJobTemplate);
19
- const effectiveUser = jobTemplate.effective_user;
24
+ export const AdvancedFields = ({
25
+ templateValues,
26
+ advancedValues,
27
+ setAdvancedValues,
28
+ }) => {
29
+ const effectiveUser = useSelector(selectEffectiveUser);
30
+ const advancedTemplateInputs = useSelector(selectAdvancedTemplateInputs);
20
31
  return (
21
32
  <>
22
- <Title headingLevel="h2" className="advanced-fields-title">
23
- {__('Advanced Fields')}
24
- </Title>
25
- <Form>
33
+ <WizardTitle
34
+ title={WIZARD_TITLES.advanced}
35
+ className="advanced-fields-title"
36
+ />
37
+ <Form id="advanced-fields-job-template" autoComplete="off">
38
+ <TemplateInputsFields
39
+ inputs={advancedTemplateInputs}
40
+ value={advancedValues.templateValues}
41
+ setValue={newValue => setAdvancedValues({ templateValues: newValue })}
42
+ />
26
43
  {effectiveUser?.overridable && (
27
44
  <EffectiveUserField
28
45
  value={advancedValues.effectiveUserValue}
@@ -33,6 +50,11 @@ export const AdvancedFields = ({ advancedValues, setAdvancedValues }) => {
33
50
  }
34
51
  />
35
52
  )}
53
+ <DescriptionField
54
+ inputValues={{ ...templateValues, ...advancedValues.templateValues }}
55
+ value={advancedValues.description}
56
+ setValue={newValue => setAdvancedValues({ description: newValue })}
57
+ />
36
58
  <TimeoutToKillField
37
59
  value={advancedValues.timeoutToKill}
38
60
  setValue={newValue =>
@@ -81,6 +103,14 @@ export const AdvancedFields = ({ advancedValues, setAdvancedValues }) => {
81
103
  })
82
104
  }
83
105
  />
106
+ <ExecutionOrderingField
107
+ isRandomizedOrdering={advancedValues.isRandomizedOrdering}
108
+ setValue={newValue =>
109
+ setAdvancedValues({
110
+ isRandomizedOrdering: newValue,
111
+ })
112
+ }
113
+ />
84
114
  </Form>
85
115
  </>
86
116
  );
@@ -89,5 +119,6 @@ export const AdvancedFields = ({ advancedValues, setAdvancedValues }) => {
89
119
  AdvancedFields.propTypes = {
90
120
  advancedValues: PropTypes.object.isRequired,
91
121
  setAdvancedValues: PropTypes.func.isRequired,
122
+ templateValues: PropTypes.object.isRequired,
92
123
  };
93
124
  export default AdvancedFields;