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.
- 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;
|