foreman_remote_execution 5.1.0 → 6.2.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/README.md +7 -0
- data/app/controllers/foreman_remote_execution/concerns/api/v2/registration_controller_extensions.rb +5 -0
- data/app/controllers/ui_job_wizard_controller.rb +13 -2
- data/app/helpers/concerns/foreman_remote_execution/hosts_helper_extensions.rb +20 -9
- data/app/helpers/remote_execution_helper.rb +5 -4
- data/app/lib/actions/remote_execution/run_host_job.rb +1 -1
- data/app/mailers/rex_job_mailer.rb +1 -1
- data/app/models/concerns/foreman_remote_execution/host_extensions.rb +9 -1
- data/app/models/concerns/foreman_remote_execution/nic_extensions.rb +6 -4
- data/app/models/concerns/foreman_remote_execution/smart_proxy_extensions.rb +1 -1
- data/app/models/job_invocation_composer.rb +1 -1
- data/app/models/remote_execution_provider.rb +10 -1
- data/app/models/ssh_execution_provider.rb +17 -4
- data/app/models/targeting.rb +1 -1
- data/app/services/default_proxy_proxy_selector.rb +1 -1
- data/app/services/remote_execution_proxy_selector.rb +7 -2
- data/app/views/api/v2/host/main.rabl +1 -0
- data/app/views/dashboard/_latest-jobs.html.erb +1 -1
- data/app/views/job_invocations/_preview_hosts_list.html.erb +1 -1
- data/app/views/template_invocations/show.html.erb +1 -1
- data/app/views/templates/{ssh → script}/check_update.erb +2 -2
- data/app/views/templates/{ssh → script}/module_action.erb +2 -2
- data/app/views/templates/{ssh → script}/package_action.erb +5 -2
- data/app/views/templates/{ssh → script}/power_action.erb +2 -2
- data/app/views/templates/{ssh → script}/puppet_agent_disable.erb +2 -2
- data/app/views/templates/{ssh → script}/puppet_agent_enable.erb +2 -2
- data/app/views/templates/{ssh → script}/puppet_install_modules_from_forge.erb +2 -2
- data/app/views/templates/{ssh → script}/puppet_install_modules_from_git.erb +2 -2
- data/app/views/templates/{ssh → script}/puppet_run_once.erb +2 -2
- data/app/views/templates/{ssh → script}/run_command.erb +2 -2
- data/app/views/templates/{ssh → script}/service_action.erb +2 -2
- data/db/migrate/20220321101835_rename_ssh_provider_to_script.rb +29 -0
- data/db/seeds.d/60-ssh_proxy_feature.rb +3 -0
- data/jsconfig.json +8 -0
- data/lib/foreman_remote_execution/engine.rb +10 -4
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/locale/action_names.rb +3 -4
- data/locale/de/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/de/foreman_remote_execution.po +356 -20
- data/locale/en/foreman_remote_execution.po +356 -20
- data/locale/en_GB/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/en_GB/foreman_remote_execution.po +356 -20
- data/locale/es/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/es/foreman_remote_execution.po +357 -21
- data/locale/foreman_remote_execution.pot +808 -296
- data/locale/fr/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/fr/foreman_remote_execution.po +357 -21
- data/locale/ja/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/ja/foreman_remote_execution.po +357 -21
- data/locale/ko/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/ko/foreman_remote_execution.po +356 -20
- data/locale/pt_BR/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/pt_BR/foreman_remote_execution.po +357 -21
- data/locale/ru/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/ru/foreman_remote_execution.po +356 -20
- data/locale/zh_CN/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/zh_CN/foreman_remote_execution.po +357 -21
- data/locale/zh_TW/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/zh_TW/foreman_remote_execution.po +356 -20
- data/package.json +6 -7
- data/test/unit/concerns/host_extensions_test.rb +2 -1
- data/test/unit/remote_execution_provider_test.rb +2 -0
- data/webpack/JobWizard/JobWizard.js +30 -7
- data/webpack/JobWizard/JobWizard.scss +5 -0
- data/webpack/JobWizard/JobWizardConstants.js +2 -1
- data/webpack/JobWizard/__tests__/fixtures.js +13 -2
- data/webpack/JobWizard/__tests__/integration.test.js +6 -1
- data/webpack/JobWizard/autofill.js +34 -9
- data/webpack/JobWizard/index.js +0 -7
- data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +24 -15
- data/webpack/JobWizard/steps/CategoryAndTemplate/index.js +25 -7
- data/webpack/JobWizard/steps/HostsAndInputs/HostSearch.js +1 -1
- data/webpack/JobWizard/steps/HostsAndInputs/__tests__/HostsAndInputs.test.js +28 -0
- data/webpack/JobWizard/steps/Schedule/RepeatOn.js +1 -1
- data/webpack/JobWizard/steps/form/Formatter.js +18 -3
- data/webpack/JobWizard/steps/form/GroupedSelectField.js +7 -1
- data/webpack/__mocks__/foremanReact/components/Pagination.js +2 -0
- data/webpack/__mocks__/foremanReact/constants.js +1 -0
- data/webpack/global_index.js +4 -0
- data/webpack/react_app/components/FeaturesDropdown/actions.js +13 -0
- data/webpack/react_app/components/FeaturesDropdown/constant.js +2 -0
- data/webpack/react_app/components/FeaturesDropdown/index.js +74 -0
- data/webpack/react_app/components/HostKebab/KebabItems.js +22 -0
- data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +6 -1
- data/webpack/react_app/components/RecentJobsCard/RecentJobsTable.js +2 -2
- data/webpack/react_app/components/RecentJobsCard/constants.js +1 -0
- data/webpack/react_app/components/TargetingHosts/TargetingHostsHelpers.js +1 -1
- data/webpack/react_app/components/TargetingHosts/TargetingHostsPage.js +1 -6
- data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +1 -9
- data/webpack/react_app/components/TargetingHosts/index.js +2 -3
- data/webpack/react_app/extend/fillKebabItems.js +11 -0
- data/webpack/react_app/extend/fillRexFeaturesDropdown.js +11 -0
- metadata +24 -16
- data/app/views/templates/README.md +0 -6
- data/webpack/__mocks__/foremanReact/components/Pagination/PaginationWrapper.js +0 -2
data/package.json
CHANGED
|
@@ -21,20 +21,19 @@
|
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"@babel/core": "^7.7.0",
|
|
24
|
-
"@theforeman/builder": "^
|
|
25
|
-
"@theforeman/eslint-plugin-foreman": "^
|
|
26
|
-
"@theforeman/stories": "^
|
|
27
|
-
"@theforeman/test": "^
|
|
28
|
-
"@theforeman/vendor-dev": "^
|
|
24
|
+
"@theforeman/builder": "^10.1.0",
|
|
25
|
+
"@theforeman/eslint-plugin-foreman": "^10.1.0",
|
|
26
|
+
"@theforeman/stories": "^10.1.0",
|
|
27
|
+
"@theforeman/test": "^10.1.0",
|
|
28
|
+
"@theforeman/vendor-dev": "^10.1.0",
|
|
29
29
|
"babel-eslint": "^10.0.0",
|
|
30
30
|
"eslint": "^6.8.0",
|
|
31
31
|
"prettier": "^1.19.1",
|
|
32
|
-
"@patternfly/react-catalog-view-extension": "^4.8.126",
|
|
33
32
|
"redux-mock-store": "^1.2.2",
|
|
34
33
|
"graphql-tag": "^2.11.0",
|
|
35
34
|
"graphql": "^15.5.0"
|
|
36
35
|
},
|
|
37
36
|
"peerDependencies": {
|
|
38
|
-
"@theforeman/vendor": "^
|
|
37
|
+
"@theforeman/vendor": "^10.1.0"
|
|
39
38
|
}
|
|
40
39
|
}
|
|
@@ -67,7 +67,8 @@ class ForemanRemoteExecutionHostExtensionsTest < ActiveSupport::TestCase
|
|
|
67
67
|
it 'should only have one execution interface' do
|
|
68
68
|
host.interfaces << FactoryBot.build(:nic_managed)
|
|
69
69
|
host.interfaces.each { |interface| interface.execution = true }
|
|
70
|
-
_(host).
|
|
70
|
+
_(host).must_be :valid?
|
|
71
|
+
_(host.interfaces.count(&:execution?)).must_equal 1
|
|
71
72
|
end
|
|
72
73
|
|
|
73
74
|
it 'returns the execution interface' do
|
|
@@ -6,9 +6,11 @@ class RemoteExecutionProviderTest < ActiveSupport::TestCase
|
|
|
6
6
|
it { _(providers).must_be_kind_of HashWithIndifferentAccess }
|
|
7
7
|
it 'makes providers accessible using symbol' do
|
|
8
8
|
_(providers[:SSH]).must_equal SSHExecutionProvider
|
|
9
|
+
_(providers[:script]).must_equal ScriptExecutionProvider
|
|
9
10
|
end
|
|
10
11
|
it 'makes providers accessible using string' do
|
|
11
12
|
_(providers['SSH']).must_equal SSHExecutionProvider
|
|
13
|
+
_(providers['script']).must_equal ScriptExecutionProvider
|
|
12
14
|
end
|
|
13
15
|
end
|
|
14
16
|
|
|
@@ -20,6 +20,8 @@ import {
|
|
|
20
20
|
selectTemplateError,
|
|
21
21
|
selectJobTemplate,
|
|
22
22
|
selectIsSubmitting,
|
|
23
|
+
selectRouterSearch,
|
|
24
|
+
selectIsLoading,
|
|
23
25
|
} from './JobWizardSelectors';
|
|
24
26
|
import Schedule from './steps/Schedule/';
|
|
25
27
|
import HostsAndInputs from './steps/HostsAndInputs/';
|
|
@@ -41,6 +43,7 @@ export const JobWizard = () => {
|
|
|
41
43
|
hostGroups: [],
|
|
42
44
|
});
|
|
43
45
|
const [hostsSearchQuery, setHostsSearchQuery] = useState('');
|
|
46
|
+
const [fills, setFills] = useState(useSelector(selectRouterSearch));
|
|
44
47
|
const dispatch = useDispatch();
|
|
45
48
|
|
|
46
49
|
const setDefaults = useCallback(
|
|
@@ -49,17 +52,26 @@ export const JobWizard = () => {
|
|
|
49
52
|
template_inputs,
|
|
50
53
|
advanced_template_inputs,
|
|
51
54
|
effective_user,
|
|
52
|
-
job_template: {
|
|
55
|
+
job_template: {
|
|
56
|
+
name,
|
|
57
|
+
execution_timeout_interval,
|
|
58
|
+
description_format,
|
|
59
|
+
job_category,
|
|
60
|
+
},
|
|
53
61
|
},
|
|
54
62
|
}) => {
|
|
63
|
+
if (!category.length) {
|
|
64
|
+
setCategory(current => (current.length ? current : job_category));
|
|
65
|
+
}
|
|
55
66
|
const advancedTemplateValues = {};
|
|
56
67
|
const defaultTemplateValues = {};
|
|
57
68
|
const inputs = template_inputs;
|
|
58
69
|
const advancedInputs = advanced_template_inputs;
|
|
59
70
|
if (inputs) {
|
|
60
|
-
setTemplateValues(
|
|
71
|
+
setTemplateValues(prev => {
|
|
61
72
|
inputs.forEach(input => {
|
|
62
|
-
defaultTemplateValues[input.name] =
|
|
73
|
+
defaultTemplateValues[input.name] =
|
|
74
|
+
prev[input.name] || input?.default || '';
|
|
63
75
|
});
|
|
64
76
|
return defaultTemplateValues;
|
|
65
77
|
});
|
|
@@ -67,7 +79,8 @@ export const JobWizard = () => {
|
|
|
67
79
|
setAdvancedValues(currentAdvancedValues => {
|
|
68
80
|
if (advancedInputs) {
|
|
69
81
|
advancedInputs.forEach(input => {
|
|
70
|
-
advancedTemplateValues[input.name] =
|
|
82
|
+
advancedTemplateValues[input.name] =
|
|
83
|
+
currentAdvancedValues[input.name] || input?.default || '';
|
|
71
84
|
});
|
|
72
85
|
}
|
|
73
86
|
const generateDefaultDescription = () => {
|
|
@@ -89,7 +102,7 @@ export const JobWizard = () => {
|
|
|
89
102
|
};
|
|
90
103
|
});
|
|
91
104
|
},
|
|
92
|
-
[]
|
|
105
|
+
[category.length]
|
|
93
106
|
);
|
|
94
107
|
useEffect(() => {
|
|
95
108
|
if (jobTemplateID) {
|
|
@@ -108,15 +121,24 @@ export const JobWizard = () => {
|
|
|
108
121
|
templateValues,
|
|
109
122
|
});
|
|
110
123
|
useAutoFill({
|
|
124
|
+
fills,
|
|
125
|
+
setFills,
|
|
111
126
|
setSelectedTargets,
|
|
112
127
|
setHostsSearchQuery,
|
|
128
|
+
setJobTemplateID,
|
|
129
|
+
setTemplateValues,
|
|
113
130
|
});
|
|
114
131
|
const templateError = !!useSelector(selectTemplateError);
|
|
115
132
|
const templateResponse = useSelector(selectJobTemplate);
|
|
116
133
|
const isSubmitting = useSelector(selectIsSubmitting);
|
|
134
|
+
const isTemplatesLoading = useSelector(state =>
|
|
135
|
+
selectIsLoading(state, JOB_TEMPLATE)
|
|
136
|
+
);
|
|
117
137
|
const isTemplate =
|
|
118
|
-
!
|
|
119
|
-
|
|
138
|
+
!isTemplatesLoading &&
|
|
139
|
+
!templateError &&
|
|
140
|
+
!!jobTemplateID &&
|
|
141
|
+
templateResponse.job_template;
|
|
120
142
|
const steps = [
|
|
121
143
|
{
|
|
122
144
|
name: WIZARD_TITLES.categoryAndTemplate,
|
|
@@ -126,6 +148,7 @@ export const JobWizard = () => {
|
|
|
126
148
|
setJobTemplate={setJobTemplateID}
|
|
127
149
|
category={category}
|
|
128
150
|
setCategory={setCategory}
|
|
151
|
+
isFeature={!!fills.feature}
|
|
129
152
|
/>
|
|
130
153
|
),
|
|
131
154
|
enableNext: isTemplate,
|
|
@@ -47,7 +47,7 @@ export const hostMethods = {
|
|
|
47
47
|
searchQuery: __('Search query'),
|
|
48
48
|
};
|
|
49
49
|
|
|
50
|
-
export const hostQuerySearchID = '
|
|
50
|
+
export const hostQuerySearchID = 'mainHostQuery';
|
|
51
51
|
export const hostsController = 'hosts';
|
|
52
52
|
|
|
53
53
|
export const dataName = {
|
|
@@ -58,3 +58,4 @@ export const HOSTS_TO_PREVIEW_AMOUNT = 20;
|
|
|
58
58
|
|
|
59
59
|
export const DEBOUNCE_HOST_COUNT = 700;
|
|
60
60
|
export const HOST_IDS = 'HOST_IDS';
|
|
61
|
+
export const REX_FEATURE = 'REX_FEATURE';
|
|
@@ -69,6 +69,7 @@ export const jobTemplateResponse = {
|
|
|
69
69
|
default: '',
|
|
70
70
|
hidden_value: false,
|
|
71
71
|
url: 'foreman_tasks/tasks',
|
|
72
|
+
resource_type_tableize: 'hosts',
|
|
72
73
|
},
|
|
73
74
|
{
|
|
74
75
|
name: 'adv date',
|
|
@@ -96,7 +97,7 @@ export const jobTemplateResponse = {
|
|
|
96
97
|
],
|
|
97
98
|
};
|
|
98
99
|
|
|
99
|
-
export const jobCategories = ['Ansible Commands', 'Puppet'
|
|
100
|
+
export const jobCategories = ['Services', 'Ansible Commands', 'Puppet'];
|
|
100
101
|
|
|
101
102
|
export const testSetup = (selectors, api) => {
|
|
102
103
|
jest.spyOn(api, 'get');
|
|
@@ -149,7 +150,12 @@ export const mockApi = api => {
|
|
|
149
150
|
api.get.mockImplementation(({ handleSuccess, ...action }) => {
|
|
150
151
|
if (action.key === 'JOB_CATEGORIES') {
|
|
151
152
|
handleSuccess &&
|
|
152
|
-
handleSuccess({
|
|
153
|
+
handleSuccess({
|
|
154
|
+
data: {
|
|
155
|
+
job_categories: jobCategories,
|
|
156
|
+
default_category: 'Ansible Commands',
|
|
157
|
+
},
|
|
158
|
+
});
|
|
153
159
|
} else if (action.key === 'JOB_TEMPLATE') {
|
|
154
160
|
handleSuccess &&
|
|
155
161
|
handleSuccess({
|
|
@@ -165,6 +171,11 @@ export const mockApi = api => {
|
|
|
165
171
|
handleSuccess({
|
|
166
172
|
data: { results: [{ name: 'host1' }, { name: 'host3' }] },
|
|
167
173
|
});
|
|
174
|
+
} else if (action.key === 'REX_FEATURE') {
|
|
175
|
+
handleSuccess &&
|
|
176
|
+
handleSuccess({
|
|
177
|
+
data: { job_template_id: 178 },
|
|
178
|
+
});
|
|
168
179
|
}
|
|
169
180
|
return { type: 'get', ...action };
|
|
170
181
|
});
|
|
@@ -22,7 +22,12 @@ describe('Job wizard fill', () => {
|
|
|
22
22
|
api.get.mockImplementation(({ handleSuccess, ...action }) => {
|
|
23
23
|
if (action.key === 'JOB_CATEGORIES') {
|
|
24
24
|
handleSuccess &&
|
|
25
|
-
handleSuccess({
|
|
25
|
+
handleSuccess({
|
|
26
|
+
data: {
|
|
27
|
+
job_categories: jobCategories,
|
|
28
|
+
default_category: 'Ansible Commands',
|
|
29
|
+
},
|
|
30
|
+
});
|
|
26
31
|
} else if (action.key === 'JOB_TEMPLATE') {
|
|
27
32
|
handleSuccess &&
|
|
28
33
|
handleSuccess({
|
|
@@ -1,22 +1,29 @@
|
|
|
1
1
|
import { useEffect } from 'react';
|
|
2
|
-
import { useDispatch
|
|
2
|
+
import { useDispatch } from 'react-redux';
|
|
3
3
|
import { get } from 'foremanReact/redux/API';
|
|
4
|
-
import { HOST_IDS } from './JobWizardConstants';
|
|
5
|
-
import { selectRouterSearch } from './JobWizardSelectors';
|
|
4
|
+
import { HOST_IDS, REX_FEATURE } from './JobWizardConstants';
|
|
6
5
|
import './JobWizard.scss';
|
|
7
6
|
|
|
8
|
-
export const useAutoFill = ({
|
|
9
|
-
|
|
7
|
+
export const useAutoFill = ({
|
|
8
|
+
fills,
|
|
9
|
+
setFills,
|
|
10
|
+
setSelectedTargets,
|
|
11
|
+
setHostsSearchQuery,
|
|
12
|
+
setJobTemplateID,
|
|
13
|
+
setTemplateValues,
|
|
14
|
+
}) => {
|
|
10
15
|
const dispatch = useDispatch();
|
|
11
16
|
|
|
12
17
|
useEffect(() => {
|
|
13
18
|
if (Object.keys(fills).length) {
|
|
14
|
-
|
|
19
|
+
const { 'host_ids[]': hostIds, search, feature, ...rest } = { ...fills };
|
|
20
|
+
setFills({});
|
|
21
|
+
if (hostIds) {
|
|
15
22
|
dispatch(
|
|
16
23
|
get({
|
|
17
24
|
key: HOST_IDS,
|
|
18
25
|
url: '/api/hosts',
|
|
19
|
-
params: { search: `id = ${
|
|
26
|
+
params: { search: `id = ${hostIds.join(' or id = ')}` },
|
|
20
27
|
handleSuccess: ({ data }) => {
|
|
21
28
|
setSelectedTargets(currentTargets => ({
|
|
22
29
|
...currentTargets,
|
|
@@ -29,8 +36,26 @@ export const useAutoFill = ({ setSelectedTargets, setHostsSearchQuery }) => {
|
|
|
29
36
|
})
|
|
30
37
|
);
|
|
31
38
|
}
|
|
32
|
-
if (
|
|
33
|
-
setHostsSearchQuery(
|
|
39
|
+
if (search) {
|
|
40
|
+
setHostsSearchQuery(search);
|
|
41
|
+
}
|
|
42
|
+
if (feature) {
|
|
43
|
+
dispatch(
|
|
44
|
+
get({
|
|
45
|
+
key: REX_FEATURE,
|
|
46
|
+
url: `/api/remote_execution_features/${feature}`,
|
|
47
|
+
handleSuccess: ({ data }) => {
|
|
48
|
+
setJobTemplateID(data.job_template_id);
|
|
49
|
+
},
|
|
50
|
+
})
|
|
51
|
+
);
|
|
52
|
+
Object.keys(rest).forEach(key => {
|
|
53
|
+
const re = /inputs\[(?<input>.*)\]/g;
|
|
54
|
+
const input = re.exec(key)?.groups?.input;
|
|
55
|
+
if (input) {
|
|
56
|
+
setTemplateValues(prev => ({ ...prev, [input]: rest[key] }));
|
|
57
|
+
}
|
|
58
|
+
});
|
|
34
59
|
}
|
|
35
60
|
}
|
|
36
61
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
data/webpack/JobWizard/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import PropTypes from 'prop-types';
|
|
3
2
|
import { Title, Divider } from '@patternfly/react-core';
|
|
4
3
|
import { translate as __ } from 'foremanReact/common/I18n';
|
|
5
4
|
import PageLayout from 'foremanReact/routes/common/PageLayout/PageLayout';
|
|
@@ -30,10 +29,4 @@ const JobWizardPage = () => {
|
|
|
30
29
|
);
|
|
31
30
|
};
|
|
32
31
|
|
|
33
|
-
JobWizardPage.propTypes = {
|
|
34
|
-
location: PropTypes.shape({
|
|
35
|
-
search: PropTypes.string,
|
|
36
|
-
}).isRequired,
|
|
37
|
-
};
|
|
38
|
-
|
|
39
32
|
export default JobWizardPage;
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
|
+
import { useSelector } from 'react-redux';
|
|
3
4
|
import { Text, TextVariants, Form, Alert } from '@patternfly/react-core';
|
|
4
5
|
import { translate as __ } from 'foremanReact/common/I18n';
|
|
5
6
|
import { SelectField } from '../form/SelectField';
|
|
6
7
|
import { GroupedSelectField } from '../form/GroupedSelectField';
|
|
7
8
|
import { WizardTitle } from '../form/WizardTitle';
|
|
8
|
-
import { WIZARD_TITLES } from '../../JobWizardConstants';
|
|
9
|
+
import { WIZARD_TITLES, JOB_TEMPLATES } from '../../JobWizardConstants';
|
|
10
|
+
import { selectIsLoading } from '../../JobWizardSelectors';
|
|
9
11
|
|
|
10
12
|
export const CategoryAndTemplate = ({
|
|
11
13
|
jobCategories,
|
|
@@ -17,18 +19,23 @@ export const CategoryAndTemplate = ({
|
|
|
17
19
|
errors,
|
|
18
20
|
}) => {
|
|
19
21
|
const templatesGroups = {};
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
22
|
+
const isTemplatesLoading = useSelector(state =>
|
|
23
|
+
selectIsLoading(state, JOB_TEMPLATES)
|
|
24
|
+
);
|
|
25
|
+
if (!isTemplatesLoading) {
|
|
26
|
+
jobTemplates.forEach(template => {
|
|
27
|
+
if (templatesGroups[template.provider_type]?.options)
|
|
28
|
+
templatesGroups[template.provider_type].options.push({
|
|
29
|
+
label: template.name,
|
|
30
|
+
value: template.id,
|
|
31
|
+
});
|
|
32
|
+
else
|
|
33
|
+
templatesGroups[template.provider_type] = {
|
|
34
|
+
options: [{ label: template.name, value: template.id }],
|
|
35
|
+
groupLabel: template.provider_type,
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
}
|
|
32
39
|
|
|
33
40
|
const selectedTemplate = jobTemplates.find(
|
|
34
41
|
template => template.id === selectedTemplateID
|
|
@@ -60,8 +67,10 @@ export const CategoryAndTemplate = ({
|
|
|
60
67
|
fieldId="job_template"
|
|
61
68
|
groups={Object.values(templatesGroups)}
|
|
62
69
|
setSelected={setJobTemplate}
|
|
63
|
-
selected={selectedTemplate}
|
|
64
|
-
isDisabled={
|
|
70
|
+
selected={isTemplatesLoading ? [] : selectedTemplate}
|
|
71
|
+
isDisabled={
|
|
72
|
+
!!(categoryError || allTemplatesError || isTemplatesLoading)
|
|
73
|
+
}
|
|
65
74
|
placeholderText={allTemplatesError ? __('Error') : ''}
|
|
66
75
|
/>
|
|
67
76
|
{isError && (
|
|
@@ -25,6 +25,7 @@ const ConnectedCategoryAndTemplate = ({
|
|
|
25
25
|
setJobTemplate,
|
|
26
26
|
category,
|
|
27
27
|
setCategory,
|
|
28
|
+
isFeature,
|
|
28
29
|
}) => {
|
|
29
30
|
const dispatch = useDispatch();
|
|
30
31
|
|
|
@@ -36,12 +37,23 @@ const ConnectedCategoryAndTemplate = ({
|
|
|
36
37
|
get({
|
|
37
38
|
key: JOB_CATEGORIES,
|
|
38
39
|
url: '/ui_job_wizard/categories',
|
|
39
|
-
handleSuccess:
|
|
40
|
-
|
|
40
|
+
handleSuccess: ({
|
|
41
|
+
data: {
|
|
42
|
+
default_category: defaultCategory,
|
|
43
|
+
job_categories: jobCategories,
|
|
44
|
+
default_template: defaultTemplate,
|
|
45
|
+
},
|
|
46
|
+
}) => {
|
|
47
|
+
if (!isFeature) {
|
|
48
|
+
setCategory(defaultCategory || jobCategories[0] || '');
|
|
49
|
+
if (defaultTemplate) setJobTemplate(defaultTemplate);
|
|
50
|
+
}
|
|
51
|
+
},
|
|
41
52
|
})
|
|
42
53
|
);
|
|
43
54
|
}
|
|
44
|
-
|
|
55
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
56
|
+
}, [jobCategoriesStatus]);
|
|
45
57
|
|
|
46
58
|
const jobCategories = useSelector(selectJobCategories);
|
|
47
59
|
useEffect(() => {
|
|
@@ -55,14 +67,19 @@ const ConnectedCategoryAndTemplate = ({
|
|
|
55
67
|
per_page: 'all',
|
|
56
68
|
}),
|
|
57
69
|
handleSuccess: response => {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
70
|
+
if (!jobTemplate)
|
|
71
|
+
setJobTemplate(
|
|
72
|
+
current =>
|
|
73
|
+
current ||
|
|
74
|
+
Number(filterJobTemplates(response?.data?.results)[0]?.id) ||
|
|
75
|
+
null
|
|
76
|
+
);
|
|
61
77
|
},
|
|
62
78
|
})
|
|
63
79
|
);
|
|
64
80
|
}
|
|
65
|
-
|
|
81
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
82
|
+
}, [category, dispatch]);
|
|
66
83
|
|
|
67
84
|
const jobTemplates = useSelector(selectJobTemplates);
|
|
68
85
|
|
|
@@ -89,6 +106,7 @@ ConnectedCategoryAndTemplate.propTypes = {
|
|
|
89
106
|
setJobTemplate: PropTypes.func.isRequired,
|
|
90
107
|
category: PropTypes.string.isRequired,
|
|
91
108
|
setCategory: PropTypes.func.isRequired,
|
|
109
|
+
isFeature: PropTypes.bool.isRequired,
|
|
92
110
|
};
|
|
93
111
|
ConnectedCategoryAndTemplate.defaultProps = { jobTemplate: null };
|
|
94
112
|
|
|
@@ -10,7 +10,7 @@ import { noop } from '../../../helpers';
|
|
|
10
10
|
|
|
11
11
|
export const HostSearch = ({ value, setValue }) => {
|
|
12
12
|
const searchQuery = useSelector(
|
|
13
|
-
state => state.autocomplete?.
|
|
13
|
+
state => state.autocomplete?.[hostQuerySearchID]?.searchQuery
|
|
14
14
|
);
|
|
15
15
|
useEffect(() => {
|
|
16
16
|
setValue(searchQuery || '');
|
|
@@ -148,4 +148,32 @@ describe('Hosts', () => {
|
|
|
148
148
|
});
|
|
149
149
|
expect(screen.queryAllByText('os=gnome')).toHaveLength(1);
|
|
150
150
|
});
|
|
151
|
+
|
|
152
|
+
it('input fill from url', async () => {
|
|
153
|
+
const inputText = 'test text';
|
|
154
|
+
routerSelectors.selectRouterLocation.mockImplementation(() => ({
|
|
155
|
+
search: `feature=test_feature&inputs[plain hidden]=${inputText}`,
|
|
156
|
+
}));
|
|
157
|
+
render(
|
|
158
|
+
<MockedProvider mocks={gqlMock} addTypename={false}>
|
|
159
|
+
<Provider store={store}>
|
|
160
|
+
<JobWizard />
|
|
161
|
+
</Provider>
|
|
162
|
+
</MockedProvider>
|
|
163
|
+
);
|
|
164
|
+
api.get.mock.calls.forEach(call => {
|
|
165
|
+
if (call[0].key === 'REX_FEATURE') {
|
|
166
|
+
expect(call[0].url).toEqual(
|
|
167
|
+
'/api/remote_execution_features/test_feature'
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
await act(async () => {
|
|
172
|
+
fireEvent.click(screen.getByText('Target hosts and inputs'));
|
|
173
|
+
});
|
|
174
|
+
const textField = screen.getByLabelText('plain hidden', {
|
|
175
|
+
selector: 'textarea',
|
|
176
|
+
});
|
|
177
|
+
expect(textField.value).toBe(inputText);
|
|
178
|
+
});
|
|
151
179
|
});
|
|
@@ -21,7 +21,7 @@ export const RepeatOn = ({
|
|
|
21
21
|
}) => {
|
|
22
22
|
const [repeatValidated, setRepeatValidated] = useState('default');
|
|
23
23
|
const handleRepeatInputChange = newValue => {
|
|
24
|
-
setRepeatValidated(newValue >= 1 ? 'default' : 'error');
|
|
24
|
+
setRepeatValidated(!newValue || newValue >= 1 ? 'default' : 'error');
|
|
25
25
|
setRepeatAmount(newValue);
|
|
26
26
|
};
|
|
27
27
|
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import React, { useEffect } from 'react';
|
|
2
|
-
import { useSelector } from 'react-redux';
|
|
2
|
+
import { useSelector, useDispatch } from 'react-redux';
|
|
3
3
|
import { FormGroup, TextInput, TextArea } from '@patternfly/react-core';
|
|
4
4
|
import PropTypes from 'prop-types';
|
|
5
5
|
import SearchBar from 'foremanReact/components/SearchBar';
|
|
6
6
|
import { getControllerSearchProps } from 'foremanReact/constants';
|
|
7
|
+
import { TRIGGERS } from 'foremanReact/components/AutoComplete/AutoCompleteConstants';
|
|
8
|
+
import { getResults } from 'foremanReact/components/AutoComplete/AutoCompleteActions';
|
|
7
9
|
import { helpLabel } from './FormHelpers';
|
|
8
10
|
import { SelectField } from './SelectField';
|
|
9
11
|
import { ResourceSelectAPI } from './ResourceSelect';
|
|
@@ -22,12 +24,25 @@ const TemplateSearchField = ({
|
|
|
22
24
|
const searchQuery = useSelector(
|
|
23
25
|
state => state.autocomplete?.[name]?.searchQuery
|
|
24
26
|
);
|
|
27
|
+
const dispatch = useDispatch();
|
|
25
28
|
useEffect(() => {
|
|
26
29
|
setValue({ ...values, [name]: searchQuery });
|
|
27
30
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
28
31
|
}, [searchQuery]);
|
|
29
32
|
const id = name.replace(/ /g, '-');
|
|
30
33
|
const props = getControllerSearchProps(controller.replace('/', '_'), name);
|
|
34
|
+
|
|
35
|
+
const setSearch = newSearchQuery => {
|
|
36
|
+
dispatch(
|
|
37
|
+
getResults({
|
|
38
|
+
url,
|
|
39
|
+
searchQuery: newSearchQuery,
|
|
40
|
+
controller,
|
|
41
|
+
trigger: TRIGGERS.INPUT_CHANGE,
|
|
42
|
+
id: name,
|
|
43
|
+
})
|
|
44
|
+
);
|
|
45
|
+
};
|
|
31
46
|
return (
|
|
32
47
|
<FormGroup
|
|
33
48
|
label={name}
|
|
@@ -45,9 +60,9 @@ const TemplateSearchField = ({
|
|
|
45
60
|
url,
|
|
46
61
|
useKeyShortcuts: true,
|
|
47
62
|
},
|
|
48
|
-
bookmarks: null,
|
|
49
63
|
}}
|
|
50
64
|
onSearch={noop}
|
|
65
|
+
onBookmarkClick={search => setSearch(search)}
|
|
51
66
|
/>
|
|
52
67
|
</FormGroup>
|
|
53
68
|
);
|
|
@@ -152,7 +167,7 @@ export const formatter = (input, values, setValue) => {
|
|
|
152
167
|
key={id}
|
|
153
168
|
name={name}
|
|
154
169
|
defaultValue={value}
|
|
155
|
-
controller={
|
|
170
|
+
controller={controller}
|
|
156
171
|
url={`/${controller}/auto_complete_search`}
|
|
157
172
|
labelText={labelText}
|
|
158
173
|
required={required}
|
|
@@ -69,6 +69,8 @@ export const GroupedSelectField = ({
|
|
|
69
69
|
className="without_select2"
|
|
70
70
|
onClear={onClear}
|
|
71
71
|
menuAppendTo={() => document.body}
|
|
72
|
+
aria-labelledby={fieldId}
|
|
73
|
+
toggleAriaLabel={`${label} toggle`}
|
|
72
74
|
{...props}
|
|
73
75
|
>
|
|
74
76
|
{options}
|
|
@@ -81,7 +83,11 @@ GroupedSelectField.propTypes = {
|
|
|
81
83
|
label: PropTypes.string.isRequired,
|
|
82
84
|
fieldId: PropTypes.string.isRequired,
|
|
83
85
|
groups: PropTypes.array,
|
|
84
|
-
selected: PropTypes.oneOfType([
|
|
86
|
+
selected: PropTypes.oneOfType([
|
|
87
|
+
PropTypes.string,
|
|
88
|
+
PropTypes.number,
|
|
89
|
+
PropTypes.array,
|
|
90
|
+
]),
|
|
85
91
|
setSelected: PropTypes.func.isRequired,
|
|
86
92
|
};
|
|
87
93
|
|
data/webpack/global_index.js
CHANGED
|
@@ -2,9 +2,13 @@ import { registerRoutes } from 'foremanReact/routes/RoutingService';
|
|
|
2
2
|
import routes from './Routes/routes';
|
|
3
3
|
import fillregistrationAdvanced from './react_app/extend/fillregistrationAdvanced';
|
|
4
4
|
import fillRecentJobsCard from './react_app/extend/fillRecentJobsCard';
|
|
5
|
+
import fillFeaturesDropdown from './react_app/extend/fillRexFeaturesDropdown';
|
|
6
|
+
import fillKebabItems from './react_app/extend/fillKebabItems';
|
|
5
7
|
import registerReducers from './react_app/extend/reducers';
|
|
6
8
|
|
|
7
9
|
registerReducers();
|
|
8
10
|
registerRoutes('foreman_remote_execution', routes);
|
|
11
|
+
fillFeaturesDropdown();
|
|
9
12
|
fillRecentJobsCard();
|
|
10
13
|
fillregistrationAdvanced();
|
|
14
|
+
fillKebabItems();
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { foremanUrl } from 'foremanReact/common/helpers';
|
|
2
|
+
import { sprintf, translate as __ } from 'foremanReact/common/I18n';
|
|
3
|
+
import { post } from 'foremanReact/redux/API';
|
|
4
|
+
|
|
5
|
+
export const runFeature = (hostId, feature, label) => dispatch => {
|
|
6
|
+
const url = foremanUrl(
|
|
7
|
+
`/job_invocations?feature=${feature}&host_ids%5B%5D=${hostId}`
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
const successToast = () => sprintf(__('%s job has been invoked'), label);
|
|
11
|
+
const errorToast = ({ message }) => message;
|
|
12
|
+
dispatch(post({ key: feature.toUpperCase(), url, successToast, errorToast }));
|
|
13
|
+
};
|