Show banner on network lost
This commit is contained in:
parent
b66d58c372
commit
62854d8e11
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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(() => {});
|
||||
|
||||
|
|
|
|||
|
|
@ -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<IProps & Styles> {
|
||||
public render() {
|
||||
const {classes, version, name, loggedIn, admin, toggleTheme, logout} = this.props;
|
||||
const {classes, version, name, loggedIn, admin, toggleTheme, logout, style} = this.props;
|
||||
|
||||
return (
|
||||
<AppBar position="absolute" className={classes.appBar}>
|
||||
<AppBar position="absolute" style={style} className={classes.appBar}>
|
||||
<Toolbar>
|
||||
<div className={classes.title}>
|
||||
<a href="https://github.com/gotify/server" className={classes.link}>
|
||||
|
|
|
|||
|
|
@ -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<WithStyles<'content'> & 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<WithStyles<'content'> & 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<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() {
|
||||
const {version, showSettings, currentTheme} = this;
|
||||
const {
|
||||
|
|
@ -84,6 +102,7 @@ class Layout extends React.Component<WithStyles<'content'> & Stores<'currentUser
|
|||
authenticating,
|
||||
user: {name, admin},
|
||||
logout,
|
||||
hasNetwork,
|
||||
},
|
||||
} = this.props;
|
||||
const theme = themeMap[currentTheme];
|
||||
|
|
@ -91,9 +110,14 @@ class Layout extends React.Component<WithStyles<'content'> & Stores<'currentUser
|
|||
return (
|
||||
<MuiThemeProvider theme={theme}>
|
||||
<HashRouter>
|
||||
<div>
|
||||
{hasNetwork ? null : (
|
||||
<NetworkLostBanner height={64} retry={this.doReconnect} />
|
||||
)}
|
||||
<div style={{display: 'flex'}}>
|
||||
<CssBaseline />
|
||||
<Header
|
||||
style={{top: hasNetwork ? 0 : 64}}
|
||||
admin={admin}
|
||||
name={name}
|
||||
version={version}
|
||||
|
|
@ -106,7 +130,7 @@ class Layout extends React.Component<WithStyles<'content'> & Stores<'currentUser
|
|||
|
||||
<main className={classes.content}>
|
||||
<Switch>
|
||||
{authenticating ? (
|
||||
{authenticating || this.reconnecting ? (
|
||||
<Route path="/">
|
||||
<LoadingSpinner />
|
||||
</Route>
|
||||
|
|
@ -128,6 +152,7 @@ class Layout extends React.Component<WithStyles<'content'> & Stores<'currentUser
|
|||
<ScrollUpButton />
|
||||
<SnackBarHandler />
|
||||
</div>
|
||||
</div>
|
||||
</HashRouter>
|
||||
</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)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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.');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
@ -44,6 +44,7 @@ class Login extends Component<Stores<'currentUser'>> {
|
|||
size="large"
|
||||
className="login"
|
||||
color="primary"
|
||||
disabled={!this.props.currentUser.hasNetwork}
|
||||
style={{marginTop: 15, marginBottom: 5}}
|
||||
onClick={this.login}>
|
||||
Login
|
||||
|
|
|
|||
Loading…
Reference in New Issue