Show banner on network lost

This commit is contained in:
Jannis Mattheis 2019-03-25 17:49:34 +01:00
parent b66d58c372
commit 62854d8e11
8 changed files with 152 additions and 68 deletions

View File

@ -15,6 +15,8 @@ export class CurrentUser {
public authenticating = false; public authenticating = false;
@observable @observable
public user: IUser = {name: 'unknown', admin: false, id: -1}; public user: IUser = {name: 'unknown', admin: false, id: -1};
@observable
public hasNetwork = true;
public constructor(private readonly snack: SnackReporter) {} public constructor(private readonly snack: SnackReporter) {}
@ -82,15 +84,18 @@ export class CurrentUser {
.then((passThrough) => { .then((passThrough) => {
this.user = passThrough.data; this.user = passThrough.data;
this.loggedIn = true; this.loggedIn = true;
this.hasNetwork = true;
return passThrough; return passThrough;
}) })
.catch((error: AxiosError) => { .catch((error: AxiosError) => {
if ( if (!error || !error.response) {
error && this.hasNetwork = false;
error.response && return Promise.reject(error);
error.response.status >= 400 && }
error.response.status < 500
) { this.hasNetwork = true;
if (error.response.status >= 400 && error.response.status < 500) {
this.logout(); this.logout();
} }
return Promise.reject(error); return Promise.reject(error);

View File

@ -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 (
<div
style={{
backgroundColor: '#e74c3c',
height,
width: '100%',
zIndex: 1300,
position: 'relative',
}}>
<Typography align="center" variant="title" style={{lineHeight: `${height}px`}}>
No network connection.{' '}
<Button variant="outlined" onClick={retry}>
Retry
</Button>
</Typography>
</div>
);
};

View File

@ -5,10 +5,8 @@ import {initAxios} from './apiAuth';
import * as config from './config'; import * as config from './config';
import Layout from './layout/Layout'; import Layout from './layout/Layout';
import registerServiceWorker from './registerServiceWorker'; import registerServiceWorker from './registerServiceWorker';
import * as Notifications from './snack/browserNotification';
import {CurrentUser} from './CurrentUser'; import {CurrentUser} from './CurrentUser';
import {AppStore} from './application/AppStore'; import {AppStore} from './application/AppStore';
import {reaction} from 'mobx';
import {WebSocketStore} from './message/WebSocketStore'; import {WebSocketStore} from './message/WebSocketStore';
import {SnackManager} from './snack/SnackManager'; import {SnackManager} from './snack/SnackManager';
import {InjectProvider, StoreMapping} from './inject'; import {InjectProvider, StoreMapping} from './inject';
@ -16,6 +14,8 @@ import {UserStore} from './user/UserStore';
import {MessagesStore} from './message/MessagesStore'; import {MessagesStore} from './message/MessagesStore';
import {ClientStore} from './client/ClientStore'; import {ClientStore} from './client/ClientStore';
import {PluginStore} from './plugin/PluginStore'; import {PluginStore} from './plugin/PluginStore';
import * as Notifications from './snack/browserNotification';
import {registerReactions} from './reactions';
const defaultDevConfig = { const defaultDevConfig = {
url: 'http://localhost:80/', url: 'http://localhost:80/',
@ -71,24 +71,7 @@ const initStores = (): StoreMapping => {
const stores = initStores(); const stores = initStores();
initAxios(stores.currentUser, stores.snackManager.snack); initAxios(stores.currentUser, stores.snackManager.snack);
reaction( registerReactions(stores);
() => 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();
}
}
);
stores.currentUser.tryAuthenticate().catch(() => {}); stores.currentUser.tryAuthenticate().catch(() => {});

View File

@ -11,7 +11,7 @@ import ExitToApp from '@material-ui/icons/ExitToApp';
import Highlight from '@material-ui/icons/Highlight'; import Highlight from '@material-ui/icons/Highlight';
import Apps from '@material-ui/icons/Apps'; import Apps from '@material-ui/icons/Apps';
import SupervisorAccount from '@material-ui/icons/SupervisorAccount'; 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 {Link} from 'react-router-dom';
import {observer} from 'mobx-react'; import {observer} from 'mobx-react';
@ -43,15 +43,16 @@ interface IProps {
toggleTheme: VoidFunction; toggleTheme: VoidFunction;
showSettings: VoidFunction; showSettings: VoidFunction;
logout: VoidFunction; logout: VoidFunction;
style: CSSProperties;
} }
@observer @observer
class Header extends Component<IProps & Styles> { class Header extends Component<IProps & Styles> {
public render() { public render() {
const {classes, version, name, loggedIn, admin, toggleTheme, logout} = this.props; const {classes, version, name, loggedIn, admin, toggleTheme, logout, style} = this.props;
return ( return (
<AppBar position="absolute" className={classes.appBar}> <AppBar position="absolute" style={style} className={classes.appBar}>
<Toolbar> <Toolbar>
<div className={classes.title}> <div className={classes.title}>
<a href="https://github.com/gotify/server" className={classes.link}> <a href="https://github.com/gotify/server" className={classes.link}>

View File

@ -20,6 +20,7 @@ import Users from '../user/Users';
import {observer} from 'mobx-react'; import {observer} from 'mobx-react';
import {observable} from 'mobx'; import {observable} from 'mobx';
import {inject, Stores} from '../inject'; import {inject, Stores} from '../inject';
import {NetworkLostBanner} from '../common/NetworkLostBanner';
const styles = (theme: Theme) => ({ const styles = (theme: Theme) => ({
content: { content: {
@ -50,7 +51,9 @@ const isThemeKey = (value: string | null): value is ThemeKey => {
}; };
@observer @observer
class Layout extends React.Component<WithStyles<'content'> & Stores<'currentUser'>> { class Layout extends React.Component<
WithStyles<'content'> & Stores<'currentUser' | 'snackManager'>
> {
private static defaultVersion = '0.0.0'; private static defaultVersion = '0.0.0';
@observable @observable
@ -59,6 +62,8 @@ class Layout extends React.Component<WithStyles<'content'> & Stores<'currentUser
private showSettings = false; private showSettings = false;
@observable @observable
private version = Layout.defaultVersion; private version = Layout.defaultVersion;
@observable
private reconnecting = false;
public componentDidMount() { public componentDidMount() {
if (this.version === Layout.defaultVersion) { if (this.version === Layout.defaultVersion) {
@ -75,6 +80,19 @@ class Layout extends React.Component<WithStyles<'content'> & 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() { public render() {
const {version, showSettings, currentTheme} = this; const {version, showSettings, currentTheme} = this;
const { const {
@ -84,6 +102,7 @@ class Layout extends React.Component<WithStyles<'content'> & Stores<'currentUser
authenticating, authenticating,
user: {name, admin}, user: {name, admin},
logout, logout,
hasNetwork,
}, },
} = this.props; } = this.props;
const theme = themeMap[currentTheme]; const theme = themeMap[currentTheme];
@ -91,9 +110,14 @@ class Layout extends React.Component<WithStyles<'content'> & Stores<'currentUser
return ( return (
<MuiThemeProvider theme={theme}> <MuiThemeProvider theme={theme}>
<HashRouter> <HashRouter>
<div>
{hasNetwork ? null : (
<NetworkLostBanner height={64} retry={this.doReconnect} />
)}
<div style={{display: 'flex'}}> <div style={{display: 'flex'}}>
<CssBaseline /> <CssBaseline />
<Header <Header
style={{top: hasNetwork ? 0 : 64}}
admin={admin} admin={admin}
name={name} name={name}
version={version} version={version}
@ -106,7 +130,7 @@ class Layout extends React.Component<WithStyles<'content'> & Stores<'currentUser
<main className={classes.content}> <main className={classes.content}>
<Switch> <Switch>
{authenticating ? ( {authenticating || this.reconnecting ? (
<Route path="/"> <Route path="/">
<LoadingSpinner /> <LoadingSpinner />
</Route> </Route>
@ -128,6 +152,7 @@ class Layout extends React.Component<WithStyles<'content'> & Stores<'currentUser
<ScrollUpButton /> <ScrollUpButton />
<SnackBarHandler /> <SnackBarHandler />
</div> </div>
</div>
</HashRouter> </HashRouter>
</MuiThemeProvider> </MuiThemeProvider>
); );
@ -139,4 +164,6 @@ class Layout extends React.Component<WithStyles<'content'> & Stores<'currentUser
} }
} }
export default withStyles(styles, {withTheme: true})<{}>(inject('currentUser')(Layout)); export default withStyles(styles, {withTheme: true})<{}>(
inject('currentUser', 'snackManager')(Layout)
);

View File

@ -42,8 +42,6 @@ export class WebSocketStore {
.catch((error: AxiosError) => { .catch((error: AxiosError) => {
if (error && error.response && error.response.status === 401) { if (error && error.response && error.response.status === 401) {
this.snack('Could not authenticate with client token, logging out.'); this.snack('Could not authenticate with client token, logging out.');
} else {
this.snack('Lost network connection, please refresh the page.');
} }
}); });
}; };

41
ui/src/reactions.ts Normal file
View File

@ -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();
}
}
);
};

View File

@ -44,6 +44,7 @@ class Login extends Component<Stores<'currentUser'>> {
size="large" size="large"
className="login" className="login"
color="primary" color="primary"
disabled={!this.props.currentUser.hasNetwork}
style={{marginTop: 15, marginBottom: 5}} style={{marginTop: 15, marginBottom: 5}}
onClick={this.login}> onClick={this.login}>
Login Login