Merge pull request #84 from jakemcdermott/tests-fixup

add more unit and functional test coverage
This commit is contained in:
Jake McDermott
2019-01-07 07:50:11 -05:00
committed by GitHub
14 changed files with 273 additions and 144 deletions

View File

@@ -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);
});
}); });

View File

@@ -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();
});
}); });

View File

@@ -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);
});
});

View File

@@ -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}

View File

@@ -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');
}); });

View File

@@ -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>
); );

View File

@@ -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');
});
}); });

View File

@@ -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}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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;
} }

View File

@@ -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

View File

@@ -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 {

View File

@@ -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);