Skip to content

Commit 279d6f7

Browse files
authored
Merge branch 'dev' into extract-meta-error-path
2 parents cf103fc + 1f4337a commit 279d6f7

14 files changed

Lines changed: 7510 additions & 45675 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22
All notable changes to `dash` will be documented in this file.
33
This project adheres to [Semantic Versioning](https://semver.org/).
44

5+
## [UNRELEASED]
6+
7+
## Fixed
8+
- [#3353](https://github.com/plotly/dash/pull/3353) Support pattern-matching/dict ids in `dcc.Loading` `target_components`
9+
10+
511
# [3.1.1] - 2025-06-29
612

713
## Fixed

components/dash-core-components/src/components/Loading.react.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ function Loading({
8787
custom_spinner,
8888
}) {
8989
const ctx = window.dash_component_api.useDashContext();
90+
9091
const loading = ctx.useSelector(
9192
loadingSelector(ctx.componentPath, target_components),
9293
equals

components/dash-core-components/src/fragments/Slider.react.js

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import {
1717
} from '../utils/formatSliderTooltip';
1818
import LoadingElement from '../utils/LoadingElement';
1919

20+
const MAX_MARKS = 500;
21+
2022
const sliderProps = [
2123
'min',
2224
'max',
@@ -76,6 +78,21 @@ export default class Slider extends Component {
7678
} = this.props;
7779
const value = this.state.value;
7880

81+
// Check if marks exceed 500 limit for performance
82+
let processedMarks = marks;
83+
if (marks && typeof marks === 'object' && marks !== null) {
84+
const marksCount = Object.keys(marks).length;
85+
if (marksCount > MAX_MARKS) {
86+
/* eslint-disable no-console */
87+
console.error(
88+
`dcc.Slider: Too many marks (${marksCount}) provided. ` +
89+
`For performance reasons, marks are limited to 500. ` +
90+
`Using auto-generated marks instead.`
91+
);
92+
processedMarks = undefined;
93+
}
94+
}
95+
7996
let tipProps, tipFormatter;
8097
if (tooltip) {
8198
/**
@@ -136,11 +153,16 @@ export default class Slider extends Component {
136153
tipFormatter={tipFormatter}
137154
style={{position: 'relative'}}
138155
value={value}
139-
marks={sanitizeMarks({min, max, marks, step})}
140-
max={setUndefined(min, max, marks).max_mark}
141-
min={setUndefined(min, max, marks).min_mark}
156+
marks={sanitizeMarks({
157+
min,
158+
max,
159+
marks: processedMarks,
160+
step,
161+
})}
162+
max={setUndefined(min, max, processedMarks).max_mark}
163+
min={setUndefined(min, max, processedMarks).min_mark}
142164
step={
143-
step === null && !isNil(marks)
165+
step === null && !isNil(processedMarks)
144166
? null
145167
: calcStep(min, max, step)
146168
}

components/dash-core-components/tests/integration/loading/test_loading_component.py

Lines changed: 32 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from multiprocessing import Lock
22
from dash import Dash, Input, Output, dcc, html
3+
from dash.dependencies import stringify_id
34
from dash.testing import wait
45
import time
56

@@ -414,9 +415,9 @@ def updateDiv(n_clicks):
414415
assert dash_dcc.get_logs() == []
415416

416417

417-
# multiple components, only one triggers the spinner
418-
def test_ldcp010_loading_component_target_components(dash_dcc):
419-
418+
# update multiple props of same component, only targeted id/prop triggers spinner
419+
# test that target_components id can be a dict id
420+
def test_ldcp011_loading_component_target_components(dash_dcc):
420421
lock = Lock()
421422

422423
app = Dash(__name__)
@@ -425,53 +426,61 @@ def test_ldcp010_loading_component_target_components(dash_dcc):
425426
[
426427
dcc.Loading(
427428
[
428-
html.Button(id="btn-1"),
429+
html.Button(id={"type": "button", "index": "one"}),
429430
html.Button(id="btn-2"),
431+
html.Button(id="btn-3"),
430432
],
431433
className="loading-1",
432-
target_components={"btn-2": "children"},
434+
target_components={
435+
stringify_id({"type": "button", "index": "one"}): "className"
436+
},
433437
)
434438
],
435439
id="root",
436440
)
437441

438-
@app.callback(Output("btn-1", "children"), [Input("btn-2", "n_clicks")])
442+
@app.callback(
443+
Output({"type": "button", "index": "one"}, "children"),
444+
[Input("btn-2", "n_clicks")],
445+
)
439446
def updateDiv1(n_clicks):
440447
if n_clicks:
441448
with lock:
442449
return "changed 1"
443-
444450
return "content 1"
445451

446-
@app.callback(Output("btn-2", "children"), [Input("btn-1", "n_clicks")])
452+
@app.callback(
453+
Output({"type": "button", "index": "one"}, "className"),
454+
[Input("btn-3", "n_clicks")],
455+
)
447456
def updateDiv2(n_clicks):
448457
if n_clicks:
449458
with lock:
450-
return "changed 2"
451-
452-
return "content 2"
459+
return "new-class"
460+
return ""
453461

454462
dash_dcc.start_server(app)
455463

456-
dash_dcc.wait_for_text_to_equal("#btn-1", "content 1")
457-
dash_dcc.wait_for_text_to_equal("#btn-2", "content 2")
458-
459-
with lock:
460-
dash_dcc.find_element("#btn-1").click()
464+
btn1id = "#" + stringify_id({"type": "button", "index": "one"})
461465

462-
dash_dcc.find_element(".loading-1 .dash-spinner")
463-
dash_dcc.wait_for_text_to_equal("#btn-2", "")
464-
465-
dash_dcc.wait_for_text_to_equal("#btn-2", "changed 2")
466+
dash_dcc.wait_for_text_to_equal(btn1id, "content 1")
466467

467468
with lock:
468469
dash_dcc.find_element("#btn-2").click()
469-
spinners = dash_dcc.find_elements(".loading-1 .dash-spinner")
470-
dash_dcc.wait_for_text_to_equal("#btn-1", "")
471470

472-
dash_dcc.wait_for_text_to_equal("#btn-1", "changed 1")
471+
spinners = dash_dcc.find_elements(".loading-1 .dash-spinner")
472+
dash_dcc.wait_for_text_to_equal(btn1id, "")
473+
dash_dcc.wait_for_text_to_equal(btn1id, "changed 1")
473474
assert spinners == []
474475

476+
with lock:
477+
dash_dcc.find_element("#btn-3").click()
478+
479+
dash_dcc.find_element(".loading-1 .dash-spinner")
480+
dash_dcc.wait_for_text_to_equal(btn1id, "")
481+
482+
dash_dcc.wait_for_class_to_equal(btn1id, "new-class")
483+
475484
assert dash_dcc.get_logs() == []
476485

477486

components/dash-core-components/tests/integration/sliders/test_sliders.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,3 +616,50 @@ def test_sls016_sliders_format_tooltips(dash_dcc):
616616
dash_dcc.percy_snapshot("sliders-format-tooltips")
617617

618618
assert dash_dcc.get_logs() == []
619+
620+
621+
def test_slsl017_marks_limit_500(dash_dcc):
622+
"""Test that slider works with exactly 500 marks"""
623+
app = Dash(__name__)
624+
marks_500 = {str(i): f"Mark {i}" for i in range(500)}
625+
app.layout = html.Div(
626+
[
627+
dcc.Slider(id="slider", min=0, max=499, marks=marks_500, value=250),
628+
html.Div(id="output"),
629+
]
630+
)
631+
632+
@app.callback(Output("output", "children"), [Input("slider", "value")])
633+
def update_output(value):
634+
return f"Selected: {value}"
635+
636+
dash_dcc.start_server(app)
637+
dash_dcc.wait_for_text_to_equal("#output", "Selected: 250")
638+
639+
# No warnings should be logged for 500 marks
640+
assert dash_dcc.get_logs() == []
641+
642+
643+
def test_slsl018_marks_limit_exceeded(dash_dcc):
644+
"""Test behavior when marks exceed 500 limit"""
645+
app = Dash(__name__)
646+
marks_501 = {str(i): f"Mark {i}" for i in range(501)}
647+
app.layout = html.Div(
648+
[
649+
dcc.Slider(id="slider", min=0, max=500, marks=marks_501, value=250),
650+
html.Div(id="output"),
651+
]
652+
)
653+
654+
@app.callback(Output("output", "children"), [Input("slider", "value")])
655+
def update_output(value):
656+
return f"Selected: {value}"
657+
658+
dash_dcc.start_server(app)
659+
dash_dcc.wait_for_text_to_equal("#output", "Selected: 250")
660+
661+
# Check that warning is logged
662+
logs = dash_dcc.get_logs()
663+
assert len(logs) > 0
664+
warning_found = any("Too many marks" in log["message"] for log in logs)
665+
assert warning_found, "Expected warning about too many marks not found in logs"

components/dash-table/.config/webpack/base.js

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ module.exports = (options = {}) => {
3131
type: 'window',
3232
}
3333
},
34-
devtool: 'source-map',
34+
devtool: mode === 'development' ? 'source-map' : false,
3535
externals: {
3636
react: 'React',
3737
'react-dom': 'ReactDOM',
@@ -53,31 +53,31 @@ module.exports = (options = {}) => {
5353
test: /\.ts(x?)$/,
5454
include: /node_modules[\\\/](highlight[.]js|d3-format)[\\\/]/,
5555
use: [
56-
{ loader: 'babel-loader', options: babel },
57-
{ loader: 'ts-loader', options: ts },
56+
{ loader: 'babel-loader', options: { ...babel, cacheDirectory: true } },
57+
{ loader: 'ts-loader', options: { ...ts, transpileOnly: true } },
5858
]
5959
},
6060
{
6161
test: /\.ts(x?)$/,
6262
exclude: /node_modules/,
6363
use: [
64-
{ loader: 'babel-loader', options: babel },
65-
{ loader: 'ts-loader', options: ts },
64+
{ loader: 'babel-loader', options: { ...babel, cacheDirectory: true } },
65+
{ loader: 'ts-loader', options: { ...ts, transpileOnly: true } },
6666
{ loader: 'webpack-preprocessor', options: JSON.stringify(preprocessor) }
6767
]
6868
},
6969
{
7070
test: /\.js$/,
7171
include: /node_modules[\\\/](highlight[.]js|d3-format)[\\\/]/,
7272
use: [
73-
{ loader: 'babel-loader', options: babel }
73+
{ loader: 'babel-loader', options: { ...babel, cacheDirectory: true } }
7474
]
7575
},
7676
{
7777
test: /\.js$/,
7878
exclude: /node_modules/,
7979
use: [
80-
{ loader: 'babel-loader', options: babel },
80+
{ loader: 'babel-loader', options: { ...babel, cacheDirectory: true } },
8181
{ loader: 'webpack-preprocessor', options: JSON.stringify(preprocessor) }
8282
]
8383
},
@@ -98,6 +98,12 @@ module.exports = (options = {}) => {
9898
}
9999
]
100100
},
101+
cache: {
102+
type: 'filesystem',
103+
buildDependencies: {
104+
config: [__filename]
105+
}
106+
},
101107
resolve: {
102108
alias: {
103109
'dash-table': path.resolve('./src/dash-table'),

0 commit comments

Comments
 (0)