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,74 @@
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 {
7
+ withRedux,
8
+ withMockedProvider,
9
+ tick,
10
+ historyMock,
11
+ } from '../../../../../testHelper';
12
+ import {
13
+ deleteMocks,
14
+ mocks,
15
+ hostId,
16
+ hostAttrs,
17
+ } from './AnsibleVariableOverrides.fixtures';
18
+
19
+ import * as toasts from '../../../../../toastHelper';
20
+
21
+ import AnsibleVariableOverrides from '../';
22
+
23
+ const TestComponent = withRedux(withMockedProvider(AnsibleVariableOverrides));
24
+
25
+ describe('AnsibleVariableOverrides', () => {
26
+ it('should open and close delete modal', async () => {
27
+ render(
28
+ <TestComponent
29
+ mocks={mocks}
30
+ hostId={hostId}
31
+ hostAttrs={hostAttrs}
32
+ history={historyMock}
33
+ />
34
+ );
35
+ await waitFor(tick);
36
+ userEvent.click(screen.getAllByRole('button', { name: 'Actions' })[0]);
37
+ userEvent.click(screen.getByText('Delete'));
38
+ await waitFor(tick);
39
+ expect(
40
+ screen.getByText('Delete Ansible Variable Override')
41
+ ).toBeInTheDocument();
42
+ userEvent.click(screen.getByText('Cancel'));
43
+ await waitFor(tick);
44
+ expect(
45
+ screen.queryByText('Delete Ansible Variable Override')
46
+ ).not.toBeInTheDocument();
47
+ });
48
+ it('should delete override', async () => {
49
+ const showToast = jest.fn();
50
+ jest.spyOn(toasts, 'showToast').mockImplementation(showToast);
51
+ render(
52
+ <TestComponent
53
+ mocks={mocks.concat(deleteMocks)}
54
+ hostId={hostId}
55
+ hostAttrs={hostAttrs}
56
+ history={historyMock}
57
+ />
58
+ );
59
+ await waitFor(tick);
60
+ expect(screen.queryByText('21')).toBeInTheDocument();
61
+ userEvent.click(screen.getAllByRole('button', { name: 'Actions' })[0]);
62
+ userEvent.click(screen.getByText('Delete'));
63
+ await waitFor(tick);
64
+ userEvent.click(screen.getByText('Confirm'));
65
+ await waitFor(tick);
66
+ expect(showToast).toHaveBeenCalledWith({
67
+ type: 'success',
68
+ message: 'Ansible variable override was successfully deleted.',
69
+ });
70
+ await waitFor(tick);
71
+ expect(screen.queryByText('21')).not.toBeInTheDocument();
72
+ expect(screen.queryByText('101')).toBeInTheDocument();
73
+ });
74
+ });
@@ -0,0 +1,188 @@
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 {
7
+ withRedux,
8
+ withMockedProvider,
9
+ tick,
10
+ historyMock,
11
+ } from '../../../../../testHelper';
12
+ import {
13
+ mocks,
14
+ updateMocks,
15
+ createMocks,
16
+ updateErrorMocks,
17
+ updateValidationMocks,
18
+ hostId,
19
+ hostAttrs,
20
+ } from './AnsibleVariableOverrides.fixtures';
21
+
22
+ import * as toasts from '../../../../../toastHelper';
23
+
24
+ import AnsibleVariableOverrides from '../';
25
+
26
+ const TestComponent = withRedux(withMockedProvider(AnsibleVariableOverrides));
27
+
28
+ describe('AnsibleVariableOverrides', () => {
29
+ it('edit existing override', async () => {
30
+ const showToast = jest.fn();
31
+ jest.spyOn(toasts, 'showToast').mockImplementation(showToast);
32
+
33
+ render(
34
+ <TestComponent
35
+ mocks={mocks.concat(updateMocks)}
36
+ hostId={hostId}
37
+ hostAttrs={hostAttrs}
38
+ history={historyMock}
39
+ />
40
+ );
41
+
42
+ await waitFor(tick);
43
+ expect(screen.getByText('21')).toBeInTheDocument();
44
+ userEvent.click(
45
+ screen.getAllByRole('button', { name: 'Edit override button' })[0]
46
+ );
47
+ userEvent.type(
48
+ screen.getByRole('textbox', { name: 'Edit override field' }),
49
+ '77'
50
+ );
51
+ userEvent.click(
52
+ screen.getAllByRole('button', {
53
+ name: 'Submit editing override button',
54
+ })[0]
55
+ );
56
+ await waitFor(tick);
57
+ expect(showToast).toHaveBeenCalledWith({
58
+ type: 'success',
59
+ message: 'Ansible variable override successfully changed.',
60
+ });
61
+ expect(screen.queryByText('21')).not.toBeInTheDocument();
62
+ expect(screen.getByText('2177')).toBeInTheDocument();
63
+ });
64
+ it('should show unexpected errors', async () => {
65
+ const showToast = jest.fn();
66
+ jest.spyOn(toasts, 'showToast').mockImplementation(showToast);
67
+
68
+ render(
69
+ <TestComponent
70
+ mocks={mocks.concat(updateErrorMocks)}
71
+ hostId={hostId}
72
+ hostAttrs={hostAttrs}
73
+ history={historyMock}
74
+ />
75
+ );
76
+ await waitFor(tick);
77
+ userEvent.click(
78
+ screen.getAllByRole('button', { name: 'Edit override button' })[0]
79
+ );
80
+ userEvent.type(
81
+ screen.getByRole('textbox', { name: 'Edit override field' }),
82
+ '77'
83
+ );
84
+ userEvent.click(
85
+ screen.getAllByRole('button', {
86
+ name: 'Submit editing override button',
87
+ })[0]
88
+ );
89
+ await waitFor(tick);
90
+ expect(showToast).toHaveBeenCalledWith({
91
+ type: 'error',
92
+ message:
93
+ 'There was a following error when changing Ansible variable override: Not enough minerals',
94
+ });
95
+ expect(
96
+ screen.getByRole('textbox', { name: 'Edit override field' })
97
+ ).toHaveValue('2177');
98
+ userEvent.click(
99
+ screen.getAllByRole('button', {
100
+ name: 'Cancel editing override button',
101
+ })[0]
102
+ );
103
+ expect(screen.getByText('21')).toBeInTheDocument();
104
+ });
105
+ it('should show client validations', async () => {
106
+ render(
107
+ <TestComponent
108
+ mocks={mocks}
109
+ hostId={hostId}
110
+ hostAttrs={hostAttrs}
111
+ history={historyMock}
112
+ />
113
+ );
114
+ await waitFor(tick);
115
+ userEvent.click(
116
+ screen.getAllByRole('button', { name: 'Edit override button' })[1]
117
+ );
118
+ const input = screen.getByRole('textbox', { name: 'Edit override field' });
119
+ const submitBtn = screen.getAllByRole('button', {
120
+ name: 'Submit editing override button',
121
+ })[1];
122
+ userEvent.clear(input);
123
+ expect(screen.getByText('is required')).toBeInTheDocument();
124
+ expect(submitBtn).toBeDisabled();
125
+ userEvent.type(input, 'd');
126
+ expect(
127
+ screen.getByText('Invalid, expected one of: a,b,c')
128
+ ).toBeInTheDocument();
129
+ expect(submitBtn).toBeDisabled();
130
+ userEvent.clear(input);
131
+ userEvent.type(input, 'c');
132
+ expect(submitBtn).not.toBeDisabled();
133
+ });
134
+ it('should show server validations', async () => {
135
+ render(
136
+ <TestComponent
137
+ mocks={mocks.concat(updateValidationMocks)}
138
+ hostId={hostId}
139
+ hostAttrs={hostAttrs}
140
+ history={historyMock}
141
+ />
142
+ );
143
+ await waitFor(tick);
144
+ userEvent.click(
145
+ screen.getAllByRole('button', { name: 'Edit override button' })[0]
146
+ );
147
+ const input = screen.getByRole('textbox', { name: 'Edit override field' });
148
+ userEvent.clear(input);
149
+ userEvent.type(input, 'foo');
150
+ userEvent.click(
151
+ screen.getAllByRole('button', {
152
+ name: 'Submit editing override button',
153
+ })[0]
154
+ );
155
+ await waitFor(tick);
156
+ expect(screen.getByText('is invalid integer')).toBeInTheDocument();
157
+ });
158
+ it('should create new override', async () => {
159
+ const showToast = jest.fn();
160
+ jest.spyOn(toasts, 'showToast').mockImplementation(showToast);
161
+
162
+ render(
163
+ <TestComponent
164
+ mocks={mocks.concat(createMocks)}
165
+ hostId={hostId}
166
+ hostAttrs={hostAttrs}
167
+ history={historyMock}
168
+ />
169
+ );
170
+ await waitFor(tick);
171
+ userEvent.click(
172
+ screen.getAllByRole('button', { name: 'Edit override button' })[1]
173
+ );
174
+ const input = screen.getByRole('textbox', { name: 'Edit override field' });
175
+ userEvent.clear(input);
176
+ userEvent.type(input, 'b');
177
+ userEvent.click(
178
+ screen.getAllByRole('button', {
179
+ name: 'Submit editing override button',
180
+ })[1]
181
+ );
182
+ await waitFor(tick);
183
+ expect(showToast).toHaveBeenCalledWith({
184
+ type: 'success',
185
+ message: 'Ansible variable override successfully changed.',
186
+ });
187
+ });
188
+ });
@@ -0,0 +1,58 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { translate as __ } from 'foremanReact/common/I18n';
4
+
5
+ import { useQuery } from '@apollo/client';
6
+ import variableOverrides from '../../../../graphql/queries/hostVariableOverrides.gql';
7
+ import AnsibleVariableOverridesTable from './AnsibleVariableOverridesTable';
8
+ import {
9
+ useParamsToVars,
10
+ useCurrentPagination,
11
+ } from '../../../../helpers/pageParamsHelper';
12
+
13
+ import { encodeId } from '../../../../globalIdHelper';
14
+ import './AnsibleVariableOverrides.scss';
15
+
16
+ const AnsibleVariableOverrides = ({ hostId, hostAttrs, history }) => {
17
+ const hostGlobalId = encodeId('Host', hostId);
18
+ const pagination = useCurrentPagination(history);
19
+
20
+ const useFetchFn = () =>
21
+ useQuery(variableOverrides, {
22
+ variables: {
23
+ id: hostGlobalId,
24
+ match: `fqdn=${hostAttrs.name}`,
25
+ ...useParamsToVars(history),
26
+ },
27
+ fetchPolicy: 'network-only',
28
+ nextFetchPolicy: 'cache-first',
29
+ });
30
+
31
+ const renameData = data => ({
32
+ variables: data.host.ansibleVariablesWithOverrides.nodes,
33
+ totalCount: data.host.ansibleVariablesWithOverrides.totalCount,
34
+ });
35
+
36
+ return (
37
+ <AnsibleVariableOverridesTable
38
+ hostId={hostId}
39
+ hostAttrs={hostAttrs}
40
+ hostGlobalId={hostGlobalId}
41
+ renameData={renameData}
42
+ fetchFn={useFetchFn}
43
+ renamedDataPath="variables"
44
+ emptyStateTitle={__('No Ansible Variables found for Host')}
45
+ permissions={['view_ansible_variables']}
46
+ pagination={pagination}
47
+ history={history}
48
+ />
49
+ );
50
+ };
51
+
52
+ AnsibleVariableOverrides.propTypes = {
53
+ hostId: PropTypes.number.isRequired,
54
+ hostAttrs: PropTypes.object.isRequired,
55
+ history: PropTypes.object.isRequired,
56
+ };
57
+
58
+ export default AnsibleVariableOverrides;
@@ -0,0 +1,79 @@
1
+ import { useQuery, useMutation } from '@apollo/client';
2
+ import { translate as __, sprintf } from 'foremanReact/common/I18n';
3
+ import jobsQuery from '../../../../graphql/queries/recurringJobs.gql';
4
+ import cancelRecurringLogic from '../../../../graphql/mutations/cancelRecurringLogic.gql';
5
+ import { showToast } from '../../../../toastHelper';
6
+
7
+ export const ansiblePurpose = (resourceName, resourceId) =>
8
+ `ansible-${resourceName}-${resourceId}`;
9
+
10
+ const jobSearch = (resourceName, resourceId, statusSearch) =>
11
+ `recurring = true && pattern_template_name = "Ansible Roles - Ansible Default" && ${statusSearch} && recurring_logic.purpose = ${ansiblePurpose(
12
+ resourceName,
13
+ resourceId
14
+ )}`;
15
+
16
+ export const scheduledJobsSearch = (resourceName, resourceId) =>
17
+ jobSearch(resourceName, resourceId, 'status = queued');
18
+ export const previousJobsSearch = (resourceName, resourceId) =>
19
+ jobSearch(resourceName, resourceId, 'status != queued');
20
+
21
+ const fetchJobsFn = (searchFn, pagination = {}) => componentProps =>
22
+ useQuery(jobsQuery, {
23
+ variables: {
24
+ search: searchFn(componentProps.resourceName, componentProps.resourceId),
25
+ ...pagination,
26
+ },
27
+ });
28
+
29
+ export const fetchRecurringFn = fetchJobsFn(scheduledJobsSearch);
30
+ export const fetchPreviousFn = pagination =>
31
+ fetchJobsFn(previousJobsSearch, pagination);
32
+
33
+ export const renameData = data => ({
34
+ jobs: data.jobInvocations.nodes,
35
+ totalCount: data.jobInvocations.totalCount,
36
+ });
37
+
38
+ export const joinErrors = errors => errors.map(err => err.message).join(', ');
39
+
40
+ const formatError = error =>
41
+ sprintf(
42
+ __('There was a following error when deleting Ansible config job: %s'),
43
+ error
44
+ );
45
+
46
+ const onError = error => {
47
+ showToast({ type: 'danger', message: formatError(error) });
48
+ };
49
+
50
+ const onCompleted = data => {
51
+ const { errors } = data.cancelRecurringLogic;
52
+ if (Array.isArray(errors) && errors.length > 0) {
53
+ showToast({
54
+ type: 'danger',
55
+ message: formatError(joinErrors(errors)),
56
+ });
57
+ } else {
58
+ showToast({
59
+ type: 'success',
60
+ message: __('Ansible job was successfully canceled.'),
61
+ });
62
+ }
63
+ };
64
+
65
+ export const useCancelMutation = (resourceName, resourceId) =>
66
+ useMutation(cancelRecurringLogic, {
67
+ onCompleted,
68
+ onError,
69
+ refetchQueries: [
70
+ {
71
+ query: jobsQuery,
72
+ variables: { search: previousJobsSearch(resourceName, resourceId) },
73
+ },
74
+ {
75
+ query: jobsQuery,
76
+ variables: { search: scheduledJobsSearch(resourceName, resourceId) },
77
+ },
78
+ ],
79
+ });
@@ -0,0 +1,106 @@
1
+ import * as Yup from 'yup';
2
+ import { translate as __, sprintf } from 'foremanReact/common/I18n';
3
+
4
+ import { showToast } from '../../../../toastHelper';
5
+ import { ansiblePurpose, joinErrors } from './JobsTabHelper';
6
+
7
+ export const frequencyOpts = [
8
+ { id: 'hourly', name: __('hourly') },
9
+ { id: 'daily', name: __('daily') },
10
+ { id: 'weekly', name: __('weekly') },
11
+ { id: 'monthly', name: __('monthly') },
12
+ ];
13
+
14
+ export const rangeValidator = date => {
15
+ if (date < new Date()) {
16
+ return __('Must not be in the past');
17
+ }
18
+ return '';
19
+ };
20
+
21
+ export const createValidationSchema = () => {
22
+ const cantBeBlank = __("can't be blank");
23
+
24
+ return Yup.object().shape({
25
+ repeat: Yup.string().required(cantBeBlank),
26
+ startTime: Yup.string().required(cantBeBlank),
27
+ startDate: Yup.string().required(cantBeBlank),
28
+ });
29
+ };
30
+
31
+ export const toCron = (date, repeat) => {
32
+ switch (repeat) {
33
+ case 'hourly':
34
+ return `${date.getMinutes()} * * * *`;
35
+ case 'daily':
36
+ return `${date.getMinutes()} ${date.getHours()} * * *`;
37
+ case 'weekly':
38
+ return `${date.getMinutes()} ${date.getHours()} * * ${date.getDay()}`;
39
+ case 'monthly':
40
+ return `${date.getMinutes()} ${date.getHours()} ${date.getDate()} * *`;
41
+ default:
42
+ return `${date.getMinutes()} * * * *`;
43
+ }
44
+ };
45
+
46
+ export const toVars = (resourceName, resourceId, date, repeat) => {
47
+ const targeting =
48
+ resourceName === 'host'
49
+ ? { hostIds: [resourceId] }
50
+ : { searchQuery: `hostgroup_id = ${resourceId}` };
51
+
52
+ return {
53
+ variables: {
54
+ jobInvocation: {
55
+ ...targeting,
56
+ feature: 'ansible_run_host',
57
+ targetingType: 'static_query',
58
+ scheduling: {
59
+ startAt: date,
60
+ },
61
+ recurrence: {
62
+ cronLine: toCron(date, repeat),
63
+ purpose: ansiblePurpose(resourceName, resourceId),
64
+ },
65
+ },
66
+ },
67
+ };
68
+ };
69
+
70
+ const formatError = error =>
71
+ sprintf(
72
+ __('There was a following error when creating Ansible job: %s'),
73
+ error
74
+ );
75
+
76
+ export const onSubmit = (callMutation, onClose, resourceName, resourceId) => (
77
+ values,
78
+ actions
79
+ ) => {
80
+ const onCompleted = response => {
81
+ actions.setSubmitting(false);
82
+ const { errors } = response.data.createJobInvocation;
83
+ if (Array.isArray(errors) && errors.length > 0) {
84
+ showToast({
85
+ type: 'danger',
86
+ message: formatError(joinErrors(errors)),
87
+ });
88
+ } else {
89
+ onClose();
90
+ showToast({
91
+ type: 'success',
92
+ message: __('Ansible job was successfully created.'),
93
+ });
94
+ }
95
+ };
96
+
97
+ const onError = error => {
98
+ actions.setSubmitting(false);
99
+ showToast({ type: 'danger', message: formatError(error) });
100
+ };
101
+
102
+ const date = new Date(`${values.startDate}T${values.startTime}`);
103
+ const variables = toVars(resourceName, resourceId, date, values.repeat);
104
+ // eslint-disable-next-line promise/prefer-await-to-then
105
+ callMutation(variables).then(onCompleted, onError);
106
+ };
@@ -0,0 +1,129 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { Formik, Field as FormikField } from 'formik';
4
+ import { useMutation } from '@apollo/client';
5
+ import { translate as __ } from 'foremanReact/common/I18n';
6
+
7
+ import {
8
+ Modal,
9
+ Button,
10
+ ModalVariant,
11
+ Spinner,
12
+ Form as PfForm,
13
+ } from '@patternfly/react-core';
14
+ import {
15
+ onSubmit,
16
+ createValidationSchema,
17
+ frequencyOpts,
18
+ rangeValidator,
19
+ } from './NewRecurringJobHelper';
20
+
21
+ import {
22
+ DatePickerField,
23
+ TimePickerField,
24
+ SelectField,
25
+ } from '../../../../formHelper';
26
+
27
+ import './NewRecurringJobModal.scss';
28
+
29
+ import { scheduledJobsSearch } from './JobsTabHelper';
30
+
31
+ import createJobInvocation from '../../../../graphql/mutations/createJobInvocation.gql';
32
+ import jobsQuery from '../../../../graphql/queries/recurringJobs.gql';
33
+
34
+ const NewRecurringJobModal = props => {
35
+ const { onClose, resourceId, resourceName } = props;
36
+
37
+ const [callMutation] = useMutation(createJobInvocation, {
38
+ refetchQueries: [
39
+ {
40
+ query: jobsQuery,
41
+ variables: { search: scheduledJobsSearch(resourceName, resourceId) },
42
+ },
43
+ ],
44
+ });
45
+
46
+ return (
47
+ <Formik
48
+ validationSchema={createValidationSchema()}
49
+ onSubmit={onSubmit(callMutation, onClose, resourceName, resourceId)}
50
+ initialValues={{
51
+ startTime: '',
52
+ startDate: '',
53
+ repeat: '',
54
+ }}
55
+ >
56
+ {formProps => {
57
+ const actions = [
58
+ <Button
59
+ aria-label="submit creating job"
60
+ key="confirm"
61
+ variant="primary"
62
+ onClick={formProps.handleSubmit}
63
+ isDisabled={formProps.isSubmitting || !formProps.isValid}
64
+ >
65
+ {__('Submit')}
66
+ </Button>,
67
+ <Button
68
+ aria-label="cancel creating job"
69
+ key="cancel"
70
+ variant="link"
71
+ onClick={onClose}
72
+ isDisabled={formProps.isSubmitting}
73
+ >
74
+ {__('Cancel')}
75
+ </Button>,
76
+ ];
77
+
78
+ if (formProps.isSubmitting) {
79
+ actions.push(<Spinner key="spinner" size="lg" />);
80
+ }
81
+
82
+ return (
83
+ <Modal
84
+ variant={ModalVariant.large}
85
+ title="Create New Recurring Ansible Run"
86
+ isOpen={props.isOpen}
87
+ className="foreman-modal modal-high"
88
+ showClose={false}
89
+ actions={actions}
90
+ disableFocusTrap
91
+ >
92
+ <PfForm>
93
+ <FormikField
94
+ name="repeat"
95
+ component={SelectField}
96
+ label="Repeat"
97
+ isRequired
98
+ selectItems={frequencyOpts}
99
+ />
100
+ <FormikField
101
+ name="startTime"
102
+ component={TimePickerField}
103
+ label="Start Time"
104
+ isRequired
105
+ is24Hour
106
+ />
107
+ <FormikField
108
+ name="startDate"
109
+ component={DatePickerField}
110
+ label="Start Date"
111
+ isRequired
112
+ validators={[rangeValidator]}
113
+ />
114
+ </PfForm>
115
+ </Modal>
116
+ );
117
+ }}
118
+ </Formik>
119
+ );
120
+ };
121
+
122
+ NewRecurringJobModal.propTypes = {
123
+ onClose: PropTypes.func.isRequired,
124
+ resourceId: PropTypes.number.isRequired,
125
+ resourceName: PropTypes.string.isRequired,
126
+ isOpen: PropTypes.bool.isRequired,
127
+ };
128
+
129
+ export default NewRecurringJobModal;
@@ -0,0 +1,7 @@
1
+ .pf-c-backdrop {
2
+ z-index: 1040;
3
+ }
4
+
5
+ .modal-high {
6
+ height: 70%;
7
+ }