Skip to content

Commit 79c6f30

Browse files
authored
Merge pull request #1237 from plotly/connection-status
API connection status notifications
2 parents 7043c13 + 441a9ac commit 79c6f30

23 files changed

+402
-312
lines changed

Diff for: CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ This project adheres to [Semantic Versioning](http://semver.org/).
66
### Added
77
- [#1240](https://github.com/plotly/dash/pull/1240) Adds `callback_context` to clientside callbacks (e.g. `dash_clientside.callback_context.triggered`). Supports `triggered`, `inputs`, `inputs_list`, `states`, and `states_list`, all of which closely resemble their serverside cousins.
88

9+
### Changed
10+
- [#1237](https://github.com/plotly/dash/pull/1237) Closes [#920](https://github.com/plotly/dash/issues/920): Converts hot reload fetch failures into a server status indicator showing whether the latest fetch succeeded or failed. Callback fetch failures still appear as errors but have a clearer message.
11+
912
## [1.12.0] - 2020-05-05
1013
### Added
1114
- [#1228](https://github.com/plotly/dash/pull/1228) Adds control over firing callbacks on page (or layout chunk) load. Individual callbacks can have their initial calls disabled in their definition `@app.callback(..., prevent_initial_call=True)` and similar for `app.clientside_callback`. The app-wide default can also be changed with `app=Dash(prevent_initial_callbacks=True)`, then individual callbacks may disable this behavior.

Diff for: dash-renderer/src/actions/api.js

+45-27
Original file line numberDiff line numberDiff line change
@@ -30,43 +30,61 @@ const request = {GET, POST};
3030

3131
export default function apiThunk(endpoint, method, store, id, body) {
3232
return (dispatch, getState) => {
33-
const config = getState().config;
33+
const {config} = getState();
3434
const url = `${urlBase(config)}${endpoint}`;
3535

36+
function setConnectionStatus(connected) {
37+
if (getState().error.backEndConnected !== connected) {
38+
dispatch({
39+
type: 'SET_CONNECTION_STATUS',
40+
payload: connected,
41+
});
42+
}
43+
}
44+
3645
dispatch({
3746
type: store,
3847
payload: {id, status: 'loading'},
3948
});
4049
return request[method](url, config.fetch, body)
41-
.then(res => {
42-
const contentType = res.headers.get('content-type');
43-
if (
44-
contentType &&
45-
contentType.indexOf('application/json') !== -1
46-
) {
47-
return res.json().then(json => {
48-
dispatch({
49-
type: store,
50-
payload: {
51-
status: res.status,
52-
content: json,
53-
id,
54-
},
50+
.then(
51+
res => {
52+
setConnectionStatus(true);
53+
const contentType = res.headers.get('content-type');
54+
if (
55+
contentType &&
56+
contentType.indexOf('application/json') !== -1
57+
) {
58+
return res.json().then(json => {
59+
dispatch({
60+
type: store,
61+
payload: {
62+
status: res.status,
63+
content: json,
64+
id,
65+
},
66+
});
67+
return json;
5568
});
56-
return json;
69+
}
70+
logWarningOnce(
71+
'Response is missing header: content-type: application/json'
72+
);
73+
return dispatch({
74+
type: store,
75+
payload: {
76+
id,
77+
status: res.status,
78+
},
5779
});
80+
},
81+
() => {
82+
// fetch rejection - this means the request didn't return,
83+
// we don't get here from 400/500 errors, only network
84+
// errors or unresponsive servers.
85+
setConnectionStatus(false);
5886
}
59-
logWarningOnce(
60-
'Response is missing header: content-type: application/json'
61-
);
62-
return dispatch({
63-
type: store,
64-
payload: {
65-
id,
66-
status: res.status,
67-
},
68-
});
69-
})
87+
)
7088
.catch(err => {
7189
const message = 'Error from API call: ' + endpoint;
7290
handleAsyncError(err, message, dispatch);

Diff for: dash-renderer/src/actions/index.js

+28-20
Original file line numberDiff line numberDiff line change
@@ -495,29 +495,37 @@ function handleServerside(config, payload, hooks) {
495495
headers: getCSRFHeader(),
496496
body: JSON.stringify(payload),
497497
})
498-
).then(res => {
499-
const {status} = res;
500-
if (status === STATUS.OK) {
501-
return res.json().then(data => {
502-
const {multi, response} = data;
503-
if (hooks.request_post !== null) {
504-
hooks.request_post(payload, response);
505-
}
498+
).then(
499+
res => {
500+
const {status} = res;
501+
if (status === STATUS.OK) {
502+
return res.json().then(data => {
503+
const {multi, response} = data;
504+
if (hooks.request_post !== null) {
505+
hooks.request_post(payload, response);
506+
}
506507

507-
if (multi) {
508-
return response;
509-
}
508+
if (multi) {
509+
return response;
510+
}
510511

511-
const {output} = payload;
512-
const id = output.substr(0, output.lastIndexOf('.'));
513-
return {[id]: response.props};
514-
});
515-
}
516-
if (status === STATUS.PREVENT_UPDATE) {
517-
return {};
512+
const {output} = payload;
513+
const id = output.substr(0, output.lastIndexOf('.'));
514+
return {[id]: response.props};
515+
});
516+
}
517+
if (status === STATUS.PREVENT_UPDATE) {
518+
return {};
519+
}
520+
throw res;
521+
},
522+
() => {
523+
// fetch rejection - this means the request didn't return,
524+
// we don't get here from 400/500 errors, only network
525+
// errors or unresponsive servers.
526+
throw new Error('Callback failed: the server did not respond.');
518527
}
519-
throw res;
520-
});
528+
);
521529
}
522530

523531
const getVals = input =>

Diff for: dash-renderer/src/components/error/FrontEnd/FrontEndErrorContainer.react.js

+5-3
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,16 @@ class FrontEndErrorContainer extends Component {
99
}
1010

1111
render() {
12-
const errorsLength = this.props.errors.length;
12+
const {errors, connected} = this.props;
13+
const errorsLength = errors.length;
1314
if (errorsLength === 0) {
1415
return null;
1516
}
1617

1718
const inAlertsTray = this.props.inAlertsTray;
1819
let cardClasses = 'dash-error-card dash-error-card--container';
1920

20-
const errorElements = this.props.errors.map((error, i) => {
21+
const errorElements = errors.map((error, i) => {
2122
return <FrontEndError e={error} isListItem={true} key={i} />;
2223
});
2324
if (inAlertsTray) {
@@ -31,7 +32,7 @@ class FrontEndErrorContainer extends Component {
3132
<strong className="test-devtools-error-count">
3233
{errorsLength}
3334
</strong>
34-
)
35+
){connected ? null : '\u00a0 🚫 Server Unavailable'}
3536
</div>
3637
</div>
3738
<div className="dash-error-card__list">{errorElements}</div>
@@ -42,6 +43,7 @@ class FrontEndErrorContainer extends Component {
4243

4344
FrontEndErrorContainer.propTypes = {
4445
errors: PropTypes.array,
46+
connected: PropTypes.bool,
4547
inAlertsTray: PropTypes.any,
4648
};
4749

Diff for: dash-renderer/src/components/error/GlobalErrorContainer.react.js

+8-2
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,14 @@ class UnconnectedGlobalErrorContainer extends Component {
1010
}
1111

1212
render() {
13-
const {error, graphs, children} = this.props;
13+
const {config, error, graphs, children} = this.props;
1414
return (
1515
<div id="_dash-global-error-container">
16-
<DebugMenu error={error} graphs={graphs}>
16+
<DebugMenu
17+
error={error}
18+
graphs={graphs}
19+
hotReload={Boolean(config.hot_reload)}
20+
>
1721
<div id="_dash-app-content">{children}</div>
1822
</DebugMenu>
1923
</div>
@@ -23,11 +27,13 @@ class UnconnectedGlobalErrorContainer extends Component {
2327

2428
UnconnectedGlobalErrorContainer.propTypes = {
2529
children: PropTypes.object,
30+
config: PropTypes.object,
2631
error: PropTypes.object,
2732
graphs: PropTypes.object,
2833
};
2934

3035
const GlobalErrorContainer = connect(state => ({
36+
config: state.config,
3137
error: state.error,
3238
graphs: state.graphs,
3339
}))(Radium(UnconnectedGlobalErrorContainer));

Diff for: dash-renderer/src/components/error/GlobalErrorOverlay.react.js

+9-4
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,18 @@ export default class GlobalErrorOverlay extends Component {
1111
}
1212

1313
render() {
14-
const {visible, error, toastsEnabled} = this.props;
14+
const {visible, error, errorsOpened} = this.props;
1515

1616
let frontEndErrors;
17-
if (toastsEnabled) {
17+
if (errorsOpened) {
1818
const errors = concat(error.frontEnd, error.backEnd);
1919

20-
frontEndErrors = <FrontEndErrorContainer errors={errors} />;
20+
frontEndErrors = (
21+
<FrontEndErrorContainer
22+
errors={errors}
23+
connected={error.backEndConnected}
24+
/>
25+
);
2126
}
2227
return (
2328
<div>
@@ -36,5 +41,5 @@ GlobalErrorOverlay.propTypes = {
3641
children: PropTypes.object,
3742
visible: PropTypes.bool,
3843
error: PropTypes.object,
39-
toastsEnabled: PropTypes.any,
44+
errorsOpened: PropTypes.any,
4045
};
+1-1
Loading

Diff for: dash-renderer/src/components/error/icons/BellIconGrey.svg

-3
This file was deleted.
+3
Loading
+3
Loading

Diff for: dash-renderer/src/components/error/icons/CloseIcon.svg

-3
This file was deleted.

Diff for: dash-renderer/src/components/error/icons/ErrorIconWhite.svg

-3
This file was deleted.

Diff for: dash-renderer/src/components/error/icons/GraphIconGrey.svg

-3
This file was deleted.

Diff for: dash-renderer/src/components/error/icons/OffIcon.svg

+3
Loading

Diff for: dash-renderer/src/components/error/icons/WarningIconWhite.svg

-3
This file was deleted.

Diff for: dash-renderer/src/components/error/icons/WhiteCloseIcon.svg

-3
This file was deleted.

Diff for: dash-renderer/src/components/error/menu/DebugAlertContainer.css

-44
This file was deleted.

Diff for: dash-renderer/src/components/error/menu/DebugAlertContainer.react.js

-38
This file was deleted.

0 commit comments

Comments
 (0)