foreman_remote_execution 4.5.6 → 5.0.0
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/.github/workflows/ruby_ci.yml +7 -0
- 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.rb +16 -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/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 +8 -0
- data/app/models/job_invocation.rb +4 -0
- data/app/models/job_invocation_composer.rb +21 -13
- data/app/models/job_template.rb +1 -1
- data/app/models/remote_execution_provider.rb +17 -2
- data/app/models/rex_mail_notification.rb +13 -0
- data/app/models/targeting.rb +2 -2
- 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/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 +2 -1
- 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 +2 -4
- data/lib/foreman_remote_execution/engine.rb +114 -6
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/package.json +6 -6
- 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/graphql/queries/job_invocation_query_test.rb +31 -0
- data/test/graphql/queries/job_invocations_query_test.rb +35 -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 +40 -7
- data/test/unit/input_template_renderer_test.rb +1 -89
- data/test/unit/job_invocation_composer_test.rb +4 -17
- 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 +34 -4
- data/test/unit/targeting_test.rb +68 -1
- data/webpack/JobWizard/JobWizard.js +106 -15
- data/webpack/JobWizard/JobWizard.scss +73 -39
- data/webpack/JobWizard/JobWizardConstants.js +36 -0
- data/webpack/JobWizard/JobWizardSelectors.js +32 -0
- data/webpack/JobWizard/__tests__/fixtures.js +81 -6
- data/webpack/JobWizard/__tests__/integration.test.js +26 -15
- 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 +7 -4
- data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +32 -9
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +216 -12
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +82 -0
- data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +1 -0
- 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 +82 -7
- data/webpack/JobWizard/steps/HostsAndInputs/__tests__/HostsAndInputs.test.js +151 -0
- data/webpack/JobWizard/steps/HostsAndInputs/__tests__/TemplateInputs.test.js +7 -4
- 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 +182 -34
- 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 +153 -19
- 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 +39 -8
- data/webpack/JobWizard/steps/form/NumberInput.js +3 -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/__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/JobStatusIcon.js +43 -0
- data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +73 -66
- data/webpack/react_app/components/RecentJobsCard/RecentJobsTable.js +98 -0
- data/webpack/react_app/components/RecentJobsCard/constants.js +11 -0
- data/webpack/react_app/components/RecentJobsCard/styles.scss +11 -0
- data/webpack/react_app/extend/fillRecentJobsCard.js +1 -1
- metadata +56 -23
- data/app/models/setting/remote_execution.rb +0 -88
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/DescriptionField.test.js +0 -23
- data/webpack/JobWizard/steps/HostsAndInputs/__tests__/SelectedChips.test.js +0 -37
- data/webpack/JobWizard/steps/form/__tests__/Formatter.test.js.example +0 -76
- data/webpack/react_app/components/RecentJobsCard/styles.css +0 -15
|
@@ -3,70 +3,104 @@
|
|
|
3
3
|
margin-bottom: 25px;
|
|
4
4
|
}
|
|
5
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
|
+
|
|
6
12
|
.pf-c-wizard__main {
|
|
7
13
|
overflow: visible;
|
|
8
14
|
z-index: calc(
|
|
9
15
|
var(--pf-c-wizard__footer--ZIndex) + 1
|
|
10
16
|
); // So the select box can be shown above the wizard footer
|
|
11
17
|
}
|
|
12
|
-
|
|
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
|
+
}
|
|
13
23
|
.pf-c-wizard__main-body {
|
|
14
24
|
max-width: 500px;
|
|
15
25
|
.advanced-fields-title {
|
|
16
26
|
margin-bottom: 10px;
|
|
17
27
|
}
|
|
18
|
-
|
|
19
|
-
.foreman-search-field {
|
|
20
|
-
.rbt-input-hint input{
|
|
21
|
-
display: none;
|
|
22
|
-
}
|
|
23
|
-
// Giving pf3 search bar a pf4 look
|
|
24
|
-
.search-bar {
|
|
25
|
-
display: block;
|
|
26
|
-
}
|
|
27
|
-
.input-group-btn {
|
|
28
|
-
display: none;
|
|
29
|
-
}
|
|
30
|
-
li {
|
|
31
|
-
font-size: 16px;
|
|
32
|
-
}
|
|
33
|
-
input {
|
|
34
|
-
font-size: 16px;
|
|
35
|
-
height: 36px;
|
|
36
|
-
}
|
|
37
|
-
.foreman-autocomplete .autocomplete-focus-shortcut {
|
|
38
|
-
top: 8px;
|
|
39
|
-
font-size: 16px;
|
|
40
|
-
}
|
|
41
|
-
.foreman-autocomplete .autocomplete-aux {
|
|
42
|
-
top: 8px;
|
|
43
|
-
font-size: 16px;
|
|
44
|
-
.autocomplete-clear-button {
|
|
45
|
-
font-size: 16px;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
28
|
+
|
|
50
29
|
}
|
|
51
30
|
|
|
52
|
-
.
|
|
53
|
-
|
|
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;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
54
|
}
|
|
55
55
|
input[type='radio'],
|
|
56
56
|
input[type='checkbox'] {
|
|
57
57
|
margin: 0;
|
|
58
58
|
}
|
|
59
59
|
.schedule-tab {
|
|
60
|
-
input[type='radio'],
|
|
61
|
-
input[type='checkbox'] {
|
|
62
|
-
margin: 0;
|
|
63
|
-
}
|
|
64
60
|
.advanced-scheduling-button {
|
|
65
61
|
text-align: start;
|
|
66
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;
|
|
67
88
|
}
|
|
68
89
|
textarea {
|
|
69
90
|
min-height: 40px;
|
|
70
91
|
min-width: 100px;
|
|
71
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
|
+
}
|
|
105
|
+
}
|
|
72
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 = {
|
|
@@ -22,3 +23,38 @@ export const WIZARD_TITLES = {
|
|
|
22
23
|
schedule: __('Schedule'),
|
|
23
24
|
review: __('Review Details'),
|
|
24
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,9 +104,11 @@ 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
|
|
|
96
109
|
jest.spyOn(selectors, 'selectTemplateInputs');
|
|
97
110
|
jest.spyOn(selectors, 'selectAdvancedTemplateInputs');
|
|
111
|
+
selectors.selectWithKatello.mockImplementation(() => true);
|
|
98
112
|
selectors.selectTemplateInputs.mockImplementation(
|
|
99
113
|
() => jobTemplateResponse.template_inputs
|
|
100
114
|
);
|
|
@@ -106,15 +120,31 @@ export const testSetup = (selectors, api) => {
|
|
|
106
120
|
jobTemplate,
|
|
107
121
|
{ ...jobTemplate, id: 2, name: 'template2' },
|
|
108
122
|
]);
|
|
123
|
+
selectors.selectJobTemplate.mockImplementation(() => jobTemplateResponse);
|
|
109
124
|
const mockStore = configureMockStore([]);
|
|
110
|
-
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
|
+
});
|
|
111
145
|
return store;
|
|
112
146
|
};
|
|
113
147
|
|
|
114
|
-
export const mockTemplate = selectors => {
|
|
115
|
-
selectors.selectJobTemplate.mockImplementation(() => jobTemplate);
|
|
116
|
-
selectors.selectJobCategoriesStatus.mockImplementation(() => 'RESOLVED');
|
|
117
|
-
};
|
|
118
148
|
export const mockApi = api => {
|
|
119
149
|
api.get.mockImplementation(({ handleSuccess, ...action }) => {
|
|
120
150
|
if (action.key === 'JOB_CATEGORIES') {
|
|
@@ -130,7 +160,52 @@ export const mockApi = api => {
|
|
|
130
160
|
handleSuccess({
|
|
131
161
|
data: { results: [jobTemplate] },
|
|
132
162
|
});
|
|
163
|
+
} else if (action.key === 'HOST_IDS') {
|
|
164
|
+
handleSuccess &&
|
|
165
|
+
handleSuccess({
|
|
166
|
+
data: { results: [{ name: 'host1' }, { name: 'host3' }] },
|
|
167
|
+
});
|
|
133
168
|
}
|
|
134
169
|
return { type: 'get', ...action };
|
|
135
170
|
});
|
|
136
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,6 +2,7 @@ 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';
|
|
@@ -11,23 +12,31 @@ import {
|
|
|
11
12
|
mockApi,
|
|
12
13
|
jobCategories,
|
|
13
14
|
jobTemplateResponse as jobTemplate,
|
|
15
|
+
gqlMock,
|
|
14
16
|
} from './fixtures';
|
|
15
17
|
|
|
16
18
|
const store = testSetup(selectors, api);
|
|
17
19
|
|
|
18
|
-
selectors.selectJobTemplate.mockImplementation(() => {});
|
|
19
|
-
|
|
20
|
-
api.get.mockImplementation(({ handleSuccess, ...action }) => {
|
|
21
|
-
if (action.key === 'JOB_CATEGORIES') {
|
|
22
|
-
handleSuccess && handleSuccess({ data: { job_categories: jobCategories } });
|
|
23
|
-
}
|
|
24
|
-
return { type: 'get', ...action };
|
|
25
|
-
});
|
|
26
20
|
describe('Job wizard fill', () => {
|
|
27
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(() => ({}));
|
|
28
37
|
const wrapper = mount(
|
|
29
38
|
<Provider store={store}>
|
|
30
|
-
<JobWizard
|
|
39
|
+
<JobWizard />
|
|
31
40
|
</Provider>
|
|
32
41
|
);
|
|
33
42
|
expect(wrapper.find('.pf-c-wizard__nav-link.pf-m-disabled')).toHaveLength(
|
|
@@ -35,7 +44,8 @@ describe('Job wizard fill', () => {
|
|
|
35
44
|
);
|
|
36
45
|
selectors.selectJobCategoriesStatus.mockImplementation(() => 'RESOLVED');
|
|
37
46
|
expect(store.getActions()).toMatchSnapshot('initial');
|
|
38
|
-
|
|
47
|
+
selectors.selectJobTemplate.mockRestore();
|
|
48
|
+
jest.spyOn(selectors, 'selectJobTemplate');
|
|
39
49
|
selectors.selectJobTemplate.mockImplementation(() => jobTemplate);
|
|
40
50
|
wrapper.find('.pf-c-button.pf-c-select__toggle-button').simulate('click');
|
|
41
51
|
await act(async () => {
|
|
@@ -43,9 +53,9 @@ describe('Job wizard fill', () => {
|
|
|
43
53
|
.find('.pf-c-select__menu-item')
|
|
44
54
|
.first()
|
|
45
55
|
.simulate('click');
|
|
46
|
-
await wrapper.update();
|
|
47
56
|
});
|
|
48
57
|
expect(store.getActions().slice(-1)).toMatchSnapshot('select template');
|
|
58
|
+
wrapper.update();
|
|
49
59
|
expect(wrapper.find('.pf-c-wizard__nav-link.pf-m-disabled')).toHaveLength(
|
|
50
60
|
0
|
|
51
61
|
);
|
|
@@ -53,15 +63,16 @@ describe('Job wizard fill', () => {
|
|
|
53
63
|
|
|
54
64
|
it('have all steps', async () => {
|
|
55
65
|
selectors.selectJobCategoriesStatus.mockImplementation(() => null);
|
|
56
|
-
selectors.selectJobTemplate.mockRestore();
|
|
57
66
|
selectors.selectJobTemplates.mockRestore();
|
|
58
67
|
selectors.selectJobCategories.mockRestore();
|
|
59
68
|
mockApi(api);
|
|
60
69
|
|
|
61
70
|
render(
|
|
62
|
-
<
|
|
63
|
-
<
|
|
64
|
-
|
|
71
|
+
<MockedProvider mocks={gqlMock} addTypename={false}>
|
|
72
|
+
<Provider store={store}>
|
|
73
|
+
<JobWizard />
|
|
74
|
+
</Provider>
|
|
75
|
+
</MockedProvider>
|
|
65
76
|
);
|
|
66
77
|
const titles = Object.values(WIZARD_TITLES);
|
|
67
78
|
const steps = [titles[1], titles[0], ...titles.slice(2)]; // the first title is selected at the beggining
|
|
@@ -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;
|