add RootDialog and Network contexts, update app bootstrapping

This commit is contained in:
John Mitchell
2019-04-08 12:19:13 -04:00
parent e20cf72dd6
commit aea4a04c66
7 changed files with 544 additions and 315 deletions

View File

@@ -6,13 +6,20 @@ import {
Page, Page,
PageHeader, PageHeader,
PageSidebar, PageSidebar,
Button
} from '@patternfly/react-core'; } from '@patternfly/react-core';
import { I18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { RootDialog } from './contexts/RootDialog';
import { withNetwork } from './contexts/Network';
import AlertModal from './components/AlertModal';
import About from './components/About'; import About from './components/About';
import NavExpandableGroup from './components/NavExpandableGroup'; import NavExpandableGroup from './components/NavExpandableGroup';
import TowerLogo from './components/TowerLogo'; import TowerLogo from './components/TowerLogo';
import PageHeaderToolbar from './components/PageHeaderToolbar'; import PageHeaderToolbar from './components/PageHeaderToolbar';
import { ConfigContext } from './context';
class App extends Component { class App extends Component {
constructor (props) { constructor (props) {
@@ -23,29 +30,24 @@ class App extends Component {
&& window.innerWidth >= parseInt(global_breakpoint_md.value, 10); && window.innerWidth >= parseInt(global_breakpoint_md.value, 10);
this.state = { this.state = {
ansible_version: null,
custom_virtualenvs: null,
isAboutModalOpen: false, isAboutModalOpen: false,
isNavOpen, isNavOpen
version: null,
}; };
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.onNavToggle = this.onNavToggle.bind(this); this.onNavToggle = this.onNavToggle.bind(this);
} }
componentDidMount () {
this.fetchConfig();
}
async onLogout () { async onLogout () {
const { api } = this.props; const { api, handleHttpError } = this.props;
try {
await api.logout(); await api.logout();
window.location.replace('/#/login'); window.location.replace('/#/login');
} catch (err) {
handleHttpError(err);
}
} }
onAboutModalOpen () { onAboutModalOpen () {
@@ -60,24 +62,12 @@ class App extends Component {
this.setState(({ isNavOpen }) => ({ isNavOpen: !isNavOpen })); this.setState(({ isNavOpen }) => ({ isNavOpen: !isNavOpen }));
} }
async fetchConfig () {
const { api } = this.props;
try {
const { data: { ansible_version, custom_virtualenvs, version } } = await api.getConfig();
this.setState({ ansible_version, custom_virtualenvs, version });
} catch (err) {
this.setState({ ansible_version: null, custom_virtualenvs: null, version: null });
}
}
render () { render () {
const { const {
ansible_version, ansible_version,
custom_virtualenvs,
isAboutModalOpen, isAboutModalOpen,
isNavOpen, isNavOpen,
version, version
} = this.state; } = this.state;
const { const {
@@ -86,63 +76,76 @@ class App extends Component {
navLabel = '', navLabel = '',
} = this.props; } = this.props;
const config = {
ansible_version,
custom_virtualenvs,
version,
};
return ( return (
<Fragment> <I18n>
<Page {({ i18n }) => (
usecondensed="True" <RootDialog>
header={( {({ title, bodyText, variant = 'info', clearRootDialogMessage }) => (
<PageHeader <Fragment>
showNavToggle {(title || bodyText) && (
onNavToggle={this.onNavToggle} <AlertModal
logo={<TowerLogo linkTo="/" />} variant={variant}
toolbar={( isOpen={!!(title || bodyText)}
<PageHeaderToolbar onClose={clearRootDialogMessage}
isAboutDisabled={!version} title={title}
onAboutClick={this.onAboutModalOpen} actions={[
onLogoutClick={this.onLogout} <Button key="close" variant="secondary" onClick={clearRootDialogMessage}>{i18n._(t`Close`)}</Button>
]}
>
{bodyText}
</AlertModal>
)}
<Page
usecondensed="True"
header={(
<PageHeader
showNavToggle
onNavToggle={this.onNavToggle}
logo={<TowerLogo linkTo="/" />}
toolbar={(
<PageHeaderToolbar
isAboutDisabled={!version}
onAboutClick={this.onAboutModalOpen}
onLogoutClick={this.onLogout}
/>
)}
/>
)}
sidebar={(
<PageSidebar
isNavOpen={isNavOpen}
nav={(
<Nav aria-label={navLabel}>
<NavList>
{routeGroups.map(({ groupId, groupTitle, routes }) => (
<NavExpandableGroup
key={groupId}
groupId={groupId}
groupTitle={groupTitle}
routes={routes}
/>
))}
</NavList>
</Nav>
)}
/>
)}
>
{render && render({ routeGroups })}
</Page>
<About
ansible_version={ansible_version}
version={version}
isOpen={isAboutModalOpen}
onClose={this.onAboutModalClose}
/> />
)} </Fragment>
/> )}
)} </RootDialog>
sidebar={( )}
<PageSidebar </I18n>
isNavOpen={isNavOpen}
nav={(
<Nav aria-label={navLabel}>
<NavList>
{routeGroups.map(({ groupId, groupTitle, routes }) => (
<NavExpandableGroup
key={groupId}
groupId={groupId}
groupTitle={groupTitle}
routes={routes}
/>
))}
</NavList>
</Nav>
)}
/>
)}
>
<ConfigContext.Provider value={config}>
{render && render({ routeGroups })}
</ConfigContext.Provider>
</Page>
<About
ansible_version={ansible_version}
version={version}
isOpen={isAboutModalOpen}
onClose={this.onAboutModalClose}
/>
</Fragment>
); );
} }
} }
export default App; export default withNetwork(App);

44
src/RootProvider.jsx Normal file
View File

@@ -0,0 +1,44 @@
import React, { Component } from 'react';
import {
I18nProvider,
} from '@lingui/react';
import { NetworkProvider } from './contexts/Network';
import { RootDialogProvider } from './contexts/RootDialog';
import { ConfigProvider } from './contexts/Config';
import ja from '../build/locales/ja/messages';
import en from '../build/locales/en/messages';
export function getLanguage (nav) {
const language = (nav.languages && nav.languages[0]) || nav.language || nav.userLanguage;
const languageWithoutRegionCode = language.toLowerCase().split(/[_-]+/)[0];
return languageWithoutRegionCode;
}
class RootProvider extends Component {
render () {
const { children } = this.props;
const catalogs = { en, ja };
const language = getLanguage(navigator);
return (
<I18nProvider
language={language}
catalogs={catalogs}
>
<RootDialogProvider>
<NetworkProvider>
<ConfigProvider>
{children}
</ConfigProvider>
</NetworkProvider>
</RootDialogProvider>
</I18nProvider>
);
}
}
export default RootProvider;

View File

@@ -1,4 +0,0 @@
import React from 'react';
// eslint-disable-next-line import/prefer-default-export
export const ConfigContext = React.createContext({});

89
src/contexts/Config.jsx Normal file
View File

@@ -0,0 +1,89 @@
import React, { Component } from 'react';
import { withNetwork } from './Network';
const ConfigContext = React.createContext({});
class provider extends Component {
constructor (props) {
super(props);
this.state = {
value: {
ansible_version: null,
custom_virtualenvs: null,
version: null,
custom_logo: null,
custom_login_info: null
}
};
this.fetchConfig = this.fetchConfig.bind(this);
}
componentDidMount () {
this.fetchConfig();
}
async fetchConfig () {
const { api, handleHttpError } = this.props;
try {
const {
data: {
ansible_version,
custom_virtualenvs,
version
}
} = await api.getConfig();
const {
data: {
custom_logo,
custom_login_info
}
} = await api.getRoot();
this.setState({
value: {
ansible_version,
custom_virtualenvs,
version,
custom_logo,
custom_login_info
}
});
} catch (err) {
handleHttpError(err) || this.setState({
value: {
ansible_version: null,
custom_virtualenvs: null,
version: null,
custom_logo: null,
custom_login_info: null
}
});
}
}
render () {
const {
value
} = this.state;
const { children } = this.props;
return (
<ConfigContext.Provider value={value}>
{children}
</ConfigContext.Provider>
);
}
}
export const ConfigProvider = withNetwork(provider);
export const Config = ({ children }) => (
<ConfigContext.Consumer>
{value => children(value)}
</ConfigContext.Consumer>
);

84
src/contexts/Network.jsx Normal file
View File

@@ -0,0 +1,84 @@
import axios from 'axios';
import React, { Component } from 'react';
import { withRouter } from 'react-router-dom';
import { withRootDialog } from './RootDialog';
import APIClient from '../api';
const NetworkContext = React.createContext({});
class prov extends Component {
constructor (props) {
super(props);
this.state = {
value: {
api: new APIClient(axios.create({ xsrfCookieName: 'csrftoken', xsrfHeaderName: 'X-CSRFToken' })),
handleHttpError: err => {
if (err.response.status === 401) {
this.handle401();
} else if (err.response.status === 404) {
this.handle404();
}
return (err.response.status === 401 || err.response.status === 404);
}
}
};
}
handle401 () {
const { handle401, history, setRootDialogMessage } = this.props;
if (handle401) {
handle401();
return;
}
history.replace('/login');
setRootDialogMessage({
bodyText: 'You have been logged out.',
});
}
handle404 () {
const { handle404, history, setRootDialogMessage } = this.props;
if (handle404) {
handle404();
return;
}
history.replace('/home');
setRootDialogMessage({
title: '404',
bodyText: 'Cannot find resource.',
variant: 'warning'
});
}
render () {
const {
children
} = this.props;
const {
value
} = this.state;
return (
<NetworkContext.Provider value={value}>
{children}
</NetworkContext.Provider>
);
}
}
export const NetworkProvider = withRootDialog(withRouter(prov));
export function withNetwork (Child) {
return (props) => (
<NetworkContext.Consumer>
{context => <Child {...props} {...context} />}
</NetworkContext.Consumer>
);
}

View File

@@ -0,0 +1,53 @@
import React, { Component } from 'react';
const RootDialogContext = React.createContext({});
export class RootDialogProvider extends Component {
constructor (props) {
super(props);
this.state = {
value: {
title: null,
setRootDialogMessage: ({ title, bodyText, variant }) => {
const { value } = this.state;
this.setState({ value: { ...value, title, bodyText, variant } });
},
clearRootDialogMessage: () => {
const { value } = this.state;
this.setState({ value: { ...value, title: null, bodyText: null, variant: null } });
}
}
};
}
render () {
const {
children
} = this.props;
const {
value
} = this.state;
return (
<RootDialogContext.Provider value={value}>
{children}
</RootDialogContext.Provider>
);
}
}
export const RootDialog = ({ children }) => (
<RootDialogContext.Consumer>
{value => children(value)}
</RootDialogContext.Consumer>
);
export function withRootDialog (Child) {
return (props) => (
<RootDialogContext.Consumer>
{context => <Child {...props} {...context} />}
</RootDialogContext.Consumer>
);
}

View File

@@ -1,15 +1,13 @@
import axios from 'axios';
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { import {
HashRouter, HashRouter,
Redirect,
Route, Route,
Switch, Switch,
Redirect
} from 'react-router-dom'; } from 'react-router-dom';
import { import {
I18n, I18n
I18nProvider,
} from '@lingui/react'; } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
@@ -19,10 +17,13 @@ import './components/Pagination/styles.scss';
import './components/DataListToolbar/styles.scss'; import './components/DataListToolbar/styles.scss';
import './components/SelectedList/styles.scss'; import './components/SelectedList/styles.scss';
import APIClient from './api'; import { Config } from './contexts/Config';
import App from './App';
import Background from './components/Background'; import Background from './components/Background';
import RootProvider from './RootProvider';
import App from './App';
import Applications from './pages/Applications'; import Applications from './pages/Applications';
import Credentials from './pages/Credentials'; import Credentials from './pages/Credentials';
import CredentialTypes from './pages/CredentialTypes'; import CredentialTypes from './pages/CredentialTypes';
@@ -46,242 +47,201 @@ import License from './pages/License';
import Teams from './pages/Teams'; import Teams from './pages/Teams';
import Templates from './pages/Templates'; import Templates from './pages/Templates';
import Users from './pages/Users'; import Users from './pages/Users';
import ja from '../build/locales/ja/messages';
import en from '../build/locales/en/messages';
//
// Initialize http
//
const http = axios.create({ xsrfCookieName: 'csrftoken', xsrfHeaderName: 'X-CSRFToken' });
//
// Derive the language and region from global user agent data. Example: es-US
// see: https://developer.mozilla.org/en-US/docs/Web/API/Navigator
//
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);
// eslint-disable-next-line import/prefer-default-export
export async function main (render) {
const el = document.getElementById('app'); const el = document.getElementById('app');
const { data: { custom_logo, custom_login_info } } = await api.getRoot();
const defaultRedirect = () => (<Redirect to="/home" />);
const loginRoutes = (
<Switch>
<Route
path="/login"
render={() => (
<Login
api={api}
logo={custom_logo}
loginInfo={custom_login_info}
/>
)}
/>
<Redirect to="/login" />
</Switch>
);
return render( return render(
<HashRouter> <HashRouter>
<I18nProvider <RootProvider>
language={language}
catalogs={catalogs}
>
<I18n> <I18n>
{({ i18n }) => ( {({ i18n }) => (
<Background> <Background>
{!api.isAuthenticated() ? loginRoutes : ( <Switch>
<Switch> <Route
<Route path="/login" render={defaultRedirect} /> path="/login"
<Route exact path="/" render={defaultRedirect} /> render={() => (
<Route <Config>
render={() => ( {({ custom_logo, custom_login_info }) => (
<App <Login
api={api} logo={custom_logo}
navLabel={i18n._(t`Primary Navigation`)} loginInfo={custom_login_info}
routeGroups={[ />
{ )}
groupTitle: i18n._(t`Views`), </Config>
groupId: 'views_group', )}
routes: [ />
{ <Route exact path="/" render={() => <Redirect to="/home" />} />
title: i18n._(t`Dashboard`), <Route
path: '/home', render={() => (
component: Dashboard <App
}, navLabel={i18n._(t`Primary Navigation`)}
{ routeGroups={[
title: i18n._(t`Jobs`), {
path: '/jobs', groupTitle: i18n._(t`Views`),
component: Jobs groupId: 'views_group',
}, routes: [
{ {
title: i18n._(t`Schedules`), title: i18n._(t`Dashboard`),
path: '/schedules', path: '/home',
component: Schedules component: Dashboard
}, },
{ {
title: i18n._(t`My View`), title: i18n._(t`Jobs`),
path: '/portal', path: '/jobs',
component: Portal component: Jobs
}, },
], {
}, title: i18n._(t`Schedules`),
{ path: '/schedules',
groupTitle: i18n._(t`Resources`), component: Schedules
groupId: 'resources_group', },
routes: [ {
{ title: i18n._(t`My View`),
title: i18n._(t`Templates`), path: '/portal',
path: '/templates', component: Portal
component: Templates },
}, ],
{ },
title: i18n._(t`Credentials`), {
path: '/credentials', groupTitle: i18n._(t`Resources`),
component: Credentials groupId: 'resources_group',
}, routes: [
{ {
title: i18n._(t`Projects`), title: i18n._(t`Templates`),
path: '/projects', path: '/templates',
component: Projects component: Templates
}, },
{ {
title: i18n._(t`Inventories`), title: i18n._(t`Credentials`),
path: '/inventories', path: '/credentials',
component: Inventories component: Credentials
}, },
{ {
title: i18n._(t`Inventory Scripts`), title: i18n._(t`Projects`),
path: '/inventory_scripts', path: '/projects',
component: InventoryScripts component: Projects
}, },
], {
}, title: i18n._(t`Inventories`),
{ path: '/inventories',
groupTitle: i18n._(t`Access`), component: Inventories
groupId: 'access_group', },
routes: [ {
{ title: i18n._(t`Inventory Scripts`),
title: i18n._(t`Organizations`), path: '/inventory_scripts',
path: '/organizations', component: InventoryScripts
component: Organizations },
}, ],
{ },
title: i18n._(t`Users`), {
path: '/users', groupTitle: i18n._(t`Access`),
component: Users groupId: 'access_group',
}, routes: [
{ {
title: i18n._(t`Teams`), title: i18n._(t`Organizations`),
path: '/teams', path: '/organizations',
component: Teams component: Organizations
}, },
], {
}, title: i18n._(t`Users`),
{ path: '/users',
groupTitle: i18n._(t`Administration`), component: Users
groupId: 'administration_group', },
routes: [ {
{ title: i18n._(t`Teams`),
title: i18n._(t`Credential Types`), path: '/teams',
path: '/credential_types', component: Teams
component: CredentialTypes },
}, ],
{ },
title: i18n._(t`Notifications`), {
path: '/notification_templates', groupTitle: i18n._(t`Administration`),
component: NotificationTemplates groupId: 'administration_group',
}, routes: [
{ {
title: i18n._(t`Management Jobs`), title: i18n._(t`Credential Types`),
path: '/management_jobs', path: '/credential_types',
component: ManagementJobs component: CredentialTypes
}, },
{ {
title: i18n._(t`Instance Groups`), title: i18n._(t`Notifications`),
path: '/instance_groups', path: '/notification_templates',
component: InstanceGroups component: NotificationTemplates
}, },
{ {
title: i18n._(t`Integrations`), title: i18n._(t`Management Jobs`),
path: '/applications', path: '/management_jobs',
component: Applications component: ManagementJobs
}, },
], {
}, title: i18n._(t`Instance Groups`),
{ path: '/instance_groups',
groupTitle: i18n._(t`Settings`), component: InstanceGroups
groupId: 'settings_group', },
routes: [ {
{ title: i18n._(t`Integrations`),
title: i18n._(t`Authentication`), path: '/applications',
path: '/auth_settings', component: Applications
component: AuthSettings },
}, ],
{ },
title: i18n._(t`Jobs`), {
path: '/jobs_settings', groupTitle: i18n._(t`Settings`),
component: JobsSettings groupId: 'settings_group',
}, routes: [
{ {
title: i18n._(t`System`), title: i18n._(t`Authentication`),
path: '/system_settings', path: '/auth_settings',
component: SystemSettings component: AuthSettings
}, },
{ {
title: i18n._(t`User Interface`), title: i18n._(t`Jobs`),
path: '/ui_settings', path: '/jobs_settings',
component: UISettings component: JobsSettings
}, },
{ {
title: i18n._(t`License`), title: i18n._(t`System`),
path: '/license', path: '/system_settings',
component: License component: SystemSettings
}, },
], {
}, title: i18n._(t`User Interface`),
]} path: '/ui_settings',
render={({ routeGroups }) => ( component: UISettings
routeGroups },
.reduce((allRoutes, { routes }) => allRoutes.concat(routes), []) {
.map(({ component: PageComponent, path }) => ( title: i18n._(t`License`),
<Route path: '/license',
key={path} component: License
path={path} },
render={({ match }) => ( ],
<PageComponent },
api={api} ]}
match={match} render={({ routeGroups }) => (
/> routeGroups
)} .reduce((allRoutes, { routes }) => allRoutes.concat(routes), [])
/> .map(({ component: PageComponent, path }) => (
)) <Route
)} key={path}
/> path={path}
)} render={({ match }) => (
/> <PageComponent match={match} />
</Switch> )}
)} />
))
)}
/>
)}
/>
</Switch>
</Background> </Background>
)} )}
</I18n> </I18n>
</I18nProvider> </RootProvider>
</HashRouter>, el </HashRouter>, el
); );
} }
main(ReactDOM.render, new APIClient(http)); main(ReactDOM.render);