foreman_remote_execution 4.7.0 → 5.0.2

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 (112) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +1 -0
  3. data/app/controllers/api/v2/job_invocations_controller.rb +16 -1
  4. data/app/controllers/ui_job_wizard_controller.rb +16 -4
  5. data/app/graphql/mutations/job_invocations/create.rb +43 -0
  6. data/app/graphql/types/job_invocation_input.rb +13 -0
  7. data/app/graphql/types/recurrence_input.rb +8 -0
  8. data/app/graphql/types/scheduling_input.rb +6 -0
  9. data/app/graphql/types/targeting_enum.rb +7 -0
  10. data/app/helpers/concerns/foreman_remote_execution/hosts_helper_extensions.rb +20 -9
  11. data/app/helpers/remote_execution_helper.rb +1 -1
  12. data/app/lib/actions/remote_execution/run_host_job.rb +6 -1
  13. data/app/lib/actions/remote_execution/run_hosts_job.rb +57 -3
  14. data/app/mailers/rex_job_mailer.rb +15 -0
  15. data/app/models/concerns/foreman_remote_execution/host_extensions.rb +12 -0
  16. data/app/models/job_invocation.rb +4 -0
  17. data/app/models/job_invocation_composer.rb +21 -13
  18. data/app/models/remote_execution_provider.rb +18 -2
  19. data/app/models/rex_mail_notification.rb +13 -0
  20. data/app/models/targeting.rb +3 -3
  21. data/app/services/ui_notifications/remote_execution_jobs/base_job_finish.rb +2 -1
  22. data/app/views/dashboard/_latest-jobs.html.erb +21 -0
  23. data/app/views/job_invocations/_preview_hosts_list.html.erb +1 -1
  24. data/app/views/job_invocations/refresh.js.erb +1 -0
  25. data/app/views/rex_job_mailer/job_finished.html.erb +24 -0
  26. data/app/views/rex_job_mailer/job_finished.text.erb +9 -0
  27. data/app/views/template_invocations/show.html.erb +3 -2
  28. data/config/routes.rb +1 -0
  29. data/db/migrate/20210816100932_rex_setting_category_to_dsl.rb +5 -0
  30. data/db/seeds.d/50-notification_blueprints.rb +14 -0
  31. data/db/seeds.d/95-mail_notifications.rb +24 -0
  32. data/foreman_remote_execution.gemspec +1 -1
  33. data/lib/foreman_remote_execution/engine.rb +116 -7
  34. data/lib/foreman_remote_execution/version.rb +1 -1
  35. data/package.json +9 -7
  36. data/test/functional/api/v2/job_invocations_controller_test.rb +20 -0
  37. data/test/functional/cockpit_controller_test.rb +0 -1
  38. data/test/graphql/mutations/job_invocations/create.rb +58 -0
  39. data/test/helpers/remote_execution_helper_test.rb +0 -1
  40. data/test/unit/actions/run_host_job_test.rb +21 -0
  41. data/test/unit/actions/run_hosts_job_test.rb +99 -4
  42. data/test/unit/concerns/host_extensions_test.rb +36 -3
  43. data/test/unit/job_invocation_composer_test.rb +3 -5
  44. data/test/unit/job_invocation_report_template_test.rb +16 -13
  45. data/test/unit/job_template_effective_user_test.rb +0 -4
  46. data/test/unit/remote_execution_provider_test.rb +46 -4
  47. data/test/unit/targeting_test.rb +68 -1
  48. data/webpack/JobWizard/JobWizard.js +142 -28
  49. data/webpack/JobWizard/JobWizard.scss +86 -33
  50. data/webpack/JobWizard/JobWizardConstants.js +44 -0
  51. data/webpack/JobWizard/JobWizardSelectors.js +32 -0
  52. data/webpack/JobWizard/__tests__/fixtures.js +89 -6
  53. data/webpack/JobWizard/__tests__/integration.test.js +29 -22
  54. data/webpack/JobWizard/__tests__/validation.test.js +141 -0
  55. data/webpack/JobWizard/autofill.js +38 -0
  56. data/webpack/JobWizard/index.js +7 -0
  57. data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +23 -9
  58. data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +32 -9
  59. data/webpack/JobWizard/steps/AdvancedFields/Fields.js +48 -1
  60. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +242 -23
  61. data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +82 -0
  62. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +5 -2
  63. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +3 -2
  64. data/webpack/JobWizard/steps/CategoryAndTemplate/index.js +3 -2
  65. data/webpack/JobWizard/steps/HostsAndInputs/HostPreviewModal.js +62 -0
  66. data/webpack/JobWizard/steps/HostsAndInputs/HostSearch.js +54 -0
  67. data/webpack/JobWizard/steps/HostsAndInputs/SelectAPI.js +33 -0
  68. data/webpack/JobWizard/steps/HostsAndInputs/SelectGQL.js +52 -0
  69. data/webpack/JobWizard/steps/HostsAndInputs/SelectedChips.js +100 -0
  70. data/webpack/JobWizard/steps/HostsAndInputs/TemplateInputs.js +23 -0
  71. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/HostsAndInputs.test.js +151 -0
  72. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/TemplateInputs.test.js +53 -0
  73. data/webpack/JobWizard/steps/HostsAndInputs/buildHostQuery.js +18 -0
  74. data/webpack/JobWizard/steps/HostsAndInputs/hostgroups.gql +8 -0
  75. data/webpack/JobWizard/steps/HostsAndInputs/hosts.gql +8 -0
  76. data/webpack/JobWizard/steps/HostsAndInputs/index.js +214 -0
  77. data/webpack/JobWizard/steps/ReviewDetails/index.js +193 -0
  78. data/webpack/JobWizard/steps/Schedule/PurposeField.js +31 -0
  79. data/webpack/JobWizard/steps/Schedule/QueryType.js +46 -43
  80. data/webpack/JobWizard/steps/Schedule/RepeatCron.js +53 -0
  81. data/webpack/JobWizard/steps/Schedule/RepeatDaily.js +37 -0
  82. data/webpack/JobWizard/steps/Schedule/RepeatHour.js +54 -0
  83. data/webpack/JobWizard/steps/Schedule/RepeatMonth.js +46 -0
  84. data/webpack/JobWizard/steps/Schedule/RepeatOn.js +95 -31
  85. data/webpack/JobWizard/steps/Schedule/RepeatWeek.js +70 -0
  86. data/webpack/JobWizard/steps/Schedule/ScheduleType.js +24 -21
  87. data/webpack/JobWizard/steps/Schedule/StartEndDates.js +78 -23
  88. data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +402 -0
  89. data/webpack/JobWizard/steps/Schedule/__tests__/StartEndDates.test.js +20 -10
  90. data/webpack/JobWizard/steps/Schedule/index.js +166 -29
  91. data/webpack/JobWizard/steps/form/DateTimePicker.js +126 -0
  92. data/webpack/JobWizard/steps/form/FormHelpers.js +4 -0
  93. data/webpack/JobWizard/steps/form/Formatter.js +49 -17
  94. data/webpack/JobWizard/steps/form/NumberInput.js +5 -2
  95. data/webpack/JobWizard/steps/form/ResourceSelect.js +29 -0
  96. data/webpack/JobWizard/steps/form/SearchSelect.js +121 -0
  97. data/webpack/JobWizard/steps/form/SelectField.js +14 -3
  98. data/webpack/JobWizard/steps/form/WizardTitle.js +14 -0
  99. data/webpack/JobWizard/steps/form/__tests__/SelectSearch.test.js +33 -0
  100. data/webpack/JobWizard/submit.js +120 -0
  101. data/webpack/JobWizard/validation.js +53 -0
  102. data/webpack/__mocks__/foremanReact/Root/Context/ForemanContext/index.js +2 -0
  103. data/webpack/__mocks__/foremanReact/common/I18n.js +2 -0
  104. data/webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteActions.js +1 -0
  105. data/webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteConstants.js +1 -0
  106. data/webpack/__mocks__/foremanReact/routes/RouterSelector.js +1 -0
  107. data/webpack/helpers.js +1 -0
  108. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +2 -1
  109. metadata +53 -7
  110. data/app/models/setting/remote_execution.rb +0 -88
  111. data/webpack/JobWizard/steps/AdvancedFields/__tests__/DescriptionField.test.js +0 -23
  112. data/webpack/JobWizard/steps/form/__tests__/Formatter.test.js.example +0 -76
@@ -5,6 +5,7 @@ import * as api from 'foremanReact/redux/API';
5
5
  import { JobWizard } from '../../JobWizard';
6
6
  import * as selectors from '../../JobWizardSelectors';
7
7
  import { testSetup, mockApi } from '../../__tests__/fixtures';
8
+ import { WIZARD_TITLES } from '../../JobWizardConstants';
8
9
 
9
10
  const store = testSetup(selectors, api);
10
11
  mockApi(api);
@@ -32,7 +33,7 @@ describe('Category And Template', () => {
32
33
  await act(async () => {
33
34
  await fireEvent.click(screen.getByText('Puppet'));
34
35
  });
35
- fireEvent.click(screen.getAllByText('Category and Template')[0]); // to remove focus
36
+ fireEvent.click(screen.getAllByText(WIZARD_TITLES.categoryAndTemplate)[0]); // to remove focus
36
37
  expect(
37
38
  screen.queryAllByLabelText('Ansible Commands', { selector: 'button' })
38
39
  ).toHaveLength(0);
@@ -47,7 +48,7 @@ describe('Category And Template', () => {
47
48
  await act(async () => {
48
49
  await fireEvent.click(screen.getByText('template2'));
49
50
  });
50
- fireEvent.click(screen.getAllByText('Category and Template')[0]); // to remove focus
51
+ fireEvent.click(screen.getAllByText(WIZARD_TITLES.categoryAndTemplate)[0]); // to remove focus
51
52
  expect(
52
53
  screen.queryAllByDisplayValue('template1', { selector: 'button' })
53
54
  ).toHaveLength(0);
@@ -54,10 +54,11 @@ const ConnectedCategoryAndTemplate = ({
54
54
  search: `job_category="${category}"`,
55
55
  per_page: 'all',
56
56
  }),
57
- handleSuccess: response =>
57
+ handleSuccess: response => {
58
58
  setJobTemplate(
59
59
  Number(filterJobTemplates(response?.data?.results)[0]?.id) || null
60
- ),
60
+ );
61
+ },
61
62
  })
62
63
  );
63
64
  }
@@ -0,0 +1,62 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { useSelector } from 'react-redux';
4
+ import URI from 'urijs';
5
+ import { List, ListItem, Modal, Button } from '@patternfly/react-core';
6
+ import { translate as __, sprintf } from 'foremanReact/common/I18n';
7
+ import { foremanUrl } from 'foremanReact/common/helpers';
8
+ import { selectHosts, selectHostCount } from '../../JobWizardSelectors';
9
+ import { HOSTS_TO_PREVIEW_AMOUNT } from '../../JobWizardConstants';
10
+
11
+ export const HostPreviewModal = ({ isOpen, setIsOpen, searchQuery }) => {
12
+ const hosts = useSelector(selectHosts);
13
+ const hostsCount = useSelector(selectHostCount);
14
+ const url = new URI(foremanUrl('/hosts'));
15
+
16
+ return (
17
+ <Modal
18
+ title={__('Preview Hosts')}
19
+ isOpen={isOpen}
20
+ onClose={() => setIsOpen(false)}
21
+ appendTo={() => document.getElementsByClassName('job-wizard')[0]}
22
+ >
23
+ <List isPlain>
24
+ {hosts.map(host => (
25
+ <ListItem key={host}>
26
+ <Button
27
+ component="a"
28
+ href={foremanUrl(`/hosts/${host}`)}
29
+ variant="link"
30
+ target="_blank"
31
+ rel="noreferrer"
32
+ >
33
+ {host}
34
+ </Button>
35
+ </ListItem>
36
+ ))}
37
+ {hostsCount > HOSTS_TO_PREVIEW_AMOUNT && (
38
+ <ListItem>
39
+ <Button
40
+ component="a"
41
+ href={url.addSearch({ search: searchQuery })}
42
+ variant="link"
43
+ target="_blank"
44
+ rel="noreferrer"
45
+ >
46
+ {sprintf(
47
+ __('...and %s more'),
48
+ hostsCount - HOSTS_TO_PREVIEW_AMOUNT
49
+ )}
50
+ </Button>
51
+ </ListItem>
52
+ )}
53
+ </List>
54
+ </Modal>
55
+ );
56
+ };
57
+
58
+ HostPreviewModal.propTypes = {
59
+ isOpen: PropTypes.bool.isRequired,
60
+ setIsOpen: PropTypes.func.isRequired,
61
+ searchQuery: PropTypes.string.isRequired,
62
+ };
@@ -0,0 +1,54 @@
1
+ import React, { useEffect } from 'react';
2
+ import { useSelector, useDispatch } from 'react-redux';
3
+ import PropTypes from 'prop-types';
4
+ import SearchBar from 'foremanReact/components/SearchBar';
5
+ import { getControllerSearchProps } from 'foremanReact/constants';
6
+ import { getResults } from 'foremanReact/components/AutoComplete/AutoCompleteActions';
7
+ import { TRIGGERS } from 'foremanReact/components/AutoComplete/AutoCompleteConstants';
8
+ import { hostsController, hostQuerySearchID } from '../../JobWizardConstants';
9
+ import { noop } from '../../../helpers';
10
+
11
+ export const HostSearch = ({ value, setValue }) => {
12
+ const searchQuery = useSelector(
13
+ state => state.autocomplete?.hostsSearch?.searchQuery
14
+ );
15
+ useEffect(() => {
16
+ setValue(searchQuery || '');
17
+ }, [setValue, searchQuery]);
18
+ const dispatch = useDispatch();
19
+ const setSearch = newSearchQuery => {
20
+ dispatch(
21
+ getResults({
22
+ url: '/hosts/auto_complete_search',
23
+ searchQuery: newSearchQuery,
24
+ controller: 'hostsController',
25
+ trigger: TRIGGERS.INPUT_CHANGE,
26
+ id: hostQuerySearchID,
27
+ })
28
+ );
29
+ };
30
+
31
+ const props = getControllerSearchProps(hostsController, hostQuerySearchID);
32
+ return (
33
+ <div className="foreman-search-field">
34
+ <SearchBar
35
+ data={{
36
+ ...props,
37
+ autocomplete: {
38
+ id: hostQuerySearchID,
39
+ url: '/hosts/auto_complete_search',
40
+ useKeyShortcuts: true,
41
+ },
42
+ }}
43
+ onSearch={noop}
44
+ initialQuery={value}
45
+ onBookmarkClick={search => setSearch(search)}
46
+ />
47
+ </div>
48
+ );
49
+ };
50
+
51
+ HostSearch.propTypes = {
52
+ value: PropTypes.string.isRequired,
53
+ setValue: PropTypes.func.isRequired,
54
+ };
@@ -0,0 +1,33 @@
1
+ import React from 'react';
2
+ import { useSelector, useDispatch } from 'react-redux';
3
+ import URI from 'urijs';
4
+ import { SelectVariant } from '@patternfly/react-core';
5
+ import { get } from 'foremanReact/redux/API';
6
+ import { selectResponse, selectIsLoading } from '../../JobWizardSelectors';
7
+ import { SearchSelect } from '../form/SearchSelect';
8
+
9
+ export const useNameSearchAPI = (apiKey, url) => {
10
+ const dispatch = useDispatch();
11
+ const uri = new URI(url);
12
+ const onSearch = search =>
13
+ dispatch(
14
+ get({
15
+ key: apiKey,
16
+ url: uri.addSearch({
17
+ search: `name~"${search}"`,
18
+ }),
19
+ })
20
+ );
21
+
22
+ const response = useSelector(state => selectResponse(state, apiKey));
23
+ const isLoading = useSelector(state => selectIsLoading(state, apiKey));
24
+ return [onSearch, response, isLoading];
25
+ };
26
+
27
+ export const SelectAPI = props => (
28
+ <SearchSelect
29
+ {...props}
30
+ variant={SelectVariant.typeaheadMulti}
31
+ useNameSearch={useNameSearchAPI}
32
+ />
33
+ );
@@ -0,0 +1,52 @@
1
+ import React, { useState } from 'react';
2
+ import { useQuery } from '@apollo/client';
3
+ import { SelectVariant } from '@patternfly/react-core';
4
+ import {
5
+ useForemanOrganization,
6
+ useForemanLocation,
7
+ } from 'foremanReact/Root/Context/ForemanContext';
8
+ import { HOSTS, HOST_GROUPS, dataName } from '../../JobWizardConstants';
9
+ import { SearchSelect } from '../form/SearchSelect';
10
+ import hostsQuery from './hosts.gql';
11
+ import hostgroupsQuery from './hostgroups.gql';
12
+
13
+ export const useNameSearchGQL = apiKey => {
14
+ const org = useForemanOrganization();
15
+ const location = useForemanLocation();
16
+ const [search, setSearch] = useState('');
17
+ const queries = {
18
+ [HOSTS]: hostsQuery,
19
+ [HOST_GROUPS]: hostgroupsQuery,
20
+ };
21
+ const { loading, data } = useQuery(queries[apiKey], {
22
+ variables: {
23
+ search: [
24
+ `name~"${search}"`,
25
+ org ? `organization_id=${org.id}` : null,
26
+ location ? `location_id=${location.id}` : null,
27
+ ]
28
+ .filter(i => i)
29
+ .join(' and '),
30
+ },
31
+ });
32
+ return [
33
+ setSearch,
34
+ {
35
+ subtotal: data?.[dataName[apiKey]]?.totalCount,
36
+ results:
37
+ data?.[dataName[apiKey]]?.nodes.map(node => ({
38
+ id: node.name,
39
+ name: node.name,
40
+ })) || [],
41
+ },
42
+ loading,
43
+ ];
44
+ };
45
+
46
+ export const SelectGQL = props => (
47
+ <SearchSelect
48
+ {...props}
49
+ variant={SelectVariant.typeaheadMulti}
50
+ useNameSearch={useNameSearchGQL}
51
+ />
52
+ );
@@ -0,0 +1,100 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { Chip, ChipGroup, Button } from '@patternfly/react-core';
4
+ import { translate as __ } from 'foremanReact/common/I18n';
5
+ import { hostMethods } from '../../JobWizardConstants';
6
+
7
+ const SelectedChip = ({ selected, setSelected, categoryName }) => {
8
+ const deleteItem = itemToRemove => {
9
+ setSelected(oldSelected =>
10
+ oldSelected.filter(({ id }) => id !== itemToRemove)
11
+ );
12
+ };
13
+ return (
14
+ <ChipGroup className="hosts-chip-group" categoryName={categoryName}>
15
+ {selected.map(({ name, id }, index) => (
16
+ <Chip
17
+ key={index}
18
+ id={id}
19
+ onClick={() => deleteItem(id)}
20
+ closeBtnAriaLabel={`Close ${name}`}
21
+ >
22
+ {name}
23
+ </Chip>
24
+ ))}
25
+ </ChipGroup>
26
+ );
27
+ };
28
+
29
+ export const SelectedChips = ({
30
+ selectedHosts,
31
+ setSelectedHosts,
32
+ selectedHostCollections,
33
+ setSelectedHostCollections,
34
+ selectedHostGroups,
35
+ setSelectedHostGroups,
36
+ hostsSearchQuery,
37
+ clearSearch,
38
+ }) => {
39
+ const clearAll = () => {
40
+ setSelectedHosts(() => []);
41
+ setSelectedHostCollections(() => []);
42
+ setSelectedHostGroups(() => []);
43
+ clearSearch();
44
+ };
45
+ const showClear =
46
+ selectedHosts.length ||
47
+ selectedHostCollections.length ||
48
+ selectedHostGroups.length ||
49
+ hostsSearchQuery;
50
+ return (
51
+ <div className="selected-chips">
52
+ <SelectedChip
53
+ selected={selectedHosts}
54
+ categoryName={hostMethods.hosts}
55
+ setSelected={setSelectedHosts}
56
+ />
57
+ <SelectedChip
58
+ selected={selectedHostCollections}
59
+ categoryName={hostMethods.hostCollections}
60
+ setSelected={setSelectedHostCollections}
61
+ />
62
+ <SelectedChip
63
+ selected={selectedHostGroups}
64
+ categoryName={hostMethods.hostGroups}
65
+ setSelected={setSelectedHostGroups}
66
+ />
67
+ <SelectedChip
68
+ selected={
69
+ hostsSearchQuery
70
+ ? [{ id: hostsSearchQuery, name: hostsSearchQuery }]
71
+ : []
72
+ }
73
+ categoryName={hostMethods.searchQuery}
74
+ setSelected={clearSearch}
75
+ />
76
+ {showClear && (
77
+ <Button variant="link" className="clear-chips" onClick={clearAll}>
78
+ {__('Clear filters')}
79
+ </Button>
80
+ )}
81
+ </div>
82
+ );
83
+ };
84
+
85
+ SelectedChips.propTypes = {
86
+ selectedHosts: PropTypes.array.isRequired,
87
+ setSelectedHosts: PropTypes.func.isRequired,
88
+ selectedHostCollections: PropTypes.array.isRequired,
89
+ setSelectedHostCollections: PropTypes.func.isRequired,
90
+ selectedHostGroups: PropTypes.array.isRequired,
91
+ setSelectedHostGroups: PropTypes.func.isRequired,
92
+ hostsSearchQuery: PropTypes.string.isRequired,
93
+ clearSearch: PropTypes.func.isRequired,
94
+ };
95
+
96
+ SelectedChip.propTypes = {
97
+ categoryName: PropTypes.string.isRequired,
98
+ selected: PropTypes.array.isRequired,
99
+ setSelected: PropTypes.func.isRequired,
100
+ };
@@ -0,0 +1,23 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { translate as __ } from 'foremanReact/common/I18n';
4
+ import { formatter } from '../form/Formatter';
5
+
6
+ export const TemplateInputs = ({ inputs, value, setValue }) => {
7
+ if (inputs.length)
8
+ return inputs.map(input => formatter(input, value, setValue));
9
+ return (
10
+ <p className="gray-text">
11
+ {__('There are no available input fields for the selected template.')}
12
+ </p>
13
+ );
14
+ };
15
+ TemplateInputs.propTypes = {
16
+ inputs: PropTypes.array.isRequired,
17
+ value: PropTypes.object,
18
+ setValue: PropTypes.func.isRequired,
19
+ };
20
+
21
+ TemplateInputs.defaultProps = {
22
+ value: {},
23
+ };
@@ -0,0 +1,151 @@
1
+ import React from 'react';
2
+ import { Provider } from 'react-redux';
3
+ import { fireEvent, screen, render, act } from '@testing-library/react';
4
+ import { MockedProvider } from '@apollo/client/testing';
5
+ import * as api from 'foremanReact/redux/API';
6
+ import * as routerSelectors from 'foremanReact/routes/RouterSelector';
7
+ import { JobWizard } from '../../../JobWizard';
8
+ import * as selectors from '../../../JobWizardSelectors';
9
+ import { testSetup, mockApi, gqlMock } from '../../../__tests__/fixtures';
10
+
11
+ const store = testSetup(selectors, api);
12
+ mockApi(api);
13
+ const lodash = require('lodash');
14
+
15
+ lodash.debounce = fn => fn;
16
+
17
+ describe('Hosts', () => {
18
+ it('Host selection chips removal and keep state between steps', async () => {
19
+ render(
20
+ <MockedProvider mocks={gqlMock} addTypename={false}>
21
+ <Provider store={store}>
22
+ <JobWizard />
23
+ </Provider>
24
+ </MockedProvider>
25
+ );
26
+ await act(async () => {
27
+ fireEvent.click(screen.getByText('Target hosts and inputs'));
28
+ await new Promise(resolve => setTimeout(resolve, 0)); // to resolve gql
29
+ });
30
+ const select = name =>
31
+ screen.getByRole('button', { name: `${name} toggle` });
32
+ fireEvent.click(select('hosts'));
33
+ await act(async () => {
34
+ fireEvent.click(screen.getByText('host1'));
35
+ fireEvent.click(screen.getByText('host2'));
36
+ });
37
+ fireEvent.click(
38
+ screen.getByText('Hosts', { selector: '.pf-c-select__toggle-text' })
39
+ );
40
+ await act(async () => {
41
+ fireEvent.click(screen.getByText('Host groups'));
42
+ });
43
+ fireEvent.click(select('host groups'));
44
+ await act(async () => {
45
+ fireEvent.click(screen.getByText('host_group1'));
46
+ });
47
+
48
+ fireEvent.click(
49
+ screen.getByText('Host groups', { selector: '.pf-c-select__toggle-text' })
50
+ );
51
+ await act(async () => {
52
+ fireEvent.click(screen.getByText('Host collections'));
53
+ });
54
+ fireEvent.click(select('host collections'));
55
+ await act(async () => {
56
+ fireEvent.click(screen.getByText('host_collection1'));
57
+ });
58
+
59
+ expect(screen.queryAllByText('host1')).toHaveLength(1);
60
+ expect(screen.queryAllByText('host2')).toHaveLength(1);
61
+ expect(screen.queryAllByText('host3')).toHaveLength(0);
62
+ const chip1 = screen.getByRole('button', { name: 'Close host1 host1' });
63
+ await act(async () => {
64
+ fireEvent.click(chip1);
65
+ });
66
+ expect(screen.queryAllByText('host1')).toHaveLength(0);
67
+ expect(screen.queryAllByText('host3')).toHaveLength(0);
68
+ expect(screen.queryAllByText('host2')).toHaveLength(1);
69
+ expect(screen.queryAllByText('host_group1')).toHaveLength(1);
70
+ expect(screen.queryAllByText('host_collection1')).toHaveLength(1);
71
+
72
+ await act(async () => {
73
+ fireEvent.click(screen.getByText('Category and Template'));
74
+ });
75
+ await act(async () => {
76
+ fireEvent.click(screen.getByText('Target hosts and inputs'));
77
+ });
78
+ expect(screen.queryAllByText('host2')).toHaveLength(1);
79
+ expect(screen.queryAllByText('host_group1')).toHaveLength(1);
80
+
81
+ await act(async () => {
82
+ fireEvent.click(screen.getByText('Clear filters'));
83
+ });
84
+
85
+ expect(screen.queryAllByText('host2')).toHaveLength(0);
86
+ expect(screen.queryAllByText('host_group1')).toHaveLength(0);
87
+ });
88
+ it('Host Collection isnt shown without katello', async () => {
89
+ selectors.selectWithKatello.mockImplementation(() => false);
90
+ render(
91
+ <MockedProvider mocks={gqlMock} addTypename={false}>
92
+ <Provider store={store}>
93
+ <JobWizard />
94
+ </Provider>
95
+ </MockedProvider>
96
+ );
97
+ await act(async () => {
98
+ fireEvent.click(screen.getByText('Target hosts and inputs'));
99
+ });
100
+
101
+ expect(screen.queryAllByText('Hosts')).toHaveLength(1);
102
+ await act(async () => {
103
+ fireEvent.click(
104
+ screen.getByText('Hosts', { selector: '.pf-c-select__toggle-text' })
105
+ );
106
+ });
107
+ expect(screen.queryAllByText('Host groups')).toHaveLength(1);
108
+ expect(screen.queryAllByText('Search query')).toHaveLength(1);
109
+ expect(screen.queryAllByText('Host collections')).toHaveLength(0);
110
+ });
111
+ it('Host fill list from url', async () => {
112
+ routerSelectors.selectRouterLocation.mockImplementation(() => ({
113
+ search: '?host_ids%5B%5D=host1&host_ids%5B%5D=host3',
114
+ }));
115
+ render(
116
+ <MockedProvider mocks={gqlMock} addTypename={false}>
117
+ <Provider store={store}>
118
+ <JobWizard />
119
+ </Provider>
120
+ </MockedProvider>
121
+ );
122
+ await act(async () => {
123
+ fireEvent.click(screen.getByText('Target hosts and inputs'));
124
+ });
125
+ api.get.mock.calls.forEach(call => {
126
+ if (call[0].key === 'HOST_IDS') {
127
+ expect(call[0].params).toEqual({ search: 'id = host1 or id = host3' });
128
+ }
129
+ });
130
+
131
+ expect(screen.queryAllByText('host1')).toHaveLength(1);
132
+ expect(screen.queryAllByText('host2')).toHaveLength(0);
133
+ expect(screen.queryAllByText('host3')).toHaveLength(1);
134
+ });
135
+ it('Host fill search from url', async () => {
136
+ routerSelectors.selectRouterLocation.mockImplementation(() => ({
137
+ search: 'search=os=gnome',
138
+ }));
139
+ render(
140
+ <MockedProvider mocks={gqlMock} addTypename={false}>
141
+ <Provider store={store}>
142
+ <JobWizard />
143
+ </Provider>
144
+ </MockedProvider>
145
+ );
146
+ await act(async () => {
147
+ fireEvent.click(screen.getByText('Target hosts and inputs'));
148
+ });
149
+ expect(screen.queryAllByText('os=gnome')).toHaveLength(1);
150
+ });
151
+ });
@@ -0,0 +1,53 @@
1
+ import React from 'react';
2
+ import { Provider } from 'react-redux';
3
+ import { fireEvent, screen, render, act } from '@testing-library/react';
4
+ import { MockedProvider } from '@apollo/client/testing';
5
+ import * as api from 'foremanReact/redux/API';
6
+ import { JobWizard } from '../../../JobWizard';
7
+ import * as selectors from '../../../JobWizardSelectors';
8
+ import { testSetup, mockApi, gqlMock } from '../../../__tests__/fixtures';
9
+ import { WIZARD_TITLES } from '../../../JobWizardConstants';
10
+
11
+ const store = testSetup(selectors, api);
12
+ mockApi(api);
13
+
14
+ describe('TemplateInputs', () => {
15
+ it('should save data between steps for template input fields', async () => {
16
+ render(
17
+ <MockedProvider mocks={gqlMock} addTypename={false}>
18
+ <Provider store={store}>
19
+ <JobWizard />
20
+ </Provider>
21
+ </MockedProvider>
22
+ );
23
+ await act(async () => {
24
+ fireEvent.click(screen.getByText(WIZARD_TITLES.hostsAndInputs));
25
+ });
26
+ const textValue = 'I am a plain text';
27
+ const textField = screen.getByLabelText('plain hidden', {
28
+ selector: 'textarea',
29
+ });
30
+
31
+ await act(async () => {
32
+ await fireEvent.change(textField, {
33
+ target: { value: textValue },
34
+ });
35
+ });
36
+ expect(
37
+ screen.getByLabelText('plain hidden', {
38
+ selector: 'textarea',
39
+ }).value
40
+ ).toBe(textValue);
41
+ await act(async () => {
42
+ fireEvent.click(screen.getByText(WIZARD_TITLES.categoryAndTemplate));
43
+ });
44
+ expect(screen.getAllByText(WIZARD_TITLES.categoryAndTemplate)).toHaveLength(
45
+ 3
46
+ );
47
+
48
+ await act(async () => {
49
+ fireEvent.click(screen.getByText(WIZARD_TITLES.hostsAndInputs));
50
+ });
51
+ expect(textField.value).toBe(textValue);
52
+ });
53
+ });
@@ -0,0 +1,18 @@
1
+ export const buildHostQuery = (selected, search) => {
2
+ const { hosts, hostCollections, hostGroups } = selected;
3
+ const hostsSearch = `(name ^ (${hosts.map(({ id }) => id).join(',')}))`;
4
+ const hostCollectionsSearch = `(host_collection_id ^ (${hostCollections
5
+ .map(({ id }) => id)
6
+ .join(',')}))`;
7
+ const hostGroupsSearch = `(hostgroup_id ^ (${hostGroups
8
+ .map(({ id }) => id)
9
+ .join(',')}))`;
10
+ return [
11
+ hosts.length ? hostsSearch : false,
12
+ hostCollections.length ? hostCollectionsSearch : false,
13
+ hostGroups.length ? hostGroupsSearch : false,
14
+ search.length ? `(${search})` : false,
15
+ ]
16
+ .filter(Boolean)
17
+ .join(' or ');
18
+ };
@@ -0,0 +1,8 @@
1
+ query($search: String!) {
2
+ hostgroups(first: 100, last: 100, search: $search) {
3
+ totalCount
4
+ nodes {
5
+ name
6
+ }
7
+ }
8
+ }
@@ -0,0 +1,8 @@
1
+ query($search: String!) {
2
+ hosts(first: 100, last: 100, search: $search) {
3
+ totalCount
4
+ nodes {
5
+ name
6
+ }
7
+ }
8
+ }