foreman_ansible 6.3.3 → 7.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (152) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/api/v2/ansible_inventories_controller.rb +1 -1
  3. data/app/graphql/mutations/ansible_variable_overrides/create.rb +26 -0
  4. data/app/graphql/mutations/ansible_variable_overrides/delete.rb +38 -0
  5. data/app/graphql/mutations/ansible_variable_overrides/update.rb +26 -0
  6. data/app/graphql/mutations/hosts/assign_ansible_roles.rb +37 -0
  7. data/app/graphql/presenters/ansible_role_presenter.rb +12 -0
  8. data/app/graphql/presenters/overriden_ansible_variable_presenter.rb +19 -0
  9. data/app/graphql/types/ansible_role.rb +9 -0
  10. data/app/graphql/types/ansible_variable.rb +23 -0
  11. data/app/graphql/types/ansible_variable_override.rb +9 -0
  12. data/app/graphql/types/inherited_ansible_role.rb +13 -0
  13. data/app/graphql/types/overriden_ansible_variable.rb +27 -0
  14. data/app/helpers/foreman_ansible/ansible_reports_helper.rb +35 -54
  15. data/app/models/concerns/foreman_ansible/host_managed_extensions.rb +23 -4
  16. data/app/models/concerns/foreman_ansible/hostgroup_extensions.rb +1 -0
  17. data/app/models/foreman_ansible/ansible_provider.rb +56 -6
  18. data/app/services/foreman_ansible/ansible_report_importer.rb +2 -2
  19. data/app/services/foreman_ansible/inventory_creator.rb +1 -1
  20. data/app/services/foreman_ansible/override_resolver.rb +22 -0
  21. data/app/views/api/v2/ansible_override_values/index.json.rabl +3 -0
  22. data/app/views/api/v2/ansible_variables/show.json.rabl +1 -1
  23. data/app/views/foreman_ansible/ansible_roles/_hostgroup_ansible_roles_button.erb +3 -0
  24. data/app/views/foreman_ansible/config_reports/_ansible.html.erb +14 -5
  25. data/app/views/foreman_ansible/job_templates/ansible_roles_-_ansible_default.erb +4 -0
  26. data/app/views/foreman_ansible/job_templates/convert_to_rhel.erb +6 -2
  27. data/app/views/foreman_ansible/job_templates/run_openscap_scans_-_ansible_default.erb +20 -0
  28. data/config/routes.rb +3 -0
  29. data/db/migrate/20210818083407_fix_ansible_setting_category_to_dsl.rb +5 -0
  30. data/lib/foreman_ansible/engine.rb +0 -18
  31. data/lib/foreman_ansible/register.rb +114 -2
  32. data/lib/foreman_ansible/version.rb +1 -1
  33. data/package.json +10 -6
  34. data/test/functional/api/v2/ansible_inventories_controller_test.rb +1 -2
  35. data/test/graphql/mutations/hosts/assign_ansible_roles_mutation_test.rb +96 -0
  36. data/test/graphql/queries/ansible_roles_query_test.rb +35 -0
  37. data/test/unit/ansible_provider_test.rb +3 -6
  38. data/test/unit/concerns/host_managed_extensions_test.rb +8 -0
  39. data/test/unit/concerns/hostgroup_extensions_test.rb +6 -0
  40. data/test/unit/helpers/ansible_reports_helper_test.rb +4 -30
  41. data/test/unit/services/override_resolver_test.rb +34 -0
  42. data/webpack/components/AnsibleHostDetail/AnsibleHostDetail.js +59 -0
  43. data/webpack/components/AnsibleHostDetail/AnsibleHostDetail.scss +6 -0
  44. data/webpack/components/AnsibleHostDetail/AnsibleHostDetail.test.js +20 -0
  45. data/webpack/components/AnsibleHostDetail/components/AnsibleHostInventory/AnsibleHostInventory.js +22 -0
  46. data/webpack/components/AnsibleHostDetail/components/AnsibleHostInventory/AnsibleHostInventory.scss +4 -0
  47. data/webpack/components/AnsibleHostDetail/components/AnsibleHostInventory/AnsibleHostInventory.test.js +104 -0
  48. data/webpack/components/AnsibleHostDetail/components/AnsibleHostInventory/index.js +38 -0
  49. data/webpack/components/AnsibleHostDetail/components/AnsibleVariableOverrides/AnsibleVariableOverrides.scss +3 -0
  50. data/webpack/components/AnsibleHostDetail/components/AnsibleVariableOverrides/AnsibleVariableOverridesTable.js +238 -0
  51. data/webpack/components/AnsibleHostDetail/components/AnsibleVariableOverrides/AnsibleVariableOverridesTableHelper.js +111 -0
  52. data/webpack/components/AnsibleHostDetail/components/AnsibleVariableOverrides/EditableAction.js +161 -0
  53. data/webpack/components/AnsibleHostDetail/components/AnsibleVariableOverrides/EditableAction.scss +7 -0
  54. data/webpack/components/AnsibleHostDetail/components/AnsibleVariableOverrides/EditableActionHelper.js +49 -0
  55. data/webpack/components/AnsibleHostDetail/components/AnsibleVariableOverrides/EditableValue.js +70 -0
  56. data/webpack/components/AnsibleHostDetail/components/AnsibleVariableOverrides/EditableValueHelper.js +35 -0
  57. data/webpack/components/AnsibleHostDetail/components/AnsibleVariableOverrides/__test__/AnsibleVariableOverrides.fixtures.js +429 -0
  58. data/webpack/components/AnsibleHostDetail/components/AnsibleVariableOverrides/__test__/AnsibleVariableOverrides.test.js +71 -0
  59. data/webpack/components/AnsibleHostDetail/components/AnsibleVariableOverrides/__test__/AnsibleVariableOverridesDelete.test.js +74 -0
  60. data/webpack/components/AnsibleHostDetail/components/AnsibleVariableOverrides/__test__/AnsibleVariableOverridesUpdate.test.js +188 -0
  61. data/webpack/components/AnsibleHostDetail/components/AnsibleVariableOverrides/index.js +58 -0
  62. data/webpack/components/AnsibleHostDetail/components/JobsTab/JobsTabHelper.js +79 -0
  63. data/webpack/components/AnsibleHostDetail/components/JobsTab/NewRecurringJobHelper.js +106 -0
  64. data/webpack/components/AnsibleHostDetail/components/JobsTab/NewRecurringJobModal.js +129 -0
  65. data/webpack/components/AnsibleHostDetail/components/JobsTab/NewRecurringJobModal.scss +7 -0
  66. data/webpack/components/AnsibleHostDetail/components/JobsTab/PreviousJobsTable.js +103 -0
  67. data/webpack/components/AnsibleHostDetail/components/JobsTab/RecurringJobsTable.js +96 -0
  68. data/webpack/components/AnsibleHostDetail/components/JobsTab/__test__/JobsTab.fixtures.js +184 -0
  69. data/webpack/components/AnsibleHostDetail/components/JobsTab/__test__/JobsTab.test.js +195 -0
  70. data/webpack/components/AnsibleHostDetail/components/JobsTab/index.js +88 -0
  71. data/webpack/components/AnsibleHostDetail/components/RolesTab/AllRolesModal/AllRolesTable.js +89 -0
  72. data/webpack/components/AnsibleHostDetail/components/RolesTab/AllRolesModal/index.js +80 -0
  73. data/webpack/components/AnsibleHostDetail/components/RolesTab/EditRolesModal/EditRolesForm.js +90 -0
  74. data/webpack/components/AnsibleHostDetail/components/RolesTab/EditRolesModal/EditRolesModal.scss +3 -0
  75. data/webpack/components/AnsibleHostDetail/components/RolesTab/EditRolesModal/EditRolesModalHelper.js +40 -0
  76. data/webpack/components/AnsibleHostDetail/components/RolesTab/EditRolesModal/index.js +82 -0
  77. data/webpack/components/AnsibleHostDetail/components/RolesTab/RolesTable.js +129 -0
  78. data/webpack/components/AnsibleHostDetail/components/RolesTab/__test__/EditRoles.test.js +85 -0
  79. data/webpack/components/AnsibleHostDetail/components/RolesTab/__test__/RolesTab.fixtures.js +180 -0
  80. data/webpack/components/AnsibleHostDetail/components/RolesTab/__test__/RolesTab.test.js +75 -0
  81. data/webpack/components/AnsibleHostDetail/components/RolesTab/index.js +51 -0
  82. data/webpack/components/AnsibleHostDetail/components/SecondaryTabRoutes.js +60 -0
  83. data/webpack/components/AnsibleHostDetail/components/TabLayout.js +12 -0
  84. data/webpack/components/AnsibleHostDetail/constants.js +9 -0
  85. data/webpack/components/AnsibleHostDetail/helpers.js +4 -0
  86. data/webpack/components/AnsibleHostDetail/index.js +6 -0
  87. data/webpack/components/AnsibleRolesAndVariables/__test__/AnsibleRolesAndVariablesImport.test.js +15 -10
  88. data/webpack/components/AnsibleRolesSwitcher/components/AnsibleRole.js +29 -0
  89. data/webpack/components/AnsibleRolesSwitcher/components/AnsibleRole.test.js +3 -0
  90. data/webpack/components/AnsibleRolesSwitcher/components/AvailableRolesList.js +2 -1
  91. data/webpack/components/AnsibleRolesSwitcher/components/__snapshots__/AnsiblePermissionDenied.test.js.snap +2 -0
  92. data/webpack/components/AnsibleRolesSwitcher/components/__snapshots__/AnsibleRole.test.js.snap +3 -3
  93. data/webpack/components/AnsibleRolesSwitcher/components/__snapshots__/AssignedRolesList.test.js.snap +4 -4
  94. data/webpack/components/AnsibleRolesSwitcher/components/__snapshots__/AvailableRolesList.test.js.snap +9 -0
  95. data/webpack/components/DualList/DualList.scss +3 -0
  96. data/webpack/components/DualList/ListControls.js +65 -0
  97. data/webpack/components/DualList/ListHeader.js +16 -0
  98. data/webpack/components/DualList/ListItem.js +69 -0
  99. data/webpack/components/DualList/ListPane.js +95 -0
  100. data/webpack/components/DualList/SelectedStatus.js +21 -0
  101. data/webpack/components/DualList/index.js +103 -0
  102. data/webpack/components/ErrorState.js +16 -0
  103. data/webpack/components/withLoading.js +135 -0
  104. data/webpack/components/withPagination.js +0 -0
  105. data/webpack/formHelper.js +131 -0
  106. data/webpack/globalIdHelper.js +13 -0
  107. data/webpack/global_index.js +18 -0
  108. data/webpack/graphql/mutations/assignAnsibleRoles.gql +17 -0
  109. data/webpack/graphql/mutations/cancelRecurringLogic.gql +12 -0
  110. data/webpack/graphql/mutations/createAnsibleVariableOverride.gql +28 -0
  111. data/webpack/graphql/mutations/createJobInvocation.gql +11 -0
  112. data/webpack/graphql/mutations/deleteAnsibleVariableOverride.gql +17 -0
  113. data/webpack/graphql/mutations/updateAnsibleVariableOverride.gql +29 -0
  114. data/webpack/graphql/queries/allAnsibleRoles.gql +13 -0
  115. data/webpack/graphql/queries/ansibleRoles.gql +13 -0
  116. data/webpack/graphql/queries/currentUserAttributes.gql +11 -0
  117. data/webpack/graphql/queries/hostAnsibleRoles.gql +17 -0
  118. data/webpack/graphql/queries/hostAvailableAnsibleRoles.gql +11 -0
  119. data/webpack/graphql/queries/hostVariableOverrides.gql +39 -0
  120. data/webpack/graphql/queries/recurringJobs.gql +28 -0
  121. data/webpack/helpers/pageParamsHelper.js +40 -0
  122. data/webpack/helpers/paginationHelper.js +9 -0
  123. data/webpack/permissionsHelper.js +58 -0
  124. data/webpack/routes/HostgroupJobs/__test__/HostgroupJobs.fixtures.js +63 -0
  125. data/webpack/routes/HostgroupJobs/__test__/HostgroupJobs.test.js +112 -0
  126. data/webpack/routes/HostgroupJobs/index.js +26 -0
  127. data/webpack/routes/routes.js +10 -0
  128. data/webpack/testHelper.js +165 -0
  129. data/webpack/toastHelper.js +4 -0
  130. metadata +130 -78
  131. data/app/assets/images/foreman_ansible/Ansible.png +0 -0
  132. data/app/models/foreman_ansible/fact_name.rb +0 -16
  133. data/app/models/setting/ansible.rb +0 -106
  134. data/app/services/foreman_ansible/fact_importer.rb +0 -99
  135. data/app/services/foreman_ansible/fact_parser.rb +0 -126
  136. data/app/services/foreman_ansible/fact_sparser.rb +0 -37
  137. data/app/services/foreman_ansible/operating_system_parser.rb +0 -102
  138. data/app/services/foreman_ansible/structured_fact_importer.rb +0 -25
  139. data/test/unit/lib/foreman_ansible_core/ansible_runner_test.rb +0 -51
  140. data/test/unit/lib/foreman_ansible_core/command_creator_test.rb +0 -64
  141. data/test/unit/lib/foreman_ansible_core/playbook_runner_test.rb +0 -110
  142. data/test/unit/services/fact_importer_test.rb +0 -52
  143. data/test/unit/services/fact_parser_test.rb +0 -281
  144. data/test/unit/services/fact_sparser_test.rb +0 -24
  145. data/test/unit/services/structured_fact_importer_test.rb +0 -30
  146. data/webpack/__mocks__/foremanReact/common/I18n.js +0 -1
  147. data/webpack/__mocks__/foremanReact/common/helpers.js +0 -13
  148. data/webpack/__mocks__/foremanReact/components/Pagination/PaginationWrapper.js +0 -2
  149. data/webpack/__mocks__/foremanReact/components/common/EmptyState.js +0 -5
  150. data/webpack/__mocks__/foremanReact/components/common/forms/OrderableSelect/helpers.js +0 -5
  151. data/webpack/__mocks__/foremanReact/redux/API.js +0 -7
  152. data/webpack/components/AnsibleRolesAndVariables/__test__/__snapshots__/AnsibleRolesAndVariablesImport.test.js.snap +0 -177
@@ -0,0 +1,80 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { useQuery } from '@apollo/client';
4
+ import { translate as __ } from 'foremanReact/common/I18n';
5
+
6
+ import { Modal, Button, ModalVariant } from '@patternfly/react-core';
7
+
8
+ import allAnsibleRolesQuery from '../../../../../graphql/queries/allAnsibleRoles.gql';
9
+ import AllRolesTable from './AllRolesTable';
10
+
11
+ import {
12
+ useParamsToVars,
13
+ useCurrentPagination,
14
+ } from '../../../../../helpers/pageParamsHelper';
15
+
16
+ const AllRolesModal = ({ hostGlobalId, onClose, history }) => {
17
+ const baseModalProps = {
18
+ variant: ModalVariant.large,
19
+ isOpen: true,
20
+ className: 'foreman-modal',
21
+ showClose: false,
22
+ title: __('All Ansible Roles'),
23
+ disableFocusTrap: true,
24
+ };
25
+
26
+ const paginationKeys = { page: 'allPage', perPage: 'allPerPage' };
27
+
28
+ const actions = [
29
+ <Button variant="link" onClick={onClose} key="close">
30
+ {__('Close')}
31
+ </Button>,
32
+ ];
33
+
34
+ const wrapper = child => (
35
+ <Modal {...baseModalProps} actions={actions}>
36
+ {child}
37
+ </Modal>
38
+ );
39
+
40
+ const loadingWrapper = child => <Modal {...baseModalProps}>{child}</Modal>;
41
+
42
+ const useFetchFn = () =>
43
+ useQuery(allAnsibleRolesQuery, {
44
+ variables: {
45
+ id: hostGlobalId,
46
+ ...useParamsToVars(history, paginationKeys),
47
+ },
48
+ fetchPolicy: 'network-only',
49
+ });
50
+
51
+ const renameData = data => ({
52
+ allAnsibleRoles: data.host.allAnsibleRoles.nodes,
53
+ totalCount: data.host.allAnsibleRoles.totalCount,
54
+ });
55
+
56
+ const pagination = useCurrentPagination(history, paginationKeys);
57
+
58
+ return (
59
+ <AllRolesTable
60
+ wrapper={wrapper}
61
+ loadingWrapper={loadingWrapper}
62
+ emptyWrapper={loadingWrapper}
63
+ fetchFn={useFetchFn}
64
+ renameData={renameData}
65
+ renamedDataPath="allAnsibleRoles"
66
+ hostGlobalId={hostGlobalId}
67
+ emptyStateTitle={__('No Ansible roles assigned')}
68
+ history={history}
69
+ pagination={pagination}
70
+ />
71
+ );
72
+ };
73
+
74
+ AllRolesModal.propTypes = {
75
+ hostGlobalId: PropTypes.string.isRequired,
76
+ onClose: PropTypes.func.isRequired,
77
+ history: PropTypes.object.isRequired,
78
+ };
79
+
80
+ export default AllRolesModal;
@@ -0,0 +1,90 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { translate as __ } from 'foremanReact/common/I18n';
3
+ import PropTypes from 'prop-types';
4
+
5
+ import { useMutation } from '@apollo/client';
6
+
7
+ import { Button, Modal, Spinner } from '@patternfly/react-core';
8
+ import { encodeId } from '../../../../../globalIdHelper';
9
+ import assignAnsibleRoles from '../../../../../graphql/mutations/assignAnsibleRoles.gql';
10
+ import withLoading from '../../../../withLoading';
11
+ import { onCompleted, onError, roleNamesToIds } from './EditRolesModalHelper';
12
+ import DualList from '../../../../DualList';
13
+
14
+ const EditRolesForm = props => {
15
+ const {
16
+ assignedRoles,
17
+ availableRoles,
18
+ closeModal,
19
+ hostId,
20
+ baseModalProps,
21
+ actions,
22
+ } = props;
23
+
24
+ const [formState, setFormState] = useState({
25
+ availableOptions: [],
26
+ chosenOptions: [],
27
+ });
28
+
29
+ useEffect(() => {
30
+ setFormState({
31
+ availableOptions: availableRoles.map(item => item.name),
32
+ chosenOptions: assignedRoles.map(item => item.name) || [],
33
+ });
34
+ }, [availableRoles, assignedRoles]);
35
+
36
+ const [callMutation, { loading }] = useMutation(assignAnsibleRoles, {
37
+ onCompleted: onCompleted(closeModal),
38
+ onError,
39
+ });
40
+
41
+ const allRoles = (availableRoles || []).concat(assignedRoles || []);
42
+
43
+ const variables = {
44
+ id: encodeId('Host', hostId),
45
+ ansibleRoleIds: roleNamesToIds(allRoles, formState.chosenOptions),
46
+ };
47
+
48
+ const formActions = [
49
+ <Button
50
+ key="confirm"
51
+ variant="primary"
52
+ onClick={() => callMutation({ variables })}
53
+ isDisabled={loading}
54
+ aria-label="submit ansible roles"
55
+ >
56
+ {__('Confirm')}
57
+ </Button>,
58
+ ...actions,
59
+ ];
60
+
61
+ if (loading) {
62
+ formActions.push(<Spinner key="spinner" size="lg" />);
63
+ }
64
+
65
+ return (
66
+ <Modal {...baseModalProps} actions={formActions}>
67
+ <DualList
68
+ availableOptions={formState.availableOptions}
69
+ chosenOptions={formState.chosenOptions}
70
+ onListChange={(newAvailable, newChosen) =>
71
+ setFormState({
72
+ availableOptions: newAvailable,
73
+ chosenOptions: newChosen,
74
+ })
75
+ }
76
+ />
77
+ </Modal>
78
+ );
79
+ };
80
+
81
+ EditRolesForm.propTypes = {
82
+ closeModal: PropTypes.func.isRequired,
83
+ assignedRoles: PropTypes.array.isRequired,
84
+ availableRoles: PropTypes.array.isRequired,
85
+ actions: PropTypes.array.isRequired,
86
+ hostId: PropTypes.number.isRequired,
87
+ baseModalProps: PropTypes.object.isRequired,
88
+ };
89
+
90
+ export default withLoading(EditRolesForm);
@@ -0,0 +1,40 @@
1
+ import { sprintf, translate as __ } from 'foremanReact/common/I18n';
2
+ import { decodeModelId } from '../../../../../globalIdHelper';
3
+ import { showToast } from '../../../../../toastHelper';
4
+
5
+ export const roleNamesToIds = (roles, names) =>
6
+ names.reduce((memo, name) => {
7
+ const role = roles.find(item => item.name === name);
8
+ if (role) {
9
+ memo.push(decodeModelId(role));
10
+ }
11
+ return memo;
12
+ }, []);
13
+
14
+ const joinErrors = errors => errors.map(err => err.message).join(', ');
15
+
16
+ const formatError = error =>
17
+ sprintf(
18
+ __('There was a following error when assigning Ansible Roles: %s'),
19
+ error
20
+ );
21
+
22
+ export const onCompleted = closeModal => data => {
23
+ const { errors } = data.assignAnsibleRoles;
24
+ if (Array.isArray(errors) && errors.length > 0) {
25
+ showToast({
26
+ type: 'error',
27
+ message: formatError(joinErrors(errors)),
28
+ });
29
+ } else {
30
+ closeModal();
31
+ showToast({
32
+ type: 'success',
33
+ message: __('Ansible Roles were successfully assigned.'),
34
+ });
35
+ }
36
+ };
37
+
38
+ export const onError = error => {
39
+ showToast({ type: 'error', message: formatError(error) });
40
+ };
@@ -0,0 +1,82 @@
1
+ import React from 'react';
2
+ import { translate as __ } from 'foremanReact/common/I18n';
3
+ import PropTypes from 'prop-types';
4
+
5
+ import { Modal, Button, ModalVariant } from '@patternfly/react-core';
6
+ import { useQuery } from '@apollo/client';
7
+
8
+ import EditRolesForm from './EditRolesForm';
9
+
10
+ import availableAnsibleRoles from '../../../../../graphql/queries/hostAvailableAnsibleRoles.gql';
11
+ import { encodeId } from '../../../../../globalIdHelper';
12
+
13
+ import './EditRolesModal.scss';
14
+
15
+ const EditRolesModal = ({
16
+ assignedRoles,
17
+ isOpen,
18
+ closeModal,
19
+ hostId,
20
+ canEditHost,
21
+ }) => {
22
+ const baseModalProps = {
23
+ variant: ModalVariant.large,
24
+ isOpen,
25
+ className: 'foreman-modal',
26
+ showClose: false,
27
+ title: __('Edit Ansible Roles'),
28
+ disableFocusTrap: true,
29
+ };
30
+
31
+ const actions = [
32
+ <Button variant="link" onClick={event => closeModal()} key="close">
33
+ {__('Close')}
34
+ </Button>,
35
+ ];
36
+
37
+ const emptyWrapper = child => (
38
+ <Modal {...baseModalProps} actions={actions}>
39
+ {child}
40
+ </Modal>
41
+ );
42
+
43
+ const loadingWrapper = child => <Modal {...baseModalProps}>{child}</Modal>;
44
+
45
+ const variables = {
46
+ id: encodeId('Host', hostId),
47
+ };
48
+
49
+ const useFetchFn = () =>
50
+ useQuery(availableAnsibleRoles, { variables, fetchPolicy: 'network-only' });
51
+
52
+ const renameData = data => ({
53
+ availableRoles: data.host.availableAnsibleRoles.nodes,
54
+ });
55
+
56
+ return (
57
+ <EditRolesForm
58
+ emptyWrapper={emptyWrapper}
59
+ loadingWrapper={loadingWrapper}
60
+ actions={actions}
61
+ baseModalProps={baseModalProps}
62
+ fetchFn={useFetchFn}
63
+ renameData={renameData}
64
+ renamedDataPath="availableRoles"
65
+ assignedRoles={assignedRoles}
66
+ closeModal={closeModal}
67
+ hostId={hostId}
68
+ allowed={canEditHost}
69
+ requiredPermissions={['edit_hosts']}
70
+ />
71
+ );
72
+ };
73
+
74
+ EditRolesModal.propTypes = {
75
+ assignedRoles: PropTypes.array.isRequired,
76
+ isOpen: PropTypes.bool.isRequired,
77
+ closeModal: PropTypes.func.isRequired,
78
+ hostId: PropTypes.number.isRequired,
79
+ canEditHost: PropTypes.bool.isRequired,
80
+ };
81
+
82
+ export default EditRolesModal;
@@ -0,0 +1,129 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { translate as __ } from 'foremanReact/common/I18n';
4
+ import { Route, Link } from 'react-router-dom';
5
+ import { usePaginationOptions } from 'foremanReact/components/Pagination/PaginationHooks';
6
+
7
+ import {
8
+ TableComposable,
9
+ Thead,
10
+ Tbody,
11
+ Tr,
12
+ Th,
13
+ Td,
14
+ } from '@patternfly/react-table';
15
+ import { Flex, FlexItem, Button, Pagination } from '@patternfly/react-core';
16
+
17
+ import EditRolesModal from './EditRolesModal';
18
+
19
+ import withLoading from '../../../withLoading';
20
+ import AllRolesModal from './AllRolesModal';
21
+ import {
22
+ preparePerPageOptions,
23
+ refreshPage,
24
+ } from '../../../../helpers/paginationHelper';
25
+
26
+ const RolesTable = ({
27
+ totalCount,
28
+ pagination,
29
+ history,
30
+ ansibleRoles,
31
+ hostId,
32
+ hostGlobalId,
33
+ canEditHost,
34
+ }) => {
35
+ const columns = [__('Name')];
36
+
37
+ const handlePerPageSelected = (event, perPage) => {
38
+ refreshPage(history, { page: 1, perPage });
39
+ };
40
+
41
+ const handlePageSelected = (event, page) => {
42
+ refreshPage(history, { ...pagination, page });
43
+ };
44
+
45
+ const perPageOptions = preparePerPageOptions(usePaginationOptions());
46
+
47
+ const editBtn = canEditHost ? (
48
+ <FlexItem>
49
+ <Link to="/Ansible/roles/edit">
50
+ <Button aria-label="edit ansible roles">
51
+ {__('Edit Ansible Roles')}
52
+ </Button>
53
+ </Link>
54
+ </FlexItem>
55
+ ) : null;
56
+
57
+ return (
58
+ <React.Fragment>
59
+ <Flex>
60
+ <FlexItem>
61
+ <h3>
62
+ <span>{__('Ansible roles assigned directly to host')}</span>
63
+ <span>{' - '}</span>
64
+ <Link to="/Ansible/roles/all">{__('view all assigned roles')}</Link>
65
+ </h3>
66
+ </FlexItem>
67
+ </Flex>
68
+ <Flex>
69
+ <FlexItem>{editBtn}</FlexItem>
70
+ <FlexItem align={{ default: 'alignRight' }}>
71
+ <Pagination
72
+ itemCount={totalCount}
73
+ page={pagination.page}
74
+ perPage={pagination.perPage}
75
+ onSetPage={handlePageSelected}
76
+ onPerPageSelect={handlePerPageSelected}
77
+ perPageOptions={perPageOptions}
78
+ variant="top"
79
+ />
80
+ </FlexItem>
81
+ </Flex>
82
+ <TableComposable variant="compact">
83
+ <Thead>
84
+ <Tr>
85
+ {columns.map(col => (
86
+ <Th key={col}>{col}</Th>
87
+ ))}
88
+ </Tr>
89
+ </Thead>
90
+ <Tbody>
91
+ {ansibleRoles.map(role => (
92
+ <Tr key={role.id}>
93
+ <Td>{role.name}</Td>
94
+ </Tr>
95
+ ))}
96
+ </Tbody>
97
+ </TableComposable>
98
+ <Route path="/Ansible/roles/edit">
99
+ <EditRolesModal
100
+ closeModal={() => history.goBack()}
101
+ isOpen
102
+ assignedRoles={ansibleRoles}
103
+ hostId={hostId}
104
+ canEditHost={canEditHost}
105
+ />
106
+ </Route>
107
+ <Route path="/Ansible/roles/all">
108
+ <AllRolesModal
109
+ onClose={() => history.push('/Ansible/roles')}
110
+ isOpen
111
+ hostGlobalId={hostGlobalId}
112
+ history={history}
113
+ />
114
+ </Route>
115
+ </React.Fragment>
116
+ );
117
+ };
118
+
119
+ RolesTable.propTypes = {
120
+ ansibleRoles: PropTypes.array.isRequired,
121
+ hostId: PropTypes.number.isRequired,
122
+ hostGlobalId: PropTypes.string.isRequired,
123
+ history: PropTypes.object.isRequired,
124
+ pagination: PropTypes.object.isRequired,
125
+ totalCount: PropTypes.number.isRequired,
126
+ canEditHost: PropTypes.bool.isRequired,
127
+ };
128
+
129
+ export default withLoading(RolesTable);
@@ -0,0 +1,85 @@
1
+ import React from 'react';
2
+ import { render, screen, waitFor } from '@testing-library/react';
3
+ import '@testing-library/jest-dom';
4
+ import userEvent from '@testing-library/user-event';
5
+
6
+ import RolesTab from '../';
7
+
8
+ import * as toasts from '../../../../../toastHelper';
9
+
10
+ import {
11
+ tick,
12
+ withMockedProvider,
13
+ withRedux,
14
+ withReactRouter,
15
+ } from '../../../../../testHelper';
16
+ import {
17
+ mocks,
18
+ hostId,
19
+ editModalOpenMocks,
20
+ assignRolesSuccessMock,
21
+ assignRolesErrorMock,
22
+ } from './RolesTab.fixtures';
23
+
24
+ const TestComponent = withReactRouter(withRedux(withMockedProvider(RolesTab)));
25
+
26
+ describe('assigning Ansible roles', () => {
27
+ it('should assign Ansible roles', async () => {
28
+ const showToast = jest.fn();
29
+ jest.spyOn(toasts, 'showToast').mockImplementation(showToast);
30
+
31
+ render(
32
+ <TestComponent
33
+ hostId={hostId}
34
+ mocks={mocks.concat(editModalOpenMocks).concat(assignRolesSuccessMock)}
35
+ canEditHost
36
+ />
37
+ );
38
+ await waitFor(tick);
39
+ userEvent.click(screen.getByRole('button', { name: 'edit ansible roles' }));
40
+ await waitFor(tick);
41
+ await waitFor(tick);
42
+ expect(screen.getByText('Available options')).toBeInTheDocument();
43
+ userEvent.click(screen.getAllByText('another.role')[1]);
44
+ userEvent.click(screen.getByRole('button', { name: 'Remove selected' }));
45
+ userEvent.click(screen.getByText('geerlingguy.ceylon'));
46
+ userEvent.click(screen.getByRole('button', { name: 'Add selected' }));
47
+ userEvent.click(
48
+ screen.getByRole('button', { name: 'submit ansible roles' })
49
+ );
50
+ await waitFor(tick);
51
+ expect(showToast).toHaveBeenCalledWith({
52
+ type: 'success',
53
+ message: 'Ansible Roles were successfully assigned.',
54
+ });
55
+ }, 8000);
56
+ it('should show errors', async () => {
57
+ const showToast = jest.fn();
58
+ jest.spyOn(toasts, 'showToast').mockImplementation(showToast);
59
+
60
+ render(
61
+ <TestComponent
62
+ hostId={hostId}
63
+ mocks={mocks.concat(editModalOpenMocks).concat(assignRolesErrorMock)}
64
+ canEditHost
65
+ />
66
+ );
67
+ await waitFor(tick);
68
+ userEvent.click(screen.getByRole('button', { name: 'edit ansible roles' }));
69
+ await waitFor(tick);
70
+ expect(screen.getByText('Available options')).toBeInTheDocument();
71
+ userEvent.click(screen.getAllByText('another.role')[1]);
72
+ userEvent.click(screen.getByRole('button', { name: 'Remove selected' }));
73
+ userEvent.click(screen.getByText('geerlingguy.ceylon'));
74
+ userEvent.click(screen.getByRole('button', { name: 'Add selected' }));
75
+ userEvent.click(
76
+ screen.getByRole('button', { name: 'submit ansible roles' })
77
+ );
78
+ await waitFor(tick);
79
+ expect(showToast).toHaveBeenCalledWith({
80
+ type: 'error',
81
+ message:
82
+ 'There was a following error when assigning Ansible Roles: is invalid',
83
+ });
84
+ }, 8000);
85
+ });
@@ -0,0 +1,180 @@
1
+ import {
2
+ mockFactory,
3
+ advancedMockFactory,
4
+ admin,
5
+ intruder,
6
+ userFactory,
7
+ } from '../../../../../testHelper';
8
+ import ansibleRolesQuery from '../../../../../graphql/queries/hostAnsibleRoles.gql';
9
+ import allAnsibleRolesQuery from '../../../../../graphql/queries/allAnsibleRoles.gql';
10
+ import availableAnsibleRolesQuery from '../../../../../graphql/queries/hostAvailableAnsibleRoles.gql';
11
+ import assignAnsibleRolesMutation from '../../../../../graphql/mutations/assignAnsibleRoles.gql';
12
+ import { decodeModelId } from '../../../../../globalIdHelper';
13
+
14
+ export const hostId = 3;
15
+ const hostGlobalId = 'MDE6SG9zdC0z';
16
+
17
+ const ansibleRolesMockFactory = mockFactory('host', ansibleRolesQuery);
18
+ const allAnsibleRolesMockFactory = mockFactory('host', allAnsibleRolesQuery);
19
+ const assignRolesMockFactory = mockFactory(
20
+ 'assignAnsibleRoles',
21
+ assignAnsibleRolesMutation
22
+ );
23
+ const editModalDataFactory = advancedMockFactory(availableAnsibleRolesQuery);
24
+
25
+ const viewer = userFactory('roles_viewer', [
26
+ {
27
+ __typename: 'Permission',
28
+ id: 'MDE6UGVybWlzc2lvbi0x',
29
+ name: 'view_ansible_roles',
30
+ },
31
+ ]);
32
+
33
+ const role1 = {
34
+ __typename: 'AnsibleRole',
35
+ id: 'MDE6QW5zaWJsZVJvbGUtMw==',
36
+ name: 'aardvaark.cube',
37
+ };
38
+
39
+ const role2 = {
40
+ __typename: 'AnsibleRole',
41
+ id: 'MDE6QW5zaWJsZVJvbGUtNQ==',
42
+ name: 'aardvaark.sphere',
43
+ };
44
+
45
+ const role3 = {
46
+ __typename: 'AnsibleRole',
47
+ id: 'MDE6QW5zaWJsZVJvbGUtMzA=',
48
+ name: 'another.role',
49
+ };
50
+
51
+ const role4 = {
52
+ __typename: 'AnsibleRole',
53
+ id: 'MDE6QW5zaWJsZVJvbGUtMzk=',
54
+ name: 'geerlingguy.ceylon',
55
+ };
56
+
57
+ const ansibleRolesMock = {
58
+ totalCount: 3,
59
+ nodes: [role1, role2, role3],
60
+ };
61
+
62
+ const ansibleRolesUpdatedMock = {
63
+ totalCount: 3,
64
+ nodes: [role1, role2, role4],
65
+ };
66
+
67
+ const availableRoles = {
68
+ nodes: [
69
+ role4,
70
+ {
71
+ __typename: 'AnsibleRole',
72
+ id: 'MDE6QW5zaWJsZVJvbGUtMQ==',
73
+ name: 'theforeman.foreman_scap_client',
74
+ },
75
+ {
76
+ __typename: 'AnsibleRole',
77
+ id: 'MDE6QW5zaWJsZVJvbGUtMg==',
78
+ name: 'adriagalin.motd',
79
+ },
80
+ {
81
+ __typename: 'AnsibleRole',
82
+ id: 'MDE6QW5zaWJsZVJvbGUtMjI=',
83
+ name: 'geerlingguy.php',
84
+ },
85
+ {
86
+ __typename: 'AnsibleRole',
87
+ id: 'MDE6QW5zaWJsZVJvbGUtNTc=',
88
+ name: 'robertdebock.epel',
89
+ },
90
+ {
91
+ __typename: 'AnsibleRole',
92
+ id: 'MDE6QW5zaWJsZVJvbGUtNTg=',
93
+ name: 'geerlingguy.nfs',
94
+ },
95
+ ],
96
+ };
97
+
98
+ export const allRolesMocks = allAnsibleRolesMockFactory(
99
+ { id: hostGlobalId, first: 20, last: 20 },
100
+ {
101
+ __typename: 'Host',
102
+ id: hostGlobalId,
103
+ allAnsibleRoles: {
104
+ totalCount: 4,
105
+ nodes: [
106
+ {
107
+ id: 'MDE6QW5zaWJsZVJvbGUtMg==',
108
+ name: 'adriagalin.motd',
109
+ inherited: true,
110
+ },
111
+ { ...role1, inherited: false },
112
+ { ...role2, inherited: false },
113
+ { ...role3, inherited: false },
114
+ ],
115
+ },
116
+ }
117
+ );
118
+
119
+ const editModalData = {
120
+ host: {
121
+ __typename: 'Host',
122
+ id: hostGlobalId,
123
+ availableAnsibleRoles: availableRoles,
124
+ },
125
+ };
126
+
127
+ export const mocks = ansibleRolesMockFactory(
128
+ { id: hostGlobalId, first: 20, last: 20 },
129
+ { __typename: 'Host', id: hostGlobalId, ownAnsibleRoles: ansibleRolesMock },
130
+ { currentUser: admin }
131
+ );
132
+
133
+ export const unauthorizedMocks = ansibleRolesMockFactory(
134
+ { id: hostGlobalId, first: 20, last: 20 },
135
+ { __typename: 'Host', id: hostGlobalId, ownAnsibleRoles: ansibleRolesMock },
136
+ { currentUser: intruder }
137
+ );
138
+
139
+ export const authorizedMocks = ansibleRolesMockFactory(
140
+ { id: hostGlobalId, first: 20, last: 20 },
141
+ { __typename: 'Host', id: hostGlobalId, ownAnsibleRoles: ansibleRolesMock },
142
+ { currentUser: viewer }
143
+ );
144
+
145
+ export const editModalOpenMocks = editModalDataFactory(
146
+ {
147
+ id: hostGlobalId,
148
+ },
149
+ editModalData
150
+ );
151
+
152
+ export const assignRolesSuccessMock = assignRolesMockFactory(
153
+ {
154
+ id: hostGlobalId,
155
+ ansibleRoleIds: [role1, role2, role4].map(decodeModelId),
156
+ },
157
+ {
158
+ host: {
159
+ __typename: 'Host',
160
+ id: hostGlobalId,
161
+ ownAnsibleRoles: ansibleRolesUpdatedMock,
162
+ },
163
+ errors: [],
164
+ }
165
+ );
166
+
167
+ export const assignRolesErrorMock = assignRolesMockFactory(
168
+ {
169
+ id: hostGlobalId,
170
+ ansibleRoleIds: [role1, role2, role4].map(decodeModelId),
171
+ },
172
+ {
173
+ host: {
174
+ __typename: 'Host',
175
+ id: hostGlobalId,
176
+ ownAnsibleRoles: ansibleRolesMock,
177
+ },
178
+ errors: [{ path: ['attributes', 'base'], message: 'is invalid' }],
179
+ }
180
+ );