mirror of
https://github.com/ansible/awx.git
synced 2026-01-14 19:30:39 -03:30
Merge pull request #84 from jakemcdermott/tests-fixup
add more unit and functional test coverage
This commit is contained in:
commit
976c490dc3
@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { HashRouter } from 'react-router-dom';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { I18nProvider } from '@lingui/react';
|
||||
|
||||
import { mount, shallow } from 'enzyme';
|
||||
@ -12,7 +12,7 @@ const DEFAULT_ACTIVE_GROUP = 'views_group';
|
||||
describe('<App />', () => {
|
||||
test('expected content is rendered', () => {
|
||||
const appWrapper = mount(
|
||||
<HashRouter>
|
||||
<MemoryRouter>
|
||||
<I18nProvider>
|
||||
<App
|
||||
routeGroups={[
|
||||
@ -37,7 +37,7 @@ describe('<App />', () => {
|
||||
)}
|
||||
/>
|
||||
</I18nProvider>
|
||||
</HashRouter>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
// page components
|
||||
@ -56,6 +56,48 @@ describe('<App />', () => {
|
||||
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', () => {
|
||||
const appWrapper = shallow(<App />);
|
||||
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) => {
|
||||
const logout = jest.fn(() => Promise.resolve());
|
||||
const api = { logout };
|
||||
@ -89,17 +120,4 @@ describe('<App />', () => {
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
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');
|
||||
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>
|
||||
<DataListToolbar
|
||||
isAllSelected={false}
|
||||
showExpandCollapse={true}
|
||||
sortedColumnKey="name"
|
||||
sortOrder="ascending"
|
||||
columns={columns}
|
||||
|
||||
@ -122,7 +122,6 @@ describe('<Pagination />', () => {
|
||||
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(
|
||||
@ -137,6 +136,7 @@ describe('<Pagination />', () => {
|
||||
/>
|
||||
</I18nProvider>
|
||||
);
|
||||
|
||||
const textInput = pagination.find(textInputSelector);
|
||||
expect(textInput.length).toBe(1);
|
||||
textInput.simulate('change');
|
||||
@ -145,7 +145,7 @@ describe('<Pagination />', () => {
|
||||
const submitForm = pagination.find(submitFormSelector);
|
||||
expect(submitForm.length).toBe(1);
|
||||
submitForm.simulate('submit');
|
||||
pagination.setState({ value: 'invalid' });
|
||||
pagination.find('Pagination').instance().setState({ value: 'invalid' });
|
||||
submitForm.simulate('submit');
|
||||
});
|
||||
|
||||
|
||||
@ -29,11 +29,10 @@ describe('<TowerLogo />', () => {
|
||||
});
|
||||
|
||||
test('adds navigation to route history on click', () => {
|
||||
const onLogoClick = jest.fn();
|
||||
logoWrapper = mount(
|
||||
<MemoryRouter>
|
||||
<I18nProvider>
|
||||
<TowerLogo onClick={onLogoClick} />
|
||||
<TowerLogo linkTo="/" />
|
||||
</I18nProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
@ -43,12 +42,26 @@ describe('<TowerLogo />', () => {
|
||||
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', () => {
|
||||
const onLogoClick = jest.fn();
|
||||
logoWrapper = mount(
|
||||
<MemoryRouter>
|
||||
<I18nProvider>
|
||||
<TowerLogo onClick={onLogoClick} />
|
||||
<TowerLogo />
|
||||
</I18nProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { mount } from 'enzyme';
|
||||
import { main } from '../src/index';
|
||||
import { main, getLanguage } from '../src/index';
|
||||
|
||||
const render = template => mount(template);
|
||||
const data = { custom_logo: 'foo', custom_login_info: '' }
|
||||
|
||||
describe('index.jsx', () => {
|
||||
test('initialization', async (done) => {
|
||||
test('login loads when unauthenticated', async (done) => {
|
||||
const isAuthenticated = () => false;
|
||||
const getRoot = jest.fn(() => Promise.resolve({ data }));
|
||||
|
||||
@ -13,7 +13,7 @@ describe('index.jsx', () => {
|
||||
const wrapper = await main(render, api);
|
||||
|
||||
expect(api.getRoot).toHaveBeenCalled();
|
||||
expect(wrapper.find('Dashboard')).toHaveLength(0);
|
||||
expect(wrapper.find('App')).toHaveLength(0);
|
||||
expect(wrapper.find('Login')).toHaveLength(1);
|
||||
|
||||
const { src } = wrapper.find('Login Brand img').props();
|
||||
@ -22,7 +22,7 @@ describe('index.jsx', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
test('dashboard is loaded when authenticated', async (done) => {
|
||||
test('app loads when authenticated', async (done) => {
|
||||
const isAuthenticated = () => true;
|
||||
const getRoot = jest.fn(() => Promise.resolve({ data }));
|
||||
|
||||
@ -30,9 +30,22 @@ describe('index.jsx', () => {
|
||||
const wrapper = await main(render, api);
|
||||
|
||||
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);
|
||||
|
||||
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,
|
||||
isNavOpen,
|
||||
version: null,
|
||||
|
||||
};
|
||||
|
||||
this.fetchConfig = this.fetchConfig.bind(this);
|
||||
this.onLogout = this.onLogout.bind(this);
|
||||
this.onAboutModalClose = this.onAboutModalClose.bind(this);
|
||||
this.onAboutModalOpen = this.onAboutModalOpen.bind(this);
|
||||
this.onLogoClick = this.onLogoClick.bind(this);
|
||||
this.onNavToggle = this.onNavToggle.bind(this);
|
||||
};
|
||||
|
||||
@ -73,10 +71,6 @@ class App extends Component {
|
||||
this.setState(({ isNavOpen }) => ({ isNavOpen: !isNavOpen }));
|
||||
}
|
||||
|
||||
onLogoClick () {
|
||||
this.setState({ activeGroup: 'views_group' });
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
ansible_version,
|
||||
@ -106,11 +100,7 @@ class App extends Component {
|
||||
<PageHeader
|
||||
showNavToggle
|
||||
onNavToggle={this.onNavToggle}
|
||||
logo={
|
||||
<TowerLogo
|
||||
onClick={this.onLogoClick}
|
||||
/>
|
||||
}
|
||||
logo={<TowerLogo linkTo="/"/>}
|
||||
toolbar={
|
||||
<PageHeaderToolbar
|
||||
isAboutDisabled={!version}
|
||||
|
||||
@ -47,9 +47,10 @@ class DataListToolbar extends React.Component {
|
||||
this.handleSearchInputChange = this.handleSearchInputChange.bind(this);
|
||||
this.onSortDropdownToggle = this.onSortDropdownToggle.bind(this);
|
||||
this.onSortDropdownSelect = this.onSortDropdownSelect.bind(this);
|
||||
this.onSearch = this.onSearch.bind(this);
|
||||
this.onSearchDropdownToggle = this.onSearchDropdownToggle.bind(this);
|
||||
this.onSearchDropdownSelect = this.onSearchDropdownSelect.bind(this);
|
||||
this.onSearch = this.onSearch.bind(this);
|
||||
this.onSort = this.onSort.bind(this);
|
||||
}
|
||||
|
||||
handleSearchInputChange (searchValue) {
|
||||
@ -62,12 +63,12 @@ class DataListToolbar extends React.Component {
|
||||
|
||||
onSortDropdownSelect ({ target }) {
|
||||
const { columns, onSort, sortOrder } = this.props;
|
||||
const { innerText } = target;
|
||||
|
||||
const [{ key }] = columns.filter(({ name }) => name === target.innerText);
|
||||
const [{ key: searchKey }] = columns.filter(({ name }) => name === innerText);
|
||||
|
||||
this.setState({ isSortDropdownOpen: false });
|
||||
|
||||
onSort(key, sortOrder);
|
||||
onSort(searchKey, sortOrder);
|
||||
}
|
||||
|
||||
onSearchDropdownToggle (isSearchDropdownOpen) {
|
||||
@ -76,11 +77,10 @@ class DataListToolbar extends React.Component {
|
||||
|
||||
onSearchDropdownSelect ({ target }) {
|
||||
const { columns } = this.props;
|
||||
const { innerText } = target;
|
||||
|
||||
const targetName = target.innerText;
|
||||
const [{ key }] = columns.filter(({ name }) => name === targetName);
|
||||
|
||||
this.setState({ isSearchDropdownOpen: false, searchKey: key });
|
||||
const [{ key: searchKey }] = columns.filter(({ name }) => name === innerText);
|
||||
this.setState({ isSearchDropdownOpen: false, searchKey });
|
||||
}
|
||||
|
||||
onSearch () {
|
||||
@ -90,13 +90,19 @@ class DataListToolbar extends React.Component {
|
||||
onSearch(searchValue);
|
||||
}
|
||||
|
||||
onSort () {
|
||||
const { onSort, sortedColumnKey, sortOrder } = this.props;
|
||||
const newSortOrder = sortOrder === 'ascending' ? 'descending' : 'ascending';
|
||||
|
||||
onSort(sortedColumnKey, newSortOrder);
|
||||
}
|
||||
|
||||
render () {
|
||||
const { up } = DropdownPosition;
|
||||
const {
|
||||
columns,
|
||||
isAllSelected,
|
||||
onSelectAll,
|
||||
onSort,
|
||||
sortedColumnKey,
|
||||
sortOrder,
|
||||
addUrl,
|
||||
@ -110,29 +116,15 @@ class DataListToolbar extends React.Component {
|
||||
searchValue,
|
||||
} = this.state;
|
||||
|
||||
const [searchColumn] = columns
|
||||
.filter(({ key }) => key === searchKey);
|
||||
const searchColumnName = searchColumn.name;
|
||||
|
||||
const [sortedColumn] = columns
|
||||
const [{ name: searchColumnName }] = columns.filter(({ key }) => key === searchKey);
|
||||
const [{ name: sortedColumnName, isNumeric }] = columns
|
||||
.filter(({ key }) => key === sortedColumnKey);
|
||||
const sortedColumnName = sortedColumn.name;
|
||||
const isSortNumeric = sortedColumn.isNumeric;
|
||||
const displayedSortIcon = () => {
|
||||
let icon;
|
||||
if (sortOrder === 'ascending') {
|
||||
icon = isSortNumeric ? (<SortNumericUpIcon />) : (<SortAlphaUpIcon />);
|
||||
} else {
|
||||
icon = isSortNumeric ? (<SortNumericDownIcon />) : (<SortAlphaDownIcon />);
|
||||
}
|
||||
return icon;
|
||||
};
|
||||
|
||||
const searchDropdownItems = columns
|
||||
.filter(({ key }) => key !== searchKey)
|
||||
.map(({ key, name }) => (
|
||||
<DropdownItem key={key} component="button">
|
||||
{ name }
|
||||
{name}
|
||||
</DropdownItem>
|
||||
));
|
||||
|
||||
@ -140,17 +132,26 @@ class DataListToolbar extends React.Component {
|
||||
.filter(({ key, isSortable }) => isSortable && key !== sortedColumnKey)
|
||||
.map(({ key, name }) => (
|
||||
<DropdownItem key={key} component="button">
|
||||
{ name }
|
||||
{name}
|
||||
</DropdownItem>
|
||||
));
|
||||
|
||||
let SortIcon;
|
||||
if (isNumeric) {
|
||||
SortIcon = sortOrder === 'ascending' ? SortNumericUpIcon : SortNumericDownIcon;
|
||||
} else {
|
||||
SortIcon = sortOrder === 'ascending' ? SortAlphaUpIcon : SortAlphaDownIcon;
|
||||
}
|
||||
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<div className="awx-toolbar">
|
||||
<Level>
|
||||
<LevelItem>
|
||||
<Toolbar style={{ marginLeft: '20px' }}>
|
||||
<Toolbar
|
||||
style={{ marginLeft: '20px' }}
|
||||
>
|
||||
<ToolbarGroup>
|
||||
<ToolbarItem>
|
||||
<Checkbox
|
||||
@ -174,7 +175,7 @@ class DataListToolbar extends React.Component {
|
||||
<DropdownToggle
|
||||
onToggle={this.onSearchDropdownToggle}
|
||||
>
|
||||
{ searchColumnName }
|
||||
{searchColumnName}
|
||||
</DropdownToggle>
|
||||
)}
|
||||
dropdownItems={searchDropdownItems}
|
||||
@ -195,7 +196,9 @@ class DataListToolbar extends React.Component {
|
||||
</div>
|
||||
</ToolbarItem>
|
||||
</ToolbarGroup>
|
||||
<ToolbarGroup className="sortDropdownGroup">
|
||||
<ToolbarGroup
|
||||
className="sortDropdownGroup"
|
||||
>
|
||||
<ToolbarItem>
|
||||
<Dropdown
|
||||
onToggle={this.onSortDropdownToggle}
|
||||
@ -206,7 +209,7 @@ class DataListToolbar extends React.Component {
|
||||
<DropdownToggle
|
||||
onToggle={this.onSortDropdownToggle}
|
||||
>
|
||||
{ sortedColumnName }
|
||||
{sortedColumnName}
|
||||
</DropdownToggle>
|
||||
)}
|
||||
dropdownItems={sortDropdownItems}
|
||||
@ -214,23 +217,29 @@ class DataListToolbar extends React.Component {
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<Button
|
||||
onClick={() => onSort(sortedColumnKey, sortOrder === 'ascending' ? 'descending' : 'ascending')}
|
||||
onClick={this.onSort}
|
||||
variant="plain"
|
||||
aria-label={i18n._(t`Sort`)}
|
||||
>
|
||||
{displayedSortIcon()}
|
||||
<SortIcon/>
|
||||
</Button>
|
||||
</ToolbarItem>
|
||||
</ToolbarGroup>
|
||||
{ showExpandCollapse && (
|
||||
{showExpandCollapse && (
|
||||
<ToolbarGroup>
|
||||
<ToolbarItem>
|
||||
<Button variant="plain" aria-label={i18n._(t`Expand`)}>
|
||||
<Button
|
||||
variant="plain"
|
||||
aria-label={i18n._(t`Expand`)}
|
||||
>
|
||||
<BarsIcon />
|
||||
</Button>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<Button variant="plain" aria-label={i18n._(t`Collapse`)}>
|
||||
<Button
|
||||
variant="plain"
|
||||
aria-label={i18n._(t`Collapse`)}
|
||||
>
|
||||
<EqualsIcon />
|
||||
</Button>
|
||||
</ToolbarItem>
|
||||
@ -239,14 +248,23 @@ class DataListToolbar extends React.Component {
|
||||
</Toolbar>
|
||||
</LevelItem>
|
||||
<LevelItem>
|
||||
<Tooltip message={i18n._(t`Delete`)} position="top">
|
||||
<Button variant="plain" aria-label={i18n._(t`Delete`)}>
|
||||
<Tooltip
|
||||
message={i18n._(t`Delete`)}
|
||||
position="top"
|
||||
>
|
||||
<Button
|
||||
variant="plain"
|
||||
aria-label={i18n._(t`Delete`)}
|
||||
>
|
||||
<TrashAltIcon />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
{addUrl && (
|
||||
<Link to={addUrl}>
|
||||
<Button variant="primary" aria-label={i18n._(t`Add`)}>
|
||||
<Button
|
||||
variant="primary"
|
||||
aria-label={i18n._(t`Add`)}
|
||||
>
|
||||
<PlusIcon />
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
@ -18,8 +18,7 @@ class Pagination extends Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
const { page } = this.props;
|
||||
|
||||
const { page } = props;
|
||||
this.state = { value: page, isOpen: false };
|
||||
|
||||
this.onPageChange = this.onPageChange.bind(this);
|
||||
@ -70,18 +69,14 @@ class Pagination extends Component {
|
||||
const { onSetPage, page, page_size } = this.props;
|
||||
const previousPage = page - 1;
|
||||
|
||||
if (previousPage >= 1) {
|
||||
onSetPage(previousPage, page_size);
|
||||
}
|
||||
onSetPage(previousPage, page_size);
|
||||
}
|
||||
|
||||
onNext () {
|
||||
const { onSetPage, page, pageCount, page_size } = this.props;
|
||||
const nextPage = page + 1;
|
||||
|
||||
if (nextPage <= pageCount) {
|
||||
onSetPage(nextPage, page_size);
|
||||
}
|
||||
onSetPage(nextPage, page_size);
|
||||
}
|
||||
|
||||
onLast () {
|
||||
@ -143,14 +138,20 @@ class Pagination extends Component {
|
||||
direction={up}
|
||||
isOpen={isOpen}
|
||||
toggle={(
|
||||
<DropdownToggle className="togglePageSize" onToggle={this.onTogglePageSize}>
|
||||
{ page_size }
|
||||
<DropdownToggle
|
||||
className="togglePageSize"
|
||||
onToggle={this.onTogglePageSize}
|
||||
>
|
||||
{page_size}
|
||||
</DropdownToggle>
|
||||
)}
|
||||
>
|
||||
{opts.map(option => (
|
||||
<DropdownItem key={option} component="button">
|
||||
{ option }
|
||||
<DropdownItem
|
||||
key={option}
|
||||
component="button"
|
||||
>
|
||||
{option}
|
||||
</DropdownItem>
|
||||
))}
|
||||
</Dropdown>
|
||||
@ -159,7 +160,7 @@ class Pagination extends Component {
|
||||
<LevelItem>
|
||||
<Split gutter="md" className="pf-u-display-flex pf-u-align-items-center">
|
||||
<SplitItem>
|
||||
<Trans>{ itemMin } - { itemMax } of { count }</Trans>
|
||||
<Trans>{itemMin} - {itemMax} of {count}</Trans>
|
||||
</SplitItem>
|
||||
<SplitItem>
|
||||
<div className="pf-c-input-group">
|
||||
@ -200,7 +201,7 @@ class Pagination extends Component {
|
||||
value={value}
|
||||
type="text"
|
||||
onChange={this.onPageChange}
|
||||
/> of { pageCount }
|
||||
/> of {pageCount}
|
||||
</Trans>
|
||||
</form>
|
||||
</SplitItem>
|
||||
|
||||
@ -18,13 +18,11 @@ class TowerLogo extends Component {
|
||||
}
|
||||
|
||||
onClick () {
|
||||
const { history, onClick: handleClick } = this.props;
|
||||
const { history, linkTo } = this.props;
|
||||
|
||||
if (!handleClick) return;
|
||||
if (!linkTo) return;
|
||||
|
||||
history.push('/');
|
||||
|
||||
handleClick();
|
||||
history.push(linkTo);
|
||||
}
|
||||
|
||||
onHover () {
|
||||
@ -35,11 +33,10 @@ class TowerLogo extends Component {
|
||||
|
||||
render () {
|
||||
const { hover } = this.state;
|
||||
const { onClick: handleClick } = this.props;
|
||||
|
||||
let src = TowerLogoHeader;
|
||||
|
||||
if (hover && handleClick) {
|
||||
if (hover) {
|
||||
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
|
||||
//
|
||||
|
||||
const language = (navigator.languages && navigator.languages[0])
|
||||
|| navigator.language
|
||||
|| navigator.userLanguage;
|
||||
const languageWithoutRegionCode = language.toLowerCase().split(/[_-]+/)[0];
|
||||
const catalogs = { en, ja };
|
||||
export function getLanguage (nav) {
|
||||
const language = (nav.languages && nav.languages[0]) || nav.language || nav.userLanguage;
|
||||
const languageWithoutRegionCode = language.toLowerCase().split(/[_-]+/)[0];
|
||||
|
||||
return languageWithoutRegionCode;
|
||||
};
|
||||
|
||||
//
|
||||
// Function Main
|
||||
//
|
||||
|
||||
export async function main (render, api) {
|
||||
const catalogs = { en, ja };
|
||||
const language = getLanguage(navigator);
|
||||
|
||||
const el = document.getElementById('app');
|
||||
// fetch additional config from server
|
||||
const { data: { custom_logo, custom_login_info } } = await api.getRoot();
|
||||
|
||||
const defaultRedirect = () => (<Redirect to="/home" />);
|
||||
const loginRoutes = (
|
||||
<Switch>
|
||||
<Route
|
||||
@ -94,7 +98,7 @@ export async function main (render, api) {
|
||||
return render(
|
||||
<HashRouter>
|
||||
<I18nProvider
|
||||
language={languageWithoutRegionCode}
|
||||
language={language}
|
||||
catalogs={catalogs}
|
||||
>
|
||||
<I18n>
|
||||
@ -102,8 +106,8 @@ export async function main (render, api) {
|
||||
<Background>
|
||||
{!api.isAuthenticated() ? loginRoutes : (
|
||||
<Switch>
|
||||
<Route path="/login" render={() => (<Redirect to="/home" />)} />
|
||||
<Route exact path="/" render={() => (<Redirect to="/home" />)} />
|
||||
<Route path="/login" render={defaultRedirect} />
|
||||
<Route exact path="/" render={defaultRedirect} />
|
||||
<Route
|
||||
render={() => (
|
||||
<App
|
||||
|
||||
@ -48,7 +48,7 @@ class AWXLogin extends Component {
|
||||
try {
|
||||
await api.login(username, password);
|
||||
} catch (error) {
|
||||
if (error.response.status === 401) {
|
||||
if (error.response && error.response.status === 401) {
|
||||
this.setState({ isInputValid: false });
|
||||
}
|
||||
} finally {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import {
|
||||
@ -17,8 +18,8 @@ import {
|
||||
CardBody,
|
||||
} from '@patternfly/react-core';
|
||||
|
||||
import AnsibleSelect from '../../../components/AnsibleSelect';
|
||||
|
||||
import { ConfigContext } from '../../../context';
|
||||
import AnsibleSelect from '../../../components/AnsibleSelect'
|
||||
const { light } = PageSectionVariants;
|
||||
|
||||
class OrganizationAdd extends React.Component {
|
||||
@ -37,8 +38,6 @@ class OrganizationAdd extends React.Component {
|
||||
description: '',
|
||||
instanceGroups: '',
|
||||
custom_virtualenv: '',
|
||||
custom_virtualenvs: [],
|
||||
hideAnsibleSelect: true,
|
||||
error:'',
|
||||
};
|
||||
|
||||
@ -59,9 +58,9 @@ class OrganizationAdd extends React.Component {
|
||||
}
|
||||
|
||||
async onSubmit() {
|
||||
const { api } = this.props;
|
||||
const data = Object.assign({}, { ...this.state });
|
||||
await api.createOrganization(data);
|
||||
|
||||
this.resetForm();
|
||||
}
|
||||
|
||||
@ -69,22 +68,10 @@ class OrganizationAdd extends React.Component {
|
||||
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() {
|
||||
const { name } = this.state;
|
||||
const enabled = name.length > 0; // TODO: add better form validation
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<PageSection variant={light} className="pf-m-condensed">
|
||||
@ -128,13 +115,16 @@ class OrganizationAdd extends React.Component {
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
<AnsibleSelect
|
||||
labelName="Ansible Environment"
|
||||
selected={this.state.custom_virtualenv}
|
||||
selectChange={this.onSelectChange}
|
||||
data={this.state.custom_virtualenvs}
|
||||
hidden={this.state.hideAnsibleSelect}
|
||||
/>
|
||||
<ConfigContext.Consumer>
|
||||
{({ custom_virtualenvs }) =>
|
||||
<AnsibleSelect
|
||||
labelName="Ansible Environment"
|
||||
selected={this.state.custom_virtualenv}
|
||||
selectChange={this.onSelectChange}
|
||||
data={custom_virtualenvs}
|
||||
/>
|
||||
}
|
||||
</ConfigContext.Consumer>
|
||||
</Gallery>
|
||||
<ActionGroup className="at-align-right">
|
||||
<Toolbar>
|
||||
@ -155,4 +145,8 @@ class OrganizationAdd extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
OrganizationAdd.contextTypes = {
|
||||
custom_virtualenvs: PropTypes.array,
|
||||
};
|
||||
|
||||
export default withRouter(OrganizationAdd);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user