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
|
@@ -5,7 +5,6 @@ import { Form } from '@patternfly/react-core';
|
|
|
5
5
|
import {
|
|
6
6
|
selectEffectiveUser,
|
|
7
7
|
selectAdvancedTemplateInputs,
|
|
8
|
-
selectTemplateInputs,
|
|
9
8
|
} from '../../JobWizardSelectors';
|
|
10
9
|
import {
|
|
11
10
|
EffectiveUserField,
|
|
@@ -22,10 +21,13 @@ import { DescriptionField } from './DescriptionField';
|
|
|
22
21
|
import { WIZARD_TITLES } from '../../JobWizardConstants';
|
|
23
22
|
import { WizardTitle } from '../form/WizardTitle';
|
|
24
23
|
|
|
25
|
-
export const AdvancedFields = ({
|
|
24
|
+
export const AdvancedFields = ({
|
|
25
|
+
templateValues,
|
|
26
|
+
advancedValues,
|
|
27
|
+
setAdvancedValues,
|
|
28
|
+
}) => {
|
|
26
29
|
const effectiveUser = useSelector(selectEffectiveUser);
|
|
27
30
|
const advancedTemplateInputs = useSelector(selectAdvancedTemplateInputs);
|
|
28
|
-
const templateInputs = useSelector(selectTemplateInputs);
|
|
29
31
|
return (
|
|
30
32
|
<>
|
|
31
33
|
<WizardTitle
|
|
@@ -49,7 +51,7 @@ export const AdvancedFields = ({ advancedValues, setAdvancedValues }) => {
|
|
|
49
51
|
/>
|
|
50
52
|
)}
|
|
51
53
|
<DescriptionField
|
|
52
|
-
|
|
54
|
+
inputValues={{ ...templateValues, ...advancedValues.templateValues }}
|
|
53
55
|
value={advancedValues.description}
|
|
54
56
|
setValue={newValue => setAdvancedValues({ description: newValue })}
|
|
55
57
|
/>
|
|
@@ -117,5 +119,6 @@ export const AdvancedFields = ({ advancedValues, setAdvancedValues }) => {
|
|
|
117
119
|
AdvancedFields.propTypes = {
|
|
118
120
|
advancedValues: PropTypes.object.isRequired,
|
|
119
121
|
setAdvancedValues: PropTypes.func.isRequired,
|
|
122
|
+
templateValues: PropTypes.object.isRequired,
|
|
120
123
|
};
|
|
121
124
|
export default AdvancedFields;
|
|
@@ -1,10 +1,19 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
1
|
+
import React, { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import { useSelector } from 'react-redux';
|
|
2
3
|
import PropTypes from 'prop-types';
|
|
3
|
-
import { FormGroup, TextInput, Button } from '@patternfly/react-core';
|
|
4
|
+
import { FormGroup, TextInput, Tooltip, Button } from '@patternfly/react-core';
|
|
4
5
|
import { translate as __ } from 'foremanReact/common/I18n';
|
|
6
|
+
import {
|
|
7
|
+
selectTemplateInputs,
|
|
8
|
+
selectAdvancedTemplateInputs,
|
|
9
|
+
} from '../../JobWizardSelectors';
|
|
5
10
|
|
|
6
|
-
export const DescriptionField = ({
|
|
7
|
-
const
|
|
11
|
+
export const DescriptionField = ({ inputValues, value, setValue }) => {
|
|
12
|
+
const inputs = [
|
|
13
|
+
...useSelector(selectTemplateInputs),
|
|
14
|
+
...useSelector(selectAdvancedTemplateInputs),
|
|
15
|
+
].map(input => input.name);
|
|
16
|
+
const generateDesc = useCallback(() => {
|
|
8
17
|
let newDesc = value;
|
|
9
18
|
if (value) {
|
|
10
19
|
const re = new RegExp('%\\{([^\\}]+)\\}', 'gm');
|
|
@@ -15,16 +24,19 @@ export const DescriptionField = ({ inputs, value, setValue }) => {
|
|
|
15
24
|
results.forEach(result => {
|
|
16
25
|
newDesc = newDesc.replace(
|
|
17
26
|
result.text,
|
|
18
|
-
|
|
19
|
-
|
|
27
|
+
inputValues[result.name] ||
|
|
28
|
+
(inputs.includes(result.name) ? '' : result.text)
|
|
20
29
|
);
|
|
21
30
|
});
|
|
22
31
|
}
|
|
23
32
|
return newDesc;
|
|
24
|
-
};
|
|
33
|
+
}, [inputs, value, inputValues]);
|
|
25
34
|
const [generatedDesc, setGeneratedDesc] = useState(generateDesc());
|
|
26
35
|
const [isPreview, setIsPreview] = useState(true);
|
|
27
36
|
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
setGeneratedDesc(generateDesc());
|
|
39
|
+
}, [generateDesc]);
|
|
28
40
|
const togglePreview = () => {
|
|
29
41
|
setGeneratedDesc(generateDesc());
|
|
30
42
|
setIsPreview(v => !v);
|
|
@@ -43,9 +55,20 @@ export const DescriptionField = ({ inputs, value, setValue }) => {
|
|
|
43
55
|
}
|
|
44
56
|
>
|
|
45
57
|
{isPreview ? (
|
|
46
|
-
<
|
|
58
|
+
<Tooltip content={generatedDesc}>
|
|
59
|
+
<div>
|
|
60
|
+
{/* div wrapper so the tooltip will be shown in chrome */}
|
|
61
|
+
<TextInput
|
|
62
|
+
aria-label="description preview"
|
|
63
|
+
id="description-preview"
|
|
64
|
+
value={generatedDesc}
|
|
65
|
+
isDisabled
|
|
66
|
+
/>
|
|
67
|
+
</div>
|
|
68
|
+
</Tooltip>
|
|
47
69
|
) : (
|
|
48
70
|
<TextInput
|
|
71
|
+
aria-label="description edit"
|
|
49
72
|
type="text"
|
|
50
73
|
autoComplete="description"
|
|
51
74
|
id="description"
|
|
@@ -58,7 +81,7 @@ export const DescriptionField = ({ inputs, value, setValue }) => {
|
|
|
58
81
|
};
|
|
59
82
|
|
|
60
83
|
DescriptionField.propTypes = {
|
|
61
|
-
|
|
84
|
+
inputValues: PropTypes.object.isRequired,
|
|
62
85
|
value: PropTypes.string,
|
|
63
86
|
setValue: PropTypes.func.isRequired,
|
|
64
87
|
};
|
|
@@ -1,14 +1,19 @@
|
|
|
1
|
+
/* eslint-disable max-lines */
|
|
1
2
|
import React from 'react';
|
|
2
3
|
import { Provider } from 'react-redux';
|
|
3
4
|
import { mount } from '@theforeman/test';
|
|
4
5
|
import { fireEvent, screen, render, act } from '@testing-library/react';
|
|
6
|
+
import { MockedProvider } from '@apollo/client/testing';
|
|
5
7
|
import * as api from 'foremanReact/redux/API';
|
|
6
8
|
import { JobWizard } from '../../../JobWizard';
|
|
7
9
|
import * as selectors from '../../../JobWizardSelectors';
|
|
8
10
|
import {
|
|
9
|
-
jobTemplateResponse
|
|
11
|
+
jobTemplateResponse,
|
|
12
|
+
jobTemplate,
|
|
10
13
|
testSetup,
|
|
11
14
|
mockApi,
|
|
15
|
+
jobCategories,
|
|
16
|
+
gqlMock,
|
|
12
17
|
} from '../../../__tests__/fixtures';
|
|
13
18
|
import { WIZARD_TITLES } from '../../../JobWizardConstants';
|
|
14
19
|
|
|
@@ -18,14 +23,16 @@ mockApi(api);
|
|
|
18
23
|
jest.spyOn(selectors, 'selectEffectiveUser');
|
|
19
24
|
|
|
20
25
|
selectors.selectEffectiveUser.mockImplementation(
|
|
21
|
-
() =>
|
|
26
|
+
() => jobTemplateResponse.effective_user
|
|
22
27
|
);
|
|
23
28
|
describe('AdvancedFields', () => {
|
|
24
29
|
it('should save data between steps for advanced fields', async () => {
|
|
25
30
|
const wrapper = mount(
|
|
26
|
-
<
|
|
27
|
-
<
|
|
28
|
-
|
|
31
|
+
<MockedProvider mocks={gqlMock} addTypename={false}>
|
|
32
|
+
<Provider store={store}>
|
|
33
|
+
<JobWizard />
|
|
34
|
+
</Provider>
|
|
35
|
+
</MockedProvider>
|
|
29
36
|
);
|
|
30
37
|
// setup
|
|
31
38
|
wrapper.find('.pf-c-button.pf-c-select__toggle-button').simulate('click');
|
|
@@ -78,9 +85,11 @@ describe('AdvancedFields', () => {
|
|
|
78
85
|
});
|
|
79
86
|
it('fill template fields', async () => {
|
|
80
87
|
render(
|
|
81
|
-
<
|
|
82
|
-
<
|
|
83
|
-
|
|
88
|
+
<MockedProvider mocks={gqlMock} addTypename={false}>
|
|
89
|
+
<Provider store={store}>
|
|
90
|
+
<JobWizard />
|
|
91
|
+
</Provider>
|
|
92
|
+
</MockedProvider>
|
|
84
93
|
);
|
|
85
94
|
await act(async () => {
|
|
86
95
|
fireEvent.click(screen.getByText(WIZARD_TITLES.advanced));
|
|
@@ -91,7 +100,10 @@ describe('AdvancedFields', () => {
|
|
|
91
100
|
const textField = screen.getByLabelText('adv plain hidden', {
|
|
92
101
|
selector: 'textarea',
|
|
93
102
|
});
|
|
94
|
-
const selectField = screen.
|
|
103
|
+
const selectField = screen.getByLabelText('adv plain select toggle');
|
|
104
|
+
const resourceSelectField = screen.getByLabelText(
|
|
105
|
+
'adv resource select toggle'
|
|
106
|
+
);
|
|
95
107
|
const searchField = screen.getByPlaceholderText('Filter...');
|
|
96
108
|
const dateField = screen.getByLabelText('adv date', {
|
|
97
109
|
selector: 'input',
|
|
@@ -101,6 +113,10 @@ describe('AdvancedFields', () => {
|
|
|
101
113
|
await act(async () => {
|
|
102
114
|
await fireEvent.click(screen.getByText('option 2'));
|
|
103
115
|
fireEvent.click(screen.getAllByText(WIZARD_TITLES.advanced)[0]); // to remove focus
|
|
116
|
+
|
|
117
|
+
fireEvent.click(resourceSelectField);
|
|
118
|
+
await fireEvent.click(screen.getByText('resource2'));
|
|
119
|
+
|
|
104
120
|
await fireEvent.change(textField, {
|
|
105
121
|
target: { value: textValue },
|
|
106
122
|
});
|
|
@@ -134,12 +150,16 @@ describe('AdvancedFields', () => {
|
|
|
134
150
|
expect(dateField.value).toBe(dateValue);
|
|
135
151
|
expect(screen.queryAllByText('option 1')).toHaveLength(0);
|
|
136
152
|
expect(screen.queryAllByText('option 2')).toHaveLength(1);
|
|
153
|
+
expect(screen.queryAllByDisplayValue('resource1')).toHaveLength(0);
|
|
154
|
+
expect(screen.queryAllByDisplayValue('resource2')).toHaveLength(1);
|
|
137
155
|
});
|
|
138
156
|
it('fill defaults into fields', async () => {
|
|
139
157
|
render(
|
|
140
|
-
<
|
|
141
|
-
<
|
|
142
|
-
|
|
158
|
+
<MockedProvider mocks={gqlMock} addTypename={false}>
|
|
159
|
+
<Provider store={store}>
|
|
160
|
+
<JobWizard />
|
|
161
|
+
</Provider>
|
|
162
|
+
</MockedProvider>
|
|
143
163
|
);
|
|
144
164
|
await act(async () => {
|
|
145
165
|
fireEvent.click(screen.getByText('Advanced Fields'));
|
|
@@ -155,5 +175,189 @@ describe('AdvancedFields', () => {
|
|
|
155
175
|
selector: 'input',
|
|
156
176
|
}).value
|
|
157
177
|
).toBe('2');
|
|
178
|
+
|
|
179
|
+
expect(
|
|
180
|
+
screen.getByLabelText('description preview', {
|
|
181
|
+
selector: 'input',
|
|
182
|
+
}).value
|
|
183
|
+
).toBe(
|
|
184
|
+
'template1 with inputs adv plain hidden="Default val" adv plain select="" adv resource select="" adv search="" adv date="" plain hidden="Default val"'
|
|
185
|
+
);
|
|
186
|
+
});
|
|
187
|
+
it('DescriptionField', async () => {
|
|
188
|
+
render(
|
|
189
|
+
<Provider store={store}>
|
|
190
|
+
<JobWizard />
|
|
191
|
+
</Provider>
|
|
192
|
+
);
|
|
193
|
+
await act(async () => {
|
|
194
|
+
fireEvent.click(screen.getByText(WIZARD_TITLES.advanced));
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const textField = screen.getByLabelText('adv plain hidden', {
|
|
198
|
+
selector: 'textarea',
|
|
199
|
+
});
|
|
200
|
+
await act(async () => {
|
|
201
|
+
await fireEvent.change(textField, {
|
|
202
|
+
target: { value: 'test command' },
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
const descriptionValue = 'Run %{adv plain hidden} %{wrong command name}';
|
|
206
|
+
|
|
207
|
+
await act(async () => {
|
|
208
|
+
fireEvent.click(screen.getByText('Edit job description template'));
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const editText = screen.getByLabelText('description edit', {
|
|
212
|
+
selector: 'input',
|
|
213
|
+
});
|
|
214
|
+
await fireEvent.change(editText, {
|
|
215
|
+
target: { value: descriptionValue },
|
|
216
|
+
});
|
|
217
|
+
await act(async () => {
|
|
218
|
+
fireEvent.click(screen.getByText('Preview job description'));
|
|
219
|
+
});
|
|
220
|
+
expect(
|
|
221
|
+
screen.getByLabelText('description preview', {
|
|
222
|
+
selector: 'input',
|
|
223
|
+
}).value
|
|
224
|
+
).toBe('Run test command %{wrong command name}');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('DescriptionField with no inputs', async () => {
|
|
228
|
+
jest.spyOn(api, 'get');
|
|
229
|
+
|
|
230
|
+
jest.spyOn(selectors, 'selectTemplateInputs');
|
|
231
|
+
jest.spyOn(selectors, 'selectAdvancedTemplateInputs');
|
|
232
|
+
selectors.selectTemplateInputs.mockImplementation(() => []);
|
|
233
|
+
selectors.selectAdvancedTemplateInputs.mockImplementation(() => []);
|
|
234
|
+
api.get.mockImplementation(({ handleSuccess, ...action }) => {
|
|
235
|
+
if (action.key === 'JOB_CATEGORIES') {
|
|
236
|
+
handleSuccess &&
|
|
237
|
+
handleSuccess({ data: { job_categories: jobCategories } });
|
|
238
|
+
} else if (action.key === 'JOB_TEMPLATE') {
|
|
239
|
+
handleSuccess &&
|
|
240
|
+
handleSuccess({
|
|
241
|
+
data: {
|
|
242
|
+
...jobTemplateResponse,
|
|
243
|
+
advanced_template_inputs: [],
|
|
244
|
+
template_inputs: [],
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
} else if (action.key === 'JOB_TEMPLATES') {
|
|
248
|
+
handleSuccess &&
|
|
249
|
+
handleSuccess({
|
|
250
|
+
data: { results: [jobTemplate] },
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
return { type: 'get', ...action };
|
|
254
|
+
});
|
|
255
|
+
render(
|
|
256
|
+
<Provider store={store}>
|
|
257
|
+
<JobWizard />
|
|
258
|
+
</Provider>
|
|
259
|
+
);
|
|
260
|
+
await act(async () => {
|
|
261
|
+
fireEvent.click(screen.getByText(WIZARD_TITLES.advanced));
|
|
262
|
+
});
|
|
263
|
+
expect(
|
|
264
|
+
screen.getByLabelText('description preview', {
|
|
265
|
+
selector: 'input',
|
|
266
|
+
}).value
|
|
267
|
+
).toBe('template1');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('DescriptionField with description_format', async () => {
|
|
271
|
+
jest.spyOn(api, 'get');
|
|
272
|
+
jest.spyOn(selectors, 'selectTemplateInputs');
|
|
273
|
+
selectors.selectTemplateInputs.mockImplementation(() => [
|
|
274
|
+
{
|
|
275
|
+
name: 'command',
|
|
276
|
+
required: true,
|
|
277
|
+
input_type: 'user',
|
|
278
|
+
description: 'some Description',
|
|
279
|
+
advanced: true,
|
|
280
|
+
value_type: 'plain',
|
|
281
|
+
resource_type: 'ansible_roles',
|
|
282
|
+
default: 'Default val',
|
|
283
|
+
hidden_value: true,
|
|
284
|
+
},
|
|
285
|
+
]);
|
|
286
|
+
api.get.mockImplementation(({ handleSuccess, ...action }) => {
|
|
287
|
+
if (action.key === 'JOB_CATEGORIES') {
|
|
288
|
+
handleSuccess &&
|
|
289
|
+
handleSuccess({ data: { job_categories: jobCategories } });
|
|
290
|
+
} else if (action.key === 'JOB_TEMPLATE') {
|
|
291
|
+
handleSuccess &&
|
|
292
|
+
handleSuccess({
|
|
293
|
+
data: {
|
|
294
|
+
...jobTemplateResponse,
|
|
295
|
+
job_template: {
|
|
296
|
+
...jobTemplateResponse.jobTemplate,
|
|
297
|
+
description_format: 'Run %{command}',
|
|
298
|
+
},
|
|
299
|
+
|
|
300
|
+
template_inputs: [
|
|
301
|
+
{
|
|
302
|
+
name: 'command',
|
|
303
|
+
required: true,
|
|
304
|
+
input_type: 'user',
|
|
305
|
+
description: 'some Description',
|
|
306
|
+
advanced: true,
|
|
307
|
+
value_type: 'plain',
|
|
308
|
+
resource_type: 'ansible_roles',
|
|
309
|
+
default: 'Default val',
|
|
310
|
+
hidden_value: true,
|
|
311
|
+
},
|
|
312
|
+
],
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
} else if (action.key === 'JOB_TEMPLATES') {
|
|
316
|
+
handleSuccess &&
|
|
317
|
+
handleSuccess({
|
|
318
|
+
data: { results: [jobTemplate] },
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
return { type: 'get', ...action };
|
|
322
|
+
});
|
|
323
|
+
render(
|
|
324
|
+
<Provider store={store}>
|
|
325
|
+
<JobWizard />
|
|
326
|
+
</Provider>
|
|
327
|
+
);
|
|
328
|
+
await act(async () => {
|
|
329
|
+
fireEvent.click(screen.getByText(WIZARD_TITLES.advanced));
|
|
330
|
+
});
|
|
331
|
+
expect(
|
|
332
|
+
screen.getByLabelText('description preview', {
|
|
333
|
+
selector: 'input',
|
|
334
|
+
}).value
|
|
335
|
+
).toBe('Run Default val');
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('search resources action', async () => {
|
|
339
|
+
jest.useFakeTimers();
|
|
340
|
+
mockApi(api);
|
|
341
|
+
const newStore = testSetup(selectors, api);
|
|
342
|
+
render(
|
|
343
|
+
<Provider store={newStore}>
|
|
344
|
+
<JobWizard />
|
|
345
|
+
</Provider>
|
|
346
|
+
);
|
|
347
|
+
await act(async () => {
|
|
348
|
+
fireEvent.click(screen.getByText(WIZARD_TITLES.advanced));
|
|
349
|
+
});
|
|
350
|
+
const resourceSelectField = screen.getByLabelText(
|
|
351
|
+
'adv resource select typeahead input'
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
await act(async () => {
|
|
355
|
+
await fireEvent.change(resourceSelectField, {
|
|
356
|
+
target: { value: 'some search' },
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
await jest.runAllTimers();
|
|
360
|
+
});
|
|
361
|
+
expect(newStore.getActions()).toMatchSnapshot('resource search');
|
|
158
362
|
});
|
|
159
363
|
});
|
data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
|
+
|
|
3
|
+
exports[`AdvancedFields search resources action: resource search 1`] = `
|
|
4
|
+
Array [
|
|
5
|
+
Object {
|
|
6
|
+
"key": "JOB_CATEGORIES",
|
|
7
|
+
"type": "get",
|
|
8
|
+
"url": "/ui_job_wizard/categories",
|
|
9
|
+
},
|
|
10
|
+
Object {
|
|
11
|
+
"key": "JOB_TEMPLATES",
|
|
12
|
+
"type": "get",
|
|
13
|
+
"url": URI {
|
|
14
|
+
"_deferred_build": true,
|
|
15
|
+
"_parts": Object {
|
|
16
|
+
"duplicateQueryParameters": false,
|
|
17
|
+
"escapeQuerySpace": true,
|
|
18
|
+
"fragment": null,
|
|
19
|
+
"hostname": null,
|
|
20
|
+
"password": null,
|
|
21
|
+
"path": "foreman/api/v2/job_templates",
|
|
22
|
+
"port": null,
|
|
23
|
+
"preventInvalidHostname": false,
|
|
24
|
+
"protocol": null,
|
|
25
|
+
"query": "search=job_category%3D%22Ansible+Commands%22&per_page=all",
|
|
26
|
+
"urn": null,
|
|
27
|
+
"username": null,
|
|
28
|
+
},
|
|
29
|
+
"_string": "",
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
Object {
|
|
33
|
+
"key": "JOB_TEMPLATE",
|
|
34
|
+
"type": "get",
|
|
35
|
+
"url": "/ui_job_wizard/template/178",
|
|
36
|
+
},
|
|
37
|
+
Object {
|
|
38
|
+
"key": "ForemanTasksTask",
|
|
39
|
+
"type": "get",
|
|
40
|
+
"url": URI {
|
|
41
|
+
"_deferred_build": true,
|
|
42
|
+
"_parts": Object {
|
|
43
|
+
"duplicateQueryParameters": false,
|
|
44
|
+
"escapeQuerySpace": true,
|
|
45
|
+
"fragment": null,
|
|
46
|
+
"hostname": null,
|
|
47
|
+
"password": null,
|
|
48
|
+
"path": "/ui_job_wizard/resources",
|
|
49
|
+
"port": null,
|
|
50
|
+
"preventInvalidHostname": false,
|
|
51
|
+
"protocol": null,
|
|
52
|
+
"query": "resource=ForemanTasks%3A%3ATask&name=",
|
|
53
|
+
"urn": null,
|
|
54
|
+
"username": null,
|
|
55
|
+
},
|
|
56
|
+
"_string": "",
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
Object {
|
|
60
|
+
"key": "ForemanTasksTask",
|
|
61
|
+
"type": "get",
|
|
62
|
+
"url": URI {
|
|
63
|
+
"_deferred_build": true,
|
|
64
|
+
"_parts": Object {
|
|
65
|
+
"duplicateQueryParameters": false,
|
|
66
|
+
"escapeQuerySpace": true,
|
|
67
|
+
"fragment": null,
|
|
68
|
+
"hostname": null,
|
|
69
|
+
"password": null,
|
|
70
|
+
"path": "/ui_job_wizard/resources",
|
|
71
|
+
"port": null,
|
|
72
|
+
"preventInvalidHostname": false,
|
|
73
|
+
"protocol": null,
|
|
74
|
+
"query": "resource=ForemanTasks%3A%3ATask&name=some+search",
|
|
75
|
+
"urn": null,
|
|
76
|
+
"username": null,
|
|
77
|
+
},
|
|
78
|
+
"_string": "",
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
]
|
|
82
|
+
`;
|
|
@@ -54,10 +54,11 @@ const ConnectedCategoryAndTemplate = ({
|
|
|
54
54
|
search: `job_category="${category}"`,
|
|
55
55
|
per_page: 'all',
|
|
56
56
|
}),
|
|
57
|
-
handleSuccess: response =>
|
|
57
|
+
handleSuccess: response => {
|
|
58
58
|
setJobTemplate(
|
|
59
59
|
Number(filterJobTemplates(response?.data?.results)[0]?.id) || null
|
|
60
|
-
)
|
|
60
|
+
);
|
|
61
|
+
},
|
|
61
62
|
})
|
|
62
63
|
);
|
|
63
64
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { useSelector } from 'react-redux';
|
|
4
|
+
import URI from 'urijs';
|
|
5
|
+
import { List, ListItem, Modal, Button } from '@patternfly/react-core';
|
|
6
|
+
import { translate as __, sprintf } from 'foremanReact/common/I18n';
|
|
7
|
+
import { foremanUrl } from 'foremanReact/common/helpers';
|
|
8
|
+
import { selectHosts, selectHostCount } from '../../JobWizardSelectors';
|
|
9
|
+
import { HOSTS_TO_PREVIEW_AMOUNT } from '../../JobWizardConstants';
|
|
10
|
+
|
|
11
|
+
export const HostPreviewModal = ({ isOpen, setIsOpen, searchQuery }) => {
|
|
12
|
+
const hosts = useSelector(selectHosts);
|
|
13
|
+
const hostsCount = useSelector(selectHostCount);
|
|
14
|
+
const url = new URI(foremanUrl('/hosts'));
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<Modal
|
|
18
|
+
title={__('Preview Hosts')}
|
|
19
|
+
isOpen={isOpen}
|
|
20
|
+
onClose={() => setIsOpen(false)}
|
|
21
|
+
appendTo={() => document.getElementsByClassName('job-wizard')[0]}
|
|
22
|
+
>
|
|
23
|
+
<List isPlain>
|
|
24
|
+
{hosts.map(host => (
|
|
25
|
+
<ListItem key={host}>
|
|
26
|
+
<Button
|
|
27
|
+
component="a"
|
|
28
|
+
href={foremanUrl(`/hosts/${host}`)}
|
|
29
|
+
variant="link"
|
|
30
|
+
target="_blank"
|
|
31
|
+
rel="noreferrer"
|
|
32
|
+
>
|
|
33
|
+
{host}
|
|
34
|
+
</Button>
|
|
35
|
+
</ListItem>
|
|
36
|
+
))}
|
|
37
|
+
{hostsCount > HOSTS_TO_PREVIEW_AMOUNT && (
|
|
38
|
+
<ListItem>
|
|
39
|
+
<Button
|
|
40
|
+
component="a"
|
|
41
|
+
href={url.addSearch({ search: searchQuery })}
|
|
42
|
+
variant="link"
|
|
43
|
+
target="_blank"
|
|
44
|
+
rel="noreferrer"
|
|
45
|
+
>
|
|
46
|
+
{sprintf(
|
|
47
|
+
__('...and %s more'),
|
|
48
|
+
hostsCount - HOSTS_TO_PREVIEW_AMOUNT
|
|
49
|
+
)}
|
|
50
|
+
</Button>
|
|
51
|
+
</ListItem>
|
|
52
|
+
)}
|
|
53
|
+
</List>
|
|
54
|
+
</Modal>
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
HostPreviewModal.propTypes = {
|
|
59
|
+
isOpen: PropTypes.bool.isRequired,
|
|
60
|
+
setIsOpen: PropTypes.func.isRequired,
|
|
61
|
+
searchQuery: PropTypes.string.isRequired,
|
|
62
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import { useSelector, useDispatch } from 'react-redux';
|
|
3
|
+
import PropTypes from 'prop-types';
|
|
4
|
+
import SearchBar from 'foremanReact/components/SearchBar';
|
|
5
|
+
import { getControllerSearchProps } from 'foremanReact/constants';
|
|
6
|
+
import { getResults } from 'foremanReact/components/AutoComplete/AutoCompleteActions';
|
|
7
|
+
import { TRIGGERS } from 'foremanReact/components/AutoComplete/AutoCompleteConstants';
|
|
8
|
+
import { hostsController, hostQuerySearchID } from '../../JobWizardConstants';
|
|
9
|
+
import { noop } from '../../../helpers';
|
|
10
|
+
|
|
11
|
+
export const HostSearch = ({ value, setValue }) => {
|
|
12
|
+
const searchQuery = useSelector(
|
|
13
|
+
state => state.autocomplete?.hostsSearch?.searchQuery
|
|
14
|
+
);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
setValue(searchQuery || '');
|
|
17
|
+
}, [setValue, searchQuery]);
|
|
18
|
+
const dispatch = useDispatch();
|
|
19
|
+
const setSearch = newSearchQuery => {
|
|
20
|
+
dispatch(
|
|
21
|
+
getResults({
|
|
22
|
+
url: '/hosts/auto_complete_search',
|
|
23
|
+
searchQuery: newSearchQuery,
|
|
24
|
+
controller: 'hostsController',
|
|
25
|
+
trigger: TRIGGERS.INPUT_CHANGE,
|
|
26
|
+
id: hostQuerySearchID,
|
|
27
|
+
})
|
|
28
|
+
);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const props = getControllerSearchProps(hostsController, hostQuerySearchID);
|
|
32
|
+
return (
|
|
33
|
+
<div className="foreman-search-field">
|
|
34
|
+
<SearchBar
|
|
35
|
+
data={{
|
|
36
|
+
...props,
|
|
37
|
+
autocomplete: {
|
|
38
|
+
id: hostQuerySearchID,
|
|
39
|
+
url: '/hosts/auto_complete_search',
|
|
40
|
+
useKeyShortcuts: true,
|
|
41
|
+
},
|
|
42
|
+
}}
|
|
43
|
+
onSearch={noop}
|
|
44
|
+
initialQuery={value}
|
|
45
|
+
onBookmarkClick={search => setSearch(search)}
|
|
46
|
+
/>
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
HostSearch.propTypes = {
|
|
52
|
+
value: PropTypes.string.isRequired,
|
|
53
|
+
setValue: PropTypes.func.isRequired,
|
|
54
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useSelector, useDispatch } from 'react-redux';
|
|
3
|
+
import URI from 'urijs';
|
|
4
|
+
import { SelectVariant } from '@patternfly/react-core';
|
|
5
|
+
import { get } from 'foremanReact/redux/API';
|
|
6
|
+
import { selectResponse, selectIsLoading } from '../../JobWizardSelectors';
|
|
7
|
+
import { SearchSelect } from '../form/SearchSelect';
|
|
8
|
+
|
|
9
|
+
export const useNameSearchAPI = (apiKey, url) => {
|
|
10
|
+
const dispatch = useDispatch();
|
|
11
|
+
const uri = new URI(url);
|
|
12
|
+
const onSearch = search =>
|
|
13
|
+
dispatch(
|
|
14
|
+
get({
|
|
15
|
+
key: apiKey,
|
|
16
|
+
url: uri.addSearch({
|
|
17
|
+
search: `name~"${search}"`,
|
|
18
|
+
}),
|
|
19
|
+
})
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const response = useSelector(state => selectResponse(state, apiKey));
|
|
23
|
+
const isLoading = useSelector(state => selectIsLoading(state, apiKey));
|
|
24
|
+
return [onSearch, response, isLoading];
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const SelectAPI = props => (
|
|
28
|
+
<SearchSelect
|
|
29
|
+
{...props}
|
|
30
|
+
variant={SelectVariant.typeaheadMulti}
|
|
31
|
+
useNameSearch={useNameSearchAPI}
|
|
32
|
+
/>
|
|
33
|
+
);
|