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.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/api/v2/job_invocations_controller.rb +1 -0
  3. data/app/controllers/foreman_remote_execution/concerns/api/v2/registration_controller_extensions.rb +5 -0
  4. data/app/controllers/ui_job_wizard_controller.rb +1 -1
  5. data/app/helpers/hosts_extensions_helper.rb +62 -0
  6. data/app/helpers/remote_execution_helper.rb +4 -3
  7. data/app/lib/actions/remote_execution/run_host_job.rb +5 -1
  8. data/app/lib/actions/remote_execution/run_hosts_job.rb +4 -0
  9. data/app/models/concerns/foreman_remote_execution/host_extensions.rb +5 -1
  10. data/app/models/concerns/foreman_remote_execution/nic_extensions.rb +6 -4
  11. data/app/models/concerns/foreman_remote_execution/smart_proxy_extensions.rb +1 -1
  12. data/app/models/job_invocation_composer.rb +7 -3
  13. data/app/models/job_template.rb +4 -1
  14. data/app/models/remote_execution_provider.rb +10 -1
  15. data/app/models/ssh_execution_provider.rb +20 -7
  16. data/app/services/default_proxy_proxy_selector.rb +1 -1
  17. data/app/services/remote_execution_proxy_selector.rb +7 -2
  18. data/app/views/api/v2/host/main.rabl +1 -0
  19. data/app/views/api/v2/job_invocations/base.json.rabl +1 -1
  20. data/app/views/dashboard/_latest-jobs.html.erb +1 -1
  21. data/app/views/job_invocations/_card_target_hosts.html.erb +8 -0
  22. data/app/views/job_invocations/_form.html.erb +2 -0
  23. data/app/views/templates/{ssh → script}/check_update.erb +2 -2
  24. data/app/views/templates/{ssh → script}/module_action.erb +2 -2
  25. data/app/views/templates/{ssh → script}/package_action.erb +5 -2
  26. data/app/views/templates/{ssh → script}/power_action.erb +2 -2
  27. data/app/views/templates/{ssh → script}/puppet_agent_disable.erb +2 -2
  28. data/app/views/templates/{ssh → script}/puppet_agent_enable.erb +2 -2
  29. data/app/views/templates/{ssh → script}/puppet_install_modules_from_forge.erb +2 -2
  30. data/app/views/templates/{ssh → script}/puppet_install_modules_from_git.erb +2 -2
  31. data/app/views/templates/{ssh → script}/puppet_run_once.erb +2 -2
  32. data/app/views/templates/{ssh → script}/run_command.erb +2 -2
  33. data/app/views/templates/{ssh → script}/service_action.erb +2 -2
  34. data/db/migrate/20220321101835_rename_ssh_provider_to_script.rb +29 -0
  35. data/db/migrate/20220331112719_add_ssh_user_to_job_invocation.rb +5 -0
  36. data/db/seeds.d/60-ssh_proxy_feature.rb +3 -0
  37. data/jsconfig.json +8 -0
  38. data/lib/foreman_remote_execution/engine.rb +10 -5
  39. data/lib/foreman_remote_execution/version.rb +1 -1
  40. data/locale/action_names.rb +3 -3
  41. data/locale/de/foreman_remote_execution.po +23 -23
  42. data/locale/en/foreman_remote_execution.po +23 -23
  43. data/locale/en_GB/foreman_remote_execution.po +23 -23
  44. data/locale/es/foreman_remote_execution.po +23 -23
  45. data/locale/foreman_remote_execution.pot +64 -66
  46. data/locale/fr/foreman_remote_execution.po +23 -23
  47. data/locale/ja/foreman_remote_execution.po +23 -23
  48. data/locale/ko/foreman_remote_execution.po +23 -23
  49. data/locale/pt_BR/foreman_remote_execution.po +23 -23
  50. data/locale/ru/foreman_remote_execution.po +23 -23
  51. data/locale/zh_CN/foreman_remote_execution.po +23 -23
  52. data/locale/zh_TW/foreman_remote_execution.po +23 -23
  53. data/package.json +6 -7
  54. data/test/unit/concerns/host_extensions_test.rb +2 -1
  55. data/test/unit/remote_execution_provider_test.rb +2 -0
  56. data/webpack/JobWizard/JobWizard.js +8 -2
  57. data/webpack/JobWizard/JobWizardConstants.js +2 -2
  58. data/webpack/JobWizard/__tests__/fixtures.js +8 -4
  59. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +2 -2
  60. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +24 -15
  61. data/webpack/JobWizard/steps/HostsAndInputs/SelectGQL.js +2 -1
  62. data/webpack/JobWizard/steps/HostsAndInputs/SelectedChips.js +1 -1
  63. data/webpack/JobWizard/steps/HostsAndInputs/buildHostQuery.js +1 -1
  64. data/webpack/JobWizard/steps/HostsAndInputs/hostgroups.gql +1 -0
  65. data/webpack/JobWizard/steps/HostsAndInputs/hosts.gql +1 -0
  66. data/webpack/JobWizard/steps/ReviewDetails/index.js +1 -1
  67. data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +15 -15
  68. data/webpack/JobWizard/steps/form/GroupedSelectField.js +7 -1
  69. data/webpack/JobWizard/steps/form/SearchSelect.js +0 -1
  70. data/webpack/__mocks__/foremanReact/common/globalIdHelpers.js +1 -0
  71. data/webpack/global_index.js +2 -4
  72. data/webpack/react_app/components/FeaturesDropdown/actions.js +13 -0
  73. data/webpack/react_app/components/FeaturesDropdown/constant.js +2 -0
  74. data/webpack/react_app/components/FeaturesDropdown/index.js +74 -0
  75. data/webpack/react_app/components/HostKebab/KebabItems.js +27 -0
  76. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +10 -5
  77. data/webpack/react_app/components/RecentJobsCard/RecentJobsTable.js +2 -2
  78. data/webpack/react_app/components/RecentJobsCard/constants.js +1 -0
  79. data/webpack/react_app/components/TargetingHosts/TargetingHostsConsts.js +1 -0
  80. data/webpack/react_app/components/TargetingHosts/TargetingHostsPage.js +8 -3
  81. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +9 -1
  82. data/webpack/react_app/components/TargetingHosts/__tests__/fixtures.js +1 -4
  83. data/webpack/react_app/components/TargetingHosts/index.js +1 -0
  84. data/webpack/react_app/extend/Fills.js +48 -0
  85. metadata +25 -17
  86. data/app/helpers/concerns/foreman_remote_execution/hosts_helper_extensions.rb +0 -62
  87. data/webpack/react_app/extend/fillRecentJobsCard.js +0 -11
  88. 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 Fields'),
22
+ advanced: __('Advanced fields'),
23
23
  schedule: __('Schedule'),
24
- review: __('Review Details'),
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: [{ name: 'host1' }, { name: 'host2' }, { name: 'host3' }],
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 Fields'));
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 Fields'));
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
- jobTemplates.forEach(template => {
21
- if (templatesGroups[template.provider_type]?.options)
22
- templatesGroups[template.provider_type].options.push({
23
- label: template.name,
24
- value: template.id,
25
- });
26
- else
27
- templatesGroups[template.provider_type] = {
28
- options: [{ label: template.name, value: template.id }],
29
- groupLabel: template.provider_type,
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={!!(categoryError || allTemplatesError)}
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.name,
39
+ id: decodeId(node.id),
39
40
  name: node.name,
40
41
  })) || [],
41
42
  },
@@ -15,7 +15,7 @@ const SelectedChip = ({ selected, setSelected, categoryName }) => {
15
15
  {selected.map(({ name, id }, index) => (
16
16
  <Chip
17
17
  key={index}
18
- id={id}
18
+ id={`${categoryName}-${id}`}
19
19
  onClick={() => deleteItem(id)}
20
20
  closeBtnAriaLabel={`Close ${name}`}
21
21
  >
@@ -1,6 +1,6 @@
1
1
  export const buildHostQuery = (selected, search) => {
2
2
  const { hosts, hostCollections, hostGroups } = selected;
3
- const hostsSearch = `(name ^ (${hosts.map(({ id }) => id).join(',')}))`;
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(',')}))`;
@@ -2,6 +2,7 @@ query($search: String!) {
2
2
  hostgroups(first: 100, last: 100, search: $search) {
3
3
  totalCount
4
4
  nodes {
5
+ id
5
6
  name
6
7
  }
7
8
  }
@@ -2,6 +2,7 @@ query($search: String!) {
2
2
  hosts(first: 100, last: 100, search: $search) {
3
3
  totalCount
4
4
  nodes {
5
+ id
5
6
  name
6
7
  }
7
8
  }
@@ -72,7 +72,7 @@ const ReviewDetails = ({
72
72
  };
73
73
  const [isAdvancedShown, setIsAdvancedShown] = useState(false);
74
74
  const detailsFirstHalf = [
75
- { label: __('Job Category'), value: jobCategory },
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 Details').disabled).toBeFalsy();
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 Details').disabled).toBeTruthy();
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 Details').disabled).toBeFalsy();
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 Details').disabled).toBeTruthy();
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 Details').disabled).toBeTruthy();
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 Details').disabled).toBeFalsy();
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 Details').disabled).toBeTruthy();
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 Details').disabled).toBeFalsy();
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 Details').disabled).toBeFalsy();
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 Details').disabled).toBeTruthy();
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 Details').disabled).toBeFalsy();
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 Details').disabled).toBeTruthy();
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 Details').disabled).toBeFalsy();
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 Details').disabled).toBeFalsy();
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 Details').disabled).toBeTruthy();
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([PropTypes.string, PropTypes.number]),
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;
@@ -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
- fillRecentJobsCard();
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,2 @@
1
+ export const REX_FEATURES_API = '/api/remote_execution_features';
2
+ export const NEW_JOB_PAGE = '/job_invocations/new?host_ids%5B%5D';
@@ -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 All Jobs')}
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 Finished Jobs')}
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 Running Jobs')}
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 Scheduled Jobs')}
51
+ {__('View scheduled jobs')}
52
52
  </DropdownItem>,
53
53
  ]}
54
54
  >
55
- <Tabs mountOnEnter activeKey={activeTab} onSelect={handleTabClick}>
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>
@@ -10,3 +10,4 @@ export const JOB_BASE_URL = '/job_invocations?search=host+%3D+';
10
10
  export const JOB_API_URL =
11
11
  '/api/job_invocations?order=start_at+DESC&search=targeted_host_id%3D';
12
12
  export const JOBS_IN_CARD = 3;
13
+ export const RECENT_JOBS_KEY = { key: 'RECENT_JOBS_KEY' };
@@ -1 +1,2 @@
1
1
  export const TARGETING_HOSTS = 'TARGETING_HOSTS';
2
+ export const TARGETING_HOSTS_AUTOCOMPLETE = 'targeting_hosts_search';
@@ -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: 'targeting_hosts_search',
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
  `;
@@ -50,10 +50,7 @@ export const TargetingHostsPageFixtures = {
50
50
  apiStatus: 'RESOLVED',
51
51
  items,
52
52
  totalHosts: 1,
53
- pagination: {
54
- page: 1,
55
- perPage: 20,
56
- },
53
+ page: 1,
57
54
  handlePagination: () => {},
58
55
  },
59
56
  };
@@ -89,6 +89,7 @@ const WrappedTargetingHosts = () => {
89
89
  items={items}
90
90
  totalHosts={totalHosts}
91
91
  handlePagination={handlePagination}
92
+ page={pagination.page}
92
93
  />
93
94
  );
94
95
  };