Skip to content

Commit

Permalink
Show banner on network lost
Browse files Browse the repository at this point in the history
  • Loading branch information
jmattheis committed Mar 25, 2019
1 parent 11fcdb2 commit 727a515
Show file tree
Hide file tree
Showing 8 changed files with 152 additions and 68 deletions.
17 changes: 11 additions & 6 deletions ui/src/CurrentUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {}

Expand Down Expand Up @@ -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);
Expand Down
28 changes: 28 additions & 0 deletions ui/src/common/NetworkLostBanner.tsx
Original file line number Diff line number Diff line change
@@ -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>
);
};
23 changes: 3 additions & 20 deletions ui/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@ 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';
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/',
Expand Down Expand Up @@ -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(() => {});

Expand Down
7 changes: 4 additions & 3 deletions ui/src/layout/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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}>
Expand Down
101 changes: 64 additions & 37 deletions ui/src/layout/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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
Expand All @@ -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) {
Expand All @@ -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 {
Expand All @@ -84,49 +102,56 @@ class Layout extends React.Component<WithStyles<'content'> & Stores<'currentUser
authenticating,
user: {name, admin},
logout,
hasNetwork,
},
} = this.props;
const theme = themeMap[currentTheme];
const loginRoute = () => (loggedIn ? <Redirect to="/" /> : <Login />);
return (
<MuiThemeProvider theme={theme}>
<HashRouter>
<div style={{display: 'flex'}}>
<CssBaseline />
<Header
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)} />
<div>
{hasNetwork ? null : (
<NetworkLostBanner height={64} retry={this.doReconnect} />
)}
<ScrollUpButton />
<SnackBarHandler />
<div style={{display: 'flex'}}>
<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>
</HashRouter>
</MuiThemeProvider>
Expand All @@ -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)
);
2 changes: 0 additions & 2 deletions ui/src/message/WebSocketStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.');
}
});
};
Expand Down
41 changes: 41 additions & 0 deletions ui/src/reactions.ts
Original file line number Diff line number Diff line change
@@ -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();
}
}
);
};
1 change: 1 addition & 0 deletions ui/src/user/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 727a515

Please sign in to comment.