foreman_remote_execution 6.0.0 → 7.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 +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
|
`;
|