foreman_remote_execution 4.7.0 → 5.1.0

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