@@ -156,7 +156,7 @@ describe('', () => {
description: '',
name: 'mock org'
});
- expect(createInstanceGroupsFn).toHaveBeenCalledWith('/api/v2/organizations/1/instance_groups', 1);
+ expect(associateInstanceGroupFn).toHaveBeenCalledWith('/api/v2/organizations/1/instance_groups', 1);
});
test('AnsibleSelect component renders if there are virtual environments', () => {
diff --git a/src/api.js b/src/api.js
index 60106abe6b..54fe242f5e 100644
--- a/src/api.js
+++ b/src/api.js
@@ -77,6 +77,12 @@ class APIClient {
return this.http.get(endpoint);
}
+ updateOrganizationDetails (id, data) {
+ const endpoint = `${API_ORGANIZATIONS}${id}/`;
+
+ return this.http.patch(endpoint, data);
+ }
+
getOrganizationInstanceGroups (id, params = {}) {
const endpoint = `${API_ORGANIZATIONS}${id}/instance_groups/`;
@@ -117,10 +123,14 @@ class APIClient {
return this.http.get(API_INSTANCE_GROUPS, { params });
}
- createInstanceGroups (url, id) {
+ associateInstanceGroup (url, id) {
return this.http.post(url, { id });
}
+ disassociateInstanceGroup (url, id) {
+ return this.http.post(url, { id, disassociate: true });
+ }
+
getUserRoles (id) {
const endpoint = `${API_USERS}${id}/roles/`;
diff --git a/src/app.scss b/src/app.scss
index 0a1e430ded..934240376c 100644
--- a/src/app.scss
+++ b/src/app.scss
@@ -228,26 +228,10 @@
}
}
-.awx-c-icon--remove {
- padding-left: 10px;
- &:hover {
- cursor: pointer;
- }
-}
-
.awx-c-list {
border-bottom: 1px solid #d7d7d7;
}
-.awx-c-tag--pill {
- color: var(--pf-global--BackgroundColor--light-100);
- background-color: rgb(0, 123, 186);
- border-radius: 3px;
- margin: 1px 2px;
- padding: 0 10px;
- display: inline-block;
-}
-
.at-c-listCardBody {
--pf-c-card__footer--PaddingX: 0;
--pf-c-card__footer--PaddingY: 0;
diff --git a/src/components/Lookup/Lookup.jsx b/src/components/Lookup/Lookup.jsx
index 54fb3dbebf..54be10b2d9 100644
--- a/src/components/Lookup/Lookup.jsx
+++ b/src/components/Lookup/Lookup.jsx
@@ -2,6 +2,7 @@ import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { SearchIcon, CubesIcon } from '@patternfly/react-icons';
import {
+ Chip,
Modal,
Button,
EmptyState,
@@ -28,9 +29,10 @@ const paginationStyling = {
class Lookup extends React.Component {
constructor (props) {
super(props);
+
this.state = {
isModalOpen: false,
- lookupSelectedItems: [],
+ lookupSelectedItems: [...props.value] || [],
results: [],
count: 0,
page: 1,
@@ -41,7 +43,6 @@ class Lookup extends React.Component {
};
this.onSetPage = this.onSetPage.bind(this);
this.handleModalToggle = this.handleModalToggle.bind(this);
- this.wrapTags = this.wrapTags.bind(this);
this.toggleSelected = this.toggleSelected.bind(this);
this.saveModal = this.saveModal.bind(this);
this.getData = this.getData.bind(this);
@@ -100,17 +101,27 @@ class Lookup extends React.Component {
};
toggleSelected (row) {
- const { lookupSelectedItems } = this.state;
- const selectedIndex = lookupSelectedItems
+ const { name, onLookupSave } = this.props;
+ const { lookupSelectedItems: updatedSelectedItems, isModalOpen } = this.state;
+
+ const selectedIndex = updatedSelectedItems
.findIndex(selectedRow => selectedRow.id === row.id);
+
if (selectedIndex > -1) {
- lookupSelectedItems.splice(selectedIndex, 1);
- this.setState({ lookupSelectedItems });
+ updatedSelectedItems.splice(selectedIndex, 1);
+ this.setState({ lookupSelectedItems: updatedSelectedItems });
} else {
this.setState(prevState => ({
lookupSelectedItems: [...prevState.lookupSelectedItems, row]
}));
}
+
+ // Updates the selected items from parent state
+ // This handles the case where the user removes chips from the lookup input
+ // while the modal is closed
+ if (!isModalOpen) {
+ onLookupSave(updatedSelectedItems, name);
+ }
}
handleModalToggle () {
@@ -134,17 +145,6 @@ class Lookup extends React.Component {
this.handleModalToggle();
}
- wrapTags (tags = []) {
- return tags.map(tag => (
-
- {tag.name}
-
-
- ));
- }
-
render () {
const {
isModalOpen,
@@ -159,6 +159,16 @@ class Lookup extends React.Component {
} = this.state;
const { lookupHeader = 'items', value, columns } = this.props;
+ const chips = value ? (
+
+ {value.map(chip => (
+ this.toggleSelected(chip)}>
+ {chip.name}
+
+ ))}
+
+ ) : null;
+
return (
{({ i18n }) => (
@@ -166,7 +176,7 @@ class Lookup extends React.Component {
- {this.wrapTags(value)}
+ {chips}
- (
-
- )}
- />
+ {organization && (
+ (
+
+ )}
+ />
+ )}
{organization && (
(
-
- edit view
-
- save/cancel and go back to view
-
-
-);
+import { ConfigContext } from '../../../../context';
+import Lookup from '../../../../components/Lookup';
+import FormActionGroup from '../../../../components/FormActionGroup';
+import AnsibleSelect from '../../../../components/AnsibleSelect';
-export default OrganizationEdit;
+class OrganizationEdit extends Component {
+ constructor (props) {
+ super(props);
+
+ this.getInstanceGroups = this.getInstanceGroups.bind(this);
+ this.getRelatedInstanceGroups = this.getRelatedInstanceGroups.bind(this);
+ this.checkValidity = this.checkValidity.bind(this);
+ this.onFieldChange = this.onFieldChange.bind(this);
+ this.onLookupSave = this.onLookupSave.bind(this);
+ this.onSubmit = this.onSubmit.bind(this);
+ this.postInstanceGroups = this.postInstanceGroups.bind(this);
+ this.onCancel = this.onCancel.bind(this);
+ this.onSuccess = this.onSuccess.bind(this);
+
+ this.state = {
+ form: {
+ name: {
+ value: '',
+ isValid: true,
+ validation: {
+ required: true
+ },
+ helperTextInvalid: i18nMark('This field must not be blank')
+ },
+ description: {
+ value: ''
+ },
+ instanceGroups: {
+ value: [],
+ initialValue: []
+ },
+ custom_virtualenv: {
+ value: '',
+ defaultValue: '/venv/ansible/'
+ }
+ },
+ error: '',
+ formIsValid: true
+ };
+ }
+
+ async componentDidMount () {
+ const { organization } = this.props;
+ const { form: formData } = this.state;
+
+ formData.name.value = organization.name;
+ formData.description.value = organization.description;
+ formData.custom_virtualenv.value = organization.custom_virtualenv;
+
+ try {
+ formData.instanceGroups.value = await this.getRelatedInstanceGroups();
+ formData.instanceGroups.initialValue = [...formData.instanceGroups.value];
+ } catch (err) {
+ this.setState({ error: err });
+ }
+
+ this.setState({ form: formData });
+ }
+
+ onFieldChange (val, evt) {
+ const targetName = evt.target.name;
+ const value = val;
+
+ const { form: updatedForm } = this.state;
+ const updatedFormEl = { ...updatedForm[targetName] };
+
+ updatedFormEl.value = value;
+ updatedForm[targetName] = updatedFormEl;
+
+ updatedFormEl.isValid = (updatedFormEl.validation)
+ ? this.checkValidity(updatedFormEl.value, updatedFormEl.validation) : true;
+
+ const formIsValid = (updatedFormEl.validation) ? updatedFormEl.isValid : true;
+
+ this.setState({ form: updatedForm, formIsValid });
+ }
+
+ onLookupSave (val, targetName) {
+ const { form: updatedForm } = this.state;
+ updatedForm[targetName].value = val;
+
+ this.setState({ form: updatedForm });
+ }
+
+ async onSubmit () {
+ const { api, organization } = this.props;
+ const { form: { name, description, custom_virtualenv } } = this.state;
+ const formData = { name, description, custom_virtualenv };
+
+ const updatedData = {};
+ Object.keys(formData)
+ .forEach(formId => {
+ updatedData[formId] = formData[formId].value;
+ });
+
+ try {
+ await api.updateOrganizationDetails(organization.id, updatedData);
+ await this.postInstanceGroups();
+ } catch (err) {
+ this.setState({ error: err });
+ } finally {
+ this.onSuccess();
+ }
+ }
+
+ onCancel () {
+ const { organization: { id }, history } = this.props;
+ history.push(`/organizations/${id}`);
+ }
+
+ onSuccess () {
+ const { organization: { id }, history } = this.props;
+ history.push(`/organizations/${id}`);
+ }
+
+ async getInstanceGroups (params) {
+ const { api } = this.props;
+ const data = await api.getInstanceGroups(params);
+ return data;
+ }
+
+ async getRelatedInstanceGroups () {
+ const {
+ api,
+ organization: { id }
+ } = this.props;
+ const { data } = await api.getOrganizationInstanceGroups(id);
+ const { results } = data;
+ return results;
+ }
+
+ checkValidity = (value, validation) => {
+ const isValid = (validation.required)
+ ? (value.trim() !== '') : true;
+
+ return isValid;
+ }
+
+ async postInstanceGroups () {
+ const { api, organization } = this.props;
+ const { form: { instanceGroups } } = this.state;
+ const url = organization.related.instance_groups;
+
+ const initialInstanceGroups = instanceGroups.initialValue.map(ig => ig.id);
+ const updatedInstanceGroups = instanceGroups.value.map(ig => ig.id);
+
+ const groupsToAssociate = [...updatedInstanceGroups]
+ .filter(x => !initialInstanceGroups.includes(x));
+ const groupsToDisassociate = [...initialInstanceGroups]
+ .filter(x => !updatedInstanceGroups.includes(x));
+
+ try {
+ await Promise.all(groupsToAssociate.map(async id => {
+ await api.associateInstanceGroup(url, id);
+ }));
+ await Promise.all(groupsToDisassociate.map(async id => {
+ await api.disassociateInstanceGroup(url, id);
+ }));
+ } catch (err) {
+ this.setState({ error: err });
+ }
+ }
+
+ render () {
+ const {
+ form: {
+ name,
+ description,
+ instanceGroups,
+ custom_virtualenv
+ },
+ formIsValid,
+ error
+ } = this.state;
+
+ const instanceGroupsLookupColumns = [
+ { name: i18nMark('Name'), key: 'name', isSortable: true },
+ { name: i18nMark('Modified'), key: 'modified', isSortable: false, isNumeric: true },
+ { name: i18nMark('Created'), key: 'created', isSortable: false, isNumeric: true }
+ ];
+
+ return (
+
+
+ {({ i18n }) => (
+
+ )}
+
+
+ );
+ }
+}
+
+OrganizationEdit.contextTypes = {
+ custom_virtualenvs: PropTypes.arrayOf(PropTypes.string)
+};
+
+export default withRouter(OrganizationEdit);
diff --git a/src/pages/Organizations/screens/OrganizationAdd.jsx b/src/pages/Organizations/screens/OrganizationAdd.jsx
index 6dbd224781..5f38a237f8 100644
--- a/src/pages/Organizations/screens/OrganizationAdd.jsx
+++ b/src/pages/Organizations/screens/OrganizationAdd.jsx
@@ -61,7 +61,7 @@ class OrganizationAdd extends React.Component {
try {
if (instanceGroups.length > 0) {
instanceGroups.forEach(async (select) => {
- await api.createInstanceGroups(instanceGroupsUrl, select.id);
+ await api.associateInstanceGroup(instanceGroupsUrl, select.id);
});
}
} catch (err) {