Compare commits

..

1 Commits

Author SHA1 Message Date
Joe Li
7d6a421b68 fix(reports): escape LIKE wildcards in text filter and preserve typed screenshot width
The report/alert list text filter passed user input straight into ILIKE,
so % and _ were treated as wildcards. Add a local _escape_like() helper
(mirroring the daos/datasource.py precedent) and pass escape="\\" to each
filter clause so the characters match literally.

The ReportModal custom screenshot-width input used `|| null` / `|| ''`,
which silently coerced a typed 0 to "no custom width". Use `?? ''` for
display and an explicit Number.isNaN() check on change, so a typed 0 is
submitted and surfaced by the server's min-width validation instead of
being dropped on the client.

Add regression tests covering report DAO autoescape behavior and the
update command's extra.dashboard.activeTabs validation to lock in the
existing correct behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 11:04:19 -07:00
25 changed files with 400 additions and 415 deletions

View File

@@ -64,7 +64,7 @@ dependencies = [
"holidays>=0.45, <1",
"humanize",
"isodate",
"jsonpath-ng>=1.8.0, <2",
"jsonpath-ng>=1.6.1, <2",
"Mako>=1.2.2",
"markdown>=3.10.2",
# marshmallow>=4 has issues: https://github.com/apache/superset/issues/33162
@@ -94,7 +94,7 @@ dependencies = [
"PyJWT>=2.4.0, <3.0",
"redis>=5.0.0, <6.0",
"rison>=2.0.0, <3.0",
"selenium>=4.44.0, <5.0",
"selenium>=4.14.0, <5.0",
"shillelagh[gsheetsapi]>=1.4.4, <2.0",
"sshtunnel>=0.4.0, <0.5",
"simplejson>=3.15.0",
@@ -107,7 +107,7 @@ dependencies = [
"typing-extensions>=4, <5",
"waitress; sys_platform == 'win32'",
"watchdog>=6.0.0",
"wtforms>=3.2.2, <4",
"wtforms>=2.3.3, <4",
"wtforms-json",
"xlsxwriter>=3.2.9, <3.3",
]
@@ -121,7 +121,7 @@ bigquery = [
"sqlalchemy-bigquery>=1.15.0",
"google-cloud-bigquery>=3.10.0",
]
clickhouse = ["clickhouse-connect>=1.1.1, <2.0"]
clickhouse = ["clickhouse-connect>=0.13.0, <2.0"]
cockroachdb = ["cockroachdb>=0.3.5, <0.4"]
crate = ["sqlalchemy-cratedb>=0.41.0, <1"]
d1 = [
@@ -161,7 +161,7 @@ hive = [
"pyhive[hive]>=0.6.5;python_version<'3.11'",
"pyhive[hive_pure_sasl]>=0.7.0",
"tableschema",
"thrift>=0.23.0, <1.0.0",
"thrift>=0.14.1, <1.0.0",
"thrift_sasl>=0.4.3, < 1.0.0",
]
impala = ["impyla>0.16.2, <0.23"]
@@ -195,7 +195,7 @@ spark = [
"pyhive[hive]>=0.6.5;python_version<'3.11'",
"pyhive[hive_pure_sasl]>=0.7",
"tableschema",
"thrift>=0.23.0, <1",
"thrift>=0.14.1, <1",
]
tdengine = [
"taospy>=2.7.21",

View File

@@ -50,7 +50,7 @@ cattrs==25.1.1
# via requests-cache
celery==5.5.2
# via apache-superset (pyproject.toml)
certifi==2026.5.20
certifi==2025.6.15
# via
# requests
# selenium
@@ -194,7 +194,7 @@ jinja2==3.1.6
# via
# flask
# flask-babel
jsonpath-ng==1.8.0
jsonpath-ng==1.7.0
# via apache-superset (pyproject.toml)
jsonschema==4.23.0
# via
@@ -286,6 +286,8 @@ pillow==12.2.0
# via apache-superset (pyproject.toml)
platformdirs==4.3.8
# via requests-cache
ply==3.11
# via jsonpath-ng
polyline==2.0.2
# via apache-superset (pyproject.toml)
prison==0.2.1
@@ -378,7 +380,7 @@ rpds-py==0.25.0
# referencing
rsa==4.9.1
# via google-auth
selenium==4.44.0
selenium==4.32.0
# via apache-superset (pyproject.toml)
setuptools==80.9.0
# via -r requirements/base.in
@@ -421,7 +423,7 @@ sshtunnel==0.4.0
# via apache-superset (pyproject.toml)
tabulate==0.10.0
# via apache-superset (pyproject.toml)
trio==0.33.0
trio==0.30.0
# via
# selenium
# trio-websocket
@@ -478,7 +480,7 @@ wrapt==1.17.2
# via deprecated
wsproto==1.2.0
# via trio-websocket
wtforms==3.2.2
wtforms==3.2.1
# via
# apache-superset (pyproject.toml)
# flask-appbuilder

View File

@@ -112,7 +112,7 @@ celery==5.5.2
# via
# -c requirements/base-constraint.txt
# apache-superset
certifi==2026.5.20
certifi==2025.6.15
# via
# -c requirements/base-constraint.txt
# httpcore
@@ -471,7 +471,7 @@ jmespath==1.1.0
# via
# boto3
# botocore
jsonpath-ng==1.8.0
jsonpath-ng==1.7.0
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -674,6 +674,10 @@ platformdirs==4.3.8
# virtualenv
pluggy==1.5.0
# via pytest
ply==3.11
# via
# -c requirements/base-constraint.txt
# jsonpath-ng
polib==1.2.0
# via apache-superset
polyline==2.0.2
@@ -921,7 +925,7 @@ s3transfer==0.16.0
# via boto3
secretstorage==3.5.0
# via keyring
selenium==4.44.0
selenium==4.32.0
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -1019,7 +1023,7 @@ tqdm==4.67.1
# prophet
trino==0.330.0
# via apache-superset
trio==0.33.0
trio==0.30.0
# via
# -c requirements/base-constraint.txt
# selenium
@@ -1121,7 +1125,7 @@ wsproto==1.2.0
# via
# -c requirements/base-constraint.txt
# trio-websocket
wtforms==3.2.2
wtforms==3.2.1
# via
# -c requirements/base-constraint.txt
# apache-superset

View File

@@ -48,7 +48,6 @@ module.exports = {
'@babel/plugin-syntax-dynamic-import',
'@babel/plugin-transform-export-namespace-from',
['@babel/plugin-transform-class-properties', { loose: true }],
'@babel/plugin-transform-class-static-block',
['@babel/plugin-transform-optional-chaining', { loose: true }],
['@babel/plugin-transform-private-methods', { loose: true }],
['@babel/plugin-transform-nullish-coalescing-operator', { loose: true }],

View File

@@ -95,7 +95,7 @@
"echarts": "^5.6.0",
"fast-glob": "^3.3.2",
"fs-extra": "^11.3.5",
"fuse.js": "^7.4.1",
"fuse.js": "^7.3.0",
"geolib": "^3.3.14",
"geostyler": "^18.6.0",
"geostyler-data": "^1.1.0",
@@ -178,13 +178,13 @@
"@babel/types": "^7.29.7",
"@emotion/babel-plugin": "^11.13.5",
"@emotion/jest": "^11.14.2",
"@formatjs/intl-durationformat": "^0.10.13",
"@formatjs/intl-durationformat": "^0.10.3",
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@playwright/test": "^1.60.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
"@storybook/addon-docs": "10.4.2",
"@storybook/addon-links": "10.4.2",
"@storybook/react-webpack5": "10.4.2",
"@storybook/addon-docs": "10.4.1",
"@storybook/addon-links": "10.4.1",
"@storybook/react-webpack5": "10.4.1",
"@storybook/test-runner": "0.24.4",
"@svgr/webpack": "^8.1.0",
"@swc/core": "^1.15.40",
@@ -242,7 +242,7 @@
"eslint-plugin-prettier": "^5.5.6",
"eslint-plugin-react-prefer-function-component": "^5.0.0",
"eslint-plugin-react-you-might-not-need-an-effect": "^0.10.4",
"eslint-plugin-storybook": "10.4.2",
"eslint-plugin-storybook": "10.4.1",
"eslint-plugin-testing-library": "^7.16.2",
"eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
"fetch-mock": "^12.6.0",
@@ -272,7 +272,7 @@
"source-map": "^0.7.6",
"source-map-support": "^0.5.21",
"speed-measure-webpack-plugin": "^1.6.0",
"storybook": "10.4.2",
"storybook": "10.4.1",
"style-loader": "^4.0.0",
"swc-loader": "^0.2.7",
"terser-webpack-plugin": "^5.6.1",
@@ -3939,38 +3939,50 @@
}
},
"node_modules/@formatjs/bigdecimal": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/@formatjs/bigdecimal/-/bigdecimal-0.2.5.tgz",
"integrity": "sha512-2XTKNrZRaCUyXK2976wfutqxMBuPO/S/zbJnQdysLI2Zy5mWPVNVEkE6tsTcSVWSE7DgO88t8DtBy+uf3I8bxg==",
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@formatjs/bigdecimal/-/bigdecimal-0.2.0.tgz",
"integrity": "sha512-GeaxHZbUoYvHL9tC5eltHLs+1zU70aPw0s7LwqgktIzF5oMhNY4o4deEtusJMsq7WFJF3Ye2zQEzdG8beVk73w==",
"dev": true,
"license": "MIT"
},
"node_modules/@formatjs/ecma402-abstract": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-3.2.0.tgz",
"integrity": "sha512-dHnqHgBo6GXYGRsepaE1wmsC2etaivOWd5VaJstZd+HI2zR3DCUjbDVZRtoPGkkXZmyHvBwrdEUuqfvzhF/DtQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@formatjs/bigdecimal": "0.2.0",
"@formatjs/fast-memoize": "3.1.1",
"@formatjs/intl-localematcher": "0.8.2"
}
},
"node_modules/@formatjs/fast-memoize": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.5.tgz",
"integrity": "sha512-KLi3fan6WnCHmigd9pmEEN8Hid0v4wiFBW576M/d07KMWYecf1CvyMI3n34vCmHT4AoVqG2n702kiHbXjzZX2A==",
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.1.tgz",
"integrity": "sha512-CbNbf+tlJn1baRnPkNePnBqTLxGliG6DDgNa/UtV66abwIjwsliPMOt0172tzxABYzSuxZBZfcp//qI8AvBWPg==",
"dev": true,
"license": "MIT"
},
"node_modules/@formatjs/intl-durationformat": {
"version": "0.10.13",
"resolved": "https://registry.npmjs.org/@formatjs/intl-durationformat/-/intl-durationformat-0.10.13.tgz",
"integrity": "sha512-A1dBcOh1YrcRf/AbmZHFVXgIYkpAaFgyGaYavO/KutbqEXY3HI63o2E1ctmxmllfg3qn3TZGtZux42EFwHNTbg==",
"version": "0.10.3",
"resolved": "https://registry.npmjs.org/@formatjs/intl-durationformat/-/intl-durationformat-0.10.3.tgz",
"integrity": "sha512-xRS3GaOlsQLwz0n56SvaddwEnl2NLPKBvYg2M32ak/27dodmVxFJz3j7Nqj7EwKyHTu3f/e+BeoKPrIDUSXTuQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@formatjs/bigdecimal": "0.2.5",
"@formatjs/intl-localematcher": "0.8.9"
"@formatjs/ecma402-abstract": "3.2.0",
"@formatjs/intl-localematcher": "0.8.2"
}
},
"node_modules/@formatjs/intl-localematcher": {
"version": "0.8.9",
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.8.9.tgz",
"integrity": "sha512-GmB0F/gYh4Hdl4rLWjgDsgT+x4pB54fkJeRh8kAZ4XFzKeCK8dGs+SBJWXO42QZtOUni+IDWKNuCw6wiL4lTvw==",
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.8.2.tgz",
"integrity": "sha512-q05KMYGJLyqFNFtIb8NhWLF5X3aK/k0wYt7dnRFuy6aLQL+vUwQ1cg5cO4qawEiINybeCPXAWlprY2mSBjSXAQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@formatjs/fast-memoize": "3.1.5"
"@formatjs/fast-memoize": "3.1.1"
}
},
"node_modules/@gar/promise-retry": {
@@ -9684,16 +9696,16 @@
"license": "MIT"
},
"node_modules/@storybook/addon-docs": {
"version": "10.4.2",
"resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.4.2.tgz",
"integrity": "sha512-CtW1O4xSKZPNtpWgpfp4yB/x4pj/of+3MvlEDfErSlr3Hp3QmEa2pCLaecR08H5LJqJFlt1PtG0UrIynTvgW9w==",
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.4.1.tgz",
"integrity": "sha512-IYqUdjoZe4VO2LFZlKL/gwy7DsQSWCq6hX+zc1MBmZo04yycDASk1tte57n9pdlW3ajw9yYMF/+lVBi+xQjyvw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@mdx-js/react": "^3.0.0",
"@storybook/csf-plugin": "10.4.2",
"@storybook/csf-plugin": "10.4.1",
"@storybook/icons": "^2.0.2",
"@storybook/react-dom-shim": "10.4.2",
"@storybook/react-dom-shim": "10.4.1",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"ts-dedent": "^2.0.0"
@@ -9704,7 +9716,7 @@
},
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"storybook": "^10.4.2"
"storybook": "^10.4.1"
},
"peerDependenciesMeta": {
"@types/react": {
@@ -9712,10 +9724,45 @@
}
}
},
"node_modules/@storybook/addon-docs/node_modules/@storybook/csf-plugin": {
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.4.1.tgz",
"integrity": "sha512-WdPepGBxDGOUDjYd8KxMtcf+us/2PAcnBczl77XtrnxxHNs0jWesxKkiJ9yiuGrge4BPhDeAj6rxjbBoaHxLBA==",
"dev": true,
"license": "MIT",
"dependencies": {
"unplugin": "^2.3.5"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/storybook"
},
"peerDependencies": {
"esbuild": "*",
"rollup": "*",
"storybook": "^10.4.1",
"vite": "*",
"webpack": "*"
},
"peerDependenciesMeta": {
"esbuild": {
"optional": true
},
"rollup": {
"optional": true
},
"vite": {
"optional": true
},
"webpack": {
"optional": true
}
}
},
"node_modules/@storybook/addon-links": {
"version": "10.4.2",
"resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-10.4.2.tgz",
"integrity": "sha512-cU8h4/m+oAr8UUwF4teZG2N1ilV+vU+98Ii/Ma+IIx9M/V7i5544UxfAz84dV5Rx2Oho6x8XH3gIvmevSyPi/Q==",
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-10.4.1.tgz",
"integrity": "sha512-h/5D23GwMuHA55sB7XDyhByF9psF7UFmaQOn72pjNAarew5eOpue5A+jXk3AKEYokHbvgQaoz+FrvWo9GEfSKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -9728,7 +9775,7 @@
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"storybook": "^10.4.2"
"storybook": "^10.4.1"
},
"peerDependenciesMeta": {
"@types/react": {
@@ -9740,13 +9787,13 @@
}
},
"node_modules/@storybook/builder-webpack5": {
"version": "10.4.2",
"resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-10.4.2.tgz",
"integrity": "sha512-nhmV0+nThCgy1y5742SS7c4vJrd5/1KfCXCNfsJ1v4Rkq7NIQnUhEIBwkSaY63lqH7FRHlFxIjwGS63veiCJuw==",
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-10.4.1.tgz",
"integrity": "sha512-3Ah4jUjg8nEms/5JV6odtQj9+pQ1DT/04s/V6dZKThGdl85YTrYUZV5OTgbNxYbmQn/TwpWWjQlcW8ulpo2WBw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@storybook/core-webpack": "10.4.2",
"@storybook/core-webpack": "10.4.1",
"case-sensitive-paths-webpack-plugin": "^2.4.0",
"cjs-module-lexer": "^1.2.3",
"css-loader": "^7.1.2",
@@ -9767,7 +9814,7 @@
"url": "https://opencollective.com/storybook"
},
"peerDependencies": {
"storybook": "^10.4.2"
"storybook": "^10.4.1"
},
"peerDependenciesMeta": {
"typescript": {
@@ -9776,9 +9823,9 @@
}
},
"node_modules/@storybook/core-webpack": {
"version": "10.4.2",
"resolved": "https://registry.npmjs.org/@storybook/core-webpack/-/core-webpack-10.4.2.tgz",
"integrity": "sha512-qnYKMruU8lvI4yaq2PA9Gmxjrc7EZ3DRBI/cVKwEgOIREoxzr1F1IE7t7+325k9Phylue7E5rD3A7yjxeEKUyw==",
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/@storybook/core-webpack/-/core-webpack-10.4.1.tgz",
"integrity": "sha512-Wert/4ou5WRl8WYWWS8bBW7Lxa/ASMEuQ3EVuG3SITAtPNvKDKqTFBjZLx9eJSefkX6fJ3yG85FFUOPsv6GemQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -9789,42 +9836,7 @@
"url": "https://opencollective.com/storybook"
},
"peerDependencies": {
"storybook": "^10.4.2"
}
},
"node_modules/@storybook/csf-plugin": {
"version": "10.4.2",
"resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.4.2.tgz",
"integrity": "sha512-GqX/2DeF3/jKs5D7gpDiuT9gd0c/f2TKcnQ5av4/s3YqeN+0nhm7btkCrDfgF16uzE1Zj3OrkxvB3AOkfxWgDg==",
"dev": true,
"license": "MIT",
"dependencies": {
"unplugin": "^2.3.5"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/storybook"
},
"peerDependencies": {
"esbuild": "*",
"rollup": "*",
"storybook": "^10.4.2",
"vite": "*",
"webpack": "*"
},
"peerDependenciesMeta": {
"esbuild": {
"optional": true
},
"rollup": {
"optional": true
},
"vite": {
"optional": true
},
"webpack": {
"optional": true
}
"storybook": "^10.4.1"
}
},
"node_modules/@storybook/global": {
@@ -9846,13 +9858,13 @@
}
},
"node_modules/@storybook/preset-react-webpack": {
"version": "10.4.2",
"resolved": "https://registry.npmjs.org/@storybook/preset-react-webpack/-/preset-react-webpack-10.4.2.tgz",
"integrity": "sha512-21ld380f0/jTTitkfhTKgP3FBnVAgMu1P1ymrRyiFYJVSJBA5YejndFFBo0ugq9iGGsHXrVdOphC/OJKbTSWRQ==",
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/@storybook/preset-react-webpack/-/preset-react-webpack-10.4.1.tgz",
"integrity": "sha512-uAR/C/oDZYhReaYpD4Rd5S4VWcXP2XO8+BwXwanKt4UHbYfOw7AQgBTeZ/6Wns/0xIXhOoA1rxO5TA2wDLUjLA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@storybook/core-webpack": "10.4.2",
"@storybook/core-webpack": "10.4.1",
"@storybook/react-docgen-typescript-plugin": "1.0.6--canary.9.0c3f3b7.0",
"@types/semver": "^7.7.1",
"magic-string": "^0.30.5",
@@ -9869,7 +9881,7 @@
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"storybook": "^10.4.2"
"storybook": "^10.4.1"
},
"peerDependenciesMeta": {
"typescript": {
@@ -9878,14 +9890,14 @@
}
},
"node_modules/@storybook/react": {
"version": "10.4.2",
"resolved": "https://registry.npmjs.org/@storybook/react/-/react-10.4.2.tgz",
"integrity": "sha512-NfEH3CrdCAgUV4Z7SPN3Iw6nofcueqtRj8iHuo77GNjz0qSfuVi9iS7a8o7x7QFSeIBZwS0Jv3CgmhN8qvoLjg==",
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/@storybook/react/-/react-10.4.1.tgz",
"integrity": "sha512-WuYz4NaUk4gmFAMliSpCbV8w6jP5OY9juBfw1huwzu2S/k5FhnVXwmrUaL0fmf3Bq/7NgkzmBBbZr6I6LuHayQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@storybook/global": "^5.0.0",
"@storybook/react-dom-shim": "10.4.2",
"@storybook/react-dom-shim": "10.4.1",
"react-docgen": "^8.0.2",
"react-docgen-typescript": "^2.2.2"
},
@@ -9898,7 +9910,7 @@
"@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"storybook": "^10.4.2",
"storybook": "^10.4.1",
"typescript": ">= 4.9.x"
},
"peerDependenciesMeta": {
@@ -9978,9 +9990,9 @@
}
},
"node_modules/@storybook/react-dom-shim": {
"version": "10.4.2",
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.4.2.tgz",
"integrity": "sha512-Eng3Yt2NCjPX94QcfyLeUFhrMj0hec2yU9J/qafBVbfj9XrFI8o+0ZwYJ7uXb9ECbvPN4y06dgt/2W/LiR417w==",
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.4.1.tgz",
"integrity": "sha512-6QFqfDNH4DMrt7yHKRfpqRopsVUc/Az+sXIdJ39IetYnHUxL3nW4NVaPc6uy/8Qi8urzUyEXL/nn7cpSIP2aPQ==",
"dev": true,
"license": "MIT",
"funding": {
@@ -9992,7 +10004,7 @@
"@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"storybook": "^10.4.2"
"storybook": "^10.4.1"
},
"peerDependenciesMeta": {
"@types/react": {
@@ -10004,15 +10016,15 @@
}
},
"node_modules/@storybook/react-webpack5": {
"version": "10.4.2",
"resolved": "https://registry.npmjs.org/@storybook/react-webpack5/-/react-webpack5-10.4.2.tgz",
"integrity": "sha512-x7xwGLxU0w6/qi29/cHhua8qiCvfE05ku4pPLTXF8TsP/zfGsY8tbdlKO2+YKp+iBG8vafVc//ZXOAty1oypDA==",
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/@storybook/react-webpack5/-/react-webpack5-10.4.1.tgz",
"integrity": "sha512-2jF231DrEk70I8+wVakCnKtpweGFNfxdaov883Rve0TFvhxZs42Y9PpKzSf4rusvSrWc9jdWuJ2k7ERbS50MLg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@storybook/builder-webpack5": "10.4.2",
"@storybook/preset-react-webpack": "10.4.2",
"@storybook/react": "10.4.2"
"@storybook/builder-webpack5": "10.4.1",
"@storybook/preset-react-webpack": "10.4.1",
"@storybook/react": "10.4.1"
},
"funding": {
"type": "opencollective",
@@ -10021,7 +10033,7 @@
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"storybook": "^10.4.2",
"storybook": "^10.4.1",
"typescript": ">= 4.9.x"
},
"peerDependenciesMeta": {
@@ -19409,9 +19421,9 @@
}
},
"node_modules/eslint-plugin-storybook": {
"version": "10.4.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-10.4.2.tgz",
"integrity": "sha512-l3/vzLRmb8VSi3X1Bo6/Pa+64naw1jFsZE5jPPA4izvVdNhH1rF4rGuOC3kDTU926qKVBQtKua8D24XWQtvcGg==",
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-10.4.1.tgz",
"integrity": "sha512-sLEvd/7lg/LtXwMjj3iFxZtoeAC/8l1Qhuw3Noa8iF8i0UIgAejUs7k6DNSqHkwrPR8caWT4+3fxdMXs1iGLTg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -19419,7 +19431,7 @@
},
"peerDependencies": {
"eslint": ">=8",
"storybook": "^10.4.2"
"storybook": "^10.4.1"
}
},
"node_modules/eslint-plugin-testing-library": {
@@ -20921,9 +20933,9 @@
}
},
"node_modules/fuse.js": {
"version": "7.4.1",
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.4.1.tgz",
"integrity": "sha512-AY7lKAXK71hi3WgUvDy6oZL67UEHOOtvCAwVdOXHyJd6ZzftBy7QqxuXt4HxmmAhYjmp/YCuOELZtIvAdlZ+fw==",
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.4.0.tgz",
"integrity": "sha512-3UqmoSFwzX1sNB1YSk+Co0EdH29XCW2p9g48OAiy93cjKqzuABsqw2VIgSN3CmsT/wo6pIJ3F0Jxeiiby8rhIQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=10"
@@ -39036,9 +39048,9 @@
}
},
"node_modules/storybook": {
"version": "10.4.2",
"resolved": "https://registry.npmjs.org/storybook/-/storybook-10.4.2.tgz",
"integrity": "sha512-5Ax5vbHxFgMBGGhQDm75Rrumm/HZC4ICFhMcJaM0UlqnC/4FKj/IaZtImZFupknyiiyUEcWHPQFA2kX3/VSv1A==",
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/storybook/-/storybook-10.4.1.tgz",
"integrity": "sha512-V1Zd2e+gBFufqAQVZ1JR8KLqALsEZ3JYSBnWwQbKa6zCfWWanR6AFMyuOkLt2gZOgGp3h2Riuz88pGNVTQSG0A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -45363,7 +45375,7 @@
"@deck.gl/extensions": "~9.2.9",
"@deck.gl/geo-layers": "~9.2.5",
"@deck.gl/layers": "~9.2.5",
"@deck.gl/mapbox": "^9.3.3",
"@deck.gl/mapbox": "~9.3.2",
"@deck.gl/mesh-layers": "~9.2.5",
"@luma.gl/constants": "~9.2.5",
"@luma.gl/core": "~9.2.5",
@@ -45411,9 +45423,9 @@
}
},
"plugins/preset-chart-deckgl/node_modules/@deck.gl/mapbox": {
"version": "9.3.3",
"resolved": "https://registry.npmjs.org/@deck.gl/mapbox/-/mapbox-9.3.3.tgz",
"integrity": "sha512-aUPqrwF6wkx+EtvKA3SaiK+UROMnZSmgEJWZ1qSKFSiH//kPuo5imbtXyan8sGhOet7NjnfEwJqFA3EBk7zDLA==",
"version": "9.3.2",
"resolved": "https://registry.npmjs.org/@deck.gl/mapbox/-/mapbox-9.3.2.tgz",
"integrity": "sha512-+T9pJwsOXwjUxyGN6oiBMfIs28VtDIG1V1Rqz4qqn4TjjNEFFw+xO0olJIg8FO5IAqw2OtePdsrMj0tX8tHdGQ==",
"license": "MIT",
"dependencies": {
"@math.gl/web-mercator": "^4.1.0"

View File

@@ -178,7 +178,7 @@
"echarts": "^5.6.0",
"fast-glob": "^3.3.2",
"fs-extra": "^11.3.5",
"fuse.js": "^7.4.1",
"fuse.js": "^7.3.0",
"geolib": "^3.3.14",
"geostyler": "^18.6.0",
"geostyler-data": "^1.1.0",
@@ -261,13 +261,13 @@
"@babel/types": "^7.29.7",
"@emotion/babel-plugin": "^11.13.5",
"@emotion/jest": "^11.14.2",
"@formatjs/intl-durationformat": "^0.10.13",
"@formatjs/intl-durationformat": "^0.10.3",
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@playwright/test": "^1.60.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
"@storybook/addon-docs": "10.4.2",
"@storybook/addon-links": "10.4.2",
"@storybook/react-webpack5": "10.4.2",
"@storybook/addon-docs": "10.4.1",
"@storybook/addon-links": "10.4.1",
"@storybook/react-webpack5": "10.4.1",
"@storybook/test-runner": "0.24.4",
"@svgr/webpack": "^8.1.0",
"@swc/core": "^1.15.40",
@@ -325,7 +325,7 @@
"eslint-plugin-prettier": "^5.5.6",
"eslint-plugin-react-prefer-function-component": "^5.0.0",
"eslint-plugin-react-you-might-not-need-an-effect": "^0.10.4",
"eslint-plugin-storybook": "10.4.2",
"eslint-plugin-storybook": "10.4.1",
"eslint-plugin-testing-library": "^7.16.2",
"eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
"fetch-mock": "^12.6.0",
@@ -355,7 +355,7 @@
"source-map": "^0.7.6",
"source-map-support": "^0.5.21",
"speed-measure-webpack-plugin": "^1.6.0",
"storybook": "10.4.2",
"storybook": "10.4.1",
"style-loader": "^4.0.0",
"swc-loader": "^0.2.7",
"terser-webpack-plugin": "^5.6.1",

View File

@@ -43,7 +43,7 @@
"d3-time": "^3.1.0",
"d3-time-format": "^4.1.0",
"dayjs": "^1.11.21",
"dompurify": "^3.4.8",
"dompurify": "^3.4.7",
"fetch-retry": "^6.0.0",
"handlebars": "^4.7.9",
"jed": "^1.1.1",

View File

@@ -35,7 +35,7 @@ test('format milliseconds in human readable format with default options', () =>
});
test('format seconds in human readable format with default options', () => {
const formatter = createDurationFormatter({ multiplier: 1000 });
expect(formatter(-0.5)).toBe('0s');
expect(formatter(-0.5)).toBe('-0s');
expect(formatter(0.5)).toBe('0s');
expect(formatter(1)).toBe('1s');
expect(formatter(30)).toBe('30s');

View File

@@ -34,7 +34,7 @@
"fast-safe-stringify": "^2.1.1",
"lodash": "^4.18.1",
"nvd3-fork": "^2.0.5",
"dompurify": "^3.4.8",
"dompurify": "^3.4.7",
"prop-types": "^15.8.1",
"urijs": "^1.19.11"
},

View File

@@ -29,7 +29,7 @@
"@deck.gl/extensions": "~9.2.9",
"@deck.gl/geo-layers": "~9.2.5",
"@deck.gl/layers": "~9.2.5",
"@deck.gl/mapbox": "~9.3.3",
"@deck.gl/mapbox": "~9.3.2",
"@deck.gl/mesh-layers": "~9.2.5",
"@luma.gl/constants": "~9.2.5",
"@luma.gl/core": "~9.2.5",

View File

@@ -168,6 +168,31 @@ test('non-text chart shows screenshot width and message content', () => {
expect(screen.getByText('Screenshot width')).toBeInTheDocument();
});
test('screenshot width input preserves a typed zero instead of dropping it', () => {
const lineChartProps = {
...defaultProps,
dashboardId: undefined,
chart: { id: 1, sliceFormData: { viz_type: VizType.Line } },
chartName: 'My Line Chart',
creationMethod: 'charts' as const,
};
render(<ReportModal {...lineChartProps} />, { useRedux: true });
const widthInput = screen.getByPlaceholderText(
'Input custom width in pixels',
);
// The old `|| null` / `|| ''` logic silently coerced a typed 0 to null, so the
// invalid width was swallowed instead of being submitted and surfaced by the
// server's min-width validation. The field must preserve the literal value.
userEvent.type(widthInput, '0');
expect(widthInput).toHaveDisplayValue('0');
// Clearing the field still yields an empty value (parsed NaN → null).
userEvent.clear(widthInput);
expect(widthInput).toHaveDisplayValue('');
});
test('dashboard report hides message content section', () => {
const dashboardProps = {
...defaultProps,

View File

@@ -296,11 +296,12 @@ function ReportModal({
<Input
type="number"
name="custom_width"
value={currentReport?.custom_width || ''}
value={currentReport?.custom_width ?? ''}
placeholder={t('Input custom width in pixels')}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
const parsedWidth = parseInt(event.target.value, 10);
setCurrentReport({
custom_width: parseInt(event.target.value, 10) || null,
custom_width: Number.isNaN(parsedWidth) ? null : parsedWidth,
});
}}
/>

View File

@@ -25,7 +25,7 @@
"@types/lodash": "^4.17.24",
"@types/node": "^25.9.1",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^8.60.1",
"@typescript-eslint/eslint-plugin": "^8.60.0",
"@typescript-eslint/parser": "^8.60.1",
"eslint": "^10.4.1",
"eslint-config-prettier": "^10.1.8",

View File

@@ -33,7 +33,7 @@
"@types/lodash": "^4.17.24",
"@types/node": "^25.9.1",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^8.60.1",
"@typescript-eslint/eslint-plugin": "^8.60.0",
"@typescript-eslint/parser": "^8.60.1",
"eslint": "^10.4.1",
"eslint-config-prettier": "^10.1.8",

View File

@@ -13,7 +13,7 @@
"express": "~5.2.1",
"http-errors": "~2.0.1",
"jsonwebtoken": "^9.0.3",
"morgan": "~1.11.0",
"morgan": "~1.10.1",
"pug": "~3.0.4"
}
},
@@ -127,6 +127,17 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/body-parser/node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
@@ -444,6 +455,17 @@
"node": ">=6.6.0"
}
},
"node_modules/express/node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/finalhandler": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
@@ -461,6 +483,18 @@
"node": ">= 0.8"
}
},
"node_modules/finalhandler/node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"license": "MIT",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -833,22 +867,18 @@
}
},
"node_modules/morgan": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.11.0.tgz",
"integrity": "sha512-zSkVu3t18r39pw4ixfBKvfZi3y2UOqr7d4WYwcj3m8nXpEQK4rPO6GLzs/CExoRgmX3y9EjmmcXqv6jq0SK46g==",
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz",
"integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==",
"dependencies": {
"basic-auth": "~2.0.1",
"debug": "2.6.9",
"depd": "~2.0.0",
"on-finished": "~2.4.1",
"on-finished": "~2.3.0",
"on-headers": "~1.1.0"
},
"engines": {
"node": ">= 0.8.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/morgan/node_modules/debug": {
@@ -898,9 +928,9 @@
}
},
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
"integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
"dependencies": {
"ee-first": "1.1.1"
},
@@ -1197,6 +1227,18 @@
"node": ">= 18"
}
},
"node_modules/send/node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"license": "MIT",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/serve-static": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
@@ -1467,6 +1509,16 @@
"qs": "^6.14.0",
"raw-body": "^3.0.1",
"type-is": "^2.0.1"
},
"dependencies": {
"on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"requires": {
"ee-first": "1.1.1"
}
}
}
},
"buffer-equal-constant-time": {
@@ -1688,6 +1740,14 @@
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
"integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="
},
"on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"requires": {
"ee-first": "1.1.1"
}
}
}
},
@@ -1702,6 +1762,16 @@
"on-finished": "^2.4.1",
"parseurl": "^1.3.3",
"statuses": "^2.0.1"
},
"dependencies": {
"on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"requires": {
"ee-first": "1.1.1"
}
}
}
},
"forwarded": {
@@ -1970,14 +2040,14 @@
}
},
"morgan": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.11.0.tgz",
"integrity": "sha512-zSkVu3t18r39pw4ixfBKvfZi3y2UOqr7d4WYwcj3m8nXpEQK4rPO6GLzs/CExoRgmX3y9EjmmcXqv6jq0SK46g==",
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz",
"integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==",
"requires": {
"basic-auth": "~2.0.1",
"debug": "2.6.9",
"depd": "~2.0.0",
"on-finished": "~2.4.1",
"on-finished": "~2.3.0",
"on-headers": "~1.1.0"
},
"dependencies": {
@@ -2017,9 +2087,9 @@
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="
},
"on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
"integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
"requires": {
"ee-first": "1.1.1"
}
@@ -2267,6 +2337,16 @@
"on-finished": "^2.4.1",
"range-parser": "^1.2.1",
"statuses": "^2.0.1"
},
"dependencies": {
"on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"requires": {
"ee-first": "1.1.1"
}
}
}
},
"serve-static": {

View File

@@ -11,7 +11,7 @@
"express": "~5.2.1",
"http-errors": "~2.0.1",
"jsonwebtoken": "^9.0.3",
"morgan": "~1.11.0",
"morgan": "~1.10.1",
"pug": "~3.0.4"
}
}

View File

@@ -103,19 +103,6 @@ class DatasourceTypeUpdateRequiredValidationError(ValidationError):
)
class ChartQueryContextDatasourceMismatchValidationError(ValidationError):
"""
Raised when a query-context-only update carries a datasource that does not
match the chart's own datasource.
"""
def __init__(self) -> None:
super().__init__(
_("The query context datasource does not match the chart datasource"),
field_name="query_context",
)
class ChartNotFoundError(CommandException):
message = "Chart not found."

View File

@@ -29,7 +29,6 @@ from superset.commands.chart.exceptions import (
ChartForbiddenError,
ChartInvalidError,
ChartNotFoundError,
ChartQueryContextDatasourceMismatchValidationError,
ChartUpdateFailedError,
DashboardsForbiddenError,
DashboardsNotFoundValidationError,
@@ -42,7 +41,6 @@ from superset.exceptions import SupersetSecurityException
from superset.models.dashboard import Dashboard
from superset.models.slice import Slice
from superset.tags.models import ObjectType
from superset.utils import json
from superset.utils.decorators import on_error, transaction
logger = logging.getLogger(__name__)
@@ -103,51 +101,6 @@ class UpdateChartCommand(UpdateMixin, BaseCommand):
if not security_manager.is_owner(dash):
raise DashboardsForbiddenError()
def _validate_query_context_datasource(
self, exceptions: list[ValidationError]
) -> None:
"""
Ensure a query-context-only update keeps the chart's own datasource.
The submitted query context is only verified when it carries a parseable
``datasource`` object; a payload that references a different datasource than
the chart's persisted one is rejected. Payloads without a datasource fall
back to the chart's datasource at execution time and need no check.
"""
if not self._model:
return
raw_query_context = self._properties.get("query_context")
if not raw_query_context:
return
try:
query_context = json.loads(raw_query_context)
except (TypeError, ValueError):
# An unparseable payload cannot be verified or replayed; leave it for
# downstream handling rather than guessing at its intent.
return
datasource = (
query_context.get("datasource") if isinstance(query_context, dict) else None
)
if not isinstance(datasource, dict):
return
try:
ids_match = int(datasource["id"]) == self._model.datasource_id
except (KeyError, TypeError, ValueError):
ids_match = False
datasource_type = datasource.get("type")
types_match = (
datasource_type is None
or str(datasource_type) == self._model.datasource_type
)
if not ids_match or not types_match:
exceptions.append(ChartQueryContextDatasourceMismatchValidationError())
def validate(self) -> None: # noqa: C901
exceptions: list[ValidationError] = []
dashboard_ids = self._properties.get("dashboards")
@@ -181,12 +134,6 @@ class UpdateChartCommand(UpdateMixin, BaseCommand):
raise ChartForbiddenError() from ex
except ValidationError as ex:
exceptions.append(ex)
else:
# The query-context-only path skips the ownership check so report and
# alert workers can refresh a chart's cached payload. Keep that payload
# bound to the chart's own datasource so it cannot be repointed at an
# unrelated one.
self._validate_query_context_datasource(exceptions)
# validate tags
try:

View File

@@ -25,6 +25,14 @@ from superset.reports.models import ReportSchedule
from superset.views.base import BaseFilter
def _escape_like(value: str) -> str:
"""
Escape LIKE/ILIKE wildcard characters so user-supplied search text is matched
literally instead of being interpreted as wildcards (e.g. ``%`` and ``_``).
"""
return value.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
class ReportScheduleFilter(BaseFilter): # pylint: disable=too-few-public-methods
def apply(self, query: Query, value: Any) -> Query:
if security_manager.can_access_all_datasources():
@@ -47,11 +55,11 @@ class ReportScheduleAllTextFilter(BaseFilter): # pylint: disable=too-few-public
def apply(self, query: Query, value: Any) -> Query:
if not value:
return query
ilike_value = f"%{value}%"
ilike_value = f"%{_escape_like(value)}%"
return query.filter(
or_(
ReportSchedule.name.ilike(ilike_value),
ReportSchedule.description.ilike(ilike_value),
ReportSchedule.sql.ilike(ilike_value),
ReportSchedule.name.ilike(ilike_value, escape="\\"),
ReportSchedule.description.ilike(ilike_value, escape="\\"),
ReportSchedule.sql.ilike(ilike_value, escape="\\"),
)
)

View File

@@ -26,7 +26,6 @@ import pandas as pd
import simplejson
from flask_babel.speaklater import LazyString
from jsonpath_ng import parse
from jsonpath_ng.jsonpath import Child, Fields, Root
from simplejson import JSONDecodeError
from superset.constants import PASSWORD_MASK
@@ -305,30 +304,6 @@ def reveal_sensitive(
return revealed_payload
def _render_jsonpath(node: Any) -> str:
"""
Render a JSONPath node as a stable dotted string (e.g. ``foo.bar``).
``str()`` of a jsonpath-ng path is not stable across releases: as of
jsonpath-ng 1.8.0 a ``Child`` node renders with surrounding parentheses
(e.g. ``(foo.bar)``). This helper produces the historic dotted notation so
that the strings returned by :func:`get_masked_fields` remain consistent and
can be round-tripped back through ``parse``.
Falls back to ``str()`` for node kinds that aren't plain field access (e.g.
array indices, slices), preserving their existing representation.
"""
if isinstance(node, Child):
left = _render_jsonpath(node.left)
right = _render_jsonpath(node.right)
return f"{left}.{right}" if left else right
if isinstance(node, Fields):
return ".".join(node.fields)
if isinstance(node, Root):
return ""
return str(node)
def get_masked_fields(
payload: dict[str, Any],
sensitive_fields: set[str],
@@ -346,10 +321,8 @@ def get_masked_fields(
for match in jsonpath_expr.find(payload):
if match.value == PASSWORD_MASK:
# Using `match.full_path` instead of json_path to account
# for wildcards. Render the path explicitly so the output is
# stable across jsonpath-ng versions (newer releases wrap
# `Child` paths in parentheses when stringified).
masked.append(f"$.{_render_jsonpath(match.full_path)}")
# for wildcards
masked.append(f"$.{match.full_path}")
return masked

View File

@@ -17,11 +17,10 @@
import pytest
from pytest_mock import MockerFixture
from superset.commands.chart.exceptions import ChartForbiddenError, ChartInvalidError
from superset.commands.chart.exceptions import ChartForbiddenError
from superset.commands.chart.update import UpdateChartCommand
from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
from superset.exceptions import SupersetSecurityException
from superset.utils import json
def _ownership_exc() -> SupersetSecurityException:
@@ -92,73 +91,3 @@ def test_update_chart_owner_can_perform_regular_update(
find_by_id.assert_called_once_with(1)
raise_for_ownership.assert_called_once()
def _query_context_payload(datasource: object) -> dict[str, object]:
return {
"query_context": json.dumps({"datasource": datasource, "queries": []}),
"query_context_generation": True,
}
def test_update_chart_query_context_matching_datasource_is_allowed(
mocker: MockerFixture,
) -> None:
"""A query context that targets the chart's own datasource is accepted."""
find_by_id = mocker.patch("superset.commands.chart.update.ChartDAO.find_by_id")
find_by_id.return_value = mocker.MagicMock(
id=1, tags=[], dashboards=[], datasource_id=42, datasource_type="table"
)
mocker.patch("superset.commands.chart.update.security_manager.raise_for_ownership")
UpdateChartCommand(
1, _query_context_payload({"id": 42, "type": "table"})
).validate()
@pytest.mark.parametrize(
"datasource",
[
{"id": 99, "type": "table"}, # different id
{"id": 42, "type": "query"}, # different type
{"id": "99", "type": "table"}, # different id as string
],
)
def test_update_chart_query_context_mismatched_datasource_is_rejected(
mocker: MockerFixture,
datasource: dict[str, object],
) -> None:
"""A query context pointing at a different datasource is rejected with a 4xx."""
find_by_id = mocker.patch("superset.commands.chart.update.ChartDAO.find_by_id")
find_by_id.return_value = mocker.MagicMock(
id=1, tags=[], dashboards=[], datasource_id=42, datasource_type="table"
)
mocker.patch("superset.commands.chart.update.security_manager.raise_for_ownership")
with pytest.raises(ChartInvalidError):
UpdateChartCommand(1, _query_context_payload(datasource)).validate()
@pytest.mark.parametrize(
"query_context",
[
"{}", # no datasource key
'{"datasource": null}', # null datasource
"not-json", # unparseable payload
],
)
def test_update_chart_query_context_without_datasource_is_allowed(
mocker: MockerFixture,
query_context: str,
) -> None:
"""Payloads with no verifiable datasource fall back to the chart's own."""
find_by_id = mocker.patch("superset.commands.chart.update.ChartDAO.find_by_id")
find_by_id.return_value = mocker.MagicMock(
id=1, tags=[], dashboards=[], datasource_id=42, datasource_type="table"
)
mocker.patch("superset.commands.chart.update.security_manager.raise_for_ownership")
UpdateChartCommand(
1,
{"query_context": query_context, "query_context_generation": True},
).validate()

View File

@@ -447,6 +447,43 @@ def test_ownership_check_raises_forbidden(mocker: MockerFixture) -> None:
cmd.validate()
# --- Dashboard extra (activeTabs) validation on update ---
def test_update_rejects_invalid_active_tab_ids(mocker: MockerFixture) -> None:
"""On PUT, activeTabs must be validated against the model's dashboard layout.
The dashboard is not in the payload, so validation must fall back to the
existing model's dashboard; tab ids absent from position_json are rejected.
"""
model = _make_model(mocker, model_type=ReportScheduleType.REPORT, database_id=None)
model.dashboard.position_json = '{"TAB-valid": {}}'
_setup_mocks(mocker, model)
cmd = UpdateReportScheduleCommand(
model_id=1,
data={"extra": {"dashboard": {"activeTabs": ["TAB-missing"]}}},
)
with pytest.raises(ReportScheduleInvalidError) as exc_info:
cmd.validate()
messages = _get_validation_messages(exc_info)
assert "extra" in messages
assert "invalid tab ids" in messages["extra"].lower()
def test_update_accepts_valid_active_tab_ids(mocker: MockerFixture) -> None:
"""A tab id present in the model dashboard's position_json passes validation."""
model = _make_model(mocker, model_type=ReportScheduleType.REPORT, database_id=None)
model.dashboard.position_json = '{"TAB-valid": {}}'
_setup_mocks(mocker, model)
cmd = UpdateReportScheduleCommand(
model_id=1,
data={"extra": {"dashboard": {"activeTabs": ["TAB-valid"]}}},
)
cmd.validate() # should not raise
# --- Database not found for alert ---

View File

@@ -89,3 +89,42 @@ def test_find_last_error_notification_returns_log_when_only_errors(
result = ReportScheduleDAO.find_last_error_notification(schedule)
assert result is error_log
@patch("superset.daos.report.ReportSchedule")
@patch("superset.daos.report.db")
def test_find_by_extra_metadata_escapes_like_wildcards(
mock_db: MagicMock, mock_report_schedule: MagicMock
) -> None:
"""LIKE wildcards in the slug must be escaped so they match literally."""
from superset.daos.report import ReportScheduleDAO
expected: list[MagicMock] = [MagicMock()]
mock_db.session.query.return_value.filter.return_value.all.return_value = expected
result = ReportScheduleDAO.find_by_extra_metadata("100%_off")
assert result is expected
# autoescape=True is what neutralises the LIKE wildcards in the slug
mock_report_schedule.extra_json.contains.assert_called_once_with(
"100%_off", autoescape=True
)
@patch("superset.daos.report.ReportSchedule")
@patch("superset.daos.report.db")
def test_find_by_native_filter_id_escapes_like_wildcards(
mock_db: MagicMock, mock_report_schedule: MagicMock
) -> None:
"""LIKE wildcards in the filter id must be escaped so they match literally."""
from superset.daos.report import ReportScheduleDAO
expected: list[MagicMock] = [MagicMock()]
mock_db.session.query.return_value.filter.return_value.all.return_value = expected
result = ReportScheduleDAO.find_by_native_filter_id("NATIVE_FILTER-%_x")
assert result is expected
mock_report_schedule.extra_json.contains.assert_called_once_with(
"NATIVE_FILTER-%_x", autoescape=True
)

View File

@@ -62,3 +62,26 @@ def test_report_schedule_all_text_filter_applies_ilike() -> None:
f = ReportScheduleAllTextFilter("name", MagicMock())
f.apply(query, "test")
query.filter.assert_called_once()
@patch("superset.reports.filters.or_")
@patch("superset.reports.filters.ReportSchedule")
def test_report_schedule_all_text_filter_escapes_wildcards(
mock_report_schedule: MagicMock, mock_or: MagicMock
) -> None:
"""User-supplied wildcards must be escaped so they match literally."""
from superset.reports.filters import ReportScheduleAllTextFilter
query = MagicMock()
f = ReportScheduleAllTextFilter("name", MagicMock())
# raw input contains every LIKE special character plus a backslash
f.apply(query, "50%_off\\promo")
# %, _ and \ are all escaped, and the literal is wrapped for a "contains" match
expected = "%50\\%\\_off\\\\promo%"
for column in (
mock_report_schedule.name,
mock_report_schedule.description,
mock_report_schedule.sql,
):
column.ilike.assert_called_once_with(expected, escape="\\")

View File

@@ -1,81 +0,0 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from superset.utils.core import split
def test_split_empty_string():
assert list(split("")) == [""]
def test_split_leading_delimiter():
assert list(split(" a")) == [
"",
"a",
]
def test_split_trailing_delimiter():
assert list(split("a ")) == [
"a",
"",
]
def test_split_only_delimiter():
assert list(split(" ")) == [
"",
"",
]
def test_split_nested_parentheses():
assert list(
split(
"a,(b,(c,d))",
delimiter=",",
)
) == [
"a",
"(b,(c,d))",
]
def test_branch_separator_found():
assert list(split("a b")) == [
"a",
"b",
]
def test_branch_separator_not_found():
assert list(split("ab")) == [
"ab",
]
def test_branch_parentheses():
assert list(split("(a b)")) == [
"(a b)",
]
def test_branch_escaped_quote():
assert list(split(r'"a\"b c" d')) == [
r'"a\"b c"',
"d",
]