Merge pull request #84 from jakemcdermott/tests-fixup

add more unit and functional test coverage
This commit is contained in:
Jake McDermott 2019-01-07 07:50:11 -05:00 committed by GitHub
commit 976c490dc3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 273 additions and 144 deletions

View File

@ -1,5 +1,5 @@
import React from 'react';
import { HashRouter } from 'react-router-dom';
import { MemoryRouter } from 'react-router-dom';
import { I18nProvider } from '@lingui/react';
import { mount, shallow } from 'enzyme';
@ -12,7 +12,7 @@ const DEFAULT_ACTIVE_GROUP = 'views_group';
describe('<App />', () => {
test('expected content is rendered', () => {
const appWrapper = mount(
<HashRouter>
<MemoryRouter>
<I18nProvider>
<App
routeGroups={[
@ -37,7 +37,7 @@ describe('<App />', () => {
)}
/>
</I18nProvider>
</HashRouter>
</MemoryRouter>
);
// page components
@ -56,6 +56,48 @@ describe('<App />', () => {
expect(appWrapper.find('#group_two').length).toBe(1);
});
test('opening the about modal renders prefetched config data', async (done) => {
const ansible_version = '111';
const version = '222';
const getConfig = jest.fn(() => Promise.resolve({ data: { ansible_version, version} }));
const api = { getConfig };
const wrapper = mount(
<MemoryRouter>
<I18nProvider>
<App api={api}/>
</I18nProvider>
</MemoryRouter>
);
await asyncFlush();
expect(getConfig).toHaveBeenCalledTimes(1);
// open about modal
const aboutDropdown = 'Dropdown QuestionCircleIcon';
const aboutButton = 'DropdownItem li button';
const aboutModalContent = 'AboutModalBoxContent';
const aboutModalClose = 'button[aria-label="Close Dialog"]';
expect(wrapper.find(aboutModalContent)).toHaveLength(0);
wrapper.find(aboutDropdown).simulate('click');
wrapper.find(aboutButton).simulate('click');
wrapper.update();
// check about modal content
const content = wrapper.find(aboutModalContent);
expect(content).toHaveLength(1);
expect(content.find('dd').text()).toContain(ansible_version);
expect(content.find('pre').text()).toContain(`< Tower ${version} >`);
// close about modal
wrapper.find(aboutModalClose).simulate('click');
expect(wrapper.find(aboutModalContent)).toHaveLength(0);
done();
});
test('onNavToggle sets state.isNavOpen to opposite', () => {
const appWrapper = shallow(<App />);
const { onNavToggle } = appWrapper.instance();
@ -66,17 +108,6 @@ describe('<App />', () => {
});
});
test('onLogoClick sets selected nav back to defaults', () => {
const appWrapper = shallow(<App />);
appWrapper.setState({ activeGroup: 'foo', activeItem: 'bar' });
expect(appWrapper.state().activeItem).toBe('bar');
expect(appWrapper.state().activeGroup).toBe('foo');
appWrapper.instance().onLogoClick();
expect(appWrapper.state().activeGroup).toBe(DEFAULT_ACTIVE_GROUP);
});
test('onLogout makes expected call to api client', async (done) => {
const logout = jest.fn(() => Promise.resolve());
const api = { logout };
@ -89,17 +120,4 @@ describe('<App />', () => {
done();
});
test('Component makes expected call to api client when mounted', () => {
const getConfig = jest.fn().mockImplementation(() => Promise.resolve({}));
const api = { getConfig };
const appWrapper = mount(
<HashRouter>
<I18nProvider>
<App api={api} />
</I18nProvider>
</HashRouter>
);
expect(getConfig).toHaveBeenCalledTimes(1);
});
});

View File

@ -58,4 +58,72 @@ describe('APIClient (api.js)', () => {
done();
});
test('logout calls expected http method', async (done) => {
const createPromise = () => Promise.resolve();
const mockHttp = ({ get: jest.fn(createPromise) });
const api = new APIClient(mockHttp);
await api.logout();
expect(mockHttp.get).toHaveBeenCalledTimes(1);
done();
});
test('getConfig calls expected http method', async (done) => {
const createPromise = () => Promise.resolve();
const mockHttp = ({ get: jest.fn(createPromise) });
const api = new APIClient(mockHttp);
await api.getConfig();
expect(mockHttp.get).toHaveBeenCalledTimes(1);
done();
});
test('getOrganizations calls http method with expected data', async (done) => {
const createPromise = () => Promise.resolve();
const mockHttp = ({ get: jest.fn(createPromise) });
const api = new APIClient(mockHttp);
const defaultParams = {};
const testParams = { foo: 'bar' };
await api.getOrganizations(testParams);
await api.getOrganizations();
expect(mockHttp.get).toHaveBeenCalledTimes(2);
expect(mockHttp.get.mock.calls[0][1]).toEqual({ params: testParams });
expect(mockHttp.get.mock.calls[1][1]).toEqual({ params: defaultParams });
done();
});
test('createOrganization calls http method with expected data', async (done) => {
const createPromise = () => Promise.resolve();
const mockHttp = ({ post: jest.fn(createPromise) });
const api = new APIClient(mockHttp);
const data = { name: 'test '};
await api.createOrganization(data);
expect(mockHttp.post).toHaveBeenCalledTimes(1);
expect(mockHttp.post.mock.calls[0][1]).toEqual(data);
done();
});
test('getOrganizationDetails calls http method with expected data', async (done) => {
const createPromise = () => Promise.resolve();
const mockHttp = ({ get: jest.fn(createPromise) });
const api = new APIClient(mockHttp);
await api.getOrganizationDetails(99);
expect(mockHttp.get).toHaveBeenCalledTimes(1);
expect(mockHttp.get.mock.calls[0][0]).toContain('99');
done();
});
});

View File

@ -29,4 +29,16 @@ describe('<AnsibleSelect />', () => {
wrapper.find('select').simulate('change');
expect(spy).toHaveBeenCalled();
});
});
test('content not rendered when data property is falsey', () => {
const wrapper = mount(
<AnsibleSelect
selected="foo"
selectChange={() => { }}
labelName={label}
data={null}
/>
);
expect(wrapper.find('FormGroup')).toHaveLength(0);
expect(wrapper.find('Select')).toHaveLength(0);
});
});

View File

@ -29,6 +29,7 @@ describe('<DataListToolbar />', () => {
<I18nProvider>
<DataListToolbar
isAllSelected={false}
showExpandCollapse={true}
sortedColumnKey="name"
sortOrder="ascending"
columns={columns}

View File

@ -122,7 +122,6 @@ describe('<Pagination />', () => {
test('submit a new page by typing in input works', () => {
const textInputSelector = '.pf-l-split__item.pf-m-main .pf-c-form-control';
const submitFormSelector = '.pf-l-split__item.pf-m-main form';
const onSetPage = jest.fn();
pagination = mount(
@ -137,6 +136,7 @@ describe('<Pagination />', () => {
/>
</I18nProvider>
);
const textInput = pagination.find(textInputSelector);
expect(textInput.length).toBe(1);
textInput.simulate('change');
@ -145,7 +145,7 @@ describe('<Pagination />', () => {
const submitForm = pagination.find(submitFormSelector);
expect(submitForm.length).toBe(1);
submitForm.simulate('submit');
pagination.setState({ value: 'invalid' });
pagination.find('Pagination').instance().setState({ value: 'invalid' });
submitForm.simulate('submit');
});

View File

@ -29,11 +29,10 @@ describe('<TowerLogo />', () => {
});
test('adds navigation to route history on click', () => {
const onLogoClick = jest.fn();
logoWrapper = mount(
<MemoryRouter>
<I18nProvider>
<TowerLogo onClick={onLogoClick} />
<TowerLogo linkTo="/" />
</I18nProvider>
</MemoryRouter>
);
@ -43,12 +42,26 @@ describe('<TowerLogo />', () => {
expect(towerLogoElem.props().history.length).toBe(2);
});
test('linkTo prop is optional', () => {
logoWrapper = mount(
<MemoryRouter>
<I18nProvider>
<TowerLogo />
</I18nProvider>
</MemoryRouter>
);
findChildren();
expect(towerLogoElem.props().history.length).toBe(1);
logoWrapper.simulate('click');
expect(towerLogoElem.props().history.length).toBe(1);
});
test('handles mouse over and out state.hover changes', () => {
const onLogoClick = jest.fn();
logoWrapper = mount(
<MemoryRouter>
<I18nProvider>
<TowerLogo onClick={onLogoClick} />
<TowerLogo />
</I18nProvider>
</MemoryRouter>
);

View File

@ -1,11 +1,11 @@
import { mount } from 'enzyme';
import { main } from '../src/index';
import { main, getLanguage } from '../src/index';
const render = template => mount(template);
const data = { custom_logo: 'foo', custom_login_info: '' }
describe('index.jsx', () => {
test('initialization', async (done) => {
test('login loads when unauthenticated', async (done) => {
const isAuthenticated = () => false;
const getRoot = jest.fn(() => Promise.resolve({ data }));
@ -13,7 +13,7 @@ describe('index.jsx', () => {
const wrapper = await main(render, api);
expect(api.getRoot).toHaveBeenCalled();
expect(wrapper.find('Dashboard')).toHaveLength(0);
expect(wrapper.find('App')).toHaveLength(0);
expect(wrapper.find('Login')).toHaveLength(1);
const { src } = wrapper.find('Login Brand img').props();
@ -22,7 +22,7 @@ describe('index.jsx', () => {
done();
});
test('dashboard is loaded when authenticated', async (done) => {
test('app loads when authenticated', async (done) => {
const isAuthenticated = () => true;
const getRoot = jest.fn(() => Promise.resolve({ data }));
@ -30,9 +30,22 @@ describe('index.jsx', () => {
const wrapper = await main(render, api);
expect(api.getRoot).toHaveBeenCalled();
expect(wrapper.find('Dashboard')).toHaveLength(1);
expect(wrapper.find('App')).toHaveLength(1);
expect(wrapper.find('Login')).toHaveLength(0);
wrapper.find('header a').simulate('click');
wrapper.update();
expect(wrapper.find('App')).toHaveLength(1);
expect(wrapper.find('Login')).toHaveLength(0);
done();
});
test('getLanguage returns the expected language code', () => {
expect(getLanguage({ languages: ['es-US'] })).toEqual('es');
expect(getLanguage({ languages: ['es-US'], language: 'fr-FR', userLanguage: 'en-US' })).toEqual('es');
expect(getLanguage({ language: 'fr-FR', userLanguage: 'en-US' })).toEqual('fr');
expect(getLanguage({ userLanguage: 'en-US' })).toEqual('en');
});
});

View File

@ -28,14 +28,12 @@ class App extends Component {
isAboutModalOpen: false,
isNavOpen,
version: null,
};
this.fetchConfig = this.fetchConfig.bind(this);
this.onLogout = this.onLogout.bind(this);
this.onAboutModalClose = this.onAboutModalClose.bind(this);
this.onAboutModalOpen = this.onAboutModalOpen.bind(this);
this.onLogoClick = this.onLogoClick.bind(this);
this.onNavToggle = this.onNavToggle.bind(this);
};
@ -73,10 +71,6 @@ class App extends Component {
this.setState(({ isNavOpen }) => ({ isNavOpen: !isNavOpen }));
}
onLogoClick () {
this.setState({ activeGroup: 'views_group' });
}
render () {
const {
ansible_version,
@ -106,11 +100,7 @@ class App extends Component {
<PageHeader
showNavToggle
onNavToggle={this.onNavToggle}
logo={
<TowerLogo
onClick={this.onLogoClick}
/>
}
logo={<TowerLogo linkTo="/"/>}
toolbar={
<PageHeaderToolbar
isAboutDisabled={!version}

View File

@ -47,9 +47,10 @@ class DataListToolbar extends React.Component {
this.handleSearchInputChange = this.handleSearchInputChange.bind(this);
this.onSortDropdownToggle = this.onSortDropdownToggle.bind(this);
this.onSortDropdownSelect = this.onSortDropdownSelect.bind(this);
this.onSearch = this.onSearch.bind(this);
this.onSearchDropdownToggle = this.onSearchDropdownToggle.bind(this);
this.onSearchDropdownSelect = this.onSearchDropdownSelect.bind(this);
this.onSearch = this.onSearch.bind(this);
this.onSort = this.onSort.bind(this);
}
handleSearchInputChange (searchValue) {
@ -62,12 +63,12 @@ class DataListToolbar extends React.Component {
onSortDropdownSelect ({ target }) {
const { columns, onSort, sortOrder } = this.props;
const { innerText } = target;
const [{ key }] = columns.filter(({ name }) => name === target.innerText);
const [{ key: searchKey }] = columns.filter(({ name }) => name === innerText);
this.setState({ isSortDropdownOpen: false });
onSort(key, sortOrder);
onSort(searchKey, sortOrder);
}
onSearchDropdownToggle (isSearchDropdownOpen) {
@ -76,11 +77,10 @@ class DataListToolbar extends React.Component {
onSearchDropdownSelect ({ target }) {
const { columns } = this.props;
const { innerText } = target;
const targetName = target.innerText;
const [{ key }] = columns.filter(({ name }) => name === targetName);
this.setState({ isSearchDropdownOpen: false, searchKey: key });
const [{ key: searchKey }] = columns.filter(({ name }) => name === innerText);
this.setState({ isSearchDropdownOpen: false, searchKey });
}
onSearch () {
@ -90,13 +90,19 @@ class DataListToolbar extends React.Component {
onSearch(searchValue);
}
onSort () {
const { onSort, sortedColumnKey, sortOrder } = this.props;
const newSortOrder = sortOrder === 'ascending' ? 'descending' : 'ascending';
onSort(sortedColumnKey, newSortOrder);
}
render () {
const { up } = DropdownPosition;
const {
columns,
isAllSelected,
onSelectAll,
onSort,
sortedColumnKey,
sortOrder,
addUrl,
@ -110,29 +116,15 @@ class DataListToolbar extends React.Component {
searchValue,
} = this.state;
const [searchColumn] = columns
.filter(({ key }) => key === searchKey);
const searchColumnName = searchColumn.name;
const [sortedColumn] = columns
const [{ name: searchColumnName }] = columns.filter(({ key }) => key === searchKey);
const [{ name: sortedColumnName, isNumeric }] = columns
.filter(({ key }) => key === sortedColumnKey);
const sortedColumnName = sortedColumn.name;
const isSortNumeric = sortedColumn.isNumeric;
const displayedSortIcon = () => {
let icon;
if (sortOrder === 'ascending') {
icon = isSortNumeric ? (<SortNumericUpIcon />) : (<SortAlphaUpIcon />);
} else {
icon = isSortNumeric ? (<SortNumericDownIcon />) : (<SortAlphaDownIcon />);
}
return icon;
};
const searchDropdownItems = columns
.filter(({ key }) => key !== searchKey)
.map(({ key, name }) => (
<DropdownItem key={key} component="button">
{ name }
{name}
</DropdownItem>
));
@ -140,17 +132,26 @@ class DataListToolbar extends React.Component {
.filter(({ key, isSortable }) => isSortable && key !== sortedColumnKey)
.map(({ key, name }) => (
<DropdownItem key={key} component="button">
{ name }
{name}
</DropdownItem>
));
let SortIcon;
if (isNumeric) {
SortIcon = sortOrder === 'ascending' ? SortNumericUpIcon : SortNumericDownIcon;
} else {
SortIcon = sortOrder === 'ascending' ? SortAlphaUpIcon : SortAlphaDownIcon;
}
return (
<I18n>
{({ i18n }) => (
<div className="awx-toolbar">
<Level>
<LevelItem>
<Toolbar style={{ marginLeft: '20px' }}>
<Toolbar
style={{ marginLeft: '20px' }}
>
<ToolbarGroup>
<ToolbarItem>
<Checkbox
@ -174,7 +175,7 @@ class DataListToolbar extends React.Component {
<DropdownToggle
onToggle={this.onSearchDropdownToggle}
>
{ searchColumnName }
{searchColumnName}
</DropdownToggle>
)}
dropdownItems={searchDropdownItems}
@ -195,7 +196,9 @@ class DataListToolbar extends React.Component {
</div>
</ToolbarItem>
</ToolbarGroup>
<ToolbarGroup className="sortDropdownGroup">
<ToolbarGroup
className="sortDropdownGroup"
>
<ToolbarItem>
<Dropdown
onToggle={this.onSortDropdownToggle}
@ -206,7 +209,7 @@ class DataListToolbar extends React.Component {
<DropdownToggle
onToggle={this.onSortDropdownToggle}
>
{ sortedColumnName }
{sortedColumnName}
</DropdownToggle>
)}
dropdownItems={sortDropdownItems}
@ -214,23 +217,29 @@ class DataListToolbar extends React.Component {
</ToolbarItem>
<ToolbarItem>
<Button
onClick={() => onSort(sortedColumnKey, sortOrder === 'ascending' ? 'descending' : 'ascending')}
onClick={this.onSort}
variant="plain"
aria-label={i18n._(t`Sort`)}
>
{displayedSortIcon()}
<SortIcon/>
</Button>
</ToolbarItem>
</ToolbarGroup>
{ showExpandCollapse && (
{showExpandCollapse && (
<ToolbarGroup>
<ToolbarItem>
<Button variant="plain" aria-label={i18n._(t`Expand`)}>
<Button
variant="plain"
aria-label={i18n._(t`Expand`)}
>
<BarsIcon />
</Button>
</ToolbarItem>
<ToolbarItem>
<Button variant="plain" aria-label={i18n._(t`Collapse`)}>
<Button
variant="plain"
aria-label={i18n._(t`Collapse`)}
>
<EqualsIcon />
</Button>
</ToolbarItem>
@ -239,14 +248,23 @@ class DataListToolbar extends React.Component {
</Toolbar>
</LevelItem>
<LevelItem>
<Tooltip message={i18n._(t`Delete`)} position="top">
<Button variant="plain" aria-label={i18n._(t`Delete`)}>
<Tooltip
message={i18n._(t`Delete`)}
position="top"
>
<Button
variant="plain"
aria-label={i18n._(t`Delete`)}
>
<TrashAltIcon />
</Button>
</Tooltip>
{addUrl && (
<Link to={addUrl}>
<Button variant="primary" aria-label={i18n._(t`Add`)}>
<Button
variant="primary"
aria-label={i18n._(t`Add`)}
>
<PlusIcon />
</Button>
</Link>

View File

@ -18,8 +18,7 @@ class Pagination extends Component {
constructor (props) {
super(props);
const { page } = this.props;
const { page } = props;
this.state = { value: page, isOpen: false };
this.onPageChange = this.onPageChange.bind(this);
@ -70,18 +69,14 @@ class Pagination extends Component {
const { onSetPage, page, page_size } = this.props;
const previousPage = page - 1;
if (previousPage >= 1) {
onSetPage(previousPage, page_size);
}
onSetPage(previousPage, page_size);
}
onNext () {
const { onSetPage, page, pageCount, page_size } = this.props;
const nextPage = page + 1;
if (nextPage <= pageCount) {
onSetPage(nextPage, page_size);
}
onSetPage(nextPage, page_size);
}
onLast () {
@ -143,14 +138,20 @@ class Pagination extends Component {
direction={up}
isOpen={isOpen}
toggle={(
<DropdownToggle className="togglePageSize" onToggle={this.onTogglePageSize}>
{ page_size }
<DropdownToggle
className="togglePageSize"
onToggle={this.onTogglePageSize}
>
{page_size}
</DropdownToggle>
)}
>
{opts.map(option => (
<DropdownItem key={option} component="button">
{ option }
<DropdownItem
key={option}
component="button"
>
{option}
</DropdownItem>
))}
</Dropdown>
@ -159,7 +160,7 @@ class Pagination extends Component {
<LevelItem>
<Split gutter="md" className="pf-u-display-flex pf-u-align-items-center">
<SplitItem>
<Trans>{ itemMin } - { itemMax } of { count }</Trans>
<Trans>{itemMin} - {itemMax} of {count}</Trans>
</SplitItem>
<SplitItem>
<div className="pf-c-input-group">
@ -200,7 +201,7 @@ class Pagination extends Component {
value={value}
type="text"
onChange={this.onPageChange}
/> of { pageCount }
/> of {pageCount}
</Trans>
</form>
</SplitItem>

View File

@ -18,13 +18,11 @@ class TowerLogo extends Component {
}
onClick () {
const { history, onClick: handleClick } = this.props;
const { history, linkTo } = this.props;
if (!handleClick) return;
if (!linkTo) return;
history.push('/');
handleClick();
history.push(linkTo);
}
onHover () {
@ -35,11 +33,10 @@ class TowerLogo extends Component {
render () {
const { hover } = this.state;
const { onClick: handleClick } = this.props;
let src = TowerLogoHeader;
if (hover && handleClick) {
if (hover) {
src = TowerLogoHeaderHover;
}

View File

@ -60,21 +60,25 @@ const http = axios.create({ xsrfCookieName: 'csrftoken', xsrfHeaderName: 'X-CSRF
// see: https://developer.mozilla.org/en-US/docs/Web/API/Navigator
//
const language = (navigator.languages && navigator.languages[0])
|| navigator.language
|| navigator.userLanguage;
const languageWithoutRegionCode = language.toLowerCase().split(/[_-]+/)[0];
const catalogs = { en, ja };
export function getLanguage (nav) {
const language = (nav.languages && nav.languages[0]) || nav.language || nav.userLanguage;
const languageWithoutRegionCode = language.toLowerCase().split(/[_-]+/)[0];
return languageWithoutRegionCode;
};
//
// Function Main
//
export async function main (render, api) {
const catalogs = { en, ja };
const language = getLanguage(navigator);
const el = document.getElementById('app');
// fetch additional config from server
const { data: { custom_logo, custom_login_info } } = await api.getRoot();
const defaultRedirect = () => (<Redirect to="/home" />);
const loginRoutes = (
<Switch>
<Route
@ -94,7 +98,7 @@ export async function main (render, api) {
return render(
<HashRouter>
<I18nProvider
language={languageWithoutRegionCode}
language={language}
catalogs={catalogs}
>
<I18n>
@ -102,8 +106,8 @@ export async function main (render, api) {
<Background>
{!api.isAuthenticated() ? loginRoutes : (
<Switch>
<Route path="/login" render={() => (<Redirect to="/home" />)} />
<Route exact path="/" render={() => (<Redirect to="/home" />)} />
<Route path="/login" render={defaultRedirect} />
<Route exact path="/" render={defaultRedirect} />
<Route
render={() => (
<App

View File

@ -48,7 +48,7 @@ class AWXLogin extends Component {
try {
await api.login(username, password);
} catch (error) {
if (error.response.status === 401) {
if (error.response && error.response.status === 401) {
this.setState({ isInputValid: false });
}
} finally {

View File

@ -1,4 +1,5 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { withRouter } from 'react-router-dom';
import { Trans } from '@lingui/macro';
import {
@ -17,8 +18,8 @@ import {
CardBody,
} from '@patternfly/react-core';
import AnsibleSelect from '../../../components/AnsibleSelect';
import { ConfigContext } from '../../../context';
import AnsibleSelect from '../../../components/AnsibleSelect'
const { light } = PageSectionVariants;
class OrganizationAdd extends React.Component {
@ -37,8 +38,6 @@ class OrganizationAdd extends React.Component {
description: '',
instanceGroups: '',
custom_virtualenv: '',
custom_virtualenvs: [],
hideAnsibleSelect: true,
error:'',
};
@ -59,9 +58,9 @@ class OrganizationAdd extends React.Component {
}
async onSubmit() {
const { api } = this.props;
const data = Object.assign({}, { ...this.state });
await api.createOrganization(data);
this.resetForm();
}
@ -69,22 +68,10 @@ class OrganizationAdd extends React.Component {
this.props.history.push('/organizations');
}
async componentDidMount() {
try {
const { data } = await api.getConfig();
this.setState({ custom_virtualenvs: [...data.custom_virtualenvs] });
if (this.state.custom_virtualenvs.length > 1) {
// Show dropdown if we have more than one ansible environment
this.setState({ hideAnsibleSelect: !this.state.hideAnsibleSelect });
}
} catch (error) {
this.setState({ error })
}
}
render() {
const { name } = this.state;
const enabled = name.length > 0; // TODO: add better form validation
return (
<Fragment>
<PageSection variant={light} className="pf-m-condensed">
@ -128,13 +115,16 @@ class OrganizationAdd extends React.Component {
onChange={this.handleChange}
/>
</FormGroup>
<AnsibleSelect
labelName="Ansible Environment"
selected={this.state.custom_virtualenv}
selectChange={this.onSelectChange}
data={this.state.custom_virtualenvs}
hidden={this.state.hideAnsibleSelect}
/>
<ConfigContext.Consumer>
{({ custom_virtualenvs }) =>
<AnsibleSelect
labelName="Ansible Environment"
selected={this.state.custom_virtualenv}
selectChange={this.onSelectChange}
data={custom_virtualenvs}
/>
}
</ConfigContext.Consumer>
</Gallery>
<ActionGroup className="at-align-right">
<Toolbar>
@ -155,4 +145,8 @@ class OrganizationAdd extends React.Component {
}
}
OrganizationAdd.contextTypes = {
custom_virtualenvs: PropTypes.array,
};
export default withRouter(OrganizationAdd);