diff --git a/ui/src/CurrentUser.ts b/ui/src/CurrentUser.ts index 82b901d..683f9fd 100644 --- a/ui/src/CurrentUser.ts +++ b/ui/src/CurrentUser.ts @@ -15,6 +15,8 @@ export class CurrentUser { public authenticating = false; @observable public user: IUser = {name: 'unknown', admin: false, id: -1}; + @observable + public hasNetwork = true; public constructor(private readonly snack: SnackReporter) {} @@ -82,15 +84,18 @@ export class CurrentUser { .then((passThrough) => { this.user = passThrough.data; this.loggedIn = true; + this.hasNetwork = true; return passThrough; }) .catch((error: AxiosError) => { - if ( - error && - error.response && - error.response.status >= 400 && - error.response.status < 500 - ) { + if (!error || !error.response) { + this.hasNetwork = false; + return Promise.reject(error); + } + + this.hasNetwork = true; + + if (error.response.status >= 400 && error.response.status < 500) { this.logout(); } return Promise.reject(error); diff --git a/ui/src/common/NetworkLostBanner.tsx b/ui/src/common/NetworkLostBanner.tsx new file mode 100644 index 0000000..a6cad16 --- /dev/null +++ b/ui/src/common/NetworkLostBanner.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import Button from '@material-ui/core/Button'; +import Typography from '@material-ui/core/Typography'; + +interface NetworkLostBannerProps { + height: number; + retry: () => void; +} + +export const NetworkLostBanner = ({height, retry}: NetworkLostBannerProps) => { + return ( +
+ + No network connection.{' '} + + +
+ ); +}; diff --git a/ui/src/index.tsx b/ui/src/index.tsx index f613113..2d22cfd 100644 --- a/ui/src/index.tsx +++ b/ui/src/index.tsx @@ -5,10 +5,8 @@ import {initAxios} from './apiAuth'; import * as config from './config'; import Layout from './layout/Layout'; import registerServiceWorker from './registerServiceWorker'; -import * as Notifications from './snack/browserNotification'; import {CurrentUser} from './CurrentUser'; import {AppStore} from './application/AppStore'; -import {reaction} from 'mobx'; import {WebSocketStore} from './message/WebSocketStore'; import {SnackManager} from './snack/SnackManager'; import {InjectProvider, StoreMapping} from './inject'; @@ -16,6 +14,8 @@ import {UserStore} from './user/UserStore'; import {MessagesStore} from './message/MessagesStore'; import {ClientStore} from './client/ClientStore'; import {PluginStore} from './plugin/PluginStore'; +import * as Notifications from './snack/browserNotification'; +import {registerReactions} from './reactions'; const defaultDevConfig = { url: 'http://localhost:80/', @@ -71,24 +71,7 @@ const initStores = (): StoreMapping => { const stores = initStores(); initAxios(stores.currentUser, stores.snackManager.snack); - reaction( - () => stores.currentUser.loggedIn, - (loggedIn) => { - if (loggedIn) { - stores.wsStore.listen((message) => { - stores.messagesStore.publishSingleMessage(message); - Notifications.notifyNewMessage(message); - }); - stores.appStore.refresh(); - } else { - stores.messagesStore.clearAll(); - stores.appStore.clear(); - stores.clientStore.clear(); - stores.userStore.clear(); - stores.wsStore.close(); - } - } - ); + registerReactions(stores); stores.currentUser.tryAuthenticate().catch(() => {}); diff --git a/ui/src/layout/Header.tsx b/ui/src/layout/Header.tsx index 54f5f55..db34f89 100644 --- a/ui/src/layout/Header.tsx +++ b/ui/src/layout/Header.tsx @@ -11,7 +11,7 @@ import ExitToApp from '@material-ui/icons/ExitToApp'; import Highlight from '@material-ui/icons/Highlight'; import Apps from '@material-ui/icons/Apps'; import SupervisorAccount from '@material-ui/icons/SupervisorAccount'; -import React, {Component} from 'react'; +import React, {Component, CSSProperties} from 'react'; import {Link} from 'react-router-dom'; import {observer} from 'mobx-react'; @@ -43,15 +43,16 @@ interface IProps { toggleTheme: VoidFunction; showSettings: VoidFunction; logout: VoidFunction; + style: CSSProperties; } @observer class Header extends Component { public render() { - const {classes, version, name, loggedIn, admin, toggleTheme, logout} = this.props; + const {classes, version, name, loggedIn, admin, toggleTheme, logout, style} = this.props; return ( - +
diff --git a/ui/src/layout/Layout.tsx b/ui/src/layout/Layout.tsx index b26c29b..9c50646 100644 --- a/ui/src/layout/Layout.tsx +++ b/ui/src/layout/Layout.tsx @@ -20,6 +20,7 @@ import Users from '../user/Users'; import {observer} from 'mobx-react'; import {observable} from 'mobx'; import {inject, Stores} from '../inject'; +import {NetworkLostBanner} from '../common/NetworkLostBanner'; const styles = (theme: Theme) => ({ content: { @@ -50,7 +51,9 @@ const isThemeKey = (value: string | null): value is ThemeKey => { }; @observer -class Layout extends React.Component & Stores<'currentUser'>> { +class Layout extends React.Component< + WithStyles<'content'> & Stores<'currentUser' | 'snackManager'> +> { private static defaultVersion = '0.0.0'; @observable @@ -59,6 +62,8 @@ class Layout extends React.Component & Stores<'currentUser private showSettings = false; @observable private version = Layout.defaultVersion; + @observable + private reconnecting = false; public componentDidMount() { if (this.version === Layout.defaultVersion) { @@ -75,6 +80,19 @@ class Layout extends React.Component & Stores<'currentUser } } + private doReconnect = () => { + this.reconnecting = true; + this.props.currentUser + .tryAuthenticate() + .then(() => { + this.reconnecting = false; + }) + .catch(() => { + this.reconnecting = false; + this.props.snackManager.snack('Reconnect failed'); + }); + }; + public render() { const {version, showSettings, currentTheme} = this; const { @@ -84,6 +102,7 @@ class Layout extends React.Component & Stores<'currentUser authenticating, user: {name, admin}, logout, + hasNetwork, }, } = this.props; const theme = themeMap[currentTheme]; @@ -91,42 +110,48 @@ class Layout extends React.Component & Stores<'currentUser return ( -
- -
(this.showSettings = true)} - logout={logout} - /> - - -
- - {authenticating ? ( - - - - ) : null} - - {loggedIn ? null : } - - - - - - - - -
- {showSettings && ( - (this.showSettings = false)} /> +
+ {hasNetwork ? null : ( + )} - - +
+ +
(this.showSettings = true)} + logout={logout} + /> + + +
+ + {authenticating || this.reconnecting ? ( + + + + ) : null} + + {loggedIn ? null : } + + + + + + + + +
+ {showSettings && ( + (this.showSettings = false)} /> + )} + + +
@@ -139,4 +164,6 @@ class Layout extends React.Component & Stores<'currentUser } } -export default withStyles(styles, {withTheme: true})<{}>(inject('currentUser')(Layout)); +export default withStyles(styles, {withTheme: true})<{}>( + inject('currentUser', 'snackManager')(Layout) +); diff --git a/ui/src/message/WebSocketStore.ts b/ui/src/message/WebSocketStore.ts index 215e219..f7747f2 100644 --- a/ui/src/message/WebSocketStore.ts +++ b/ui/src/message/WebSocketStore.ts @@ -42,8 +42,6 @@ export class WebSocketStore { .catch((error: AxiosError) => { if (error && error.response && error.response.status === 401) { this.snack('Could not authenticate with client token, logging out.'); - } else { - this.snack('Lost network connection, please refresh the page.'); } }); }; diff --git a/ui/src/reactions.ts b/ui/src/reactions.ts new file mode 100644 index 0000000..7be1a8b --- /dev/null +++ b/ui/src/reactions.ts @@ -0,0 +1,41 @@ +import {StoreMapping} from './inject'; +import {reaction} from 'mobx'; +import * as Notifications from './snack/browserNotification'; + +export const registerReactions = (stores: StoreMapping) => { + const clearAll = () => { + stores.messagesStore.clearAll(); + stores.appStore.clear(); + stores.clientStore.clear(); + stores.userStore.clear(); + stores.wsStore.close(); + }; + const loadAll = () => { + stores.wsStore.listen((message) => { + stores.messagesStore.publishSingleMessage(message); + Notifications.notifyNewMessage(message); + }); + stores.appStore.refresh(); + }; + + reaction( + () => stores.currentUser.loggedIn, + (loggedIn) => { + if (loggedIn) { + loadAll(); + } else { + clearAll(); + } + } + ); + + reaction( + () => stores.currentUser.hasNetwork, + (hasNetwork) => { + if (hasNetwork) { + clearAll(); + loadAll(); + } + } + ); +}; diff --git a/ui/src/user/Login.tsx b/ui/src/user/Login.tsx index 825ed14..57f9273 100644 --- a/ui/src/user/Login.tsx +++ b/ui/src/user/Login.tsx @@ -44,6 +44,7 @@ class Login extends Component> { size="large" className="login" color="primary" + disabled={!this.props.currentUser.hasNetwork} style={{marginTop: 15, marginBottom: 5}} onClick={this.login}> Login