foreman_remote_execution 4.8.0 → 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/app/controllers/api/v2/job_invocations_controller.rb +9 -0
- 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/lib/actions/remote_execution/run_host_job.rb +4 -0
- data/app/models/concerns/foreman_remote_execution/host_extensions.rb +8 -0
- data/app/models/job_invocation_composer.rb +1 -1
- data/app/models/targeting.rb +2 -2
- data/app/views/job_invocations/refresh.js.erb +1 -0
- data/config/routes.rb +1 -0
- data/db/migrate/20210816100932_rex_setting_category_to_dsl.rb +5 -0
- data/lib/foreman_remote_execution/engine.rb +110 -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 +10 -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/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 +1 -1
- data/test/unit/job_template_effective_user_test.rb +0 -4
- data/test/unit/remote_execution_provider_test.rb +0 -4
- data/test/unit/targeting_test.rb +68 -1
- data/webpack/JobWizard/JobWizard.js +94 -13
- data/webpack/JobWizard/JobWizard.scss +59 -35
- data/webpack/JobWizard/JobWizardConstants.js +28 -1
- 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/StartEndDates.js +59 -19
- data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +258 -11
- data/webpack/JobWizard/steps/Schedule/__tests__/StartEndDates.test.js +11 -2
- data/webpack/JobWizard/steps/Schedule/index.js +97 -21
- data/webpack/JobWizard/steps/form/DateTimePicker.js +41 -8
- 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/RecentJobsCard.js +1 -1
- metadata +38 -6
- data/app/models/setting/remote_execution.rb +0 -94
- 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
|
@@ -4,10 +4,6 @@ class JobTemplateEffectiveUserTest < ActiveSupport::TestCase
|
|
|
4
4
|
let(:job_template) { FactoryBot.build(:job_template, :job_category => '') }
|
|
5
5
|
let(:effective_user) { job_template.effective_user }
|
|
6
6
|
|
|
7
|
-
before do
|
|
8
|
-
Setting::RemoteExecution.load_defaults
|
|
9
|
-
end
|
|
10
|
-
|
|
11
7
|
describe 'by default' do
|
|
12
8
|
it 'is overridable' do
|
|
13
9
|
assert effective_user.overridable?
|
|
@@ -66,10 +66,6 @@ class RemoteExecutionProviderTest < ActiveSupport::TestCase
|
|
|
66
66
|
before { User.current = FactoryBot.build(:user, :admin) }
|
|
67
67
|
after { User.current = nil }
|
|
68
68
|
|
|
69
|
-
before do
|
|
70
|
-
Setting::RemoteExecution.load_defaults
|
|
71
|
-
end
|
|
72
|
-
|
|
73
69
|
let(:job_invocation) { FactoryBot.create(:job_invocation, :with_template) }
|
|
74
70
|
let(:template_invocation) { job_invocation.pattern_template_invocations.first }
|
|
75
71
|
let(:host) { FactoryBot.create(:host) }
|
data/test/unit/targeting_test.rb
CHANGED
|
@@ -77,24 +77,73 @@ class TargetingTest < ActiveSupport::TestCase
|
|
|
77
77
|
it { _(targeting.reload.hosts).must_be_empty }
|
|
78
78
|
end
|
|
79
79
|
|
|
80
|
+
describe '#resolve_hosts!' do
|
|
81
|
+
let(:second_host) { FactoryBot.create(:host) }
|
|
82
|
+
let(:infra_host) { FactoryBot.create(:host, :with_infrastructure_facet) }
|
|
83
|
+
let(:targeting) { FactoryBot.build(:targeting) }
|
|
84
|
+
|
|
85
|
+
before do
|
|
86
|
+
host
|
|
87
|
+
second_host
|
|
88
|
+
infra_host
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
context 'with infrastructure host permission' do
|
|
92
|
+
before do
|
|
93
|
+
setup_user('view', 'hosts')
|
|
94
|
+
setup_user('execute_jobs_on', 'infrastructure_hosts')
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it 'resolves all hosts' do
|
|
98
|
+
hosts = [host, second_host, infra_host]
|
|
99
|
+
targeting.search_query = "name ^ (#{hosts.map(&:name).join(',')})"
|
|
100
|
+
targeting.user = User.current
|
|
101
|
+
targeting.resolve_hosts!
|
|
102
|
+
|
|
103
|
+
targeting.hosts.must_include host
|
|
104
|
+
targeting.hosts.must_include second_host
|
|
105
|
+
targeting.hosts.must_include infra_host
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
context 'without infrastructure host permission' do
|
|
110
|
+
before { setup_user('view', 'hosts') }
|
|
111
|
+
|
|
112
|
+
it 'ignores infrastructure hosts' do
|
|
113
|
+
hosts = [host, second_host, infra_host]
|
|
114
|
+
targeting.search_query = "name ^ (#{hosts.map(&:name).join(',')})"
|
|
115
|
+
targeting.user = User.current
|
|
116
|
+
targeting.resolve_hosts!
|
|
117
|
+
|
|
118
|
+
targeting.hosts.must_include host
|
|
119
|
+
targeting.hosts.must_include second_host
|
|
120
|
+
targeting.hosts.wont_include infra_host
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
80
125
|
describe '#build_query_from_hosts(ids)' do
|
|
81
126
|
let(:second_host) { FactoryBot.create(:host) }
|
|
127
|
+
let(:infra_host) { FactoryBot.create(:host, :with_infrastructure_facet) }
|
|
82
128
|
|
|
83
129
|
before do
|
|
84
130
|
host
|
|
85
131
|
second_host
|
|
132
|
+
infra_host
|
|
86
133
|
end
|
|
87
134
|
|
|
88
135
|
context 'for two hosts' do
|
|
89
|
-
let(:query) { Targeting.build_query_from_hosts([ host.id, second_host.id ]) }
|
|
136
|
+
let(:query) { Targeting.build_query_from_hosts([ host.id, second_host.id, infra_host.id ]) }
|
|
90
137
|
|
|
91
138
|
it 'builds query using host names joining inside ^' do
|
|
92
139
|
_(query).must_include host.name
|
|
93
140
|
_(query).must_include second_host.name
|
|
141
|
+
_(query).must_include infra_host.name
|
|
94
142
|
_(query).must_include 'name ^'
|
|
95
143
|
|
|
96
144
|
Host.search_for(query).must_include host
|
|
97
145
|
Host.search_for(query).must_include second_host
|
|
146
|
+
Host.search_for(query).must_include infra_host
|
|
98
147
|
end
|
|
99
148
|
end
|
|
100
149
|
|
|
@@ -105,6 +154,7 @@ class TargetingTest < ActiveSupport::TestCase
|
|
|
105
154
|
_(query).must_equal "name ^ (#{host.name})"
|
|
106
155
|
Host.search_for(query).must_include host
|
|
107
156
|
Host.search_for(query).wont_include second_host
|
|
157
|
+
Host.search_for(query).wont_include infra_host
|
|
108
158
|
end
|
|
109
159
|
end
|
|
110
160
|
|
|
@@ -114,6 +164,23 @@ class TargetingTest < ActiveSupport::TestCase
|
|
|
114
164
|
it 'builds query to find all hosts' do
|
|
115
165
|
Host.search_for(query).must_include host
|
|
116
166
|
Host.search_for(query).must_include second_host
|
|
167
|
+
Host.search_for(query).must_include infra_host
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
context 'without infrastructure host permission' do
|
|
172
|
+
before { User.current = nil }
|
|
173
|
+
|
|
174
|
+
it 'ignores the infrastructure host' do
|
|
175
|
+
query = Targeting.build_query_from_hosts([host.id, second_host.id, infra_host.id])
|
|
176
|
+
_(query).must_include host.name
|
|
177
|
+
_(query).must_include second_host.name
|
|
178
|
+
_(query).wont_include infra_host.name
|
|
179
|
+
_(query).must_include 'name ^'
|
|
180
|
+
|
|
181
|
+
Host.search_for(query).must_include host
|
|
182
|
+
Host.search_for(query).must_include second_host
|
|
183
|
+
Host.search_for(query).wont_include infra_host
|
|
117
184
|
end
|
|
118
185
|
end
|
|
119
186
|
end
|
|
@@ -4,6 +4,11 @@ import { useDispatch, useSelector } from 'react-redux';
|
|
|
4
4
|
import { Wizard } from '@patternfly/react-core';
|
|
5
5
|
import { get } from 'foremanReact/redux/API';
|
|
6
6
|
import history from 'foremanReact/history';
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
useForemanOrganization,
|
|
10
|
+
useForemanLocation,
|
|
11
|
+
} from 'foremanReact/Root/Context/ForemanContext';
|
|
7
12
|
import CategoryAndTemplate from './steps/CategoryAndTemplate/';
|
|
8
13
|
import { AdvancedFields } from './steps/AdvancedFields/AdvancedFields';
|
|
9
14
|
import {
|
|
@@ -11,18 +16,31 @@ import {
|
|
|
11
16
|
WIZARD_TITLES,
|
|
12
17
|
initialScheduleState,
|
|
13
18
|
} from './JobWizardConstants';
|
|
14
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
selectTemplateError,
|
|
21
|
+
selectJobTemplate,
|
|
22
|
+
selectIsSubmitting,
|
|
23
|
+
} from './JobWizardSelectors';
|
|
15
24
|
import Schedule from './steps/Schedule/';
|
|
16
25
|
import HostsAndInputs from './steps/HostsAndInputs/';
|
|
26
|
+
import ReviewDetails from './steps/ReviewDetails/';
|
|
27
|
+
import { useValidation } from './validation';
|
|
28
|
+
import { useAutoFill } from './autofill';
|
|
29
|
+
import { submit } from './submit';
|
|
17
30
|
import './JobWizard.scss';
|
|
18
31
|
|
|
19
32
|
export const JobWizard = () => {
|
|
20
33
|
const [jobTemplateID, setJobTemplateID] = useState(null);
|
|
21
34
|
const [category, setCategory] = useState('');
|
|
22
|
-
const [advancedValues, setAdvancedValues] = useState({});
|
|
35
|
+
const [advancedValues, setAdvancedValues] = useState({ templateValues: {} });
|
|
23
36
|
const [templateValues, setTemplateValues] = useState({}); // TODO use templateValues in advanced fields - description https://github.com/theforeman/foreman_remote_execution/pull/605
|
|
24
|
-
const [selectedHosts, setSelectedHosts] = useState(['host1', 'host2']);
|
|
25
37
|
const [scheduleValue, setScheduleValue] = useState(initialScheduleState);
|
|
38
|
+
const [selectedTargets, setSelectedTargets] = useState({
|
|
39
|
+
hosts: [],
|
|
40
|
+
hostCollections: [],
|
|
41
|
+
hostGroups: [],
|
|
42
|
+
});
|
|
43
|
+
const [hostsSearchQuery, setHostsSearchQuery] = useState('');
|
|
26
44
|
const dispatch = useDispatch();
|
|
27
45
|
|
|
28
46
|
const setDefaults = useCallback(
|
|
@@ -31,7 +49,7 @@ export const JobWizard = () => {
|
|
|
31
49
|
template_inputs,
|
|
32
50
|
advanced_template_inputs,
|
|
33
51
|
effective_user,
|
|
34
|
-
job_template: { execution_timeout_interval, description_format },
|
|
52
|
+
job_template: { name, execution_timeout_interval, description_format },
|
|
35
53
|
},
|
|
36
54
|
}) => {
|
|
37
55
|
const advancedTemplateValues = {};
|
|
@@ -52,12 +70,21 @@ export const JobWizard = () => {
|
|
|
52
70
|
advancedTemplateValues[input.name] = input?.default || '';
|
|
53
71
|
});
|
|
54
72
|
}
|
|
73
|
+
const generateDefaultDescription = () => {
|
|
74
|
+
if (description_format) return description_format;
|
|
75
|
+
const allInputs = [...advancedInputs, ...inputs];
|
|
76
|
+
if (!allInputs.length) return name;
|
|
77
|
+
const inputsString = allInputs
|
|
78
|
+
.map(({ name: inputname }) => `${inputname}="%{${inputname}}"`)
|
|
79
|
+
.join(' ');
|
|
80
|
+
return `${name} with inputs ${inputsString}`;
|
|
81
|
+
};
|
|
55
82
|
return {
|
|
56
83
|
...currentAdvancedValues,
|
|
57
84
|
effectiveUserValue: effective_user?.value || '',
|
|
58
85
|
timeoutToKill: execution_timeout_interval || '',
|
|
59
86
|
templateValues: advancedTemplateValues,
|
|
60
|
-
description:
|
|
87
|
+
description: generateDefaultDescription() || '',
|
|
61
88
|
isRandomizedOrdering: false,
|
|
62
89
|
};
|
|
63
90
|
});
|
|
@@ -76,8 +103,20 @@ export const JobWizard = () => {
|
|
|
76
103
|
}
|
|
77
104
|
}, [jobTemplateID, setDefaults, dispatch]);
|
|
78
105
|
|
|
106
|
+
const [valid, setValid] = useValidation({
|
|
107
|
+
advancedValues,
|
|
108
|
+
templateValues,
|
|
109
|
+
});
|
|
110
|
+
useAutoFill({
|
|
111
|
+
setSelectedTargets,
|
|
112
|
+
setHostsSearchQuery,
|
|
113
|
+
});
|
|
79
114
|
const templateError = !!useSelector(selectTemplateError);
|
|
80
|
-
const
|
|
115
|
+
const templateResponse = useSelector(selectJobTemplate);
|
|
116
|
+
const isSubmitting = useSelector(selectIsSubmitting);
|
|
117
|
+
const isTemplate =
|
|
118
|
+
!templateError && !!jobTemplateID && templateResponse.job_template;
|
|
119
|
+
|
|
81
120
|
const steps = [
|
|
82
121
|
{
|
|
83
122
|
name: WIZARD_TITLES.categoryAndTemplate,
|
|
@@ -89,6 +128,7 @@ export const JobWizard = () => {
|
|
|
89
128
|
setCategory={setCategory}
|
|
90
129
|
/>
|
|
91
130
|
),
|
|
131
|
+
enableNext: isTemplate,
|
|
92
132
|
},
|
|
93
133
|
{
|
|
94
134
|
name: WIZARD_TITLES.hostsAndInputs,
|
|
@@ -96,11 +136,14 @@ export const JobWizard = () => {
|
|
|
96
136
|
<HostsAndInputs
|
|
97
137
|
templateValues={templateValues}
|
|
98
138
|
setTemplateValues={setTemplateValues}
|
|
99
|
-
|
|
100
|
-
|
|
139
|
+
selected={selectedTargets}
|
|
140
|
+
setSelected={setSelectedTargets}
|
|
141
|
+
hostsSearchQuery={hostsSearchQuery}
|
|
142
|
+
setHostsSearchQuery={setHostsSearchQuery}
|
|
101
143
|
/>
|
|
102
144
|
),
|
|
103
145
|
canJumpTo: isTemplate,
|
|
146
|
+
enableNext: isTemplate && valid.hostsAndInputs,
|
|
104
147
|
},
|
|
105
148
|
{
|
|
106
149
|
name: WIZARD_TITLES.advanced,
|
|
@@ -113,10 +156,11 @@ export const JobWizard = () => {
|
|
|
113
156
|
...newValues,
|
|
114
157
|
}));
|
|
115
158
|
}}
|
|
116
|
-
|
|
159
|
+
templateValues={templateValues}
|
|
117
160
|
/>
|
|
118
161
|
),
|
|
119
|
-
canJumpTo: isTemplate,
|
|
162
|
+
canJumpTo: isTemplate && valid.hostsAndInputs,
|
|
163
|
+
enableNext: isTemplate && valid.hostsAndInputs && valid.advanced,
|
|
120
164
|
},
|
|
121
165
|
{
|
|
122
166
|
name: WIZARD_TITLES.schedule,
|
|
@@ -124,17 +168,41 @@ export const JobWizard = () => {
|
|
|
124
168
|
<Schedule
|
|
125
169
|
scheduleValue={scheduleValue}
|
|
126
170
|
setScheduleValue={setScheduleValue}
|
|
171
|
+
setValid={newValue => {
|
|
172
|
+
setValid(currentValid => ({ ...currentValid, schedule: newValue }));
|
|
173
|
+
}}
|
|
127
174
|
/>
|
|
128
175
|
),
|
|
129
|
-
canJumpTo: isTemplate,
|
|
176
|
+
canJumpTo: isTemplate && valid.hostsAndInputs && valid.advanced,
|
|
177
|
+
enableNext:
|
|
178
|
+
isTemplate && valid.hostsAndInputs && valid.advanced && valid.schedule,
|
|
130
179
|
},
|
|
131
180
|
{
|
|
132
181
|
name: WIZARD_TITLES.review,
|
|
133
|
-
component:
|
|
182
|
+
component: (
|
|
183
|
+
<ReviewDetails
|
|
184
|
+
jobCategory={category}
|
|
185
|
+
jobTemplateID={jobTemplateID}
|
|
186
|
+
advancedValues={advancedValues}
|
|
187
|
+
scheduleValue={scheduleValue}
|
|
188
|
+
templateValues={templateValues}
|
|
189
|
+
selectedTargets={selectedTargets}
|
|
190
|
+
hostsSearchQuery={hostsSearchQuery}
|
|
191
|
+
/>
|
|
192
|
+
),
|
|
134
193
|
nextButtonText: 'Run',
|
|
135
|
-
canJumpTo:
|
|
194
|
+
canJumpTo:
|
|
195
|
+
isTemplate && valid.hostsAndInputs && valid.advanced && valid.schedule,
|
|
196
|
+
enableNext:
|
|
197
|
+
isTemplate &&
|
|
198
|
+
valid.hostsAndInputs &&
|
|
199
|
+
valid.advanced &&
|
|
200
|
+
valid.schedule &&
|
|
201
|
+
!isSubmitting,
|
|
136
202
|
},
|
|
137
203
|
];
|
|
204
|
+
const location = useForemanLocation();
|
|
205
|
+
const organization = useForemanOrganization();
|
|
138
206
|
return (
|
|
139
207
|
<Wizard
|
|
140
208
|
onClose={() => history.goBack()}
|
|
@@ -142,6 +210,19 @@ export const JobWizard = () => {
|
|
|
142
210
|
steps={steps}
|
|
143
211
|
height="100%"
|
|
144
212
|
className="job-wizard"
|
|
213
|
+
onSave={() => {
|
|
214
|
+
submit({
|
|
215
|
+
jobTemplateID,
|
|
216
|
+
templateValues,
|
|
217
|
+
advancedValues,
|
|
218
|
+
scheduleValue,
|
|
219
|
+
dispatch,
|
|
220
|
+
selectedTargets,
|
|
221
|
+
hostsSearchQuery,
|
|
222
|
+
location,
|
|
223
|
+
organization,
|
|
224
|
+
});
|
|
225
|
+
}}
|
|
145
226
|
/>
|
|
146
227
|
);
|
|
147
228
|
};
|
|
@@ -3,54 +3,54 @@
|
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
+
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.gray-text {
|
|
32
|
+
color: var(--pf-global--Color--dark-200);
|
|
50
33
|
}
|
|
51
34
|
|
|
52
|
-
.hosts-
|
|
53
|
-
|
|
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'] {
|
|
@@ -60,6 +60,17 @@
|
|
|
60
60
|
.advanced-scheduling-button {
|
|
61
61
|
text-align: start;
|
|
62
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
|
+
}
|
|
63
74
|
}
|
|
64
75
|
|
|
65
76
|
.pf-c-date-picker {
|
|
@@ -79,4 +90,17 @@
|
|
|
79
90
|
min-height: 40px;
|
|
80
91
|
min-width: 100px;
|
|
81
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
|
+
}
|
|
82
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 = {
|
|
@@ -26,8 +27,34 @@ export const WIZARD_TITLES = {
|
|
|
26
27
|
export const initialScheduleState = {
|
|
27
28
|
repeatType: repeatTypes.noRepeat,
|
|
28
29
|
repeatAmount: '',
|
|
29
|
-
|
|
30
|
+
repeatData: {},
|
|
31
|
+
startsAt: '',
|
|
32
|
+
startsBefore: '',
|
|
30
33
|
ends: '',
|
|
31
34
|
isFuture: false,
|
|
32
35
|
isNeverEnds: false,
|
|
36
|
+
isTypeStatic: true,
|
|
37
|
+
purpose: '',
|
|
33
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
|
+
];
|