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.
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
  };