mirror of
https://github.com/ansible/awx.git
synced 2026-01-13 19:10:07 -03:30
Add AppContainer and move bootstrapping to App component
This commit is contained in:
parent
990eead3ac
commit
cb453de6a4
@ -1,216 +1,82 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { global_breakpoint_md } from '@patternfly/react-tokens';
|
||||
import React from 'react';
|
||||
import {
|
||||
Nav,
|
||||
NavList,
|
||||
Page,
|
||||
PageHeader as PFPageHeader,
|
||||
PageSidebar,
|
||||
} from '@patternfly/react-core';
|
||||
import styled from 'styled-components';
|
||||
import { t } from '@lingui/macro';
|
||||
import { withI18n } from '@lingui/react';
|
||||
useRouteMatch,
|
||||
useLocation,
|
||||
HashRouter,
|
||||
Route,
|
||||
Switch,
|
||||
Redirect,
|
||||
} from 'react-router-dom';
|
||||
import { I18n, I18nProvider } from '@lingui/react';
|
||||
|
||||
import { ConfigAPI, MeAPI, RootAPI } from './api';
|
||||
import About from './components/About';
|
||||
import AlertModal from './components/AlertModal';
|
||||
import NavExpandableGroup from './components/NavExpandableGroup';
|
||||
import BrandLogo from './components/BrandLogo';
|
||||
import PageHeaderToolbar from './components/PageHeaderToolbar';
|
||||
import ErrorDetail from './components/ErrorDetail';
|
||||
import { ConfigProvider } from './contexts/Config';
|
||||
import AppContainer from './components/AppContainer';
|
||||
import Background from './components/Background';
|
||||
import NotFound from './screens/NotFound';
|
||||
import Login from './screens/Login';
|
||||
|
||||
const PageHeader = styled(PFPageHeader)`
|
||||
& .pf-c-page__header-brand-link {
|
||||
color: inherit;
|
||||
import ja from './locales/ja/messages';
|
||||
import en from './locales/en/messages';
|
||||
import { isAuthenticated } from './util/auth';
|
||||
import { getLanguageWithoutRegionCode } from './util/language';
|
||||
|
||||
&:hover {
|
||||
color: inherit;
|
||||
}
|
||||
import getRouteConfig from './routeConfig';
|
||||
|
||||
& svg {
|
||||
height: 76px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
const ProtectedRoute = ({ children, ...rest }) =>
|
||||
isAuthenticated(document.cookie) ? (
|
||||
<Route {...rest}>{children}</Route>
|
||||
) : (
|
||||
<Redirect to="/login" />
|
||||
);
|
||||
|
||||
class App extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
function App() {
|
||||
const catalogs = { en, ja };
|
||||
const language = getLanguageWithoutRegionCode(navigator);
|
||||
const match = useRouteMatch();
|
||||
const { hash, search, pathname } = useLocation();
|
||||
|
||||
// initialize with a closed navbar if window size is small
|
||||
const isNavOpen =
|
||||
typeof window !== 'undefined' &&
|
||||
window.innerWidth >= parseInt(global_breakpoint_md.value, 10);
|
||||
|
||||
this.state = {
|
||||
ansible_version: null,
|
||||
custom_virtualenvs: null,
|
||||
me: null,
|
||||
version: null,
|
||||
isAboutModalOpen: false,
|
||||
isNavOpen,
|
||||
configError: null,
|
||||
};
|
||||
|
||||
this.handleLogout = this.handleLogout.bind(this);
|
||||
this.handleAboutClose = this.handleAboutClose.bind(this);
|
||||
this.handleAboutOpen = this.handleAboutOpen.bind(this);
|
||||
this.handleNavToggle = this.handleNavToggle.bind(this);
|
||||
this.handleConfigErrorClose = this.handleConfigErrorClose.bind(this);
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
await this.loadConfig();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
async handleLogout() {
|
||||
const { history } = this.props;
|
||||
await RootAPI.logout();
|
||||
history.replace('/login');
|
||||
}
|
||||
|
||||
handleAboutOpen() {
|
||||
this.setState({ isAboutModalOpen: true });
|
||||
}
|
||||
|
||||
handleAboutClose() {
|
||||
this.setState({ isAboutModalOpen: false });
|
||||
}
|
||||
|
||||
handleNavToggle() {
|
||||
this.setState(({ isNavOpen }) => ({ isNavOpen: !isNavOpen }));
|
||||
}
|
||||
|
||||
handleConfigErrorClose() {
|
||||
this.setState({
|
||||
configError: null,
|
||||
});
|
||||
}
|
||||
|
||||
async loadConfig() {
|
||||
try {
|
||||
const [configRes, meRes] = await Promise.all([
|
||||
ConfigAPI.read(),
|
||||
MeAPI.read(),
|
||||
]);
|
||||
const {
|
||||
data: {
|
||||
ansible_version,
|
||||
custom_virtualenvs,
|
||||
project_base_dir,
|
||||
project_local_paths,
|
||||
version,
|
||||
},
|
||||
} = configRes;
|
||||
const {
|
||||
data: {
|
||||
results: [me],
|
||||
},
|
||||
} = meRes;
|
||||
|
||||
this.setState({
|
||||
ansible_version,
|
||||
custom_virtualenvs,
|
||||
project_base_dir,
|
||||
project_local_paths,
|
||||
version,
|
||||
me,
|
||||
});
|
||||
} catch (err) {
|
||||
this.setState({ configError: err });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
ansible_version,
|
||||
custom_virtualenvs,
|
||||
project_base_dir,
|
||||
project_local_paths,
|
||||
isAboutModalOpen,
|
||||
isNavOpen,
|
||||
me,
|
||||
version,
|
||||
configError,
|
||||
} = this.state;
|
||||
const { i18n, routeConfig = [], navLabel = '', children } = this.props;
|
||||
|
||||
const header = (
|
||||
<PageHeader
|
||||
showNavToggle
|
||||
onNavToggle={this.handleNavToggle}
|
||||
logo={<BrandLogo />}
|
||||
logoProps={{ href: '/' }}
|
||||
toolbar={
|
||||
<PageHeaderToolbar
|
||||
loggedInUser={me}
|
||||
isAboutDisabled={!version}
|
||||
onAboutClick={this.handleAboutOpen}
|
||||
onLogoutClick={this.handleLogout}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
const sidebar = (
|
||||
<PageSidebar
|
||||
isNavOpen={isNavOpen}
|
||||
theme="dark"
|
||||
nav={
|
||||
<Nav aria-label={navLabel} theme="dark">
|
||||
<NavList>
|
||||
{routeConfig.map(({ groupId, groupTitle, routes }) => (
|
||||
<NavExpandableGroup
|
||||
key={groupId}
|
||||
groupId={groupId}
|
||||
groupTitle={groupTitle}
|
||||
routes={routes}
|
||||
/>
|
||||
))}
|
||||
</NavList>
|
||||
</Nav>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Page usecondensed="True" header={header} sidebar={sidebar}>
|
||||
<ConfigProvider
|
||||
value={{
|
||||
ansible_version,
|
||||
custom_virtualenvs,
|
||||
project_base_dir,
|
||||
project_local_paths,
|
||||
me,
|
||||
version,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ConfigProvider>
|
||||
</Page>
|
||||
<About
|
||||
ansible_version={ansible_version}
|
||||
version={version}
|
||||
isOpen={isAboutModalOpen}
|
||||
onClose={this.handleAboutClose}
|
||||
/>
|
||||
<AlertModal
|
||||
isOpen={configError}
|
||||
variant="error"
|
||||
title={i18n._(t`Error!`)}
|
||||
onClose={this.handleConfigErrorClose}
|
||||
>
|
||||
{i18n._(t`Failed to retrieve configuration.`)}
|
||||
<ErrorDetail error={configError} />
|
||||
</AlertModal>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<I18nProvider language={language} catalogs={catalogs}>
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<Background>
|
||||
<Switch>
|
||||
<Route exact strict path="/*/">
|
||||
<Redirect to={`${pathname.slice(0, -1)}${search}${hash}`} />
|
||||
</Route>
|
||||
<Route path="/login">
|
||||
<Login isAuthenticated={isAuthenticated} />
|
||||
</Route>
|
||||
<Route exact path="/">
|
||||
<Redirect to="/home" />
|
||||
</Route>
|
||||
<ProtectedRoute>
|
||||
<AppContainer navRouteConfig={getRouteConfig(i18n)}>
|
||||
<Switch>
|
||||
{getRouteConfig(i18n)
|
||||
.flatMap(({ routes }) => routes)
|
||||
.map(({ path, screen: Screen }) => (
|
||||
<ProtectedRoute auth key={path} path={path}>
|
||||
<Screen match={match} />
|
||||
</ProtectedRoute>
|
||||
))
|
||||
.concat(
|
||||
<ProtectedRoute auth key="not-found" path="*">
|
||||
<NotFound />
|
||||
</ProtectedRoute>
|
||||
)}
|
||||
</Switch>
|
||||
</AppContainer>
|
||||
</ProtectedRoute>
|
||||
</Switch>
|
||||
</Background>
|
||||
)}
|
||||
</I18n>
|
||||
</I18nProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export { App as _App };
|
||||
export default withI18n()(withRouter(App));
|
||||
export default () => (
|
||||
<HashRouter>
|
||||
<App />
|
||||
</HashRouter>
|
||||
);
|
||||
|
||||
@ -1,120 +1,14 @@
|
||||
import React from 'react';
|
||||
|
||||
import { mountWithContexts, waitForElement } from '../testUtils/enzymeHelpers';
|
||||
import { ConfigAPI, MeAPI, RootAPI } from './api';
|
||||
import { asyncFlush } from './setupTests';
|
||||
import { mountWithContexts } from '../testUtils/enzymeHelpers';
|
||||
|
||||
import App from './App';
|
||||
|
||||
jest.mock('./api');
|
||||
|
||||
describe('<App />', () => {
|
||||
const ansible_version = '111';
|
||||
const custom_virtualenvs = [];
|
||||
const version = '222';
|
||||
|
||||
beforeEach(() => {
|
||||
ConfigAPI.read = () =>
|
||||
Promise.resolve({
|
||||
data: {
|
||||
ansible_version,
|
||||
custom_virtualenvs,
|
||||
version,
|
||||
},
|
||||
});
|
||||
MeAPI.read = () => Promise.resolve({ data: { results: [{}] } });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('expected content is rendered', () => {
|
||||
const routeConfig = [
|
||||
{
|
||||
groupTitle: 'Group One',
|
||||
groupId: 'group_one',
|
||||
routes: [
|
||||
{ title: 'Foo', path: '/foo' },
|
||||
{ title: 'Bar', path: '/bar' },
|
||||
],
|
||||
},
|
||||
{
|
||||
groupTitle: 'Group Two',
|
||||
groupId: 'group_two',
|
||||
routes: [{ title: 'Fiz', path: '/fiz' }],
|
||||
},
|
||||
];
|
||||
const appWrapper = mountWithContexts(
|
||||
<App routeConfig={routeConfig}>
|
||||
{routeConfig.map(({ groupId }) => (
|
||||
<div key={groupId} id={groupId} />
|
||||
))}
|
||||
</App>
|
||||
);
|
||||
|
||||
// page components
|
||||
expect(appWrapper.length).toBe(1);
|
||||
expect(appWrapper.find('PageHeader').length).toBe(1);
|
||||
expect(appWrapper.find('PageSidebar').length).toBe(1);
|
||||
|
||||
// sidebar groups and route links
|
||||
expect(appWrapper.find('NavExpandableGroup').length).toBe(2);
|
||||
expect(appWrapper.find('a[href="/#/foo"]').length).toBe(1);
|
||||
expect(appWrapper.find('a[href="/#/bar"]').length).toBe(1);
|
||||
expect(appWrapper.find('a[href="/#/fiz"]').length).toBe(1);
|
||||
|
||||
expect(appWrapper.find('#group_one').length).toBe(1);
|
||||
expect(appWrapper.find('#group_two').length).toBe(1);
|
||||
});
|
||||
|
||||
test('opening the about modal renders prefetched config data', async done => {
|
||||
test('renders ok', () => {
|
||||
const wrapper = mountWithContexts(<App />);
|
||||
wrapper.update();
|
||||
|
||||
// open about modal
|
||||
const aboutDropdown = 'Dropdown QuestionCircleIcon';
|
||||
const aboutButton = 'DropdownItem li button';
|
||||
const aboutModalContent = 'AboutModalBoxContent';
|
||||
const aboutModalClose = 'button[aria-label="Close Dialog"]';
|
||||
|
||||
await waitForElement(wrapper, aboutDropdown);
|
||||
wrapper.find(aboutDropdown).simulate('click');
|
||||
|
||||
const button = await waitForElement(
|
||||
wrapper,
|
||||
aboutButton,
|
||||
el => !el.props().disabled
|
||||
);
|
||||
button.simulate('click');
|
||||
|
||||
// check about modal content
|
||||
const content = await waitForElement(wrapper, aboutModalContent);
|
||||
expect(content.find('dd').text()).toContain(ansible_version);
|
||||
expect(content.find('pre').text()).toContain(`< AWX ${version} >`);
|
||||
|
||||
// close about modal
|
||||
wrapper.find(aboutModalClose).simulate('click');
|
||||
expect(wrapper.find(aboutModalContent)).toHaveLength(0);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
test('handleNavToggle sets state.isNavOpen to opposite', () => {
|
||||
const appWrapper = mountWithContexts(<App />).find('App');
|
||||
|
||||
const { handleNavToggle } = appWrapper.instance();
|
||||
[true, false, true, false, true].forEach(expected => {
|
||||
expect(appWrapper.state().isNavOpen).toBe(expected);
|
||||
handleNavToggle();
|
||||
});
|
||||
});
|
||||
|
||||
test('onLogout makes expected call to api client', async done => {
|
||||
const appWrapper = mountWithContexts(<App />).find('App');
|
||||
appWrapper.instance().handleLogout();
|
||||
await asyncFlush();
|
||||
expect(RootAPI.logout).toHaveBeenCalledTimes(1);
|
||||
done();
|
||||
expect(wrapper.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,27 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import { I18nProvider } from '@lingui/react';
|
||||
|
||||
import { HashRouter } from 'react-router-dom';
|
||||
|
||||
import { getLanguageWithoutRegionCode } from './util/language';
|
||||
import ja from './locales/ja/messages';
|
||||
import en from './locales/en/messages';
|
||||
|
||||
class RootProvider extends Component {
|
||||
render() {
|
||||
const { children } = this.props;
|
||||
|
||||
const catalogs = { en, ja };
|
||||
const language = getLanguageWithoutRegionCode(navigator);
|
||||
|
||||
return (
|
||||
<HashRouter>
|
||||
<I18nProvider language={language} catalogs={catalogs}>
|
||||
{children}
|
||||
</I18nProvider>
|
||||
</HashRouter>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default RootProvider;
|
||||
@ -4,6 +4,7 @@ class Config extends Base {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/config/';
|
||||
this.read = this.read.bind(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
142
awx/ui_next/src/components/AppContainer/AppContainer.jsx
Normal file
142
awx/ui_next/src/components/AppContainer/AppContainer.jsx
Normal file
@ -0,0 +1,142 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useHistory, useLocation, withRouter } from 'react-router-dom';
|
||||
import { global_breakpoint_md } from '@patternfly/react-tokens';
|
||||
import {
|
||||
Nav,
|
||||
NavList,
|
||||
Page,
|
||||
PageHeader as PFPageHeader,
|
||||
PageSidebar,
|
||||
} from '@patternfly/react-core';
|
||||
import { t } from '@lingui/macro';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { ConfigAPI, MeAPI, RootAPI } from '../../api';
|
||||
import { ConfigProvider } from '../../contexts/Config';
|
||||
import About from '../About';
|
||||
import AlertModal from '../AlertModal';
|
||||
import ErrorDetail from '../ErrorDetail';
|
||||
import BrandLogo from './BrandLogo';
|
||||
import NavExpandableGroup from './NavExpandableGroup';
|
||||
import PageHeaderToolbar from './PageHeaderToolbar';
|
||||
|
||||
const PageHeader = styled(PFPageHeader)`
|
||||
& .pf-c-page__header-brand-link {
|
||||
color: inherit;
|
||||
|
||||
&:hover {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
& svg {
|
||||
height: 76px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
function AppContainer({ i18n, navRouteConfig = [], children }) {
|
||||
const history = useHistory();
|
||||
const { pathname } = useLocation();
|
||||
const [config, setConfig] = useState({});
|
||||
const [configError, setConfigError] = useState(null);
|
||||
const [isAboutModalOpen, setIsAboutModalOpen] = useState(false);
|
||||
const [isNavOpen, setIsNavOpen] = useState(
|
||||
typeof window !== 'undefined' &&
|
||||
window.innerWidth >= parseInt(global_breakpoint_md.value, 10)
|
||||
);
|
||||
|
||||
const handleAboutModalOpen = () => setIsAboutModalOpen(true);
|
||||
const handleAboutModalClose = () => setIsAboutModalOpen(false);
|
||||
const handleConfigErrorClose = () => setConfigError(null);
|
||||
const handleNavToggle = () => setIsNavOpen(!isNavOpen);
|
||||
|
||||
const handleLogout = async () => {
|
||||
await RootAPI.logout();
|
||||
history.replace('/login');
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const loadConfig = async () => {
|
||||
if (config?.version) return;
|
||||
try {
|
||||
const [
|
||||
{ data },
|
||||
{
|
||||
data: {
|
||||
results: [me],
|
||||
},
|
||||
},
|
||||
] = await Promise.all([ConfigAPI.read(), MeAPI.read()]);
|
||||
setConfig({ ...data, me });
|
||||
} catch (err) {
|
||||
setConfigError(err);
|
||||
}
|
||||
};
|
||||
loadConfig();
|
||||
}, [config, pathname]);
|
||||
|
||||
const header = (
|
||||
<PageHeader
|
||||
showNavToggle
|
||||
onNavToggle={handleNavToggle}
|
||||
logo={<BrandLogo />}
|
||||
logoProps={{ href: '/' }}
|
||||
toolbar={
|
||||
<PageHeaderToolbar
|
||||
loggedInUser={config?.me}
|
||||
isAboutDisabled={!config?.version}
|
||||
onAboutClick={handleAboutModalOpen}
|
||||
onLogoutClick={handleLogout}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
const sidebar = (
|
||||
<PageSidebar
|
||||
isNavOpen={isNavOpen}
|
||||
theme="dark"
|
||||
nav={
|
||||
<Nav aria-label={i18n._(t`Navigation`)} theme="dark">
|
||||
<NavList>
|
||||
{navRouteConfig.map(({ groupId, groupTitle, routes }) => (
|
||||
<NavExpandableGroup
|
||||
key={groupId}
|
||||
groupId={groupId}
|
||||
groupTitle={groupTitle}
|
||||
routes={routes}
|
||||
/>
|
||||
))}
|
||||
</NavList>
|
||||
</Nav>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Page usecondensed="True" header={header} sidebar={sidebar}>
|
||||
<ConfigProvider value={config}>{children}</ConfigProvider>
|
||||
</Page>
|
||||
<About
|
||||
ansible_version={config?.ansible_version}
|
||||
version={config?.version}
|
||||
isOpen={isAboutModalOpen}
|
||||
onClose={handleAboutModalClose}
|
||||
/>
|
||||
<AlertModal
|
||||
isOpen={configError}
|
||||
variant="error"
|
||||
title={i18n._(t`Error!`)}
|
||||
onClose={handleConfigErrorClose}
|
||||
>
|
||||
{i18n._(t`Failed to retrieve configuration.`)}
|
||||
<ErrorDetail error={configError} />
|
||||
</AlertModal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { AppContainer as _AppContainer };
|
||||
export default withI18n()(withRouter(AppContainer));
|
||||
125
awx/ui_next/src/components/AppContainer/AppContainer.test.jsx
Normal file
125
awx/ui_next/src/components/AppContainer/AppContainer.test.jsx
Normal file
@ -0,0 +1,125 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
import {
|
||||
mountWithContexts,
|
||||
waitForElement,
|
||||
} from '../../../testUtils/enzymeHelpers';
|
||||
import { ConfigAPI, MeAPI, RootAPI } from '../../api';
|
||||
|
||||
import AppContainer from './AppContainer';
|
||||
|
||||
jest.mock('../../api');
|
||||
|
||||
describe('<AppContainer />', () => {
|
||||
const ansible_version = '111';
|
||||
const custom_virtualenvs = [];
|
||||
const version = '222';
|
||||
|
||||
beforeEach(() => {
|
||||
ConfigAPI.read.mockResolvedValue({
|
||||
data: {
|
||||
ansible_version,
|
||||
custom_virtualenvs,
|
||||
version,
|
||||
},
|
||||
});
|
||||
MeAPI.read.mockResolvedValue({ data: { results: [{}] } });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('expected content is rendered', async () => {
|
||||
const routeConfig = [
|
||||
{
|
||||
groupTitle: 'Group One',
|
||||
groupId: 'group_one',
|
||||
routes: [
|
||||
{ title: 'Foo', path: '/foo' },
|
||||
{ title: 'Bar', path: '/bar' },
|
||||
],
|
||||
},
|
||||
{
|
||||
groupTitle: 'Group Two',
|
||||
groupId: 'group_two',
|
||||
routes: [{ title: 'Fiz', path: '/fiz' }],
|
||||
},
|
||||
];
|
||||
|
||||
let wrapper;
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<AppContainer navRouteConfig={routeConfig}>
|
||||
{routeConfig.map(({ groupId }) => (
|
||||
<div key={groupId} id={groupId} />
|
||||
))}
|
||||
</AppContainer>
|
||||
);
|
||||
});
|
||||
|
||||
// page components
|
||||
expect(wrapper.length).toBe(1);
|
||||
expect(wrapper.find('PageHeader').length).toBe(1);
|
||||
expect(wrapper.find('PageSidebar').length).toBe(1);
|
||||
|
||||
// sidebar groups and route links
|
||||
expect(wrapper.find('NavExpandableGroup').length).toBe(2);
|
||||
expect(wrapper.find('a[href="/#/foo"]').length).toBe(1);
|
||||
expect(wrapper.find('a[href="/#/bar"]').length).toBe(1);
|
||||
expect(wrapper.find('a[href="/#/fiz"]').length).toBe(1);
|
||||
|
||||
expect(wrapper.find('#group_one').length).toBe(1);
|
||||
expect(wrapper.find('#group_two').length).toBe(1);
|
||||
});
|
||||
|
||||
test('opening the about modal renders prefetched config data', async () => {
|
||||
const aboutDropdown = 'Dropdown QuestionCircleIcon';
|
||||
const aboutButton = 'DropdownItem li button';
|
||||
const aboutModalContent = 'AboutModalBoxContent';
|
||||
const aboutModalClose = 'button[aria-label="Close Dialog"]';
|
||||
|
||||
let wrapper;
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<AppContainer />);
|
||||
});
|
||||
|
||||
// open about dropdown menu
|
||||
await waitForElement(wrapper, aboutDropdown);
|
||||
wrapper.find(aboutDropdown).simulate('click');
|
||||
|
||||
// open about modal
|
||||
(
|
||||
await waitForElement(wrapper, aboutButton, el => !el.props().disabled)
|
||||
).simulate('click');
|
||||
|
||||
// check about modal content
|
||||
const content = await waitForElement(wrapper, aboutModalContent);
|
||||
expect(content.find('dd').text()).toContain(ansible_version);
|
||||
expect(content.find('pre').text()).toContain(`< AWX ${version} >`);
|
||||
|
||||
// close about modal
|
||||
wrapper.find(aboutModalClose).simulate('click');
|
||||
expect(wrapper.find(aboutModalContent)).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('logout makes expected call to api client', async () => {
|
||||
const userMenuButton = 'UserIcon';
|
||||
const logoutButton = '#logout-button button';
|
||||
|
||||
let wrapper;
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<AppContainer />);
|
||||
});
|
||||
|
||||
// open the user menu
|
||||
expect(wrapper.find(logoutButton)).toHaveLength(0);
|
||||
wrapper.find(userMenuButton).simulate('click');
|
||||
expect(wrapper.find(logoutButton)).toHaveLength(1);
|
||||
|
||||
// logout
|
||||
wrapper.find(logoutButton).simulate('click');
|
||||
expect(RootAPI.logout).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
3
awx/ui_next/src/components/AppContainer/index.jsx
Normal file
3
awx/ui_next/src/components/AppContainer/index.jsx
Normal file
@ -0,0 +1,3 @@
|
||||
import AppContainer from './AppContainer';
|
||||
|
||||
export default AppContainer;
|
||||
@ -1 +0,0 @@
|
||||
export { default } from './BrandLogo';
|
||||
@ -1 +0,0 @@
|
||||
export { default } from './NavExpandableGroup';
|
||||
@ -1 +0,0 @@
|
||||
export { default } from './PageHeaderToolbar';
|
||||
@ -1,93 +1,12 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Route, Switch, Redirect } from 'react-router-dom';
|
||||
import { I18n } from '@lingui/react';
|
||||
|
||||
import '@patternfly/react-core/dist/styles/base.css';
|
||||
|
||||
import { isAuthenticated } from './util/auth';
|
||||
import Background from './components/Background';
|
||||
import NotFound from './screens/NotFound';
|
||||
import Login from './screens/Login';
|
||||
|
||||
import App from './App';
|
||||
import RootProvider from './RootProvider';
|
||||
import { BrandName } from './variables';
|
||||
import getRouteConfig from './routeConfig';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export function main(render) {
|
||||
const el = document.getElementById('app');
|
||||
document.title = `Ansible ${BrandName}`;
|
||||
document.title = `Ansible ${BrandName}`;
|
||||
|
||||
const removeTrailingSlash = (
|
||||
<Route
|
||||
exact
|
||||
strict
|
||||
path="/*/"
|
||||
render={({
|
||||
history: {
|
||||
location: { pathname, search, hash },
|
||||
},
|
||||
}) => <Redirect to={`${pathname.slice(0, -1)}${search}${hash}`} />}
|
||||
/>
|
||||
);
|
||||
|
||||
const AppRoute = ({ auth, children, ...rest }) =>
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
auth ? (
|
||||
isAuthenticated(document.cookie) ? (
|
||||
<Route {...rest}>{children}</Route>
|
||||
) : (
|
||||
<Redirect to="/login" />
|
||||
)
|
||||
) : isAuthenticated(document.cookie) ? (
|
||||
<Redirect to="/home" />
|
||||
) : (
|
||||
<Route {...rest}>{children}</Route>
|
||||
);
|
||||
|
||||
return render(
|
||||
<RootProvider>
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<Background>
|
||||
<Switch>
|
||||
{removeTrailingSlash}
|
||||
<AppRoute path="/login">
|
||||
<Login isAuthenticated={isAuthenticated} />
|
||||
</AppRoute>
|
||||
<AppRoute exact path="/">
|
||||
<Login isAuthenticated={isAuthenticated} />
|
||||
</AppRoute>
|
||||
<AppRoute auth>
|
||||
<App routeConfig={getRouteConfig(i18n)}>
|
||||
<Switch>
|
||||
{getRouteConfig(i18n)
|
||||
.flatMap(({ routes }) => routes)
|
||||
.map(({ path, screen: Screen }) => (
|
||||
<AppRoute
|
||||
auth
|
||||
key={path}
|
||||
path={path}
|
||||
render={({ match }) => <Screen match={match} />}
|
||||
/>
|
||||
))
|
||||
.concat(
|
||||
<AppRoute auth key="not-found" path="*">
|
||||
<NotFound />
|
||||
</AppRoute>
|
||||
)}
|
||||
</Switch>
|
||||
</App>
|
||||
</AppRoute>
|
||||
</Switch>
|
||||
</Background>
|
||||
)}
|
||||
</I18n>
|
||||
</RootProvider>,
|
||||
el || document.createElement('div')
|
||||
);
|
||||
}
|
||||
|
||||
main(ReactDOM.render);
|
||||
ReactDOM.render(
|
||||
<App />,
|
||||
document.getElementById('app') || document.createElement('div')
|
||||
);
|
||||
|
||||
@ -1,13 +1,17 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { main } from './index';
|
||||
import ReactDOM from 'react-dom';
|
||||
import App from './App';
|
||||
|
||||
const render = template => mount(<MemoryRouter>{template}</MemoryRouter>);
|
||||
jest.mock('react-dom', () => ({ render: jest.fn() }));
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.setAttribute('id', 'app');
|
||||
document.body.appendChild(div);
|
||||
|
||||
require('./index.jsx');
|
||||
|
||||
describe('index.jsx', () => {
|
||||
test('index.jsx loads without issue', () => {
|
||||
const wrapper = main(render);
|
||||
expect(wrapper.find('RootProvider')).toHaveLength(1);
|
||||
it('renders ok', () => {
|
||||
expect(ReactDOM.render).toHaveBeenCalledWith(<App />, div);
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user