mirror of
https://github.com/ansible/awx.git
synced 2026-05-09 18:37:36 -02:30
Merge pull request #84 from jakemcdermott/tests-fixup
add more unit and functional test coverage
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { HashRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import { I18nProvider } from '@lingui/react';
|
import { I18nProvider } from '@lingui/react';
|
||||||
|
|
||||||
import { mount, shallow } from 'enzyme';
|
import { mount, shallow } from 'enzyme';
|
||||||
@@ -12,7 +12,7 @@ const DEFAULT_ACTIVE_GROUP = 'views_group';
|
|||||||
describe('<App />', () => {
|
describe('<App />', () => {
|
||||||
test('expected content is rendered', () => {
|
test('expected content is rendered', () => {
|
||||||
const appWrapper = mount(
|
const appWrapper = mount(
|
||||||
<HashRouter>
|
<MemoryRouter>
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<App
|
<App
|
||||||
routeGroups={[
|
routeGroups={[
|
||||||
@@ -37,7 +37,7 @@ describe('<App />', () => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</HashRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
|
|
||||||
// page components
|
// page components
|
||||||
@@ -56,6 +56,48 @@ describe('<App />', () => {
|
|||||||
expect(appWrapper.find('#group_two').length).toBe(1);
|
expect(appWrapper.find('#group_two').length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('opening the about modal renders prefetched config data', async (done) => {
|
||||||
|
const ansible_version = '111';
|
||||||
|
const version = '222';
|
||||||
|
|
||||||
|
const getConfig = jest.fn(() => Promise.resolve({ data: { ansible_version, version} }));
|
||||||
|
const api = { getConfig };
|
||||||
|
|
||||||
|
const wrapper = mount(
|
||||||
|
<MemoryRouter>
|
||||||
|
<I18nProvider>
|
||||||
|
<App api={api}/>
|
||||||
|
</I18nProvider>
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
|
||||||
|
await asyncFlush();
|
||||||
|
expect(getConfig).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
// open about modal
|
||||||
|
const aboutDropdown = 'Dropdown QuestionCircleIcon';
|
||||||
|
const aboutButton = 'DropdownItem li button';
|
||||||
|
const aboutModalContent = 'AboutModalBoxContent';
|
||||||
|
const aboutModalClose = 'button[aria-label="Close Dialog"]';
|
||||||
|
|
||||||
|
expect(wrapper.find(aboutModalContent)).toHaveLength(0);
|
||||||
|
wrapper.find(aboutDropdown).simulate('click');
|
||||||
|
wrapper.find(aboutButton).simulate('click');
|
||||||
|
wrapper.update();
|
||||||
|
|
||||||
|
// check about modal content
|
||||||
|
const content = wrapper.find(aboutModalContent);
|
||||||
|
expect(content).toHaveLength(1);
|
||||||
|
expect(content.find('dd').text()).toContain(ansible_version);
|
||||||
|
expect(content.find('pre').text()).toContain(`< Tower ${version} >`);
|
||||||
|
|
||||||
|
// close about modal
|
||||||
|
wrapper.find(aboutModalClose).simulate('click');
|
||||||
|
expect(wrapper.find(aboutModalContent)).toHaveLength(0);
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
test('onNavToggle sets state.isNavOpen to opposite', () => {
|
test('onNavToggle sets state.isNavOpen to opposite', () => {
|
||||||
const appWrapper = shallow(<App />);
|
const appWrapper = shallow(<App />);
|
||||||
const { onNavToggle } = appWrapper.instance();
|
const { onNavToggle } = appWrapper.instance();
|
||||||
@@ -66,17 +108,6 @@ describe('<App />', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('onLogoClick sets selected nav back to defaults', () => {
|
|
||||||
const appWrapper = shallow(<App />);
|
|
||||||
|
|
||||||
appWrapper.setState({ activeGroup: 'foo', activeItem: 'bar' });
|
|
||||||
expect(appWrapper.state().activeItem).toBe('bar');
|
|
||||||
expect(appWrapper.state().activeGroup).toBe('foo');
|
|
||||||
|
|
||||||
appWrapper.instance().onLogoClick();
|
|
||||||
expect(appWrapper.state().activeGroup).toBe(DEFAULT_ACTIVE_GROUP);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('onLogout makes expected call to api client', async (done) => {
|
test('onLogout makes expected call to api client', async (done) => {
|
||||||
const logout = jest.fn(() => Promise.resolve());
|
const logout = jest.fn(() => Promise.resolve());
|
||||||
const api = { logout };
|
const api = { logout };
|
||||||
@@ -89,17 +120,4 @@ describe('<App />', () => {
|
|||||||
|
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Component makes expected call to api client when mounted', () => {
|
|
||||||
const getConfig = jest.fn().mockImplementation(() => Promise.resolve({}));
|
|
||||||
const api = { getConfig };
|
|
||||||
const appWrapper = mount(
|
|
||||||
<HashRouter>
|
|
||||||
<I18nProvider>
|
|
||||||
<App api={api} />
|
|
||||||
</I18nProvider>
|
|
||||||
</HashRouter>
|
|
||||||
);
|
|
||||||
expect(getConfig).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -58,4 +58,72 @@ describe('APIClient (api.js)', () => {
|
|||||||
|
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('logout calls expected http method', async (done) => {
|
||||||
|
const createPromise = () => Promise.resolve();
|
||||||
|
const mockHttp = ({ get: jest.fn(createPromise) });
|
||||||
|
|
||||||
|
const api = new APIClient(mockHttp);
|
||||||
|
await api.logout();
|
||||||
|
|
||||||
|
expect(mockHttp.get).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getConfig calls expected http method', async (done) => {
|
||||||
|
const createPromise = () => Promise.resolve();
|
||||||
|
const mockHttp = ({ get: jest.fn(createPromise) });
|
||||||
|
|
||||||
|
const api = new APIClient(mockHttp);
|
||||||
|
await api.getConfig();
|
||||||
|
|
||||||
|
expect(mockHttp.get).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getOrganizations calls http method with expected data', async (done) => {
|
||||||
|
const createPromise = () => Promise.resolve();
|
||||||
|
const mockHttp = ({ get: jest.fn(createPromise) });
|
||||||
|
const api = new APIClient(mockHttp);
|
||||||
|
|
||||||
|
const defaultParams = {};
|
||||||
|
const testParams = { foo: 'bar' };
|
||||||
|
|
||||||
|
await api.getOrganizations(testParams);
|
||||||
|
await api.getOrganizations();
|
||||||
|
|
||||||
|
expect(mockHttp.get).toHaveBeenCalledTimes(2);
|
||||||
|
expect(mockHttp.get.mock.calls[0][1]).toEqual({ params: testParams });
|
||||||
|
expect(mockHttp.get.mock.calls[1][1]).toEqual({ params: defaultParams });
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('createOrganization calls http method with expected data', async (done) => {
|
||||||
|
const createPromise = () => Promise.resolve();
|
||||||
|
const mockHttp = ({ post: jest.fn(createPromise) });
|
||||||
|
|
||||||
|
const api = new APIClient(mockHttp);
|
||||||
|
const data = { name: 'test '};
|
||||||
|
await api.createOrganization(data);
|
||||||
|
|
||||||
|
expect(mockHttp.post).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockHttp.post.mock.calls[0][1]).toEqual(data);
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getOrganizationDetails calls http method with expected data', async (done) => {
|
||||||
|
const createPromise = () => Promise.resolve();
|
||||||
|
const mockHttp = ({ get: jest.fn(createPromise) });
|
||||||
|
|
||||||
|
const api = new APIClient(mockHttp);
|
||||||
|
await api.getOrganizationDetails(99);
|
||||||
|
|
||||||
|
expect(mockHttp.get).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockHttp.get.mock.calls[0][0]).toContain('99');
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -29,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}
|
||||||
|
|||||||
@@ -122,7 +122,6 @@ describe('<Pagination />', () => {
|
|||||||
test('submit a new page by typing in input works', () => {
|
test('submit a new page by typing in input works', () => {
|
||||||
const textInputSelector = '.pf-l-split__item.pf-m-main .pf-c-form-control';
|
const textInputSelector = '.pf-l-split__item.pf-m-main .pf-c-form-control';
|
||||||
const submitFormSelector = '.pf-l-split__item.pf-m-main form';
|
const submitFormSelector = '.pf-l-split__item.pf-m-main form';
|
||||||
|
|
||||||
const onSetPage = jest.fn();
|
const onSetPage = jest.fn();
|
||||||
|
|
||||||
pagination = mount(
|
pagination = mount(
|
||||||
@@ -137,6 +136,7 @@ describe('<Pagination />', () => {
|
|||||||
/>
|
/>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
const textInput = pagination.find(textInputSelector);
|
const textInput = pagination.find(textInputSelector);
|
||||||
expect(textInput.length).toBe(1);
|
expect(textInput.length).toBe(1);
|
||||||
textInput.simulate('change');
|
textInput.simulate('change');
|
||||||
@@ -145,7 +145,7 @@ describe('<Pagination />', () => {
|
|||||||
const submitForm = pagination.find(submitFormSelector);
|
const submitForm = pagination.find(submitFormSelector);
|
||||||
expect(submitForm.length).toBe(1);
|
expect(submitForm.length).toBe(1);
|
||||||
submitForm.simulate('submit');
|
submitForm.simulate('submit');
|
||||||
pagination.setState({ value: 'invalid' });
|
pagination.find('Pagination').instance().setState({ value: 'invalid' });
|
||||||
submitForm.simulate('submit');
|
submitForm.simulate('submit');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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,12 +42,26 @@ describe('<TowerLogo />', () => {
|
|||||||
expect(towerLogoElem.props().history.length).toBe(2);
|
expect(towerLogoElem.props().history.length).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('linkTo prop is optional', () => {
|
||||||
|
logoWrapper = mount(
|
||||||
|
<MemoryRouter>
|
||||||
|
<I18nProvider>
|
||||||
|
<TowerLogo />
|
||||||
|
</I18nProvider>
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
findChildren();
|
||||||
|
expect(towerLogoElem.props().history.length).toBe(1);
|
||||||
|
logoWrapper.simulate('click');
|
||||||
|
expect(towerLogoElem.props().history.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
test('handles mouse over and out state.hover changes', () => {
|
test('handles mouse over and out state.hover changes', () => {
|
||||||
const onLogoClick = jest.fn();
|
const onLogoClick = jest.fn();
|
||||||
logoWrapper = mount(
|
logoWrapper = mount(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<TowerLogo onClick={onLogoClick} />
|
<TowerLogo />
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { mount } from 'enzyme';
|
import { mount } from 'enzyme';
|
||||||
import { main } from '../src/index';
|
import { main, getLanguage } from '../src/index';
|
||||||
|
|
||||||
const render = template => mount(template);
|
const render = template => mount(template);
|
||||||
const data = { custom_logo: 'foo', custom_login_info: '' }
|
const data = { custom_logo: 'foo', custom_login_info: '' }
|
||||||
|
|
||||||
describe('index.jsx', () => {
|
describe('index.jsx', () => {
|
||||||
test('initialization', async (done) => {
|
test('login loads when unauthenticated', async (done) => {
|
||||||
const isAuthenticated = () => false;
|
const isAuthenticated = () => false;
|
||||||
const getRoot = jest.fn(() => Promise.resolve({ data }));
|
const getRoot = jest.fn(() => Promise.resolve({ data }));
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ describe('index.jsx', () => {
|
|||||||
const wrapper = await main(render, api);
|
const wrapper = await main(render, api);
|
||||||
|
|
||||||
expect(api.getRoot).toHaveBeenCalled();
|
expect(api.getRoot).toHaveBeenCalled();
|
||||||
expect(wrapper.find('Dashboard')).toHaveLength(0);
|
expect(wrapper.find('App')).toHaveLength(0);
|
||||||
expect(wrapper.find('Login')).toHaveLength(1);
|
expect(wrapper.find('Login')).toHaveLength(1);
|
||||||
|
|
||||||
const { src } = wrapper.find('Login Brand img').props();
|
const { src } = wrapper.find('Login Brand img').props();
|
||||||
@@ -22,7 +22,7 @@ describe('index.jsx', () => {
|
|||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('dashboard is loaded when authenticated', async (done) => {
|
test('app loads when authenticated', async (done) => {
|
||||||
const isAuthenticated = () => true;
|
const isAuthenticated = () => true;
|
||||||
const getRoot = jest.fn(() => Promise.resolve({ data }));
|
const getRoot = jest.fn(() => Promise.resolve({ data }));
|
||||||
|
|
||||||
@@ -30,9 +30,22 @@ describe('index.jsx', () => {
|
|||||||
const wrapper = await main(render, api);
|
const wrapper = await main(render, api);
|
||||||
|
|
||||||
expect(api.getRoot).toHaveBeenCalled();
|
expect(api.getRoot).toHaveBeenCalled();
|
||||||
expect(wrapper.find('Dashboard')).toHaveLength(1);
|
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);
|
expect(wrapper.find('Login')).toHaveLength(0);
|
||||||
|
|
||||||
done();
|
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');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
12
src/App.jsx
12
src/App.jsx
@@ -28,14 +28,12 @@ class App extends Component {
|
|||||||
isAboutModalOpen: false,
|
isAboutModalOpen: false,
|
||||||
isNavOpen,
|
isNavOpen,
|
||||||
version: null,
|
version: null,
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.fetchConfig = this.fetchConfig.bind(this);
|
this.fetchConfig = this.fetchConfig.bind(this);
|
||||||
this.onLogout = this.onLogout.bind(this);
|
this.onLogout = this.onLogout.bind(this);
|
||||||
this.onAboutModalClose = this.onAboutModalClose.bind(this);
|
this.onAboutModalClose = this.onAboutModalClose.bind(this);
|
||||||
this.onAboutModalOpen = this.onAboutModalOpen.bind(this);
|
this.onAboutModalOpen = this.onAboutModalOpen.bind(this);
|
||||||
this.onLogoClick = this.onLogoClick.bind(this);
|
|
||||||
this.onNavToggle = this.onNavToggle.bind(this);
|
this.onNavToggle = this.onNavToggle.bind(this);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -73,10 +71,6 @@ class App extends Component {
|
|||||||
this.setState(({ isNavOpen }) => ({ isNavOpen: !isNavOpen }));
|
this.setState(({ isNavOpen }) => ({ isNavOpen: !isNavOpen }));
|
||||||
}
|
}
|
||||||
|
|
||||||
onLogoClick () {
|
|
||||||
this.setState({ activeGroup: 'views_group' });
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const {
|
const {
|
||||||
ansible_version,
|
ansible_version,
|
||||||
@@ -106,11 +100,7 @@ class App extends Component {
|
|||||||
<PageHeader
|
<PageHeader
|
||||||
showNavToggle
|
showNavToggle
|
||||||
onNavToggle={this.onNavToggle}
|
onNavToggle={this.onNavToggle}
|
||||||
logo={
|
logo={<TowerLogo linkTo="/"/>}
|
||||||
<TowerLogo
|
|
||||||
onClick={this.onLogoClick}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
toolbar={
|
toolbar={
|
||||||
<PageHeaderToolbar
|
<PageHeaderToolbar
|
||||||
isAboutDisabled={!version}
|
isAboutDisabled={!version}
|
||||||
|
|||||||
@@ -47,9 +47,10 @@ class DataListToolbar extends React.Component {
|
|||||||
this.handleSearchInputChange = this.handleSearchInputChange.bind(this);
|
this.handleSearchInputChange = this.handleSearchInputChange.bind(this);
|
||||||
this.onSortDropdownToggle = this.onSortDropdownToggle.bind(this);
|
this.onSortDropdownToggle = this.onSortDropdownToggle.bind(this);
|
||||||
this.onSortDropdownSelect = this.onSortDropdownSelect.bind(this);
|
this.onSortDropdownSelect = this.onSortDropdownSelect.bind(this);
|
||||||
this.onSearch = this.onSearch.bind(this);
|
|
||||||
this.onSearchDropdownToggle = this.onSearchDropdownToggle.bind(this);
|
this.onSearchDropdownToggle = this.onSearchDropdownToggle.bind(this);
|
||||||
this.onSearchDropdownSelect = this.onSearchDropdownSelect.bind(this);
|
this.onSearchDropdownSelect = this.onSearchDropdownSelect.bind(this);
|
||||||
|
this.onSearch = this.onSearch.bind(this);
|
||||||
|
this.onSort = this.onSort.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSearchInputChange (searchValue) {
|
handleSearchInputChange (searchValue) {
|
||||||
@@ -62,12 +63,12 @@ class DataListToolbar extends React.Component {
|
|||||||
|
|
||||||
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) {
|
||||||
@@ -76,11 +77,10 @@ class DataListToolbar extends React.Component {
|
|||||||
|
|
||||||
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 () {
|
onSearch () {
|
||||||
@@ -90,13 +90,19 @@ class DataListToolbar extends React.Component {
|
|||||||
onSearch(searchValue);
|
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,
|
||||||
onSelectAll,
|
onSelectAll,
|
||||||
onSort,
|
|
||||||
sortedColumnKey,
|
sortedColumnKey,
|
||||||
sortOrder,
|
sortOrder,
|
||||||
addUrl,
|
addUrl,
|
||||||
@@ -110,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>
|
||||||
));
|
));
|
||||||
|
|
||||||
@@ -140,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
|
||||||
@@ -174,7 +175,7 @@ class DataListToolbar extends React.Component {
|
|||||||
<DropdownToggle
|
<DropdownToggle
|
||||||
onToggle={this.onSearchDropdownToggle}
|
onToggle={this.onSearchDropdownToggle}
|
||||||
>
|
>
|
||||||
{ searchColumnName }
|
{searchColumnName}
|
||||||
</DropdownToggle>
|
</DropdownToggle>
|
||||||
)}
|
)}
|
||||||
dropdownItems={searchDropdownItems}
|
dropdownItems={searchDropdownItems}
|
||||||
@@ -195,7 +196,9 @@ class DataListToolbar extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
</ToolbarItem>
|
</ToolbarItem>
|
||||||
</ToolbarGroup>
|
</ToolbarGroup>
|
||||||
<ToolbarGroup className="sortDropdownGroup">
|
<ToolbarGroup
|
||||||
|
className="sortDropdownGroup"
|
||||||
|
>
|
||||||
<ToolbarItem>
|
<ToolbarItem>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
onToggle={this.onSortDropdownToggle}
|
onToggle={this.onSortDropdownToggle}
|
||||||
@@ -206,7 +209,7 @@ class DataListToolbar extends React.Component {
|
|||||||
<DropdownToggle
|
<DropdownToggle
|
||||||
onToggle={this.onSortDropdownToggle}
|
onToggle={this.onSortDropdownToggle}
|
||||||
>
|
>
|
||||||
{ sortedColumnName }
|
{sortedColumnName}
|
||||||
</DropdownToggle>
|
</DropdownToggle>
|
||||||
)}
|
)}
|
||||||
dropdownItems={sortDropdownItems}
|
dropdownItems={sortDropdownItems}
|
||||||
@@ -214,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>
|
||||||
@@ -239,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>
|
||||||
|
|||||||
@@ -18,8 +18,7 @@ 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.onPageChange = this.onPageChange.bind(this);
|
||||||
@@ -70,18 +69,14 @@ class Pagination extends Component {
|
|||||||
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 () {
|
||||||
@@ -143,14 +138,20 @@ class Pagination extends Component {
|
|||||||
direction={up}
|
direction={up}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
toggle={(
|
toggle={(
|
||||||
<DropdownToggle className="togglePageSize" onToggle={this.onTogglePageSize}>
|
<DropdownToggle
|
||||||
{ page_size }
|
className="togglePageSize"
|
||||||
|
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>
|
||||||
@@ -159,7 +160,7 @@ class Pagination extends Component {
|
|||||||
<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">
|
||||||
@@ -200,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>
|
||||||
|
|||||||
@@ -18,13 +18,11 @@ class TowerLogo extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 () {
|
||||||
@@ -35,11 +33,10 @@ class TowerLogo extends Component {
|
|||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,21 +60,25 @@ const http = axios.create({ xsrfCookieName: 'csrftoken', xsrfHeaderName: 'X-CSRF
|
|||||||
// see: https://developer.mozilla.org/en-US/docs/Web/API/Navigator
|
// see: https://developer.mozilla.org/en-US/docs/Web/API/Navigator
|
||||||
//
|
//
|
||||||
|
|
||||||
const language = (navigator.languages && navigator.languages[0])
|
export function getLanguage (nav) {
|
||||||
|| navigator.language
|
const language = (nav.languages && nav.languages[0]) || nav.language || nav.userLanguage;
|
||||||
|| navigator.userLanguage;
|
const languageWithoutRegionCode = language.toLowerCase().split(/[_-]+/)[0];
|
||||||
const languageWithoutRegionCode = language.toLowerCase().split(/[_-]+/)[0];
|
|
||||||
const catalogs = { en, ja };
|
return languageWithoutRegionCode;
|
||||||
|
};
|
||||||
|
|
||||||
//
|
//
|
||||||
// Function Main
|
// Function Main
|
||||||
//
|
//
|
||||||
|
|
||||||
export async function main (render, api) {
|
export async function main (render, api) {
|
||||||
|
const catalogs = { en, ja };
|
||||||
|
const language = getLanguage(navigator);
|
||||||
|
|
||||||
const el = document.getElementById('app');
|
const el = document.getElementById('app');
|
||||||
// fetch additional config from server
|
|
||||||
const { data: { custom_logo, custom_login_info } } = await api.getRoot();
|
const { data: { custom_logo, custom_login_info } } = await api.getRoot();
|
||||||
|
|
||||||
|
const defaultRedirect = () => (<Redirect to="/home" />);
|
||||||
const loginRoutes = (
|
const loginRoutes = (
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route
|
<Route
|
||||||
@@ -94,7 +98,7 @@ export async function main (render, api) {
|
|||||||
return render(
|
return render(
|
||||||
<HashRouter>
|
<HashRouter>
|
||||||
<I18nProvider
|
<I18nProvider
|
||||||
language={languageWithoutRegionCode}
|
language={language}
|
||||||
catalogs={catalogs}
|
catalogs={catalogs}
|
||||||
>
|
>
|
||||||
<I18n>
|
<I18n>
|
||||||
@@ -102,8 +106,8 @@ export async function main (render, api) {
|
|||||||
<Background>
|
<Background>
|
||||||
{!api.isAuthenticated() ? loginRoutes : (
|
{!api.isAuthenticated() ? loginRoutes : (
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/login" render={() => (<Redirect to="/home" />)} />
|
<Route path="/login" render={defaultRedirect} />
|
||||||
<Route exact path="/" render={() => (<Redirect to="/home" />)} />
|
<Route exact path="/" render={defaultRedirect} />
|
||||||
<Route
|
<Route
|
||||||
render={() => (
|
render={() => (
|
||||||
<App
|
<App
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class AWXLogin extends Component {
|
|||||||
try {
|
try {
|
||||||
await api.login(username, password);
|
await api.login(username, password);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.response.status === 401) {
|
if (error.response && error.response.status === 401) {
|
||||||
this.setState({ isInputValid: false });
|
this.setState({ isInputValid: false });
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { Fragment } from 'react';
|
import React, { Fragment } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
import { Trans } from '@lingui/macro';
|
import { Trans } from '@lingui/macro';
|
||||||
import {
|
import {
|
||||||
@@ -17,8 +18,8 @@ import {
|
|||||||
CardBody,
|
CardBody,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
|
|
||||||
import AnsibleSelect from '../../../components/AnsibleSelect';
|
import { ConfigContext } from '../../../context';
|
||||||
|
import AnsibleSelect from '../../../components/AnsibleSelect'
|
||||||
const { light } = PageSectionVariants;
|
const { light } = PageSectionVariants;
|
||||||
|
|
||||||
class OrganizationAdd extends React.Component {
|
class OrganizationAdd extends React.Component {
|
||||||
@@ -37,8 +38,6 @@ class OrganizationAdd extends React.Component {
|
|||||||
description: '',
|
description: '',
|
||||||
instanceGroups: '',
|
instanceGroups: '',
|
||||||
custom_virtualenv: '',
|
custom_virtualenv: '',
|
||||||
custom_virtualenvs: [],
|
|
||||||
hideAnsibleSelect: true,
|
|
||||||
error:'',
|
error:'',
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -59,9 +58,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.createOrganization(data);
|
await api.createOrganization(data);
|
||||||
|
|
||||||
this.resetForm();
|
this.resetForm();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,22 +68,10 @@ class OrganizationAdd extends React.Component {
|
|||||||
this.props.history.push('/organizations');
|
this.props.history.push('/organizations');
|
||||||
}
|
}
|
||||||
|
|
||||||
async componentDidMount() {
|
|
||||||
try {
|
|
||||||
const { data } = await api.getConfig();
|
|
||||||
this.setState({ custom_virtualenvs: [...data.custom_virtualenvs] });
|
|
||||||
if (this.state.custom_virtualenvs.length > 1) {
|
|
||||||
// Show dropdown if we have more than one ansible environment
|
|
||||||
this.setState({ hideAnsibleSelect: !this.state.hideAnsibleSelect });
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
this.setState({ error })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { name } = this.state;
|
const { name } = this.state;
|
||||||
const enabled = name.length > 0; // TODO: add better form validation
|
const enabled = name.length > 0; // TODO: add better form validation
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<PageSection variant={light} className="pf-m-condensed">
|
<PageSection variant={light} className="pf-m-condensed">
|
||||||
@@ -128,13 +115,16 @@ class OrganizationAdd extends React.Component {
|
|||||||
onChange={this.handleChange}
|
onChange={this.handleChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<AnsibleSelect
|
<ConfigContext.Consumer>
|
||||||
labelName="Ansible Environment"
|
{({ custom_virtualenvs }) =>
|
||||||
selected={this.state.custom_virtualenv}
|
<AnsibleSelect
|
||||||
selectChange={this.onSelectChange}
|
labelName="Ansible Environment"
|
||||||
data={this.state.custom_virtualenvs}
|
selected={this.state.custom_virtualenv}
|
||||||
hidden={this.state.hideAnsibleSelect}
|
selectChange={this.onSelectChange}
|
||||||
/>
|
data={custom_virtualenvs}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</ConfigContext.Consumer>
|
||||||
</Gallery>
|
</Gallery>
|
||||||
<ActionGroup className="at-align-right">
|
<ActionGroup className="at-align-right">
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
@@ -155,4 +145,8 @@ class OrganizationAdd extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
OrganizationAdd.contextTypes = {
|
||||||
|
custom_virtualenvs: PropTypes.array,
|
||||||
|
};
|
||||||
|
|
||||||
export default withRouter(OrganizationAdd);
|
export default withRouter(OrganizationAdd);
|
||||||
|
|||||||
Reference in New Issue
Block a user