foreman_remote_execution 4.7.0 → 5.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +1 -0
- data/app/controllers/api/v2/job_invocations_controller.rb +16 -1
- data/app/controllers/ui_job_wizard_controller.rb +16 -4
- data/app/graphql/mutations/job_invocations/create.rb +43 -0
- data/app/graphql/types/job_invocation_input.rb +13 -0
- data/app/graphql/types/recurrence_input.rb +8 -0
- data/app/graphql/types/scheduling_input.rb +6 -0
- data/app/graphql/types/targeting_enum.rb +7 -0
- data/app/helpers/concerns/foreman_remote_execution/hosts_helper_extensions.rb +20 -9
- data/app/helpers/remote_execution_helper.rb +1 -1
- data/app/lib/actions/remote_execution/run_host_job.rb +6 -1
- data/app/lib/actions/remote_execution/run_hosts_job.rb +57 -3
- data/app/mailers/rex_job_mailer.rb +15 -0
- data/app/models/concerns/foreman_remote_execution/host_extensions.rb +12 -0
- data/app/models/job_invocation.rb +4 -0
- data/app/models/job_invocation_composer.rb +21 -13
- data/app/models/remote_execution_provider.rb +18 -2
- data/app/models/rex_mail_notification.rb +13 -0
- data/app/models/targeting.rb +3 -3
- data/app/services/ui_notifications/remote_execution_jobs/base_job_finish.rb +2 -1
- data/app/views/dashboard/_latest-jobs.html.erb +21 -0
- data/app/views/job_invocations/_preview_hosts_list.html.erb +1 -1
- data/app/views/job_invocations/refresh.js.erb +1 -0
- data/app/views/rex_job_mailer/job_finished.html.erb +24 -0
- data/app/views/rex_job_mailer/job_finished.text.erb +9 -0
- data/app/views/template_invocations/show.html.erb +3 -2
- data/config/routes.rb +1 -0
- data/db/migrate/20210816100932_rex_setting_category_to_dsl.rb +5 -0
- data/db/seeds.d/50-notification_blueprints.rb +14 -0
- data/db/seeds.d/95-mail_notifications.rb +24 -0
- data/foreman_remote_execution.gemspec +1 -1
- data/lib/foreman_remote_execution/engine.rb +116 -7
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/package.json +9 -7
- data/test/functional/api/v2/job_invocations_controller_test.rb +20 -0
- data/test/functional/cockpit_controller_test.rb +0 -1
- data/test/graphql/mutations/job_invocations/create.rb +58 -0
- data/test/helpers/remote_execution_helper_test.rb +0 -1
- data/test/unit/actions/run_host_job_test.rb +21 -0
- data/test/unit/actions/run_hosts_job_test.rb +99 -4
- data/test/unit/concerns/host_extensions_test.rb +36 -3
- data/test/unit/job_invocation_composer_test.rb +3 -5
- data/test/unit/job_invocation_report_template_test.rb +16 -13
- data/test/unit/job_template_effective_user_test.rb +0 -4
- data/test/unit/remote_execution_provider_test.rb +46 -4
- data/test/unit/targeting_test.rb +68 -1
- data/webpack/JobWizard/JobWizard.js +142 -28
- data/webpack/JobWizard/JobWizard.scss +86 -33
- data/webpack/JobWizard/JobWizardConstants.js +44 -0
- data/webpack/JobWizard/JobWizardSelectors.js +32 -0
- data/webpack/JobWizard/__tests__/fixtures.js +89 -6
- data/webpack/JobWizard/__tests__/integration.test.js +29 -22
- data/webpack/JobWizard/__tests__/validation.test.js +141 -0
- data/webpack/JobWizard/autofill.js +38 -0
- data/webpack/JobWizard/index.js +7 -0
- data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +23 -9
- data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +32 -9
- data/webpack/JobWizard/steps/AdvancedFields/Fields.js +48 -1
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +242 -23
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +82 -0
- data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +5 -2
- data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +3 -2
- data/webpack/JobWizard/steps/CategoryAndTemplate/index.js +3 -2
- data/webpack/JobWizard/steps/HostsAndInputs/HostPreviewModal.js +62 -0
- data/webpack/JobWizard/steps/HostsAndInputs/HostSearch.js +54 -0
- data/webpack/JobWizard/steps/HostsAndInputs/SelectAPI.js +33 -0
- data/webpack/JobWizard/steps/HostsAndInputs/SelectGQL.js +52 -0
- data/webpack/JobWizard/steps/HostsAndInputs/SelectedChips.js +100 -0
- data/webpack/JobWizard/steps/HostsAndInputs/TemplateInputs.js +23 -0
- data/webpack/JobWizard/steps/HostsAndInputs/__tests__/HostsAndInputs.test.js +151 -0
- data/webpack/JobWizard/steps/HostsAndInputs/__tests__/TemplateInputs.test.js +53 -0
- data/webpack/JobWizard/steps/HostsAndInputs/buildHostQuery.js +18 -0
- data/webpack/JobWizard/steps/HostsAndInputs/hostgroups.gql +8 -0
- data/webpack/JobWizard/steps/HostsAndInputs/hosts.gql +8 -0
- data/webpack/JobWizard/steps/HostsAndInputs/index.js +214 -0
- data/webpack/JobWizard/steps/ReviewDetails/index.js +193 -0
- data/webpack/JobWizard/steps/Schedule/PurposeField.js +31 -0
- data/webpack/JobWizard/steps/Schedule/QueryType.js +46 -43
- data/webpack/JobWizard/steps/Schedule/RepeatCron.js +53 -0
- data/webpack/JobWizard/steps/Schedule/RepeatDaily.js +37 -0
- data/webpack/JobWizard/steps/Schedule/RepeatHour.js +54 -0
- data/webpack/JobWizard/steps/Schedule/RepeatMonth.js +46 -0
- data/webpack/JobWizard/steps/Schedule/RepeatOn.js +95 -31
- data/webpack/JobWizard/steps/Schedule/RepeatWeek.js +70 -0
- data/webpack/JobWizard/steps/Schedule/ScheduleType.js +24 -21
- data/webpack/JobWizard/steps/Schedule/StartEndDates.js +78 -23
- data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +402 -0
- data/webpack/JobWizard/steps/Schedule/__tests__/StartEndDates.test.js +20 -10
- data/webpack/JobWizard/steps/Schedule/index.js +166 -29
- data/webpack/JobWizard/steps/form/DateTimePicker.js +126 -0
- data/webpack/JobWizard/steps/form/FormHelpers.js +4 -0
- data/webpack/JobWizard/steps/form/Formatter.js +49 -17
- data/webpack/JobWizard/steps/form/NumberInput.js +5 -2
- data/webpack/JobWizard/steps/form/ResourceSelect.js +29 -0
- data/webpack/JobWizard/steps/form/SearchSelect.js +121 -0
- data/webpack/JobWizard/steps/form/SelectField.js +14 -3
- data/webpack/JobWizard/steps/form/WizardTitle.js +14 -0
- data/webpack/JobWizard/steps/form/__tests__/SelectSearch.test.js +33 -0
- data/webpack/JobWizard/submit.js +120 -0
- data/webpack/JobWizard/validation.js +53 -0
- data/webpack/__mocks__/foremanReact/Root/Context/ForemanContext/index.js +2 -0
- data/webpack/__mocks__/foremanReact/common/I18n.js +2 -0
- data/webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteActions.js +1 -0
- data/webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteConstants.js +1 -0
- data/webpack/__mocks__/foremanReact/routes/RouterSelector.js +1 -0
- data/webpack/helpers.js +1 -0
- data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +2 -1
- metadata +53 -7
- data/app/models/setting/remote_execution.rb +0 -88
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/DescriptionField.test.js +0 -23
- 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
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
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
|
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
|
-
<
|
62
|
-
<
|
63
|
-
|
71
|
+
<MockedProvider mocks={gqlMock} addTypename={false}>
|
72
|
+
<Provider store={store}>
|
73
|
+
<JobWizard />
|
74
|
+
</Provider>
|
75
|
+
</MockedProvider>
|
64
76
|
);
|
65
|
-
const
|
66
|
-
|
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
|
+
};
|
data/webpack/JobWizard/index.js
CHANGED
@@ -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;
|