Skip to content

Commit 00eb045

Browse files
committed
Add devtool hook
1 parent 1f4337a commit 00eb045

File tree

7 files changed

+112
-11
lines changed

7 files changed

+112
-11
lines changed

dash/_hooks.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ def __init__(self) -> None:
4646
"callback": [],
4747
"index": [],
4848
"custom_data": [],
49+
"dev_tools": [],
4950
}
5051
self._js_dist = []
5152
self._css_dist = []
@@ -216,6 +217,24 @@ def wrap(func: _t.Callable[[_t.Dict], _t.Any]):
216217

217218
return wrap
218219

220+
def devtool(self, namespace: str, component_type: str, props=None):
221+
"""
222+
Add a component to be rendered inside the dev tools.
223+
224+
If it's a dash component, it can be used in callbacks provided
225+
that it has an id and the dependency is set with allow_optional=True.
226+
227+
`props` can be a function, in which case it will be called before
228+
sending the component to the frontend.
229+
"""
230+
self._ns["dev_tools"].append(
231+
{
232+
"namespace": namespace,
233+
"type": component_type,
234+
"props": props or {},
235+
}
236+
)
237+
219238

220239
hooks = _Hooks()
221240

dash/dash-renderer/src/APIController.react.js

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -103,22 +103,22 @@ const UnconnectedContainer = props => {
103103

104104
content = (
105105
<>
106-
{Array.isArray(layout) ? (
107-
layout.map((c, i) =>
106+
{Array.isArray(layout.components) ? (
107+
layout.components.map((c, i) =>
108108
isSimpleComponent(c) ? (
109109
c
110110
) : (
111111
<DashWrapper
112112
_dashprivate_error={error}
113-
componentPath={[i]}
113+
componentPath={['components', i]}
114114
key={i}
115115
/>
116116
)
117117
)
118118
) : (
119119
<DashWrapper
120120
_dashprivate_error={error}
121-
componentPath={[]}
121+
componentPath={['components']}
122122
/>
123123
)}
124124
</>
@@ -153,7 +153,7 @@ function storeEffect(props, events, setErrorLoading) {
153153
}
154154
dispatch(apiThunk('_dash-layout', 'GET', 'layoutRequest'));
155155
} else if (layoutRequest.status === STATUS.OK) {
156-
if (isEmpty(layout)) {
156+
if (isEmpty(layout.components)) {
157157
if (typeof hooks.layout_post === 'function') {
158158
hooks.layout_post(layoutRequest.content);
159159
}
@@ -163,7 +163,12 @@ function storeEffect(props, events, setErrorLoading) {
163163
);
164164
dispatch(
165165
setPaths(
166-
computePaths(finalLayout, [], null, events.current)
166+
computePaths(
167+
finalLayout,
168+
['components'],
169+
null,
170+
events.current
171+
)
167172
)
168173
);
169174
dispatch(setLayout(finalLayout));
@@ -194,7 +199,7 @@ function storeEffect(props, events, setErrorLoading) {
194199
!isEmpty(graphs) &&
195200
// LayoutRequest and its computed stores
196201
layoutRequest.status === STATUS.OK &&
197-
!isEmpty(layout) &&
202+
!isEmpty(layout.components) &&
198203
// Hasn't already hydrated
199204
appLifecycle === getAppState('STARTED')
200205
) {

dash/dash-renderer/src/actions/dependencies.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
any,
66
ap,
77
assoc,
8+
concat,
89
difference,
910
equals,
1011
evolve,
@@ -568,10 +569,20 @@ export function validateCallbacksToLayout(state_, dispatchError) {
568569
function validateMap(map, cls, doState) {
569570
for (const id in map) {
570571
const idProps = map[id];
572+
const fcb = flatten(values(idProps));
573+
const optional = all(
574+
({allow_optional}) => allow_optional,
575+
flatten(fcb.map(cb => concat(cb.outputs, cb.inputs))).filter(
576+
dep => dep.id === id
577+
)
578+
);
579+
if (optional) {
580+
continue;
581+
}
571582
const idPath = getPath(paths, id);
572583
if (!idPath) {
573584
if (validateIds) {
574-
missingId(id, cls, flatten(values(idProps)));
585+
missingId(id, cls, fcb);
575586
}
576587
} else {
577588
for (const property in idProps) {

dash/dash-renderer/src/components/error/menu/DebugMenu.react.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import Expand from '../icons/Expand.svg';
1313
import {VersionInfo} from './VersionInfo.react';
1414
import {CallbackGraphContainer} from '../CallbackGraph/CallbackGraphContainer.react';
1515
import {FrontEndErrorContainer} from '../FrontEnd/FrontEndErrorContainer.react';
16+
import ExternalWrapper from '../../../wrapper/ExternalWrapper';
17+
import {useSelector} from 'react-redux';
1618

1719
const classes = (base, variant, variant2) =>
1820
`${base} ${base}--${variant}` + (variant2 ? ` ${base}--${variant2}` : '');
@@ -35,6 +37,7 @@ const MenuContent = ({
3537
toggleCallbackGraph,
3638
config
3739
}) => {
40+
const ready = useSelector(state => state.appLifecycle === 'HYDRATED');
3841
const _StatusIcon = hotReload
3942
? connected
4043
? CheckIcon
@@ -47,6 +50,25 @@ const MenuContent = ({
4750
: 'unavailable'
4851
: 'cold';
4952

53+
let custom = null;
54+
if (config.dev_tools?.length && ready) {
55+
custom = (
56+
<>
57+
{config.dev_tools.map((devtool, i) => (
58+
<ExternalWrapper
59+
component={devtool}
60+
componentPath={['__dash_devtools', i]}
61+
key={devtool?.props?.id ? devtool.props.id : i}
62+
/>
63+
))}
64+
<div
65+
className='dash-debug-menu__divider'
66+
style={{marginRight: 0}}
67+
/>
68+
</>
69+
);
70+
}
71+
5072
return (
5173
<div className='dash-debug-menu__content'>
5274
<button
@@ -91,6 +113,7 @@ const MenuContent = ({
91113
className='dash-debug-menu__divider'
92114
style={{marginRight: 0}}
93115
/>
116+
{custom}
94117
</div>
95118
);
96119
};

dash/dash-renderer/src/reducers/layout.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,15 @@ import {
1010

1111
import {getAction} from '../actions/constants';
1212

13-
const layout = (state = {}, action) => {
13+
const layout = (state = {components: []}, action) => {
1414
if (action.type === getAction('SET_LAYOUT')) {
1515
if (Array.isArray(action.payload)) {
16-
return [...action.payload];
16+
state.components = [...action.payload];
17+
} else {
18+
state.components = {...action.payload};
1719
}
18-
return {...action.payload};
20+
21+
return state;
1922
} else if (
2023
includes(action.type, [
2124
'UNDO_PROP_CHANGE',

dash/dash.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -890,6 +890,16 @@ def _config(self):
890890

891891
config["validation_layout"] = validation_layout
892892

893+
if self._dev_tools.ui:
894+
# Add custom dev tools hooks if the ui is activated.
895+
custom_dev_tools = []
896+
for hook_dev_tools in self._hooks.get_hooks("dev_tools"):
897+
props = hook_dev_tools.get("props", {})
898+
if callable(props):
899+
props = props()
900+
custom_dev_tools.append({**hook_dev_tools, "props": props})
901+
config["dev_tools"] = custom_dev_tools
902+
893903
return config
894904

895905
def serve_reload_hash(self):

tests/integration/test_hooks.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ def hook_cleanup():
1515
hooks._ns["callback"] = []
1616
hooks._ns["index"] = []
1717
hooks._ns["custom_data"] = []
18+
hooks._ns["dev_tools"] = []
1819
hooks._css_dist = []
1920
hooks._js_dist = []
2021
hooks._finals = {}
@@ -210,3 +211,32 @@ def cb(_):
210211
dash_duo.start_server(app)
211212
dash_duo.wait_for_element("#btn").click()
212213
dash_duo.wait_for_text_to_equal("#output", "custom-data")
214+
215+
216+
def test_hook011_devtool_hook(hook_cleanup, dash_duo):
217+
hooks.devtool(
218+
"dash_html_components", "Button", {"children": "devtool", "id": "devtool"}
219+
)
220+
221+
app = Dash()
222+
app.layout = html.Div(["hooked", html.Div(id="output")])
223+
224+
@app.callback(
225+
Output("output", "children"),
226+
Input("devtool", "n_clicks", allow_optional=True),
227+
prevent_initial_call=True,
228+
)
229+
def cb(_):
230+
return "hooked from devtools"
231+
232+
dash_duo.start_server(
233+
app,
234+
debug=True,
235+
use_reloader=False,
236+
use_debugger=True,
237+
dev_tools_hot_reload=False,
238+
dev_tools_props_check=False,
239+
dev_tools_disable_version_check=True,
240+
)
241+
dash_duo.wait_for_element("#devtool").click()
242+
dash_duo.wait_for_text_to_equal("#output", "hooked from devtools")

0 commit comments

Comments
 (0)