Compare commits

...

13 Commits

Author SHA1 Message Date
J0s3-H3nr1qu3
277c814c5a fix(i18n): review and complete Portuguese (pt_PT) translation catalog (#42137)
Co-authored-by: José Teixeira <jose.teixeira@douroeci.com>
2026-07-20 14:37:43 +01:00
dependabot[bot]
7e63b29741 chore(deps): bump antd from 6.5.0 to 6.5.1 in /docs (#42226)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-20 03:56:10 -07:00
dependabot[bot]
b5ae7cbd0d chore(deps): bump caniuse-lite from 1.0.30001803 to 1.0.30001805 in /docs (#42228)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-20 03:56:03 -07:00
dependabot[bot]
e044d7783a chore(deps): bump dompurify from 3.4.11 to 3.4.12 in /superset-frontend (#42231)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-20 03:55:59 -07:00
dependabot[bot]
9e8b9ac8cf chore(deps-dev): bump tsx from 4.23.0 to 4.23.1 in /superset-frontend (#42232)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-20 03:55:55 -07:00
dependabot[bot]
19588826c8 chore(deps-dev): bump baseline-browser-mapping from 2.10.42 to 2.10.43 in /superset-frontend (#42233)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-20 03:55:50 -07:00
dependabot[bot]
8b415502cf chore(deps): bump chrono-node from 2.9.1 to 2.10.0 in /superset-frontend (#42235)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-20 03:55:43 -07:00
Rehan Islam
09148d0af6 fix(time-table): correctly sort computed columns (#41976) 2026-07-19 20:56:53 -07:00
Shaitan
3e40bebba8 fix(import): enforce write permission for objects created by legacy import paths (#42205)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 20:50:58 -07:00
Hans Yu
36dbff091f refactor: do not cascade to TaggedObject (#42223) 2026-07-19 20:18:53 -07:00
David
9e9b7c4bbd feat(ci): run helm-unittest on pull requests (#42206) 2026-07-20 07:36:19 +07:00
dependabot[bot]
42a2aede78 chore(deps): bump pandas from 2.1.4 to 2.3.3 (#42192)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 15:42:02 -07:00
lguichard78
c4d50472a9 fix(login): oauth and ldap login ignoring superset_app_root (#34657) 2026-07-18 14:58:22 -07:00
28 changed files with 3507 additions and 6867 deletions

View File

@@ -31,7 +31,7 @@ jobs:
- name: Set up Helm
uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5.0.1
with:
version: v3.16.4
version: v3.21.3
- name: Setup Python
uses: ./.github/actions/setup-backend/
@@ -59,3 +59,9 @@ jobs:
CT_LINT_CONF: lintconf.yaml
CT_SINCE: HEAD
CT_CHART_REPOS: bitnami=https://charts.bitnami.com/bitnami
- name: Set up helm-unittest
run: helm plugin install https://github.com/helm-unittest/helm-unittest --version v1.1.1
- name: Run helm-unittest
run: helm unittest helm/superset

View File

@@ -61,9 +61,9 @@
"@storybook/addon-docs": "^10.5.0",
"@superset-ui/core": "^0.20.4",
"@swc/core": "^1.15.43",
"antd": "^6.5.0",
"antd": "^6.5.1",
"baseline-browser-mapping": "^2.10.40",
"caniuse-lite": "^1.0.30001803",
"caniuse-lite": "^1.0.30001805",
"docusaurus-plugin-openapi-docs": "^5.1.2",
"docusaurus-theme-openapi-docs": "^5.1.2",
"js-yaml": "^5.2.0",

View File

@@ -227,7 +227,7 @@
resolved "https://registry.yarnpkg.com/@ant-design/icons-svg/-/icons-svg-4.5.0.tgz#7b1c567e489840d747f211d3688949bcba363ad2"
integrity sha512-1BTUFyKPTBZ53MuTP8s0k5SFEXL7o3VHEOwLgzaoWKwnBeqIcqUtVshc4SKzhI6uACfqhJqBwBUE9FsWR3uULA==
"@ant-design/icons@^6.2.5", "@ant-design/icons@^6.3.1":
"@ant-design/icons@^6.2.5", "@ant-design/icons@^6.3.2":
version "6.3.2"
resolved "https://registry.yarnpkg.com/@ant-design/icons/-/icons-6.3.2.tgz#8291dffc53003db9a5df59f80ed758473cd5c8df"
integrity sha512-B6O5a5XJ4wjtNOfZejXYwHW5zvKV5gYkjGf11dHGLEbKn0ABDGndo41+gfIiXyTFhvESj4XTotuud33mUFid0g==
@@ -3506,13 +3506,13 @@
"@rc-component/util" "^1.9.0"
clsx "^2.1.1"
"@rc-component/dropdown@~1.0.0", "@rc-component/dropdown@~1.0.2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@rc-component/dropdown/-/dropdown-1.0.2.tgz#c6010dac9e3ce0d7cf305523083d499dc779819e"
integrity sha512-6PY2ecUSYhDPhkNHHb4wfeAya04WhpmUSKzdR60G+kMNVUCX2vjT/AgTS0Lz0I/K6xrPMJ3enQbwVpeN3sHCgg==
"@rc-component/dropdown@~1.0.0", "@rc-component/dropdown@~1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@rc-component/dropdown/-/dropdown-1.0.3.tgz#9825e55df54b66284b2630a16ec8281244dc3236"
integrity sha512-YTST/N6kpqpDz3IMuM/PSSZnrDpSOA6dgHv12gPA90ZTSLv2CoqkZ0+9NtwTY6BeO7dstPblSic2QJg7dSFy/g==
dependencies:
"@rc-component/trigger" "^3.0.0"
"@rc-component/util" "^1.2.1"
"@rc-component/util" "^1.11.1"
clsx "^2.1.1"
"@rc-component/form@~1.8.5":
@@ -3634,7 +3634,7 @@
"@rc-component/util" "^1.11.1"
clsx "^2.1.1"
"@rc-component/portal@^2.1.0", "@rc-component/portal@^2.1.2", "@rc-component/portal@^2.1.3", "@rc-component/portal@^2.2.0":
"@rc-component/portal@^2.1.0", "@rc-component/portal@^2.1.2", "@rc-component/portal@^2.1.3", "@rc-component/portal@^2.2.0", "@rc-component/portal@^2.2.1":
version "2.2.1"
resolved "https://registry.yarnpkg.com/@rc-component/portal/-/portal-2.2.1.tgz#37c34b4c8cd73f53cc7072c96dd0e9ac332669ec"
integrity sha512-ck+r1kW/JSv0wxPji3KN2ss9K6Z0qqwusw/mf/0JobXhZ8hC2ejZwCJObW/SvDi0uhA0VzmCnx0CaCci95tcmA==
@@ -3717,10 +3717,10 @@
"@rc-component/util" "^1.3.0"
clsx "^2.1.1"
"@rc-component/table@~1.10.2":
version "1.10.2"
resolved "https://registry.yarnpkg.com/@rc-component/table/-/table-1.10.2.tgz#7b052fd5eb2ccf6996a93eebeb7af7036a262c8b"
integrity sha512-b3PjqB9Gp25p5t/zq+9QrbXbodkptT8/zvLmwgd2FNPUUtaYyDnQqfxeD5a7ao8E8lpinLHsi2u2vdfPhyNvAw==
"@rc-component/table@~1.10.4":
version "1.10.4"
resolved "https://registry.yarnpkg.com/@rc-component/table/-/table-1.10.4.tgz#8c4e33bc150aa39f579c15426421348a789de326"
integrity sha512-HwoTnrwc29zeoXkXGhWqzJh8FIibGUxi1jM4LtoSzmR9d5Vv5osUQpZxnXKBP8iOCvyD6BQzZm1nXJRcnrxpAg==
dependencies:
"@rc-component/context" "^2.0.1"
"@rc-component/resize-observer" "^1.0.0"
@@ -3779,15 +3779,15 @@
"@rc-component/virtual-list" "^1.2.0"
clsx "^2.1.1"
"@rc-component/trigger@^3.0.0", "@rc-component/trigger@^3.6.15", "@rc-component/trigger@^3.7.1", "@rc-component/trigger@^3.9.1":
version "3.9.1"
resolved "https://registry.yarnpkg.com/@rc-component/trigger/-/trigger-3.9.1.tgz#3f730315c558bc392921a563ac9109a1d094ef23"
integrity sha512-LNsYvz60mrLJ/kRvKcHE7boUvcQfVMCfRqZ71x3Fo9AOiZ1KKIEqkzMA8DNvz2V3Bcvir/vwQNn7JF1NPODQ7Q==
"@rc-component/trigger@^3.0.0", "@rc-component/trigger@^3.10.0", "@rc-component/trigger@^3.6.15", "@rc-component/trigger@^3.7.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@rc-component/trigger/-/trigger-3.10.1.tgz#cb28e1bc0745a2af6897dd7ec774f9b56dc88f86"
integrity sha512-mXlDN0IXdtV8Yqqm8195ECCyrbmfvvfKvwVvSlH0+qvKD6BUF8gRhEjSy0FOcD1+CcDRHgTiX99LoxfQrmh3Cw==
dependencies:
"@rc-component/motion" "^1.1.4"
"@rc-component/portal" "^2.2.0"
"@rc-component/resize-observer" "^1.1.1"
"@rc-component/util" "^1.2.1"
"@rc-component/motion" "^1.3.3"
"@rc-component/portal" "^2.2.1"
"@rc-component/resize-observer" "^1.1.2"
"@rc-component/util" "^1.11.1"
clsx "^2.1.1"
"@rc-component/upload@~1.1.1":
@@ -6124,16 +6124,16 @@ ansis@^3.2.0:
resolved "https://registry.yarnpkg.com/ansis/-/ansis-3.17.0.tgz#fa8d9c2a93fe7d1177e0c17f9eeb562a58a832d7"
integrity sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==
antd@^6.5.0:
version "6.5.0"
resolved "https://registry.yarnpkg.com/antd/-/antd-6.5.0.tgz#29d3de40f354ab965e201f79c2d0e7658cecc655"
integrity sha512-9zbVc9UukfGuqCvIAov01nlpDQWfARNmZQyt21ZhqLX7ilXmi4cdkp12xA48WEmXRXwZvno8A03qQuGE9JG8fg==
antd@^6.5.1:
version "6.5.1"
resolved "https://registry.yarnpkg.com/antd/-/antd-6.5.1.tgz#2623db1f3b0ae32a2e311ef20f2f1413934d0094"
integrity sha512-VZVVF9zYI6S0NHqboVhCoY9Iiqj6dphW1NPB+sEaAf2HuIQ0haXWXj7ZvAXTRDzusktV6+cvvrSZEdRi4twATg==
dependencies:
"@ant-design/colors" "^8.0.1"
"@ant-design/cssinjs" "^2.1.2"
"@ant-design/cssinjs-utils" "^2.1.2"
"@ant-design/fast-color" "^3.0.1"
"@ant-design/icons" "^6.3.1"
"@ant-design/icons" "^6.3.2"
"@ant-design/react-slick" "~2.0.0"
"@babel/runtime" "^7.29.2"
"@rc-component/cascader" "~1.17.0"
@@ -6142,7 +6142,7 @@ antd@^6.5.0:
"@rc-component/color-picker" "~3.1.1"
"@rc-component/dialog" "~1.10.0"
"@rc-component/drawer" "~1.4.2"
"@rc-component/dropdown" "~1.0.2"
"@rc-component/dropdown" "~1.0.3"
"@rc-component/form" "~1.8.5"
"@rc-component/image" "~1.9.0"
"@rc-component/input" "~1.3.1"
@@ -6163,13 +6163,13 @@ antd@^6.5.0:
"@rc-component/slider" "~1.1.1"
"@rc-component/steps" "~1.2.2"
"@rc-component/switch" "~1.0.3"
"@rc-component/table" "~1.10.2"
"@rc-component/table" "~1.10.4"
"@rc-component/tabs" "~1.11.0"
"@rc-component/tooltip" "~1.4.0"
"@rc-component/tour" "~2.4.0"
"@rc-component/tree" "~1.3.2"
"@rc-component/tree-select" "~1.11.0"
"@rc-component/trigger" "^3.9.1"
"@rc-component/trigger" "^3.10.0"
"@rc-component/upload" "~1.1.1"
"@rc-component/util" "^1.11.1"
clsx "^2.1.1"
@@ -6698,10 +6698,10 @@ caniuse-api@^3.0.0:
lodash.memoize "^4.1.2"
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001799, caniuse-lite@^1.0.30001803:
version "1.0.30001803"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001803.tgz#b2a5d696e042bc8304dcd4942c39fe330fbbcb24"
integrity sha512-g/uHREV2ZpK9qMalCsWaxmA6ol+DX8GYhuf3T40RKoP+oL7vhRJh8LNt73PCjpnR6l14FzfPrB5Yux4PKm2meg==
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001799, caniuse-lite@^1.0.30001805:
version "1.0.30001805"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001805.tgz#78d5d5968a69b7ff81af87a96d7ddc7ea6670b1e"
integrity sha512-52noaS3DubycKSXaU30TwPGIp+POyQSUVa5jBEq3vkRkY0kjyb3LQgvhU6WGyCcyXqVLWO0Cw0Q6BSdD0kUfVA==
ccount@^2.0.0:
version "2.0.1"

View File

@@ -84,7 +84,7 @@ dependencies = [
"packaging",
# --------------------------
# pandas and related (wanting pandas[performance] without numba as it's 100+MB and not needed)
"pandas[excel]>=2.1.4, <2.4",
"pandas[excel]>=2.3.3, <2.4",
"bottleneck", # recommended performance dependency for pandas, see https://pandas.pydata.org/docs/getting_started/install.html#performance-dependencies-recommended
# --------------------------
"parsedatetime",

View File

@@ -34,7 +34,7 @@ filterwarnings =
# error:"SqlaTable" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
# error:"SqlMetric" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
# error:"TableColumn" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
# error:"TaggedObject" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
error:"TaggedObject" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
error:The autoload parameter is deprecated:sqlalchemy.exc.RemovedIn20Warning
error:The current statement is being autocommitted using implicit autocommit:sqlalchemy.exc.RemovedIn20Warning
error:The connection.execute\(\) method:sqlalchemy.exc.RemovedIn20Warning

View File

@@ -275,7 +275,7 @@ packaging==25.0
# kombu
# limits
# shillelagh
pandas==2.1.4
pandas==2.3.3
# via apache-superset (pyproject.toml)
paramiko==3.5.1
# via
@@ -331,6 +331,8 @@ pyparsing==3.2.3
# via apache-superset (pyproject.toml)
pysocks==1.7.1
# via urllib3
python-calamine==0.8.2
# via pandas
python-dateutil==2.9.0.post0
# via
# apache-superset (pyproject.toml)

View File

@@ -638,7 +638,7 @@ packaging==25.0
# pytest
# shillelagh
# sqlalchemy-bigquery
pandas==2.1.4
pandas==2.3.3
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -819,6 +819,10 @@ pytest-mock==3.10.0
# via
# apache-superset
# apache-superset-extensions-cli
python-calamine==0.8.2
# via
# -c requirements/base-constraint.txt
# pandas
python-dateutil==2.9.0.post0
# via
# -c requirements/base-constraint.txt

View File

@@ -84,7 +84,7 @@
"ag-grid-community": "36.0.0",
"ag-grid-react": "36.0.0",
"antd": "^6.0.0",
"chrono-node": "^2.9.1",
"chrono-node": "^2.10.0",
"classnames": "^2.2.5",
"content-disposition": "^2.0.1",
"d3-scale": "^4.0.2",
@@ -221,7 +221,7 @@
"babel-loader": "^10.1.1",
"babel-plugin-dynamic-import-node": "^2.3.3",
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
"baseline-browser-mapping": "^2.10.42",
"baseline-browser-mapping": "^2.10.43",
"cheerio": "1.2.0",
"concurrently": "^10.0.3",
"copy-webpack-plugin": "^14.0.0",
@@ -278,7 +278,7 @@
"swc-loader": "^0.2.7",
"ts-jest": "^29.4.11",
"tscw-config": "^1.1.2",
"tsx": "^4.23.0",
"tsx": "^4.23.1",
"typescript": "5.4.5",
"unzipper": "^0.12.5",
"vm-browserify": "^1.1.2",
@@ -15213,9 +15213,9 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
"version": "2.10.42",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.42.tgz",
"integrity": "sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==",
"version": "2.10.43",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.43.tgz",
"integrity": "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -16360,9 +16360,9 @@
}
},
"node_modules/chrono-node": {
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/chrono-node/-/chrono-node-2.9.1.tgz",
"integrity": "sha512-nqP8Zp11efCYQIESXPxeDM8ikzN5BDb3Zzou+a66fZq+X2hzKFdsNLQE2/uBAh//BZEMbaMo1eTnagK7hOenAg==",
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/chrono-node/-/chrono-node-2.10.0.tgz",
"integrity": "sha512-5fJ4zr5W/5DEf+8FMPMXF2qk9L5dc1rAP9Pw009iMlKAghPgx5o7aUcnjEMe6PTwqiqTa5yzmfSm05EHuwr31Q==",
"license": "MIT",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
@@ -18669,9 +18669,9 @@
}
},
"node_modules/dompurify": {
"version": "3.4.11",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.11.tgz",
"integrity": "sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==",
"version": "3.4.12",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.12.tgz",
"integrity": "sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
@@ -40266,9 +40266,9 @@
"license": "0BSD"
},
"node_modules/tsx": {
"version": "4.23.0",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.0.tgz",
"integrity": "sha512-eUdUIaCr963q2h5u3+QwvYp0+eqPvn+egeqZUm0hwERCqqx1E3kK5ehbGCvqSE5MQAULr67ww0cA3jKc3YkM1w==",
"version": "4.23.1",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.1.tgz",
"integrity": "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -43549,7 +43549,7 @@
"d3-time": "^3.1.0",
"d3-time-format": "^4.1.0",
"dayjs": "^1.11.21",
"dompurify": "^3.4.11",
"dompurify": "^3.4.12",
"fetch-retry": "^6.0.0",
"handlebars": "^4.7.9",
"jed": "^1.1.1",
@@ -43932,7 +43932,7 @@
"dependencies": {
"d3": "^3.5.17",
"d3-tip": "^0.9.1",
"dompurify": "^3.4.11",
"dompurify": "^3.4.12",
"fast-safe-stringify": "^2.1.1",
"lodash": "^4.18.1",
"lodash-es": "^4.18.1",

View File

@@ -169,7 +169,7 @@
"ag-grid-community": "36.0.0",
"ag-grid-react": "36.0.0",
"antd": "^6.0.0",
"chrono-node": "^2.9.1",
"chrono-node": "^2.10.0",
"classnames": "^2.2.5",
"content-disposition": "^2.0.1",
"d3-scale": "^4.0.2",
@@ -306,7 +306,7 @@
"babel-loader": "^10.1.1",
"babel-plugin-dynamic-import-node": "^2.3.3",
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
"baseline-browser-mapping": "^2.10.42",
"baseline-browser-mapping": "^2.10.43",
"cheerio": "1.2.0",
"concurrently": "^10.0.3",
"copy-webpack-plugin": "^14.0.0",
@@ -363,7 +363,7 @@
"swc-loader": "^0.2.7",
"ts-jest": "^29.4.11",
"tscw-config": "^1.1.2",
"tsx": "^4.23.0",
"tsx": "^4.23.1",
"typescript": "5.4.5",
"unzipper": "^0.12.5",
"vm-browserify": "^1.1.2",

View File

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

View File

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

View File

@@ -133,7 +133,7 @@ export default function Login() {
sessionStorage.setItem('login_attempted', 'true');
// Use standard form submission for Flask-AppBuilder compatibility
SupersetClient.postForm(loginEndpoint, values, '');
SupersetClient.postForm(ensureAppRoot(loginEndpoint), values, '');
};
const getAuthIconElement = (

View File

@@ -20,7 +20,11 @@ import { useMemo, ReactNode } from 'react';
import { InfoTooltip, TableView } from '@superset-ui/core/components';
import { t } from '@apache-superset/core/translation';
import { styled } from '@apache-superset/core/theme';
import { sortNumberWithMixedTypes, processTimeTableData } from './utils';
import {
sortNumberWithMixedTypes,
processTimeTableData,
calculateCellValue,
} from './utils';
import { ValueCell, LeftCell, Sparkline } from './components';
import type { TimeTableProps } from './types';
@@ -94,13 +98,19 @@ const TimeTable = ({
};
}
const { value, errorMsg } = calculateCellValue(
valueField,
columnConfig,
reversedEntries,
);
return {
...acc,
[columnConfig.key]: (
<ValueCell
valueField={valueField}
value={value}
errorMsg={errorMsg}
column={columnConfig}
reversedEntries={reversedEntries}
/>
),
};

View File

@@ -16,6 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { render, screen } from '@superset-ui/core/spec';
import ValueCell from './ValueCell';
@@ -25,130 +26,38 @@ const mockColumn = {
d3format: '.2f',
};
const mockEntries = [
{ time: '2023-01-03', sales: 300, price: 30 },
{ time: '2023-01-02', sales: 200, price: 20 },
{ time: '2023-01-01', sales: 100, price: 10 },
];
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('ValueCell', () => {
test('should render simple value without special column type', () => {
render(
<ValueCell
valueField="sales"
column={mockColumn}
reversedEntries={mockEntries}
/>,
);
test('should render a formatted value', () => {
render(<ValueCell value={300} column={mockColumn} />);
expect(screen.getByText('300.00')).toBeInTheDocument();
});
test('should handle time column type with diff comparison', () => {
const timeColumn = {
...mockColumn,
colType: 'time',
comparisonType: 'diff',
timeLag: 1,
};
render(
<ValueCell
valueField="sales"
column={timeColumn}
reversedEntries={mockEntries}
/>,
);
expect(screen.getByText('100.00')).toBeInTheDocument();
});
test('should handle time column type with percentage comparison', () => {
const timeColumn = {
...mockColumn,
colType: 'time',
comparisonType: 'perc',
timeLag: 1,
};
render(
<ValueCell
valueField="sales"
column={timeColumn}
reversedEntries={mockEntries}
/>,
);
test('should render decimal values', () => {
render(<ValueCell value={1.5} column={mockColumn} />);
expect(screen.getByText('1.50')).toBeInTheDocument();
});
test('should handle time column type with percentage change', () => {
const timeColumn = {
...mockColumn,
colType: 'time',
comparisonType: 'perc_change',
timeLag: 1,
};
test('should render zero', () => {
render(<ValueCell value={0} column={mockColumn} />);
render(
<ValueCell
valueField="sales"
column={timeColumn}
reversedEntries={mockEntries}
/>,
);
expect(screen.getByText('0.50')).toBeInTheDocument();
expect(screen.getByText('0.00')).toBeInTheDocument();
});
test('should handle contrib column type', () => {
const contribColumn = {
...mockColumn,
colType: 'contrib',
};
test('should render negative values', () => {
render(<ValueCell value={-200} column={mockColumn} />);
render(
<ValueCell
valueField="sales"
column={contribColumn}
reversedEntries={mockEntries}
/>,
);
expect(screen.getByText('0.91')).toBeInTheDocument();
expect(screen.getByText('-200.00')).toBeInTheDocument();
});
test('should handle avg column type', () => {
const avgColumn = {
...mockColumn,
colType: 'avg',
timeLag: 2,
};
test('should render an error message instead of the value', () => {
render(
<ValueCell
valueField="sales"
column={avgColumn}
reversedEntries={mockEntries}
/>,
);
expect(screen.getByText('250.00')).toBeInTheDocument();
});
test('should show error message for excessive time lag', () => {
const timeColumn = {
...mockColumn,
colType: 'time',
timeLag: 10,
};
render(
<ValueCell
valueField="sales"
column={timeColumn}
reversedEntries={mockEntries}
value={null}
errorMsg="The time lag set at 10 is too large"
column={mockColumn}
/>,
);
@@ -157,49 +66,6 @@ describe('ValueCell', () => {
).toBeInTheDocument();
});
test('should handle negative time lag', () => {
const timeColumn = {
...mockColumn,
colType: 'time',
comparisonType: 'diff',
timeLag: -1,
};
render(
<ValueCell
valueField="sales"
column={timeColumn}
reversedEntries={mockEntries}
/>,
);
expect(screen.getByText('200.00')).toBeInTheDocument();
});
test('should handle null/undefined values in avg calculation', () => {
const avgColumn = {
...mockColumn,
colType: 'avg',
timeLag: 3,
};
const entriesWithNulls = [
{ time: '2023-01-03', sales: 300 },
{ time: '2023-01-02', sales: null },
{ time: '2023-01-01', sales: 100 },
];
render(
<ValueCell
valueField="sales"
column={avgColumn}
reversedEntries={entriesWithNulls}
/>,
);
expect(screen.getByText('200.00')).toBeInTheDocument();
});
test('should apply color styling when bounds are provided', () => {
const columnWithBounds = {
...mockColumn,
@@ -207,14 +73,11 @@ describe('ValueCell', () => {
};
const { container } = render(
<ValueCell
valueField="sales"
column={columnWithBounds}
reversedEntries={mockEntries}
/>,
<ValueCell value={300} column={columnWithBounds} />,
);
const span = container.querySelector('span[data-value="300"]');
expect(span).toBeInTheDocument();
});
});

View File

@@ -16,37 +16,31 @@
* specific language governing permissions and limitations
* under the License.
*/
import { ReactElement, useMemo } from 'react';
import { colorFromBounds, calculateCellValue } from '../../utils';
import { ReactElement } from 'react';
import { colorFromBounds } from '../../utils';
import FormattedNumber from '../FormattedNumber';
import type { ColumnConfig, Entry } from '../../types';
import type { ColumnConfig } from '../../types';
interface ValueCellProps {
valueField: string;
value: number | null;
errorMsg?: string;
column: ColumnConfig;
reversedEntries: Entry[];
}
/**
* Renders a value cell with different calculation types (time, contrib, avg)
* and applies color coding based on bounds
* Renders a precomputed value or error message and applies color coding
* based on the configured column bounds.
*/
const ValueCell = ({
valueField,
value,
errorMsg,
column,
reversedEntries,
}: ValueCellProps): ReactElement => {
const { value: v, errorMsg } = useMemo(
() => calculateCellValue(valueField, column, reversedEntries),
[valueField, column, reversedEntries],
);
const color = colorFromBounds(v, column.bounds);
const color = colorFromBounds(value, column.bounds);
return (
<span
key={column.key}
data-value={v}
data-value={value}
css={theme =>
color && {
boxShadow: `inset 0px -2.5px 0px 0px ${color}`,
@@ -56,7 +50,7 @@ const ValueCell = ({
>
{errorMsg || (
<span style={{ color: color || undefined }}>
<FormattedNumber num={v} format={column.d3format} />
<FormattedNumber num={value} format={column.d3format} />
</span>
)}
</span>

View File

@@ -16,26 +16,16 @@
* specific language governing permissions and limitations
* under the License.
*/
import { sortNumberWithMixedTypes } from './sortUtils';
import type { ColumnConfig } from '../../types';
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('sortNumberWithMixedTypes', () => {
const createMockRow = (
value: any,
columnOverrides: Partial<ColumnConfig> = {},
) => ({
const createMockRow = (value: any) => ({
values: {
testColumn: {
props: {
valueField: 'metric',
column: {
key: 'testColumn',
colType: 'time',
bounds: undefined,
...columnOverrides,
},
reversedEntries: [{ metric: value }],
value,
},
},
},
@@ -47,7 +37,7 @@ describe('sortNumberWithMixedTypes', () => {
const result = sortNumberWithMixedTypes(rowA, rowB, 'testColumn');
expect(result).toBeLessThan(0); // rowA should come before rowB
expect(result).toBeLessThan(0);
});
test('should sort numbers in descending order', () => {
@@ -73,12 +63,13 @@ describe('sortNumberWithMixedTypes', () => {
const rowB = createMockRow(10);
const result = sortNumberWithMixedTypes(rowA, rowB, 'testColumn');
expect(typeof result).toBe('number');
});
test('should handle string numbers', () => {
const rowA = createMockRow('10', { colType: undefined });
const rowB = createMockRow('20', { colType: undefined });
const rowA = createMockRow('10');
const rowB = createMockRow('20');
const result = sortNumberWithMixedTypes(rowA, rowB, 'testColumn');
@@ -114,24 +105,18 @@ describe('sortNumberWithMixedTypes', () => {
});
test('should sort ValueCell-like props numerically', () => {
const createValueCellRow = (metricValue: number | null) => ({
const createValueCellMock = (value: number | null) => ({
values: {
testColumn: {
props: {
valueField: 'metric',
column: {
key: 'testColumn',
colType: 'time',
bounds: undefined,
},
reversedEntries: [{ metric: metricValue }],
value,
},
},
},
});
const smaller = createValueCellRow(1);
const larger = createValueCellRow(5);
const smaller = createValueCellMock(1);
const larger = createValueCellMock(5);
const result = sortNumberWithMixedTypes(smaller, larger, 'testColumn');

View File

@@ -49,7 +49,6 @@ function compareValues(
* @param rowA - First row to compare
* @param rowB - Second row to compare
* @param columnId - Column identifier for sorting
* @param descending - Whether to sort in descending order
* @returns Numeric comparison result for react-table
* react-table handles the asc/desc direction flip internally after calling
* this function, so we only return the raw comparison result.
@@ -63,32 +62,48 @@ export function sortNumberWithMixedTypes(
const cellB = rowB.values?.[columnId];
// Both ValueCell and Sparkline cells pass React elements here.
// ValueCell uses { valueField, column, reversedEntries }
// Sparkline/SparklineCell uses { valueField, column, entries }
// Normalize to reversedEntries before delegating to calculateCellValue.
// ValueCell provides the precomputed value directly.
// Sparkline provides { valueField, column, entries } and requires
// calculating the sortable value from its entries.
const propsA = cellA?.props as
| {
valueField: string;
column: ColumnConfig;
reversedEntries?: Entry[];
value?: number | null;
valueField?: string;
column?: ColumnConfig;
entries?: Entry[];
}
| undefined;
const propsB = cellB?.props as
| {
valueField: string;
column: ColumnConfig;
reversedEntries?: Entry[];
value?: number | null;
valueField?: string;
column?: ColumnConfig;
entries?: Entry[];
}
| undefined;
const reversedEntriesA =
propsA?.reversedEntries ?? propsA?.entries?.slice().reverse();
const reversedEntriesB =
propsB?.reversedEntries ?? propsB?.entries?.slice().reverse();
if (!propsA || !propsB) {
return 0;
}
if (!propsA || !propsB || !reversedEntriesA || !reversedEntriesB) {
// ValueCell already provides the computed value.
if ('value' in propsA && 'value' in propsB) {
return compareValues(propsA.value, propsB.value, 'asSmallest');
}
// Sparkline still needs calculation.
const reversedEntriesA = propsA.entries?.slice().reverse();
const reversedEntriesB = propsB.entries?.slice().reverse();
if (
!reversedEntriesA ||
!reversedEntriesB ||
!propsA.valueField ||
!propsA.column ||
!propsB.valueField ||
!propsB.column
) {
return 0;
}
@@ -97,6 +112,7 @@ export function sortNumberWithMixedTypes(
propsA.column,
reversedEntriesA,
);
const { value: valueB } = calculateCellValue(
propsB.valueField,
propsB.column,

View File

@@ -28,6 +28,16 @@ from werkzeug.local import LocalProxy
# form.
flask_appbuilder.Model.__allow_unmapped__ = True
# pandas >= 2.2 advertises a minimum SQLAlchemy of 2.0 and silently ignores
# older installations, breaking DataFrame.to_sql / read_sql with SQLAlchemy
# 1.4 engines. Its SQL layer still works with 1.4, so restore support until
# Superset itself requires SQLAlchemy >= 2. Must run before any pandas SQL IO.
from superset.utils.pandas_sqlalchemy_compat import ( # noqa: E402
restore_pandas_sqlalchemy_support,
)
restore_pandas_sqlalchemy_support()
from superset.app import create_app # noqa: E402, F401
from superset.extensions import ( # noqa: E402
appbuilder, # noqa: F401

View File

@@ -23,15 +23,17 @@ from typing import Any, Optional
from flask_babel import lazy_gettext as _
from sqlalchemy.orm import make_transient
from superset import db
from superset import db, security_manager
from superset.commands.base import BaseCommand
from superset.commands.dataset.importers.v0 import import_dataset
from superset.commands.exceptions import ImportFailedError
from superset.connectors.sqla.models import SqlaTable, SqlMetric, TableColumn
from superset.exceptions import DashboardImportException
from superset.migrations.shared.native_filters import migrate_dashboard
from superset.models.dashboard import Dashboard
from superset.models.slice import Slice
from superset.utils import json
from superset.utils.core import get_user
from superset.utils.dashboard_filter_scopes_converter import (
convert_filter_scopes,
copy_filter_scopes,
@@ -305,6 +307,18 @@ def import_dashboards(
if not data:
raise DashboardImportException(_("No data in file"))
dataset_id_mapping: dict[int, int] = {}
# This legacy path creates/updates the embedded datasets. Mirror the
# versioned (v1) import commands and require dataset write permission for the
# objects being created here. Only enforced when there is something to import
# and a request user is present, so the CLI import paths keep working.
if (
data["datasources"]
and get_user()
and not security_manager.can_access("can_write", "Dataset")
):
raise ImportFailedError(
"User doesn't have permission to create or update datasets"
)
for table in data["datasources"]:
new_dataset_id = import_dataset(table, database_id, import_time=import_time)
params = json.loads(table.params)

View File

@@ -21,10 +21,11 @@ import yaml
from flask_appbuilder import Model
from sqlalchemy.orm.session import make_transient
from superset import db
from superset import db, security_manager
from superset.commands.base import BaseCommand
from superset.commands.database.exceptions import DatabaseNotFoundError
from superset.commands.dataset.exceptions import DatasetInvalidError
from superset.commands.exceptions import ImportFailedError
from superset.commands.importers.exceptions import IncorrectVersionError
from superset.connectors.sqla.models import (
BaseDatasource,
@@ -36,6 +37,7 @@ from superset.constants import PASSWORD_MASK
from superset.databases.utils import make_url_safe
from superset.models.core import Database
from superset.utils import json
from superset.utils.core import get_user
from superset.utils.decorators import transaction
from superset.utils.dict_import_export import DATABASES_KEY
@@ -211,8 +213,22 @@ def import_from_dict(data: dict[str, Any], sync: Optional[list[str]] = None) ->
if not sync:
sync = []
if isinstance(data, dict):
logger.info("Importing %d %s", len(data.get(DATABASES_KEY, [])), DATABASES_KEY)
for database in data.get(DATABASES_KEY, []):
databases = data.get(DATABASES_KEY, [])
# This legacy path creates/updates the embedded database connections.
# Mirror the versioned (v1) import commands and require database write
# permission for the objects being created here. Only enforced when there
# is something to import and a request user is present, so the CLI import
# paths keep working.
if (
databases
and get_user()
and not security_manager.can_access("can_write", "Database")
):
raise ImportFailedError(
"User doesn't have permission to create or update databases"
)
logger.info("Importing %d %s", len(databases), DATABASES_KEY)
for database in databases:
db_obj = Database.import_from_dict(database, sync=sync)
# ``import_from_dict`` sets fields via setattr, bypassing
# ``set_sqlalchemy_uri``. Call it explicitly so that any plaintext

View File

@@ -98,7 +98,10 @@ class Tag(CoreTag, AuditMixinNullable):
description = Column(Text)
objects = relationship(
"TaggedObject", back_populates="tag", overlaps="objects,tags"
"TaggedObject",
back_populates="tag",
cascade_backrefs=False,
overlaps="objects,tags",
)
users_favorited = relationship(
@@ -122,7 +125,12 @@ class TaggedObject(Model, AuditMixinNullable):
object_id = Column(Integer)
object_type = Column(Enum(ObjectType))
tag = relationship("Tag", back_populates="objects", overlaps="tags")
tag = relationship(
"Tag",
back_populates="objects",
cascade_backrefs=False,
overlaps="tags",
)
__table_args__ = (
UniqueConstraint(
"tag_id", "object_id", "object_type", name="uix_tagged_object"

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,80 @@
# 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.
"""Compatibility shim letting pandas >= 2.2 use SQLAlchemy 1.4 engines.
pandas 2.2 raised its advertised minimum SQLAlchemy version to 2.0 as a
support-policy change. When an older SQLAlchemy is installed, pandas does not
fail loudly: ``pandas.io.sql`` silently pretends SQLAlchemy is absent, treats
Engine/Connection arguments as raw DBAPI connections, and falls back to its
sqlite-only code path, breaking every ``DataFrame.to_sql`` / ``read_sql``
call site (dataset uploads, example data loading, annotation queries, filter
values).
The pandas SQL layer itself still works with SQLAlchemy 1.4 because it only
uses the API subset common to SQLAlchemy 1.4 and 2.x. Lowering the advertised
minimum back to the pandas 2.1 value restores the working behavior.
This module is obsolete once Superset requires SQLAlchemy >= 2; at that point
the patch becomes a no-op and the module (and its call site in
``superset/__init__.py``) can be deleted.
"""
import logging
import sqlalchemy
from packaging.version import Version
logger = logging.getLogger(__name__)
# The last pandas release line to support SQLAlchemy 1.4 (pandas 2.1)
# required at least this version.
_SQLALCHEMY_MINIMUM = "1.4.16"
def restore_pandas_sqlalchemy_support() -> None:
"""Lower pandas' advertised SQLAlchemy minimum so 1.4 engines work.
Only applies when the installed SQLAlchemy predates 2.0 and pandas
advertises a 2.x minimum; in every other combination this is a no-op.
Safe to call multiple times.
"""
if Version(sqlalchemy.__version__) >= Version("2.0.0"):
# pandas supports SQLAlchemy 2.x natively; nothing to patch.
return
try:
from pandas.compat import _optional
except ImportError:
# The private module moved in a newer pandas; SQL IO with a pre-2.0
# SQLAlchemy will misbehave, so make the situation diagnosable.
logger.warning(
"Could not adjust pandas' minimum SQLAlchemy version; "
"DataFrame.to_sql/read_sql may not accept SQLAlchemy %s engines",
sqlalchemy.__version__,
)
return
advertised = _optional.VERSIONS.get("sqlalchemy")
if advertised and Version(advertised) > Version(_SQLALCHEMY_MINIMUM):
_optional.VERSIONS["sqlalchemy"] = _SQLALCHEMY_MINIMUM
logger.debug(
"Lowered pandas' minimum SQLAlchemy version from %s to %s so "
"pandas SQL IO keeps working with the installed SQLAlchemy %s",
advertised,
_SQLALCHEMY_MINIMUM,
sqlalchemy.__version__,
)

View File

@@ -0,0 +1,16 @@
# 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.

View File

@@ -0,0 +1,92 @@
# 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.
# pylint: disable=import-outside-toplevel, unused-argument
import pytest
from pytest_mock import MockerFixture
from superset.utils import json
# A minimal unversioned dashboard export carrying an embedded dataset. The
# dataset object is enough to drive the datasets loop in ``import_dashboards``.
CONTENT_WITH_DATASET = json.dumps(
{
"datasources": [
{
"__SqlaTable__": {
"table_name": "sample_table",
"params": json.dumps({"remote_id": 1, "database_name": "main"}),
}
}
],
"dashboards": [],
}
)
def test_import_dashboards_requires_dataset_write(mocker: MockerFixture) -> None:
"""
A user must hold dataset write permission to create/update the datasets
embedded in a legacy dashboard export.
"""
from superset import security_manager
from superset.commands.dashboard.importers import v0
from superset.commands.exceptions import ImportFailedError
mocker.patch.object(v0, "get_user", return_value=object())
can_access = mocker.patch.object(security_manager, "can_access", return_value=False)
import_dataset = mocker.patch.object(v0, "import_dataset")
with pytest.raises(ImportFailedError):
v0.import_dashboards(CONTENT_WITH_DATASET)
can_access.assert_called_once_with("can_write", "Dataset")
import_dataset.assert_not_called()
def test_import_dashboards_allows_dataset_write(mocker: MockerFixture) -> None:
"""
A user holding dataset write permission proceeds to import the datasets.
"""
from superset import security_manager
from superset.commands.dashboard.importers import v0
mocker.patch.object(v0, "get_user", return_value=object())
mocker.patch.object(security_manager, "can_access", return_value=True)
import_dataset = mocker.patch.object(v0, "import_dataset", return_value=42)
v0.import_dashboards(CONTENT_WITH_DATASET)
import_dataset.assert_called_once()
def test_import_dashboards_skips_check_without_user(mocker: MockerFixture) -> None:
"""
The command-line import paths run without a request user; the permission
check is skipped so those paths keep working.
"""
from superset import security_manager
from superset.commands.dashboard.importers import v0
mocker.patch.object(v0, "get_user", return_value=None)
can_access = mocker.patch.object(security_manager, "can_access")
import_dataset = mocker.patch.object(v0, "import_dataset", return_value=42)
v0.import_dashboards(CONTENT_WITH_DATASET)
can_access.assert_not_called()
import_dataset.assert_called_once()

View File

@@ -0,0 +1,16 @@
# 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.

View File

@@ -0,0 +1,77 @@
# 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.
# pylint: disable=import-outside-toplevel, unused-argument
import pytest
from pytest_mock import MockerFixture
# A minimal unversioned dataset export carrying an embedded database connection.
DATA_WITH_DATABASE = {"databases": [{"database_name": "sample_db"}]}
def test_import_from_dict_requires_database_write(mocker: MockerFixture) -> None:
"""
A user must hold database write permission to create/update the database
connections embedded in a legacy dataset export.
"""
from superset import security_manager
from superset.commands.dataset.importers import v0
from superset.commands.exceptions import ImportFailedError
mocker.patch.object(v0, "get_user", return_value=object())
can_access = mocker.patch.object(security_manager, "can_access", return_value=False)
imported = mocker.patch.object(v0.Database, "import_from_dict")
with pytest.raises(ImportFailedError):
v0.import_from_dict(DATA_WITH_DATABASE)
can_access.assert_called_once_with("can_write", "Database")
imported.assert_not_called()
def test_import_from_dict_allows_database_write(mocker: MockerFixture) -> None:
"""
A user holding database write permission proceeds to import the databases.
"""
from superset import security_manager
from superset.commands.dataset.importers import v0
mocker.patch.object(v0, "get_user", return_value=object())
mocker.patch.object(security_manager, "can_access", return_value=True)
imported = mocker.patch.object(v0.Database, "import_from_dict", return_value=None)
v0.import_from_dict(DATA_WITH_DATABASE)
imported.assert_called_once()
def test_import_from_dict_skips_check_without_user(mocker: MockerFixture) -> None:
"""
The command-line import paths run without a request user; the permission
check is skipped so those paths keep working.
"""
from superset import security_manager
from superset.commands.dataset.importers import v0
mocker.patch.object(v0, "get_user", return_value=None)
can_access = mocker.patch.object(security_manager, "can_access")
imported = mocker.patch.object(v0.Database, "import_from_dict", return_value=None)
v0.import_from_dict(DATA_WITH_DATABASE)
can_access.assert_not_called()
imported.assert_called_once()

View File

@@ -0,0 +1,67 @@
# 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.
import pandas as pd
from sqlalchemy import create_engine, types
from superset.utils.pandas_sqlalchemy_compat import (
restore_pandas_sqlalchemy_support,
)
def test_to_sql_accepts_sqlalchemy_engine_and_dtypes() -> None:
"""
``DataFrame.to_sql`` must accept a SQLAlchemy engine plus SQLAlchemy
``dtype`` objects regardless of the installed pandas/SQLAlchemy combo.
This is the exact call shape used by dataset uploads
(``BaseEngineSpec.df_to_sql``), example data loading, and the test data
loaders; it breaks when pandas silently rejects the installed SQLAlchemy
as too old (pandas >= 2.2 with SQLAlchemy 1.x) and no compat shim is
applied.
"""
restore_pandas_sqlalchemy_support()
engine = create_engine("sqlite://")
df = pd.DataFrame(
{
"name": ["a", "b"],
"num": [1, 2],
"ds": pd.to_datetime(["2021-01-01", "2021-01-02"]),
}
)
df.to_sql(
"birth_names",
engine,
index=False,
dtype={"ds": types.DateTime(), "name": types.String(255)},
method="multi",
chunksize=100,
)
df.to_sql("birth_names", engine, index=False, if_exists="replace")
result = pd.read_sql_query("SELECT name, num FROM birth_names", engine)
assert result["name"].tolist() == ["a", "b"]
assert result["num"].tolist() == [1, 2]
def test_restore_pandas_sqlalchemy_support_is_idempotent() -> None:
from pandas.compat import _optional
restore_pandas_sqlalchemy_support()
first = _optional.VERSIONS.get("sqlalchemy")
restore_pandas_sqlalchemy_support()
assert _optional.VERSIONS.get("sqlalchemy") == first