mirror of
https://github.com/ansible/awx.git
synced 2026-01-18 21:21:21 -03:30
Implement React Context API
- Move API GET request to /v2/config out to the top level of our App. - Store /v2/config response data in sessionStorage. - Use Context API to pass down relevant data to Organizations component. - Wrap our AnsibleSelect component as a context consumer and pass in the list of Ansible Environments of the logged in user. - Clear sessionStorage object when user logs out. - Update unit tests.
This commit is contained in:
parent
f678e158f8
commit
9bc87b3e80
@ -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');
|
||||
|
||||
axios.create = jest.fn(() => axios);
|
||||
@ -9,7 +17,16 @@ axios.create.mockReturnValue({
|
||||
get: axios.get,
|
||||
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.customClearMocks = () => {
|
||||
|
||||
@ -2,16 +2,29 @@ import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import AnsibleSelect from '../../src/components/AnsibleSelect';
|
||||
|
||||
const mockData = ['foo', 'bar'];
|
||||
const label = "test select"
|
||||
const mockData = ["/venv/baz/", "/venv/ansible/"];
|
||||
describe('<AnsibleSelect />', () => {
|
||||
test('initially renders succesfully', async() => {
|
||||
const wrapper = mount(<AnsibleSelect selected="foo" data={mockData} selectChange={() => {}} />);
|
||||
wrapper.setState({ isHidden: false });
|
||||
test('initially renders succesfully', async () => {
|
||||
mount(
|
||||
<AnsibleSelect
|
||||
selected="foo"
|
||||
selectChange={() => { }}
|
||||
labelName={label}
|
||||
data={mockData}
|
||||
/>
|
||||
);
|
||||
});
|
||||
test('calls "onSelectChange" on dropdown select change', () => {
|
||||
const spy = jest.spyOn(AnsibleSelect.prototype, 'onSelectChange');
|
||||
const wrapper = mount(<AnsibleSelect selected="foo" data={mockData} selectChange={() => {}} />);
|
||||
wrapper.setState({ isHidden: false });
|
||||
const wrapper = mount(
|
||||
<AnsibleSelect
|
||||
selected="foo"
|
||||
selectChange={() => { }}
|
||||
labelName={label}
|
||||
data={mockData}
|
||||
/>
|
||||
);
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
wrapper.find('select').simulate('change');
|
||||
expect(spy).toHaveBeenCalled();
|
||||
|
||||
@ -1,8 +1,29 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import OrganizationAdd from '../../../../src/pages/Organizations/views/Organization.add';
|
||||
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 />', () => {
|
||||
test('initially renders succesfully', () => {
|
||||
mount(
|
||||
|
||||
@ -53,6 +53,7 @@
|
||||
"@patternfly/react-styles": "^2.3.0",
|
||||
"@patternfly/react-tokens": "^1.9.0",
|
||||
"axios": "^0.18.0",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.4.1",
|
||||
"react-dom": "^16.4.1",
|
||||
"react-router-dom": "^4.3.1"
|
||||
|
||||
228
src/App.jsx
228
src/App.jsx
@ -1,6 +1,11 @@
|
||||
import React, { Fragment } from 'react';
|
||||
<<<<<<< HEAD
|
||||
import { I18nProvider, I18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
=======
|
||||
import { ConfigContext } from './context';
|
||||
|
||||
>>>>>>> Implement React Context API
|
||||
import {
|
||||
Redirect,
|
||||
Switch,
|
||||
@ -22,7 +27,7 @@ import {
|
||||
import { global_breakpoint_md as breakpointMd } from '@patternfly/react-tokens';
|
||||
|
||||
import api from './api';
|
||||
import { API_LOGOUT } from './endpoints';
|
||||
import { API_LOGOUT, API_CONFIG } from './endpoints';
|
||||
|
||||
import HelpDropdown from './components/HelpDropdown';
|
||||
import LogoutButton from './components/LogoutButton';
|
||||
@ -68,24 +73,39 @@ const languageWithoutRegionCode = language.toLowerCase().split(/[_-]+/)[0];
|
||||
|
||||
|
||||
class App extends React.Component {
|
||||
constructor (props) {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const isNavOpen = typeof window !== 'undefined' && window.innerWidth >= parseInt(breakpointMd.value, 10);
|
||||
this.state = {
|
||||
isNavOpen
|
||||
isNavOpen,
|
||||
};
|
||||
}
|
||||
|
||||
getSessionObject(key) {
|
||||
return JSON.parse(sessionStorage.getItem(key) || '{}');
|
||||
}
|
||||
|
||||
onNavToggle = () => {
|
||||
this.setState(({ isNavOpen }) => ({ isNavOpen: !isNavOpen }));
|
||||
};
|
||||
|
||||
onDevLogout = async () => {
|
||||
await api.get(API_LOGOUT);
|
||||
this.setState({ activeGroup: 'views_group', activeItem: 'views_group_dashboard' });
|
||||
if (sessionStorage.config) {
|
||||
sessionStorage.clear();
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
async componentDidMount() {
|
||||
// Grab our config data from the API and store in sessionStorage
|
||||
if (!sessionStorage.config) {
|
||||
const { data } = await api.get(API_CONFIG);
|
||||
sessionStorage.setItem('config', JSON.stringify(data));
|
||||
}
|
||||
}
|
||||
render() {
|
||||
const { isNavOpen } = this.state;
|
||||
const { logo, loginInfo, history } = this.props;
|
||||
|
||||
@ -119,17 +139,12 @@ class App extends React.Component {
|
||||
}}
|
||||
/>
|
||||
<Switch>
|
||||
<ConditionalRedirect
|
||||
shouldRedirect={() => api.isAuthenticated()}
|
||||
redirectPath="/"
|
||||
path="/login"
|
||||
component={() => <Login logo={logo} loginInfo={loginInfo} />}
|
||||
/>
|
||||
<ConditionalRedirect shouldRedirect={() => api.isAuthenticated()} redirectPath="/" path="/login" component={() => <Login logo={logo} loginInfo={loginInfo} />} />
|
||||
<Fragment>
|
||||
<Page
|
||||
header={(
|
||||
<PageHeader
|
||||
logo={<TowerLogo />}
|
||||
logo={<TowerLogo onClick={this.onLogoClick} />}
|
||||
toolbar={PageToolbar}
|
||||
showNavToggle
|
||||
onNavToggle={this.onNavToggle}
|
||||
@ -139,66 +154,133 @@ class App extends React.Component {
|
||||
<PageSidebar
|
||||
isNavOpen={isNavOpen}
|
||||
nav={(
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<Nav aria-label={i18n._(t`Primary Navigation`)}>
|
||||
<NavList>
|
||||
<NavExpandableGroup
|
||||
groupId="views_group"
|
||||
title={i18n._("Views")}
|
||||
routes={[
|
||||
{ path: '/home', title: i18n._('Dashboard') },
|
||||
{ path: '/jobs', title: i18n._('Jobs') },
|
||||
{ path: '/schedules', title: i18n._('Schedules') },
|
||||
{ path: '/portal', title: i18n._('Portal Mode') },
|
||||
]}
|
||||
/>
|
||||
<NavExpandableGroup
|
||||
groupId="resources_group"
|
||||
title={i18n._("Resources")}
|
||||
routes={[
|
||||
{ path: '/templates', title: i18n._('Templates') },
|
||||
{ path: '/credentials', title: i18n._('Credentials') },
|
||||
{ path: '/projects', title: i18n._('Projects') },
|
||||
{ path: '/inventories', title: i18n._('Inventories') },
|
||||
{ path: '/inventory_scripts', title: i18n._('Inventory Scripts') }
|
||||
]}
|
||||
/>
|
||||
<NavExpandableGroup
|
||||
groupId="access_group"
|
||||
title={i18n._("Access")}
|
||||
routes={[
|
||||
{ path: '/organizations', title: i18n._('Organizations') },
|
||||
{ path: '/users', title: i18n._('Users') },
|
||||
{ path: '/teams', title: i18n._('Teams') }
|
||||
]}
|
||||
/>
|
||||
<NavExpandableGroup
|
||||
groupId="administration_group"
|
||||
title={i18n._("Administration")}
|
||||
routes={[
|
||||
{ path: '/credential_types', title: i18n._('Credential Types') },
|
||||
{ path: '/notification_templates', title: i18n._('Notifications') },
|
||||
{ path: '/management_jobs', title: i18n._('Management Jobs') },
|
||||
{ path: '/instance_groups', title: i18n._('Instance Groups') },
|
||||
{ path: '/applications', title: i18n._('Integrations') }
|
||||
]}
|
||||
/>
|
||||
<NavExpandableGroup
|
||||
groupId="settings_group"
|
||||
title={i18n._("Settings")}
|
||||
routes={[
|
||||
{ path: '/auth_settings', title: i18n._('Authentication') },
|
||||
{ path: '/jobs_settings', title: i18n._('Jobs') },
|
||||
{ path: '/system_settings', title: i18n._('System') },
|
||||
{ path: '/ui_settings', title: i18n._('User Interface') },
|
||||
{ path: '/license', title: i18n._('License') }
|
||||
]}
|
||||
/>
|
||||
</NavList>
|
||||
</Nav>
|
||||
)}
|
||||
</I18n>
|
||||
<Nav aria-label="Primary Navigation">
|
||||
<NavList>
|
||||
<SideNavItems
|
||||
history={history}
|
||||
items={[
|
||||
{
|
||||
groupName: 'views',
|
||||
title: 'Views',
|
||||
routes: [
|
||||
{
|
||||
path: 'home',
|
||||
title: 'Dashboard'
|
||||
},
|
||||
{
|
||||
path: 'jobs',
|
||||
title: 'Jobs'
|
||||
},
|
||||
{
|
||||
path: 'schedules',
|
||||
title: 'Schedules'
|
||||
},
|
||||
{
|
||||
path: 'portal',
|
||||
title: 'Portal Mode'
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
groupName: 'resources',
|
||||
title: 'Resources',
|
||||
routes: [
|
||||
{
|
||||
path: 'templates',
|
||||
title: 'Templates'
|
||||
},
|
||||
{
|
||||
path: 'credentials',
|
||||
title: 'Credentials'
|
||||
},
|
||||
{
|
||||
path: 'projects',
|
||||
title: 'Projects'
|
||||
},
|
||||
{
|
||||
path: 'inventories',
|
||||
title: 'Inventories'
|
||||
},
|
||||
{
|
||||
path: 'inventory_scripts',
|
||||
title: 'Inventory Scripts'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
groupName: 'access',
|
||||
title: 'Access',
|
||||
routes: [
|
||||
{
|
||||
path: 'organizations',
|
||||
title: 'Organizations'
|
||||
},
|
||||
{
|
||||
path: 'users',
|
||||
title: 'Users'
|
||||
},
|
||||
{
|
||||
path: 'teams',
|
||||
title: 'Teams'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
groupName: 'administration',
|
||||
title: 'Administration',
|
||||
routes: [
|
||||
{
|
||||
path: 'credential_types',
|
||||
title: 'Credential Types',
|
||||
},
|
||||
{
|
||||
path: 'notification_templates',
|
||||
title: 'Notifications'
|
||||
},
|
||||
{
|
||||
path: 'management_jobs',
|
||||
title: 'Management Jobs'
|
||||
},
|
||||
{
|
||||
path: 'instance_groups',
|
||||
title: 'Instance Groups'
|
||||
},
|
||||
{
|
||||
path: 'applications',
|
||||
title: 'Integrations'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
groupName: 'settings',
|
||||
title: 'Settings',
|
||||
routes: [
|
||||
{
|
||||
path: 'auth_settings',
|
||||
title: 'Authentication',
|
||||
},
|
||||
{
|
||||
path: 'jobs_settings',
|
||||
title: 'Jobs'
|
||||
},
|
||||
{
|
||||
path: 'system_settings',
|
||||
title: 'System'
|
||||
},
|
||||
{
|
||||
path: 'ui_settings',
|
||||
title: 'User Interface'
|
||||
},
|
||||
{
|
||||
path: 'license',
|
||||
title: 'License'
|
||||
}
|
||||
]
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</NavList>
|
||||
</Nav>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
@ -214,7 +296,9 @@ class App extends React.Component {
|
||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/projects" component={Projects} />
|
||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/inventories" component={Inventories} />
|
||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/inventory_scripts" component={InventoryScripts} />
|
||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/organizations" component={Organizations} />
|
||||
<ConfigContext.Provider value={this.getSessionObject('config')}>
|
||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/organizations" component={Organizations} />
|
||||
</ConfigContext.Provider>
|
||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/users" component={Users} />
|
||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/teams" component={Teams} />
|
||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/credential_types" component={CredentialTypes} />
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
FormGroup,
|
||||
Select,
|
||||
@ -16,19 +17,19 @@ class AnsibleSelect extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { hidden } = this.props;
|
||||
if (hidden) {
|
||||
return null;
|
||||
} else {
|
||||
if (this.props.data.length > 1) {
|
||||
return (
|
||||
<FormGroup label={this.props.labelName} fieldId="ansible-select">
|
||||
<Select value={this.props.selected} onChange={this.onSelectChange} aria-label="Select Input">
|
||||
{this.props.data.map((env, index) => (
|
||||
<SelectOption isDisabled={env.disabled} key={index} value={env} label={env} />
|
||||
{this.props.data.map((datum, index) => (
|
||||
<SelectOption isDisabled={datum.disabled} key={index} value={datum} label={datum} />
|
||||
))}
|
||||
</Select>
|
||||
</FormGroup>
|
||||
);
|
||||
)
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
3
src/context.jsx
Normal file
3
src/context.jsx
Normal file
@ -0,0 +1,3 @@
|
||||
import React from "react";
|
||||
|
||||
export const ConfigContext = React.createContext({});
|
||||
@ -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,7 +18,8 @@ import {
|
||||
CardBody,
|
||||
} 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 AnsibleSelect from '../../../components/AnsibleSelect'
|
||||
const { light } = PageSectionVariants;
|
||||
@ -38,8 +40,6 @@ class OrganizationAdd extends React.Component {
|
||||
description: '',
|
||||
instanceGroups: '',
|
||||
custom_virtualenv: '',
|
||||
custom_virtualenvs: [],
|
||||
hideAnsibleSelect: true,
|
||||
error:'',
|
||||
};
|
||||
|
||||
@ -69,22 +69,10 @@ class OrganizationAdd extends React.Component {
|
||||
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() {
|
||||
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 +116,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 +146,8 @@ class OrganizationAdd extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
OrganizationAdd.contextTypes = {
|
||||
custom_virtualenvs: PropTypes.array,
|
||||
};
|
||||
|
||||
export default withRouter(OrganizationAdd);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user