mirror of
https://github.com/apache/superset.git
synced 2026-07-20 13:45:47 +00:00
Compare commits
13 Commits
refactor/s
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
277c814c5a | ||
|
|
7e63b29741 | ||
|
|
b5ae7cbd0d | ||
|
|
e044d7783a | ||
|
|
9e8b9ac8cf | ||
|
|
19588826c8 | ||
|
|
8b415502cf | ||
|
|
09148d0af6 | ||
|
|
3e40bebba8 | ||
|
|
36dbff091f | ||
|
|
9e9b7c4bbd | ||
|
|
42a2aede78 | ||
|
|
c4d50472a9 |
@@ -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
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
34
superset-frontend/package-lock.json
generated
34
superset-frontend/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
80
superset/utils/pandas_sqlalchemy_compat.py
Normal file
80
superset/utils/pandas_sqlalchemy_compat.py
Normal 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__,
|
||||
)
|
||||
@@ -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.
|
||||
@@ -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()
|
||||
16
tests/unit_tests/datasets/commands/importers/v0/__init__.py
Normal file
16
tests/unit_tests/datasets/commands/importers/v0/__init__.py
Normal 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.
|
||||
@@ -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()
|
||||
67
tests/unit_tests/utils/pandas_sqlalchemy_compat_test.py
Normal file
67
tests/unit_tests/utils/pandas_sqlalchemy_compat_test.py
Normal 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
|
||||
Reference in New Issue
Block a user