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.
- checksums.yaml +4 -4
- data/app/controllers/api/v2/ansible_inventories_controller.rb +1 -1
- data/app/graphql/mutations/ansible_variable_overrides/create.rb +26 -0
- data/app/graphql/mutations/ansible_variable_overrides/delete.rb +38 -0
- data/app/graphql/mutations/ansible_variable_overrides/update.rb +26 -0
- data/app/graphql/mutations/hosts/assign_ansible_roles.rb +37 -0
- data/app/graphql/presenters/ansible_role_presenter.rb +12 -0
- data/app/graphql/presenters/overriden_ansible_variable_presenter.rb +19 -0
- data/app/graphql/types/ansible_role.rb +9 -0
- data/app/graphql/types/ansible_variable.rb +23 -0
- data/app/graphql/types/ansible_variable_override.rb +9 -0
- data/app/graphql/types/inherited_ansible_role.rb +13 -0
- data/app/graphql/types/overriden_ansible_variable.rb +27 -0
- data/app/helpers/foreman_ansible/ansible_roles_data_preparations.rb +22 -22
- data/app/models/concerns/foreman_ansible/host_managed_extensions.rb +23 -4
- data/app/models/concerns/foreman_ansible/hostgroup_extensions.rb +2 -1
- data/app/models/foreman_ansible/ansible_provider.rb +7 -5
- data/app/services/foreman_ansible/ansible_report_importer.rb +2 -2
- data/app/services/foreman_ansible/inventory_creator.rb +1 -1
- data/app/services/foreman_ansible/override_resolver.rb +22 -0
- data/app/views/api/v2/ansible_override_values/index.json.rabl +3 -0
- data/app/views/api/v2/ansible_variables/show.json.rabl +1 -1
- data/app/views/foreman_ansible/ansible_roles/_hostgroup_ansible_roles_button.erb +3 -0
- data/app/views/foreman_ansible/job_templates/convert_to_rhel.erb +6 -2
- data/app/views/foreman_ansible/job_templates/run_openscap_scans_-_ansible_default.erb +20 -0
- data/config/routes.rb +3 -0
- data/db/migrate/20210818083407_fix_ansible_setting_category_to_dsl.rb +5 -0
- data/lib/foreman_ansible/engine.rb +0 -17
- data/lib/foreman_ansible/register.rb +115 -4
- data/lib/foreman_ansible/version.rb +1 -1
- data/package.json +4 -2
- data/test/functional/api/v2/ansible_inventories_controller_test.rb +1 -2
- data/test/graphql/mutations/hosts/assign_ansible_roles_mutation_test.rb +96 -0
- data/test/graphql/queries/ansible_roles_query_test.rb +35 -0
- data/test/unit/ansible_provider_test.rb +3 -6
- data/test/unit/concerns/host_managed_extensions_test.rb +8 -0
- data/test/unit/concerns/hostgroup_extensions_test.rb +6 -0
- data/test/unit/hostgroup_ansible_role_test.rb +13 -0
- data/test/unit/services/override_resolver_test.rb +34 -0
- data/webpack/components/AnsibleHostDetail/AnsibleHostDetail.js +51 -27
- data/webpack/components/AnsibleHostDetail/AnsibleHostDetail.test.js +12 -6
- data/webpack/components/AnsibleHostDetail/components/AnsibleHostInventory/AnsibleHostInventory.js +22 -0
- data/webpack/components/AnsibleHostDetail/components/AnsibleHostInventory/AnsibleHostInventory.scss +4 -0
- data/webpack/components/AnsibleHostDetail/components/AnsibleHostInventory/AnsibleHostInventory.test.js +104 -0
- data/webpack/components/AnsibleHostDetail/components/AnsibleHostInventory/index.js +38 -0
- data/webpack/components/AnsibleHostDetail/components/AnsibleVariableOverrides/AnsibleVariableOverrides.scss +3 -0
- data/webpack/components/AnsibleHostDetail/components/AnsibleVariableOverrides/AnsibleVariableOverridesTable.js +238 -0
- data/webpack/components/AnsibleHostDetail/components/AnsibleVariableOverrides/AnsibleVariableOverridesTableHelper.js +111 -0
- data/webpack/components/AnsibleHostDetail/components/AnsibleVariableOverrides/EditableAction.js +161 -0
- data/webpack/components/AnsibleHostDetail/components/AnsibleVariableOverrides/EditableAction.scss +7 -0
- data/webpack/components/AnsibleHostDetail/components/AnsibleVariableOverrides/EditableActionHelper.js +49 -0
- data/webpack/components/AnsibleHostDetail/components/AnsibleVariableOverrides/EditableValue.js +70 -0
- data/webpack/components/AnsibleHostDetail/components/AnsibleVariableOverrides/EditableValueHelper.js +35 -0
- data/webpack/components/AnsibleHostDetail/components/AnsibleVariableOverrides/__test__/AnsibleVariableOverrides.fixtures.js +429 -0
- data/webpack/components/AnsibleHostDetail/components/AnsibleVariableOverrides/__test__/AnsibleVariableOverrides.test.js +71 -0
- data/webpack/components/AnsibleHostDetail/components/AnsibleVariableOverrides/__test__/AnsibleVariableOverridesDelete.test.js +74 -0
- data/webpack/components/AnsibleHostDetail/components/AnsibleVariableOverrides/__test__/AnsibleVariableOverridesUpdate.test.js +188 -0
- data/webpack/components/AnsibleHostDetail/components/AnsibleVariableOverrides/index.js +58 -0
- data/webpack/components/AnsibleHostDetail/components/JobsTab/JobsTabHelper.js +79 -0
- data/webpack/components/AnsibleHostDetail/components/JobsTab/NewRecurringJobHelper.js +106 -0
- data/webpack/components/AnsibleHostDetail/components/JobsTab/NewRecurringJobModal.js +129 -0
- data/webpack/components/AnsibleHostDetail/components/JobsTab/NewRecurringJobModal.scss +7 -0
- data/webpack/components/AnsibleHostDetail/components/JobsTab/PreviousJobsTable.js +103 -0
- data/webpack/components/AnsibleHostDetail/components/JobsTab/RecurringJobsTable.js +96 -0
- data/webpack/components/AnsibleHostDetail/components/JobsTab/__test__/JobsTab.fixtures.js +184 -0
- data/webpack/components/AnsibleHostDetail/components/JobsTab/__test__/JobsTab.test.js +195 -0
- data/webpack/components/AnsibleHostDetail/components/JobsTab/index.js +88 -0
- data/webpack/components/AnsibleHostDetail/components/RolesTab/AllRolesModal/AllRolesTable.js +89 -0
- data/webpack/components/AnsibleHostDetail/components/RolesTab/AllRolesModal/index.js +80 -0
- data/webpack/components/AnsibleHostDetail/components/RolesTab/EditRolesModal/EditRolesForm.js +90 -0
- data/webpack/components/AnsibleHostDetail/components/RolesTab/EditRolesModal/EditRolesModal.scss +3 -0
- data/webpack/components/AnsibleHostDetail/components/RolesTab/EditRolesModal/EditRolesModalHelper.js +40 -0
- data/webpack/components/AnsibleHostDetail/components/RolesTab/EditRolesModal/index.js +82 -0
- data/webpack/components/AnsibleHostDetail/components/RolesTab/RolesTable.js +129 -0
- data/webpack/components/AnsibleHostDetail/components/RolesTab/__test__/EditRoles.test.js +85 -0
- data/webpack/components/AnsibleHostDetail/components/RolesTab/__test__/RolesTab.fixtures.js +180 -0
- data/webpack/components/AnsibleHostDetail/components/RolesTab/__test__/RolesTab.test.js +75 -0
- data/webpack/components/AnsibleHostDetail/components/RolesTab/index.js +51 -0
- data/webpack/components/AnsibleHostDetail/components/SecondaryTabRoutes.js +60 -0
- data/webpack/components/AnsibleHostDetail/components/TabLayout.js +12 -0
- data/webpack/components/AnsibleHostDetail/constants.js +9 -0
- data/webpack/components/AnsibleHostDetail/helpers.js +4 -0
- data/webpack/components/AnsibleRolesAndVariables/__test__/AnsibleRolesAndVariablesImport.test.js +15 -10
- data/webpack/components/AnsibleRolesSwitcher/components/AnsibleRole.js +29 -0
- data/webpack/components/AnsibleRolesSwitcher/components/AnsibleRole.test.js +3 -0
- data/webpack/components/AnsibleRolesSwitcher/components/AvailableRolesList.js +2 -1
- data/webpack/components/AnsibleRolesSwitcher/components/__snapshots__/AnsibleRole.test.js.snap +3 -3
- data/webpack/components/AnsibleRolesSwitcher/components/__snapshots__/AvailableRolesList.test.js.snap +4 -0
- data/webpack/components/DualList/DualList.scss +3 -0
- data/webpack/components/DualList/ListControls.js +65 -0
- data/webpack/components/DualList/ListHeader.js +16 -0
- data/webpack/components/DualList/ListItem.js +69 -0
- data/webpack/components/DualList/ListPane.js +95 -0
- data/webpack/components/DualList/SelectedStatus.js +21 -0
- data/webpack/components/DualList/index.js +103 -0
- data/webpack/components/ErrorState.js +16 -0
- data/webpack/components/withLoading.js +135 -0
- data/webpack/components/withPagination.js +0 -0
- data/webpack/formHelper.js +131 -0
- data/webpack/globalIdHelper.js +13 -0
- data/webpack/global_index.js +7 -1
- data/webpack/graphql/mutations/assignAnsibleRoles.gql +17 -0
- data/webpack/graphql/mutations/cancelRecurringLogic.gql +12 -0
- data/webpack/graphql/mutations/createAnsibleVariableOverride.gql +28 -0
- data/webpack/graphql/mutations/createJobInvocation.gql +11 -0
- data/webpack/graphql/mutations/deleteAnsibleVariableOverride.gql +17 -0
- data/webpack/graphql/mutations/updateAnsibleVariableOverride.gql +29 -0
- data/webpack/graphql/queries/allAnsibleRoles.gql +13 -0
- data/webpack/graphql/queries/ansibleRoles.gql +13 -0
- data/webpack/graphql/queries/currentUserAttributes.gql +11 -0
- data/webpack/graphql/queries/hostAnsibleRoles.gql +17 -0
- data/webpack/graphql/queries/hostAvailableAnsibleRoles.gql +11 -0
- data/webpack/graphql/queries/hostVariableOverrides.gql +39 -0
- data/webpack/graphql/queries/recurringJobs.gql +28 -0
- data/webpack/helpers/pageParamsHelper.js +40 -0
- data/webpack/helpers/paginationHelper.js +9 -0
- data/webpack/permissionsHelper.js +58 -0
- data/webpack/routes/HostgroupJobs/__test__/HostgroupJobs.fixtures.js +63 -0
- data/webpack/routes/HostgroupJobs/__test__/HostgroupJobs.test.js +112 -0
- data/webpack/routes/HostgroupJobs/index.js +26 -0
- data/webpack/routes/routes.js +10 -0
- data/webpack/testHelper.js +165 -0
- data/webpack/toastHelper.js +4 -0
- metadata +127 -54
- data/app/assets/images/foreman_ansible/Ansible.png +0 -0
- data/app/models/foreman_ansible/fact_name.rb +0 -16
- data/app/models/setting/ansible.rb +0 -106
- data/app/services/foreman_ansible/fact_importer.rb +0 -99
- data/app/services/foreman_ansible/fact_parser.rb +0 -126
- data/app/services/foreman_ansible/fact_sparser.rb +0 -37
- data/app/services/foreman_ansible/operating_system_parser.rb +0 -102
- data/app/services/foreman_ansible/structured_fact_importer.rb +0 -25
- data/test/unit/services/fact_importer_test.rb +0 -52
- data/test/unit/services/fact_parser_test.rb +0 -281
- data/test/unit/services/fact_sparser_test.rb +0 -24
- data/test/unit/services/structured_fact_importer_test.rb +0 -30
- 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;
|