mirror of
https://github.com/ansible/awx.git
synced 2026-03-20 18:37:39 -02:30
Add AppContainer and move bootstrapping to App component
This commit is contained in:
@@ -1,216 +1,82 @@
|
|||||||
import React, { Component, Fragment } from 'react';
|
import React from 'react';
|
||||||
import { withRouter } from 'react-router-dom';
|
|
||||||
import { global_breakpoint_md } from '@patternfly/react-tokens';
|
|
||||||
import {
|
import {
|
||||||
Nav,
|
useRouteMatch,
|
||||||
NavList,
|
useLocation,
|
||||||
Page,
|
HashRouter,
|
||||||
PageHeader as PFPageHeader,
|
Route,
|
||||||
PageSidebar,
|
Switch,
|
||||||
} from '@patternfly/react-core';
|
Redirect,
|
||||||
import styled from 'styled-components';
|
} from 'react-router-dom';
|
||||||
import { t } from '@lingui/macro';
|
import { I18n, I18nProvider } from '@lingui/react';
|
||||||
import { withI18n } from '@lingui/react';
|
|
||||||
|
|
||||||
import { ConfigAPI, MeAPI, RootAPI } from './api';
|
import AppContainer from './components/AppContainer';
|
||||||
import About from './components/About';
|
import Background from './components/Background';
|
||||||
import AlertModal from './components/AlertModal';
|
import NotFound from './screens/NotFound';
|
||||||
import NavExpandableGroup from './components/NavExpandableGroup';
|
import Login from './screens/Login';
|
||||||
import BrandLogo from './components/BrandLogo';
|
|
||||||
import PageHeaderToolbar from './components/PageHeaderToolbar';
|
|
||||||
import ErrorDetail from './components/ErrorDetail';
|
|
||||||
import { ConfigProvider } from './contexts/Config';
|
|
||||||
|
|
||||||
const PageHeader = styled(PFPageHeader)`
|
import ja from './locales/ja/messages';
|
||||||
& .pf-c-page__header-brand-link {
|
import en from './locales/en/messages';
|
||||||
color: inherit;
|
import { isAuthenticated } from './util/auth';
|
||||||
|
import { getLanguageWithoutRegionCode } from './util/language';
|
||||||
|
|
||||||
&:hover {
|
import getRouteConfig from './routeConfig';
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
& svg {
|
const ProtectedRoute = ({ children, ...rest }) =>
|
||||||
height: 76px;
|
isAuthenticated(document.cookie) ? (
|
||||||
}
|
<Route {...rest}>{children}</Route>
|
||||||
}
|
) : (
|
||||||
`;
|
<Redirect to="/login" />
|
||||||
|
);
|
||||||
|
|
||||||
class App extends Component {
|
function App() {
|
||||||
constructor(props) {
|
const catalogs = { en, ja };
|
||||||
super(props);
|
const language = getLanguageWithoutRegionCode(navigator);
|
||||||
|
const match = useRouteMatch();
|
||||||
|
const { hash, search, pathname } = useLocation();
|
||||||
|
|
||||||
// initialize with a closed navbar if window size is small
|
return (
|
||||||
const isNavOpen =
|
<I18nProvider language={language} catalogs={catalogs}>
|
||||||
typeof window !== 'undefined' &&
|
<I18n>
|
||||||
window.innerWidth >= parseInt(global_breakpoint_md.value, 10);
|
{({ i18n }) => (
|
||||||
|
<Background>
|
||||||
this.state = {
|
<Switch>
|
||||||
ansible_version: null,
|
<Route exact strict path="/*/">
|
||||||
custom_virtualenvs: null,
|
<Redirect to={`${pathname.slice(0, -1)}${search}${hash}`} />
|
||||||
me: null,
|
</Route>
|
||||||
version: null,
|
<Route path="/login">
|
||||||
isAboutModalOpen: false,
|
<Login isAuthenticated={isAuthenticated} />
|
||||||
isNavOpen,
|
</Route>
|
||||||
configError: null,
|
<Route exact path="/">
|
||||||
};
|
<Redirect to="/home" />
|
||||||
|
</Route>
|
||||||
this.handleLogout = this.handleLogout.bind(this);
|
<ProtectedRoute>
|
||||||
this.handleAboutClose = this.handleAboutClose.bind(this);
|
<AppContainer navRouteConfig={getRouteConfig(i18n)}>
|
||||||
this.handleAboutOpen = this.handleAboutOpen.bind(this);
|
<Switch>
|
||||||
this.handleNavToggle = this.handleNavToggle.bind(this);
|
{getRouteConfig(i18n)
|
||||||
this.handleConfigErrorClose = this.handleConfigErrorClose.bind(this);
|
.flatMap(({ routes }) => routes)
|
||||||
}
|
.map(({ path, screen: Screen }) => (
|
||||||
|
<ProtectedRoute auth key={path} path={path}>
|
||||||
async componentDidMount() {
|
<Screen match={match} />
|
||||||
await this.loadConfig();
|
</ProtectedRoute>
|
||||||
}
|
))
|
||||||
|
.concat(
|
||||||
// eslint-disable-next-line class-methods-use-this
|
<ProtectedRoute auth key="not-found" path="*">
|
||||||
async handleLogout() {
|
<NotFound />
|
||||||
const { history } = this.props;
|
</ProtectedRoute>
|
||||||
await RootAPI.logout();
|
)}
|
||||||
history.replace('/login');
|
</Switch>
|
||||||
}
|
</AppContainer>
|
||||||
|
</ProtectedRoute>
|
||||||
handleAboutOpen() {
|
</Switch>
|
||||||
this.setState({ isAboutModalOpen: true });
|
</Background>
|
||||||
}
|
)}
|
||||||
|
</I18n>
|
||||||
handleAboutClose() {
|
</I18nProvider>
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export { App as _App };
|
export default () => (
|
||||||
export default withI18n()(withRouter(App));
|
<HashRouter>
|
||||||
|
<App />
|
||||||
|
</HashRouter>
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,120 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { mountWithContexts, waitForElement } from '../testUtils/enzymeHelpers';
|
import { mountWithContexts } from '../testUtils/enzymeHelpers';
|
||||||
import { ConfigAPI, MeAPI, RootAPI } from './api';
|
|
||||||
import { asyncFlush } from './setupTests';
|
|
||||||
|
|
||||||
import App from './App';
|
import App from './App';
|
||||||
|
|
||||||
jest.mock('./api');
|
jest.mock('./api');
|
||||||
|
|
||||||
describe('<App />', () => {
|
describe('<App />', () => {
|
||||||
const ansible_version = '111';
|
test('renders ok', () => {
|
||||||
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 => {
|
|
||||||
const wrapper = mountWithContexts(<App />);
|
const wrapper = mountWithContexts(<App />);
|
||||||
wrapper.update();
|
expect(wrapper.length).toBe(1);
|
||||||
|
|
||||||
// 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();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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) {
|
constructor(http) {
|
||||||
super(http);
|
super(http);
|
||||||
this.baseUrl = '/api/v2/config/';
|
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 React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
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 '@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 App from './App';
|
||||||
import RootProvider from './RootProvider';
|
|
||||||
import { BrandName } from './variables';
|
import { BrandName } from './variables';
|
||||||
import getRouteConfig from './routeConfig';
|
|
||||||
|
|
||||||
// eslint-disable-next-line import/prefer-default-export
|
document.title = `Ansible ${BrandName}`;
|
||||||
export function main(render) {
|
|
||||||
const el = document.getElementById('app');
|
|
||||||
document.title = `Ansible ${BrandName}`;
|
|
||||||
|
|
||||||
const removeTrailingSlash = (
|
ReactDOM.render(
|
||||||
<Route
|
<App />,
|
||||||
exact
|
document.getElementById('app') || document.createElement('div')
|
||||||
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);
|
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mount } from 'enzyme';
|
import ReactDOM from 'react-dom';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import App from './App';
|
||||||
import { main } from './index';
|
|
||||||
|
|
||||||
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', () => {
|
describe('index.jsx', () => {
|
||||||
test('index.jsx loads without issue', () => {
|
it('renders ok', () => {
|
||||||
const wrapper = main(render);
|
expect(ReactDOM.render).toHaveBeenCalledWith(<App />, div);
|
||||||
expect(wrapper.find('RootProvider')).toHaveLength(1);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user