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,69 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import classNames from 'classnames';
|
3
|
+
import PropTypes from 'prop-types';
|
4
|
+
|
5
|
+
const ListItem = props => {
|
6
|
+
const draggableBtn = (
|
7
|
+
<div className="pf-c-dual-list-selector__draggable">
|
8
|
+
<button
|
9
|
+
className="pf-c-button pf-m-plain"
|
10
|
+
type="button"
|
11
|
+
aria-pressed="false"
|
12
|
+
aria-label="Reorder"
|
13
|
+
id="draggable-list-item-2-draggable-button"
|
14
|
+
aria-labelledby="draggable-list-item-2-draggable-button draggable-list-item-2-item-text"
|
15
|
+
aria-describedby="draggable-help"
|
16
|
+
>
|
17
|
+
<i className="fas fa-grip-vertical" aria-hidden="true" />
|
18
|
+
</button>
|
19
|
+
</div>
|
20
|
+
);
|
21
|
+
|
22
|
+
const orderBtn = (
|
23
|
+
<span className="foreman-dual-list-order">{`${props.index + 1}.`}</span>
|
24
|
+
);
|
25
|
+
|
26
|
+
return (
|
27
|
+
<li className="pf-c-dual-list-selector__list-item">
|
28
|
+
<div
|
29
|
+
className={classNames('pf-c-dual-list-selector__list-item-row ', {
|
30
|
+
'pf-m-selected': props.selected,
|
31
|
+
'pf-m-ghost-row': props.dragging,
|
32
|
+
})}
|
33
|
+
>
|
34
|
+
{props.draggable && draggableBtn}
|
35
|
+
<button
|
36
|
+
className="pf-c-dual-list-selector__item"
|
37
|
+
type="button"
|
38
|
+
onClick={props.onClick}
|
39
|
+
>
|
40
|
+
<span className="pf-c-dual-list-selector__item-main">
|
41
|
+
<span className="pf-c-dual-list-selector__item-text">
|
42
|
+
{props.draggable && orderBtn}
|
43
|
+
<span>{props.name}</span>
|
44
|
+
</span>
|
45
|
+
</span>
|
46
|
+
<span className="pf-c-dual-list-selector__item-count">
|
47
|
+
<span className="pf-c-badge pf-m-read" />
|
48
|
+
</span>
|
49
|
+
</button>
|
50
|
+
</div>
|
51
|
+
</li>
|
52
|
+
);
|
53
|
+
};
|
54
|
+
|
55
|
+
ListItem.propTypes = {
|
56
|
+
selected: PropTypes.bool.isRequired,
|
57
|
+
dragging: PropTypes.bool,
|
58
|
+
draggable: PropTypes.bool,
|
59
|
+
onClick: PropTypes.func.isRequired,
|
60
|
+
name: PropTypes.string.isRequired,
|
61
|
+
index: PropTypes.number.isRequired,
|
62
|
+
};
|
63
|
+
|
64
|
+
ListItem.defaultProps = {
|
65
|
+
draggable: false,
|
66
|
+
dragging: false,
|
67
|
+
};
|
68
|
+
|
69
|
+
export default ListItem;
|
@@ -0,0 +1,95 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { DndProvider } from 'react-dnd';
|
3
|
+
import HTML5Backend from 'react-dnd-html5-backend';
|
4
|
+
import PropTypes from 'prop-types';
|
5
|
+
|
6
|
+
import {
|
7
|
+
orderable,
|
8
|
+
orderDragged,
|
9
|
+
} from 'foremanReact/components/common/forms/OrderableSelect/helpers';
|
10
|
+
|
11
|
+
import ListItem from './ListItem';
|
12
|
+
import ListHeader from './ListHeader';
|
13
|
+
import SelectedStatus from './SelectedStatus';
|
14
|
+
|
15
|
+
const OrderableListItem = orderable(ListItem, {
|
16
|
+
type: 'orderableItem',
|
17
|
+
getItem: props => ({ id: props.name }),
|
18
|
+
direction: 'vertical',
|
19
|
+
});
|
20
|
+
|
21
|
+
const ListPane = ({
|
22
|
+
items,
|
23
|
+
isItemSelected,
|
24
|
+
draggable,
|
25
|
+
onItemClick,
|
26
|
+
paneClass,
|
27
|
+
title,
|
28
|
+
selected,
|
29
|
+
onSetOrderedItems,
|
30
|
+
}) => {
|
31
|
+
const moveValue = (dragIndex, hoverIndex) => {
|
32
|
+
onSetOrderedItems(orderDragged(items, dragIndex, hoverIndex));
|
33
|
+
};
|
34
|
+
|
35
|
+
const renderOrderableItem = (item, idx) => (
|
36
|
+
<OrderableListItem
|
37
|
+
key={item}
|
38
|
+
name={item}
|
39
|
+
index={idx}
|
40
|
+
moveValue={moveValue}
|
41
|
+
onClick={onItemClick(item)}
|
42
|
+
selected={isItemSelected(item)}
|
43
|
+
draggable={draggable}
|
44
|
+
/>
|
45
|
+
);
|
46
|
+
|
47
|
+
const renderListItem = (item, idx) => (
|
48
|
+
<ListItem
|
49
|
+
key={item}
|
50
|
+
name={item}
|
51
|
+
index={idx}
|
52
|
+
onClick={onItemClick(item)}
|
53
|
+
selected={isItemSelected(item)}
|
54
|
+
/>
|
55
|
+
);
|
56
|
+
|
57
|
+
const renderItem = draggable ? renderOrderableItem : renderListItem;
|
58
|
+
|
59
|
+
return (
|
60
|
+
<div
|
61
|
+
className={`pf-c-dual-list-selector__pane pf-m-available ${paneClass}`}
|
62
|
+
>
|
63
|
+
<ListHeader title={title} />
|
64
|
+
<SelectedStatus
|
65
|
+
selectedCount={selected.length}
|
66
|
+
totalCount={items.length}
|
67
|
+
/>
|
68
|
+
<div className="pf-c-dual-list-selector__menu">
|
69
|
+
<DndProvider backend={HTML5Backend}>
|
70
|
+
<ul className="pf-c-dual-list-selector__list">
|
71
|
+
{items.map(renderItem)}
|
72
|
+
</ul>
|
73
|
+
</DndProvider>
|
74
|
+
</div>
|
75
|
+
</div>
|
76
|
+
);
|
77
|
+
};
|
78
|
+
|
79
|
+
ListPane.propTypes = {
|
80
|
+
items: PropTypes.array.isRequired,
|
81
|
+
isItemSelected: PropTypes.func.isRequired,
|
82
|
+
draggable: PropTypes.bool,
|
83
|
+
onItemClick: PropTypes.func.isRequired,
|
84
|
+
paneClass: PropTypes.string.isRequired,
|
85
|
+
title: PropTypes.string.isRequired,
|
86
|
+
selected: PropTypes.array.isRequired,
|
87
|
+
onSetOrderedItems: PropTypes.func,
|
88
|
+
};
|
89
|
+
|
90
|
+
ListPane.defaultProps = {
|
91
|
+
draggable: false,
|
92
|
+
onSetOrderedItems: () => {},
|
93
|
+
};
|
94
|
+
|
95
|
+
export default ListPane;
|
@@ -0,0 +1,21 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import PropTypes from 'prop-types';
|
3
|
+
import { translate as __, sprintf } from 'foremanReact/common/I18n';
|
4
|
+
|
5
|
+
const SelectedStatus = ({ selectedCount, totalCount }) => (
|
6
|
+
<div className="pf-c-dual-list-selector__status">
|
7
|
+
<span className="pf-c-dual-list-selector__status-text">
|
8
|
+
{sprintf(__('%(selectedCount)s of %(totalCount)s items selected'), {
|
9
|
+
selectedCount,
|
10
|
+
totalCount,
|
11
|
+
})}
|
12
|
+
</span>
|
13
|
+
</div>
|
14
|
+
);
|
15
|
+
|
16
|
+
SelectedStatus.propTypes = {
|
17
|
+
selectedCount: PropTypes.number.isRequired,
|
18
|
+
totalCount: PropTypes.number.isRequired,
|
19
|
+
};
|
20
|
+
|
21
|
+
export default SelectedStatus;
|
@@ -0,0 +1,103 @@
|
|
1
|
+
import React, { useState } from 'react';
|
2
|
+
import PropTypes from 'prop-types';
|
3
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
4
|
+
|
5
|
+
import ListControls from './ListControls';
|
6
|
+
import ListPane from './ListPane';
|
7
|
+
import './DualList.scss';
|
8
|
+
|
9
|
+
const DualList = props => {
|
10
|
+
const defaultSelectState = { availableSelected: [], chosenSelected: [] };
|
11
|
+
const [selectState, setSelectState] = useState(defaultSelectState);
|
12
|
+
|
13
|
+
const onItemClick = stateName => itemName => (...args) => {
|
14
|
+
if (isItemSelected(stateName)(itemName)) {
|
15
|
+
setSelectState({
|
16
|
+
...selectState,
|
17
|
+
[stateName]: selectState[stateName].filter(item => item !== itemName),
|
18
|
+
});
|
19
|
+
} else {
|
20
|
+
setSelectState({
|
21
|
+
...selectState,
|
22
|
+
[stateName]: [...selectState[stateName], itemName],
|
23
|
+
});
|
24
|
+
}
|
25
|
+
};
|
26
|
+
|
27
|
+
const isItemSelected = stateName => item =>
|
28
|
+
selectState[stateName].includes(item);
|
29
|
+
|
30
|
+
const onAddAll = () =>
|
31
|
+
props.onListChange([], [...props.chosenOptions, ...props.availableOptions]);
|
32
|
+
const onRemoveAll = () =>
|
33
|
+
props.onListChange([...props.availableOptions, ...props.chosenOptions], []);
|
34
|
+
|
35
|
+
const onAddSelected = () => {
|
36
|
+
const [newAvailable, newChosen] = moveItems(
|
37
|
+
props.availableOptions,
|
38
|
+
props.chosenOptions,
|
39
|
+
selectState.availableSelected
|
40
|
+
);
|
41
|
+
setSelectState(defaultSelectState);
|
42
|
+
props.onListChange(newAvailable, newChosen);
|
43
|
+
};
|
44
|
+
|
45
|
+
const onRemoveSelected = () => {
|
46
|
+
const [newChosen, newAvailable] = moveItems(
|
47
|
+
props.chosenOptions,
|
48
|
+
props.availableOptions,
|
49
|
+
selectState.chosenSelected
|
50
|
+
);
|
51
|
+
setSelectState(defaultSelectState);
|
52
|
+
props.onListChange(newAvailable, newChosen);
|
53
|
+
};
|
54
|
+
|
55
|
+
const moveItems = (removeFrom, addTo, selected) => {
|
56
|
+
const newRemoveFrom = removeFrom.filter(item => !selected.includes(item));
|
57
|
+
return [newRemoveFrom, [...addTo, ...selected]];
|
58
|
+
};
|
59
|
+
|
60
|
+
const onChosenSetOrder = items =>
|
61
|
+
props.onListChange(props.availableOptions, items);
|
62
|
+
|
63
|
+
return (
|
64
|
+
<div className="pf-c-dual-list-selector">
|
65
|
+
<ListPane
|
66
|
+
title={__('Available options')}
|
67
|
+
items={props.availableOptions}
|
68
|
+
paneClass="pf-m-available"
|
69
|
+
onItemClick={onItemClick('availableSelected')}
|
70
|
+
selected={selectState.availableSelected}
|
71
|
+
isItemSelected={isItemSelected('availableSelected')}
|
72
|
+
/>
|
73
|
+
<ListControls
|
74
|
+
onAddSelected={onAddSelected}
|
75
|
+
onRemoveSelected={onRemoveSelected}
|
76
|
+
onRemoveAll={onRemoveAll}
|
77
|
+
onAddAll={onAddAll}
|
78
|
+
addAllDisabled={props.availableOptions.length === 0}
|
79
|
+
removeAllDisabled={props.chosenOptions.length === 0}
|
80
|
+
addSelectedDisabled={selectState.availableSelected.length === 0}
|
81
|
+
removeSelectedDisabled={selectState.chosenSelected.length === 0}
|
82
|
+
/>
|
83
|
+
<ListPane
|
84
|
+
title={__('Chosen options')}
|
85
|
+
items={props.chosenOptions}
|
86
|
+
paneClass="pf-m-chosen"
|
87
|
+
draggable
|
88
|
+
onItemClick={onItemClick('chosenSelected')}
|
89
|
+
selected={selectState.chosenSelected}
|
90
|
+
isItemSelected={isItemSelected('chosenSelected')}
|
91
|
+
onSetOrderedItems={onChosenSetOrder}
|
92
|
+
/>
|
93
|
+
</div>
|
94
|
+
);
|
95
|
+
};
|
96
|
+
|
97
|
+
DualList.propTypes = {
|
98
|
+
onListChange: PropTypes.func.isRequired,
|
99
|
+
chosenOptions: PropTypes.array.isRequired,
|
100
|
+
availableOptions: PropTypes.array.isRequired,
|
101
|
+
};
|
102
|
+
|
103
|
+
export default DualList;
|
@@ -0,0 +1,16 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import EmptyState from 'foremanReact/components/common/EmptyState/EmptyStatePattern';
|
3
|
+
|
4
|
+
import { EmptyStateIcon } from '@patternfly/react-core';
|
5
|
+
import { ExclamationCircleIcon } from '@patternfly/react-icons';
|
6
|
+
import { global_danger_color_200 as dangerColor } from '@patternfly/react-tokens';
|
7
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
8
|
+
|
9
|
+
const ErrorState = props => {
|
10
|
+
const icon = (
|
11
|
+
<EmptyStateIcon icon={ExclamationCircleIcon} color={dangerColor.value} />
|
12
|
+
);
|
13
|
+
return <EmptyState header={__('Error!')} icon={icon} {...props} />;
|
14
|
+
};
|
15
|
+
|
16
|
+
export default ErrorState;
|
@@ -0,0 +1,135 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
3
|
+
import PropTypes from 'prop-types';
|
4
|
+
|
5
|
+
import Skeleton from 'react-loading-skeleton';
|
6
|
+
import EmptyState from 'foremanReact/components/common/EmptyState/EmptyStatePattern';
|
7
|
+
import { LockIcon } from '@patternfly/react-icons';
|
8
|
+
import { EmptyStateIcon } from '@patternfly/react-core';
|
9
|
+
import {
|
10
|
+
permissionCheck,
|
11
|
+
permissionDeniedMsg,
|
12
|
+
allowPrimaryAction,
|
13
|
+
} from '../permissionsHelper';
|
14
|
+
import ErrorState from './ErrorState';
|
15
|
+
|
16
|
+
const pluckData = (data, path) => {
|
17
|
+
const split = path.split('.');
|
18
|
+
return split.reduce((memo, item) => {
|
19
|
+
if (item) {
|
20
|
+
return memo[item];
|
21
|
+
}
|
22
|
+
throw new Error('Unexpected empty segment in response data path');
|
23
|
+
}, data);
|
24
|
+
};
|
25
|
+
|
26
|
+
const withLoading = Component => {
|
27
|
+
const defaultEmptyStateProps = {
|
28
|
+
header: __('Nothing Found!'),
|
29
|
+
description: '',
|
30
|
+
};
|
31
|
+
|
32
|
+
const Subcomponent = ({
|
33
|
+
fetchFn,
|
34
|
+
renameData,
|
35
|
+
renamedDataPath,
|
36
|
+
showEmptyState,
|
37
|
+
emptyWrapper,
|
38
|
+
loadingWrapper,
|
39
|
+
wrapper,
|
40
|
+
emptyStateProps,
|
41
|
+
permissions,
|
42
|
+
allowed,
|
43
|
+
requiredPermissions,
|
44
|
+
primaryActionPermissions,
|
45
|
+
...rest
|
46
|
+
}) => {
|
47
|
+
const { loading, error, data } = fetchFn(rest);
|
48
|
+
|
49
|
+
if (loading) {
|
50
|
+
return loadingWrapper(<Skeleton count={5} />);
|
51
|
+
}
|
52
|
+
|
53
|
+
if (error) {
|
54
|
+
return emptyWrapper(<ErrorState description={error.message} />);
|
55
|
+
}
|
56
|
+
|
57
|
+
const check = permissionCheck(data.currentUser, permissions);
|
58
|
+
|
59
|
+
if (!check.allowed) {
|
60
|
+
return emptyWrapper(
|
61
|
+
<EmptyState
|
62
|
+
icon={<EmptyStateIcon icon={LockIcon} />}
|
63
|
+
header={__('Permission denied')}
|
64
|
+
description={permissionDeniedMsg(
|
65
|
+
check.permissions.map(item => item.name)
|
66
|
+
)}
|
67
|
+
/>
|
68
|
+
);
|
69
|
+
}
|
70
|
+
|
71
|
+
if (!allowed) {
|
72
|
+
return emptyWrapper(
|
73
|
+
<EmptyState
|
74
|
+
icon={<EmptyStateIcon icon={LockIcon} />}
|
75
|
+
header={__('Permission denied')}
|
76
|
+
description={permissionDeniedMsg(requiredPermissions)}
|
77
|
+
/>
|
78
|
+
);
|
79
|
+
}
|
80
|
+
|
81
|
+
const renamedData = renameData(data);
|
82
|
+
const result = pluckData(renamedData, renamedDataPath);
|
83
|
+
|
84
|
+
if (
|
85
|
+
showEmptyState &&
|
86
|
+
((Array.isArray(result) && result.length === 0) || !result)
|
87
|
+
) {
|
88
|
+
return emptyWrapper(
|
89
|
+
<EmptyState
|
90
|
+
{...{
|
91
|
+
...defaultEmptyStateProps,
|
92
|
+
...allowPrimaryAction(
|
93
|
+
emptyStateProps,
|
94
|
+
data.currentUser,
|
95
|
+
primaryActionPermissions
|
96
|
+
),
|
97
|
+
}}
|
98
|
+
/>
|
99
|
+
);
|
100
|
+
}
|
101
|
+
|
102
|
+
return wrapper(<Component {...rest} {...renamedData} />);
|
103
|
+
};
|
104
|
+
|
105
|
+
Subcomponent.propTypes = {
|
106
|
+
fetchFn: PropTypes.func.isRequired,
|
107
|
+
renamedDataPath: PropTypes.string.isRequired,
|
108
|
+
renameData: PropTypes.func,
|
109
|
+
showEmptyState: PropTypes.bool,
|
110
|
+
loadingWrapper: PropTypes.func,
|
111
|
+
emptyWrapper: PropTypes.func,
|
112
|
+
emptyStateProps: PropTypes.object,
|
113
|
+
wrapper: PropTypes.func,
|
114
|
+
permissions: PropTypes.array,
|
115
|
+
allowed: PropTypes.bool,
|
116
|
+
primaryActionPermissions: PropTypes.array,
|
117
|
+
};
|
118
|
+
|
119
|
+
Subcomponent.defaultProps = {
|
120
|
+
renameData: data => data,
|
121
|
+
showEmptyState: true,
|
122
|
+
loadingWrapper: child => child,
|
123
|
+
emptyWrapper: child => child,
|
124
|
+
wrapper: child => child,
|
125
|
+
emptyStateProps: defaultEmptyStateProps,
|
126
|
+
permissions: [],
|
127
|
+
requiredPermissions: [],
|
128
|
+
primaryActionPermissions: [],
|
129
|
+
allowed: true,
|
130
|
+
};
|
131
|
+
|
132
|
+
return Subcomponent;
|
133
|
+
};
|
134
|
+
|
135
|
+
export default withLoading;
|
File without changes
|
@@ -0,0 +1,131 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import PropTypes from 'prop-types';
|
3
|
+
import {
|
4
|
+
FormGroup,
|
5
|
+
FormSelect,
|
6
|
+
FormSelectOption,
|
7
|
+
DatePicker,
|
8
|
+
TimePicker,
|
9
|
+
} from '@patternfly/react-core';
|
10
|
+
import ExclamationCircleIcon from '@patternfly/react-icons/dist/js/icons/exclamation-circle-icon';
|
11
|
+
|
12
|
+
const wrapFieldProps = fieldProps => {
|
13
|
+
const { onChange } = fieldProps;
|
14
|
+
// modify onChange args to correctly wire formik with pf4 input handlers
|
15
|
+
const wrappedOnChange = (value, event) => {
|
16
|
+
onChange(event);
|
17
|
+
};
|
18
|
+
|
19
|
+
return { ...fieldProps, onChange: wrappedOnChange };
|
20
|
+
};
|
21
|
+
|
22
|
+
const wrapPickerProps = fieldProps => {
|
23
|
+
const { onChange } = fieldProps;
|
24
|
+
// because pf4 does not provide consistent handlers for its inputs
|
25
|
+
const wrappedOnChange = value => {
|
26
|
+
onChange({ target: { name: fieldProps.name, value } });
|
27
|
+
};
|
28
|
+
|
29
|
+
return { ...fieldProps, onChange: wrappedOnChange };
|
30
|
+
};
|
31
|
+
|
32
|
+
const shouldValidate = (form, fieldName) => {
|
33
|
+
if (form.touched[fieldName]) {
|
34
|
+
return form.errors[fieldName] ? 'error' : 'success';
|
35
|
+
}
|
36
|
+
|
37
|
+
return 'noval';
|
38
|
+
};
|
39
|
+
|
40
|
+
export const SelectField = ({
|
41
|
+
selectItems,
|
42
|
+
field,
|
43
|
+
form,
|
44
|
+
label,
|
45
|
+
isRequired,
|
46
|
+
blankLabel,
|
47
|
+
}) => {
|
48
|
+
const fieldProps = wrapFieldProps(field);
|
49
|
+
|
50
|
+
const valid = shouldValidate(form, field.name);
|
51
|
+
|
52
|
+
return (
|
53
|
+
<FormGroup
|
54
|
+
label={label}
|
55
|
+
isRequired={isRequired}
|
56
|
+
helperTextInvalid={form.errors[field.name]}
|
57
|
+
helperTextInvalidIcon={<ExclamationCircleIcon />}
|
58
|
+
validated={valid}
|
59
|
+
>
|
60
|
+
<FormSelect
|
61
|
+
{...fieldProps}
|
62
|
+
className="without_select2"
|
63
|
+
aria-label={fieldProps.name}
|
64
|
+
validated={valid}
|
65
|
+
isDisabled={form.isSubmitting}
|
66
|
+
>
|
67
|
+
<FormSelectOption key={0} value="" label={blankLabel} />
|
68
|
+
{selectItems.map((item, idx) => (
|
69
|
+
<FormSelectOption key={idx + 1} value={item.id} label={item.name} />
|
70
|
+
))}
|
71
|
+
</FormSelect>
|
72
|
+
</FormGroup>
|
73
|
+
);
|
74
|
+
};
|
75
|
+
|
76
|
+
SelectField.propTypes = {
|
77
|
+
selectItems: PropTypes.array,
|
78
|
+
field: PropTypes.object.isRequired,
|
79
|
+
form: PropTypes.object.isRequired,
|
80
|
+
label: PropTypes.string.isRequired,
|
81
|
+
blankLabel: PropTypes.string,
|
82
|
+
isRequired: PropTypes.bool,
|
83
|
+
};
|
84
|
+
|
85
|
+
SelectField.defaultProps = {
|
86
|
+
selectItems: [],
|
87
|
+
isRequired: false,
|
88
|
+
blankLabel: '',
|
89
|
+
};
|
90
|
+
|
91
|
+
const pickerWithHandlers = Component => {
|
92
|
+
const Subcomponent = ({ form, field, isRequired, label, ...rest }) => {
|
93
|
+
const { onChange, onBlur } = wrapPickerProps(field);
|
94
|
+
const valid = shouldValidate(form, field.name);
|
95
|
+
|
96
|
+
return (
|
97
|
+
<FormGroup
|
98
|
+
label={label}
|
99
|
+
isRequired={isRequired}
|
100
|
+
helperTextInvalid={form.errors[field.name]}
|
101
|
+
helperTextInvalidIcon={<ExclamationCircleIcon />}
|
102
|
+
validated={valid}
|
103
|
+
>
|
104
|
+
<Component
|
105
|
+
aria-label={field.name}
|
106
|
+
onChange={onChange}
|
107
|
+
onBlur={onBlur}
|
108
|
+
{...rest}
|
109
|
+
validated={valid}
|
110
|
+
isDisabled={form.isSubmitting}
|
111
|
+
/>
|
112
|
+
</FormGroup>
|
113
|
+
);
|
114
|
+
};
|
115
|
+
|
116
|
+
Subcomponent.propTypes = {
|
117
|
+
form: PropTypes.object.isRequired,
|
118
|
+
field: PropTypes.object.isRequired,
|
119
|
+
isRequired: PropTypes.bool,
|
120
|
+
label: PropTypes.string.isRequired,
|
121
|
+
};
|
122
|
+
|
123
|
+
Subcomponent.defaultProps = {
|
124
|
+
isRequired: false,
|
125
|
+
};
|
126
|
+
|
127
|
+
return Subcomponent;
|
128
|
+
};
|
129
|
+
|
130
|
+
export const DatePickerField = pickerWithHandlers(DatePicker);
|
131
|
+
export const TimePickerField = pickerWithHandlers(TimePicker);
|
@@ -0,0 +1,13 @@
|
|
1
|
+
const idSeparator = '-';
|
2
|
+
const versionSeparator = ':';
|
3
|
+
const defaultVersion = '01';
|
4
|
+
|
5
|
+
export const decodeModelId = model => decodeId(model.id);
|
6
|
+
|
7
|
+
export const decodeId = id => {
|
8
|
+
const split = atob(id).split(idSeparator);
|
9
|
+
return parseInt(split[split.length - 1], 10);
|
10
|
+
};
|
11
|
+
|
12
|
+
export const encodeId = (typename, id) =>
|
13
|
+
btoa([defaultVersion, versionSeparator, typename, idSeparator, id].join(''));
|
data/webpack/global_index.js
CHANGED
@@ -1,12 +1,18 @@
|
|
1
1
|
import React from 'react';
|
2
|
+
import { registerRoutes } from 'foremanReact/routes/RoutingService';
|
2
3
|
|
3
4
|
import { addGlobalFill } from 'foremanReact/components/common/Fill/GlobalFill';
|
4
5
|
|
6
|
+
import routes from './routes/routes';
|
5
7
|
import AnsibleHostDetail from './components/AnsibleHostDetail';
|
6
8
|
|
9
|
+
import { ANSIBLE_KEY } from './components/AnsibleHostDetail/constants';
|
10
|
+
|
7
11
|
addGlobalFill(
|
8
12
|
'host-details-page-tabs',
|
9
|
-
|
13
|
+
ANSIBLE_KEY,
|
10
14
|
<AnsibleHostDetail key="ansible-host-detail" />,
|
11
15
|
500
|
12
16
|
);
|
17
|
+
|
18
|
+
registerRoutes('foreman_ansible', routes);
|
@@ -0,0 +1,17 @@
|
|
1
|
+
mutation AssignAnsibleRoles($id: ID!, $ansibleRoleIds: [Int!]!) {
|
2
|
+
assignAnsibleRoles(input: { id: $id, ansibleRoleIds: $ansibleRoleIds }) {
|
3
|
+
host {
|
4
|
+
id
|
5
|
+
ownAnsibleRoles {
|
6
|
+
nodes {
|
7
|
+
id
|
8
|
+
name
|
9
|
+
}
|
10
|
+
}
|
11
|
+
}
|
12
|
+
errors {
|
13
|
+
path
|
14
|
+
message
|
15
|
+
}
|
16
|
+
}
|
17
|
+
}
|
@@ -0,0 +1,28 @@
|
|
1
|
+
mutation CreateAnsibleVariableOverride(
|
2
|
+
$hostId: Int!,
|
3
|
+
$value: RawJson!,
|
4
|
+
$lookupKeyId: Int!,
|
5
|
+
$match: String!) {
|
6
|
+
createAnsibleVariableOverride(input: { hostId: $hostId, lookupKeyId: $lookupKeyId, value: $value, match: $match }) {
|
7
|
+
overridenAnsibleVariable {
|
8
|
+
id
|
9
|
+
lookupValues(match: $match) {
|
10
|
+
nodes {
|
11
|
+
id
|
12
|
+
match
|
13
|
+
value
|
14
|
+
omit
|
15
|
+
}
|
16
|
+
}
|
17
|
+
currentValue {
|
18
|
+
element
|
19
|
+
value
|
20
|
+
elementName
|
21
|
+
}
|
22
|
+
}
|
23
|
+
errors {
|
24
|
+
path
|
25
|
+
message
|
26
|
+
}
|
27
|
+
}
|
28
|
+
}
|
@@ -0,0 +1,17 @@
|
|
1
|
+
mutation DeleteAnsibleVariableOverride($id: ID!, $hostId: Int!, $variableId: Int!) {
|
2
|
+
deleteAnsibleVariableOverride(input: { id: $id, hostId: $hostId, variableId: $variableId }) {
|
3
|
+
id
|
4
|
+
overridenAnsibleVariable {
|
5
|
+
id
|
6
|
+
currentValue {
|
7
|
+
value
|
8
|
+
element
|
9
|
+
elementName
|
10
|
+
}
|
11
|
+
}
|
12
|
+
errors {
|
13
|
+
path
|
14
|
+
message
|
15
|
+
}
|
16
|
+
}
|
17
|
+
}
|