mirror of
https://github.com/ansible/awx.git
synced 2026-01-13 11:00:03 -03:30
Merge pull request #7 from ansible/testing
add unit and functional testing to the app
This commit is contained in:
commit
ca0127d889
16
.eslintrc
16
.eslintrc
@ -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
1
.gitignore
vendored
@ -1 +1,2 @@
|
||||
node_modules/
|
||||
coverage/
|
||||
21
__mocks__/axios.js
Normal file
21
__mocks__/axios.js
Normal 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;
|
||||
1
__tests__/stubs/svgStub.js
Normal file
1
__tests__/stubs/svgStub.js
Normal file
@ -0,0 +1 @@
|
||||
module.exports = 'svg-stub';
|
||||
37
__tests__/tests/App.test.jsx
Normal file
37
__tests__/tests/App.test.jsx
Normal 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);
|
||||
});
|
||||
});
|
||||
27
__tests__/tests/ConditionalRedirect.test.jsx
Normal file
27
__tests__/tests/ConditionalRedirect.test.jsx
Normal 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);
|
||||
});
|
||||
});
|
||||
37
__tests__/tests/LoginPage.test.jsx
Normal file
37
__tests__/tests/LoginPage.test.jsx
Normal 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
117
__tests__/tests/api.test.js
Normal 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
4
enzyme.config.js
Normal 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
5237
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
28
package.json
28
package.json
@ -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)$"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
95
src/App.jsx
95
src/App.jsx
@ -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>
|
||||
|
||||
11
src/api.js
11
src/api.js
@ -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 }));
|
||||
|
||||
23
src/components/ConditionalRedirect.jsx
Normal file
23
src/components/ConditionalRedirect.jsx
Normal 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;
|
||||
@ -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">*</span>
|
||||
|
||||
@ -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: [
|
||||
{
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user