From aea4a04c66b49635eed07b2fe37ea50df03ed7a8 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Mon, 8 Apr 2019 12:19:13 -0400 Subject: [PATCH] add RootDialog and Network contexts, update app bootstrapping --- src/App.jsx | 165 +++++++------- src/RootProvider.jsx | 44 ++++ src/context.jsx | 4 - src/contexts/Config.jsx | 89 ++++++++ src/contexts/Network.jsx | 84 ++++++++ src/contexts/RootDialog.jsx | 53 +++++ src/index.jsx | 420 ++++++++++++++++-------------------- 7 files changed, 544 insertions(+), 315 deletions(-) create mode 100644 src/RootProvider.jsx delete mode 100644 src/context.jsx create mode 100644 src/contexts/Config.jsx create mode 100644 src/contexts/Network.jsx create mode 100644 src/contexts/RootDialog.jsx diff --git a/src/App.jsx b/src/App.jsx index e4fdd54243..7e4699e33d 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -6,13 +6,20 @@ import { Page, PageHeader, PageSidebar, + Button } 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 NavExpandableGroup from './components/NavExpandableGroup'; import TowerLogo from './components/TowerLogo'; import PageHeaderToolbar from './components/PageHeaderToolbar'; -import { ConfigContext } from './context'; class App extends Component { constructor (props) { @@ -23,29 +30,24 @@ class App extends Component { && window.innerWidth >= parseInt(global_breakpoint_md.value, 10); this.state = { - ansible_version: null, - custom_virtualenvs: null, isAboutModalOpen: false, - isNavOpen, - version: null, + isNavOpen }; - this.fetchConfig = this.fetchConfig.bind(this); this.onLogout = this.onLogout.bind(this); this.onAboutModalClose = this.onAboutModalClose.bind(this); this.onAboutModalOpen = this.onAboutModalOpen.bind(this); this.onNavToggle = this.onNavToggle.bind(this); } - componentDidMount () { - this.fetchConfig(); - } - async onLogout () { - const { api } = this.props; - - await api.logout(); - window.location.replace('/#/login'); + const { api, handleHttpError } = this.props; + try { + await api.logout(); + window.location.replace('/#/login'); + } catch (err) { + handleHttpError(err); + } } onAboutModalOpen () { @@ -60,24 +62,12 @@ class App extends Component { 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 () { const { ansible_version, - custom_virtualenvs, isAboutModalOpen, isNavOpen, - version, + version } = this.state; const { @@ -86,63 +76,76 @@ class App extends Component { navLabel = '', } = this.props; - const config = { - ansible_version, - custom_virtualenvs, - version, - }; - return ( - - } - toolbar={( - + {({ i18n }) => ( + + {({ title, bodyText, variant = 'info', clearRootDialogMessage }) => ( + + {(title || bodyText) && ( + {i18n._(t`Close`)} + ]} + > + {bodyText} + + )} + } + toolbar={( + + )} + /> + )} + sidebar={( + + + {routeGroups.map(({ groupId, groupTitle, routes }) => ( + + ))} + + + )} + /> + )} + > + {render && render({ routeGroups })} + + - )} - /> - )} - sidebar={( - - - {routeGroups.map(({ groupId, groupTitle, routes }) => ( - - ))} - - - )} - /> - )} - > - - {render && render({ routeGroups })} - - - - + + )} + + )} + ); } } -export default App; +export default withNetwork(App); diff --git a/src/RootProvider.jsx b/src/RootProvider.jsx new file mode 100644 index 0000000000..7a868e63a3 --- /dev/null +++ b/src/RootProvider.jsx @@ -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 ( + + + + + {children} + + + + + ); + } +} + +export default RootProvider; diff --git a/src/context.jsx b/src/context.jsx deleted file mode 100644 index ab413dd313..0000000000 --- a/src/context.jsx +++ /dev/null @@ -1,4 +0,0 @@ -import React from 'react'; - -// eslint-disable-next-line import/prefer-default-export -export const ConfigContext = React.createContext({}); diff --git a/src/contexts/Config.jsx b/src/contexts/Config.jsx new file mode 100644 index 0000000000..6129f80413 --- /dev/null +++ b/src/contexts/Config.jsx @@ -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 ( + + {children} + + ); + } +} + +export const ConfigProvider = withNetwork(provider); + +export const Config = ({ children }) => ( + + {value => children(value)} + +); diff --git a/src/contexts/Network.jsx b/src/contexts/Network.jsx new file mode 100644 index 0000000000..c00d5b2895 --- /dev/null +++ b/src/contexts/Network.jsx @@ -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 ( + + {children} + + ); + } +} + +export const NetworkProvider = withRootDialog(withRouter(prov)); + +export function withNetwork (Child) { + return (props) => ( + + {context => } + + ); +} + diff --git a/src/contexts/RootDialog.jsx b/src/contexts/RootDialog.jsx new file mode 100644 index 0000000000..089c59087f --- /dev/null +++ b/src/contexts/RootDialog.jsx @@ -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 ( + + {children} + + ); + } +} + +export const RootDialog = ({ children }) => ( + + {value => children(value)} + +); + +export function withRootDialog (Child) { + return (props) => ( + + {context => } + + ); +} diff --git a/src/index.jsx b/src/index.jsx index d3dd6407e6..9f542327a5 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -1,15 +1,13 @@ -import axios from 'axios'; import React from 'react'; import ReactDOM from 'react-dom'; import { HashRouter, - Redirect, Route, Switch, + Redirect } from 'react-router-dom'; import { - I18n, - I18nProvider, + I18n } from '@lingui/react'; import { t } from '@lingui/macro'; @@ -19,10 +17,13 @@ import './components/Pagination/styles.scss'; import './components/DataListToolbar/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 RootProvider from './RootProvider'; +import App from './App'; + import Applications from './pages/Applications'; import Credentials from './pages/Credentials'; import CredentialTypes from './pages/CredentialTypes'; @@ -46,242 +47,201 @@ import License from './pages/License'; import Teams from './pages/Teams'; import Templates from './pages/Templates'; 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 { data: { custom_logo, custom_login_info } } = await api.getRoot(); - - const defaultRedirect = () => (); - const loginRoutes = ( - - ( - - )} - /> - - - ); return render( - + {({ i18n }) => ( - {!api.isAuthenticated() ? loginRoutes : ( - - - - ( - ( - routeGroups - .reduce((allRoutes, { routes }) => allRoutes.concat(routes), []) - .map(({ component: PageComponent, path }) => ( - ( - - )} - /> - )) - )} - /> - )} - /> - - )} + + ( + + {({ custom_logo, custom_login_info }) => ( + + )} + + )} + /> + } /> + ( + ( + routeGroups + .reduce((allRoutes, { routes }) => allRoutes.concat(routes), []) + .map(({ component: PageComponent, path }) => ( + ( + + )} + /> + )) + )} + /> + )} + /> + )} - + , el ); } -main(ReactDOM.render, new APIClient(http)); +main(ReactDOM.render);