foreman_rh_cloud 3.0.19 → 3.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +17 -2
  3. data/app/controllers/insights_cloud/hits_controller.rb +37 -0
  4. data/app/controllers/insights_cloud/settings_controller.rb +1 -1
  5. data/app/controllers/insights_cloud/tasks_controller.rb +1 -2
  6. data/app/models/inventory_sync/inventory_status.rb +6 -0
  7. data/app/models/setting/rh_cloud.rb +5 -5
  8. data/app/services/foreman_rh_cloud/remediations_retriever.rb +78 -0
  9. data/app/services/foreman_rh_cloud/template_renderer_helper.rb +22 -0
  10. data/app/views/job_templates/rh_cloud_remediations.erb +14 -0
  11. data/config/routes.rb +1 -0
  12. data/db/migrate/20210404000001_change_resolutions.foreman_rh_cloud.rb +10 -0
  13. data/db/seeds.d/50_job_templates.rb +14 -0
  14. data/lib/foreman_rh_cloud/engine.rb +13 -1
  15. data/lib/foreman_rh_cloud/version.rb +1 -1
  16. data/lib/insights_cloud.rb +12 -0
  17. data/lib/insights_cloud/async/insights_full_sync.rb +16 -6
  18. data/lib/insights_cloud/async/insights_resolutions_sync.rb +69 -0
  19. data/lib/insights_cloud/async/insights_rules_sync.rb +13 -17
  20. data/lib/insights_cloud/async/insights_scheduled_sync.rb +1 -1
  21. data/lib/tasks/rh_cloud_inventory.rake +1 -1
  22. data/package.json +1 -1
  23. data/test/factories/insights_factories.rb +22 -0
  24. data/test/jobs/insights_full_sync_test.rb +12 -8
  25. data/test/jobs/insights_resolutions_sync_test.rb +74 -0
  26. data/test/jobs/insights_rules_sync_test.rb +5 -3
  27. data/test/jobs/inventory_full_sync_test.rb +1 -1
  28. data/test/unit/services/foreman_rh_cloud/remediations_retriever_test.rb +49 -0
  29. data/test/unit/services/foreman_rh_cloud/template_renderer_helper_test.rb +28 -0
  30. data/webpack/ForemanInventoryUpload/Components/InventorySettings/AdvancedSetting/AdvancedSettingsConstants.js +5 -3
  31. data/webpack/ForemanInventoryUpload/Components/PageHeader/components/PageDescription/PageDescription.js +15 -2
  32. data/webpack/ForemanInventoryUpload/Components/PageHeader/components/PageDescription/__tests__/__snapshots__/PageDescription.test.js.snap +13 -2
  33. data/webpack/InsightsCloudSync/Components/InsightsTable/InsightsTable.js +1 -1
  34. data/webpack/InsightsCloudSync/Components/InsightsTable/InsightsTableSelectors.js +3 -0
  35. data/webpack/InsightsCloudSync/Components/RemediationModal/RemediateButton.js +59 -0
  36. data/webpack/InsightsCloudSync/Components/RemediationModal/RemediationActions.js +12 -0
  37. data/webpack/InsightsCloudSync/Components/RemediationModal/RemediationHelpers.js +43 -0
  38. data/webpack/InsightsCloudSync/Components/RemediationModal/RemediationModal.js +101 -0
  39. data/webpack/InsightsCloudSync/Components/RemediationModal/RemediationModal.scss +9 -0
  40. data/webpack/InsightsCloudSync/Components/RemediationModal/RemediationModalFooter.js +43 -0
  41. data/webpack/InsightsCloudSync/Components/RemediationModal/RemediationTableConstants.js +38 -0
  42. data/webpack/InsightsCloudSync/Components/RemediationModal/Resolutions.js +55 -0
  43. data/webpack/InsightsCloudSync/Components/RemediationModal/index.js +34 -0
  44. data/webpack/InsightsCloudSync/InsightsCloudSync.js +8 -3
  45. data/webpack/InsightsCloudSync/InsightsCloudSync.scss +5 -0
  46. data/webpack/InsightsCloudSync/__snapshots__/InsightsCloudSync.test.js.snap +9 -6
  47. data/webpack/{InsightsCloudSync/Components/InsightsTable/components → common/table}/EmptyState.js +0 -0
  48. data/webpack/common/table/helpers.js +7 -0
  49. metadata +33 -9
@@ -6,7 +6,7 @@ import { Table, TableHeader, TableBody } from '@patternfly/react-table';
6
6
  import { useForemanSettings } from 'foremanReact/Root/Context/ForemanContext';
7
7
  import SelectAllAlert from './SelectAllAlert';
8
8
  import { columns } from './InsightsTableConstants';
9
- import TableEmptyState from './components/EmptyState';
9
+ import TableEmptyState from '../../../common/table/EmptyState';
10
10
  import {
11
11
  modifySelectedRows,
12
12
  getSortColumnIndex,
@@ -76,3 +76,6 @@ export const selectItemCount = state =>
76
76
 
77
77
  export const selectHasToken = state =>
78
78
  selectAPIResponse(state, INSIGHTS_HITS_API_KEY).hasToken;
79
+
80
+ export const selectExperimental = state =>
81
+ selectAPIResponse(state, INSIGHTS_HITS_API_KEY).isExperimentalMode;
@@ -0,0 +1,59 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { isEmpty } from 'lodash';
4
+ import { Button, Popover } from '@patternfly/react-core';
5
+ import { translate as __ } from 'foremanReact/common/I18n';
6
+ import { foremanUrl } from '../../../ForemanRhCloudHelpers';
7
+
8
+ const RemediateButton = ({ isExperimentalMode, selectedIds, toggleModal }) => {
9
+ const [isVisible, setVisible] = React.useState(true);
10
+
11
+ const popoverContent = __(
12
+ `To use this feature, please enable <a href=${foremanUrl(
13
+ '/settings?search=name+%3D+lab_features'
14
+ )}>Show Experimental Labs</a> in settings.`
15
+ );
16
+
17
+ let button = (
18
+ <Button
19
+ variant="primary"
20
+ isDisabled={isEmpty(selectedIds)}
21
+ onClick={() => {
22
+ if (!isExperimentalMode) {
23
+ setVisible(value => !value);
24
+ } else {
25
+ toggleModal();
26
+ }
27
+ }}
28
+ >
29
+ {__('Remediate')}
30
+ </Button>
31
+ );
32
+
33
+ if (!isExperimentalMode) {
34
+ button = (
35
+ <Popover
36
+ isVisible={isVisible}
37
+ bodyContent={
38
+ <div dangerouslySetInnerHTML={{ __html: popoverContent }} />
39
+ }
40
+ >
41
+ {button}
42
+ </Popover>
43
+ );
44
+ }
45
+ return button;
46
+ };
47
+
48
+ RemediateButton.propTypes = {
49
+ selectedIds: PropTypes.object,
50
+ isExperimentalMode: PropTypes.bool,
51
+ toggleModal: PropTypes.func.isRequired,
52
+ };
53
+
54
+ RemediateButton.defaultProps = {
55
+ selectedIds: {},
56
+ isExperimentalMode: false,
57
+ };
58
+
59
+ export default RemediateButton;
@@ -0,0 +1,12 @@
1
+ import { get } from 'foremanReact/redux/API';
2
+ import {
3
+ REMEDIATIONS_API_KEY,
4
+ REMEDIATIONS_PATH,
5
+ } from './RemediationTableConstants';
6
+
7
+ export const fetchRemediations = ({ selectedIds, isAllSelected, query }) =>
8
+ get({
9
+ key: REMEDIATIONS_API_KEY,
10
+ url: REMEDIATIONS_PATH,
11
+ params: { ids: Object.keys(selectedIds), isAllSelected, query },
12
+ });
@@ -0,0 +1,43 @@
1
+ /* eslint-disable camelcase */
2
+ import React from 'react';
3
+ import { orderBy } from 'lodash';
4
+ import Resolutions from './Resolutions';
5
+
6
+ export const modifyRows = (remediations, setResolutions, setHostsIds) => {
7
+ if (remediations.length === 0) return [];
8
+
9
+ const resolutionToSubmit = [];
10
+ const hostsIdsToSubmit = new Set();
11
+ const modifiedRemediations = orderBy(
12
+ remediations.asMutable(),
13
+ [r => r.resolutions?.length || 0],
14
+ ['desc']
15
+ ).map(({ id, host_id, hostname, title, resolutions, reboot }) => {
16
+ hostsIdsToSubmit.add(host_id);
17
+ const selectedResolution = resolutions[0]?.id;
18
+ resolutionToSubmit.push({
19
+ hit_id: id,
20
+ resolution_id: selectedResolution /** defaults to the first resolution if many */,
21
+ });
22
+ return {
23
+ cells: [
24
+ hostname,
25
+ title,
26
+ <div>
27
+ <Resolutions
28
+ hit_id={id}
29
+ resolutions={resolutions}
30
+ setResolutions={setResolutions}
31
+ selectedResolution={selectedResolution}
32
+ />
33
+ </div>,
34
+ reboot,
35
+ ],
36
+ id,
37
+ };
38
+ });
39
+
40
+ setResolutions(resolutionToSubmit);
41
+ setHostsIds(Array.from(hostsIdsToSubmit));
42
+ return modifiedRemediations;
43
+ };
@@ -0,0 +1,101 @@
1
+ /* eslint-disable react-hooks/exhaustive-deps */
2
+ import React, { useEffect } from 'react';
3
+ import PropTypes from 'prop-types';
4
+ import { Table, TableHeader, TableBody } from '@patternfly/react-table';
5
+ import { Modal, ModalVariant } from '@patternfly/react-core';
6
+ import { STATUS } from 'foremanReact/constants';
7
+ import { translate as __ } from 'foremanReact/common/I18n';
8
+ import { columns } from './RemediationTableConstants';
9
+ import { modifyRows } from './RemediationHelpers';
10
+ import ModalFooter from './RemediationModalFooter';
11
+ import TableEmptyState from '../../../common/table/EmptyState';
12
+ import './RemediationModal.scss';
13
+ import RemediateButton from './RemediateButton';
14
+
15
+ const RemediationModal = ({
16
+ selectedIds,
17
+ fetchRemediations,
18
+ remediations,
19
+ status,
20
+ error,
21
+ isAllSelected,
22
+ query,
23
+ isExperimentalMode,
24
+ }) => {
25
+ const [rows, setRows] = React.useState([]);
26
+ const [open, setOpen] = React.useState(false);
27
+ const [resolutions, setResolutions] = React.useState([]);
28
+ const [hostsIds, setHostsIds] = React.useState([]);
29
+ const toggleModal = () => setOpen(prevValue => !prevValue);
30
+
31
+ useEffect(() => {
32
+ if (open) fetchRemediations({ selectedIds, isAllSelected, query });
33
+ }, [open]);
34
+
35
+ useEffect(() => {
36
+ const modifiedRows =
37
+ status === STATUS.PENDING
38
+ ? []
39
+ : modifyRows(remediations, setResolutions, setHostsIds);
40
+ setRows(modifiedRows);
41
+ }, [remediations, status]);
42
+
43
+ return (
44
+ <React.Fragment>
45
+ <RemediateButton
46
+ isExperimentalMode={isExperimentalMode}
47
+ selectedIds={selectedIds}
48
+ toggleModal={toggleModal}
49
+ />{' '}
50
+ <Modal
51
+ id="remediation-modal"
52
+ appendTo={document.body}
53
+ variant={ModalVariant.large}
54
+ title={__('Remediation summary')}
55
+ isOpen={open}
56
+ onClose={toggleModal}
57
+ footer={
58
+ <ModalFooter
59
+ toggleModal={toggleModal}
60
+ resolutions={resolutions}
61
+ hostsIds={hostsIds}
62
+ />
63
+ }
64
+ >
65
+ <Table
66
+ className="remediations-table"
67
+ aria-label="remediations Table"
68
+ cells={columns}
69
+ rows={rows}
70
+ >
71
+ <TableHeader />
72
+ <TableBody />
73
+ </Table>
74
+ <TableEmptyState status={status} error={error} />
75
+ </Modal>
76
+ </React.Fragment>
77
+ );
78
+ };
79
+
80
+ RemediationModal.propTypes = {
81
+ selectedIds: PropTypes.object,
82
+ fetchRemediations: PropTypes.func.isRequired,
83
+ remediations: PropTypes.array,
84
+ status: PropTypes.string,
85
+ error: PropTypes.string,
86
+ isAllSelected: PropTypes.bool,
87
+ query: PropTypes.string,
88
+ isExperimentalMode: PropTypes.bool,
89
+ };
90
+
91
+ RemediationModal.defaultProps = {
92
+ selectedIds: {},
93
+ remediations: [],
94
+ status: null,
95
+ error: null,
96
+ isAllSelected: false,
97
+ query: null,
98
+ isExperimentalMode: false,
99
+ };
100
+
101
+ export default RemediationModal;
@@ -0,0 +1,9 @@
1
+ #remediation-modal {
2
+ height: 80vh;
3
+
4
+ .resolution-radio {
5
+ label {
6
+ margin-top: 5px;
7
+ }
8
+ }
9
+ }
@@ -0,0 +1,43 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { Button } from '@patternfly/react-core';
4
+ import { translate as __ } from 'foremanReact/common/I18n';
5
+ import { JOB_INVOCATION_PATH } from './RemediationTableConstants';
6
+
7
+ const ModalFooter = ({ toggleModal, resolutions, hostsIds }) => {
8
+ let token = document.querySelector('meta[name="csrf-token"]');
9
+ token = token?.content || '';
10
+ return (
11
+ <form action={JOB_INVOCATION_PATH} method="post">
12
+ <Button type="submit" key="confirm" variant="primary">
13
+ {__('Remediate')}
14
+ </Button>
15
+ <Button key="cancel" variant="link" onClick={toggleModal}>
16
+ {__('Cancel')}
17
+ </Button>
18
+ <input type="hidden" name="feature" value="rh_cloud_remediate_hosts" />
19
+ <input type="hidden" name="authenticity_token" value={token} />
20
+ <input
21
+ type="hidden"
22
+ name="inputs[hit_remediation_pairs]"
23
+ value={JSON.stringify(resolutions)}
24
+ />
25
+ {hostsIds.map(id => (
26
+ <input type="hidden" name="host_ids[]" key={id} value={id} />
27
+ ))}
28
+ </form>
29
+ );
30
+ };
31
+
32
+ ModalFooter.propTypes = {
33
+ toggleModal: PropTypes.func.isRequired,
34
+ resolutions: PropTypes.array,
35
+ hostsIds: PropTypes.array,
36
+ };
37
+
38
+ ModalFooter.defaultProps = {
39
+ resolutions: [],
40
+ hostsIds: [],
41
+ };
42
+
43
+ export default ModalFooter;
@@ -0,0 +1,38 @@
1
+ import React from 'react';
2
+ import { cellWidth } from '@patternfly/react-table';
3
+ import { CheckCircleIcon } from '@patternfly/react-icons';
4
+ import { translate as __ } from 'foremanReact/common/I18n';
5
+ import { foremanUrl } from '../../../ForemanRhCloudHelpers';
6
+
7
+ export const rebootFormatter = ({ title: reboot }) => ({
8
+ children: reboot ? <CheckCircleIcon color="green" /> : __('No'),
9
+ });
10
+
11
+ export const columns = [
12
+ {
13
+ sortKey: 'hostname',
14
+ title: __('Hostname'),
15
+ transforms: [cellWidth(20)],
16
+ },
17
+ {
18
+ title: __('Recommendation'),
19
+ transforms: [cellWidth(35)],
20
+ },
21
+ {
22
+ title: __('Resolution'),
23
+ transforms: [cellWidth(30)],
24
+ },
25
+ {
26
+ title: __('Reboot Required'),
27
+ transforms: [cellWidth(15)],
28
+ cellTransforms: [rebootFormatter],
29
+ },
30
+ ];
31
+
32
+ export const REMEDIATIONS_PATH = foremanUrl('/insights_cloud/hits/resolutions');
33
+
34
+ export const JOB_INVOCATION_PATH = foremanUrl('/job_invocations/new');
35
+
36
+ export const REMEDIATIONS_API_KEY = 'INSIGHTS_REMEDIATIONS';
37
+
38
+ export const SUBMIT_RESOLUTIONS = 'SUBMIT_INSIGHTS_RESOLUTIONS';
@@ -0,0 +1,55 @@
1
+ /* eslint-disable camelcase */
2
+ import React from 'react';
3
+ import PropTypes from 'prop-types';
4
+ import { Radio } from '@patternfly/react-core';
5
+
6
+ const Resolutions = ({
7
+ resolutions,
8
+ setResolutions,
9
+ selectedResolution,
10
+ hit_id,
11
+ }) => {
12
+ const [checkedID, setCheckedID] = React.useState(selectedResolution);
13
+
14
+ if (resolutions.length === 1) return <>{resolutions[0].description}</>;
15
+
16
+ return (
17
+ <>
18
+ {resolutions.map(({ id: resolution_id, description }) => (
19
+ <Radio
20
+ key={resolution_id}
21
+ className="resolution-radio"
22
+ id={resolution_id}
23
+ isChecked={resolution_id === checkedID}
24
+ onChange={() =>
25
+ setResolutions(stateRes =>
26
+ stateRes.map(res => {
27
+ if (hit_id === res.hit_id) {
28
+ setCheckedID(resolution_id);
29
+ return { ...res, resolution_id };
30
+ }
31
+ return res;
32
+ })
33
+ )
34
+ }
35
+ label={description}
36
+ />
37
+ ))}
38
+ </>
39
+ );
40
+ };
41
+
42
+ Resolutions.propTypes = {
43
+ setResolutions: PropTypes.func.isRequired,
44
+ resolutions: PropTypes.array,
45
+ hit_id: PropTypes.number,
46
+ selectedResolution: PropTypes.number,
47
+ };
48
+
49
+ Resolutions.defaultProps = {
50
+ resolutions: [],
51
+ hit_id: null,
52
+ selectedResolution: null,
53
+ };
54
+
55
+ export default Resolutions;
@@ -0,0 +1,34 @@
1
+ import { bindActionCreators } from 'redux';
2
+ import { connect } from 'react-redux';
3
+ import {
4
+ selectAPIResponse,
5
+ selectAPIStatus,
6
+ selectAPIErrorMessage,
7
+ } from 'foremanReact/redux/API/APISelectors';
8
+ import * as actions from './RemediationActions';
9
+ import RemediationModal from './RemediationModal';
10
+ import { REMEDIATIONS_API_KEY } from './RemediationTableConstants';
11
+ import {
12
+ selectExperimental,
13
+ selectIsAllSelected,
14
+ selectSearch,
15
+ selectSelectedIds,
16
+ } from '../InsightsTable/InsightsTableSelectors';
17
+
18
+ // map state to props
19
+ const mapStateToProps = state => ({
20
+ selectedIds: selectSelectedIds(state),
21
+ remediations: selectAPIResponse(state, REMEDIATIONS_API_KEY).hits || [],
22
+ status: selectAPIStatus(state, REMEDIATIONS_API_KEY),
23
+ error: selectAPIErrorMessage(state, REMEDIATIONS_API_KEY),
24
+ itemCount: selectAPIResponse(state, REMEDIATIONS_API_KEY).itemCount || 0,
25
+ isAllSelected: selectIsAllSelected(state),
26
+ query: selectSearch(state),
27
+ isExperimentalMode: selectExperimental(state),
28
+ });
29
+
30
+ // map action dispatchers to props
31
+ const mapDispatchToProps = dispatch => bindActionCreators(actions, dispatch);
32
+
33
+ // export connected component
34
+ export default connect(mapStateToProps, mapDispatchToProps)(RemediationModal);
@@ -6,10 +6,12 @@ import PageLayout from 'foremanReact/routes/common/PageLayout/PageLayout';
6
6
  import InsightsHeader from './Components/InsightsHeader';
7
7
  import { NoTokenEmptyState } from './Components/NoTokenEmptyState';
8
8
  import InsightsTable from './Components/InsightsTable';
9
+ import RemediationModal from './Components/RemediationModal';
9
10
  import {
10
11
  INSIGHTS_SYNC_PAGE_TITLE,
11
12
  INSIGHTS_SEARCH_PROPS,
12
13
  } from './InsightsCloudSyncConstants';
14
+ import './InsightsCloudSync.scss';
13
15
 
14
16
  const InsightsCloudSync = ({
15
17
  syncInsights,
@@ -32,9 +34,12 @@ const InsightsCloudSync = ({
32
34
  onSearch={nextQuery => fetchInsights({ query: nextQuery, page: 1 })}
33
35
  header={INSIGHTS_SYNC_PAGE_TITLE}
34
36
  toolbarButtons={
35
- <Button variant="primary" onClick={syncInsights}>
36
- {__('Start recommendations sync')}
37
- </Button>
37
+ <>
38
+ <RemediationModal />
39
+ <Button variant="secondary" onClick={syncInsights}>
40
+ {__('Start recommendations sync')}
41
+ </Button>
42
+ </>
38
43
  }
39
44
  searchQuery={query}
40
45
  beforeToolbarComponent={<InsightsHeader />}