mirror of
https://github.com/ansible/awx.git
synced 2026-01-14 03:10:42 -03:30
Merge remote-tracking branch 'origin/master' into lookup-form-component
This commit is contained in:
commit
f34ec4be10
164
CONTRIBUTING.md
Normal file
164
CONTRIBUTING.md
Normal file
@ -0,0 +1,164 @@
|
||||
# Ansible AWX/Tower V2
|
||||
|
||||
Hi there! We're excited to have you as a contributor.
|
||||
|
||||
Have questions about this document or anything not covered here? Feel free to reach out to any of the contributors of this repository found here: https://github.com/ansible/awx-pf/graphs/contributors
|
||||
|
||||
## Table of contents
|
||||
|
||||
* [Things to know prior to submitting code](#things-to-know-prior-to-submitting-code)
|
||||
* [Setting up your development environment](#setting-up-your-development-environment)
|
||||
* [Prerequisites](#prerequisites)
|
||||
* [Node and npm](#node-and-npm)
|
||||
* [Build the user interface](#build-the-user-interface)
|
||||
* [Accessing the AWX web interface](#accessing-the-awx-web-interface)
|
||||
* [Working with React](#working-with-react)
|
||||
* [Class constructors vs Class properties](#class-constructors-vs-class-properties)
|
||||
* [Binding](#binding)
|
||||
* [Testing](#testing)
|
||||
* [Jest](#jest)
|
||||
* [Enzyme](#enzyme)
|
||||
|
||||
## Things to know prior to submitting code
|
||||
|
||||
- All code submissions are done through pull requests against the `master` branch.
|
||||
- If collaborating with someone else on the same branch, please use `--force-with-lease` instead of `--force` when pushing up code. This will prevent you from accidentally overwriting commits pushed by someone else. For more information, see https://git-scm.com/docs/git-push#git-push---force-with-leaseltrefnamegt
|
||||
|
||||
## Setting up your development environment
|
||||
|
||||
The UI is built using [ReactJS](https://reactjs.org/docs/getting-started.html) and [Patternfly](https://www.patternfly.org/).
|
||||
|
||||
### Prerequisites
|
||||
|
||||
#### Node and npm
|
||||
|
||||
The AWX UI requires the following:
|
||||
|
||||
- Node 8.x LTS
|
||||
- NPM 6.x LTS
|
||||
|
||||
Run the following to install all the dependencies:
|
||||
```bash
|
||||
(host) $ npm run install
|
||||
```
|
||||
|
||||
#### Build the User Interface
|
||||
|
||||
Run the following to build the AWX UI:
|
||||
|
||||
```bash
|
||||
(host) $ npm run start
|
||||
```
|
||||
|
||||
## Accessing the AWX web interface
|
||||
|
||||
You can now log into the AWX web interface at [https://127.0.0.1:3001](https://127.0.0.1:3001).
|
||||
|
||||
## Working with React
|
||||
### Class constructors vs Class properties
|
||||
It is good practice to use constructor-bound instance methods rather than methods as class properties. Methods as arrow functions provide lexical scope and are bound to the Component class instance instead of the class itself. This makes it so we cannot easily test a Component's methods without invoking an instance of the Component and calling the method directly within our tests.
|
||||
|
||||
BAD:
|
||||
```javascript
|
||||
class MyComponent extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
myEventHandler = () => {
|
||||
// do a thing
|
||||
}
|
||||
}
|
||||
```
|
||||
GOOD:
|
||||
```javascript
|
||||
class MyComponent extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.myEventHandler = this.myEventHandler.bind(this);
|
||||
}
|
||||
|
||||
myEventHandler() {
|
||||
// do a thing
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Binding
|
||||
It is good practice to bind our class methods within our class constructor method for the following reasons:
|
||||
1. Avoid defining the method every time `render()` is called.
|
||||
2. [Performance advantages](https://stackoverflow.com/a/44844916).
|
||||
3. Ease of [testing](https://github.com/airbnb/enzyme/issues/365).
|
||||
|
||||
### Component Lifecycle
|
||||
|
||||
A React Component has various [lifecylce methods](http://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/). Understanding the basic lifecycle of a Component will help you determine which method to utilize for your specific need. Below are some general guidelines:
|
||||
|
||||
BAD:
|
||||
Use `render` method to make asynchronous calls.
|
||||
```javascript
|
||||
render () {
|
||||
const { data } = await api.get(API_CONFIG);
|
||||
|
||||
return(<div>`Hello, ${data.usename}!`</div>);
|
||||
}
|
||||
```
|
||||
GOOD:
|
||||
Use `componentDidMount()` method to make asynchronous calls to retrieve data a Componenet may need.
|
||||
```javascript
|
||||
async componentDidMount () {
|
||||
try {
|
||||
const { data } = await api.get(API_CONFIG);
|
||||
this.setState({ data });
|
||||
} catch (error) {
|
||||
this.setState({ error });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return(<div>`Hello, ${this.state.data.usename}!`</div>)
|
||||
}
|
||||
```
|
||||
## Testing
|
||||
All code, new or otherwise, should have at least 80% test coverage.
|
||||
### Jest
|
||||
We use (Jest)[https://jestjs.io/] for our JS testing framework.
|
||||
Like many other JS test frameworks (Karma, Mocha, etc), Jest includes their own `spyOn` method as a way for us to test our class methods.
|
||||
```javascript
|
||||
const spy = jest.spyOn(MyButton.prototype, 'onSubmit');
|
||||
```
|
||||
|
||||
Jest also allows us to mock the data we expect from an external dependency, such as an API.
|
||||
```javascript
|
||||
axios.get.mockImplementation((endpoint) => {
|
||||
if (endpoint === '/api/v2/config') {
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve({ data: { foo: 'bar' });
|
||||
});
|
||||
}
|
||||
else {
|
||||
return 'get results';
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Enzyme
|
||||
We use (Enzyme)[https://airbnb.io/enzyme/] to test our React Components.
|
||||
### Mounting Components wrapped with withRouter
|
||||
If you are testing a Component wrapped in React Router's `withRouter` class, you can mount the component by wrapping it with the `<MemoryRouter>` component.
|
||||
```javascript
|
||||
test('initially renders succesfully', () => {
|
||||
mount(
|
||||
<MemoryRouter>
|
||||
<OrganizationAdd
|
||||
match={{ path: '/organizations/add', url: '/organizations/add' }}
|
||||
location={{ search: '', pathname: '/organizations/add' }}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
);
|
||||
});
|
||||
```
|
||||
You can test the wrapped Component's methods like so:
|
||||
```javascript
|
||||
const spy = jest.spyOn(OrganizationAdd.WrappedComponent.prototype, 'onCancel');
|
||||
```
|
||||
@ -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,72 +1,124 @@
|
||||
import React from 'react';
|
||||
import { HashRouter as Router } from 'react-router-dom';
|
||||
import { shallow, mount } from 'enzyme';
|
||||
import App from '../src/App';
|
||||
import api from '../src/api';
|
||||
import { API_LOGOUT, API_CONFIG } from '../src/endpoints';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { I18nProvider } from '@lingui/react';
|
||||
|
||||
import Dashboard from '../src/pages/Dashboard';
|
||||
import Login from '../src/pages/Login';
|
||||
import { mount, shallow } from 'enzyme';
|
||||
import { asyncFlush } from '../jest.setup';
|
||||
|
||||
import App from '../src/App';
|
||||
|
||||
const DEFAULT_ACTIVE_GROUP = 'views_group';
|
||||
const DEFAULT_ACTIVE_ITEM = 'views_group_dashboard';
|
||||
|
||||
describe('<App />', () => {
|
||||
test('renders without crashing', () => {
|
||||
const appWrapper = shallow(<App />);
|
||||
test('expected content is rendered', () => {
|
||||
const appWrapper = mount(
|
||||
<MemoryRouter>
|
||||
<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>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
// page components
|
||||
expect(appWrapper.length).toBe(1);
|
||||
expect(appWrapper.find('PageHeader').length).toBe(1);
|
||||
expect(appWrapper.find('PageSidebar').length).toBe(1);
|
||||
|
||||
// sidebar groups and route links
|
||||
expect(appWrapper.find('NavExpandableGroup').length).toBe(2);
|
||||
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);
|
||||
|
||||
// inline render
|
||||
expect(appWrapper.find('#group_one').length).toBe(1);
|
||||
expect(appWrapper.find('#group_two').length).toBe(1);
|
||||
});
|
||||
|
||||
test('renders login page when not authenticated', () => {
|
||||
api.isAuthenticated = jest.fn();
|
||||
api.isAuthenticated.mockReturnValue(false);
|
||||
test('opening the about modal renders prefetched config data', async (done) => {
|
||||
const ansible_version = '111';
|
||||
const version = '222';
|
||||
|
||||
const appWrapper = mount(<Router><App /></Router>);
|
||||
const getConfig = jest.fn(() => Promise.resolve({ data: { ansible_version, version} }));
|
||||
const api = { getConfig };
|
||||
|
||||
const login = appWrapper.find(Login);
|
||||
expect(login.length).toBe(1);
|
||||
const dashboard = appWrapper.find(Dashboard);
|
||||
expect(dashboard.length).toBe(0);
|
||||
});
|
||||
const wrapper = mount(
|
||||
<MemoryRouter>
|
||||
<I18nProvider>
|
||||
<App api={api}/>
|
||||
</I18nProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
test('renders dashboard when authenticated', () => {
|
||||
api.isAuthenticated = jest.fn();
|
||||
api.isAuthenticated.mockReturnValue(true);
|
||||
await asyncFlush();
|
||||
expect(getConfig).toHaveBeenCalledTimes(1);
|
||||
|
||||
const appWrapper = mount(<Router><App /></Router>);
|
||||
// open about modal
|
||||
const aboutDropdown = 'Dropdown QuestionCircleIcon';
|
||||
const aboutButton = 'DropdownItem li button';
|
||||
const aboutModalContent = 'AboutModalBoxContent';
|
||||
const aboutModalClose = 'button[aria-label="Close Dialog"]';
|
||||
|
||||
const dashboard = appWrapper.find(Dashboard);
|
||||
expect(dashboard.length).toBe(1);
|
||||
const login = appWrapper.find(Login);
|
||||
expect(login.length).toBe(0);
|
||||
expect(wrapper.find(aboutModalContent)).toHaveLength(0);
|
||||
wrapper.find(aboutDropdown).simulate('click');
|
||||
wrapper.find(aboutButton).simulate('click');
|
||||
wrapper.update();
|
||||
|
||||
// check about modal content
|
||||
const content = wrapper.find(aboutModalContent);
|
||||
expect(content).toHaveLength(1);
|
||||
expect(content.find('dd').text()).toContain(ansible_version);
|
||||
expect(content.find('pre').text()).toContain(`< Tower ${version} >`);
|
||||
|
||||
// close about modal
|
||||
wrapper.find(aboutModalClose).simulate('click');
|
||||
expect(wrapper.find(aboutModalContent)).toHaveLength(0);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
test('onNavToggle sets state.isNavOpen to opposite', () => {
|
||||
const appWrapper = shallow(<App.WrappedComponent />);
|
||||
expect(appWrapper.state().isNavOpen).toBe(true);
|
||||
appWrapper.instance().onNavToggle();
|
||||
expect(appWrapper.state().isNavOpen).toBe(false);
|
||||
const appWrapper = shallow(<App />);
|
||||
const { onNavToggle } = appWrapper.instance();
|
||||
|
||||
[true, false, true, false, true].forEach(expected => {
|
||||
expect(appWrapper.state().isNavOpen).toBe(expected);
|
||||
onNavToggle();
|
||||
});
|
||||
});
|
||||
|
||||
test('onLogoClick sets selected nav back to defaults', () => {
|
||||
const appWrapper = shallow(<App.WrappedComponent />);
|
||||
appWrapper.setState({ activeGroup: 'foo', activeItem: 'bar' });
|
||||
expect(appWrapper.state().activeItem).toBe('bar');
|
||||
expect(appWrapper.state().activeGroup).toBe('foo');
|
||||
appWrapper.instance().onLogoClick();
|
||||
expect(appWrapper.state().activeGroup).toBe(DEFAULT_ACTIVE_GROUP);
|
||||
});
|
||||
test('onLogout makes expected call to api client', async (done) => {
|
||||
const logout = jest.fn(() => Promise.resolve());
|
||||
const api = { logout };
|
||||
|
||||
test('api.logout called from logout button', async () => {
|
||||
api.get = jest.fn().mockImplementation(() => Promise.resolve({}));
|
||||
const appWrapper = shallow(<App.WrappedComponent />);
|
||||
appWrapper.instance().onDevLogout();
|
||||
appWrapper.setState({ activeGroup: 'foo', activeItem: 'bar' });
|
||||
expect(api.get).toHaveBeenCalledWith(API_LOGOUT);
|
||||
const appWrapper = shallow(<App api={api} />);
|
||||
|
||||
appWrapper.instance().onLogout();
|
||||
await asyncFlush();
|
||||
expect(appWrapper.state().activeItem).toBe(DEFAULT_ACTIVE_ITEM);
|
||||
expect(appWrapper.state().activeGroup).toBe(DEFAULT_ACTIVE_GROUP);
|
||||
expect(api.logout).toHaveBeenCalledTimes(1);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
test('Componenet makes REST call to API_CONFIG endpoint when mounted', () => {
|
||||
|
||||
@ -1,80 +1,129 @@
|
||||
import mockAxios from 'axios';
|
||||
import APIClient from '../src/api';
|
||||
import * as endpoints from '../src/endpoints';
|
||||
|
||||
const CSRF_COOKIE_NAME = 'csrftoken';
|
||||
const CSRF_HEADER_NAME = 'X-CSRFToken';
|
||||
|
||||
const LOGIN_CONTENT_TYPE = 'application/x-www-form-urlencoded';
|
||||
const invalidCookie = 'invalid';
|
||||
const validLoggedOutCookie = 'current_user=%7B%22id%22%3A1%2C%22type%22%3A%22user%22%2C%22url%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2F%22%2C%22related%22%3A%7B%22admin_of_organizations%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fadmin_of_organizations%2F%22%2C%22authorized_tokens%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fauthorized_tokens%2F%22%2C%22roles%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Froles%2F%22%2C%22organizations%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Forganizations%2F%22%2C%22access_list%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Faccess_list%2F%22%2C%22teams%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fteams%2F%22%2C%22tokens%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Ftokens%2F%22%2C%22personal_tokens%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fpersonal_tokens%2F%22%2C%22credentials%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fcredentials%2F%22%2C%22activity_stream%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Factivity_stream%2F%22%2C%22projects%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fprojects%2F%22%7D%2C%22summary_fields%22%3A%7B%7D%2C%22created%22%3A%222018-10-19T16%3A30%3A59.141963Z%22%2C%22username%22%3A%22admin%22%2C%22first_name%22%3A%22%22%2C%22last_name%22%3A%22%22%2C%22email%22%3A%22%22%2C%22is_superuser%22%3Atrue%2C%22is_system_auditor%22%3Afalse%2C%22ldap_dn%22%3A%22%22%2C%22external_account%22%3Anull%2C%22auth%22%3A%5B%5D%7D; userLoggedIn=false; csrftoken=lhOHpLQUFHlIVqx8CCZmEpdEZAz79GIRBIT3asBzTbPE7HS7wizt7WBsgJClz8Ge';
|
||||
const validLoggedInCookie = 'current_user=%7B%22id%22%3A1%2C%22type%22%3A%22user%22%2C%22url%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2F%22%2C%22related%22%3A%7B%22admin_of_organizations%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fadmin_of_organizations%2F%22%2C%22authorized_tokens%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fauthorized_tokens%2F%22%2C%22roles%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Froles%2F%22%2C%22organizations%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Forganizations%2F%22%2C%22access_list%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Faccess_list%2F%22%2C%22teams%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fteams%2F%22%2C%22tokens%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Ftokens%2F%22%2C%22personal_tokens%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fpersonal_tokens%2F%22%2C%22credentials%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fcredentials%2F%22%2C%22activity_stream%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Factivity_stream%2F%22%2C%22projects%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fprojects%2F%22%7D%2C%22summary_fields%22%3A%7B%7D%2C%22created%22%3A%222018-10-19T16%3A30%3A59.141963Z%22%2C%22username%22%3A%22admin%22%2C%22first_name%22%3A%22%22%2C%22last_name%22%3A%22%22%2C%22email%22%3A%22%22%2C%22is_superuser%22%3Atrue%2C%22is_system_auditor%22%3Afalse%2C%22ldap_dn%22%3A%22%22%2C%22external_account%22%3Anull%2C%22auth%22%3A%5B%5D%7D; userLoggedIn=true; csrftoken=lhOHpLQUFHlIVqx8CCZmEpdEZAz79GIRBIT3asBzTbPE7HS7wizt7WBsgJClz8Ge';
|
||||
|
||||
describe('APIClient (api.js)', () => {
|
||||
afterEach(() => {
|
||||
mockAxios.customClearMocks();
|
||||
test('isAuthenticated returns false when cookie is invalid', () => {
|
||||
APIClient.getCookie = jest.fn(() => invalidCookie);
|
||||
|
||||
const api = new APIClient();
|
||||
expect(api.isAuthenticated()).toBe(false);
|
||||
});
|
||||
|
||||
test('constructor calls axios create', () => {
|
||||
const csrfObj = {
|
||||
xsrfCookieName: CSRF_COOKIE_NAME,
|
||||
xsrfHeaderName: CSRF_HEADER_NAME
|
||||
};
|
||||
expect(mockAxios.create).toHaveBeenCalledTimes(1);
|
||||
expect(mockAxios.create).toHaveBeenCalledWith(csrfObj);
|
||||
expect(APIClient.http).toHaveProperty('get');
|
||||
test('isAuthenticated returns false when cookie is unauthenticated', () => {
|
||||
APIClient.getCookie = jest.fn(() => validLoggedOutCookie);
|
||||
|
||||
const api = new APIClient();
|
||||
expect(api.isAuthenticated()).toBe(false);
|
||||
});
|
||||
|
||||
test('isAuthenticated checks authentication and sets cookie from document', () => {
|
||||
APIClient.getCookie = jest.fn();
|
||||
const invalidCookie = 'invalid';
|
||||
const validLoggedOutCookie = 'current_user=%7B%22id%22%3A1%2C%22type%22%3A%22user%22%2C%22url%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2F%22%2C%22related%22%3A%7B%22admin_of_organizations%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fadmin_of_organizations%2F%22%2C%22authorized_tokens%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fauthorized_tokens%2F%22%2C%22roles%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Froles%2F%22%2C%22organizations%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Forganizations%2F%22%2C%22access_list%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Faccess_list%2F%22%2C%22teams%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fteams%2F%22%2C%22tokens%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Ftokens%2F%22%2C%22personal_tokens%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fpersonal_tokens%2F%22%2C%22credentials%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fcredentials%2F%22%2C%22activity_stream%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Factivity_stream%2F%22%2C%22projects%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fprojects%2F%22%7D%2C%22summary_fields%22%3A%7B%7D%2C%22created%22%3A%222018-10-19T16%3A30%3A59.141963Z%22%2C%22username%22%3A%22admin%22%2C%22first_name%22%3A%22%22%2C%22last_name%22%3A%22%22%2C%22email%22%3A%22%22%2C%22is_superuser%22%3Atrue%2C%22is_system_auditor%22%3Afalse%2C%22ldap_dn%22%3A%22%22%2C%22external_account%22%3Anull%2C%22auth%22%3A%5B%5D%7D; userLoggedIn=false; csrftoken=lhOHpLQUFHlIVqx8CCZmEpdEZAz79GIRBIT3asBzTbPE7HS7wizt7WBsgJClz8Ge';
|
||||
const validLoggedInCookie = 'current_user=%7B%22id%22%3A1%2C%22type%22%3A%22user%22%2C%22url%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2F%22%2C%22related%22%3A%7B%22admin_of_organizations%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fadmin_of_organizations%2F%22%2C%22authorized_tokens%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fauthorized_tokens%2F%22%2C%22roles%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Froles%2F%22%2C%22organizations%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Forganizations%2F%22%2C%22access_list%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Faccess_list%2F%22%2C%22teams%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fteams%2F%22%2C%22tokens%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Ftokens%2F%22%2C%22personal_tokens%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fpersonal_tokens%2F%22%2C%22credentials%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fcredentials%2F%22%2C%22activity_stream%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Factivity_stream%2F%22%2C%22projects%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fprojects%2F%22%7D%2C%22summary_fields%22%3A%7B%7D%2C%22created%22%3A%222018-10-19T16%3A30%3A59.141963Z%22%2C%22username%22%3A%22admin%22%2C%22first_name%22%3A%22%22%2C%22last_name%22%3A%22%22%2C%22email%22%3A%22%22%2C%22is_superuser%22%3Atrue%2C%22is_system_auditor%22%3Afalse%2C%22ldap_dn%22%3A%22%22%2C%22external_account%22%3Anull%2C%22auth%22%3A%5B%5D%7D; userLoggedIn=true; csrftoken=lhOHpLQUFHlIVqx8CCZmEpdEZAz79GIRBIT3asBzTbPE7HS7wizt7WBsgJClz8Ge';
|
||||
APIClient.getCookie.mockReturnValue(invalidCookie);
|
||||
expect(APIClient.isAuthenticated()).toBe(false);
|
||||
APIClient.getCookie.mockReturnValue(validLoggedOutCookie);
|
||||
expect(APIClient.isAuthenticated()).toBe(false);
|
||||
APIClient.getCookie.mockReturnValue(validLoggedInCookie);
|
||||
expect(APIClient.isAuthenticated()).toBe(true);
|
||||
test('isAuthenticated returns true when cookie is valid and authenticated', () => {
|
||||
APIClient.getCookie = jest.fn(() => validLoggedInCookie);
|
||||
|
||||
const api = new APIClient();
|
||||
expect(api.isAuthenticated()).toBe(true);
|
||||
});
|
||||
|
||||
test('login calls get and post to login route, and sets cookie from document', (done) => {
|
||||
const un = 'foo';
|
||||
const pw = 'bar';
|
||||
const next = 'baz';
|
||||
const headers = { 'Content-Type': LOGIN_CONTENT_TYPE };
|
||||
const data = `username=${un}&password=${pw}&next=${next}`;
|
||||
APIClient.setCookie = jest.fn();
|
||||
APIClient.login(un, pw, next).then(() => {
|
||||
expect(mockAxios.get).toHaveBeenCalledTimes(1);
|
||||
expect(mockAxios.get).toHaveBeenCalledWith(endpoints.API_LOGIN, { headers });
|
||||
expect(mockAxios.post).toHaveBeenCalledTimes(1);
|
||||
expect(mockAxios.post).toHaveBeenCalledWith(endpoints.API_LOGIN, data, { headers });
|
||||
done();
|
||||
});
|
||||
test('login calls get and post with expected content headers', async (done) => {
|
||||
const headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
|
||||
|
||||
const createPromise = () => Promise.resolve();
|
||||
const mockHttp = ({ get: jest.fn(createPromise), post: jest.fn(createPromise) });
|
||||
|
||||
const api = new APIClient(mockHttp);
|
||||
await api.login('username', 'password');
|
||||
|
||||
expect(mockHttp.get).toHaveBeenCalledTimes(1);
|
||||
expect(mockHttp.get.mock.calls[0]).toContainEqual({ headers });
|
||||
|
||||
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) => {
|
||||
const un = '/foo/';
|
||||
const pw = '/bar/';
|
||||
const next = '/baz/';
|
||||
const headers = { 'Content-Type': LOGIN_CONTENT_TYPE };
|
||||
const data = `username=${encodeURIComponent(un)}&password=${encodeURIComponent(pw)}&next=${encodeURIComponent(next)}`;
|
||||
APIClient.login(un, pw, next).then(() => {
|
||||
expect(mockAxios.post).toHaveBeenCalledTimes(1);
|
||||
expect(mockAxios.post).toHaveBeenCalledWith(endpoints.API_LOGIN, data, { headers });
|
||||
done();
|
||||
});
|
||||
test('login sends expected data', async (done) => {
|
||||
const createPromise = () => Promise.resolve();
|
||||
const mockHttp = ({ get: jest.fn(createPromise), post: jest.fn(createPromise) });
|
||||
|
||||
const api = new APIClient(mockHttp);
|
||||
await api.login('foo', 'bar');
|
||||
await api.login('foo', 'bar', 'baz');
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
test('login redirect defaults to config route when not explicitly passed', (done) => {
|
||||
const un = 'foo';
|
||||
const pw = 'bar';
|
||||
const headers = { 'Content-Type': LOGIN_CONTENT_TYPE };
|
||||
const data = `username=${un}&password=${pw}&next=${encodeURIComponent(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();
|
||||
});
|
||||
test('logout calls expected http method', async (done) => {
|
||||
const createPromise = () => Promise.resolve();
|
||||
const mockHttp = ({ get: jest.fn(createPromise) });
|
||||
|
||||
const api = new APIClient(mockHttp);
|
||||
await api.logout();
|
||||
|
||||
expect(mockHttp.get).toHaveBeenCalledTimes(1);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
test('getConfig calls expected http method', async (done) => {
|
||||
const createPromise = () => Promise.resolve();
|
||||
const mockHttp = ({ get: jest.fn(createPromise) });
|
||||
|
||||
const api = new APIClient(mockHttp);
|
||||
await api.getConfig();
|
||||
|
||||
expect(mockHttp.get).toHaveBeenCalledTimes(1);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
test('getOrganizations calls http method with expected data', async (done) => {
|
||||
const createPromise = () => Promise.resolve();
|
||||
const mockHttp = ({ get: jest.fn(createPromise) });
|
||||
const api = new APIClient(mockHttp);
|
||||
|
||||
const defaultParams = {};
|
||||
const testParams = { foo: 'bar' };
|
||||
|
||||
await api.getOrganizations(testParams);
|
||||
await api.getOrganizations();
|
||||
|
||||
expect(mockHttp.get).toHaveBeenCalledTimes(2);
|
||||
expect(mockHttp.get.mock.calls[0][1]).toEqual({ params: testParams });
|
||||
expect(mockHttp.get.mock.calls[1][1]).toEqual({ params: defaultParams });
|
||||
done();
|
||||
});
|
||||
|
||||
test('createOrganization calls http method with expected data', async (done) => {
|
||||
const createPromise = () => Promise.resolve();
|
||||
const mockHttp = ({ post: jest.fn(createPromise) });
|
||||
|
||||
const api = new APIClient(mockHttp);
|
||||
const data = { name: 'test '};
|
||||
await api.createOrganization(data);
|
||||
|
||||
expect(mockHttp.post).toHaveBeenCalledTimes(1);
|
||||
expect(mockHttp.post.mock.calls[0][1]).toEqual(data);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
test('getOrganizationDetails calls http method with expected data', async (done) => {
|
||||
const createPromise = () => Promise.resolve();
|
||||
const mockHttp = ({ get: jest.fn(createPromise) });
|
||||
|
||||
const api = new APIClient(mockHttp);
|
||||
await api.getOrganizationDetails(99);
|
||||
|
||||
expect(mockHttp.get).toHaveBeenCalledTimes(1);
|
||||
expect(mockHttp.get.mock.calls[0][0]).toContain('99');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { I18nProvider } from '@lingui/react';
|
||||
import api from '../../src/api';
|
||||
import { API_CONFIG } from '../../src/endpoints';
|
||||
import About from '../../src/components/About';
|
||||
|
||||
describe('<About />', () => {
|
||||
@ -19,16 +17,16 @@ describe('<About />', () => {
|
||||
aboutWrapper.unmount();
|
||||
});
|
||||
|
||||
test('close button calls onAboutModalClose', () => {
|
||||
const onAboutModalClose = jest.fn();
|
||||
test('close button calls onClose handler', () => {
|
||||
const onClose = jest.fn();
|
||||
aboutWrapper = mount(
|
||||
<I18nProvider>
|
||||
<About isOpen onAboutModalClose={onAboutModalClose} />
|
||||
<About isOpen onClose={onClose} />
|
||||
</I18nProvider>
|
||||
);
|
||||
closeButton = aboutWrapper.find('AboutModalBoxCloseButton Button');
|
||||
closeButton.simulate('click');
|
||||
expect(onAboutModalClose).toBeCalled();
|
||||
expect(onClose).toBeCalled();
|
||||
aboutWrapper.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@ -29,4 +29,16 @@ describe('<AnsibleSelect />', () => {
|
||||
wrapper.find('select').simulate('change');
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
test('content not rendered when data property is falsey', () => {
|
||||
const wrapper = mount(
|
||||
<AnsibleSelect
|
||||
selected="foo"
|
||||
selectChange={() => { }}
|
||||
labelName={label}
|
||||
data={null}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('FormGroup')).toHaveLength(0);
|
||||
expect(wrapper.find('Select')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
14
__tests__/components/Background.test.jsx
Normal file
14
__tests__/components/Background.test.jsx
Normal file
@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
import { I18nProvider } from '@lingui/react';
|
||||
import Background from '../../src/components/Background';
|
||||
|
||||
describe('Background', () => {
|
||||
test('renders the expected content', () => {
|
||||
const wrapper = mount(<Background><div id="test"/></Background>);
|
||||
expect(wrapper).toHaveLength(1);
|
||||
expect(wrapper.find('BackgroundImage')).toHaveLength(1);
|
||||
expect(wrapper.find('#test')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
@ -4,7 +4,6 @@ import { I18nProvider } from '@lingui/react';
|
||||
import DataListToolbar from '../../src/components/DataListToolbar';
|
||||
|
||||
describe('<DataListToolbar />', () => {
|
||||
const columns = [{ name: 'Name', key: 'name', isSortable: true }];
|
||||
let toolbar;
|
||||
|
||||
afterEach(() => {
|
||||
@ -15,6 +14,8 @@ describe('<DataListToolbar />', () => {
|
||||
});
|
||||
|
||||
test('it triggers the expected callbacks', () => {
|
||||
const columns = [{ name: 'Name', key: 'name', isSortable: true }];
|
||||
|
||||
const search = 'button[aria-label="Search"]';
|
||||
const searchTextInput = 'input[aria-label="Search text input"]';
|
||||
const selectAll = 'input[aria-label="Select all"]';
|
||||
@ -28,6 +29,7 @@ describe('<DataListToolbar />', () => {
|
||||
<I18nProvider>
|
||||
<DataListToolbar
|
||||
isAllSelected={false}
|
||||
showExpandCollapse={true}
|
||||
sortedColumnKey="name"
|
||||
sortOrder="ascending"
|
||||
columns={columns}
|
||||
@ -55,4 +57,166 @@ describe('<DataListToolbar />', () => {
|
||||
expect(onSearch).toHaveBeenCalledTimes(1);
|
||||
expect(onSearch).toBeCalledWith('test-321');
|
||||
});
|
||||
|
||||
test('dropdown items sortable columns work', () => {
|
||||
const sortDropdownToggleSelector = '.pf-l-toolbar__group.sortDropdownGroup .pf-l-toolbar__item button';
|
||||
const sortDropdownItemsSelector = '.pf-l-toolbar__group.sortDropdownGroup button.pf-c-dropdown__menu-item';
|
||||
const searchDropdownToggleSelector = '.pf-c-dropdown.searchKeyDropdown .pf-c-dropdown__toggle';
|
||||
const searchDropdownItemsSelector = '.pf-c-dropdown.searchKeyDropdown button.pf-c-dropdown__menu-item';
|
||||
|
||||
const multipleColumns = [
|
||||
{ name: 'Foo', key: 'foo', isSortable: true },
|
||||
{ name: 'Bar', key: 'bar', isSortable: true },
|
||||
{ name: 'Bakery', key: 'bakery', isSortable: true },
|
||||
{ name: 'Baz', key: 'baz' }
|
||||
];
|
||||
|
||||
const onSearch = jest.fn();
|
||||
const onSort = jest.fn();
|
||||
const onSelectAll = jest.fn();
|
||||
|
||||
toolbar = mount(
|
||||
<I18nProvider>
|
||||
<DataListToolbar
|
||||
isAllSelected={false}
|
||||
sortedColumnKey="foo"
|
||||
sortOrder="ascending"
|
||||
columns={multipleColumns}
|
||||
onSearch={onSearch}
|
||||
onSort={onSort}
|
||||
onSelectAll={onSelectAll}
|
||||
/>
|
||||
</I18nProvider>
|
||||
);
|
||||
const sortDropdownToggle = toolbar.find(sortDropdownToggleSelector);
|
||||
expect(sortDropdownToggle.length).toBe(2);
|
||||
sortDropdownToggle.at(1).simulate('click');
|
||||
sortDropdownToggle.at(0).simulate('click');
|
||||
toolbar.update();
|
||||
|
||||
const sortDropdownItems = toolbar.find(sortDropdownItemsSelector);
|
||||
expect(sortDropdownItems.length).toBe(2);
|
||||
|
||||
const mockedSortEvent = { target: { innerText: 'Bar' } };
|
||||
sortDropdownItems.at(0).simulate('click', mockedSortEvent);
|
||||
toolbar = mount(
|
||||
<I18nProvider>
|
||||
<DataListToolbar
|
||||
isAllSelected={false}
|
||||
sortedColumnKey="foo"
|
||||
sortOrder="descending"
|
||||
columns={multipleColumns}
|
||||
onSearch={onSearch}
|
||||
onSort={onSort}
|
||||
onSelectAll={onSelectAll}
|
||||
/>
|
||||
</I18nProvider>
|
||||
);
|
||||
toolbar.update();
|
||||
|
||||
const sortDropdownToggleDescending = toolbar.find(sortDropdownToggleSelector);
|
||||
expect(sortDropdownToggleDescending.length).toBe(2);
|
||||
sortDropdownToggleDescending.at(1).simulate('click');
|
||||
sortDropdownToggleDescending.at(0).simulate('click');
|
||||
toolbar.update();
|
||||
|
||||
const sortDropdownItemsDescending = toolbar.find(sortDropdownItemsSelector);
|
||||
expect(sortDropdownItemsDescending.length).toBe(2);
|
||||
|
||||
const mockedSortEventDescending = { target: { innerText: 'Bar' } };
|
||||
sortDropdownItems.at(0).simulate('click', mockedSortEventDescending);
|
||||
toolbar.update();
|
||||
|
||||
const searchDropdownToggle = toolbar.find(searchDropdownToggleSelector);
|
||||
expect(searchDropdownToggle.length).toBe(1);
|
||||
searchDropdownToggle.at(0).simulate('click');
|
||||
toolbar.update();
|
||||
|
||||
const searchDropdownItems = toolbar.find(searchDropdownItemsSelector);
|
||||
expect(searchDropdownItems.length).toBe(3);
|
||||
|
||||
const mockedSearchEvent = { target: { innerText: 'Bar' } };
|
||||
searchDropdownItems.at(0).simulate('click', mockedSearchEvent);
|
||||
});
|
||||
|
||||
test('it displays correct sort icon', () => {
|
||||
const downNumericIconSelector = 'SortNumericDownIcon';
|
||||
const upNumericIconSelector = 'SortNumericUpIcon';
|
||||
const downAlphaIconSelector = 'SortAlphaDownIcon';
|
||||
const upAlphaIconSelector = 'SortAlphaUpIcon';
|
||||
|
||||
const numericColumns = [{ name: 'ID', key: 'id', isSortable: true, isNumeric: true }];
|
||||
const alphaColumns = [{ name: 'Name', key: 'name', isSortable: true, isNumeric: false }];
|
||||
const onSearch = jest.fn();
|
||||
const onSort = jest.fn();
|
||||
const onSelectAll = jest.fn();
|
||||
|
||||
toolbar = mount(
|
||||
<I18nProvider>
|
||||
<DataListToolbar
|
||||
isAllSelected={false}
|
||||
sortedColumnKey="id"
|
||||
sortOrder="descending"
|
||||
columns={numericColumns}
|
||||
onSearch={onSearch}
|
||||
onSort={onSort}
|
||||
onSelectAll={onSelectAll}
|
||||
/>
|
||||
</I18nProvider>
|
||||
);
|
||||
|
||||
const downNumericIcon = toolbar.find(downNumericIconSelector);
|
||||
expect(downNumericIcon.length).toBe(1);
|
||||
|
||||
toolbar = mount(
|
||||
<I18nProvider>
|
||||
<DataListToolbar
|
||||
isAllSelected={false}
|
||||
sortedColumnKey="id"
|
||||
sortOrder="ascending"
|
||||
columns={numericColumns}
|
||||
onSearch={onSearch}
|
||||
onSort={onSort}
|
||||
onSelectAll={onSelectAll}
|
||||
/>
|
||||
</I18nProvider>
|
||||
);
|
||||
|
||||
const upNumericIcon = toolbar.find(upNumericIconSelector);
|
||||
expect(upNumericIcon.length).toBe(1);
|
||||
|
||||
toolbar = mount(
|
||||
<I18nProvider>
|
||||
<DataListToolbar
|
||||
isAllSelected={false}
|
||||
sortedColumnKey="name"
|
||||
sortOrder="descending"
|
||||
columns={alphaColumns}
|
||||
onSearch={onSearch}
|
||||
onSort={onSort}
|
||||
onSelectAll={onSelectAll}
|
||||
/>
|
||||
</I18nProvider>
|
||||
);
|
||||
|
||||
const downAlphaIcon = toolbar.find(downAlphaIconSelector);
|
||||
expect(downAlphaIcon.length).toBe(1);
|
||||
|
||||
toolbar = mount(
|
||||
<I18nProvider>
|
||||
<DataListToolbar
|
||||
isAllSelected={false}
|
||||
sortedColumnKey="name"
|
||||
sortOrder="ascending"
|
||||
columns={alphaColumns}
|
||||
onSearch={onSearch}
|
||||
onSort={onSort}
|
||||
onSelectAll={onSelectAll}
|
||||
/>
|
||||
</I18nProvider>
|
||||
);
|
||||
|
||||
const upAlphaIcon = toolbar.find(upAlphaIconSelector);
|
||||
expect(upAlphaIcon.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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">
|
||||
<NavExpandableGroup
|
||||
groupId="test"
|
||||
title="Test"
|
||||
groupTitle="Test"
|
||||
routes={[
|
||||
{ path: '/foo', title: 'Foo' },
|
||||
{ path: '/bar', title: 'Bar' },
|
||||
@ -45,7 +45,7 @@ describe('NavExpandableGroup', () => {
|
||||
<Nav aria-label="Test Navigation">
|
||||
<NavExpandableGroup
|
||||
groupId="test"
|
||||
title="Test"
|
||||
groupTitle="Test"
|
||||
routes={[
|
||||
{ path: '/foo', title: 'Foo' },
|
||||
{ path: '/bar', title: 'Bar' },
|
||||
|
||||
48
__tests__/components/PageHeaderToolbar.test.jsx
Normal file
48
__tests__/components/PageHeaderToolbar.test.jsx
Normal file
@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
import { I18nProvider } from '@lingui/react';
|
||||
import PageHeaderToolbar from '../../src/components/PageHeaderToolbar';
|
||||
|
||||
describe('PageHeaderToolbar', () => {
|
||||
const pageHelpDropdownSelector = 'Dropdown QuestionCircleIcon';
|
||||
const pageUserDropdownSelector = 'Dropdown UserIcon';
|
||||
|
||||
test('expected content is rendered on initialization', () => {
|
||||
const wrapper = mount(<I18nProvider><PageHeaderToolbar/></I18nProvider>);
|
||||
|
||||
expect(wrapper.find(pageHelpDropdownSelector)).toHaveLength(1);
|
||||
expect(wrapper.find(pageUserDropdownSelector)).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('dropdowns have expected items and callbacks', () => {
|
||||
const onAboutClick = jest.fn();
|
||||
const onLogoutClick = jest.fn();
|
||||
const wrapper = mount(
|
||||
<MemoryRouter>
|
||||
<I18nProvider>
|
||||
<PageHeaderToolbar
|
||||
onAboutClick={onAboutClick}
|
||||
onLogoutClick={onLogoutClick}
|
||||
/>
|
||||
</I18nProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
expect(wrapper.find('DropdownItem')).toHaveLength(0);
|
||||
wrapper.find(pageHelpDropdownSelector).simulate('click');
|
||||
expect(wrapper.find('DropdownItem')).toHaveLength(2);
|
||||
|
||||
const about = wrapper.find('DropdownItem li button');
|
||||
about.simulate('click');
|
||||
expect(onAboutClick).toHaveBeenCalled();
|
||||
|
||||
expect(wrapper.find('DropdownItem')).toHaveLength(0);
|
||||
wrapper.find(pageUserDropdownSelector).simulate('click');
|
||||
expect(wrapper.find('DropdownItem')).toHaveLength(2);
|
||||
|
||||
const logout = wrapper.find('DropdownItem li button');
|
||||
logout.simulate('click');
|
||||
expect(onLogoutClick).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@ -72,4 +72,97 @@ describe('<Pagination />', () => {
|
||||
expect(onSetPage).toHaveBeenCalledTimes(2);
|
||||
expect(onSetPage).toBeCalledWith(1, 5);
|
||||
});
|
||||
|
||||
test('previous button does not work on page 1', () => {
|
||||
const previous = 'button[aria-label="First"]';
|
||||
const onSetPage = jest.fn();
|
||||
|
||||
pagination = mount(
|
||||
<I18nProvider>
|
||||
<Pagination
|
||||
count={21}
|
||||
page={1}
|
||||
pageCount={5}
|
||||
page_size={5}
|
||||
pageSizeOptions={[5, 10, 25, 50]}
|
||||
onSetPage={onSetPage}
|
||||
/>
|
||||
</I18nProvider>
|
||||
);
|
||||
pagination.find(previous).simulate('click');
|
||||
expect(onSetPage).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test('changing pageSize works', () => {
|
||||
const pageSizeDropdownToggleSelector = 'DropdownToggle DropdownToggle[className="togglePageSize"]';
|
||||
const pageSizeDropdownItemsSelector = 'DropdownItem';
|
||||
const onSetPage = jest.fn();
|
||||
|
||||
pagination = mount(
|
||||
<I18nProvider>
|
||||
<Pagination
|
||||
count={21}
|
||||
page={1}
|
||||
pageCount={5}
|
||||
page_size={5}
|
||||
pageSizeOptions={[5, 10, 25, 50]}
|
||||
onSetPage={onSetPage}
|
||||
/>
|
||||
</I18nProvider>
|
||||
);
|
||||
const pageSizeDropdownToggle = pagination.find(pageSizeDropdownToggleSelector);
|
||||
expect(pageSizeDropdownToggle.length).toBe(1);
|
||||
pageSizeDropdownToggle.at(0).simulate('click');
|
||||
|
||||
const pageSizeDropdownItems = pagination.find(pageSizeDropdownItemsSelector);
|
||||
expect(pageSizeDropdownItems.length).toBe(3);
|
||||
pageSizeDropdownItems.at(1).simulate('click');
|
||||
});
|
||||
|
||||
test('submit a new page by typing in input works', () => {
|
||||
const textInputSelector = '.pf-l-split__item.pf-m-main .pf-c-form-control';
|
||||
const submitFormSelector = '.pf-l-split__item.pf-m-main form';
|
||||
const onSetPage = jest.fn();
|
||||
|
||||
pagination = mount(
|
||||
<I18nProvider>
|
||||
<Pagination
|
||||
count={21}
|
||||
page={1}
|
||||
pageCount={5}
|
||||
page_size={5}
|
||||
pageSizeOptions={[5, 10, 25, 50]}
|
||||
onSetPage={onSetPage}
|
||||
/>
|
||||
</I18nProvider>
|
||||
);
|
||||
|
||||
const textInput = pagination.find(textInputSelector);
|
||||
expect(textInput.length).toBe(1);
|
||||
textInput.simulate('change');
|
||||
pagination.setProps({ page: 2 });
|
||||
|
||||
const submitForm = pagination.find(submitFormSelector);
|
||||
expect(submitForm.length).toBe(1);
|
||||
submitForm.simulate('submit');
|
||||
pagination.find('Pagination').instance().setState({ value: 'invalid' });
|
||||
submitForm.simulate('submit');
|
||||
});
|
||||
|
||||
test('text input page change is disabled when only 1 page', () => {
|
||||
const onSetPage = jest.fn();
|
||||
|
||||
pagination = mount(
|
||||
<I18nProvider>
|
||||
<Pagination
|
||||
count={4}
|
||||
page={1}
|
||||
pageCount={1}
|
||||
page_size={5}
|
||||
pageSizeOptions={[5, 10, 25, 50]}
|
||||
onSetPage={onSetPage}
|
||||
/>
|
||||
</I18nProvider>
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -29,11 +29,10 @@ describe('<TowerLogo />', () => {
|
||||
});
|
||||
|
||||
test('adds navigation to route history on click', () => {
|
||||
const onLogoClick = jest.fn();
|
||||
logoWrapper = mount(
|
||||
<MemoryRouter>
|
||||
<I18nProvider>
|
||||
<TowerLogo onClick={onLogoClick} />
|
||||
<TowerLogo linkTo="/" />
|
||||
</I18nProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
@ -43,7 +42,7 @@ describe('<TowerLogo />', () => {
|
||||
expect(towerLogoElem.props().history.length).toBe(2);
|
||||
});
|
||||
|
||||
test('gracefully handles not being passed click handler', () => {
|
||||
test('linkTo prop is optional', () => {
|
||||
logoWrapper = mount(
|
||||
<MemoryRouter>
|
||||
<I18nProvider>
|
||||
@ -62,7 +61,7 @@ describe('<TowerLogo />', () => {
|
||||
logoWrapper = mount(
|
||||
<MemoryRouter>
|
||||
<I18nProvider>
|
||||
<TowerLogo onClick={onLogoClick} />
|
||||
<TowerLogo />
|
||||
</I18nProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
@ -1,22 +1,51 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { mount } from 'enzyme';
|
||||
import { main, getLanguage } from '../src/index';
|
||||
|
||||
import api from '../src/api';
|
||||
|
||||
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() }));
|
||||
const render = template => mount(template);
|
||||
const data = { custom_logo: 'foo', custom_login_info: '' }
|
||||
|
||||
describe('index.jsx', () => {
|
||||
test('renders without crashing', async () => {
|
||||
api.getRoot = jest.fn().mockImplementation(() => Promise
|
||||
.resolve({ data: { custom_logo, custom_login_info } }));
|
||||
test('login loads when unauthenticated', async (done) => {
|
||||
const isAuthenticated = () => false;
|
||||
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('App')).toHaveLength(0);
|
||||
expect(wrapper.find('Login')).toHaveLength(1);
|
||||
|
||||
const { src } = wrapper.find('Login Brand img').props();
|
||||
expect(src).toContain(data.custom_logo);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
test('app loads 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('App')).toHaveLength(1);
|
||||
expect(wrapper.find('Login')).toHaveLength(0);
|
||||
|
||||
wrapper.find('header a').simulate('click');
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find('App')).toHaveLength(1);
|
||||
expect(wrapper.find('Login')).toHaveLength(0);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
test('getLanguage returns the expected language code', () => {
|
||||
expect(getLanguage({ languages: ['es-US'] })).toEqual('es');
|
||||
expect(getLanguage({ languages: ['es-US'], language: 'fr-FR', userLanguage: 'en-US' })).toEqual('es');
|
||||
expect(getLanguage({ language: 'fr-FR', userLanguage: 'en-US' })).toEqual('fr');
|
||||
expect(getLanguage({ userLanguage: 'en-US' })).toEqual('en');
|
||||
});
|
||||
});
|
||||
|
||||
@ -3,12 +3,12 @@ import { MemoryRouter } from 'react-router-dom';
|
||||
import { mount, shallow } from 'enzyme';
|
||||
import { I18nProvider } from '@lingui/react';
|
||||
import { asyncFlush } from '../../jest.setup';
|
||||
import AtLogin from '../../src/pages/Login';
|
||||
import api from '../../src/api';
|
||||
import AWXLogin from '../../src/pages/Login';
|
||||
import APIClient from '../../src/api';
|
||||
|
||||
describe('<Login />', () => {
|
||||
let loginWrapper;
|
||||
let atLogin;
|
||||
let awxLogin;
|
||||
let loginPage;
|
||||
let loginForm;
|
||||
let usernameInput;
|
||||
@ -16,21 +16,23 @@ describe('<Login />', () => {
|
||||
let submitButton;
|
||||
let loginHeaderLogo;
|
||||
|
||||
const api = new APIClient({});
|
||||
|
||||
const findChildren = () => {
|
||||
atLogin = loginWrapper.find('AtLogin');
|
||||
awxLogin = loginWrapper.find('AWXLogin');
|
||||
loginPage = loginWrapper.find('LoginPage');
|
||||
loginForm = loginWrapper.find('LoginForm');
|
||||
usernameInput = loginWrapper.find('input#pf-login-username-id');
|
||||
passwordInput = loginWrapper.find('input#pf-login-password-id');
|
||||
submitButton = loginWrapper.find('Button[type="submit"]');
|
||||
loginHeaderLogo = loginWrapper.find('LoginHeaderBrand Brand');
|
||||
loginHeaderLogo = loginPage.find('img');
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
loginWrapper = mount(
|
||||
<MemoryRouter>
|
||||
<I18nProvider>
|
||||
<AtLogin />
|
||||
<AWXLogin api={api} />
|
||||
</I18nProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
@ -49,7 +51,7 @@ describe('<Login />', () => {
|
||||
expect(usernameInput.props().value).toBe('');
|
||||
expect(passwordInput.length).toBe(1);
|
||||
expect(passwordInput.props().value).toBe('');
|
||||
expect(atLogin.state().isValidPassword).toBe(true);
|
||||
expect(awxLogin.state().isInputValid).toBe(true);
|
||||
expect(submitButton.length).toBe(1);
|
||||
expect(submitButton.props().isDisabled).toBe(false);
|
||||
expect(loginHeaderLogo.length).toBe(1);
|
||||
@ -59,7 +61,7 @@ describe('<Login />', () => {
|
||||
loginWrapper = mount(
|
||||
<MemoryRouter>
|
||||
<I18nProvider>
|
||||
<AtLogin logo="images/foo.jpg" alt="Foo Application" />
|
||||
<AWXLogin api={api} logo="images/foo.jpg" alt="Foo Application" />
|
||||
</I18nProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
@ -73,7 +75,7 @@ describe('<Login />', () => {
|
||||
loginWrapper = mount(
|
||||
<MemoryRouter>
|
||||
<I18nProvider>
|
||||
<AtLogin />
|
||||
<AWXLogin api={api} />
|
||||
</I18nProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
@ -84,49 +86,49 @@ describe('<Login />', () => {
|
||||
});
|
||||
|
||||
test('state maps to un/pw input value props', () => {
|
||||
atLogin.setState({ username: 'un', password: 'pw' });
|
||||
expect(atLogin.state().username).toBe('un');
|
||||
expect(atLogin.state().password).toBe('pw');
|
||||
awxLogin.setState({ username: 'un', password: 'pw' });
|
||||
expect(awxLogin.state().username).toBe('un');
|
||||
expect(awxLogin.state().password).toBe('pw');
|
||||
findChildren();
|
||||
expect(usernameInput.props().value).toBe('un');
|
||||
expect(passwordInput.props().value).toBe('pw');
|
||||
});
|
||||
|
||||
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);
|
||||
usernameInput.instance().value = 'uname';
|
||||
usernameInput.simulate('change');
|
||||
expect(atLogin.state().username).toBe('uname');
|
||||
expect(atLogin.state().isValidPassword).toBe(true);
|
||||
expect(awxLogin.state().username).toBe('uname');
|
||||
expect(awxLogin.state().isInputValid).toBe(true);
|
||||
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);
|
||||
passwordInput.instance().value = 'pword';
|
||||
passwordInput.simulate('change');
|
||||
expect(atLogin.state().password).toBe('pword');
|
||||
expect(atLogin.state().isValidPassword).toBe(true);
|
||||
expect(awxLogin.state().password).toBe('pword');
|
||||
expect(awxLogin.state().isInputValid).toBe(true);
|
||||
expect(loginWrapper.find('.pf-c-form__helper-text.pf-m-error').length).toBe(0);
|
||||
});
|
||||
|
||||
test('api.login not called when loading', () => {
|
||||
api.login = jest.fn().mockImplementation(() => Promise.resolve({}));
|
||||
expect(atLogin.state().loading).toBe(false);
|
||||
atLogin.setState({ loading: true });
|
||||
expect(awxLogin.state().isLoading).toBe(false);
|
||||
awxLogin.setState({ isLoading: true });
|
||||
submitButton.simulate('click');
|
||||
expect(api.login).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test('submit calls api.login successfully', async () => {
|
||||
api.login = jest.fn().mockImplementation(() => Promise.resolve({}));
|
||||
expect(atLogin.state().loading).toBe(false);
|
||||
atLogin.setState({ username: 'unamee', password: 'pwordd' });
|
||||
expect(awxLogin.state().isLoading).toBe(false);
|
||||
awxLogin.setState({ username: 'unamee', password: 'pwordd' });
|
||||
submitButton.simulate('click');
|
||||
expect(api.login).toHaveBeenCalledTimes(1);
|
||||
expect(api.login).toHaveBeenCalledWith('unamee', 'pwordd');
|
||||
expect(atLogin.state().loading).toBe(true);
|
||||
expect(awxLogin.state().isLoading).toBe(true);
|
||||
await asyncFlush();
|
||||
expect(atLogin.state().loading).toBe(false);
|
||||
expect(awxLogin.state().isLoading).toBe(false);
|
||||
});
|
||||
|
||||
test('submit calls api.login handles 401 error', async () => {
|
||||
@ -135,16 +137,16 @@ describe('<Login />', () => {
|
||||
err.response = { status: 401, message: 'problem' };
|
||||
return Promise.reject(err);
|
||||
});
|
||||
expect(atLogin.state().loading).toBe(false);
|
||||
expect(atLogin.state().isValidPassword).toBe(true);
|
||||
atLogin.setState({ username: 'unamee', password: 'pwordd' });
|
||||
expect(awxLogin.state().isLoading).toBe(false);
|
||||
expect(awxLogin.state().isInputValid).toBe(true);
|
||||
awxLogin.setState({ username: 'unamee', password: 'pwordd' });
|
||||
submitButton.simulate('click');
|
||||
expect(api.login).toHaveBeenCalledTimes(1);
|
||||
expect(api.login).toHaveBeenCalledWith('unamee', 'pwordd');
|
||||
expect(atLogin.state().loading).toBe(true);
|
||||
expect(awxLogin.state().isLoading).toBe(true);
|
||||
await asyncFlush();
|
||||
expect(atLogin.state().isValidPassword).toBe(false);
|
||||
expect(atLogin.state().loading).toBe(false);
|
||||
expect(awxLogin.state().isInputValid).toBe(false);
|
||||
expect(awxLogin.state().isLoading).toBe(false);
|
||||
});
|
||||
|
||||
test('submit calls api.login handles non-401 error', async () => {
|
||||
@ -153,20 +155,20 @@ describe('<Login />', () => {
|
||||
err.response = { status: 500, message: 'problem' };
|
||||
return Promise.reject(err);
|
||||
});
|
||||
expect(atLogin.state().loading).toBe(false);
|
||||
atLogin.setState({ username: 'unamee', password: 'pwordd' });
|
||||
expect(awxLogin.state().isLoading).toBe(false);
|
||||
awxLogin.setState({ username: 'unamee', password: 'pwordd' });
|
||||
submitButton.simulate('click');
|
||||
expect(api.login).toHaveBeenCalledTimes(1);
|
||||
expect(api.login).toHaveBeenCalledWith('unamee', 'pwordd');
|
||||
expect(atLogin.state().loading).toBe(true);
|
||||
expect(awxLogin.state().isLoading).toBe(true);
|
||||
await asyncFlush();
|
||||
expect(atLogin.state().loading).toBe(false);
|
||||
expect(awxLogin.state().isLoading).toBe(false);
|
||||
});
|
||||
|
||||
test('render Redirect to / when already authenticated', () => {
|
||||
api.isAuthenticated = jest.fn();
|
||||
api.isAuthenticated.mockReturnValue(true);
|
||||
loginWrapper = shallow(<AtLogin />);
|
||||
loginWrapper = shallow(<AWXLogin api={api} />);
|
||||
const redirectElem = loginWrapper.find('Redirect');
|
||||
expect(redirectElem.length).toBe(1);
|
||||
expect(redirectElem.props().to).toBe('/');
|
||||
|
||||
@ -3,9 +3,8 @@ import getTabName from '../../../src/pages/Organizations/utils';
|
||||
describe('getTabName', () => {
|
||||
test('returns tab name', () => {
|
||||
expect(getTabName('details')).toBe('Details');
|
||||
expect(getTabName('users')).toBe('Users');
|
||||
expect(getTabName('access')).toBe('Access');
|
||||
expect(getTabName('teams')).toBe('Teams');
|
||||
expect(getTabName('admins')).toBe('Admins');
|
||||
expect(getTabName('notifications')).toBe('Notifications');
|
||||
expect(getTabName('unknown')).toBe('');
|
||||
expect(getTabName()).toBe('');
|
||||
|
||||
220
package-lock.json
generated
220
package-lock.json
generated
@ -1311,21 +1311,22 @@
|
||||
"integrity": "sha512-sIRfo/tk4NSnaRwHIHLUf4XoqzNNa4MMa8ZWivzzSfdZ5pCbgvZtyEUqKnQAEH6zuaCM9S8HyhxcNXnm/xaYaQ=="
|
||||
},
|
||||
"@patternfly/react-core": {
|
||||
"version": "1.37.2",
|
||||
"resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-1.37.2.tgz",
|
||||
"integrity": "sha512-zzHwqGEsRWzw9uRkbrf6PmUpcl6EMxQSbUJ1zmv7Ryc32CcSMgrDL4ZA3x/tf4TAYTMRBKUK3O8S5veRjxpFuw==",
|
||||
"version": "1.43.5",
|
||||
"resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-1.43.5.tgz",
|
||||
"integrity": "sha512-xO37/q5BJEdGvAoPllm/gbcwCtW7t0Ae7mKm5UU7d4i5bycEjd0UwJacYxCA6GFTwxN5kzn61XEpAUPY49U3pA==",
|
||||
"requires": {
|
||||
"@patternfly/react-icons": "^2.9.1",
|
||||
"@patternfly/react-icons": "^2.9.5",
|
||||
"@patternfly/react-styles": "^2.3.0",
|
||||
"@patternfly/react-tokens": "^1.0.0",
|
||||
"@tippy.js/react": "^1.1.1",
|
||||
"exenv": "^1.2.2",
|
||||
"focus-trap-react": "^4.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@patternfly/react-icons": {
|
||||
"version": "2.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-2.9.1.tgz",
|
||||
"integrity": "sha512-CBTpGXvqr91rBpxeb5/l2BimrtRlMkBKnIOTgX7V44MIIq3YE3P6A6CQK0fgIH1HGvCdiNf5sXbQz9xp+pB/3A=="
|
||||
"version": "2.9.5",
|
||||
"resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-2.9.5.tgz",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "10.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.1.tgz",
|
||||
@ -2110,9 +2120,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"array-flatten": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.1.tgz",
|
||||
"integrity": "sha1-Qmu52oQJDBg42BLIFQryCoMx4pY=",
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz",
|
||||
"integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==",
|
||||
"dev": true
|
||||
},
|
||||
"array-includes": {
|
||||
@ -5819,7 +5829,7 @@
|
||||
},
|
||||
"finalhandler": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "http://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz",
|
||||
"integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
@ -6814,9 +6824,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"handle-thing": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-1.2.5.tgz",
|
||||
"integrity": "sha1-/Xqtcmvxpf0W38KbL3pmAdJxOcQ=",
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.0.tgz",
|
||||
"integrity": "sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ==",
|
||||
"dev": true
|
||||
},
|
||||
"handlebars": {
|
||||
@ -7053,7 +7063,7 @@
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
@ -7138,7 +7148,7 @@
|
||||
},
|
||||
"http-errors": {
|
||||
"version": "1.6.3",
|
||||
"resolved": "http://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
|
||||
"integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
@ -7167,7 +7177,7 @@
|
||||
},
|
||||
"http-proxy-middleware": {
|
||||
"version": "0.18.0",
|
||||
"resolved": "http://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.18.0.tgz",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.18.0.tgz",
|
||||
"integrity": "sha512-Fs25KVMPAIIcgjMZkVHJoKg9VcXcC1C8yb9JUgeDvVXY0S/zgVIhMb+qVswDIgtJe2DfckMSY2d6TuTEutlk6Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
@ -11105,10 +11115,15 @@
|
||||
"integrity": "sha512-Vy9eH1dRD9wHjYt/QqXcTz+RnX/zg53xK+KljFSX30PvdDMb2z+c6uDUeblUGqqJgz3QFsdlA0IJvHziPmWtQg==",
|
||||
"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": {
|
||||
"version": "1.0.19",
|
||||
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.19.tgz",
|
||||
"integrity": "sha512-23aeQKW9KgHe6citUrG3r9HjeX6vls0h713TAa+CwTKZwNIr/pD2ApaxYF4Um3ZZyq4ar+Siv3+fhoHaIwSOSw==",
|
||||
"version": "1.0.20",
|
||||
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.20.tgz",
|
||||
"integrity": "sha512-Yxe4mTyDzTd59PZJY4ojZR8F+E5e97iq2ZOHPz3HDgSvYC5siNad2tLooQ5y5QHyQhc3xVqvyk/eNA3wuoa7Sw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"async": "^1.5.2",
|
||||
@ -11118,7 +11133,7 @@
|
||||
"dependencies": {
|
||||
"async": {
|
||||
"version": "1.5.2",
|
||||
"resolved": "http://registry.npmjs.org/async/-/async-1.5.2.tgz",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz",
|
||||
"integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=",
|
||||
"dev": true
|
||||
}
|
||||
@ -13326,59 +13341,79 @@
|
||||
"dev": true
|
||||
},
|
||||
"spdy": {
|
||||
"version": "3.4.7",
|
||||
"resolved": "https://registry.npmjs.org/spdy/-/spdy-3.4.7.tgz",
|
||||
"integrity": "sha1-Qv9B7OXMD5mjpsKKq7c/XDsDrLw=",
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.0.tgz",
|
||||
"integrity": "sha512-ot0oEGT/PGUpzf/6uk4AWLqkq+irlqHXkrdbk51oWONh3bxQmBuljxPNl66zlRRcIJStWq0QkLUCPOPjgjvU0Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"debug": "^2.6.8",
|
||||
"handle-thing": "^1.2.5",
|
||||
"debug": "^4.1.0",
|
||||
"handle-thing": "^2.0.0",
|
||||
"http-deceiver": "^1.2.7",
|
||||
"safe-buffer": "^5.0.1",
|
||||
"select-hose": "^2.0.0",
|
||||
"spdy-transport": "^2.0.18"
|
||||
"spdy-transport": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
|
||||
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
|
||||
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"spdy-transport": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-2.1.0.tgz",
|
||||
"integrity": "sha512-bpUeGpZcmZ692rrTiqf9/2EUakI6/kXX1Rpe0ib/DyOzbiexVfXkw6GnvI9hVGvIwVaUhkaBojjCZwLNRGQg1g==",
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz",
|
||||
"integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"debug": "^2.6.8",
|
||||
"detect-node": "^2.0.3",
|
||||
"debug": "^4.1.0",
|
||||
"detect-node": "^2.0.4",
|
||||
"hpack.js": "^2.1.6",
|
||||
"obuf": "^1.1.1",
|
||||
"readable-stream": "^2.2.9",
|
||||
"safe-buffer": "^5.0.1",
|
||||
"wbuf": "^1.7.2"
|
||||
"obuf": "^1.1.2",
|
||||
"readable-stream": "^3.0.6",
|
||||
"wbuf": "^1.7.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
|
||||
"debug": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
|
||||
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
|
||||
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
|
||||
"dev": true
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "2.3.6",
|
||||
"resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
|
||||
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.1.1.tgz",
|
||||
"integrity": "sha512-DkN66hPyqDhnIQ6Jcsvx9bFjhw214O4poMBcIMgPVpQvNy9a0e0Uhg5SqySyDKAmUlwt8LonTBz1ezOnM8pUdA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.2.0.tgz",
|
||||
"integrity": "sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
@ -13891,6 +13926,14 @@
|
||||
"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": {
|
||||
"version": "0.0.33",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
|
||||
@ -14395,9 +14438,9 @@
|
||||
}
|
||||
},
|
||||
"url-parse": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.3.tgz",
|
||||
"integrity": "sha512-rh+KuAW36YKo0vClhQzLLveoj8FwPJNu65xLb7Mrt+eZht0IPT0IXgSv8gcMegZ6NvjJUALf6Mf25POlMwD1Fw==",
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.4.tgz",
|
||||
"integrity": "sha512-/92DTTorg4JjktLNLe6GPS2/RvAd/RGr6LuktmWSMLEOa6rjnlrFXNgSbSmkNvCoL2T028A0a1JaJLzRMlFoHg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"querystringify": "^2.0.0",
|
||||
@ -15158,17 +15201,17 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"mime": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-2.3.1.tgz",
|
||||
"integrity": "sha512-OEUllcVoydBHGN1z84yfQDimn58pZNNNXgZlHXSboxMlFvgI6MXSWpWKpFRra7H1HxpVhHTkrghfRW49k6yjeg==",
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-2.4.0.tgz",
|
||||
"integrity": "sha512-ikBcWwyqXQSHKtciCcctu9YfPbFYZ4+gbHEmE0Q8jzcTYQg5dHCr3g2wwAZjPoJfQVXZq6KXAjpXOTf5/cjT7w==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"webpack-dev-server": {
|
||||
"version": "3.1.10",
|
||||
"resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.1.10.tgz",
|
||||
"integrity": "sha512-RqOAVjfqZJtQcB0LmrzJ5y4Jp78lv9CK0MZ1YJDTaTmedMZ9PU9FLMQNrMCfVu8hHzaVLVOJKBlGEHMN10z+ww==",
|
||||
"version": "3.1.14",
|
||||
"resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.1.14.tgz",
|
||||
"integrity": "sha512-mGXDgz5SlTxcF3hUpfC8hrQ11yhAttuUQWf1Wmb+6zo3x6rb7b9mIfuQvAPLdfDRCGRGvakBWHdHOa0I9p/EVQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-html": "0.0.7",
|
||||
@ -15190,12 +15233,14 @@
|
||||
"portfinder": "^1.0.9",
|
||||
"schema-utils": "^1.0.0",
|
||||
"selfsigned": "^1.9.1",
|
||||
"semver": "^5.6.0",
|
||||
"serve-index": "^1.7.2",
|
||||
"sockjs": "0.3.19",
|
||||
"sockjs-client": "1.3.0",
|
||||
"spdy": "^3.4.1",
|
||||
"spdy": "^4.0.0",
|
||||
"strip-ansi": "^3.0.0",
|
||||
"supports-color": "^5.1.0",
|
||||
"url": "^0.11.0",
|
||||
"webpack-dev-middleware": "3.4.0",
|
||||
"webpack-log": "^2.0.0",
|
||||
"yargs": "12.0.2"
|
||||
@ -15247,13 +15292,13 @@
|
||||
}
|
||||
},
|
||||
"execa": {
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/execa/-/execa-0.10.0.tgz",
|
||||
"integrity": "sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw==",
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz",
|
||||
"integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"cross-spawn": "^6.0.0",
|
||||
"get-stream": "^3.0.0",
|
||||
"get-stream": "^4.0.0",
|
||||
"is-stream": "^1.1.0",
|
||||
"npm-run-path": "^2.0.0",
|
||||
"p-finally": "^1.0.0",
|
||||
@ -15270,6 +15315,15 @@
|
||||
"locate-path": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"get-stream": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
|
||||
"integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"pump": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"globby": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz",
|
||||
@ -15344,20 +15398,20 @@
|
||||
"dev": true
|
||||
},
|
||||
"os-locale": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.0.1.tgz",
|
||||
"integrity": "sha512-7g5e7dmXPtzcP4bgsZ8ixDVqA7oWYuEz4lOSujeWyliPai4gfVDiFIcwBg3aGCPnmSGfzOKTK3ccPn0CKv3DBw==",
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz",
|
||||
"integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"execa": "^0.10.0",
|
||||
"execa": "^1.0.0",
|
||||
"lcid": "^2.0.0",
|
||||
"mem": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"p-limit": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.0.0.tgz",
|
||||
"integrity": "sha512-fl5s52lI5ahKCernzzIyAP0QAZbGIovtVHGwpcu1Jr/EpzLVDI2myISHwGqK7m8uQFugVWSrbxH7XnhGtvEc+A==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.1.0.tgz",
|
||||
"integrity": "sha512-NhURkNcrVB+8hNfLuysU8enY5xn2KXphsHBaC2YmRNTZRc7RWusw6apSpdEj3jo4CMb6W9nrF6tTnsJsJeyu6g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"p-try": "^2.0.0"
|
||||
@ -15393,6 +15447,16 @@
|
||||
"find-up": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"pump": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
||||
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"end-of-stream": "^1.1.0",
|
||||
"once": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||
@ -15444,9 +15508,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-colors": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.1.tgz",
|
||||
"integrity": "sha512-Xt+zb6nqgvV9SWAVp0EG3lRsHcbq5DDgqjPPz6pwgtj6RKz65zGXMNa82oJfOSBA/to6GmRP7Dr+6o+kbApTzQ==",
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz",
|
||||
"integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
|
||||
@ -43,12 +43,12 @@
|
||||
"style-loader": "^0.23.0",
|
||||
"webpack": "^4.23.1",
|
||||
"webpack-cli": "^3.0.8",
|
||||
"webpack-dev-server": "^3.1.4"
|
||||
"webpack-dev-server": "^3.1.14"
|
||||
},
|
||||
"dependencies": {
|
||||
"@lingui/react": "^2.7.2",
|
||||
"@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-styles": "^2.3.0",
|
||||
"@patternfly/react-tokens": "^1.9.0",
|
||||
|
||||
347
src/App.jsx
347
src/App.jsx
@ -1,261 +1,148 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import { ConfigContext } from './context';
|
||||
|
||||
import { I18nProvider, I18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import { global_breakpoint_md } from '@patternfly/react-tokens';
|
||||
import {
|
||||
Redirect,
|
||||
Switch,
|
||||
withRouter
|
||||
} from 'react-router-dom';
|
||||
|
||||
import {
|
||||
BackgroundImage,
|
||||
BackgroundImageSrc,
|
||||
Nav,
|
||||
NavList,
|
||||
Page,
|
||||
PageHeader,
|
||||
PageSidebar,
|
||||
Toolbar,
|
||||
ToolbarGroup,
|
||||
ToolbarItem
|
||||
} from '@patternfly/react-core';
|
||||
import { global_breakpoint_md as breakpointMd } from '@patternfly/react-tokens';
|
||||
|
||||
import api from './api';
|
||||
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 About from './components/About';
|
||||
import NavExpandableGroup from './components/NavExpandableGroup';
|
||||
import TowerLogo from './components/TowerLogo';
|
||||
import PageHeaderToolbar from './components/PageHeaderToolbar';
|
||||
import { ConfigContext } from './context';
|
||||
|
||||
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';
|
||||
|
||||
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) {
|
||||
class App extends Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
const isNavOpen = typeof window !== 'undefined' && window.innerWidth >= parseInt(breakpointMd.value, 10);
|
||||
this.state = {
|
||||
isNavOpen,
|
||||
config: {},
|
||||
error: false,
|
||||
};
|
||||
}
|
||||
// initialize with a closed navbar if window size is small
|
||||
const isNavOpen = typeof window !== 'undefined'
|
||||
&& window.innerWidth >= parseInt(global_breakpoint_md.value, 10);
|
||||
|
||||
onNavToggle = () => {
|
||||
this.setState(({ isNavOpen }) => ({ isNavOpen: !isNavOpen }));
|
||||
this.state = {
|
||||
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.onNavToggle = this.onNavToggle.bind(this);
|
||||
};
|
||||
|
||||
onLogoClick = () => {
|
||||
this.setState({ activeGroup: 'views_group' });
|
||||
componentDidMount () {
|
||||
this.fetchConfig();
|
||||
}
|
||||
|
||||
onDevLogout = async () => {
|
||||
await api.get(API_LOGOUT);
|
||||
this.setState({ activeGroup: 'views_group', activeItem: 'views_group_dashboard' });
|
||||
}
|
||||
async fetchConfig () {
|
||||
const { api } = this.props;
|
||||
|
||||
async componentDidMount() {
|
||||
// Grab our config data from the API and store in state
|
||||
try {
|
||||
const { data } = await api.get(API_CONFIG);
|
||||
this.setState({ config: data });
|
||||
} catch (error) {
|
||||
this.setState({ error });
|
||||
const { data: { ansible_version, custom_virtualenvs, version } } = await api.getConfig();
|
||||
this.setState({ ansible_version, custom_virtualenvs, version });
|
||||
} catch (err) {
|
||||
this.setState({ ansible_version: null, custom_virtualenvs: null, version: null });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isNavOpen, config } = this.state;
|
||||
const { logo, loginInfo, history } = this.props;
|
||||
async onLogout () {
|
||||
const { api } = this.props;
|
||||
|
||||
const PageToolbar = (
|
||||
<Toolbar>
|
||||
<ToolbarGroup>
|
||||
<ToolbarItem>
|
||||
<HelpDropdown />
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<LogoutButton onDevLogout={() => this.onDevLogout()} />
|
||||
</ToolbarItem>
|
||||
</ToolbarGroup>
|
||||
</Toolbar>
|
||||
);
|
||||
await api.logout();
|
||||
window.location.replace('/#/login')
|
||||
}
|
||||
|
||||
onAboutModalOpen () {
|
||||
this.setState({ isAboutModalOpen: true });
|
||||
}
|
||||
|
||||
onAboutModalClose () {
|
||||
this.setState({ isAboutModalOpen: false });
|
||||
}
|
||||
|
||||
onNavToggle () {
|
||||
this.setState(({ isNavOpen }) => ({ isNavOpen: !isNavOpen }));
|
||||
}
|
||||
|
||||
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 (
|
||||
<I18nProvider language={languageWithoutRegionCode} catalogs={catalogs}>
|
||||
<Fragment>
|
||||
<BackgroundImage
|
||||
src={{
|
||||
[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'
|
||||
}}
|
||||
/>
|
||||
<Fragment>
|
||||
<Page
|
||||
usecondensed="True"
|
||||
header={(
|
||||
<PageHeader
|
||||
showNavToggle
|
||||
onNavToggle={this.onNavToggle}
|
||||
logo={<TowerLogo linkTo="/"/>}
|
||||
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}>
|
||||
<Switch>
|
||||
<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>
|
||||
{render && render({ routeGroups })}
|
||||
</ConfigContext.Provider>
|
||||
</Fragment>
|
||||
</I18nProvider>
|
||||
</Page>
|
||||
<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';
|
||||
|
||||
import * as endpoints from './endpoints';
|
||||
|
||||
const CSRF_COOKIE_NAME = 'csrftoken';
|
||||
const CSRF_HEADER_NAME = 'X-CSRFToken';
|
||||
const API_ROOT = '/api/';
|
||||
const API_LOGIN = `${API_ROOT}login/`;
|
||||
const API_LOGOUT = `${API_ROOT}logout/`;
|
||||
const API_V2 = `${API_ROOT}v2/`;
|
||||
const API_CONFIG = `${API_V2}config/`;
|
||||
const API_ORGANIZATIONS = `${API_V2}organizations/`;
|
||||
|
||||
const LOGIN_CONTENT_TYPE = 'application/x-www-form-urlencoded';
|
||||
|
||||
class APIClient {
|
||||
constructor () {
|
||||
this.http = axios.create({
|
||||
xsrfCookieName: CSRF_COOKIE_NAME,
|
||||
xsrfHeaderName: CSRF_HEADER_NAME,
|
||||
});
|
||||
}
|
||||
|
||||
/* eslint class-methods-use-this: ["error", { "exceptMethods": ["getCookie"] }] */
|
||||
getCookie () {
|
||||
static getCookie () {
|
||||
return document.cookie;
|
||||
}
|
||||
|
||||
isAuthenticated () {
|
||||
let authenticated = false;
|
||||
constructor (httpAdapter) {
|
||||
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) {
|
||||
authenticated = parsed.pop().split(';').shift() === 'true';
|
||||
@ -32,7 +29,7 @@ class APIClient {
|
||||
return authenticated;
|
||||
}
|
||||
|
||||
async login (username, password, redirect = endpoints.API_CONFIG) {
|
||||
async login (username, password, redirect = API_CONFIG) {
|
||||
const un = encodeURIComponent(username);
|
||||
const pw = encodeURIComponent(password);
|
||||
const next = encodeURIComponent(redirect);
|
||||
@ -40,13 +37,37 @@ class APIClient {
|
||||
const data = `username=${un}&password=${pw}&next=${next}`;
|
||||
const headers = { 'Content-Type': LOGIN_CONTENT_TYPE };
|
||||
|
||||
await this.http.get(endpoints.API_LOGIN, { headers });
|
||||
await this.http.post(endpoints.API_LOGIN, data, { headers });
|
||||
await this.http.get(API_LOGIN, { 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;
|
||||
|
||||
@ -56,7 +56,7 @@
|
||||
// page header overrides
|
||||
//
|
||||
|
||||
.pf-l-page__main-section.pf-m-condensed {
|
||||
.pf-c-page__main-section.pf-m-condensed {
|
||||
padding-top: 16px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
@ -13,10 +13,8 @@ import heroImg from '@patternfly/patternfly-next/assets/images/pfbg_992.jpg';
|
||||
import brandImg from '../../images/tower-logo-white.svg';
|
||||
import logoImg from '../../images/tower-logo-login.svg';
|
||||
|
||||
import { ConfigContext } from '../context';
|
||||
|
||||
class About extends React.Component {
|
||||
createSpeechBubble = (version) => {
|
||||
static createSpeechBubble (version) {
|
||||
let text = `Tower ${version}`;
|
||||
let top = '';
|
||||
let bottom = '';
|
||||
@ -33,52 +31,56 @@ class About extends React.Component {
|
||||
return top + text + bottom;
|
||||
}
|
||||
|
||||
handleModalToggle = () => {
|
||||
const { onAboutModalClose } = this.props;
|
||||
onAboutModalClose();
|
||||
};
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.createSpeechBubble = this.constructor.createSpeechBubble.bind(this);
|
||||
}
|
||||
|
||||
render () {
|
||||
const { isOpen } = this.props;
|
||||
const {
|
||||
ansible_version,
|
||||
version,
|
||||
isOpen,
|
||||
onClose
|
||||
} = this.props;
|
||||
|
||||
const speechBubble = this.createSpeechBubble(version);
|
||||
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<ConfigContext.Consumer>
|
||||
{({ ansible_version, version }) => (
|
||||
<AboutModal
|
||||
isOpen={isOpen}
|
||||
onClose={this.handleModalToggle}
|
||||
productName="Ansible Tower"
|
||||
trademark={i18n._(t`Copyright 2018 Red Hat, Inc.`)}
|
||||
brandImageSrc={brandImg}
|
||||
brandImageAlt={i18n._(t`Brand Image`)}
|
||||
logoImageSrc={logoImg}
|
||||
logoImageAlt={i18n._(t`AboutModal Logo`)}
|
||||
heroImageSrc={heroImg}
|
||||
>
|
||||
<pre>
|
||||
{this.createSpeechBubble(version)}
|
||||
{`
|
||||
<AboutModal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
productName="Ansible Tower"
|
||||
trademark={i18n._(t`Copyright 2018 Red Hat, Inc.`)}
|
||||
brandImageSrc={brandImg}
|
||||
brandImageAlt={i18n._(t`Brand Image`)}
|
||||
logoImageSrc={logoImg}
|
||||
logoImageAlt={i18n._(t`AboutModal Logo`)}
|
||||
heroImageSrc={heroImg}
|
||||
>
|
||||
<pre>
|
||||
{ speechBubble }
|
||||
{`
|
||||
\\
|
||||
\\ ^__^
|
||||
\\ ^__^
|
||||
(oo)\\_______
|
||||
(__) A )\\
|
||||
||----w |
|
||||
|| ||
|
||||
`}
|
||||
</pre>
|
||||
|
||||
<TextContent>
|
||||
<TextList component="dl">
|
||||
<TextListItem component="dt">
|
||||
<Trans>Ansible Version</Trans>
|
||||
</TextListItem>
|
||||
<TextListItem component="dd">{ansible_version}</TextListItem>
|
||||
</TextList>
|
||||
</TextContent>
|
||||
</AboutModal>
|
||||
)}
|
||||
</ConfigContext.Consumer>
|
||||
</pre>
|
||||
<TextContent>
|
||||
<TextList component="dl">
|
||||
<TextListItem component="dt">
|
||||
<Trans>Ansible Version</Trans>
|
||||
</TextListItem>
|
||||
<TextListItem component="dd">{ ansible_version }</TextListItem>
|
||||
</TextList>
|
||||
</TextContent>
|
||||
</AboutModal>
|
||||
)}
|
||||
</I18n>
|
||||
);
|
||||
|
||||
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,47 +43,66 @@ class DataListToolbar extends React.Component {
|
||||
searchKey: sortedColumnKey,
|
||||
searchValue: '',
|
||||
};
|
||||
|
||||
this.handleSearchInputChange = this.handleSearchInputChange.bind(this);
|
||||
this.onSortDropdownToggle = this.onSortDropdownToggle.bind(this);
|
||||
this.onSortDropdownSelect = this.onSortDropdownSelect.bind(this);
|
||||
this.onSearchDropdownToggle = this.onSearchDropdownToggle.bind(this);
|
||||
this.onSearchDropdownSelect = this.onSearchDropdownSelect.bind(this);
|
||||
this.onSearch = this.onSearch.bind(this);
|
||||
this.onSort = this.onSort.bind(this);
|
||||
}
|
||||
|
||||
handleSearchInputChange = searchValue => {
|
||||
handleSearchInputChange (searchValue) {
|
||||
this.setState({ searchValue });
|
||||
};
|
||||
}
|
||||
|
||||
onSortDropdownToggle = isSortDropdownOpen => {
|
||||
onSortDropdownToggle (isSortDropdownOpen) {
|
||||
this.setState({ isSortDropdownOpen });
|
||||
};
|
||||
}
|
||||
|
||||
onSortDropdownSelect = ({ target }) => {
|
||||
onSortDropdownSelect ({ target }) {
|
||||
const { columns, onSort, sortOrder } = this.props;
|
||||
const { innerText } = target;
|
||||
|
||||
const [{ key }] = columns.filter(({ name }) => name === target.innerText);
|
||||
const [{ key: searchKey }] = columns.filter(({ name }) => name === innerText);
|
||||
|
||||
this.setState({ isSortDropdownOpen: false });
|
||||
onSort(searchKey, sortOrder);
|
||||
}
|
||||
|
||||
onSort(key, sortOrder);
|
||||
};
|
||||
|
||||
onSearchDropdownToggle = isSearchDropdownOpen => {
|
||||
onSearchDropdownToggle (isSearchDropdownOpen) {
|
||||
this.setState({ isSearchDropdownOpen });
|
||||
};
|
||||
}
|
||||
|
||||
onSearchDropdownSelect = ({ target }) => {
|
||||
onSearchDropdownSelect ({ target }) {
|
||||
const { columns } = this.props;
|
||||
const { innerText } = target;
|
||||
|
||||
const targetName = target.innerText;
|
||||
const [{ key }] = columns.filter(({ name }) => name === targetName);
|
||||
const [{ key: searchKey }] = columns.filter(({ name }) => name === innerText);
|
||||
this.setState({ isSearchDropdownOpen: false, searchKey });
|
||||
}
|
||||
|
||||
this.setState({ isSearchDropdownOpen: false, searchKey: key });
|
||||
};
|
||||
onSearch () {
|
||||
const { searchValue } = this.state;
|
||||
const { onSearch } = this.props;
|
||||
|
||||
onSearch(searchValue);
|
||||
}
|
||||
|
||||
onSort () {
|
||||
const { onSort, sortedColumnKey, sortOrder } = this.props;
|
||||
const newSortOrder = sortOrder === 'ascending' ? 'descending' : 'ascending';
|
||||
|
||||
onSort(sortedColumnKey, newSortOrder);
|
||||
}
|
||||
|
||||
render () {
|
||||
const { up } = DropdownPosition;
|
||||
const {
|
||||
columns,
|
||||
isAllSelected,
|
||||
onSearch,
|
||||
onSelectAll,
|
||||
onSort,
|
||||
sortedColumnKey,
|
||||
sortOrder,
|
||||
addUrl,
|
||||
@ -97,29 +116,15 @@ class DataListToolbar extends React.Component {
|
||||
searchValue,
|
||||
} = this.state;
|
||||
|
||||
const [searchColumn] = columns
|
||||
.filter(({ key }) => key === searchKey);
|
||||
const searchColumnName = searchColumn.name;
|
||||
|
||||
const [sortedColumn] = columns
|
||||
const [{ name: searchColumnName }] = columns.filter(({ key }) => key === searchKey);
|
||||
const [{ name: sortedColumnName, isNumeric }] = columns
|
||||
.filter(({ key }) => key === sortedColumnKey);
|
||||
const sortedColumnName = sortedColumn.name;
|
||||
const isSortNumeric = sortedColumn.isNumeric;
|
||||
const displayedSortIcon = () => {
|
||||
let icon;
|
||||
if (sortOrder === 'ascending') {
|
||||
icon = isSortNumeric ? (<SortNumericUpIcon />) : (<SortAlphaUpIcon />);
|
||||
} else {
|
||||
icon = isSortNumeric ? (<SortNumericDownIcon />) : (<SortAlphaDownIcon />);
|
||||
}
|
||||
return icon;
|
||||
};
|
||||
|
||||
const searchDropdownItems = columns
|
||||
.filter(({ key }) => key !== searchKey)
|
||||
.map(({ key, name }) => (
|
||||
<DropdownItem key={key} component="button">
|
||||
{ name }
|
||||
{name}
|
||||
</DropdownItem>
|
||||
));
|
||||
|
||||
@ -127,17 +132,26 @@ class DataListToolbar extends React.Component {
|
||||
.filter(({ key, isSortable }) => isSortable && key !== sortedColumnKey)
|
||||
.map(({ key, name }) => (
|
||||
<DropdownItem key={key} component="button">
|
||||
{ name }
|
||||
{name}
|
||||
</DropdownItem>
|
||||
));
|
||||
|
||||
let SortIcon;
|
||||
if (isNumeric) {
|
||||
SortIcon = sortOrder === 'ascending' ? SortNumericUpIcon : SortNumericDownIcon;
|
||||
} else {
|
||||
SortIcon = sortOrder === 'ascending' ? SortAlphaUpIcon : SortAlphaDownIcon;
|
||||
}
|
||||
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<div className="awx-toolbar">
|
||||
<Level>
|
||||
<LevelItem>
|
||||
<Toolbar style={{ marginLeft: '20px' }}>
|
||||
<Toolbar
|
||||
style={{ marginLeft: '20px' }}
|
||||
>
|
||||
<ToolbarGroup>
|
||||
<ToolbarItem>
|
||||
<Checkbox
|
||||
@ -152,6 +166,7 @@ class DataListToolbar extends React.Component {
|
||||
<ToolbarItem>
|
||||
<div className="pf-c-input-group">
|
||||
<Dropdown
|
||||
className="searchKeyDropdown"
|
||||
onToggle={this.onSearchDropdownToggle}
|
||||
onSelect={this.onSearchDropdownSelect}
|
||||
direction={up}
|
||||
@ -160,7 +175,7 @@ class DataListToolbar extends React.Component {
|
||||
<DropdownToggle
|
||||
onToggle={this.onSearchDropdownToggle}
|
||||
>
|
||||
{ searchColumnName }
|
||||
{searchColumnName}
|
||||
</DropdownToggle>
|
||||
)}
|
||||
dropdownItems={searchDropdownItems}
|
||||
@ -174,14 +189,16 @@ class DataListToolbar extends React.Component {
|
||||
<Button
|
||||
variant="tertiary"
|
||||
aria-label={i18n._(t`Search`)}
|
||||
onClick={() => onSearch(searchValue)}
|
||||
onClick={this.onSearch}
|
||||
>
|
||||
<i className="fas fa-search" aria-hidden="true" />
|
||||
</Button>
|
||||
</div>
|
||||
</ToolbarItem>
|
||||
</ToolbarGroup>
|
||||
<ToolbarGroup>
|
||||
<ToolbarGroup
|
||||
className="sortDropdownGroup"
|
||||
>
|
||||
<ToolbarItem>
|
||||
<Dropdown
|
||||
onToggle={this.onSortDropdownToggle}
|
||||
@ -192,7 +209,7 @@ class DataListToolbar extends React.Component {
|
||||
<DropdownToggle
|
||||
onToggle={this.onSortDropdownToggle}
|
||||
>
|
||||
{ sortedColumnName }
|
||||
{sortedColumnName}
|
||||
</DropdownToggle>
|
||||
)}
|
||||
dropdownItems={sortDropdownItems}
|
||||
@ -200,23 +217,29 @@ class DataListToolbar extends React.Component {
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<Button
|
||||
onClick={() => onSort(sortedColumnKey, sortOrder === 'ascending' ? 'descending' : 'ascending')}
|
||||
onClick={this.onSort}
|
||||
variant="plain"
|
||||
aria-label={i18n._(t`Sort`)}
|
||||
>
|
||||
{displayedSortIcon()}
|
||||
<SortIcon/>
|
||||
</Button>
|
||||
</ToolbarItem>
|
||||
</ToolbarGroup>
|
||||
{ showExpandCollapse && (
|
||||
{showExpandCollapse && (
|
||||
<ToolbarGroup>
|
||||
<ToolbarItem>
|
||||
<Button variant="plain" aria-label={i18n._(t`Expand`)}>
|
||||
<Button
|
||||
variant="plain"
|
||||
aria-label={i18n._(t`Expand`)}
|
||||
>
|
||||
<BarsIcon />
|
||||
</Button>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<Button variant="plain" aria-label={i18n._(t`Collapse`)}>
|
||||
<Button
|
||||
variant="plain"
|
||||
aria-label={i18n._(t`Collapse`)}
|
||||
>
|
||||
<EqualsIcon />
|
||||
</Button>
|
||||
</ToolbarItem>
|
||||
@ -225,14 +248,23 @@ class DataListToolbar extends React.Component {
|
||||
</Toolbar>
|
||||
</LevelItem>
|
||||
<LevelItem>
|
||||
<Tooltip message={i18n._(t`Delete`)} position="top">
|
||||
<Button variant="plain" aria-label={i18n._(t`Delete`)}>
|
||||
<Tooltip
|
||||
message={i18n._(t`Delete`)}
|
||||
position="top"
|
||||
>
|
||||
<Button
|
||||
variant="plain"
|
||||
aria-label={i18n._(t`Delete`)}
|
||||
>
|
||||
<TrashAltIcon />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
{addUrl && (
|
||||
<Link to={addUrl}>
|
||||
<Button variant="primary" aria-label={i18n._(t`Add`)}>
|
||||
<Button
|
||||
variant="primary"
|
||||
aria-label={i18n._(t`Add`)}
|
||||
>
|
||||
<PlusIcon />
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
@ -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
|
||||
// an array of url paths associated with any NavItem component rendered by this component.
|
||||
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;
|
||||
|
||||
return history.location.pathname.startsWith(path);
|
||||
};
|
||||
}
|
||||
|
||||
render () {
|
||||
const { routes, groupId, staticContext, ...rest } = this.props;
|
||||
const { groupId, groupTitle, routes } = this.props;
|
||||
const isActive = this.isActiveGroup();
|
||||
|
||||
return (
|
||||
@ -33,7 +38,7 @@ class NavExpandableGroup extends Component {
|
||||
isActive={isActive}
|
||||
isExpanded={isActive}
|
||||
groupId={groupId}
|
||||
{...rest}
|
||||
title={groupTitle}
|
||||
>
|
||||
{routes.map(({ path, title }) => (
|
||||
<NavItem
|
||||
|
||||
129
src/components/PageHeaderToolbar.jsx
Normal file
129
src/components/PageHeaderToolbar.jsx
Normal file
@ -0,0 +1,129 @@
|
||||
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';
|
||||
|
||||
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.onUserSelect = this.onUserSelect.bind(this);
|
||||
this.onUserToggle = this.onUserToggle.bind(this);
|
||||
}
|
||||
|
||||
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}
|
||||
>
|
||||
{i18n._(t`Logout`)}
|
||||
</DropdownItem>
|
||||
]}
|
||||
/>
|
||||
</ToolbarItem>
|
||||
</ToolbarGroup>
|
||||
</Toolbar>
|
||||
)}
|
||||
</I18n>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default PageHeaderToolbar;
|
||||
@ -7,14 +7,9 @@ import {
|
||||
DropdownDirection,
|
||||
DropdownItem,
|
||||
DropdownToggle,
|
||||
Form,
|
||||
FormGroup,
|
||||
Level,
|
||||
LevelItem,
|
||||
TextInput,
|
||||
Toolbar,
|
||||
ToolbarGroup,
|
||||
ToolbarItem,
|
||||
Split,
|
||||
SplitItem,
|
||||
} from '@patternfly/react-core';
|
||||
@ -23,24 +18,32 @@ class Pagination extends Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
const { page } = this.props;
|
||||
|
||||
const { page } = props;
|
||||
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) {
|
||||
const { page } = this.props;
|
||||
|
||||
if (prevProps.page !== page) {
|
||||
this.setState({ value: page });
|
||||
this.onPageChange(page);
|
||||
}
|
||||
}
|
||||
|
||||
onPageChange = value => {
|
||||
onPageChange (value) {
|
||||
this.setState({ value });
|
||||
};
|
||||
}
|
||||
|
||||
onSubmit = event => {
|
||||
onSubmit (event) {
|
||||
const { onSetPage, page, pageCount, page_size } = this.props;
|
||||
const { value } = this.state;
|
||||
|
||||
@ -51,46 +54,42 @@ class Pagination extends Component {
|
||||
|
||||
if (isValid) {
|
||||
onSetPage(value, page_size);
|
||||
} else{
|
||||
} else {
|
||||
this.setState({ value: page });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
onFirst = () => {
|
||||
const { onSetPage, page_size} = this.props;
|
||||
onFirst () {
|
||||
const { onSetPage, page_size } = this.props;
|
||||
|
||||
onSetPage(1, page_size);
|
||||
};
|
||||
}
|
||||
|
||||
onPrevious = () => {
|
||||
onPrevious () {
|
||||
const { onSetPage, page, page_size } = this.props;
|
||||
const previousPage = page - 1;
|
||||
|
||||
if (previousPage >= 1) {
|
||||
onSetPage(previousPage, page_size)
|
||||
}
|
||||
};
|
||||
onSetPage(previousPage, page_size);
|
||||
}
|
||||
|
||||
onNext = () => {
|
||||
onNext () {
|
||||
const { onSetPage, page, pageCount, page_size } = this.props;
|
||||
const nextPage = page + 1;
|
||||
|
||||
if (nextPage <= pageCount) {
|
||||
onSetPage(nextPage, page_size)
|
||||
}
|
||||
};
|
||||
onSetPage(nextPage, page_size);
|
||||
}
|
||||
|
||||
onLast = () => {
|
||||
onLast () {
|
||||
const { onSetPage, pageCount, page_size } = this.props;
|
||||
|
||||
onSetPage(pageCount, page_size)
|
||||
};
|
||||
}
|
||||
|
||||
onTogglePageSize = isOpen => {
|
||||
onTogglePageSize (isOpen) {
|
||||
this.setState({ isOpen });
|
||||
};
|
||||
}
|
||||
|
||||
onSelectPageSize = ({ target }) => {
|
||||
onSelectPageSize ({ target }) {
|
||||
const { onSetPage } = this.props;
|
||||
|
||||
const page = 1;
|
||||
@ -99,7 +98,7 @@ class Pagination extends Component {
|
||||
this.setState({ isOpen: false });
|
||||
|
||||
onSetPage(page, page_size);
|
||||
};
|
||||
}
|
||||
|
||||
render () {
|
||||
const { up } = DropdownDirection;
|
||||
@ -140,22 +139,28 @@ class Pagination extends Component {
|
||||
isOpen={isOpen}
|
||||
toggle={(
|
||||
<DropdownToggle
|
||||
onToggle={this.onTogglePageSize}>
|
||||
{ page_size }
|
||||
className="togglePageSize"
|
||||
onToggle={this.onTogglePageSize}
|
||||
>
|
||||
{page_size}
|
||||
</DropdownToggle>
|
||||
)}>
|
||||
)}
|
||||
>
|
||||
{opts.map(option => (
|
||||
<DropdownItem key={option} component="button">
|
||||
{ option }
|
||||
<DropdownItem
|
||||
key={option}
|
||||
component="button"
|
||||
>
|
||||
{option}
|
||||
</DropdownItem>
|
||||
))}
|
||||
</Dropdown>
|
||||
<Trans>Per Page</Trans>
|
||||
<Trans> Per Page</Trans>
|
||||
</LevelItem>
|
||||
<LevelItem>
|
||||
<Split gutter="md" className="pf-u-display-flex pf-u-align-items-center">
|
||||
<SplitItem>
|
||||
<Trans>{ itemMin } - { itemMax } of { count }</Trans>
|
||||
<Trans>{itemMin} - {itemMax} of {count}</Trans>
|
||||
</SplitItem>
|
||||
<SplitItem>
|
||||
<div className="pf-c-input-group">
|
||||
@ -196,7 +201,7 @@ class Pagination extends Component {
|
||||
value={value}
|
||||
type="text"
|
||||
onChange={this.onPageChange}
|
||||
/> of { pageCount }
|
||||
/> of {pageCount}
|
||||
</Trans>
|
||||
</form>
|
||||
</SplitItem>
|
||||
|
||||
40
src/components/Tabs/Tab.jsx
Normal file
40
src/components/Tabs/Tab.jsx
Normal file
@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import './tabs.scss';
|
||||
|
||||
|
||||
const Tab = ({ location, match, tab, currentTab, children, breadcrumb }) => {
|
||||
const tabClasses = () => {
|
||||
let classes = 'pf-c-tabs__item';
|
||||
if (tab === currentTab) {
|
||||
classes += ' pf-m-current';
|
||||
}
|
||||
|
||||
return classes;
|
||||
};
|
||||
|
||||
const tabParams = () => {
|
||||
const params = new URLSearchParams(location.search);
|
||||
if (params.get('tab') !== undefined) {
|
||||
params.set('tab', tab);
|
||||
} else {
|
||||
params.append('tab', tab);
|
||||
}
|
||||
|
||||
return `?${params.toString()}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<li className={tabClasses()}>
|
||||
<Link
|
||||
className={'pf-c-tabs__button'}
|
||||
to={{ pathname: `${match.url}`, search: tabParams(), state: { breadcrumb } }}
|
||||
replace={tab === currentTab}>
|
||||
{children}
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
export default Tab;
|
||||
13
src/components/Tabs/Tabs.jsx
Normal file
13
src/components/Tabs/Tabs.jsx
Normal file
@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import './tabs.scss';
|
||||
|
||||
|
||||
const Tabs = ({ children, labelText }) => (
|
||||
<div className="pf-c-tabs" aria-label={labelText}>
|
||||
<ul className="pf-c-tabs__list">
|
||||
{children}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Tabs;
|
||||
50
src/components/Tabs/tabs.scss
Normal file
50
src/components/Tabs/tabs.scss
Normal file
@ -0,0 +1,50 @@
|
||||
.at-c-orgPane {
|
||||
a {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.pf-c-card__header {
|
||||
--pf-c-card__header--PaddingBottom: 0;
|
||||
--pf-c-card__header--PaddingX: 0;
|
||||
--pf-c-card__header--PaddingTop: 0;
|
||||
}
|
||||
|
||||
.pf-c-tabs {
|
||||
--pf-global--link--Color: #484848;
|
||||
--pf-global--link--Color--hover: #484848;
|
||||
--pf-global--link--TextDecoration--hover: none;
|
||||
|
||||
&:before {
|
||||
border-bottom: 1px solid var(--pf-c-tabs__item--BorderColor);
|
||||
border-top: 1px solid var(--pf-c-tabs__item--BorderColor);
|
||||
bottom: 0;
|
||||
content: " ";
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.pf-c-tabs__button {
|
||||
--pf-c-tabs__button--PaddingLeft: 20px;
|
||||
--pf-c-tabs__button--PaddingRight: 20px;
|
||||
}
|
||||
|
||||
.pf-c-tabs__item.pf-m-current
|
||||
.pf-c-tabs__button::after {
|
||||
border-bottom: 3px solid var(--pf-c-tabs__item--m-current--Color);
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.pf-c-tabs__item:not(.pf-m-current):hover
|
||||
.pf-c-tabs__button::after {
|
||||
border-bottom: 3px solid var(--pf-global--Color--dark-200);
|
||||
border-top: none;
|
||||
}
|
||||
}
|
||||
|
||||
.pf-c-breadcrumb__item.heading {
|
||||
flex: 100%;
|
||||
font-size: 20px;
|
||||
}
|
||||
@ -12,31 +12,31 @@ class TowerLogo extends Component {
|
||||
super(props);
|
||||
|
||||
this.state = { hover: false };
|
||||
|
||||
this.onClick = this.onClick.bind(this);
|
||||
this.onHover = this.onHover.bind(this);
|
||||
}
|
||||
|
||||
onClick = () => {
|
||||
const { history, onClick: handleClick } = this.props;
|
||||
onClick () {
|
||||
const { history, linkTo } = this.props;
|
||||
|
||||
if (!handleClick) return;
|
||||
if (!linkTo) return;
|
||||
|
||||
history.push('/');
|
||||
history.push(linkTo);
|
||||
}
|
||||
|
||||
handleClick();
|
||||
};
|
||||
|
||||
onHover = () => {
|
||||
onHover () {
|
||||
const { hover } = this.state;
|
||||
|
||||
this.setState({ hover: !hover });
|
||||
};
|
||||
}
|
||||
|
||||
render () {
|
||||
const { hover } = this.state;
|
||||
const { onClick: handleClick } = this.props;
|
||||
|
||||
let src = TowerLogoHeader;
|
||||
|
||||
if (hover && handleClick) {
|
||||
if (hover) {
|
||||
src = TowerLogoHeaderHover;
|
||||
}
|
||||
|
||||
|
||||
@ -1,8 +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/`;
|
||||
export const API_INSTANCE_GROUPS = `${API_V2}instance_groups/`;
|
||||
285
src/index.jsx
285
src/index.jsx
@ -1,27 +1,286 @@
|
||||
import axios from 'axios';
|
||||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
|
||||
import ReactDOM from 'react-dom';
|
||||
import {
|
||||
HashRouter as Router
|
||||
HashRouter,
|
||||
Redirect,
|
||||
Route,
|
||||
Switch,
|
||||
} from 'react-router-dom';
|
||||
import App from './App';
|
||||
import api from './api';
|
||||
import { API_ROOT } from './endpoints';
|
||||
import {
|
||||
I18n,
|
||||
I18nProvider,
|
||||
} from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
|
||||
import '@patternfly/react-core/dist/styles/base.css';
|
||||
import '@patternfly/patternfly-next/patternfly.css';
|
||||
|
||||
import './app.scss';
|
||||
import './components/Pagination/styles.scss';
|
||||
import './components/DataListToolbar/styles.scss';
|
||||
|
||||
const el = document.getElementById('app');
|
||||
import APIClient from './api';
|
||||
|
||||
const main = async () => {
|
||||
const { custom_logo, custom_login_info } = await api.get(API_ROOT);
|
||||
render(<Router><App logo={custom_logo} loginInfo={custom_login_info} /></Router>, el);
|
||||
import App from './App';
|
||||
import Background from './components/Background';
|
||||
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
|
||||
//
|
||||
|
||||
export function getLanguage (nav) {
|
||||
const language = (nav.languages && nav.languages[0]) || nav.language || nav.userLanguage;
|
||||
const languageWithoutRegionCode = language.toLowerCase().split(/[_-]+/)[0];
|
||||
|
||||
return languageWithoutRegionCode;
|
||||
};
|
||||
|
||||
main();
|
||||
//
|
||||
// Function Main
|
||||
//
|
||||
|
||||
export default main;
|
||||
export async function main (render, api) {
|
||||
const catalogs = { en, ja };
|
||||
const language = getLanguage(navigator);
|
||||
|
||||
const el = document.getElementById('app');
|
||||
const { data: { custom_logo, custom_login_info } } = await api.getRoot();
|
||||
|
||||
const defaultRedirect = () => (<Redirect to="/home" />);
|
||||
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={language}
|
||||
catalogs={catalogs}
|
||||
>
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<Background>
|
||||
{!api.isAuthenticated() ? loginRoutes : (
|
||||
<Switch>
|
||||
<Route path="/login" render={defaultRedirect} />
|
||||
<Route exact path="/" render={defaultRedirect} />
|
||||
<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(ReactDOM.render, new APIClient(http));
|
||||
|
||||
@ -8,53 +8,57 @@ import {
|
||||
} from '@patternfly/react-core';
|
||||
|
||||
import towerLogo from '../../images/tower-logo-header.svg';
|
||||
import api from '../api';
|
||||
|
||||
class AtLogin extends Component {
|
||||
class AWXLogin extends Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
username: '',
|
||||
password: '',
|
||||
isValidPassword: true,
|
||||
loading: false
|
||||
isInputValid: true,
|
||||
isLoading: false
|
||||
};
|
||||
|
||||
this.onChangeUsername = this.onChangeUsername.bind(this);
|
||||
this.onChangePassword = this.onChangePassword.bind(this);
|
||||
this.onLoginButtonClick = this.onLoginButtonClick.bind(this);
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this.unmounting = true; // todo: state management
|
||||
onChangeUsername (value) {
|
||||
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 });
|
||||
|
||||
handlePasswordChange = value => this.safeSetState({ password: value, isValidPassword: true });
|
||||
|
||||
handleSubmit = async event => {
|
||||
const { username, password, loading } = this.state;
|
||||
async onLoginButtonClick (event) {
|
||||
const { username, password, isLoading } = this.state;
|
||||
const { api } = this.props;
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
if (!loading) {
|
||||
this.safeSetState({ loading: true });
|
||||
if (isLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await api.login(username, password);
|
||||
} catch (error) {
|
||||
if (error.response.status === 401) {
|
||||
this.safeSetState({ isValidPassword: false });
|
||||
}
|
||||
} finally {
|
||||
this.safeSetState({ loading: false });
|
||||
this.setState({ isLoading: true });
|
||||
|
||||
try {
|
||||
await api.login(username, password);
|
||||
} catch (error) {
|
||||
if (error.response && error.response.status === 401) {
|
||||
this.setState({ isInputValid: false });
|
||||
}
|
||||
} finally {
|
||||
this.setState({ isLoading: false });
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { username, password, isValidPassword } = this.state;
|
||||
const { logo, alt } = this.props;
|
||||
const { username, password, isInputValid } = this.state;
|
||||
const { api, alt, loginInfo, logo } = this.props;
|
||||
const logoSrc = logo ? `data:image/jpeg;${logo}` : towerLogo;
|
||||
|
||||
if (api.isAuthenticated()) {
|
||||
@ -65,20 +69,21 @@ class AtLogin extends Component {
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<LoginPage
|
||||
mainBrandImgSrc={logoSrc}
|
||||
mainBrandImgAlt={alt || 'Ansible Tower'}
|
||||
brandImgSrc={logoSrc}
|
||||
brandImgAlt={alt || 'Ansible Tower'}
|
||||
loginTitle={i18n._(t`Welcome to Ansible Tower! Please Sign In.`)}
|
||||
textContent={loginInfo}
|
||||
>
|
||||
<LoginForm
|
||||
usernameLabel={i18n._(t`Username`)}
|
||||
usernameValue={username}
|
||||
onChangeUsername={this.handleUsernameChange}
|
||||
passwordLabel={i18n._(t`Password`)}
|
||||
passwordValue={password}
|
||||
onChangePassword={this.handlePasswordChange}
|
||||
isValidPassword={isValidPassword}
|
||||
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>
|
||||
)}
|
||||
@ -87,4 +92,4 @@ class AtLogin extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default AtLogin;
|
||||
export default AWXLogin;
|
||||
|
||||
@ -3,7 +3,9 @@ import { Trans } from '@lingui/macro';
|
||||
import {
|
||||
PageSection,
|
||||
PageSectionVariants,
|
||||
Title,
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbHeading
|
||||
} from '@patternfly/react-core';
|
||||
import {
|
||||
Link
|
||||
@ -21,20 +23,22 @@ const OrganizationBreadcrumb = ({ parentObj, organization, currentTab, location
|
||||
.map(({ url, name }, index) => {
|
||||
let elem;
|
||||
if (noLastLink && parentObj.length - 1 === index) {
|
||||
elem = (<Fragment key={name}>{name}</Fragment>);
|
||||
elem = (<BreadcrumbHeading className="heading" key={name}>{name}</BreadcrumbHeading>);
|
||||
} else {
|
||||
elem = (
|
||||
<Link
|
||||
key={name}
|
||||
to={{ pathname: url, state: { breadcrumb: parentObj, organization } }}
|
||||
>
|
||||
{name}
|
||||
</Link>
|
||||
<BreadcrumbItem key={name}>
|
||||
<Link
|
||||
key={name}
|
||||
to={{ pathname: url, state: { breadcrumb: parentObj, organization } }}
|
||||
>
|
||||
{name}
|
||||
</Link>
|
||||
</BreadcrumbItem>
|
||||
);
|
||||
}
|
||||
return elem;
|
||||
})
|
||||
.reduce((prev, curr) => [prev, ' > ', curr])}
|
||||
.reduce((prev, curr) => [prev, curr])}
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
@ -42,25 +46,31 @@ const OrganizationBreadcrumb = ({ parentObj, organization, currentTab, location
|
||||
breadcrumb = (
|
||||
<Fragment>
|
||||
{generateCrumb()}
|
||||
{' > '}
|
||||
{getTabName(currentTab)}
|
||||
<BreadcrumbHeading className="heading">
|
||||
{getTabName(currentTab)}
|
||||
</BreadcrumbHeading>
|
||||
</Fragment>
|
||||
);
|
||||
} else if (location.pathname.indexOf('edit') > -1) {
|
||||
breadcrumb = (
|
||||
<Fragment>
|
||||
{generateCrumb()}
|
||||
<Trans>{' > edit'}</Trans>
|
||||
<BreadcrumbHeading className="heading">
|
||||
<Trans>Edit</Trans>
|
||||
</BreadcrumbHeading>
|
||||
</Fragment>
|
||||
);
|
||||
} else if (location.pathname.indexOf('add') > -1) {
|
||||
breadcrumb = (
|
||||
<Fragment>
|
||||
{generateCrumb()}
|
||||
<Trans>{' > add'}</Trans>
|
||||
<BreadcrumbHeading className="heading">
|
||||
<Trans>Add</Trans>
|
||||
</BreadcrumbHeading>
|
||||
</Fragment>
|
||||
);
|
||||
} else {
|
||||
|
||||
breadcrumb = (
|
||||
<Fragment>
|
||||
{generateCrumb(true)}
|
||||
@ -71,7 +81,7 @@ const OrganizationBreadcrumb = ({ parentObj, organization, currentTab, location
|
||||
|
||||
return (
|
||||
<PageSection variant={light} className="pf-m-condensed">
|
||||
<Title size="2xl">{breadcrumb}</Title>
|
||||
<Breadcrumb>{breadcrumb}</Breadcrumb>
|
||||
</PageSection>
|
||||
);
|
||||
};
|
||||
|
||||
@ -6,10 +6,7 @@ import {
|
||||
CardHeader,
|
||||
CardBody,
|
||||
PageSection,
|
||||
PageSectionVariants,
|
||||
ToolbarGroup,
|
||||
ToolbarItem,
|
||||
ToolbarSection,
|
||||
PageSectionVariants
|
||||
} from '@patternfly/react-core';
|
||||
import {
|
||||
Switch,
|
||||
@ -17,39 +14,10 @@ import {
|
||||
Route
|
||||
} from 'react-router-dom';
|
||||
|
||||
import Tab from '../../../components/Tabs/Tab';
|
||||
import Tabs from '../../../components/Tabs/Tabs';
|
||||
import getTabName from '../utils';
|
||||
|
||||
import '../tabs.scss';
|
||||
|
||||
const DetailTab = ({ location, match, tab, currentTab, children, breadcrumb }) => {
|
||||
const tabClasses = () => {
|
||||
let classes = 'at-c-tabs__tab';
|
||||
if (tab === currentTab) {
|
||||
classes += ' at-m-selected';
|
||||
}
|
||||
|
||||
return classes;
|
||||
};
|
||||
|
||||
const updateTab = () => {
|
||||
const params = new URLSearchParams(location.search);
|
||||
if (params.get('tab') !== undefined) {
|
||||
params.set('tab', tab);
|
||||
} else {
|
||||
params.append('tab', tab);
|
||||
}
|
||||
|
||||
return `?${params.toString()}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<ToolbarItem className={tabClasses()}>
|
||||
<Link to={{ pathname: `${match.url}`, search: updateTab(), state: { breadcrumb } }} replace={tab === currentTab}>
|
||||
{children}
|
||||
</Link>
|
||||
</ToolbarItem>
|
||||
);
|
||||
};
|
||||
|
||||
const OrganizationDetail = ({
|
||||
location,
|
||||
@ -61,6 +29,7 @@ const OrganizationDetail = ({
|
||||
}) => {
|
||||
// TODO: set objectName by param or through grabbing org detail get from api
|
||||
const { medium } = PageSectionVariants;
|
||||
const tabList=['details', 'access', 'teams', 'notifications'];
|
||||
|
||||
const deleteResourceView = () => (
|
||||
<Fragment>
|
||||
@ -93,34 +62,29 @@ const OrganizationDetail = ({
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
const detailTabs = (tabs) => (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<ToolbarSection aria-label={i18n._(t`Organization detail tabs`)}>
|
||||
<ToolbarGroup className="at-c-tabs">
|
||||
{tabs.map(tab => (
|
||||
<DetailTab
|
||||
key={tab}
|
||||
tab={tab}
|
||||
location={location}
|
||||
match={match}
|
||||
currentTab={currentTab}
|
||||
breadcrumb={parentBreadcrumbObj}
|
||||
>
|
||||
{getTabName(tab)}
|
||||
</DetailTab>
|
||||
))}
|
||||
</ToolbarGroup>
|
||||
</ToolbarSection>
|
||||
)}
|
||||
</I18n>
|
||||
);
|
||||
|
||||
return (
|
||||
<PageSection variant={medium}>
|
||||
<Card className="at-c-orgPane">
|
||||
<CardHeader>
|
||||
{detailTabs(['details', 'users', 'teams', 'admins', 'notifications'])}
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<Tabs labelText={i18n._(t`Organization detail tabs`)}>
|
||||
{tabList.map(tab => (
|
||||
<Tab
|
||||
key={tab}
|
||||
tab={tab}
|
||||
location={location}
|
||||
match={match}
|
||||
currentTab={currentTab}
|
||||
breadcrumb={parentBreadcrumbObj}
|
||||
>
|
||||
<Trans>{getTabName(tab)}</Trans>
|
||||
</Tab>
|
||||
))}
|
||||
</Tabs>
|
||||
)}
|
||||
</I18n>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
{(currentTab && currentTab !== 'details') ? (
|
||||
|
||||
@ -14,7 +14,6 @@ export default ({
|
||||
name,
|
||||
userCount,
|
||||
teamCount,
|
||||
adminCount,
|
||||
isSelected,
|
||||
onSelect,
|
||||
detailUrl,
|
||||
@ -46,7 +45,7 @@ export default ({
|
||||
</span>
|
||||
</div>
|
||||
<div className="pf-c-data-list__cell">
|
||||
<Link to={`${detailUrl}?tab=users`}>
|
||||
<Link to={`${detailUrl}?tab=access`}>
|
||||
<Trans>Users</Trans>
|
||||
</Link>
|
||||
<Badge isRead>
|
||||
@ -62,14 +61,6 @@ export default ({
|
||||
{teamCount}
|
||||
{' '}
|
||||
</Badge>
|
||||
<Link to={`${detailUrl}?tab=admins`}>
|
||||
<Trans>Admins</Trans>
|
||||
</Link>
|
||||
<Badge isRead>
|
||||
{' '}
|
||||
{adminCount}
|
||||
{' '}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="pf-c-data-list__cell" />
|
||||
</li>
|
||||
|
||||
@ -5,12 +5,31 @@ import OrganizationAdd from './views/Organization.add';
|
||||
import OrganizationView from './views/Organization.view';
|
||||
import OrganizationsList from './views/Organizations.list';
|
||||
|
||||
const Organizations = ({ match }) => (
|
||||
export default ({ api, match }) => (
|
||||
<Switch>
|
||||
<Route path={`${match.path}/add`} component={OrganizationAdd} />
|
||||
<Route path={`${match.path}/:id`} component={OrganizationView} />
|
||||
<Route path={`${match.path}`} component={OrganizationsList} />
|
||||
<Route
|
||||
path={`${match.path}/add`}
|
||||
render={() => (
|
||||
<OrganizationAdd
|
||||
api={api}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path={`${match.path}/:id`}
|
||||
render={() => (
|
||||
<OrganizationView
|
||||
api={api}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path={`${match.path}`}
|
||||
render={() => (
|
||||
<OrganizationsList
|
||||
api={api}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
);
|
||||
|
||||
export default Organizations;
|
||||
|
||||
@ -1,18 +0,0 @@
|
||||
.at-c-tabs {
|
||||
padding: 0 5px !important;
|
||||
margin: 0 -10px !important;
|
||||
|
||||
.at-c-tabs__tab {
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.at-c-tabs__tab.at-m-selected {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.at-c-orgPane {
|
||||
a {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@ -2,12 +2,10 @@ const getTabName = (tab) => {
|
||||
let tabName = '';
|
||||
if (tab === 'details') {
|
||||
tabName = 'Details';
|
||||
} else if (tab === 'users') {
|
||||
tabName = 'Users';
|
||||
} else if (tab === 'access') {
|
||||
tabName = 'Access';
|
||||
} else if (tab === 'teams') {
|
||||
tabName = 'Teams';
|
||||
} else if (tab === 'admins') {
|
||||
tabName = 'Admins';
|
||||
} else if (tab === 'notifications') {
|
||||
tabName = 'Notifications';
|
||||
}
|
||||
|
||||
@ -19,11 +19,9 @@ import {
|
||||
} from '@patternfly/react-core';
|
||||
|
||||
import { ConfigContext } from '../../../context';
|
||||
import { API_ORGANIZATIONS } from '../../../endpoints';
|
||||
import { API_INSTANCE_GROUPS } from '../../../endpoints';
|
||||
import api from '../../../api';
|
||||
import AnsibleSelect from '../../../components/AnsibleSelect';
|
||||
import Lookup from '../../../components/Lookup';
|
||||
import AnsibleSelect from '../../../components/AnsibleSelect'
|
||||
const { light } = PageSectionVariants;
|
||||
|
||||
class OrganizationAdd extends React.Component {
|
||||
@ -71,8 +69,9 @@ class OrganizationAdd extends React.Component {
|
||||
}
|
||||
|
||||
async onSubmit() {
|
||||
const { api } = this.props;
|
||||
const data = Object.assign({}, { ...this.state });
|
||||
await api.post(API_ORGANIZATIONS, data);
|
||||
await api.createOrganization(data);
|
||||
this.resetForm();
|
||||
}
|
||||
|
||||
@ -81,7 +80,8 @@ class OrganizationAdd extends React.Component {
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
const { data } = await api.get(API_INSTANCE_GROUPS);
|
||||
const { api } = this.props;
|
||||
const { data } = await api.getInstanceGroups();
|
||||
let results = [];
|
||||
data.results.map((result) => {
|
||||
results.push({ id: result.id, name: result.name, isChecked: false });
|
||||
@ -92,6 +92,7 @@ class OrganizationAdd extends React.Component {
|
||||
render() {
|
||||
const { name, results } = this.state;
|
||||
const enabled = name.length > 0; // TODO: add better form validation
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<PageSection variant={light} className="pf-m-condensed">
|
||||
|
||||
@ -2,16 +2,13 @@ import React, { Component, Fragment } from 'react';
|
||||
import { i18nMark } from '@lingui/react';
|
||||
import {
|
||||
Switch,
|
||||
Route
|
||||
Route,
|
||||
withRouter,
|
||||
} from 'react-router-dom';
|
||||
|
||||
import OrganizationBreadcrumb from '../components/OrganizationBreadcrumb';
|
||||
import OrganizationDetail from '../components/OrganizationDetail';
|
||||
import OrganizationEdit from '../components/OrganizationEdit';
|
||||
|
||||
import api from '../../../api';
|
||||
import { API_ORGANIZATIONS } from '../../../endpoints';
|
||||
|
||||
class OrganizationView extends Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
@ -30,6 +27,8 @@ class OrganizationView extends Component {
|
||||
loading: false,
|
||||
mounted: false
|
||||
};
|
||||
|
||||
this.fetchOrganization = this.fetchOrganization.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
@ -47,13 +46,15 @@ class OrganizationView extends Component {
|
||||
|
||||
async fetchOrganization () {
|
||||
const { mounted } = this.state;
|
||||
const { api } = this.props;
|
||||
|
||||
if (mounted) {
|
||||
this.setState({ error: false, loading: true });
|
||||
|
||||
const { match } = this.props;
|
||||
const { parentBreadcrumbObj, organization } = this.state;
|
||||
try {
|
||||
const { data } = await api.get(`${API_ORGANIZATIONS}${match.params.id}/`);
|
||||
const { data } = await api.getOrganizationDetails(match.params.id);
|
||||
if (organization === 'loading') {
|
||||
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 Pagination from '../../../components/Pagination';
|
||||
|
||||
import api from '../../../api';
|
||||
import { API_ORGANIZATIONS } from '../../../endpoints';
|
||||
|
||||
import {
|
||||
encodeQueryString,
|
||||
parseQueryString,
|
||||
@ -56,6 +53,15 @@ class Organizations extends Component {
|
||||
results: [],
|
||||
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 () {
|
||||
@ -78,7 +84,7 @@ class Organizations extends Component {
|
||||
return Object.assign({}, this.defaultParams, searchParams, overrides);
|
||||
}
|
||||
|
||||
onSort = (sortedColumnKey, sortOrder) => {
|
||||
onSort(sortedColumnKey, sortOrder) {
|
||||
const { page_size } = this.state;
|
||||
|
||||
let order_by = sortedColumnKey;
|
||||
@ -90,26 +96,26 @@ class Organizations extends Component {
|
||||
const queryParams = this.getQueryParams({ order_by, page_size });
|
||||
|
||||
this.fetchOrganizations(queryParams);
|
||||
};
|
||||
}
|
||||
|
||||
onSetPage = (pageNumber, pageSize) => {
|
||||
onSetPage (pageNumber, pageSize) {
|
||||
const page = parseInt(pageNumber, 10);
|
||||
const page_size = parseInt(pageSize, 10);
|
||||
|
||||
const queryParams = this.getQueryParams({ page, page_size });
|
||||
|
||||
this.fetchOrganizations(queryParams);
|
||||
};
|
||||
}
|
||||
|
||||
onSelectAll = isSelected => {
|
||||
onSelectAll (isSelected) {
|
||||
const { results } = this.state;
|
||||
|
||||
const selected = isSelected ? results.map(o => o.id) : [];
|
||||
|
||||
this.setState({ selected });
|
||||
};
|
||||
}
|
||||
|
||||
onSelect = id => {
|
||||
onSelect (id) {
|
||||
const { selected } = this.state;
|
||||
|
||||
const isSelected = selected.includes(id);
|
||||
@ -119,7 +125,7 @@ class Organizations extends Component {
|
||||
} else {
|
||||
this.setState({ selected: selected.concat(id) });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
updateUrl (queryParams) {
|
||||
const { history, location } = this.props;
|
||||
@ -132,6 +138,7 @@ class Organizations extends Component {
|
||||
}
|
||||
|
||||
async fetchOrganizations (queryParams) {
|
||||
const { api } = this.props;
|
||||
const { page, page_size, order_by } = queryParams;
|
||||
|
||||
let sortOrder = 'ascending';
|
||||
@ -145,7 +152,7 @@ class Organizations extends Component {
|
||||
this.setState({ error: false, loading: true });
|
||||
|
||||
try {
|
||||
const { data } = await api.get(API_ORGANIZATIONS, queryParams);
|
||||
const { data } = await api.getOrganizations(queryParams);
|
||||
const { count, results } = data;
|
||||
|
||||
const pageCount = Math.ceil(count / page_size);
|
||||
@ -218,7 +225,6 @@ class Organizations extends Component {
|
||||
parentBreadcrumb={parentBreadcrumb}
|
||||
userCount={o.summary_fields.related_field_counts.users}
|
||||
teamCount={o.summary_fields.related_field_counts.teams}
|
||||
adminCount={o.summary_fields.related_field_counts.admins}
|
||||
isSelected={selected.includes(o.id)}
|
||||
onSelect={() => this.onSelect(o.id)}
|
||||
/>
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
|
||||
const TARGET_PORT = 8043;
|
||||
const TARGET = `https://localhost:${TARGET_PORT}`;
|
||||
const TARGET_PORT = process.env.TARGET_PORT || 8043;
|
||||
const TARGET_HOST = process.env.TARGET_HOST || 'localhost';
|
||||
const TARGET = `https://${TARGET_HOST}:${TARGET_PORT}`;
|
||||
|
||||
module.exports = {
|
||||
entry: './src/index.jsx',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user