From b8fc402d552367bee08d6447741307163ae2d285 Mon Sep 17 00:00:00 2001 From: kialam Date: Mon, 17 Dec 2018 11:44:11 -0500 Subject: [PATCH] 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. --- __mocks__/axios.js | 21 +++++++++-- __tests__/components/AnsibleSelect.test.jsx | 25 +++++++++---- .../views/Organization.add.test.jsx | 27 ++++++++++++-- package.json | 1 + src/App.jsx | 28 ++++++++++++--- .../AnsibleSelect/AnsibleSelect.jsx | 15 ++++---- src/context.jsx | 3 ++ .../Organizations/views/Organization.add.jsx | 36 +++++++++---------- 8 files changed, 115 insertions(+), 41 deletions(-) create mode 100644 src/context.jsx diff --git a/__mocks__/axios.js b/__mocks__/axios.js index 23f96b475f..aad497303f 100644 --- a/__mocks__/axios.js +++ b/__mocks__/axios.js @@ -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 = () => { diff --git a/__tests__/components/AnsibleSelect.test.jsx b/__tests__/components/AnsibleSelect.test.jsx index 47a068ef4f..d2eacf2129 100644 --- a/__tests__/components/AnsibleSelect.test.jsx +++ b/__tests__/components/AnsibleSelect.test.jsx @@ -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('', () => { - test('initially renders succesfully', async() => { - const wrapper = mount( {}} />); - wrapper.setState({ isHidden: false }); + test('initially renders succesfully', async () => { + mount( + { }} + labelName={label} + data={mockData} + /> + ); }); test('calls "onSelectChange" on dropdown select change', () => { const spy = jest.spyOn(AnsibleSelect.prototype, 'onSelectChange'); - const wrapper = mount( {}} />); - wrapper.setState({ isHidden: false }); + const wrapper = mount( + { }} + labelName={label} + data={mockData} + /> + ); expect(spy).not.toHaveBeenCalled(); wrapper.find('select').simulate('change'); expect(spy).toHaveBeenCalled(); diff --git a/__tests__/pages/Organizations/views/Organization.add.test.jsx b/__tests__/pages/Organizations/views/Organization.add.test.jsx index 434621985d..38a16e732e 100644 --- a/__tests__/pages/Organizations/views/Organization.add.test.jsx +++ b/__tests__/pages/Organizations/views/Organization.add.test.jsx @@ -1,6 +1,27 @@ import React from 'react'; import { mount } from 'enzyme'; -import OrganizationAdd from '../../../../src/pages/Organizations/views/Organization.add'; + +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('', () => { test('initially renders succesfully', () => { @@ -20,8 +41,8 @@ describe('', () => { /> ); expect(spy).not.toHaveBeenCalled(); - wrapper.find('input#add-org-form-name').simulate('change', {target: {value: 'foo'}}); - wrapper.find('input#add-org-form-description').simulate('change', {target: {value: 'bar'}}); + wrapper.find('input#add-org-form-name').simulate('change', { target: { value: 'foo' } }); + wrapper.find('input#add-org-form-description').simulate('change', { target: { value: 'bar' } }); expect(spy).toHaveBeenCalledTimes(2); }); test('calls "onSubmit" when Save button is clicked', () => { diff --git a/package.json b/package.json index 19dad2c599..594f912fc1 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,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" diff --git a/src/App.jsx b/src/App.jsx index f400ce3e53..065f6f6011 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,4 +1,6 @@ import React, { Fragment } from 'react'; +import { ConfigContext } from './context'; + import { Redirect, Switch, @@ -22,7 +24,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'; @@ -90,15 +92,19 @@ const SideNavItems = ({ items, history }) => { }; 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 })); }; @@ -110,9 +116,19 @@ class App extends React.Component { 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; @@ -302,7 +318,9 @@ class App extends React.Component { !api.isAuthenticated()} redirectPath="/login" path="/projects" component={Projects} /> !api.isAuthenticated()} redirectPath="/login" path="/inventories" component={Inventories} /> !api.isAuthenticated()} redirectPath="/login" path="/inventory_scripts" component={InventoryScripts} /> - !api.isAuthenticated()} redirectPath="/login" path="/organizations" component={Organizations} /> + + !api.isAuthenticated()} redirectPath="/login" path="/organizations" component={Organizations} /> + !api.isAuthenticated()} redirectPath="/login" path="/users" component={Users} /> !api.isAuthenticated()} redirectPath="/login" path="/teams" component={Teams} /> !api.isAuthenticated()} redirectPath="/login" path="/credential_types" component={CredentialTypes} /> diff --git a/src/components/AnsibleSelect/AnsibleSelect.jsx b/src/components/AnsibleSelect/AnsibleSelect.jsx index d411c1d17a..001e83853b 100644 --- a/src/components/AnsibleSelect/AnsibleSelect.jsx +++ b/src/components/AnsibleSelect/AnsibleSelect.jsx @@ -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 ( - ); + ) + } + else { + return null; } } } diff --git a/src/context.jsx b/src/context.jsx new file mode 100644 index 0000000000..446f82bd63 --- /dev/null +++ b/src/context.jsx @@ -0,0 +1,3 @@ +import React from "react"; + +export const ConfigContext = React.createContext({}); diff --git a/src/pages/Organizations/views/Organization.add.jsx b/src/pages/Organizations/views/Organization.add.jsx index a95f69831b..82f5b17150 100644 --- a/src/pages/Organizations/views/Organization.add.jsx +++ b/src/pages/Organizations/views/Organization.add.jsx @@ -1,4 +1,5 @@ import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; import { PageSection, PageSectionVariants, @@ -15,7 +16,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; @@ -35,8 +37,6 @@ class OrganizationAdd extends React.Component { description: '', instanceGroups: '', custom_virtualenv: '', - custom_virtualenvs: [], - hideAnsibleSelect: true, }; onSelectChange(value, _) { @@ -61,17 +61,10 @@ class OrganizationAdd extends React.Component { this.resetForm(); } - async componentDidMount() { - 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 }); - } - } render() { const { name } = this.state; const enabled = name.length > 0; // TODO: add better form validation + return ( @@ -113,13 +106,16 @@ class OrganizationAdd extends React.Component { onChange={this.handleChange} /> -