1
0
mirror of https://github.com/ansible/awx.git synced 2024-11-01 08:21:15 +03:00

Merge pull request #119 from marshmalien/org-edit

Add Org Edit View
This commit is contained in:
Marliana Lara 2019-02-26 15:04:08 -05:00 committed by GitHub
commit 6f26383e06
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 614 additions and 82 deletions

View File

@ -138,4 +138,36 @@ describe('APIClient (api.js)', () => {
done(); done();
}); });
test('associateInstanceGroup calls expected http method with expected data', async (done) => {
const createPromise = () => Promise.resolve();
const mockHttp = ({ post: jest.fn(createPromise) });
const api = new APIClient(mockHttp);
const url = 'foo/bar/';
const id = 1;
await api.associateInstanceGroup(url, id);
expect(mockHttp.post).toHaveBeenCalledTimes(1);
expect(mockHttp.post.mock.calls[0][0]).toEqual(url);
expect(mockHttp.post.mock.calls[0][1]).toEqual({ id });
done();
});
test('disassociateInstanceGroup calls expected http method with expected data', async (done) => {
const createPromise = () => Promise.resolve();
const mockHttp = ({ post: jest.fn(createPromise) });
const api = new APIClient(mockHttp);
const url = 'foo/bar/';
const id = 1;
await api.disassociateInstanceGroup(url, id);
expect(mockHttp.post).toHaveBeenCalledTimes(1);
expect(mockHttp.post.mock.calls[0][0]).toEqual(url);
expect(mockHttp.post.mock.calls[0][1]).toEqual({ id, disassociate: true });
done();
});
}); });

View File

@ -60,7 +60,7 @@ describe('<Lookup />', () => {
</I18nProvider> </I18nProvider>
).find('Lookup'); ).find('Lookup');
expect(spy).not.toHaveBeenCalled(); expect(spy).not.toHaveBeenCalled();
expect(wrapper.state('lookupSelectedItems')).toEqual([]); expect(wrapper.state('lookupSelectedItems')).toEqual(mockSelected);
const searchItem = wrapper.find('.pf-c-input-group__text#search'); const searchItem = wrapper.find('.pf-c-input-group__text#search');
searchItem.first().simulate('click'); searchItem.first().simulate('click');
expect(spy).toHaveBeenCalled(); expect(spy).toHaveBeenCalled();
@ -110,12 +110,11 @@ describe('<Lookup />', () => {
/> />
</I18nProvider> </I18nProvider>
); );
const removeIcon = wrapper.find('.awx-c-icon--remove').first(); const removeIcon = wrapper.find('button[aria-label="close"]').first();
removeIcon.simulate('click'); removeIcon.simulate('click');
expect(spy).toHaveBeenCalled(); expect(spy).toHaveBeenCalled();
}); });
test('"wrapTags" method properly handles data', () => { test('renders chips from prop value', () => {
const spy = jest.spyOn(Lookup.prototype, 'wrapTags');
mockData = [{ name: 'foo', id: 0 }, { name: 'bar', id: 1 }]; mockData = [{ name: 'foo', id: 0 }, { name: 'bar', id: 1 }];
const wrapper = mount( const wrapper = mount(
<I18nProvider> <I18nProvider>
@ -129,20 +128,18 @@ describe('<Lookup />', () => {
sortedColumnKey="name" sortedColumnKey="name"
/> />
</I18nProvider> </I18nProvider>
); ).find('Lookup');
expect(spy).toHaveBeenCalled(); const chip = wrapper.find('li.pf-c-chip');
const pill = wrapper.find('span.awx-c-tag--pill'); expect(chip).toHaveLength(2);
expect(pill).toHaveLength(2);
}); });
test('toggleSelected successfully adds/removes row from lookupSelectedItems state', () => { test('toggleSelected successfully adds/removes row from lookupSelectedItems state', () => {
mockData = [{ name: 'foo', id: 1 }]; mockData = [];
const wrapper = mount( const wrapper = mount(
<I18nProvider> <I18nProvider>
<Lookup <Lookup
lookup_header="Foo Bar" lookup_header="Foo Bar"
onLookupSave={() => { }} onLookupSave={() => { }}
value={mockData} value={mockData}
selected={[]}
getItems={() => { }} getItems={() => { }}
columns={mockColumns} columns={mockColumns}
sortedColumnKey="name" sortedColumnKey="name"
@ -164,7 +161,7 @@ describe('<Lookup />', () => {
expect(wrapper.state('lookupSelectedItems')).toEqual([]); expect(wrapper.state('lookupSelectedItems')).toEqual([]);
}); });
test('saveModal calls callback with selected items', () => { test('saveModal calls callback with selected items', () => {
mockData = [{ name: 'foo', id: 1 }]; mockData = [];
const onLookupSaveFn = jest.fn(); const onLookupSaveFn = jest.fn();
const wrapper = mount( const wrapper = mount(
<I18nProvider> <I18nProvider>
@ -198,10 +195,10 @@ describe('<Lookup />', () => {
<Lookup <Lookup
lookup_header="Foo Bar" lookup_header="Foo Bar"
onLookupSave={() => { }} onLookupSave={() => { }}
data={mockData} value={mockData}
selected={[]}
columns={mockColumns} columns={mockColumns}
sortedColumnKey="name" sortedColumnKey="name"
getItems={() => { }}
/> />
</I18nProvider> </I18nProvider>
).find('Lookup'); ).find('Lookup');
@ -217,10 +214,10 @@ describe('<Lookup />', () => {
<Lookup <Lookup
lookup_header="Foo Bar" lookup_header="Foo Bar"
onLookupSave={() => { }} onLookupSave={() => { }}
data={mockData} value={mockData}
selected={[]}
columns={mockColumns} columns={mockColumns}
sortedColumnKey="name" sortedColumnKey="name"
getItems={() => { }}
/> />
</I18nProvider> </I18nProvider>
).find('Lookup'); ).find('Lookup');

View File

@ -1,16 +1,244 @@
import React from 'react'; import React from 'react';
import { mount } from 'enzyme'; import { mount } from 'enzyme';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import { I18nProvider } from '@lingui/react';
import { ConfigContext } from '../../../../../src/context';
import APIClient from '../../../../../src/api';
import OrganizationEdit from '../../../../../src/pages/Organizations/screens/Organization/OrganizationEdit'; import OrganizationEdit from '../../../../../src/pages/Organizations/screens/Organization/OrganizationEdit';
describe('<OrganizationEdit />', () => { describe('<OrganizationEdit />', () => {
test('initially renders succesfully', () => { const mockData = {
name: 'Foo',
description: 'Bar',
custom_virtualenv: 'Fizz',
id: 1,
related: {
instance_groups: '/api/v2/organizations/1/instance_groups'
}
};
test('should request related instance groups from api', () => {
const mockInstanceGroups = [
{ name: 'One', id: 1 },
{ name: 'Two', id: 2 }
];
const getOrganizationInstanceGroups = jest.fn(() => (
Promise.resolve({ data: { results: mockInstanceGroups } })
));
mount( mount(
<MemoryRouter initialEntries={['/organizations/1/edit']} initialIndex={0}> <I18nProvider>
<OrganizationEdit <MemoryRouter initialEntries={['/organizations/1']} initialIndex={0}>
match={{ path: '/organizations/:id/edit', url: '/organizations/1/edit', params: { id: 1 } }} <OrganizationEdit
/> match={{ params: { id: '1' } }}
api={{
getOrganizationInstanceGroups
}}
organization={mockData}
/>
</MemoryRouter>
</I18nProvider>
).find('OrganizationEdit');
expect(getOrganizationInstanceGroups).toHaveBeenCalledTimes(1);
});
test('componentDidMount should set instanceGroups to state', async () => {
const mockInstanceGroups = [
{ name: 'One', id: 1 },
{ name: 'Two', id: 2 }
];
const getOrganizationInstanceGroups = jest.fn(() => (
Promise.resolve({ data: { results: mockInstanceGroups } })
));
const wrapper = mount(
<I18nProvider>
<MemoryRouter initialEntries={['/organizations/1']} initialIndex={0}>
<OrganizationEdit
match={{
path: '/organizations/:id',
url: '/organizations/1',
params: { id: '1' }
}}
organization={mockData}
api={{ getOrganizationInstanceGroups }}
/>
</MemoryRouter>
</I18nProvider>
).find('OrganizationEdit');
await wrapper.instance().componentDidMount();
expect(wrapper.state().form.instanceGroups.value).toEqual(mockInstanceGroups);
});
test('onLookupSave successfully sets instanceGroups state', () => {
const api = jest.fn();
const wrapper = mount(
<MemoryRouter>
<I18nProvider>
<OrganizationEdit
organization={mockData}
api={api}
match={{ path: '/organizations/:id/edit', url: '/organizations/1/edit' }}
/>
</I18nProvider>
</MemoryRouter>
).find('OrganizationEdit');
wrapper.instance().onLookupSave([
{
id: 1,
name: 'foo'
}
], 'instanceGroups');
expect(wrapper.state().form.instanceGroups.value).toEqual([
{
id: 1,
name: 'foo'
}
]);
});
test('calls "onFieldChange" when input values change', () => {
const api = new APIClient();
const spy = jest.spyOn(OrganizationEdit.WrappedComponent.prototype, 'onFieldChange');
const wrapper = mount(
<MemoryRouter>
<I18nProvider>
<OrganizationEdit
organization={mockData}
api={api}
match={{ path: '/organizations/:id/edit', url: '/organizations/1/edit' }}
/>
</I18nProvider>
</MemoryRouter>
).find('OrganizationEdit');
expect(spy).not.toHaveBeenCalled();
wrapper.instance().onFieldChange('foo', { target: { name: 'name' } });
wrapper.instance().onFieldChange('bar', { target: { name: 'description' } });
expect(spy).toHaveBeenCalledTimes(2);
});
test('AnsibleSelect component renders if there are virtual environments', () => {
const api = jest.fn();
const config = {
custom_virtualenvs: ['foo', 'bar'],
};
const wrapper = mount(
<MemoryRouter>
<I18nProvider>
<ConfigContext.Provider value={config}>
<OrganizationEdit
match={{
path: '/organizations/:id',
url: '/organizations/1',
params: { id: '1' }
}}
organization={mockData}
api={api}
/>
</ConfigContext.Provider>
</I18nProvider>
</MemoryRouter> </MemoryRouter>
); );
expect(wrapper.find('FormSelect')).toHaveLength(1);
expect(wrapper.find('FormSelectOption')).toHaveLength(2);
});
test('calls onSubmit when Save button is clicked', () => {
const api = jest.fn();
const spy = jest.spyOn(OrganizationEdit.WrappedComponent.prototype, 'onSubmit');
const wrapper = mount(
<MemoryRouter>
<I18nProvider>
<OrganizationEdit
match={{
path: '/organizations/:id',
url: '/organizations/1',
params: { id: '1' }
}}
organization={mockData}
api={api}
/>
</I18nProvider>
</MemoryRouter>
);
expect(spy).not.toHaveBeenCalled();
wrapper.find('button[aria-label="Save"]').prop('onClick')();
expect(spy).toBeCalled();
});
test('onSubmit associates and disassociates instance groups', async () => {
const mockInstanceGroups = [
{ name: 'One', id: 1 },
{ name: 'Two', id: 2 }
];
const getOrganizationInstanceGroupsFn = jest.fn(() => (
Promise.resolve({ data: { results: mockInstanceGroups } })
));
const mockDataForm = {
name: 'Foo',
description: 'Bar',
custom_virtualenv: 'Fizz',
};
const updateOrganizationDetailsFn = jest.fn().mockResolvedValue(1, mockDataForm);
const associateInstanceGroupFn = jest.fn().mockResolvedValue('done');
const disassociateInstanceGroupFn = jest.fn().mockResolvedValue('done');
const api = {
getOrganizationInstanceGroups: getOrganizationInstanceGroupsFn,
updateOrganizationDetails: updateOrganizationDetailsFn,
associateInstanceGroup: associateInstanceGroupFn,
disassociateInstanceGroup: disassociateInstanceGroupFn
};
const wrapper = mount(
<I18nProvider>
<MemoryRouter initialEntries={['/organizations/1']} initialIndex={0}>
<OrganizationEdit
match={{
path: '/organizations/:id',
url: '/organizations/1',
params: { id: '1' }
}}
organization={mockData}
api={api}
/>
</MemoryRouter>
</I18nProvider>
).find('OrganizationEdit');
await wrapper.instance().componentDidMount();
wrapper.instance().onLookupSave([
{ name: 'One', id: 1 },
{ name: 'Three', id: 3 }
], 'instanceGroups');
await wrapper.instance().onSubmit();
expect(updateOrganizationDetailsFn).toHaveBeenCalledWith(1, mockDataForm);
expect(associateInstanceGroupFn).toHaveBeenCalledWith('/api/v2/organizations/1/instance_groups', 3);
expect(associateInstanceGroupFn).not.toHaveBeenCalledWith('/api/v2/organizations/1/instance_groups', 1);
expect(associateInstanceGroupFn).not.toHaveBeenCalledWith('/api/v2/organizations/1/instance_groups', 2);
expect(disassociateInstanceGroupFn).toHaveBeenCalledWith('/api/v2/organizations/1/instance_groups', 2);
expect(disassociateInstanceGroupFn).not.toHaveBeenCalledWith('/api/v2/organizations/1/instance_groups', 1);
expect(disassociateInstanceGroupFn).not.toHaveBeenCalledWith('/api/v2/organizations/1/instance_groups', 3);
});
test('calls "onCancel" when Cancel button is clicked', () => {
const api = jest.fn();
const spy = jest.spyOn(OrganizationEdit.WrappedComponent.prototype, 'onCancel');
const wrapper = mount(
<MemoryRouter>
<I18nProvider>
<OrganizationEdit
match={{
path: '/organizations/:id',
url: '/organizations/1',
params: { id: '1' }
}}
organization={mockData}
api={api}
/>
</I18nProvider>
</MemoryRouter>
);
expect(spy).not.toHaveBeenCalled();
wrapper.find('button[aria-label="Cancel"]').prop('onClick')();
expect(spy).toBeCalled();
}); });
}); });

View File

@ -71,7 +71,7 @@ describe('<OrganizationAdd />', () => {
test('Successful form submission triggers redirect', (done) => { test('Successful form submission triggers redirect', (done) => {
const onSuccess = jest.spyOn(OrganizationAdd.WrappedComponent.prototype, 'onSuccess'); const onSuccess = jest.spyOn(OrganizationAdd.WrappedComponent.prototype, 'onSuccess');
const mockedResp = { data: { id: 1, related: { instance_groups: '/bar' } } }; const mockedResp = { data: { id: 1, related: { instance_groups: '/bar' } } };
const api = { createOrganization: jest.fn().mockResolvedValue(mockedResp), createInstanceGroups: jest.fn().mockResolvedValue('done') }; const api = { createOrganization: jest.fn().mockResolvedValue(mockedResp), associateInstanceGroup: jest.fn().mockResolvedValue('done') };
const wrapper = mount( const wrapper = mount(
<MemoryRouter> <MemoryRouter>
<I18nProvider> <I18nProvider>
@ -131,10 +131,10 @@ describe('<OrganizationAdd />', () => {
} }
} }
}); });
const createInstanceGroupsFn = jest.fn().mockResolvedValue('done'); const associateInstanceGroupFn = jest.fn().mockResolvedValue('done');
const api = { const api = {
createOrganization: createOrganizationFn, createOrganization: createOrganizationFn,
createInstanceGroups: createInstanceGroupsFn associateInstanceGroup: associateInstanceGroupFn
}; };
const wrapper = mount( const wrapper = mount(
<MemoryRouter> <MemoryRouter>
@ -156,7 +156,7 @@ describe('<OrganizationAdd />', () => {
description: '', description: '',
name: 'mock org' 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', () => { test('AnsibleSelect component renders if there are virtual environments', () => {

View File

@ -70,6 +70,12 @@ class APIClient {
return this.http.get(endpoint); return this.http.get(endpoint);
} }
updateOrganizationDetails (id, data) {
const endpoint = `${API_ORGANIZATIONS}${id}/`;
return this.http.patch(endpoint, data);
}
getOrganizationInstanceGroups (id, params = {}) { getOrganizationInstanceGroups (id, params = {}) {
const endpoint = `${API_ORGANIZATIONS}${id}/instance_groups/`; const endpoint = `${API_ORGANIZATIONS}${id}/instance_groups/`;
@ -110,9 +116,13 @@ class APIClient {
return this.http.get(API_INSTANCE_GROUPS, { params }); return this.http.get(API_INSTANCE_GROUPS, { params });
} }
createInstanceGroups (url, id) { associateInstanceGroup (url, id) {
return this.http.post(url, { id }); return this.http.post(url, { id });
} }
disassociateInstanceGroup (url, id) {
return this.http.post(url, { id, disassociate: true });
}
} }
export default APIClient; export default APIClient;

View File

@ -228,26 +228,10 @@
} }
} }
.awx-c-icon--remove {
padding-left: 10px;
&:hover {
cursor: pointer;
}
}
.awx-c-list { .awx-c-list {
border-bottom: 1px solid #d7d7d7; 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 { .at-c-listCardBody {
--pf-c-card__footer--PaddingX: 0; --pf-c-card__footer--PaddingX: 0;
--pf-c-card__footer--PaddingY: 0; --pf-c-card__footer--PaddingY: 0;

View File

@ -2,6 +2,7 @@ import React, { Fragment } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { SearchIcon, CubesIcon } from '@patternfly/react-icons'; import { SearchIcon, CubesIcon } from '@patternfly/react-icons';
import { import {
Chip,
Modal, Modal,
Button, Button,
EmptyState, EmptyState,
@ -28,9 +29,10 @@ const paginationStyling = {
class Lookup extends React.Component { class Lookup extends React.Component {
constructor (props) { constructor (props) {
super(props); super(props);
this.state = { this.state = {
isModalOpen: false, isModalOpen: false,
lookupSelectedItems: [], lookupSelectedItems: [...props.value] || [],
results: [], results: [],
count: 0, count: 0,
page: 1, page: 1,
@ -41,7 +43,6 @@ class Lookup extends React.Component {
}; };
this.onSetPage = this.onSetPage.bind(this); this.onSetPage = this.onSetPage.bind(this);
this.handleModalToggle = this.handleModalToggle.bind(this); this.handleModalToggle = this.handleModalToggle.bind(this);
this.wrapTags = this.wrapTags.bind(this);
this.toggleSelected = this.toggleSelected.bind(this); this.toggleSelected = this.toggleSelected.bind(this);
this.saveModal = this.saveModal.bind(this); this.saveModal = this.saveModal.bind(this);
this.getData = this.getData.bind(this); this.getData = this.getData.bind(this);
@ -100,17 +101,27 @@ class Lookup extends React.Component {
}; };
toggleSelected (row) { toggleSelected (row) {
const { lookupSelectedItems } = this.state; const { name, onLookupSave } = this.props;
const selectedIndex = lookupSelectedItems const { lookupSelectedItems: updatedSelectedItems, isModalOpen } = this.state;
const selectedIndex = updatedSelectedItems
.findIndex(selectedRow => selectedRow.id === row.id); .findIndex(selectedRow => selectedRow.id === row.id);
if (selectedIndex > -1) { if (selectedIndex > -1) {
lookupSelectedItems.splice(selectedIndex, 1); updatedSelectedItems.splice(selectedIndex, 1);
this.setState({ lookupSelectedItems }); this.setState({ lookupSelectedItems: updatedSelectedItems });
} else { } else {
this.setState(prevState => ({ this.setState(prevState => ({
lookupSelectedItems: [...prevState.lookupSelectedItems, row] 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 () { handleModalToggle () {
@ -134,17 +145,6 @@ class Lookup extends React.Component {
this.handleModalToggle(); this.handleModalToggle();
} }
wrapTags (tags = []) {
return tags.map(tag => (
<span className="awx-c-tag--pill" key={tag.id}>
{tag.name}
<Button className="awx-c-icon--remove" id={tag.id} onClick={() => this.toggleSelected(tag)}>
x
</Button>
</span>
));
}
render () { render () {
const { const {
isModalOpen, isModalOpen,
@ -159,6 +159,16 @@ class Lookup extends React.Component {
} = this.state; } = this.state;
const { lookupHeader = 'items', value, columns } = this.props; const { lookupHeader = 'items', value, columns } = this.props;
const chips = value ? (
<div className="pf-c-chip-group">
{value.map(chip => (
<Chip key={chip.id} onClick={() => this.toggleSelected(chip)}>
{chip.name}
</Chip>
))}
</div>
) : null;
return ( return (
<I18n> <I18n>
{({ i18n }) => ( {({ i18n }) => (
@ -166,7 +176,7 @@ class Lookup extends React.Component {
<Button className="pf-c-input-group__text" aria-label="search" id="search" onClick={this.handleModalToggle}> <Button className="pf-c-input-group__text" aria-label="search" id="search" onClick={this.handleModalToggle}>
<SearchIcon /> <SearchIcon />
</Button> </Button>
<div className="pf-c-form-control">{this.wrapTags(value)}</div> <div className="pf-c-form-control">{chips}</div>
<Modal <Modal
className="awx-c-modal" className="awx-c-modal"
title={`Select ${lookupHeader}`} title={`Select ${lookupHeader}`}

View File

@ -119,14 +119,18 @@ class Organization extends Component {
to="/organizations/:id/details" to="/organizations/:id/details"
exact exact
/> />
<Route {organization && (
path="/organizations/:id/edit" <Route
render={() => ( path="/organizations/:id/edit"
<OrganizationEdit render={() => (
match={match} <OrganizationEdit
/> api={api}
)} match={match}
/> organization={organization}
/>
)}
/>
)}
{organization && ( {organization && (
<Route <Route
path="/organizations/:id/details" path="/organizations/:id/details"

View File

@ -1,17 +1,284 @@
import React from 'react'; import React, { Component } from 'react';
import { Trans } from '@lingui/macro'; import PropTypes from 'prop-types';
import { withRouter } from 'react-router-dom';
import { I18n, i18nMark } from '@lingui/react';
import { t } from '@lingui/macro';
import { import {
Link CardBody,
} from 'react-router-dom'; Form,
import { CardBody } from '@patternfly/react-core'; FormGroup,
TextInput,
} from '@patternfly/react-core';
const OrganizationEdit = ({ match }) => ( import { ConfigContext } from '../../../../context';
<CardBody> import Lookup from '../../../../components/Lookup';
<h1><Trans>edit view</Trans></h1> import FormActionGroup from '../../../../components/FormActionGroup';
<Link to={`/organizations/${match.params.id}`}> import AnsibleSelect from '../../../../components/AnsibleSelect';
<Trans>save/cancel and go back to view</Trans>
</Link>
</CardBody>
);
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 (
<CardBody>
<I18n>
{({ i18n }) => (
<Form autoComplete="off">
<div style={{ display: 'grid', gridGap: '20px', gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))' }}>
<FormGroup
fieldId="edit-org-form-name"
helperTextInvalid={name.helperTextInvalid}
isRequired
isValid={name.isValid}
label={i18n._(t`Name`)}
>
<TextInput
id="edit-org-form-name"
isRequired
isValid={name.isValid}
name="name"
onChange={this.onFieldChange}
value={name.value || ''}
/>
</FormGroup>
<FormGroup
fieldId="edit-org-form-description"
label={i18n._(t`Description`)}
>
<TextInput
id="edit-org-form-description"
name="description"
onChange={this.onFieldChange}
value={description.value || ''}
/>
</FormGroup>
<ConfigContext.Consumer>
{({ custom_virtualenvs }) => (
custom_virtualenvs && custom_virtualenvs.length > 1 && (
<FormGroup
fieldId="edit-org-custom-virtualenv"
label={i18n._(t`Ansible Environment`)}
>
<AnsibleSelect
data={custom_virtualenvs}
defaultSelected={custom_virtualenv.defaultEnv}
label={i18n._(t`Ansible Environment`)}
name="custom_virtualenv"
onChange={this.onFieldChange}
value={custom_virtualenv.value || ''}
/>
</FormGroup>
)
)}
</ConfigContext.Consumer>
</div>
<FormGroup
fieldId="edit-org-form-instance-groups"
label={i18n._(t`Instance Groups`)}
>
<Lookup
columns={instanceGroupsLookupColumns}
getItems={this.getInstanceGroups}
lookupHeader={i18n._(t`Instance Groups`)}
name="instanceGroups"
onLookupSave={this.onLookupSave}
sortedColumnKey="name"
value={instanceGroups.value}
/>
</FormGroup>
<FormActionGroup
onCancel={this.onCancel}
onSubmit={this.onSubmit}
submitDisabled={!formIsValid}
/>
{ error ? <div>error</div> : '' }
</Form>
)}
</I18n>
</CardBody>
);
}
}
OrganizationEdit.contextTypes = {
custom_virtualenvs: PropTypes.arrayOf(PropTypes.string)
};
export default withRouter(OrganizationEdit);

View File

@ -61,7 +61,7 @@ class OrganizationAdd extends React.Component {
try { try {
if (instanceGroups.length > 0) { if (instanceGroups.length > 0) {
instanceGroups.forEach(async (select) => { instanceGroups.forEach(async (select) => {
await api.createInstanceGroups(instanceGroupsUrl, select.id); await api.associateInstanceGroup(instanceGroupsUrl, select.id);
}); });
} }
} catch (err) { } catch (err) {