Merge pull request #7 from ansible/testing

add unit and functional testing to the app
This commit is contained in:
Jake McDermott 2018-10-24 22:03:43 -04:00 committed by GitHub
commit ca0127d889
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 4710 additions and 950 deletions

View File

@ -9,7 +9,7 @@
}
},
"extends": [
"airbnb",
"airbnb"
],
"settings": {
"react": {
@ -18,22 +18,24 @@
},
"env": {
"browser": true,
"node": true
"node": true,
"jest": true
},
"globals": {
"window": true,
"window": true
},
"rules": {
"camelcase": "off",
"arrow-parens": "off",
"comma-dangle": "off",
"comma-dangle": "off",
"import/no-extraneous-dependencies": ["error", { "devDependencies": true }],
"indent": ["error", 2, {
"SwitchCase": 1
}],
"max-len": ['error', {
"max-len": ["error", {
"code": 100,
"ignoreStrings": true,
"ignoreTemplateLiterals": true,
"ignoreTemplateLiterals": true
}],
"no-continue": "off",
"no-debugger": "off",
@ -42,7 +44,7 @@
"no-plusplus": "off",
"no-underscore-dangle": "off",
"no-use-before-define": "off",
"no-multiple-empty-lines": ["error", { max: 1 }],
"no-multiple-empty-lines": ["error", { "max": 1 }],
"object-curly-newline": "off",
"space-before-function-paren": ["error", "always"],
"no-trailing-spaces": ["error"],

1
.gitignore vendored
View File

@ -1 +1,2 @@
node_modules/
coverage/

21
__mocks__/axios.js Normal file
View File

@ -0,0 +1,21 @@
const axios = require('axios');
jest.genMockFromModule('axios');
axios.create = jest.fn(() => axios);
axios.get = jest.fn(() => axios);
axios.post = jest.fn(() => axios);
axios.create.mockReturnValue({
get: axios.get,
post: axios.post
});
axios.get.mockResolvedValue('get results');
axios.post.mockResolvedValue('post results');
axios.customClearMocks = () => {
axios.create.mockClear();
axios.get.mockClear();
axios.post.mockClear();
};
module.exports = axios;

View File

@ -0,0 +1 @@
module.exports = 'svg-stub';

View File

@ -0,0 +1,37 @@
import React from 'react';
import { shallow, mount } from 'enzyme';
import App from '../../src/App';
import api from '../../src/api';
import Dashboard from '../../src/pages/Dashboard';
import Login from '../../src/pages/Login';
describe('<App />', () => {
test('renders without crashing', () => {
const appWrapper = shallow(<App />);
expect(appWrapper.length).toBe(1);
});
test('renders login page when not authenticated', () => {
api.isAuthenticated = jest.fn();
api.isAuthenticated.mockReturnValue(false);
const appWrapper = mount(<App />);
const login = appWrapper.find(Login);
expect(login.length).toBe(1);
const dashboard = appWrapper.find(Dashboard);
expect(dashboard.length).toBe(0);
});
test('renders dashboard when authenticated', () => {
api.isAuthenticated = jest.fn();
api.isAuthenticated.mockReturnValue(true);
const appWrapper = mount(<App />);
const dashboard = appWrapper.find(Dashboard);
expect(dashboard.length).toBe(1);
const login = appWrapper.find(Login);
expect(login.length).toBe(0);
});
});

View File

@ -0,0 +1,27 @@
import React, { Component } from 'react';
import {
Route,
Redirect
} from 'react-router-dom';
import { shallow } from 'enzyme';
import ConditionalRedirect from '../../src/components/ConditionalRedirect';
describe('<ConditionalRedirect />', () => {
test('renders Redirect when shouldRedirect is passed truthy func', () => {
const truthyFunc = () => true;
const shouldHaveRedirectChild = shallow(<ConditionalRedirect shouldRedirect={() => truthyFunc()} />);
const redirectChild = shouldHaveRedirectChild.find(Redirect);
expect(redirectChild.length).toBe(1);
const routeChild = shouldHaveRedirectChild.find(Route);
expect(routeChild.length).toBe(0);
});
test('renders Route when shouldRedirect is passed falsy func', () => {
const falsyFunc = () => false;
const shouldHaveRouteChild = shallow(<ConditionalRedirect shouldRedirect={() => falsyFunc()} />);
const routeChild = shouldHaveRouteChild.find(Route);
expect(routeChild.length).toBe(1);
const redirectChild = shouldHaveRouteChild.find(Redirect);
expect(redirectChild.length).toBe(0);
});
});

View File

@ -0,0 +1,37 @@
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { shallow, mount } from 'enzyme';
import LoginPage from '../../src/pages/Login';
import api from '../../src/api';
import Dashboard from '../../src/pages/Dashboard';
describe('<LoginPage />', () => {
let loginWrapper, usernameInput, passwordInput, errorTextArea, submitButton;
beforeEach(() => {
loginWrapper = mount(<MemoryRouter><LoginPage /></MemoryRouter>);
usernameInput = loginWrapper.find('.pf-c-form__group#username TextInput');
passwordInput = loginWrapper.find('.pf-c-form__group#password TextInput');
errorTextArea = loginWrapper.find('.pf-c-form__helper-text.pf-m-error');
submitButton = loginWrapper.find('Button[type="submit"]');
});
test('initially renders without crashing', () => {
expect(loginWrapper.length).toBe(1);
expect(usernameInput.length).toBe(1);
expect(passwordInput.length).toBe(1);
expect(errorTextArea.length).toBe(1);
expect(submitButton.length).toBe(1);
});
// initially renders empty username and password fields, sets empty error message and makes submit button not disabled
// typing into username and password fields (if the component is not unmounting) will clear out any error message
// when the submit Button is clicked, as long as it is not disabled state.loading is set to true
// api.login is called with param 1 username and param 2 password
// if api.login returns an error, the state.error should be set to LOGIN_ERROR_MESSAGE, if the error object returned has response.status set to 401.
// regardless of error or not, after api.login returns, state.loading should be set to false
// if api.isAuthenticated mock returns true, Redirect to / should be rendered
});

117
__tests__/tests/api.test.js Normal file
View File

@ -0,0 +1,117 @@
import mockAxios from 'axios';
import APIClient from '../../src/api';
const API_ROOT = '/api/';
const API_LOGIN = `${API_ROOT}login/`;
const API_LOGOUT = `${API_ROOT}logout/`;
const API_V2 = `${API_ROOT}v2/`;
const API_CONFIG = `${API_V2}config/`;
const API_PROJECTS = `${API_V2}projects/`;
const API_ORGANIZATIONS = `${API_V2}organizations/`;
const CSRF_COOKIE_NAME = 'csrftoken';
const CSRF_HEADER_NAME = 'X-CSRFToken';
const LOGIN_CONTENT_TYPE = 'application/x-www-form-urlencoded';
describe('APIClient (api.js)', () => {
afterEach(() => {
mockAxios.customClearMocks();
});
test('constructor calls axios create', () => {
const csrfObj = {
xsrfCookieName: CSRF_COOKIE_NAME,
xsrfHeaderName: CSRF_HEADER_NAME
};
expect(mockAxios.create).toHaveBeenCalledTimes(1);
expect(mockAxios.create).toHaveBeenCalledWith(csrfObj);
expect(APIClient.http).toHaveProperty('get');
});
test('isAuthenticated checks authentication and sets cookie from document', () => {
APIClient.getCookie = jest.fn();
const invalidCookie = 'invalid';
const validLoggedOutCookie = 'current_user=%7B%22id%22%3A1%2C%22type%22%3A%22user%22%2C%22url%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2F%22%2C%22related%22%3A%7B%22admin_of_organizations%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fadmin_of_organizations%2F%22%2C%22authorized_tokens%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fauthorized_tokens%2F%22%2C%22roles%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Froles%2F%22%2C%22organizations%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Forganizations%2F%22%2C%22access_list%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Faccess_list%2F%22%2C%22teams%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fteams%2F%22%2C%22tokens%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Ftokens%2F%22%2C%22personal_tokens%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fpersonal_tokens%2F%22%2C%22credentials%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fcredentials%2F%22%2C%22activity_stream%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Factivity_stream%2F%22%2C%22projects%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fprojects%2F%22%7D%2C%22summary_fields%22%3A%7B%7D%2C%22created%22%3A%222018-10-19T16%3A30%3A59.141963Z%22%2C%22username%22%3A%22admin%22%2C%22first_name%22%3A%22%22%2C%22last_name%22%3A%22%22%2C%22email%22%3A%22%22%2C%22is_superuser%22%3Atrue%2C%22is_system_auditor%22%3Afalse%2C%22ldap_dn%22%3A%22%22%2C%22external_account%22%3Anull%2C%22auth%22%3A%5B%5D%7D; userLoggedIn=false; csrftoken=lhOHpLQUFHlIVqx8CCZmEpdEZAz79GIRBIT3asBzTbPE7HS7wizt7WBsgJClz8Ge';
const validLoggedInCookie = 'current_user=%7B%22id%22%3A1%2C%22type%22%3A%22user%22%2C%22url%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2F%22%2C%22related%22%3A%7B%22admin_of_organizations%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fadmin_of_organizations%2F%22%2C%22authorized_tokens%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fauthorized_tokens%2F%22%2C%22roles%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Froles%2F%22%2C%22organizations%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Forganizations%2F%22%2C%22access_list%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Faccess_list%2F%22%2C%22teams%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fteams%2F%22%2C%22tokens%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Ftokens%2F%22%2C%22personal_tokens%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fpersonal_tokens%2F%22%2C%22credentials%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fcredentials%2F%22%2C%22activity_stream%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Factivity_stream%2F%22%2C%22projects%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fprojects%2F%22%7D%2C%22summary_fields%22%3A%7B%7D%2C%22created%22%3A%222018-10-19T16%3A30%3A59.141963Z%22%2C%22username%22%3A%22admin%22%2C%22first_name%22%3A%22%22%2C%22last_name%22%3A%22%22%2C%22email%22%3A%22%22%2C%22is_superuser%22%3Atrue%2C%22is_system_auditor%22%3Afalse%2C%22ldap_dn%22%3A%22%22%2C%22external_account%22%3Anull%2C%22auth%22%3A%5B%5D%7D; userLoggedIn=true; csrftoken=lhOHpLQUFHlIVqx8CCZmEpdEZAz79GIRBIT3asBzTbPE7HS7wizt7WBsgJClz8Ge';
APIClient.getCookie.mockReturnValue(invalidCookie);
expect(APIClient.isAuthenticated()).toBe(false);
APIClient.getCookie.mockReturnValue(validLoggedOutCookie);
expect(APIClient.isAuthenticated()).toBe(false);
APIClient.getCookie.mockReturnValue(validLoggedInCookie);
expect(APIClient.isAuthenticated()).toBe(true);
});
test('login calls get and post to login route, and sets cookie from document', (done) => {
const un = 'foo';
const pw = 'bar';
const next = 'baz';
const headers = { 'Content-Type': LOGIN_CONTENT_TYPE };
const data = `username=${un}&password=${pw}&next=${next}`;
APIClient.setCookie = jest.fn();
APIClient.login(un, pw, next).then(() => {
expect(mockAxios.get).toHaveBeenCalledTimes(1);
expect(mockAxios.get).toHaveBeenCalledWith(API_LOGIN, { headers });
expect(mockAxios.post).toHaveBeenCalledTimes(1);
expect(mockAxios.post).toHaveBeenCalledWith(API_LOGIN, data, { headers });
done();
});
});
test('login encodes uri components for username, password and redirect', (done) => {
const un = '/foo/';
const pw = '/bar/';
const next = '/baz/';
const headers = { 'Content-Type': LOGIN_CONTENT_TYPE };
const data = `username=${encodeURIComponent(un)}&password=${encodeURIComponent(pw)}&next=${encodeURIComponent(next)}`;
APIClient.login(un, pw, next).then(() => {
expect(mockAxios.post).toHaveBeenCalledTimes(1);
expect(mockAxios.post).toHaveBeenCalledWith(API_LOGIN, data, { headers });
done();
});
});
test('login redirect defaults to config route when not explicitly passed', (done) => {
const un = 'foo';
const pw = 'bar';
const headers = { 'Content-Type': LOGIN_CONTENT_TYPE };
const data = `username=${un}&password=${pw}&next=${encodeURIComponent(API_CONFIG)}`;
APIClient.setCookie = jest.fn();
APIClient.login(un, pw).then(() => {
expect(mockAxios.post).toHaveBeenCalledTimes(1);
expect(mockAxios.post).toHaveBeenCalledWith(API_LOGIN, data, { headers });
done();
});
});
test('logout calls get to logout route', () => {
APIClient.logout();
expect(mockAxios.get).toHaveBeenCalledTimes(1);
expect(mockAxios.get).toHaveBeenCalledWith(API_LOGOUT);
});
test('getConfig calls get to config route', () => {
APIClient.getConfig();
expect(mockAxios.get).toHaveBeenCalledTimes(1);
expect(mockAxios.get).toHaveBeenCalledWith(API_CONFIG);
});
test('getProjects calls get to projects route', () => {
APIClient.getProjects();
expect(mockAxios.get).toHaveBeenCalledTimes(1);
expect(mockAxios.get).toHaveBeenCalledWith(API_PROJECTS);
});
test('getOrganigzations calls get to organizations route', () => {
APIClient.getOrganizations();
expect(mockAxios.get).toHaveBeenCalledTimes(1);
expect(mockAxios.get).toHaveBeenCalledWith(API_ORGANIZATIONS);
});
test('getRoot calls get to root route', () => {
APIClient.getRoot();
expect(mockAxios.get).toHaveBeenCalledTimes(1);
expect(mockAxios.get).toHaveBeenCalledWith(API_ROOT);
});
});

4
enzyme.config.js Normal file
View File

@ -0,0 +1,4 @@
const enzyme = require('enzyme');
const Adapter = require('enzyme-adapter-react-16');
enzyme.configure({ adapter: new Adapter() });

5237
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -2,10 +2,10 @@
"name": "awx-react",
"version": "1.0.0",
"description": "",
"main": "index.js",
"main": "index.jsx",
"scripts": {
"start": "webpack-dev-server --config ./webpack.config.js --mode development",
"test": "echo \"No test specified\" && exit 0",
"test": "jest --watchAll --coverage",
"lint": "./node_modules/eslint/bin/eslint.js src/**/*.js src/**/*.jsx"
},
"keywords": [],
@ -14,16 +14,20 @@
"devDependencies": {
"babel-core": "^6.26.3",
"babel-eslint": "^10.0.0",
"babel-jest": "^23.6.0",
"babel-loader": "^7.1.5",
"babel-preset-react": "^6.24.1",
"babel-preset-stage-2": "^6.24.1",
"css-loader": "^1.0.0",
"enzyme": "^3.7.0",
"enzyme-adapter-react-16": "^1.6.0",
"eslint": "^5.6.0",
"eslint-config-airbnb": "^17.1.0",
"eslint-plugin-import": "^2.14.0",
"eslint-plugin-jsx-a11y": "^6.1.1",
"eslint-plugin-react": "^7.11.1",
"file-loader": "^2.0.0",
"jest": "^23.6.0",
"node-sass": "^4.9.3",
"react-hot-loader": "^4.3.3",
"sass-loader": "^7.1.0",
@ -42,5 +46,25 @@
"react-redux": "^5.0.7",
"react-router-dom": "^4.3.1",
"redux": "^4.0.0"
},
"jest": {
"collectCoverageFrom": [
"src/**/*.{js,jsx}"
],
"moduleNameMapper": {
"^[./a-zA-Z0-9$_-]+\\.svg$": "<rootDir>/__tests__/stubs/svgStub.js"
},
"setupTestFrameworkScriptFile": "<rootDir>/enzyme.config.js",
"testMatch": [
"<rootDir>/__tests__/tests/**/*.{js,jsx}"
],
"testEnvironment": "jsdom",
"testURL": "http://127.0.0.1:3001",
"transform": {
"^.+\\.(js|jsx)$": "<rootDir>/node_modules/babel-jest"
},
"transformIgnorePatterns": [
"[/\\\\]node_modules[/\\\\].+\\.(js|jsx)$"
]
}
}

View File

@ -14,6 +14,7 @@ import {
Button,
ButtonVariant,
Nav,
NavExpandable,
NavGroup,
NavItem,
Page,
@ -34,6 +35,7 @@ import api from './api';
import About from './components/About';
import TowerLogo from './components/TowerLogo';
import ConditionalRedirect from './components/ConditionalRedirect';
import Applications from './pages/Applications';
import Credentials from './pages/Credentials';
@ -55,32 +57,6 @@ import Teams from './pages/Teams';
import Templates from './pages/Templates';
import Users from './pages/Users';
const AuthenticatedRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={props => (
api.isAuthenticated() ? (
<Component {...props}/>
) : (
<Redirect to={{
pathname: '/login',
state: { from: props.location }
}}/>
)
)}/>
);
const UnauthenticatedRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={props => (
!api.isAuthenticated() ? (
<Component {...props}/>
) : (
<Redirect to={{
pathname: '/',
state: { from: props.location }
}}/>
)
)}/>
);
class App extends React.Component {
constructor(props) {
super(props);
@ -90,6 +66,8 @@ class App extends React.Component {
isNavOpen: (typeof window !== 'undefined' &&
window.innerWidth >= parseInt(breakpointMd.value, 10)),
};
this.state.activeGroup = this.state.activeItem.startsWith("settings_group_") ? "settings": "";
}
onNavToggle = () => {
@ -98,8 +76,8 @@ class App extends React.Component {
this.setState({ isNavOpen: !isNavOpen });
}
onNavSelect = ({ itemId }) => {
this.setState({ activeItem: itemId });
onNavSelect = ({ groupId, itemId }) => {
this.setState({ activeGroup: groupId || "", activeItem: itemId });
};
onLogoClick = () => {
@ -114,7 +92,7 @@ class App extends React.Component {
}
render() {
const { activeItem, isNavOpen } = this.state;
const { activeItem, activeGroup, isNavOpen } = this.state;
const { logo, loginInfo } = this.props;
return (
@ -132,8 +110,8 @@ class App extends React.Component {
[BackgroundImageSrc.filter]: '/assets/images/background-filter.svg'
}} />
<Switch>
<UnauthenticatedRoute path="/login" component={() => <Login logo={logo} loginInfo={loginInfo} />} />
<AuthenticatedRoute component={() => (
<ConditionalRedirect shouldRedirect={() => api.isAuthenticated()} redirectPath="/" path="/login" component={() => <Login logo={logo} loginInfo={loginInfo} />} />
<Fragment>
<Page
header={(
<PageHeader
@ -172,33 +150,46 @@ class App extends React.Component {
<NavItem to="#/management_jobs" itemId="management_jobs" isActive={activeItem === 'management_jobs'}>Management Jobs</NavItem>
<NavItem to="#/instance_groups" itemId="instance_groups" isActive={activeItem === 'instance_groups'}>Instance Groups</NavItem>
<NavItem to="#/applications" itemId="applications" isActive={activeItem === 'applications'}>Applications</NavItem>
<NavItem to="#/settings" itemId="settings" isActive={activeItem === 'settings'}>Settings</NavItem>
<NavExpandable title="Settings" groupId="settings_group" isActive={activeGroup === 'settings_group'}>
<NavItem to="#/settings/auth" groupId="settings_group" itemId="settings_group_auth" isActive={activeItem === 'settings_group_auth'}>
Authentication
</NavItem>
<NavItem to="#/settings/jobs" groupId="settings_group" itemId="settings_group_jobs" isActive={activeItem === 'settings_group_jobs'}>
Jobs
</NavItem>
<NavItem to="#/settings/system" groupId="settings_group" itemId="settings_group_system" isActive={activeItem === 'settings_group_system'}>
System
</NavItem>
<NavItem to="#/settings/ui" groupId="settings_group" itemId="settings_group_ui" isActive={activeItem === 'settings_group_ui'}>
User Interface
</NavItem>
</NavExpandable>
</NavGroup>
</Nav>
)}
/>
)}>
<Route exact path="/" component={() => (<Redirect to="/home" />)} />
<Route path="/home" component={Dashboard} />
<Route path="/jobs" component={Jobs} />
<Route path="/schedules" component={Schedules} />
<Route path="/portal" component={Portal} />
<Route path="/templates" component={Templates} />
<Route path="/credentials" component={Credentials} />
<Route path="/projects" component={Projects} />
<Route path="/inventories" component={Inventories} />
<Route path="/inventory_scripts" component={InventoryScripts} />
<Route path="/organizations" component={Organizations} />
<Route path="/users" component={Users} />
<Route path="/teams" component={Teams} />
<Route path="/credential_types" component={CredentialTypes} />
<Route path="/notification_templates" component={NotificationTemplates} />
<Route path="/management_jobs" component={ManagementJobs} />
<Route path="/instance_groups" component={InstanceGroups} />
<Route path="/applications" component={Applications} />
<Route path="/settings" component={Settings} />
<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" path="/jobs" component={Jobs} />
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/schedules" component={Schedules} />
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/portal" component={Portal} />
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/templates" component={Templates} />
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/credentials" component={Credentials} />
<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} />
<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} />
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/notification_templates" component={NotificationTemplates} />
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/management_jobs" component={ManagementJobs} />
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/instance_groups" component={InstanceGroups} />
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/applications" component={Applications} />
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/settings" component={Settings} />
</Page>
)} />
</Fragment>
</Switch>
</Fragment>
</Router>

View File

@ -11,6 +11,8 @@ const API_ORGANIZATIONS = `${API_V2}organizations/`;
const CSRF_COOKIE_NAME = 'csrftoken';
const CSRF_HEADER_NAME = 'X-CSRFToken';
const LOGIN_CONTENT_TYPE = 'application/x-www-form-urlencoded';
class APIClient {
constructor () {
this.http = axios.create({
@ -19,10 +21,15 @@ class APIClient {
});
}
/* eslint class-methods-use-this: ["error", { "exceptMethods": ["getCookie"] }] */
getCookie () {
return document.cookie;
}
isAuthenticated () {
let authenticated = false;
const parsed = (`; ${document.cookie}`).split('; userLoggedIn=');
const parsed = (`; ${this.getCookie()}`).split('; userLoggedIn=');
if (parsed.length === 2) {
authenticated = parsed.pop().split(';').shift() === 'true';
@ -37,7 +44,7 @@ class APIClient {
const next = encodeURIComponent(redirect);
const data = `username=${un}&password=${pw}&next=${next}`;
const headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
const headers = { 'Content-Type': LOGIN_CONTENT_TYPE };
return this.http.get(API_LOGIN, { headers })
.then(() => this.http.post(API_LOGIN, data, { headers }));

View File

@ -0,0 +1,23 @@
import React from 'react';
import {
Route,
Redirect
} from 'react-router-dom';
const ConditionalRedirect = ({
component: Component,
shouldRedirect,
redirectPath,
location,
...props
}) => (shouldRedirect() ? (
<Redirect to={{
pathname: redirectPath,
state: { from: location }
}}
/>
) : (
<Route {...props} render={rest => (<Component {...rest} />)} />
));
export default ConditionalRedirect;

View File

@ -97,7 +97,7 @@ class LoginPage extends Component {
onChange={this.handleUsernameChange}
/>
</div>
<div className="pf-c-form__group">
<div className="pf-c-form__group" id="password">
<label className="pf-c-form__label" htmlFor="pw">
Password
<span className="pf-c-form__label__required" aria-hidden="true">&#42;</span>

View File

@ -1,9 +1,10 @@
const webpack = require('webpack');
const TARGET_PORT = 8043;
const TARGET = `https://localhost:${TARGET_PORT}`;
module.exports = {
entry: './src/index.js',
entry: './src/index.jsx',
module: {
rules: [
{