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,103 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { translate as __ } from 'foremanReact/common/I18n';
4
+ import { usePaginationOptions } from 'foremanReact/components/Pagination/PaginationHooks';
5
+
6
+ import RelativeDateTime from 'foremanReact/components/common/dates/RelativeDateTime';
7
+
8
+ import {
9
+ TableComposable,
10
+ Thead,
11
+ Tbody,
12
+ Tr,
13
+ Th,
14
+ Td,
15
+ } from '@patternfly/react-table';
16
+ import { Flex, FlexItem, Pagination } from '@patternfly/react-core';
17
+
18
+ import { decodeId } from '../../../../globalIdHelper';
19
+ import withLoading from '../../../withLoading';
20
+ import {
21
+ preparePerPageOptions,
22
+ refreshPage,
23
+ } from '../../../../helpers/paginationHelper';
24
+
25
+ const PreviousJobsTable = ({ history, totalCount, jobs, pagination }) => {
26
+ const columns = [
27
+ __('Description'),
28
+ __('Result'),
29
+ __('State'),
30
+ __('Executed at'),
31
+ __('Schedule'),
32
+ ];
33
+
34
+ const handlePerPageSelected = (event, perPage) => {
35
+ refreshPage(history, { page: 1, perPage });
36
+ };
37
+
38
+ const handlePageSelected = (event, page) => {
39
+ refreshPage(history, { ...pagination, page });
40
+ };
41
+
42
+ const perPageOptions = preparePerPageOptions(usePaginationOptions());
43
+
44
+ return (
45
+ <React.Fragment>
46
+ <h3>{__('Previously executed jobs')}</h3>
47
+ <Flex className="pf-u-pt-md">
48
+ <FlexItem align={{ default: 'alignRight' }}>
49
+ <Pagination
50
+ itemCount={totalCount}
51
+ page={pagination.page}
52
+ perPage={pagination.perPage}
53
+ onSetPage={handlePageSelected}
54
+ onPerPageSelect={handlePerPageSelected}
55
+ perPageOptions={perPageOptions}
56
+ variant="top"
57
+ />
58
+ </FlexItem>
59
+ </Flex>
60
+ <TableComposable variant="compact">
61
+ <Thead>
62
+ <Tr>
63
+ {columns.map(col => (
64
+ <Th key={col}>{col}</Th>
65
+ ))}
66
+ </Tr>
67
+ </Thead>
68
+ <Tbody>
69
+ {jobs.map(job => (
70
+ <Tr key={job.id}>
71
+ <Td>
72
+ <a
73
+ onClick={() =>
74
+ window.tfm.nav.pushUrl(
75
+ `/job_invocations/${decodeId(job.id)}`
76
+ )
77
+ }
78
+ >
79
+ {job.description}
80
+ </a>
81
+ </Td>
82
+ <Td>{job.task.result}</Td>
83
+ <Td>{job.task.state}</Td>
84
+ <Td>
85
+ <RelativeDateTime date={job.startAt} />
86
+ </Td>
87
+ <Td>{job.recurringLogic.cronLine}</Td>
88
+ </Tr>
89
+ ))}
90
+ </Tbody>
91
+ </TableComposable>
92
+ </React.Fragment>
93
+ );
94
+ };
95
+
96
+ PreviousJobsTable.propTypes = {
97
+ jobs: PropTypes.array.isRequired,
98
+ history: PropTypes.object.isRequired,
99
+ totalCount: PropTypes.number.isRequired,
100
+ pagination: PropTypes.object.isRequired,
101
+ };
102
+
103
+ export default withLoading(PreviousJobsTable);
@@ -0,0 +1,96 @@
1
+ import React from 'react';
2
+ import { useDispatch } from 'react-redux';
3
+ import PropTypes from 'prop-types';
4
+ import { translate as __ } from 'foremanReact/common/I18n';
5
+ import RelativeDateTime from 'foremanReact/components/common/dates/RelativeDateTime';
6
+ import { openConfirmModal } from 'foremanReact/components/ConfirmModal';
7
+
8
+ import {
9
+ TableComposable,
10
+ Thead,
11
+ Tbody,
12
+ Tr,
13
+ Th,
14
+ Td,
15
+ } from '@patternfly/react-table';
16
+
17
+ import { useCancelMutation } from './JobsTabHelper';
18
+ import withLoading from '../../../withLoading';
19
+ import { decodeId } from '../../../../globalIdHelper';
20
+
21
+ const RecurringJobsTable = ({ jobs, resourceName, resourceId }) => {
22
+ const columns = [__('Description'), __('Schedule'), __('Next Run')];
23
+ const dispatch = useDispatch();
24
+
25
+ const [callMutation] = useCancelMutation(resourceName, resourceId);
26
+
27
+ const onJobCancel = rlId => () => {
28
+ dispatch(
29
+ openConfirmModal({
30
+ title: __('Cancel Ansible config job'),
31
+ message: __('Are you sure you want to cancel Ansible config job?'),
32
+ isWarning: true,
33
+ onConfirm: () => callMutation({ variables: { id: rlId } }),
34
+ })
35
+ );
36
+ };
37
+
38
+ const actionItems = job => {
39
+ const items = [];
40
+ if (job.recurringLogic.meta.canEdit) {
41
+ items.push({
42
+ title: __('Cancel'),
43
+ onClick: onJobCancel(job.recurringLogic.id),
44
+ key: 'cancel',
45
+ });
46
+ }
47
+
48
+ return { items };
49
+ };
50
+
51
+ return (
52
+ <React.Fragment>
53
+ <h3>{__('Scheduled recurring jobs')}</h3>
54
+ <TableComposable variant="compact">
55
+ <Thead>
56
+ <Tr>
57
+ {columns.map(col => (
58
+ <Th key={col}>{col}</Th>
59
+ ))}
60
+ <Th />
61
+ </Tr>
62
+ </Thead>
63
+ <Tbody>
64
+ {jobs.map(job => (
65
+ <Tr key={job.id}>
66
+ <Td>
67
+ <a
68
+ onClick={() =>
69
+ window.tfm.nav.pushUrl(
70
+ `/job_invocations/${decodeId(job.id)}`
71
+ )
72
+ }
73
+ >
74
+ {job.description}
75
+ </a>
76
+ </Td>
77
+ <Td>{job.recurringLogic.cronLine}</Td>
78
+ <Td>
79
+ <RelativeDateTime date={job.startAt} />
80
+ </Td>
81
+ <Td actions={actionItems(job)} />
82
+ </Tr>
83
+ ))}
84
+ </Tbody>
85
+ </TableComposable>
86
+ </React.Fragment>
87
+ );
88
+ };
89
+
90
+ RecurringJobsTable.propTypes = {
91
+ jobs: PropTypes.array.isRequired,
92
+ resourceId: PropTypes.number.isRequired,
93
+ resourceName: PropTypes.string.isRequired,
94
+ };
95
+
96
+ export default withLoading(RecurringJobsTable);
@@ -0,0 +1,184 @@
1
+ import { scheduledJobsSearch, previousJobsSearch } from '../JobsTabHelper';
2
+ import { admin, mockFactory, userFactory } from '../../../../../testHelper';
3
+
4
+ import recurringJobsQuery from '.../../../../graphql/queries/recurringJobs.gql';
5
+ import createJobMutation from '../../../../../graphql/mutations/createJobInvocation.gql';
6
+ import cancelRecurringLogicMutation from '../../../../../graphql/mutations/cancelRecurringLogic.gql';
7
+
8
+ import { toVars, toCron } from '../NewRecurringJobHelper';
9
+
10
+ export const hostId = 3;
11
+
12
+ const today = new Date();
13
+ const futureDate = new Date(today.setDate(today.getDate() + 3));
14
+ futureDate.setMilliseconds(0);
15
+ futureDate.setSeconds(0);
16
+ export { futureDate };
17
+
18
+ const viewer = userFactory('viewer', [
19
+ { id: 'MDE6UGVybWlzc2lvbi0zMjE=', name: 'view_recurring_logics' },
20
+ { id: 'MDE6UGVybWlzc2lvbi0yNTg=', name: 'view_job_invocations' },
21
+ { id: 'MDE6UGVybWlzc2lvbi0xNzg=', name: 'view_foreman_tasks' },
22
+ ]);
23
+
24
+ const firstRecurringLogicGlobalId =
25
+ 'MDE6Rm9yZW1hblRhc2tzOjpSZWN1cnJpbmdMb2dpYy0x';
26
+ const firstRecurringLogic = {
27
+ __typename: 'ForemanTasks::RecurringLogic',
28
+ id: firstRecurringLogicGlobalId,
29
+ cronLine: toCron(futureDate, 'weekly'),
30
+ meta: {
31
+ canEdit: true,
32
+ },
33
+ };
34
+
35
+ const secondRecurringLogic = {
36
+ ...firstRecurringLogic,
37
+ id: 'MDE6Rm9yZW1hblRhc2tzOjpSZWN1cnJpbmdMb2dpYy03NQ==',
38
+ meta: {
39
+ canEdit: false,
40
+ },
41
+ };
42
+
43
+ export const firstJob = {
44
+ __typename: 'JobInvocation',
45
+ id: 'MDE6Sm9iSW52b2NhdGlvbi0yNTY=',
46
+ description: 'Run Ansible roles',
47
+ startAt: futureDate.toISOString(),
48
+ statusLabel: 'queued',
49
+ recurringLogic: firstRecurringLogic,
50
+ task: {
51
+ __typename: 'ForemanTasks::Task',
52
+ id:
53
+ 'MDE6Rm9yZW1hblRhc2tzOjpUYXNrLTg2OGE5NjRlLWZmMzctNGUxZS1iMzVkLTA5NzdkY2JkOTZhMw==',
54
+ state: 'scheduled',
55
+ result: 'pending',
56
+ },
57
+ };
58
+
59
+ export const secondJob = {
60
+ __typename: 'JobInvocation',
61
+ id: 'MDE6Sm9iSW52b2NhdGlvbi0yNzE=',
62
+ description: 'Run Ansible roles',
63
+ startAt: '2021-06-31T13:37:00+02:00',
64
+ statusLabel: 'succeeded',
65
+ recurringLogic: {
66
+ __typename: 'ForemanTasks::RecurringLogic',
67
+ id: 'MDE6Rm9yZW1hblRhc2tzOjpSZWN1cnJpbmdMb2dpYy0yMw==',
68
+ cronLine: '54 10 15 * *',
69
+ meta: {
70
+ canEdit: true,
71
+ },
72
+ },
73
+ task: {
74
+ __typename: 'ForemanTasks::Task',
75
+ id:
76
+ 'MDE6Rm9yZW1hblRhc2tzOjpUYXNrLWY4ZDJkZTU4LWQ3YmMtNGQ5OS05NDZkLTI4NDNlZWRhYzUwZQ==',
77
+ state: 'stopped',
78
+ result: 'success',
79
+ },
80
+ };
81
+
82
+ export const thirdJob = {
83
+ ...firstJob,
84
+ id: 'MDE6Sm9iSW52b2NhdGlvbi00NDg=',
85
+ recurringLogic: secondRecurringLogic,
86
+ };
87
+
88
+ export const jobInvocationsMockFactory = mockFactory(
89
+ 'jobInvocations',
90
+ recurringJobsQuery
91
+ );
92
+ export const jobCreateMockFactory = mockFactory(
93
+ 'createJobInvocation',
94
+ createJobMutation
95
+ );
96
+
97
+ const jobCancelMockFactory = mockFactory(
98
+ 'cancelRecurringLogic',
99
+ cancelRecurringLogicMutation
100
+ );
101
+
102
+ const emptyScheduledJobsMock = jobInvocationsMockFactory(
103
+ { search: scheduledJobsSearch('host', hostId) },
104
+ { nodes: [], totalCount: 0 },
105
+ { currentUser: admin }
106
+ );
107
+ const emptyScheduledViewerMock = jobInvocationsMockFactory(
108
+ { search: scheduledJobsSearch('host', hostId) },
109
+ { nodes: [], totalCount: 0 },
110
+ { currentUser: viewer }
111
+ );
112
+ const scheduledViewerMock = jobInvocationsMockFactory(
113
+ { search: scheduledJobsSearch('host', hostId) },
114
+ { nodes: [thirdJob], totalCount: 1 },
115
+ { currentUser: viewer }
116
+ );
117
+ const emptyScheduledJobsRefetchMock = jobInvocationsMockFactory(
118
+ { search: scheduledJobsSearch('host', hostId) },
119
+ { nodes: [], totalCount: 0 },
120
+ { refetchData: { nodes: [firstJob], totalCount: 1 }, currentUser: admin }
121
+ );
122
+ const emptyPreviousJobsMock = jobInvocationsMockFactory(
123
+ { search: previousJobsSearch('host', hostId), first: 20, last: 20 },
124
+ { nodes: [], totalCount: 0 },
125
+ { currentUser: admin }
126
+ );
127
+ const emptyPreviousViewerMock = jobInvocationsMockFactory(
128
+ { search: previousJobsSearch('host', hostId) },
129
+ { nodes: [], totalCount: 0 },
130
+ { currentUser: viewer }
131
+ );
132
+ const scheduledJobsMocks = jobInvocationsMockFactory(
133
+ { search: scheduledJobsSearch('host', hostId) },
134
+ { nodes: [firstJob], totalCount: 1 },
135
+ { currentUser: admin }
136
+ );
137
+ const previousJobsMocks = jobInvocationsMockFactory(
138
+ { search: previousJobsSearch('host', hostId), first: 20, last: 20 },
139
+ { nodes: [secondJob], totalCount: 1 },
140
+ { currentUser: admin }
141
+ );
142
+
143
+ export const emptyMocks = emptyScheduledJobsMock.concat(emptyPreviousJobsMock);
144
+ export const emptyViewerMocks = emptyScheduledViewerMock.concat(
145
+ emptyPreviousViewerMock
146
+ );
147
+
148
+ export const scheduledAndPreviousMocks = scheduledJobsMocks.concat(
149
+ previousJobsMocks
150
+ );
151
+
152
+ const createJobMock = jobCreateMockFactory(
153
+ toVars('host', hostId, futureDate, 'weekly').variables,
154
+ { jobInvocation: { id: 'MDE6Sm9iSW52b2NhdGlvbi00MTU=' }, errors: [] }
155
+ );
156
+
157
+ export const createMocks = emptyScheduledJobsRefetchMock
158
+ .concat(emptyPreviousJobsMock)
159
+ .concat(createJobMock);
160
+
161
+ const scheduledWithRefetch = jobInvocationsMockFactory(
162
+ { search: scheduledJobsSearch('host', hostId) },
163
+ { nodes: [firstJob], totalCount: 1 },
164
+ { refetchData: { nodes: [], totalCount: 0 }, currentUser: admin }
165
+ );
166
+
167
+ const previousWithRefetch = jobInvocationsMockFactory(
168
+ { search: previousJobsSearch('host', hostId), first: 20, last: 20 },
169
+ { nodes: [], totalCount: 0 },
170
+ { refetchData: { nodes: [firstJob], totalCount: 1 }, currentUser: admin }
171
+ );
172
+
173
+ const cancelJobMock = jobCancelMockFactory(
174
+ { id: firstRecurringLogicGlobalId },
175
+ { recurringLogic: firstRecurringLogic, errors: [] }
176
+ );
177
+
178
+ export const cancelMocks = scheduledWithRefetch
179
+ .concat(previousWithRefetch)
180
+ .concat(cancelJobMock);
181
+
182
+ export const cancelViewerMocks = scheduledViewerMock.concat(
183
+ emptyPreviousViewerMock
184
+ );
@@ -0,0 +1,195 @@
1
+ import React from 'react';
2
+ import { render, screen, waitFor } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+ import '@testing-library/jest-dom';
5
+
6
+ import { i18nProviderWrapperFactory } from 'foremanReact/common/i18nProviderWrapperFactory';
7
+ import JobsTab from '../';
8
+ import {
9
+ emptyMocks,
10
+ emptyViewerMocks,
11
+ scheduledAndPreviousMocks,
12
+ cancelMocks,
13
+ cancelViewerMocks,
14
+ createMocks,
15
+ hostId,
16
+ futureDate,
17
+ } from './JobsTab.fixtures';
18
+ import * as toasts from '../../../../../toastHelper';
19
+
20
+ import { toCron } from '../NewRecurringJobHelper';
21
+
22
+ import {
23
+ tick,
24
+ withRouter,
25
+ withMockedProvider,
26
+ withRedux,
27
+ historyMock,
28
+ } from '../../../../../testHelper';
29
+
30
+ const TestComponent = withRedux(withRouter(withMockedProvider(JobsTab)));
31
+
32
+ const now = new Date('2021-08-28 00:00:00 -1100');
33
+ const ComponentWithIntl = i18nProviderWrapperFactory(now, 'UTC')(TestComponent);
34
+
35
+ describe('JobsTab', () => {
36
+ it('should load the page', async () => {
37
+ render(
38
+ <ComponentWithIntl
39
+ resourceName="host"
40
+ resourceId={hostId}
41
+ mocks={scheduledAndPreviousMocks}
42
+ history={historyMock}
43
+ />
44
+ );
45
+ await waitFor(tick);
46
+ await waitFor(tick);
47
+ screen
48
+ .getAllByText('Run Ansible roles')
49
+ .map(element => expect(element).toBeInTheDocument());
50
+ expect(screen.getByText('Scheduled recurring jobs')).toBeInTheDocument();
51
+ expect(screen.getByText('Previously executed jobs')).toBeInTheDocument();
52
+ expect(screen.getByText(toCron(futureDate, 'weekly'))).toBeInTheDocument();
53
+ expect(screen.getByText('54 10 15 * *')).toBeInTheDocument();
54
+ });
55
+ it('should show empty state', async () => {
56
+ render(
57
+ <ComponentWithIntl
58
+ resourceName="host"
59
+ resourceId={hostId}
60
+ mocks={emptyMocks}
61
+ history={historyMock}
62
+ />
63
+ );
64
+ await waitFor(tick);
65
+ await waitFor(tick);
66
+ expect(
67
+ screen.getByText('No config job for Ansible roles scheduled')
68
+ ).toBeInTheDocument();
69
+ expect(screen.getByText('Schedule recurring job')).toBeInTheDocument();
70
+ });
71
+ it('should not show create button for viewer', async () => {
72
+ render(
73
+ <ComponentWithIntl
74
+ resourceName="host"
75
+ resourceId={hostId}
76
+ mocks={emptyViewerMocks}
77
+ history={historyMock}
78
+ />
79
+ );
80
+ await waitFor(tick);
81
+ await waitFor(tick);
82
+ expect(
83
+ screen.getByText('No config job for Ansible roles scheduled')
84
+ ).toBeInTheDocument();
85
+ expect(
86
+ screen.queryByText('Schedule recurring job')
87
+ ).not.toBeInTheDocument();
88
+ });
89
+ it('should create new recurring job', async () => {
90
+ const showToast = jest.fn();
91
+ jest.spyOn(toasts, 'showToast').mockImplementation(showToast);
92
+
93
+ render(
94
+ <ComponentWithIntl
95
+ resourceName="host"
96
+ resourceId={hostId}
97
+ mocks={createMocks}
98
+ history={historyMock}
99
+ />
100
+ );
101
+ await waitFor(tick);
102
+ userEvent.click(
103
+ screen.getByRole('button', { name: 'schedule recurring job' })
104
+ );
105
+ await waitFor(tick);
106
+ userEvent.selectOptions(screen.getByLabelText(/repeat/), 'weekly');
107
+ userEvent.type(
108
+ screen.getByLabelText(/startTime/),
109
+ futureDate
110
+ .toISOString()
111
+ .split('T')[1]
112
+ .slice(0, 5)
113
+ );
114
+ userEvent.type(
115
+ screen.getByLabelText(/startDate/),
116
+ futureDate.toISOString().split('T')[0]
117
+ );
118
+ expect(
119
+ screen.getByRole('button', { name: 'submit creating job' })
120
+ ).not.toBeDisabled();
121
+ userEvent.click(
122
+ screen.getByRole('button', { name: 'submit creating job' })
123
+ );
124
+ await waitFor(tick);
125
+ await waitFor(tick);
126
+ await waitFor(tick);
127
+ expect(showToast).toHaveBeenCalledWith({
128
+ type: 'success',
129
+ message: 'Ansible job was successfully created.',
130
+ });
131
+ await waitFor(tick);
132
+ expect(screen.getByText(toCron(futureDate, 'weekly'))).toBeInTheDocument();
133
+ expect(screen.getByText('in 3 days')).toBeInTheDocument();
134
+ expect(
135
+ screen.queryByText('No config job for Ansible roles scheduled')
136
+ ).not.toBeInTheDocument();
137
+ });
138
+ it('should cancel existing recurring job', async () => {
139
+ const showToast = jest.fn();
140
+ jest.spyOn(toasts, 'showToast').mockImplementation(showToast);
141
+ render(
142
+ <ComponentWithIntl
143
+ resourceId={hostId}
144
+ resourceName="host"
145
+ history={historyMock}
146
+ mocks={cancelMocks}
147
+ />
148
+ );
149
+ await waitFor(tick);
150
+ await waitFor(tick);
151
+ expect(
152
+ screen.queryByText('No config job for Ansible roles scheduled')
153
+ ).not.toBeInTheDocument();
154
+ userEvent.click(screen.getAllByRole('button', { name: 'Actions' })[0]);
155
+ userEvent.click(screen.getByText('Cancel'));
156
+ await waitFor(tick);
157
+ expect(
158
+ screen.getByText('Are you sure you want to cancel Ansible config job?')
159
+ ).toBeInTheDocument();
160
+ userEvent.click(screen.getByText('Confirm'));
161
+ await waitFor(tick);
162
+ await waitFor(tick);
163
+ expect(showToast).toHaveBeenCalledWith({
164
+ type: 'success',
165
+ message: 'Ansible job was successfully canceled.',
166
+ });
167
+ expect(
168
+ screen.queryByText('Are you sure you want to cancel Ansible config job?')
169
+ ).not.toBeInTheDocument();
170
+ await waitFor(tick);
171
+ await waitFor(tick);
172
+ await waitFor(tick);
173
+ expect(
174
+ screen.getByText('No config job for Ansible roles scheduled')
175
+ ).toBeInTheDocument();
176
+ });
177
+ it('should not show cancel button if user is not allowed to cancel', async () => {
178
+ render(
179
+ <ComponentWithIntl
180
+ resourceId={hostId}
181
+ resourceName="host"
182
+ history={historyMock}
183
+ mocks={cancelViewerMocks}
184
+ />
185
+ );
186
+ await waitFor(tick);
187
+ await waitFor(tick);
188
+ expect(
189
+ screen.queryByText('No config job for Ansible roles scheduled')
190
+ ).not.toBeInTheDocument();
191
+ expect(screen.queryAllByRole('button', { name: 'Actions' })).toHaveLength(
192
+ 0
193
+ );
194
+ });
195
+ });
@@ -0,0 +1,88 @@
1
+ import React, { useState } from 'react';
2
+ import PropTypes from 'prop-types';
3
+
4
+ import { translate as __ } from 'foremanReact/common/I18n';
5
+
6
+ import { Grid, GridItem, Button } from '@patternfly/react-core';
7
+
8
+ import { fetchRecurringFn, fetchPreviousFn, renameData } from './JobsTabHelper';
9
+ import {
10
+ useParamsToVars,
11
+ useCurrentPagination,
12
+ } from '../../../../helpers/pageParamsHelper';
13
+
14
+ import RecurringJobsTable from './RecurringJobsTable';
15
+ import PreviousJobsTable from './PreviousJobsTable';
16
+ import NewRecurringJobModal from './NewRecurringJobModal';
17
+
18
+ const JobsTab = ({ resourceName, resourceId, history }) => {
19
+ const [modalOpen, setModalOpen] = useState(false);
20
+ const toggleModal = () => setModalOpen(!modalOpen);
21
+
22
+ const permissions = [
23
+ 'view_job_invocations',
24
+ 'view_recurring_logics',
25
+ 'view_foreman_tasks',
26
+ ];
27
+
28
+ const pagination = useCurrentPagination(history);
29
+
30
+ const primaryActionPermissions = [
31
+ 'create_job_invocations',
32
+ 'create_recurring_logics',
33
+ ];
34
+
35
+ const scheduleBtn = (
36
+ <Button aria-label="schedule recurring job" onClick={toggleModal}>
37
+ {__('Schedule recurring job')}
38
+ </Button>
39
+ );
40
+
41
+ return (
42
+ <Grid>
43
+ <GridItem span={12}>
44
+ <RecurringJobsTable
45
+ resourceId={resourceId}
46
+ resourceName={resourceName}
47
+ fetchFn={fetchRecurringFn}
48
+ renameData={renameData}
49
+ renamedDataPath="jobs"
50
+ emptyStateProps={{
51
+ header: __('No config job for Ansible roles scheduled'),
52
+ action: scheduleBtn,
53
+ }}
54
+ permissions={permissions}
55
+ primaryActionPermissions={primaryActionPermissions}
56
+ />
57
+ </GridItem>
58
+ <GridItem span={12}>
59
+ <PreviousJobsTable
60
+ resourceId={resourceId}
61
+ resourceName={resourceName}
62
+ fetchFn={fetchPreviousFn(useParamsToVars(history))}
63
+ renameData={renameData}
64
+ emptyWrapper={() => null}
65
+ renamedDataPath="jobs"
66
+ emptyStateProps={{ header: __('No previous job executions found') }}
67
+ permissions={permissions}
68
+ pagination={pagination}
69
+ history={history}
70
+ />
71
+ </GridItem>
72
+ <NewRecurringJobModal
73
+ isOpen={modalOpen}
74
+ onClose={toggleModal}
75
+ resourceId={resourceId}
76
+ resourceName={resourceName}
77
+ />
78
+ </Grid>
79
+ );
80
+ };
81
+
82
+ JobsTab.propTypes = {
83
+ resourceName: PropTypes.string.isRequired,
84
+ resourceId: PropTypes.number.isRequired,
85
+ history: PropTypes.object.isRequired,
86
+ };
87
+
88
+ export default JobsTab;