foreman_remote_execution 4.7.0 → 5.0.2

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 (112) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +1 -0
  3. data/app/controllers/api/v2/job_invocations_controller.rb +16 -1
  4. data/app/controllers/ui_job_wizard_controller.rb +16 -4
  5. data/app/graphql/mutations/job_invocations/create.rb +43 -0
  6. data/app/graphql/types/job_invocation_input.rb +13 -0
  7. data/app/graphql/types/recurrence_input.rb +8 -0
  8. data/app/graphql/types/scheduling_input.rb +6 -0
  9. data/app/graphql/types/targeting_enum.rb +7 -0
  10. data/app/helpers/concerns/foreman_remote_execution/hosts_helper_extensions.rb +20 -9
  11. data/app/helpers/remote_execution_helper.rb +1 -1
  12. data/app/lib/actions/remote_execution/run_host_job.rb +6 -1
  13. data/app/lib/actions/remote_execution/run_hosts_job.rb +57 -3
  14. data/app/mailers/rex_job_mailer.rb +15 -0
  15. data/app/models/concerns/foreman_remote_execution/host_extensions.rb +12 -0
  16. data/app/models/job_invocation.rb +4 -0
  17. data/app/models/job_invocation_composer.rb +21 -13
  18. data/app/models/remote_execution_provider.rb +18 -2
  19. data/app/models/rex_mail_notification.rb +13 -0
  20. data/app/models/targeting.rb +3 -3
  21. data/app/services/ui_notifications/remote_execution_jobs/base_job_finish.rb +2 -1
  22. data/app/views/dashboard/_latest-jobs.html.erb +21 -0
  23. data/app/views/job_invocations/_preview_hosts_list.html.erb +1 -1
  24. data/app/views/job_invocations/refresh.js.erb +1 -0
  25. data/app/views/rex_job_mailer/job_finished.html.erb +24 -0
  26. data/app/views/rex_job_mailer/job_finished.text.erb +9 -0
  27. data/app/views/template_invocations/show.html.erb +3 -2
  28. data/config/routes.rb +1 -0
  29. data/db/migrate/20210816100932_rex_setting_category_to_dsl.rb +5 -0
  30. data/db/seeds.d/50-notification_blueprints.rb +14 -0
  31. data/db/seeds.d/95-mail_notifications.rb +24 -0
  32. data/foreman_remote_execution.gemspec +1 -1
  33. data/lib/foreman_remote_execution/engine.rb +116 -7
  34. data/lib/foreman_remote_execution/version.rb +1 -1
  35. data/package.json +9 -7
  36. data/test/functional/api/v2/job_invocations_controller_test.rb +20 -0
  37. data/test/functional/cockpit_controller_test.rb +0 -1
  38. data/test/graphql/mutations/job_invocations/create.rb +58 -0
  39. data/test/helpers/remote_execution_helper_test.rb +0 -1
  40. data/test/unit/actions/run_host_job_test.rb +21 -0
  41. data/test/unit/actions/run_hosts_job_test.rb +99 -4
  42. data/test/unit/concerns/host_extensions_test.rb +36 -3
  43. data/test/unit/job_invocation_composer_test.rb +3 -5
  44. data/test/unit/job_invocation_report_template_test.rb +16 -13
  45. data/test/unit/job_template_effective_user_test.rb +0 -4
  46. data/test/unit/remote_execution_provider_test.rb +46 -4
  47. data/test/unit/targeting_test.rb +68 -1
  48. data/webpack/JobWizard/JobWizard.js +142 -28
  49. data/webpack/JobWizard/JobWizard.scss +86 -33
  50. data/webpack/JobWizard/JobWizardConstants.js +44 -0
  51. data/webpack/JobWizard/JobWizardSelectors.js +32 -0
  52. data/webpack/JobWizard/__tests__/fixtures.js +89 -6
  53. data/webpack/JobWizard/__tests__/integration.test.js +29 -22
  54. data/webpack/JobWizard/__tests__/validation.test.js +141 -0
  55. data/webpack/JobWizard/autofill.js +38 -0
  56. data/webpack/JobWizard/index.js +7 -0
  57. data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +23 -9
  58. data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +32 -9
  59. data/webpack/JobWizard/steps/AdvancedFields/Fields.js +48 -1
  60. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +242 -23
  61. data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +82 -0
  62. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +5 -2
  63. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +3 -2
  64. data/webpack/JobWizard/steps/CategoryAndTemplate/index.js +3 -2
  65. data/webpack/JobWizard/steps/HostsAndInputs/HostPreviewModal.js +62 -0
  66. data/webpack/JobWizard/steps/HostsAndInputs/HostSearch.js +54 -0
  67. data/webpack/JobWizard/steps/HostsAndInputs/SelectAPI.js +33 -0
  68. data/webpack/JobWizard/steps/HostsAndInputs/SelectGQL.js +52 -0
  69. data/webpack/JobWizard/steps/HostsAndInputs/SelectedChips.js +100 -0
  70. data/webpack/JobWizard/steps/HostsAndInputs/TemplateInputs.js +23 -0
  71. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/HostsAndInputs.test.js +151 -0
  72. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/TemplateInputs.test.js +53 -0
  73. data/webpack/JobWizard/steps/HostsAndInputs/buildHostQuery.js +18 -0
  74. data/webpack/JobWizard/steps/HostsAndInputs/hostgroups.gql +8 -0
  75. data/webpack/JobWizard/steps/HostsAndInputs/hosts.gql +8 -0
  76. data/webpack/JobWizard/steps/HostsAndInputs/index.js +214 -0
  77. data/webpack/JobWizard/steps/ReviewDetails/index.js +193 -0
  78. data/webpack/JobWizard/steps/Schedule/PurposeField.js +31 -0
  79. data/webpack/JobWizard/steps/Schedule/QueryType.js +46 -43
  80. data/webpack/JobWizard/steps/Schedule/RepeatCron.js +53 -0
  81. data/webpack/JobWizard/steps/Schedule/RepeatDaily.js +37 -0
  82. data/webpack/JobWizard/steps/Schedule/RepeatHour.js +54 -0
  83. data/webpack/JobWizard/steps/Schedule/RepeatMonth.js +46 -0
  84. data/webpack/JobWizard/steps/Schedule/RepeatOn.js +95 -31
  85. data/webpack/JobWizard/steps/Schedule/RepeatWeek.js +70 -0
  86. data/webpack/JobWizard/steps/Schedule/ScheduleType.js +24 -21
  87. data/webpack/JobWizard/steps/Schedule/StartEndDates.js +78 -23
  88. data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +402 -0
  89. data/webpack/JobWizard/steps/Schedule/__tests__/StartEndDates.test.js +20 -10
  90. data/webpack/JobWizard/steps/Schedule/index.js +166 -29
  91. data/webpack/JobWizard/steps/form/DateTimePicker.js +126 -0
  92. data/webpack/JobWizard/steps/form/FormHelpers.js +4 -0
  93. data/webpack/JobWizard/steps/form/Formatter.js +49 -17
  94. data/webpack/JobWizard/steps/form/NumberInput.js +5 -2
  95. data/webpack/JobWizard/steps/form/ResourceSelect.js +29 -0
  96. data/webpack/JobWizard/steps/form/SearchSelect.js +121 -0
  97. data/webpack/JobWizard/steps/form/SelectField.js +14 -3
  98. data/webpack/JobWizard/steps/form/WizardTitle.js +14 -0
  99. data/webpack/JobWizard/steps/form/__tests__/SelectSearch.test.js +33 -0
  100. data/webpack/JobWizard/submit.js +120 -0
  101. data/webpack/JobWizard/validation.js +53 -0
  102. data/webpack/__mocks__/foremanReact/Root/Context/ForemanContext/index.js +2 -0
  103. data/webpack/__mocks__/foremanReact/common/I18n.js +2 -0
  104. data/webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteActions.js +1 -0
  105. data/webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteConstants.js +1 -0
  106. data/webpack/__mocks__/foremanReact/routes/RouterSelector.js +1 -0
  107. data/webpack/helpers.js +1 -0
  108. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +2 -1
  109. metadata +53 -7
  110. data/app/models/setting/remote_execution.rb +0 -88
  111. data/webpack/JobWizard/steps/AdvancedFields/__tests__/DescriptionField.test.js +0 -23
  112. data/webpack/JobWizard/steps/form/__tests__/Formatter.test.js.example +0 -76
@@ -1,53 +1,106 @@
1
1
  .job-wizard {
2
+ .wizard-title {
3
+ margin-bottom: 25px;
4
+ }
5
+
6
+ .pf-c-wizard__nav.pf-m-expanded {
7
+ z-index: calc(
8
+ var(--pf-c-wizard__footer--ZIndex) + 2
9
+ ); // So the small screen navigation can be shown above the select box
10
+ }
11
+
2
12
  .pf-c-wizard__main {
13
+ overflow: visible;
3
14
  z-index: calc(
4
15
  var(--pf-c-wizard__footer--ZIndex) + 1
5
16
  ); // So the select box can be shown above the wizard footer
6
17
  }
7
-
18
+ .pf-c-wizard__nav {
19
+ z-index: calc(
20
+ var(--pf-c-wizard__footer--ZIndex) + 2
21
+ ); // So the navigation box can be shown above the wizard body
22
+ }
8
23
  .pf-c-wizard__main-body {
9
24
  max-width: 500px;
10
25
  .advanced-fields-title {
11
26
  margin-bottom: 10px;
12
27
  }
13
- #advanced-fields-job-template {
14
- .foreman-search-field {
15
- // Giving pf3 search bar a pf4 look
16
- .search-bar {
17
- display: block;
18
- }
19
- .input-group-btn {
20
- display: none;
21
- }
22
- li {
23
- font-size: 16px;
24
- }
25
- input {
26
- font-size: 16px;
27
- height: 36px;
28
- }
29
- .foreman-autocomplete .autocomplete-focus-shortcut {
30
- top: 8px;
31
- font-size: 16px;
32
- }
33
- .foreman-autocomplete .autocomplete-aux {
34
- top: 8px;
35
- font-size: 16px;
36
- .autocomplete-clear-button {
37
- font-size: 16px;
38
- }
39
- }
28
+
29
+ }
30
+
31
+ .gray-text {
32
+ color: var(--pf-global--Color--dark-200);
33
+ }
34
+
35
+ .target-hosts-and-inputs {
36
+ .hosts-chip-group {
37
+ margin-top: 8px;
38
+ float: left;
39
+ clear: left;
40
+ display: block;
41
+ }
42
+ .clear-chips {
43
+ margin-top: 8px;
44
+ }
45
+ .pf-c-select__toggle-typeahead {
46
+ border: 0px;
47
+ }
48
+
49
+ .target-method-select {
50
+ .pf-c-select__toggle-wrapper {
51
+ flex-wrap: nowrap;
40
52
  }
41
53
  }
42
54
  }
43
-
55
+ input[type='radio'],
56
+ input[type='checkbox'] {
57
+ margin: 0;
58
+ }
44
59
  .schedule-tab {
45
- input[type='radio'],
46
- input[type='checkbox'] {
47
- margin: 0;
48
- }
49
60
  .advanced-scheduling-button {
50
61
  text-align: start;
51
62
  }
63
+ #repeat-on-weekly {
64
+ display: grid;
65
+ grid-template-columns: repeat(7, 1fr);
66
+ }
67
+ .pf-l-grid {
68
+ gap: var(--pf-c-form--GridGap);
69
+ }
70
+ #repeat-on-hourly {
71
+ max-height: 300px;
72
+ overflow: scroll;
73
+ }
74
+ }
75
+
76
+ .pf-c-date-picker {
77
+ vertical-align: top;
78
+ }
79
+
80
+ .time-picker {
81
+ width: 150px;
82
+ }
83
+
84
+ input[type='radio'],
85
+ input[type='checkbox'] {
86
+ // overwriting bootstrap/_forms.scss margin: 4px 0 0;
87
+ margin: 0;
88
+ }
89
+ textarea {
90
+ min-height: 40px;
91
+ min-width: 100px;
92
+ }
93
+ #host-selection {
94
+ width: 500px;
95
+ }
96
+
97
+ .pf-c-modal-box {
98
+ width: auto;
99
+ }
100
+
101
+ .review-details {
102
+ .advanced-fields {
103
+ margin-left: 10px;
104
+ }
52
105
  }
53
106
  }
@@ -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;