Merge pull request #72 from ansible/react-context-api

Implement React Context API
This commit is contained in:
Jake McDermott
2019-01-03 15:28:54 -05:00
committed by GitHub
11 changed files with 291 additions and 241 deletions

View File

@@ -1,5 +1,13 @@
const axios = require('axios'); import * as endpoints from '../src/endpoints';
const axios = require('axios');
const mockAPIConfigData = {
data: {
custom_virtualenvs: ['foo', 'bar'],
ansible_version: "2.7.2",
version: "2.1.1-40-g2758a3848"
}
};
jest.genMockFromModule('axios'); jest.genMockFromModule('axios');
axios.create = jest.fn(() => axios); axios.create = jest.fn(() => axios);
@@ -9,7 +17,16 @@ axios.create.mockReturnValue({
get: axios.get, get: axios.get,
post: axios.post post: axios.post
}); });
axios.get.mockResolvedValue('get results'); axios.get.mockImplementation((endpoint) => {
if (endpoint === endpoints.API_CONFIG) {
return new Promise((resolve, reject) => {
resolve(mockAPIConfigData);
});
}
else {
return 'get results';
}
});
axios.post.mockResolvedValue('post results'); axios.post.mockResolvedValue('post results');
axios.customClearMocks = () => { axios.customClearMocks = () => {

View File

@@ -3,7 +3,7 @@ import { MemoryRouter } from 'react-router-dom';
import { shallow, mount } from 'enzyme'; import { shallow, mount } from 'enzyme';
import App from '../src/App'; import App from '../src/App';
import api from '../src/api'; import api from '../src/api';
import { API_LOGOUT } from '../src/endpoints'; import { API_LOGOUT, API_CONFIG } from '../src/endpoints';
import Dashboard from '../src/pages/Dashboard'; import Dashboard from '../src/pages/Dashboard';
import Login from '../src/pages/Login'; import Login from '../src/pages/Login';
@@ -53,7 +53,13 @@ describe('<App />', () => {
expect(logOutButton.length).toBe(1); expect(logOutButton.length).toBe(1);
logOutButton.simulate('click'); logOutButton.simulate('click');
appWrapper.setState({ activeGroup: 'foo', activeItem: 'bar' }); appWrapper.setState({ activeGroup: 'foo', activeItem: 'bar' });
expect(api.get).toHaveBeenCalledTimes(1);
expect(api.get).toHaveBeenCalledWith(API_LOGOUT); expect(api.get).toHaveBeenCalledWith(API_LOGOUT);
}); });
test('Componenet makes REST call to API_CONFIG endpoint when mounted', () => {
api.get = jest.fn().mockImplementation(() => Promise.resolve({}));
const appWrapper = shallow(<App.WrappedComponent />);
expect(api.get).toHaveBeenCalledTimes(1);
expect(api.get).toHaveBeenCalledWith(API_CONFIG);
});
}); });

View File

@@ -31,26 +31,4 @@ describe('<About />', () => {
expect(onAboutModalClose).toBeCalled(); expect(onAboutModalClose).toBeCalled();
aboutWrapper.unmount(); aboutWrapper.unmount();
}); });
test('sets error on api request failure', async () => {
api.get = jest.fn().mockImplementation(() => {
const err = new Error('404 error');
err.response = { status: 404, message: 'problem' };
return Promise.reject(err);
});
aboutWrapper = mount(
<I18nProvider>
<About isOpen />
</I18nProvider>
);
const aboutComponentInstance = aboutWrapper.find(About).instance();
await aboutComponentInstance.componentDidMount();
expect(aboutComponentInstance.state.error.response.status).toBe(404);
aboutWrapper.unmount();
});
test('API Config endpoint is valid', () => {
expect(API_CONFIG).toBeDefined();
});
}); });

View File

@@ -2,16 +2,29 @@ import React from 'react';
import { mount } from 'enzyme'; import { mount } from 'enzyme';
import AnsibleSelect from '../../src/components/AnsibleSelect'; import AnsibleSelect from '../../src/components/AnsibleSelect';
const mockData = ['foo', 'bar']; const label = "test select"
const mockData = ["/venv/baz/", "/venv/ansible/"];
describe('<AnsibleSelect />', () => { describe('<AnsibleSelect />', () => {
test('initially renders succesfully', async() => { test('initially renders succesfully', async () => {
const wrapper = mount(<AnsibleSelect selected="foo" data={mockData} selectChange={() => {}} />); mount(
wrapper.setState({ isHidden: false }); <AnsibleSelect
selected="foo"
selectChange={() => { }}
labelName={label}
data={mockData}
/>
);
}); });
test('calls "onSelectChange" on dropdown select change', () => { test('calls "onSelectChange" on dropdown select change', () => {
const spy = jest.spyOn(AnsibleSelect.prototype, 'onSelectChange'); const spy = jest.spyOn(AnsibleSelect.prototype, 'onSelectChange');
const wrapper = mount(<AnsibleSelect selected="foo" data={mockData} selectChange={() => {}} />); const wrapper = mount(
wrapper.setState({ isHidden: false }); <AnsibleSelect
selected="foo"
selectChange={() => { }}
labelName={label}
data={mockData}
/>
);
expect(spy).not.toHaveBeenCalled(); expect(spy).not.toHaveBeenCalled();
wrapper.find('select').simulate('change'); wrapper.find('select').simulate('change');
expect(spy).toHaveBeenCalled(); expect(spy).toHaveBeenCalled();

View File

@@ -1,8 +1,29 @@
import React from 'react'; import React from 'react';
import { mount } from 'enzyme'; import { mount } from 'enzyme';
import OrganizationAdd from '../../../../src/pages/Organizations/views/Organization.add';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
let OrganizationAdd;
const getAppWithConfigContext = (context = {
custom_virtualenvs: ['foo', 'bar']
}) => {
// Mock the ConfigContext module being used in our OrganizationAdd component
jest.doMock('../../../../src/context', () => {
return {
ConfigContext: {
Consumer: (props) => props.children(context)
}
}
});
// Return the updated OrganizationAdd module with mocked context
return require('../../../../src/pages/Organizations/views/Organization.add').default;
};
beforeEach(() => {
OrganizationAdd = getAppWithConfigContext();
})
describe('<OrganizationAdd />', () => { describe('<OrganizationAdd />', () => {
test('initially renders succesfully', () => { test('initially renders succesfully', () => {
mount( mount(

View File

@@ -53,6 +53,7 @@
"@patternfly/react-styles": "^2.3.0", "@patternfly/react-styles": "^2.3.0",
"@patternfly/react-tokens": "^1.9.0", "@patternfly/react-tokens": "^1.9.0",
"axios": "^0.18.0", "axios": "^0.18.0",
"prop-types": "^15.6.2",
"react": "^16.4.1", "react": "^16.4.1",
"react-dom": "^16.4.1", "react-dom": "^16.4.1",
"react-router-dom": "^4.3.1" "react-router-dom": "^4.3.1"

View File

@@ -1,4 +1,6 @@
import React, { Fragment } from 'react'; import React, { Fragment } from 'react';
import { ConfigContext } from './context';
import { I18nProvider, I18n } from '@lingui/react'; import { I18nProvider, I18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { import {
@@ -22,7 +24,7 @@ import {
import { global_breakpoint_md as breakpointMd } from '@patternfly/react-tokens'; import { global_breakpoint_md as breakpointMd } from '@patternfly/react-tokens';
import api from './api'; import api from './api';
import { API_LOGOUT } from './endpoints'; import { API_LOGOUT, API_CONFIG } from './endpoints';
import HelpDropdown from './components/HelpDropdown'; import HelpDropdown from './components/HelpDropdown';
import LogoutButton from './components/LogoutButton'; import LogoutButton from './components/LogoutButton';
@@ -61,19 +63,21 @@ const catalogs = { en, ja };
// This spits out the language and the region. Example: es-US // This spits out the language and the region. Example: es-US
const language = (navigator.languages && navigator.languages[0]) const language = (navigator.languages && navigator.languages[0])
|| navigator.language || navigator.language
|| navigator.userLanguage; || navigator.userLanguage;
const languageWithoutRegionCode = language.toLowerCase().split(/[_-]+/)[0]; const languageWithoutRegionCode = language.toLowerCase().split(/[_-]+/)[0];
class App extends React.Component { class App extends React.Component {
constructor (props) { constructor(props) {
super(props); super(props);
const isNavOpen = typeof window !== 'undefined' && window.innerWidth >= parseInt(breakpointMd.value, 10); const isNavOpen = typeof window !== 'undefined' && window.innerWidth >= parseInt(breakpointMd.value, 10);
this.state = { this.state = {
isNavOpen isNavOpen,
config: {},
error: false,
}; };
} }
@@ -81,12 +85,27 @@ class App extends React.Component {
this.setState(({ isNavOpen }) => ({ isNavOpen: !isNavOpen })); this.setState(({ isNavOpen }) => ({ isNavOpen: !isNavOpen }));
}; };
onDevLogout = async () => { onLogoClick = () => {
await api.get(API_LOGOUT); this.setState({ activeGroup: 'views_group' });
} }
render () { onDevLogout = async () => {
const { isNavOpen } = this.state; await api.get(API_LOGOUT);
this.setState({ activeGroup: 'views_group', activeItem: 'views_group_dashboard' });
}
async componentDidMount() {
// Grab our config data from the API and store in state
try {
const { data } = await api.get(API_CONFIG);
this.setState({ config: data });
} catch (error) {
this.setState({ error });
}
}
render() {
const { isNavOpen, config } = this.state;
const { logo, loginInfo, history } = this.props; const { logo, loginInfo, history } = this.props;
const PageToolbar = ( const PageToolbar = (
@@ -118,118 +137,121 @@ class App extends React.Component {
[BackgroundImageSrc.filter]: '/assets/images/background-filter.svg' [BackgroundImageSrc.filter]: '/assets/images/background-filter.svg'
}} }}
/> />
<Switch> <ConfigContext.Provider value={config}>
<ConditionalRedirect <Switch>
shouldRedirect={() => api.isAuthenticated()} <ConditionalRedirect
redirectPath="/" shouldRedirect={() => api.isAuthenticated()}
path="/login" redirectPath="/"
component={() => <Login logo={logo} loginInfo={loginInfo} />} path="/login"
/> component={() => <Login logo={logo} loginInfo={loginInfo} />}
<Fragment> />
<Page <Fragment>
header={( <Page
<PageHeader header={(
logo={<TowerLogo />} <PageHeader
toolbar={PageToolbar} logo={<TowerLogo onClick={this.onLogoClick} />}
showNavToggle toolbar={PageToolbar}
onNavToggle={this.onNavToggle} showNavToggle
/> onNavToggle={this.onNavToggle}
)} />
sidebar={( )}
<PageSidebar sidebar={(
isNavOpen={isNavOpen} <PageSidebar
nav={( isNavOpen={isNavOpen}
<I18n> nav={(
{({ i18n }) => ( <I18n>
<Nav aria-label={i18n._(t`Primary Navigation`)}> {({ i18n }) => (
<NavList> <Nav aria-label={i18n._(t`Primary Navigation`)}>
<NavExpandableGroup <NavList>
groupId="views_group" <NavExpandableGroup
title={i18n._("Views")} groupId="views_group"
routes={[ title={i18n._("Views")}
{ path: '/home', title: i18n._('Dashboard') }, routes={[
{ path: '/jobs', title: i18n._('Jobs') }, { path: '/home', title: i18n._('Dashboard') },
{ path: '/schedules', title: i18n._('Schedules') }, { path: '/jobs', title: i18n._('Jobs') },
{ path: '/portal', title: i18n._('Portal Mode') }, { path: '/schedules', title: i18n._('Schedules') },
]} { path: '/portal', title: i18n._('Portal Mode') },
/> ]}
<NavExpandableGroup />
groupId="resources_group" <NavExpandableGroup
title={i18n._("Resources")} groupId="resources_group"
routes={[ title={i18n._("Resources")}
{ path: '/templates', title: i18n._('Templates') }, routes={[
{ path: '/credentials', title: i18n._('Credentials') }, { path: '/templates', title: i18n._('Templates') },
{ path: '/projects', title: i18n._('Projects') }, { path: '/credentials', title: i18n._('Credentials') },
{ path: '/inventories', title: i18n._('Inventories') }, { path: '/projects', title: i18n._('Projects') },
{ path: '/inventory_scripts', title: i18n._('Inventory Scripts') } { path: '/inventories', title: i18n._('Inventories') },
]} { path: '/inventory_scripts', title: i18n._('Inventory Scripts') }
/> ]}
<NavExpandableGroup />
groupId="access_group" <NavExpandableGroup
title={i18n._("Access")} groupId="access_group"
routes={[ title={i18n._("Access")}
{ path: '/organizations', title: i18n._('Organizations') }, routes={[
{ path: '/users', title: i18n._('Users') }, { path: '/organizations', title: i18n._('Organizations') },
{ path: '/teams', title: i18n._('Teams') } { path: '/users', title: i18n._('Users') },
]} { path: '/teams', title: i18n._('Teams') }
/> ]}
<NavExpandableGroup />
groupId="administration_group" <NavExpandableGroup
title={i18n._("Administration")} groupId="administration_group"
routes={[ title={i18n._("Administration")}
{ path: '/credential_types', title: i18n._('Credential Types') }, routes={[
{ path: '/notification_templates', title: i18n._('Notifications') }, { path: '/credential_types', title: i18n._('Credential Types') },
{ path: '/management_jobs', title: i18n._('Management Jobs') }, { path: '/notification_templates', title: i18n._('Notifications') },
{ path: '/instance_groups', title: i18n._('Instance Groups') }, { path: '/management_jobs', title: i18n._('Management Jobs') },
{ path: '/applications', title: i18n._('Integrations') } { path: '/instance_groups', title: i18n._('Instance Groups') },
]} { path: '/applications', title: i18n._('Integrations') }
/> ]}
<NavExpandableGroup />
groupId="settings_group" <NavExpandableGroup
title={i18n._("Settings")} groupId="settings_group"
routes={[ title={i18n._("Settings")}
{ path: '/auth_settings', title: i18n._('Authentication') }, routes={[
{ path: '/jobs_settings', title: i18n._('Jobs') }, { path: '/auth_settings', title: i18n._('Authentication') },
{ path: '/system_settings', title: i18n._('System') }, { path: '/jobs_settings', title: i18n._('Jobs') },
{ path: '/ui_settings', title: i18n._('User Interface') }, { path: '/system_settings', title: i18n._('System') },
{ path: '/license', title: i18n._('License') } { path: '/ui_settings', title: i18n._('User Interface') },
]} { path: '/license', title: i18n._('License') }
/> ]}
</NavList> />
</Nav> </NavList>
)} </Nav>
</I18n> )}
)} </I18n>
/> )}
)} />
useCondensed )}
> useCondensed
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" exact path="/" component={() => (<Redirect to="/home" />)} /> >
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/home" component={Dashboard} /> <ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" exact path="/" component={() => (<Redirect to="/home" />)} />
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/jobs" component={Jobs} /> <ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/home" component={Dashboard} />
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/schedules" component={Schedules} /> <ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/jobs" component={Jobs} />
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/portal" component={Portal} /> <ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/schedules" component={Schedules} />
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/templates" component={Templates} /> <ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/portal" component={Portal} />
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/credentials" component={Credentials} /> <ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/templates" component={Templates} />
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/projects" component={Projects} /> <ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/credentials" component={Credentials} />
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/inventories" component={Inventories} /> <ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/projects" component={Projects} />
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/inventory_scripts" component={InventoryScripts} /> <ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/inventories" component={Inventories} />
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/organizations" component={Organizations} /> <ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/inventory_scripts" component={InventoryScripts} />
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/users" component={Users} /> <ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/organizations" component={Organizations} />
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/teams" component={Teams} /> <ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/users" component={Users} />
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/credential_types" component={CredentialTypes} /> <ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/teams" component={Teams} />
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/notification_templates" component={NotificationTemplates} /> <ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/credential_types" component={CredentialTypes} />
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/management_jobs" component={ManagementJobs} /> <ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/notification_templates" component={NotificationTemplates} />
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/instance_groups" component={InstanceGroups} /> <ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/management_jobs" component={ManagementJobs} />
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/applications" component={Applications} /> <ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/instance_groups" component={InstanceGroups} />
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/auth_settings" component={AuthSettings} /> <ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/applications" component={Applications} />
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/jobs_settings" component={JobsSettings} /> <ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/auth_settings" component={AuthSettings} />
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/system_settings" component={SystemSettings} /> <ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/jobs_settings" component={JobsSettings} />
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/ui_settings" component={UISettings} /> <ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/system_settings" component={SystemSettings} />
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/license" component={License} /> <ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/ui_settings" component={UISettings} />
</Page> <ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/license" component={License} />
</Fragment>
</Switch> </Page>
</Fragment>
</Switch>
</ConfigContext.Provider>
</Fragment> </Fragment>
</I18nProvider> </I18nProvider>
); );

View File

@@ -1,46 +1,21 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { I18n } from '@lingui/react'; import { I18n } from '@lingui/react';
import { Trans, t } from '@lingui/macro'; import { Trans, t } from '@lingui/macro';
import { import {
AboutModal, AboutModal,
TextContent, TextContent,
TextList, TextList,
TextListItem } from '@patternfly/react-core'; TextListItem
} from '@patternfly/react-core';
import heroImg from '@patternfly/patternfly-next/assets/images/pfbg_992.jpg'; import heroImg from '@patternfly/patternfly-next/assets/images/pfbg_992.jpg';
import brandImg from '../../images/tower-logo-white.svg'; import brandImg from '../../images/tower-logo-white.svg';
import logoImg from '../../images/tower-logo-login.svg'; import logoImg from '../../images/tower-logo-login.svg';
import api from '../api'; import { ConfigContext } from '../context';
import { API_CONFIG } from '../endpoints';
class About extends React.Component { class About extends React.Component {
unmounting = false;
constructor (props) {
super(props);
this.state = {
config: {},
error: false
};
}
async componentDidMount () {
try {
const { data } = await api.get(API_CONFIG);
this.safeSetState({ config: data });
} catch (error) {
this.safeSetState({ error });
}
}
componentWillUnmount () {
this.unmounting = true;
}
safeSetState = obj => !this.unmounting && this.setState(obj);
createSpeechBubble = (version) => { createSpeechBubble = (version) => {
let text = `Tower ${version}`; let text = `Tower ${version}`;
let top = ''; let top = '';
@@ -65,26 +40,25 @@ class About extends React.Component {
render () { render () {
const { isOpen } = this.props; const { isOpen } = this.props;
const { config = {}, error } = this.state;
const { ansible_version = 'loading', version = 'loading' } = config;
return ( return (
<I18n> <I18n>
{({ i18n }) => ( {({ i18n }) => (
<AboutModal <ConfigContext.Consumer>
isOpen={isOpen} {({ ansible_version, version }) => (
onClose={this.handleModalToggle} <AboutModal
productName="Ansible Tower" isOpen={isOpen}
trademark={i18n._(t`Copyright 2018 Red Hat, Inc.`)} onClose={this.handleModalToggle}
brandImageSrc={brandImg} productName="Ansible Tower"
brandImageAlt={i18n._(t`Brand Image`)} trademark={i18n._(t`Copyright 2018 Red Hat, Inc.`)}
logoImageSrc={logoImg} brandImageSrc={brandImg}
logoImageAlt={i18n._(t`AboutModal Logo`)} brandImageAlt={i18n._(t`Brand Image`)}
heroImageSrc={heroImg} logoImageSrc={logoImg}
> logoImageAlt={i18n._(t`AboutModal Logo`)}
<pre> heroImageSrc={heroImg}
{ this.createSpeechBubble(version) } >
{` <pre>
{this.createSpeechBubble(version)}
{`
\\ \\
\\ ^__^ \\ ^__^
(oo)\\_______ (oo)\\_______
@@ -92,22 +66,28 @@ class About extends React.Component {
||----w | ||----w |
|| || || ||
`} `}
</pre> </pre>
<TextContent> <TextContent>
<TextList component="dl"> <TextList component="dl">
<TextListItem component="dt"> <TextListItem component="dt">
<Trans>Ansible Version</Trans> <Trans>Ansible Version</Trans>
</TextListItem> </TextListItem>
<TextListItem component="dd">{ ansible_version }</TextListItem> <TextListItem component="dd">{ansible_version}</TextListItem>
</TextList> </TextList>
</TextContent> </TextContent>
{ error ? <div>error</div> : ''} </AboutModal>
</AboutModal> )}
</ConfigContext.Consumer>
)} )}
</I18n> </I18n>
); );
} }
} }
About.contextTypes = {
ansible_version: PropTypes.string,
version: PropTypes.string,
};
export default About; export default About;

View File

@@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { import {
FormGroup, FormGroup,
Select, Select,
@@ -11,24 +12,37 @@ class AnsibleSelect extends React.Component {
this.onSelectChange = this.onSelectChange.bind(this); this.onSelectChange = this.onSelectChange.bind(this);
} }
state = {
count: 1,
}
static getDerivedStateFromProps(nexProps, _) {
if (nexProps.data) {
return {
count: nexProps.data.length,
}
}
return null;
}
onSelectChange(val, _) { onSelectChange(val, _) {
this.props.selectChange(val); this.props.selectChange(val);
} }
render() { render() {
const { hidden } = this.props; const { count } = this.state;
if (hidden) { if (count > 1) {
return null;
} else {
return ( return (
<FormGroup label={this.props.labelName} fieldId="ansible-select"> <FormGroup label={this.props.labelName} fieldId="ansible-select">
<Select value={this.props.selected} onChange={this.onSelectChange} aria-label="Select Input"> <Select value={this.props.selected} onChange={this.onSelectChange} aria-label="Select Input">
{this.props.data.map((env, index) => ( {this.props.data.map((datum, index) => (
<SelectOption isDisabled={env.disabled} key={index} value={env} label={env} /> <SelectOption isDisabled={datum.disabled} key={index} value={datum} label={datum} />
))} ))}
</Select> </Select>
</FormGroup> </FormGroup>
); )
}
else {
return null;
} }
} }
} }

3
src/context.jsx Normal file
View File

@@ -0,0 +1,3 @@
import React from "react";
export const ConfigContext = React.createContext({});

View File

@@ -1,4 +1,5 @@
import React, { Fragment } from 'react'; import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { withRouter } from 'react-router-dom'; import { withRouter } from 'react-router-dom';
import { Trans } from '@lingui/macro'; import { Trans } from '@lingui/macro';
import { import {
@@ -17,7 +18,8 @@ import {
CardBody, CardBody,
} from '@patternfly/react-core'; } from '@patternfly/react-core';
import { API_ORGANIZATIONS, API_CONFIG } from '../../../endpoints'; import { ConfigContext } from '../../../context';
import { API_ORGANIZATIONS } from '../../../endpoints';
import api from '../../../api'; import api from '../../../api';
import AnsibleSelect from '../../../components/AnsibleSelect' import AnsibleSelect from '../../../components/AnsibleSelect'
const { light } = PageSectionVariants; const { light } = PageSectionVariants;
@@ -38,8 +40,6 @@ class OrganizationAdd extends React.Component {
description: '', description: '',
instanceGroups: '', instanceGroups: '',
custom_virtualenv: '', custom_virtualenv: '',
custom_virtualenvs: [],
hideAnsibleSelect: true,
error:'', error:'',
}; };
@@ -69,22 +69,10 @@ class OrganizationAdd extends React.Component {
this.props.history.push('/organizations'); this.props.history.push('/organizations');
} }
async componentDidMount() {
try {
const { data } = await api.get(API_CONFIG);
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() { render() {
const { name } = this.state; const { name } = this.state;
const enabled = name.length > 0; // TODO: add better form validation const enabled = name.length > 0; // TODO: add better form validation
return ( return (
<Fragment> <Fragment>
<PageSection variant={light} className="pf-m-condensed"> <PageSection variant={light} className="pf-m-condensed">
@@ -128,13 +116,16 @@ class OrganizationAdd extends React.Component {
onChange={this.handleChange} onChange={this.handleChange}
/> />
</FormGroup> </FormGroup>
<AnsibleSelect <ConfigContext.Consumer>
labelName="Ansible Environment" {({ custom_virtualenvs }) =>
selected={this.state.custom_virtualenv} <AnsibleSelect
selectChange={this.onSelectChange} labelName="Ansible Environment"
data={this.state.custom_virtualenvs} selected={this.state.custom_virtualenv}
hidden={this.state.hideAnsibleSelect} selectChange={this.onSelectChange}
/> data={custom_virtualenvs}
/>
}
</ConfigContext.Consumer>
</Gallery> </Gallery>
<ActionGroup className="at-align-right"> <ActionGroup className="at-align-right">
<Toolbar> <Toolbar>
@@ -155,4 +146,8 @@ class OrganizationAdd extends React.Component {
} }
} }
OrganizationAdd.contextTypes = {
custom_virtualenvs: PropTypes.array,
};
export default withRouter(OrganizationAdd); export default withRouter(OrganizationAdd);