mirror of
https://github.com/ansible/awx.git
synced 2026-05-20 15:27:47 -02:30
Merge pull request #81 from jakemcdermott/update-and-refactor
wip - update to pf-react 1.43 and refactor some things
This commit is contained in:
@@ -1,38 +0,0 @@
|
|||||||
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);
|
|
||||||
axios.get = jest.fn(() => axios);
|
|
||||||
axios.post = jest.fn(() => axios);
|
|
||||||
axios.create.mockReturnValue({
|
|
||||||
get: axios.get,
|
|
||||||
post: axios.post
|
|
||||||
});
|
|
||||||
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 = () => {
|
|
||||||
axios.create.mockClear();
|
|
||||||
axios.get.mockClear();
|
|
||||||
axios.post.mockClear();
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = axios;
|
|
||||||
@@ -1,65 +1,105 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { HashRouter } from 'react-router-dom';
|
||||||
import { shallow, mount } from 'enzyme';
|
import { I18nProvider } from '@lingui/react';
|
||||||
import App from '../src/App';
|
|
||||||
import api from '../src/api';
|
|
||||||
import { API_LOGOUT, API_CONFIG } from '../src/endpoints';
|
|
||||||
|
|
||||||
import Dashboard from '../src/pages/Dashboard';
|
import { mount, shallow } from 'enzyme';
|
||||||
import Login from '../src/pages/Login';
|
import { asyncFlush } from '../jest.setup';
|
||||||
|
|
||||||
|
import App from '../src/App';
|
||||||
|
|
||||||
|
const DEFAULT_ACTIVE_GROUP = 'views_group';
|
||||||
|
|
||||||
describe('<App />', () => {
|
describe('<App />', () => {
|
||||||
test('renders without crashing', () => {
|
test('expected content is rendered', () => {
|
||||||
const appWrapper = shallow(<App />);
|
const appWrapper = mount(
|
||||||
|
<HashRouter>
|
||||||
|
<I18nProvider>
|
||||||
|
<App
|
||||||
|
routeGroups={[
|
||||||
|
{
|
||||||
|
groupTitle: 'Group One',
|
||||||
|
groupId: 'group_one',
|
||||||
|
routes: [
|
||||||
|
{ title: 'Foo', path: '/foo' },
|
||||||
|
{ title: 'Bar', path: '/bar' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
groupTitle: 'Group Two',
|
||||||
|
groupId: 'group_two',
|
||||||
|
routes: [
|
||||||
|
{ title: 'Fiz', path: '/fiz' },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
render={({ routeGroups }) => (
|
||||||
|
routeGroups.map(({ groupId }) => (<div key={groupId} id={groupId} />))
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</I18nProvider>
|
||||||
|
</HashRouter>
|
||||||
|
);
|
||||||
|
|
||||||
|
// page components
|
||||||
expect(appWrapper.length).toBe(1);
|
expect(appWrapper.length).toBe(1);
|
||||||
});
|
expect(appWrapper.find('PageHeader').length).toBe(1);
|
||||||
|
expect(appWrapper.find('PageSidebar').length).toBe(1);
|
||||||
|
|
||||||
test('renders login page when not authenticated', () => {
|
// sidebar groups and route links
|
||||||
api.isAuthenticated = jest.fn();
|
expect(appWrapper.find('NavExpandableGroup').length).toBe(2);
|
||||||
api.isAuthenticated.mockReturnValue(false);
|
expect(appWrapper.find('a[href="/#/foo"]').length).toBe(1);
|
||||||
|
expect(appWrapper.find('a[href="/#/bar"]').length).toBe(1);
|
||||||
|
expect(appWrapper.find('a[href="/#/fiz"]').length).toBe(1);
|
||||||
|
|
||||||
const appWrapper = mount(<MemoryRouter><App /></MemoryRouter>);
|
// inline render
|
||||||
|
expect(appWrapper.find('#group_one').length).toBe(1);
|
||||||
const login = appWrapper.find(Login);
|
expect(appWrapper.find('#group_two').length).toBe(1);
|
||||||
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(<MemoryRouter><App /></MemoryRouter>);
|
|
||||||
|
|
||||||
const dashboard = appWrapper.find(Dashboard);
|
|
||||||
expect(dashboard.length).toBe(1);
|
|
||||||
const login = appWrapper.find(Login);
|
|
||||||
expect(login.length).toBe(0);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('onNavToggle sets state.isNavOpen to opposite', () => {
|
test('onNavToggle sets state.isNavOpen to opposite', () => {
|
||||||
const appWrapper = shallow(<App.WrappedComponent />);
|
const appWrapper = shallow(<App />);
|
||||||
expect(appWrapper.state().isNavOpen).toBe(true);
|
const { onNavToggle } = appWrapper.instance();
|
||||||
appWrapper.instance().onNavToggle();
|
|
||||||
expect(appWrapper.state().isNavOpen).toBe(false);
|
[true, false, true, false, true].forEach(expected => {
|
||||||
|
expect(appWrapper.state().isNavOpen).toBe(expected);
|
||||||
|
onNavToggle();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('api.logout called from logout button', async () => {
|
test('onLogoClick sets selected nav back to defaults', () => {
|
||||||
const logOutButtonSelector = 'button[aria-label="Logout"]';
|
const appWrapper = shallow(<App />);
|
||||||
api.get = jest.fn().mockImplementation(() => Promise.resolve({}));
|
|
||||||
const appWrapper = mount(<MemoryRouter><App /></MemoryRouter>);
|
|
||||||
const logOutButton = appWrapper.find(logOutButtonSelector);
|
|
||||||
expect(logOutButton.length).toBe(1);
|
|
||||||
logOutButton.simulate('click');
|
|
||||||
appWrapper.setState({ activeGroup: 'foo', activeItem: 'bar' });
|
appWrapper.setState({ activeGroup: 'foo', activeItem: 'bar' });
|
||||||
expect(api.get).toHaveBeenCalledWith(API_LOGOUT);
|
expect(appWrapper.state().activeItem).toBe('bar');
|
||||||
|
expect(appWrapper.state().activeGroup).toBe('foo');
|
||||||
|
|
||||||
|
appWrapper.instance().onLogoClick();
|
||||||
|
expect(appWrapper.state().activeGroup).toBe(DEFAULT_ACTIVE_GROUP);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Componenet makes REST call to API_CONFIG endpoint when mounted', () => {
|
test('onLogout makes expected call to api client', async (done) => {
|
||||||
api.get = jest.fn().mockImplementation(() => Promise.resolve({}));
|
const logout = jest.fn(() => Promise.resolve());
|
||||||
const appWrapper = shallow(<App.WrappedComponent />);
|
const api = { logout };
|
||||||
expect(api.get).toHaveBeenCalledTimes(1);
|
|
||||||
expect(api.get).toHaveBeenCalledWith(API_CONFIG);
|
const appWrapper = shallow(<App api={api} />);
|
||||||
|
|
||||||
|
appWrapper.instance().onLogout();
|
||||||
|
await asyncFlush();
|
||||||
|
expect(api.logout).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Component makes expected call to api client when mounted', () => {
|
||||||
|
const getConfig = jest.fn().mockImplementation(() => Promise.resolve({}));
|
||||||
|
const api = { getConfig };
|
||||||
|
const appWrapper = mount(
|
||||||
|
<HashRouter>
|
||||||
|
<I18nProvider>
|
||||||
|
<App api={api} />
|
||||||
|
</I18nProvider>
|
||||||
|
</HashRouter>
|
||||||
|
);
|
||||||
|
expect(getConfig).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,80 +1,61 @@
|
|||||||
import mockAxios from 'axios';
|
|
||||||
import APIClient from '../src/api';
|
import APIClient from '../src/api';
|
||||||
import * as endpoints from '../src/endpoints';
|
|
||||||
|
|
||||||
const CSRF_COOKIE_NAME = 'csrftoken';
|
const invalidCookie = 'invalid';
|
||||||
const CSRF_HEADER_NAME = 'X-CSRFToken';
|
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';
|
||||||
const LOGIN_CONTENT_TYPE = 'application/x-www-form-urlencoded';
|
|
||||||
|
|
||||||
describe('APIClient (api.js)', () => {
|
describe('APIClient (api.js)', () => {
|
||||||
afterEach(() => {
|
test('isAuthenticated returns false when cookie is invalid', () => {
|
||||||
mockAxios.customClearMocks();
|
APIClient.getCookie = jest.fn(() => invalidCookie);
|
||||||
|
|
||||||
|
const api = new APIClient();
|
||||||
|
expect(api.isAuthenticated()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('constructor calls axios create', () => {
|
test('isAuthenticated returns false when cookie is unauthenticated', () => {
|
||||||
const csrfObj = {
|
APIClient.getCookie = jest.fn(() => validLoggedOutCookie);
|
||||||
xsrfCookieName: CSRF_COOKIE_NAME,
|
|
||||||
xsrfHeaderName: CSRF_HEADER_NAME
|
const api = new APIClient();
|
||||||
};
|
expect(api.isAuthenticated()).toBe(false);
|
||||||
expect(mockAxios.create).toHaveBeenCalledTimes(1);
|
|
||||||
expect(mockAxios.create).toHaveBeenCalledWith(csrfObj);
|
|
||||||
expect(APIClient.http).toHaveProperty('get');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('isAuthenticated checks authentication and sets cookie from document', () => {
|
test('isAuthenticated returns true when cookie is valid and authenticated', () => {
|
||||||
APIClient.getCookie = jest.fn();
|
APIClient.getCookie = jest.fn(() => validLoggedInCookie);
|
||||||
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 api = new APIClient();
|
||||||
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';
|
expect(api.isAuthenticated()).toBe(true);
|
||||||
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) => {
|
test('login calls get and post with expected content headers', async (done) => {
|
||||||
const un = 'foo';
|
const headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
|
||||||
const pw = 'bar';
|
|
||||||
const next = 'baz';
|
const createPromise = () => Promise.resolve();
|
||||||
const headers = { 'Content-Type': LOGIN_CONTENT_TYPE };
|
const mockHttp = ({ get: jest.fn(createPromise), post: jest.fn(createPromise) });
|
||||||
const data = `username=${un}&password=${pw}&next=${next}`;
|
|
||||||
APIClient.setCookie = jest.fn();
|
const api = new APIClient(mockHttp);
|
||||||
APIClient.login(un, pw, next).then(() => {
|
await api.login('username', 'password');
|
||||||
expect(mockAxios.get).toHaveBeenCalledTimes(1);
|
|
||||||
expect(mockAxios.get).toHaveBeenCalledWith(endpoints.API_LOGIN, { headers });
|
expect(mockHttp.get).toHaveBeenCalledTimes(1);
|
||||||
expect(mockAxios.post).toHaveBeenCalledTimes(1);
|
expect(mockHttp.get.mock.calls[0]).toContainEqual({ headers });
|
||||||
expect(mockAxios.post).toHaveBeenCalledWith(endpoints.API_LOGIN, data, { headers });
|
|
||||||
done();
|
expect(mockHttp.post).toHaveBeenCalledTimes(1);
|
||||||
});
|
expect(mockHttp.post.mock.calls[0]).toContainEqual({ headers });
|
||||||
|
|
||||||
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('login encodes uri components for username, password and redirect', (done) => {
|
test('login sends expected data', async (done) => {
|
||||||
const un = '/foo/';
|
const createPromise = () => Promise.resolve();
|
||||||
const pw = '/bar/';
|
const mockHttp = ({ get: jest.fn(createPromise), post: jest.fn(createPromise) });
|
||||||
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(endpoints.API_LOGIN, data, { headers });
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('login redirect defaults to config route when not explicitly passed', (done) => {
|
const api = new APIClient(mockHttp);
|
||||||
const un = 'foo';
|
await api.login('foo', 'bar');
|
||||||
const pw = 'bar';
|
await api.login('foo', 'bar', 'baz');
|
||||||
const headers = { 'Content-Type': LOGIN_CONTENT_TYPE };
|
|
||||||
const data = `username=${un}&password=${pw}&next=${encodeURIComponent(endpoints.API_CONFIG)}`;
|
|
||||||
APIClient.setCookie = jest.fn();
|
|
||||||
APIClient.login(un, pw).then(() => {
|
|
||||||
expect(mockAxios.post).toHaveBeenCalledTimes(1);
|
|
||||||
expect(mockAxios.post).toHaveBeenCalledWith(endpoints.API_LOGIN, data, { headers });
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
expect(mockHttp.post).toHaveBeenCalledTimes(2);
|
||||||
|
expect(mockHttp.post.mock.calls[0]).toContainEqual('username=foo&password=bar&next=%2Fapi%2Fv2%2Fconfig%2F');
|
||||||
|
expect(mockHttp.post.mock.calls[1]).toContainEqual('username=foo&password=bar&next=baz');
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mount } from 'enzyme';
|
import { mount } from 'enzyme';
|
||||||
import { I18nProvider } from '@lingui/react';
|
import { I18nProvider } from '@lingui/react';
|
||||||
import api from '../../src/api';
|
|
||||||
import { API_CONFIG } from '../../src/endpoints';
|
|
||||||
import About from '../../src/components/About';
|
import About from '../../src/components/About';
|
||||||
|
|
||||||
describe('<About />', () => {
|
describe('<About />', () => {
|
||||||
@@ -19,16 +17,16 @@ describe('<About />', () => {
|
|||||||
aboutWrapper.unmount();
|
aboutWrapper.unmount();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('close button calls onAboutModalClose', () => {
|
test('close button calls onClose handler', () => {
|
||||||
const onAboutModalClose = jest.fn();
|
const onClose = jest.fn();
|
||||||
aboutWrapper = mount(
|
aboutWrapper = mount(
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<About isOpen onAboutModalClose={onAboutModalClose} />
|
<About isOpen onClose={onClose} />
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
);
|
);
|
||||||
closeButton = aboutWrapper.find('AboutModalBoxCloseButton Button');
|
closeButton = aboutWrapper.find('AboutModalBoxCloseButton Button');
|
||||||
closeButton.simulate('click');
|
closeButton.simulate('click');
|
||||||
expect(onAboutModalClose).toBeCalled();
|
expect(onClose).toBeCalled();
|
||||||
aboutWrapper.unmount();
|
aboutWrapper.unmount();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
import React 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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { mount } from 'enzyme';
|
|
||||||
import { I18nProvider } from '@lingui/react';
|
|
||||||
import HelpDropdown from '../../src/components/HelpDropdown';
|
|
||||||
|
|
||||||
let questionCircleIcon;
|
|
||||||
let dropdownWrapper;
|
|
||||||
let dropdownComponentInstance;
|
|
||||||
let dropdownToggle;
|
|
||||||
let dropdownItems;
|
|
||||||
let dropdownItem;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
dropdownWrapper = mount(
|
|
||||||
<I18nProvider>
|
|
||||||
<HelpDropdown />
|
|
||||||
</I18nProvider>
|
|
||||||
);
|
|
||||||
dropdownComponentInstance = dropdownWrapper.find(HelpDropdown).instance();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
dropdownWrapper.unmount();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('<HelpDropdown />', () => {
|
|
||||||
test('initially renders without crashing', () => {
|
|
||||||
expect(dropdownWrapper.length).toBe(1);
|
|
||||||
expect(dropdownComponentInstance.state.isOpen).toEqual(false);
|
|
||||||
expect(dropdownComponentInstance.state.showAboutModal).toEqual(false);
|
|
||||||
questionCircleIcon = dropdownWrapper.find('QuestionCircleIcon');
|
|
||||||
expect(questionCircleIcon.length).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('renders two dropdown items', () => {
|
|
||||||
dropdownComponentInstance.setState({ isOpen: true });
|
|
||||||
dropdownWrapper.update();
|
|
||||||
dropdownItems = dropdownWrapper.find('DropdownItem');
|
|
||||||
expect(dropdownItems.length).toBe(2);
|
|
||||||
const dropdownTexts = dropdownItems.map(item => item.text());
|
|
||||||
expect(dropdownTexts).toEqual(['Help', 'About']);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('onToggle sets state.isOpen to opposite', () => {
|
|
||||||
dropdownComponentInstance.setState({ isOpen: true });
|
|
||||||
dropdownWrapper.update();
|
|
||||||
dropdownToggle = dropdownWrapper.find('DropdownToggle > DropdownToggle');
|
|
||||||
dropdownToggle.simulate('click');
|
|
||||||
expect(dropdownComponentInstance.state.isOpen).toEqual(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('about dropdown item sets state.showAboutModal to true', () => {
|
|
||||||
dropdownComponentInstance.setState({ isOpen: true });
|
|
||||||
dropdownWrapper.update();
|
|
||||||
dropdownItem = dropdownWrapper.find('DropdownItem a').at(1);
|
|
||||||
dropdownItem.simulate('click');
|
|
||||||
expect(dropdownComponentInstance.state.showAboutModal).toEqual(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('onAboutModalClose sets state.showAboutModal to false', () => {
|
|
||||||
dropdownComponentInstance.setState({ showAboutModal: true });
|
|
||||||
dropdownWrapper.update();
|
|
||||||
const aboutModal = dropdownWrapper.find('AboutModal');
|
|
||||||
aboutModal.find('AboutModalBoxCloseButton Button').simulate('click');
|
|
||||||
expect(dropdownComponentInstance.state.showAboutModal).toEqual(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { mount } from 'enzyme';
|
|
||||||
import { I18nProvider } from '@lingui/react';
|
|
||||||
import LogoutButton from '../../src/components/LogoutButton';
|
|
||||||
|
|
||||||
let buttonWrapper;
|
|
||||||
let buttonElem;
|
|
||||||
let userIconElem;
|
|
||||||
|
|
||||||
const findChildren = () => {
|
|
||||||
buttonElem = buttonWrapper.find('Button');
|
|
||||||
userIconElem = buttonWrapper.find('UserIcon');
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('<LogoutButton />', () => {
|
|
||||||
test('initially renders without crashing', () => {
|
|
||||||
const onDevLogout = jest.fn();
|
|
||||||
buttonWrapper = mount(
|
|
||||||
<I18nProvider>
|
|
||||||
<LogoutButton onDevLogout={onDevLogout} />
|
|
||||||
</I18nProvider>
|
|
||||||
);
|
|
||||||
findChildren();
|
|
||||||
expect(buttonWrapper.length).toBe(1);
|
|
||||||
expect(buttonElem.length).toBe(1);
|
|
||||||
expect(userIconElem.length).toBe(1);
|
|
||||||
buttonElem.simulate('keyDown', { keyCode: 40, which: 40 });
|
|
||||||
expect(onDevLogout).toHaveBeenCalledTimes(0);
|
|
||||||
buttonElem.simulate('keyDown', { keyCode: 13, which: 13 });
|
|
||||||
expect(onDevLogout).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -12,7 +12,7 @@ describe('NavExpandableGroup', () => {
|
|||||||
<Nav aria-label="Test Navigation">
|
<Nav aria-label="Test Navigation">
|
||||||
<NavExpandableGroup
|
<NavExpandableGroup
|
||||||
groupId="test"
|
groupId="test"
|
||||||
title="Test"
|
groupTitle="Test"
|
||||||
routes={[
|
routes={[
|
||||||
{ path: '/foo', title: 'Foo' },
|
{ path: '/foo', title: 'Foo' },
|
||||||
{ path: '/bar', title: 'Bar' },
|
{ path: '/bar', title: 'Bar' },
|
||||||
@@ -45,7 +45,7 @@ describe('NavExpandableGroup', () => {
|
|||||||
<Nav aria-label="Test Navigation">
|
<Nav aria-label="Test Navigation">
|
||||||
<NavExpandableGroup
|
<NavExpandableGroup
|
||||||
groupId="test"
|
groupId="test"
|
||||||
title="Test"
|
groupTitle="Test"
|
||||||
routes={[
|
routes={[
|
||||||
{ path: '/foo', title: 'Foo' },
|
{ path: '/foo', title: 'Foo' },
|
||||||
{ path: '/bar', title: 'Bar' },
|
{ path: '/bar', title: 'Bar' },
|
||||||
|
|||||||
16
__tests__/components/PageHeaderToolbar.test.jsx
Normal file
16
__tests__/components/PageHeaderToolbar.test.jsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { mount } from 'enzyme';
|
||||||
|
|
||||||
|
import { I18nProvider } from '@lingui/react';
|
||||||
|
import PageHeaderToolbar from '../../src/components/PageHeaderToolbar';
|
||||||
|
|
||||||
|
describe('PageHeaderToolbar', () => {
|
||||||
|
test('renders the expected content', () => {
|
||||||
|
const wrapper = mount(
|
||||||
|
<I18nProvider>
|
||||||
|
<PageHeaderToolbar />
|
||||||
|
</I18nProvider>
|
||||||
|
);
|
||||||
|
expect(wrapper.find('Toolbar')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,22 +1,38 @@
|
|||||||
import React from 'react';
|
import { mount } from 'enzyme';
|
||||||
import ReactDOM from 'react-dom';
|
import { main } from '../src/index';
|
||||||
|
|
||||||
import api from '../src/api';
|
const render = template => mount(template);
|
||||||
|
const data = { custom_logo: 'foo', custom_login_info: '' }
|
||||||
import indexToRender from '../src/index';
|
|
||||||
|
|
||||||
const custom_logo = (<div>logo</div>);
|
|
||||||
const custom_login_info = 'custom login info';
|
|
||||||
|
|
||||||
jest.mock('react-dom', () => ({ render: jest.fn() }));
|
|
||||||
|
|
||||||
describe('index.jsx', () => {
|
describe('index.jsx', () => {
|
||||||
test('renders without crashing', async () => {
|
test('initialization', async (done) => {
|
||||||
api.getRoot = jest.fn().mockImplementation(() => Promise
|
const isAuthenticated = () => false;
|
||||||
.resolve({ data: { custom_logo, custom_login_info } }));
|
const getRoot = jest.fn(() => Promise.resolve({ data }));
|
||||||
|
|
||||||
await indexToRender();
|
const api = { getRoot, isAuthenticated };
|
||||||
|
const wrapper = await main(render, api);
|
||||||
|
|
||||||
expect(ReactDOM.render).toHaveBeenCalled();
|
expect(api.getRoot).toHaveBeenCalled();
|
||||||
|
expect(wrapper.find('Dashboard')).toHaveLength(0);
|
||||||
|
expect(wrapper.find('Login')).toHaveLength(1);
|
||||||
|
|
||||||
|
const { src } = wrapper.find('Login Brand img').props();
|
||||||
|
expect(src).toContain(data.custom_logo);
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('dashboard is loaded when authenticated', async (done) => {
|
||||||
|
const isAuthenticated = () => true;
|
||||||
|
const getRoot = jest.fn(() => Promise.resolve({ data }));
|
||||||
|
|
||||||
|
const api = { getRoot, isAuthenticated };
|
||||||
|
const wrapper = await main(render, api);
|
||||||
|
|
||||||
|
expect(api.getRoot).toHaveBeenCalled();
|
||||||
|
expect(wrapper.find('Dashboard')).toHaveLength(1);
|
||||||
|
expect(wrapper.find('Login')).toHaveLength(0);
|
||||||
|
|
||||||
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ import { MemoryRouter } from 'react-router-dom';
|
|||||||
import { mount, shallow } from 'enzyme';
|
import { mount, shallow } from 'enzyme';
|
||||||
import { I18nProvider } from '@lingui/react';
|
import { I18nProvider } from '@lingui/react';
|
||||||
import { asyncFlush } from '../../jest.setup';
|
import { asyncFlush } from '../../jest.setup';
|
||||||
import AtLogin from '../../src/pages/Login';
|
import AWXLogin from '../../src/pages/Login';
|
||||||
import api from '../../src/api';
|
import APIClient from '../../src/api';
|
||||||
|
|
||||||
describe('<Login />', () => {
|
describe('<Login />', () => {
|
||||||
let loginWrapper;
|
let loginWrapper;
|
||||||
let atLogin;
|
let awxLogin;
|
||||||
let loginPage;
|
let loginPage;
|
||||||
let loginForm;
|
let loginForm;
|
||||||
let usernameInput;
|
let usernameInput;
|
||||||
@@ -16,21 +16,23 @@ describe('<Login />', () => {
|
|||||||
let submitButton;
|
let submitButton;
|
||||||
let loginHeaderLogo;
|
let loginHeaderLogo;
|
||||||
|
|
||||||
|
const api = new APIClient({});
|
||||||
|
|
||||||
const findChildren = () => {
|
const findChildren = () => {
|
||||||
atLogin = loginWrapper.find('AtLogin');
|
awxLogin = loginWrapper.find('AWXLogin');
|
||||||
loginPage = loginWrapper.find('LoginPage');
|
loginPage = loginWrapper.find('LoginPage');
|
||||||
loginForm = loginWrapper.find('LoginForm');
|
loginForm = loginWrapper.find('LoginForm');
|
||||||
usernameInput = loginWrapper.find('input#pf-login-username-id');
|
usernameInput = loginWrapper.find('input#pf-login-username-id');
|
||||||
passwordInput = loginWrapper.find('input#pf-login-password-id');
|
passwordInput = loginWrapper.find('input#pf-login-password-id');
|
||||||
submitButton = loginWrapper.find('Button[type="submit"]');
|
submitButton = loginWrapper.find('Button[type="submit"]');
|
||||||
loginHeaderLogo = loginWrapper.find('LoginHeaderBrand Brand');
|
loginHeaderLogo = loginPage.find('img');
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
loginWrapper = mount(
|
loginWrapper = mount(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<AtLogin />
|
<AWXLogin api={api} />
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
@@ -49,7 +51,7 @@ describe('<Login />', () => {
|
|||||||
expect(usernameInput.props().value).toBe('');
|
expect(usernameInput.props().value).toBe('');
|
||||||
expect(passwordInput.length).toBe(1);
|
expect(passwordInput.length).toBe(1);
|
||||||
expect(passwordInput.props().value).toBe('');
|
expect(passwordInput.props().value).toBe('');
|
||||||
expect(atLogin.state().isValidPassword).toBe(true);
|
expect(awxLogin.state().isInputValid).toBe(true);
|
||||||
expect(submitButton.length).toBe(1);
|
expect(submitButton.length).toBe(1);
|
||||||
expect(submitButton.props().isDisabled).toBe(false);
|
expect(submitButton.props().isDisabled).toBe(false);
|
||||||
expect(loginHeaderLogo.length).toBe(1);
|
expect(loginHeaderLogo.length).toBe(1);
|
||||||
@@ -59,7 +61,7 @@ describe('<Login />', () => {
|
|||||||
loginWrapper = mount(
|
loginWrapper = mount(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<AtLogin logo="images/foo.jpg" alt="Foo Application" />
|
<AWXLogin api={api} logo="images/foo.jpg" alt="Foo Application" />
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
@@ -73,7 +75,7 @@ describe('<Login />', () => {
|
|||||||
loginWrapper = mount(
|
loginWrapper = mount(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<AtLogin />
|
<AWXLogin api={api} />
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
@@ -84,49 +86,49 @@ describe('<Login />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('state maps to un/pw input value props', () => {
|
test('state maps to un/pw input value props', () => {
|
||||||
atLogin.setState({ username: 'un', password: 'pw' });
|
awxLogin.setState({ username: 'un', password: 'pw' });
|
||||||
expect(atLogin.state().username).toBe('un');
|
expect(awxLogin.state().username).toBe('un');
|
||||||
expect(atLogin.state().password).toBe('pw');
|
expect(awxLogin.state().password).toBe('pw');
|
||||||
findChildren();
|
findChildren();
|
||||||
expect(usernameInput.props().value).toBe('un');
|
expect(usernameInput.props().value).toBe('un');
|
||||||
expect(passwordInput.props().value).toBe('pw');
|
expect(passwordInput.props().value).toBe('pw');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('updating un/pw clears out error', () => {
|
test('updating un/pw clears out error', () => {
|
||||||
atLogin.setState({ isValidPassword: false });
|
awxLogin.setState({ isInputValid: false });
|
||||||
expect(loginWrapper.find('.pf-c-form__helper-text.pf-m-error').length).toBe(1);
|
expect(loginWrapper.find('.pf-c-form__helper-text.pf-m-error').length).toBe(1);
|
||||||
usernameInput.instance().value = 'uname';
|
usernameInput.instance().value = 'uname';
|
||||||
usernameInput.simulate('change');
|
usernameInput.simulate('change');
|
||||||
expect(atLogin.state().username).toBe('uname');
|
expect(awxLogin.state().username).toBe('uname');
|
||||||
expect(atLogin.state().isValidPassword).toBe(true);
|
expect(awxLogin.state().isInputValid).toBe(true);
|
||||||
expect(loginWrapper.find('.pf-c-form__helper-text.pf-m-error').length).toBe(0);
|
expect(loginWrapper.find('.pf-c-form__helper-text.pf-m-error').length).toBe(0);
|
||||||
atLogin.setState({ isValidPassword: false });
|
awxLogin.setState({ isInputValid: false });
|
||||||
expect(loginWrapper.find('.pf-c-form__helper-text.pf-m-error').length).toBe(1);
|
expect(loginWrapper.find('.pf-c-form__helper-text.pf-m-error').length).toBe(1);
|
||||||
passwordInput.instance().value = 'pword';
|
passwordInput.instance().value = 'pword';
|
||||||
passwordInput.simulate('change');
|
passwordInput.simulate('change');
|
||||||
expect(atLogin.state().password).toBe('pword');
|
expect(awxLogin.state().password).toBe('pword');
|
||||||
expect(atLogin.state().isValidPassword).toBe(true);
|
expect(awxLogin.state().isInputValid).toBe(true);
|
||||||
expect(loginWrapper.find('.pf-c-form__helper-text.pf-m-error').length).toBe(0);
|
expect(loginWrapper.find('.pf-c-form__helper-text.pf-m-error').length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('api.login not called when loading', () => {
|
test('api.login not called when loading', () => {
|
||||||
api.login = jest.fn().mockImplementation(() => Promise.resolve({}));
|
api.login = jest.fn().mockImplementation(() => Promise.resolve({}));
|
||||||
expect(atLogin.state().loading).toBe(false);
|
expect(awxLogin.state().isLoading).toBe(false);
|
||||||
atLogin.setState({ loading: true });
|
awxLogin.setState({ isLoading: true });
|
||||||
submitButton.simulate('click');
|
submitButton.simulate('click');
|
||||||
expect(api.login).toHaveBeenCalledTimes(0);
|
expect(api.login).toHaveBeenCalledTimes(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('submit calls api.login successfully', async () => {
|
test('submit calls api.login successfully', async () => {
|
||||||
api.login = jest.fn().mockImplementation(() => Promise.resolve({}));
|
api.login = jest.fn().mockImplementation(() => Promise.resolve({}));
|
||||||
expect(atLogin.state().loading).toBe(false);
|
expect(awxLogin.state().isLoading).toBe(false);
|
||||||
atLogin.setState({ username: 'unamee', password: 'pwordd' });
|
awxLogin.setState({ username: 'unamee', password: 'pwordd' });
|
||||||
submitButton.simulate('click');
|
submitButton.simulate('click');
|
||||||
expect(api.login).toHaveBeenCalledTimes(1);
|
expect(api.login).toHaveBeenCalledTimes(1);
|
||||||
expect(api.login).toHaveBeenCalledWith('unamee', 'pwordd');
|
expect(api.login).toHaveBeenCalledWith('unamee', 'pwordd');
|
||||||
expect(atLogin.state().loading).toBe(true);
|
expect(awxLogin.state().isLoading).toBe(true);
|
||||||
await asyncFlush();
|
await asyncFlush();
|
||||||
expect(atLogin.state().loading).toBe(false);
|
expect(awxLogin.state().isLoading).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('submit calls api.login handles 401 error', async () => {
|
test('submit calls api.login handles 401 error', async () => {
|
||||||
@@ -135,16 +137,16 @@ describe('<Login />', () => {
|
|||||||
err.response = { status: 401, message: 'problem' };
|
err.response = { status: 401, message: 'problem' };
|
||||||
return Promise.reject(err);
|
return Promise.reject(err);
|
||||||
});
|
});
|
||||||
expect(atLogin.state().loading).toBe(false);
|
expect(awxLogin.state().isLoading).toBe(false);
|
||||||
expect(atLogin.state().isValidPassword).toBe(true);
|
expect(awxLogin.state().isInputValid).toBe(true);
|
||||||
atLogin.setState({ username: 'unamee', password: 'pwordd' });
|
awxLogin.setState({ username: 'unamee', password: 'pwordd' });
|
||||||
submitButton.simulate('click');
|
submitButton.simulate('click');
|
||||||
expect(api.login).toHaveBeenCalledTimes(1);
|
expect(api.login).toHaveBeenCalledTimes(1);
|
||||||
expect(api.login).toHaveBeenCalledWith('unamee', 'pwordd');
|
expect(api.login).toHaveBeenCalledWith('unamee', 'pwordd');
|
||||||
expect(atLogin.state().loading).toBe(true);
|
expect(awxLogin.state().isLoading).toBe(true);
|
||||||
await asyncFlush();
|
await asyncFlush();
|
||||||
expect(atLogin.state().isValidPassword).toBe(false);
|
expect(awxLogin.state().isInputValid).toBe(false);
|
||||||
expect(atLogin.state().loading).toBe(false);
|
expect(awxLogin.state().isLoading).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('submit calls api.login handles non-401 error', async () => {
|
test('submit calls api.login handles non-401 error', async () => {
|
||||||
@@ -153,20 +155,20 @@ describe('<Login />', () => {
|
|||||||
err.response = { status: 500, message: 'problem' };
|
err.response = { status: 500, message: 'problem' };
|
||||||
return Promise.reject(err);
|
return Promise.reject(err);
|
||||||
});
|
});
|
||||||
expect(atLogin.state().loading).toBe(false);
|
expect(awxLogin.state().isLoading).toBe(false);
|
||||||
atLogin.setState({ username: 'unamee', password: 'pwordd' });
|
awxLogin.setState({ username: 'unamee', password: 'pwordd' });
|
||||||
submitButton.simulate('click');
|
submitButton.simulate('click');
|
||||||
expect(api.login).toHaveBeenCalledTimes(1);
|
expect(api.login).toHaveBeenCalledTimes(1);
|
||||||
expect(api.login).toHaveBeenCalledWith('unamee', 'pwordd');
|
expect(api.login).toHaveBeenCalledWith('unamee', 'pwordd');
|
||||||
expect(atLogin.state().loading).toBe(true);
|
expect(awxLogin.state().isLoading).toBe(true);
|
||||||
await asyncFlush();
|
await asyncFlush();
|
||||||
expect(atLogin.state().loading).toBe(false);
|
expect(awxLogin.state().isLoading).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('render Redirect to / when already authenticated', () => {
|
test('render Redirect to / when already authenticated', () => {
|
||||||
api.isAuthenticated = jest.fn();
|
api.isAuthenticated = jest.fn();
|
||||||
api.isAuthenticated.mockReturnValue(true);
|
api.isAuthenticated.mockReturnValue(true);
|
||||||
loginWrapper = shallow(<AtLogin />);
|
loginWrapper = shallow(<AWXLogin api={api} />);
|
||||||
const redirectElem = loginWrapper.find('Redirect');
|
const redirectElem = loginWrapper.find('Redirect');
|
||||||
expect(redirectElem.length).toBe(1);
|
expect(redirectElem.length).toBe(1);
|
||||||
expect(redirectElem.props().to).toBe('/');
|
expect(redirectElem.props().to).toBe('/');
|
||||||
|
|||||||
37
package-lock.json
generated
37
package-lock.json
generated
@@ -1311,21 +1311,22 @@
|
|||||||
"integrity": "sha512-sIRfo/tk4NSnaRwHIHLUf4XoqzNNa4MMa8ZWivzzSfdZ5pCbgvZtyEUqKnQAEH6zuaCM9S8HyhxcNXnm/xaYaQ=="
|
"integrity": "sha512-sIRfo/tk4NSnaRwHIHLUf4XoqzNNa4MMa8ZWivzzSfdZ5pCbgvZtyEUqKnQAEH6zuaCM9S8HyhxcNXnm/xaYaQ=="
|
||||||
},
|
},
|
||||||
"@patternfly/react-core": {
|
"@patternfly/react-core": {
|
||||||
"version": "1.37.2",
|
"version": "1.43.5",
|
||||||
"resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-1.37.2.tgz",
|
"resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-1.43.5.tgz",
|
||||||
"integrity": "sha512-zzHwqGEsRWzw9uRkbrf6PmUpcl6EMxQSbUJ1zmv7Ryc32CcSMgrDL4ZA3x/tf4TAYTMRBKUK3O8S5veRjxpFuw==",
|
"integrity": "sha512-xO37/q5BJEdGvAoPllm/gbcwCtW7t0Ae7mKm5UU7d4i5bycEjd0UwJacYxCA6GFTwxN5kzn61XEpAUPY49U3pA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@patternfly/react-icons": "^2.9.1",
|
"@patternfly/react-icons": "^2.9.5",
|
||||||
"@patternfly/react-styles": "^2.3.0",
|
"@patternfly/react-styles": "^2.3.0",
|
||||||
"@patternfly/react-tokens": "^1.0.0",
|
"@patternfly/react-tokens": "^1.0.0",
|
||||||
|
"@tippy.js/react": "^1.1.1",
|
||||||
"exenv": "^1.2.2",
|
"exenv": "^1.2.2",
|
||||||
"focus-trap-react": "^4.0.1"
|
"focus-trap-react": "^4.0.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@patternfly/react-icons": {
|
"@patternfly/react-icons": {
|
||||||
"version": "2.9.1",
|
"version": "2.9.5",
|
||||||
"resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-2.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-2.9.5.tgz",
|
||||||
"integrity": "sha512-CBTpGXvqr91rBpxeb5/l2BimrtRlMkBKnIOTgX7V44MIIq3YE3P6A6CQK0fgIH1HGvCdiNf5sXbQz9xp+pB/3A=="
|
"integrity": "sha512-5e/BD2ER5jifUjUgbIilApOfhVldlAjhQdh7EwH/M3M+qzIb+2qKxV/xQ6hWD3AA71lcYIxvPMMHgdWIAl5oPQ=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1358,6 +1359,15 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-1.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-1.9.0.tgz",
|
||||||
"integrity": "sha512-wxlxeY5B37FkI9W3x4EQyZ9Q8lra3xBYEUg5CFCmWQZTvdH4vAC19l7mE+AQZqHXD4unvltS0ndi753LeHPyAg=="
|
"integrity": "sha512-wxlxeY5B37FkI9W3x4EQyZ9Q8lra3xBYEUg5CFCmWQZTvdH4vAC19l7mE+AQZqHXD4unvltS0ndi753LeHPyAg=="
|
||||||
},
|
},
|
||||||
|
"@tippy.js/react": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tippy.js/react/-/react-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-TkL1VufxgUvTMouDoBGv2vTdtUxtLUaRpspI4Rv0DsoKe2Ex1E5bl/qISk434mhuAhEnXuemrcgTaPWrfDvmGw==",
|
||||||
|
"requires": {
|
||||||
|
"prop-types": "^15.6.2",
|
||||||
|
"tippy.js": "^3.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/node": {
|
"@types/node": {
|
||||||
"version": "10.12.1",
|
"version": "10.12.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.1.tgz",
|
||||||
@@ -11105,6 +11115,11 @@
|
|||||||
"integrity": "sha512-Vy9eH1dRD9wHjYt/QqXcTz+RnX/zg53xK+KljFSX30PvdDMb2z+c6uDUeblUGqqJgz3QFsdlA0IJvHziPmWtQg==",
|
"integrity": "sha512-Vy9eH1dRD9wHjYt/QqXcTz+RnX/zg53xK+KljFSX30PvdDMb2z+c6uDUeblUGqqJgz3QFsdlA0IJvHziPmWtQg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"popper.js": {
|
||||||
|
"version": "1.14.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.14.6.tgz",
|
||||||
|
"integrity": "sha512-AGwHGQBKumlk/MDfrSOf0JHhJCImdDMcGNoqKmKkU+68GFazv3CQ6q9r7Ja1sKDZmYWTckY/uLyEznheTDycnA=="
|
||||||
|
},
|
||||||
"portfinder": {
|
"portfinder": {
|
||||||
"version": "1.0.19",
|
"version": "1.0.19",
|
||||||
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.19.tgz",
|
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.19.tgz",
|
||||||
@@ -13891,6 +13906,14 @@
|
|||||||
"setimmediate": "^1.0.4"
|
"setimmediate": "^1.0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"tippy.js": {
|
||||||
|
"version": "3.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-3.3.0.tgz",
|
||||||
|
"integrity": "sha512-2gIQg57EFSCBqE97NZbakSkGBJF0GzdOhx/lneGQGMzJiJyvbpyKgNy4l4qofq0nEbXACl7C/jW/ErsdQa21aQ==",
|
||||||
|
"requires": {
|
||||||
|
"popper.js": "^1.14.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"tmp": {
|
"tmp": {
|
||||||
"version": "0.0.33",
|
"version": "0.0.33",
|
||||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
|
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lingui/react": "^2.7.2",
|
"@lingui/react": "^2.7.2",
|
||||||
"@patternfly/patternfly-next": "^1.0.84",
|
"@patternfly/patternfly-next": "^1.0.84",
|
||||||
"@patternfly/react-core": "^1.37.2",
|
"@patternfly/react-core": "^1.43.5",
|
||||||
"@patternfly/react-icons": "^2.9.1",
|
"@patternfly/react-icons": "^2.9.1",
|
||||||
"@patternfly/react-styles": "^2.3.0",
|
"@patternfly/react-styles": "^2.3.0",
|
||||||
"@patternfly/react-tokens": "^1.9.0",
|
"@patternfly/react-tokens": "^1.9.0",
|
||||||
|
|||||||
357
src/App.jsx
357
src/App.jsx
@@ -1,261 +1,158 @@
|
|||||||
import React, { Fragment } from 'react';
|
import React, { Component, Fragment } from 'react';
|
||||||
import { ConfigContext } from './context';
|
import { global_breakpoint_md } from '@patternfly/react-tokens';
|
||||||
|
|
||||||
import { I18nProvider, I18n } from '@lingui/react';
|
|
||||||
import { t } from '@lingui/macro';
|
|
||||||
import {
|
import {
|
||||||
Redirect,
|
|
||||||
Switch,
|
|
||||||
withRouter
|
|
||||||
} from 'react-router-dom';
|
|
||||||
|
|
||||||
import {
|
|
||||||
BackgroundImage,
|
|
||||||
BackgroundImageSrc,
|
|
||||||
Nav,
|
Nav,
|
||||||
NavList,
|
NavList,
|
||||||
Page,
|
Page,
|
||||||
PageHeader,
|
PageHeader,
|
||||||
PageSidebar,
|
PageSidebar,
|
||||||
Toolbar,
|
|
||||||
ToolbarGroup,
|
|
||||||
ToolbarItem
|
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import { global_breakpoint_md as breakpointMd } from '@patternfly/react-tokens';
|
|
||||||
|
|
||||||
import api from './api';
|
import About from './components/About';
|
||||||
import { API_LOGOUT, API_CONFIG } from './endpoints';
|
|
||||||
|
|
||||||
import HelpDropdown from './components/HelpDropdown';
|
|
||||||
import LogoutButton from './components/LogoutButton';
|
|
||||||
import TowerLogo from './components/TowerLogo';
|
|
||||||
import ConditionalRedirect from './components/ConditionalRedirect';
|
|
||||||
import NavExpandableGroup from './components/NavExpandableGroup';
|
import NavExpandableGroup from './components/NavExpandableGroup';
|
||||||
|
import TowerLogo from './components/TowerLogo';
|
||||||
|
import PageHeaderToolbar from './components/PageHeaderToolbar';
|
||||||
|
import { ConfigContext } from './context';
|
||||||
|
|
||||||
import Applications from './pages/Applications';
|
class App extends Component {
|
||||||
import Credentials from './pages/Credentials';
|
constructor (props) {
|
||||||
import CredentialTypes from './pages/CredentialTypes';
|
|
||||||
import Dashboard from './pages/Dashboard';
|
|
||||||
import InstanceGroups from './pages/InstanceGroups';
|
|
||||||
import Inventories from './pages/Inventories';
|
|
||||||
import InventoryScripts from './pages/InventoryScripts';
|
|
||||||
import Jobs from './pages/Jobs';
|
|
||||||
import Login from './pages/Login';
|
|
||||||
import ManagementJobs from './pages/ManagementJobs';
|
|
||||||
import NotificationTemplates from './pages/NotificationTemplates';
|
|
||||||
import Organizations from './pages/Organizations';
|
|
||||||
import Portal from './pages/Portal';
|
|
||||||
import Projects from './pages/Projects';
|
|
||||||
import Schedules from './pages/Schedules';
|
|
||||||
import AuthSettings from './pages/AuthSettings';
|
|
||||||
import JobsSettings from './pages/JobsSettings';
|
|
||||||
import SystemSettings from './pages/SystemSettings';
|
|
||||||
import UISettings from './pages/UISettings';
|
|
||||||
import License from './pages/License';
|
|
||||||
import Teams from './pages/Teams';
|
|
||||||
import Templates from './pages/Templates';
|
|
||||||
import Users from './pages/Users';
|
|
||||||
|
|
||||||
import ja from '../build/locales/ja/messages';
|
|
||||||
import en from '../build/locales/en/messages';
|
|
||||||
|
|
||||||
const catalogs = { en, ja };
|
|
||||||
|
|
||||||
// This spits out the language and the region. Example: es-US
|
|
||||||
const language = (navigator.languages && navigator.languages[0])
|
|
||||||
|| navigator.language
|
|
||||||
|| navigator.userLanguage;
|
|
||||||
|
|
||||||
const languageWithoutRegionCode = language.toLowerCase().split(/[_-]+/)[0];
|
|
||||||
|
|
||||||
|
|
||||||
class App extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
const isNavOpen = typeof window !== 'undefined' && window.innerWidth >= parseInt(breakpointMd.value, 10);
|
// initialize with a closed navbar if window size is small
|
||||||
this.state = {
|
const isNavOpen = typeof window !== 'undefined'
|
||||||
isNavOpen,
|
&& window.innerWidth >= parseInt(global_breakpoint_md.value, 10);
|
||||||
config: {},
|
|
||||||
error: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
onNavToggle = () => {
|
this.state = {
|
||||||
this.setState(({ isNavOpen }) => ({ isNavOpen: !isNavOpen }));
|
ansible_version: null,
|
||||||
|
custom_virtualenvs: null,
|
||||||
|
isAboutModalOpen: false,
|
||||||
|
isNavOpen,
|
||||||
|
version: null,
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
this.fetchConfig = this.fetchConfig.bind(this);
|
||||||
|
this.onLogout = this.onLogout.bind(this);
|
||||||
|
this.onAboutModalClose = this.onAboutModalClose.bind(this);
|
||||||
|
this.onAboutModalOpen = this.onAboutModalOpen.bind(this);
|
||||||
|
this.onLogoClick = this.onLogoClick.bind(this);
|
||||||
|
this.onNavToggle = this.onNavToggle.bind(this);
|
||||||
};
|
};
|
||||||
|
|
||||||
onLogoClick = () => {
|
componentDidMount () {
|
||||||
this.setState({ activeGroup: 'views_group' });
|
this.fetchConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
onDevLogout = async () => {
|
async fetchConfig () {
|
||||||
await api.get(API_LOGOUT);
|
const { api } = this.props;
|
||||||
this.setState({ activeGroup: 'views_group', activeItem: 'views_group_dashboard' });
|
|
||||||
}
|
|
||||||
|
|
||||||
async componentDidMount() {
|
|
||||||
// Grab our config data from the API and store in state
|
|
||||||
try {
|
try {
|
||||||
const { data } = await api.get(API_CONFIG);
|
const { data: { ansible_version, custom_virtualenvs, version } } = await api.getConfig();
|
||||||
this.setState({ config: data });
|
this.setState({ ansible_version, custom_virtualenvs, version });
|
||||||
} catch (error) {
|
} catch (err) {
|
||||||
this.setState({ error });
|
this.setState({ ansible_version: null, custom_virtualenvs: null, version: null });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
async onLogout () {
|
||||||
const { isNavOpen, config } = this.state;
|
const { api } = this.props;
|
||||||
const { logo, loginInfo, history } = this.props;
|
|
||||||
|
|
||||||
const PageToolbar = (
|
await api.logout();
|
||||||
<Toolbar>
|
window.location.replace('/#/login')
|
||||||
<ToolbarGroup>
|
}
|
||||||
<ToolbarItem>
|
|
||||||
<HelpDropdown />
|
onAboutModalOpen () {
|
||||||
</ToolbarItem>
|
this.setState({ isAboutModalOpen: true });
|
||||||
<ToolbarItem>
|
}
|
||||||
<LogoutButton onDevLogout={() => this.onDevLogout()} />
|
|
||||||
</ToolbarItem>
|
onAboutModalClose () {
|
||||||
</ToolbarGroup>
|
this.setState({ isAboutModalOpen: false });
|
||||||
</Toolbar>
|
}
|
||||||
);
|
|
||||||
|
onNavToggle () {
|
||||||
|
this.setState(({ isNavOpen }) => ({ isNavOpen: !isNavOpen }));
|
||||||
|
}
|
||||||
|
|
||||||
|
onLogoClick () {
|
||||||
|
this.setState({ activeGroup: 'views_group' });
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const {
|
||||||
|
ansible_version,
|
||||||
|
custom_virtualenvs,
|
||||||
|
isAboutModalOpen,
|
||||||
|
isNavOpen,
|
||||||
|
version,
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const {
|
||||||
|
render,
|
||||||
|
routeGroups = [],
|
||||||
|
navLabel = '',
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
ansible_version,
|
||||||
|
custom_virtualenvs,
|
||||||
|
version,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<I18nProvider language={languageWithoutRegionCode} catalogs={catalogs}>
|
<Fragment>
|
||||||
<Fragment>
|
<Page
|
||||||
<BackgroundImage
|
usecondensed="True"
|
||||||
src={{
|
header={(
|
||||||
[BackgroundImageSrc.lg]: '/assets/images/pfbg_1200.jpg',
|
<PageHeader
|
||||||
[BackgroundImageSrc.md]: '/assets/images/pfbg_992.jpg',
|
showNavToggle
|
||||||
[BackgroundImageSrc.md2x]: '/assets/images/pfbg_992@2x.jpg',
|
onNavToggle={this.onNavToggle}
|
||||||
[BackgroundImageSrc.sm]: '/assets/images/pfbg_768.jpg',
|
logo={
|
||||||
[BackgroundImageSrc.sm2x]: '/assets/images/pfbg_768@2x.jpg',
|
<TowerLogo
|
||||||
[BackgroundImageSrc.xl]: '/assets/images/pfbg_2000.jpg',
|
onClick={this.onLogoClick}
|
||||||
[BackgroundImageSrc.xs]: '/assets/images/pfbg_576.jpg',
|
/>
|
||||||
[BackgroundImageSrc.xs2x]: '/assets/images/pfbg_576@2x.jpg',
|
}
|
||||||
[BackgroundImageSrc.filter]: '/assets/images/background-filter.svg'
|
toolbar={
|
||||||
}}
|
<PageHeaderToolbar
|
||||||
/>
|
isAboutDisabled={!version}
|
||||||
|
onAboutClick={this.onAboutModalOpen}
|
||||||
|
onLogoutClick={this.onLogout}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
sidebar={
|
||||||
|
<PageSidebar
|
||||||
|
isNavOpen={isNavOpen}
|
||||||
|
nav={(
|
||||||
|
<Nav aria-label={navLabel}>
|
||||||
|
<NavList>
|
||||||
|
{routeGroups.map(({ groupId, groupTitle, routes }) => (
|
||||||
|
<NavExpandableGroup
|
||||||
|
key={groupId}
|
||||||
|
groupId={groupId}
|
||||||
|
groupTitle={groupTitle}
|
||||||
|
routes={routes}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</NavList>
|
||||||
|
</Nav>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
<ConfigContext.Provider value={config}>
|
<ConfigContext.Provider value={config}>
|
||||||
<Switch>
|
{render && render({ routeGroups })}
|
||||||
<ConditionalRedirect
|
|
||||||
shouldRedirect={() => api.isAuthenticated()}
|
|
||||||
redirectPath="/"
|
|
||||||
path="/login"
|
|
||||||
component={() => <Login logo={logo} loginInfo={loginInfo} />}
|
|
||||||
/>
|
|
||||||
<Fragment>
|
|
||||||
<Page
|
|
||||||
header={(
|
|
||||||
<PageHeader
|
|
||||||
logo={<TowerLogo onClick={this.onLogoClick} />}
|
|
||||||
toolbar={PageToolbar}
|
|
||||||
showNavToggle
|
|
||||||
onNavToggle={this.onNavToggle}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
sidebar={(
|
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
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" 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="/auth_settings" component={AuthSettings} />
|
|
||||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/jobs_settings" component={JobsSettings} />
|
|
||||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/system_settings" component={SystemSettings} />
|
|
||||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/ui_settings" component={UISettings} />
|
|
||||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/license" component={License} />
|
|
||||||
|
|
||||||
</Page>
|
|
||||||
</Fragment>
|
|
||||||
</Switch>
|
|
||||||
</ConfigContext.Provider>
|
</ConfigContext.Provider>
|
||||||
</Fragment>
|
</Page>
|
||||||
</I18nProvider>
|
<About
|
||||||
|
ansible_version={ansible_version}
|
||||||
|
version={version}
|
||||||
|
isOpen={isAboutModalOpen}
|
||||||
|
onClose={this.onAboutModalClose}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withRouter(App);
|
export default App;
|
||||||
|
|||||||
69
src/api.js
69
src/api.js
@@ -1,29 +1,26 @@
|
|||||||
import axios from 'axios';
|
const API_ROOT = '/api/';
|
||||||
|
const API_LOGIN = `${API_ROOT}login/`;
|
||||||
import * as endpoints from './endpoints';
|
const API_LOGOUT = `${API_ROOT}logout/`;
|
||||||
|
const API_V2 = `${API_ROOT}v2/`;
|
||||||
const CSRF_COOKIE_NAME = 'csrftoken';
|
const API_CONFIG = `${API_V2}config/`;
|
||||||
const CSRF_HEADER_NAME = 'X-CSRFToken';
|
const API_ORGANIZATIONS = `${API_V2}organizations/`;
|
||||||
|
|
||||||
const LOGIN_CONTENT_TYPE = 'application/x-www-form-urlencoded';
|
const LOGIN_CONTENT_TYPE = 'application/x-www-form-urlencoded';
|
||||||
|
|
||||||
class APIClient {
|
class APIClient {
|
||||||
constructor () {
|
static getCookie () {
|
||||||
this.http = axios.create({
|
|
||||||
xsrfCookieName: CSRF_COOKIE_NAME,
|
|
||||||
xsrfHeaderName: CSRF_HEADER_NAME,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/* eslint class-methods-use-this: ["error", { "exceptMethods": ["getCookie"] }] */
|
|
||||||
getCookie () {
|
|
||||||
return document.cookie;
|
return document.cookie;
|
||||||
}
|
}
|
||||||
|
|
||||||
isAuthenticated () {
|
constructor (httpAdapter) {
|
||||||
let authenticated = false;
|
this.http = httpAdapter;
|
||||||
|
}
|
||||||
|
|
||||||
const parsed = (`; ${this.getCookie()}`).split('; userLoggedIn=');
|
isAuthenticated () {
|
||||||
|
const cookie = this.constructor.getCookie();
|
||||||
|
const parsed = (`; ${cookie}`).split('; userLoggedIn=');
|
||||||
|
|
||||||
|
let authenticated = false;
|
||||||
|
|
||||||
if (parsed.length === 2) {
|
if (parsed.length === 2) {
|
||||||
authenticated = parsed.pop().split(';').shift() === 'true';
|
authenticated = parsed.pop().split(';').shift() === 'true';
|
||||||
@@ -32,7 +29,7 @@ class APIClient {
|
|||||||
return authenticated;
|
return authenticated;
|
||||||
}
|
}
|
||||||
|
|
||||||
async login (username, password, redirect = endpoints.API_CONFIG) {
|
async login (username, password, redirect = API_CONFIG) {
|
||||||
const un = encodeURIComponent(username);
|
const un = encodeURIComponent(username);
|
||||||
const pw = encodeURIComponent(password);
|
const pw = encodeURIComponent(password);
|
||||||
const next = encodeURIComponent(redirect);
|
const next = encodeURIComponent(redirect);
|
||||||
@@ -40,13 +37,37 @@ class APIClient {
|
|||||||
const data = `username=${un}&password=${pw}&next=${next}`;
|
const data = `username=${un}&password=${pw}&next=${next}`;
|
||||||
const headers = { 'Content-Type': LOGIN_CONTENT_TYPE };
|
const headers = { 'Content-Type': LOGIN_CONTENT_TYPE };
|
||||||
|
|
||||||
await this.http.get(endpoints.API_LOGIN, { headers });
|
await this.http.get(API_LOGIN, { headers });
|
||||||
await this.http.post(endpoints.API_LOGIN, data, { headers });
|
const response = await this.http.post(API_LOGIN, data, { headers });
|
||||||
|
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
get = (endpoint, params = {}) => this.http.get(endpoint, { params });
|
logout () {
|
||||||
|
return this.http.get(API_LOGOUT);
|
||||||
|
}
|
||||||
|
|
||||||
post = (endpoint, data) => this.http.post(endpoint, data);
|
getRoot () {
|
||||||
|
return this.http.get(API_ROOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
getConfig () {
|
||||||
|
return this.http.get(API_CONFIG);
|
||||||
|
}
|
||||||
|
|
||||||
|
getOrganizations (params = {}) {
|
||||||
|
return this.http.get(API_ORGANIZATIONS, { params });
|
||||||
|
}
|
||||||
|
|
||||||
|
createOrganization (data) {
|
||||||
|
return this.http.post(API_ORGANIZATIONS, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
getOrganizationDetails (id) {
|
||||||
|
const endpoint = `${API_ORGANIZATIONS}${id}/`;
|
||||||
|
|
||||||
|
return this.http.get(endpoint);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new APIClient();
|
export default APIClient;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { Component } 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 {
|
||||||
@@ -13,10 +12,8 @@ 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 { ConfigContext } from '../context';
|
class About extends Component {
|
||||||
|
static createSpeechBubble (version) {
|
||||||
class About extends React.Component {
|
|
||||||
createSpeechBubble = (version) => {
|
|
||||||
let text = `Tower ${version}`;
|
let text = `Tower ${version}`;
|
||||||
let top = '';
|
let top = '';
|
||||||
let bottom = '';
|
let bottom = '';
|
||||||
@@ -33,61 +30,60 @@ class About extends React.Component {
|
|||||||
return top + text + bottom;
|
return top + text + bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleModalToggle = () => {
|
constructor (props) {
|
||||||
const { onAboutModalClose } = this.props;
|
super(props);
|
||||||
onAboutModalClose();
|
|
||||||
};
|
this.createSpeechBubble = this.constructor.createSpeechBubble.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { isOpen } = this.props;
|
const {
|
||||||
|
ansible_version,
|
||||||
|
version,
|
||||||
|
isOpen,
|
||||||
|
onClose
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const speechBubble = this.createSpeechBubble(version);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<I18n>
|
<I18n>
|
||||||
{({ i18n }) => (
|
{({ i18n }) => (
|
||||||
<ConfigContext.Consumer>
|
<AboutModal
|
||||||
{({ ansible_version, version }) => (
|
isOpen={isOpen}
|
||||||
<AboutModal
|
onClose={onClose}
|
||||||
isOpen={isOpen}
|
productName="Ansible Tower"
|
||||||
onClose={this.handleModalToggle}
|
trademark={i18n._(t`Copyright 2018 Red Hat, Inc.`)}
|
||||||
productName="Ansible Tower"
|
brandImageSrc={brandImg}
|
||||||
trademark={i18n._(t`Copyright 2018 Red Hat, Inc.`)}
|
brandImageAlt={i18n._(t`Brand Image`)}
|
||||||
brandImageSrc={brandImg}
|
logoImageSrc={logoImg}
|
||||||
brandImageAlt={i18n._(t`Brand Image`)}
|
logoImageAlt={i18n._(t`AboutModal Logo`)}
|
||||||
logoImageSrc={logoImg}
|
heroImageSrc={heroImg}
|
||||||
logoImageAlt={i18n._(t`AboutModal Logo`)}
|
>
|
||||||
heroImageSrc={heroImg}
|
<pre>
|
||||||
>
|
{ speechBubble }
|
||||||
<pre>
|
{`
|
||||||
{this.createSpeechBubble(version)}
|
|
||||||
{`
|
|
||||||
\\
|
\\
|
||||||
\\ ^__^
|
\\ ^__^
|
||||||
(oo)\\_______
|
(oo)\\_______
|
||||||
(__) A )\\
|
(__) A )\\
|
||||||
||----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>
|
</AboutModal>
|
||||||
</AboutModal>
|
|
||||||
)}
|
|
||||||
</ConfigContext.Consumer>
|
|
||||||
)}
|
)}
|
||||||
</I18n>
|
</I18n>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
About.contextTypes = {
|
|
||||||
ansible_version: PropTypes.string,
|
|
||||||
version: PropTypes.string,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default About;
|
export default About;
|
||||||
|
|||||||
25
src/components/Background.jsx
Normal file
25
src/components/Background.jsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import React, { Fragment } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
BackgroundImage,
|
||||||
|
BackgroundImageSrc,
|
||||||
|
} from '@patternfly/react-core';
|
||||||
|
|
||||||
|
const backgroundImageConfig = {
|
||||||
|
[BackgroundImageSrc.lg]: '/assets/images/pfbg_1200.jpg',
|
||||||
|
[BackgroundImageSrc.md]: '/assets/images/pfbg_992.jpg',
|
||||||
|
[BackgroundImageSrc.md2x]: '/assets/images/pfbg_992@2x.jpg',
|
||||||
|
[BackgroundImageSrc.sm]: '/assets/images/pfbg_768.jpg',
|
||||||
|
[BackgroundImageSrc.sm2x]: '/assets/images/pfbg_768@2x.jpg',
|
||||||
|
[BackgroundImageSrc.xl]: '/assets/images/pfbg_2000.jpg',
|
||||||
|
[BackgroundImageSrc.xs]: '/assets/images/pfbg_576.jpg',
|
||||||
|
[BackgroundImageSrc.xs2x]: '/assets/images/pfbg_576@2x.jpg',
|
||||||
|
[BackgroundImageSrc.filter]: '/assets/images/background-filter.svg',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ({ children }) => (
|
||||||
|
<Fragment>
|
||||||
|
<BackgroundImage src={backgroundImageConfig} />
|
||||||
|
{ children }
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -43,17 +43,24 @@ class DataListToolbar extends React.Component {
|
|||||||
searchKey: sortedColumnKey,
|
searchKey: sortedColumnKey,
|
||||||
searchValue: '',
|
searchValue: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.handleSearchInputChange = this.handleSearchInputChange.bind(this);
|
||||||
|
this.onSortDropdownToggle = this.onSortDropdownToggle.bind(this);
|
||||||
|
this.onSortDropdownSelect = this.onSortDropdownSelect.bind(this);
|
||||||
|
this.onSearch = this.onSearch.bind(this);
|
||||||
|
this.onSearchDropdownToggle = this.onSearchDropdownToggle.bind(this);
|
||||||
|
this.onSearchDropdownSelect = this.onSearchDropdownSelect.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSearchInputChange = searchValue => {
|
handleSearchInputChange (searchValue) {
|
||||||
this.setState({ searchValue });
|
this.setState({ searchValue });
|
||||||
};
|
}
|
||||||
|
|
||||||
onSortDropdownToggle = isSortDropdownOpen => {
|
onSortDropdownToggle (isSortDropdownOpen) {
|
||||||
this.setState({ isSortDropdownOpen });
|
this.setState({ isSortDropdownOpen });
|
||||||
};
|
}
|
||||||
|
|
||||||
onSortDropdownSelect = ({ target }) => {
|
onSortDropdownSelect ({ target }) {
|
||||||
const { columns, onSort, sortOrder } = this.props;
|
const { columns, onSort, sortOrder } = this.props;
|
||||||
|
|
||||||
const [{ key }] = columns.filter(({ name }) => name === target.innerText);
|
const [{ key }] = columns.filter(({ name }) => name === target.innerText);
|
||||||
@@ -61,27 +68,33 @@ class DataListToolbar extends React.Component {
|
|||||||
this.setState({ isSortDropdownOpen: false });
|
this.setState({ isSortDropdownOpen: false });
|
||||||
|
|
||||||
onSort(key, sortOrder);
|
onSort(key, sortOrder);
|
||||||
};
|
}
|
||||||
|
|
||||||
onSearchDropdownToggle = isSearchDropdownOpen => {
|
onSearchDropdownToggle (isSearchDropdownOpen) {
|
||||||
this.setState({ isSearchDropdownOpen });
|
this.setState({ isSearchDropdownOpen });
|
||||||
};
|
}
|
||||||
|
|
||||||
onSearchDropdownSelect = ({ target }) => {
|
onSearchDropdownSelect ({ target }) {
|
||||||
const { columns } = this.props;
|
const { columns } = this.props;
|
||||||
|
|
||||||
const targetName = target.innerText;
|
const targetName = target.innerText;
|
||||||
const [{ key }] = columns.filter(({ name }) => name === targetName);
|
const [{ key }] = columns.filter(({ name }) => name === targetName);
|
||||||
|
|
||||||
this.setState({ isSearchDropdownOpen: false, searchKey: key });
|
this.setState({ isSearchDropdownOpen: false, searchKey: key });
|
||||||
};
|
}
|
||||||
|
|
||||||
|
onSearch () {
|
||||||
|
const { searchValue } = this.state;
|
||||||
|
const { onSearch } = this.props;
|
||||||
|
|
||||||
|
onSearch(searchValue);
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { up } = DropdownPosition;
|
const { up } = DropdownPosition;
|
||||||
const {
|
const {
|
||||||
columns,
|
columns,
|
||||||
isAllSelected,
|
isAllSelected,
|
||||||
onSearch,
|
|
||||||
onSelectAll,
|
onSelectAll,
|
||||||
onSort,
|
onSort,
|
||||||
sortedColumnKey,
|
sortedColumnKey,
|
||||||
@@ -175,7 +188,7 @@ class DataListToolbar extends React.Component {
|
|||||||
<Button
|
<Button
|
||||||
variant="tertiary"
|
variant="tertiary"
|
||||||
aria-label={i18n._(t`Search`)}
|
aria-label={i18n._(t`Search`)}
|
||||||
onClick={() => onSearch(searchValue)}
|
onClick={this.onSearch}
|
||||||
>
|
>
|
||||||
<i className="fas fa-search" aria-hidden="true" />
|
<i className="fas fa-search" aria-hidden="true" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
import React, { Component, Fragment } from 'react';
|
|
||||||
import { Trans } from '@lingui/macro';
|
|
||||||
import {
|
|
||||||
Dropdown,
|
|
||||||
DropdownItem,
|
|
||||||
DropdownToggle,
|
|
||||||
DropdownPosition,
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
import { QuestionCircleIcon } from '@patternfly/react-icons';
|
|
||||||
import AboutModal from './About';
|
|
||||||
|
|
||||||
class HelpDropdown extends Component {
|
|
||||||
state = {
|
|
||||||
isOpen: false,
|
|
||||||
showAboutModal: false
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { isOpen, showAboutModal } = this.state;
|
|
||||||
const dropdownItems = [
|
|
||||||
<DropdownItem
|
|
||||||
href="https://docs.ansible.com/ansible-tower/latest/html/userguide/index.html"
|
|
||||||
target="_blank"
|
|
||||||
key="help"
|
|
||||||
>
|
|
||||||
<Trans>Help</Trans>
|
|
||||||
</DropdownItem>,
|
|
||||||
<DropdownItem
|
|
||||||
onClick={() => this.setState({ showAboutModal: true })}
|
|
||||||
key="about"
|
|
||||||
>
|
|
||||||
<Trans>About</Trans>
|
|
||||||
</DropdownItem>
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<Dropdown
|
|
||||||
onSelect={() => this.setState({ isOpen: !isOpen })}
|
|
||||||
toggle={(
|
|
||||||
<DropdownToggle onToggle={(isToggleOpen) => this.setState({ isOpen: isToggleOpen })}>
|
|
||||||
<QuestionCircleIcon />
|
|
||||||
</DropdownToggle>
|
|
||||||
)}
|
|
||||||
isOpen={isOpen}
|
|
||||||
dropdownItems={dropdownItems}
|
|
||||||
position={DropdownPosition.right}
|
|
||||||
/>
|
|
||||||
{showAboutModal
|
|
||||||
? (
|
|
||||||
<AboutModal
|
|
||||||
isOpen={showAboutModal}
|
|
||||||
onAboutModalClose={() => this.setState({ showAboutModal: !showAboutModal })}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
: null }
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default HelpDropdown;
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { I18n } from '@lingui/react';
|
|
||||||
import { t } from '@lingui/macro';
|
|
||||||
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
ButtonVariant
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
|
|
||||||
import { UserIcon } from '@patternfly/react-icons';
|
|
||||||
|
|
||||||
const LogoutButton = ({ onDevLogout }) => (
|
|
||||||
<I18n>
|
|
||||||
{({ i18n }) => (
|
|
||||||
<Button
|
|
||||||
id="button-logout"
|
|
||||||
aria-label={i18n._(t`Logout`)}
|
|
||||||
variant={ButtonVariant.plain}
|
|
||||||
onClick={onDevLogout}
|
|
||||||
onKeyDown={event => {
|
|
||||||
if (event.keyCode === 13) {
|
|
||||||
onDevLogout();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<UserIcon />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</I18n>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default LogoutButton;
|
|
||||||
@@ -14,18 +14,23 @@ class NavExpandableGroup extends Component {
|
|||||||
// Extract a list of paths from the route params and store them for later. This creates
|
// Extract a list of paths from the route params and store them for later. This creates
|
||||||
// an array of url paths associated with any NavItem component rendered by this component.
|
// an array of url paths associated with any NavItem component rendered by this component.
|
||||||
this.navItemPaths = routes.map(({ path }) => path);
|
this.navItemPaths = routes.map(({ path }) => path);
|
||||||
|
|
||||||
|
this.isActiveGroup = this.isActiveGroup.bind(this);
|
||||||
|
this.isActivePath = this.isActivePath.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
isActiveGroup = () => this.navItemPaths.some(this.isActivePath);
|
isActiveGroup () {
|
||||||
|
return this.navItemPaths.some(this.isActivePath);
|
||||||
|
}
|
||||||
|
|
||||||
isActivePath = (path) => {
|
isActivePath (path) {
|
||||||
const { history } = this.props;
|
const { history } = this.props;
|
||||||
|
|
||||||
return history.location.pathname.startsWith(path);
|
return history.location.pathname.startsWith(path);
|
||||||
};
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { routes, groupId, staticContext, ...rest } = this.props;
|
const { groupId, groupTitle, routes } = this.props;
|
||||||
const isActive = this.isActiveGroup();
|
const isActive = this.isActiveGroup();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -33,7 +38,7 @@ class NavExpandableGroup extends Component {
|
|||||||
isActive={isActive}
|
isActive={isActive}
|
||||||
isExpanded={isActive}
|
isExpanded={isActive}
|
||||||
groupId={groupId}
|
groupId={groupId}
|
||||||
{...rest}
|
title={groupTitle}
|
||||||
>
|
>
|
||||||
{routes.map(({ path, title }) => (
|
{routes.map(({ path, title }) => (
|
||||||
<NavItem
|
<NavItem
|
||||||
|
|||||||
140
src/components/PageHeaderToolbar.jsx
Normal file
140
src/components/PageHeaderToolbar.jsx
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { I18n } from '@lingui/react';
|
||||||
|
import {
|
||||||
|
Dropdown,
|
||||||
|
DropdownItem,
|
||||||
|
DropdownToggle,
|
||||||
|
DropdownPosition,
|
||||||
|
Toolbar,
|
||||||
|
ToolbarGroup,
|
||||||
|
ToolbarItem,
|
||||||
|
} from '@patternfly/react-core';
|
||||||
|
import {
|
||||||
|
QuestionCircleIcon,
|
||||||
|
UserIcon,
|
||||||
|
} from '@patternfly/react-icons';
|
||||||
|
|
||||||
|
const DOCLINK = 'https://docs.ansible.com/ansible-tower/latest/html/userguide/index.html';
|
||||||
|
const KEY_ENTER = 13;
|
||||||
|
|
||||||
|
class PageHeaderToolbar extends Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props);
|
||||||
|
this.state = { isHelpOpen: false, isUserOpen: false };
|
||||||
|
|
||||||
|
this.onHelpSelect = this.onHelpSelect.bind(this);
|
||||||
|
this.onHelpToggle = this.onHelpToggle.bind(this);
|
||||||
|
this.onLogoutKeyDown = this.onLogoutKeyDown.bind(this);
|
||||||
|
this.onUserSelect = this.onUserSelect.bind(this);
|
||||||
|
this.onUserToggle = this.onUserToggle.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
onLogoutKeyDown ({ keyCode }) {
|
||||||
|
const { onLogoutClick } = this.props;
|
||||||
|
|
||||||
|
if (keyCode === KEY_ENTER) {
|
||||||
|
onLogoutClick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onHelpSelect () {
|
||||||
|
const { isHelpOpen } = this.state;
|
||||||
|
|
||||||
|
this.setState({ isHelpOpen: !isHelpOpen });
|
||||||
|
}
|
||||||
|
|
||||||
|
onUserSelect () {
|
||||||
|
const { isUserOpen } = this.state;
|
||||||
|
|
||||||
|
this.setState({ isUserOpen: !isUserOpen });
|
||||||
|
}
|
||||||
|
|
||||||
|
onHelpToggle (isOpen) {
|
||||||
|
this.setState({ isHelpOpen: isOpen });
|
||||||
|
}
|
||||||
|
|
||||||
|
onUserToggle (isOpen) {
|
||||||
|
this.setState({ isUserOpen: isOpen });
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { isHelpOpen, isUserOpen } = this.state;
|
||||||
|
const { isAboutDisabled, onAboutClick, onLogoutClick } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<I18n>
|
||||||
|
{({ i18n }) => (
|
||||||
|
<Toolbar>
|
||||||
|
<ToolbarGroup>
|
||||||
|
<ToolbarItem>
|
||||||
|
<Dropdown
|
||||||
|
isOpen={isHelpOpen}
|
||||||
|
position={DropdownPosition.right}
|
||||||
|
onSelect={this.onHelpSelect}
|
||||||
|
toggle={(
|
||||||
|
<DropdownToggle
|
||||||
|
onToggle={this.onHelpToggle}
|
||||||
|
>
|
||||||
|
<QuestionCircleIcon />
|
||||||
|
</DropdownToggle>
|
||||||
|
)}
|
||||||
|
dropdownItems={[
|
||||||
|
<DropdownItem
|
||||||
|
key="help"
|
||||||
|
target="_blank"
|
||||||
|
href={DOCLINK}
|
||||||
|
>
|
||||||
|
{i18n._(t`Help`)}
|
||||||
|
</DropdownItem>,
|
||||||
|
<DropdownItem
|
||||||
|
key="about"
|
||||||
|
component="button"
|
||||||
|
isDisabled={isAboutDisabled}
|
||||||
|
onClick={onAboutClick}
|
||||||
|
>
|
||||||
|
{i18n._(t`About`)}
|
||||||
|
</DropdownItem>
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</ToolbarItem>
|
||||||
|
<ToolbarItem>
|
||||||
|
<Dropdown
|
||||||
|
isOpen={isUserOpen}
|
||||||
|
position={DropdownPosition.right}
|
||||||
|
onSelect={this.onUserSelect}
|
||||||
|
toggle={(
|
||||||
|
<DropdownToggle
|
||||||
|
onToggle={this.onUserToggle}
|
||||||
|
>
|
||||||
|
<UserIcon />
|
||||||
|
</DropdownToggle>
|
||||||
|
)}
|
||||||
|
dropdownItems={[
|
||||||
|
<DropdownItem key="user">
|
||||||
|
<Link to="/home">
|
||||||
|
{i18n._(t`User Details`)}
|
||||||
|
</Link>
|
||||||
|
</DropdownItem>,
|
||||||
|
<DropdownItem
|
||||||
|
key="logout"
|
||||||
|
component="button"
|
||||||
|
onClick={onLogoutClick}
|
||||||
|
onKeyDown={this.onLogoutKeyDown}
|
||||||
|
>
|
||||||
|
{i18n._(t`Logout`)}
|
||||||
|
</DropdownItem>
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</ToolbarItem>
|
||||||
|
</ToolbarGroup>
|
||||||
|
</Toolbar>
|
||||||
|
)}
|
||||||
|
</I18n>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PageHeaderToolbar;
|
||||||
@@ -21,6 +21,15 @@ class Pagination extends Component {
|
|||||||
const { page } = this.props;
|
const { page } = this.props;
|
||||||
|
|
||||||
this.state = { value: page, isOpen: false };
|
this.state = { value: page, isOpen: false };
|
||||||
|
|
||||||
|
this.onPageChange = this.onPageChange.bind(this);
|
||||||
|
this.onSubmit = this.onSubmit.bind(this);
|
||||||
|
this.onFirst = this.onFirst.bind(this);
|
||||||
|
this.onPrevious = this.onPrevious.bind(this);
|
||||||
|
this.onNext = this.onNext.bind(this);
|
||||||
|
this.onLast = this.onLast.bind(this);
|
||||||
|
this.onTogglePageSize = this.onTogglePageSize.bind(this);
|
||||||
|
this.onSelectPageSize = this.onSelectPageSize.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate (prevProps) {
|
componentDidUpdate (prevProps) {
|
||||||
@@ -31,11 +40,11 @@ class Pagination extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onPageChange = value => {
|
onPageChange (value) {
|
||||||
this.setState({ value });
|
this.setState({ value });
|
||||||
};
|
}
|
||||||
|
|
||||||
onSubmit = event => {
|
onSubmit (event) {
|
||||||
const { onSetPage, page, pageCount, page_size } = this.props;
|
const { onSetPage, page, pageCount, page_size } = this.props;
|
||||||
const { value } = this.state;
|
const { value } = this.state;
|
||||||
|
|
||||||
@@ -49,43 +58,43 @@ class Pagination extends Component {
|
|||||||
} else {
|
} else {
|
||||||
this.setState({ value: page });
|
this.setState({ value: page });
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
onFirst = () => {
|
onFirst () {
|
||||||
const { onSetPage, page_size } = this.props;
|
const { onSetPage, page_size } = this.props;
|
||||||
|
|
||||||
onSetPage(1, page_size);
|
onSetPage(1, page_size);
|
||||||
};
|
}
|
||||||
|
|
||||||
onPrevious = () => {
|
onPrevious () {
|
||||||
const { onSetPage, page, page_size } = this.props;
|
const { onSetPage, page, page_size } = this.props;
|
||||||
const previousPage = page - 1;
|
const previousPage = page - 1;
|
||||||
|
|
||||||
if (previousPage >= 1) {
|
if (previousPage >= 1) {
|
||||||
onSetPage(previousPage, page_size);
|
onSetPage(previousPage, page_size);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
onNext = () => {
|
onNext () {
|
||||||
const { onSetPage, page, pageCount, page_size } = this.props;
|
const { onSetPage, page, pageCount, page_size } = this.props;
|
||||||
const nextPage = page + 1;
|
const nextPage = page + 1;
|
||||||
|
|
||||||
if (nextPage <= pageCount) {
|
if (nextPage <= pageCount) {
|
||||||
onSetPage(nextPage, page_size);
|
onSetPage(nextPage, page_size);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
onLast = () => {
|
onLast () {
|
||||||
const { onSetPage, pageCount, page_size } = this.props;
|
const { onSetPage, pageCount, page_size } = this.props;
|
||||||
|
|
||||||
onSetPage(pageCount, page_size)
|
onSetPage(pageCount, page_size)
|
||||||
};
|
}
|
||||||
|
|
||||||
onTogglePageSize = isOpen => {
|
onTogglePageSize (isOpen) {
|
||||||
this.setState({ isOpen });
|
this.setState({ isOpen });
|
||||||
};
|
}
|
||||||
|
|
||||||
onSelectPageSize = ({ target }) => {
|
onSelectPageSize ({ target }) {
|
||||||
const { onSetPage } = this.props;
|
const { onSetPage } = this.props;
|
||||||
|
|
||||||
const page = 1;
|
const page = 1;
|
||||||
@@ -94,7 +103,7 @@ class Pagination extends Component {
|
|||||||
this.setState({ isOpen: false });
|
this.setState({ isOpen: false });
|
||||||
|
|
||||||
onSetPage(page, page_size);
|
onSetPage(page, page_size);
|
||||||
};
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { up } = DropdownDirection;
|
const { up } = DropdownDirection;
|
||||||
|
|||||||
@@ -12,18 +12,26 @@ class TowerLogo extends Component {
|
|||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = { hover: false };
|
this.state = { hover: false };
|
||||||
|
|
||||||
|
this.onClick = this.onClick.bind(this);
|
||||||
|
this.onHover = this.onHover.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
onClick = () => {
|
onClick () {
|
||||||
const { history } = this.props;
|
const { history, onClick: handleClick } = this.props;
|
||||||
history.push('/');
|
|
||||||
};
|
|
||||||
|
|
||||||
onHover = () => {
|
if (!handleClick) return;
|
||||||
|
|
||||||
|
history.push('/');
|
||||||
|
|
||||||
|
handleClick();
|
||||||
|
}
|
||||||
|
|
||||||
|
onHover () {
|
||||||
const { hover } = this.state;
|
const { hover } = this.state;
|
||||||
|
|
||||||
this.setState({ hover: !hover });
|
this.setState({ hover: !hover });
|
||||||
};
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { hover } = this.state;
|
const { hover } = this.state;
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
export const API_ROOT = '/api/';
|
|
||||||
export const API_LOGIN = `${API_ROOT}login/`;
|
|
||||||
export const API_LOGOUT = `${API_ROOT}logout/`;
|
|
||||||
export const API_V2 = `${API_ROOT}v2/`;
|
|
||||||
export const API_CONFIG = `${API_V2}config/`;
|
|
||||||
export const API_PROJECTS = `${API_V2}projects/`;
|
|
||||||
export const API_ORGANIZATIONS = `${API_V2}organizations/`;
|
|
||||||
283
src/index.jsx
283
src/index.jsx
@@ -1,27 +1,282 @@
|
|||||||
|
import axios from 'axios';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render } from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
HashRouter as Router
|
HashRouter,
|
||||||
|
Redirect,
|
||||||
|
Route,
|
||||||
|
Switch,
|
||||||
} from 'react-router-dom';
|
} from 'react-router-dom';
|
||||||
import App from './App';
|
import {
|
||||||
import api from './api';
|
I18n,
|
||||||
import { API_ROOT } from './endpoints';
|
I18nProvider,
|
||||||
|
} from '@lingui/react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
|
||||||
import '@patternfly/react-core/dist/styles/base.css';
|
import '@patternfly/react-core/dist/styles/base.css';
|
||||||
import '@patternfly/patternfly-next/patternfly.css';
|
import '@patternfly/patternfly-next/patternfly.css';
|
||||||
|
|
||||||
import './app.scss';
|
import './app.scss';
|
||||||
import './components/Pagination/styles.scss';
|
import './components/Pagination/styles.scss';
|
||||||
import './components/DataListToolbar/styles.scss';
|
import './components/DataListToolbar/styles.scss';
|
||||||
|
|
||||||
const el = document.getElementById('app');
|
import APIClient from './api';
|
||||||
|
|
||||||
const main = async () => {
|
import App from './App';
|
||||||
const { custom_logo, custom_login_info } = await api.get(API_ROOT);
|
import Background from './components/Background';
|
||||||
render(<Router><App logo={custom_logo} loginInfo={custom_login_info} /></Router>, el);
|
import Applications from './pages/Applications';
|
||||||
|
import Credentials from './pages/Credentials';
|
||||||
|
import CredentialTypes from './pages/CredentialTypes';
|
||||||
|
import Dashboard from './pages/Dashboard';
|
||||||
|
import InstanceGroups from './pages/InstanceGroups';
|
||||||
|
import Inventories from './pages/Inventories';
|
||||||
|
import InventoryScripts from './pages/InventoryScripts';
|
||||||
|
import Jobs from './pages/Jobs';
|
||||||
|
import Login from './pages/Login';
|
||||||
|
import ManagementJobs from './pages/ManagementJobs';
|
||||||
|
import NotificationTemplates from './pages/NotificationTemplates';
|
||||||
|
import Organizations from './pages/Organizations';
|
||||||
|
import Portal from './pages/Portal';
|
||||||
|
import Projects from './pages/Projects';
|
||||||
|
import Schedules from './pages/Schedules';
|
||||||
|
import AuthSettings from './pages/AuthSettings';
|
||||||
|
import JobsSettings from './pages/JobsSettings';
|
||||||
|
import SystemSettings from './pages/SystemSettings';
|
||||||
|
import UISettings from './pages/UISettings';
|
||||||
|
import License from './pages/License';
|
||||||
|
import Teams from './pages/Teams';
|
||||||
|
import Templates from './pages/Templates';
|
||||||
|
import Users from './pages/Users';
|
||||||
|
import ja from '../build/locales/ja/messages';
|
||||||
|
import en from '../build/locales/en/messages';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Initialize http
|
||||||
|
//
|
||||||
|
|
||||||
|
const http = axios.create({ xsrfCookieName: 'csrftoken', xsrfHeaderName: 'X-CSRFToken' });
|
||||||
|
|
||||||
|
//
|
||||||
|
// Derive the language and region from global user agent data. Example: es-US
|
||||||
|
// see: https://developer.mozilla.org/en-US/docs/Web/API/Navigator
|
||||||
|
//
|
||||||
|
|
||||||
|
const language = (navigator.languages && navigator.languages[0])
|
||||||
|
|| navigator.language
|
||||||
|
|| navigator.userLanguage;
|
||||||
|
const languageWithoutRegionCode = language.toLowerCase().split(/[_-]+/)[0];
|
||||||
|
const catalogs = { en, ja };
|
||||||
|
|
||||||
|
//
|
||||||
|
// Function Main
|
||||||
|
//
|
||||||
|
|
||||||
|
export async function main (render, api) {
|
||||||
|
const el = document.getElementById('app');
|
||||||
|
// fetch additional config from server
|
||||||
|
const { data: { custom_logo, custom_login_info } } = await api.getRoot();
|
||||||
|
|
||||||
|
const loginRoutes = (
|
||||||
|
<Switch>
|
||||||
|
<Route
|
||||||
|
path="/login"
|
||||||
|
render={() => (
|
||||||
|
<Login
|
||||||
|
api={api}
|
||||||
|
logo={custom_logo}
|
||||||
|
loginInfo={custom_login_info}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Redirect to="/login" />
|
||||||
|
</Switch>
|
||||||
|
);
|
||||||
|
|
||||||
|
return render(
|
||||||
|
<HashRouter>
|
||||||
|
<I18nProvider
|
||||||
|
language={languageWithoutRegionCode}
|
||||||
|
catalogs={catalogs}
|
||||||
|
>
|
||||||
|
<I18n>
|
||||||
|
{({ i18n }) => (
|
||||||
|
<Background>
|
||||||
|
{!api.isAuthenticated() ? loginRoutes : (
|
||||||
|
<Switch>
|
||||||
|
<Route path="/login" render={() => (<Redirect to="/home" />)} />
|
||||||
|
<Route exact path="/" render={() => (<Redirect to="/home" />)} />
|
||||||
|
<Route
|
||||||
|
render={() => (
|
||||||
|
<App
|
||||||
|
api={api}
|
||||||
|
navLabel={i18n._(t`Primary Navigation`)}
|
||||||
|
routeGroups={[
|
||||||
|
{
|
||||||
|
groupTitle: i18n._(t`Views`),
|
||||||
|
groupId: 'views_group',
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
title: i18n._(t`Dashboard`),
|
||||||
|
path: '/home',
|
||||||
|
component: Dashboard
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: i18n._(t`Jobs`),
|
||||||
|
path: '/jobs',
|
||||||
|
component: Jobs
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: i18n._(t`Schedules`),
|
||||||
|
path: '/schedules',
|
||||||
|
component: Schedules
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: i18n._(t`Portal Mode`),
|
||||||
|
path: '/portal',
|
||||||
|
component: Portal
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
groupTitle: i18n._(t`Resources`),
|
||||||
|
groupId: 'resources_group',
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
title: i18n._(t`Templates`),
|
||||||
|
path: '/templates',
|
||||||
|
component: Templates
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: i18n._(t`Credentials`),
|
||||||
|
path: '/credentials',
|
||||||
|
component: Credentials
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: i18n._(t`Projects`),
|
||||||
|
path: '/projects',
|
||||||
|
component: Projects
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: i18n._(t`Inventories`),
|
||||||
|
path: '/inventories',
|
||||||
|
component: Inventories
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: i18n._(t`Inventory Scripts`),
|
||||||
|
path: '/inventory_scripts',
|
||||||
|
component: InventoryScripts
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
groupTitle: i18n._(t`Access`),
|
||||||
|
groupId: 'access_group',
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
title: i18n._(t`Organizations`),
|
||||||
|
path: '/organizations',
|
||||||
|
component: Organizations
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: i18n._(t`Users`),
|
||||||
|
path: '/users',
|
||||||
|
component: Users
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: i18n._(t`Teams`),
|
||||||
|
path: '/teams',
|
||||||
|
component: Teams
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
groupTitle: i18n._(t`Administration`),
|
||||||
|
groupId: 'administration_group',
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
title: i18n._(t`Credential Types`),
|
||||||
|
path: '/credential_types',
|
||||||
|
component: CredentialTypes
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: i18n._(t`Notifications`),
|
||||||
|
path: '/notification_templates',
|
||||||
|
component: NotificationTemplates
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: i18n._(t`Management Jobs`),
|
||||||
|
path: '/management_jobs',
|
||||||
|
component: ManagementJobs
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: i18n._(t`Instance Groups`),
|
||||||
|
path: '/instance_groups',
|
||||||
|
component: InstanceGroups
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: i18n._(t`Integrations`),
|
||||||
|
path: '/applications',
|
||||||
|
component: Applications
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
groupTitle: i18n._(t`Settings`),
|
||||||
|
groupId: 'settings_group',
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
title: i18n._(t`Authentication`),
|
||||||
|
path: '/auth_settings',
|
||||||
|
component: AuthSettings
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: i18n._(t`Jobs`),
|
||||||
|
path: '/jobs_settings',
|
||||||
|
component: JobsSettings
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: i18n._(t`System`),
|
||||||
|
path: '/system_settings',
|
||||||
|
component: SystemSettings
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: i18n._(t`User Interface`),
|
||||||
|
path: '/ui_settings',
|
||||||
|
component: UISettings
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: i18n._(t`License`),
|
||||||
|
path: '/license',
|
||||||
|
component: License
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
render={({ routeGroups }) => (
|
||||||
|
routeGroups
|
||||||
|
.reduce((allRoutes, { routes }) => allRoutes.concat(routes), [])
|
||||||
|
.map(({ component: PageComponent, path }) => (
|
||||||
|
<Route
|
||||||
|
key={path}
|
||||||
|
path={path}
|
||||||
|
render={({ match }) => (
|
||||||
|
<PageComponent
|
||||||
|
api={api}
|
||||||
|
match={match}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Switch>
|
||||||
|
)}
|
||||||
|
</Background>
|
||||||
|
)}
|
||||||
|
</I18n>
|
||||||
|
</I18nProvider>
|
||||||
|
</HashRouter>, el);
|
||||||
};
|
};
|
||||||
|
|
||||||
main();
|
main(ReactDOM.render, new APIClient(http));
|
||||||
|
|
||||||
export default main;
|
|
||||||
|
|||||||
@@ -8,53 +8,57 @@ import {
|
|||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
|
|
||||||
import towerLogo from '../../images/tower-logo-header.svg';
|
import towerLogo from '../../images/tower-logo-header.svg';
|
||||||
import api from '../api';
|
|
||||||
|
|
||||||
class AtLogin extends Component {
|
class AWXLogin extends Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
isValidPassword: true,
|
isInputValid: true,
|
||||||
loading: false
|
isLoading: false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.onChangeUsername = this.onChangeUsername.bind(this);
|
||||||
|
this.onChangePassword = this.onChangePassword.bind(this);
|
||||||
|
this.onLoginButtonClick = this.onLoginButtonClick.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount () {
|
onChangeUsername (value) {
|
||||||
this.unmounting = true; // todo: state management
|
this.setState({ username: value, isInputValid: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
safeSetState = obj => !this.unmounting && this.setState(obj);
|
onChangePassword (value) {
|
||||||
|
this.setState({ password: value, isInputValid: true });
|
||||||
|
}
|
||||||
|
|
||||||
handleUsernameChange = value => this.safeSetState({ username: value, isValidPassword: true });
|
async onLoginButtonClick (event) {
|
||||||
|
const { username, password, isLoading } = this.state;
|
||||||
handlePasswordChange = value => this.safeSetState({ password: value, isValidPassword: true });
|
const { api } = this.props;
|
||||||
|
|
||||||
handleSubmit = async event => {
|
|
||||||
const { username, password, loading } = this.state;
|
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
if (!loading) {
|
if (isLoading) {
|
||||||
this.safeSetState({ loading: true });
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
this.setState({ isLoading: true });
|
||||||
await api.login(username, password);
|
|
||||||
} catch (error) {
|
try {
|
||||||
if (error.response.status === 401) {
|
await api.login(username, password);
|
||||||
this.safeSetState({ isValidPassword: false });
|
} catch (error) {
|
||||||
}
|
if (error.response.status === 401) {
|
||||||
} finally {
|
this.setState({ isInputValid: false });
|
||||||
this.safeSetState({ loading: false });
|
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
this.setState({ isLoading: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { username, password, isValidPassword } = this.state;
|
const { username, password, isInputValid } = this.state;
|
||||||
const { logo, alt } = this.props;
|
const { api, alt, loginInfo, logo } = this.props;
|
||||||
const logoSrc = logo ? `data:image/jpeg;${logo}` : towerLogo;
|
const logoSrc = logo ? `data:image/jpeg;${logo}` : towerLogo;
|
||||||
|
|
||||||
if (api.isAuthenticated()) {
|
if (api.isAuthenticated()) {
|
||||||
@@ -65,20 +69,21 @@ class AtLogin extends Component {
|
|||||||
<I18n>
|
<I18n>
|
||||||
{({ i18n }) => (
|
{({ i18n }) => (
|
||||||
<LoginPage
|
<LoginPage
|
||||||
mainBrandImgSrc={logoSrc}
|
brandImgSrc={logoSrc}
|
||||||
mainBrandImgAlt={alt || 'Ansible Tower'}
|
brandImgAlt={alt || 'Ansible Tower'}
|
||||||
loginTitle={i18n._(t`Welcome to Ansible Tower! Please Sign In.`)}
|
loginTitle={i18n._(t`Welcome to Ansible Tower! Please Sign In.`)}
|
||||||
|
textContent={loginInfo}
|
||||||
>
|
>
|
||||||
<LoginForm
|
<LoginForm
|
||||||
usernameLabel={i18n._(t`Username`)}
|
usernameLabel={i18n._(t`Username`)}
|
||||||
usernameValue={username}
|
|
||||||
onChangeUsername={this.handleUsernameChange}
|
|
||||||
passwordLabel={i18n._(t`Password`)}
|
passwordLabel={i18n._(t`Password`)}
|
||||||
passwordValue={password}
|
|
||||||
onChangePassword={this.handlePasswordChange}
|
|
||||||
isValidPassword={isValidPassword}
|
|
||||||
passwordHelperTextInvalid={i18n._(t`Invalid username or password. Please try again.`)}
|
passwordHelperTextInvalid={i18n._(t`Invalid username or password. Please try again.`)}
|
||||||
onLoginButtonClick={this.handleSubmit}
|
usernameValue={username}
|
||||||
|
passwordValue={password}
|
||||||
|
isValidPassword={isInputValid}
|
||||||
|
onChangeUsername={this.onChangeUsername}
|
||||||
|
onChangePassword={this.onChangePassword}
|
||||||
|
onLoginButtonClick={this.onLoginButtonClick}
|
||||||
/>
|
/>
|
||||||
</LoginPage>
|
</LoginPage>
|
||||||
)}
|
)}
|
||||||
@@ -87,4 +92,4 @@ class AtLogin extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AtLogin;
|
export default AWXLogin;
|
||||||
|
|||||||
@@ -5,12 +5,31 @@ import OrganizationAdd from './views/Organization.add';
|
|||||||
import OrganizationView from './views/Organization.view';
|
import OrganizationView from './views/Organization.view';
|
||||||
import OrganizationsList from './views/Organizations.list';
|
import OrganizationsList from './views/Organizations.list';
|
||||||
|
|
||||||
const Organizations = ({ match }) => (
|
export default ({ api, match }) => (
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path={`${match.path}/add`} component={OrganizationAdd} />
|
<Route
|
||||||
<Route path={`${match.path}/:id`} component={OrganizationView} />
|
path={`${match.path}/add`}
|
||||||
<Route path={`${match.path}`} component={OrganizationsList} />
|
render={() => (
|
||||||
|
<OrganizationAdd
|
||||||
|
api={api}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path={`${match.path}/:id`}
|
||||||
|
render={() => (
|
||||||
|
<OrganizationView
|
||||||
|
api={api}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path={`${match.path}`}
|
||||||
|
render={() => (
|
||||||
|
<OrganizationsList
|
||||||
|
api={api}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</Switch>
|
</Switch>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default Organizations;
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
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 {
|
||||||
@@ -18,10 +17,8 @@ import {
|
|||||||
CardBody,
|
CardBody,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
|
|
||||||
import { ConfigContext } from '../../../context';
|
import AnsibleSelect from '../../../components/AnsibleSelect';
|
||||||
import { API_ORGANIZATIONS } from '../../../endpoints';
|
|
||||||
import api from '../../../api';
|
|
||||||
import AnsibleSelect from '../../../components/AnsibleSelect'
|
|
||||||
const { light } = PageSectionVariants;
|
const { light } = PageSectionVariants;
|
||||||
|
|
||||||
class OrganizationAdd extends React.Component {
|
class OrganizationAdd extends React.Component {
|
||||||
@@ -40,6 +37,8 @@ class OrganizationAdd extends React.Component {
|
|||||||
description: '',
|
description: '',
|
||||||
instanceGroups: '',
|
instanceGroups: '',
|
||||||
custom_virtualenv: '',
|
custom_virtualenv: '',
|
||||||
|
custom_virtualenvs: [],
|
||||||
|
hideAnsibleSelect: true,
|
||||||
error:'',
|
error:'',
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -61,7 +60,8 @@ class OrganizationAdd extends React.Component {
|
|||||||
|
|
||||||
async onSubmit() {
|
async onSubmit() {
|
||||||
const data = Object.assign({}, { ...this.state });
|
const data = Object.assign({}, { ...this.state });
|
||||||
await api.post(API_ORGANIZATIONS, data);
|
await api.createOrganization(data);
|
||||||
|
|
||||||
this.resetForm();
|
this.resetForm();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,10 +69,22 @@ class OrganizationAdd extends React.Component {
|
|||||||
this.props.history.push('/organizations');
|
this.props.history.push('/organizations');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async componentDidMount() {
|
||||||
|
try {
|
||||||
|
const { data } = await api.getConfig();
|
||||||
|
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">
|
||||||
@@ -116,16 +128,13 @@ class OrganizationAdd extends React.Component {
|
|||||||
onChange={this.handleChange}
|
onChange={this.handleChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<ConfigContext.Consumer>
|
<AnsibleSelect
|
||||||
{({ custom_virtualenvs }) =>
|
labelName="Ansible Environment"
|
||||||
<AnsibleSelect
|
selected={this.state.custom_virtualenv}
|
||||||
labelName="Ansible Environment"
|
selectChange={this.onSelectChange}
|
||||||
selected={this.state.custom_virtualenv}
|
data={this.state.custom_virtualenvs}
|
||||||
selectChange={this.onSelectChange}
|
hidden={this.state.hideAnsibleSelect}
|
||||||
data={custom_virtualenvs}
|
/>
|
||||||
/>
|
|
||||||
}
|
|
||||||
</ConfigContext.Consumer>
|
|
||||||
</Gallery>
|
</Gallery>
|
||||||
<ActionGroup className="at-align-right">
|
<ActionGroup className="at-align-right">
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
@@ -146,8 +155,4 @@ class OrganizationAdd extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
OrganizationAdd.contextTypes = {
|
|
||||||
custom_virtualenvs: PropTypes.array,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default withRouter(OrganizationAdd);
|
export default withRouter(OrganizationAdd);
|
||||||
|
|||||||
@@ -2,16 +2,13 @@ import React, { Component, Fragment } from 'react';
|
|||||||
import { i18nMark } from '@lingui/react';
|
import { i18nMark } from '@lingui/react';
|
||||||
import {
|
import {
|
||||||
Switch,
|
Switch,
|
||||||
Route
|
Route,
|
||||||
|
withRouter,
|
||||||
} from 'react-router-dom';
|
} from 'react-router-dom';
|
||||||
|
|
||||||
import OrganizationBreadcrumb from '../components/OrganizationBreadcrumb';
|
import OrganizationBreadcrumb from '../components/OrganizationBreadcrumb';
|
||||||
import OrganizationDetail from '../components/OrganizationDetail';
|
import OrganizationDetail from '../components/OrganizationDetail';
|
||||||
import OrganizationEdit from '../components/OrganizationEdit';
|
import OrganizationEdit from '../components/OrganizationEdit';
|
||||||
|
|
||||||
import api from '../../../api';
|
|
||||||
import { API_ORGANIZATIONS } from '../../../endpoints';
|
|
||||||
|
|
||||||
class OrganizationView extends Component {
|
class OrganizationView extends Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -30,6 +27,8 @@ class OrganizationView extends Component {
|
|||||||
loading: false,
|
loading: false,
|
||||||
mounted: false
|
mounted: false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.fetchOrganization = this.fetchOrganization.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
@@ -47,13 +46,15 @@ class OrganizationView extends Component {
|
|||||||
|
|
||||||
async fetchOrganization () {
|
async fetchOrganization () {
|
||||||
const { mounted } = this.state;
|
const { mounted } = this.state;
|
||||||
|
const { api } = this.props;
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
this.setState({ error: false, loading: true });
|
this.setState({ error: false, loading: true });
|
||||||
|
|
||||||
const { match } = this.props;
|
const { match } = this.props;
|
||||||
const { parentBreadcrumbObj, organization } = this.state;
|
const { parentBreadcrumbObj, organization } = this.state;
|
||||||
try {
|
try {
|
||||||
const { data } = await api.get(`${API_ORGANIZATIONS}${match.params.id}/`);
|
const { data } = await api.getOrganizationDetails(match.params.id);
|
||||||
if (organization === 'loading') {
|
if (organization === 'loading') {
|
||||||
this.setState({ organization: data });
|
this.setState({ organization: data });
|
||||||
}
|
}
|
||||||
@@ -118,4 +119,4 @@ class OrganizationView extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default OrganizationView;
|
export default withRouter(OrganizationView);
|
||||||
|
|||||||
@@ -17,9 +17,6 @@ import DataListToolbar from '../../../components/DataListToolbar';
|
|||||||
import OrganizationListItem from '../components/OrganizationListItem';
|
import OrganizationListItem from '../components/OrganizationListItem';
|
||||||
import Pagination from '../../../components/Pagination';
|
import Pagination from '../../../components/Pagination';
|
||||||
|
|
||||||
import api from '../../../api';
|
|
||||||
import { API_ORGANIZATIONS } from '../../../endpoints';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
encodeQueryString,
|
encodeQueryString,
|
||||||
parseQueryString,
|
parseQueryString,
|
||||||
@@ -56,6 +53,15 @@ class Organizations extends Component {
|
|||||||
results: [],
|
results: [],
|
||||||
selected: [],
|
selected: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.onSearch = this.onSearch.bind(this);
|
||||||
|
this.getQueryParams = this.getQueryParams.bind(this);
|
||||||
|
this.onSort = this.onSort.bind(this);
|
||||||
|
this.onSetPage = this.onSetPage.bind(this);
|
||||||
|
this.onSelectAll = this.onSelectAll.bind(this);
|
||||||
|
this.onSelect = this.onSelect.bind(this);
|
||||||
|
this.updateUrl = this.updateUrl.bind(this);
|
||||||
|
this.fetchOrganizations = this.fetchOrganizations.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
@@ -78,7 +84,7 @@ class Organizations extends Component {
|
|||||||
return Object.assign({}, this.defaultParams, searchParams, overrides);
|
return Object.assign({}, this.defaultParams, searchParams, overrides);
|
||||||
}
|
}
|
||||||
|
|
||||||
onSort = (sortedColumnKey, sortOrder) => {
|
onSort(sortedColumnKey, sortOrder) {
|
||||||
const { page_size } = this.state;
|
const { page_size } = this.state;
|
||||||
|
|
||||||
let order_by = sortedColumnKey;
|
let order_by = sortedColumnKey;
|
||||||
@@ -90,26 +96,26 @@ class Organizations extends Component {
|
|||||||
const queryParams = this.getQueryParams({ order_by, page_size });
|
const queryParams = this.getQueryParams({ order_by, page_size });
|
||||||
|
|
||||||
this.fetchOrganizations(queryParams);
|
this.fetchOrganizations(queryParams);
|
||||||
};
|
}
|
||||||
|
|
||||||
onSetPage = (pageNumber, pageSize) => {
|
onSetPage (pageNumber, pageSize) {
|
||||||
const page = parseInt(pageNumber, 10);
|
const page = parseInt(pageNumber, 10);
|
||||||
const page_size = parseInt(pageSize, 10);
|
const page_size = parseInt(pageSize, 10);
|
||||||
|
|
||||||
const queryParams = this.getQueryParams({ page, page_size });
|
const queryParams = this.getQueryParams({ page, page_size });
|
||||||
|
|
||||||
this.fetchOrganizations(queryParams);
|
this.fetchOrganizations(queryParams);
|
||||||
};
|
}
|
||||||
|
|
||||||
onSelectAll = isSelected => {
|
onSelectAll (isSelected) {
|
||||||
const { results } = this.state;
|
const { results } = this.state;
|
||||||
|
|
||||||
const selected = isSelected ? results.map(o => o.id) : [];
|
const selected = isSelected ? results.map(o => o.id) : [];
|
||||||
|
|
||||||
this.setState({ selected });
|
this.setState({ selected });
|
||||||
};
|
}
|
||||||
|
|
||||||
onSelect = id => {
|
onSelect (id) {
|
||||||
const { selected } = this.state;
|
const { selected } = this.state;
|
||||||
|
|
||||||
const isSelected = selected.includes(id);
|
const isSelected = selected.includes(id);
|
||||||
@@ -119,7 +125,7 @@ class Organizations extends Component {
|
|||||||
} else {
|
} else {
|
||||||
this.setState({ selected: selected.concat(id) });
|
this.setState({ selected: selected.concat(id) });
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
updateUrl (queryParams) {
|
updateUrl (queryParams) {
|
||||||
const { history, location } = this.props;
|
const { history, location } = this.props;
|
||||||
@@ -132,6 +138,7 @@ class Organizations extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fetchOrganizations (queryParams) {
|
async fetchOrganizations (queryParams) {
|
||||||
|
const { api } = this.props;
|
||||||
const { page, page_size, order_by } = queryParams;
|
const { page, page_size, order_by } = queryParams;
|
||||||
|
|
||||||
let sortOrder = 'ascending';
|
let sortOrder = 'ascending';
|
||||||
@@ -145,7 +152,7 @@ class Organizations extends Component {
|
|||||||
this.setState({ error: false, loading: true });
|
this.setState({ error: false, loading: true });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data } = await api.get(API_ORGANIZATIONS, queryParams);
|
const { data } = await api.getOrganizations(queryParams);
|
||||||
const { count, results } = data;
|
const { count, results } = data;
|
||||||
|
|
||||||
const pageCount = Math.ceil(count / page_size);
|
const pageCount = Math.ceil(count / page_size);
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const webpack = require('webpack');
|
const webpack = require('webpack');
|
||||||
|
|
||||||
const TARGET_PORT = 8043;
|
const TARGET_PORT = process.env.TARGET_PORT || 8043;
|
||||||
const TARGET = `https://localhost:${TARGET_PORT}`;
|
const TARGET_HOST = process.env.TARGET_HOST || 'localhost';
|
||||||
|
const TARGET = `https://${TARGET_HOST}:${TARGET_PORT}`;
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
entry: './src/index.jsx',
|
entry: './src/index.jsx',
|
||||||
|
|||||||
Reference in New Issue
Block a user