foreman_remote_execution 4.7.0 → 5.0.2

Sign up to get free protection for your applications and to get access to all the features.
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
+ }