foreman_openscap 4.3.1 → 5.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (105) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/api/v2/compliance/arf_reports_controller.rb +0 -6
  3. data/app/controllers/api/v2/compliance/oval_policies_controller.rb +1 -1
  4. data/app/graphql/mutations/oval_contents/delete.rb +9 -0
  5. data/app/graphql/mutations/oval_policies/delete.rb +9 -0
  6. data/app/graphql/mutations/oval_policies/update.rb +15 -0
  7. data/app/graphql/types/oval_check.rb +11 -0
  8. data/app/graphql/types/oval_content.rb +2 -0
  9. data/app/graphql/types/oval_policy.rb +3 -0
  10. data/app/helpers/arf_report_dashboard_helper.rb +2 -4
  11. data/app/helpers/compliance_hosts_helper.rb +1 -1
  12. data/app/helpers/policies_helper.rb +1 -1
  13. data/app/models/concerns/foreman_openscap/host_extensions.rb +0 -6
  14. data/app/models/concerns/foreman_openscap/oval_facet_hostgroup_extensions.rb +15 -0
  15. data/app/models/foreman_openscap/oval_content.rb +2 -0
  16. data/app/services/foreman_openscap/client_config/base.rb +1 -0
  17. data/app/services/foreman_openscap/client_config/puppet.rb +6 -2
  18. data/app/services/foreman_openscap/oval/configure.rb +1 -1
  19. data/app/services/foreman_openscap/oval/setup.rb +5 -5
  20. data/app/services/foreman_openscap/oval/setup_check.rb +5 -2
  21. data/app/views/api/v2/compliance/oval_contents/destroy.json.rabl +3 -0
  22. data/app/views/arf_reports/_metrics.html.erb +4 -4
  23. data/app/views/compliance_hosts/show.html.erb +4 -6
  24. data/app/views/dashboard/_compliance_reports_breakdown_widget.html.erb +4 -3
  25. data/app/views/policy_dashboard/_policy_chart_widget.html.erb +3 -2
  26. data/db/migrate/20200117135424_migrate_port_overrides_to_int.rb +2 -1
  27. data/db/migrate/20201202110213_update_puppet_port_param_type.rb +2 -1
  28. data/db/migrate/20210819143316_drop_unused_tables.rb +6 -0
  29. data/lib/foreman_openscap/engine.rb +5 -7
  30. data/lib/foreman_openscap/version.rb +1 -1
  31. data/package.json +3 -6
  32. data/test/functional/api/v2/compliance/oval_reports_controller_test.rb +1 -1
  33. data/test/functional/api/v2/compliance/policies_controller_test.rb +2 -0
  34. data/test/graphql/mutations/oval_policies/delete_mutation_test.rb +63 -0
  35. data/test/graphql/queries/oval_content_query_test.rb +29 -0
  36. data/test/helpers/arf_report_dashboard_helper_test.rb +9 -10
  37. data/test/helpers/policy_dashboard_helper_test.rb +1 -1
  38. data/test/test_plugin_helper.rb +9 -4
  39. data/test/unit/policy_test.rb +1 -1
  40. data/test/unit/services/config_name_service_test.rb +1 -0
  41. data/test/unit/services/hostgroup_overrider_test.rb +2 -1
  42. data/test/unit/services/lookup_key_overrider_test.rb +4 -1
  43. data/test/unit/services/oval/setup_check_test.rb +37 -0
  44. data/webpack/components/ConfirmModal.js +63 -0
  45. data/webpack/components/ConfirmModal.scss +3 -0
  46. data/webpack/components/EditableInput.js +157 -0
  47. data/webpack/components/EditableInput.scss +3 -0
  48. data/webpack/components/EmptyState.js +12 -3
  49. data/webpack/components/IndexLayout.js +11 -4
  50. data/webpack/components/IndexTable/index.js +17 -18
  51. data/webpack/components/LinkButton.js +26 -0
  52. data/webpack/components/withDeleteModal.js +51 -0
  53. data/webpack/components/withLoading.js +41 -4
  54. data/webpack/graphql/mutations/deleteOvalContent.gql +9 -0
  55. data/webpack/graphql/mutations/deleteOvalPolicy.gql +9 -0
  56. data/webpack/graphql/mutations/updateOvalPolicy.gql +14 -0
  57. data/webpack/graphql/queries/currentUserAttributes.gql +11 -0
  58. data/webpack/graphql/queries/cves.gql +5 -0
  59. data/webpack/graphql/queries/hostgroups.gql +14 -0
  60. data/webpack/graphql/queries/ovalContent.gql +8 -0
  61. data/webpack/graphql/queries/ovalContents.gql +8 -0
  62. data/webpack/graphql/queries/ovalPolicies.gql +8 -0
  63. data/webpack/graphql/queries/ovalPolicy.gql +5 -0
  64. data/webpack/helpers/formFieldsHelper.js +63 -0
  65. data/webpack/helpers/mutationHelper.js +68 -0
  66. data/webpack/helpers/pathsHelper.js +5 -0
  67. data/webpack/helpers/permissionsHelper.js +42 -0
  68. data/webpack/helpers/toastHelper.js +3 -0
  69. data/webpack/routes/OvalContents/OvalContentsIndex/OvalContentsIndex.js +26 -0
  70. data/webpack/routes/OvalContents/OvalContentsIndex/OvalContentsTable.js +50 -5
  71. data/webpack/routes/OvalContents/OvalContentsIndex/__tests__/OvalContentsDestroy.fixtures.js +105 -0
  72. data/webpack/routes/OvalContents/OvalContentsIndex/__tests__/OvalContentsDestroy.test.js +124 -0
  73. data/webpack/routes/OvalContents/OvalContentsIndex/__tests__/OvalContentsIndex.fixtures.js +93 -77
  74. data/webpack/routes/OvalContents/OvalContentsIndex/__tests__/OvalContentsIndex.test.js +53 -6
  75. data/webpack/routes/OvalContents/OvalContentsIndex/index.js +7 -1
  76. data/webpack/routes/OvalContents/OvalContentsNew/OvalContentsNew.js +138 -0
  77. data/webpack/routes/OvalContents/OvalContentsNew/OvalContentsNew.scss +3 -0
  78. data/webpack/routes/OvalContents/OvalContentsNew/OvalContentsNewHelper.js +73 -0
  79. data/webpack/routes/OvalContents/OvalContentsNew/__tests__/OvalContentsNew.test.js +104 -0
  80. data/webpack/routes/OvalContents/OvalContentsNew/index.js +13 -0
  81. data/webpack/routes/OvalContents/OvalContentsShow/OvalContentsShow.js +62 -0
  82. data/webpack/routes/OvalContents/OvalContentsShow/OvalContentsShow.test.js +45 -0
  83. data/webpack/routes/OvalContents/OvalContentsShow/OvalContentsShowHelper.js +0 -0
  84. data/webpack/routes/OvalContents/OvalContentsShow/index.js +35 -0
  85. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/OvalPoliciesIndex.js +18 -2
  86. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/OvalPoliciesTable.js +16 -3
  87. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/__tests__/OvalPoliciesDestroy.fixtures.js +101 -0
  88. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/__tests__/OvalPoliciesDestroy.test.js +117 -0
  89. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/__tests__/OvalPoliciesIndex.fixtures.js +71 -21
  90. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/__tests__/OvalPoliciesIndex.test.js +34 -2
  91. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/index.js +7 -1
  92. data/webpack/routes/OvalPolicies/OvalPoliciesShow/CvesTab.js +1 -0
  93. data/webpack/routes/OvalPolicies/OvalPoliciesShow/DetailsTab.js +85 -0
  94. data/webpack/routes/OvalPolicies/OvalPoliciesShow/HostgroupsTab.js +49 -0
  95. data/webpack/routes/OvalPolicies/OvalPoliciesShow/HostgroupsTable.js +38 -0
  96. data/webpack/routes/OvalPolicies/OvalPoliciesShow/OvalPoliciesShow.js +15 -12
  97. data/webpack/routes/OvalPolicies/OvalPoliciesShow/OvalPoliciesShowHelper.js +77 -0
  98. data/webpack/routes/OvalPolicies/OvalPoliciesShow/__tests__/OvalPoliciesEdit.fixtures.js +48 -0
  99. data/webpack/routes/OvalPolicies/OvalPoliciesShow/__tests__/OvalPoliciesEdit.test.js +175 -0
  100. data/webpack/routes/OvalPolicies/OvalPoliciesShow/__tests__/OvalPoliciesShow.fixtures.js +40 -4
  101. data/webpack/routes/OvalPolicies/OvalPoliciesShow/__tests__/OvalPoliciesShow.test.js +64 -4
  102. data/webpack/routes/OvalPolicies/OvalPoliciesShow/index.js +4 -0
  103. data/webpack/routes/routes.js +14 -0
  104. data/webpack/testHelper.js +42 -2
  105. metadata +53 -7
@@ -1,6 +1,11 @@
1
1
  import policiesQuery from '../../../../graphql/queries/ovalPolicies.gql';
2
2
  import { ovalPoliciesPath } from '../../../../helpers/pathsHelper';
3
- import { mockFactory } from '../../../../testHelper';
3
+ import {
4
+ mockFactory,
5
+ admin,
6
+ intruder,
7
+ userFactory,
8
+ } from '../../../../testHelper';
4
9
 
5
10
  const policiesMockFactory = mockFactory('ovalPolicies', policiesQuery);
6
11
 
@@ -14,23 +19,37 @@ export const pageParamsHistoryMock = {
14
19
  push: pushMock,
15
20
  };
16
21
 
22
+ const viewer = userFactory('viewer', [
23
+ {
24
+ __typename: 'Permission',
25
+ id: 'MDE6UGVybWlzc2lvbi0yOTY=',
26
+ name: 'view_oval_policies',
27
+ },
28
+ ]);
29
+
30
+ const firstPolicy = (meta = { canDestroy: true }) => ({
31
+ __typename: 'ForemanOpenscap::OvalPolicy',
32
+ id: 'MDE6Rm9yZW1hbk9wZW5zY2FwOjpPdmFsUG9saWN5LTE=',
33
+ name: 'first policy',
34
+ meta,
35
+ ovalContent: { name: 'first content' },
36
+ });
37
+ const secondPolicy = (meta = { canDestroy: true }) => ({
38
+ __typename: 'ForemanOpenscap::OvalPolicy',
39
+ id: 'MDE6Rm9yZW1hbk9wZW5zY2FwOjpPdmFsUG9saWN5LTQw',
40
+ name: 'second policy',
41
+ meta,
42
+ ovalContent: { name: 'second content' },
43
+ });
44
+ const policiesData = {
45
+ totalCount: 2,
46
+ nodes: [firstPolicy(), secondPolicy()],
47
+ };
48
+
17
49
  export const mocks = policiesMockFactory(
18
50
  { first: 20, last: 20 },
19
- {
20
- totalCount: 2,
21
- nodes: [
22
- {
23
- id: 'abc',
24
- name: 'first policy',
25
- ovalContent: { name: 'first content' },
26
- },
27
- {
28
- id: 'xyz',
29
- name: 'second policy',
30
- ovalContent: { name: 'second content' },
31
- },
32
- ],
33
- }
51
+ policiesData,
52
+ { currentUser: admin }
34
53
  );
35
54
  export const pageParamsMocks = policiesMockFactory(
36
55
  { first: 10, last: 5 },
@@ -38,24 +57,55 @@ export const pageParamsMocks = policiesMockFactory(
38
57
  totalCount: 7,
39
58
  nodes: [
40
59
  {
41
- id: 'xyz',
60
+ __typename: 'ForemanOpenscap::OvalPolicy',
61
+ id: 'MDE6Rm9yZW1hbk9wZW5zY2FwOjpPdmFsUG9saWN5LTQx',
42
62
  name: 'sixth policy',
63
+ meta: { canDestroy: true },
43
64
  ovalContent: { name: 'sixth content' },
44
65
  },
45
66
  {
46
- id: 'abc',
67
+ __typename: 'ForemanOpenscap::OvalPolicy',
68
+ id: 'MDE6Rm9yZW1hbk9wZW5zY2FwOjpPdmFsUG9saWN5LTQy',
47
69
  name: 'seventh policy',
70
+ meta: { canDestroy: true },
48
71
  ovalContent: { name: 'seventh content' },
49
72
  },
50
73
  ],
51
- }
74
+ },
75
+ { currentUser: admin }
52
76
  );
77
+
53
78
  export const emptyMocks = policiesMockFactory(
54
79
  { first: 20, last: 20 },
55
- { totalCount: 0, nodes: [] }
80
+ { totalCount: 0, nodes: [] },
81
+ { currentUser: admin }
56
82
  );
57
83
  export const errorMocks = policiesMockFactory(
58
84
  { first: 20, last: 20 },
59
85
  { totalCount: 0, nodes: [] },
60
- [{ message: 'Something very bad happened.' }]
86
+ {
87
+ errors: [{ message: 'Something very bad happened.', path: 'base' }],
88
+ currentUser: admin,
89
+ }
90
+ );
91
+ export const viewerMocks = policiesMockFactory(
92
+ { first: 20, last: 20 },
93
+ policiesData,
94
+ { currentUser: viewer }
95
+ );
96
+ export const unauthorizedMocks = policiesMockFactory(
97
+ { first: 20, last: 20 },
98
+ policiesData,
99
+ { currentUser: intruder }
100
+ );
101
+ export const noDeleteMocks = policiesMockFactory(
102
+ { first: 20, last: 20 },
103
+ {
104
+ totalCount: 2,
105
+ nodes: [
106
+ firstPolicy({ canDestroy: false }),
107
+ secondPolicy({ canDestroy: false }),
108
+ ],
109
+ },
110
+ { currentUser: admin }
61
111
  );
@@ -7,6 +7,7 @@ import '@testing-library/jest-dom';
7
7
  import {
8
8
  withMockedProvider,
9
9
  withRouter,
10
+ withRedux,
10
11
  tick,
11
12
  historyMock,
12
13
  } from '../../../../testHelper';
@@ -18,12 +19,16 @@ import {
18
19
  pageParamsHistoryMock,
19
20
  emptyMocks,
20
21
  errorMocks,
22
+ viewerMocks,
23
+ unauthorizedMocks,
21
24
  } from './OvalPoliciesIndex.fixtures';
22
25
 
23
- import OvalPoliciesIndex from '../OvalPoliciesIndex';
26
+ import OvalPoliciesIndex from '../index';
24
27
  import { ovalPoliciesPath } from '../../../../helpers/pathsHelper';
25
28
 
26
- const TestComponent = withRouter(withMockedProvider(OvalPoliciesIndex));
29
+ const TestComponent = withRouter(
30
+ withRedux(withMockedProvider(OvalPoliciesIndex))
31
+ );
27
32
 
28
33
  describe('OvalPoliciesIndex', () => {
29
34
  it('should load page', async () => {
@@ -40,6 +45,15 @@ describe('OvalPoliciesIndex', () => {
40
45
  expect(within(pageItems).getByText(/1 - 2/)).toBeInTheDocument();
41
46
  expect(within(pageItems).getByText('of')).toBeInTheDocument();
42
47
  expect(within(pageItems).getByText('2')).toBeInTheDocument();
48
+
49
+ expect(screen.getByText('first policy').closest('a')).toHaveAttribute(
50
+ 'href',
51
+ '/experimental/compliance/oval_policies/1'
52
+ );
53
+ expect(screen.getByText('second policy').closest('a')).toHaveAttribute(
54
+ 'href',
55
+ '/experimental/compliance/oval_policies/40'
56
+ );
43
57
  });
44
58
  it('should load page with page params', async () => {
45
59
  const { container } = render(
@@ -75,4 +89,22 @@ describe('OvalPoliciesIndex', () => {
75
89
  ).toBeInTheDocument();
76
90
  expect(screen.getByText('Error!')).toBeInTheDocument();
77
91
  });
92
+ it('should load page for user with permissions', async () => {
93
+ render(<TestComponent history={historyMock} mocks={viewerMocks} />);
94
+ await waitFor(tick);
95
+ expect(screen.queryByText('Loading')).not.toBeInTheDocument();
96
+ expect(screen.getByText('first policy')).toBeInTheDocument();
97
+ });
98
+ it('should not load page for user without permissions', async () => {
99
+ render(<TestComponent history={historyMock} mocks={unauthorizedMocks} />);
100
+ await waitFor(tick);
101
+ expect(screen.queryByText('Loading')).not.toBeInTheDocument();
102
+ expect(screen.queryByText('first policy')).not.toBeInTheDocument();
103
+ expect(
104
+ screen.getByText(
105
+ 'You are not authorized to view the page. Request the following permissions from administrator: view_oval_policies.'
106
+ )
107
+ ).toBeInTheDocument();
108
+ expect(screen.getByText('Permission denied')).toBeInTheDocument();
109
+ });
78
110
  });
@@ -1,7 +1,13 @@
1
1
  import React from 'react';
2
+ import { useDispatch } from 'react-redux';
3
+ import { showToast } from '../../../helpers/toastHelper';
2
4
 
3
5
  import OvalPoliciesIndex from './OvalPoliciesIndex';
4
6
 
5
- const WrappedOvalPoliciesIndex = props => <OvalPoliciesIndex {...props} />;
7
+ const WrappedOvalPoliciesIndex = props => {
8
+ const dispatch = useDispatch();
9
+
10
+ return <OvalPoliciesIndex {...props} showToast={showToast(dispatch)} />;
11
+ };
6
12
 
7
13
  export default WrappedOvalPoliciesIndex;
@@ -36,6 +36,7 @@ const CvesTab = props => {
36
36
  resultPath="cves.nodes"
37
37
  pagination={pagination}
38
38
  emptyStateTitle={__('No CVEs found.')}
39
+ permissions={['view_oval_policies']}
39
40
  />
40
41
  );
41
42
  };
@@ -0,0 +1,85 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { useMutation } from '@apollo/client';
4
+
5
+ import {
6
+ TextList,
7
+ TextContent,
8
+ TextArea,
9
+ TextListItem,
10
+ TextListVariants,
11
+ TextListItemVariants,
12
+ TextInput,
13
+ } from '@patternfly/react-core';
14
+
15
+ import { translate as __ } from 'foremanReact/common/I18n';
16
+
17
+ import EditableInput from '../../../components/EditableInput';
18
+
19
+ import { onAttrUpdate, policySchedule } from './OvalPoliciesShowHelper';
20
+ import updateOvalPolicyMutation from '../../../graphql/mutations/updateOvalPolicy.gql';
21
+
22
+ const DetailsTab = props => {
23
+ const { policy, showToast } = props;
24
+
25
+ const [callMutation] = useMutation(updateOvalPolicyMutation);
26
+
27
+ return (
28
+ <TextContent className="pf-u-pt-md">
29
+ <TextList component={TextListVariants.dl}>
30
+ <TextListItem component={TextListItemVariants.dt}>
31
+ {__('Name')}
32
+ </TextListItem>
33
+ <TextListItem
34
+ aria-label="label text value"
35
+ component={TextListItemVariants.dd}
36
+ className="foreman-spaced-list"
37
+ >
38
+ <EditableInput
39
+ value={policy.name}
40
+ onConfirm={onAttrUpdate('name', policy, callMutation, showToast)}
41
+ component={TextInput}
42
+ attrName="name"
43
+ />
44
+ </TextListItem>
45
+ <TextListItem component={TextListItemVariants.dt}>
46
+ {__('Period')}
47
+ </TextListItem>
48
+ <TextListItem
49
+ aria-label="label text value"
50
+ component={TextListItemVariants.dd}
51
+ className="foreman-spaced-list"
52
+ >
53
+ {policySchedule(policy)}
54
+ </TextListItem>
55
+ <TextListItem component={TextListItemVariants.dt}>
56
+ {__('Description')}
57
+ </TextListItem>
58
+ <TextListItem
59
+ aria-label="label text value"
60
+ component={TextListItemVariants.dd}
61
+ className="foreman-spaced-list"
62
+ >
63
+ <EditableInput
64
+ value={policy.description}
65
+ onConfirm={onAttrUpdate(
66
+ 'description',
67
+ policy,
68
+ callMutation,
69
+ showToast
70
+ )}
71
+ component={TextArea}
72
+ attrName="description"
73
+ />
74
+ </TextListItem>
75
+ </TextList>
76
+ </TextContent>
77
+ );
78
+ };
79
+
80
+ DetailsTab.propTypes = {
81
+ policy: PropTypes.object.isRequired,
82
+ showToast: PropTypes.func.isRequired,
83
+ };
84
+
85
+ export default DetailsTab;
@@ -0,0 +1,49 @@
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
+
7
+ import HostgroupsTable from './HostgroupsTable';
8
+
9
+ import hostgroups from '../../../graphql/queries/hostgroups.gql';
10
+ import {
11
+ useParamsToVars,
12
+ useCurrentPagination,
13
+ } from '../../../helpers/pageParamsHelper';
14
+
15
+ const HostgroupsTab = props => {
16
+ const useFetchFn = componentProps =>
17
+ useQuery(hostgroups, {
18
+ variables: {
19
+ search: `oval_policy_id = ${componentProps.match.params.id}`,
20
+ ...useParamsToVars(componentProps.history),
21
+ },
22
+ });
23
+
24
+ const renameData = data => ({
25
+ hostgroups: data.hostgroups.nodes,
26
+ totalCount: data.hostgroups.totalCount,
27
+ });
28
+
29
+ const pagination = useCurrentPagination(props.history);
30
+
31
+ return (
32
+ <HostgroupsTable
33
+ {...props}
34
+ fetchFn={useFetchFn}
35
+ renameData={renameData}
36
+ resultPath="hostgroups.nodes"
37
+ pagination={pagination}
38
+ emptyStateTitle={__('No Hostgroups found.')}
39
+ permissions={['view_hostgroups']}
40
+ />
41
+ );
42
+ };
43
+
44
+ HostgroupsTab.propTypes = {
45
+ match: PropTypes.object.isRequired,
46
+ history: PropTypes.object.isRequired,
47
+ };
48
+
49
+ export default HostgroupsTab;
@@ -0,0 +1,38 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { translate as __ } from 'foremanReact/common/I18n';
4
+
5
+ import withLoading from '../../../components/withLoading';
6
+ import IndexTable from '../../../components/IndexTable';
7
+
8
+ const CvesTable = props => {
9
+ const columns = [{ title: __('Name') }];
10
+
11
+ const rows = props.hostgroups.map(hostgroup => ({
12
+ cells: [{ title: hostgroup.name }],
13
+ hostgroup,
14
+ }));
15
+
16
+ const actions = [];
17
+
18
+ return (
19
+ <IndexTable
20
+ columns={columns}
21
+ rows={rows}
22
+ actions={actions}
23
+ pagination={props.pagination}
24
+ totalCount={props.totalCount}
25
+ history={props.history}
26
+ ariaTableLabel={__('Table of hostgroups for OVAL policy')}
27
+ />
28
+ );
29
+ };
30
+
31
+ CvesTable.propTypes = {
32
+ hostgroups: PropTypes.array.isRequired,
33
+ pagination: PropTypes.object.isRequired,
34
+ totalCount: PropTypes.number.isRequired,
35
+ history: PropTypes.object.isRequired,
36
+ };
37
+
38
+ export default withLoading(CvesTable);
@@ -7,20 +7,19 @@ import {
7
7
  Button,
8
8
  Grid,
9
9
  GridItem,
10
- TextContent,
11
10
  Text,
12
11
  TextVariants,
13
12
  Tabs,
14
13
  Tab,
15
14
  TabTitleText,
16
15
  } from '@patternfly/react-core';
17
- import '@patternfly/patternfly/patternfly-addons.scss';
18
16
 
19
17
  import withLoading from '../../../components/withLoading';
20
-
21
18
  import CvesTab from './CvesTab';
19
+ import HostgroupsTab from './HostgroupsTab';
20
+ import DetailsTab from './DetailsTab';
22
21
 
23
- import { policySchedule, newJobFormPath } from './OvalPoliciesShowHelper';
22
+ import { newJobFormPath } from './OvalPoliciesShowHelper';
24
23
  import { resolvePath } from '../../../helpers/pathsHelper';
25
24
 
26
25
  const OvalPoliciesShow = props => {
@@ -51,18 +50,22 @@ const OvalPoliciesShow = props => {
51
50
  <Tabs mountOnEnter activeKey={activeTab} onSelect={handleTabSelect}>
52
51
  <Tab
53
52
  eventKey="details"
54
- title={<TabTitleText>Details</TabTitleText>}
53
+ title={<TabTitleText>{__('Details')}</TabTitleText>}
55
54
  >
56
- <TextContent className="pf-u-pt-md">
57
- <Text component={TextVariants.h3}>Period</Text>
58
- <Text component={TextVariants.p}>{policySchedule(policy)}</Text>
59
- <Text component={TextVariants.h3}>Description</Text>
60
- <Text component={TextVariants.p}>{policy.description}</Text>
61
- </TextContent>
55
+ <DetailsTab {...props} />
62
56
  </Tab>
63
- <Tab eventKey="cves" title={<TabTitleText>CVEs</TabTitleText>}>
57
+ <Tab
58
+ eventKey="cves"
59
+ title={<TabTitleText>{__('CVEs')}</TabTitleText>}
60
+ >
64
61
  <CvesTab {...props} />
65
62
  </Tab>
63
+ <Tab
64
+ eventKey="hostgroups"
65
+ title={<TabTitleText>{__('Hostgroups')}</TabTitleText>}
66
+ >
67
+ <HostgroupsTab {...props} />
68
+ </Tab>
66
69
  </Tabs>
67
70
  </GridItem>
68
71
  </Grid>
@@ -1,3 +1,5 @@
1
+ import { translate as __, sprintf } from 'foremanReact/common/I18n';
2
+
1
3
  import { decodeId } from '../../../helpers/globalIdHelper';
2
4
  import { addSearch } from '../../../helpers/pageParamsHelper';
3
5
  import { newJobPath } from '../../../helpers/pathsHelper';
@@ -37,3 +39,78 @@ export const newJobFormPath = (policy, policyId) =>
37
39
  host_ids: targetingScopedSearchQuery(policy),
38
40
  'inputs[oval_policies]': policyId,
39
41
  });
42
+
43
+ const policyToAttrs = (policy, attrs) =>
44
+ Object.entries(policy).reduce((memo, [key, value]) => {
45
+ if (attrs.includes(key)) {
46
+ memo[key] = value;
47
+ }
48
+ return memo;
49
+ }, {});
50
+
51
+ const onUpdateSuccess = (
52
+ closeEditable,
53
+ stopSubmitting,
54
+ showToast,
55
+ attr,
56
+ onValidationError
57
+ ) => result => {
58
+ const { errors } = result.data.updateOvalPolicy;
59
+ if (Array.isArray(errors) && errors.length > 0) {
60
+ stopSubmitting();
61
+ if (
62
+ errors.length === 1 &&
63
+ errors[0].path.join(' ') === `attributes ${attr}`
64
+ ) {
65
+ onValidationError(errors[0].message);
66
+ } else {
67
+ showToast({
68
+ type: 'error',
69
+ message: formatError(joinErrors(errors)),
70
+ });
71
+ }
72
+ } else {
73
+ closeEditable();
74
+ showToast({
75
+ type: 'success',
76
+ message: __('OVAL policy was successfully updated.'),
77
+ });
78
+ }
79
+ };
80
+
81
+ const formatError = error =>
82
+ sprintf(
83
+ __('There was a following error when updating OVAL policy: %s'),
84
+ error
85
+ );
86
+
87
+ const joinErrors = errors => errors.map(err => err.message).join(', ');
88
+
89
+ const onUpdateError = (showToast, stopSubmitting) => error => {
90
+ stopSubmitting();
91
+ showToast({ type: 'error', message: formatError(error.message) });
92
+ };
93
+
94
+ export const onAttrUpdate = (attr, policy, callMutation, showToast) => (
95
+ newValue,
96
+ closeEditable,
97
+ stopSubmitting,
98
+ onValidationError
99
+ ) => {
100
+ const vars = policyToAttrs(policy, ['id', 'name', 'description', 'cronLine']);
101
+ vars[attr] = newValue;
102
+ return (
103
+ callMutation({ variables: vars })
104
+ // eslint-disable-next-line promise/prefer-await-to-then
105
+ .then(
106
+ onUpdateSuccess(
107
+ closeEditable,
108
+ stopSubmitting,
109
+ showToast,
110
+ attr,
111
+ onValidationError
112
+ )
113
+ )
114
+ .catch(onUpdateError(showToast, stopSubmitting))
115
+ );
116
+ };
@@ -0,0 +1,48 @@
1
+ import { mockFactory } from '../../../../testHelper';
2
+ import updateOvalPolicyMutation from '../../../../graphql/mutations/updateOvalPolicy.gql';
3
+ import { ovalPolicy } from './OvalPoliciesShow.fixtures';
4
+
5
+ const updateOvalPolicyMockFactory = mockFactory(
6
+ 'updateOvalPolicy',
7
+ updateOvalPolicyMutation
8
+ );
9
+
10
+ export const updatedName = 'updated policy name';
11
+
12
+ const variables = {
13
+ id: ovalPolicy.id,
14
+ name: updatedName,
15
+ cronLine: ovalPolicy.cronLine,
16
+ description: ovalPolicy.description,
17
+ };
18
+ const responsePolicy = {
19
+ ovalPolicy: {
20
+ __typename: 'ForemanOpenscap::OvalPolicy',
21
+ id: ovalPolicy.id,
22
+ name: updatedName,
23
+ description: ovalPolicy.description,
24
+ cronLine: ovalPolicy.cronLine,
25
+ },
26
+ errors: [],
27
+ };
28
+
29
+ export const policyUpdateMock = updateOvalPolicyMockFactory(
30
+ variables,
31
+ responsePolicy
32
+ );
33
+
34
+ export const policyUpdateErrorMock = updateOvalPolicyMockFactory(
35
+ variables,
36
+ responsePolicy,
37
+ { errors: [{ message: 'This is an unexpected failure.' }] }
38
+ );
39
+
40
+ export const policyUpdateValidationMock = updateOvalPolicyMockFactory(
41
+ variables,
42
+ {
43
+ ovalPolicy,
44
+ errors: [
45
+ { path: ['attributes', 'name'], message: 'has already been taken' },
46
+ ],
47
+ }
48
+ );