mirror of
https://github.com/ansible/awx.git
synced 2026-03-02 09:18:48 -03:30
Merge remote-tracking branch 'origin/master' into lookup-form-component
This commit is contained in:
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 React from 'react';
|
||||||
import { HashRouter as Router } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import { shallow, mount } from 'enzyme';
|
import { I18nProvider } from '@lingui/react';
|
||||||
import App from '../src/App';
|
|
||||||
import api from '../src/api';
|
|
||||||
import { API_LOGOUT, API_CONFIG } from '../src/endpoints';
|
|
||||||
|
|
||||||
import Dashboard from '../src/pages/Dashboard';
|
import { mount, shallow } from 'enzyme';
|
||||||
import Login from '../src/pages/Login';
|
|
||||||
import { asyncFlush } from '../jest.setup';
|
import { asyncFlush } from '../jest.setup';
|
||||||
|
|
||||||
|
import App from '../src/App';
|
||||||
|
|
||||||
const DEFAULT_ACTIVE_GROUP = 'views_group';
|
const DEFAULT_ACTIVE_GROUP = 'views_group';
|
||||||
const DEFAULT_ACTIVE_ITEM = 'views_group_dashboard';
|
|
||||||
|
|
||||||
describe('<App />', () => {
|
describe('<App />', () => {
|
||||||
test('renders without crashing', () => {
|
test('expected content is rendered', () => {
|
||||||
const appWrapper = shallow(<App />);
|
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.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', () => {
|
test('opening the about modal renders prefetched config data', async (done) => {
|
||||||
api.isAuthenticated = jest.fn();
|
const ansible_version = '111';
|
||||||
api.isAuthenticated.mockReturnValue(false);
|
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);
|
const wrapper = mount(
|
||||||
expect(login.length).toBe(1);
|
<MemoryRouter>
|
||||||
const dashboard = appWrapper.find(Dashboard);
|
<I18nProvider>
|
||||||
expect(dashboard.length).toBe(0);
|
<App api={api}/>
|
||||||
});
|
</I18nProvider>
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
|
||||||
test('renders dashboard when authenticated', () => {
|
await asyncFlush();
|
||||||
api.isAuthenticated = jest.fn();
|
expect(getConfig).toHaveBeenCalledTimes(1);
|
||||||
api.isAuthenticated.mockReturnValue(true);
|
|
||||||
|
|
||||||
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(wrapper.find(aboutModalContent)).toHaveLength(0);
|
||||||
expect(dashboard.length).toBe(1);
|
wrapper.find(aboutDropdown).simulate('click');
|
||||||
const login = appWrapper.find(Login);
|
wrapper.find(aboutButton).simulate('click');
|
||||||
expect(login.length).toBe(0);
|
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', () => {
|
test('onNavToggle sets state.isNavOpen to opposite', () => {
|
||||||
const appWrapper = shallow(<App.WrappedComponent />);
|
const appWrapper = shallow(<App />);
|
||||||
expect(appWrapper.state().isNavOpen).toBe(true);
|
const { onNavToggle } = appWrapper.instance();
|
||||||
appWrapper.instance().onNavToggle();
|
|
||||||
expect(appWrapper.state().isNavOpen).toBe(false);
|
[true, false, true, false, true].forEach(expected => {
|
||||||
|
expect(appWrapper.state().isNavOpen).toBe(expected);
|
||||||
|
onNavToggle();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('onLogoClick sets selected nav back to defaults', () => {
|
test('onLogout makes expected call to api client', async (done) => {
|
||||||
const appWrapper = shallow(<App.WrappedComponent />);
|
const logout = jest.fn(() => Promise.resolve());
|
||||||
appWrapper.setState({ activeGroup: 'foo', activeItem: 'bar' });
|
const api = { logout };
|
||||||
expect(appWrapper.state().activeItem).toBe('bar');
|
|
||||||
expect(appWrapper.state().activeGroup).toBe('foo');
|
|
||||||
appWrapper.instance().onLogoClick();
|
|
||||||
expect(appWrapper.state().activeGroup).toBe(DEFAULT_ACTIVE_GROUP);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('api.logout called from logout button', async () => {
|
const appWrapper = shallow(<App api={api} />);
|
||||||
api.get = jest.fn().mockImplementation(() => Promise.resolve({}));
|
|
||||||
const appWrapper = shallow(<App.WrappedComponent />);
|
appWrapper.instance().onLogout();
|
||||||
appWrapper.instance().onDevLogout();
|
|
||||||
appWrapper.setState({ activeGroup: 'foo', activeItem: 'bar' });
|
|
||||||
expect(api.get).toHaveBeenCalledWith(API_LOGOUT);
|
|
||||||
await asyncFlush();
|
await asyncFlush();
|
||||||
expect(appWrapper.state().activeItem).toBe(DEFAULT_ACTIVE_ITEM);
|
expect(api.logout).toHaveBeenCalledTimes(1);
|
||||||
expect(appWrapper.state().activeGroup).toBe(DEFAULT_ACTIVE_GROUP);
|
|
||||||
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Componenet makes REST call to API_CONFIG endpoint when mounted', () => {
|
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 APIClient from '../src/api';
|
||||||
import * as endpoints from '../src/endpoints';
|
|
||||||
|
|
||||||
const CSRF_COOKIE_NAME = 'csrftoken';
|
const invalidCookie = 'invalid';
|
||||||
const CSRF_HEADER_NAME = 'X-CSRFToken';
|
const validLoggedOutCookie = 'current_user=%7B%22id%22%3A1%2C%22type%22%3A%22user%22%2C%22url%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2F%22%2C%22related%22%3A%7B%22admin_of_organizations%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fadmin_of_organizations%2F%22%2C%22authorized_tokens%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fauthorized_tokens%2F%22%2C%22roles%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Froles%2F%22%2C%22organizations%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Forganizations%2F%22%2C%22access_list%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Faccess_list%2F%22%2C%22teams%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fteams%2F%22%2C%22tokens%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Ftokens%2F%22%2C%22personal_tokens%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fpersonal_tokens%2F%22%2C%22credentials%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fcredentials%2F%22%2C%22activity_stream%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Factivity_stream%2F%22%2C%22projects%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fprojects%2F%22%7D%2C%22summary_fields%22%3A%7B%7D%2C%22created%22%3A%222018-10-19T16%3A30%3A59.141963Z%22%2C%22username%22%3A%22admin%22%2C%22first_name%22%3A%22%22%2C%22last_name%22%3A%22%22%2C%22email%22%3A%22%22%2C%22is_superuser%22%3Atrue%2C%22is_system_auditor%22%3Afalse%2C%22ldap_dn%22%3A%22%22%2C%22external_account%22%3Anull%2C%22auth%22%3A%5B%5D%7D; userLoggedIn=false; csrftoken=lhOHpLQUFHlIVqx8CCZmEpdEZAz79GIRBIT3asBzTbPE7HS7wizt7WBsgJClz8Ge';
|
||||||
|
const validLoggedInCookie = 'current_user=%7B%22id%22%3A1%2C%22type%22%3A%22user%22%2C%22url%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2F%22%2C%22related%22%3A%7B%22admin_of_organizations%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fadmin_of_organizations%2F%22%2C%22authorized_tokens%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fauthorized_tokens%2F%22%2C%22roles%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Froles%2F%22%2C%22organizations%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Forganizations%2F%22%2C%22access_list%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Faccess_list%2F%22%2C%22teams%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fteams%2F%22%2C%22tokens%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Ftokens%2F%22%2C%22personal_tokens%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fpersonal_tokens%2F%22%2C%22credentials%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fcredentials%2F%22%2C%22activity_stream%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Factivity_stream%2F%22%2C%22projects%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fprojects%2F%22%7D%2C%22summary_fields%22%3A%7B%7D%2C%22created%22%3A%222018-10-19T16%3A30%3A59.141963Z%22%2C%22username%22%3A%22admin%22%2C%22first_name%22%3A%22%22%2C%22last_name%22%3A%22%22%2C%22email%22%3A%22%22%2C%22is_superuser%22%3Atrue%2C%22is_system_auditor%22%3Afalse%2C%22ldap_dn%22%3A%22%22%2C%22external_account%22%3Anull%2C%22auth%22%3A%5B%5D%7D; userLoggedIn=true; csrftoken=lhOHpLQUFHlIVqx8CCZmEpdEZAz79GIRBIT3asBzTbPE7HS7wizt7WBsgJClz8Ge';
|
||||||
const LOGIN_CONTENT_TYPE = 'application/x-www-form-urlencoded';
|
|
||||||
|
|
||||||
describe('APIClient (api.js)', () => {
|
describe('APIClient (api.js)', () => {
|
||||||
afterEach(() => {
|
test('isAuthenticated returns false when cookie is invalid', () => {
|
||||||
mockAxios.customClearMocks();
|
APIClient.getCookie = jest.fn(() => invalidCookie);
|
||||||
|
|
||||||
|
const api = new APIClient();
|
||||||
|
expect(api.isAuthenticated()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('constructor calls axios create', () => {
|
test('isAuthenticated returns false when cookie is unauthenticated', () => {
|
||||||
const csrfObj = {
|
APIClient.getCookie = jest.fn(() => validLoggedOutCookie);
|
||||||
xsrfCookieName: CSRF_COOKIE_NAME,
|
|
||||||
xsrfHeaderName: CSRF_HEADER_NAME
|
const api = new APIClient();
|
||||||
};
|
expect(api.isAuthenticated()).toBe(false);
|
||||||
expect(mockAxios.create).toHaveBeenCalledTimes(1);
|
|
||||||
expect(mockAxios.create).toHaveBeenCalledWith(csrfObj);
|
|
||||||
expect(APIClient.http).toHaveProperty('get');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('isAuthenticated checks authentication and sets cookie from document', () => {
|
test('isAuthenticated returns true when cookie is valid and authenticated', () => {
|
||||||
APIClient.getCookie = jest.fn();
|
APIClient.getCookie = jest.fn(() => validLoggedInCookie);
|
||||||
const invalidCookie = 'invalid';
|
|
||||||
const validLoggedOutCookie = 'current_user=%7B%22id%22%3A1%2C%22type%22%3A%22user%22%2C%22url%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2F%22%2C%22related%22%3A%7B%22admin_of_organizations%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fadmin_of_organizations%2F%22%2C%22authorized_tokens%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fauthorized_tokens%2F%22%2C%22roles%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Froles%2F%22%2C%22organizations%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Forganizations%2F%22%2C%22access_list%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Faccess_list%2F%22%2C%22teams%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fteams%2F%22%2C%22tokens%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Ftokens%2F%22%2C%22personal_tokens%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fpersonal_tokens%2F%22%2C%22credentials%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fcredentials%2F%22%2C%22activity_stream%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Factivity_stream%2F%22%2C%22projects%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fprojects%2F%22%7D%2C%22summary_fields%22%3A%7B%7D%2C%22created%22%3A%222018-10-19T16%3A30%3A59.141963Z%22%2C%22username%22%3A%22admin%22%2C%22first_name%22%3A%22%22%2C%22last_name%22%3A%22%22%2C%22email%22%3A%22%22%2C%22is_superuser%22%3Atrue%2C%22is_system_auditor%22%3Afalse%2C%22ldap_dn%22%3A%22%22%2C%22external_account%22%3Anull%2C%22auth%22%3A%5B%5D%7D; userLoggedIn=false; csrftoken=lhOHpLQUFHlIVqx8CCZmEpdEZAz79GIRBIT3asBzTbPE7HS7wizt7WBsgJClz8Ge';
|
const api = new APIClient();
|
||||||
const validLoggedInCookie = 'current_user=%7B%22id%22%3A1%2C%22type%22%3A%22user%22%2C%22url%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2F%22%2C%22related%22%3A%7B%22admin_of_organizations%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fadmin_of_organizations%2F%22%2C%22authorized_tokens%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fauthorized_tokens%2F%22%2C%22roles%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Froles%2F%22%2C%22organizations%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Forganizations%2F%22%2C%22access_list%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Faccess_list%2F%22%2C%22teams%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fteams%2F%22%2C%22tokens%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Ftokens%2F%22%2C%22personal_tokens%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fpersonal_tokens%2F%22%2C%22credentials%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fcredentials%2F%22%2C%22activity_stream%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Factivity_stream%2F%22%2C%22projects%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fprojects%2F%22%7D%2C%22summary_fields%22%3A%7B%7D%2C%22created%22%3A%222018-10-19T16%3A30%3A59.141963Z%22%2C%22username%22%3A%22admin%22%2C%22first_name%22%3A%22%22%2C%22last_name%22%3A%22%22%2C%22email%22%3A%22%22%2C%22is_superuser%22%3Atrue%2C%22is_system_auditor%22%3Afalse%2C%22ldap_dn%22%3A%22%22%2C%22external_account%22%3Anull%2C%22auth%22%3A%5B%5D%7D; userLoggedIn=true; csrftoken=lhOHpLQUFHlIVqx8CCZmEpdEZAz79GIRBIT3asBzTbPE7HS7wizt7WBsgJClz8Ge';
|
expect(api.isAuthenticated()).toBe(true);
|
||||||
APIClient.getCookie.mockReturnValue(invalidCookie);
|
|
||||||
expect(APIClient.isAuthenticated()).toBe(false);
|
|
||||||
APIClient.getCookie.mockReturnValue(validLoggedOutCookie);
|
|
||||||
expect(APIClient.isAuthenticated()).toBe(false);
|
|
||||||
APIClient.getCookie.mockReturnValue(validLoggedInCookie);
|
|
||||||
expect(APIClient.isAuthenticated()).toBe(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('login calls get and post to login route, and sets cookie from document', (done) => {
|
test('login calls get and post with expected content headers', async (done) => {
|
||||||
const un = 'foo';
|
const headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
|
||||||
const pw = 'bar';
|
|
||||||
const next = 'baz';
|
const createPromise = () => Promise.resolve();
|
||||||
const headers = { 'Content-Type': LOGIN_CONTENT_TYPE };
|
const mockHttp = ({ get: jest.fn(createPromise), post: jest.fn(createPromise) });
|
||||||
const data = `username=${un}&password=${pw}&next=${next}`;
|
|
||||||
APIClient.setCookie = jest.fn();
|
const api = new APIClient(mockHttp);
|
||||||
APIClient.login(un, pw, next).then(() => {
|
await api.login('username', 'password');
|
||||||
expect(mockAxios.get).toHaveBeenCalledTimes(1);
|
|
||||||
expect(mockAxios.get).toHaveBeenCalledWith(endpoints.API_LOGIN, { headers });
|
expect(mockHttp.get).toHaveBeenCalledTimes(1);
|
||||||
expect(mockAxios.post).toHaveBeenCalledTimes(1);
|
expect(mockHttp.get.mock.calls[0]).toContainEqual({ headers });
|
||||||
expect(mockAxios.post).toHaveBeenCalledWith(endpoints.API_LOGIN, data, { headers });
|
|
||||||
done();
|
expect(mockHttp.post).toHaveBeenCalledTimes(1);
|
||||||
});
|
expect(mockHttp.post.mock.calls[0]).toContainEqual({ headers });
|
||||||
|
|
||||||
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('login encodes uri components for username, password and redirect', (done) => {
|
test('login sends expected data', async (done) => {
|
||||||
const un = '/foo/';
|
const createPromise = () => Promise.resolve();
|
||||||
const pw = '/bar/';
|
const mockHttp = ({ get: jest.fn(createPromise), post: jest.fn(createPromise) });
|
||||||
const next = '/baz/';
|
|
||||||
const headers = { 'Content-Type': LOGIN_CONTENT_TYPE };
|
const api = new APIClient(mockHttp);
|
||||||
const data = `username=${encodeURIComponent(un)}&password=${encodeURIComponent(pw)}&next=${encodeURIComponent(next)}`;
|
await api.login('foo', 'bar');
|
||||||
APIClient.login(un, pw, next).then(() => {
|
await api.login('foo', 'bar', 'baz');
|
||||||
expect(mockAxios.post).toHaveBeenCalledTimes(1);
|
|
||||||
expect(mockAxios.post).toHaveBeenCalledWith(endpoints.API_LOGIN, data, { headers });
|
expect(mockHttp.post).toHaveBeenCalledTimes(2);
|
||||||
done();
|
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) => {
|
test('logout calls expected http method', async (done) => {
|
||||||
const un = 'foo';
|
const createPromise = () => Promise.resolve();
|
||||||
const pw = 'bar';
|
const mockHttp = ({ get: jest.fn(createPromise) });
|
||||||
const headers = { 'Content-Type': LOGIN_CONTENT_TYPE };
|
|
||||||
const data = `username=${un}&password=${pw}&next=${encodeURIComponent(endpoints.API_CONFIG)}`;
|
const api = new APIClient(mockHttp);
|
||||||
APIClient.setCookie = jest.fn();
|
await api.logout();
|
||||||
APIClient.login(un, pw).then(() => {
|
|
||||||
expect(mockAxios.post).toHaveBeenCalledTimes(1);
|
expect(mockHttp.get).toHaveBeenCalledTimes(1);
|
||||||
expect(mockAxios.post).toHaveBeenCalledWith(endpoints.API_LOGIN, data, { headers });
|
|
||||||
done();
|
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 React from 'react';
|
||||||
import { mount } from 'enzyme';
|
import { mount } from 'enzyme';
|
||||||
import { I18nProvider } from '@lingui/react';
|
import { I18nProvider } from '@lingui/react';
|
||||||
import api from '../../src/api';
|
|
||||||
import { API_CONFIG } from '../../src/endpoints';
|
|
||||||
import About from '../../src/components/About';
|
import About from '../../src/components/About';
|
||||||
|
|
||||||
describe('<About />', () => {
|
describe('<About />', () => {
|
||||||
@@ -19,16 +17,16 @@ describe('<About />', () => {
|
|||||||
aboutWrapper.unmount();
|
aboutWrapper.unmount();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('close button calls onAboutModalClose', () => {
|
test('close button calls onClose handler', () => {
|
||||||
const onAboutModalClose = jest.fn();
|
const onClose = jest.fn();
|
||||||
aboutWrapper = mount(
|
aboutWrapper = mount(
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<About isOpen onAboutModalClose={onAboutModalClose} />
|
<About isOpen onClose={onClose} />
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
);
|
);
|
||||||
closeButton = aboutWrapper.find('AboutModalBoxCloseButton Button');
|
closeButton = aboutWrapper.find('AboutModalBoxCloseButton Button');
|
||||||
closeButton.simulate('click');
|
closeButton.simulate('click');
|
||||||
expect(onAboutModalClose).toBeCalled();
|
expect(onClose).toBeCalled();
|
||||||
aboutWrapper.unmount();
|
aboutWrapper.unmount();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -29,4 +29,16 @@ describe('<AnsibleSelect />', () => {
|
|||||||
wrapper.find('select').simulate('change');
|
wrapper.find('select').simulate('change');
|
||||||
expect(spy).toHaveBeenCalled();
|
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';
|
import DataListToolbar from '../../src/components/DataListToolbar';
|
||||||
|
|
||||||
describe('<DataListToolbar />', () => {
|
describe('<DataListToolbar />', () => {
|
||||||
const columns = [{ name: 'Name', key: 'name', isSortable: true }];
|
|
||||||
let toolbar;
|
let toolbar;
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -15,6 +14,8 @@ describe('<DataListToolbar />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('it triggers the expected callbacks', () => {
|
test('it triggers the expected callbacks', () => {
|
||||||
|
const columns = [{ name: 'Name', key: 'name', isSortable: true }];
|
||||||
|
|
||||||
const search = 'button[aria-label="Search"]';
|
const search = 'button[aria-label="Search"]';
|
||||||
const searchTextInput = 'input[aria-label="Search text input"]';
|
const searchTextInput = 'input[aria-label="Search text input"]';
|
||||||
const selectAll = 'input[aria-label="Select all"]';
|
const selectAll = 'input[aria-label="Select all"]';
|
||||||
@@ -28,6 +29,7 @@ describe('<DataListToolbar />', () => {
|
|||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<DataListToolbar
|
<DataListToolbar
|
||||||
isAllSelected={false}
|
isAllSelected={false}
|
||||||
|
showExpandCollapse={true}
|
||||||
sortedColumnKey="name"
|
sortedColumnKey="name"
|
||||||
sortOrder="ascending"
|
sortOrder="ascending"
|
||||||
columns={columns}
|
columns={columns}
|
||||||
@@ -55,4 +57,166 @@ describe('<DataListToolbar />', () => {
|
|||||||
expect(onSearch).toHaveBeenCalledTimes(1);
|
expect(onSearch).toHaveBeenCalledTimes(1);
|
||||||
expect(onSearch).toBeCalledWith('test-321');
|
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">
|
<Nav aria-label="Test Navigation">
|
||||||
<NavExpandableGroup
|
<NavExpandableGroup
|
||||||
groupId="test"
|
groupId="test"
|
||||||
title="Test"
|
groupTitle="Test"
|
||||||
routes={[
|
routes={[
|
||||||
{ path: '/foo', title: 'Foo' },
|
{ path: '/foo', title: 'Foo' },
|
||||||
{ path: '/bar', title: 'Bar' },
|
{ path: '/bar', title: 'Bar' },
|
||||||
@@ -45,7 +45,7 @@ describe('NavExpandableGroup', () => {
|
|||||||
<Nav aria-label="Test Navigation">
|
<Nav aria-label="Test Navigation">
|
||||||
<NavExpandableGroup
|
<NavExpandableGroup
|
||||||
groupId="test"
|
groupId="test"
|
||||||
title="Test"
|
groupTitle="Test"
|
||||||
routes={[
|
routes={[
|
||||||
{ path: '/foo', title: 'Foo' },
|
{ path: '/foo', title: 'Foo' },
|
||||||
{ path: '/bar', title: 'Bar' },
|
{ path: '/bar', title: 'Bar' },
|
||||||
|
|||||||
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).toHaveBeenCalledTimes(2);
|
||||||
expect(onSetPage).toBeCalledWith(1, 5);
|
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', () => {
|
test('adds navigation to route history on click', () => {
|
||||||
const onLogoClick = jest.fn();
|
|
||||||
logoWrapper = mount(
|
logoWrapper = mount(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<TowerLogo onClick={onLogoClick} />
|
<TowerLogo linkTo="/" />
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
@@ -43,7 +42,7 @@ describe('<TowerLogo />', () => {
|
|||||||
expect(towerLogoElem.props().history.length).toBe(2);
|
expect(towerLogoElem.props().history.length).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('gracefully handles not being passed click handler', () => {
|
test('linkTo prop is optional', () => {
|
||||||
logoWrapper = mount(
|
logoWrapper = mount(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
@@ -62,7 +61,7 @@ describe('<TowerLogo />', () => {
|
|||||||
logoWrapper = mount(
|
logoWrapper = mount(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<TowerLogo onClick={onLogoClick} />
|
<TowerLogo />
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,22 +1,51 @@
|
|||||||
import React from 'react';
|
import { mount } from 'enzyme';
|
||||||
import ReactDOM from 'react-dom';
|
import { main, getLanguage } from '../src/index';
|
||||||
|
|
||||||
import api from '../src/api';
|
const render = template => mount(template);
|
||||||
|
const data = { custom_logo: 'foo', custom_login_info: '' }
|
||||||
import indexToRender from '../src/index';
|
|
||||||
|
|
||||||
const custom_logo = (<div>logo</div>);
|
|
||||||
const custom_login_info = 'custom login info';
|
|
||||||
|
|
||||||
jest.mock('react-dom', () => ({ render: jest.fn() }));
|
|
||||||
|
|
||||||
describe('index.jsx', () => {
|
describe('index.jsx', () => {
|
||||||
test('renders without crashing', async () => {
|
test('login loads when unauthenticated', async (done) => {
|
||||||
api.getRoot = jest.fn().mockImplementation(() => Promise
|
const isAuthenticated = () => false;
|
||||||
.resolve({ data: { custom_logo, custom_login_info } }));
|
const getRoot = jest.fn(() => Promise.resolve({ data }));
|
||||||
|
|
||||||
await indexToRender();
|
const api = { getRoot, isAuthenticated };
|
||||||
|
const wrapper = await main(render, api);
|
||||||
|
|
||||||
expect(ReactDOM.render).toHaveBeenCalled();
|
expect(api.getRoot).toHaveBeenCalled();
|
||||||
|
expect(wrapper.find('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 { mount, shallow } from 'enzyme';
|
||||||
import { I18nProvider } from '@lingui/react';
|
import { I18nProvider } from '@lingui/react';
|
||||||
import { asyncFlush } from '../../jest.setup';
|
import { asyncFlush } from '../../jest.setup';
|
||||||
import AtLogin from '../../src/pages/Login';
|
import AWXLogin from '../../src/pages/Login';
|
||||||
import api from '../../src/api';
|
import APIClient from '../../src/api';
|
||||||
|
|
||||||
describe('<Login />', () => {
|
describe('<Login />', () => {
|
||||||
let loginWrapper;
|
let loginWrapper;
|
||||||
let atLogin;
|
let awxLogin;
|
||||||
let loginPage;
|
let loginPage;
|
||||||
let loginForm;
|
let loginForm;
|
||||||
let usernameInput;
|
let usernameInput;
|
||||||
@@ -16,21 +16,23 @@ describe('<Login />', () => {
|
|||||||
let submitButton;
|
let submitButton;
|
||||||
let loginHeaderLogo;
|
let loginHeaderLogo;
|
||||||
|
|
||||||
|
const api = new APIClient({});
|
||||||
|
|
||||||
const findChildren = () => {
|
const findChildren = () => {
|
||||||
atLogin = loginWrapper.find('AtLogin');
|
awxLogin = loginWrapper.find('AWXLogin');
|
||||||
loginPage = loginWrapper.find('LoginPage');
|
loginPage = loginWrapper.find('LoginPage');
|
||||||
loginForm = loginWrapper.find('LoginForm');
|
loginForm = loginWrapper.find('LoginForm');
|
||||||
usernameInput = loginWrapper.find('input#pf-login-username-id');
|
usernameInput = loginWrapper.find('input#pf-login-username-id');
|
||||||
passwordInput = loginWrapper.find('input#pf-login-password-id');
|
passwordInput = loginWrapper.find('input#pf-login-password-id');
|
||||||
submitButton = loginWrapper.find('Button[type="submit"]');
|
submitButton = loginWrapper.find('Button[type="submit"]');
|
||||||
loginHeaderLogo = loginWrapper.find('LoginHeaderBrand Brand');
|
loginHeaderLogo = loginPage.find('img');
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
loginWrapper = mount(
|
loginWrapper = mount(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<AtLogin />
|
<AWXLogin api={api} />
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
@@ -49,7 +51,7 @@ describe('<Login />', () => {
|
|||||||
expect(usernameInput.props().value).toBe('');
|
expect(usernameInput.props().value).toBe('');
|
||||||
expect(passwordInput.length).toBe(1);
|
expect(passwordInput.length).toBe(1);
|
||||||
expect(passwordInput.props().value).toBe('');
|
expect(passwordInput.props().value).toBe('');
|
||||||
expect(atLogin.state().isValidPassword).toBe(true);
|
expect(awxLogin.state().isInputValid).toBe(true);
|
||||||
expect(submitButton.length).toBe(1);
|
expect(submitButton.length).toBe(1);
|
||||||
expect(submitButton.props().isDisabled).toBe(false);
|
expect(submitButton.props().isDisabled).toBe(false);
|
||||||
expect(loginHeaderLogo.length).toBe(1);
|
expect(loginHeaderLogo.length).toBe(1);
|
||||||
@@ -59,7 +61,7 @@ describe('<Login />', () => {
|
|||||||
loginWrapper = mount(
|
loginWrapper = mount(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<AtLogin logo="images/foo.jpg" alt="Foo Application" />
|
<AWXLogin api={api} logo="images/foo.jpg" alt="Foo Application" />
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
@@ -73,7 +75,7 @@ describe('<Login />', () => {
|
|||||||
loginWrapper = mount(
|
loginWrapper = mount(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<AtLogin />
|
<AWXLogin api={api} />
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
@@ -84,49 +86,49 @@ describe('<Login />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('state maps to un/pw input value props', () => {
|
test('state maps to un/pw input value props', () => {
|
||||||
atLogin.setState({ username: 'un', password: 'pw' });
|
awxLogin.setState({ username: 'un', password: 'pw' });
|
||||||
expect(atLogin.state().username).toBe('un');
|
expect(awxLogin.state().username).toBe('un');
|
||||||
expect(atLogin.state().password).toBe('pw');
|
expect(awxLogin.state().password).toBe('pw');
|
||||||
findChildren();
|
findChildren();
|
||||||
expect(usernameInput.props().value).toBe('un');
|
expect(usernameInput.props().value).toBe('un');
|
||||||
expect(passwordInput.props().value).toBe('pw');
|
expect(passwordInput.props().value).toBe('pw');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('updating un/pw clears out error', () => {
|
test('updating un/pw clears out error', () => {
|
||||||
atLogin.setState({ isValidPassword: false });
|
awxLogin.setState({ isInputValid: false });
|
||||||
expect(loginWrapper.find('.pf-c-form__helper-text.pf-m-error').length).toBe(1);
|
expect(loginWrapper.find('.pf-c-form__helper-text.pf-m-error').length).toBe(1);
|
||||||
usernameInput.instance().value = 'uname';
|
usernameInput.instance().value = 'uname';
|
||||||
usernameInput.simulate('change');
|
usernameInput.simulate('change');
|
||||||
expect(atLogin.state().username).toBe('uname');
|
expect(awxLogin.state().username).toBe('uname');
|
||||||
expect(atLogin.state().isValidPassword).toBe(true);
|
expect(awxLogin.state().isInputValid).toBe(true);
|
||||||
expect(loginWrapper.find('.pf-c-form__helper-text.pf-m-error').length).toBe(0);
|
expect(loginWrapper.find('.pf-c-form__helper-text.pf-m-error').length).toBe(0);
|
||||||
atLogin.setState({ isValidPassword: false });
|
awxLogin.setState({ isInputValid: false });
|
||||||
expect(loginWrapper.find('.pf-c-form__helper-text.pf-m-error').length).toBe(1);
|
expect(loginWrapper.find('.pf-c-form__helper-text.pf-m-error').length).toBe(1);
|
||||||
passwordInput.instance().value = 'pword';
|
passwordInput.instance().value = 'pword';
|
||||||
passwordInput.simulate('change');
|
passwordInput.simulate('change');
|
||||||
expect(atLogin.state().password).toBe('pword');
|
expect(awxLogin.state().password).toBe('pword');
|
||||||
expect(atLogin.state().isValidPassword).toBe(true);
|
expect(awxLogin.state().isInputValid).toBe(true);
|
||||||
expect(loginWrapper.find('.pf-c-form__helper-text.pf-m-error').length).toBe(0);
|
expect(loginWrapper.find('.pf-c-form__helper-text.pf-m-error').length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('api.login not called when loading', () => {
|
test('api.login not called when loading', () => {
|
||||||
api.login = jest.fn().mockImplementation(() => Promise.resolve({}));
|
api.login = jest.fn().mockImplementation(() => Promise.resolve({}));
|
||||||
expect(atLogin.state().loading).toBe(false);
|
expect(awxLogin.state().isLoading).toBe(false);
|
||||||
atLogin.setState({ loading: true });
|
awxLogin.setState({ isLoading: true });
|
||||||
submitButton.simulate('click');
|
submitButton.simulate('click');
|
||||||
expect(api.login).toHaveBeenCalledTimes(0);
|
expect(api.login).toHaveBeenCalledTimes(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('submit calls api.login successfully', async () => {
|
test('submit calls api.login successfully', async () => {
|
||||||
api.login = jest.fn().mockImplementation(() => Promise.resolve({}));
|
api.login = jest.fn().mockImplementation(() => Promise.resolve({}));
|
||||||
expect(atLogin.state().loading).toBe(false);
|
expect(awxLogin.state().isLoading).toBe(false);
|
||||||
atLogin.setState({ username: 'unamee', password: 'pwordd' });
|
awxLogin.setState({ username: 'unamee', password: 'pwordd' });
|
||||||
submitButton.simulate('click');
|
submitButton.simulate('click');
|
||||||
expect(api.login).toHaveBeenCalledTimes(1);
|
expect(api.login).toHaveBeenCalledTimes(1);
|
||||||
expect(api.login).toHaveBeenCalledWith('unamee', 'pwordd');
|
expect(api.login).toHaveBeenCalledWith('unamee', 'pwordd');
|
||||||
expect(atLogin.state().loading).toBe(true);
|
expect(awxLogin.state().isLoading).toBe(true);
|
||||||
await asyncFlush();
|
await asyncFlush();
|
||||||
expect(atLogin.state().loading).toBe(false);
|
expect(awxLogin.state().isLoading).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('submit calls api.login handles 401 error', async () => {
|
test('submit calls api.login handles 401 error', async () => {
|
||||||
@@ -135,16 +137,16 @@ describe('<Login />', () => {
|
|||||||
err.response = { status: 401, message: 'problem' };
|
err.response = { status: 401, message: 'problem' };
|
||||||
return Promise.reject(err);
|
return Promise.reject(err);
|
||||||
});
|
});
|
||||||
expect(atLogin.state().loading).toBe(false);
|
expect(awxLogin.state().isLoading).toBe(false);
|
||||||
expect(atLogin.state().isValidPassword).toBe(true);
|
expect(awxLogin.state().isInputValid).toBe(true);
|
||||||
atLogin.setState({ username: 'unamee', password: 'pwordd' });
|
awxLogin.setState({ username: 'unamee', password: 'pwordd' });
|
||||||
submitButton.simulate('click');
|
submitButton.simulate('click');
|
||||||
expect(api.login).toHaveBeenCalledTimes(1);
|
expect(api.login).toHaveBeenCalledTimes(1);
|
||||||
expect(api.login).toHaveBeenCalledWith('unamee', 'pwordd');
|
expect(api.login).toHaveBeenCalledWith('unamee', 'pwordd');
|
||||||
expect(atLogin.state().loading).toBe(true);
|
expect(awxLogin.state().isLoading).toBe(true);
|
||||||
await asyncFlush();
|
await asyncFlush();
|
||||||
expect(atLogin.state().isValidPassword).toBe(false);
|
expect(awxLogin.state().isInputValid).toBe(false);
|
||||||
expect(atLogin.state().loading).toBe(false);
|
expect(awxLogin.state().isLoading).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('submit calls api.login handles non-401 error', async () => {
|
test('submit calls api.login handles non-401 error', async () => {
|
||||||
@@ -153,20 +155,20 @@ describe('<Login />', () => {
|
|||||||
err.response = { status: 500, message: 'problem' };
|
err.response = { status: 500, message: 'problem' };
|
||||||
return Promise.reject(err);
|
return Promise.reject(err);
|
||||||
});
|
});
|
||||||
expect(atLogin.state().loading).toBe(false);
|
expect(awxLogin.state().isLoading).toBe(false);
|
||||||
atLogin.setState({ username: 'unamee', password: 'pwordd' });
|
awxLogin.setState({ username: 'unamee', password: 'pwordd' });
|
||||||
submitButton.simulate('click');
|
submitButton.simulate('click');
|
||||||
expect(api.login).toHaveBeenCalledTimes(1);
|
expect(api.login).toHaveBeenCalledTimes(1);
|
||||||
expect(api.login).toHaveBeenCalledWith('unamee', 'pwordd');
|
expect(api.login).toHaveBeenCalledWith('unamee', 'pwordd');
|
||||||
expect(atLogin.state().loading).toBe(true);
|
expect(awxLogin.state().isLoading).toBe(true);
|
||||||
await asyncFlush();
|
await asyncFlush();
|
||||||
expect(atLogin.state().loading).toBe(false);
|
expect(awxLogin.state().isLoading).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('render Redirect to / when already authenticated', () => {
|
test('render Redirect to / when already authenticated', () => {
|
||||||
api.isAuthenticated = jest.fn();
|
api.isAuthenticated = jest.fn();
|
||||||
api.isAuthenticated.mockReturnValue(true);
|
api.isAuthenticated.mockReturnValue(true);
|
||||||
loginWrapper = shallow(<AtLogin />);
|
loginWrapper = shallow(<AWXLogin api={api} />);
|
||||||
const redirectElem = loginWrapper.find('Redirect');
|
const redirectElem = loginWrapper.find('Redirect');
|
||||||
expect(redirectElem.length).toBe(1);
|
expect(redirectElem.length).toBe(1);
|
||||||
expect(redirectElem.props().to).toBe('/');
|
expect(redirectElem.props().to).toBe('/');
|
||||||
|
|||||||
@@ -3,9 +3,8 @@ import getTabName from '../../../src/pages/Organizations/utils';
|
|||||||
describe('getTabName', () => {
|
describe('getTabName', () => {
|
||||||
test('returns tab name', () => {
|
test('returns tab name', () => {
|
||||||
expect(getTabName('details')).toBe('Details');
|
expect(getTabName('details')).toBe('Details');
|
||||||
expect(getTabName('users')).toBe('Users');
|
expect(getTabName('access')).toBe('Access');
|
||||||
expect(getTabName('teams')).toBe('Teams');
|
expect(getTabName('teams')).toBe('Teams');
|
||||||
expect(getTabName('admins')).toBe('Admins');
|
|
||||||
expect(getTabName('notifications')).toBe('Notifications');
|
expect(getTabName('notifications')).toBe('Notifications');
|
||||||
expect(getTabName('unknown')).toBe('');
|
expect(getTabName('unknown')).toBe('');
|
||||||
expect(getTabName()).toBe('');
|
expect(getTabName()).toBe('');
|
||||||
|
|||||||
220
package-lock.json
generated
220
package-lock.json
generated
@@ -1311,21 +1311,22 @@
|
|||||||
"integrity": "sha512-sIRfo/tk4NSnaRwHIHLUf4XoqzNNa4MMa8ZWivzzSfdZ5pCbgvZtyEUqKnQAEH6zuaCM9S8HyhxcNXnm/xaYaQ=="
|
"integrity": "sha512-sIRfo/tk4NSnaRwHIHLUf4XoqzNNa4MMa8ZWivzzSfdZ5pCbgvZtyEUqKnQAEH6zuaCM9S8HyhxcNXnm/xaYaQ=="
|
||||||
},
|
},
|
||||||
"@patternfly/react-core": {
|
"@patternfly/react-core": {
|
||||||
"version": "1.37.2",
|
"version": "1.43.5",
|
||||||
"resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-1.37.2.tgz",
|
"resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-1.43.5.tgz",
|
||||||
"integrity": "sha512-zzHwqGEsRWzw9uRkbrf6PmUpcl6EMxQSbUJ1zmv7Ryc32CcSMgrDL4ZA3x/tf4TAYTMRBKUK3O8S5veRjxpFuw==",
|
"integrity": "sha512-xO37/q5BJEdGvAoPllm/gbcwCtW7t0Ae7mKm5UU7d4i5bycEjd0UwJacYxCA6GFTwxN5kzn61XEpAUPY49U3pA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@patternfly/react-icons": "^2.9.1",
|
"@patternfly/react-icons": "^2.9.5",
|
||||||
"@patternfly/react-styles": "^2.3.0",
|
"@patternfly/react-styles": "^2.3.0",
|
||||||
"@patternfly/react-tokens": "^1.0.0",
|
"@patternfly/react-tokens": "^1.0.0",
|
||||||
|
"@tippy.js/react": "^1.1.1",
|
||||||
"exenv": "^1.2.2",
|
"exenv": "^1.2.2",
|
||||||
"focus-trap-react": "^4.0.1"
|
"focus-trap-react": "^4.0.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@patternfly/react-icons": {
|
"@patternfly/react-icons": {
|
||||||
"version": "2.9.1",
|
"version": "2.9.5",
|
||||||
"resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-2.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-2.9.5.tgz",
|
||||||
"integrity": "sha512-CBTpGXvqr91rBpxeb5/l2BimrtRlMkBKnIOTgX7V44MIIq3YE3P6A6CQK0fgIH1HGvCdiNf5sXbQz9xp+pB/3A=="
|
"integrity": "sha512-5e/BD2ER5jifUjUgbIilApOfhVldlAjhQdh7EwH/M3M+qzIb+2qKxV/xQ6hWD3AA71lcYIxvPMMHgdWIAl5oPQ=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1358,6 +1359,15 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-1.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-1.9.0.tgz",
|
||||||
"integrity": "sha512-wxlxeY5B37FkI9W3x4EQyZ9Q8lra3xBYEUg5CFCmWQZTvdH4vAC19l7mE+AQZqHXD4unvltS0ndi753LeHPyAg=="
|
"integrity": "sha512-wxlxeY5B37FkI9W3x4EQyZ9Q8lra3xBYEUg5CFCmWQZTvdH4vAC19l7mE+AQZqHXD4unvltS0ndi753LeHPyAg=="
|
||||||
},
|
},
|
||||||
|
"@tippy.js/react": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tippy.js/react/-/react-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-TkL1VufxgUvTMouDoBGv2vTdtUxtLUaRpspI4Rv0DsoKe2Ex1E5bl/qISk434mhuAhEnXuemrcgTaPWrfDvmGw==",
|
||||||
|
"requires": {
|
||||||
|
"prop-types": "^15.6.2",
|
||||||
|
"tippy.js": "^3.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/node": {
|
"@types/node": {
|
||||||
"version": "10.12.1",
|
"version": "10.12.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.1.tgz",
|
||||||
@@ -2110,9 +2120,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"array-flatten": {
|
"array-flatten": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz",
|
||||||
"integrity": "sha1-Qmu52oQJDBg42BLIFQryCoMx4pY=",
|
"integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"array-includes": {
|
"array-includes": {
|
||||||
@@ -5819,7 +5829,7 @@
|
|||||||
},
|
},
|
||||||
"finalhandler": {
|
"finalhandler": {
|
||||||
"version": "1.1.1",
|
"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==",
|
"integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
@@ -6814,9 +6824,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"handle-thing": {
|
"handle-thing": {
|
||||||
"version": "1.2.5",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-1.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.0.tgz",
|
||||||
"integrity": "sha1-/Xqtcmvxpf0W38KbL3pmAdJxOcQ=",
|
"integrity": "sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"handlebars": {
|
"handlebars": {
|
||||||
@@ -7053,7 +7063,7 @@
|
|||||||
},
|
},
|
||||||
"string_decoder": {
|
"string_decoder": {
|
||||||
"version": "1.1.1",
|
"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==",
|
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
@@ -7138,7 +7148,7 @@
|
|||||||
},
|
},
|
||||||
"http-errors": {
|
"http-errors": {
|
||||||
"version": "1.6.3",
|
"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=",
|
"integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
@@ -7167,7 +7177,7 @@
|
|||||||
},
|
},
|
||||||
"http-proxy-middleware": {
|
"http-proxy-middleware": {
|
||||||
"version": "0.18.0",
|
"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==",
|
"integrity": "sha512-Fs25KVMPAIIcgjMZkVHJoKg9VcXcC1C8yb9JUgeDvVXY0S/zgVIhMb+qVswDIgtJe2DfckMSY2d6TuTEutlk6Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
@@ -11105,10 +11115,15 @@
|
|||||||
"integrity": "sha512-Vy9eH1dRD9wHjYt/QqXcTz+RnX/zg53xK+KljFSX30PvdDMb2z+c6uDUeblUGqqJgz3QFsdlA0IJvHziPmWtQg==",
|
"integrity": "sha512-Vy9eH1dRD9wHjYt/QqXcTz+RnX/zg53xK+KljFSX30PvdDMb2z+c6uDUeblUGqqJgz3QFsdlA0IJvHziPmWtQg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"popper.js": {
|
||||||
|
"version": "1.14.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.14.6.tgz",
|
||||||
|
"integrity": "sha512-AGwHGQBKumlk/MDfrSOf0JHhJCImdDMcGNoqKmKkU+68GFazv3CQ6q9r7Ja1sKDZmYWTckY/uLyEznheTDycnA=="
|
||||||
|
},
|
||||||
"portfinder": {
|
"portfinder": {
|
||||||
"version": "1.0.19",
|
"version": "1.0.20",
|
||||||
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.19.tgz",
|
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.20.tgz",
|
||||||
"integrity": "sha512-23aeQKW9KgHe6citUrG3r9HjeX6vls0h713TAa+CwTKZwNIr/pD2ApaxYF4Um3ZZyq4ar+Siv3+fhoHaIwSOSw==",
|
"integrity": "sha512-Yxe4mTyDzTd59PZJY4ojZR8F+E5e97iq2ZOHPz3HDgSvYC5siNad2tLooQ5y5QHyQhc3xVqvyk/eNA3wuoa7Sw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"async": "^1.5.2",
|
"async": "^1.5.2",
|
||||||
@@ -11118,7 +11133,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"async": {
|
"async": {
|
||||||
"version": "1.5.2",
|
"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=",
|
"integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=",
|
||||||
"dev": true
|
"dev": true
|
||||||
}
|
}
|
||||||
@@ -13326,59 +13341,79 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"spdy": {
|
"spdy": {
|
||||||
"version": "3.4.7",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/spdy/-/spdy-3.4.7.tgz",
|
"resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.0.tgz",
|
||||||
"integrity": "sha1-Qv9B7OXMD5mjpsKKq7c/XDsDrLw=",
|
"integrity": "sha512-ot0oEGT/PGUpzf/6uk4AWLqkq+irlqHXkrdbk51oWONh3bxQmBuljxPNl66zlRRcIJStWq0QkLUCPOPjgjvU0Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"debug": "^2.6.8",
|
"debug": "^4.1.0",
|
||||||
"handle-thing": "^1.2.5",
|
"handle-thing": "^2.0.0",
|
||||||
"http-deceiver": "^1.2.7",
|
"http-deceiver": "^1.2.7",
|
||||||
"safe-buffer": "^5.0.1",
|
|
||||||
"select-hose": "^2.0.0",
|
"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": {
|
"spdy-transport": {
|
||||||
"version": "2.1.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz",
|
||||||
"integrity": "sha512-bpUeGpZcmZ692rrTiqf9/2EUakI6/kXX1Rpe0ib/DyOzbiexVfXkw6GnvI9hVGvIwVaUhkaBojjCZwLNRGQg1g==",
|
"integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"debug": "^2.6.8",
|
"debug": "^4.1.0",
|
||||||
"detect-node": "^2.0.3",
|
"detect-node": "^2.0.4",
|
||||||
"hpack.js": "^2.1.6",
|
"hpack.js": "^2.1.6",
|
||||||
"obuf": "^1.1.1",
|
"obuf": "^1.1.2",
|
||||||
"readable-stream": "^2.2.9",
|
"readable-stream": "^3.0.6",
|
||||||
"safe-buffer": "^5.0.1",
|
"wbuf": "^1.7.3"
|
||||||
"wbuf": "^1.7.2"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"isarray": {
|
"debug": {
|
||||||
"version": "1.0.0",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
|
||||||
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
|
"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
|
"dev": true
|
||||||
},
|
},
|
||||||
"readable-stream": {
|
"readable-stream": {
|
||||||
"version": "2.3.6",
|
"version": "3.1.1",
|
||||||
"resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.1.1.tgz",
|
||||||
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
|
"integrity": "sha512-DkN66hPyqDhnIQ6Jcsvx9bFjhw214O4poMBcIMgPVpQvNy9a0e0Uhg5SqySyDKAmUlwt8LonTBz1ezOnM8pUdA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"core-util-is": "~1.0.0",
|
"inherits": "^2.0.3",
|
||||||
"inherits": "~2.0.3",
|
"string_decoder": "^1.1.1",
|
||||||
"isarray": "~1.0.0",
|
"util-deprecate": "^1.0.1"
|
||||||
"process-nextick-args": "~2.0.0",
|
|
||||||
"safe-buffer": "~5.1.1",
|
|
||||||
"string_decoder": "~1.1.1",
|
|
||||||
"util-deprecate": "~1.0.1"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"string_decoder": {
|
"string_decoder": {
|
||||||
"version": "1.1.1",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.2.0.tgz",
|
||||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
"integrity": "sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"safe-buffer": "~5.1.0"
|
"safe-buffer": "~5.1.0"
|
||||||
@@ -13891,6 +13926,14 @@
|
|||||||
"setimmediate": "^1.0.4"
|
"setimmediate": "^1.0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"tippy.js": {
|
||||||
|
"version": "3.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-3.3.0.tgz",
|
||||||
|
"integrity": "sha512-2gIQg57EFSCBqE97NZbakSkGBJF0GzdOhx/lneGQGMzJiJyvbpyKgNy4l4qofq0nEbXACl7C/jW/ErsdQa21aQ==",
|
||||||
|
"requires": {
|
||||||
|
"popper.js": "^1.14.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"tmp": {
|
"tmp": {
|
||||||
"version": "0.0.33",
|
"version": "0.0.33",
|
||||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
|
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
|
||||||
@@ -14395,9 +14438,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"url-parse": {
|
"url-parse": {
|
||||||
"version": "1.4.3",
|
"version": "1.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.4.tgz",
|
||||||
"integrity": "sha512-rh+KuAW36YKo0vClhQzLLveoj8FwPJNu65xLb7Mrt+eZht0IPT0IXgSv8gcMegZ6NvjJUALf6Mf25POlMwD1Fw==",
|
"integrity": "sha512-/92DTTorg4JjktLNLe6GPS2/RvAd/RGr6LuktmWSMLEOa6rjnlrFXNgSbSmkNvCoL2T028A0a1JaJLzRMlFoHg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"querystringify": "^2.0.0",
|
"querystringify": "^2.0.0",
|
||||||
@@ -15158,17 +15201,17 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mime": {
|
"mime": {
|
||||||
"version": "2.3.1",
|
"version": "2.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/mime/-/mime-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/mime/-/mime-2.4.0.tgz",
|
||||||
"integrity": "sha512-OEUllcVoydBHGN1z84yfQDimn58pZNNNXgZlHXSboxMlFvgI6MXSWpWKpFRra7H1HxpVhHTkrghfRW49k6yjeg==",
|
"integrity": "sha512-ikBcWwyqXQSHKtciCcctu9YfPbFYZ4+gbHEmE0Q8jzcTYQg5dHCr3g2wwAZjPoJfQVXZq6KXAjpXOTf5/cjT7w==",
|
||||||
"dev": true
|
"dev": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"webpack-dev-server": {
|
"webpack-dev-server": {
|
||||||
"version": "3.1.10",
|
"version": "3.1.14",
|
||||||
"resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.1.10.tgz",
|
"resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.1.14.tgz",
|
||||||
"integrity": "sha512-RqOAVjfqZJtQcB0LmrzJ5y4Jp78lv9CK0MZ1YJDTaTmedMZ9PU9FLMQNrMCfVu8hHzaVLVOJKBlGEHMN10z+ww==",
|
"integrity": "sha512-mGXDgz5SlTxcF3hUpfC8hrQ11yhAttuUQWf1Wmb+6zo3x6rb7b9mIfuQvAPLdfDRCGRGvakBWHdHOa0I9p/EVQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"ansi-html": "0.0.7",
|
"ansi-html": "0.0.7",
|
||||||
@@ -15190,12 +15233,14 @@
|
|||||||
"portfinder": "^1.0.9",
|
"portfinder": "^1.0.9",
|
||||||
"schema-utils": "^1.0.0",
|
"schema-utils": "^1.0.0",
|
||||||
"selfsigned": "^1.9.1",
|
"selfsigned": "^1.9.1",
|
||||||
|
"semver": "^5.6.0",
|
||||||
"serve-index": "^1.7.2",
|
"serve-index": "^1.7.2",
|
||||||
"sockjs": "0.3.19",
|
"sockjs": "0.3.19",
|
||||||
"sockjs-client": "1.3.0",
|
"sockjs-client": "1.3.0",
|
||||||
"spdy": "^3.4.1",
|
"spdy": "^4.0.0",
|
||||||
"strip-ansi": "^3.0.0",
|
"strip-ansi": "^3.0.0",
|
||||||
"supports-color": "^5.1.0",
|
"supports-color": "^5.1.0",
|
||||||
|
"url": "^0.11.0",
|
||||||
"webpack-dev-middleware": "3.4.0",
|
"webpack-dev-middleware": "3.4.0",
|
||||||
"webpack-log": "^2.0.0",
|
"webpack-log": "^2.0.0",
|
||||||
"yargs": "12.0.2"
|
"yargs": "12.0.2"
|
||||||
@@ -15247,13 +15292,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"execa": {
|
"execa": {
|
||||||
"version": "0.10.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/execa/-/execa-0.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz",
|
||||||
"integrity": "sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw==",
|
"integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"cross-spawn": "^6.0.0",
|
"cross-spawn": "^6.0.0",
|
||||||
"get-stream": "^3.0.0",
|
"get-stream": "^4.0.0",
|
||||||
"is-stream": "^1.1.0",
|
"is-stream": "^1.1.0",
|
||||||
"npm-run-path": "^2.0.0",
|
"npm-run-path": "^2.0.0",
|
||||||
"p-finally": "^1.0.0",
|
"p-finally": "^1.0.0",
|
||||||
@@ -15270,6 +15315,15 @@
|
|||||||
"locate-path": "^3.0.0"
|
"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": {
|
"globby": {
|
||||||
"version": "6.1.0",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz",
|
||||||
@@ -15344,20 +15398,20 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"os-locale": {
|
"os-locale": {
|
||||||
"version": "3.0.1",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz",
|
||||||
"integrity": "sha512-7g5e7dmXPtzcP4bgsZ8ixDVqA7oWYuEz4lOSujeWyliPai4gfVDiFIcwBg3aGCPnmSGfzOKTK3ccPn0CKv3DBw==",
|
"integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"execa": "^0.10.0",
|
"execa": "^1.0.0",
|
||||||
"lcid": "^2.0.0",
|
"lcid": "^2.0.0",
|
||||||
"mem": "^4.0.0"
|
"mem": "^4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"p-limit": {
|
"p-limit": {
|
||||||
"version": "2.0.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.1.0.tgz",
|
||||||
"integrity": "sha512-fl5s52lI5ahKCernzzIyAP0QAZbGIovtVHGwpcu1Jr/EpzLVDI2myISHwGqK7m8uQFugVWSrbxH7XnhGtvEc+A==",
|
"integrity": "sha512-NhURkNcrVB+8hNfLuysU8enY5xn2KXphsHBaC2YmRNTZRc7RWusw6apSpdEj3jo4CMb6W9nrF6tTnsJsJeyu6g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"p-try": "^2.0.0"
|
"p-try": "^2.0.0"
|
||||||
@@ -15393,6 +15447,16 @@
|
|||||||
"find-up": "^3.0.0"
|
"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": {
|
"supports-color": {
|
||||||
"version": "5.5.0",
|
"version": "5.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||||
@@ -15444,9 +15508,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-colors": {
|
"ansi-colors": {
|
||||||
"version": "3.2.1",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz",
|
||||||
"integrity": "sha512-Xt+zb6nqgvV9SWAVp0EG3lRsHcbq5DDgqjPPz6pwgtj6RKz65zGXMNa82oJfOSBA/to6GmRP7Dr+6o+kbApTzQ==",
|
"integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,12 +43,12 @@
|
|||||||
"style-loader": "^0.23.0",
|
"style-loader": "^0.23.0",
|
||||||
"webpack": "^4.23.1",
|
"webpack": "^4.23.1",
|
||||||
"webpack-cli": "^3.0.8",
|
"webpack-cli": "^3.0.8",
|
||||||
"webpack-dev-server": "^3.1.4"
|
"webpack-dev-server": "^3.1.14"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lingui/react": "^2.7.2",
|
"@lingui/react": "^2.7.2",
|
||||||
"@patternfly/patternfly-next": "^1.0.84",
|
"@patternfly/patternfly-next": "^1.0.84",
|
||||||
"@patternfly/react-core": "^1.37.2",
|
"@patternfly/react-core": "^1.43.5",
|
||||||
"@patternfly/react-icons": "^2.9.1",
|
"@patternfly/react-icons": "^2.9.1",
|
||||||
"@patternfly/react-styles": "^2.3.0",
|
"@patternfly/react-styles": "^2.3.0",
|
||||||
"@patternfly/react-tokens": "^1.9.0",
|
"@patternfly/react-tokens": "^1.9.0",
|
||||||
|
|||||||
347
src/App.jsx
347
src/App.jsx
@@ -1,261 +1,148 @@
|
|||||||
import React, { Fragment } from 'react';
|
import React, { Component, Fragment } from 'react';
|
||||||
import { ConfigContext } from './context';
|
import { global_breakpoint_md } from '@patternfly/react-tokens';
|
||||||
|
|
||||||
import { I18nProvider, I18n } from '@lingui/react';
|
|
||||||
import { t } from '@lingui/macro';
|
|
||||||
import {
|
import {
|
||||||
Redirect,
|
|
||||||
Switch,
|
|
||||||
withRouter
|
|
||||||
} from 'react-router-dom';
|
|
||||||
|
|
||||||
import {
|
|
||||||
BackgroundImage,
|
|
||||||
BackgroundImageSrc,
|
|
||||||
Nav,
|
Nav,
|
||||||
NavList,
|
NavList,
|
||||||
Page,
|
Page,
|
||||||
PageHeader,
|
PageHeader,
|
||||||
PageSidebar,
|
PageSidebar,
|
||||||
Toolbar,
|
|
||||||
ToolbarGroup,
|
|
||||||
ToolbarItem
|
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import { global_breakpoint_md as breakpointMd } from '@patternfly/react-tokens';
|
|
||||||
|
|
||||||
import api from './api';
|
import About from './components/About';
|
||||||
import { API_LOGOUT, API_CONFIG } from './endpoints';
|
|
||||||
|
|
||||||
import HelpDropdown from './components/HelpDropdown';
|
|
||||||
import LogoutButton from './components/LogoutButton';
|
|
||||||
import TowerLogo from './components/TowerLogo';
|
|
||||||
import ConditionalRedirect from './components/ConditionalRedirect';
|
|
||||||
import NavExpandableGroup from './components/NavExpandableGroup';
|
import NavExpandableGroup from './components/NavExpandableGroup';
|
||||||
|
import TowerLogo from './components/TowerLogo';
|
||||||
|
import PageHeaderToolbar from './components/PageHeaderToolbar';
|
||||||
|
import { ConfigContext } from './context';
|
||||||
|
|
||||||
import Applications from './pages/Applications';
|
class App extends Component {
|
||||||
import Credentials from './pages/Credentials';
|
constructor (props) {
|
||||||
import CredentialTypes from './pages/CredentialTypes';
|
|
||||||
import Dashboard from './pages/Dashboard';
|
|
||||||
import InstanceGroups from './pages/InstanceGroups';
|
|
||||||
import Inventories from './pages/Inventories';
|
|
||||||
import InventoryScripts from './pages/InventoryScripts';
|
|
||||||
import Jobs from './pages/Jobs';
|
|
||||||
import Login from './pages/Login';
|
|
||||||
import ManagementJobs from './pages/ManagementJobs';
|
|
||||||
import NotificationTemplates from './pages/NotificationTemplates';
|
|
||||||
import Organizations from './pages/Organizations';
|
|
||||||
import Portal from './pages/Portal';
|
|
||||||
import Projects from './pages/Projects';
|
|
||||||
import Schedules from './pages/Schedules';
|
|
||||||
import AuthSettings from './pages/AuthSettings';
|
|
||||||
import JobsSettings from './pages/JobsSettings';
|
|
||||||
import SystemSettings from './pages/SystemSettings';
|
|
||||||
import UISettings from './pages/UISettings';
|
|
||||||
import License from './pages/License';
|
|
||||||
import Teams from './pages/Teams';
|
|
||||||
import Templates from './pages/Templates';
|
|
||||||
import Users from './pages/Users';
|
|
||||||
|
|
||||||
import ja from '../build/locales/ja/messages';
|
|
||||||
import en from '../build/locales/en/messages';
|
|
||||||
|
|
||||||
const catalogs = { en, ja };
|
|
||||||
|
|
||||||
// This spits out the language and the region. Example: es-US
|
|
||||||
const language = (navigator.languages && navigator.languages[0])
|
|
||||||
|| navigator.language
|
|
||||||
|| navigator.userLanguage;
|
|
||||||
|
|
||||||
const languageWithoutRegionCode = language.toLowerCase().split(/[_-]+/)[0];
|
|
||||||
|
|
||||||
|
|
||||||
class App extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
const isNavOpen = typeof window !== 'undefined' && window.innerWidth >= parseInt(breakpointMd.value, 10);
|
// initialize with a closed navbar if window size is small
|
||||||
this.state = {
|
const isNavOpen = typeof window !== 'undefined'
|
||||||
isNavOpen,
|
&& window.innerWidth >= parseInt(global_breakpoint_md.value, 10);
|
||||||
config: {},
|
|
||||||
error: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
onNavToggle = () => {
|
this.state = {
|
||||||
this.setState(({ isNavOpen }) => ({ isNavOpen: !isNavOpen }));
|
ansible_version: null,
|
||||||
|
custom_virtualenvs: null,
|
||||||
|
isAboutModalOpen: false,
|
||||||
|
isNavOpen,
|
||||||
|
version: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.fetchConfig = this.fetchConfig.bind(this);
|
||||||
|
this.onLogout = this.onLogout.bind(this);
|
||||||
|
this.onAboutModalClose = this.onAboutModalClose.bind(this);
|
||||||
|
this.onAboutModalOpen = this.onAboutModalOpen.bind(this);
|
||||||
|
this.onNavToggle = this.onNavToggle.bind(this);
|
||||||
};
|
};
|
||||||
|
|
||||||
onLogoClick = () => {
|
componentDidMount () {
|
||||||
this.setState({ activeGroup: 'views_group' });
|
this.fetchConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
onDevLogout = async () => {
|
async fetchConfig () {
|
||||||
await api.get(API_LOGOUT);
|
const { api } = this.props;
|
||||||
this.setState({ activeGroup: 'views_group', activeItem: 'views_group_dashboard' });
|
|
||||||
}
|
|
||||||
|
|
||||||
async componentDidMount() {
|
|
||||||
// Grab our config data from the API and store in state
|
|
||||||
try {
|
try {
|
||||||
const { data } = await api.get(API_CONFIG);
|
const { data: { ansible_version, custom_virtualenvs, version } } = await api.getConfig();
|
||||||
this.setState({ config: data });
|
this.setState({ ansible_version, custom_virtualenvs, version });
|
||||||
} catch (error) {
|
} catch (err) {
|
||||||
this.setState({ error });
|
this.setState({ ansible_version: null, custom_virtualenvs: null, version: null });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
async onLogout () {
|
||||||
const { isNavOpen, config } = this.state;
|
const { api } = this.props;
|
||||||
const { logo, loginInfo, history } = this.props;
|
|
||||||
|
|
||||||
const PageToolbar = (
|
await api.logout();
|
||||||
<Toolbar>
|
window.location.replace('/#/login')
|
||||||
<ToolbarGroup>
|
}
|
||||||
<ToolbarItem>
|
|
||||||
<HelpDropdown />
|
onAboutModalOpen () {
|
||||||
</ToolbarItem>
|
this.setState({ isAboutModalOpen: true });
|
||||||
<ToolbarItem>
|
}
|
||||||
<LogoutButton onDevLogout={() => this.onDevLogout()} />
|
|
||||||
</ToolbarItem>
|
onAboutModalClose () {
|
||||||
</ToolbarGroup>
|
this.setState({ isAboutModalOpen: false });
|
||||||
</Toolbar>
|
}
|
||||||
);
|
|
||||||
|
onNavToggle () {
|
||||||
|
this.setState(({ isNavOpen }) => ({ isNavOpen: !isNavOpen }));
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const {
|
||||||
|
ansible_version,
|
||||||
|
custom_virtualenvs,
|
||||||
|
isAboutModalOpen,
|
||||||
|
isNavOpen,
|
||||||
|
version,
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const {
|
||||||
|
render,
|
||||||
|
routeGroups = [],
|
||||||
|
navLabel = '',
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
ansible_version,
|
||||||
|
custom_virtualenvs,
|
||||||
|
version,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<I18nProvider language={languageWithoutRegionCode} catalogs={catalogs}>
|
<Fragment>
|
||||||
<Fragment>
|
<Page
|
||||||
<BackgroundImage
|
usecondensed="True"
|
||||||
src={{
|
header={(
|
||||||
[BackgroundImageSrc.lg]: '/assets/images/pfbg_1200.jpg',
|
<PageHeader
|
||||||
[BackgroundImageSrc.md]: '/assets/images/pfbg_992.jpg',
|
showNavToggle
|
||||||
[BackgroundImageSrc.md2x]: '/assets/images/pfbg_992@2x.jpg',
|
onNavToggle={this.onNavToggle}
|
||||||
[BackgroundImageSrc.sm]: '/assets/images/pfbg_768.jpg',
|
logo={<TowerLogo linkTo="/"/>}
|
||||||
[BackgroundImageSrc.sm2x]: '/assets/images/pfbg_768@2x.jpg',
|
toolbar={
|
||||||
[BackgroundImageSrc.xl]: '/assets/images/pfbg_2000.jpg',
|
<PageHeaderToolbar
|
||||||
[BackgroundImageSrc.xs]: '/assets/images/pfbg_576.jpg',
|
isAboutDisabled={!version}
|
||||||
[BackgroundImageSrc.xs2x]: '/assets/images/pfbg_576@2x.jpg',
|
onAboutClick={this.onAboutModalOpen}
|
||||||
[BackgroundImageSrc.filter]: '/assets/images/background-filter.svg'
|
onLogoutClick={this.onLogout}
|
||||||
}}
|
/>
|
||||||
/>
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
sidebar={
|
||||||
|
<PageSidebar
|
||||||
|
isNavOpen={isNavOpen}
|
||||||
|
nav={(
|
||||||
|
<Nav aria-label={navLabel}>
|
||||||
|
<NavList>
|
||||||
|
{routeGroups.map(({ groupId, groupTitle, routes }) => (
|
||||||
|
<NavExpandableGroup
|
||||||
|
key={groupId}
|
||||||
|
groupId={groupId}
|
||||||
|
groupTitle={groupTitle}
|
||||||
|
routes={routes}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</NavList>
|
||||||
|
</Nav>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
<ConfigContext.Provider value={config}>
|
<ConfigContext.Provider value={config}>
|
||||||
<Switch>
|
{render && render({ routeGroups })}
|
||||||
<ConditionalRedirect
|
|
||||||
shouldRedirect={() => api.isAuthenticated()}
|
|
||||||
redirectPath="/"
|
|
||||||
path="/login"
|
|
||||||
component={() => <Login logo={logo} loginInfo={loginInfo} />}
|
|
||||||
/>
|
|
||||||
<Fragment>
|
|
||||||
<Page
|
|
||||||
header={(
|
|
||||||
<PageHeader
|
|
||||||
logo={<TowerLogo onClick={this.onLogoClick} />}
|
|
||||||
toolbar={PageToolbar}
|
|
||||||
showNavToggle
|
|
||||||
onNavToggle={this.onNavToggle}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
sidebar={(
|
|
||||||
<PageSidebar
|
|
||||||
isNavOpen={isNavOpen}
|
|
||||||
nav={(
|
|
||||||
<I18n>
|
|
||||||
{({ i18n }) => (
|
|
||||||
<Nav aria-label={i18n._(t`Primary Navigation`)}>
|
|
||||||
<NavList>
|
|
||||||
<NavExpandableGroup
|
|
||||||
groupId="views_group"
|
|
||||||
title={i18n._("Views")}
|
|
||||||
routes={[
|
|
||||||
{ path: '/home', title: i18n._('Dashboard') },
|
|
||||||
{ path: '/jobs', title: i18n._('Jobs') },
|
|
||||||
{ path: '/schedules', title: i18n._('Schedules') },
|
|
||||||
{ path: '/portal', title: i18n._('Portal Mode') },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<NavExpandableGroup
|
|
||||||
groupId="resources_group"
|
|
||||||
title={i18n._("Resources")}
|
|
||||||
routes={[
|
|
||||||
{ path: '/templates', title: i18n._('Templates') },
|
|
||||||
{ path: '/credentials', title: i18n._('Credentials') },
|
|
||||||
{ path: '/projects', title: i18n._('Projects') },
|
|
||||||
{ path: '/inventories', title: i18n._('Inventories') },
|
|
||||||
{ path: '/inventory_scripts', title: i18n._('Inventory Scripts') }
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<NavExpandableGroup
|
|
||||||
groupId="access_group"
|
|
||||||
title={i18n._("Access")}
|
|
||||||
routes={[
|
|
||||||
{ path: '/organizations', title: i18n._('Organizations') },
|
|
||||||
{ path: '/users', title: i18n._('Users') },
|
|
||||||
{ path: '/teams', title: i18n._('Teams') }
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<NavExpandableGroup
|
|
||||||
groupId="administration_group"
|
|
||||||
title={i18n._("Administration")}
|
|
||||||
routes={[
|
|
||||||
{ path: '/credential_types', title: i18n._('Credential Types') },
|
|
||||||
{ path: '/notification_templates', title: i18n._('Notifications') },
|
|
||||||
{ path: '/management_jobs', title: i18n._('Management Jobs') },
|
|
||||||
{ path: '/instance_groups', title: i18n._('Instance Groups') },
|
|
||||||
{ path: '/applications', title: i18n._('Integrations') }
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<NavExpandableGroup
|
|
||||||
groupId="settings_group"
|
|
||||||
title={i18n._("Settings")}
|
|
||||||
routes={[
|
|
||||||
{ path: '/auth_settings', title: i18n._('Authentication') },
|
|
||||||
{ path: '/jobs_settings', title: i18n._('Jobs') },
|
|
||||||
{ path: '/system_settings', title: i18n._('System') },
|
|
||||||
{ path: '/ui_settings', title: i18n._('User Interface') },
|
|
||||||
{ path: '/license', title: i18n._('License') }
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</NavList>
|
|
||||||
</Nav>
|
|
||||||
)}
|
|
||||||
</I18n>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
useCondensed
|
|
||||||
>
|
|
||||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" exact path="/" component={() => (<Redirect to="/home" />)} />
|
|
||||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/home" component={Dashboard} />
|
|
||||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/jobs" component={Jobs} />
|
|
||||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/schedules" component={Schedules} />
|
|
||||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/portal" component={Portal} />
|
|
||||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/templates" component={Templates} />
|
|
||||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/credentials" component={Credentials} />
|
|
||||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/projects" component={Projects} />
|
|
||||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/inventories" component={Inventories} />
|
|
||||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/inventory_scripts" component={InventoryScripts} />
|
|
||||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/organizations" component={Organizations} />
|
|
||||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/users" component={Users} />
|
|
||||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/teams" component={Teams} />
|
|
||||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/credential_types" component={CredentialTypes} />
|
|
||||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/notification_templates" component={NotificationTemplates} />
|
|
||||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/management_jobs" component={ManagementJobs} />
|
|
||||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/instance_groups" component={InstanceGroups} />
|
|
||||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/applications" component={Applications} />
|
|
||||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/auth_settings" component={AuthSettings} />
|
|
||||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/jobs_settings" component={JobsSettings} />
|
|
||||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/system_settings" component={SystemSettings} />
|
|
||||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/ui_settings" component={UISettings} />
|
|
||||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/license" component={License} />
|
|
||||||
|
|
||||||
</Page>
|
|
||||||
</Fragment>
|
|
||||||
</Switch>
|
|
||||||
</ConfigContext.Provider>
|
</ConfigContext.Provider>
|
||||||
</Fragment>
|
</Page>
|
||||||
</I18nProvider>
|
<About
|
||||||
|
ansible_version={ansible_version}
|
||||||
|
version={version}
|
||||||
|
isOpen={isAboutModalOpen}
|
||||||
|
onClose={this.onAboutModalClose}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withRouter(App);
|
export default App;
|
||||||
|
|||||||
69
src/api.js
69
src/api.js
@@ -1,29 +1,26 @@
|
|||||||
import axios from 'axios';
|
const API_ROOT = '/api/';
|
||||||
|
const API_LOGIN = `${API_ROOT}login/`;
|
||||||
import * as endpoints from './endpoints';
|
const API_LOGOUT = `${API_ROOT}logout/`;
|
||||||
|
const API_V2 = `${API_ROOT}v2/`;
|
||||||
const CSRF_COOKIE_NAME = 'csrftoken';
|
const API_CONFIG = `${API_V2}config/`;
|
||||||
const CSRF_HEADER_NAME = 'X-CSRFToken';
|
const API_ORGANIZATIONS = `${API_V2}organizations/`;
|
||||||
|
|
||||||
const LOGIN_CONTENT_TYPE = 'application/x-www-form-urlencoded';
|
const LOGIN_CONTENT_TYPE = 'application/x-www-form-urlencoded';
|
||||||
|
|
||||||
class APIClient {
|
class APIClient {
|
||||||
constructor () {
|
static getCookie () {
|
||||||
this.http = axios.create({
|
|
||||||
xsrfCookieName: CSRF_COOKIE_NAME,
|
|
||||||
xsrfHeaderName: CSRF_HEADER_NAME,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/* eslint class-methods-use-this: ["error", { "exceptMethods": ["getCookie"] }] */
|
|
||||||
getCookie () {
|
|
||||||
return document.cookie;
|
return document.cookie;
|
||||||
}
|
}
|
||||||
|
|
||||||
isAuthenticated () {
|
constructor (httpAdapter) {
|
||||||
let authenticated = false;
|
this.http = httpAdapter;
|
||||||
|
}
|
||||||
|
|
||||||
const parsed = (`; ${this.getCookie()}`).split('; userLoggedIn=');
|
isAuthenticated () {
|
||||||
|
const cookie = this.constructor.getCookie();
|
||||||
|
const parsed = (`; ${cookie}`).split('; userLoggedIn=');
|
||||||
|
|
||||||
|
let authenticated = false;
|
||||||
|
|
||||||
if (parsed.length === 2) {
|
if (parsed.length === 2) {
|
||||||
authenticated = parsed.pop().split(';').shift() === 'true';
|
authenticated = parsed.pop().split(';').shift() === 'true';
|
||||||
@@ -32,7 +29,7 @@ class APIClient {
|
|||||||
return authenticated;
|
return authenticated;
|
||||||
}
|
}
|
||||||
|
|
||||||
async login (username, password, redirect = endpoints.API_CONFIG) {
|
async login (username, password, redirect = API_CONFIG) {
|
||||||
const un = encodeURIComponent(username);
|
const un = encodeURIComponent(username);
|
||||||
const pw = encodeURIComponent(password);
|
const pw = encodeURIComponent(password);
|
||||||
const next = encodeURIComponent(redirect);
|
const next = encodeURIComponent(redirect);
|
||||||
@@ -40,13 +37,37 @@ class APIClient {
|
|||||||
const data = `username=${un}&password=${pw}&next=${next}`;
|
const data = `username=${un}&password=${pw}&next=${next}`;
|
||||||
const headers = { 'Content-Type': LOGIN_CONTENT_TYPE };
|
const headers = { 'Content-Type': LOGIN_CONTENT_TYPE };
|
||||||
|
|
||||||
await this.http.get(endpoints.API_LOGIN, { headers });
|
await this.http.get(API_LOGIN, { headers });
|
||||||
await this.http.post(endpoints.API_LOGIN, data, { headers });
|
const response = await this.http.post(API_LOGIN, data, { headers });
|
||||||
|
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
get = (endpoint, params = {}) => this.http.get(endpoint, { params });
|
logout () {
|
||||||
|
return this.http.get(API_LOGOUT);
|
||||||
|
}
|
||||||
|
|
||||||
post = (endpoint, data) => this.http.post(endpoint, data);
|
getRoot () {
|
||||||
|
return this.http.get(API_ROOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
getConfig () {
|
||||||
|
return this.http.get(API_CONFIG);
|
||||||
|
}
|
||||||
|
|
||||||
|
getOrganizations (params = {}) {
|
||||||
|
return this.http.get(API_ORGANIZATIONS, { params });
|
||||||
|
}
|
||||||
|
|
||||||
|
createOrganization (data) {
|
||||||
|
return this.http.post(API_ORGANIZATIONS, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
getOrganizationDetails (id) {
|
||||||
|
const endpoint = `${API_ORGANIZATIONS}${id}/`;
|
||||||
|
|
||||||
|
return this.http.get(endpoint);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new APIClient();
|
export default APIClient;
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
// page header overrides
|
// page header overrides
|
||||||
//
|
//
|
||||||
|
|
||||||
.pf-l-page__main-section.pf-m-condensed {
|
.pf-c-page__main-section.pf-m-condensed {
|
||||||
padding-top: 16px;
|
padding-top: 16px;
|
||||||
padding-bottom: 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 brandImg from '../../images/tower-logo-white.svg';
|
||||||
import logoImg from '../../images/tower-logo-login.svg';
|
import logoImg from '../../images/tower-logo-login.svg';
|
||||||
|
|
||||||
import { ConfigContext } from '../context';
|
|
||||||
|
|
||||||
class About extends React.Component {
|
class About extends React.Component {
|
||||||
createSpeechBubble = (version) => {
|
static createSpeechBubble (version) {
|
||||||
let text = `Tower ${version}`;
|
let text = `Tower ${version}`;
|
||||||
let top = '';
|
let top = '';
|
||||||
let bottom = '';
|
let bottom = '';
|
||||||
@@ -33,52 +31,56 @@ class About extends React.Component {
|
|||||||
return top + text + bottom;
|
return top + text + bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleModalToggle = () => {
|
constructor (props) {
|
||||||
const { onAboutModalClose } = this.props;
|
super(props);
|
||||||
onAboutModalClose();
|
|
||||||
};
|
this.createSpeechBubble = this.constructor.createSpeechBubble.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { isOpen } = this.props;
|
const {
|
||||||
|
ansible_version,
|
||||||
|
version,
|
||||||
|
isOpen,
|
||||||
|
onClose
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const speechBubble = this.createSpeechBubble(version);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<I18n>
|
<I18n>
|
||||||
{({ i18n }) => (
|
{({ i18n }) => (
|
||||||
<ConfigContext.Consumer>
|
<AboutModal
|
||||||
{({ ansible_version, version }) => (
|
isOpen={isOpen}
|
||||||
<AboutModal
|
onClose={onClose}
|
||||||
isOpen={isOpen}
|
productName="Ansible Tower"
|
||||||
onClose={this.handleModalToggle}
|
trademark={i18n._(t`Copyright 2018 Red Hat, Inc.`)}
|
||||||
productName="Ansible Tower"
|
brandImageSrc={brandImg}
|
||||||
trademark={i18n._(t`Copyright 2018 Red Hat, Inc.`)}
|
brandImageAlt={i18n._(t`Brand Image`)}
|
||||||
brandImageSrc={brandImg}
|
logoImageSrc={logoImg}
|
||||||
brandImageAlt={i18n._(t`Brand Image`)}
|
logoImageAlt={i18n._(t`AboutModal Logo`)}
|
||||||
logoImageSrc={logoImg}
|
heroImageSrc={heroImg}
|
||||||
logoImageAlt={i18n._(t`AboutModal Logo`)}
|
>
|
||||||
heroImageSrc={heroImg}
|
<pre>
|
||||||
>
|
{ speechBubble }
|
||||||
<pre>
|
{`
|
||||||
{this.createSpeechBubble(version)}
|
|
||||||
{`
|
|
||||||
\\
|
\\
|
||||||
\\ ^__^
|
\\ ^__^
|
||||||
(oo)\\_______
|
(oo)\\_______
|
||||||
(__) A )\\
|
(__) A )\\
|
||||||
||----w |
|
||----w |
|
||||||
|| ||
|
|| ||
|
||||||
`}
|
`}
|
||||||
</pre>
|
</pre>
|
||||||
|
<TextContent>
|
||||||
<TextContent>
|
<TextList component="dl">
|
||||||
<TextList component="dl">
|
<TextListItem component="dt">
|
||||||
<TextListItem component="dt">
|
<Trans>Ansible Version</Trans>
|
||||||
<Trans>Ansible Version</Trans>
|
</TextListItem>
|
||||||
</TextListItem>
|
<TextListItem component="dd">{ ansible_version }</TextListItem>
|
||||||
<TextListItem component="dd">{ansible_version}</TextListItem>
|
</TextList>
|
||||||
</TextList>
|
</TextContent>
|
||||||
</TextContent>
|
</AboutModal>
|
||||||
</AboutModal>
|
|
||||||
)}
|
|
||||||
</ConfigContext.Consumer>
|
|
||||||
)}
|
)}
|
||||||
</I18n>
|
</I18n>
|
||||||
);
|
);
|
||||||
|
|||||||
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,
|
searchKey: sortedColumnKey,
|
||||||
searchValue: '',
|
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 });
|
this.setState({ searchValue });
|
||||||
};
|
}
|
||||||
|
|
||||||
onSortDropdownToggle = isSortDropdownOpen => {
|
onSortDropdownToggle (isSortDropdownOpen) {
|
||||||
this.setState({ isSortDropdownOpen });
|
this.setState({ isSortDropdownOpen });
|
||||||
};
|
}
|
||||||
|
|
||||||
onSortDropdownSelect = ({ target }) => {
|
onSortDropdownSelect ({ target }) {
|
||||||
const { columns, onSort, sortOrder } = this.props;
|
const { columns, onSort, sortOrder } = this.props;
|
||||||
|
const { innerText } = target;
|
||||||
|
|
||||||
const [{ key }] = columns.filter(({ name }) => name === target.innerText);
|
const [{ key: searchKey }] = columns.filter(({ name }) => name === innerText);
|
||||||
|
|
||||||
this.setState({ isSortDropdownOpen: false });
|
this.setState({ isSortDropdownOpen: false });
|
||||||
|
onSort(searchKey, sortOrder);
|
||||||
|
}
|
||||||
|
|
||||||
onSort(key, sortOrder);
|
onSearchDropdownToggle (isSearchDropdownOpen) {
|
||||||
};
|
|
||||||
|
|
||||||
onSearchDropdownToggle = isSearchDropdownOpen => {
|
|
||||||
this.setState({ isSearchDropdownOpen });
|
this.setState({ isSearchDropdownOpen });
|
||||||
};
|
}
|
||||||
|
|
||||||
onSearchDropdownSelect = ({ target }) => {
|
onSearchDropdownSelect ({ target }) {
|
||||||
const { columns } = this.props;
|
const { columns } = this.props;
|
||||||
|
const { innerText } = target;
|
||||||
|
|
||||||
const targetName = target.innerText;
|
const [{ key: searchKey }] = columns.filter(({ name }) => name === innerText);
|
||||||
const [{ key }] = columns.filter(({ name }) => name === targetName);
|
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 () {
|
render () {
|
||||||
const { up } = DropdownPosition;
|
const { up } = DropdownPosition;
|
||||||
const {
|
const {
|
||||||
columns,
|
columns,
|
||||||
isAllSelected,
|
isAllSelected,
|
||||||
onSearch,
|
|
||||||
onSelectAll,
|
onSelectAll,
|
||||||
onSort,
|
|
||||||
sortedColumnKey,
|
sortedColumnKey,
|
||||||
sortOrder,
|
sortOrder,
|
||||||
addUrl,
|
addUrl,
|
||||||
@@ -97,29 +116,15 @@ class DataListToolbar extends React.Component {
|
|||||||
searchValue,
|
searchValue,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const [searchColumn] = columns
|
const [{ name: searchColumnName }] = columns.filter(({ key }) => key === searchKey);
|
||||||
.filter(({ key }) => key === searchKey);
|
const [{ name: sortedColumnName, isNumeric }] = columns
|
||||||
const searchColumnName = searchColumn.name;
|
|
||||||
|
|
||||||
const [sortedColumn] = columns
|
|
||||||
.filter(({ key }) => key === sortedColumnKey);
|
.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
|
const searchDropdownItems = columns
|
||||||
.filter(({ key }) => key !== searchKey)
|
.filter(({ key }) => key !== searchKey)
|
||||||
.map(({ key, name }) => (
|
.map(({ key, name }) => (
|
||||||
<DropdownItem key={key} component="button">
|
<DropdownItem key={key} component="button">
|
||||||
{ name }
|
{name}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
));
|
));
|
||||||
|
|
||||||
@@ -127,17 +132,26 @@ class DataListToolbar extends React.Component {
|
|||||||
.filter(({ key, isSortable }) => isSortable && key !== sortedColumnKey)
|
.filter(({ key, isSortable }) => isSortable && key !== sortedColumnKey)
|
||||||
.map(({ key, name }) => (
|
.map(({ key, name }) => (
|
||||||
<DropdownItem key={key} component="button">
|
<DropdownItem key={key} component="button">
|
||||||
{ name }
|
{name}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
));
|
));
|
||||||
|
|
||||||
|
let SortIcon;
|
||||||
|
if (isNumeric) {
|
||||||
|
SortIcon = sortOrder === 'ascending' ? SortNumericUpIcon : SortNumericDownIcon;
|
||||||
|
} else {
|
||||||
|
SortIcon = sortOrder === 'ascending' ? SortAlphaUpIcon : SortAlphaDownIcon;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<I18n>
|
<I18n>
|
||||||
{({ i18n }) => (
|
{({ i18n }) => (
|
||||||
<div className="awx-toolbar">
|
<div className="awx-toolbar">
|
||||||
<Level>
|
<Level>
|
||||||
<LevelItem>
|
<LevelItem>
|
||||||
<Toolbar style={{ marginLeft: '20px' }}>
|
<Toolbar
|
||||||
|
style={{ marginLeft: '20px' }}
|
||||||
|
>
|
||||||
<ToolbarGroup>
|
<ToolbarGroup>
|
||||||
<ToolbarItem>
|
<ToolbarItem>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
@@ -152,6 +166,7 @@ class DataListToolbar extends React.Component {
|
|||||||
<ToolbarItem>
|
<ToolbarItem>
|
||||||
<div className="pf-c-input-group">
|
<div className="pf-c-input-group">
|
||||||
<Dropdown
|
<Dropdown
|
||||||
|
className="searchKeyDropdown"
|
||||||
onToggle={this.onSearchDropdownToggle}
|
onToggle={this.onSearchDropdownToggle}
|
||||||
onSelect={this.onSearchDropdownSelect}
|
onSelect={this.onSearchDropdownSelect}
|
||||||
direction={up}
|
direction={up}
|
||||||
@@ -160,7 +175,7 @@ class DataListToolbar extends React.Component {
|
|||||||
<DropdownToggle
|
<DropdownToggle
|
||||||
onToggle={this.onSearchDropdownToggle}
|
onToggle={this.onSearchDropdownToggle}
|
||||||
>
|
>
|
||||||
{ searchColumnName }
|
{searchColumnName}
|
||||||
</DropdownToggle>
|
</DropdownToggle>
|
||||||
)}
|
)}
|
||||||
dropdownItems={searchDropdownItems}
|
dropdownItems={searchDropdownItems}
|
||||||
@@ -174,14 +189,16 @@ class DataListToolbar extends React.Component {
|
|||||||
<Button
|
<Button
|
||||||
variant="tertiary"
|
variant="tertiary"
|
||||||
aria-label={i18n._(t`Search`)}
|
aria-label={i18n._(t`Search`)}
|
||||||
onClick={() => onSearch(searchValue)}
|
onClick={this.onSearch}
|
||||||
>
|
>
|
||||||
<i className="fas fa-search" aria-hidden="true" />
|
<i className="fas fa-search" aria-hidden="true" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</ToolbarItem>
|
</ToolbarItem>
|
||||||
</ToolbarGroup>
|
</ToolbarGroup>
|
||||||
<ToolbarGroup>
|
<ToolbarGroup
|
||||||
|
className="sortDropdownGroup"
|
||||||
|
>
|
||||||
<ToolbarItem>
|
<ToolbarItem>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
onToggle={this.onSortDropdownToggle}
|
onToggle={this.onSortDropdownToggle}
|
||||||
@@ -192,7 +209,7 @@ class DataListToolbar extends React.Component {
|
|||||||
<DropdownToggle
|
<DropdownToggle
|
||||||
onToggle={this.onSortDropdownToggle}
|
onToggle={this.onSortDropdownToggle}
|
||||||
>
|
>
|
||||||
{ sortedColumnName }
|
{sortedColumnName}
|
||||||
</DropdownToggle>
|
</DropdownToggle>
|
||||||
)}
|
)}
|
||||||
dropdownItems={sortDropdownItems}
|
dropdownItems={sortDropdownItems}
|
||||||
@@ -200,23 +217,29 @@ class DataListToolbar extends React.Component {
|
|||||||
</ToolbarItem>
|
</ToolbarItem>
|
||||||
<ToolbarItem>
|
<ToolbarItem>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => onSort(sortedColumnKey, sortOrder === 'ascending' ? 'descending' : 'ascending')}
|
onClick={this.onSort}
|
||||||
variant="plain"
|
variant="plain"
|
||||||
aria-label={i18n._(t`Sort`)}
|
aria-label={i18n._(t`Sort`)}
|
||||||
>
|
>
|
||||||
{displayedSortIcon()}
|
<SortIcon/>
|
||||||
</Button>
|
</Button>
|
||||||
</ToolbarItem>
|
</ToolbarItem>
|
||||||
</ToolbarGroup>
|
</ToolbarGroup>
|
||||||
{ showExpandCollapse && (
|
{showExpandCollapse && (
|
||||||
<ToolbarGroup>
|
<ToolbarGroup>
|
||||||
<ToolbarItem>
|
<ToolbarItem>
|
||||||
<Button variant="plain" aria-label={i18n._(t`Expand`)}>
|
<Button
|
||||||
|
variant="plain"
|
||||||
|
aria-label={i18n._(t`Expand`)}
|
||||||
|
>
|
||||||
<BarsIcon />
|
<BarsIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</ToolbarItem>
|
</ToolbarItem>
|
||||||
<ToolbarItem>
|
<ToolbarItem>
|
||||||
<Button variant="plain" aria-label={i18n._(t`Collapse`)}>
|
<Button
|
||||||
|
variant="plain"
|
||||||
|
aria-label={i18n._(t`Collapse`)}
|
||||||
|
>
|
||||||
<EqualsIcon />
|
<EqualsIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</ToolbarItem>
|
</ToolbarItem>
|
||||||
@@ -225,14 +248,23 @@ class DataListToolbar extends React.Component {
|
|||||||
</Toolbar>
|
</Toolbar>
|
||||||
</LevelItem>
|
</LevelItem>
|
||||||
<LevelItem>
|
<LevelItem>
|
||||||
<Tooltip message={i18n._(t`Delete`)} position="top">
|
<Tooltip
|
||||||
<Button variant="plain" aria-label={i18n._(t`Delete`)}>
|
message={i18n._(t`Delete`)}
|
||||||
|
position="top"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="plain"
|
||||||
|
aria-label={i18n._(t`Delete`)}
|
||||||
|
>
|
||||||
<TrashAltIcon />
|
<TrashAltIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{addUrl && (
|
{addUrl && (
|
||||||
<Link to={addUrl}>
|
<Link to={addUrl}>
|
||||||
<Button variant="primary" aria-label={i18n._(t`Add`)}>
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
aria-label={i18n._(t`Add`)}
|
||||||
|
>
|
||||||
<PlusIcon />
|
<PlusIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</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
|
// Extract a list of paths from the route params and store them for later. This creates
|
||||||
// an array of url paths associated with any NavItem component rendered by this component.
|
// an array of url paths associated with any NavItem component rendered by this component.
|
||||||
this.navItemPaths = routes.map(({ path }) => path);
|
this.navItemPaths = routes.map(({ path }) => path);
|
||||||
|
|
||||||
|
this.isActiveGroup = this.isActiveGroup.bind(this);
|
||||||
|
this.isActivePath = this.isActivePath.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
isActiveGroup = () => this.navItemPaths.some(this.isActivePath);
|
isActiveGroup () {
|
||||||
|
return this.navItemPaths.some(this.isActivePath);
|
||||||
|
}
|
||||||
|
|
||||||
isActivePath = (path) => {
|
isActivePath (path) {
|
||||||
const { history } = this.props;
|
const { history } = this.props;
|
||||||
|
|
||||||
return history.location.pathname.startsWith(path);
|
return history.location.pathname.startsWith(path);
|
||||||
};
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { routes, groupId, staticContext, ...rest } = this.props;
|
const { groupId, groupTitle, routes } = this.props;
|
||||||
const isActive = this.isActiveGroup();
|
const isActive = this.isActiveGroup();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -33,7 +38,7 @@ class NavExpandableGroup extends Component {
|
|||||||
isActive={isActive}
|
isActive={isActive}
|
||||||
isExpanded={isActive}
|
isExpanded={isActive}
|
||||||
groupId={groupId}
|
groupId={groupId}
|
||||||
{...rest}
|
title={groupTitle}
|
||||||
>
|
>
|
||||||
{routes.map(({ path, title }) => (
|
{routes.map(({ path, title }) => (
|
||||||
<NavItem
|
<NavItem
|
||||||
|
|||||||
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,
|
DropdownDirection,
|
||||||
DropdownItem,
|
DropdownItem,
|
||||||
DropdownToggle,
|
DropdownToggle,
|
||||||
Form,
|
|
||||||
FormGroup,
|
|
||||||
Level,
|
Level,
|
||||||
LevelItem,
|
LevelItem,
|
||||||
TextInput,
|
TextInput,
|
||||||
Toolbar,
|
|
||||||
ToolbarGroup,
|
|
||||||
ToolbarItem,
|
|
||||||
Split,
|
Split,
|
||||||
SplitItem,
|
SplitItem,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
@@ -23,24 +18,32 @@ class Pagination extends Component {
|
|||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
const { page } = this.props;
|
const { page } = props;
|
||||||
|
|
||||||
this.state = { value: page, isOpen: false };
|
this.state = { value: page, isOpen: false };
|
||||||
|
|
||||||
|
this.onPageChange = this.onPageChange.bind(this);
|
||||||
|
this.onSubmit = this.onSubmit.bind(this);
|
||||||
|
this.onFirst = this.onFirst.bind(this);
|
||||||
|
this.onPrevious = this.onPrevious.bind(this);
|
||||||
|
this.onNext = this.onNext.bind(this);
|
||||||
|
this.onLast = this.onLast.bind(this);
|
||||||
|
this.onTogglePageSize = this.onTogglePageSize.bind(this);
|
||||||
|
this.onSelectPageSize = this.onSelectPageSize.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate (prevProps) {
|
componentDidUpdate (prevProps) {
|
||||||
const { page } = this.props;
|
const { page } = this.props;
|
||||||
|
|
||||||
if (prevProps.page !== page) {
|
if (prevProps.page !== page) {
|
||||||
this.setState({ value: page });
|
this.onPageChange(page);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onPageChange = value => {
|
onPageChange (value) {
|
||||||
this.setState({ value });
|
this.setState({ value });
|
||||||
};
|
}
|
||||||
|
|
||||||
onSubmit = event => {
|
onSubmit (event) {
|
||||||
const { onSetPage, page, pageCount, page_size } = this.props;
|
const { onSetPage, page, pageCount, page_size } = this.props;
|
||||||
const { value } = this.state;
|
const { value } = this.state;
|
||||||
|
|
||||||
@@ -51,46 +54,42 @@ class Pagination extends Component {
|
|||||||
|
|
||||||
if (isValid) {
|
if (isValid) {
|
||||||
onSetPage(value, page_size);
|
onSetPage(value, page_size);
|
||||||
} else{
|
} else {
|
||||||
this.setState({ value: page });
|
this.setState({ value: page });
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
onFirst = () => {
|
onFirst () {
|
||||||
const { onSetPage, page_size} = this.props;
|
const { onSetPage, page_size } = this.props;
|
||||||
|
|
||||||
onSetPage(1, page_size);
|
onSetPage(1, page_size);
|
||||||
};
|
}
|
||||||
|
|
||||||
onPrevious = () => {
|
onPrevious () {
|
||||||
const { onSetPage, page, page_size } = this.props;
|
const { onSetPage, page, page_size } = this.props;
|
||||||
const previousPage = page - 1;
|
const previousPage = page - 1;
|
||||||
|
|
||||||
if (previousPage >= 1) {
|
onSetPage(previousPage, page_size);
|
||||||
onSetPage(previousPage, page_size)
|
}
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onNext = () => {
|
onNext () {
|
||||||
const { onSetPage, page, pageCount, page_size } = this.props;
|
const { onSetPage, page, pageCount, page_size } = this.props;
|
||||||
const nextPage = page + 1;
|
const nextPage = page + 1;
|
||||||
|
|
||||||
if (nextPage <= pageCount) {
|
onSetPage(nextPage, page_size);
|
||||||
onSetPage(nextPage, page_size)
|
}
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onLast = () => {
|
onLast () {
|
||||||
const { onSetPage, pageCount, page_size } = this.props;
|
const { onSetPage, pageCount, page_size } = this.props;
|
||||||
|
|
||||||
onSetPage(pageCount, page_size)
|
onSetPage(pageCount, page_size)
|
||||||
};
|
}
|
||||||
|
|
||||||
onTogglePageSize = isOpen => {
|
onTogglePageSize (isOpen) {
|
||||||
this.setState({ isOpen });
|
this.setState({ isOpen });
|
||||||
};
|
}
|
||||||
|
|
||||||
onSelectPageSize = ({ target }) => {
|
onSelectPageSize ({ target }) {
|
||||||
const { onSetPage } = this.props;
|
const { onSetPage } = this.props;
|
||||||
|
|
||||||
const page = 1;
|
const page = 1;
|
||||||
@@ -99,7 +98,7 @@ class Pagination extends Component {
|
|||||||
this.setState({ isOpen: false });
|
this.setState({ isOpen: false });
|
||||||
|
|
||||||
onSetPage(page, page_size);
|
onSetPage(page, page_size);
|
||||||
};
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { up } = DropdownDirection;
|
const { up } = DropdownDirection;
|
||||||
@@ -140,22 +139,28 @@ class Pagination extends Component {
|
|||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
toggle={(
|
toggle={(
|
||||||
<DropdownToggle
|
<DropdownToggle
|
||||||
onToggle={this.onTogglePageSize}>
|
className="togglePageSize"
|
||||||
{ page_size }
|
onToggle={this.onTogglePageSize}
|
||||||
|
>
|
||||||
|
{page_size}
|
||||||
</DropdownToggle>
|
</DropdownToggle>
|
||||||
)}>
|
)}
|
||||||
|
>
|
||||||
{opts.map(option => (
|
{opts.map(option => (
|
||||||
<DropdownItem key={option} component="button">
|
<DropdownItem
|
||||||
{ option }
|
key={option}
|
||||||
|
component="button"
|
||||||
|
>
|
||||||
|
{option}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
))}
|
))}
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
<Trans>Per Page</Trans>
|
<Trans> Per Page</Trans>
|
||||||
</LevelItem>
|
</LevelItem>
|
||||||
<LevelItem>
|
<LevelItem>
|
||||||
<Split gutter="md" className="pf-u-display-flex pf-u-align-items-center">
|
<Split gutter="md" className="pf-u-display-flex pf-u-align-items-center">
|
||||||
<SplitItem>
|
<SplitItem>
|
||||||
<Trans>{ itemMin } - { itemMax } of { count }</Trans>
|
<Trans>{itemMin} - {itemMax} of {count}</Trans>
|
||||||
</SplitItem>
|
</SplitItem>
|
||||||
<SplitItem>
|
<SplitItem>
|
||||||
<div className="pf-c-input-group">
|
<div className="pf-c-input-group">
|
||||||
@@ -196,7 +201,7 @@ class Pagination extends Component {
|
|||||||
value={value}
|
value={value}
|
||||||
type="text"
|
type="text"
|
||||||
onChange={this.onPageChange}
|
onChange={this.onPageChange}
|
||||||
/> of { pageCount }
|
/> of {pageCount}
|
||||||
</Trans>
|
</Trans>
|
||||||
</form>
|
</form>
|
||||||
</SplitItem>
|
</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);
|
super(props);
|
||||||
|
|
||||||
this.state = { hover: false };
|
this.state = { hover: false };
|
||||||
|
|
||||||
|
this.onClick = this.onClick.bind(this);
|
||||||
|
this.onHover = this.onHover.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
onClick = () => {
|
onClick () {
|
||||||
const { history, onClick: handleClick } = this.props;
|
const { history, linkTo } = this.props;
|
||||||
|
|
||||||
if (!handleClick) return;
|
if (!linkTo) return;
|
||||||
|
|
||||||
history.push('/');
|
history.push(linkTo);
|
||||||
|
}
|
||||||
|
|
||||||
handleClick();
|
onHover () {
|
||||||
};
|
|
||||||
|
|
||||||
onHover = () => {
|
|
||||||
const { hover } = this.state;
|
const { hover } = this.state;
|
||||||
|
|
||||||
this.setState({ hover: !hover });
|
this.setState({ hover: !hover });
|
||||||
};
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { hover } = this.state;
|
const { hover } = this.state;
|
||||||
const { onClick: handleClick } = this.props;
|
|
||||||
|
|
||||||
let src = TowerLogoHeader;
|
let src = TowerLogoHeader;
|
||||||
|
|
||||||
if (hover && handleClick) {
|
if (hover) {
|
||||||
src = TowerLogoHeaderHover;
|
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 React from 'react';
|
||||||
import { render } from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
HashRouter as Router
|
HashRouter,
|
||||||
|
Redirect,
|
||||||
|
Route,
|
||||||
|
Switch,
|
||||||
} from 'react-router-dom';
|
} from 'react-router-dom';
|
||||||
import App from './App';
|
import {
|
||||||
import api from './api';
|
I18n,
|
||||||
import { API_ROOT } from './endpoints';
|
I18nProvider,
|
||||||
|
} from '@lingui/react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
|
||||||
import '@patternfly/react-core/dist/styles/base.css';
|
import '@patternfly/react-core/dist/styles/base.css';
|
||||||
import '@patternfly/patternfly-next/patternfly.css';
|
import '@patternfly/patternfly-next/patternfly.css';
|
||||||
|
|
||||||
import './app.scss';
|
import './app.scss';
|
||||||
import './components/Pagination/styles.scss';
|
import './components/Pagination/styles.scss';
|
||||||
import './components/DataListToolbar/styles.scss';
|
import './components/DataListToolbar/styles.scss';
|
||||||
|
|
||||||
const el = document.getElementById('app');
|
import APIClient from './api';
|
||||||
|
|
||||||
const main = async () => {
|
import App from './App';
|
||||||
const { custom_logo, custom_login_info } = await api.get(API_ROOT);
|
import Background from './components/Background';
|
||||||
render(<Router><App logo={custom_logo} loginInfo={custom_login_info} /></Router>, el);
|
import Applications from './pages/Applications';
|
||||||
|
import Credentials from './pages/Credentials';
|
||||||
|
import CredentialTypes from './pages/CredentialTypes';
|
||||||
|
import Dashboard from './pages/Dashboard';
|
||||||
|
import InstanceGroups from './pages/InstanceGroups';
|
||||||
|
import Inventories from './pages/Inventories';
|
||||||
|
import InventoryScripts from './pages/InventoryScripts';
|
||||||
|
import Jobs from './pages/Jobs';
|
||||||
|
import Login from './pages/Login';
|
||||||
|
import ManagementJobs from './pages/ManagementJobs';
|
||||||
|
import NotificationTemplates from './pages/NotificationTemplates';
|
||||||
|
import Organizations from './pages/Organizations';
|
||||||
|
import Portal from './pages/Portal';
|
||||||
|
import Projects from './pages/Projects';
|
||||||
|
import Schedules from './pages/Schedules';
|
||||||
|
import AuthSettings from './pages/AuthSettings';
|
||||||
|
import JobsSettings from './pages/JobsSettings';
|
||||||
|
import SystemSettings from './pages/SystemSettings';
|
||||||
|
import UISettings from './pages/UISettings';
|
||||||
|
import License from './pages/License';
|
||||||
|
import Teams from './pages/Teams';
|
||||||
|
import Templates from './pages/Templates';
|
||||||
|
import Users from './pages/Users';
|
||||||
|
import ja from '../build/locales/ja/messages';
|
||||||
|
import en from '../build/locales/en/messages';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Initialize http
|
||||||
|
//
|
||||||
|
|
||||||
|
const http = axios.create({ xsrfCookieName: 'csrftoken', xsrfHeaderName: 'X-CSRFToken' });
|
||||||
|
|
||||||
|
//
|
||||||
|
// Derive the language and region from global user agent data. Example: es-US
|
||||||
|
// see: https://developer.mozilla.org/en-US/docs/Web/API/Navigator
|
||||||
|
//
|
||||||
|
|
||||||
|
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';
|
} from '@patternfly/react-core';
|
||||||
|
|
||||||
import towerLogo from '../../images/tower-logo-header.svg';
|
import towerLogo from '../../images/tower-logo-header.svg';
|
||||||
import api from '../api';
|
|
||||||
|
|
||||||
class AtLogin extends Component {
|
class AWXLogin extends Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
isValidPassword: true,
|
isInputValid: true,
|
||||||
loading: false
|
isLoading: false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.onChangeUsername = this.onChangeUsername.bind(this);
|
||||||
|
this.onChangePassword = this.onChangePassword.bind(this);
|
||||||
|
this.onLoginButtonClick = this.onLoginButtonClick.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount () {
|
onChangeUsername (value) {
|
||||||
this.unmounting = true; // todo: state management
|
this.setState({ username: value, isInputValid: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
safeSetState = obj => !this.unmounting && this.setState(obj);
|
onChangePassword (value) {
|
||||||
|
this.setState({ password: value, isInputValid: true });
|
||||||
|
}
|
||||||
|
|
||||||
handleUsernameChange = value => this.safeSetState({ username: value, isValidPassword: true });
|
async onLoginButtonClick (event) {
|
||||||
|
const { username, password, isLoading } = this.state;
|
||||||
handlePasswordChange = value => this.safeSetState({ password: value, isValidPassword: true });
|
const { api } = this.props;
|
||||||
|
|
||||||
handleSubmit = async event => {
|
|
||||||
const { username, password, loading } = this.state;
|
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
if (!loading) {
|
if (isLoading) {
|
||||||
this.safeSetState({ loading: true });
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
this.setState({ isLoading: true });
|
||||||
await api.login(username, password);
|
|
||||||
} catch (error) {
|
try {
|
||||||
if (error.response.status === 401) {
|
await api.login(username, password);
|
||||||
this.safeSetState({ isValidPassword: false });
|
} catch (error) {
|
||||||
}
|
if (error.response && error.response.status === 401) {
|
||||||
} finally {
|
this.setState({ isInputValid: false });
|
||||||
this.safeSetState({ loading: false });
|
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
this.setState({ isLoading: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { username, password, isValidPassword } = this.state;
|
const { username, password, isInputValid } = this.state;
|
||||||
const { logo, alt } = this.props;
|
const { api, alt, loginInfo, logo } = this.props;
|
||||||
const logoSrc = logo ? `data:image/jpeg;${logo}` : towerLogo;
|
const logoSrc = logo ? `data:image/jpeg;${logo}` : towerLogo;
|
||||||
|
|
||||||
if (api.isAuthenticated()) {
|
if (api.isAuthenticated()) {
|
||||||
@@ -65,20 +69,21 @@ class AtLogin extends Component {
|
|||||||
<I18n>
|
<I18n>
|
||||||
{({ i18n }) => (
|
{({ i18n }) => (
|
||||||
<LoginPage
|
<LoginPage
|
||||||
mainBrandImgSrc={logoSrc}
|
brandImgSrc={logoSrc}
|
||||||
mainBrandImgAlt={alt || 'Ansible Tower'}
|
brandImgAlt={alt || 'Ansible Tower'}
|
||||||
loginTitle={i18n._(t`Welcome to Ansible Tower! Please Sign In.`)}
|
loginTitle={i18n._(t`Welcome to Ansible Tower! Please Sign In.`)}
|
||||||
|
textContent={loginInfo}
|
||||||
>
|
>
|
||||||
<LoginForm
|
<LoginForm
|
||||||
usernameLabel={i18n._(t`Username`)}
|
usernameLabel={i18n._(t`Username`)}
|
||||||
usernameValue={username}
|
|
||||||
onChangeUsername={this.handleUsernameChange}
|
|
||||||
passwordLabel={i18n._(t`Password`)}
|
passwordLabel={i18n._(t`Password`)}
|
||||||
passwordValue={password}
|
|
||||||
onChangePassword={this.handlePasswordChange}
|
|
||||||
isValidPassword={isValidPassword}
|
|
||||||
passwordHelperTextInvalid={i18n._(t`Invalid username or password. Please try again.`)}
|
passwordHelperTextInvalid={i18n._(t`Invalid username or password. Please try again.`)}
|
||||||
onLoginButtonClick={this.handleSubmit}
|
usernameValue={username}
|
||||||
|
passwordValue={password}
|
||||||
|
isValidPassword={isInputValid}
|
||||||
|
onChangeUsername={this.onChangeUsername}
|
||||||
|
onChangePassword={this.onChangePassword}
|
||||||
|
onLoginButtonClick={this.onLoginButtonClick}
|
||||||
/>
|
/>
|
||||||
</LoginPage>
|
</LoginPage>
|
||||||
)}
|
)}
|
||||||
@@ -87,4 +92,4 @@ class AtLogin extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AtLogin;
|
export default AWXLogin;
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import { Trans } from '@lingui/macro';
|
|||||||
import {
|
import {
|
||||||
PageSection,
|
PageSection,
|
||||||
PageSectionVariants,
|
PageSectionVariants,
|
||||||
Title,
|
Breadcrumb,
|
||||||
|
BreadcrumbItem,
|
||||||
|
BreadcrumbHeading
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import {
|
import {
|
||||||
Link
|
Link
|
||||||
@@ -21,20 +23,22 @@ const OrganizationBreadcrumb = ({ parentObj, organization, currentTab, location
|
|||||||
.map(({ url, name }, index) => {
|
.map(({ url, name }, index) => {
|
||||||
let elem;
|
let elem;
|
||||||
if (noLastLink && parentObj.length - 1 === index) {
|
if (noLastLink && parentObj.length - 1 === index) {
|
||||||
elem = (<Fragment key={name}>{name}</Fragment>);
|
elem = (<BreadcrumbHeading className="heading" key={name}>{name}</BreadcrumbHeading>);
|
||||||
} else {
|
} else {
|
||||||
elem = (
|
elem = (
|
||||||
<Link
|
<BreadcrumbItem key={name}>
|
||||||
key={name}
|
<Link
|
||||||
to={{ pathname: url, state: { breadcrumb: parentObj, organization } }}
|
key={name}
|
||||||
>
|
to={{ pathname: url, state: { breadcrumb: parentObj, organization } }}
|
||||||
{name}
|
>
|
||||||
</Link>
|
{name}
|
||||||
|
</Link>
|
||||||
|
</BreadcrumbItem>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return elem;
|
return elem;
|
||||||
})
|
})
|
||||||
.reduce((prev, curr) => [prev, ' > ', curr])}
|
.reduce((prev, curr) => [prev, curr])}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -42,25 +46,31 @@ const OrganizationBreadcrumb = ({ parentObj, organization, currentTab, location
|
|||||||
breadcrumb = (
|
breadcrumb = (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{generateCrumb()}
|
{generateCrumb()}
|
||||||
{' > '}
|
<BreadcrumbHeading className="heading">
|
||||||
{getTabName(currentTab)}
|
{getTabName(currentTab)}
|
||||||
|
</BreadcrumbHeading>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
} else if (location.pathname.indexOf('edit') > -1) {
|
} else if (location.pathname.indexOf('edit') > -1) {
|
||||||
breadcrumb = (
|
breadcrumb = (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{generateCrumb()}
|
{generateCrumb()}
|
||||||
<Trans>{' > edit'}</Trans>
|
<BreadcrumbHeading className="heading">
|
||||||
|
<Trans>Edit</Trans>
|
||||||
|
</BreadcrumbHeading>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
} else if (location.pathname.indexOf('add') > -1) {
|
} else if (location.pathname.indexOf('add') > -1) {
|
||||||
breadcrumb = (
|
breadcrumb = (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{generateCrumb()}
|
{generateCrumb()}
|
||||||
<Trans>{' > add'}</Trans>
|
<BreadcrumbHeading className="heading">
|
||||||
|
<Trans>Add</Trans>
|
||||||
|
</BreadcrumbHeading>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
breadcrumb = (
|
breadcrumb = (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{generateCrumb(true)}
|
{generateCrumb(true)}
|
||||||
@@ -71,7 +81,7 @@ const OrganizationBreadcrumb = ({ parentObj, organization, currentTab, location
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageSection variant={light} className="pf-m-condensed">
|
<PageSection variant={light} className="pf-m-condensed">
|
||||||
<Title size="2xl">{breadcrumb}</Title>
|
<Breadcrumb>{breadcrumb}</Breadcrumb>
|
||||||
</PageSection>
|
</PageSection>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,10 +6,7 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardBody,
|
CardBody,
|
||||||
PageSection,
|
PageSection,
|
||||||
PageSectionVariants,
|
PageSectionVariants
|
||||||
ToolbarGroup,
|
|
||||||
ToolbarItem,
|
|
||||||
ToolbarSection,
|
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import {
|
import {
|
||||||
Switch,
|
Switch,
|
||||||
@@ -17,39 +14,10 @@ import {
|
|||||||
Route
|
Route
|
||||||
} from 'react-router-dom';
|
} from 'react-router-dom';
|
||||||
|
|
||||||
|
import Tab from '../../../components/Tabs/Tab';
|
||||||
|
import Tabs from '../../../components/Tabs/Tabs';
|
||||||
import getTabName from '../utils';
|
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 = ({
|
const OrganizationDetail = ({
|
||||||
location,
|
location,
|
||||||
@@ -61,6 +29,7 @@ const OrganizationDetail = ({
|
|||||||
}) => {
|
}) => {
|
||||||
// TODO: set objectName by param or through grabbing org detail get from api
|
// TODO: set objectName by param or through grabbing org detail get from api
|
||||||
const { medium } = PageSectionVariants;
|
const { medium } = PageSectionVariants;
|
||||||
|
const tabList=['details', 'access', 'teams', 'notifications'];
|
||||||
|
|
||||||
const deleteResourceView = () => (
|
const deleteResourceView = () => (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
@@ -93,34 +62,29 @@ const OrganizationDetail = ({
|
|||||||
</Fragment>
|
</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 (
|
return (
|
||||||
<PageSection variant={medium}>
|
<PageSection variant={medium}>
|
||||||
<Card className="at-c-orgPane">
|
<Card className="at-c-orgPane">
|
||||||
<CardHeader>
|
<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>
|
</CardHeader>
|
||||||
<CardBody>
|
<CardBody>
|
||||||
{(currentTab && currentTab !== 'details') ? (
|
{(currentTab && currentTab !== 'details') ? (
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ export default ({
|
|||||||
name,
|
name,
|
||||||
userCount,
|
userCount,
|
||||||
teamCount,
|
teamCount,
|
||||||
adminCount,
|
|
||||||
isSelected,
|
isSelected,
|
||||||
onSelect,
|
onSelect,
|
||||||
detailUrl,
|
detailUrl,
|
||||||
@@ -46,7 +45,7 @@ export default ({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="pf-c-data-list__cell">
|
<div className="pf-c-data-list__cell">
|
||||||
<Link to={`${detailUrl}?tab=users`}>
|
<Link to={`${detailUrl}?tab=access`}>
|
||||||
<Trans>Users</Trans>
|
<Trans>Users</Trans>
|
||||||
</Link>
|
</Link>
|
||||||
<Badge isRead>
|
<Badge isRead>
|
||||||
@@ -62,14 +61,6 @@ export default ({
|
|||||||
{teamCount}
|
{teamCount}
|
||||||
{' '}
|
{' '}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Link to={`${detailUrl}?tab=admins`}>
|
|
||||||
<Trans>Admins</Trans>
|
|
||||||
</Link>
|
|
||||||
<Badge isRead>
|
|
||||||
{' '}
|
|
||||||
{adminCount}
|
|
||||||
{' '}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="pf-c-data-list__cell" />
|
<div className="pf-c-data-list__cell" />
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -5,12 +5,31 @@ import OrganizationAdd from './views/Organization.add';
|
|||||||
import OrganizationView from './views/Organization.view';
|
import OrganizationView from './views/Organization.view';
|
||||||
import OrganizationsList from './views/Organizations.list';
|
import OrganizationsList from './views/Organizations.list';
|
||||||
|
|
||||||
const Organizations = ({ match }) => (
|
export default ({ api, match }) => (
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path={`${match.path}/add`} component={OrganizationAdd} />
|
<Route
|
||||||
<Route path={`${match.path}/:id`} component={OrganizationView} />
|
path={`${match.path}/add`}
|
||||||
<Route path={`${match.path}`} component={OrganizationsList} />
|
render={() => (
|
||||||
|
<OrganizationAdd
|
||||||
|
api={api}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path={`${match.path}/:id`}
|
||||||
|
render={() => (
|
||||||
|
<OrganizationView
|
||||||
|
api={api}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path={`${match.path}`}
|
||||||
|
render={() => (
|
||||||
|
<OrganizationsList
|
||||||
|
api={api}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</Switch>
|
</Switch>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default Organizations;
|
|
||||||
|
|||||||
@@ -1,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 = '';
|
let tabName = '';
|
||||||
if (tab === 'details') {
|
if (tab === 'details') {
|
||||||
tabName = 'Details';
|
tabName = 'Details';
|
||||||
} else if (tab === 'users') {
|
} else if (tab === 'access') {
|
||||||
tabName = 'Users';
|
tabName = 'Access';
|
||||||
} else if (tab === 'teams') {
|
} else if (tab === 'teams') {
|
||||||
tabName = 'Teams';
|
tabName = 'Teams';
|
||||||
} else if (tab === 'admins') {
|
|
||||||
tabName = 'Admins';
|
|
||||||
} else if (tab === 'notifications') {
|
} else if (tab === 'notifications') {
|
||||||
tabName = 'Notifications';
|
tabName = 'Notifications';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,11 +19,9 @@ import {
|
|||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
|
|
||||||
import { ConfigContext } from '../../../context';
|
import { ConfigContext } from '../../../context';
|
||||||
import { API_ORGANIZATIONS } from '../../../endpoints';
|
|
||||||
import { API_INSTANCE_GROUPS } from '../../../endpoints';
|
|
||||||
import api from '../../../api';
|
import api from '../../../api';
|
||||||
import AnsibleSelect from '../../../components/AnsibleSelect';
|
|
||||||
import Lookup from '../../../components/Lookup';
|
import Lookup from '../../../components/Lookup';
|
||||||
|
import AnsibleSelect from '../../../components/AnsibleSelect'
|
||||||
const { light } = PageSectionVariants;
|
const { light } = PageSectionVariants;
|
||||||
|
|
||||||
class OrganizationAdd extends React.Component {
|
class OrganizationAdd extends React.Component {
|
||||||
@@ -71,8 +69,9 @@ class OrganizationAdd extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async onSubmit() {
|
async onSubmit() {
|
||||||
|
const { api } = this.props;
|
||||||
const data = Object.assign({}, { ...this.state });
|
const data = Object.assign({}, { ...this.state });
|
||||||
await api.post(API_ORGANIZATIONS, data);
|
await api.createOrganization(data);
|
||||||
this.resetForm();
|
this.resetForm();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +80,8 @@ class OrganizationAdd extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
const { data } = await api.get(API_INSTANCE_GROUPS);
|
const { api } = this.props;
|
||||||
|
const { data } = await api.getInstanceGroups();
|
||||||
let results = [];
|
let results = [];
|
||||||
data.results.map((result) => {
|
data.results.map((result) => {
|
||||||
results.push({ id: result.id, name: result.name, isChecked: false });
|
results.push({ id: result.id, name: result.name, isChecked: false });
|
||||||
@@ -92,6 +92,7 @@ class OrganizationAdd extends React.Component {
|
|||||||
render() {
|
render() {
|
||||||
const { name, results } = this.state;
|
const { name, results } = this.state;
|
||||||
const enabled = name.length > 0; // TODO: add better form validation
|
const enabled = name.length > 0; // TODO: add better form validation
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<PageSection variant={light} className="pf-m-condensed">
|
<PageSection variant={light} className="pf-m-condensed">
|
||||||
|
|||||||
@@ -2,16 +2,13 @@ import React, { Component, Fragment } from 'react';
|
|||||||
import { i18nMark } from '@lingui/react';
|
import { i18nMark } from '@lingui/react';
|
||||||
import {
|
import {
|
||||||
Switch,
|
Switch,
|
||||||
Route
|
Route,
|
||||||
|
withRouter,
|
||||||
} from 'react-router-dom';
|
} from 'react-router-dom';
|
||||||
|
|
||||||
import OrganizationBreadcrumb from '../components/OrganizationBreadcrumb';
|
import OrganizationBreadcrumb from '../components/OrganizationBreadcrumb';
|
||||||
import OrganizationDetail from '../components/OrganizationDetail';
|
import OrganizationDetail from '../components/OrganizationDetail';
|
||||||
import OrganizationEdit from '../components/OrganizationEdit';
|
import OrganizationEdit from '../components/OrganizationEdit';
|
||||||
|
|
||||||
import api from '../../../api';
|
|
||||||
import { API_ORGANIZATIONS } from '../../../endpoints';
|
|
||||||
|
|
||||||
class OrganizationView extends Component {
|
class OrganizationView extends Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -30,6 +27,8 @@ class OrganizationView extends Component {
|
|||||||
loading: false,
|
loading: false,
|
||||||
mounted: false
|
mounted: false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.fetchOrganization = this.fetchOrganization.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
@@ -47,13 +46,15 @@ class OrganizationView extends Component {
|
|||||||
|
|
||||||
async fetchOrganization () {
|
async fetchOrganization () {
|
||||||
const { mounted } = this.state;
|
const { mounted } = this.state;
|
||||||
|
const { api } = this.props;
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
this.setState({ error: false, loading: true });
|
this.setState({ error: false, loading: true });
|
||||||
|
|
||||||
const { match } = this.props;
|
const { match } = this.props;
|
||||||
const { parentBreadcrumbObj, organization } = this.state;
|
const { parentBreadcrumbObj, organization } = this.state;
|
||||||
try {
|
try {
|
||||||
const { data } = await api.get(`${API_ORGANIZATIONS}${match.params.id}/`);
|
const { data } = await api.getOrganizationDetails(match.params.id);
|
||||||
if (organization === 'loading') {
|
if (organization === 'loading') {
|
||||||
this.setState({ organization: data });
|
this.setState({ organization: data });
|
||||||
}
|
}
|
||||||
@@ -118,4 +119,4 @@ class OrganizationView extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default OrganizationView;
|
export default withRouter(OrganizationView);
|
||||||
|
|||||||
@@ -17,9 +17,6 @@ import DataListToolbar from '../../../components/DataListToolbar';
|
|||||||
import OrganizationListItem from '../components/OrganizationListItem';
|
import OrganizationListItem from '../components/OrganizationListItem';
|
||||||
import Pagination from '../../../components/Pagination';
|
import Pagination from '../../../components/Pagination';
|
||||||
|
|
||||||
import api from '../../../api';
|
|
||||||
import { API_ORGANIZATIONS } from '../../../endpoints';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
encodeQueryString,
|
encodeQueryString,
|
||||||
parseQueryString,
|
parseQueryString,
|
||||||
@@ -56,6 +53,15 @@ class Organizations extends Component {
|
|||||||
results: [],
|
results: [],
|
||||||
selected: [],
|
selected: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.onSearch = this.onSearch.bind(this);
|
||||||
|
this.getQueryParams = this.getQueryParams.bind(this);
|
||||||
|
this.onSort = this.onSort.bind(this);
|
||||||
|
this.onSetPage = this.onSetPage.bind(this);
|
||||||
|
this.onSelectAll = this.onSelectAll.bind(this);
|
||||||
|
this.onSelect = this.onSelect.bind(this);
|
||||||
|
this.updateUrl = this.updateUrl.bind(this);
|
||||||
|
this.fetchOrganizations = this.fetchOrganizations.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
@@ -78,7 +84,7 @@ class Organizations extends Component {
|
|||||||
return Object.assign({}, this.defaultParams, searchParams, overrides);
|
return Object.assign({}, this.defaultParams, searchParams, overrides);
|
||||||
}
|
}
|
||||||
|
|
||||||
onSort = (sortedColumnKey, sortOrder) => {
|
onSort(sortedColumnKey, sortOrder) {
|
||||||
const { page_size } = this.state;
|
const { page_size } = this.state;
|
||||||
|
|
||||||
let order_by = sortedColumnKey;
|
let order_by = sortedColumnKey;
|
||||||
@@ -90,26 +96,26 @@ class Organizations extends Component {
|
|||||||
const queryParams = this.getQueryParams({ order_by, page_size });
|
const queryParams = this.getQueryParams({ order_by, page_size });
|
||||||
|
|
||||||
this.fetchOrganizations(queryParams);
|
this.fetchOrganizations(queryParams);
|
||||||
};
|
}
|
||||||
|
|
||||||
onSetPage = (pageNumber, pageSize) => {
|
onSetPage (pageNumber, pageSize) {
|
||||||
const page = parseInt(pageNumber, 10);
|
const page = parseInt(pageNumber, 10);
|
||||||
const page_size = parseInt(pageSize, 10);
|
const page_size = parseInt(pageSize, 10);
|
||||||
|
|
||||||
const queryParams = this.getQueryParams({ page, page_size });
|
const queryParams = this.getQueryParams({ page, page_size });
|
||||||
|
|
||||||
this.fetchOrganizations(queryParams);
|
this.fetchOrganizations(queryParams);
|
||||||
};
|
}
|
||||||
|
|
||||||
onSelectAll = isSelected => {
|
onSelectAll (isSelected) {
|
||||||
const { results } = this.state;
|
const { results } = this.state;
|
||||||
|
|
||||||
const selected = isSelected ? results.map(o => o.id) : [];
|
const selected = isSelected ? results.map(o => o.id) : [];
|
||||||
|
|
||||||
this.setState({ selected });
|
this.setState({ selected });
|
||||||
};
|
}
|
||||||
|
|
||||||
onSelect = id => {
|
onSelect (id) {
|
||||||
const { selected } = this.state;
|
const { selected } = this.state;
|
||||||
|
|
||||||
const isSelected = selected.includes(id);
|
const isSelected = selected.includes(id);
|
||||||
@@ -119,7 +125,7 @@ class Organizations extends Component {
|
|||||||
} else {
|
} else {
|
||||||
this.setState({ selected: selected.concat(id) });
|
this.setState({ selected: selected.concat(id) });
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
updateUrl (queryParams) {
|
updateUrl (queryParams) {
|
||||||
const { history, location } = this.props;
|
const { history, location } = this.props;
|
||||||
@@ -132,6 +138,7 @@ class Organizations extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fetchOrganizations (queryParams) {
|
async fetchOrganizations (queryParams) {
|
||||||
|
const { api } = this.props;
|
||||||
const { page, page_size, order_by } = queryParams;
|
const { page, page_size, order_by } = queryParams;
|
||||||
|
|
||||||
let sortOrder = 'ascending';
|
let sortOrder = 'ascending';
|
||||||
@@ -145,7 +152,7 @@ class Organizations extends Component {
|
|||||||
this.setState({ error: false, loading: true });
|
this.setState({ error: false, loading: true });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data } = await api.get(API_ORGANIZATIONS, queryParams);
|
const { data } = await api.getOrganizations(queryParams);
|
||||||
const { count, results } = data;
|
const { count, results } = data;
|
||||||
|
|
||||||
const pageCount = Math.ceil(count / page_size);
|
const pageCount = Math.ceil(count / page_size);
|
||||||
@@ -218,7 +225,6 @@ class Organizations extends Component {
|
|||||||
parentBreadcrumb={parentBreadcrumb}
|
parentBreadcrumb={parentBreadcrumb}
|
||||||
userCount={o.summary_fields.related_field_counts.users}
|
userCount={o.summary_fields.related_field_counts.users}
|
||||||
teamCount={o.summary_fields.related_field_counts.teams}
|
teamCount={o.summary_fields.related_field_counts.teams}
|
||||||
adminCount={o.summary_fields.related_field_counts.admins}
|
|
||||||
isSelected={selected.includes(o.id)}
|
isSelected={selected.includes(o.id)}
|
||||||
onSelect={() => this.onSelect(o.id)}
|
onSelect={() => this.onSelect(o.id)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const webpack = require('webpack');
|
const webpack = require('webpack');
|
||||||
|
|
||||||
const TARGET_PORT = 8043;
|
const TARGET_PORT = process.env.TARGET_PORT || 8043;
|
||||||
const TARGET = `https://localhost:${TARGET_PORT}`;
|
const TARGET_HOST = process.env.TARGET_HOST || 'localhost';
|
||||||
|
const TARGET = `https://${TARGET_HOST}:${TARGET_PORT}`;
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
entry: './src/index.jsx',
|
entry: './src/index.jsx',
|
||||||
|
|||||||
Reference in New Issue
Block a user