foreman_ansible 6.4.1 → 7.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (137) 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_roles_data_preparations.rb +22 -22
  15. data/app/models/concerns/foreman_ansible/host_managed_extensions.rb +23 -4
  16. data/app/models/concerns/foreman_ansible/hostgroup_extensions.rb +2 -1
  17. data/app/models/foreman_ansible/ansible_provider.rb +7 -5
  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/job_templates/convert_to_rhel.erb +6 -2
  25. data/app/views/foreman_ansible/job_templates/run_openscap_scans_-_ansible_default.erb +20 -0
  26. data/config/routes.rb +3 -0
  27. data/db/migrate/20210818083407_fix_ansible_setting_category_to_dsl.rb +5 -0
  28. data/lib/foreman_ansible/engine.rb +0 -17
  29. data/lib/foreman_ansible/register.rb +115 -4
  30. data/lib/foreman_ansible/version.rb +1 -1
  31. data/package.json +4 -2
  32. data/test/functional/api/v2/ansible_inventories_controller_test.rb +1 -2
  33. data/test/graphql/mutations/hosts/assign_ansible_roles_mutation_test.rb +96 -0
  34. data/test/graphql/queries/ansible_roles_query_test.rb +35 -0
  35. data/test/unit/ansible_provider_test.rb +3 -6
  36. data/test/unit/concerns/host_managed_extensions_test.rb +8 -0
  37. data/test/unit/concerns/hostgroup_extensions_test.rb +6 -0
  38. data/test/unit/hostgroup_ansible_role_test.rb +13 -0
  39. data/test/unit/services/override_resolver_test.rb +34 -0
  40. data/webpack/components/AnsibleHostDetail/AnsibleHostDetail.js +51 -27
  41. data/webpack/components/AnsibleHostDetail/AnsibleHostDetail.test.js +12 -6
  42. data/webpack/components/AnsibleHostDetail/components/AnsibleHostInventory/AnsibleHostInventory.js +22 -0
  43. data/webpack/components/AnsibleHostDetail/components/AnsibleHostInventory/AnsibleHostInventory.scss +4 -0
  44. data/webpack/components/AnsibleHostDetail/components/AnsibleHostInventory/AnsibleHostInventory.test.js +104 -0
  45. data/webpack/components/AnsibleHostDetail/components/AnsibleHostInventory/index.js +38 -0
  46. data/webpack/components/AnsibleHostDetail/components/AnsibleVariableOverrides/AnsibleVariableOverrides.scss +3 -0
  47. data/webpack/components/AnsibleHostDetail/components/AnsibleVariableOverrides/AnsibleVariableOverridesTable.js +238 -0
  48. data/webpack/components/AnsibleHostDetail/components/AnsibleVariableOverrides/AnsibleVariableOverridesTableHelper.js +111 -0
  49. data/webpack/components/AnsibleHostDetail/components/AnsibleVariableOverrides/EditableAction.js +161 -0
  50. data/webpack/components/AnsibleHostDetail/components/AnsibleVariableOverrides/EditableAction.scss +7 -0
  51. data/webpack/components/AnsibleHostDetail/components/AnsibleVariableOverrides/EditableActionHelper.js +49 -0
  52. data/webpack/components/AnsibleHostDetail/components/AnsibleVariableOverrides/EditableValue.js +70 -0
  53. data/webpack/components/AnsibleHostDetail/components/AnsibleVariableOverrides/EditableValueHelper.js +35 -0
  54. data/webpack/components/AnsibleHostDetail/components/AnsibleVariableOverrides/__test__/AnsibleVariableOverrides.fixtures.js +429 -0
  55. data/webpack/components/AnsibleHostDetail/components/AnsibleVariableOverrides/__test__/AnsibleVariableOverrides.test.js +71 -0
  56. data/webpack/components/AnsibleHostDetail/components/AnsibleVariableOverrides/__test__/AnsibleVariableOverridesDelete.test.js +74 -0
  57. data/webpack/components/AnsibleHostDetail/components/AnsibleVariableOverrides/__test__/AnsibleVariableOverridesUpdate.test.js +188 -0
  58. data/webpack/components/AnsibleHostDetail/components/AnsibleVariableOverrides/index.js +58 -0
  59. data/webpack/components/AnsibleHostDetail/components/JobsTab/JobsTabHelper.js +79 -0
  60. data/webpack/components/AnsibleHostDetail/components/JobsTab/NewRecurringJobHelper.js +106 -0
  61. data/webpack/components/AnsibleHostDetail/components/JobsTab/NewRecurringJobModal.js +129 -0
  62. data/webpack/components/AnsibleHostDetail/components/JobsTab/NewRecurringJobModal.scss +7 -0
  63. data/webpack/components/AnsibleHostDetail/components/JobsTab/PreviousJobsTable.js +103 -0
  64. data/webpack/components/AnsibleHostDetail/components/JobsTab/RecurringJobsTable.js +96 -0
  65. data/webpack/components/AnsibleHostDetail/components/JobsTab/__test__/JobsTab.fixtures.js +184 -0
  66. data/webpack/components/AnsibleHostDetail/components/JobsTab/__test__/JobsTab.test.js +195 -0
  67. data/webpack/components/AnsibleHostDetail/components/JobsTab/index.js +88 -0
  68. data/webpack/components/AnsibleHostDetail/components/RolesTab/AllRolesModal/AllRolesTable.js +89 -0
  69. data/webpack/components/AnsibleHostDetail/components/RolesTab/AllRolesModal/index.js +80 -0
  70. data/webpack/components/AnsibleHostDetail/components/RolesTab/EditRolesModal/EditRolesForm.js +90 -0
  71. data/webpack/components/AnsibleHostDetail/components/RolesTab/EditRolesModal/EditRolesModal.scss +3 -0
  72. data/webpack/components/AnsibleHostDetail/components/RolesTab/EditRolesModal/EditRolesModalHelper.js +40 -0
  73. data/webpack/components/AnsibleHostDetail/components/RolesTab/EditRolesModal/index.js +82 -0
  74. data/webpack/components/AnsibleHostDetail/components/RolesTab/RolesTable.js +129 -0
  75. data/webpack/components/AnsibleHostDetail/components/RolesTab/__test__/EditRoles.test.js +85 -0
  76. data/webpack/components/AnsibleHostDetail/components/RolesTab/__test__/RolesTab.fixtures.js +180 -0
  77. data/webpack/components/AnsibleHostDetail/components/RolesTab/__test__/RolesTab.test.js +75 -0
  78. data/webpack/components/AnsibleHostDetail/components/RolesTab/index.js +51 -0
  79. data/webpack/components/AnsibleHostDetail/components/SecondaryTabRoutes.js +60 -0
  80. data/webpack/components/AnsibleHostDetail/components/TabLayout.js +12 -0
  81. data/webpack/components/AnsibleHostDetail/constants.js +9 -0
  82. data/webpack/components/AnsibleHostDetail/helpers.js +4 -0
  83. data/webpack/components/AnsibleRolesAndVariables/__test__/AnsibleRolesAndVariablesImport.test.js +15 -10
  84. data/webpack/components/AnsibleRolesSwitcher/components/AnsibleRole.js +29 -0
  85. data/webpack/components/AnsibleRolesSwitcher/components/AnsibleRole.test.js +3 -0
  86. data/webpack/components/AnsibleRolesSwitcher/components/AvailableRolesList.js +2 -1
  87. data/webpack/components/AnsibleRolesSwitcher/components/__snapshots__/AnsibleRole.test.js.snap +3 -3
  88. data/webpack/components/AnsibleRolesSwitcher/components/__snapshots__/AvailableRolesList.test.js.snap +4 -0
  89. data/webpack/components/DualList/DualList.scss +3 -0
  90. data/webpack/components/DualList/ListControls.js +65 -0
  91. data/webpack/components/DualList/ListHeader.js +16 -0
  92. data/webpack/components/DualList/ListItem.js +69 -0
  93. data/webpack/components/DualList/ListPane.js +95 -0
  94. data/webpack/components/DualList/SelectedStatus.js +21 -0
  95. data/webpack/components/DualList/index.js +103 -0
  96. data/webpack/components/ErrorState.js +16 -0
  97. data/webpack/components/withLoading.js +135 -0
  98. data/webpack/components/withPagination.js +0 -0
  99. data/webpack/formHelper.js +131 -0
  100. data/webpack/globalIdHelper.js +13 -0
  101. data/webpack/global_index.js +7 -1
  102. data/webpack/graphql/mutations/assignAnsibleRoles.gql +17 -0
  103. data/webpack/graphql/mutations/cancelRecurringLogic.gql +12 -0
  104. data/webpack/graphql/mutations/createAnsibleVariableOverride.gql +28 -0
  105. data/webpack/graphql/mutations/createJobInvocation.gql +11 -0
  106. data/webpack/graphql/mutations/deleteAnsibleVariableOverride.gql +17 -0
  107. data/webpack/graphql/mutations/updateAnsibleVariableOverride.gql +29 -0
  108. data/webpack/graphql/queries/allAnsibleRoles.gql +13 -0
  109. data/webpack/graphql/queries/ansibleRoles.gql +13 -0
  110. data/webpack/graphql/queries/currentUserAttributes.gql +11 -0
  111. data/webpack/graphql/queries/hostAnsibleRoles.gql +17 -0
  112. data/webpack/graphql/queries/hostAvailableAnsibleRoles.gql +11 -0
  113. data/webpack/graphql/queries/hostVariableOverrides.gql +39 -0
  114. data/webpack/graphql/queries/recurringJobs.gql +28 -0
  115. data/webpack/helpers/pageParamsHelper.js +40 -0
  116. data/webpack/helpers/paginationHelper.js +9 -0
  117. data/webpack/permissionsHelper.js +58 -0
  118. data/webpack/routes/HostgroupJobs/__test__/HostgroupJobs.fixtures.js +63 -0
  119. data/webpack/routes/HostgroupJobs/__test__/HostgroupJobs.test.js +112 -0
  120. data/webpack/routes/HostgroupJobs/index.js +26 -0
  121. data/webpack/routes/routes.js +10 -0
  122. data/webpack/testHelper.js +165 -0
  123. data/webpack/toastHelper.js +4 -0
  124. metadata +127 -54
  125. data/app/assets/images/foreman_ansible/Ansible.png +0 -0
  126. data/app/models/foreman_ansible/fact_name.rb +0 -16
  127. data/app/models/setting/ansible.rb +0 -106
  128. data/app/services/foreman_ansible/fact_importer.rb +0 -99
  129. data/app/services/foreman_ansible/fact_parser.rb +0 -126
  130. data/app/services/foreman_ansible/fact_sparser.rb +0 -37
  131. data/app/services/foreman_ansible/operating_system_parser.rb +0 -102
  132. data/app/services/foreman_ansible/structured_fact_importer.rb +0 -25
  133. data/test/unit/services/fact_importer_test.rb +0 -52
  134. data/test/unit/services/fact_parser_test.rb +0 -281
  135. data/test/unit/services/fact_sparser_test.rb +0 -24
  136. data/test/unit/services/structured_fact_importer_test.rb +0 -30
  137. data/webpack/components/AnsibleRolesAndVariables/__test__/__snapshots__/AnsibleRolesAndVariablesImport.test.js.snap +0 -177
@@ -0,0 +1,104 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import '@testing-library/jest-dom';
4
+ import AnsibleHostInventory from './AnsibleHostInventory';
5
+
6
+ const inventoryData = {
7
+ all: {
8
+ hosts: ['jim-dorsay.tlv.redhat.com'],
9
+ vars: {},
10
+ },
11
+ _meta: {
12
+ hostvars: {
13
+ 'jim-dorsay.tlv.redhat.com': {
14
+ foreman: {
15
+ hostname: 'jim-dorsay',
16
+ fqdn: 'jim-dorsay.tlv.redhat.com',
17
+ hostgroup: 'libvirt',
18
+ foreman_subnets: [
19
+ {
20
+ name: 'subnet',
21
+ network: '192.168.122.0',
22
+ mask: '255.255.255.0',
23
+ gateway: '',
24
+ dns_primary: '',
25
+ dns_secondary: '',
26
+ from: '',
27
+ to: '',
28
+ boot_mode: 'DHCP',
29
+ ipam: 'DHCP',
30
+ vlanid: null,
31
+ mtu: 1500,
32
+ nic_delay: null,
33
+ network_type: 'IPv4',
34
+ description: '',
35
+ },
36
+ ],
37
+ foreman_interfaces: [
38
+ {
39
+ ip: '192.168.122.139',
40
+ ip6: '',
41
+ mac: '52:54:00:bd:01:0d',
42
+ name: 'jim-dorsay.tlv.redhat.com',
43
+ attrs: {},
44
+ virtual: false,
45
+ link: true,
46
+ identifier: '',
47
+ managed: true,
48
+ primary: true,
49
+ provision: true,
50
+ subnet: {},
51
+ subnet6: null,
52
+ tag: null,
53
+ attached_to: null,
54
+ type: 'Interface',
55
+ },
56
+ ],
57
+ location: 'Default Location',
58
+ location_title: 'Default Location',
59
+ organization: 'Default Organization',
60
+ organization_title: 'Default Organization',
61
+ domainname: 'tlv.redhat.com',
62
+ foreman_domain_description: '',
63
+ owner_name: 'Admin User',
64
+ owner_email: 'root@localdomain',
65
+ ssh_authorized_keys: [],
66
+ foreman_users: {
67
+ admin: {
68
+ firstname: 'Admin',
69
+ lastname: 'User',
70
+ mail: 'root@localdomain',
71
+ description: '',
72
+ fullname: 'Admin User',
73
+ name: 'admin',
74
+ ssh_authorized_keys: [],
75
+ },
76
+ },
77
+ root_pw:
78
+ '$5$I6cmWY8Uy4NaTfqW$wkKsQzinnd2iNTHvYPYPxO/YkHlvT/sgVF1n6paZvd8',
79
+ foreman_config_groups: [],
80
+ puppetmaster: '',
81
+ },
82
+ foreman_ansible_roles: ['foreman_ansible_test'],
83
+ ansible_roles_check_mode: false,
84
+ host_packages: '',
85
+ host_registration_insights: false,
86
+ host_registration_remote_execution: true,
87
+ remote_execution_ssh_keys: [
88
+ 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC6ERZ9H22Di8mHRcxoTyIc+kcKCpGYq6G4/Ub9WK+DZlZZNMXCcO0WydV9GyJX9eIGrStDxZvxtJQQoOvYRS8d92RZPy3wIR4GtingmVNpwehNePHAcF1Aj9nqm+A4V49R3PqC28ctzBaJAbrvnNh/xDCsTW/ogexu8yN4iKkt0ZijUTnAZ2w1dXit23iITm0I8NPYqxRFNQOYtLYBZDKX+rPanjkyYGEyvi/RkhhHOimMjhAR0Qo9Vqm438LZWrZzGAffiE3AVYBWd3Eh2B5nW1q4cmS7CYOfsQCmO9u9x9lFzZCfdIFVWBhSyqR7cw/M7rTmixUNoX3QvSM0A+z/pBx++SgB+LhV2Zmek68NvcG/9LElD3pVESyvqtRbQQtb7Y6e553QUBn4lOg/N/p67TzfySCm0QhU5dKyj52Bvg6yhrwvqbVw1lwpjf6CNLERcRsTomIUDLVzDdcxo+u5GZ2J/hi5xH7NNMT89oPErwChm8wUDQyAdKGntCFP4D8= yifatfanimakias@localhost.localdomain',
89
+ ],
90
+ remote_execution_ssh_user: 'root',
91
+ remote_execution_effective_user_method: 'sudo',
92
+ remote_execution_connect_by_ip: false,
93
+ },
94
+ },
95
+ },
96
+ };
97
+
98
+ describe('AnsibleHostInventory', () => {
99
+ it('should show inventory', () => {
100
+ render(<AnsibleHostInventory inventoryData={inventoryData} />);
101
+ expect(screen.getByText('_meta:')).toBeInTheDocument();
102
+ expect(screen.getByText('all:')).toBeInTheDocument();
103
+ });
104
+ });
@@ -0,0 +1,38 @@
1
+ import React, { useMemo } from 'react';
2
+ import { foremanUrl } from 'foremanReact/common/helpers';
3
+ import { useAPI } from 'foremanReact/common/hooks/API/APIHooks';
4
+ import Skeleton from 'react-loading-skeleton';
5
+ import { STATUS } from 'foremanReact/constants';
6
+
7
+ import PropTypes from 'prop-types';
8
+ import AnsibleHostInventory from './AnsibleHostInventory';
9
+ import ErrorState from '../../../ErrorState';
10
+
11
+ const WrappedAnsibleHostInventory = ({ hostId }) => {
12
+ const params = useMemo(() => ({ params: { host_ids: [hostId] } }), [hostId]);
13
+
14
+ const url = hostId && foremanUrl('/ansible/api/ansible_inventories/hosts');
15
+ const { response: inventory, status } = useAPI('get', url, params);
16
+
17
+ if (status === STATUS.PENDING) {
18
+ return <Skeleton count={5} />;
19
+ }
20
+
21
+ if (status === STATUS.ERROR) {
22
+ return (
23
+ <ErrorState description={inventory?.response?.data?.error?.message} />
24
+ );
25
+ }
26
+
27
+ return <AnsibleHostInventory inventoryData={inventory} />;
28
+ };
29
+
30
+ WrappedAnsibleHostInventory.propTypes = {
31
+ hostId: PropTypes.number,
32
+ };
33
+
34
+ WrappedAnsibleHostInventory.defaultProps = {
35
+ hostId: undefined,
36
+ };
37
+
38
+ export default WrappedAnsibleHostInventory;
@@ -0,0 +1,3 @@
1
+ .ansible-tab-margin {
2
+ margin: 1.8rem 1rem;
3
+ }
@@ -0,0 +1,238 @@
1
+ import React, { useReducer } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { useDispatch } from 'react-redux';
4
+ import { useMutation } from '@apollo/client';
5
+
6
+ import { sprintf, translate as __ } from 'foremanReact/common/I18n';
7
+ import { usePaginationOptions } from 'foremanReact/components/Pagination/PaginationHooks';
8
+ import { openConfirmModal } from 'foremanReact/components/ConfirmModal';
9
+ import {
10
+ TableComposable,
11
+ Thead,
12
+ Tbody,
13
+ Tr,
14
+ Th,
15
+ Td,
16
+ } from '@patternfly/react-table';
17
+ import { Flex, FlexItem, Pagination } from '@patternfly/react-core';
18
+
19
+ import deleteAnsibleVariableOverride from '../../../../graphql/mutations/deleteAnsibleVariableOverride.gql';
20
+ import EditableAction from './EditableAction';
21
+ import EditableValue from './EditableValue';
22
+ import { decodeModelId } from '../../../../globalIdHelper';
23
+ import {
24
+ formatSourceAttr,
25
+ findOverride,
26
+ validateValue,
27
+ onCompleted,
28
+ onError,
29
+ } from './AnsibleVariableOverridesTableHelper';
30
+
31
+ import withLoading from '../../../withLoading';
32
+ import {
33
+ preparePerPageOptions,
34
+ refreshPage,
35
+ } from '../../../../helpers/paginationHelper';
36
+
37
+ const reducer = (state, action) =>
38
+ state.map((item, index) => {
39
+ if (action.idx === index) {
40
+ return { ...item, ...action.payload };
41
+ }
42
+ return item;
43
+ });
44
+
45
+ const initState = vars =>
46
+ vars.map((variable, idx) => ({
47
+ open: false,
48
+ value: variable.currentValue
49
+ ? variable.currentValue.value
50
+ : variable.defaultValue,
51
+ validation: { key: 'noval', msg: '' },
52
+ working: false,
53
+ }));
54
+
55
+ const AnsibleVariableOverridesTable = ({
56
+ variables,
57
+ hostAttrs,
58
+ hostId,
59
+ hostGlobalId,
60
+ totalCount,
61
+ pagination,
62
+ history,
63
+ }) => {
64
+ const columns = [
65
+ __('Name'),
66
+ __('Ansible Role'),
67
+ __('Type'),
68
+ __('Value'),
69
+ __('Source attribute'),
70
+ ];
71
+
72
+ const handlePerPageSelected = (event, perPage) => {
73
+ refreshPage(history, { page: 1, perPage });
74
+ };
75
+
76
+ const handlePageSelected = (event, page) => {
77
+ refreshPage(history, { ...pagination, page });
78
+ };
79
+
80
+ const perPageOptions = preparePerPageOptions(usePaginationOptions());
81
+
82
+ const [editableState, innerDispatch] = useReducer(
83
+ reducer,
84
+ variables,
85
+ initState
86
+ );
87
+
88
+ const toggleWorking = idx => flag => {
89
+ innerDispatch({ idx, payload: { working: flag } });
90
+ };
91
+
92
+ const setEditable = (idx, flag) => () => {
93
+ innerDispatch({ idx, payload: { open: flag } });
94
+ };
95
+
96
+ const onValueChange = (idx, variable) => value => {
97
+ const payload = {
98
+ value,
99
+ validation: validateValue(variable, value),
100
+ };
101
+ innerDispatch({ idx, payload });
102
+ };
103
+
104
+ const onSubmitSuccess = (idx, variable) => newValue => {
105
+ innerDispatch({
106
+ idx,
107
+ payload: {
108
+ open: false,
109
+ working: false,
110
+ value: newValue,
111
+ validation: validateValue(variable, newValue),
112
+ },
113
+ });
114
+ };
115
+
116
+ const onValidationError = idx => error => {
117
+ innerDispatch({
118
+ idx,
119
+ payload: {
120
+ working: false,
121
+ validation: { key: 'error', msg: error },
122
+ },
123
+ });
124
+ };
125
+
126
+ const dispatch = useDispatch();
127
+ const [callMutation] = useMutation(deleteAnsibleVariableOverride);
128
+
129
+ const deleteAction = (variable, idx) => ({
130
+ title: __('Delete'),
131
+ onClick: () => {
132
+ dispatch(
133
+ openConfirmModal({
134
+ title: __('Delete Ansible Variable Override'),
135
+ message:
136
+ variable &&
137
+ sprintf(
138
+ __('Are you sure you want to delete override for %s?'),
139
+ variable.key
140
+ ),
141
+ onConfirm: () => {
142
+ callMutation({
143
+ variables: {
144
+ id: findOverride(variable, hostAttrs.name).id,
145
+ hostId,
146
+ variableId: decodeModelId(variable),
147
+ },
148
+ }).then(onCompleted(onValueChange(idx, variable)), onError); // eslint-disable-line
149
+ },
150
+ })
151
+ );
152
+ },
153
+ });
154
+
155
+ const actionsResolver = (variable, idx) => {
156
+ const actions = [];
157
+ if (variable.currentValue?.element === 'fqdn' && variable.meta.canEdit) {
158
+ actions.push(deleteAction(variable, idx));
159
+ }
160
+ return actions;
161
+ };
162
+
163
+ return (
164
+ <React.Fragment>
165
+ <Flex>
166
+ <FlexItem align={{ default: 'alignRight' }}>
167
+ <Pagination
168
+ itemCount={totalCount}
169
+ page={pagination.page}
170
+ perPage={pagination.perPage}
171
+ onSetPage={handlePageSelected}
172
+ onPerPageSelect={handlePerPageSelected}
173
+ perPageOptions={perPageOptions}
174
+ variant="top"
175
+ />
176
+ </FlexItem>
177
+ </Flex>
178
+ <TableComposable variant="compact">
179
+ <Thead>
180
+ <Tr>
181
+ {columns.map(col => (
182
+ <Th key={col}>{col}</Th>
183
+ ))}
184
+ <Th />
185
+ </Tr>
186
+ </Thead>
187
+ <Tbody>
188
+ {variables.map((variable, idx) => (
189
+ <Tr key={idx}>
190
+ <Td>{variable.key}</Td>
191
+ <Td>{variable.ansibleRoleName}</Td>
192
+ <Td>{variable.parameterType}</Td>
193
+ <Td>
194
+ <EditableValue
195
+ variable={variable}
196
+ editing={editableState[idx].open}
197
+ onChange={onValueChange(idx, variable)}
198
+ value={editableState[idx].value}
199
+ validation={editableState[idx].validation}
200
+ working={editableState[idx].working}
201
+ />
202
+ </Td>
203
+ <Td>{formatSourceAttr(variable)}</Td>
204
+ <Td>
205
+ <EditableAction
206
+ open={editableState[idx].open}
207
+ onClose={setEditable(idx, false)}
208
+ onOpen={setEditable(idx, true)}
209
+ toggleWorking={toggleWorking(idx)}
210
+ variable={variable}
211
+ state={editableState[idx]}
212
+ hostId={hostId}
213
+ hostName={hostAttrs.name}
214
+ hostGlobalId={hostGlobalId}
215
+ onSubmitSuccess={onSubmitSuccess(idx, variable)}
216
+ onValidationError={onValidationError(idx)}
217
+ />
218
+ </Td>
219
+ <Td actions={{ items: actionsResolver(variable, idx) }} />
220
+ </Tr>
221
+ ))}
222
+ </Tbody>
223
+ </TableComposable>
224
+ </React.Fragment>
225
+ );
226
+ };
227
+
228
+ AnsibleVariableOverridesTable.propTypes = {
229
+ variables: PropTypes.array.isRequired,
230
+ hostAttrs: PropTypes.object.isRequired,
231
+ hostId: PropTypes.number.isRequired,
232
+ hostGlobalId: PropTypes.string.isRequired,
233
+ totalCount: PropTypes.number.isRequired,
234
+ pagination: PropTypes.object.isRequired,
235
+ history: PropTypes.object.isRequired,
236
+ };
237
+
238
+ export default withLoading(AnsibleVariableOverridesTable);
@@ -0,0 +1,111 @@
1
+ import React from 'react';
2
+ import { TimesIcon, CheckIcon } from '@patternfly/react-icons';
3
+ import { sprintf, translate as __ } from 'foremanReact/common/I18n';
4
+
5
+ import { showToast } from '../../../../toastHelper';
6
+
7
+ const formatSourceLink = currentValue =>
8
+ `${__(currentValue.element)}: ${currentValue.elementName}`;
9
+
10
+ export const formatSourceAttr = variable =>
11
+ variable.currentValue
12
+ ? formatSourceLink(variable.currentValue)
13
+ : __('Default value');
14
+
15
+ export const formatValue = variable => {
16
+ const value = variable.currentValue
17
+ ? variable.currentValue.value
18
+ : variable.defaultValue;
19
+
20
+ switch (variable.parameterType) {
21
+ case 'boolean':
22
+ return value ? <CheckIcon /> : <TimesIcon />;
23
+ case 'yaml':
24
+ case 'hash':
25
+ case 'array':
26
+ return JSON.stringify(value);
27
+ default:
28
+ return value;
29
+ }
30
+ };
31
+
32
+ const joinErrors = errors => errors.map(err => err.message).join(', ');
33
+
34
+ export const onCompleted = onSubmitSuccess => response => {
35
+ const {
36
+ errors,
37
+ overridenAnsibleVariable,
38
+ } = response.data.deleteAnsibleVariableOverride;
39
+ if (Array.isArray(errors) && errors.length > 0) {
40
+ showToast({
41
+ type: 'error',
42
+ message: formatError(joinErrors(errors)),
43
+ });
44
+ } else {
45
+ onSubmitSuccess(overridenAnsibleVariable.currentValue.value);
46
+ showToast({
47
+ type: 'success',
48
+ message: __('Ansible variable override was successfully deleted.'),
49
+ });
50
+ }
51
+ };
52
+
53
+ export const findOverride = (variable, hostname) =>
54
+ variable.lookupValues.nodes.find(
55
+ item =>
56
+ item.value === variable.currentValue.value &&
57
+ item.match === `fqdn=${hostname}`
58
+ );
59
+
60
+ const formatError = error =>
61
+ sprintf(
62
+ __(
63
+ 'There was a following error when deleting Ansible variable override: %s'
64
+ ),
65
+ error
66
+ );
67
+
68
+ export const onError = response => {
69
+ showToast({ type: 'error', message: formatError(response.error) });
70
+ };
71
+
72
+ const validationSuccess = { key: 'success', msg: '' };
73
+
74
+ const validateRegexp = (variable, value) => {
75
+ if (new RegExp(variable.validatorRule).test(value)) {
76
+ return validationSuccess;
77
+ }
78
+ return {
79
+ key: 'error',
80
+ msg: sprintf(
81
+ __('Invalid, expected to match a regex: %s'),
82
+ variable.validatorRule
83
+ ),
84
+ };
85
+ };
86
+
87
+ const validateList = (variable, value) => {
88
+ if (variable.validatorRule.split(',').find(item => item.trim() === value)) {
89
+ return validationSuccess;
90
+ }
91
+ return {
92
+ key: 'error',
93
+ msg: sprintf(__('Invalid, expected one of: %s'), variable.validatorRule),
94
+ };
95
+ };
96
+
97
+ export const validateValue = (variable, value) => {
98
+ if (variable.required && !value) {
99
+ return { key: 'error', msg: __('is required') };
100
+ }
101
+
102
+ if (variable.validatorType === 'regexp') {
103
+ return validateRegexp(variable, value);
104
+ }
105
+
106
+ if (variable.validatorType === 'list') {
107
+ return validateList(variable, value);
108
+ }
109
+
110
+ return { key: 'noval', msg: '' };
111
+ };
@@ -0,0 +1,161 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { useMutation } from '@apollo/client';
4
+ import classNames from 'classnames';
5
+
6
+ import { Button, Spinner } from '@patternfly/react-core';
7
+ import { TimesIcon, CheckIcon, PencilAltIcon } from '@patternfly/react-icons';
8
+
9
+ import './EditableAction.scss';
10
+
11
+ import { decodeModelId } from '../../../../globalIdHelper';
12
+ import updateAnsibleVariableOverrideMutation from '../../../../graphql/mutations/updateAnsibleVariableOverride.gql';
13
+ import createAnsibleVariableOverrideMutation from '../../../../graphql/mutations/createAnsibleVariableOverride.gql';
14
+ import {
15
+ onCompleted,
16
+ onError,
17
+ hasError,
18
+ createMatcher,
19
+ } from './EditableActionHelper';
20
+
21
+ const EditableAction = ({
22
+ onValidationError,
23
+ toggleWorking,
24
+ onSubmitSuccess,
25
+ open,
26
+ onClose,
27
+ onOpen,
28
+ state,
29
+ variable,
30
+ hostId,
31
+ hostName,
32
+ }) => {
33
+ const [callUpdateMutation] = useMutation(
34
+ updateAnsibleVariableOverrideMutation,
35
+ {
36
+ onCompleted: onCompleted(
37
+ 'updateAnsibleVariableOverride',
38
+ onValidationError,
39
+ toggleWorking,
40
+ onSubmitSuccess
41
+ ),
42
+ onError: onError(toggleWorking),
43
+ }
44
+ );
45
+
46
+ const [callCreateMutation] = useMutation(
47
+ createAnsibleVariableOverrideMutation,
48
+ {
49
+ onCompleted: onCompleted(
50
+ 'createAnsibleVariableOverride',
51
+ onValidationError,
52
+ toggleWorking,
53
+ onSubmitSuccess
54
+ ),
55
+ onError: onError(toggleWorking),
56
+ }
57
+ );
58
+
59
+ const onSubmit = event => {
60
+ if (!variable.currentValue || variable.currentValue.element !== 'fqdn') {
61
+ return createOverride();
62
+ }
63
+ return updateOverride();
64
+ };
65
+
66
+ const updateOverride = () => {
67
+ const match = createMatcher(variable.currentValue.elementName);
68
+ const lookupValue = variable.lookupValues.nodes.find(
69
+ item => item.match === match
70
+ );
71
+ toggleWorking(true);
72
+ callUpdateMutation({
73
+ variables: {
74
+ id: lookupValue.id,
75
+ value: state.value,
76
+ hostId,
77
+ ansibleVariableId: decodeModelId(variable),
78
+ match,
79
+ },
80
+ });
81
+ };
82
+
83
+ const createOverride = () => {
84
+ const match = createMatcher(hostName);
85
+ toggleWorking(true);
86
+ callCreateMutation({
87
+ variables: {
88
+ hostId,
89
+ lookupKeyId: decodeModelId(variable),
90
+ value: state.value,
91
+ match,
92
+ },
93
+ });
94
+ };
95
+
96
+ if (!variable.meta.canEdit) {
97
+ return null;
98
+ }
99
+
100
+ return (
101
+ <React.Fragment>
102
+ <div>
103
+ <div
104
+ className={classNames({
105
+ hideElement: !open,
106
+ editableActionItem: true,
107
+ })}
108
+ >
109
+ <Button
110
+ variant="plain"
111
+ onClick={onClose}
112
+ isDisabled={state.working}
113
+ aria-label="Cancel editing override button"
114
+ >
115
+ <TimesIcon />
116
+ </Button>
117
+ <Button
118
+ variant="plain"
119
+ onClick={onSubmit}
120
+ isDisabled={state.working || hasError(state)}
121
+ aria-label="Submit editing override button"
122
+ >
123
+ <CheckIcon />
124
+ </Button>
125
+ <span className={!state.working ? 'hideElement' : ''}>
126
+ <Spinner size="md" />
127
+ </span>
128
+ </div>
129
+ <div
130
+ className={classNames({
131
+ hideElement: open,
132
+ editableActionItem: true,
133
+ })}
134
+ >
135
+ <Button
136
+ onClick={onOpen}
137
+ variant="plain"
138
+ aria-label="Edit override button"
139
+ >
140
+ <PencilAltIcon />
141
+ </Button>
142
+ </div>
143
+ </div>
144
+ </React.Fragment>
145
+ );
146
+ };
147
+
148
+ EditableAction.propTypes = {
149
+ onValidationError: PropTypes.func.isRequired,
150
+ toggleWorking: PropTypes.func.isRequired,
151
+ onSubmitSuccess: PropTypes.func.isRequired,
152
+ onClose: PropTypes.func.isRequired,
153
+ onOpen: PropTypes.func.isRequired,
154
+ variable: PropTypes.object.isRequired,
155
+ state: PropTypes.object.isRequired,
156
+ open: PropTypes.bool.isRequired,
157
+ hostId: PropTypes.number.isRequired,
158
+ hostName: PropTypes.string.isRequired,
159
+ };
160
+
161
+ export default EditableAction;
@@ -0,0 +1,7 @@
1
+ .hideElement {
2
+ display: none;
3
+ }
4
+
5
+ .editableActionItem {
6
+ width: 150px;
7
+ }