Skip to content

Commit 0d528ac

Browse files
authored
Merge pull request #1310 from plotly/multi-loading
fix multiple concurrent loading states
2 parents e362444 + a225922 commit 0d528ac

File tree

3 files changed

+180
-12
lines changed

3 files changed

+180
-12
lines changed

Diff for: CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
All notable changes to `dash` will be documented in this file.
33
This project adheres to [Semantic Versioning](http://semver.org/).
44

5+
## UNRELEASED
6+
### Fixed
7+
- [#1310](https://github.com/plotly/dash/pull/1310) Fix a regression since 1.13.0 preventing more than one loading state from being shown at a time.
8+
59
## [1.13.3] - 2020-06-19
610

711
## [1.13.2] - 2020-06-18

Diff for: dash-renderer/src/observers/loadingMap.ts

+7-12
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import {
22
equals,
33
flatten,
4-
forEach,
54
isEmpty,
65
map,
76
reduce
@@ -43,26 +42,22 @@ const observer: IStoreObserverDefinition<IStoreState> = {
4342
const nextMap: any = isEmpty(loadingPaths) ?
4443
null :
4544
reduce(
46-
(res, path) => {
45+
(res, {id, property, path}) => {
4746
let target = res;
48-
const idprop = {
49-
id: path.id,
50-
property: path.property
51-
};
47+
const idprop = {id, property};
5248

5349
// Assign all affected props for this path and nested paths
5450
target.__dashprivate__idprops__ = target.__dashprivate__idprops__ || [];
5551
target.__dashprivate__idprops__.push(idprop);
5652

57-
forEach(p => {
58-
target = (target[p] =
59-
target[p] ??
60-
p === 'children' ? [] : {}
61-
)
53+
path.forEach((p, i) => {
54+
target = (target[p] = target[p] ??
55+
(p === 'children' && typeof path[i + 1] === 'number' ? [] : {})
56+
);
6257

6358
target.__dashprivate__idprops__ = target.__dashprivate__idprops__ || [];
6459
target.__dashprivate__idprops__.push(idprop);
65-
}, path.path);
60+
});
6661

6762
// Assign one affected prop for this path
6863
target.__dashprivate__idprop__ = target.__dashprivate__idprop__ || idprop;

Diff for: tests/integration/renderer/test_loading_states.py

+169
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
from multiprocessing import Lock
2+
3+
import dash
4+
from dash.dependencies import Input, Output
5+
6+
import dash_core_components as dcc
7+
import dash_html_components as html
8+
9+
10+
def test_rdls001_multi_loading_components(dash_duo):
11+
lock = Lock()
12+
13+
app = dash.Dash(__name__)
14+
15+
app.layout = html.Div(
16+
children=[
17+
html.H3("Edit text input to see loading state"),
18+
dcc.Input(id="input-3", value="Input triggers the loading states"),
19+
dcc.Loading(
20+
className="loading-1",
21+
children=[html.Div(id="loading-output-1")],
22+
type="default",
23+
),
24+
html.Div(
25+
[
26+
dcc.Loading(
27+
className="loading-2",
28+
children=[html.Div([html.Div(id="loading-output-2")])],
29+
type="circle",
30+
),
31+
dcc.Loading(
32+
className="loading-3",
33+
children=dcc.Graph(id="graph"),
34+
type="cube",
35+
),
36+
]
37+
),
38+
],
39+
)
40+
41+
@app.callback(
42+
[
43+
Output("graph", "figure"),
44+
Output("loading-output-1", "children"),
45+
Output("loading-output-2", "children"),
46+
],
47+
[Input("input-3", "value")],
48+
)
49+
def input_triggers_nested(value):
50+
with lock:
51+
return dict(data=[dict(y=[1, 4, 2, 3])]), value, value
52+
53+
def wait_for_all_spinners():
54+
dash_duo.find_element(".loading-1 .dash-spinner.dash-default-spinner")
55+
dash_duo.find_element(".loading-2 .dash-spinner.dash-sk-circle")
56+
dash_duo.find_element(".loading-3 .dash-spinner.dash-cube-container")
57+
58+
def wait_for_no_spinners():
59+
dash_duo.wait_for_no_elements(".dash-spinner")
60+
61+
with lock:
62+
dash_duo.start_server(app)
63+
wait_for_all_spinners()
64+
65+
wait_for_no_spinners()
66+
67+
with lock:
68+
dash_duo.find_element("#input-3").send_keys("X")
69+
wait_for_all_spinners()
70+
71+
wait_for_no_spinners()
72+
73+
74+
def test_rdls002_chained_loading_states(dash_duo):
75+
lock1, lock2, lock34 = Lock(), Lock(), Lock()
76+
app = dash.Dash(__name__)
77+
78+
def loading_wrapped_div(_id, color):
79+
return html.Div(
80+
dcc.Loading(
81+
html.Div(
82+
id=_id,
83+
style={"width": 200, "height": 200, "backgroundColor": color},
84+
),
85+
className=_id,
86+
),
87+
style={"display": "inline-block"},
88+
)
89+
90+
app.layout = html.Div(
91+
[
92+
html.Button(id="button", children="Start", n_clicks=0),
93+
loading_wrapped_div("output-1", "hotpink"),
94+
loading_wrapped_div("output-2", "rebeccapurple"),
95+
loading_wrapped_div("output-3", "green"),
96+
loading_wrapped_div("output-4", "#FF851B"),
97+
]
98+
)
99+
100+
@app.callback(Output("output-1", "children"), [Input("button", "n_clicks")])
101+
def update_output_1(n_clicks):
102+
with lock1:
103+
return "Output 1: {}".format(n_clicks)
104+
105+
@app.callback(Output("output-2", "children"), [Input("output-1", "children")])
106+
def update_output_2(children):
107+
with lock2:
108+
return "Output 2: {}".format(children)
109+
110+
@app.callback(
111+
[Output("output-3", "children"), Output("output-4", "children")],
112+
[Input("output-2", "children")],
113+
)
114+
def update_output_34(children):
115+
with lock34:
116+
return "Output 3: {}".format(children), "Output 4: {}".format(children)
117+
118+
dash_duo.start_server(app)
119+
120+
def find_spinners(*nums):
121+
if not nums:
122+
dash_duo.wait_for_no_elements(".dash-spinner")
123+
return
124+
125+
for n in nums:
126+
dash_duo.find_element(".output-{} .dash-spinner".format(n))
127+
128+
assert len(dash_duo.find_elements(".dash-spinner")) == len(nums)
129+
130+
def find_text(spec):
131+
templates = [
132+
"Output 1: {}",
133+
"Output 2: Output 1: {}",
134+
"Output 3: Output 2: Output 1: {}",
135+
"Output 4: Output 2: Output 1: {}",
136+
]
137+
for n, v in spec.items():
138+
dash_duo.wait_for_text_to_equal(
139+
"#output-{}".format(n), templates[n - 1].format(v)
140+
)
141+
142+
find_text({1: 0, 2: 0, 3: 0, 4: 0})
143+
find_spinners()
144+
145+
btn = dash_duo.find_element("#button")
146+
# Can't use lock context managers here, because we want to acquire the
147+
# second lock before releasing the first
148+
lock1.acquire()
149+
btn.click()
150+
151+
find_spinners(1)
152+
find_text({2: 0, 3: 0, 4: 0})
153+
154+
lock2.acquire()
155+
lock1.release()
156+
157+
find_spinners(2)
158+
find_text({1: 1, 3: 0, 4: 0})
159+
160+
lock34.acquire()
161+
lock2.release()
162+
163+
find_spinners(3, 4)
164+
find_text({1: 1, 2: 1})
165+
166+
lock34.release()
167+
168+
find_spinners()
169+
find_text({1: 1, 2: 1, 3: 1, 4: 1})

0 commit comments

Comments
 (0)