foreman_remote_execution 6.0.0 → 7.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/controllers/api/v2/job_invocations_controller.rb +1 -0
- data/app/controllers/foreman_remote_execution/concerns/api/v2/registration_controller_extensions.rb +5 -0
- data/app/controllers/ui_job_wizard_controller.rb +1 -1
- data/app/helpers/hosts_extensions_helper.rb +62 -0
- data/app/helpers/remote_execution_helper.rb +4 -3
- data/app/lib/actions/remote_execution/run_host_job.rb +5 -1
- data/app/lib/actions/remote_execution/run_hosts_job.rb +4 -0
- data/app/models/concerns/foreman_remote_execution/host_extensions.rb +5 -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 +7 -3
- data/app/models/job_template.rb +4 -1
- data/app/models/remote_execution_provider.rb +10 -1
- data/app/models/ssh_execution_provider.rb +20 -7
- 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/api/v2/job_invocations/base.json.rabl +1 -1
- data/app/views/dashboard/_latest-jobs.html.erb +1 -1
- data/app/views/job_invocations/_card_target_hosts.html.erb +8 -0
- data/app/views/job_invocations/_form.html.erb +2 -0
- 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/migrate/20220331112719_add_ssh_user_to_job_invocation.rb +5 -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 -5
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/locale/action_names.rb +3 -3
- data/locale/de/foreman_remote_execution.po +23 -23
- data/locale/en/foreman_remote_execution.po +23 -23
- data/locale/en_GB/foreman_remote_execution.po +23 -23
- data/locale/es/foreman_remote_execution.po +23 -23
- data/locale/foreman_remote_execution.pot +64 -66
- data/locale/fr/foreman_remote_execution.po +23 -23
- data/locale/ja/foreman_remote_execution.po +23 -23
- data/locale/ko/foreman_remote_execution.po +23 -23
- data/locale/pt_BR/foreman_remote_execution.po +23 -23
- data/locale/ru/foreman_remote_execution.po +23 -23
- data/locale/zh_CN/foreman_remote_execution.po +23 -23
- data/locale/zh_TW/foreman_remote_execution.po +23 -23
- 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 +8 -2
- data/webpack/JobWizard/JobWizardConstants.js +2 -2
- data/webpack/JobWizard/__tests__/fixtures.js +8 -4
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +2 -2
- data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +24 -15
- data/webpack/JobWizard/steps/HostsAndInputs/SelectGQL.js +2 -1
- data/webpack/JobWizard/steps/HostsAndInputs/SelectedChips.js +1 -1
- data/webpack/JobWizard/steps/HostsAndInputs/buildHostQuery.js +1 -1
- data/webpack/JobWizard/steps/HostsAndInputs/hostgroups.gql +1 -0
- data/webpack/JobWizard/steps/HostsAndInputs/hosts.gql +1 -0
- data/webpack/JobWizard/steps/ReviewDetails/index.js +1 -1
- data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +15 -15
- data/webpack/JobWizard/steps/form/GroupedSelectField.js +7 -1
- data/webpack/JobWizard/steps/form/SearchSelect.js +0 -1
- data/webpack/__mocks__/foremanReact/common/globalIdHelpers.js +1 -0
- data/webpack/global_index.js +2 -4
- 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 +27 -0
- data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +10 -5
- 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/TargetingHostsConsts.js +1 -0
- data/webpack/react_app/components/TargetingHosts/TargetingHostsPage.js +8 -3
- data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +9 -1
- data/webpack/react_app/components/TargetingHosts/__tests__/fixtures.js +1 -4
- data/webpack/react_app/components/TargetingHosts/index.js +1 -0
- data/webpack/react_app/extend/Fills.js +48 -0
- metadata +25 -17
- data/app/helpers/concerns/foreman_remote_execution/hosts_helper_extensions.rb +0 -62
- data/webpack/react_app/extend/fillRecentJobsCard.js +0 -11
- data/webpack/react_app/extend/fillregistrationAdvanced.js +0 -11
@@ -19,9 +19,9 @@ export const repeatTypes = {
|
|
19
19
|
export const WIZARD_TITLES = {
|
20
20
|
categoryAndTemplate: __('Category and Template'),
|
21
21
|
hostsAndInputs: __('Target hosts and inputs'),
|
22
|
-
advanced: __('Advanced
|
22
|
+
advanced: __('Advanced fields'),
|
23
23
|
schedule: __('Schedule'),
|
24
|
-
review: __('Review
|
24
|
+
review: __('Review details'),
|
25
25
|
};
|
26
26
|
|
27
27
|
export const initialScheduleState = {
|
@@ -193,7 +193,11 @@ export const gqlMock = [
|
|
193
193
|
data: {
|
194
194
|
hosts: {
|
195
195
|
totalCount: 3,
|
196
|
-
nodes: [
|
196
|
+
nodes: [
|
197
|
+
{ id: 'MDE6SG9zdC0x', name: 'host1' },
|
198
|
+
{ id: 'MDE6SG9zdC0y', name: 'host2' },
|
199
|
+
{ id: 'MDE6SG9zdC0z', name: 'host3' },
|
200
|
+
],
|
197
201
|
},
|
198
202
|
},
|
199
203
|
},
|
@@ -211,9 +215,9 @@ export const gqlMock = [
|
|
211
215
|
hostgroups: {
|
212
216
|
totalCount: 3,
|
213
217
|
nodes: [
|
214
|
-
{ name: 'host_group1' },
|
215
|
-
{ name: 'host_group2' },
|
216
|
-
{ name: 'host_group3' },
|
218
|
+
{ id: 'MDE6SG9zdGdyb3VwLTE=', name: 'host_group1' },
|
219
|
+
{ id: 'MDE6SG9zdGdyb3VwLTI=', name: 'host_group2' },
|
220
|
+
{ id: 'MDE6SG9zdGdyb3VwLTM=', name: 'host_group3' },
|
217
221
|
],
|
218
222
|
},
|
219
223
|
},
|
@@ -143,7 +143,7 @@ describe('AdvancedFields', () => {
|
|
143
143
|
);
|
144
144
|
|
145
145
|
await act(async () => {
|
146
|
-
fireEvent.click(screen.getByText('Advanced
|
146
|
+
fireEvent.click(screen.getByText('Advanced fields'));
|
147
147
|
});
|
148
148
|
expect(textField.value).toBe(textValue);
|
149
149
|
expect(searchField.value).toBe(searchValue);
|
@@ -162,7 +162,7 @@ describe('AdvancedFields', () => {
|
|
162
162
|
</MockedProvider>
|
163
163
|
);
|
164
164
|
await act(async () => {
|
165
|
-
fireEvent.click(screen.getByText('Advanced
|
165
|
+
fireEvent.click(screen.getByText('Advanced fields'));
|
166
166
|
});
|
167
167
|
|
168
168
|
expect(
|
@@ -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 && (
|
@@ -5,6 +5,7 @@ import {
|
|
5
5
|
useForemanOrganization,
|
6
6
|
useForemanLocation,
|
7
7
|
} from 'foremanReact/Root/Context/ForemanContext';
|
8
|
+
import { decodeId } from 'foremanReact/common/globalIdHelpers';
|
8
9
|
import { HOSTS, HOST_GROUPS, dataName } from '../../JobWizardConstants';
|
9
10
|
import { SearchSelect } from '../form/SearchSelect';
|
10
11
|
import hostsQuery from './hosts.gql';
|
@@ -35,7 +36,7 @@ export const useNameSearchGQL = apiKey => {
|
|
35
36
|
subtotal: data?.[dataName[apiKey]]?.totalCount,
|
36
37
|
results:
|
37
38
|
data?.[dataName[apiKey]]?.nodes.map(node => ({
|
38
|
-
id: node.
|
39
|
+
id: decodeId(node.id),
|
39
40
|
name: node.name,
|
40
41
|
})) || [],
|
41
42
|
},
|
@@ -1,6 +1,6 @@
|
|
1
1
|
export const buildHostQuery = (selected, search) => {
|
2
2
|
const { hosts, hostCollections, hostGroups } = selected;
|
3
|
-
const hostsSearch = `(
|
3
|
+
const hostsSearch = `(id ^ (${hosts.map(({ id }) => id).join(',')}))`;
|
4
4
|
const hostCollectionsSearch = `(host_collection_id ^ (${hostCollections
|
5
5
|
.map(({ id }) => id)
|
6
6
|
.join(',')}))`;
|
@@ -72,7 +72,7 @@ const ReviewDetails = ({
|
|
72
72
|
};
|
73
73
|
const [isAdvancedShown, setIsAdvancedShown] = useState(false);
|
74
74
|
const detailsFirstHalf = [
|
75
|
-
{ label: __('Job
|
75
|
+
{ label: __('Job category'), value: jobCategory },
|
76
76
|
{ label: __('Job template'), value: jobTemplate },
|
77
77
|
{ label: __('Target hosts'), value: stringHosts() },
|
78
78
|
...templateInputs.map(({ name }) => ({
|
@@ -186,7 +186,7 @@ describe('Schedule', () => {
|
|
186
186
|
expect(
|
187
187
|
screen.getByPlaceholderText('Repeat N times').hasAttribute('disabled')
|
188
188
|
).toBeTruthy();
|
189
|
-
expect(screen.getByText('Review
|
189
|
+
expect(screen.getByText('Review details').disabled).toBeFalsy();
|
190
190
|
await act(async () => {
|
191
191
|
fireEvent.click(
|
192
192
|
screen.getByLabelText('Does not repeat', { selector: 'button' })
|
@@ -196,7 +196,7 @@ describe('Schedule', () => {
|
|
196
196
|
await act(async () => {
|
197
197
|
fireEvent.click(screen.getByText('Cronline'));
|
198
198
|
});
|
199
|
-
expect(screen.getByText('Review
|
199
|
+
expect(screen.getByText('Review details').disabled).toBeTruthy();
|
200
200
|
const newRepeatTimes = '3';
|
201
201
|
const repeatNTimes = screen.getByPlaceholderText('Repeat N times');
|
202
202
|
expect(repeatNTimes.value).toBe('');
|
@@ -216,7 +216,7 @@ describe('Schedule', () => {
|
|
216
216
|
});
|
217
217
|
});
|
218
218
|
expect(cronline.value).toBe(newCronline);
|
219
|
-
expect(screen.getByText('Review
|
219
|
+
expect(screen.getByText('Review details').disabled).toBeFalsy();
|
220
220
|
|
221
221
|
await act(async () => {
|
222
222
|
fireEvent.click(screen.getByText('Category and Template'));
|
@@ -236,7 +236,7 @@ describe('Schedule', () => {
|
|
236
236
|
fireEvent.click(screen.getByText('Monthly'));
|
237
237
|
});
|
238
238
|
|
239
|
-
expect(screen.getByText('Review
|
239
|
+
expect(screen.getByText('Review details').disabled).toBeTruthy();
|
240
240
|
const newDays = '1,2,3';
|
241
241
|
const days = screen.getByLabelText('days');
|
242
242
|
expect(days.value).toBe('');
|
@@ -248,7 +248,7 @@ describe('Schedule', () => {
|
|
248
248
|
});
|
249
249
|
expect(days.value).toBe(newDays);
|
250
250
|
|
251
|
-
expect(screen.getByText('Review
|
251
|
+
expect(screen.getByText('Review details').disabled).toBeTruthy();
|
252
252
|
const newAtMonthly = '13:07';
|
253
253
|
const at = () => screen.getByLabelText('repeat-at');
|
254
254
|
expect(at().value).toBe('');
|
@@ -259,13 +259,13 @@ describe('Schedule', () => {
|
|
259
259
|
});
|
260
260
|
expect(at().value).toBe(newAtMonthly);
|
261
261
|
|
262
|
-
expect(screen.getByText('Review
|
262
|
+
expect(screen.getByText('Review details').disabled).toBeFalsy();
|
263
263
|
fireEvent.click(screen.getByText('Monthly'));
|
264
264
|
await act(async () => {
|
265
265
|
fireEvent.click(screen.getByText('Weekly'));
|
266
266
|
});
|
267
267
|
|
268
|
-
expect(screen.getByText('Review
|
268
|
+
expect(screen.getByText('Review details').disabled).toBeTruthy();
|
269
269
|
const dayTue = screen.getByLabelText('Tue checkbox');
|
270
270
|
const daySat = screen.getByLabelText('Sat checkbox');
|
271
271
|
expect(dayTue.checked).toBe(false);
|
@@ -293,19 +293,19 @@ describe('Schedule', () => {
|
|
293
293
|
});
|
294
294
|
expect(at().value).toBe(newAtWeekly);
|
295
295
|
|
296
|
-
expect(screen.getByText('Review
|
296
|
+
expect(screen.getByText('Review details').disabled).toBeFalsy();
|
297
297
|
fireEvent.click(screen.getByText('Weekly'));
|
298
298
|
await act(async () => {
|
299
299
|
fireEvent.click(screen.getByText('Daily'));
|
300
300
|
});
|
301
301
|
|
302
|
-
expect(screen.getByText('Review
|
302
|
+
expect(screen.getByText('Review details').disabled).toBeFalsy();
|
303
303
|
await act(async () => {
|
304
304
|
fireEvent.change(at(), {
|
305
305
|
target: { value: '' },
|
306
306
|
});
|
307
307
|
});
|
308
|
-
expect(screen.getByText('Review
|
308
|
+
expect(screen.getByText('Review details').disabled).toBeTruthy();
|
309
309
|
const newAtDaily = '17:07';
|
310
310
|
expect(at().value).toBe('');
|
311
311
|
await act(async () => {
|
@@ -314,14 +314,14 @@ describe('Schedule', () => {
|
|
314
314
|
});
|
315
315
|
});
|
316
316
|
expect(at().value).toBe(newAtDaily);
|
317
|
-
expect(screen.getByText('Review
|
317
|
+
expect(screen.getByText('Review details').disabled).toBeFalsy();
|
318
318
|
|
319
319
|
fireEvent.click(screen.getByText('Daily'));
|
320
320
|
await act(async () => {
|
321
321
|
fireEvent.click(screen.getByText('Hourly'));
|
322
322
|
});
|
323
323
|
|
324
|
-
expect(screen.getByText('Review
|
324
|
+
expect(screen.getByText('Review details').disabled).toBeTruthy();
|
325
325
|
const newMinutes = '6';
|
326
326
|
const atHourly = screen.getByLabelText('repeat-at-minute-typeahead');
|
327
327
|
expect(atHourly.value).toBe('');
|
@@ -331,7 +331,7 @@ describe('Schedule', () => {
|
|
331
331
|
await act(async () => {
|
332
332
|
fireEvent.click(screen.getByText(newMinutes));
|
333
333
|
});
|
334
|
-
expect(screen.getByText('Review
|
334
|
+
expect(screen.getByText('Review details').disabled).toBeFalsy();
|
335
335
|
expect(atHourly.value).toBe(newMinutes);
|
336
336
|
});
|
337
337
|
it('should show invalid error on start date after end', async () => {
|
@@ -353,7 +353,7 @@ describe('Schedule', () => {
|
|
353
353
|
expect(
|
354
354
|
screen.queryAllByText('End time needs to be after start time')
|
355
355
|
).toHaveLength(0);
|
356
|
-
expect(screen.getByText('Review
|
356
|
+
expect(screen.getByText('Review details').disabled).toBeFalsy();
|
357
357
|
await act(async () => {
|
358
358
|
await fireEvent.change(startsDateField, {
|
359
359
|
target: { value: '2020/10/15' },
|
@@ -368,7 +368,7 @@ describe('Schedule', () => {
|
|
368
368
|
screen.queryAllByText('End time needs to be after start time')
|
369
369
|
).toHaveLength(1);
|
370
370
|
|
371
|
-
expect(screen.getByText('Review
|
371
|
+
expect(screen.getByText('Review details').disabled).toBeTruthy();
|
372
372
|
});
|
373
373
|
it('purpose and ends should be disabled when no reaccurence ', async () => {
|
374
374
|
render(
|
@@ -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
|
|
@@ -93,7 +93,6 @@ export const SearchSelect = ({
|
|
93
93
|
autoSearch(value || '');
|
94
94
|
}}
|
95
95
|
placeholderText={placeholderText}
|
96
|
-
onFilter={() => null} // https://github.com/patternfly/patternfly-react/issues/6321
|
97
96
|
typeAheadAriaLabel={`${name} typeahead input`}
|
98
97
|
>
|
99
98
|
{selectOptions}
|
@@ -0,0 +1 @@
|
|
1
|
+
export const decodeId = whatever => whatever;
|
data/webpack/global_index.js
CHANGED
@@ -1,10 +1,8 @@
|
|
1
1
|
import { registerRoutes } from 'foremanReact/routes/RoutingService';
|
2
2
|
import routes from './Routes/routes';
|
3
|
-
import fillregistrationAdvanced from './react_app/extend/fillregistrationAdvanced';
|
4
|
-
import fillRecentJobsCard from './react_app/extend/fillRecentJobsCard';
|
5
3
|
import registerReducers from './react_app/extend/reducers';
|
4
|
+
import registerFills from './react_app/extend/Fills';
|
6
5
|
|
7
6
|
registerReducers();
|
8
7
|
registerRoutes('foreman_remote_execution', routes);
|
9
|
-
|
10
|
-
fillregistrationAdvanced();
|
8
|
+
registerFills();
|
@@ -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
|
+
};
|
@@ -0,0 +1,74 @@
|
|
1
|
+
import PropTypes from 'prop-types';
|
2
|
+
import React, { useState } from 'react';
|
3
|
+
import { useDispatch } from 'react-redux';
|
4
|
+
import {
|
5
|
+
DropdownItem,
|
6
|
+
Dropdown,
|
7
|
+
DropdownToggle,
|
8
|
+
DropdownToggleAction,
|
9
|
+
} from '@patternfly/react-core';
|
10
|
+
import { push } from 'connected-react-router';
|
11
|
+
|
12
|
+
import { useAPI } from 'foremanReact/common/hooks/API/APIHooks';
|
13
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
14
|
+
import { foremanUrl } from 'foremanReact/common/helpers';
|
15
|
+
import { STATUS } from 'foremanReact/constants';
|
16
|
+
|
17
|
+
import { REX_FEATURES_API, NEW_JOB_PAGE } from './constant';
|
18
|
+
import { runFeature } from './actions';
|
19
|
+
|
20
|
+
const FeaturesDropdown = ({ hostId }) => {
|
21
|
+
const [isOpen, setIsOpen] = useState(false);
|
22
|
+
const {
|
23
|
+
response: { results: features },
|
24
|
+
status,
|
25
|
+
} = useAPI('get', foremanUrl(REX_FEATURES_API));
|
26
|
+
|
27
|
+
const dispatch = useDispatch();
|
28
|
+
const dropdownItems = features
|
29
|
+
?.filter(feature => feature.host_action_button)
|
30
|
+
?.map(({ name, label, id, description }) => (
|
31
|
+
<DropdownItem
|
32
|
+
onClick={() => dispatch(runFeature(hostId, label, name))}
|
33
|
+
key={id}
|
34
|
+
description={description}
|
35
|
+
>
|
36
|
+
{name}
|
37
|
+
</DropdownItem>
|
38
|
+
));
|
39
|
+
const scheduleJob = [
|
40
|
+
<DropdownToggleAction
|
41
|
+
onClick={() => dispatch(push(`${NEW_JOB_PAGE}=${hostId}`))}
|
42
|
+
key="schedule-job-action"
|
43
|
+
>
|
44
|
+
{__('Schedule a job')}
|
45
|
+
</DropdownToggleAction>,
|
46
|
+
];
|
47
|
+
|
48
|
+
return (
|
49
|
+
<Dropdown
|
50
|
+
alignments={{ default: 'right' }}
|
51
|
+
onSelect={() => setIsOpen(false)}
|
52
|
+
toggle={
|
53
|
+
<DropdownToggle
|
54
|
+
splitButtonItems={scheduleJob}
|
55
|
+
toggleVariant="primary"
|
56
|
+
onToggle={() => setIsOpen(prev => !prev)}
|
57
|
+
isDisabled={status === STATUS.PENDING}
|
58
|
+
splitButtonVariant="action"
|
59
|
+
/>
|
60
|
+
}
|
61
|
+
isOpen={isOpen}
|
62
|
+
dropdownItems={dropdownItems}
|
63
|
+
/>
|
64
|
+
);
|
65
|
+
};
|
66
|
+
|
67
|
+
FeaturesDropdown.propTypes = {
|
68
|
+
hostId: PropTypes.number,
|
69
|
+
};
|
70
|
+
FeaturesDropdown.defaultProps = {
|
71
|
+
hostId: undefined,
|
72
|
+
};
|
73
|
+
|
74
|
+
export default FeaturesDropdown;
|
@@ -0,0 +1,27 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { useSelector } from 'react-redux';
|
3
|
+
import { DropdownItem } from '@patternfly/react-core';
|
4
|
+
import { CodeIcon } from '@patternfly/react-icons';
|
5
|
+
import { selectAPIResponse } from 'foremanReact/redux/API/APISelectors';
|
6
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
7
|
+
import { HOST_DETAILS_KEY } from 'foremanReact/components/HostDetails/consts';
|
8
|
+
|
9
|
+
const HostKebabItems = () => {
|
10
|
+
const { cockpit_url: consoleUrl } = useSelector(state =>
|
11
|
+
selectAPIResponse(state, HOST_DETAILS_KEY)
|
12
|
+
);
|
13
|
+
|
14
|
+
if (!consoleUrl) return null;
|
15
|
+
return (
|
16
|
+
<DropdownItem
|
17
|
+
icon={<CodeIcon />}
|
18
|
+
href={consoleUrl}
|
19
|
+
target="_blank"
|
20
|
+
rel="noreferrer"
|
21
|
+
>
|
22
|
+
{__('Web Console')}
|
23
|
+
</DropdownItem>
|
24
|
+
);
|
25
|
+
};
|
26
|
+
|
27
|
+
export default HostKebabItems;
|
@@ -28,7 +28,7 @@ const RecentJobsCard = ({ hostDetails: { name, id } }) => {
|
|
28
28
|
href={foremanUrl(`${JOB_BASE_URL}${name}`)}
|
29
29
|
key="link-to-all"
|
30
30
|
>
|
31
|
-
{__('View
|
31
|
+
{__('View all jobs')}
|
32
32
|
</DropdownItem>,
|
33
33
|
<DropdownItem
|
34
34
|
href={foremanUrl(
|
@@ -36,23 +36,28 @@ const RecentJobsCard = ({ hostDetails: { name, id } }) => {
|
|
36
36
|
)}
|
37
37
|
key="link-to-finished"
|
38
38
|
>
|
39
|
-
{__('View
|
39
|
+
{__('View finished jobs')}
|
40
40
|
</DropdownItem>,
|
41
41
|
<DropdownItem
|
42
42
|
href={foremanUrl(`${JOB_BASE_URL}${name}+and+status+%3D+running`)}
|
43
43
|
key="link-to-running"
|
44
44
|
>
|
45
|
-
{__('View
|
45
|
+
{__('View running jobs')}
|
46
46
|
</DropdownItem>,
|
47
47
|
<DropdownItem
|
48
48
|
href={foremanUrl(`${JOB_BASE_URL}${name}+and+status+%3D+queued`)}
|
49
49
|
key="link-to-scheduled"
|
50
50
|
>
|
51
|
-
{__('View
|
51
|
+
{__('View scheduled jobs')}
|
52
52
|
</DropdownItem>,
|
53
53
|
]}
|
54
54
|
>
|
55
|
-
<Tabs
|
55
|
+
<Tabs
|
56
|
+
mountOnEnter
|
57
|
+
unmountOnExit
|
58
|
+
activeKey={activeTab}
|
59
|
+
onSelect={handleTabClick}
|
60
|
+
>
|
56
61
|
<Tab
|
57
62
|
eventKey={FINISHED_TAB}
|
58
63
|
title={<TabTitleText>{__('Finished')}</TabTitleText>}
|
@@ -19,7 +19,7 @@ import { translate as __ } from 'foremanReact/common/I18n';
|
|
19
19
|
import { foremanUrl } from 'foremanReact/common/helpers';
|
20
20
|
|
21
21
|
import JobStatusIcon from './JobStatusIcon';
|
22
|
-
import { JOB_API_URL, JOBS_IN_CARD } from './constants';
|
22
|
+
import { JOB_API_URL, JOBS_IN_CARD, RECENT_JOBS_KEY } from './constants';
|
23
23
|
|
24
24
|
const RecentJobsTable = ({ status, hostId }) => {
|
25
25
|
const jobsUrl =
|
@@ -30,7 +30,7 @@ const RecentJobsTable = ({ status, hostId }) => {
|
|
30
30
|
const {
|
31
31
|
response: { results: jobs },
|
32
32
|
status: responseStatus,
|
33
|
-
} = useAPI('get', jobsUrl);
|
33
|
+
} = useAPI('get', jobsUrl, RECENT_JOBS_KEY);
|
34
34
|
|
35
35
|
return (
|
36
36
|
<DataList aria-label="recent-jobs-table" isCompact>
|
@@ -7,6 +7,7 @@ import Pagination from 'foremanReact/components/Pagination';
|
|
7
7
|
import { getControllerSearchProps } from 'foremanReact/constants';
|
8
8
|
|
9
9
|
import TargetingHosts from './TargetingHosts';
|
10
|
+
import { TARGETING_HOSTS_AUTOCOMPLETE } from './TargetingHostsConsts';
|
10
11
|
import './TargetingHostsPage.scss';
|
11
12
|
|
12
13
|
const TargetingHostsPage = ({
|
@@ -16,6 +17,7 @@ const TargetingHostsPage = ({
|
|
16
17
|
items,
|
17
18
|
totalHosts,
|
18
19
|
handlePagination,
|
20
|
+
page,
|
19
21
|
}) => (
|
20
22
|
<div id="targeting_hosts">
|
21
23
|
<Grid.Row>
|
@@ -23,24 +25,26 @@ const TargetingHostsPage = ({
|
|
23
25
|
<SearchBar
|
24
26
|
onSearch={query => handleSearch(query)}
|
25
27
|
data={{
|
26
|
-
...getControllerSearchProps('hosts'),
|
28
|
+
...getControllerSearchProps('hosts', TARGETING_HOSTS_AUTOCOMPLETE),
|
27
29
|
autocomplete: {
|
28
|
-
id:
|
30
|
+
id: TARGETING_HOSTS_AUTOCOMPLETE,
|
29
31
|
searchQuery,
|
30
32
|
url: '/hosts/auto_complete_search',
|
31
33
|
useKeyShortcuts: true,
|
32
34
|
},
|
33
|
-
bookmarks: {},
|
34
35
|
}}
|
36
|
+
onBookmarkClick={handleSearch}
|
35
37
|
/>
|
36
38
|
</Grid.Col>
|
37
39
|
</Grid.Row>
|
38
40
|
<br />
|
39
41
|
<TargetingHosts apiStatus={apiStatus} items={items} />
|
40
42
|
<Pagination
|
43
|
+
page={page}
|
41
44
|
itemCount={totalHosts}
|
42
45
|
onChange={args => handlePagination(args)}
|
43
46
|
className="targeting-hosts-pagination"
|
47
|
+
updateParamsByUrl={false}
|
44
48
|
/>
|
45
49
|
</div>
|
46
50
|
);
|
@@ -52,6 +56,7 @@ TargetingHostsPage.propTypes = {
|
|
52
56
|
items: PropTypes.array.isRequired,
|
53
57
|
totalHosts: PropTypes.number.isRequired,
|
54
58
|
handlePagination: PropTypes.func.isRequired,
|
59
|
+
page: PropTypes.number.isRequired,
|
55
60
|
};
|
56
61
|
|
57
62
|
TargetingHostsPage.defaultProps = {
|
@@ -23,10 +23,16 @@ exports[`TargetingHostsPage renders 1`] = `
|
|
23
23
|
"url": "/hosts/auto_complete_search",
|
24
24
|
"useKeyShortcuts": true,
|
25
25
|
},
|
26
|
-
"bookmarks": Object {
|
26
|
+
"bookmarks": Object {
|
27
|
+
"canCreate": true,
|
28
|
+
"documentationUrl": "4.1.5Searching",
|
29
|
+
"id": "targeting_hosts_search",
|
30
|
+
"url": "/api/bookmarks",
|
31
|
+
},
|
27
32
|
"controller": "hosts",
|
28
33
|
}
|
29
34
|
}
|
35
|
+
onBookmarkClick={[Function]}
|
30
36
|
onChange={[Function]}
|
31
37
|
onSearch={[Function]}
|
32
38
|
/>
|
@@ -56,6 +62,8 @@ exports[`TargetingHostsPage renders 1`] = `
|
|
56
62
|
className="targeting-hosts-pagination"
|
57
63
|
itemCount={1}
|
58
64
|
onChange={[Function]}
|
65
|
+
page={1}
|
66
|
+
updateParamsByUrl={false}
|
59
67
|
/>
|
60
68
|
</div>
|
61
69
|
`;
|