Show banner on network lost
This commit is contained in:
parent
b66d58c372
commit
62854d8e11
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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 * 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(() => {});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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}>
|
||||||
|
|
|
||||||
|
|
@ -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,42 +110,48 @@ class Layout extends React.Component<WithStyles<'content'> & Stores<'currentUser
|
||||||
return (
|
return (
|
||||||
<MuiThemeProvider theme={theme}>
|
<MuiThemeProvider theme={theme}>
|
||||||
<HashRouter>
|
<HashRouter>
|
||||||
<div style={{display: 'flex'}}>
|
<div>
|
||||||
<CssBaseline />
|
{hasNetwork ? null : (
|
||||||
<Header
|
<NetworkLostBanner height={64} retry={this.doReconnect} />
|
||||||
admin={admin}
|
|
||||||
name={name}
|
|
||||||
version={version}
|
|
||||||
loggedIn={loggedIn}
|
|
||||||
toggleTheme={this.toggleTheme.bind(this)}
|
|
||||||
showSettings={() => (this.showSettings = true)}
|
|
||||||
logout={logout}
|
|
||||||
/>
|
|
||||||
<Navigation loggedIn={loggedIn} />
|
|
||||||
|
|
||||||
<main className={classes.content}>
|
|
||||||
<Switch>
|
|
||||||
{authenticating ? (
|
|
||||||
<Route path="/">
|
|
||||||
<LoadingSpinner />
|
|
||||||
</Route>
|
|
||||||
) : null}
|
|
||||||
<Route exact path="/login" render={loginRoute} />
|
|
||||||
{loggedIn ? null : <Redirect to="/login" />}
|
|
||||||
<Route exact path="/" component={Messages} />
|
|
||||||
<Route exact path="/messages/:id" component={Messages} />
|
|
||||||
<Route exact path="/applications" component={Applications} />
|
|
||||||
<Route exact path="/clients" component={Clients} />
|
|
||||||
<Route exact path="/users" component={Users} />
|
|
||||||
<Route exact path="/plugins" component={Plugins} />
|
|
||||||
<Route exact path="/plugins/:id" component={PluginDetailView} />
|
|
||||||
</Switch>
|
|
||||||
</main>
|
|
||||||
{showSettings && (
|
|
||||||
<SettingsDialog fClose={() => (this.showSettings = false)} />
|
|
||||||
)}
|
)}
|
||||||
<ScrollUpButton />
|
<div style={{display: 'flex'}}>
|
||||||
<SnackBarHandler />
|
<CssBaseline />
|
||||||
|
<Header
|
||||||
|
style={{top: hasNetwork ? 0 : 64}}
|
||||||
|
admin={admin}
|
||||||
|
name={name}
|
||||||
|
version={version}
|
||||||
|
loggedIn={loggedIn}
|
||||||
|
toggleTheme={this.toggleTheme.bind(this)}
|
||||||
|
showSettings={() => (this.showSettings = true)}
|
||||||
|
logout={logout}
|
||||||
|
/>
|
||||||
|
<Navigation loggedIn={loggedIn} />
|
||||||
|
|
||||||
|
<main className={classes.content}>
|
||||||
|
<Switch>
|
||||||
|
{authenticating || this.reconnecting ? (
|
||||||
|
<Route path="/">
|
||||||
|
<LoadingSpinner />
|
||||||
|
</Route>
|
||||||
|
) : null}
|
||||||
|
<Route exact path="/login" render={loginRoute} />
|
||||||
|
{loggedIn ? null : <Redirect to="/login" />}
|
||||||
|
<Route exact path="/" component={Messages} />
|
||||||
|
<Route exact path="/messages/:id" component={Messages} />
|
||||||
|
<Route exact path="/applications" component={Applications} />
|
||||||
|
<Route exact path="/clients" component={Clients} />
|
||||||
|
<Route exact path="/users" component={Users} />
|
||||||
|
<Route exact path="/plugins" component={Plugins} />
|
||||||
|
<Route exact path="/plugins/:id" component={PluginDetailView} />
|
||||||
|
</Switch>
|
||||||
|
</main>
|
||||||
|
{showSettings && (
|
||||||
|
<SettingsDialog fClose={() => (this.showSettings = false)} />
|
||||||
|
)}
|
||||||
|
<ScrollUpButton />
|
||||||
|
<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)
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -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.');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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"
|
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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue