foreman_remote_execution 4.6.0 → 5.0.1

Sign up to get free protection for your applications and to get access to all the features.
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;