diff --git a/CHANGELOG.md b/CHANGELOG.md index 70685b2b1a..79fa8a2150 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Added - [#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. +### Changed +- [#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. + ## [1.12.0] - 2020-05-05 ### Added - [#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 --git a/dash-renderer/src/actions/api.js b/dash-renderer/src/actions/api.js index 788da55b71..8a78708d9f 100644 --- a/dash-renderer/src/actions/api.js +++ b/dash-renderer/src/actions/api.js @@ -30,43 +30,61 @@ const request = {GET, POST}; export default function apiThunk(endpoint, method, store, id, body) { return (dispatch, getState) => { - const config = getState().config; + const {config} = getState(); const url = `${urlBase(config)}${endpoint}`; + function setConnectionStatus(connected) { + if (getState().error.backEndConnected !== connected) { + dispatch({ + type: 'SET_CONNECTION_STATUS', + payload: connected, + }); + } + } + dispatch({ type: store, payload: {id, status: 'loading'}, }); return request[method](url, config.fetch, body) - .then(res => { - const contentType = res.headers.get('content-type'); - if ( - contentType && - contentType.indexOf('application/json') !== -1 - ) { - return res.json().then(json => { - dispatch({ - type: store, - payload: { - status: res.status, - content: json, - id, - }, + .then( + res => { + setConnectionStatus(true); + const contentType = res.headers.get('content-type'); + if ( + contentType && + contentType.indexOf('application/json') !== -1 + ) { + return res.json().then(json => { + dispatch({ + type: store, + payload: { + status: res.status, + content: json, + id, + }, + }); + return json; }); - return json; + } + logWarningOnce( + 'Response is missing header: content-type: application/json' + ); + return dispatch({ + type: store, + payload: { + id, + status: res.status, + }, }); + }, + () => { + // fetch rejection - this means the request didn't return, + // we don't get here from 400/500 errors, only network + // errors or unresponsive servers. + setConnectionStatus(false); } - logWarningOnce( - 'Response is missing header: content-type: application/json' - ); - return dispatch({ - type: store, - payload: { - id, - status: res.status, - }, - }); - }) + ) .catch(err => { const message = 'Error from API call: ' + endpoint; handleAsyncError(err, message, dispatch); diff --git a/dash-renderer/src/actions/index.js b/dash-renderer/src/actions/index.js index 05d0427381..a48c81a687 100644 --- a/dash-renderer/src/actions/index.js +++ b/dash-renderer/src/actions/index.js @@ -495,29 +495,37 @@ function handleServerside(config, payload, hooks) { headers: getCSRFHeader(), body: JSON.stringify(payload), }) - ).then(res => { - const {status} = res; - if (status === STATUS.OK) { - return res.json().then(data => { - const {multi, response} = data; - if (hooks.request_post !== null) { - hooks.request_post(payload, response); - } + ).then( + res => { + const {status} = res; + if (status === STATUS.OK) { + return res.json().then(data => { + const {multi, response} = data; + if (hooks.request_post !== null) { + hooks.request_post(payload, response); + } - if (multi) { - return response; - } + if (multi) { + return response; + } - const {output} = payload; - const id = output.substr(0, output.lastIndexOf('.')); - return {[id]: response.props}; - }); - } - if (status === STATUS.PREVENT_UPDATE) { - return {}; + const {output} = payload; + const id = output.substr(0, output.lastIndexOf('.')); + return {[id]: response.props}; + }); + } + if (status === STATUS.PREVENT_UPDATE) { + return {}; + } + throw res; + }, + () => { + // fetch rejection - this means the request didn't return, + // we don't get here from 400/500 errors, only network + // errors or unresponsive servers. + throw new Error('Callback failed: the server did not respond.'); } - throw res; - }); + ); } const getVals = input => diff --git a/dash-renderer/src/components/error/FrontEnd/FrontEndErrorContainer.react.js b/dash-renderer/src/components/error/FrontEnd/FrontEndErrorContainer.react.js index ddb18ac8d3..794070aebb 100644 --- a/dash-renderer/src/components/error/FrontEnd/FrontEndErrorContainer.react.js +++ b/dash-renderer/src/components/error/FrontEnd/FrontEndErrorContainer.react.js @@ -9,7 +9,8 @@ class FrontEndErrorContainer extends Component { } render() { - const errorsLength = this.props.errors.length; + const {errors, connected} = this.props; + const errorsLength = errors.length; if (errorsLength === 0) { return null; } @@ -17,7 +18,7 @@ class FrontEndErrorContainer extends Component { const inAlertsTray = this.props.inAlertsTray; let cardClasses = 'dash-error-card dash-error-card--container'; - const errorElements = this.props.errors.map((error, i) => { + const errorElements = errors.map((error, i) => { return ; }); if (inAlertsTray) { @@ -31,7 +32,7 @@ class FrontEndErrorContainer extends Component { {errorsLength} - ) + ){connected ? null : '\u00a0 🚫 Server Unavailable'}
{errorElements}
@@ -42,6 +43,7 @@ class FrontEndErrorContainer extends Component { FrontEndErrorContainer.propTypes = { errors: PropTypes.array, + connected: PropTypes.bool, inAlertsTray: PropTypes.any, }; diff --git a/dash-renderer/src/components/error/GlobalErrorContainer.react.js b/dash-renderer/src/components/error/GlobalErrorContainer.react.js index 6f61b07fcf..bd9fa3887a 100644 --- a/dash-renderer/src/components/error/GlobalErrorContainer.react.js +++ b/dash-renderer/src/components/error/GlobalErrorContainer.react.js @@ -10,10 +10,14 @@ class UnconnectedGlobalErrorContainer extends Component { } render() { - const {error, graphs, children} = this.props; + const {config, error, graphs, children} = this.props; return (
- +
{children}
@@ -23,11 +27,13 @@ class UnconnectedGlobalErrorContainer extends Component { UnconnectedGlobalErrorContainer.propTypes = { children: PropTypes.object, + config: PropTypes.object, error: PropTypes.object, graphs: PropTypes.object, }; const GlobalErrorContainer = connect(state => ({ + config: state.config, error: state.error, graphs: state.graphs, }))(Radium(UnconnectedGlobalErrorContainer)); diff --git a/dash-renderer/src/components/error/GlobalErrorOverlay.react.js b/dash-renderer/src/components/error/GlobalErrorOverlay.react.js index 8ef4655902..bdbc46b34d 100644 --- a/dash-renderer/src/components/error/GlobalErrorOverlay.react.js +++ b/dash-renderer/src/components/error/GlobalErrorOverlay.react.js @@ -11,13 +11,18 @@ export default class GlobalErrorOverlay extends Component { } render() { - const {visible, error, toastsEnabled} = this.props; + const {visible, error, errorsOpened} = this.props; let frontEndErrors; - if (toastsEnabled) { + if (errorsOpened) { const errors = concat(error.frontEnd, error.backEnd); - frontEndErrors = ; + frontEndErrors = ( + + ); } return (
@@ -36,5 +41,5 @@ GlobalErrorOverlay.propTypes = { children: PropTypes.object, visible: PropTypes.bool, error: PropTypes.object, - toastsEnabled: PropTypes.any, + errorsOpened: PropTypes.any, }; diff --git a/dash-renderer/src/components/error/icons/BellIcon.svg b/dash-renderer/src/components/error/icons/BellIcon.svg index d73e9e4680..d397df2930 100755 --- a/dash-renderer/src/components/error/icons/BellIcon.svg +++ b/dash-renderer/src/components/error/icons/BellIcon.svg @@ -1,3 +1,3 @@ - + diff --git a/dash-renderer/src/components/error/icons/BellIconGrey.svg b/dash-renderer/src/components/error/icons/BellIconGrey.svg deleted file mode 100755 index c3b91aca6d..0000000000 --- a/dash-renderer/src/components/error/icons/BellIconGrey.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/dash-renderer/src/components/error/icons/CheckIcon.svg b/dash-renderer/src/components/error/icons/CheckIcon.svg new file mode 100644 index 0000000000..b5ad358c68 --- /dev/null +++ b/dash-renderer/src/components/error/icons/CheckIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/dash-renderer/src/components/error/icons/ClockIcon.svg b/dash-renderer/src/components/error/icons/ClockIcon.svg new file mode 100644 index 0000000000..36dd569e5b --- /dev/null +++ b/dash-renderer/src/components/error/icons/ClockIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/dash-renderer/src/components/error/icons/CloseIcon.svg b/dash-renderer/src/components/error/icons/CloseIcon.svg deleted file mode 100755 index 90eb4191fc..0000000000 --- a/dash-renderer/src/components/error/icons/CloseIcon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/dash-renderer/src/components/error/icons/ErrorIconWhite.svg b/dash-renderer/src/components/error/icons/ErrorIconWhite.svg deleted file mode 100755 index 85688a2989..0000000000 --- a/dash-renderer/src/components/error/icons/ErrorIconWhite.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/dash-renderer/src/components/error/icons/GraphIconGrey.svg b/dash-renderer/src/components/error/icons/GraphIconGrey.svg deleted file mode 100644 index e81bae0294..0000000000 --- a/dash-renderer/src/components/error/icons/GraphIconGrey.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/dash-renderer/src/components/error/icons/OffIcon.svg b/dash-renderer/src/components/error/icons/OffIcon.svg new file mode 100644 index 0000000000..44f805eb6d --- /dev/null +++ b/dash-renderer/src/components/error/icons/OffIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/dash-renderer/src/components/error/icons/WarningIconWhite.svg b/dash-renderer/src/components/error/icons/WarningIconWhite.svg deleted file mode 100755 index d77f988141..0000000000 --- a/dash-renderer/src/components/error/icons/WarningIconWhite.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/dash-renderer/src/components/error/icons/WhiteCloseIcon.svg b/dash-renderer/src/components/error/icons/WhiteCloseIcon.svg deleted file mode 100755 index ccecc2c0f0..0000000000 --- a/dash-renderer/src/components/error/icons/WhiteCloseIcon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/dash-renderer/src/components/error/menu/DebugAlertContainer.css b/dash-renderer/src/components/error/menu/DebugAlertContainer.css deleted file mode 100644 index a186016e73..0000000000 --- a/dash-renderer/src/components/error/menu/DebugAlertContainer.css +++ /dev/null @@ -1,44 +0,0 @@ -.dash-debug-alert-container { - box-sizing: border-box; - background: #f3f6fa; - border-radius: 2px; - padding: 8px; - display: flex; - flex-direction: column; - justify-content: center; - transition: background-color 0.1s, border 0.1s; -} -.dash-debug-alert-container:hover { - cursor: pointer; -} -.dash-debug-alert-container--opened { - background-color: #119dff; - color: white; -} -.dash-debug-alert-container__icon { - width: 12px; - height: 12px; - margin-right: 4px; -} -.dash-debug-alert-container__icon--warning { - height: auto; -} - -.dash-debug-alert { - display: flex; - align-items: center; - font-size: 10px; -} - -.dash-debug-alert-label { - display: flex; - position: fixed; - bottom: 81px; - right: 29px; - z-index: 10001; - box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.25), - 0px 1px 3px rgba(162, 177, 198, 0.32); - border-radius: 32px; - background-color: white; - padding: 4px; -} diff --git a/dash-renderer/src/components/error/menu/DebugAlertContainer.react.js b/dash-renderer/src/components/error/menu/DebugAlertContainer.react.js deleted file mode 100644 index bd4833db36..0000000000 --- a/dash-renderer/src/components/error/menu/DebugAlertContainer.react.js +++ /dev/null @@ -1,38 +0,0 @@ -import './DebugAlertContainer.css'; -import {Component} from 'react'; -import PropTypes from 'prop-types'; -import ErrorIconWhite from '../icons/ErrorIconWhite.svg'; - -class DebugAlertContainer extends Component { - constructor(props) { - super(props); - } - render() { - const {alertsOpened} = this.props; - return ( -
-
- {alertsOpened ? ( - - ) : ( - '🛑 ' - )} - {this.props.errors.length} -
-
- ); - } -} - -DebugAlertContainer.propTypes = { - errors: PropTypes.object, - alertsOpened: PropTypes.bool, - onClick: PropTypes.func, -}; - -export {DebugAlertContainer}; diff --git a/dash-renderer/src/components/error/menu/DebugMenu.css b/dash-renderer/src/components/error/menu/DebugMenu.css index 8a7aad5439..092e379db2 100644 --- a/dash-renderer/src/components/error/menu/DebugMenu.css +++ b/dash-renderer/src/components/error/menu/DebugMenu.css @@ -1,50 +1,54 @@ .dash-debug-menu { - transition: width 0.05s, background-color 0.1s; + transition: 0.3s; position: fixed; bottom: 35px; right: 35px; display: flex; justify-content: center; align-items: center; - z-index: 10000; -} -.dash-debug-menu--closed { + z-index: 10001; background-color: #119dff; border-radius: 100%; width: 64px; height: 64px; + cursor: pointer; } -.dash-debug-menu--opened { - box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.25), - 0px 1px 3px rgba(162, 177, 198, 0.32); - border-radius: 4px; - padding: 12px 0px; - background-color: white; +.dash-debug-menu--open { + transform: rotate(-180deg); } -.dash-debug-menu--closed:hover { - cursor: pointer; +.dash-debug-menu:hover { background-color: #108de4; } .dash-debug-menu__icon { - width: 24px; - height: 28px; -} -.dash-debug-menu__icon--close { - width: 14px; - height: 14px; -} -.dash-debug-menu__icon--bell { + width: auto; height: 24px; - width: 28px; } -.dash-debug-menu__icon--debug { - height: 24px; - width: auto; + +.dash-debug-menu__outer { + transition: 0.3s; + box-sizing: border-box; + position: fixed; + bottom: 27px; + right: 27px; + display: flex; + justify-content: center; + align-items: center; + z-index: 10000; + height: 80px; + border-radius: 40px; + padding: 5px 78px 5px 5px; + background-color: #fff; + box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.25), + 0px 1px 3px rgba(162, 177, 198, 0.32); } -.dash-debug-menu__icon--graph { - height: 24px; +.dash-debug-menu__outer--closed { + height: 60px; + width: 60px; + bottom: 37px; + right: 37px; + padding: 0; } .dash-debug-menu__content { @@ -59,44 +63,114 @@ justify-content: center; align-items: center; width: 74px; - margin-left: 10px; -} -.dash-debug-menu__button-label { - color: #A2B1C6; - font-size: 10px; - margin-top: 4px; } .dash-debug-menu__button { - background-color: white; + position: relative; + background-color: #B9C2CE; border-radius: 100%; - border: 1px solid #B9C2CE; width: 64px; height: 64px; font-size: 10px; display: flex; + flex-direction: column; justify-content: center; align-items: center; transition: background-color 0.2s; - color: black; + color: #fff; + cursor: pointer; +} +.dash-debug-menu__button:hover { + background-color: #a1a9b5; } .dash-debug-menu__button--enabled { background-color: #00CC96; - color: white; } -.dash-debug-menu__button--small { - width: 32px; - height: 32px; - background-color: #B9C2CE; +.dash-debug-menu__button.dash-debug-menu__button--enabled:hover { + background-color: #03bb8a; } -.dash-debug-menu__button:hover { + +.dash-debug-menu__button-label { + cursor: inherit; +} + +.dash-debug-menu__button::before { + visibility: hidden; + pointer-events: none; + position: absolute; + box-sizing: border-box; + bottom: 110%; + left: 50%; + margin-left: -60px; + padding: 7px; + width: 120px; + border-radius: 3px; + background-color: rgba(68,68,68,0.7); + color: #fff; + text-align: center; + font-size: 10px; + line-height: 1.2; +} +.dash-debug-menu__button:hover::before { + visibility: visible; +} +.dash-debug-menu__button--callbacks::before { + content: "Toggle Callback Graph"; +} +.dash-debug-menu__button--errors::before { + content: "Toggle Errors"; +} +.dash-debug-menu__button--available, +.dash-debug-menu__button--available:hover { + background-color: #00CC96; + cursor: default; +} +.dash-debug-menu__button--available::before { + content: "Server Available"; +} +.dash-debug-menu__button--unavailable, +.dash-debug-menu__button--unavailable:hover { + background-color: #F1564E; + cursor: default; +} +.dash-debug-menu__button--unavailable::before { + content: "Server Unavailable. Check if the process has halted or crashed."; +} +.dash-debug-menu__button--cold, +.dash-debug-menu__button--cold:hover { + background-color: #FDDA68; + cursor: default; +} +.dash-debug-menu__button--cold::before { + content: "Hot Reload Disabled"; +} + +.dash-debug-alert { + display: flex; + align-items: center; + font-size: 10px; +} + +.dash-debug-alert-label { + display: flex; + position: fixed; + bottom: 81px; + right: 29px; + z-index: 10002; cursor: pointer; - background-color: #f5f5f5; + box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.25), + 0px 1px 3px rgba(162, 177, 198, 0.32); + border-radius: 32px; + background-color: white; + padding: 4px; } -.dash-debug-menu__button--small:hover { - background-color: #a1a9b5; + +.dash-debug-error-count { + display: block; + margin: 0 3px; } -.dash-debug-menu__button--enabled:hover { - background-color: #03bb8a; +.dash-debug-disconnected { + font-size: 14px; + margin-left: 3px; } diff --git a/dash-renderer/src/components/error/menu/DebugMenu.react.js b/dash-renderer/src/components/error/menu/DebugMenu.react.js index e25c8cc2bc..636323eb48 100644 --- a/dash-renderer/src/components/error/menu/DebugMenu.react.js +++ b/dash-renderer/src/components/error/menu/DebugMenu.react.js @@ -1,144 +1,154 @@ import React, {Component} from 'react'; -import {concat, isEmpty} from 'ramda'; +import PropTypes from 'prop-types'; + import './DebugMenu.css'; -import DebugIcon from '../icons/DebugIcon.svg'; -import WhiteCloseIcon from '../icons/WhiteCloseIcon.svg'; import BellIcon from '../icons/BellIcon.svg'; -import BellIconGrey from '../icons/BellIconGrey.svg'; +import CheckIcon from '../icons/CheckIcon.svg'; +import ClockIcon from '../icons/ClockIcon.svg'; +import DebugIcon from '../icons/DebugIcon.svg'; import GraphIcon from '../icons/GraphIcon.svg'; -import GraphIconGrey from '../icons/GraphIconGrey.svg'; -import PropTypes from 'prop-types'; -import {DebugAlertContainer} from './DebugAlertContainer.react'; +import OffIcon from '../icons/OffIcon.svg'; + import GlobalErrorOverlay from '../GlobalErrorOverlay.react'; import {CallbackGraphContainer} from '../CallbackGraph/CallbackGraphContainer.react'; +const classes = (base, variant, variant2) => + `${base} ${base}--${variant}` + (variant2 ? ` ${base}--${variant2}` : ''); + +const buttonFactory = ( + enabled, + buttonVariant, + toggle, + _Icon, + iconVariant, + label +) => ( +
+
+ <_Icon className={classes('dash-debug-menu__icon', iconVariant)} /> + {label ? ( + + ) : null} +
+
+); + class DebugMenu extends Component { constructor(props) { super(props); this.state = { opened: false, - alertsOpened: false, callbackGraphOpened: false, - toastsEnabled: true, + errorsOpened: true, }; } render() { - const { - opened, - alertsOpened, - toastsEnabled, - callbackGraphOpened, - } = this.state; - const {error, graphs} = this.props; - - const menuClasses = opened - ? 'dash-debug-menu dash-debug-menu--opened' - : 'dash-debug-menu dash-debug-menu--closed'; + const {opened, errorsOpened, callbackGraphOpened} = this.state; + const {error, graphs, hotReload} = this.props; + + const errCount = error.frontEnd.length + error.backEnd.length; + const connected = error.backEndConnected; + + const toggleErrors = () => { + this.setState({errorsOpened: !errorsOpened}); + }; + + const status = hotReload + ? connected + ? 'available' + : 'unavailable' + : 'cold'; + const _StatusIcon = hotReload + ? connected + ? CheckIcon + : OffIcon + : ClockIcon; const menuContent = opened ? (
{callbackGraphOpened ? ( ) : null} - {error.frontEnd.length > 0 || error.backEnd.length > 0 ? ( -
- - this.setState({alertsOpened: !alertsOpened}) - } - /> -
- ) : null} -
-
- this.setState({ - callbackGraphOpened: !callbackGraphOpened, - }) - } - > - {callbackGraphOpened ? ( - - ) : ( - - )} -
- -
-
-
- this.setState({ - toastsEnabled: !toastsEnabled, - }) - } - > - {toastsEnabled ? ( - - ) : ( - - )} -
- -
-
-
{ - e.stopPropagation(); - this.setState({opened: false}); - }} - > - -
-
+ {buttonFactory( + callbackGraphOpened, + 'callbacks', + () => { + this.setState({ + callbackGraphOpened: !callbackGraphOpened, + }); + }, + GraphIcon, + 'graph', + 'Callbacks' + )} + {buttonFactory( + errorsOpened, + 'errors', + toggleErrors, + BellIcon, + 'bell', + errCount + ' Error' + (errCount === 1 ? '' : 's') + )} + {buttonFactory( + false, + status, + null, + _StatusIcon, + 'indicator', + 'Server' + )}
) : ( - +
); const alertsLabel = - error.frontEnd.length + error.backEnd.length > 0 && !opened ? ( + (errCount || !connected) && !opened ? (
-
- 🛑  {error.frontEnd.length + error.backEnd.length} +
+ {errCount ? ( +
+ {'🛑 ' + errCount} +
+ ) : null} + {connected ? null : ( +
🚫
+ )}
) : null; + const openVariant = opened ? 'open' : 'closed'; + return (
{alertsLabel} +
+ {menuContent} +
this.setState({opened: true})} + className={classes('dash-debug-menu', openVariant)} + onClick={() => { + this.setState({opened: !opened}); + }} > - {menuContent} +
0} + errorsOpened={errorsOpened} > {this.props.children} @@ -151,6 +161,7 @@ DebugMenu.propTypes = { children: PropTypes.object, error: PropTypes.object, graphs: PropTypes.object, + hotReload: PropTypes.bool, }; export {DebugMenu}; diff --git a/dash-renderer/src/reducers/error.js b/dash-renderer/src/reducers/error.js index b796024e49..4188703054 100644 --- a/dash-renderer/src/reducers/error.js +++ b/dash-renderer/src/reducers/error.js @@ -3,11 +3,13 @@ import {mergeRight} from 'ramda'; const initialError = { frontEnd: [], backEnd: [], + backEndConnected: true, }; export default function error(state = initialError, action) { switch (action.type) { case 'ON_ERROR': { + const {frontEnd, backEnd, backEndConnected} = state; // log errors to the console for stack tracing and so they're // available even with debugging off /* eslint-disable-next-line no-console */ @@ -17,21 +19,26 @@ export default function error(state = initialError, action) { return { frontEnd: [ mergeRight(action.payload, {timestamp: new Date()}), - ...state.frontEnd, + ...frontEnd, ], - backEnd: state.backEnd, + backEnd, + backEndConnected, }; } else if (action.payload.type === 'backEnd') { return { - frontEnd: state.frontEnd, + frontEnd, backEnd: [ mergeRight(action.payload, {timestamp: new Date()}), - ...state.backEnd, + ...backEnd, ], + backEndConnected, }; } return state; } + case 'SET_CONNECTION_STATUS': { + return mergeRight(state, {backEndConnected: action.payload}); + } default: { return state; diff --git a/tests/integration/devtools/test_devtools_ui.py b/tests/integration/devtools/test_devtools_ui.py index 7d6ce5abc3..878e6377b7 100644 --- a/tests/integration/devtools/test_devtools_ui.py +++ b/tests/integration/devtools/test_devtools_ui.py @@ -1,3 +1,5 @@ +from time import sleep + import dash_core_components as dcc import dash_html_components as html import dash @@ -25,9 +27,9 @@ def test_dvui001_disable_props_check_config(dash_duo): dash_duo.wait_for_text_to_equal("#tcid", "Hello Props Check") assert dash_duo.find_elements("#broken svg.main-svg"), "graph should be rendered" - assert dash_duo.find_elements( - ".dash-debug-menu" - ), "the debug menu icon should show up" + # open the debug menu so we see the "hot reload off" indicator + dash_duo.find_element(".dash-debug-menu").click() + sleep(1) # wait for debug menu opening animation dash_duo.percy_snapshot("devtools - disable props check - Graph should render") diff --git a/tests/integration/devtools/test_hot_reload.py b/tests/integration/devtools/test_hot_reload.py index b57bba4de5..0b0a0fae28 100644 --- a/tests/integration/devtools/test_hot_reload.py +++ b/tests/integration/devtools/test_hot_reload.py @@ -3,6 +3,8 @@ import dash_html_components as html import dash +from dash.dependencies import Input, Output +from dash.exceptions import PreventUpdate RED_BG = """ @@ -14,15 +16,27 @@ def test_dvhr001_hot_reload(dash_duo): app = dash.Dash(__name__, assets_folder="hr_assets") - app.layout = html.Div([html.H3("Hot reload")], id="hot-reload-content") + app.layout = html.Div([ + html.H3("Hot reload", id="text"), + html.Button("Click", id="btn") + ], id="hot-reload-content") - dash_duo.start_server( - app, + @app.callback(Output("text", "children"), [Input("btn", "n_clicks")]) + def new_text(n): + if not n: + raise PreventUpdate + return n + + hot_reload_settings = dict( dev_tools_hot_reload=True, + dev_tools_ui=True, + dev_tools_serve_dev_bundles=True, dev_tools_hot_reload_interval=0.1, dev_tools_hot_reload_max_retry=100, ) + dash_duo.start_server(app, **hot_reload_settings) + # default overload color is blue dash_duo.wait_for_style_to_equal( "#hot-reload-content", "background-color", "rgba(0, 0, 255, 1)" @@ -51,3 +65,34 @@ def test_dvhr001_hot_reload(dash_duo): dash_duo.wait_for_style_to_equal( "#hot-reload-content", "background-color", "rgba(0, 0, 255, 1)" ) + + # Now check the server status indicator functionality + + dash_duo.find_element(".dash-debug-menu").click() + dash_duo.find_element(".dash-debug-menu__button--available") + sleep(1) # wait for opening animation + dash_duo.percy_snapshot(name="hot-reload-available") + + dash_duo.server.stop() + sleep(1) # make sure we would have requested the reload hash multiple times + dash_duo.find_element(".dash-debug-menu__button--unavailable") + dash_duo.wait_for_no_elements(".dash-fe-error__title") + dash_duo.percy_snapshot(name="hot-reload-unavailable") + + dash_duo.find_element(".dash-debug-menu").click() + sleep(1) # wait for opening animation + dash_duo.find_element(".dash-debug-disconnected") + dash_duo.percy_snapshot(name="hot-reload-unavailable-small") + + dash_duo.find_element("#btn").click() + dash_duo.wait_for_text_to_equal( + ".dash-fe-error__title", "Callback failed: the server did not respond." + ) + + # start up the server again + dash_duo.start_server(app, **hot_reload_settings) + + # rerenders with debug menu closed after reload + # reopen and check that server is now available + dash_duo.find_element(".dash-debug-menu--closed").click() + dash_duo.find_element(".dash-debug-menu__button--available")