Skip to content

Commit 62854d8

Browse files
committed
Show banner on network lost
1 parent b66d58c commit 62854d8

File tree

8 files changed

+152
-68
lines changed

8 files changed

+152
-68
lines changed

ui/src/CurrentUser.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ export class CurrentUser {
1515
public authenticating = false;
1616
@observable
1717
public user: IUser = {name: 'unknown', admin: false, id: -1};
18+
@observable
19+
public hasNetwork = true;
1820

1921
public constructor(private readonly snack: SnackReporter) {}
2022

@@ -82,15 +84,18 @@ export class CurrentUser {
8284
.then((passThrough) => {
8385
this.user = passThrough.data;
8486
this.loggedIn = true;
87+
this.hasNetwork = true;
8588
return passThrough;
8689
})
8790
.catch((error: AxiosError) => {
88-
if (
89-
error &&
90-
error.response &&
91-
error.response.status >= 400 &&
92-
error.response.status < 500
93-
) {
91+
if (!error || !error.response) {
92+
this.hasNetwork = false;
93+
return Promise.reject(error);
94+
}
95+
96+
this.hasNetwork = true;
97+
98+
if (error.response.status >= 400 && error.response.status < 500) {
9499
this.logout();
95100
}
96101
return Promise.reject(error);
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import React from 'react';
2+
import Button from '@material-ui/core/Button';
3+
import Typography from '@material-ui/core/Typography';
4+
5+
interface NetworkLostBannerProps {
6+
height: number;
7+
retry: () => void;
8+
}
9+
10+
export const NetworkLostBanner = ({height, retry}: NetworkLostBannerProps) => {
11+
return (
12+
<div
13+
style={{
14+
backgroundColor: '#e74c3c',
15+
height,
16+
width: '100%',
17+
zIndex: 1300,
18+
position: 'relative',
19+
}}>
20+
<Typography align="center" variant="title" style={{lineHeight: `${height}px`}}>
21+
No network connection.{' '}
22+
<Button variant="outlined" onClick={retry}>
23+
Retry
24+
</Button>
25+
</Typography>
26+
</div>
27+
);
28+
};

ui/src/index.tsx

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,17 @@ import {initAxios} from './apiAuth';
55
import * as config from './config';
66
import Layout from './layout/Layout';
77
import registerServiceWorker from './registerServiceWorker';
8-
import * as Notifications from './snack/browserNotification';
98
import {CurrentUser} from './CurrentUser';
109
import {AppStore} from './application/AppStore';
11-
import {reaction} from 'mobx';
1210
import {WebSocketStore} from './message/WebSocketStore';
1311
import {SnackManager} from './snack/SnackManager';
1412
import {InjectProvider, StoreMapping} from './inject';
1513
import {UserStore} from './user/UserStore';
1614
import {MessagesStore} from './message/MessagesStore';
1715
import {ClientStore} from './client/ClientStore';
1816
import {PluginStore} from './plugin/PluginStore';
17+
import * as Notifications from './snack/browserNotification';
18+
import {registerReactions} from './reactions';
1919

2020
const defaultDevConfig = {
2121
url: 'http://localhost:80/',
@@ -71,24 +71,7 @@ const initStores = (): StoreMapping => {
7171
const stores = initStores();
7272
initAxios(stores.currentUser, stores.snackManager.snack);
7373

74-
reaction(
75-
() => stores.currentUser.loggedIn,
76-
(loggedIn) => {
77-
if (loggedIn) {
78-
stores.wsStore.listen((message) => {
79-
stores.messagesStore.publishSingleMessage(message);
80-
Notifications.notifyNewMessage(message);
81-
});
82-
stores.appStore.refresh();
83-
} else {
84-
stores.messagesStore.clearAll();
85-
stores.appStore.clear();
86-
stores.clientStore.clear();
87-
stores.userStore.clear();
88-
stores.wsStore.close();
89-
}
90-
}
91-
);
74+
registerReactions(stores);
9275

9376
stores.currentUser.tryAuthenticate().catch(() => {});
9477

ui/src/layout/Header.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import ExitToApp from '@material-ui/icons/ExitToApp';
1111
import Highlight from '@material-ui/icons/Highlight';
1212
import Apps from '@material-ui/icons/Apps';
1313
import SupervisorAccount from '@material-ui/icons/SupervisorAccount';
14-
import React, {Component} from 'react';
14+
import React, {Component, CSSProperties} from 'react';
1515
import {Link} from 'react-router-dom';
1616
import {observer} from 'mobx-react';
1717

@@ -43,15 +43,16 @@ interface IProps {
4343
toggleTheme: VoidFunction;
4444
showSettings: VoidFunction;
4545
logout: VoidFunction;
46+
style: CSSProperties;
4647
}
4748

4849
@observer
4950
class Header extends Component<IProps & Styles> {
5051
public render() {
51-
const {classes, version, name, loggedIn, admin, toggleTheme, logout} = this.props;
52+
const {classes, version, name, loggedIn, admin, toggleTheme, logout, style} = this.props;
5253

5354
return (
54-
<AppBar position="absolute" className={classes.appBar}>
55+
<AppBar position="absolute" style={style} className={classes.appBar}>
5556
<Toolbar>
5657
<div className={classes.title}>
5758
<a href="https://github.com/gotify/server" className={classes.link}>

ui/src/layout/Layout.tsx

Lines changed: 64 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import Users from '../user/Users';
2020
import {observer} from 'mobx-react';
2121
import {observable} from 'mobx';
2222
import {inject, Stores} from '../inject';
23+
import {NetworkLostBanner} from '../common/NetworkLostBanner';
2324

2425
const styles = (theme: Theme) => ({
2526
content: {
@@ -50,7 +51,9 @@ const isThemeKey = (value: string | null): value is ThemeKey => {
5051
};
5152

5253
@observer
53-
class Layout extends React.Component<WithStyles<'content'> & Stores<'currentUser'>> {
54+
class Layout extends React.Component<
55+
WithStyles<'content'> & Stores<'currentUser' | 'snackManager'>
56+
> {
5457
private static defaultVersion = '0.0.0';
5558

5659
@observable
@@ -59,6 +62,8 @@ class Layout extends React.Component<WithStyles<'content'> & Stores<'currentUser
5962
private showSettings = false;
6063
@observable
6164
private version = Layout.defaultVersion;
65+
@observable
66+
private reconnecting = false;
6267

6368
public componentDidMount() {
6469
if (this.version === Layout.defaultVersion) {
@@ -75,6 +80,19 @@ class Layout extends React.Component<WithStyles<'content'> & Stores<'currentUser
7580
}
7681
}
7782

83+
private doReconnect = () => {
84+
this.reconnecting = true;
85+
this.props.currentUser
86+
.tryAuthenticate()
87+
.then(() => {
88+
this.reconnecting = false;
89+
})
90+
.catch(() => {
91+
this.reconnecting = false;
92+
this.props.snackManager.snack('Reconnect failed');
93+
});
94+
};
95+
7896
public render() {
7997
const {version, showSettings, currentTheme} = this;
8098
const {
@@ -84,49 +102,56 @@ class Layout extends React.Component<WithStyles<'content'> & Stores<'currentUser
84102
authenticating,
85103
user: {name, admin},
86104
logout,
105+
hasNetwork,
87106
},
88107
} = this.props;
89108
const theme = themeMap[currentTheme];
90109
const loginRoute = () => (loggedIn ? <Redirect to="/" /> : <Login />);
91110
return (
92111
<MuiThemeProvider theme={theme}>
93112
<HashRouter>
94-
<div style={{display: 'flex'}}>
95-
<CssBaseline />
96-
<Header
97-
admin={admin}
98-
name={name}
99-
version={version}
100-
loggedIn={loggedIn}
101-
toggleTheme={this.toggleTheme.bind(this)}
102-
showSettings={() => (this.showSettings = true)}
103-
logout={logout}
104-
/>
105-
<Navigation loggedIn={loggedIn} />
106-
107-
<main className={classes.content}>
108-
<Switch>
109-
{authenticating ? (
110-
<Route path="/">
111-
<LoadingSpinner />
112-
</Route>
113-
) : null}
114-
<Route exact path="/login" render={loginRoute} />
115-
{loggedIn ? null : <Redirect to="/login" />}
116-
<Route exact path="/" component={Messages} />
117-
<Route exact path="/messages/:id" component={Messages} />
118-
<Route exact path="/applications" component={Applications} />
119-
<Route exact path="/clients" component={Clients} />
120-
<Route exact path="/users" component={Users} />
121-
<Route exact path="/plugins" component={Plugins} />
122-
<Route exact path="/plugins/:id" component={PluginDetailView} />
123-
</Switch>
124-
</main>
125-
{showSettings && (
126-
<SettingsDialog fClose={() => (this.showSettings = false)} />
113+
<div>
114+
{hasNetwork ? null : (
115+
<NetworkLostBanner height={64} retry={this.doReconnect} />
127116
)}
128-
<ScrollUpButton />
129-
<SnackBarHandler />
117+
<div style={{display: 'flex'}}>
118+
<CssBaseline />
119+
<Header
120+
style={{top: hasNetwork ? 0 : 64}}
121+
admin={admin}
122+
name={name}
123+
version={version}
124+
loggedIn={loggedIn}
125+
toggleTheme={this.toggleTheme.bind(this)}
126+
showSettings={() => (this.showSettings = true)}
127+
logout={logout}
128+
/>
129+
<Navigation loggedIn={loggedIn} />
130+
131+
<main className={classes.content}>
132+
<Switch>
133+
{authenticating || this.reconnecting ? (
134+
<Route path="/">
135+
<LoadingSpinner />
136+
</Route>
137+
) : null}
138+
<Route exact path="/login" render={loginRoute} />
139+
{loggedIn ? null : <Redirect to="/login" />}
140+
<Route exact path="/" component={Messages} />
141+
<Route exact path="/messages/:id" component={Messages} />
142+
<Route exact path="/applications" component={Applications} />
143+
<Route exact path="/clients" component={Clients} />
144+
<Route exact path="/users" component={Users} />
145+
<Route exact path="/plugins" component={Plugins} />
146+
<Route exact path="/plugins/:id" component={PluginDetailView} />
147+
</Switch>
148+
</main>
149+
{showSettings && (
150+
<SettingsDialog fClose={() => (this.showSettings = false)} />
151+
)}
152+
<ScrollUpButton />
153+
<SnackBarHandler />
154+
</div>
130155
</div>
131156
</HashRouter>
132157
</MuiThemeProvider>
@@ -139,4 +164,6 @@ class Layout extends React.Component<WithStyles<'content'> & Stores<'currentUser
139164
}
140165
}
141166

142-
export default withStyles(styles, {withTheme: true})<{}>(inject('currentUser')(Layout));
167+
export default withStyles(styles, {withTheme: true})<{}>(
168+
inject('currentUser', 'snackManager')(Layout)
169+
);

ui/src/message/WebSocketStore.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,6 @@ export class WebSocketStore {
4242
.catch((error: AxiosError) => {
4343
if (error && error.response && error.response.status === 401) {
4444
this.snack('Could not authenticate with client token, logging out.');
45-
} else {
46-
this.snack('Lost network connection, please refresh the page.');
4745
}
4846
});
4947
};

ui/src/reactions.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import {StoreMapping} from './inject';
2+
import {reaction} from 'mobx';
3+
import * as Notifications from './snack/browserNotification';
4+
5+
export const registerReactions = (stores: StoreMapping) => {
6+
const clearAll = () => {
7+
stores.messagesStore.clearAll();
8+
stores.appStore.clear();
9+
stores.clientStore.clear();
10+
stores.userStore.clear();
11+
stores.wsStore.close();
12+
};
13+
const loadAll = () => {
14+
stores.wsStore.listen((message) => {
15+
stores.messagesStore.publishSingleMessage(message);
16+
Notifications.notifyNewMessage(message);
17+
});
18+
stores.appStore.refresh();
19+
};
20+
21+
reaction(
22+
() => stores.currentUser.loggedIn,
23+
(loggedIn) => {
24+
if (loggedIn) {
25+
loadAll();
26+
} else {
27+
clearAll();
28+
}
29+
}
30+
);
31+
32+
reaction(
33+
() => stores.currentUser.hasNetwork,
34+
(hasNetwork) => {
35+
if (hasNetwork) {
36+
clearAll();
37+
loadAll();
38+
}
39+
}
40+
);
41+
};

ui/src/user/Login.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ class Login extends Component<Stores<'currentUser'>> {
4444
size="large"
4545
className="login"
4646
color="primary"
47+
disabled={!this.props.currentUser.hasNetwork}
4748
style={{marginTop: 15, marginBottom: 5}}
4849
onClick={this.login}>
4950
Login

0 commit comments

Comments
 (0)