Compare commits

..
Author SHA1 Message Date
Elizabeth Thompson 3eb76219d5 Merge remote-tracking branch 'origin/master' into HEAD
# Conflicts:
#	tests/unit_tests/commands/importers/v1/utils_test.py
2026-08-25 17:57:35 +00:00
Chen, Ting-AnandJoe Li 3585e8235a fix(i18n): review Traditional Chinese count labels (#43063)
Co-authored-by: Joe Li <joe@preset.io>
2026-08-25 10:28:12 -07:00
Evan RusackasandSuperset Dev 34ffa37aaa chore(ci): drop inert SQLALCHEMY_WARN_20 flag from unit-test CI (#43495)
Co-authored-by: Superset Dev <dev@superset.apache.org>
2026-08-25 10:19:38 -07:00
Russlan Ramdowar 02b3e43b66 fix(number-format): preserve custom smart formatter id (#43439) 2026-08-25 10:11:52 -07:00
DmitryandDmitry Kucher f9530f31ab fix(table): keep each metric's own aggregate in the summary row by default (#43421)
Co-authored-by: Dmitry Kucher <dima@virtuman.com>
2026-08-25 10:10:02 -07:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>rusackasClaude Opus 4.8
fd3849cef1 chore(deps): bump antd from 6.6.0 to 6.6.1 in /superset-frontend (#43509)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: rusackas <evan@rusackas.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-25 05:56:18 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ef0ad01aa5 chore(deps-dev): bump the storybook group across 1 directory with 5 updates (#43507)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-25 05:52:46 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> d480152735 chore(deps): bump the storybook group in /docs with 2 updates (#43505)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-25 02:46:30 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> cbfa94c4dd chore(deps): bump antd from 6.6.0 to 6.6.1 in /docs (#43506)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-25 02:46:26 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> d2aeb29223 chore(deps-dev): bump baseline-browser-mapping from 2.11.14 to 2.11.15 in /superset-frontend (#43508)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-25 02:46:22 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 7bad69f523 chore(deps): bump dayjs from 1.11.22 to 1.11.23 in /superset-frontend (#43510)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-25 02:46:17 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 9dd8c42f3d chore(deps): bump dompurify from 3.4.12 to 3.4.13 in /superset-frontend (#43511)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-25 02:46:12 -07:00
Elizabeth ThompsonandClaude Opus 4.8 58fa19bf90 fix(importers): catch JSONDecodeError in load_configs masked_encrypted_extra merge
load_configs() parses each config's `masked_encrypted_extra` field with
json.loads() before schema validation runs, in order to merge caller-supplied
`encrypted_extra_secrets` into it. That field comes straight from user-uploaded
import YAML, so a malformed value raised a raw simplejson.JSONDecodeError (a
ValueError, not a marshmallow.ValidationError). The enclosing except only
catches ValidationError, so the decode error escaped uncaught out of
ImportModelsCommand.validate() and surfaced as an opaque 500 instead of the
structured 422 every other per-file validation failure produces.

Add a sibling `except json.JSONDecodeError` clause that converts the decode
error into a ValidationError with the same {file_name: {field: [msg]}} shape,
so it flows into the existing exceptions list and downstream
CommandInvalidError aggregation. Additive catch only; existing success paths
are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-17 16:47:55 +00:00
26 changed files with 434 additions and 710 deletions
@@ -53,12 +53,6 @@ jobs:
python-version: ${{ github.event_name == 'pull_request' && fromJSON('["current"]') || fromJSON('["current", "next"]') }}
env:
PYTHONPATH: ${{ github.workspace }}
# Promotes the SQLAlchemy 2.0 deprecation warnings already locked in as
# errors via pytest.ini's `filterwarnings` to actually run in CI, so a
# regression on those fails the build instead of relying on a
# contributor remembering to set this locally. See the migration
# battleplan: https://github.com/apache/superset/discussions/40273
SQLALCHEMY_WARN_20: "1"
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+3 -3
View File
@@ -58,10 +58,10 @@
"@fontsource/inter": "^5.3.0",
"@mdx-js/react": "^3.1.1",
"@saucelabs/theme-github-codeblock": "^0.3.0",
"@storybook/addon-docs": "^10.5.8",
"@storybook/addon-docs": "^10.5.9",
"@superset-ui/core": "^0.20.4",
"@swc/core": "^1.16.0",
"antd": "^6.6.0",
"antd": "^6.6.1",
"baseline-browser-mapping": "^2.11.15",
"caniuse-lite": "^1.0.30001809",
"docusaurus-plugin-openapi-docs": "^5.2.0",
@@ -77,7 +77,7 @@
"react-table": "^7.8.0",
"remark-import-partial": "^0.0.2",
"reselect": "^5.2.0",
"storybook": "^10.5.8",
"storybook": "^10.5.9",
"swagger-ui-react": "^5.32.13",
"swc-loader": "^0.2.7",
"tinycolor2": "^1.4.2",
+28 -28
View File
@@ -3789,7 +3789,7 @@
"@rc-component/util" "^1.3.0"
clsx "^2.1.1"
"@rc-component/select@~1.10.0":
"@rc-component/select@~1.10.0", "@rc-component/select@~1.10.1":
version "1.10.1"
resolved "https://registry.yarnpkg.com/@rc-component/select/-/select-1.10.1.tgz#323b2f458a637e8e752f8341094783741c613c34"
integrity sha512-H+yQsl+qED9NilQ3g6zdpsMwUgwVjrcMTkNHAWRVU/MoNCYgTbDgU+MIMgZDK+rVdd2JUfI/MkysMcZZ0cyQKw==
@@ -3824,7 +3824,7 @@
"@rc-component/util" "^1.3.0"
clsx "^2.1.1"
"@rc-component/table@~1.11.0":
"@rc-component/table@~1.11.1":
version "1.11.1"
resolved "https://registry.yarnpkg.com/@rc-component/table/-/table-1.11.1.tgz#7b5c2a7c26fd37b6a403082029b5a72fcb330a4d"
integrity sha512-OWdS6DMmeWb7bJBGqPxYZpQbzBlBiXZUu2sqo6Ii7Sjs9GeK1IsrXrWk26SL2c6KEseabswdxrRj7WUm9LdECw==
@@ -3866,7 +3866,7 @@
"@rc-component/util" "^1.7.0"
clsx "^2.1.1"
"@rc-component/tree-select@~1.16.0":
"@rc-component/tree-select@~1.16.1":
version "1.16.1"
resolved "https://registry.yarnpkg.com/@rc-component/tree-select/-/tree-select-1.16.1.tgz#dcaea96e396e98108cb29cc051840d4fbdda38cc"
integrity sha512-a1Oi6EJhqAhdOxxupdJi6fP0RPHMKn5TcfkX2+llaQ4lF4nwfH7b6SCHcnsybaa2s+pk1yZYwVyeOYkDnEBRdg==
@@ -4122,23 +4122,23 @@
resolved "https://registry.yarnpkg.com/@standard-schema/utils/-/utils-0.3.0.tgz#3d5e608f16c2390c10528e98e59aef6bf73cae7b"
integrity sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==
"@storybook/addon-docs@^10.5.8":
version "10.5.8"
resolved "https://registry.yarnpkg.com/@storybook/addon-docs/-/addon-docs-10.5.8.tgz#767c10c7a4cc1b625b93f869b2a2b09fc8514f2e"
integrity sha512-NlHiMKW/UvW/uL8HXFDCEVwoH3qZeGYZ/qlWax4d7H471b/T54MBq2KcB4ZrdA785FfIH3numAJdBb5jwn00Mg==
"@storybook/addon-docs@^10.5.9":
version "10.5.9"
resolved "https://registry.yarnpkg.com/@storybook/addon-docs/-/addon-docs-10.5.9.tgz#6d871977f7ad833dc142d12ee43490105dec4d07"
integrity sha512-8sFsMkZYrrdqCLdV+hnwTwDF7RaBsBPRwl4wfc8ve9Q/7Yhi5REe/Xjvd8x1yn6fBPPw9tnID9dx6Agdnr81fw==
dependencies:
"@mdx-js/react" "^3.0.0"
"@storybook/csf-plugin" "10.5.8"
"@storybook/csf-plugin" "10.5.9"
"@storybook/icons" "^2.0.2"
"@storybook/react-dom-shim" "10.5.8"
"@storybook/react-dom-shim" "10.5.9"
react "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
react-dom "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
ts-dedent "^2.0.0"
"@storybook/csf-plugin@10.5.8":
version "10.5.8"
resolved "https://registry.yarnpkg.com/@storybook/csf-plugin/-/csf-plugin-10.5.8.tgz#c626c5bfe55d0e279b2457e5cf150a788d1fc637"
integrity sha512-/FHiMyOWWEXfwK/lM0WxmkP9GLzbSJJuzGtfeuNWSOVDnvAMbjavitxfHb5wSbWKIQo0XYC1EJ2Y7x91XNYP4w==
"@storybook/csf-plugin@10.5.9":
version "10.5.9"
resolved "https://registry.yarnpkg.com/@storybook/csf-plugin/-/csf-plugin-10.5.9.tgz#805e4c93a1704b220351d62bb0c74ce3b78c5e10"
integrity sha512-4H5QIHQVtQYCuL43GCRLGjNQhZpQg9gL03ja0DV80kO2Dn9LEt6ol87bSnSjn4VDgcAXtgTzXFvRLknfVgAAqg==
dependencies:
unplugin "^2.3.5"
@@ -4152,10 +4152,10 @@
resolved "https://registry.yarnpkg.com/@storybook/icons/-/icons-2.1.0.tgz#edfc2450a39c5e780f28c6cbc49acd7bff59b41a"
integrity sha512-Fxh9vYpX9bQqFeHRiY8h2ApeRGDzRSMLwJwNZ/AIRqnyOKHxRKL+yFe+ctEkVJmuptRE9u1Hrn8ZZNHyfDKKNg==
"@storybook/react-dom-shim@10.5.8":
version "10.5.8"
resolved "https://registry.yarnpkg.com/@storybook/react-dom-shim/-/react-dom-shim-10.5.8.tgz#40cc3e32af424baa2e4109a325dae2ede29999e2"
integrity sha512-N8D13/Xny+V3kfe1KBgsAHS0nKWXLLdgOOXS9poKdYzVwVCN+CGEGBxWX0zMMtdCptqa6/57em9coPlZMoO+bg==
"@storybook/react-dom-shim@10.5.9":
version "10.5.9"
resolved "https://registry.yarnpkg.com/@storybook/react-dom-shim/-/react-dom-shim-10.5.9.tgz#549793845bb8b966acd36002d33d7b54cc5c92d4"
integrity sha512-7qZD6CSa64p1m/zX9tG4ALcEHlEk0Bx+5++4RL3MFlRCgCFfAomXsCHTzF0RyF8cI4vl+hS7Y0ryjQchVs6UQA==
"@superset-ui/core@^0.20.4":
version "0.20.4"
@@ -6181,10 +6181,10 @@ ansis@^3.2.0:
resolved "https://registry.yarnpkg.com/ansis/-/ansis-3.17.0.tgz#fa8d9c2a93fe7d1177e0c17f9eeb562a58a832d7"
integrity sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==
antd@^6.6.0:
version "6.6.0"
resolved "https://registry.yarnpkg.com/antd/-/antd-6.6.0.tgz#8acb84c54b36594b5c1a9084c8acb6a03b79961b"
integrity sha512-UDwWIbpmrCHB9ZQ+bPh4vQfB6DTI2ulIyoQ0Tc9xxalFblttiNGHl3ySBD9SyV/8+gUjFzfSx1+iU1Fog2i46w==
antd@^6.6.1:
version "6.6.1"
resolved "https://registry.yarnpkg.com/antd/-/antd-6.6.1.tgz#3235d76413b525b1f3287b87bdaf6ba0e7148521"
integrity sha512-QHIHYoUk9N9nJy1T9fyxWKjY0qApdTEDd/6lzqYng8Uryv9FejNmbhKvYF7obGqB+TuLXQsPVF7fOVgyzM1KrQ==
dependencies:
"@ant-design/colors" "^8.0.1"
"@ant-design/cssinjs" "^2.1.2"
@@ -6217,16 +6217,16 @@ antd@^6.6.0:
"@rc-component/rate" "~1.0.1"
"@rc-component/resize-observer" "^1.1.2"
"@rc-component/segmented" "~1.3.0"
"@rc-component/select" "~1.10.0"
"@rc-component/select" "~1.10.1"
"@rc-component/slider" "~1.1.1"
"@rc-component/steps" "~1.2.2"
"@rc-component/switch" "~1.0.3"
"@rc-component/table" "~1.11.0"
"@rc-component/table" "~1.11.1"
"@rc-component/tabs" "~1.12.0"
"@rc-component/tooltip" "~1.5.0"
"@rc-component/tour" "~2.4.0"
"@rc-component/tree" "~1.4.0"
"@rc-component/tree-select" "~1.16.0"
"@rc-component/tree-select" "~1.16.1"
"@rc-component/trigger" "^3.10.1"
"@rc-component/upload" "~1.1.1"
"@rc-component/util" "^1.12.0"
@@ -14783,10 +14783,10 @@ stop-iteration-iterator@^1.1.0:
es-errors "^1.3.0"
internal-slot "^1.1.0"
storybook@^10.5.8:
version "10.5.8"
resolved "https://registry.yarnpkg.com/storybook/-/storybook-10.5.8.tgz#d5f051983e6232c0a73ea02149a72c7bafb43275"
integrity sha512-rR4oFMSiWBSqI0lvsJPtcQUPj8+hzj3TkLu+Mw61Wo6YxPSb5FsLSHai0jZnuaIdKIlmu25KCfwlSQl4e1uvnA==
storybook@^10.5.9:
version "10.5.9"
resolved "https://registry.yarnpkg.com/storybook/-/storybook-10.5.9.tgz#61f476fd73785dcf09e9198ddf404b9b8c06964a"
integrity sha512-UfdMKSjEhIKr8LbqYyIE5r7vT/drL/PxN75YaouJ+UG0FssEy6cf49OdTF3kstAqVMHskc+zEqyRoiQHZXHwgA==
dependencies:
"@storybook/global" "^5.0.0"
"@storybook/icons" "^2.0.2"
+86 -270
View File
@@ -81,12 +81,12 @@
"@visx/xychart": "^4.0.0",
"ag-grid-community": "36.1.0",
"ag-grid-react": "36.1.0",
"antd": "^6.6.0",
"antd": "^6.6.1",
"chrono-node": "^2.10.1",
"classnames": "^2.2.5",
"content-disposition": "^2.0.1",
"d3-scale": "^4.0.2",
"dayjs": "^1.11.22",
"dayjs": "^1.11.23",
"dom-to-image-more": "^3.10.2",
"dom-to-pdf": "^0.3.2",
"echarts": "^6.1.0",
@@ -180,9 +180,9 @@
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@playwright/test": "^1.62.1",
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
"@storybook/addon-docs": "10.5.8",
"@storybook/addon-links": "10.5.8",
"@storybook/react-webpack5": "10.5.8",
"@storybook/addon-docs": "10.5.9",
"@storybook/addon-links": "10.5.9",
"@storybook/react-webpack5": "10.5.9",
"@storybook/test-runner": "0.24.4",
"@svgr/webpack": "^8.1.0",
"@swc/core": "^1.16.0",
@@ -218,7 +218,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.11.14",
"baseline-browser-mapping": "^2.11.15",
"cheerio": "1.2.0",
"concurrently": "^10.0.5",
"copy-webpack-plugin": "^14.0.0",
@@ -235,7 +235,7 @@
"eslint-plugin-no-only-tests": "^3.4.0",
"eslint-plugin-react-prefer-function-component": "^5.0.0",
"eslint-plugin-react-you-might-not-need-an-effect": "^1.0.1",
"eslint-plugin-storybook": "10.5.8",
"eslint-plugin-storybook": "10.5.9",
"eslint-plugin-testing-library": "^7.16.2",
"eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
"fetch-mock": "^12.6.0",
@@ -266,7 +266,7 @@
"source-map": "^0.8.0",
"source-map-support": "^0.5.21",
"speed-measure-webpack-plugin": "^1.6.0",
"storybook": "10.5.8",
"storybook": "10.5.9",
"style-loader": "^4.0.0",
"stylelint": "^17.14.1",
"swc-loader": "^0.2.7",
@@ -9946,9 +9946,9 @@
}
},
"node_modules/@rc-component/select": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@rc-component/select/-/select-1.10.0.tgz",
"integrity": "sha512-u/3yuF2kEXvTJXPy3P7qkVBkGGZcQo+m1uTuQqJa6qCnwdmDoEn1Rs3zTTi6y/RsLGiQGM0drPN5c/RfflCnow==",
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/@rc-component/select/-/select-1.10.1.tgz",
"integrity": "sha512-H+yQsl+qED9NilQ3g6zdpsMwUgwVjrcMTkNHAWRVU/MoNCYgTbDgU+MIMgZDK+rVdd2JUfI/MkysMcZZ0cyQKw==",
"license": "MIT",
"dependencies": {
"@rc-component/overflow": "^1.0.0",
@@ -10765,16 +10765,16 @@
"license": "MIT"
},
"node_modules/@storybook/addon-docs": {
"version": "10.5.8",
"resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.5.8.tgz",
"integrity": "sha512-NlHiMKW/UvW/uL8HXFDCEVwoH3qZeGYZ/qlWax4d7H471b/T54MBq2KcB4ZrdA785FfIH3numAJdBb5jwn00Mg==",
"version": "10.5.9",
"resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.5.9.tgz",
"integrity": "sha512-8sFsMkZYrrdqCLdV+hnwTwDF7RaBsBPRwl4wfc8ve9Q/7Yhi5REe/Xjvd8x1yn6fBPPw9tnID9dx6Agdnr81fw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@mdx-js/react": "^3.0.0",
"@storybook/csf-plugin": "10.5.8",
"@storybook/csf-plugin": "10.5.9",
"@storybook/icons": "^2.0.2",
"@storybook/react-dom-shim": "10.5.8",
"@storybook/react-dom-shim": "10.5.9",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"ts-dedent": "^2.0.0"
@@ -10785,7 +10785,7 @@
},
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"storybook": "10.5.8"
"storybook": "10.5.9"
},
"peerDependenciesMeta": {
"@types/react": {
@@ -10794,9 +10794,9 @@
}
},
"node_modules/@storybook/addon-docs/node_modules/@storybook/csf-plugin": {
"version": "10.5.8",
"resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.5.8.tgz",
"integrity": "sha512-/FHiMyOWWEXfwK/lM0WxmkP9GLzbSJJuzGtfeuNWSOVDnvAMbjavitxfHb5wSbWKIQo0XYC1EJ2Y7x91XNYP4w==",
"version": "10.5.9",
"resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.5.9.tgz",
"integrity": "sha512-4H5QIHQVtQYCuL43GCRLGjNQhZpQg9gL03ja0DV80kO2Dn9LEt6ol87bSnSjn4VDgcAXtgTzXFvRLknfVgAAqg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -10809,7 +10809,7 @@
"peerDependencies": {
"esbuild": "*",
"rollup": "*",
"storybook": "10.5.8",
"storybook": "10.5.9",
"vite": "*",
"webpack": "*"
},
@@ -10829,9 +10829,9 @@
}
},
"node_modules/@storybook/addon-docs/node_modules/@storybook/react-dom-shim": {
"version": "10.5.8",
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.5.8.tgz",
"integrity": "sha512-N8D13/Xny+V3kfe1KBgsAHS0nKWXLLdgOOXS9poKdYzVwVCN+CGEGBxWX0zMMtdCptqa6/57em9coPlZMoO+bg==",
"version": "10.5.9",
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.5.9.tgz",
"integrity": "sha512-7qZD6CSa64p1m/zX9tG4ALcEHlEk0Bx+5++4RL3MFlRCgCFfAomXsCHTzF0RyF8cI4vl+hS7Y0ryjQchVs6UQA==",
"dev": true,
"license": "MIT",
"funding": {
@@ -10843,7 +10843,7 @@
"@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"storybook": "10.5.8"
"storybook": "10.5.9"
},
"peerDependenciesMeta": {
"@types/react": {
@@ -10855,9 +10855,9 @@
}
},
"node_modules/@storybook/addon-links": {
"version": "10.5.8",
"resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-10.5.8.tgz",
"integrity": "sha512-mpWw4alBJVGqgVh897LZ2keN/xnMHcH93wKJG+oGg4+cdEUA+06hCs5T4k+AS5Aa+EZ6LvdOoi2VPHssyQlCCA==",
"version": "10.5.9",
"resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-10.5.9.tgz",
"integrity": "sha512-ZDbPl6ia6hqjoV+CpQU3DjkXpc0TxUq6+y/rFD8w21dJMdqNWzY8zajHC8r4CfTWANjM1pGPUYisWtTKi1MxZw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -10870,7 +10870,7 @@
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"storybook": "10.5.8"
"storybook": "10.5.9"
},
"peerDependenciesMeta": {
"@types/react": {
@@ -10964,15 +10964,15 @@
}
},
"node_modules/@storybook/react-webpack5": {
"version": "10.5.8",
"resolved": "https://registry.npmjs.org/@storybook/react-webpack5/-/react-webpack5-10.5.8.tgz",
"integrity": "sha512-HkPi42WaoNSHC0DAERsJEF7Vhnluzsp/aiuhnH65GGYG5TmdLL9G8KDiYvXHGDCyb4RfoAPYrtzxaLMFfPPFvQ==",
"version": "10.5.9",
"resolved": "https://registry.npmjs.org/@storybook/react-webpack5/-/react-webpack5-10.5.9.tgz",
"integrity": "sha512-mrCJub/WAt6RAU1P+bpBvcdTHMHePXLuNM+TuJl0Sl3r9Ta0YjDvtdODweud9sTEpVN3ao/fk0LpiaDMfLd84w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@storybook/builder-webpack5": "10.5.8",
"@storybook/preset-react-webpack": "10.5.8",
"@storybook/react": "10.5.8"
"@storybook/builder-webpack5": "10.5.9",
"@storybook/preset-react-webpack": "10.5.9",
"@storybook/react": "10.5.9"
},
"funding": {
"type": "opencollective",
@@ -10981,7 +10981,7 @@
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"storybook": "10.5.8",
"storybook": "10.5.9",
"typescript": ">= 4.9.x"
},
"peerDependenciesMeta": {
@@ -10991,13 +10991,13 @@
}
},
"node_modules/@storybook/react-webpack5/node_modules/@storybook/builder-webpack5": {
"version": "10.5.8",
"resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-10.5.8.tgz",
"integrity": "sha512-ke5x27gtWQ4gpXCLWxdGkr8ZlJwBykV/KjbBTAlC04dmS9OkI9MBzGj+TteUlgrcaN7LwoNTR5zRmxKStOZYzQ==",
"version": "10.5.9",
"resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-10.5.9.tgz",
"integrity": "sha512-XTLC95jP75V9NfhoUzDNKDCn9r0ZqXz4ZhrNzQXVDz4vNWkjU3/uDL6Ywh9WFu6Xrb+onQqSR9hlHjS/DOZj7Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@storybook/core-webpack": "10.5.8",
"@storybook/core-webpack": "10.5.9",
"case-sensitive-paths-webpack-plugin": "^2.4.0",
"cjs-module-lexer": "^1.2.3",
"css-loader": "^7.1.2",
@@ -11019,7 +11019,7 @@
"url": "https://opencollective.com/storybook"
},
"peerDependencies": {
"storybook": "10.5.8"
"storybook": "10.5.9"
},
"peerDependenciesMeta": {
"typescript": {
@@ -11028,9 +11028,9 @@
}
},
"node_modules/@storybook/react-webpack5/node_modules/@storybook/builder-webpack5/node_modules/@storybook/core-webpack": {
"version": "10.5.8",
"resolved": "https://registry.npmjs.org/@storybook/core-webpack/-/core-webpack-10.5.8.tgz",
"integrity": "sha512-HccINB0UbTtnyJtKpaX+C35BRTSnAwnreIMwwI+LpeUd4x9mQg0G9orB7lfBBZwd5LQf8YhM2Vkjiawzo41GLg==",
"version": "10.5.9",
"resolved": "https://registry.npmjs.org/@storybook/core-webpack/-/core-webpack-10.5.9.tgz",
"integrity": "sha512-YmXR9RJdQpH8EtWEIjLTr5LMGiCXSmZp/A9UkATY5xHM4qG2QwJB++jU2wv4nGDyaPiUcbzT2PQSXN6RXhiUZA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -11041,17 +11041,17 @@
"url": "https://opencollective.com/storybook"
},
"peerDependencies": {
"storybook": "10.5.8"
"storybook": "10.5.9"
}
},
"node_modules/@storybook/react-webpack5/node_modules/@storybook/preset-react-webpack": {
"version": "10.5.8",
"resolved": "https://registry.npmjs.org/@storybook/preset-react-webpack/-/preset-react-webpack-10.5.8.tgz",
"integrity": "sha512-0JjgVoX5t9Wb+gwddYHx/Ej7KFqwd65lpHXEhBoT4pFWRqVI0pvfHu42M+DRGjsgOye3uE+3pH4yHR3+0/fCHA==",
"version": "10.5.9",
"resolved": "https://registry.npmjs.org/@storybook/preset-react-webpack/-/preset-react-webpack-10.5.9.tgz",
"integrity": "sha512-LOr8SoM2CejVCHeLyxbdRMiKuQb2q95CE5D75oK3+513mMZ8LxVPVApxy8B6FvFYNe9CTqVK+95jETefOqabTw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@storybook/core-webpack": "10.5.8",
"@storybook/core-webpack": "10.5.9",
"@storybook/react-docgen-typescript-plugin": "1.0.6--canary.9.0c3f3b7.0",
"@types/semver": "^7.7.1",
"magic-string": "^0.30.5",
@@ -11068,7 +11068,7 @@
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"storybook": "10.5.8"
"storybook": "10.5.9"
},
"peerDependenciesMeta": {
"typescript": {
@@ -11077,9 +11077,9 @@
}
},
"node_modules/@storybook/react-webpack5/node_modules/@storybook/preset-react-webpack/node_modules/@storybook/core-webpack": {
"version": "10.5.8",
"resolved": "https://registry.npmjs.org/@storybook/core-webpack/-/core-webpack-10.5.8.tgz",
"integrity": "sha512-HccINB0UbTtnyJtKpaX+C35BRTSnAwnreIMwwI+LpeUd4x9mQg0G9orB7lfBBZwd5LQf8YhM2Vkjiawzo41GLg==",
"version": "10.5.9",
"resolved": "https://registry.npmjs.org/@storybook/core-webpack/-/core-webpack-10.5.9.tgz",
"integrity": "sha512-YmXR9RJdQpH8EtWEIjLTr5LMGiCXSmZp/A9UkATY5xHM4qG2QwJB++jU2wv4nGDyaPiUcbzT2PQSXN6RXhiUZA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -11090,18 +11090,18 @@
"url": "https://opencollective.com/storybook"
},
"peerDependencies": {
"storybook": "10.5.8"
"storybook": "10.5.9"
}
},
"node_modules/@storybook/react-webpack5/node_modules/@storybook/react": {
"version": "10.5.8",
"resolved": "https://registry.npmjs.org/@storybook/react/-/react-10.5.8.tgz",
"integrity": "sha512-6qqkmqX6imtL+0Z9Uan2tIfYivOI0FiVmWr0zpqqQR15AkJ18JfNcNTQoyjeAlCO0Kei56SWqnu2qLq52TYplg==",
"version": "10.5.9",
"resolved": "https://registry.npmjs.org/@storybook/react/-/react-10.5.9.tgz",
"integrity": "sha512-kApGOuNT26NkpioTsr1iT/Q2c44tA7OIsNUSyFqtT7W8k3fRn/jQWfrDegYxty0WG0wxdNMZq2ndRfOOF8HaHw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@storybook/global": "^5.0.0",
"@storybook/react-dom-shim": "10.5.8",
"@storybook/react-dom-shim": "10.5.9",
"react-docgen": "^8.0.2",
"react-docgen-typescript": "^2.2.2"
},
@@ -11114,7 +11114,7 @@
"@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"storybook": "10.5.8",
"storybook": "10.5.9",
"typescript": ">= 4.9.x"
},
"peerDependenciesMeta": {
@@ -11130,9 +11130,9 @@
}
},
"node_modules/@storybook/react-webpack5/node_modules/@storybook/react/node_modules/@storybook/react-dom-shim": {
"version": "10.5.8",
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.5.8.tgz",
"integrity": "sha512-N8D13/Xny+V3kfe1KBgsAHS0nKWXLLdgOOXS9poKdYzVwVCN+CGEGBxWX0zMMtdCptqa6/57em9coPlZMoO+bg==",
"version": "10.5.9",
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.5.9.tgz",
"integrity": "sha512-7qZD6CSa64p1m/zX9tG4ALcEHlEk0Bx+5++4RL3MFlRCgCFfAomXsCHTzF0RyF8cI4vl+hS7Y0ryjQchVs6UQA==",
"dev": true,
"license": "MIT",
"funding": {
@@ -11144,7 +11144,7 @@
"@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"storybook": "10.5.8"
"storybook": "10.5.9"
},
"peerDependenciesMeta": {
"@types/react": {
@@ -14928,9 +14928,9 @@
}
},
"node_modules/antd": {
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/antd/-/antd-6.6.0.tgz",
"integrity": "sha512-UDwWIbpmrCHB9ZQ+bPh4vQfB6DTI2ulIyoQ0Tc9xxalFblttiNGHl3ySBD9SyV/8+gUjFzfSx1+iU1Fog2i46w==",
"version": "6.6.1",
"resolved": "https://registry.npmjs.org/antd/-/antd-6.6.1.tgz",
"integrity": "sha512-QHIHYoUk9N9nJy1T9fyxWKjY0qApdTEDd/6lzqYng8Uryv9FejNmbhKvYF7obGqB+TuLXQsPVF7fOVgyzM1KrQ==",
"license": "MIT",
"dependencies": {
"@ant-design/colors": "^8.0.1",
@@ -14964,16 +14964,16 @@
"@rc-component/rate": "~1.0.1",
"@rc-component/resize-observer": "^1.1.2",
"@rc-component/segmented": "~1.3.0",
"@rc-component/select": "~1.10.0",
"@rc-component/select": "~1.10.1",
"@rc-component/slider": "~1.1.1",
"@rc-component/steps": "~1.2.2",
"@rc-component/switch": "~1.0.3",
"@rc-component/table": "~1.11.0",
"@rc-component/table": "~1.11.1",
"@rc-component/tabs": "~1.12.0",
"@rc-component/tooltip": "~1.5.0",
"@rc-component/tour": "~2.4.0",
"@rc-component/tree": "~1.4.0",
"@rc-component/tree-select": "~1.16.0",
"@rc-component/tree-select": "~1.16.1",
"@rc-component/trigger": "^3.10.1",
"@rc-component/upload": "~1.1.1",
"@rc-component/util": "^1.12.0",
@@ -15715,9 +15715,9 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
"version": "2.11.14",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.14.tgz",
"integrity": "sha512-JyJ954WzuIR8/FFzX0o5krdSTrBAkcCSRfWSleRsIHSWV+cZe2FI1PKggVkFke1hBldRs+LRxUczzE9iPmgZww==",
"version": "2.11.15",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.15.tgz",
"integrity": "sha512-FwMjJJ7HnyZpWe+oWxegG0fezZyBZUagI5LZEoO3GCbtbKNwRfMH9Ue5d5v01PNePBy1QSfPSDTTeVL0Hb9EzA==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -18559,9 +18559,9 @@
}
},
"node_modules/dayjs": {
"version": "1.11.22",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.22.tgz",
"integrity": "sha512-1YRnxzt/AabP3GHxnaB9/b+ZScCKu5TeF+co+BWG+lnWVIwEcTFc1FVE0WLNmNO3sA6GGXL40i5qkHfbLzpwrg==",
"version": "1.11.23",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.23.tgz",
"integrity": "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==",
"license": "MIT"
},
"node_modules/debounce": {
@@ -19140,11 +19140,10 @@
}
},
"node_modules/dompurify": {
"version": "3.4.12",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.12.tgz",
"integrity": "sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==",
"version": "3.4.13",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.13.tgz",
"integrity": "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optional": true,
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
@@ -20237,9 +20236,9 @@
}
},
"node_modules/eslint-plugin-storybook": {
"version": "10.5.8",
"resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-10.5.8.tgz",
"integrity": "sha512-bf9W5nZyWdIaCUZf4aEZnEeD1mn+csNYX8dYUQjAo6L7/DkSLtr65R4zFZ1xeS4m6dOXO6UtUySesCSw4e8w1g==",
"version": "10.5.9",
"resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-10.5.9.tgz",
"integrity": "sha512-4Hrqccy/zttV0S/32TSUbqtizzj4sbkgYvv7UzHveH58v96PhrO+fSpOFAqYvzNDzdOU6smKwObFtzX+RZqj4w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -20248,7 +20247,7 @@
},
"peerDependencies": {
"eslint": ">=8",
"storybook": "10.5.8"
"storybook": "10.5.9"
}
},
"node_modules/eslint-plugin-testing-library": {
@@ -37735,9 +37734,9 @@
}
},
"node_modules/storybook": {
"version": "10.5.8",
"resolved": "https://registry.npmjs.org/storybook/-/storybook-10.5.8.tgz",
"integrity": "sha512-rR4oFMSiWBSqI0lvsJPtcQUPj8+hzj3TkLu+Mw61Wo6YxPSb5FsLSHai0jZnuaIdKIlmu25KCfwlSQl4e1uvnA==",
"version": "10.5.9",
"resolved": "https://registry.npmjs.org/storybook/-/storybook-10.5.9.tgz",
"integrity": "sha512-UfdMKSjEhIKr8LbqYyIE5r7vT/drL/PxN75YaouJ+UG0FssEy6cf49OdTF3kstAqVMHskc+zEqyRoiQHZXHwgA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -42986,7 +42985,7 @@
"d3-scale": "^4.0.2",
"d3-time": "^3.1.0",
"d3-time-format": "^4.1.0",
"dayjs": "^1.11.22",
"dayjs": "^1.11.23",
"dompurify": "^3.4.13",
"fetch-retry": "^6.0.0",
"handlebars": "^4.7.9",
@@ -43146,189 +43145,6 @@
"version": "0.20.3",
"license": "Apache-2.0"
},
"plugins/legacy-plugin-chart-calendar": {
"name": "@superset-ui/legacy-plugin-chart-calendar",
"version": "0.20.3",
"extraneous": true,
"license": "Apache-2.0",
"dependencies": {
"d3-array": "^3.2.4",
"d3-selection": "^3.0.0",
"d3-tip": "^0.9.1",
"prop-types": "^15.8.1"
},
"peerDependencies": {
"@apache-superset/core": "*",
"@emotion/react": "^11.4.1",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"react": "^18.3.0"
}
},
"plugins/legacy-plugin-chart-chord": {
"name": "@superset-ui/legacy-plugin-chart-chord",
"version": "0.20.3",
"extraneous": true,
"license": "Apache-2.0",
"dependencies": {
"d3": "^3.5.17",
"prop-types": "^15.8.1",
"react": "^19.2.7"
},
"peerDependencies": {
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*"
}
},
"plugins/legacy-plugin-chart-country-map": {
"name": "@superset-ui/legacy-plugin-chart-country-map",
"version": "0.20.3",
"extraneous": true,
"license": "Apache-2.0",
"dependencies": {
"d3": "^3.5.17",
"d3-array": "^3.2.4",
"prop-types": "^15.8.1"
},
"peerDependencies": {
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"react": "^18.3.0"
}
},
"plugins/legacy-plugin-chart-horizon": {
"name": "@superset-ui/legacy-plugin-chart-horizon",
"version": "0.20.3",
"extraneous": true,
"license": "Apache-2.0",
"dependencies": {
"d3-array": "^3.2.4",
"d3-scale": "^4.0.2",
"prop-types": "^15.8.1"
},
"peerDependencies": {
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"react": "^18.3.0"
}
},
"plugins/legacy-plugin-chart-paired-t-test": {
"name": "@superset-ui/legacy-plugin-chart-paired-t-test",
"version": "0.20.3",
"extraneous": true,
"license": "Apache-2.0",
"dependencies": {
"distributions": "^2.2.0",
"prop-types": "^15.8.1",
"reactable": "^1.1.0"
},
"peerDependencies": {
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"react": "^18.3.0"
}
},
"plugins/legacy-plugin-chart-parallel-coordinates": {
"name": "@superset-ui/legacy-plugin-chart-parallel-coordinates",
"version": "0.20.3",
"extraneous": true,
"license": "Apache-2.0",
"dependencies": {
"d3v3": "npm:d3@3.5.17",
"prop-types": "^15.8.1"
},
"peerDependencies": {
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"react": "^18.3.0"
}
},
"plugins/legacy-plugin-chart-partition": {
"name": "@superset-ui/legacy-plugin-chart-partition",
"version": "0.20.3",
"extraneous": true,
"license": "Apache-2.0",
"dependencies": {
"d3": "^3.5.17",
"d3-hierarchy": "^3.1.2",
"prop-types": "^15.8.1"
},
"peerDependencies": {
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@testing-library/jest-dom": "*",
"@testing-library/react": "^15.0.0",
"react": "^18.3.0",
"react-dom": "^18.3.0"
}
},
"plugins/legacy-plugin-chart-rose": {
"name": "@superset-ui/legacy-plugin-chart-rose",
"version": "0.20.3",
"extraneous": true,
"license": "Apache-2.0",
"dependencies": {
"d3": "^3.5.17",
"nvd3-fork": "^2.0.5",
"prop-types": "^15.8.1"
},
"peerDependencies": {
"@apache-superset/core": "*",
"@emotion/react": "^11.4.1",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"react": "^18.3.0"
}
},
"plugins/legacy-plugin-chart-world-map": {
"name": "@superset-ui/legacy-plugin-chart-world-map",
"version": "0.20.3",
"extraneous": true,
"license": "Apache-2.0",
"dependencies": {
"d3": "^3.5.17",
"d3-array": "^3.2.4",
"datamaps": "^0.5.10",
"prop-types": "^15.8.1",
"tinycolor2": "^1.6.0"
},
"peerDependencies": {
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"react": "^18.3.0"
}
},
"plugins/legacy-preset-chart-nvd3": {
"name": "@superset-ui/legacy-preset-chart-nvd3",
"version": "0.20.3",
"extraneous": true,
"license": "Apache-2.0",
"dependencies": {
"d3": "^3.5.17",
"d3-tip": "^0.9.1",
"dompurify": "^3.4.12",
"fast-safe-stringify": "^2.1.1",
"lodash": "^4.18.1",
"lodash-es": "^4.18.1",
"nvd3-fork": "^2.0.5",
"prop-types": "^15.8.1",
"urijs": "^1.19.11"
},
"peerDependencies": {
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"dayjs": "^1.11.21",
"react": "^18.3.0"
}
},
"plugins/plugin-chart-ag-grid-table": {
"name": "@superset-ui/plugin-chart-ag-grid-table",
"version": "0.20.3",
+8 -8
View File
@@ -158,12 +158,12 @@
"@visx/xychart": "^4.0.0",
"ag-grid-community": "36.1.0",
"ag-grid-react": "36.1.0",
"antd": "^6.6.0",
"antd": "^6.6.1",
"chrono-node": "^2.10.1",
"classnames": "^2.2.5",
"content-disposition": "^2.0.1",
"d3-scale": "^4.0.2",
"dayjs": "^1.11.22",
"dayjs": "^1.11.23",
"dom-to-image-more": "^3.10.2",
"dom-to-pdf": "^0.3.2",
"echarts": "^6.1.0",
@@ -257,9 +257,9 @@
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@playwright/test": "^1.62.1",
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
"@storybook/addon-docs": "10.5.8",
"@storybook/addon-links": "10.5.8",
"@storybook/react-webpack5": "10.5.8",
"@storybook/addon-docs": "10.5.9",
"@storybook/addon-links": "10.5.9",
"@storybook/react-webpack5": "10.5.9",
"@storybook/test-runner": "0.24.4",
"@svgr/webpack": "^8.1.0",
"@swc/core": "^1.16.0",
@@ -295,7 +295,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.11.14",
"baseline-browser-mapping": "^2.11.15",
"cheerio": "1.2.0",
"concurrently": "^10.0.5",
"copy-webpack-plugin": "^14.0.0",
@@ -312,7 +312,7 @@
"eslint-plugin-no-only-tests": "^3.4.0",
"eslint-plugin-react-prefer-function-component": "^5.0.0",
"eslint-plugin-react-you-might-not-need-an-effect": "^1.0.1",
"eslint-plugin-storybook": "10.5.8",
"eslint-plugin-storybook": "10.5.9",
"eslint-plugin-testing-library": "^7.16.2",
"eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
"fetch-mock": "^12.6.0",
@@ -343,7 +343,7 @@
"source-map": "^0.8.0",
"source-map-support": "^0.5.21",
"speed-measure-webpack-plugin": "^1.6.0",
"storybook": "10.5.8",
"storybook": "10.5.9",
"style-loader": "^4.0.0",
"stylelint": "^17.14.1",
"swc-loader": "^0.2.7",
@@ -17,7 +17,7 @@
* under the License.
*/
import { QueryFormMetric } from '@superset-ui/core';
import { getTotalsMetrics } from './getTotalsMetrics';
import { getTotalsMetrics, toTotalsAggregate } from './getTotalsMetrics';
const simpleMetric = (aggregate: string): QueryFormMetric =>
({
@@ -76,4 +76,31 @@ describe('getTotalsMetrics', () => {
test('returns an empty array when given no metrics', () => {
expect(getTotalsMetrics([], 'AVG')).toEqual([]);
});
test("ORIGINAL keeps each metric's own aggregate", () => {
const metrics = [
simpleMetric('COUNT_DISTINCT'),
sqlMetric(),
savedMetric(),
];
const result = getTotalsMetrics(metrics, 'ORIGINAL');
expect(result).toBe(metrics);
expect(result[0]).toEqual(
expect.objectContaining({ aggregate: 'COUNT_DISTINCT' }),
);
});
});
describe('toTotalsAggregate', () => {
test.each(['SUM', 'AVG'] as const)('passes %s through', value => {
expect(toTotalsAggregate(value)).toBe(value);
});
test.each([undefined, null, '', 'MEDIAN', 'sum'])(
'falls back to ORIGINAL for %p',
value => {
expect(toTotalsAggregate(value)).toBe('ORIGINAL');
},
);
});
@@ -18,26 +18,46 @@
*/
import { isAdhocMetricSimple, QueryFormMetric } from '@superset-ui/core';
export type TotalsAggregate = 'SUM' | 'AVG';
/**
* How the "Show summary" totals row aggregates each metric.
*
* ``ORIGINAL`` keeps every metric's own aggregation. It is the default because
* overriding is not universally valid: ``SUM`` over a ``COUNT_DISTINCT`` of a
* non-numeric column (a uuid, say) is rejected outright by the database, and
* over a numeric id column it silently produces a meaningless number.
*/
export type TotalsAggregate = 'ORIGINAL' | 'SUM' | 'AVG';
/**
* Build the metrics for a chart's "Show summary" totals query, overriding
* each Simple (adhoc) metric's aggregate function with the user-chosen
* totals aggregate. The totals query has no GROUP BY, so the database
* evaluates each metric fresh over all rows -- swapping the aggregate here
* is a correct, independent computation, not a re-aggregation of
* already-aggregated per-row values.
* Build the metrics for a chart's "Show summary" totals query.
*
* Custom-SQL metrics and saved (string) metrics pass through unchanged:
* there is no safe way to rewrite an arbitrary SQL expression's aggregate
* function without parsing it, so the totals row keeps their own native
* aggregate for those.
* With SUM or AVG, each Simple (adhoc) metric is cloned with its aggregate
* replaced. The totals query has no GROUP BY, so the database evaluates each
* metric fresh over all rows -- that swap is an independent computation, not a
* re-aggregation of already-aggregated per-row values.
*
* Custom-SQL and saved (string) metrics always pass through unchanged: there is
* no safe way to rewrite an arbitrary SQL expression's aggregate without
* parsing it, so the totals row keeps their own native aggregate.
*/
export function getTotalsMetrics(
metrics: QueryFormMetric[],
aggregate: TotalsAggregate,
): QueryFormMetric[] {
if (aggregate === 'ORIGINAL') {
return metrics;
}
return metrics.map(metric =>
isAdhocMetricSimple(metric) ? { ...metric, aggregate } : metric,
);
}
/**
* Narrow a raw ``totals_aggregate`` form-data value to a TotalsAggregate.
*
* Anything other than an explicit SUM/AVG — including charts saved before the
* control existed — keeps each metric's own aggregation.
*/
export function toTotalsAggregate(value: unknown): TotalsAggregate {
return value === 'SUM' || value === 'AVG' ? value : 'ORIGINAL';
}
@@ -67,7 +67,7 @@
"d3-scale": "^4.0.2",
"d3-time": "^3.1.0",
"d3-time-format": "^4.1.0",
"dayjs": "^1.11.22",
"dayjs": "^1.11.23",
"dompurify": "^3.4.13",
"fetch-retry": "^6.0.0",
"handlebars": "^4.7.9",
@@ -64,9 +64,8 @@ export default function createSmartNumberFormatter(
description,
formatFunc: value => `${getSign(value)}${formatValue(value)}`,
id:
id || signed
? NumberFormats.SMART_NUMBER_SIGNED
: NumberFormats.SMART_NUMBER,
id ??
(signed ? NumberFormats.SMART_NUMBER_SIGNED : NumberFormats.SMART_NUMBER),
label: label ?? 'Adaptive formatter',
});
}
@@ -24,6 +24,12 @@ describe('createSmartNumberFormatter(options)', () => {
const formatter = createSmartNumberFormatter();
expect(formatter).toBeInstanceOf(NumberFormatter);
});
test('uses the supplied formatter id regardless of signed option', () => {
expect(createSmartNumberFormatter({ id: 'custom' }).id).toBe('custom');
expect(
createSmartNumberFormatter({ id: 'custom-signed', signed: true }).id,
).toBe('custom-signed');
});
describe('using default options', () => {
const formatter = createSmartNumberFormatter();
test('formats 0 correctly', () => {
@@ -38,7 +38,7 @@ import {
getTotalsMetrics,
isTimeComparison,
timeCompareOperator,
TotalsAggregate,
toTotalsAggregate,
} from '@superset-ui/chart-controls';
import { isEmpty } from 'lodash-es';
import { TableChartFormData } from './types';
@@ -696,13 +696,16 @@ export const buildQueryUncached: BuildQuery<TableChartFormData> = (
formData.show_totals &&
queryMode === QueryMode.Aggregate,
);
const totalsAggregate: TotalsAggregate =
formData.totals_aggregate === 'AVG' ? 'AVG' : 'SUM';
const totalsAggregate = toTotalsAggregate(formData.totals_aggregate);
// Raw-mode summary columns have no metric of their own to preserve, so
// ORIGINAL has nothing to fall back to; sum them as before.
const rawSummaryAggregate =
totalsAggregate === 'ORIGINAL' ? 'SUM' : totalsAggregate;
const totalsMetrics =
rawSummaryColumns.length > 0
? rawSummaryColumns.map(columnName => ({
expressionType: 'SIMPLE' as const,
aggregate: totalsAggregate,
aggregate: rawSummaryAggregate,
column: { column_name: columnName },
label: columnName,
}))
@@ -503,14 +503,18 @@ const config: ControlPanelConfig = {
label: t('Summary aggregation'),
renderTrigger: true,
description: t(
'Aggregation used for the summary row, independent of each ' +
"metric's own aggregation. Only applies to simple metrics " +
'(a metric built from custom SQL keeps its own aggregation ' +
'in the summary row).',
'Aggregation used for the summary row. By default each metric ' +
'keeps its own aggregation; Sum and Average override it for ' +
'the summary row only. The override applies to simple ' +
'metrics (a metric built from custom SQL always keeps its ' +
'own aggregation). Overriding a count or a distinct count ' +
'sums the counted column instead, which fails outright on a ' +
'non-numeric column.',
),
default: 'SUM',
default: 'ORIGINAL',
clearable: false,
choices: [
['ORIGINAL', t("Each metric's own")],
['SUM', t('Sum')],
['AVG', t('Average')],
],
@@ -1561,7 +1561,7 @@ describe('plugin-chart-ag-grid-table', () => {
expect(queries[1].metrics).toEqual(['count']);
});
test('defaults aggregate-mode totals to SUM for a simple metric', () => {
test("defaults aggregate-mode totals to the metric's own aggregate", () => {
const simpleMetric = {
expressionType: 'SIMPLE' as const,
column: { column_name: 'sales' },
@@ -1580,9 +1580,29 @@ describe('plugin-chart-ag-grid-table', () => {
{ ownState: {} },
);
expect(queries[1].metrics).toEqual([
{ ...simpleMetric, aggregate: 'SUM' },
]);
expect(queries[1].metrics).toEqual([simpleMetric]);
});
test('keeps COUNT_DISTINCT in aggregate-mode totals by default', () => {
const countDistinctMetric = {
expressionType: 'SIMPLE' as const,
column: { column_name: 'contract_id' },
aggregate: 'COUNT_DISTINCT' as const,
label: 'contracts',
};
const { queries } = buildQuery(
{
viz_type: VizType.Table,
datasource: '11__table',
query_mode: QueryMode.Aggregate,
groupby: ['state'],
metrics: [countDistinctMetric],
show_totals: true,
},
{ ownState: {} },
);
expect(queries[1].metrics).toEqual([countDistinctMetric]);
});
test('overrides aggregate-mode totals to AVG for a simple metric when totals_aggregate is set', () => {
@@ -34,7 +34,7 @@ import {
getTotalsMetrics,
isTimeComparison,
timeCompareOperator,
TotalsAggregate,
toTotalsAggregate,
} from '@superset-ui/chart-controls';
import { isEmpty } from 'lodash-es';
import { TableChartFormData } from './types';
@@ -349,8 +349,7 @@ export const buildQuery: BuildQuery<TableChartFormData> = (
formData.show_totals &&
queryMode === QueryMode.Aggregate
) {
const totalsAggregate: TotalsAggregate =
formData.totals_aggregate === 'AVG' ? 'AVG' : 'SUM';
const totalsAggregate = toTotalsAggregate(formData.totals_aggregate);
extraQueries.push({
...queryObject,
columns: [],
@@ -475,14 +475,18 @@ const config: ControlPanelConfig = {
type: 'SelectControl',
label: t('Summary aggregation'),
description: t(
'Aggregation used for the summary row, independent of each ' +
"metric's own aggregation. Only applies to simple metrics " +
'(a metric built from custom SQL keeps its own aggregation ' +
'in the summary row).',
'Aggregation used for the summary row. By default each metric ' +
'keeps its own aggregation; Sum and Average override it for ' +
'the summary row only. The override applies to simple ' +
'metrics (a metric built from custom SQL always keeps its ' +
'own aggregation). Overriding a count or a distinct count ' +
'sums the counted column instead, which fails outright on a ' +
'non-numeric column.',
),
default: 'SUM',
default: 'ORIGINAL',
clearable: false,
choices: [
['ORIGINAL', t("Each metric's own")],
['SUM', t('Sum')],
['AVG', t('Average')],
],
@@ -340,7 +340,7 @@ describe('plugin-chart-table', () => {
label: 'sum_sales',
};
test('defaults the totals query metric aggregate to SUM', () => {
test("defaults to each metric's own aggregate", () => {
const { queries } = buildQueryCached({
...basicFormData,
query_mode: QueryMode.Aggregate,
@@ -350,9 +350,28 @@ describe('plugin-chart-table', () => {
});
expect(queries).toHaveLength(2);
expect(queries[1].metrics).toEqual([
{ ...simpleMetric, aggregate: 'SUM' },
]);
expect(queries[1].metrics).toEqual([simpleMetric]);
});
test('keeps COUNT_DISTINCT in the summary row by default', () => {
// Overriding this to SUM sums the counted column instead of counting
// it, which is meaningless on a numeric id and is rejected outright by
// the database on a non-numeric one (e.g. a uuid).
const countDistinctMetric = {
expressionType: 'SIMPLE' as const,
column: { column_name: 'contract_id' },
aggregate: 'COUNT_DISTINCT' as const,
label: 'contracts',
};
const { queries } = buildQueryCached({
...basicFormData,
query_mode: QueryMode.Aggregate,
metrics: [countDistinctMetric],
groupby: ['category'],
show_totals: true,
});
expect(queries[1].metrics).toEqual([countDistinctMetric]);
});
test('overrides simple metric aggregate with totals_aggregate for the summary query only', () => {
@@ -669,11 +669,11 @@ describe('UploadDataModal Collapse Tabs', () => {
useRedux: true,
});
const generalInfoTab = screen.getByRole('tab', {
name: /expanded General information/i,
name: /General information/i,
});
expect(generalInfoTab).toHaveAttribute('aria-expanded', 'true');
const fileSettingsTab = screen.getByRole('tab', {
name: /collapsed File settings/i,
name: /File settings/i,
});
await userEvent.click(fileSettingsTab);
await waitFor(() => {
@@ -689,11 +689,11 @@ describe('UploadDataModal Collapse Tabs', () => {
useRedux: true,
});
const generalInfoTab = screen.getByRole('tab', {
name: /expanded General information/i,
name: /General information/i,
});
expect(generalInfoTab).toHaveAttribute('aria-expanded', 'true');
const fileSettingsTab = screen.getByRole('tab', {
name: /collapsed File settings/i,
name: /File settings/i,
});
await userEvent.click(fileSettingsTab);
await waitFor(() => {
@@ -709,11 +709,11 @@ describe('UploadDataModal Collapse Tabs', () => {
useRedux: true,
});
const generalInfoTab = screen.getByRole('tab', {
name: /expanded General information/i,
name: /General information/i,
});
expect(generalInfoTab).toHaveAttribute('aria-expanded', 'true');
const fileSettingsTab = screen.getByRole('tab', {
name: /collapsed File settings/i,
name: /File settings/i,
});
await userEvent.click(fileSettingsTab);
await waitFor(() => {
@@ -182,7 +182,7 @@ test('With sql role - renders all panels on the page on page load', async () =>
test('With sql role - renders distinct recent activities', async () => {
await renderWelcome();
const recentPanel = screen.getByRole('button', { name: 'collapsed Recents' });
const recentPanel = screen.getByRole('button', { name: 'Recents' });
userEvent.click(recentPanel);
await waitFor(() =>
expect(
-12
View File
@@ -32,13 +32,11 @@ from superset.commands.chart.exceptions import (
DashboardsForbiddenError,
DashboardsNotFoundValidationError,
)
from superset.commands.exceptions import DatasourceTypeInvalidError
from superset.commands.utils import get_datasource_by_id, populate_subjects
from superset.daos.chart import ChartDAO
from superset.daos.dashboard import DashboardDAO
from superset.exceptions import SupersetSecurityException
from superset.utils import json
from superset.utils.core import DatasourceType
from superset.utils.decorators import on_error, transaction
logger = logging.getLogger(__name__)
@@ -73,16 +71,6 @@ class CreateChartCommand(CreateMixin, BaseCommand):
# Validate/Populate datasource
try:
# Slice.datasource only ever resolves the ``table`` relationship
# (see Slice.datasource in superset/models/slice.py), so a chart
# pointed at any other datasource_type would "create"
# successfully but could never actually render. Reject those
# up front instead of failing later -- either at this lookup
# (SavedQuery/Query have no ``.name`` attribute, so accessing it
# below raises an unhandled AttributeError) or silently, by
# producing a permanently broken chart.
if datasource_type != DatasourceType.TABLE:
raise DatasourceTypeInvalidError()
datasource = get_datasource_by_id(datasource_id, datasource_type)
self._properties["datasource_name"] = datasource.name
security_manager.raise_for_access(datasource=datasource)
+1 -17
View File
@@ -35,7 +35,6 @@ from superset.commands.chart.exceptions import (
DashboardsNotFoundValidationError,
DatasourceTypeUpdateRequiredValidationError,
)
from superset.commands.exceptions import DatasourceTypeInvalidError
from superset.commands.utils import (
compute_subjects,
get_datasource_by_id,
@@ -50,7 +49,6 @@ from superset.models.dashboard import Dashboard
from superset.models.slice import Slice
from superset.tags.models import ObjectType
from superset.utils import json
from superset.utils.core import DatasourceType
from superset.utils.decorators import on_error, transaction
from superset.versioning.changes.normalization import (
register_matching_normalization_context,
@@ -223,22 +221,8 @@ class UpdateChartCommand(UpdateMixin, BaseCommand):
exceptions.append(ex)
# Validate/Populate datasource
# An empty datasource_type was already flagged above via
# DatasourceTypeUpdateRequiredValidationError; skip this block so
# we don't clobber that message with DatasourceTypeInvalidError.
if datasource_id is not None and datasource_type:
if datasource_id is not None:
try:
# Slice.datasource only ever resolves the ``table``
# relationship (see Slice.datasource in
# superset/models/slice.py), so repointing a chart at any
# other datasource_type would "succeed" but leave the chart
# permanently unable to render. Reject those up front
# instead of failing later -- either at this lookup
# (SavedQuery/Query have no ``.name`` attribute, so
# accessing it below raises an unhandled AttributeError) or
# silently.
if datasource_type != DatasourceType.TABLE:
raise DatasourceTypeInvalidError()
datasource = get_datasource_by_id(datasource_id, datasource_type)
self._properties["datasource_name"] = datasource.name
security_manager.raise_for_access(datasource=datasource)
+14
View File
@@ -324,6 +324,20 @@ def load_configs(
)
exc.messages = {file_name: exc.messages}
exceptions.append(exc)
except json.JSONDecodeError as exc:
# masked_encrypted_extra comes straight from the imported YAML
# (before schema validation) and may not be valid JSON. Convert
# the raw decode error into a ValidationError so it flows into
# the aggregated CommandInvalidError like every other per-file
# validation failure, instead of escaping as an opaque 500.
logger.error(
"Invalid JSON in masked_encrypted_extra for %s: %s",
file_name,
exc,
)
exceptions.append(
ValidationError({file_name: {"masked_encrypted_extra": [str(exc)]}})
)
return configs
@@ -379,37 +379,37 @@ msgstr "虛擬"
msgid "%s aggregates(s)"
msgstr "%s 聚合"
#, fuzzy, python-format
#, python-format
msgid "%s column"
msgid_plural "%s columns"
msgstr[0] "%s "
msgstr[0] "%s 個欄位"
#, python-format
msgid "%s column(s)"
msgstr "%s "
msgstr "%s 個欄位"
#, fuzzy, python-format
#, python-format
msgid "%s day ago"
msgid_plural "%s days ago"
msgstr[0] "1前"
msgstr[0] "%s 天前"
#, fuzzy, python-format
#, python-format
msgid "%s hr ago"
msgid_plural "%s hr ago"
msgstr[0] "%s "
msgstr[0] "%s 小時前"
#, fuzzy, python-format
#, python-format
msgid "%s imported"
msgstr "數據集已導入"
msgstr "已匯入 %s"
#, fuzzy, python-format
#, python-format
msgid "%s item"
msgid_plural "%s items"
msgstr[0] "%s 個項"
msgstr[0] "%s 個項"
#, fuzzy, python-format
#, python-format
msgid "%s item(s)"
msgstr "%s 個項"
msgstr "%s 個項"
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ar, cs, de,
# es, fa, fr, ja, lv, mi, nl, pl, pt_BR, ru, sk, sl, sr, sr_Latn, tr, uk]
@@ -419,15 +419,15 @@ msgid ""
"all selected objects."
msgstr "%s 個項目無法標記,因為您對所有選取的物件沒有編輯權限。"
#, fuzzy, python-format
#, python-format
msgid "%s metric"
msgid_plural "%s metrics"
msgstr[0] "排序指標"
msgstr[0] "%s 個指標"
#, fuzzy, python-format
#, python-format
msgid "%s min ago"
msgid_plural "%s min ago"
msgstr[0] ""
msgstr[0] "%s 分鐘前"
#, python-format
msgid ""
@@ -451,47 +451,47 @@ msgstr[0] "%s 個選項"
msgid "%s option(s)"
msgstr "%s 個選項"
#, fuzzy, python-format
#, python-format
msgid "%s out of %s column"
msgid_plural "%s out of %s columns"
msgstr[0] "自定義列"
msgstr[0] "已選取 %s%s 個欄位"
#, fuzzy, python-format
#, python-format
msgid "%s out of %s metric"
msgid_plural "%s out of %s metrics"
msgstr[0] "排序指標"
msgstr[0] "已選取 %s%s 個指標"
#, fuzzy, python-format
#, python-format
msgid "%s out of %s selected"
msgstr "%s 已選定"
msgstr "已選取 %s%s 個項目"
#, fuzzy, python-format
#, python-format
msgid "%s recipients"
msgstr "%s 最近"
msgstr "%s 收件者"
#, fuzzy, python-format
#, python-format
msgid "%s record..."
msgid_plural "%s records..."
msgstr[0] "%s 異常"
msgstr[0] "%s 筆記錄..."
#, fuzzy, python-format
#, python-format
msgid "%s row"
msgid_plural "%s rows"
msgstr[0] "%s "
msgstr[0] "%s "
#, fuzzy, python-format
#, python-format
msgid "%s s ago"
msgid_plural "%s s ago"
msgstr[0] "30 天之前"
msgstr[0] "%s 秒前"
#, python-format
msgid "%s saved metric(s)"
msgstr "%s 保存的指標"
#, fuzzy, python-format
#, python-format
msgid "%s second"
msgid_plural "%s seconds"
msgstr[0] "5 秒"
msgstr[0] "%s 秒"
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, es, sr,
# sr_Latn]
@@ -511,13 +511,13 @@ msgstr "%s 個語意檢視新增失敗"
msgid "%s semantic view(s) failed to add: %s"
msgstr "%s 個語意檢視新增失敗:%s"
#, fuzzy, python-format
#, python-format
msgid "%s tab selected"
msgstr "%s 已選定"
msgstr "已選取「%s」分頁"
#, fuzzy, python-format
#, python-format
msgid "%s updated"
msgstr "上次更新 %s"
msgstr "更新 %s"
#, python-format
msgid "%s%s"
@@ -677,13 +677,11 @@ msgstr "每年年初的頻率"
msgid "10 minute"
msgstr "10 分鐘"
#, fuzzy
msgid "10 seconds"
msgstr "30 秒"
msgstr "10 秒"
#, fuzzy
msgid "10/90 percentiles"
msgstr "9/91 百分位"
msgstr "10/90 百分位"
#. do-not-translate
msgid "10000"
@@ -696,9 +694,8 @@ msgstr "週"
msgid "104 weeks ago"
msgstr "104 週之前"
#, fuzzy
msgid "12 hours"
msgstr "1 小時"
msgstr "12 小時"
msgid "15 minute"
msgstr "15 分鐘"
@@ -761,9 +758,8 @@ msgstr "2/98 百分位"
msgid "22"
msgstr "22"
#, fuzzy
msgid "24 hours"
msgstr "6 小時"
msgstr "24 小時"
#, fuzzy
msgid "28 days"
@@ -831,9 +827,8 @@ msgstr "5 秒"
msgid "5 seconds"
msgstr "5 秒"
#, fuzzy
msgid "5/95 percentiles"
msgstr "9/91 百分位"
msgstr "5/95 百分位"
#, fuzzy
msgid "52 weeks"
@@ -35,14 +35,12 @@ from superset.extensions import cache_manager, db, security_manager
from superset.models.core import Database, FavStar, FavStarClassName
from superset.models.dashboard import Dashboard
from superset.models.slice import Slice
from superset.models.sql_lab import SavedQuery
from superset.reports.models import ReportSchedule, ReportScheduleType
from superset.subjects.models import Subject
from superset.subjects.types import SubjectType
from superset.tags.models import ObjectType, Tag, TaggedObject, TagType
from superset.utils import json
from superset.utils.core import get_example_default_schema
from superset.utils.database import get_example_database
from tests.integration_tests.base_api_tests import ApiEditorsTestCaseMixin
from tests.integration_tests.base_tests import (
subjects_from_users,
@@ -662,47 +660,6 @@ class TestChartApi(ApiEditorsTestCaseMixin, InsertChartMixin, SupersetTestCase):
response = json.loads(rv.data.decode("utf-8"))
assert response == {"message": {"datasource_id": ["Datasource does not exist"]}}
def test_create_chart_from_saved_query_rejected_cleanly(self):
"""
Chart API: creating a chart with datasource_type="saved_query" must
fail with a clean validation error, not the unhandled 500 "Fatal
error" reported in apache/superset#29697. Slice.datasource only
ever resolves the "table" relationship, so even a chart that
"created" successfully with this datasource_type could never
actually render -- "saved_query" is a real, existing row here
(not a bad ID), reproducing the original report exactly rather
than a not-found case.
"""
self.login(ADMIN_USERNAME)
example_db = get_example_database()
saved_query = SavedQuery(
db_id=example_db.id,
label="issue-29697-repro",
schema=get_example_default_schema(),
sql="SELECT 1 AS value",
)
db.session.add(saved_query)
db.session.commit()
saved_query_id = saved_query.id
chart_data = {
"slice_name": "issue-29697-repro-chart",
"datasource_id": saved_query_id,
"datasource_type": "saved_query",
"viz_type": "table",
}
try:
rv = self.post_assert_metric("/api/v1/chart/", chart_data, "post")
assert rv.status_code == 422
response = json.loads(rv.data.decode("utf-8"))
assert response == {
"message": {"datasource_type": ["Datasource type is invalid"]}
}
finally:
db.session.delete(db.session.query(SavedQuery).get(saved_query_id))
db.session.commit()
@pytest.mark.usefixtures("load_world_bank_dashboard_with_slices")
def test_create_chart_validate_user_is_dashboard_editor(self):
"""
@@ -1,154 +0,0 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Unit tests for CreateChartCommand.
Regression coverage for apache/superset#29697: POST /api/v1/chart/ with
datasource_type="saved_query" (or "query") crashes with an unhandled
AttributeError -- reported to API clients as an opaque 500 "Fatal error" --
because SavedQuery and Query models have no ``.name`` attribute, and because
Slice.datasource only ever resolves a ``table``-typed datasource, so even a
successfully created chart of another type could never actually render.
"""
import pytest
from pytest_mock import MockerFixture
from superset.commands.chart.create import CreateChartCommand
from superset.commands.chart.exceptions import ChartForbiddenError, ChartInvalidError
from superset.commands.exceptions import DatasourceTypeInvalidError
from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
from superset.exceptions import SupersetSecurityException
def _base_mocks(mocker: MockerFixture) -> None:
mocker.patch(
"superset.commands.chart.create.DashboardDAO.find_by_ids", return_value=[]
)
mocker.patch(
"superset.commands.chart.create.populate_subjects",
side_effect=lambda properties, exceptions: None,
)
@pytest.mark.parametrize("datasource_type", ["saved_query", "query"])
def test_create_chart_rejects_non_table_datasource_type(
mocker: MockerFixture, datasource_type: str
) -> None:
"""A chart can only ever query a table-backed datasource -- Slice.datasource
only ever resolves the ``table`` relationship, so any other type would
produce a chart that "creates" successfully but can never render.
The two types fail differently before this fix, which is exactly why
both are covered here:
- "saved_query": SavedQuery has no ``.name`` attribute, so validation
crashes with an unhandled AttributeError -- surfaced to API clients as
an opaque 500 "Fatal error" (apache/superset#29697).
- "query": Query *does* define a synthetic ``.name`` property (used for
CTAS table naming, not as a real display name), so this one doesn't
crash -- it silently "succeeds" and creates a chart with a nonsense
name and a datasource that Slice.datasource can never resolve.
``get_datasource_by_id`` is mocked with ``spec=`` the real model classes
so accessing ``.name`` on the mock behaves exactly like the real ORM
objects do if the new guard doesn't stop the code from getting there;
``raise_for_access`` is mocked to a no-op so nothing downstream masks
that behavior.
"""
from superset.models.sql_lab import Query, SavedQuery
_base_mocks(mocker)
model_cls = SavedQuery if datasource_type == "saved_query" else Query
get_datasource_by_id = mocker.patch(
"superset.commands.chart.create.get_datasource_by_id",
return_value=mocker.MagicMock(spec=model_cls),
)
mocker.patch("superset.commands.chart.create.security_manager.raise_for_access")
with pytest.raises(ChartInvalidError) as exc_info:
CreateChartCommand(
{
"datasource_id": 11,
"datasource_type": datasource_type,
"slice_name": "some_name",
"viz_type": "table",
}
).validate()
assert any(
isinstance(ex, DatasourceTypeInvalidError) for ex in exc_info.value._exceptions
)
# The invalid type must be rejected before ever touching the datasource
# lookup, not caught incidentally by some downstream failure.
get_datasource_by_id.assert_not_called()
def test_create_chart_accepts_table_datasource(mocker: MockerFixture) -> None:
"""The one supported datasource_type must keep working."""
_base_mocks(mocker)
datasource = mocker.MagicMock(name="table_datasource")
datasource.name = "my_table"
mocker.patch(
"superset.commands.chart.create.get_datasource_by_id",
return_value=datasource,
)
mocker.patch("superset.commands.chart.create.security_manager.raise_for_access")
cmd = CreateChartCommand(
{
"datasource_id": 11,
"datasource_type": "table",
"slice_name": "some_name",
"viz_type": "table",
}
)
cmd.validate()
assert cmd._properties["datasource_name"] == "my_table"
def test_create_chart_datasource_access_denied_still_raises_forbidden(
mocker: MockerFixture,
) -> None:
"""The invalid-type guard must not shadow the existing access-denied path
for a legitimately table-typed datasource the user can't access."""
_base_mocks(mocker)
datasource = mocker.MagicMock()
datasource.name = "my_table"
mocker.patch(
"superset.commands.chart.create.get_datasource_by_id",
return_value=datasource,
)
mocker.patch(
"superset.commands.chart.create.security_manager.raise_for_access",
side_effect=SupersetSecurityException(
SupersetError(
error_type=SupersetErrorType.DATASOURCE_SECURITY_ACCESS_ERROR,
message="No access",
level=ErrorLevel.ERROR,
)
),
)
with pytest.raises(ChartForbiddenError):
CreateChartCommand(
{
"datasource_id": 11,
"datasource_type": "table",
"slice_name": "some_name",
"viz_type": "table",
}
).validate()
+1 -74
View File
@@ -17,13 +17,8 @@
import pytest
from pytest_mock import MockerFixture
from superset.commands.chart.exceptions import (
ChartForbiddenError,
ChartInvalidError,
DatasourceTypeUpdateRequiredValidationError,
)
from superset.commands.chart.exceptions import ChartForbiddenError, ChartInvalidError
from superset.commands.chart.update import UpdateChartCommand
from superset.commands.exceptions import DatasourceTypeInvalidError
from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
from superset.exceptions import SupersetSecurityException
from superset.utils import json
@@ -243,71 +238,3 @@ def test_update_chart_query_context_without_datasource_is_allowed(
1,
{"query_context": query_context, "query_context_generation": True},
).validate()
@pytest.mark.parametrize("datasource_type", ["saved_query", "query"])
def test_update_chart_rejects_repointing_to_non_table_datasource(
mocker: MockerFixture, datasource_type: str
) -> None:
"""Repointing a chart's datasource_id must be rejected the same way
CreateChartCommand rejects it (apache/superset#29697): Slice.datasource
only ever resolves the ``table`` relationship, so repointing at a
saved_query or query datasource would "succeed" but leave the chart
permanently unable to render -- or, for saved_query specifically, crash
on SavedQuery's missing ``.name`` attribute before that point is even
reached. This is a regular (non-query-context) update, so it goes
through editorship + compute_subjects, unlike the query-context-only
tests above."""
find_by_id = mocker.patch("superset.commands.chart.update.ChartDAO.find_by_id")
find_by_id.return_value = mocker.MagicMock(id=1, tags=[], dashboards=[])
mocker.patch("superset.commands.chart.update.security_manager.raise_for_editorship")
mocker.patch(
"superset.commands.chart.update.compute_subjects",
side_effect=lambda model, properties, exceptions: None,
)
get_datasource_by_id = mocker.patch(
"superset.commands.chart.update.get_datasource_by_id"
)
with pytest.raises(ChartInvalidError) as exc_info:
UpdateChartCommand(
1, {"datasource_id": 11, "datasource_type": datasource_type}
).validate()
assert any(
isinstance(ex, DatasourceTypeInvalidError) for ex in exc_info.value._exceptions
)
get_datasource_by_id.assert_not_called()
def test_update_chart_missing_datasource_type_keeps_required_error(
mocker: MockerFixture,
) -> None:
"""When datasource_id is given without datasource_type, the response
must keep reporting DatasourceTypeUpdateRequiredValidationError
("Datasource type is required") rather than having it overwritten by
DatasourceTypeInvalidError ("Datasource type is invalid") -- both
exceptions key their message under ``datasource_type``, and
normalized_messages() only keeps the last one written for a given key."""
find_by_id = mocker.patch("superset.commands.chart.update.ChartDAO.find_by_id")
find_by_id.return_value = mocker.MagicMock(id=1, tags=[], dashboards=[])
mocker.patch("superset.commands.chart.update.security_manager.raise_for_editorship")
mocker.patch(
"superset.commands.chart.update.compute_subjects",
side_effect=lambda model, properties, exceptions: None,
)
get_datasource_by_id = mocker.patch(
"superset.commands.chart.update.get_datasource_by_id"
)
with pytest.raises(ChartInvalidError) as exc_info:
UpdateChartCommand(1, {"datasource_id": 11}).validate()
assert any(
isinstance(ex, DatasourceTypeUpdateRequiredValidationError)
for ex in exc_info.value._exceptions
)
assert not any(
isinstance(ex, DatasourceTypeInvalidError) for ex in exc_info.value._exceptions
)
get_datasource_by_id.assert_not_called()
@@ -166,6 +166,108 @@ class TestLoadYaml:
load_yaml("test.yaml", 'key: "unterminated string')
class TestLoadConfigs:
"""
load_configs() merges caller-supplied ``encrypted_extra_secrets`` into the
``masked_encrypted_extra`` field of each config, which comes straight from
the imported YAML (before schema validation). A malformed value there used
to raise a raw simplejson.JSONDecodeError that escaped uncaught (opaque
500); it must instead be collected as a ValidationError like every other
per-file failure.
"""
@staticmethod
def _trivial_schema(): # type: ignore[no-untyped-def]
from marshmallow import EXCLUDE, Schema
class TrivialSchema(Schema):
class Meta:
unknown = EXCLUDE
return TrivialSchema()
@patch("superset.commands.importers.v1.utils.db")
def test_invalid_json_in_masked_encrypted_extra_is_collected(
self, mock_db: object
) -> None:
"""A non-JSON ``masked_encrypted_extra`` is converted into a
ValidationError appended to ``exceptions`` rather than raising."""
from marshmallow.exceptions import ValidationError
from superset.commands.importers.v1.utils import load_configs
# No existing databases / ssh tunnels in the (mocked) metadata DB.
mock_db.session.query.return_value.all.return_value = [] # type: ignore[attr-defined]
file_name = "databases/db.yaml"
contents = {
file_name: (
"uuid: abc-123\n"
"password: secret\n"
"masked_encrypted_extra: not valid json\n"
)
}
exceptions: list[ValidationError] = []
configs = load_configs(
contents=contents,
schemas={"databases/": self._trivial_schema()},
passwords={},
exceptions=exceptions,
ssh_tunnel_passwords={},
ssh_tunnel_private_keys={},
ssh_tunnel_priv_key_passwords={},
encrypted_extra_secrets={file_name: {"$.foo": "actual_secret"}},
)
# The bad file is not added to configs, and a structured error is
# collected instead of a raw JSONDecodeError propagating out.
assert file_name not in configs
assert len(exceptions) == 1
assert isinstance(exceptions[0], ValidationError)
assert file_name in exceptions[0].messages
assert "masked_encrypted_extra" in exceptions[0].messages[file_name]
@patch("superset.commands.importers.v1.utils.db")
def test_valid_json_in_masked_encrypted_extra_still_merges(
self, mock_db: object
) -> None:
"""Control: valid JSON in ``masked_encrypted_extra`` still has the
secrets merged in and produces no exceptions."""
from marshmallow.exceptions import ValidationError
from superset.commands.importers.v1.utils import load_configs
from superset.utils import json
mock_db.session.query.return_value.all.return_value = [] # type: ignore[attr-defined]
file_name = "databases/db.yaml"
contents = {
file_name: (
"uuid: abc-123\n"
"password: secret\n"
'masked_encrypted_extra: \'{"foo": "XXXXXXXXXX"}\'\n'
)
}
exceptions: list[ValidationError] = []
configs = load_configs(
contents=contents,
schemas={"databases/": self._trivial_schema()},
passwords={},
exceptions=exceptions,
ssh_tunnel_passwords={},
ssh_tunnel_private_keys={},
ssh_tunnel_priv_key_passwords={},
encrypted_extra_secrets={file_name: {"$.foo": "actual_secret"}},
)
assert exceptions == []
assert file_name in configs
merged = json.loads(configs[file_name]["masked_encrypted_extra"])
assert merged == {"foo": "actual_secret"}
class TestLoadConfigsNonMappingYaml:
"""A syntactically valid YAML document whose top-level value is a
scalar or list (not a mapping) must be reported as a schema validation