diff --git a/__tests__/components/AnsibleSelect.test.jsx b/__tests__/components/AnsibleSelect.test.jsx index d00a96fd9a..bb1a0b18e3 100644 --- a/__tests__/components/AnsibleSelect.test.jsx +++ b/__tests__/components/AnsibleSelect.test.jsx @@ -8,19 +8,22 @@ describe('', () => { test('initially renders succesfully', async () => { mount( { }} + value="foo" + name="bar" + onChange={() => { }} labelName={label} data={mockData} /> ); }); + test('calls "onSelectChange" on dropdown select change', () => { const spy = jest.spyOn(AnsibleSelect.prototype, 'onSelectChange'); const wrapper = mount( { }} + value="foo" + name="bar" + onChange={() => { }} labelName={label} data={mockData} /> @@ -29,11 +32,13 @@ describe('', () => { wrapper.find('select').simulate('change'); expect(spy).toHaveBeenCalled(); }); + test('content not rendered when data property is falsey', () => { const wrapper = mount( { }} + value="foo" + name="bar" + onChange={() => { }} labelName={label} data={null} /> diff --git a/__tests__/components/Lookup.test.jsx b/__tests__/components/Lookup.test.jsx index d5623aad55..5b1d5233cf 100644 --- a/__tests__/components/Lookup.test.jsx +++ b/__tests__/components/Lookup.test.jsx @@ -7,14 +7,35 @@ let mockData = [{ name: 'foo', id: 1, isChecked: false }]; describe('', () => { test('initially renders succesfully', () => { mount( - { }} - data={mockData} - selected={[]} - /> + + { }} + getItems={() => { }} + /> + ); }); + test('API response is formatted properly', (done) => { + const wrapper = mount( + + { }} + getItems={() => ({ data: { results: [{ name: 'test instance', id: 1 }] } })} + /> + + ).find('Lookup'); + + setImmediate(() => { + expect(wrapper.state().results).toEqual([{ id: 1, name: 'test instance' }]); + done(); + }); + }); test('Opens modal when search icon is clicked', () => { const spy = jest.spyOn(Lookup.prototype, 'handleModalToggle'); const mockSelected = [{ name: 'foo', id: 1 }]; @@ -22,9 +43,10 @@ describe('', () => { { }} - data={mockData} - selected={mockSelected} + getItems={() => { }} /> ).find('Lookup'); @@ -39,34 +61,39 @@ describe('', () => { }]); expect(wrapper.state('isModalOpen')).toEqual(true); }); - test('calls "toggleSelected" when a user changes a checkbox', () => { + test('calls "toggleSelected" when a user changes a checkbox', (done) => { const spy = jest.spyOn(Lookup.prototype, 'toggleSelected'); + const mockSelected = [{ name: 'foo', id: 1 }]; const wrapper = mount( { }} - data={mockData} - selected={[]} + getItems={() => ({ data: { results: [{ name: 'test instance', id: 1 }] } })} /> ); - const searchItem = wrapper.find('.pf-c-input-group__text#search'); - searchItem.first().simulate('click'); - wrapper.find('input[type="checkbox"]').simulate('change'); - expect(spy).toHaveBeenCalled(); + setImmediate(() => { + const searchItem = wrapper.find('.pf-c-input-group__text#search'); + searchItem.first().simulate('click'); + wrapper.find('input[type="checkbox"]').simulate('change'); + expect(spy).toHaveBeenCalled(); + done(); + }); }); test('calls "toggleSelected" when remove icon is clicked', () => { const spy = jest.spyOn(Lookup.prototype, 'toggleSelected'); mockData = [{ name: 'foo', id: 1, isChecked: false }, { name: 'bar', id: 2, isChecked: true }]; - const mockSelected = [{ name: 'foo', id: 1 }, { name: 'bar', id: 2 }]; const wrapper = mount( { }} - data={mockData} - selected={mockSelected} + getItems={() => { }} /> ); @@ -124,9 +151,10 @@ describe('', () => { { }} /> ).find('Lookup'); @@ -142,6 +170,6 @@ describe('', () => { expect(onLookupSaveFn).toHaveBeenCalledWith([{ id: 1, name: 'foo' - }]); + }], 'fooBar'); }); }); diff --git a/__tests__/pages/Organizations/screens/OrganizationAdd.test.jsx b/__tests__/pages/Organizations/screens/OrganizationAdd.test.jsx index 27e0b7b07b..6a41d1d1ff 100644 --- a/__tests__/pages/Organizations/screens/OrganizationAdd.test.jsx +++ b/__tests__/pages/Organizations/screens/OrganizationAdd.test.jsx @@ -1,27 +1,32 @@ import React from 'react'; import { mount } from 'enzyme'; import { MemoryRouter } from 'react-router-dom'; +import { I18nProvider } from '@lingui/react'; import OrganizationAdd from '../../../../src/pages/Organizations/screens/OrganizationAdd'; describe('', () => { test('initially renders succesfully', () => { mount( - + + + ); }); - test('calls "handleChange" when input values change', () => { - const spy = jest.spyOn(OrganizationAdd.WrappedComponent.prototype, 'handleChange'); + test('calls "onFieldChange" when input values change', () => { + const spy = jest.spyOn(OrganizationAdd.WrappedComponent.prototype, 'onFieldChange'); const wrapper = mount( - + + + ); expect(spy).not.toHaveBeenCalled(); @@ -33,79 +38,69 @@ describe('', () => { const spy = jest.spyOn(OrganizationAdd.WrappedComponent.prototype, 'onSubmit'); const wrapper = mount( - + + + ); expect(spy).not.toHaveBeenCalled(); - wrapper.find('button.at-C-SubmitButton').prop('onClick')(); + wrapper.find('button[aria-label="Save"]').prop('onClick')(); expect(spy).toBeCalled(); }); test('calls "onCancel" when Cancel button is clicked', () => { const spy = jest.spyOn(OrganizationAdd.WrappedComponent.prototype, 'onCancel'); const wrapper = mount( - + + + ); expect(spy).not.toHaveBeenCalled(); - wrapper.find('button.at-C-CancelButton').prop('onClick')(); + wrapper.find('button[aria-label="Cancel"]').prop('onClick')(); expect(spy).toBeCalled(); }); - test('API response is formatted properly', (done) => { - const mockedResp = { data: { results: [{ name: 'test instance', id: 1 }] } }; - const api = { getInstanceGroups: jest.fn().mockResolvedValue(mockedResp) }; - const wrapper = mount( - - - - ); - - setImmediate(() => { - const orgAddElem = wrapper.find('OrganizationAdd'); - expect([{ id: 1, isChecked: false, name: 'test instance' }]).toEqual(orgAddElem.state().results); - done(); - }); - }); - test('Successful form submission triggers redirect', (done) => { const onSuccess = jest.spyOn(OrganizationAdd.WrappedComponent.prototype, 'onSuccess'); - const resetForm = jest.spyOn(OrganizationAdd.WrappedComponent.prototype, 'resetForm'); const mockedResp = { data: { id: 1, related: { instance_groups: '/bar' } } }; const api = { createOrganization: jest.fn().mockResolvedValue(mockedResp), createInstanceGroups: jest.fn().mockResolvedValue('done') }; const wrapper = mount( - + + + ); wrapper.find('input#add-org-form-name').simulate('change', { target: { value: 'foo' } }); - wrapper.find('button.at-C-SubmitButton').prop('onClick')(); + wrapper.find('button[aria-label="Save"]').prop('onClick')(); setImmediate(() => { expect(onSuccess).toHaveBeenCalled(); - expect(resetForm).toHaveBeenCalled(); done(); }); }); - test('updateSelectedInstanceGroups successfully sets selectedInstanceGroups state', () => { + test('onLookupSave successfully sets instanceGroups state', () => { const wrapper = mount( - + + + ).find('OrganizationAdd'); - wrapper.instance().updateSelectedInstanceGroups([ + wrapper.instance().onLookupSave([ { id: 1, name: 'foo' } - ]); - expect(wrapper.state('selectedInstanceGroups')).toEqual([ + ], 'instanceGroups'); + expect(wrapper.state('instanceGroups')).toEqual([ { id: 1, name: 'foo' @@ -113,14 +108,16 @@ describe('', () => { ]); }); - test('onSelectChange successfully sets custom_virtualenv state', () => { + test('onFieldChange successfully sets custom_virtualenv state', () => { const wrapper = mount( - + + + ).find('OrganizationAdd'); - wrapper.instance().onSelectChange('foobar'); - expect(wrapper.state('custom_virtualenv')).toBe('foobar'); + wrapper.instance().onFieldChange('fooBar', { target: { name: 'custom_virtualenv' } }); + expect(wrapper.state('custom_virtualenv')).toBe('fooBar'); }); test('onSubmit posts instance groups from selectedInstanceGroups', async () => { @@ -140,12 +137,14 @@ describe('', () => { }; const wrapper = mount( - + + + ).find('OrganizationAdd'); wrapper.setState({ name: 'mock org', - selectedInstanceGroups: [{ + instanceGroups: [{ id: 1, name: 'foo' }] diff --git a/src/api.js b/src/api.js index 028959b186..f0d3c866b8 100644 --- a/src/api.js +++ b/src/api.js @@ -106,8 +106,8 @@ class APIClient { return this.http.post(endpoint, data); } - getInstanceGroups () { - return this.http.get(API_INSTANCE_GROUPS); + getInstanceGroups (params) { + return this.http.get(API_INSTANCE_GROUPS, { params }); } createInstanceGroups (url, id) { diff --git a/src/app.scss b/src/app.scss index 555c87ecbc..393c1ac3f6 100644 --- a/src/app.scss +++ b/src/app.scss @@ -157,8 +157,11 @@ // .pf-c-modal-box__footer { - --pf-c-modal-box__footer--PaddingTop: 0; - --pf-c-modal-box__footer--PaddingBottom: 0; + --pf-c-modal-box__footer--PaddingTop: 20px; + --pf-c-modal-box__footer--PaddingRight: 20px; + --pf-c-modal-box__footer--PaddingBottom: 20px; + --pf-c-modal-box__footer--PaddingLeft: 20px; + justify-content: flex-end; } .pf-c-modal-box__header { @@ -171,6 +174,7 @@ .pf-c-modal-box__body { --pf-c-modal-box__body--PaddingLeft: 20px; --pf-c-modal-box__body--PaddingRight: 20px; + --pf-c-modal-box__body--PaddingBottom: 5px; } // @@ -215,12 +219,6 @@ } } -.at-align-right { - display: flex; - flex-direction: row; - justify-content: flex-end; -} - .awx-c-list { border-top: 1px solid #d7d7d7; border-bottom: 1px solid #d7d7d7; @@ -240,4 +238,4 @@ --pf-c-card__footer--PaddingY: 0; --pf-c-card__body--PaddingX: 0; --pf-c-card__body--PaddingY: 0; -} \ No newline at end of file +} diff --git a/src/components/AnsibleSelect/AnsibleSelect.jsx b/src/components/AnsibleSelect/AnsibleSelect.jsx index 4b2a6520f9..28563eefc9 100644 --- a/src/components/AnsibleSelect/AnsibleSelect.jsx +++ b/src/components/AnsibleSelect/AnsibleSelect.jsx @@ -1,7 +1,6 @@ import React from 'react'; import { - FormGroup, Select, SelectOption, } from '@patternfly/react-core'; @@ -25,32 +24,28 @@ class AnsibleSelect extends React.Component { return null; } - onSelectChange (val) { - const { selectChange } = this.props; - selectChange(val); + onSelectChange (val, event) { + const { onChange, name } = this.props; + event.target.name = name; + onChange(val, event); } render () { const { count } = this.state; - const { labelName, selected, data, defaultSelected } = this.props; + const { label = '', value, data, defaultSelected } = this.props; let elem; if (count > 1) { elem = ( - - - + ); } else { elem = null; } - return elem; } } - export default AnsibleSelect; diff --git a/src/components/DataListToolbar/styles.scss b/src/components/DataListToolbar/styles.scss index 7c17f50d56..535d6aacaf 100644 --- a/src/components/DataListToolbar/styles.scss +++ b/src/components/DataListToolbar/styles.scss @@ -1,6 +1,6 @@ .awx-toolbar { --awx-toolbar--BackgroundColor: var(--pf-global--BackgroundColor--light-100); - --awx-toolbar--BorderColor: var(--pf-global--Color--light-200); + --awx-toolbar--BorderColor: #ebebeb; --awx-toolbar--BorderWidth: var(--pf-global--BorderWidth--sm); border-bottom: var(--awx-toolbar--BorderWidth) solid var(--awx-toolbar--BorderColor); diff --git a/src/components/FormActionGroup.jsx b/src/components/FormActionGroup.jsx new file mode 100644 index 0000000000..3da293fe62 --- /dev/null +++ b/src/components/FormActionGroup.jsx @@ -0,0 +1,39 @@ +import React from 'react'; + +import { I18n } from '@lingui/react'; +import { t } from '@lingui/macro'; + +import { + ActionGroup, + Toolbar, + ToolbarGroup, + Button +} from '@patternfly/react-core'; + +const formActionGroupStyle = { + display: 'flex', + flexDirection: 'row', + justifyContent: 'flex-end', + marginTop: '10px' +}; + +const buttonGroupStyle = { + marginRight: '20px' +}; + +export default ({ onSubmit, submitDisabled, onCancel }) => ( + + {({ i18n }) => ( + + + + + + + + + + + )} + +); diff --git a/src/components/Lookup/Lookup.jsx b/src/components/Lookup/Lookup.jsx index c4ffe97030..729325192f 100644 --- a/src/components/Lookup/Lookup.jsx +++ b/src/components/Lookup/Lookup.jsx @@ -1,33 +1,82 @@ -import React from 'react'; +import React, { Fragment } from 'react'; -import { SearchIcon } from '@patternfly/react-icons'; +import { SearchIcon, CubesIcon } from '@patternfly/react-icons'; import { Modal, Button, - ActionGroup, - Toolbar, - ToolbarGroup, + EmptyState, + EmptyStateIcon, + EmptyStateBody, + Title } from '@patternfly/react-core'; import { I18n } from '@lingui/react'; -import { t, Trans } from '@lingui/macro'; +import { Trans, t } from '@lingui/macro'; import CheckboxListItem from '../ListItem'; - import SelectedList from '../SelectedList'; +import Pagination from '../Pagination'; + +const paginationStyling = { + paddingLeft: '0', + justifyContent: 'flex-end', + borderRight: '1px solid #ebebeb', + borderBottom: '1px solid #ebebeb', + borderTop: '0' +}; class Lookup extends React.Component { constructor (props) { super(props); this.state = { isModalOpen: false, - lookupSelectedItems: [] + lookupSelectedItems: [], + results: [], + count: 0, + page: 1, + page_size: 5, + error: null }; + 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); } + componentDidMount () { + const { page_size, page } = this.state; + this.getData({ page_size, page }); + } + + async getData (queryParams) { + const { getItems } = this.props; + const { page } = queryParams; + + this.setState({ error: false }); + + try { + const { data } = await getItems(queryParams); + const { results, count } = data; + + const stateToUpdate = { + page, + results, + count + }; + + this.setState(stateToUpdate); + } catch (err) { + this.setState({ error: true }); + } + } + + onSetPage = async (pageNumber, pageSize) => { + const page = parseInt(pageNumber, 10); + const page_size = parseInt(pageSize, 10); + this.getData({ page_size, page }); + }; + toggleSelected (row) { const { lookupSelectedItems } = this.state; const selectedIndex = lookupSelectedItems @@ -44,12 +93,12 @@ class Lookup extends React.Component { handleModalToggle () { const { isModalOpen } = this.state; - const { selected } = this.props; + const { value } = this.props; // Resets the selected items from parent state whenever modal is opened // This handles the case where the user closes/cancels the modal and // opens it again if (!isModalOpen) { - this.setState({ lookupSelectedItems: [...selected] }); + this.setState({ lookupSelectedItems: [...value] }); } this.setState((prevState) => ({ isModalOpen: !prevState.isModalOpen, @@ -57,13 +106,13 @@ class Lookup extends React.Component { } saveModal () { - const { onLookupSave } = this.props; + const { onLookupSave, name } = this.props; const { lookupSelectedItems } = this.state; - onLookupSave(lookupSelectedItems); + onLookupSave(lookupSelectedItems, name); this.handleModalToggle(); } - wrapTags (tags) { + wrapTags (tags = []) { return tags.map(tag => ( {tag.name} @@ -75,34 +124,61 @@ class Lookup extends React.Component { } render () { - const { isModalOpen, lookupSelectedItems } = this.state; - const { data, lookupHeader, selected } = this.props; + const { isModalOpen, lookupSelectedItems, error, results, count, page, page_size } = this.state; + const { lookupHeader = 'items', value } = this.props; + return ( -
- -
{this.wrapTags(selected)}
- -
    - {data.map(i => ( - item.id === i.id)} - onSelect={() => this.toggleSelected(i)} - /> - ))} -
- {lookupSelectedItems.length > 0 && ( - - {({ i18n }) => ( + + {({ i18n }) => ( +
+ +
{this.wrapTags(value)}
+ {i18n._(t`Save`)}, + + ]} + > + {(results.length === 0) ? ( + + + + <Trans>{`No ${lookupHeader} Found`}</Trans> + + + {`Please add ${lookupHeader.toLowerCase()} to populate this list`} + + + ) : ( + +
    + {results.map(i => ( + item.id === i.id)} + onSelect={() => this.toggleSelected(i)} + /> + ))} +
+ +
+ )} + {lookupSelectedItems.length > 0 && ( )} - - )} - - - - - - - - - - -
-
+ { error ?
error
: '' } +
+
+ )} + ); } } diff --git a/src/components/Pagination/Pagination.jsx b/src/components/Pagination/Pagination.jsx index 8bbfe6360d..c96e9c94a1 100644 --- a/src/components/Pagination/Pagination.jsx +++ b/src/components/Pagination/Pagination.jsx @@ -105,10 +105,13 @@ class Pagination extends Component { pageCount, page_size, pageSizeOptions, + style } = this.props; const { value, isOpen } = this.state; - - const opts = pageSizeOptions.slice().reverse().filter(o => o !== page_size); + let opts; + if (pageSizeOptions) { + opts = pageSizeOptions.slice().reverse().filter(o => o !== page_size); + } const isOnFirst = page === 1; const isOnLast = page === pageCount; @@ -119,33 +122,35 @@ class Pagination extends Component { return ( {({ i18n }) => ( -
-
- Items Per Page - - {page_size} - - )} - > - {opts.map(option => ( - - {option} - - ))} - -
+
+ {opts && ( +
+ Items Per Page + + {page_size} + + )} + > + {opts.map(option => ( + + {option} + + ))} + +
+ )}
{`Items ${itemMin} - ${itemMax} of ${count}`} diff --git a/src/components/Pagination/styles.scss b/src/components/Pagination/styles.scss index 3da41d5c7b..78141870ba 100644 --- a/src/components/Pagination/styles.scss +++ b/src/components/Pagination/styles.scss @@ -1,6 +1,6 @@ .awx-pagination { --awx-pagination--BackgroundColor: var(--pf-global--BackgroundColor--light-100); - --awx-pagination--BorderColor: var(--pf-global--BackgroundColor--light-300); + --awx-pagination--BorderColor: #dbdbdb; --awx-pagination--disabled-BackgroundColor: #f2f2f2; --awx-pagination--disabled-Color: #C2C2CA; diff --git a/src/components/SelectedList/SelectedList.jsx b/src/components/SelectedList/SelectedList.jsx index 3e11549024..1a82e45ef9 100644 --- a/src/components/SelectedList/SelectedList.jsx +++ b/src/components/SelectedList/SelectedList.jsx @@ -3,6 +3,18 @@ import { Chip } from '@patternfly/react-core'; +const selectedRowStyling = { + paddingTop: '15px', + paddingBottom: '5px', + borderLeft: '0', + borderRight: '0' +}; + +const selectedLabelStyling = { + fontSize: '14px', + fontWeight: 'bold' +}; + class SelectedList extends Component { constructor (props) { super(props); @@ -23,8 +35,8 @@ class SelectedList extends Component { const { showOverflow } = this.state; return (
-
-
+
+
{label}
diff --git a/src/components/SelectedList/styles.scss b/src/components/SelectedList/styles.scss index dbf385d251..4bf49ecfa7 100644 --- a/src/components/SelectedList/styles.scss +++ b/src/components/SelectedList/styles.scss @@ -1,6 +1,6 @@ .awx-selectedList { --awx-selectedList--BackgroundColor: var(--pf-global--BackgroundColor--light-100); - --awx-selectedList--BorderColor: #d7d7d7; + --awx-selectedList--BorderColor: #ebebeb; --awx-selectedList--BorderWidth: var(--pf-global--BorderWidth--sm); --awx-selectedList--FontSize: var(--pf-c-chip__text--FontSize); diff --git a/src/pages/Organizations/screens/OrganizationAdd.jsx b/src/pages/Organizations/screens/OrganizationAdd.jsx index 27558e2757..6b31536b10 100644 --- a/src/pages/Organizations/screens/OrganizationAdd.jsx +++ b/src/pages/Organizations/screens/OrganizationAdd.jsx @@ -6,10 +6,6 @@ import { Form, FormGroup, TextInput, - ActionGroup, - Toolbar, - ToolbarGroup, - Button, Gallery, Card, CardBody, @@ -18,76 +14,57 @@ import { import { ConfigContext } from '../../../context'; import Lookup from '../../../components/Lookup'; import AnsibleSelect from '../../../components/AnsibleSelect'; - -const format = (data) => { - const results = data.results.map((result) => ({ - id: result.id, - name: result.name, - isChecked: false - })); - return results; -}; +import FormActionGroup from '../../../components/FormActionGroup'; class OrganizationAdd extends React.Component { constructor (props) { super(props); - this.handleChange = this.handleChange.bind(this); - this.onSelectChange = this.onSelectChange.bind(this); + this.getInstanceGroups = this.getInstanceGroups.bind(this); + this.onFieldChange = this.onFieldChange.bind(this); + this.onLookupSave = this.onLookupSave.bind(this); this.onSubmit = this.onSubmit.bind(this); - this.resetForm = this.resetForm.bind(this); - this.onSuccess = this.onSuccess.bind(this); this.onCancel = this.onCancel.bind(this); - this.updateSelectedInstanceGroups = this.updateSelectedInstanceGroups.bind(this); + this.onSuccess = this.onSuccess.bind(this); + + this.state = { + name: '', + description: '', + custom_virtualenv: '', + instanceGroups: [], + error: '', + defaultEnv: '/venv/ansible/', + }; } - state = { - name: '', - description: '', - results: [], - custom_virtualenv: '', - error: '', - selectedInstanceGroups: [], - defaultEnv: '/venv/ansible/' - }; - - async componentDidMount () { - const { api } = this.props; - try { - const { data } = await api.getInstanceGroups(); - const results = format(data); - this.setState({ results }); - } catch (error) { - this.setState({ error }); - } + onFieldChange (val, evt) { + this.setState({ [evt.target.name]: val || evt.target.value }); } - onSelectChange (value) { - this.setState({ custom_virtualenv: value }); + onLookupSave (val, targetName) { + this.setState({ [targetName]: val }); } async onSubmit () { const { api } = this.props; - const { name, description, custom_virtualenv } = this.state; + const { name, description, custom_virtualenv, instanceGroups } = this.state; const data = { name, description, custom_virtualenv }; - const { selectedInstanceGroups } = this.state; try { const { data: response } = await api.createOrganization(data); - const url = response.related.instance_groups; + const instanceGroupsUrl = response.related.instance_groups; try { - if (selectedInstanceGroups.length > 0) { - selectedInstanceGroups.forEach(async (select) => { - await api.createInstanceGroups(url, select.id); + if (instanceGroups.length > 0) { + instanceGroups.forEach(async (select) => { + await api.createInstanceGroups(instanceGroupsUrl, select.id); }); } } catch (err) { this.setState({ error: err }); } finally { - this.resetForm(); this.onSuccess(response.id); } } catch (err) { @@ -105,32 +82,19 @@ class OrganizationAdd extends React.Component { history.push(`/organizations/${id}`); } - updateSelectedInstanceGroups (selectedInstanceGroups) { - this.setState({ selectedInstanceGroups }); - } - - handleChange (_, evt) { - this.setState({ [evt.target.name]: evt.target.value }); - } - - resetForm () { - this.setState({ - name: '', - description: '', - }); - const { results } = this.state; - const reset = results.map((result) => ({ id: result.id, name: result.name, isChecked: false })); - this.setState({ results: reset }); + async getInstanceGroups (params) { + const { api } = this.props; + const data = await api.getInstanceGroups(params); + return data; } render () { const { name, - results, description, custom_virtualenv, - selectedInstanceGroups, defaultEnv, + instanceGroups, error } = this.state; const enabled = name.length > 0; // TODO: add better form validation @@ -148,11 +112,10 @@ class OrganizationAdd extends React.Component { > @@ -160,40 +123,39 @@ class OrganizationAdd extends React.Component { id="add-org-form-description" name="description" value={description} - onChange={this.handleChange} + onChange={this.onFieldChange} /> - + {({ custom_virtualenvs }) => ( - + + + )} - - - - - - - - - - - { error ?
error
: '' } + + {error ?
error
: ''}