Skip to content

Commit ab73036

Browse files
authored
Merge pull request #3371 from plotly/devtool-hook
Add devtool hook
2 parents bfca375 + a8f5d8a commit ab73036

File tree

15 files changed

+152
-31
lines changed

15 files changed

+152
-31
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@ This project adheres to [Semantic Versioning](https://semver.org/).
66

77
## Added
88
- [#3369](https://github.com/plotly/dash/pull/3369) Expose `dash.NoUpdate` type
9+
- [#3371](https://github.com/plotly/dash/pull/3371) Add devtool hook to add components to the devtool bar ui.
910

1011
## Fixed
1112
- [#3353](https://github.com/plotly/dash/pull/3353) Support pattern-matching/dict ids in `dcc.Loading` `target_components`
12-
13+
- [#3371](https://github.com/plotly/dash/pull/3371) Fix allow_optional triggering a warning for not found input.
1314

1415
# [3.1.1] - 2025-06-29
1516

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(
576+
fcb.map(cb => concat(cb.outputs, cb.inputs, cb.states))
577+
).filter(dep => dep.id === id)
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/actions/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ function triggerDefaultState(dispatch, getState) {
8989

9090
dispatch(
9191
addRequestedCallbacks(
92-
getLayoutCallbacks(graphs, paths, layout, {
92+
getLayoutCallbacks(graphs, paths, layout.components, {
9393
outputsOnly: true
9494
})
9595
)

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/async_tests/test_async_callbacks.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,10 @@ async def update_input(value):
119119
paths = dash_duo.redux_state_paths
120120
assert paths["objs"] == {}
121121
assert paths["strs"] == {
122-
"input": ["props", "children", 0],
123-
"output": ["props", "children", 1],
122+
"input": ["components", "props", "children", 0],
123+
"output": ["components", "props", "children", 1],
124124
"sub-input-1": [
125+
"components",
125126
"props",
126127
"children",
127128
1,
@@ -132,6 +133,7 @@ async def update_input(value):
132133
0,
133134
],
134135
"sub-output-1": [
136+
"components",
135137
"props",
136138
"children",
137139
1,

tests/integration/callbacks/state_path.json

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
"chapter1": {
33
"objs": {},
44
"strs": {
5-
"toc": ["props", "children", 0],
6-
"body": ["props", "children", 1],
5+
"toc": ["components", "props", "children", 0],
6+
"body": ["components", "props", "children", 1],
77
"chapter1-header": [
8+
"components",
89
"props",
910
"children",
1011
1,
@@ -15,6 +16,7 @@
1516
0
1617
],
1718
"chapter1-controls": [
19+
"components",
1820
"props",
1921
"children",
2022
1,
@@ -25,6 +27,7 @@
2527
1
2628
],
2729
"chapter1-label": [
30+
"components",
2831
"props",
2932
"children",
3033
1,
@@ -35,6 +38,7 @@
3538
2
3639
],
3740
"chapter1-graph": [
41+
"components",
3842
"props",
3943
"children",
4044
1,
@@ -49,9 +53,10 @@
4953
"chapter2": {
5054
"objs": {},
5155
"strs": {
52-
"toc": ["props", "children", 0],
53-
"body": ["props", "children", 1],
56+
"toc": ["components", "props", "children", 0],
57+
"body": ["components", "props", "children", 1],
5458
"chapter2-header": [
59+
"components",
5560
"props",
5661
"children",
5762
1,
@@ -62,6 +67,7 @@
6267
0
6368
],
6469
"chapter2-controls": [
70+
"components",
6571
"props",
6672
"children",
6773
1,
@@ -72,6 +78,7 @@
7278
1
7379
],
7480
"chapter2-label": [
81+
"components",
7582
"props",
7683
"children",
7784
1,
@@ -82,6 +89,7 @@
8289
2
8390
],
8491
"chapter2-graph": [
92+
"components",
8593
"props",
8694
"children",
8795
1,
@@ -96,9 +104,10 @@
96104
"chapter3": {
97105
"objs": {},
98106
"strs": {
99-
"toc": ["props", "children", 0],
100-
"body": ["props", "children", 1],
107+
"toc": ["components", "props", "children", 0],
108+
"body": ["components", "props", "children", 1],
101109
"chapter3-header": [
110+
"components",
102111
"props",
103112
"children",
104113
1,
@@ -112,6 +121,7 @@
112121
0
113122
],
114123
"chapter3-label": [
124+
"components",
115125
"props",
116126
"children",
117127
1,
@@ -125,6 +135,7 @@
125135
1
126136
],
127137
"chapter3-graph": [
138+
"components",
128139
"props",
129140
"children",
130141
1,
@@ -138,6 +149,7 @@
138149
2
139150
],
140151
"chapter3-controls": [
152+
"components",
141153
"props",
142154
"children",
143155
1,

0 commit comments

Comments
 (0)